Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cdbcfaa
fix(responses): promote additional_tools to top-level tools for xAI a…
omar391 Oct 23, 2026
0e6b3f4
fix(management): typed promoteAdditionalTools configuration, PATCH su…
omar391 Oct 24, 2026
ecc1bba
fix(responses): recalculate replay boundary when dropping reasoning i…
omar391 Oct 24, 2026
33a613e
fix(responses): allow explicit promoteAdditionalTools for forward aut…
omar391 Oct 25, 2026
52b3ab2
docs(providers): extract keychain configuration to linked sibling pag…
omar391 Oct 28, 2026
8a3552a
fix(responses): recalculate replay boundary when stripping canonical-…
omar391 Oct 28, 2026
fc2629e
fix(responses): isolate historical tool wrappers and preserve provena…
omar391 Oct 28, 2026
514a0e6
Merge origin/dev into additional-tools promotion carry
lidge-jun Oct 28, 2026
6f7c117
fix(responses): preserve live catalogs across replay rewrites
lidge-jun Oct 28, 2026
8bdb199
fix(responses): preserve replay provenance before input rewrites
lidge-jun Oct 28, 2026
27a27e0
Merge origin/dev into additional-tools provenance carry
lidge-jun Oct 28, 2026
48a8e31
fix(responses): keep replay provenance authoritative after rewrites
lidge-jun Oct 28, 2026
71c91fd
fix(responses): bound loaded-tool promotion to current discoveries
lidge-jun Oct 29, 2026
90ac07f
Merge origin/dev into PR 7637 historical discovery carry
lidge-jun Oct 29, 2026
31be65e
test(responses): update pre-promotion xAI tests for additional_tools …
omar391 Oct 29, 2026
6dfe3ab
Merge branch 'upstream/dev' into fix/responses-additional-tools-promo…
omar391 Oct 29, 2026
c62e494
fix(responses): keep existing item provenance when tagging the replay…
lidge-jun Oct 30, 2026
4319468
refactor(responses): drop an unused OpenCode Go import
lidge-jun Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs-site/src/content/docs/reference/configuration/keychain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Storing keys in the OS keychain
description: Storing provider API keys and Vertex credentials in macOS Keychain, Windows Credential Manager, or Linux Secret Service.
---

For Vertex, the dedicated API-key pool input (`POST /api/providers/keys`) accepts complete GCP
`service_account` or `authorized_user` credential JSON after the provider has been created.
The provider create/edit form and `ocx provider add --api-key` refuse credential JSON before
saving: create the provider without a key, then add the JSON through its key pool. Paste the
JSON contents, not a Windows, Unix, or relative file path. Multiple
JSON objects separated by commas or whitespace are stored together as separate pool entries;
the first becomes active. Configuration contains only `gcp-sa:<account>` references, never the
credential JSON. Mixed JSON/literal-key bulk pastes are rejected as a whole; add literal keys
separately. Storage failures leave the configuration unchanged and report a fixed error without
echoing credential material. Empty marker accounts are invalid and do not fall back to host credentials.
Paste one credential object or a sequence of objects, without an enclosing array or quoted string.
Malformed credential text is rejected. If a selected stored credential is removed or becomes unreadable,
Vertex refuses the request even when it previously cached an access token for that credential.
Removing a key or provider, or replacing a provider with `ocx provider add --force`, deletes its unreferenced GCP credentials after the configuration is
saved; a credential referenced by another provider is retained. If OS-store cleanup fails, a
warning requests manual inspection without undoing the committed configuration removal.
Ordinary keychain store/restore actions leave GCP markers and JSON in the OS store. Restoring
credential JSON through a `keychain:` alias is refused, and cleanup preserves accounts still
referenced through either marker spelling.
Using a `keychain:` alias containing GCP JSON as an API key also refuses the request; select its
`gcp-sa:` reference for Vertex authentication instead.
Literal API keys must be plain token text: JSON objects, arrays and quoted JSON strings are refused
in literal-key resolution and restore, including wrappers containing escaped credential text.
Credential writes and marker persistence share the cleanup transaction lock, so concurrent
removal cannot delete the stored JSON between read-back and the marker commit.

By default a provider's `apiKey` and `apiKeyPool` sit in `config.json` (mode 0600, atomic writes).
If you would rather keep the key material out of the file, move it into the OS credential store:

```bash
ocx provider keychain deepseek status # store: file | env | keychain, and whether the keychain answers
ocx provider keychain deepseek store # move active key + pool into the OS keychain
ocx provider keychain deepseek restore # bring the plaintext back and delete the keychain items
```

