Skip to content

feat(zdr): support zero data retention via CMD_ZDR and provider headers - #194

Merged
BrainerVirus merged 5 commits into
BrainerVirus:mainfrom
mohemohe:feature/zdr
Oct 19, 2026
Merged

BrainerVirus merged 5 commits into
BrainerVirus:mainfrom
mohemohe:feature/zdr

Conversation

@mohemohe

Copy link
Copy Markdown
Contributor

Summary

Adds opt-in zero data retention (ZDR) for the Command Code provider: with CMD_ZDR=1 (or zdr: true in the plugin
config) every request carries x-cmd-zdr: 1, the same opt-in the Command Code CLI exposes.

Reference: Provider API — Zero data retention

The plugin talks to the Provider API directly (/provider/v1/...), so the CLI-only CMD_ZDR variable had no effect here before. The header routes the request through ZDR-capable upstreams only and fails with 422 cmd_zdr_no_providers instead of falling back to a non-ZDR provider, so it is worth making it explicitly configurable.

Changes

  • src/zdr.ts (new) — single implementation of the ZDR decision and header
    • CMD_ZDR counts as on for 1/true/yes/on (trimmed, case-insensitive)
    • zdrEnabled() = config zdr: true OR the env var
    • withZdrHeaders() merges x-cmd-zdr: 1 without discarding existing headers, and never creates a headers key
      when ZDR is off
  • plugin.ts
    • V1 server(): writes the header into provider.commandcode.options.headers. V1 merges provider options into
      every model of the provider, whichever provider.npm a model overrides, so one map reaches the Chat Completions,
      Messages, and Responses routes. Custom npm transports keep owning their options and are left untouched
    • V2 setup(): writes the header into the registered provider's info.headers on add, and merges it into the
      existing provider on update
  • index.ts — the createCommandCode SDK factory honors CMD_ZDR too (an explicit headers value wins)
  • src/schemas.ts — PluginFileConfigSchema gains zdr: z.boolean().default(false)
  • Docs — README.md (config table + ZDR section) and docs/specs/2026-10-14-zdr-request-header.md (new spec)
  • Tests — tests/unit/zdr.test.ts (new) plus V1/V2/SDK/schema unit additions, and
    tests/integration/zdr.integration.test.ts (new, live)

Behavior

  • Precedence: an explicit user value always wins ("x-cmd-zdr": "0" stays as-is), and other headers in the same map
    survive untouched. With ZDR off the generated config is identical to before
  • API behavior: routing goes through ZDR-capable upstreams only; a model without one fails with
    422 cmd_zdr_no_providers rather than falling back. The upstream can change, so cost can change with it
  • Anthropic models on /messages are already zero-data-retention at the account level; the header is accepted there
    for parity and does not change routing
  • Usage: CMD_ZDR=1 opencode, or { "zdr": true } in ~/.config/opencode/opencode-commandcode.json

Verification

Unit gate (offline, runs in CI):

bun run check
# 250 pass / 0 fail (oxlint --deny-warnings + oxfmt --check + bun test tests/unit/ + tsc --noEmit)

Live verification (manual, outside bun run check; skipped when COMMANDCODE_API_KEY is unset):

COMMANDCODE_API_KEY=... bun run test:integration

Observed while preparing this PR (command-code@1.82.1, live Provider API):

ZDR probe: 18/78 chat models probed, 9 served, 1 refused
ZDR-capable sample: gpt-5.6-sol, gpt-6-luna, gpt-5.6-luna
ZDR-refused sample: deepseek/deepseek-v4.1-flash-fast

11 pass / 0 fail

@BrainerVirus

BrainerVirus commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Thanks for contributing this! ZDR support is a useful addition. Lint, formatting, all 250 unit tests, typechecking, and the local package check passed. Review found a few things to address before approval:

1. Header casing

In src/zdr.ts, an existing X-Cmd-Zdr: 0 gets combined with the default as 1, 0, rather than preserving the user’s value.

Please check existing header names case-insensitively before adding the default, and add mixed-case regression tests.

2. Config fallback

With { "zdr": true, "debugStartupLogs": "false" }, the invalid logging field causes the entire config to fall back to defaults—including zdr: false.

Please preserve valid settings or reject invalid config rather than silently disabling ZDR. Add coverage through both plugin hooks.

3. SDK enforcement

