diff --git a/.ci/test-count-floors.json b/.ci/test-count-floors.json index 25f3195fe..8add96f6f 100644 --- a/.ci/test-count-floors.json +++ b/.ci/test-count-floors.json @@ -1,17 +1,17 @@ { "floors": { - "core": 325, - "opencode": 1741, - "pi": 155 + "core": 1508, + "opencode": 2010, + "pi": 220 }, "measurement": { - "head": "eb10f1479ff7fb91bdfbc2ce266666a569f41db5", + "head": "688a8becd40c25d851116f987b55707c99f3d870", "dirtyPaths": 0 }, "evidence": { - "_note": "Pinned Bun 1.3.14 runner totals after the mural cache anchor fix. The clean branch commit was measured locally; clean PR CI independently matched all totals. Runner counts, not source declaration counts, are the authority at the gate.", - "core": "325 passing at clean branch commit eb10f147; independently matched by clean CI run 37244390451", - "opencode": "1741 passing at clean branch commit eb10f147; independently matched by clean CI run 37244390451", - "pi": "155 passing at clean branch commit eb10f147; independently matched by clean CI run 37244390451" + "_note": "Pinned Bun 1.3.14 unit totals measured on native cutover head 688a8bec by CI run 38161601147. The unit census passed, including disposable packed hosts; the later E2E step failed separately. Runner counts, not source declaration counts, are the authority at the gate.", + "core": "1508 passing at head 688a8bec; CI run 38161601147 MEASURED: core 1508", + "opencode": "2010 passing at head 688a8bec, including packed host cases; CI run 38161601147 MEASURED: opencode 2010", + "pi": "220 passing at head 688a8bec; CI run 38161601147 MEASURED: pi 220" } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f40fde78..6fe0f0a8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,16 @@ jobs: fetch-depth: 0 - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 - run: bun install --frozen-lockfile + - name: Install the locked OC1 config dependency fixture + run: npm ci --prefix packages/e2e-tests/fixtures/opencode1-config-deps --ignore-scripts --no-audit --no-fund - run: bun run check:workspace-lock - run: bun run types - run: bun run build + - run: bun test --cwd packages/opencode scripts/check-bundle-globals.test.ts - run: bun test scripts/check-pi-dist-imports.test.ts - run: bun run check:pi-dist-imports + - run: bun run --cwd packages/core test:scripts + - run: bun test scripts/tests/run-mutation-ci.test.ts scripts/tests/run-mutation-bun.test.ts - run: bun run check:claustrum-golden env: GITHUB_TOKEN: ${{ github.token }} @@ -34,7 +39,8 @@ jobs: env: TUI_SMOKE_SKIP_BUILD: "1" - run: bun run test:pi-host - - run: npm install -g opencode-ai@1.18.18 - run: bun run test:e2e + env: + ANTHROPIC_AUTH_E2E_OPENCODE_CONFIG_DEPS: ${{ github.workspace }}/packages/e2e-tests/fixtures/opencode1-config-deps - run: bun run format:check - run: bun run lint diff --git a/.github/workflows/mutations.yml b/.github/workflows/mutations.yml new file mode 100644 index 000000000..6e2920b8e --- /dev/null +++ b/.github/workflows/mutations.yml @@ -0,0 +1,54 @@ +name: Mutation guards + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: '17 3 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mutation-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + guards: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + MUTATION_EVENT: ${{ github.event_name }} + MUTATION_BASE: ${{ github.event.pull_request.base.sha }} + MUTATION_HOME: ${{ github.workspace }}/node_modules/.cache/ckdev-mutate-0.9.2 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + - run: bun install --frozen-lockfile + - name: Build shared Core for test preloads + run: bun run --cwd packages/core build + - name: Install pinned mutation runner + shell: bash + run: | + CARGO_HOME="$MUTATION_HOME/cargo-home" \ + CARGO_TARGET_DIR="$MUTATION_HOME/target" \ + cargo install --locked --git https://github.com/cortexkit/commons \ + --rev b491012199d98b09fd167e00783df297d35a62b8 \ + --root "$MUTATION_HOME/install" cortexkit-mutate + echo "$MUTATION_HOME/install/bin" >> "$GITHUB_PATH" + - name: Verify CI selection and report guards + run: bun test scripts/tests/run-mutation-ci.test.ts scripts/tests/run-mutation-bun.test.ts + - name: Replay mutation guards + run: bun scripts/run-mutation-ci.ts + - name: Retain reports and raw test evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: mutation-${{ github.run_id }}-${{ github.run_attempt }} + path: tmp/mutations/ + if-no-files-found: warn diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 25a3781bb..d37b24c67 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,12 +43,12 @@ **OpenCode Request Lifecycle:** -1. **Plugin load** — OpenCode loads `@cortexkit/opencode-anthropic-auth` plugin, which starts background refresh timers, creates `QuotaManager` and `FallbackAccountManager`, creates or loads the rotation-invariant main-account slot ID, initializes RPC server, and registers `auth.loader` + `provider.models` hooks. The model hook publishes Fable 5.1, limited-access Mythos 5.1, Opus 5.5, and Sonnet 5.5 with their native adaptive effort variants, and publishes native adaptive `low` through `max` effort variants for Opus 5 and Opus 5.5 instead of legacy manual-thinking budgets. On boot, the plugin resolves the initial sidebar routing only after the asynchronous account-storage load completes, ensuring a peer's concurrent routing-authoritative publish during the load is not overwritten by a stale pre-load snapshot. — `packages/opencode/src/index.ts` +1. **Plugin load** — OpenCode loads `@cortexkit/opencode-anthropic-auth` plugin, which starts background refresh timers, creates `QuotaManager` and `FallbackAccountManager`, creates or loads the main-account slot ID that stays stable when OAuth tokens rotate, initializes RPC server, and registers `auth.loader` + `provider.models` hooks. The model hook publishes Fable 5.1, limited-access Mythos 5.1, Opus 5.5, Sonnet 5.5, and Haiku 5.5 with their native adaptive effort variants, and publishes native adaptive `low` through `max` effort variants for Opus 5 and Opus 5.5 instead of legacy manual-thinking budgets. On boot, the plugin waits for account storage to load before resolving initial sidebar routing from the current shared state. A snapshot taken before the load could otherwise overwrite an active account and route published by another plugin instance during the load. — `packages/opencode/src/index.ts` 2. **Auth loader** — When OpenCode creates an Anthropic session, the plugin's `auth.loader` runs: captures the OAuth `getAuth` function and starts the main token-refresh background loop. Main refreshes are coalesced by account-storage path across every project plugin instance in the process, so a slow host credential write cannot make peer requests time out behind the refresh file lock. Before a main OAuth request uses quota state, the plugin resolves Anthropic's account UUID through Claude Code bootstrap; concurrent lookups for the same access token share one promise, while a request whose credentials genuinely change during resolution aborts. A sticky request whose old access token receives 401 re-reads host auth and directly retries with a concurrently rotated, still-valid access token rather than refreshing the new credential again. The OAuth exchange callback and the post-login acknowledgement read the account store best-effort: a corrupt or partially written file degrades to the missing-entry path and never discards just-exchanged credentials. — `packages/opencode/src/index.ts` (AnthropicAuthPlugin → auth.loader) 3. **Command registration** — Plugin registers `/claude-cache`, `/claude-cachekeep`, `/claude-prime`, `/claude-start`, `/claude-quota`, `/claude-dump`, `/claude-fast`, `/claude-routing`, `/claude-killswitch`, `/claude-account`, `/claude-logging` — `packages/opencode/src/index.ts` (config hook) 4. **Request interception** — OpenCode's messages hook observes messages to mark effort transitions and assign user-turn prompt UUIDs, `chat.headers` injects internal correlation headers (`x-cortexkit-effort-plan`, `x-cortexkit-billing-lineage`), and OpenCode's fetch wrapper intercepts Anthropic fetch calls, resolving and stripping internal headers before dispatch — `packages/opencode/src/index.ts`, `packages/opencode/src/billing-lineage.ts` (experimental fetch wrapping) 5. **URL rewrite** — `rewriteUrl()` adds `?beta=true` to `/v1/messages`, normalizes versioned base paths without duplicating version segments, and overrides base URL when `ANTHROPIC_BASE_URL` or a proxy route `baseURL` is set — `packages/opencode/src/transform.ts` -6. **Request body rewrite** — `rewriteRequestBody()` removes only provably empty trailing assistant messages, refuses meaningful or unknown assistant tails before cache/billing/signing, and strips trailing whitespace after tool_use, normalizes Fable/Mythos 5/5.1, Sonnet 5/5.5, and Opus 5/5.5 adaptive thinking (mapping Sonnet 5.5 opt-outs to `between_tools` and removing its unsupported forced tool choice, enforcing always-on adaptive thinking on Opus 5.5 and normalizing `[1m]` context-window qualifiers via `normalizeAnthropicModelId`), injects the Claude Code 2.1.280 billing header with a bounded process-local suffix pin derived from the initial visible user text and optional request lineage (`cc_prev_req` and `cc_prompt_id`), sanitizes the system prompt (removes OpenCode identity), prepends Claude Code identity, applies cache strategy (explicit/automatic/hybrid), adds fast mode only to Opus 4.8/5/5.5, remaps model names on proxy routes using `ANTHROPIC_DEFAULT_*_MODEL` tier-specific or `ANTHROPIC_MODEL` generic overrides, prefixes tool names and named-tool choices with `mcp_`, removes forced `tool_choice` on Opus 5.5 and Fable/Mythos 5.1 (OpenCode still rejects missing structured output), opts eligible OAuth Fable 5/5.1, Opus 5, and Opus 5.5 requests into Anthropic server-side safety fallback, restores stored fallback boundary markers, conditionally applies the configured Fable 5.1 thinking-prefix behavior only when signed or redacted thinking remains in the outgoing history, keeps top-level Fable 5.1 effort anchored to the earliest retained turn while inserting later effort changes as empty system markers, applies cache diagnostics opt-in (`diagnostics.previous_message_id`), shapes a correlated OAuth lane-start request to one output token without thinking, strips request lineage before reusing bodies in source cache warming or prewarming, and creates `cch` over the final serialized body — `packages/opencode/src/transform.ts`, `packages/opencode/src/server-fallback.ts`, `packages/core/src/thinking-binding.ts`, `packages/core/src/model-remap.ts`, `packages/opencode/src/cache-diagnostics.ts`, `packages/core/src/cch.ts` +6. **Request body rewrite** — `rewriteRequestBody()` removes only provably empty trailing assistant messages, refuses meaningful or unknown assistant tails before cache/billing/signing, and strips trailing whitespace after tool_use, normalizes Fable/Mythos 5/5.1, Sonnet 5/5.5, Haiku 5.5, and Opus 5/5.5 adaptive thinking (mapping Sonnet 5.5 opt-outs to `between_tools` and removing its unsupported forced tool choice, enforcing always-on adaptive thinking on Opus 5.5 and normalizing `[1m]` context-window qualifiers via `normalizeAnthropicModelId`), injects the Claude Code 2.1.280 billing header with a bounded process-local suffix pin derived from the initial visible user text and optional request lineage (`cc_prev_req` and `cc_prompt_id`), sanitizes the system prompt (removes OpenCode identity), prepends Claude Code identity, applies cache strategy (explicit/automatic/hybrid), adds fast mode only to Opus 4.8/5/5.5, remaps model names on proxy routes using `ANTHROPIC_DEFAULT_*_MODEL` tier-specific or `ANTHROPIC_MODEL` generic overrides, prefixes tool names and named-tool choices with `mcp_`, removes forced `tool_choice` on Opus 5.5 and Fable/Mythos 5.1 (OpenCode still rejects missing structured output), opts eligible OAuth Fable 5/5.1, Opus 5, and Opus 5.5 requests into Anthropic server-side safety fallback, restores stored fallback boundary markers, conditionally applies the configured Fable 5.1 thinking-prefix behavior only when signed or redacted thinking remains in the outgoing history, keeps top-level Fable 5.1 effort anchored to the earliest retained turn while inserting later effort changes as empty system markers, applies cache diagnostics opt-in (`diagnostics.previous_message_id`), shapes a correlated OAuth lane-start request to one output token without thinking, strips request lineage before reusing bodies in source cache warming or prewarming, and creates `cch` over the final serialized body — `packages/opencode/src/transform.ts`, `packages/opencode/src/server-fallback.ts`, `packages/core/src/thinking-binding.ts`, `packages/core/src/model-remap.ts`, `packages/opencode/src/cache-diagnostics.ts`, `packages/core/src/cch.ts` 7. **Routing** — `shouldFallbackStatus()` checks if response should trigger fallback; `FallbackAccountManager` iterates accounts in ordered modes, while `StickySessionRouter` assigns cold sessions by reset-normalized spendable OAuth quota and weighted initial-prompt deficit, then persists hashed session affinity across processes/restarts. Sticky routes retain transient failures, including DNS-related refresh backoffs on a fixed five-minute retry interval while the fallback's current access token remains valid, hold confirmed 5h exhaustion when reset is within 15 minutes, and migrate for longer confirmed exhaustion/permanent account failure. All modes respect model-scoped quotas and killswitch thresholds (including per-model scoped thresholds). API-key routes remain excluded until fresh general OAuth exhaustion is confirmed; model-scoped exhaustion can move only among OAuth accounts. A complete sticky pool with no eligible route returns an explicit 401 for a permanent main-account authentication failure, an explicit 401 naming fallback OAuth accounts that require re-login, or a quota-policy 429 rather than falling through to an excluded account; incomplete pools fail retryably. Killswitch blocks are classified as scoped-driven (matching a specific model's weekly limit) or account-level (5h/7d limits) with a model-specific or generic retry hint — `packages/core/src/routing.ts`, `packages/core/src/sticky-routing.ts`, `packages/core/src/accounts.ts`, `packages/opencode/src/index.ts` 8. **Relay** — `sendViaRelay()` sends full or patched body to Cloudflare Worker, which streams Anthropic response back. HTTP relay responses and WebSocket `response_start` control messages deliver genuine upstream headers to the OpenCode account-bound quota harvester and billing lineage tracker (committing upstream `req_*` request IDs for subsequent session turns); synthetic optimistic response headers are never harvest input, and relay-to-direct fallback stays owned by the direct path — `packages/core/src/relay.ts`, `packages/opencode/src/index.ts` 9. **SSE stream and content-filter safety fallback** — By default, OAuth Fable 5/5.1 and Opus 5 requests send `fallbacks: "default"` with Anthropic's `server-side-fallback-2026-07-01` beta. The stream wrapper detects initial `fallback` blocks, sticky `fallback_message` usage iterations, and restoration to the requested model. Because OpenCode does not natively preserve `fallback` content blocks, the wrapper rewrites each block into a hidden signed thinking marker for storage, then the next eligible request restores the exact Anthropic fallback boundary before signing and sending. If an eligible OAuth response still terminates in refusal, the deterministic 10-successful-response Opus 4.8 recovery activates as a client-side backstop. Source-model prewarms explicitly remove `fallbacks` so they cannot be sticky-routed back to a fallback model. If a served fallback completes a `tool_use` before its terminal refusal, the wrapper preserves that completed call, changes the terminal finish to `tool_use`, and lets OpenCode continue on Opus 4.8 with the existing `tool_result` instead of replaying the tool. The active Anthropic-selected target and later restoration are written per session to the TUI sidebar state; when no matching TUI is connected, OpenCode Desktop receives an ignored/no-reply `promptAsync` notice that the plugin schedules delivery as soon as `session.idle` is observed instead of waiting for a later `session.updated` event, which OpenCode 1.18 no longer emits after idle; delivery still escapes the awaited event handler with `setImmediate` because OpenCode evaluates the run-loop exit condition only after awaiting event handlers. Intermediate `tool-calls` completions do not flush notices. When a refusal arms client recovery, the Desktop switch notice is not queued until the first downgraded Opus response completes successfully, so a transient idle state between the refused source response and OpenCode's internal retry cannot insert a user message or consume the retry response. The plugin probes `session.status()` outside that critical section and revalidates its idle-delivery lease after both the asynchronous status lookup and prompt-context lookup so a new prompt cannot adopt the ignored notice as its retry parent. A newly created user-message event revokes that lease before OpenCode's later busy-status event, while repeated updates for the user message that produced the current idle event do not suppress delivery; if multiple fallback state changes overtake delivery, only the newest notice is retained. Delivered notice message IDs are tracked per session (bounded to four IDs across at most 128 recently used sessions) and excluded from user-message lease revocation so the plugin's own notice cannot cancel its successor; delivery additionally requires the notice still head its queue, with a superseded queue looping to the newest instead of dropping it. Inconclusive probes re-arm for at most four attempts, with attempt 0 on `setImmediate` and later attempts delayed by `25` ms multiplied by the attempt number, and requires message-ID placement before the active assistant; consecutive notices use strictly increasing IDs bounded by that assistant, taking both host history and the latest locally prepared notice into account. If no safe ID can be allocated, delivery stays queued rather than letting the host mint an untracked ID. IDs are registered before dispatch so the notice's own user-message event cannot revoke its delivery lease. This removes the unsafe host-assigned-ID fallback when placement fails. Custom API-key routes strip the beta and `fallbacks` field. Setting `OPENCODE_ANTHROPIC_AUTH_FALLBACK_MODE=legacy` bypasses the server policy and uses the same deterministic Opus 4.8 recovery from the first refusal, keyed by session and source-model family with account-bound source-model prewarming and standby cache bridges beyond Anthropic's 20-block lookback — `packages/opencode/src/transform.ts`, `packages/opencode/src/server-fallback.ts`, `packages/opencode/src/fable-fallback.ts`, `packages/opencode/src/prompt-context.ts`, `packages/opencode/src/index.ts` @@ -57,7 +57,7 @@ **Pi Request Lifecycle:** 1. **Extension load** — Pi loads `@cortexkit/pi-anthropic-auth` package, which calls `registerCommands()` and `pi.registerProvider("anthropic", ...)`, and hooks `turn_start` to derive Fable 5.1 effort history directly from the session branch (deriving compaction trims locally when `buildContextEntries()` is absent) — `packages/pi/src/index.ts`, `packages/pi/src/effort-history.ts` -2. **Provider registration** — Provider defines OAuth login/refresh functions (delegating to core's `authorize`/`exchange`/`refreshClaudeOAuthToken`), exposes the CortexKit model catalog including Fable 5.1, limited-access Mythos 5.1, Opus 5, and Opus 5.5, and registers a `streamSimple` function — `packages/pi/src/index.ts` +2. **Provider registration** — Provider defines OAuth login/refresh functions (delegating to core's `authorize`/`exchange`/`refreshClaudeOAuthToken`), exposes the CortexKit model catalog including Fable 5.1, limited-access Mythos 5.1, Opus 5/5.5, Sonnet 5.5, and Haiku 5.5 (with native request-wide pricing tiers above 100K input), and registers a `streamSimple` function — `packages/pi/src/index.ts` 3. **Request conversion** — `buildAnthropicRequest()` builds the Claude Code-compatible body and signature. Pi's transcript-replay helpers live in `packages/pi/src/transcript.ts` rather than being imported from the host SDK at runtime, because Oh My Pi's legacy Pi SDK shim does not export them; the emitted Pi bundle's host imports are syntax-checked in CI and release. Flattens host system prompts whether provided as strings or ordered prompt blocks (e.g. Oh My Pi string/text block arrays) on blank lines so the paragraph split classifies identical text. The recognized Pi documentation paragraph is kept out of top-level `system[]`—where its path-enumeration/cross-reference text can cause OAuth billing rejection—and is prepended as a separately cached block before the first user's text. If Pi changes the documentation heading or prompt layout, the converter moves the complete host prompt to that safe cached block rather than restoring the rejected top-level shape. Signed thinking is replayed only when the stored assistant message came from the same Anthropic API and model as the current request, matching Pi's built-in origin check; foreign signatures are stripped before Anthropic validation while visible reasoning is retained as ordinary text. Pi redacted reasoning maps losslessly to Anthropic `redacted_thinking`. The converter pins the first-user billing suffix per Pi session, places cache breakpoints without exceeding 4 blocks by walking cache control holders and keeping automatic and hybrid placement separate, and an OAuth Fable 5.1 continuation with replayed signed or redacted thinking adds `thinking.block_binding.prefix_mismatch_behavior = "drop_block"` plus the matching beta so compaction drops stale thinking instead of rejecting the request; first turns and API-key routes receive neither control. — `packages/pi/src/convert.ts`, `packages/core/src/thinking-binding.ts` 4. **Stream implementation** — `streamCortexKitAnthropic()` in `packages/pi/src/stream.ts` sends via relay or direct, handles ordered or persistent sticky-balanced routing (including model-scoped OAuth routing and the confirmed-general-exhaustion gate for API-key routes), configures version-normalized base paths (`buildExplicitBaseMessagesUrl`) and safe proxy custom headers (`applyCustomHeaders`) for API-key routes, manages cache keepalive, maps returned Claude Code tool names against the same normalized host tool set used to build the outgoing request (Pi ≥0.86 puts tools in transcript system messages rather than `context.tools`), and converts streamed `redacted_thinking` blocks into Pi's replayable redacted reasoning representation 5. **Slash commands** — `/claude-*` commands registered in `packages/pi/src/commands.ts` reuse core command execution functions @@ -165,9 +165,9 @@ - Pattern: Request-time restoration converts CortexKit-signed hidden markers back to Anthropic `fallback` blocks before `cch` signing. The streaming rewriter converts newly returned fallback blocks into hidden thinking markers that OpenCode can persist, observes top-level served models and `usage.iterations[].type === "fallback_message"`, and emits one outcome at the terminal message delta. It also tracks completed `tool_use` blocks: when a terminal refusal follows one, it activates client recovery and exposes a normal `tool_use` finish so OpenCode preserves the call and continues with the existing result. Marker restoration is enabled only for eligible server-fallback requests; custom API-key routes drop internal markers and server-fallback opt-ins. **ThinkingBindingControls:** -- Purpose: Controls how Fable 5.1, Opus 5.5, and Sonnet 5.5 handle replayed thinking whose original conversation prefix changed after host compaction or prompt transformation +- Purpose: Controls how Fable 5.1, Opus 5.5, Sonnet 5.5, and Haiku 5.5 handle replayed thinking whose original conversation prefix changed after host compaction or prompt transformation - Location: `packages/core/src/thinking-binding.ts` -- Pattern: Shared outgoing-body inspection recognizes non-empty signed `thinking` and `redacted_thinking` blocks. OAuth Fable 5.1, Opus 5.5, and Sonnet 5.5 continuations with adaptive thinking use `thinkingBinding.prefixMismatchBehavior`: `account-default` sends no override and leaves the behavior to Anthropic, while `error` or `drop_block` adds `thinking.block_binding.prefix_mismatch_behavior` and beta `thinking-binding-controls-2026-08-01`. Sonnet 5.5's `between_tools` mode rejects this field and does not receive it. First turns, Mythos 5.1, older models, and API-key routes remain unchanged. +- Pattern: Shared outgoing-body inspection recognizes non-empty signed `thinking` and `redacted_thinking` blocks. When an OAuth request replays thinking from an earlier assistant response, changes to the messages, instructions or tools before that response can cause Anthropic to reject its signature. For Fable 5.1, Opus 5.5, Sonnet 5.5, and Haiku 5.5 requests using adaptive thinking, `thinkingBinding.prefixMismatchBehavior` controls that case: `account-default` sends no override and leaves the decision to Anthropic, `error` requests rejection of mismatched thinking, and `drop_block` requests removal of the affected thinking blocks. Both explicit settings add `thinking.block_binding.prefix_mismatch_behavior` and beta `thinking-binding-controls-2026-08-01`. Sonnet 5.5's `between_tools` mode rejects this field and does not receive it. First turns, Mythos 5.1, older models, and API-key routes remain unchanged. **MidConversationOutputConfig:** - Purpose: Preserves Fable 5.1 prompt-cache prefixes while users change reasoning effort between turns diff --git a/CHANGELOG.md b/CHANGELOG.md index 002cac0d0..f039de010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This repo is a CortexKit-maintained Anthropic auth monorepo for OpenCode and Pi. ### Features +- Add Claude Haiku 5.5 (`claude-haiku-5-5`) to Core, OpenCode, and Pi with a 1M context, 128K output and native `low` through `max` adaptive effort. Preserve supported forced tool choice, remove rejected manual thinking budgets and sampling parameters, and honor OpenCode thinking opt-outs at high effort or below. Standard input/output pricing is $0.10/$0.50 per million tokens; all rates increase fivefold above 100K total input tokens, including cached input. OpenCode and Pi apply the selected pricing tier to all input, output and cached tokens in the request, not just the tokens above the threshold. `thinkingBinding.prefixMismatchBehavior` set to `error` or `drop_block` also covers replayed Haiku 5.5 thinking after earlier conversation content changes. Subscription OAuth requests continue to show zero API cost in the host. Haiku 5.5 does not enable fast mode or automatic safety fallback, and quota priming continues to use Haiku 4.5. + - Add Claude Sonnet 5.5 (`claude-sonnet-5-5`) to Core, OpenCode, and Pi with its 1M context, 128K output, and official pricing. OpenCode and Pi request readable adaptive thinking. OpenCode maps explicit thinking opt-outs to Sonnet 5.5's `between_tools` mode, removes unsupported forced tool choice, and offers native `low` through `max` effort variants. Apply configured `error` or `drop_block` behavior to replayed Sonnet 5.5 and Opus 5.5 thinking when earlier messages, instructions, or tools have changed; the default account behavior stays unchanged. Pi exposes the model's supported `low`–`max` effort levels while refusing unsupported `minimal` and unavailable `off`. ### Fixes diff --git a/README.md b/README.md index 8d94e4d5a..e20122493 100644 --- a/README.md +++ b/README.md @@ -2,73 +2,91 @@ Claude Pro/Max OAuth support for both [OpenCode](https://opencode.ai) and [Pi](https://pi.dev), maintained by CortexKit. -This repo is a Bun workspace monorepo with two user-facing integrations and one shared core package. The OpenCode package is a CortexKit-maintained fork of the original `@ex-machina/opencode-anthropic-auth` plugin. The Pi package is a native Pi provider extension that overrides Pi's built-in Anthropic provider. Both integrations share the same Anthropic OAuth, fallback-account, quota, prompt-cache, relay, dump, and request-signing logic through `@cortexkit/anthropic-auth-core`. +This repo is a Bun workspace monorepo with two user-facing integrations and one shared core package. The OpenCode package is a CortexKit-maintained fork of the original `@ex-machina/opencode-anthropic-auth` plugin. The Pi package is a native Pi extension that replaces the implementation of Pi's built-in Anthropic provider while keeping its `anthropic` provider ID and model IDs. Both integrations share the same Anthropic OAuth, account-pool, quota, prompt-cache, relay, dump, and request-signing logic through `@cortexkit/anthropic-auth-core`. + +> [!IMPORTANT] +> These instructions describe the upcoming native release, which is not yet published. ## Packages | Package | Agent | Purpose | | --- | --- | --- | | `@cortexkit/opencode-anthropic-auth` | OpenCode | OpenCode plugin and CLI for Claude OAuth, request rewriting, fallback accounts, quotas, cache controls, dumps, and relay setup. | -| `@cortexkit/pi-anthropic-auth` | Pi | Pi package/extension that registers a CortexKit Anthropic provider under Pi's built-in `anthropic` provider ID. | +| `@cortexkit/pi-anthropic-auth` | Pi | Pi extension that replaces the implementation of Pi's built-in Anthropic provider, keeping the `anthropic` provider ID and model IDs. | | `@cortexkit/anthropic-auth-core` | Shared | Reusable OAuth, account, quota, cache, relay, dump, SSE, and request-signing logic used by both integrations. | ## Support matrix | Capability | OpenCode | Pi | | --- | --- | --- | -| Primary Claude Pro/Max OAuth | OpenCode `/connect anthropic` | Pi `/login anthropic` | -| Provider integration point | OpenCode plugin fetch/request transform | Pi `registerProvider("anthropic")` provider override | -| Sidecar config | `~/.config/opencode/anthropic-auth.json` | `~/.pi/agent/anthropic-auth.json` | -| Runtime state | `~/.config/opencode/anthropic-auth-state.json` | next to the Pi sidecar as `anthropic-auth-state.json` | -| Commands | `/claude-cache`, `/claude-cachekeep`, `/claude-prime`, `/claude-start`, `/claude-routing`, `/claude-fast`, `/claude-quota`, `/claude-dump`, `/claude-killswitch` | `/claude-cache`, `/claude-cachekeep`, `/claude-prime` (status only), `/claude-routing`, `/claude-fast`, `/claude-quota`, `/claude-dump` | +| Claude Pro/Max OAuth (local) | Local account pool, filled by `setup` migration, `setup --login`, or `/connect anthropic` | Local account pool, filled by `setup` migration, `setup --login`, or the `/claude` menu | +| Claude accounts from the Claustrum vault | Optional, enrolled separately through `setup` | Optional, enrolled separately through `setup` | +| Provider integration point | OpenCode plugin fetch/request transform (OpenCode 1 and OpenCode 2) | Pi `registerProvider("anthropic")`, replacing the built-in implementation | +| Local account pool | `~/.config/opencode/anthropic-auth-pool.json` | `~/.pi/agent/anthropic-auth-pool.json` | +| Old sidecar (migration source only) | `~/.config/opencode/anthropic-auth.json` | `~/.pi/agent/anthropic-auth.json` | +| Slash command | `/claude` menu | `/claude` menu | +| Menu actions not available in Pi | — | Refresh quota, killswitch, Prime on/off, lane start | | Quota sidebar widget | OpenCode TUI plugin via `tui.json` | Not available | -| Fallback accounts, quota routing, killswitch, relay, dumps, fast mode | Supported | Supported through the same shared core and Pi sidecar | +| Fallback accounts, quota routing, relay, dumps, fast mode | Supported | Supported through the same shared core | ## What CortexKit adds over the original plugin -- **Fallback Claude accounts**: keep each agent's normal Anthropic login as the primary account, then route to ordered fallback OAuth accounts on auth/quota/rate-limit failures. -- **Routing modes**: use `/claude-routing sticky-balanced` to distribute cold sessions by current OAuth quota headroom while keeping each session on one account, or choose `main-first` / `fallback-first` ordering. +- **Fallback Claude accounts**: keep a main Claude account and route to ordered fallback OAuth accounts or API-key routes on auth/quota/rate-limit failures. +- **Routing modes**: choose **Routing mode** → `sticky-balanced` in the `/claude` menu to spread new sessions across OAuth accounts by current quota headroom while keeping each session on one account, or choose `main-first` / `fallback-first` ordering. - **Quota-aware routing**: skip main or fallback accounts when their 5-hour or 7-day Claude quota falls below your configured minimum. -- **Persistent Claude cache controls**: manage Anthropic 1-hour prompt caching from `/claude-cache` with explicit, automatic, or hybrid modes. -- **Cache keepalive**: use `/claude-cachekeep always` or `/claude-cachekeep HH-HH` to pre-warm hybrid cache anchors for active sessions before the 1-hour TTL expires. -- **Quota window priming**: opt in with `/claude-prime on` to start each 5-hour quota window about one minute after it resets instead of waiting for the next normal prompt. -- **Lane start (OpenCode only)**: use `/claude-start` to fire one synthetic, one-token turn through the current session's normal model, agent, variant, quota, routing, cache, and request pipeline. -- **Fast mode toggle**: use `/claude-fast on|off` to request Anthropic fast mode for supported Opus models (`claude-opus-4-8`, `claude-opus-5`, and `claude-opus-5-5`). +- **Persistent Claude cache controls**: manage Anthropic 1-hour prompt caching from the `/claude` menu's Cache group with explicit, automatic, or hybrid modes. +- **Cache keepalive**: use **Keep cache warm always** or **Cache keep window** to pre-warm hybrid cache anchors for active sessions before the 1-hour TTL expires. +- **Quota window priming**: opt in with **Enable Prime** to start each 5-hour quota window about one minute after it resets instead of waiting for the next normal prompt. +- **Lane start (OpenCode only)**: use **Fire lane start** to send one synthetic, one-token turn through the current session's normal model, agent, variant, quota, routing, cache, and request pipeline. +- **Fast mode toggle**: use **Enable fast mode** / **Disable fast mode** to request Anthropic fast mode for supported Opus models (`claude-opus-4-8`, `claude-opus-5`, and `claude-opus-5-5`). - **Adaptive reasoning visibility**: request summarized adaptive thinking for Claude Fable 5/5.1, Mythos 5/5.1, Opus 5/5.5, and Sonnet 5.5. OpenCode exposes native `low`, `medium`, `high`, `xhigh`, and `max` effort variants for Fable 5.1, Opus 5/5.5, and Sonnet 5.5. On Opus 5.5, thinking is always on (adaptive summarized) per the model specification. OAuth Fable 5.1 sessions can change effort between turns without rewriting the cached prefix. +- **Claude Haiku 5.5**: `claude-haiku-5-5` supports a 1M context, 128K output and native `low` through `max` adaptive effort. Forced tool choice remains supported. Standard input/output rates are $0.10/$0.50 per million tokens; all rates increase fivefold above 100K total input tokens, including cached input. Quota priming still uses Haiku 4.5. - **Fable/Opus 5 and 5.5 safety fallback (OpenCode)**: eligible OAuth requests try Anthropic's server-side safety fallback first. The plugin preserves Anthropic's fallback conversation boundary across OpenCode history and automatically starts its deterministic 10-response Opus 4.8 recovery if the response still ends in refusal. The TUI sidebar and OpenCode Desktop report the active target model and restoration. Set `OPENCODE_ANTHROPIC_AUTH_FALLBACK_MODE=legacy` to bypass the server policy and use client-side recovery exclusively. -- **Live quota visibility**: use `/claude-quota` to see main and fallback quota state, reset times, and refresh errors. +- **Live quota visibility**: the `/claude` menu's Quota group shows main and fallback quota state, reset times, and refresh errors. - **Host-local quota feed (OpenCode)**: optionally publish sanitized response-header quota observations so another local CortexKit process can reuse fresh account state without polling Anthropic's rate-limited usage endpoint. - **Quota sidebar widget**: register the OpenCode TUI plugin in `tui.json` to render a live sidebar with per-account quota, routing, cache, and health state. -- **Killswitch**: per-account hard-block thresholds that stop requests before hitting Anthropic's rate limits, with synthetic 429 retry-after when all accounts are exhausted. +- **Killswitch (OpenCode menu)**: per-account hard-block thresholds that stop requests before hitting Anthropic's rate limits, with synthetic 429 retry-after when all accounts are exhausted. - **User-owned Cloudflare relay**: optionally provision your own Worker relay to reduce repeated client upload bytes for large OpenCode or Pi requests. -- **Claude-compatible request hardening**: Claude Code 2.1.258 identity and final-body billing signing, user-turn prompt and request lineage across OpenCode tool loops, live-session-stable billing suffixes, Fable 5.1 thinking-prefix recovery, safer token refresh persistence, replay-safe fallback retries, and subagent cache isolation. +- **Optional Claustrum vault custody**: enroll OpenCode or Pi with setup so it serves Claude accounts from the vault instead of its local account pool. +- **Claude-compatible request hardening**: Claude Code 2.1.280 identity and final-body billing signing, user-turn prompt and request lineage across OpenCode tool loops, live-session-stable billing suffixes, Fable 5.1 thinking-prefix recovery, safer token refresh persistence, replay-safe fallback retries, and subagent cache isolation. ## What these integrations do - Let OpenCode and Pi use Claude Pro/Max OAuth credentials instead of an Anthropic API key. - In OpenCode, intercept the final Anthropic request and rewrite it into the Claude-compatible shape expected by Anthropic OAuth access. -- In Pi, replace Pi's built-in Anthropic provider with a CortexKit provider override that uses the same Claude-compatible request path. +- In Pi, replace the implementation of Pi's built-in Anthropic provider, keeping its provider and model IDs, with one that uses the same Claude-compatible request path. - Add Claude billing headers with stable `cc_version`, OpenCode user-turn `cc_prompt_id`/`cc_prev_req` lineage, and body-derived `cch` signing. -- Support fallback Claude accounts stored in a local per-agent sidecar file. -- Keep fallback OAuth tokens fresh in the background. +- Keep Claude OAuth accounts and API-key routes in a per-host local account pool, or serve accounts from an enrolled Claustrum vault. +- Keep local OAuth tokens fresh in the background. - Apply quota thresholds before routing to main or fallback accounts. -- Add `/claude-cache`, `/claude-cachekeep`, `/claude-prime`, `/claude-start`, `/claude-fast`, `/claude-quota`, and `/claude-dump` commands to OpenCode. +- Add one `/claude` menu for accounts, quota, routing, limits, cache, diagnostics, fast mode, Prime, and lane start. - Optionally relay large requests through a Cloudflare Worker owned by the user. ## Quick setup (recommended) -Set up OpenCode, Pi, and optional Claustrum custody in one command: +In this guide, a *host* is OpenCode or Pi. Setup is an *offline* step: every OpenCode, LiteCode, and Pi process must be closed while it runs. Offline does not mean without network; setup still uses the network, for example to install packages and to sign in to Claude. Quit those processes, then run: ```bash bunx @cortexkit/opencode-anthropic-auth setup ``` -The interactive setup wizard: -- Detects OpenCode, Pi, `ck`, and the running Claustrum daemon -- Configures `@cortexkit/opencode-anthropic-auth` in `opencode.jsonc` and `tui.jsonc` -- Installs `@cortexkit/pi-anthropic-auth` into Pi via `pi install` -- Optionally activates Claustrum vault custody for both OpenCode and Pi (approving consumer enrollment, granting `category:anthropic-native`, discovering accounts, and installing OpenCode's activation tombstone) -- All accounts logged in via `ck auth login --provider anthropic` are discovered automatically without manual binds, manifest files, or restarts +What setup does: + +- detects OpenCode, Pi, `ck`, and a running Claustrum daemon (Claustrum is a local credential-vault service; `ck` is its command-line tool), and lets you select which hosts to configure; +- adds `@cortexkit/opencode-anthropic-auth` to `opencode.jsonc` (or `opencode.json`) and `tui.jsonc` (or `tui.json`), and installs `@cortexkit/pi-anthropic-auth` with `pi install`; +- migrates each selected host to its local account pool, unless you choose Claustrum vault custody; +- enrolls each selected host in the vault when you choose vault custody (it asks, and defaults to no). + +Setup refuses to change anything while an OpenCode, LiteCode, or Pi process is running. + +| Flag | Effect | +| --- | --- | +| `--yes`, `-y` | Do not prompt. Selects every detected host and keeps local custody unless `--claustrum` is also given. | +| `--dry-run` | Print the planned actions and change nothing. | +| `--login` | Sign in to Claude during setup and store that account as the main account in the local pool. | +| `--claustrum` | Choose vault custody. Takes effect only when the Claustrum daemon is running and `ck` is installed; otherwise setup keeps local custody. Cannot be combined with `--login`. | +| `--no-claustrum` | Keep local custody without asking. | +| `--remove-pi-auth` | With `--yes`, consent to moving a stored Pi Anthropic login (see [Pi stored logins](#pi-stored-logins)). Plain `--yes` is not consent. | --- @@ -84,15 +102,15 @@ Add the OpenCode plugin to your OpenCode configuration: } ``` -Pinning is strongly recommended for any OpenCode plugin: +Pinning is strongly recommended for any OpenCode plugin. Replace `VERSION` with the release you install: ```json { - "plugin": ["@cortexkit/opencode-anthropic-auth@2.0.0"] + "plugin": ["@cortexkit/opencode-anthropic-auth@VERSION"] } ``` -After changing plugin config, restart OpenCode. +OpenCode 1 and OpenCode 2 load the same package. After changing plugin config, restart OpenCode. > [!TIP] > If OpenCode keeps using an old build, clear OpenCode's plugin cache with `rm -rf ~/.cache/opencode` and restart. @@ -101,87 +119,122 @@ After changing plugin config, restart OpenCode. Install the Pi package with Pi's package manager: -```bash -pi install npm:@cortexkit/pi-anthropic-auth@2.0.0 -``` - -For an unpinned install: - ```bash pi install npm:@cortexkit/pi-anthropic-auth ``` +To pin a release, replace `VERSION` with its version number and keep the quotes: `pi install "npm:@cortexkit/pi-anthropic-auth@VERSION"`. + To try it for one run without adding it to Pi settings: ```bash pi -e npm:@cortexkit/pi-anthropic-auth ``` -The Pi package registers a CortexKit Anthropic provider extension under Pi's built-in `anthropic` provider ID. After installation, start or restart Pi and authenticate with Pi's normal login command: +The Pi package replaces the implementation of Pi's built-in Anthropic provider; the `anthropic` provider ID and model IDs stay the same. It authenticates from the local account pool or the vault, not from Pi's `/login anthropic` credential. Run [setup](#quick-setup-recommended) before starting Pi. -```text -/login anthropic -``` +Pi's files live in `~/.pi/agent/` by default. `PI_CODING_AGENT_DIR` or `PI_AGENT_DIR` changes that directory, and `PI_ANTHROPIC_AUTH_FILE` changes the old sidecar path from which the pool's location is derived. -Pi package state lives separately from OpenCode in: +## Accounts and credential custody -```text -~/.pi/agent/anthropic-auth.json -``` +Each host takes Claude credentials from exactly one source: -Override the path with `PI_ANTHROPIC_AUTH_FILE`. The package also respects `PI_AGENT_DIR` when deriving the default sidecar path. +- **Local custody** (default): the host's local account pool. +- **Vault custody**: the Claustrum vault, after the host is enrolled through setup. -## Primary account authentication +The host's Claustrum enrollment record decides which credential source requests use; settings files do not. An approved enrollment keeps the host on the vault even if its token file is missing, so the host refuses rather than falling back to local accounts. If the enrollment record cannot be read, requests the plugin would serve refuse until it is repaired or disconnected; ordinary API-key authentication is unaffected. See [docs/custody-state-machine.md](docs/custody-state-machine.md) for the full contract. -OpenCode keeps its native `anthropic` entry as the main account. In `local` mode, sign in through OpenCode: +### Local account pool -```text -/connect anthropic -``` +The pool holds the host's Claude OAuth accounts and Anthropic-compatible API-key fallback routes, plus plugin settings. One OAuth account is the main account, tried first by default; the others are fallbacks, tried in order when the main account is unusable. The pool's location follows the host's old sidecar path: its files are created in the same directory, so `OPENCODE_ANTHROPIC_AUTH_FILE` and `PI_ANTHROPIC_AUTH_FILE` move the pool too. -Before the main account can enter Claustrum custody, the operator onboards it into the vault with Claustrum's tooling. Until the dedicated import tooling lands, use the interim path in the custody state machine's §8, “Adding a new account today.” The plugin never runs `ck`, never imports or migrates credentials, and never writes the host auth slot. +| Host | Pool | Old sidecar (migration source) | +| --- | --- | --- | +| OpenCode | `~/.config/opencode/anthropic-auth-pool.json` | `~/.config/opencode/anthropic-auth.json` | +| Pi | `~/.pi/agent/anthropic-auth-pool.json` | `~/.pi/agent/anthropic-auth.json` | -Verify the migration gate against the installed `ck-auth` binary, not an announcement or a claimed deployment revision. In an isolated scratch data directory, a production-shaped tombstone import must refuse with `refusing Claustrum tombstone material` and leave the audit chain unchanged; in the same run, a real-material import with `--replace` must succeed. The refusal alone is not enough because a broken import path also refuses. The exact tombstone write is: +Private credential state is kept beside it in `anthropic-auth-pool-state.json`. Every process of a host shares that host's pool. OpenCode and Pi keep separate pools. -```json -{ "type": "oauth", "access": "", "refresh": "claustrum-tombstone:v1:anthropic", "expires": 0 } +Setup migrates each host once, while OpenCode, LiteCode, and Pi are closed: + +- Fallback accounts, API-key routes, and settings from the old sidecar files are imported into the pool. The old sidecar files are not read afterwards. +- OpenCode's stored Claude OAuth login becomes the pool's main account. OpenCode keeps only an inert placeholder: a fixed, non-secret marker that is not a usable credential and only tells OpenCode to route Claude requests through the plugin. A stored OpenCode API key is left unchanged. +- Pi's stored Anthropic OAuth login is moved into the pool only with your consent. + +Requests refuse until migration has completed; they never fall back to credentials still stored in OpenCode's or Pi's own auth file. Before importing a stored main OAuth login, migration checks the Claude account UUID in the legacy main profile. That profile's recorded access-token fingerprint must match the host login; migration does not query Anthropic for identity. If it cannot, it changes nothing and tells you to run: + +```bash +bunx @cortexkit/opencode-anthropic-auth setup --login --no-claustrum ``` -`access` is empty so the sealer's shape gate rejects the material by construction. +`--login` opens a Claude sign-in during setup and stores the account as the main account. -OpenCode's upstream authentication options are still supported: +### Signing in -- Claude Pro/Max OAuth through `claude.ai`. -- Anthropic Console OAuth that creates an API key. -- Manually entered Anthropic API key. +In local custody, after running setup (see [Local account pool](#local-account-pool)), add the main account in any of these ways: -For Pi, install the Pi package, restart Pi, then use Pi's Anthropic login flow: +- **OpenCode 1**: run `/connect anthropic` and choose **Claude Pro/Max**. The account is saved in the local pool; OpenCode stores only the inert placeholder. +- **OpenCode 2**: connect Anthropic and choose **Claude Pro / Max (native account pool)**. +- **Pi**: after setup completes, open `/claude`, then use **Start OAuth sign-in** and **Finish OAuth sign-in**. The first account becomes the main account; later accounts are fallbacks, tried when the main account cannot serve. +- **Any host**: `setup --login`, while the host is stopped. -```text -/login anthropic -``` +OpenCode's stock API-key options (**Create an API Key** and **Manually enter API Key** in OpenCode 1) remain ordinary OpenCode API-key authentication. The plugin passes those requests through unchanged. -The Pi package registers under Pi's built-in `anthropic` provider ID and stores primary OAuth credentials through Pi's normal credential flow. CortexKit package state, fallback accounts, cache mode, dump mode, and relay config live in the Pi sidecar file. +Local sign-in refuses while the host is enrolled in the vault (see [Claustrum vault custody](#claustrum-vault-custody)), with `Disconnect from the vault first.` -## Sidecar config +### Claustrum vault custody -OpenCode package state lives in: +Vault custody is optional and chosen per host. Quit every OpenCode, LiteCode, and Pi process, make sure the Claustrum daemon is running and `ck` is installed, then run: -```text -~/.config/opencode/anthropic-auth.json +```bash +bunx @cortexkit/opencode-anthropic-auth setup --claustrum ``` -Override the OpenCode path with `OPENCODE_ANTHROPIC_AUTH_FILE`. +For each selected host, setup proposes the `anthropic-auth-opencode` or `anthropic-auth-pi` enrollment, approves it with `ck auth enroll approve`, and grants that enrollment read access to the `anthropic-native` credential category (`ck auth grant --principal enrolled:anthropic-auth- --selector-kind category --selector anthropic-native --operation read`). It then refreshes the vault inventory and requires its default Anthropic credential (`oauth:anthropic`) to have a vault-reported account identity; otherwise it stops with `The vault has no verified default Claude account. Sign in through the vault and rerun setup.` + +Vault setup writes the selected host's enrollment and, for Pi with your consent, Pi's auth file (see [Pi stored logins](#pi-stored-logins)). It does not read, import, change, or delete the local pool, migration journal, local runtime metadata, or routing assignments, it never imports local OAuth accounts into the vault, and it does not write OpenCode's auth file. If the vault reports that an earlier enrollment request was replaced by a newer one, already used, or no longer exists, setup clears that request and stops; run setup again. + +After setup, select the vault in the host: -Pi package state uses the same JSON shape but a separate file: +- **OpenCode 1**: run `/connect anthropic` and choose **Claude via credential vault**. This option appears only while OpenCode holds a vault enrollment, and it checks OpenCode's Anthropic login slot without changing it. If OpenCode already holds an Anthropic login, or the slot cannot be read, it refuses: remove that login with OpenCode's own tools, then choose the vault login, or disconnect from the vault. An OpenCode OAuth login other than the vault placeholder is never used while enrolled; requests refuse with `This host uses the credential vault, but OpenCode holds a separate Anthropic login. Remove it and choose the vault login, or disconnect from the vault.` +- **OpenCode 2**: connect Anthropic and choose **Claude vault accounts**. It refuses unless OpenCode is enrolled. +- **Pi**: no extra step; see [Pi stored logins](#pi-stored-logins). + +Plain OpenCode API keys are never changed and keep working as stock OpenCode authentication in every custody state, including an unreadable enrollment. OpenCode 2 serves a request only when it carries the plugin's placeholder; API keys from OpenCode's login, configuration, or environment, and any other connection, pass through unchanged. + +While a host is enrolled: + +- Claude accounts come only from the vault's scoped inventory. The vault's default account is the main account. Accounts added with `ck auth login --provider anthropic` are discovered without a restart and receive an initial quota check. +- Every model request, quota or profile query, quota-window priming request, and cache keepalive request obtains fresh vault authorization. A vault refusal never falls back to a local credential. +- Local pool accounts, including API-key routes, are not used, refreshed, quota-polled, kept warm, or shown. They stay unchanged and return after disconnect. +- If the vault's default account changes to a different Claude account, sessions pinned to the old default are assigned afresh; other session pins are kept. +- If the vault has no usable Claude account, requests refuse. Add accounts with `ck auth login --provider anthropic`, or disconnect from the vault. +- The `/claude` menu still enables and disables vault accounts and resets recorded retry waits, but hides local sign-in, API-key additions, account removal and reordering, and enrollment reset. + +### Pi stored logins + +Pi prefers its stored login over the extension's credentials. Setup asks before moving or stashing that login; with `--yes`, pass `--remove-pi-auth` to consent. + +- **Local custody**: setup moves Pi's stored Anthropic OAuth login into the local pool and removes Pi's copy. Until that happens, Pi refuses to serve with `Pi still has a host Anthropic OAuth credential. Run offline setup to migrate it and approve its removal before using the native pool.` +- **Vault custody**: setup moves Pi's stored Anthropic login (OAuth or API key) into a private stash and leaves a fixed, non-secret API-key marker in Pi's auth file so Pi does not refresh the stashed OAuth login. Disconnecting restores the original login byte-for-byte before the enrollment is removed. + +### Disconnecting from the vault + +Open `/claude` and choose **Disconnect from the vault** in the Accounts group. The action is offered while the host is enrolled, and also when the enrollment record cannot be read, so a damaged record can always be cleared. It asks for confirmation: ```text -~/.pi/agent/anthropic-auth.json +Disconnect this host from the vault and return to local accounts? The vault keeps this host's enrollment approved until an operator revokes it there. ``` -Override the Pi path with `PI_ANTHROPIC_AUTH_FILE`. +Disconnect never reads or changes OpenCode's stored sign-ins. For Pi, it restores the stashed login first. If Pi's auth file already holds a different login, disconnect stops, keeps both logins and the enrollment, and asks you to resolve them with setup. After a successful disconnect, the host returns to its local pool. If that pool has never been migrated, stop the hosts and run `setup --no-claustrum`; disconnect does not perform migration. -Example: +Only the setup command creates or polls enrollment requests. In local custody, **Reset host enrollment** clears a denied or blocked enrollment request without proposing a new one; run setup afterwards. It never resets a pending request or an approved enrollment. + +## Settings + +Plugin settings live in the host's local account pool (and, while the host is enrolled in the vault, in a separate `anthropic-auth-vault-settings.json`). Change the settings that have a `/claude` menu action from the menu; see [The /claude menu](#the-claude-menu). + +During migration, setup imports these settings from the old sidecar file (`~/.config/opencode/anthropic-auth.json` or `~/.pi/agent/anthropic-auth.json`, or the paths set by `OPENCODE_ANTHROPIC_AUTH_FILE` and `PI_ANTHROPIC_AUTH_FILE`). The old file is not read after migration. The imported shape is: ```json { @@ -247,28 +300,25 @@ Example: } ``` -The `routing` block controls `/claude-routing`, `claudeCache` controls `/claude-cache`, `cacheKeep` controls `/claude-cachekeep`, and `claudeFast` controls `/claude-fast`. Killswitch `scoped` thresholds apply to matching model-scoped quota windows such as Fable weekly quota. OpenCode zeroes Anthropic OAuth model costs by default because OAuth usage is quota-based; set `costZeroing.enabled` to `false` only if you want OpenCode to display the provider's model pricing instead. Set `quota.showToasts` to `true` to opt into OpenCode quota toast notifications after quota refreshes. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback account labels. +The `routing` block holds the routing mode, `claudeCache` the cache mode, `cacheKeep` the cache keepalive schedule, and `claudeFast` the fast-mode switch; the `/claude` menu changes all of them. Killswitch `scoped` thresholds apply to model-specific quota windows, such as a separate weekly limit for Claude Fable models (`claude-fable-*`). A positive 5-hour or 7-day killswitch floor is a minimum remaining percentage. It blocks an account with no quota reading for that window, regardless of `quota.failClosedOnUnknownQuota`; with no positive floor, missing readings follow `quota.failClosedOnUnknownQuota`. A positive `scoped` floor (a minimum remaining percent for such a model-specific window) blocks when scoped-window information is missing; a reported empty list means the model has no separate window, so the floor does not apply. OpenCode zeroes Anthropic OAuth model costs by default because OAuth usage is quota-based; set `costZeroing.enabled` to `false` only if you want OpenCode to display the provider's model pricing instead. Set `quota.showToasts` to `true` to opt into OpenCode quota toast notifications after quota refreshes. `thinkingBinding.prefixMismatchBehavior` controls replayed signed/redacted thinking on OAuth Fable 5.1 requests. `account-default` (the default) sends no explicit prefix-mismatch override. Use `error` to make prefix changes fail deterministically during compaction testing, or `drop_block` to ask Anthropic to discard mismatched thinking blocks. The beta header is added only when an explicit behavior is configured and replayable thinking is present. `quotaHeaderFeed.enabled` is an OpenCode-only, restart-required opt-in. It publishes only allowlisted quota-window values, an opaque account reference, an observation timestamp, and the configured OAuth-account count; it never publishes tokens, raw headers, request bodies, model IDs, or refresh errors. Per-process lease files use owner-only permissions under `$TMPDIR/opencode-anthropic-auth/quota-header-feed`, expire after three minutes, and can be redirected with `OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR`. -Runtime data is stored separately in `anthropic-auth-state.json`: fallback OAuth tokens, API-route keys, token refresh backoff, quota snapshots, and quota API backoff. `sticky-balanced` session assignments use a separate `anthropic-auth-routing-state.json`; session IDs are SHA-256 hashed in that file. Background refresh and quota checks write only runtime state, so editing `anthropic-auth.json` does not get overwritten by another running plugin instance. - -## OpenCode lane-start setting - +Private runtime data (OAuth tokens, API-route keys, token refresh backoff, quota snapshots, and quota API backoff) is kept in `anthropic-auth-pool-state.json`, separate from settings. `sticky-balanced` session assignments use `anthropic-auth-pool-routing-state.json` in local custody and `anthropic-auth-vault-routing-state.json` in vault custody; session IDs are SHA-256 hashed in both. ## Fallback accounts -Fallback accounts are separate Claude OAuth accounts or Anthropic-compatible API-key routes managed by this plugin. By default, the main account is tried first unless quota policy says it is currently unusable. Fallbacks are then tried in sidecar order when the primary request returns a configured fallback status. +Fallback accounts are separate Claude OAuth accounts or Anthropic-compatible API-key routes in the local pool, or additional accounts in the vault while the host is enrolled. By default, the main account is tried first unless quota policy says it is currently unusable. Fallbacks are then tried in account order when the primary request returns an HTTP status listed in `fallbackOn` (defaults below). -Use `/claude-routing fallback-first` to prefer usable fallback accounts before the main account, `/claude-routing main-first` to restore the default, or `/claude-routing sticky-balanced` to allocate each new session to an OAuth account according to spendable 5-hour, 7-day, and matching model-scoped quota headroom. The command persists `routing.mode` and takes effect on the next request without restarting. `/claude-routing reset` clears only the current session's assignment so its next request is allocated again. +In the `/claude` menu, choose **Routing mode** → `fallback-first` to prefer usable fallback accounts before the main account, `main-first` to restore the default, or `sticky-balanced` to allocate each new session to an OAuth account according to its remaining quota above your configured minimums in the 5-hour, 7-day, and any model-specific window. The choice persists and takes effect on the next request without restarting. **Reset routing session** clears only the current session's assignment so its next request is allocated again. `sticky-balanced` uses a weighted deficit allocator: current quota headroom is normalized by time until reset, and the initial prompt size is counted until the next quota refresh. The selected account is then persisted by hashed session ID across plugin instances and restarts. Existing hybrid CacheKeep sessions seed their current OAuth route when the mode is enabled, avoiding an unnecessary account switch for an already-warm session. Changing the user-selected model discards the old assignment and its model-specific CacheKeep preference before quota-based reselection; transparent safety-recovery model changes retain the original account. -Affinity survives network failures, relay failures, 5xx responses, unconfirmed 429s, quota-probe failures, and changes in relative account weights. Confirmed 7-day or matching model-scoped exhaustion migrates the session. Confirmed 5-hour exhaustion migrates it only when more than 15 minutes remain before reset; at 15 minutes or less, the route is retained and returns `Retry-After` rather than moving a large prompt cache. Disabled/removed accounts, permanent re-login failures, and killswitch blocks are also eligible for migration. +Session affinity, keeping a session on the same account, survives network failures, relay failures, 5xx responses, unconfirmed 429s, quota-probe failures, and changes in relative account weights. Confirmed 7-day or matching model-scoped exhaustion migrates the session. Confirmed 5-hour exhaustion migrates it only when more than 15 minutes remain before reset; at 15 minutes or less, the route is retained and returns `Retry-After` rather than moving a large prompt cache. Disabled/removed accounts, permanent re-login failures, and killswitch blocks are also eligible for migration. -OpenCode Fable content-filter recovery uses the session's sticky assignment for both the original Fable request and transparent Opus 4.8 turns; its Fable prewarm already uses that same OAuth account. A session that starts directly on Opus first prefers an otherwise usable account whose Fable quota is exhausted, preserving Fable-capable quota. API-key routes are not included in quota-balanced first assignment and retain their stricter confirmed-exhaustion gate. +When a Fable request is refused and OpenCode temporarily switches the session to Opus 4.8 (see [Safety fallback](#safety-fallback-opencode)), the original Fable request, the Opus 4.8 turns, and the cache prewarm sent before returning to Fable all use the session's assigned account. A session that starts directly on Opus first prefers an otherwise usable account whose Fable quota is exhausted, preserving Fable-capable quota. API-key routes are not included in quota-balanced first assignment and retain their stricter confirmed-exhaustion gate. Default fallback statuses: @@ -276,7 +326,7 @@ Default fallback statuses: [401, 403, 429] ``` -Add and inspect OpenCode fallback accounts with the CLI: +Add and inspect OpenCode fallback accounts with the CLI (local custody only, after running setup for OpenCode): ```bash bunx @cortexkit/opencode-anthropic-auth login personal-alt @@ -284,34 +334,23 @@ bunx @cortexkit/opencode-anthropic-auth api add kie-opus bunx @cortexkit/opencode-anthropic-auth list ``` -Prefer npm? Use `npx -y @cortexkit/opencode-anthropic-auth ...` with the same subcommands. +Prefer npm? Use `npx -y @cortexkit/opencode-anthropic-auth ...` with the same subcommands. `login` and `api add` save to OpenCode's local pool and never change OpenCode's auth file; both refuse with `Disconnect from the vault first.` while OpenCode is enrolled in the vault, because vault accounts then replace the local pool. `api add` reads `OPENCODE_ANTHROPIC_AUTH_API_BASE_URL`, `OPENCODE_ANTHROPIC_AUTH_API_KEY`, and `OPENCODE_ANTHROPIC_AUTH_API_AUTH_HEADER` instead of prompting when they are set. -API fallback routes use `Authorization: Bearer ` by default and rewrite requests to the configured Anthropic-compatible base URL. For Kie, use `https://api.kie.ai/claude` as the base URL; the plugin appends `/v1/messages` automatically. API-key routes are only eligible after direct evidence that the main OAuth quota is exhausted: a fresh token-bound quota snapshot at 0% remaining, or an actual main OAuth model response with HTTP 429 / streaming rate-limit error followed by a live quota check that confirms 0% remaining. Low-but-nonzero quota, stale cached quota, unconfirmed 429s, 401, or 403 do not trigger API-key routes. API-route keys are stored in `anthropic-auth-state.json`, while `anthropic-auth.json` keeps the route label, type, enabled flag, base URL, and auth-header mode. +In both hosts, the `/claude` menu's Accounts group offers the same local changes: **Start OAuth sign-in** / **Finish OAuth sign-in**, **Add API-key fallback**, **Enable account**, **Disable account**, **Remove account**, **Move account up**, and **Move account down**. Account actions ask for the complete account ID shown in the account status. -OpenCode cost accounting stays simple: when the native Anthropic auth entry is OAuth, OpenCode sees zero-cost Claude OAuth models by default. If a request falls through to an API-key route, token accounting is still recorded, but OpenCode's built-in dollar cost is not route-aware. Advanced users can set `costZeroing.enabled` to `false` to show Anthropic model pricing for OAuth sessions too. +API fallback routes use `Authorization: Bearer ` by default and rewrite requests to the configured Anthropic-compatible base URL. For Kie, use `https://api.kie.ai/claude` as the base URL; the plugin appends `/v1/messages` automatically. API-key routes are only eligible after direct evidence that the main OAuth quota is exhausted: a fresh quota reading for the current Claude account showing 0% remaining, or an actual main OAuth model response with HTTP 429 / streaming rate-limit error followed by a live quota check that confirms 0% remaining. Low-but-nonzero quota, stale cached quota, unconfirmed 429s, 401, or 403 do not trigger API-key routes. API-route keys are stored with the pool's private state, separate from public route settings. **Add API-key fallback** selects either `authorization-bearer` or `x-api-key` for the authentication header. -For Pi fallback accounts, write the same account JSON shape to `~/.pi/agent/anthropic-auth.json`. The CLI helper currently lives in the OpenCode package, so you can also point it at Pi's sidecar path when logging in a fallback account: +OpenCode cost accounting stays simple: when the native Anthropic auth entry is OAuth, OpenCode sees zero-cost Claude OAuth models by default. If a request falls through to an API-key route, token accounting is still recorded, but OpenCode's built-in dollar cost is not route-aware. To retain Anthropic model pricing for OAuth sessions during migration, set `costZeroing.enabled` to `false` in the old `anthropic-auth.json` before the first local setup. Setup imports that setting; the menu does not expose it. -```bash -OPENCODE_ANTHROPIC_AUTH_FILE="$HOME/.pi/agent/anthropic-auth.json" \ - bunx @cortexkit/opencode-anthropic-auth login personal-alt -``` +The CLI manages only OpenCode's locally stored Claude accounts and API-key fallbacks. Add Pi fallback accounts from Pi's `/claude` menu. Fallback retries are only attempted when the request body is safely replayable. If the original body is non-replayable or already consumed, the plugin returns the primary response unchanged. ### Token refresh -Fallback OAuth tokens refresh in the background so idle accounts do not expire before they are needed. Refresh token rotation is persisted immediately. The plugin also re-reads the latest sidecar account before refreshing, which avoids using stale refresh-token snapshots when multiple background paths run close together. Persisted refresh backoffs are bound to the refresh token that produced them, so replacing a rejected credential immediately clears its stale latch. - -If Anthropic reports `invalid_grant`, that account must be logged in again. `/claude-account reset-backoff` manually clears the main account's refresh backoff and its matching quota backoff. - -### Claustrum scoped custody (OpenCode and Pi) - -Use `bunx @cortexkit/opencode-anthropic-auth setup` to enroll each host, grant `category:anthropic-native` read access, and activate custody while the hosts are stopped. OpenCode retains a non-secret OAuth tombstone so its custom fetch stays active; Pi uses native ambient authentication and a separate, independently revocable enrollment. The setup command asks for consent before removing a conflicting Pi OAuth credential. +Local OAuth tokens refresh in the background so idle accounts do not expire before they are needed. Refresh token rotation is persisted immediately. The plugin also re-reads the latest stored account before refreshing, which avoids using stale refresh-token snapshots when multiple background paths run close together. Persisted refresh backoffs are bound to the refresh token that produced them, so replacing a rejected credential immediately clears the backoff left by the old refresh token. In vault custody, the vault alone refreshes credentials. -Claustrum's scoped inventory is the authority for every OAuth account, including main. New accounts added via `ck auth login --provider anthropic` are discovered automatically without per-account binding or a restart. The plugin stores only secret-free routing preferences, fetches initial quota for new accounts, and authorizes each outbound request via `getScoped`. A vault refusal never falls back to local OAuth tokens. API-key routes remain separate. An older Claustrum configuration without a scoped roster fails closed until setup completes. - -`/claude-account` reads custody and enrollment status without creating or polling a request. Only the offline setup wizard enrolls a host; it can resume a pending request or renew a proven-expired one. `/claude-account claustrum` directs you to setup; `/claude-account local` explains the verified local re-login requirement rather than changing custody while OpenCode is running. `/claude-account enrollment-reset` only clears denied or blocked ceremony state under lock, never proposes a replacement or resets an approved token. Run setup afterward. +If Anthropic reports `invalid_grant`, that account must be signed in again. **Reset account backoff** in the `/claude` menu's Accounts group manually clears the main account's refresh backoff and its matching quota backoff. ## Quota-aware routing @@ -330,23 +369,17 @@ With this config, an account is skipped when it has less than 10% remaining in t Main-account quota is cached. If the main account is known to be exhausted, the plugin skips it until the relevant reset time. If the cached main quota is stale but usable, the request proceeds and quota refresh happens in the background. -Show current quota state: - -```text -/claude-quota -``` - -In OpenCode, this includes the main Anthropic account and sidecar fallback accounts. In Pi, the command reports sidecar fallback account quota state from `~/.pi/agent/anthropic-auth.json`. +Open `/claude` to see current quota state in the Quota group (**Quota summary**). It covers the main account and every fallback from the local pool, or from the vault while enrolled. In OpenCode, **Refresh quota** requests fresh quota; Pi shows the summary without that action. Reset times are rendered as relative durations, such as `resets in 10m` or `resets in 1h 15m`. ### Quota header feed -The optional quota header feed writes one lease file per process under `${TMPDIR:-/tmp}/opencode-anthropic-auth/quota-header-feed/`. Set `OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR` to override the directory. A file contains only accounts whose response headers THAT process harvested. Consumers MUST union entries from every file inside `lease_horizon_ms`, then deduplicate by account. "Newest file wins" drops accounts seen by other processes. +The optional quota header feed writes one lease file per process under `${TMPDIR:-/tmp}/opencode-anthropic-auth/quota-header-feed/`. Set `OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR` to override the directory. Each file contains only quota headers observed by the process that wrote it. Each file records `lease_horizon_ms` (default 180000, three minutes). Consumers must discard entries whose `observed_at_ms` is in the future or at least that old, then combine fresh entries from every file and deduplicate by account. "Newest file wins" drops accounts seen by other processes. -For each account, the newest entry wins for quota values. Resolve `anthropic_account_uuid` from any entry in that account's group that carries it. During a rollout, an older pre-restart publisher can write the newest entry without that field beside newer code that has it. +For each account, use the entry with the greatest `observed_at_ms` for quota values. Resolve `anthropic_account_uuid` from any entry in that account's group that carries it. During an upgrade, an older plugin process that is still running can write the newest entry without that field while an updated process writes entries that have it. -Each entry always includes `anthropic_account_uuid`; an absent key identifies an older producer. Its value is provider-derived or `null`, never a local substitute. A consumer that sees `null` must count a gap, not fall back to `account_ref`; that field is store-local and never a join key. +Each entry always includes `anthropic_account_uuid`; an absent key identifies an older producer. Its value is provider-derived or `null`, never a local substitute. A consumer that sees `null` must count a gap, not fall back to `account_ref`: that field is a row ID local to one account store, not a global account identifier, so it must never be used to match accounts across stores. ## Safety fallback (OpenCode) @@ -366,7 +399,7 @@ Legacy mode routes a refused Fable 5/5.1 or Opus 5 session through Opus 4.8 for ## Quota sidebar (OpenCode TUI) -OpenCode can render a live quota sidebar widget that surfaces plugin state without running a slash command. The widget is OpenCode-only; Pi has no TUI surface. +OpenCode can render a live quota sidebar widget that surfaces plugin state without opening the `/claude` menu. The widget is OpenCode-only; Pi has no TUI surface. OpenCode loads TUI plugins from a separate `tui.json` (or `tui.jsonc`) file in your OpenCode config directory, not from the main `plugin` array. Register the package there: @@ -380,7 +413,7 @@ Pinning is recommended, matching the main plugin entry: ```json { - "plugin": ["@cortexkit/opencode-anthropic-auth@2.0.0"] + "plugin": ["@cortexkit/opencode-anthropic-auth@VERSION"] } ``` @@ -395,21 +428,29 @@ The sidebar polls plugin state and refreshes on OpenCode session and message eve Click the `CLAUDE` header to collapse or expand the sidebar. Collapsed, it shows the active account's 5-hour quota usage and a fast-mode row when fast mode is on; the header shows the plugin version (or a `LIMITED` badge when degraded). Collapse state persists across restarts by default via `tui-preferences.jsonc` (`rememberCollapsed`); set `"rememberCollapsed": false` for the old per-session behavior. -### Command modals (OpenCode TUI) +## The /claude menu -In the OpenCode TUI, the `/claude-*` commands open interactive modal dialogs instead of posting a text reply: +Both hosts register one slash command, `/claude`. It opens a menu grouped by area; each action collects its own parameters, so typed arguments are not used (Pi rejects them and points you to the menu). Earlier separate `/claude-*` commands are no longer registered. -- `/claude-quota` — a read-only view rendering the same per-account quota bars and pacing as the sidebar. -- `/claude-routing` — select main-first, fallback-first, or sticky-balanced, or reset the current sticky assignment. -- `/claude-fast` — toggle fast mode on or off. -- `/claude-dump` — toggle request dump capture on or off. -- `/claude-cache` — select the 1-hour cache mode (off, explicit, automatic, or hybrid). -- `/claude-cachekeep` — select `always`, enter a cache keepalive window (`HH-HH`), or turn it `off`. -- `/claude-prime` — turn quota-window priming on or off, or view its status and usage. -- `/claude-killswitch` — enable or disable the killswitch, or edit per-account `5h,1w,scoped` thresholds. -- `/claude-account` — manage fallback routes, inspect sanitized Claustrum custody state, or use `reset-backoff` to clear main OAuth refresh and matching quota backoffs. +| Group | Status shown | Actions | Pi | +| --- | --- | --- | --- | +| Accounts | Account status and usage | Enable account, Disable account, Remove account, Move account up, Move account down, Reset account backoff, Reset host enrollment, Add API-key fallback, Start OAuth sign-in, Finish OAuth sign-in, Custody setup guidance, Disconnect from the vault | All | +| Quota | Quota summary | Refresh quota | Summary only | +| Routing | Routing status | Routing mode (`main-first`, `fallback-first`, `sticky-balanced`), Reset routing session | All | +| Limits | Killswitch status | Enable killswitch, Disable killswitch, Set killswitch thresholds | Not available | +| Cache | Cache status, Cache keep status | Enable cache, Disable cache, Cache mode (`explicit`, `automatic`, `hybrid`), Keep cache warm always, Disable cache keep, Cache keep window, Cache keep for subagents (`on`/`off`) | All | +| Diagnostics | Dump status, Logging status | Enable request dumps, Disable request dumps, Logging level (`error`, `warn`, `info`, `debug`, `trace`) | All | +| Extras | Fast mode status, Prime quota window status | Enable fast mode, Disable fast mode, Enable Prime, Disable Prime, Fire lane start | Fast mode actions; Prime status without on/off | -Applying a change in a modal persists it through the same configuration the slash arguments use, so the modal and the typed command (`/claude-routing fallback-first`, etc.) are equivalent. Outside the OpenCode TUI (OpenCode desktop or headless), the commands print their text summary as before; Pi is unaffected. +The Accounts actions shown depend on custody: + +- **Local custody**: every Accounts action except **Disconnect from the vault**. +- **Vault custody**: the Accounts group shows `Accounts are managed in the vault. Disconnect from the vault to use local accounts.` Enable, disable, **Reset account backoff**, and **Disconnect from the vault** remain. Local sign-in, API-key additions, removal, reordering, **Reset host enrollment**, and **Custody setup guidance** are hidden and refused. +- **Unreadable enrollment**: the Accounts group shows the same actions as vault custody, so **Disconnect from the vault** stays available. + +**Custody setup guidance** changes nothing; it explains that custody changes require stopping the hosts and running [setup](#quick-setup-recommended). **Remove account**, **Reset host enrollment**, and **Disconnect from the vault** ask for confirmation. Sign-in, API-key, guidance, and disconnect actions need an interactive session. + +In the OpenCode TUI, `/claude` opens an interactive dialog. Outside the TUI (OpenCode Desktop or headless), it posts a text summary of the menu. In Pi without a UI, it prints the status sections only. The modal bridge runs a loopback-only RPC server (bound to `127.0.0.1`, bearer-authenticated) so the OpenCode server process can signal the separate TUI process. Its runtime directory defaults to a per-project path under the OpenCode state directory; override it with `OPENCODE_ANTHROPIC_AUTH_RPC_DIR`. @@ -504,18 +545,7 @@ const order = computeEffectiveOrder(root, 'my-plugin', 200) ## Claude prompt cache control -Both OpenCode and Pi packages add a slash command for Anthropic's 1-hour ephemeral prompt-cache TTL: - -```text -/claude-cache -/claude-cache on -/claude-cache off -/claude-cache mode explicit -/claude-cache mode automatic -/claude-cache mode hybrid -``` - -Without arguments, `/claude-cache` shows the current setting. +Both OpenCode and Pi control Anthropic's 1-hour ephemeral prompt-cache lifetime from the `/claude` menu's Cache group: **Enable cache**, **Disable cache**, and **Cache mode** (`explicit`, `automatic`, or `hybrid`). **Cache status** shows the current setting. Modes: @@ -527,18 +557,11 @@ In OpenCode, subagent requests do not receive 1-hour TTL caching. The plugin det ### Cache keepalive -`/claude-cachekeep` keeps recently used hybrid-mode session caches warm while the agent process is running: +Cache keepalive keeps recently used hybrid-mode session caches warm while the agent process is running. Control it from the Cache group: **Keep cache warm always**, **Cache keep window** (start and end hour), **Disable cache keep**, and **Cache keep for subagents** (`on`/`off`). -```text -/claude-cachekeep -/claude-cachekeep always -/claude-cachekeep 09-23 -/claude-cachekeep off -``` +**Keep cache warm always** keeps tracked sessions warm continuously for as long as the agent process remains open. Windows use local 24-hour time and are start-inclusive/end-exclusive: start `9`, end `23` runs from 09:00 until 22:59, and overnight windows such as start `23`, end `9` are accepted. -`always` keeps tracked sessions warm continuously for as long as the agent process remains open. Hour ranges use local 24-hour time and are start-inclusive/end-exclusive: `09-23` runs from 09:00 until 22:59, and overnight windows such as `23-09` are accepted. - -Cache keepalive only tracks requests when `/claude-cache` is enabled in `hybrid` mode. For each active session seen in the configured schedule, the package keeps an in-memory clone of the latest rewritten Anthropic request and sends a non-streaming `max_tokens: 0` pre-warm request about five minutes before the 1-hour cache entry would expire. Running `/claude-cachekeep` without arguments lists every live tracked session across OpenCode projects and plugin processes; Pi reports its own separately scoped live sessions. +Cache keepalive only tracks requests when the cache is enabled in `hybrid` mode. For each active session seen in the configured schedule, the package keeps an in-memory clone of the latest rewritten Anthropic request and sends a non-streaming `max_tokens: 0` pre-warm request about five minutes before the 1-hour cache entry would expire. **Cache keep status** lists every live tracked session across OpenCode projects and plugin processes; Pi reports its own separately scoped live sessions. Request bodies, headers, and tokens remain in memory. A lease-backed file under the system temporary directory shares only session IDs and cache timing for cross-instance status; records from stopped processes disappear from status after about three minutes. @@ -546,46 +569,30 @@ Pre-warm requests preserve explicit cache anchors but remove response-only field ## Claude quota window priming -Quota-window priming is off by default. OpenCode controls it with: - -```text -/claude-prime -/claude-prime on -/claude-prime off -``` +Quota-window priming is off by default. In OpenCode, turn it on or off with **Enable Prime** and **Disable Prime** in the `/claude` menu's Extras group; **Prime quota window status** shows its state and usage. When enabled, the plugin watches each OAuth account's 5-hour quota reset. About one minute after a confirmed reset, it sends one minimal `claude-haiku-4-5` request so the next window starts without waiting for a normal prompt. Usage is measured from response accounting. -Scheduled fires happen only after the quota window has reset. If an idle account has no cached reset time, the plugin sends one bootstrap request to establish its first observed window. Atomic temporary-file claims ensure that multiple OpenCode processes sharing the same account config send only one request per account and reset. +Scheduled fires happen only after the quota window has reset. If an idle account has no cached reset time, the plugin sends one bootstrap request to establish its first observed window. When several processes share an account store, each tries to create a claim file for that account and reset; creating the file is atomic, so exactly one process succeeds and sends the request. -Prime marker identities live in `anthropic-auth-state.json`. Plugin-owned refresh rotations preserve the main account's lineage, while a host credential replacement creates a new lineage. An existing main lineage without a refresh-token binding attaches to the current credential on its first check without changing identity. On upgrade, an existing fallback account receives an identity during its first prime check; that one-time marker change can send one extra request in the current window. +Claim files live in a temporary directory, separated by account store and credential lineage. Cleanup removes files older than six hours. Ordinary token refresh keeps the lineage; a new sign-in starts a new one. Private runtime state stores cumulative priming usage counters, not a history of primed windows. Legacy claim files are not migrated. Without the old claim, the new scheduler can treat an already primed window as unclaimed and send one extra request. -Pi exposes `/claude-prime` as a status-only command. Its `on` and `off` arguments are ignored; enable or disable priming from OpenCode. +Pi shows **Prime quota window status** but has no Prime on/off actions; enable or disable priming from OpenCode. ## OpenCode lane start -`/claude-start` is an OpenCode-only command. It queues one synthetic turn for the current session: +**Fire lane start** in the `/claude` menu's Extras group is OpenCode-only. It queues one synthetic turn for the current session. -```text -/claude-start -``` - -The bare command fires immediately. The synthetic prompt uses the session's current model, agent, and variant, then travels through the ordinary quota, routing, cache, relay, signing, and response pipeline. OpenCode shapes that OAuth request to `max_tokens: 1` while keeping streaming enabled, and correlates the request by its synthetic message ID. A queued modal is a request to start the turn, not a provider-success claim. +The action fires immediately. The synthetic prompt, a turn generated by the plugin rather than typed by you, uses the session's current model, agent, and variant, then travels through the ordinary quota, routing, cache, relay, signing, and response pipeline. OpenCode shapes that OAuth request to `max_tokens: 1` while keeping streaming enabled, and correlates the request by its synthetic message ID. A queued turn means OpenCode accepted it into the session's queue; it does not mean Anthropic has completed the request. > [!IMPORTANT] > This is a real billed request. The one-token limit reduces generated output only; Anthropic still charges for prompt-cache reads and any new cache writes. Use it when you plan to resume the session before the refreshed cache expires. -Pi does not expose this command. +Pi does not offer this action to warm the session's prompt cache. ## Claude fast mode -Both OpenCode and Pi packages can persistently request Anthropic fast mode for supported Opus models: - -```text -/claude-fast -/claude-fast on -/claude-fast off -``` +Both OpenCode and Pi can save a fast-mode setting that applies to later requests and survives process restarts. It requests Anthropic fast mode for supported Opus models with **Enable fast mode** and **Disable fast mode** in the `/claude` menu's Extras group. **Fast mode status** shows the current setting. When enabled, supported requests add `speed: "fast"` to the Anthropic JSON body and include the `fast-mode-2026-02-01` beta header. Unsupported models are left at standard speed. Anthropic supports fast mode for `claude-opus-4-8`, `claude-opus-5`, and `claude-opus-5-5`. Opus 4.7 rejects `speed: "fast"`, while Opus 4.6 runs at standard speed; Claude Fable and Mythos 5/5.1 are not fast-mode models. @@ -624,21 +631,21 @@ The relay is opt-in and user-owned. CortexKit does not run shared relay infrastr When enabled, the package sends large Anthropic request bodies to a Cloudflare Worker that you own. The first request for a session sends a full body; later requests send compact patches keyed by session affinity (`x-session-affinity` in OpenCode, Pi's stream `sessionId` in Pi). The Worker reconstructs the full Anthropic `/v1/messages` request and streams Anthropic's SSE response back to the client. -New relay setups default to HTTP transport: +`relay setup` saves HTTP transport: ```json "transport": "http" ``` -HTTP is the safest release default and still sends compact full-sync/patch payloads through your Worker. WebSocket is available as an opt-in persistent session transport: +HTTP is the default because it also works with relay Workers deployed by older releases. It still sends compact payloads through your Worker: the full request body for a session's first request, then only the changes for later requests. WebSocket is an opt-in persistent session transport for OpenCode 1 and Pi: ```json "transport": "websocket" ``` -WebSocket mode uses protocol v2 on `/ws`, keeps one connection per OpenCode `x-session-affinity`, and serializes same-session requests until the previous stream finishes. Existing deployed Workers must be redeployed with this package's current Worker script before `"transport": "websocket"` will work; older relay Workers only understand the legacy protocol. +WebSocket mode uses protocol v2 on `/ws`, keeps one connection per session (OpenCode `x-session-affinity`, Pi stream `sessionId`), and serializes same-session requests until the previous stream finishes. Existing deployed Workers must be redeployed with this package's current Worker script before `"transport": "websocket"` will work; older relay Workers only understand the legacy protocol. OpenCode 2 supports direct and HTTP relay transport only; with WebSocket selected, it refuses requests before sending them with `request refused: WebSocket relay is not supported on OpenCode 2; select HTTP relay or direct transport`. -Set up a relay in your Cloudflare account with the OpenCode package CLI: +Set up a relay in your Cloudflare account with the OpenCode package CLI. The CLI saves relay settings in OpenCode's local account pool, so run [setup](#quick-setup-recommended) for OpenCode first to prepare the local account pool before the CLI saves settings: ```bash CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... bunx @cortexkit/opencode-anthropic-auth relay setup @@ -656,9 +663,9 @@ The setup command: 2. Uploads the relay Worker module. 3. Generates a relay shared secret. 4. Enables the Worker. -5. Writes the local `relay` block to `~/.config/opencode/anthropic-auth.json`. +5. Saves the relay URL, shared secret, and HTTP transport in OpenCode's local account pool. -For Pi, copy the generated `relay` block into `~/.pi/agent/anthropic-auth.json` if you want the Pi package to use the same user-owned Worker. +`relay setup` configures OpenCode's relay settings, not Pi's. During the first local setup, existing relay settings (including `transport`) are copied from `anthropic-auth.json` into that application's pool; the `/claude` menu has no relay actions. The Cloudflare API token is used only during setup and is not stored by the plugin. Re-running setup with the same Worker name uploads the current Worker script again; this is how existing relay Workers are upgraded for protocol changes such as WebSocket v2. @@ -687,13 +694,7 @@ Direct fallback logs `falling back direct`. ### Request dumps -For relay/cache debugging, enable exact request dumps from inside OpenCode or Pi: - -```text -/claude-dump on -/claude-dump off -/claude-dump -``` +For relay/cache debugging, enable exact request dumps from inside OpenCode or Pi with **Enable request dumps** and **Disable request dumps** in the `/claude` menu's Diagnostics group. **Dump status** shows the current setting. When enabled, the plugin writes artifacts under the OS temp directory: @@ -710,7 +711,7 @@ Each filename includes a sanitized session/affinity segment so dumps from differ Lane-start requests use the `-start-` dump marker; CacheKeep keeps `-prewarm-cachekeep-`. Their cache-diagnostics records use `source: "start"` with `synthetic: true`, alongside ordinary `turn` records. -Dump state is persisted in the active sidecar config as `dump.enabled` (`~/.config/opencode/anthropic-auth.json` for OpenCode, `~/.pi/agent/anthropic-auth.json` for Pi). Dumps may contain prompt content and should be treated as sensitive local debugging artifacts. +The dump setting is saved with the host's other plugin settings. Dumps may contain prompt content and should be treated as sensitive local debugging artifacts. ## Environment variables @@ -718,16 +719,21 @@ Dump state is persisted in the active sidecar config as `dump.enabled` (`~/.conf | --- | --- | | `ANTHROPIC_BASE_URL` | Override the Anthropic API endpoint. Must be HTTP(S). | | `ANTHROPIC_INSECURE` | Set to `1` or `true` to skip TLS verification when `ANTHROPIC_BASE_URL` is set. | -| `OPENCODE_ANTHROPIC_AUTH_FILE` | Override the OpenCode sidecar config path. | +| `OPENCODE_ANTHROPIC_AUTH_FILE` | Override OpenCode's old sidecar path. The local account pool and its related files are created next to it. | +| `OPENCODE_ANTHROPIC_AUTH_STATE_FILE` | Override the old sidecar state file that setup reads during migration. | | `OPENCODE_ANTHROPIC_AUTH_CLAUSTRUM_ENROLLMENT_FILE` | Override the owner-only OpenCode Claustrum enrollment-token path. | +| `OPENCODE_ANTHROPIC_AUTH_CLAUSTRUM_CONNECTION_FILE` | Override the Claustrum connection file OpenCode uses. | | `OPENCODE_ANTHROPIC_AUTH_FALLBACK_MODE` | Set to `legacy` to bypass Anthropic's server policy and use deterministic 10-response client recovery exclusively. The default tries server-side safety fallback first and uses client recovery as a backstop. | | `OPENCODE_ANTHROPIC_AUTH_ROUTING_STATE_FILE` | Override the persistent sticky-balanced session assignment file. | | `OPENCODE_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR` | Override the temporary OpenCode CacheKeep session lease directory. | -| `OPENCODE_ANTHROPIC_AUTH_RPC_DIR` | Override the directory for the OpenCode TUI command-modal RPC bridge (port file + token). Defaults to a per-project path under the OpenCode state directory. | -| `PI_ANTHROPIC_AUTH_FILE` | Override the Pi sidecar config path. | +| `OPENCODE_ANTHROPIC_AUTH_RPC_DIR` | Override the directory for the OpenCode TUI menu RPC bridge (port file + token). Defaults to a per-project path under the OpenCode state directory. | +| `PI_ANTHROPIC_AUTH_FILE` | Override Pi's old sidecar path. The local account pool and its related files are created next to it. | +| `PI_ANTHROPIC_AUTH_CLAUSTRUM_ENROLLMENT_FILE` | Override the owner-only Pi Claustrum enrollment-token path. | +| `PI_ANTHROPIC_AUTH_CLAUSTRUM_CONNECTION_FILE` | Override the Claustrum connection file Pi uses. | | `PI_ANTHROPIC_AUTH_ROUTING_STATE_FILE` | Override the persistent Pi sticky-balanced session assignment file. | | `PI_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR` | Override the temporary Pi CacheKeep session lease directory. | -| `PI_AGENT_DIR` | Override Pi's agent directory when deriving the default sidecar path. | +| `PI_CODING_AGENT_DIR`, `PI_AGENT_DIR` | Override Pi's agent directory when deriving default paths. | +| `XDG_STATE_HOME` | Base directory for the default enrollment files (`cortexkit/anthropic-auth/-enrollment.json`). Defaults to `~/.local/state`. | | `CLOUDFLARE_API_TOKEN` | Cloudflare token used by `bunx @cortexkit/opencode-anthropic-auth relay setup`. Not stored. | | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID used by relay setup. | @@ -739,7 +745,7 @@ For Claude Pro/Max OAuth requests, the plugin works at the final Anthropic wire- 2. Normalizes Claude-compatible OAuth headers and beta flags. 3. Removes OpenCode-specific identity text from system blocks. 4. Prepends Claude Code identity and billing-header blocks. -5. Rewrites cache controls according to `/claude-cache` mode. +5. Rewrites cache controls according to the configured cache mode. 6. Renames MCP tool names into Claude-compatible PascalCase form. 7. Opts eligible Fable 5/5.1 and Opus 5 OAuth requests into Anthropic's server-side safety fallback, restores stored fallback boundaries, and activates replay-safe client recovery if the response still refuses. 8. Computes final-body `cch` over the fully serialized request body. @@ -826,10 +832,12 @@ The release workflow runs checks, publishes the core, OpenCode, and Pi packages ## Troubleshooting - Clear OpenCode's plugin cache after plugin config changes: `rm -rf ~/.cache/opencode`. -- Restart OpenCode or Pi after changing sidecar config; some settings are loaded at startup. -- If an OpenCode fallback account shows `invalid_grant`, run `bunx @cortexkit/opencode-anthropic-auth login