From 97ab6597b32383d73538146e296e98892d633d87 Mon Sep 17 00:00:00 2001 From: Yanzhen Lu Date: Thu, 9 Apr 2026 19:25:35 +0800 Subject: [PATCH] fix(gemini): pass custom http options to sdk --- core/llm/llms/OpenRouter.ts | 15 ++++ packages/openai-adapters/src/apis/Gemini.ts | 25 +++++++ .../openai-adapters/src/apis/OpenRouter.ts | 5 +- packages/openai-adapters/src/index.ts | 1 + .../src/test/gemini-adapter.vitest.ts | 68 +++++++++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 packages/openai-adapters/src/test/gemini-adapter.vitest.ts diff --git a/core/llm/llms/OpenRouter.ts b/core/llm/llms/OpenRouter.ts index b2772824583..0c389f7bd70 100644 --- a/core/llm/llms/OpenRouter.ts +++ b/core/llm/llms/OpenRouter.ts @@ -1,5 +1,7 @@ import { ChatCompletionCreateParams } from "openai/resources/index"; +import { OPENROUTER_HEADERS } from "@continuedev/openai-adapters"; + import { LLMOptions } from "../../index.js"; import { osModelsEditPrompt } from "../templates/edit.js"; @@ -18,6 +20,19 @@ class OpenRouter extends OpenAI { useLegacyCompletionsEndpoint: false, }; + constructor(options: LLMOptions) { + super({ + ...options, + requestOptions: { + ...options.requestOptions, + headers: { + ...OPENROUTER_HEADERS, + ...options.requestOptions?.headers, + }, + }, + }); + } + private isAnthropicModel(model?: string): boolean { if (!model) return false; const modelLower = model.toLowerCase(); diff --git a/packages/openai-adapters/src/apis/Gemini.ts b/packages/openai-adapters/src/apis/Gemini.ts index f7298267849..729a2b26ffe 100644 --- a/packages/openai-adapters/src/apis/Gemini.ts +++ b/packages/openai-adapters/src/apis/Gemini.ts @@ -65,6 +65,30 @@ interface GeminiToolDelta }; } +function buildGoogleGenAIHttpOptions(config: GeminiConfig) { + const httpOptions: { + headers?: Record; + timeout?: number; + baseUrl?: string; + apiVersion?: string; + } = {}; + + if (config.requestOptions?.headers) { + httpOptions.headers = config.requestOptions.headers; + } + + if (config.requestOptions?.timeout !== undefined) { + httpOptions.timeout = config.requestOptions.timeout; + } + + if (config.apiBase) { + httpOptions.baseUrl = config.apiBase; + httpOptions.apiVersion = ""; + } + + return Object.keys(httpOptions).length ? httpOptions : undefined; +} + export class GeminiApi implements BaseLlmApi { apiBase: string = "https://generativelanguage.googleapis.com/v1beta/"; private genAI: GoogleGenAI; @@ -79,6 +103,7 @@ export class GeminiApi implements BaseLlmApi { () => new GoogleGenAI({ apiKey: this.config.apiKey, + httpOptions: buildGoogleGenAIHttpOptions(this.config), }), ); } diff --git a/packages/openai-adapters/src/apis/OpenRouter.ts b/packages/openai-adapters/src/apis/OpenRouter.ts index 7c45fddeed6..542699d20c3 100644 --- a/packages/openai-adapters/src/apis/OpenRouter.ts +++ b/packages/openai-adapters/src/apis/OpenRouter.ts @@ -10,9 +10,10 @@ export interface OpenRouterConfig extends OpenAIConfig { // TODO: Extract detailed error info from OpenRouter's error.metadata.raw to surface better messages -const OPENROUTER_HEADERS: Record = { +export const OPENROUTER_HEADERS: Record = { "HTTP-Referer": "https://www.continue.dev/", - "X-Title": "Continue", + "X-OpenRouter-Title": "Continue", + "X-OpenRouter-Categories": "ide-extension", }; export class OpenRouterApi extends OpenAIApi { diff --git a/packages/openai-adapters/src/index.ts b/packages/openai-adapters/src/index.ts index 467c7a71ae9..c9eb4da00fa 100644 --- a/packages/openai-adapters/src/index.ts +++ b/packages/openai-adapters/src/index.ts @@ -243,4 +243,5 @@ export { } from "./apis/AnthropicUtils.js"; export { isResponsesModel } from "./apis/openaiResponses.js"; +export { OPENROUTER_HEADERS } from "./apis/OpenRouter.js"; export { extractBase64FromDataUrl, parseDataUrl } from "./util/url.js"; diff --git a/packages/openai-adapters/src/test/gemini-adapter.vitest.ts b/packages/openai-adapters/src/test/gemini-adapter.vitest.ts new file mode 100644 index 00000000000..94728cfe19c --- /dev/null +++ b/packages/openai-adapters/src/test/gemini-adapter.vitest.ts @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const generateContentStream = vi.fn(); +const GoogleGenAIMock = vi.fn().mockImplementation(() => ({ + models: { + generateContentStream, + }, +})); + +vi.mock("@google/genai", () => ({ + GoogleGenAI: GoogleGenAIMock, +})); + +vi.mock("../util/nativeFetch.js", () => ({ + withNativeFetch: (fn: () => unknown) => fn(), +})); + +describe("GeminiApi", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("passes custom headers and apiBase through GoogleGenAI httpOptions", async () => { + const { GeminiApi } = await import("../apis/Gemini.js"); + + new GeminiApi({ + provider: "gemini", + apiKey: "primary-api-key", + apiBase: + "https://example.com/v1/streaming-models/locations/europe-west4/publishers/google", + requestOptions: { + timeout: 10000, + headers: { + "x-api-key": "secondary-api-key", + "Content-Type": "application/json", + }, + }, + }); + + expect(GoogleGenAIMock).toHaveBeenCalledWith({ + apiKey: "primary-api-key", + httpOptions: { + apiVersion: "", + baseUrl: + "https://example.com/v1/streaming-models/locations/europe-west4/publishers/google", + timeout: 10000, + headers: { + "x-api-key": "secondary-api-key", + "Content-Type": "application/json", + }, + }, + }); + }); + + it("omits httpOptions when no custom request options are provided", async () => { + const { GeminiApi } = await import("../apis/Gemini.js"); + + new GeminiApi({ + provider: "gemini", + apiKey: "primary-api-key", + }); + + expect(GoogleGenAIMock).toHaveBeenCalledWith({ + apiKey: "primary-api-key", + httpOptions: undefined, + }); + }); +});