Skip to content

fix(opencode): write auth.json in opencode's real schema + proxy fixes for Gemini/GPT - #119

Merged
dgokeeffe merged 1 commit into
mainfrom
fix/opencode-auth-schema-and-proxy
Aug 5, 2026
Merged

fix(opencode): write auth.json in opencode's real schema + proxy fixes for Gemini/GPT#119
dgokeeffe merged 1 commit into
mainfrom
fix/opencode-auth-schema-and-proxy

Conversation

@dgokeeffe

Copy link
Copy Markdown
Collaborator

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 the enterprise_config.npm_env wiring.

1. auth.json was the wrong shape (real bug on main)

opencode stores credentials as 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,
    ...
}) {}

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, and both sides wrote it:

  • setup_opencode.py{"databricks": {"api_key": ...}}
  • cli_auth._update_opencode() → rotated api_key every ~10 min

So 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_key explicitly — 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 existing workspace_sync_dest() helper. Rotation is now also scoped to type == "api", so oauth/wellknown credentials 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 no tools, so the top-level cleanup below it — stream_options, $schemanever 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

  • Strip exclusiveMinimum, exclusiveMaximum, multipleOf, uniqueItems — Gemini 400s the whole request on these, 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. Stripping globally would silently downgrade output on reasoning-capable models.
  • Flatten Anthropic-style content arrays to a string in both message and streaming delta. Absent content stays absent rather than becoming "", 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 pydantic-core pin (main has 2.46.4 after #110).

Verification

535 tests pass. New tests/test_opencode_auth_schema.py holds 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.

⚠️ 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.

Credit to @mpkrass7 for finding the auth.json schema mismatch and the Gemini/GPT request-shaping issues.

… 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>
@dgokeeffe
dgokeeffe merged commit 80afd9e into main Aug 5, 2026
@dgokeeffe
dgokeeffe deleted the fix/opencode-auth-schema-and-proxy branch August 5, 2026 10:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant