diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index b3a077f6..172e0ada 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -303,7 +303,7 @@ export const PROVIDERS = { // baseUrl is not used; VertexExecutor.buildUrl() constructs it dynamically vertex: { baseUrl: "https://aiplatform.googleapis.com", - format: "gemini" + format: "vertex" }, // Vertex AI - Partner models (Claude, Llama, Mistral, GLM) via SA JSON // Uses OpenAI-compatible global endpoint (or rawPredict for Anthropic) diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js index 3ee72082..3b426123 100644 --- a/open-sse/handlers/chatCore/nonStreamingHandler.js +++ b/open-sse/handlers/chatCore/nonStreamingHandler.js @@ -15,7 +15,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) return responseBody; // Gemini / Antigravity - if (targetFormat === FORMATS.GEMINI || targetFormat === FORMATS.ANTIGRAVITY || targetFormat === FORMATS.GEMINI_CLI) { + if (targetFormat === FORMATS.GEMINI || targetFormat === FORMATS.ANTIGRAVITY || targetFormat === FORMATS.GEMINI_CLI || targetFormat === FORMATS.VERTEX) { const response = responseBody.response || responseBody; if (!response?.candidates?.[0]) return responseBody; diff --git a/open-sse/translator/formats.js b/open-sse/translator/formats.js index ebfed28a..f5594183 100644 --- a/open-sse/translator/formats.js +++ b/open-sse/translator/formats.js @@ -6,6 +6,7 @@ export const FORMATS = { CLAUDE: "claude", GEMINI: "gemini", GEMINI_CLI: "gemini-cli", + VERTEX: "vertex", CODEX: "codex", ANTIGRAVITY: "antigravity", KIRO: "kiro", diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index fe622ff9..d0d1c957 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -32,6 +32,7 @@ function ensureInitialized() { require("./request/openai-to-claude.js"); require("./request/gemini-to-openai.js"); require("./request/openai-to-gemini.js"); + require("./request/openai-to-vertex.js"); require("./request/antigravity-to-openai.js"); require("./request/openai-responses.js"); require("./request/openai-to-kiro.js"); diff --git a/open-sse/translator/request/openai-to-vertex.js b/open-sse/translator/request/openai-to-vertex.js new file mode 100644 index 00000000..39e024a1 --- /dev/null +++ b/open-sse/translator/request/openai-to-vertex.js @@ -0,0 +1,50 @@ +import { register } from "../index.js"; +import { FORMATS } from "../formats.js"; +import { openaiToGeminiRequest } from "./openai-to-gemini.js"; + +/** + * Post-process a Gemini-format body for Vertex AI compatibility: + * + * 1. Strip `id` from every `functionCall` and `functionResponse` part. + * Vertex AI rejects requests that include these fields. + * + * 2. Strip synthetic `thoughtSignature` parts injected by the base translator. + * Vertex rejects fake thought signatures in multi-turn tool-call history; + * only real signatures emitted by Vertex itself should be replayed. + */ +function stripVertexIncompatibleFields(body) { + if (!body?.contents) return body; + + for (const turn of body.contents) { + if (!Array.isArray(turn.parts)) continue; + + // Remove standalone synthetic thoughtSignature parts (text === "" with thoughtSignature) + turn.parts = turn.parts.filter( + p => !(p.thoughtSignature !== undefined && p.text === "" && !p.thought) + ); + + for (const part of turn.parts) { + // Strip id from functionCall + if (part.functionCall && "id" in part.functionCall) { + delete part.functionCall.id; + } + // Strip id from functionResponse + if (part.functionResponse && "id" in part.functionResponse) { + delete part.functionResponse.id; + } + // Strip thoughtSignature injected alongside functionCall (synthetic signature) + if (part.functionCall && "thoughtSignature" in part) { + delete part.thoughtSignature; + } + } + } + + return body; +} + +export function openaiToVertexRequest(model, body, stream, credentials) { + const gemini = openaiToGeminiRequest(model, body, stream, credentials); + return stripVertexIncompatibleFields(gemini); +} + +register(FORMATS.OPENAI, FORMATS.VERTEX, openaiToVertexRequest, null); diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 000cc1f7..96e93cb3 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -237,4 +237,5 @@ 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);