This commit is contained in:
decolua
2026-04-01 11:48:38 +07:00
parent 9708541f6d
commit 93b8668e9e
8 changed files with 148 additions and 119 deletions

View File

@@ -2,18 +2,10 @@
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
serverExternalPackages: ["better-sqlite3"], serverExternalPackages: ["better-sqlite3"],
outputFileTracingExcludes: {
"*": [
"**/Cookies/**",
"**/AppData/Local/**",
"**/node_modules/.cache/**",
],
},
images: { images: {
unoptimized: true unoptimized: true
}, },
env: {}, env: {},
turbopack: {},
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
// Ignore fs/path modules in browser bundle // Ignore fs/path modules in browser bundle
if (!isServer) { if (!isServer) {

View File

@@ -62,8 +62,8 @@ export const CLIENT_METADATA = {
// Internal anti-loop header // Internal anti-loop header
export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" }; export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
// Prefix added to client tools when forwarding to Antigravity provider (anti-ban cloaking) // Suffix added to client tools when forwarding to Antigravity provider (anti-ban cloaking)
export const AG_TOOL_PREFIX = "ide_"; export const AG_TOOL_SUFFIX = "_ide";
// AG native default tools — kept as decoys with neutral description/properties // AG native default tools — kept as decoys with neutral description/properties
// These names must match exactly what AG sends in the real request log // These names must match exactly what AG sends in the real request log

View File

@@ -1,7 +1,7 @@
import crypto from "crypto"; import crypto from "crypto";
import { BaseExecutor } from "./base.js"; import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js"; import { PROVIDERS } from "../config/providers.js";
import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAULT_TOOLS, AG_TOOL_PREFIX } from "../config/appConstants.js"; import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX } from "../config/appConstants.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { deriveSessionId } from "../utils/sessionManager.js"; import { deriveSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js";
@@ -259,9 +259,9 @@ export class AntigravityExecutor extends BaseExecutor {
/** /**
* Cloak tools before sending to Antigravity provider (anti-ban): * Cloak tools before sending to Antigravity provider (anti-ban):
* - Rename client tools with ide_ prefix * - Rename client tools with _ide suffix
* - Inject AG default decoy tools (same names, neutral description/properties) * - Inject AG default decoy tools after client tools
* Returns { cloakedBody, toolNameMap } where toolNameMap maps prefixed → original * Returns { cloakedBody, toolNameMap } where toolNameMap maps suffixed → original
*/ */
static cloakTools(body) { static cloakTools(body) {
const tools = body.request?.tools; const tools = body.request?.tools;
@@ -270,28 +270,28 @@ export class AntigravityExecutor extends BaseExecutor {
} }
const toolNameMap = new Map(); const toolNameMap = new Map();
const allDeclarations = []; const clientDeclarations = [];
// First: add AG decoy tools (to appear first in the list) // First: collect renamed client tools
allDeclarations.push(...AG_DECOY_TOOLS);
// Second: add renamed client tools
for (const toolGroup of tools) { for (const toolGroup of tools) {
if (!toolGroup.functionDeclarations) continue; if (!toolGroup.functionDeclarations) continue;
for (const func of toolGroup.functionDeclarations) { for (const func of toolGroup.functionDeclarations) {
// Skip if already an AG default tool name // Skip if already an AG default tool name
if (AG_DEFAULT_TOOLS.has(func.name)) { if (AG_DEFAULT_TOOLS.has(func.name)) {
allDeclarations.push(func); clientDeclarations.push(func);
continue; continue;
} }
const prefixed = `${AG_TOOL_PREFIX}${func.name}`; const suffixed = `${func.name}${AG_TOOL_SUFFIX}`;
toolNameMap.set(prefixed, func.name); toolNameMap.set(suffixed, func.name);
allDeclarations.push({ ...func, name: prefixed }); clientDeclarations.push({ ...func, name: suffixed });
} }
} }
// Client tools first, then AG decoy tools
const allDeclarations = [...clientDeclarations, ...AG_DECOY_TOOLS];
// Rename tool names in conversation history (contents) // Rename tool names in conversation history (contents)
const cloakedContents = body.request?.contents?.map(msg => { const cloakedContents = body.request?.contents?.map(msg => {
if (!msg.parts) return msg; if (!msg.parts) return msg;
@@ -303,7 +303,7 @@ export class AntigravityExecutor extends BaseExecutor {
...part, ...part,
functionCall: { functionCall: {
...part.functionCall, ...part.functionCall,
name: `${AG_TOOL_PREFIX}${part.functionCall.name}` name: `${part.functionCall.name}${AG_TOOL_SUFFIX}`
} }
}; };
} }
@@ -314,7 +314,7 @@ export class AntigravityExecutor extends BaseExecutor {
...part, ...part,
functionResponse: { functionResponse: {
...part.functionResponse, ...part.functionResponse,
name: `${AG_TOOL_PREFIX}${part.functionResponse.name}` name: `${part.functionResponse.name}${AG_TOOL_SUFFIX}`
} }
}; };
} }
@@ -325,7 +325,7 @@ export class AntigravityExecutor extends BaseExecutor {
return { ...msg, parts: cloakedParts }; return { ...msg, parts: cloakedParts };
}); });
// Single functionDeclarations group with decoys first, then renamed client tools // Single functionDeclarations group: client tools first, then decoys
return { return {
cloakedBody: { cloakedBody: {
...body, ...body,
@@ -340,111 +340,111 @@ export class AntigravityExecutor extends BaseExecutor {
} }
} }
// AG decoy tools — same names as AG native defaults, redirect to ide_ prefixed tools // AG decoy tools — same names as AG native defaults, redirect to _ide suffixed tools
const AG_DECOY_TOOLS = [ const AG_DECOY_TOOLS = [
{ {
name: "browser_subagent", name: "browser_subagent",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "command_status", name: "command_status",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "find_by_name", name: "find_by_name",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "generate_image", name: "generate_image",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "grep_search", name: "grep_search",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "list_dir", name: "list_dir",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "list_resources", name: "list_resources",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "mcp_sequential-thinking_sequentialthinking", name: "mcp_sequential-thinking_sequentialthinking",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "multi_replace_file_content", name: "multi_replace_file_content",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "notify_user", name: "notify_user",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "read_resource", name: "read_resource",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "read_terminal", name: "read_terminal",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "read_url_content", name: "read_url_content",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "replace_file_content", name: "replace_file_content",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "run_command", name: "run_command",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "search_web", name: "search_web",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "send_command_input", name: "send_command_input",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "task_boundary", name: "task_boundary",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "view_content_chunk", name: "view_content_chunk",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "view_file", name: "view_file",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
}, },
{ {
name: "write_to_file", name: "write_to_file",
description: "Use ide_ prefixed tools instead", description: "This tool is currently unavailable.",
parameters: { type: "OBJECT", properties: {}, required: [] } parameters: { type: "OBJECT", properties: {}, required: [] }
} }
]; ];

View File

@@ -1,16 +1,9 @@
import { register } from "../index.js"; import { register } from "../index.js";
import { FORMATS } from "../formats.js"; import { FORMATS } from "../formats.js";
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js"; import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js"; import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
// Decode base64url → standard base64 (for restoring thoughtSignature)
function fromBase64Url(b64url) {
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
// Restore padding
while (b64.length % 4) b64 += "=";
return b64;
}
function generateUUID() { function generateUUID() {
return crypto.randomUUID(); return crypto.randomUUID();
} }
@@ -65,32 +58,26 @@ function openaiToGeminiBase(model, body, stream) {
result.generationConfig.maxOutputTokens = body.max_tokens; result.generationConfig.maxOutputTokens = body.max_tokens;
} }
// Strip embedded thoughtSignature from a tool_call id ("rawId_TSIG_sig" → "rawId") // Build tool_call_id -> name map
const stripSig = (id) => {
const sep = id ? id.indexOf("_TSIG_") : -1;
return sep !== -1 ? id.slice(0, sep) : id;
};
// Build tool_call_id -> name map (keyed by rawId)
const tcID2Name = {}; const tcID2Name = {};
if (body.messages && Array.isArray(body.messages)) { if (body.messages && Array.isArray(body.messages)) {
for (const msg of body.messages) { for (const msg of body.messages) {
if (msg.role === "assistant" && msg.tool_calls) { if (msg.role === "assistant" && msg.tool_calls) {
for (const tc of msg.tool_calls) { for (const tc of msg.tool_calls) {
if (tc.type === "function" && tc.id && tc.function?.name) { if (tc.type === "function" && tc.id && tc.function?.name) {
tcID2Name[stripSig(tc.id)] = tc.function.name; tcID2Name[tc.id] = tc.function.name;
} }
} }
} }
} }
} }
// Build tool responses cache (keyed by rawId) // Build tool responses cache
const toolResponses = {}; const toolResponses = {};
if (body.messages && Array.isArray(body.messages)) { if (body.messages && Array.isArray(body.messages)) {
for (const msg of body.messages) { for (const msg of body.messages) {
if (msg.role === "tool" && msg.tool_call_id) { if (msg.role === "tool" && msg.tool_call_id) {
toolResponses[stripSig(msg.tool_call_id)] = msg.content; toolResponses[msg.tool_call_id] = msg.content;
} }
} }
} }
@@ -115,6 +102,18 @@ function openaiToGeminiBase(model, body, stream) {
} else if (role === "assistant") { } else if (role === "assistant") {
const parts = []; const parts = [];
// Thinking/reasoning → thought part with signature
if (msg.reasoning_content) {
parts.push({
thought: true,
text: msg.reasoning_content
});
parts.push({
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
text: ""
});
}
if (content) { if (content) {
const text = typeof content === "string" ? content : extractTextContent(content); const text = typeof content === "string" ? content : extractTextContent(content);
if (text) { if (text) {
@@ -128,22 +127,15 @@ function openaiToGeminiBase(model, body, stream) {
if (tc.type !== "function") continue; if (tc.type !== "function") continue;
const args = tryParseJSON(tc.function?.arguments || "{}"); const args = tryParseJSON(tc.function?.arguments || "{}");
// Extract thoughtSignature embedded in ID as "rawId_TSIG_base64urlsig" parts.push({
const sepIdx = tc.id.indexOf("_TSIG_"); thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
const rawId = sepIdx !== -1 ? tc.id.slice(0, sepIdx) : tc.id;
const encodedSig = sepIdx !== -1 ? tc.id.slice(sepIdx + 6) : "";
const fcPart = {
functionCall: { functionCall: {
id: rawId, id: tc.id,
name: sanitizeGeminiFunctionName(tc.function.name), name: sanitizeGeminiFunctionName(tc.function.name),
args: args args: args
} }
}; });
if (encodedSig) { toolCallIds.push(tc.id);
fcPart.thoughtSignature = fromBase64Url(encodedSig);
}
parts.push(fcPart);
toolCallIds.push(rawId);
} }
if (parts.length > 0) { if (parts.length > 0) {

View File

@@ -1,11 +1,6 @@
import { register } from "../index.js"; import { register } from "../index.js";
import { FORMATS } from "../formats.js"; import { FORMATS } from "../formats.js";
// Encode base64 → base64url (safe for tool_call IDs: only [a-zA-Z0-9_-])
function toBase64Url(b64) {
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
// Convert Gemini response chunk to OpenAI format // Convert Gemini response chunk to OpenAI format
export function geminiToOpenAIResponse(chunk, state) { export function geminiToOpenAIResponse(chunk, state) {
if (!chunk) return null; if (!chunk) return null;
@@ -23,7 +18,6 @@ export function geminiToOpenAIResponse(chunk, state) {
state.messageId = response.responseId || `msg_${Date.now()}`; state.messageId = response.responseId || `msg_${Date.now()}`;
state.model = response.modelVersion || "gemini"; state.model = response.modelVersion || "gemini";
state.functionIndex = 0; state.functionIndex = 0;
state.pendingThoughtSignature = null;
results.push({ results.push({
id: `chatcmpl-${state.messageId}`, id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk", object: "chat.completion.chunk",
@@ -40,18 +34,14 @@ export function geminiToOpenAIResponse(chunk, state) {
// Process parts // Process parts
if (content?.parts) { if (content?.parts) {
for (const part of content.parts) { for (const part of content.parts) {
const partThoughtSig = part.thoughtSignature || part.thought_signature || ""; const hasThoughtSig = part.thoughtSignature || part.thought_signature;
const isThought = part.thought === true; const isThought = part.thought === true;
// Accumulate thoughtSignature across parts: a sig-only part precedes the functionCall part // Handle thought signature (thinking mode)
if (partThoughtSig) { if (hasThoughtSig) {
state.pendingThoughtSignature = partThoughtSig;
}
const hasTextContent = part.text !== undefined && part.text !== ""; const hasTextContent = part.text !== undefined && part.text !== "";
const hasFunctionCall = !!part.functionCall; const hasFunctionCall = !!part.functionCall;
// Emit reasoning/thought text
if (hasTextContent) { if (hasTextContent) {
results.push({ results.push({
id: `chatcmpl-${state.messageId}`, id: `chatcmpl-${state.messageId}`,
@@ -68,23 +58,65 @@ export function geminiToOpenAIResponse(chunk, state) {
}); });
} }
// Emit function call, attaching the best available thoughtSignature
if (hasFunctionCall) { if (hasFunctionCall) {
const rawName = part.functionCall.name; const rawName = part.functionCall.name;
// Restore original tool name from mapping (AG cloaking) // Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName; const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = part.functionCall.args || {}; const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++; const toolCallIndex = state.functionIndex++;
// Use signature from this part, or the one carried from a preceding part
const thoughtSig = partThoughtSig || state.pendingThoughtSignature || "";
if (thoughtSig) state.pendingThoughtSignature = null; // consumed
// Encode signature using _TSIG_ delimiter and base64url for safe tool_call ID
const toolCallId = thoughtSig
? `${fcName}-${Date.now()}-${toolCallIndex}_TSIG_${toBase64Url(thoughtSig)}`
: `${fcName}-${Date.now()}-${toolCallIndex}`;
const toolCall = { const toolCall = {
id: toolCallId, id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs)
}
};
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null
}]
});
}
continue;
}
// Text content (non-thinking)
if (part.text !== undefined && part.text !== "") {
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { content: part.text },
finish_reason: null
}]
});
}
// Function call
if (part.functionCall) {
const rawName = part.functionCall.name;
// Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex, index: toolCallIndex,
type: "function", type: "function",
function: { function: {
@@ -209,5 +241,4 @@ export function geminiToOpenAIResponse(chunk, state) {
register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse); register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse); register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse); register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse);
register(FORMATS.VERTEX, FORMATS.OPENAI, null, geminiToOpenAIResponse);

View File

@@ -1,12 +1,12 @@
{ {
"name": "9router-app", "name": "9router-app",
"version": "0.3.69", "version": "0.3.72",
"description": "9Router web dashboard", "description": "9Router web dashboard",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --port 20128", "dev": "next dev --webpack --port 20128",
"build": "cross-env NODE_ENV=production next build", "build": "NODE_ENV=production next build --webpack",
"start": "cross-env NODE_ENV=production next start", "start": "NODE_ENV=production next start",
"dev:bun": "bun --bun next dev --webpack --port 20128", "dev:bun": "bun --bun next dev --webpack --port 20128",
"build:bun": "NODE_ENV=production bun --bun next build --webpack", "build:bun": "NODE_ENV=production bun --bun next build --webpack",
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js" "start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
@@ -44,7 +44,6 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"cross-env": "^10.1.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",

View File

@@ -1185,9 +1185,6 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code> <code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
{isFree && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Free</span>
)}
<div className="relative group/btn"> <div className="relative group/btn">
<button <button
onClick={() => onCopy(fullModel, `model-${modelId}`)} onClick={() => onCopy(fullModel, `model-${modelId}`)}

View File

@@ -95,6 +95,7 @@ export async function POST(request) {
if (!settings.agents) settings.agents = {}; if (!settings.agents) settings.agents = {};
if (!settings.agents.defaults) settings.agents.defaults = {}; if (!settings.agents.defaults) settings.agents.defaults = {};
if (!settings.agents.defaults.model) settings.agents.defaults.model = {}; if (!settings.agents.defaults.model) settings.agents.defaults.model = {};
if (!settings.agents.defaults.models) settings.agents.defaults.models = {};
if (!settings.models) settings.models = {}; if (!settings.models) settings.models = {};
if (!settings.models.providers) settings.models.providers = {}; if (!settings.models.providers) settings.models.providers = {};
@@ -102,7 +103,13 @@ export async function POST(request) {
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
// Update agents.defaults.model.primary // Update agents.defaults.model.primary
settings.agents.defaults.model.primary = `9router/${model}`; const fullModelId = `9router/${model}`;
settings.agents.defaults.model.primary = fullModelId;
// IMPORTANT: Add to allowlist in agents.defaults.models
if (!settings.agents.defaults.models[fullModelId]) {
settings.agents.defaults.models[fullModelId] = {};
}
// Update models.providers.9router // Update models.providers.9router
settings.models.providers["9router"] = { settings.models.providers["9router"] = {
@@ -161,6 +168,17 @@ export async function DELETE() {
} }
} }
// Remove 9router models from agents.defaults.models allowlist
if (settings.agents?.defaults?.models) {
const keysToRemove = Object.keys(settings.agents.defaults.models).filter((k) => k.startsWith("9router/"));
for (const key of keysToRemove) {
delete settings.agents.defaults.models[key];
}
if (Object.keys(settings.agents.defaults.models).length === 0) {
delete settings.agents.defaults.models;
}
}
// Reset agents.defaults.model.primary if it uses 9router // Reset agents.defaults.model.primary if it uses 9router
if (settings.agents?.defaults?.model?.primary?.startsWith("9router/")) { if (settings.agents?.defaults?.model?.primary?.startsWith("9router/")) {
delete settings.agents.defaults.model.primary; delete settings.agents.defaults.model.primary;