fix(opencode): sync provider model hotfixes#482
Conversation
📝 WalkthroughWalkthroughAdds Azure-aware model selection, provider-model hook machinery (including post-config rerun), OpenAI/Azure POST-body sanitization, expanded reasoning-effort/variant logic (OpenAI/Codex/GPT-5/Mistral), Copilot variant refactor, Codex OAuth allowlist update, reasoning-part emission change, and corresponding tests and wiring. ChangesProvider model, transform, and message flow
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant Loader as Provider.Loader
participant Config as ConfigStore
participant Plugin as Plugin.Hooks
participant Upstream as Upstream(Azure/OpenAI/Gateway)
Client->>Loader: request generation
Loader->>Config: load providers & models
Config->>Plugin: provide config models/hooks
Plugin->>Loader: apply provider-model hooks (postConfig when flagged) rgba(120,180,60,0.5)
Loader->>Loader: sanitize POST via stripOpenAIResponseInputIDs
Loader->>Upstream: selectAzureLanguageModel / send request
Upstream-->>Loader: response
Loader-->>Client: normalized result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request refactors model and provider handling, notably moving Codex model filtering to a provider hook and enhancing GitHub Copilot's variant detection for reasoning and adaptive thinking. It introduces support for the Cloudflare AI Gateway, defaults interleaved reasoning fields for DeepSeek models, and updates Azure provider settings to disable storage by default. Additionally, the logic for handling reasoning message parts during model transitions has been improved. I have no feedback to provide.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/opencode/src/provider/transform.ts (1)
688-701:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winWrap the
@ai-sdk/openaiswitch clause in braces.Line 688 declares a lexical variable directly under the
casestatement, triggering Biome'snoSwitchDeclarationserror rule. Add braces to block-scope the clause.Suggested fix
- case "@ai-sdk/openai": - // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai - const openaiEfforts = openaiReasoningEfforts(model.api.id, model.release_date) - if (!openaiEfforts) return {} - return Object.fromEntries( - openaiEfforts.map((effort) => [ - effort, - { - reasoningEffort: effort, - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - }, - ]), - ) + case "@ai-sdk/openai": { + // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai + const openaiEfforts = openaiReasoningEfforts(model.api.id, model.release_date) + if (!openaiEfforts) return {} + return Object.fromEntries( + openaiEfforts.map((effort) => [ + effort, + { + reasoningEffort: effort, + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + ]), + ) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/opencode/src/provider/transform.ts` around lines 688 - 701, The switch clause for case "@ai-sdk/openai" declares lexical variables (openaiEfforts) directly under the case which triggers the noSwitchDeclarations error; wrap the entire case body in a block (add { ... } after the case) so openaiEfforts and any other const/let declarations are block-scoped, preserving the existing logic that calls openaiReasoningEfforts(model.api.id, model.release_date), guards on !openaiEfforts, and returns the Object.fromEntries(...) mapping.packages/opencode/src/provider/provider.ts (1)
1530-1544:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard the OpenAI/Azure body rewrite against non-string payloads.
At line 1535, the code uses
JSON.parse(opts.body as string)without verifying thatopts.bodyis actually a string—it could beFormData, a stream, or anotherBodyInittype from the Fetch API. Similarly, line 1539 assumes every item inbody.inputis an object with anincheck that could fail on primitives. In this generic fetch wrapper, either condition failing will throw before the request is sent, converting an otherwise valid provider call into a hard failure.Suggested fix
if ( (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") && - opts.body && + typeof opts.body === "string" && opts.method === "POST" ) { - const body = JSON.parse(opts.body as string) - const keepIds = body.store === true - if (!keepIds && Array.isArray(body.input)) { - for (const item of body.input) { - if ("id" in item) { - delete item.id - } - } - opts.body = JSON.stringify(body) - } + try { + const body = JSON.parse(opts.body) as Record<string, unknown> + const keepIds = body.store === true + if (!keepIds && Array.isArray(body.input)) { + for (const item of body.input) { + if (item && typeof item === "object" && "id" in item) { + delete (item as Record<string, unknown>).id + } + } + opts.body = JSON.stringify(body) + } + } catch { + // leave non-JSON payloads untouched + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/opencode/src/provider/provider.ts` around lines 1530 - 1544, The body-rewrite for OpenAI/Azure requests in provider.ts is unsafe because it unconditionally JSON.parse(opts.body as string) and assumes body.input contains objects; change the logic in the block that checks model.api.npm (the code creating keepIds and parsing body) to first verify opts.body is a string (typeof opts.body === "string") before JSON.parse, and only proceed if parsing yields an object with Array.isArray(body.input); when iterating body.input ensure each item is an object (typeof item === "object" && item !== null) before checking or deleting the "id" property; if any guard fails, leave opts.body untouched so non-string BodyInit types (FormData/streams/primitives) are not mutated.
🧹 Nitpick comments (1)
packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts (1)
13-15: ⚡ Quick winCapture all intercepted gateway requests to avoid retry-order flakiness
capturedis overwritten per request, so a multi-call flow can make assertions depend on last-call ordering rather than the relevant upstream payload.Suggested refactor
-type Captured = { url: string; outerBody: unknown } +type Captured = { url: string; outerBody: unknown } const realFetch = globalThis.fetch -let captured: Captured | null = null +let captured: Captured[] = [] beforeEach(() => { - captured = null + captured = [] const handle = async ( @@ - captured = { url, outerBody: bodyText ? JSON.parse(bodyText) : null } + captured.push({ url, outerBody: bodyText ? JSON.parse(bodyText) : null }) @@ async function callThroughGateway(apiId: string, providerOptions: ProviderOptions) { const aigateway = createAiGateway({ accountId: "test", gateway: "test", apiKey: "test" }) const unified = createUnified() await generateText({ model: aigateway(unified(apiId)), prompt: "hi", providerOptions }) - return extractUpstreamQuery(captured?.outerBody) + return captured.map((entry) => extractUpstreamQuery(entry.outerBody)).find((query) => query !== undefined) }Also applies to: 29-30, 81-86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts` around lines 13 - 15, The test currently overwrites the single variable captured for each intercepted fetch, causing flakiness; change captured from a single Captured | null to an array (e.g., let captured: Captured[] = []) and in the fetch interception handler push each new captured object instead of assigning it, then update all places that read captured (including the other occurrences in this test around the fetch stubs and assertions) to index into the array (e.g., captured[0], captured[1]) or assert on array contents/length to make multi-call flows stable; ensure type signatures and any teardown that referenced the old captured are adjusted accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/opencode/src/provider/provider.ts`:
- Around line 1530-1544: The body-rewrite for OpenAI/Azure requests in
provider.ts is unsafe because it unconditionally JSON.parse(opts.body as string)
and assumes body.input contains objects; change the logic in the block that
checks model.api.npm (the code creating keepIds and parsing body) to first
verify opts.body is a string (typeof opts.body === "string") before JSON.parse,
and only proceed if parsing yields an object with Array.isArray(body.input);
when iterating body.input ensure each item is an object (typeof item ===
"object" && item !== null) before checking or deleting the "id" property; if any
guard fails, leave opts.body untouched so non-string BodyInit types
(FormData/streams/primitives) are not mutated.
In `@packages/opencode/src/provider/transform.ts`:
- Around line 688-701: The switch clause for case "@ai-sdk/openai" declares
lexical variables (openaiEfforts) directly under the case which triggers the
noSwitchDeclarations error; wrap the entire case body in a block (add { ... }
after the case) so openaiEfforts and any other const/let declarations are
block-scoped, preserving the existing logic that calls
openaiReasoningEfforts(model.api.id, model.release_date), guards on
!openaiEfforts, and returns the Object.fromEntries(...) mapping.
---
Nitpick comments:
In `@packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts`:
- Around line 13-15: The test currently overwrites the single variable captured
for each intercepted fetch, causing flakiness; change captured from a single
Captured | null to an array (e.g., let captured: Captured[] = []) and in the
fetch interception handler push each new captured object instead of assigning
it, then update all places that read captured (including the other occurrences
in this test around the fetch stubs and assertions) to index into the array
(e.g., captured[0], captured[1]) or assert on array contents/length to make
multi-call flows stable; ensure type signatures and any teardown that referenced
the old captured are adjusted accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 919de33f-256d-4439-adad-27abe41e57e1
📒 Files selected for processing (11)
packages/opencode/src/plugin/codex.tspackages/opencode/src/plugin/github-copilot/models.tspackages/opencode/src/provider/provider.tspackages/opencode/src/provider/transform.tspackages/opencode/src/session/message-v2.tspackages/opencode/test/plugin/codex.test.tspackages/opencode/test/plugin/github-copilot-models.test.tspackages/opencode/test/provider/cf-ai-gateway-e2e.test.tspackages/opencode/test/provider/provider.test.tspackages/opencode/test/provider/transform.test.tspackages/opencode/test/session/message-v2.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/opencode/src/provider/transform.ts (1)
675-677:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse the shared GPT-5 matcher for Azure reasoning variants.
This branch only recognizes
gpt-5andgpt-5-*, so versioned Azure IDs likegpt-5.4never get theminimalvariant. That makes Azure GPT-5.x variants diverge from the OpenAI path in the same file.Proposed fix
- if (id.includes("gpt-5-") || id === "gpt-5") { + if (GPT5_FAMILY_RE.test(id)) { azureEfforts.unshift("minimal") }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/opencode/src/provider/transform.ts` around lines 675 - 677, The branch that adds "minimal" to azureEfforts currently checks id.includes("gpt-5-") || id === "gpt-5" and misses versioned Azure names like "gpt-5.4"; replace that ad-hoc check with the project's shared GPT-5 matcher used elsewhere (e.g., the helper function named isGpt5Model or matchesGpt5) so the condition becomes "if (sharedGpt5Matcher(id)) { azureEfforts.unshift('minimal') }", ensuring all GPT-5 variants (including gpt-5.x) get the minimal Azure variant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/opencode/src/plugin/codex.ts`:
- Around line 445-447: The filter is using the alias key (modelId) to decide
Codex OAuth allowlist status, letting aliases like "my-codex-alias" bypass
rules; update the predicate to use the upstream id instead: call
shouldKeepCodexOAuthModel with model.api.id (instead of modelId) so the check is
based on the real API id (e.g., change shouldKeepCodexOAuthModel(modelId,
model.api.id) to shouldKeepCodexOAuthModel(model.api.id) or otherwise pass
model.api.id as the identifier argument), referencing provider.models, modelId,
model.api.id and shouldKeepCodexOAuthModel to locate the change.
---
Outside diff comments:
In `@packages/opencode/src/provider/transform.ts`:
- Around line 675-677: The branch that adds "minimal" to azureEfforts currently
checks id.includes("gpt-5-") || id === "gpt-5" and misses versioned Azure names
like "gpt-5.4"; replace that ad-hoc check with the project's shared GPT-5
matcher used elsewhere (e.g., the helper function named isGpt5Model or
matchesGpt5) so the condition becomes "if (sharedGpt5Matcher(id)) {
azureEfforts.unshift('minimal') }", ensuring all GPT-5 variants (including
gpt-5.x) get the minimal Azure variant.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e747d60d-ea0e-4225-b42a-20759fc36d66
📒 Files selected for processing (6)
packages/opencode/src/plugin/codex.tspackages/opencode/src/provider/provider.tspackages/opencode/src/provider/transform.tspackages/opencode/test/provider/cf-ai-gateway-e2e.test.tspackages/opencode/test/provider/provider.test.tspackages/plugin/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/opencode/src/provider/transform.ts`:
- Around line 1145-1151: The message-level provider remapping in message() is
out of sync with providerOptions(): update message() so it uses the same
dotted-key logic as providerOptions() — detect if model.providerID.includes(".")
and use model.providerID.split(".")[0] as the remap key for dotted providers,
otherwise fall back to sdkKey(model.api.npm) ?? model.providerID — this ensures
request-level and message-level options are serialized under the same provider
namespace (match the logic used in providerOptions()).
In `@packages/opencode/test/provider/provider.test.ts`:
- Around line 2958-3006: Before mutating global auth with Auth.remove or
Auth.set in the test, capture the current auth state (e.g., read/auth-export the
existing auth.json via the Auth API or filesystem) and save it to a local
variable; after the test (in the finally block) restore the original state by
re-applying the saved snapshot (using Auth.set or writing back the saved
auth.json) so pre-existing credentials are preserved and tests cannot clobber
global auth or leak across parallel runs; apply this snapshot/restore pattern
around the Auth.remove call in this test (and the similar tests noted) inside
the Instance.provide/init/finally flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e4cbe1a0-5ad3-4e4b-85ae-e7a20a054e7f
📒 Files selected for processing (5)
packages/opencode/src/plugin/codex.tspackages/opencode/src/provider/transform.tspackages/opencode/test/plugin/codex.test.tspackages/opencode/test/provider/provider.test.tspackages/opencode/test/provider/transform.test.ts
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/opencode/src/provider/transform.ts`:
- Around line 721-724: The Copilot adaptive-effort clamp only checks
model.api.id.includes("opus-4.7") and misses the alternate spelling "opus-4-7";
update the conditional in the block that handles model.providerID ===
"github-copilot" to accept both spellings (e.g., check model.api.id for
includes("opus-4.7") OR includes("opus-4-7") or normalize the id string before
checking) so efforts = ["medium"] runs for either form, then continue filtering
out "max" and "xhigh" as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f0df2e59-0309-4570-a26d-752326160b69
📒 Files selected for processing (3)
packages/opencode/src/provider/transform.tspackages/opencode/test/provider/provider.test.tspackages/opencode/test/provider/transform.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/opencode/test/provider/transform.test.ts
- packages/opencode/test/provider/provider.test.ts
Refs #477. Port PR Slice 2 from the upstream opencode sync range after 17701628bd. This slice covers auth/client credential hotfixes only; provider/model work already landed in #482 and this PR intentionally excludes runtime/session/tool safety, desktop packaging, HttpApi listener migration, generated SDK/OpenAPI, dependency, and lockfile work. Upstream behavior covered: - #25529: provider auth login inherits stderr so child-command failures are visible. - #25591: ACP internal SDK client sends server auth. - #25592: SDK/CLI error formatting no longer surfaces bare `{}` for empty or opaque server errors. - #25596: internal clients respect `OPENCODE_SERVER_USERNAME`. - #25600: `opencode run --attach` supports explicit `--username/-u`. - #25636: app startup captures `auth_token` credentials, removes the token from URL/history, and keeps terminal websocket auth working when credentials came from a startup token. Implementation summary: - Added `ServerAuth` as the shared Basic auth helper for header construction and credential validation. - Rewired server middleware, ACP, plugin-internal clients, and `run --attach` to use shared auth behavior. - Preserved Basic Auth browser challenge behavior with `WWW-Authenticate: Basic realm="opencode"`. - Avoided mutating incoming Fetch/Bun request headers; `auth_token` query credentials are parsed as local auth candidates. - Added app helpers for auth token encode/decode and startup token server resolution. - Preserved startup-token credentials over same-URL persisted servers while keeping display names and preventing the runtime `authToken` marker from entering persisted server records. - Moved terminal websocket URL construction into a helper with explicit tests for non-same-origin saved credentials, same-origin saved credentials, and same-origin startup-token credentials. - Wrapped empty generated SDK error payloads into readable `Error` objects while passing non-empty parsed errors through. Review follow-up: - Fixed Gemini's request-header mutation finding by removing `c.req.raw.headers.set(...)` from auth middleware. - Restored Basic Auth challenge headers after switching away from `hono/basic-auth`. - Hardened server persistence types so `authToken` remains runtime-only. - Preserved stored display names when startup token credentials override a same-URL server. Verification: - `bun test test/server/auth.test.ts test/util/error.test.ts`: 13 passed. - `bun test --preload ./happydom.ts src/utils/server.test.ts src/context/server.test.ts src/utils/terminal-websocket-url.test.ts`: 10 passed. - `bun run typecheck` in `packages/opencode`: passed. - `bun run typecheck` in `packages/app`: passed. - `bun run typecheck` in `packages/sdk/js`: passed. - `git diff --check`: clean. - PR CI before merge: ci, codeql, desktop-smoke, e2e-artifacts, dependency-review, commit-lint, CodeRabbit, and semantic PR title all success.
Summary
Why
#477 tracks PawWork's upstream opencode sync after
17701628bd. This PR implements the first bounded provider/model slice while intentionally excluding auth/client credentials, runtime/session retry and serialization fixes, desktop/app work, HttpApi/Effect/listener work, generated SDK/OpenAPI files, and dependency changes.Related Issue
Refs #477
Human Review Status
Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.
Review Focus
message-v2replay change for reasoning parts when continuing a transcript with a different model.Risk Notes
ai-gateway-providerdependency.How To Verify
Screenshots or Recordings
Not applicable. This PR has no visible UI changes.
Checklist
dev, and my PR title and commit messages use Conventional Commits in EnglishMaintainer labeling requested: type fix, area provider/opencode, priority appropriate for #477 sync slice.
Summary by CodeRabbit
New Features
Bug Fixes
Tests