The SDK calls /alpha/generate, but the linked ZDR documentation covers /provider/v1/*. The current SDK test checks a successful response, which doesn’t prove the endpoint enforces ZDR.

Please establish that endpoint’s ZDR support and test enforcement, or limit the SDK claims to sending the header until support is confirmed.

4. Live-test cancellation

Test timeouts don’t cancel the underlying requests, so paid probes may continue afterward. Please add request cancellation and stop scheduling probes when the deadline expires.

Small cleanup and CI

  • The test at tests/integration/zdr.integration.test.ts:216 only logs a message; it can be removed.
  • Please sync with main before final checks.
  • CI appears to be waiting for maintainer permission to run—we’ll handle that separately.

We haven’t verified live ZDR enforcement or real OpenCode V1/V2 requests yet. Happy to help with the remaining changes if useful. Thanks again for the contribution!

@BrainerVirus

BrainerVirus commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

We have now checked real requests using the local PR code at ed0834a on OpenCode V1.19.3 in Docker and V2.0.27 in an isolated local process, without using the installed npm plugin for either test.

Both versions used the free glyph-cluster:free model on /provider/v1/chat/completions:

  • ZDR on: observed x-cmd-zdr: 1 on outgoing requests; the API refused with 422 cmd_zdr_no_providers.
  • ZDR off: the same model and prompt, with no ZDR header, returned 200 and OK. Some control requests hit rate limits before succeeding.

This confirms header propagation and the API’s refusal behavior for the tested Chat Completions path on both generations. Thanks for adding this! The fixes in the main review still apply. SDK /alpha/generate, Messages, and Responses remain unverified; these tests do not independently audit server-side data retention.

Add a zero-data-retention opt-in that sends `x-cmd-zdr: 1` on every
Command Code request. Enable it with env `CMD_ZDR=1` (1/true/yes/on)
or `zdr: true` in the plugin config file.

V1 merges the header into provider options.headers, V2 registers it
on the commandcode provider, and createCommandCode honors CMD_ZDR.
Explicit user headers always win, and custom npm transports are left
untouched.
Match x-cmd-zdr case-insensitively and collapse every spelling into the
lowercase key, so a user X-Cmd-Zdr: 0 is kept instead of being sent as
"1, 0".

Fall back per field in PluginFileConfigSchema so an invalid sibling no
longer discards a valid zdr opt-in; a present but invalid zdr value fails
safe to on.

createCommandCode calls /alpha/generate, which the ZDR docs do not cover,
so languageModel() now throws when ZDR is requested instead of sending
the header; an explicit x-cmd-zdr: 0 opts out of CMD_ZDR.
Tie every live request to an AbortSignal that fires before the test
timeout, so a timed-out run cancels in-flight paid probes and schedules
no new ones. Drop the live SDK test (the SDK now fails closed) and the
log-only skip test.
Scope ZDR to /provider/v1/*, describe case-insensitive header handling,
per-field config fallback, and the SDK fail-closed behavior, and limit
the Messages/Responses claim to what has been verified live.
OpenCode's plugin loader calls only default.server for this package, so
it never invokes createCommandCode. The guard lives in languageModel()
because every caller, a direct import or the create* lookup for a
provider npm set to this package, resolves a model there before it can
send a request.
@mohemohe

Copy link
Copy Markdown
Contributor Author

@BrainerVirus Thank you for the review. Below are the respective changes.

1. Header casing

Fixed in 85e9414.

  • findZdrHeader() (src/zdr.ts) looks up x-cmd-zdr case-insensitively. If more than one spelling is present, the lowercase one wins.
  • When ZDR is on, withZdrHeaders() folds every spelling (X-Cmd-Zdr, X-CMD-ZDR, …) into a single lowercase x-cmd-zdr key and keeps the user’s value. X-Cmd-Zdr: 0 now stays 0 instead of being sent as 1, 0.
  • The caller’s map is not mutated, and other headers are left alone. With ZDR off, the map is returned unchanged.

Regression tests:

  • tests/unit/zdr.test.ts: case-insensitive lookup, folding into one key, no mutation, off-state passthrough
  • tests/unit/plugin.test.ts / tests/unit/plugin-v2.test.ts: X-Cmd-Zdr: 0 with CMD_ZDR=1 ends up as { "x-cmd-zdr": "0" } on both V1 and V2

2. Config fallback

Fixed in 85e9414. I went with "keep the valid settings":

  • Each field of PluginFileConfigSchema now has its own .catch(). An invalid field falls back to its own default and no longer takes the rest of the config with it. { "zdr": true, "debugStartupLogs": "false" } now gives zdr: true, debugStartupLogs: false.
  • zdr fails safe. A missing key still means off, but a value that is present and invalid ("true", 1, null) counts as on. A mistaken opt-in can at worst produce a visible 422 cmd_zdr_no_providers. A silently dropped opt-in would send prompts to non-ZDR upstreams, so I chose the former.

Coverage:

  • tests/unit/schemas.test.ts: per-field fallback and the invalid-zdr case
  • Both plugin hooks:
    • V1 server().config in tests/unit/plugin.test.ts
    • V2 setup() in tests/unit/plugin-v2.test.ts
  • These hook tests write a real opencode-commandcode.json with CMD_ZDR unset (helper: tests/helpers/plugin-config.ts), so the header can only come from the config file.

3. SDK enforcement

I couldn’t establish that /alpha/generate enforces ZDR, so the SDK no longer claims to support it (85e9414, docs in 08585b1 / 399f580).

  • Scope. Both plugin generations send every model through /provider/v1/*:
    • The default @ai-sdk/openai-compatible package uses it.
    • The @ai-sdk/anthropic (Messages) and @ai-sdk/openai (Responses) packages that a model can select inherit the same provider base URL.
    • createCommandCode is the only path to /alpha/generate. The plugin never calls it. It is reached only by a direct import, or when a user points a provider npm at this package.
  • Fail closed. When ZDR is requested, languageModel() throws before any request is sent. "Requested" means CMD_ZDR is on, or an x-cmd-zdr: 1 header is passed in any casing. An explicit x-cmd-zdr: 0 opts the SDK out of CMD_ZDR. The SDK no longer injects the header at all.
  • Tests. The old live SDK test only showed that a request succeeded, so I removed it. The unit tests in tests/unit/index.test.ts now assert:
    • The guard throws and fetch is never called.
    • An explicit header in any casing also throws.
    • The 0 opt-out works.
    • No ZDR header is sent when ZDR is off.
  • Docs. README and the spec now scope ZDR to /provider/v1/*. They also say live verification covers Chat Completions only; the header is sent on Messages and Responses, but routing there is unverified.

4. Live-test cancellation

Fixed in 5a64a57.

  • Every live request (GET /models, each probe, and the control request) carries an AbortSignal. It fires 15s before the test timeout (REQUEST_BUDGET_MS = NETWORK_TIMEOUT_MS - 15_000).
  • The probe workers check signal.aborted before starting a new probe. When the deadline passes, in-flight probes are cancelled, nothing new is scheduled, and the cancelled probes are dropped without failing beforeAll.
  • I checked the mechanism against a local delayed server. With 4 concurrent in-flight fetches, all 4 were rejected with TimeoutError at the deadline, and the server saw each request cancelled.
  • After this change I re-ran the live suite against the real API (bun test tests/integration/): 9 pass / 0 fail. That run finished well inside the deadline, so the abort path itself was covered by the local check above.
ZDR probe: 17/82 chat models probed, 8 served, 1 refused
ZDR-capable sample: gpt-6-luna, gpt-5.6-sol, gpt-5.6-luna
ZDR-refused sample: deepseek/deepseek-v4.1-flash-fast

Small cleanup and CI

  • The log-only test at tests/integration/zdr.integration.test.ts:216 is removed (5a64a57).
  • Rebased onto current main (af0b968, v0.11.1). The only conflict was the README feature list, and I kept both bullets: the new "(Free)" label and ZDR.

@BrainerVirus BrainerVirus left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM — verification details in the review comment.

@BrainerVirus

BrainerVirus commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Thanks for contributing this, @mohemohe.

Re-checked the head (85e9414 + 5a64a57 + docs 08585b1/399f580) on a clean clone, then live on the real API with a temporary key.

Build + suite — bun run build && bun run check → 274 pass / 0 fail, tsc clean. (Note: check alone fails release-candidate.test.ts on main too — it needs a built dist/; building first turns it green.)

1. Header casing — confirmed. Case-insensitive lookup, every spelling folded into one lowercase key with the user's value kept, no mutation. Plant (case-sensitive lookup) fails 2 tests in zdr.test.ts. Live (V1.19.3, Docker): CMD_ZDR=1 + X-Cmd-Zdr: 0 in the provider headers → the model answered "OK" — without the fold it would have sent 1 and been refused.

2. Config fallback — confirmed. Per-field .catch(); {"zdr":true,"debugStartupLogs":"false"} keeps both. The zdr fail-safe direction (present-but-invalid → on) is the right trade-off: a spurious 422 beats a silent downgrade to non-ZDR upstreams. Plant (.catch(false)) fails the schema test.

3. SDK enforcement — confirmed. createCommandCode throws before any request when ZDR is requested (env or any-cased header) and 0 opts out; no header injection. Plant (guard disabled) fails 2 tests in index.test.ts. Scoping ZDR to /provider/v1/*, and stating Messages/Responses are unverified, matches the API docs.

4. Live-test cancellation — the budget AbortSignal and the drop-at-deadline behaviour are right; the local delayed-server check is a valid proof.

Live verification (real API, temporary key, free/cheap models)

  • Live suite: bun test tests/integration/ → 9 pass / 0 fail (18/84 chat models probed; 9 served; 1 refused — deepseek/deepseek-v4.1-flash-fast).
  • V1.19.3 (Docker, the local plugin via file://, glyph-cluster:free):
    • CMD_ZDR=1 → refused: "This model has no zero-data-retention upstream. Remove the x-cmd-zdr header or choose a different model." (the 422).
    • no ZDR → OK (control: the header is what causes the refusal).
    • CMD_ZDR=1 + X-Cmd-Zdr: 0 → OK.
  • V1.19.3 + config file {"zdr": true} + gpt-6-luna (cheapest ZDR-capable, $0.20/$0.75 per M) → OK.
  • V2 (2.0.32, isolated process + service port) + config {"zdr": true} + gpt-6-luna → OK.

This does not audit server-side retention and does not re-claim /alpha/generate — consistent with the docs. LGTM — approving.

@BrainerVirus
BrainerVirus enabled auto-merge (squash) October 19, 2026 15:58
@BrainerVirus
BrainerVirus merged commit 57c8b29 into BrainerVirus:main Oct 19, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants