diff --git a/next.config.mjs b/next.config.mjs index 8e848dd2..31644a31 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,18 +2,10 @@ const nextConfig = { output: "standalone", serverExternalPackages: ["better-sqlite3"], - outputFileTracingExcludes: { - "*": [ - "**/Cookies/**", - "**/AppData/Local/**", - "**/node_modules/.cache/**", - ], - }, images: { unoptimized: true }, env: {}, - turbopack: {}, webpack: (config, { isServer }) => { // Ignore fs/path modules in browser bundle if (!isServer) { diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index 60a94ea8..7988817a 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -62,8 +62,8 @@ export const CLIENT_METADATA = { // Internal anti-loop header export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" }; -// Prefix added to client tools when forwarding to Antigravity provider (anti-ban cloaking) -export const AG_TOOL_PREFIX = "ide_"; +// Suffix added to client tools when forwarding to Antigravity provider (anti-ban cloaking) +export const AG_TOOL_SUFFIX = "_ide"; // AG native default tools — kept as decoys with neutral description/properties // These names must match exactly what AG sends in the real request log diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 2a38b70e..4753d41e 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -1,7 +1,7 @@ import crypto from "crypto"; import { BaseExecutor } from "./base.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 { deriveSessionId } from "../utils/sessionManager.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): - * - Rename client tools with ide_ prefix - * - Inject AG default decoy tools (same names, neutral description/properties) - * Returns { cloakedBody, toolNameMap } where toolNameMap maps prefixed → original + * - Rename client tools with _ide suffix + * - Inject AG default decoy tools after client tools + * Returns { cloakedBody, toolNameMap } where toolNameMap maps suffixed → original */ static cloakTools(body) { const tools = body.request?.tools; @@ -270,28 +270,28 @@ export class AntigravityExecutor extends BaseExecutor { } const toolNameMap = new Map(); - const allDeclarations = []; + const clientDeclarations = []; - // First: add AG decoy tools (to appear first in the list) - allDeclarations.push(...AG_DECOY_TOOLS); - - // Second: add renamed client tools + // First: collect renamed client tools for (const toolGroup of tools) { if (!toolGroup.functionDeclarations) continue; for (const func of toolGroup.functionDeclarations) { // Skip if already an AG default tool name if (AG_DEFAULT_TOOLS.has(func.name)) { - allDeclarations.push(func); + clientDeclarations.push(func); continue; } - const prefixed = `${AG_TOOL_PREFIX}${func.name}`; - toolNameMap.set(prefixed, func.name); - allDeclarations.push({ ...func, name: prefixed }); + const suffixed = `${func.name}${AG_TOOL_SUFFIX}`; + toolNameMap.set(suffixed, func.name); + 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) const cloakedContents = body.request?.contents?.map(msg => { if (!msg.parts) return msg; @@ -303,7 +303,7 @@ export class AntigravityExecutor extends BaseExecutor { ...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, 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 }; }); - // Single functionDeclarations group with decoys first, then renamed client tools + // Single functionDeclarations group: client tools first, then decoys return { cloakedBody: { ...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 = [ { name: "browser_subagent", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "command_status", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "find_by_name", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "generate_image", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "grep_search", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "list_dir", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "list_resources", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "mcp_sequential-thinking_sequentialthinking", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "multi_replace_file_content", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "notify_user", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "read_resource", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "read_terminal", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "read_url_content", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "replace_file_content", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "run_command", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "search_web", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "send_command_input", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "task_boundary", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "view_content_chunk", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "view_file", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } }, { name: "write_to_file", - description: "Use ide_ prefixed tools instead", + description: "This tool is currently unavailable.", parameters: { type: "OBJECT", properties: {}, required: [] } } ]; diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index 463d1140..bfa84d13 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -1,16 +1,9 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; +import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js"; import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.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() { return crypto.randomUUID(); } @@ -65,32 +58,26 @@ function openaiToGeminiBase(model, body, stream) { result.generationConfig.maxOutputTokens = body.max_tokens; } - // Strip embedded thoughtSignature from a tool_call id ("rawId_TSIG_sig" → "rawId") - 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) + // Build tool_call_id -> name map const tcID2Name = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { if (msg.role === "assistant" && msg.tool_calls) { for (const tc of msg.tool_calls) { 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 = {}; if (body.messages && Array.isArray(body.messages)) { for (const msg of body.messages) { 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") { 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) { const text = typeof content === "string" ? content : extractTextContent(content); if (text) { @@ -128,22 +127,15 @@ function openaiToGeminiBase(model, body, stream) { if (tc.type !== "function") continue; const args = tryParseJSON(tc.function?.arguments || "{}"); - // Extract thoughtSignature embedded in ID as "rawId_TSIG_base64urlsig" - const sepIdx = tc.id.indexOf("_TSIG_"); - const rawId = sepIdx !== -1 ? tc.id.slice(0, sepIdx) : tc.id; - const encodedSig = sepIdx !== -1 ? tc.id.slice(sepIdx + 6) : ""; - const fcPart = { + parts.push({ + thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE, functionCall: { - id: rawId, + id: tc.id, name: sanitizeGeminiFunctionName(tc.function.name), args: args } - }; - if (encodedSig) { - fcPart.thoughtSignature = fromBase64Url(encodedSig); - } - parts.push(fcPart); - toolCallIds.push(rawId); + }); + toolCallIds.push(tc.id); } if (parts.length > 0) { diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 96c66ef9..abe6c1b3 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -1,11 +1,6 @@ import { register } from "../index.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 export function geminiToOpenAIResponse(chunk, state) { if (!chunk) return null; @@ -23,7 +18,6 @@ export function geminiToOpenAIResponse(chunk, state) { state.messageId = response.responseId || `msg_${Date.now()}`; state.model = response.modelVersion || "gemini"; state.functionIndex = 0; - state.pendingThoughtSignature = null; results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", @@ -40,19 +34,66 @@ export function geminiToOpenAIResponse(chunk, state) { // Process parts if (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; - - // Accumulate thoughtSignature across parts: a sig-only part precedes the functionCall part - if (partThoughtSig) { - state.pendingThoughtSignature = partThoughtSig; + + // Handle thought signature (thinking mode) + if (hasThoughtSig) { + const hasTextContent = part.text !== undefined && part.text !== ""; + const hasFunctionCall = !!part.functionCall; + + if (hasTextContent) { + results.push({ + id: `chatcmpl-${state.messageId}`, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: state.model, + choices: [{ + index: 0, + delta: isThought + ? { reasoning_content: part.text } + : { content: part.text }, + finish_reason: null + }] + }); + } + + if (hasFunctionCall) { + 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, + 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; } - const hasTextContent = part.text !== undefined && part.text !== ""; - const hasFunctionCall = !!part.functionCall; - - // Emit reasoning/thought text - if (hasTextContent) { + // Text content (non-thinking) + if (part.text !== undefined && part.text !== "") { results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", @@ -60,31 +101,22 @@ export function geminiToOpenAIResponse(chunk, state) { model: state.model, choices: [{ index: 0, - delta: isThought - ? { reasoning_content: part.text } - : { content: part.text }, + delta: { content: part.text }, finish_reason: null }] }); } - // Emit function call, attaching the best available thoughtSignature - if (hasFunctionCall) { + // 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++; - // 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 = { - id: toolCallId, + id: `${fcName}-${Date.now()}-${toolCallIndex}`, index: toolCallIndex, type: "function", function: { @@ -92,9 +124,9 @@ export function geminiToOpenAIResponse(chunk, state) { arguments: JSON.stringify(fcArgs) } }; - + state.toolCalls.set(toolCallIndex, toolCall); - + results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", @@ -209,5 +241,4 @@ export function geminiToOpenAIResponse(chunk, state) { register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse); register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse); register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse); -register(FORMATS.VERTEX, FORMATS.OPENAI, null, geminiToOpenAIResponse); diff --git a/package.json b/package.json index 2dc7e8b0..bd1a4f8c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "9router-app", - "version": "0.3.69", + "version": "0.3.72", "description": "9Router web dashboard", "private": true, "scripts": { "dev": "next dev --webpack --port 20128", - "build": "cross-env NODE_ENV=production next build", - "start": "cross-env NODE_ENV=production next start", + "build": "NODE_ENV=production next build --webpack", + "start": "NODE_ENV=production next start", "dev:bun": "bun --bun next dev --webpack --port 20128", "build:bun": "NODE_ENV=production bun --bun next build --webpack", "start:bun": "NODE_ENV=production bun ./.next/standalone/server.js" @@ -44,7 +44,6 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", - "cross-env": "^10.1.0", "eslint": "^9", "eslint-config-next": "16.1.6", "postcss": "^8.5.6", diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index d4d55165..d55adbe0 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -1185,9 +1185,6 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
{fullModel}
- {isFree && (
- Free
- )}