fix(opencode): write auth.json in opencode's real schema + proxy fixes for Gemini/GPT - #119
Merged
Merged
Conversation
… for Gemini/GPT Cherry-picks the still-relevant parts of #79 (mpkrass7) onto current main. Merging that branch as-is would have reverted a lot: it predates main's SP-OAuth token resolution, proxy tracing, mtime-invalidated token cache, the opus-4-8 catalog with 1M context limits, and the enterprise_config npm_env wiring. Only the fixes main is actually missing are taken. ## auth.json was the wrong shape opencode stores credentials as a map of provider-id -> credential, where the credential is a discriminated union on `type`. The API-key variant keeps the secret in `key`: export class Api extends Schema.Class<Api>("ApiAuth")({ type: Schema.Literal("api"), key: Schema.String, metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) {} const _Info = Schema.Union([Oauth, Api, WellKnown]) .annotate({ discriminator: "type", identifier: "Auth" }) (opencode, packages/opencode/src/auth/index.ts) `api_key` is not a field opencode recognises. Both sides wrote it: setup_opencode.py produced `{"databricks": {"api_key": ...}}`, and cli_auth._update_opencode() rotated `api_key` every 10 minutes — so the credential was unloadable and rotation updated a key nothing reads. It went unnoticed because the two were *consistently* wrong (every unit test agreed with them — the old tests asserted `api_key` explicitly), and because OpenCode routes through the content-filter proxy, which injects a fresh bearer token per request and masks the broken credential at runtime. The shape now lives once in `utils.opencode_api_credential()` / `is_opencode_api_credential()`, shared by writer and rotator. That, rather than fixing both call sites, is what stops it drifting again — same reasoning as the existing `workspace_sync_dest()` helper. Rotation is also now scoped to `type == "api"`, so an `oauth` or `wellknown` credential can't have a PAT written into it. ## Tool-less requests were skipping sanitisation entirely `sanitize_tool_schemas()` early-returned when a request had no `tools`, so the top-level cleanup below it — `stream_options`, `$schema`, and now the reasoning keys — never ran on a plain chat turn. Found by a test written for the new GPT stripping, which failed until the early return came out. Pre-existing on main. ## Proxy compatibility fixes - Strip `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `uniqueItems`. Gemini 400s the whole request on these rather than ignoring them, so an unstripped key makes the tool unusable, not merely unvalidated. - Drop `minimum`/`maximum` on `type: integer` only. Kept for `number`, since over-stripping loses real constraints. - Strip `reasoning_effort` / `reasoningSummary` for GPT-routed models only, matched on the model id. Stripping globally would silently downgrade output on models that support reasoning. - Flatten Anthropic-style `content` block arrays to a plain string in both the non-streaming `message` and the streaming `delta`. OpenAI-shaped clients expect a string and render the raw array otherwise. Absent `content` is left absent rather than set to "", so a tool-call-only message isn't turned into an empty assistant turn. ## Not taken The setup_opencode.py model-catalog rewrite (main's is newer) and the requirements.txt `pydantic-core` pin (main already has 2.46.4 after #110). ## Verification 535 tests pass. New coverage: tests/test_opencode_auth_schema.py holds the writer/rotator contract that was missing — whatever the writer emits, the real rotator must be able to rotate, with 0600 preserved — plus the schema-stripping, GPT-scoping, flattening, and tool-less-request cases. Confirmed the auth and early-return tests fail without their fixes. Note: the Gemini/GPT fixes are latent while app.yaml disables Codex and Gemini (no compatible gateway endpoints). The auth.json fix is not latent — OpenCode is enabled. Co-authored-by: Marshall Krassenstein <mpkrass7@users.noreply.github.com>
This was referenced Aug 5, 2026
dgokeeffe
added a commit
that referenced
this pull request
Aug 5, 2026
… for Gemini/GPT (#119) Cherry-picks the still-relevant parts of #79 (mpkrass7) onto current main. Merging that branch as-is would have reverted a lot: it predates main's SP-OAuth token resolution, proxy tracing, mtime-invalidated token cache, the opus-4-8 catalog with 1M context limits, and the enterprise_config npm_env wiring. Only the fixes main is actually missing are taken. ## auth.json was the wrong shape opencode stores credentials as a map of provider-id -> credential, where the credential is a discriminated union on `type`. The API-key variant keeps the secret in `key`: export class Api extends Schema.Class<Api>("ApiAuth")({ type: Schema.Literal("api"), key: Schema.String, metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) {} const _Info = Schema.Union([Oauth, Api, WellKnown]) .annotate({ discriminator: "type", identifier: "Auth" }) (opencode, packages/opencode/src/auth/index.ts) `api_key` is not a field opencode recognises. Both sides wrote it: setup_opencode.py produced `{"databricks": {"api_key": ...}}`, and cli_auth._update_opencode() rotated `api_key` every 10 minutes — so the credential was unloadable and rotation updated a key nothing reads. It went unnoticed because the two were *consistently* wrong (every unit test agreed with them — the old tests asserted `api_key` explicitly), and because OpenCode routes through the content-filter proxy, which injects a fresh bearer token per request and masks the broken credential at runtime. The shape now lives once in `utils.opencode_api_credential()` / `is_opencode_api_credential()`, shared by writer and rotator. That, rather than fixing both call sites, is what stops it drifting again — same reasoning as the existing `workspace_sync_dest()` helper. Rotation is also now scoped to `type == "api"`, so an `oauth` or `wellknown` credential can't have a PAT written into it. ## Tool-less requests were skipping sanitisation entirely `sanitize_tool_schemas()` early-returned when a request had no `tools`, so the top-level cleanup below it — `stream_options`, `$schema`, and now the reasoning keys — never ran on a plain chat turn. Found by a test written for the new GPT stripping, which failed until the early return came out. Pre-existing on main. ## Proxy compatibility fixes - Strip `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `uniqueItems`. Gemini 400s the whole request on these rather than ignoring them, so an unstripped key makes the tool unusable, not merely unvalidated. - Drop `minimum`/`maximum` on `type: integer` only. Kept for `number`, since over-stripping loses real constraints. - Strip `reasoning_effort` / `reasoningSummary` for GPT-routed models only, matched on the model id. Stripping globally would silently downgrade output on models that support reasoning. - Flatten Anthropic-style `content` block arrays to a plain string in both the non-streaming `message` and the streaming `delta`. OpenAI-shaped clients expect a string and render the raw array otherwise. Absent `content` is left absent rather than set to "", so a tool-call-only message isn't turned into an empty assistant turn. ## Not taken The setup_opencode.py model-catalog rewrite (main's is newer) and the requirements.txt `pydantic-core` pin (main already has 2.46.4 after #110). ## Verification 535 tests pass. New coverage: tests/test_opencode_auth_schema.py holds the writer/rotator contract that was missing — whatever the writer emits, the real rotator must be able to rotate, with 0600 preserved — plus the schema-stripping, GPT-scoping, flattening, and tool-less-request cases. Confirmed the auth and early-return tests fail without their fixes. Note: the Gemini/GPT fixes are latent while app.yaml disables Codex and Gemini (no compatible gateway endpoints). The auth.json fix is not latent — OpenCode is enabled. Co-authored-by: Marshall Krassenstein <mpkrass7@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry-picks the still-relevant parts of #79 (@mpkrass7) onto current
main. Merging that branch wholesale would have reverted a lot — it predates main's SP-OAuth token resolution, proxy tracing, mtime-invalidated token cache, the opus-4-8 catalog with 1M context limits, and theenterprise_config.npm_envwiring.1.
auth.jsonwas the wrong shape (real bug on main)opencode stores credentials as a discriminated union on
type; the API-key variant keeps the secret inkey:opencode,
packages/opencode/src/auth/index.tsapi_keyis not a field opencode recognises, and both sides wrote it:setup_opencode.py→{"databricks": {"api_key": ...}}cli_auth._update_opencode()→ rotatedapi_keyevery ~10 minSo the credential was unloadable, and every rotation updated a key nothing reads.
Why nobody noticed: the two were consistently wrong — the old tests asserted
api_keyexplicitly — and OpenCode routes through the content-filter proxy, which injects a fresh bearer per request and masks the broken credential at runtime.The shape now lives once in
utils.opencode_api_credential()/is_opencode_api_credential(), shared by writer and rotator. That's what stops the drift recurring, rather than just fixing both call sites — same reasoning as the existingworkspace_sync_dest()helper. Rotation is now also scoped totype == "api", sooauth/wellknowncredentials can't have a PAT written into them.2. Tool-less requests skipped sanitisation entirely (also pre-existing)
sanitize_tool_schemas()early-returned when a request had notools, so the top-level cleanup below it —stream_options,$schema— never ran on a plain chat turn. Found by a test written for the new GPT stripping, which failed until the early return came out.3. Proxy compatibility fixes
exclusiveMinimum,exclusiveMaximum,multipleOf,uniqueItems— Gemini 400s the whole request on these, so an unstripped key makes the tool unusable, not merely unvalidated.minimum/maximumontype: integeronly — kept fornumber, since over-stripping loses real constraints.reasoning_effort/reasoningSummaryfor GPT-routed models only. Stripping globally would silently downgrade output on reasoning-capable models.contentarrays to a string in bothmessageand streamingdelta. Absentcontentstays absent rather than becoming"", so a tool-call-only message isn't turned into an empty assistant turn.Not taken
The
setup_opencode.pymodel-catalog rewrite (main's is newer) and thepydantic-corepin (main has 2.46.4 after #110).Verification
535 tests pass. New
tests/test_opencode_auth_schema.pyholds the contract that was missing: whatever the writer emits, the real rotator must be able to rotate, with 0600 preserved. Confirmed the auth and early-return tests fail without their fixes.app.yamldisables Codex and Gemini (no compatible gateway endpoints). The auth.json fix is not latent — OpenCode is enabled.Credit to @mpkrass7 for finding the auth.json schema mismatch and the Gemini/GPT request-shaping issues.