From b1234ce73ff36fe6be2efb62503835eab34f97f4 Mon Sep 17 00:00:00 2001 From: Awowen Date: Thu, 8 Jan 2026 21:49:22 +0100 Subject: [PATCH] feat: add query parameter support for OpenAI-compatible providers Adds a queryParams option to OpenaiCompatibleProviderSettings that appends query parameters to all API requests. This enables support for providers that require query string parameters for API versioning, tenant identification, or feature flags. Fixes #7198 --- .../src/openai-compatible-provider.ts | 19 ++- .../test/provider/openai-compatible.test.ts | 132 ++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/provider/openai-compatible.test.ts diff --git a/packages/opencode/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts b/packages/opencode/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts index e71658c2fa0..fa7d64ed6e2 100644 --- a/packages/opencode/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +++ b/packages/opencode/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts @@ -29,6 +29,11 @@ export interface OpenaiCompatibleProviderSettings { */ headers?: Record + /** + * Custom query parameter to append to the requests + */ + queryParams?: Record + /** * Custom fetch implementation. */ @@ -65,11 +70,21 @@ export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings const getHeaders = () => withUserAgentSuffix(headers, `ai-sdk/openai-compatible/${VERSION}`) + const getUrl = ({ path }: { path: string }) => { + const url = new URL(`${baseURL}${path}`) + if (options.queryParams) { + for (const [key, value] of Object.entries(options.queryParams)) { + url.searchParams.set(key, value) + } + } + return url.toString() + } + const createChatModel = (modelId: OpenaiCompatibleModelId) => { return new OpenAICompatibleChatLanguageModel(modelId, { provider: `${options.name ?? "openai-compatible"}.chat`, headers: getHeaders, - url: ({ path }) => `${baseURL}${path}`, + url: getUrl, fetch: options.fetch, }) } @@ -78,7 +93,7 @@ export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings return new OpenAIResponsesLanguageModel(modelId, { provider: `${options.name ?? "openai-compatible"}.responses`, headers: getHeaders, - url: ({ path }) => `${baseURL}${path}`, + url: getUrl, fetch: options.fetch, }) } diff --git a/packages/opencode/test/provider/openai-compatible.test.ts b/packages/opencode/test/provider/openai-compatible.test.ts new file mode 100644 index 00000000000..5bcea364aaa --- /dev/null +++ b/packages/opencode/test/provider/openai-compatible.test.ts @@ -0,0 +1,132 @@ +import { test, expect } from "bun:test" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { Provider } from "../../src/provider/provider" + +test("provider with queryParams in config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-openai": { + name: "Custom OpenAI with Query Params", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "gpt-4": { + name: "GPT-4", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + options: { + apiKey: "test-key", + baseURL: "https://api.example.com/v1", + queryParams: { + "api-version": "2024-01-01", + tenant: "my-org", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-openai"]).toBeDefined() + expect(providers["custom-openai"].name).toBe("Custom OpenAI with Query Params") + expect(providers["custom-openai"].options.queryParams).toEqual({ + "api-version": "2024-01-01", + tenant: "my-org", + }) + }, + }) +}) + +test("provider without queryParams still works", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-openai": { + name: "Custom OpenAI", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "gpt-4": { + name: "GPT-4", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + options: { + apiKey: "test-key", + baseURL: "https://api.example.com/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-openai"]).toBeDefined() + expect(providers["custom-openai"].name).toBe("Custom OpenAI") + expect(providers["custom-openai"].options.queryParams).toBeUndefined() + }, + }) +}) + +test("provider with empty queryParams", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-openai": { + name: "Custom OpenAI Empty Params", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "gpt-4": { + name: "GPT-4", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + options: { + apiKey: "test-key", + baseURL: "https://api.example.com/v1", + queryParams: {}, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-openai"]).toBeDefined() + expect(providers["custom-openai"].options.queryParams).toEqual({}) + }, + }) +})