diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 81703836524..8f53a585605 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1067,6 +1067,8 @@ export namespace Provider { const s = await state() const provider = s.providers[model.providerID] const options = { ...provider.options } + const wire = options["wireApi"] ?? options["wire_api"] + const openaiCompatResponses = model.api.npm === "@ai-sdk/openai-compatible" && wire === "responses" if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { delete options.fetch @@ -1110,7 +1112,7 @@ export namespace Provider { // Codex uses #[serde(skip_serializing)] on id fields for all item types: // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall // IDs are only re-attached for Azure with store=true - if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { + if ((model.api.npm === "@ai-sdk/openai" || openaiCompatResponses) && opts.body && opts.method === "POST") { const body = JSON.parse(opts.body as string) const isAzure = model.providerID.includes("azure") const keepIds = isAzure && body.store === true @@ -1131,6 +1133,18 @@ export namespace Provider { }) } + if (openaiCompatResponses) { + log.info("using openai responses for openai-compatible provider", { + providerID: model.providerID, + }) + const loaded = createOpenAI({ + name: model.providerID, + ...options, + }) + s.sdk.set(key, loaded) + return loaded as SDK + } + const bundledFn = BUNDLED_PROVIDERS[model.api.npm] if (bundledFn) { log.info("using bundled provider", { providerID: model.providerID, pkg: model.api.npm }) @@ -1195,10 +1209,14 @@ export namespace Provider { const provider = s.providers[model.providerID] const sdk = await getSDK(model) + const wire = provider.options?.["wireApi"] ?? provider.options?.["wire_api"] + const openaiCompatResponses = model.api.npm === "@ai-sdk/openai-compatible" && wire === "responses" try { const language = s.modelLoaders[model.providerID] ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options) + : openaiCompatResponses && (sdk as any).responses + ? (sdk as any).responses(model.api.id) : sdk.languageModel(model.api.id) s.models.set(key, language) return language diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6980be05188..990756174d4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -684,12 +684,15 @@ export namespace ProviderTransform { providerOptions?: Record }): Record { const result: Record = {} + const wire = input.providerOptions?.["wireApi"] ?? input.providerOptions?.["wire_api"] + const openaiCompatResponses = input.model.api.npm === "@ai-sdk/openai-compatible" && wire === "responses" // openai and providers using openai package should set store to false by default. if ( input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/openai" || - input.model.api.npm === "@ai-sdk/github-copilot" + input.model.api.npm === "@ai-sdk/github-copilot" || + openaiCompatResponses ) { result["store"] = false } @@ -759,7 +762,9 @@ export namespace ProviderTransform { if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) { if (!input.model.api.id.includes("gpt-5-pro")) { result["reasoningEffort"] = "medium" - result["reasoningSummary"] = "auto" + if (input.model.api.npm !== "@ai-sdk/openai-compatible" || openaiCompatResponses) { + result["reasoningSummary"] = "auto" + } } // Only set textVerbosity for non-chat gpt-5.x models @@ -837,7 +842,7 @@ export namespace ProviderTransform { amazon: "bedrock", } - export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { + export function providerOptions(model: Provider.Model, options: { [x: string]: any }, provider?: Provider.Info) { if (model.api.npm === "@ai-sdk/gateway") { // Gateway providerOptions are split across two namespaces: // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.) @@ -868,7 +873,11 @@ export namespace ProviderTransform { return result } - const key = sdkKey(model.api.npm) ?? model.providerID + const wire = provider?.options?.["wireApi"] ?? provider?.options?.["wire_api"] + const key = + model.api.npm === "@ai-sdk/openai-compatible" && wire === "responses" + ? "openai" + : sdkKey(model.api.npm) ?? model.providerID return { [key]: options } } diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 4e42fb0d2ec..23fdba9a879 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -199,7 +199,7 @@ export namespace LLM { temperature: params.temperature, topP: params.topP, topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), + providerOptions: ProviderTransform.providerOptions(input.model, params.options, provider), activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2329846351c..28dda00e2b7 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -175,6 +175,62 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { }) }) +describe("ProviderTransform.options - gpt-5 reasoning defaults", () => { + const sessionID = "test-session-123" + + const createModel = (npm: string) => + ({ + id: "test/gpt-5-codex", + providerID: "test", + api: { + id: "gpt-5-codex", + url: "https://api.test.com", + npm, + }, + name: "gpt-5-codex", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } }, + limit: { context: 128000, output: 4096 }, + status: "active", + options: {}, + headers: {}, + }) as any + + test("openai-compatible gpt-5 does not set reasoningSummary", () => { + const model = createModel("@ai-sdk/openai-compatible") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result.reasoningEffort).toBe("medium") + expect(result.reasoningSummary).toBeUndefined() + }) + + test("openai-compatible gpt-5 with responses wire sets reasoningSummary", () => { + const model = createModel("@ai-sdk/openai-compatible") + const result = ProviderTransform.options({ + model, + sessionID, + providerOptions: { wireApi: "responses" }, + }) + expect(result.store).toBe(false) + expect(result.reasoningEffort).toBe("medium") + expect(result.reasoningSummary).toBe("auto") + }) + + test("openai gpt-5 still sets reasoningSummary", () => { + const model = createModel("@ai-sdk/openai") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result.reasoningEffort).toBe("medium") + expect(result.reasoningSummary).toBe("auto") + }) +}) + describe("ProviderTransform.options - gateway", () => { const sessionID = "test-session-123" @@ -371,6 +427,36 @@ describe("ProviderTransform.providerOptions", () => { groq: { reasoningFormat: "parsed" }, }) }) + + test("routes openai-compatible responses wire options under openai key", () => { + const model = createModel({ + providerID: "packycode-k1", + api: { + id: "gpt-5-codex", + url: "https://api.packycode.com/v1", + npm: "@ai-sdk/openai-compatible", + }, + }) + const result = ProviderTransform.providerOptions( + model, + { reasoningEffort: "high", reasoningSummary: "auto" }, + { + id: "packycode-k1", + name: "PackyCode", + npm: "@ai-sdk/openai-compatible", + env: [], + options: { wireApi: "responses" }, + models: {}, + } as any, + ) + + expect(result).toEqual({ + openai: { + reasoningEffort: "high", + reasoningSummary: "auto", + }, + }) + }) }) describe("ProviderTransform.schema - gemini array items", () => {