The same operations are `GET`/`POST /api/providers/keychain`. After `store`, `config.json` holds
`"apiKey": "keychain:deepseek"` (pool entries `keychain:deepseek/<id>`) and the secret lives under the
`opencodex.provider-api-key.v1` service in macOS Keychain, Windows Credential Manager, or the Linux
Secret Service. Backups of `config.json` therefore carry references only. Key rotation and failover
keep working: pool entries compare by reference, so a rotation never writes plaintext back.

Before touching the config, `store` writes and reads back every entry; if the keychain is unavailable
or the read-back does not match, it refuses with 503 and leaves the file as it was. At request time
a reference that cannot be read yields no credential and one warning per key — there is no plaintext
fallback, by design.

When not to opt in: a proxy running as a headless service (systemd, launchd, Task Scheduler) or in a
container usually has no unlocked keychain session, so requests would fail closed. Use an
`${ENV_VAR}` reference in the service environment there instead. Env references are left untouched
by `store`.
56 changes: 4 additions & 52 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `preserveResponsesMessageMetadata?` | `boolean` | Default off. Preserve private `internal_chat_message_metadata_passthrough` on input items for explicitly trusted launcher relays, including custom native v1 compact requests. This forwards private turn/session metadata to that destination; do not enable for ordinary gateways. |
| `dropResponsesReasoningItems?` | `boolean` | Remove replayed Responses `reasoning` items from continuation input entirely, including custom native v1 compact requests, instead of forwarding them with blanked content. This is lossy: summaries, item ids, and `encrypted_content` go with them, so no reasoning item from the previous turn remains in the forwarded input. It does not remove `previous_response_id`, so an upstream holding its own turn state may still reach it. Enable only for an upstream that rejects reasoning replay outright — the built-in Volcengine Ark Coding Plan preset sets it because validated tool continuations answered `400 InvalidParameter` otherwise. Set it to `false` on that provider to forward reasoning items again. |
| `disabled?` | `boolean` | Keep the provider on disk but exclude it from routing and model/catalog listings. |
| `apiKey?` | `string` | API key, an `${ENV_VAR}` / `$ENV_VAR` reference, or a `keychain:<provider>` reference written by `ocx provider keychain <name> store`. References resolve at request time. See [Storing keys in the OS keychain](#storing-keys-in-the-os-keychain). |
| `apiKey?` | `string` | API key, an `${ENV_VAR}` / `$ENV_VAR` reference, or a `keychain:<provider>` reference written by `ocx provider keychain <name> store`. References resolve at request time. See [Storing keys in the OS keychain](/reference/configuration/keychain/). |
| `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key header style. Defaults to native `x-api-key`; valid only for key-auth `anthropic` providers. |
| `apiKeyPool?` | `ApiKeyPoolEntry[]` | Multi-key pool. `apiKey` mirrors the active entry; each item has `id`, `key`, optional `label`, and optional numeric `addedAt`. |
| `apiKeyPoolStrategy?` | `"round-robin" \| "fill-first" \| "quota"` | How a warm key is chosen **before** the first attempt when the committed key is already cooling. Omitted keeps rotation reactive-only: the pool moves after a 429 or 401 and not before. `round-robin` takes the next key in the pool, `fill-first` keeps the first eligible one, and `quota` prefers the key with the most remaining headroom, falling back to `fill-first` order for a provider whose per-key quota is unknown. A healthy committed key is never overridden, so a manual key selection stands. |
Expand Down Expand Up @@ -370,6 +370,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `xaiResponsesXSearch?` | `boolean` | Disabled by default. On an xAI Responses destination, append the provider-hosted `x_search` declaration only when a live `web_search` tool survives final request normalization. Existing declarations are not duplicated, caller `tool_choice`/`allowed_tools` selectors are never widened, and this is separate from the web-search sidecar's `search.xSearch` options. |
| `modelPreferHostedTools?` | `Record<string,string[]>` | Exact-model opt-in for non-forward Responses gateways that reserve a hosted-tool namespace. Currently accepts only `["image_generation"]`; a matching model must use the `openai-responses` wire and support that hosted tool. It removes colliding client `image_gen` declarations and rewrites their selectors to preserve caller tool choice. For OpenAI API virtual `-pro` models, the selected public ID is matched first and the resolved base wire-model ID is a fallback. `modelAdapters` resolves the public ID first, then the base ID; the second resolution determines the final wire. Other models retain normal alias behavior. |
| `annotateEmptyToolOutputs?` | `boolean` | Replace a present-but-empty tool result with a short marker before it reaches the model, so a blank result is not read as a missing one. Applies to blank strings and text-only part arrays; image, file, and encrypted parts are never touched. Defaults to `true` for DeepSeek from the built-in registry and is otherwise unset. Set `false` to opt a provider out — an explicit `false` is preserved across later edits that omit the field. `PATCH /api/providers?name=<provider>` accepts `true`, `false`, or `null` to clear the override and return to registry-default behavior. |
| `promoteAdditionalTools?` | `boolean` | Whether to promote Codex Desktop's `additional_tools` input items into top-level `tools` and strip the wrapper from `input`. Enabled automatically for OpenCode Go and xAI (`api.x.ai` and `cli-chat-proxy.grok.com`); other OpenAI-strict third-party gateways that return HTTP 422 for unknown item types in `input` can opt in here. `PATCH /api/providers?name=<provider>` accepts a boolean or `null` to clear it. |
| `unsupportedHostedTools?` | `string[]` | Hosted tool declarations this Responses destination rejects, so they are stripped from `tools`, from client-loaded `additional_tools`, and from `tool_choice` instead of being forwarded and rejected upstream. Use it for an OpenAI-compatible gateway with a narrower capability set — one that accepts plain Responses requests and `function` tools but returns HTTP 400 for hosted `web_search` — so a text-only prompt is not failed by a capability it never needed. Accepts only hosted tool type names (`web_search`, `web_search_preview`, `file_search`, `computer_use_preview`, `computer_use`, `code_interpreter`, `image_generation`, `image_gen`, `mcp`, `tool_search`, `local_shell`, `x_search`); an unrecognized name is rejected rather than silently ignored. Spelling variants of one capability are aliased, so `["web_search"]` also denies `web_search_preview`. A provider cannot both deny a hosted tool here and prefer it in `modelPreferHostedTools`. Xiaomi MiMo Responses destinations (`xiaomimimo.com` and its subdomains, including `api.xiaomimimo.com` and `token-plan-cn.xiaomimimo.com`) automatically strip `web_search` and `web_search_preview` while preserving function tools; no declaration is needed for these hosts. This is independent of `supportsResponsesCustomTools`; set both for a gateway that also rejects native custom tools. `PATCH /api/providers?name=<provider>` accepts an array or `null` to clear it. |
| `reasoningEffortMap?` | `Record<string, string>` | Provider-wide wire aliases for reasoning labels. Map a label to `"__omit__"` to drop the reasoning field from the upstream request entirely: `reasoning_effort` on an OpenAI-compatible wire, and Ollama's native `think` field on the Ollama native adapter (#2356). |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | Per-model wire aliases for reasoning labels. Map a label to `"__omit__"` to drop the reasoning field from the upstream request entirely. |
Expand Down Expand Up @@ -1589,60 +1590,11 @@ same mapping applies to a native `vercel/<model-id>` selector: use the encoded
`vercel-ai-gateway/vercel-<model-id>` selector in OpenCodex and keep `vercel/<model-id>` as the
model key.

## Static model allowlists

## Storing keys in the OS keychain

For Vertex, the dedicated API-key pool input (`POST /api/providers/keys`) accepts complete GCP
`service_account` or `authorized_user` credential JSON after the provider has been created.
The provider create/edit form and `ocx provider add --api-key` refuse credential JSON before
saving: create the provider without a key, then add the JSON through its key pool. Paste the
JSON contents, not a Windows, Unix, or relative file path. Multiple
JSON objects separated by commas or whitespace are stored together as separate pool entries;
the first becomes active. Configuration contains only `gcp-sa:<account>` references, never the
credential JSON. Mixed JSON/literal-key bulk pastes are rejected as a whole; add literal keys
separately. Storage failures leave the configuration unchanged and report a fixed error without
echoing credential material. Empty marker accounts are invalid and do not fall back to host credentials.
Paste one credential object or a sequence of objects, without an enclosing array or quoted string.
Malformed credential text is rejected. If a selected stored credential is removed or becomes unreadable,
Vertex refuses the request even when it previously cached an access token for that credential.
Removing a key or provider, or replacing a provider with `ocx provider add --force`, deletes its unreferenced GCP credentials after the configuration is
saved; a credential referenced by another provider is retained. If OS-store cleanup fails, a
warning requests manual inspection without undoing the committed configuration removal.
Ordinary keychain store/restore actions leave GCP markers and JSON in the OS store. Restoring
credential JSON through a `keychain:` alias is refused, and cleanup preserves accounts still
referenced through either marker spelling.
Using a `keychain:` alias containing GCP JSON as an API key also refuses the request; select its
`gcp-sa:` reference for Vertex authentication instead.
Literal API keys must be plain token text: JSON objects, arrays and quoted JSON strings are refused
in literal-key resolution and restore, including wrappers containing escaped credential text.
Credential writes and marker persistence share the cleanup transaction lock, so concurrent
removal cannot delete the stored JSON between read-back and the marker commit.

By default a provider's `apiKey` and `apiKeyPool` sit in `config.json` (mode 0600, atomic writes).
If you would rather keep the key material out of the file, move it into the OS credential store:

```bash
ocx provider keychain deepseek status # store: file | env | keychain, and whether the keychain answers
ocx provider keychain deepseek store # move active key + pool into the OS keychain
ocx provider keychain deepseek restore # bring the plaintext back and delete the keychain items
```
API keys and credential references can be stored in the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service) instead of `config.json`. See [Storing keys in the OS keychain](/reference/configuration/keychain/) for details on migration commands, storage rules, and headless-service caveats.

The same operations are `GET`/`POST /api/providers/keychain`. After `store`, `config.json` holds
`"apiKey": "keychain:deepseek"` (pool entries `keychain:deepseek/<id>`) and the secret lives under the
`opencodex.provider-api-key.v1` service in macOS Keychain, Windows Credential Manager, or the Linux
Secret Service. Backups of `config.json` therefore carry references only. Key rotation and failover
keep working: pool entries compare by reference, so a rotation never writes plaintext back.

Before touching the config, `store` writes and reads back every entry; if the keychain is unavailable
or the read-back does not match, it refuses with 503 and leaves the file as it was. At request time
a reference that cannot be read yields no credential and one warning per key — there is no plaintext
fallback, by design.

When not to opt in: a proxy running as a headless service (systemd, launchd, Task Scheduler) or in a
container usually has no unlocked keychain session, so requests would fail closed. Use an
`${ENV_VAR}` reference in the service environment there instead. Env references are left untouched
by `store`.
## Static model allowlists

The `zhipu-bigmodel-responses` preset seeds `glm-5.3`, `glm-5.3-flash` and `glm-5-turbo` with
`liveModels: false` for `https://open.bigmodel.cn/api/v1`. Its static roster and
Expand Down
3 changes: 3 additions & 0 deletions scripts/test-layout/layout-providers.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"xai-additional-tools.test.ts": "providers/xai",
"xai-replay-provenance.test.ts": "providers/xai",
"xai-loaded-tool-provenance.test.ts": "providers/xai",
"free-directory-runcrate.test.ts": "providers",
"cc-switch-import.test.ts": "providers",
"kiro-completion-with-real-tools.test.ts": "providers/kiro"
Expand Down
32 changes: 27 additions & 5 deletions src/adapters/openai-responses/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { normalizeRoutedAgentMessages } from "../routed-agent-messages";
import { frameCompactionHistory } from "../../responses/compaction-history-framing";
import { nameRoutedIdentity, repairIdentityInResponsesBody, stripRoutedIdentity } from "../identity";
import { stripBracketedModelSuffix } from "../openai-chat";
import { normalizeOpenCodeGoAdditionalTools } from "../opencode-go-additional-tools";
import { isOpenCodeGoResponsesUrl, isXaiResponsesUrl, promoteResponsesAdditionalTools } from "../opencode-go-additional-tools";
import { isXaiResponsesDestination } from "../../providers/xai-transport";
import { applyGithubCopilotContextTier } from "../../providers/github-copilot-context";
import { Buffer } from "node:buffer";
Expand Down Expand Up @@ -48,6 +48,7 @@ import { usageFromResponsesPayload } from "./usage";
import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems, restoreBridgedWebSearchCalls } from "./tool-output-recovery";
import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
import { tagHistoricalInputItems } from "../../responses/item-provenance";
import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
Expand Down Expand Up @@ -174,8 +175,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
let plaintextV2AgentMessageAliasedToolNames: ReadonlySet<string> | undefined;
let convertedMuseToolNameAliases: Map<string, string> | undefined;
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
let outBody = stripPreviousResponseId(
parsed._rawBody,
const replayPrefixLen = parsed._replayPrefixLen;
let outBody = parsed._rawBody;
// Clone and tag before any input rewrite can remove, replace, or reorder replay items.
if (
typeof replayPrefixLen === "number"
&& replayPrefixLen > 0
&& isPlainObject(outBody)
&& Array.isArray(outBody.input)
) {
outBody = {
...outBody,
input: tagHistoricalInputItems(outBody.input, replayPrefixLen),
};
}
outBody = stripPreviousResponseId(
outBody,
forward || parsed._previousResponseInputExpanded === true,
);
if (!forward) outBody = normalizeRoutedAgentMessages(outBody, {
Expand Down Expand Up @@ -360,8 +375,15 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// Last, so promoted namespace children are also cleared of Codex-private fields.
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
}
if (!forward) {
outBody = normalizeOpenCodeGoAdditionalTools(outBody, url, parsed._replayPrefixLen);
if (
provider.promoteAdditionalTools === true
|| (!forward && (
isOpenCodeGoResponsesUrl(url)
|| isXaiResponsesDestination(provider)
|| isXaiResponsesUrl(url)
))
) {
outBody = promoteResponsesAdditionalTools(outBody);
}
// Same native-v2/portable predicate as the routedCompaction gate in handleResponses():
// neither forward auth nor native v1 endpoint support implies trigger support. The
Expand Down
Loading
Loading