Skip to content
22 changes: 14 additions & 8 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ import { ProviderTransform } from "./transform"
export namespace Provider {
const log = Log.create({ service: "provider" })

function isGpt5OrLater(modelID: string): boolean {
const match = /^gpt-(\d+)/.exec(modelID)
if (!match) {
return false
}
return Number(match[1]) >= 5
}

function shouldUseCopilotResponsesApi(modelID: string): boolean {
return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")
}

const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
"@ai-sdk/anthropic": createAnthropic,
Expand Down Expand Up @@ -120,10 +132,7 @@ export namespace Provider {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("codex")) {
return sdk.responses(modelID)
}
return sdk.chat(modelID)
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
},
options: {},
}
Expand All @@ -132,10 +141,7 @@ export namespace Provider {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("codex")) {
return sdk.responses(modelID)
}
return sdk.chat(modelID)
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
},
options: {},
}
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,11 @@ export namespace ProviderTransform {
const result: Record<string, any> = {}

// 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") {
if (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do u need this? hmm

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for copilot? is that what vscode does too? or other copilot clients

Copy link
Contributor Author

@christso christso Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refined the condition to use api.npm === "@ai-sdk/github-copilot" instead of providerID.startsWith("github-copilot") for consistency with the existing api.npm === "@ai-sdk/openai" check.

TL;DR: Removing the conditional would technically work (SDKs currently strip unknown options), but it introduces unnecessary overhead, future risk, and is semantically incorrect. Refactoring to a cleaner architecture is out of scope for this fix.


Details

Why not just remove the conditional?

While SDKs currently strip unknown providerOptions silently (Zod schemas without .strict()), removing the conditional is problematic:

  • Unnecessary overhead — sending options that will be stripped is wasteful
  • Future risk — if a provider adds .strict() validation or their API starts rejecting unknown fields, it would break
  • Relying on implementation details — the current "strip unknown fields" behavior is not a guaranteed contract

Why not refactor to a VS Code-style "caller decides" approach?

VS Code uses a pass-through model where extensions decide what options to send. To achieve this in opencode would require significant architectural changes:

  1. Move store logic out of ProviderTransform.options() (the centralized option builder)
  2. Push the decision up to callers in session/llm.ts or higher
  3. Or implement a capability flag system like pi-mono's compat.supportsStore with auto-detection

All of these are out of scope for what should be a one-line fix.

Research findings:

  1. VS Code uses a pass-through approach where providerOptions are typed as { [name: string]: any } with no filtering—but extensions decide what to send, they don't set options unconditionally.

  2. AI SDK behavior: Both @ai-sdk/anthropic and @ai-sdk/google use Zod schemas to validate providerOptions. Since neither uses .strict(), unknown keys are silently stripped—but this is an implementation detail, not a contract.

  3. pi-mono uses a cleaner compat.supportsStore capability flag with auto-detection—a better architecture but significant refactoring.

Why keep the conditional:

  • Semantic correctness — only send OpenAI params to OpenAI-compatible providers
  • Consistency — matches existing patterns in transform.ts for other provider-specific options (OpenRouter usage.include, Google thinkingConfig, ZhipuAI thinking, etc.)
  • Defensive programming — don't rely on SDKs ignoring unknown fields
  • Minimal change — works within current architecture without refactoring

input.model.providerID === "openai" ||
input.model.api.npm === "@ai-sdk/openai" ||
input.model.api.npm === "@ai-sdk/github-copilot"
) {
result["store"] = false
}

Expand Down