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")) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if they support it for all gpt-5 variants just change condition to modelID.includes("gpt-5")?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

gpt-5-mini is not supported and I was probably being overly conservative. Should I remove opt-in and just get all gpt-5 variants (excluding gpt-5-mini) to use responses API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've simplified the logic now. No opt-in. Includes all gpt-5 variants except gpt-5-mini (refer to #5866 for reasoning).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All gpt models support it now, so just do the "gpt-5" check and we can merge

@christso christso Jan 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

E2E testing confirmed gpt-5-mini does NOT support the Responses API. GitHub Copilot's backend returns: model gpt-5-mini is not supported via Responses API. The exclusion must stay.
image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's strange cause it works for me and their api says it supports it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OpenAI supports gpt-5-mini on Responses API, but GitHub Copilot does NOT. Are you testing against OpenAI directly (api.openai.com) or GitHub Copilot (api.githubcopilot.com)?

You can copy this curl command and run it with your own token. The API explicitly returns error code unsupported_api_for_model.

# Test: gpt-5-mini on GitHub Copilot Responses API
# Replace YOUR_TOKEN with your GitHub Copilot token from:
#   ~/.local/share/opencode/auth.json (github-copilot.refresh)

COPILOT_TOKEN="YOUR_TOKEN"

echo "=== Chat API (works) ===" && \
curl -s -X POST "https://api.githubcopilot.com/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${COPILOT_TOKEN}" \
-H "User-Agent: opencode/1.0.0" \
-H "Openai-Intent: conversation-edits" \
-d '{"model": "gpt-5-mini", "messages": [{"role": "user", "content": "Say hi"}], "max_tokens": 10}'

echo -e "\n\n=== Responses API (fails) ===" && \
curl -s -X POST "https://api.githubcopilot.com/responses" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${COPILOT_TOKEN}" \
-H "User-Agent: opencode/1.0.0" \
-H "Openai-Intent: conversation-edits" \
-d '{"model": "gpt-5-mini", "input": [{"role": "user", "content": [{"type": "input_text", "text": "Say hi"}]}]}'

Results:
┌──────────────────────────┬───────────────────────────────────────────────────────────┐
│           API            │                        gpt-5-mini                         │
├──────────────────────────┼───────────────────────────────────────────────────────────┤
│ Chat (/chat/completions) │ ✅ Works                                                  │
├──────────────────────────┼───────────────────────────────────────────────────────────┤
│ Responses (/responses)   │ ❌ "model gpt-5-mini is not supported via Responses API." │
└──────────────────────────┴───────────────────────────────────────────────────────────┘

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm testing directly w/ copilot and it does work and when I hit copilot endpoint to get their models list that works too, also discussed w/ multiple others and it worked for them as well

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

but ig there is some oddity to it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm on copilot enterprise, maybe it behaves differently.

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 @@ -484,7 +484,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
Copy Markdown
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
Copy Markdown
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

@christso christso Jan 16, 2026

Copy link
Copy Markdown
Contributor Author

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.providerID.startsWith("github-copilot")
) {
result["store"] = false
}

Expand Down