78 Commits
Author SHA1 Message Date
6e3590303b feat(partners): add Concentrate and Exa to partner roster (#2141)
Adds Concentrate (concentrate.ai) and Exa (exa.ai) to the README
partners table and the web landing page, with light/dark logo variants
self-hosted under docs/assets/ and web/public/partners/.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-19 14:13:09 +08:00
645d596ea4 fix(code-reviewer): require inline diff input and preserve read-only search in embedded-search builds (#2102)
* feat: add code reviewer agent

* feat(agent): add code-reviewer built-in agent implementation and tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(code-reviewer): address review comments

- Fix broken step numbering in system prompt (glob/grep as sub-bullets)
- Remove unnecessary wrapper in getSystemPrompt
- Drop unused beforeEach/afterEach lifecycle in tests
- Remove redundant registration test (beforeAll already throws)
- Fix CLAUDE_CONFIG_DIR leak in beforeAll (restore in finally)
- Replace @ts-ignore with explicit ToolUseContext cast
- Use placeholder in README agentRouting example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger rerun — pre-existing test failures on main

* fix(code-reviewer): enforce read-only contract by disallowing Bash

Add Bash to disallowedTools so the reviewer cannot run shell commands
regardless of parent session's acceptEdits/bypassPermissions mode.
resolveAgentTools() treated undefined tools as wildcard — Bash was
available and could auto-approve mkdir/rm/mv in acceptEdits mode.

Remove Bash guidance from system prompt; diff must now be supplied by
the caller inline. Update test to assert Bash is in disallowedTools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(code-reviewer): deny all shell tools (Bash and PowerShell)

Use SHELL_TOOL_NAMES constant instead of just BASH_TOOL_NAME to ensure
all shell-capable tools are denied from the code-reviewer agent. This
prevents Windows sessions with PowerShell enabled from bypassing the
read-only contract.

- Import SHELL_TOOL_NAMES from shellToolUtils
- Use spread operator to include both Bash and PowerShell in disallowedTools
- Update test to verify PowerShell is denied

Fixes the finding: [P2] Deny all shell tools for the reviewer agent

* fix(code-reviewer): explicit read-only allow-list; drop unrelated artifacts

Switch code-reviewer to an explicit `tools` allow-list (Read, Glob, Grep)
instead of relying on wildcard access minus a deny-list. resolveAgentTools()
resolves only the named tools, so write-capable mcp__* server tools (and any
other mutation-capable tool) can never be handed to the read-only reviewer.
Keep the mutation deny-list as defense-in-depth.

Remove generated/scratch artifacts unrelated to the reviewer agent:
AGENTS.md, ARCHITECTURE.md, the .openlore/ .gitignore rule, temp_reference/,
and the .tmp/sdk-consumer-* scratch files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: restore temp_reference/ gitignore entry from main

Entry was present on main from #1350 and was unintentionally removed
during PR cleanup. Restoring it so temp_reference/ scratch directories
remain untracked after merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(code-reviewer): require inline diff input and preserve read-only search in embedded-search builds
Summary:
- Update the `code-reviewer` built-in agent to require the caller to provide the diff or changed hunks inline in the prompt.
- Preserve read-only search behavior in embedded-search builds by omitting `Glob`/`Grep` from the explicit tool allow-list when embedded search tools are enabled.
- Keep a strict read-only policy by disallowing shell and mutation tools.

Usage:
- The `code-reviewer` agent is now explicitly guided to only review changes when the diff is provided inline.
- In embedded-search builds, the agent only receives `Read` and cannot access `Glob`/`Grep`.
- This prevents the agent from attempting shell-based diff discovery and enforces caller-provided diff input.

Test plan:
- `bun test src/tools/AgentTool/built-in/codeReviewerAgent.test.ts` — 12 pass, 0 fail
- `bun run build` — success
- `bun run smoke` — success
- `bun run security:pr-scan` — success
- `git diff --check` — success

Credit: prior work from #1381/#1420.

* fix(code-reviewer): clear cached agent definitions and markdown loader cache after test cleanup

* fix(code-reviewer): address all P2/P3 review findings from jatmn

- Always list [Read, Glob, Grep] in the tool allow-list; in embedded-search
  builds resolveAgentTools() silently drops unavailable Glob/Grep at runtime.
  The system prompt explicitly documents the narrowed search contract for
  embedded builds instead of silently degrading.

- Update the code-reviewer invocation example in prompt.ts to include the
  diff inline, satisfying the reviewer's contract and preventing an avoidable
  extra turn.

- Rewrite the test suite with the same isolation protocol as
  loadAgentsDir.test.ts: shared mutation lock, OPENCLAUDE_CONFIG_DIR,
  setClaudeConfigHomeDirForTesting, setAllowedSettingSources, and full
  env/cache restore in finally blocks.

- Exercise both embedded-search branches: non-embedded tests verify Glob/Grep
  guidance, embedded tests verify the limited-search documentation and
  Read-only path.

- Fix settings file path in README.md and docs/agent-routing.md from
  ~/.openclaude.json to ~/.openclaude/settings.json (the path the runtime
  actually loads).

- Add blank line after fenced code block (MD031), add code-reviewer to the
  routable built-in agent list in both README and agent-routing docs.

* fix(code-reviewer): restore prior setting sources in test cleanup, add credential security warning

- Capture getAllowedSettingSources() before overwriting and restore it
  in the finally block instead of resetting to the default list, preventing
  state leakage to concurrent suites.

- Add plaintext-credential security warning before the agentModels JSON
  example in README.

- Update 'All settings-driven' to 'Configured via settings, agent
  frontmatter, and environment variables' for accuracy.

* fix(code-reviewer): address remaining PR feedback (P1/P3)

* docs: document feature gate for Explore and Plan agents

* docs: document inline-diff requirement for code-reviewer agent

* fix(code-reviewer): address P1/P2 review findings — teammate boundary, resume safety, lock-aware tests

[P1] Reject built-in agent types from teammate spawn path to preserve
read-only boundary. The teammate branch bypasses resolveAgentTools(),
so built-ins like code-reviewer would receive Bash/Edit/Write tools.
Guard added in AgentTool.tsx before spawnTeammate is called.

[P1] Fail closed when resuming an unavailable agent type instead of
silently falling back to GENERAL_PURPOSE_AGENT. A resumed code-reviewer
must never gain edit-capable tools through a compatibility fallback.

[P2] Snapshot environment and config state only after acquiring the
shared mutation lock in codeReviewerAgent.test.ts. Moved from module-
scope const to post-lock capture in beforeAll, with cleanup and lock
release in afterAll's finally block.

Regression tests added for all three findings.

* fix(code-reviewer): guard lock release against failed acquisition

Only call releaseSharedMutationLock() in afterAll when the lock was
successfully acquired. Prevents releasing another suite's lock if
acquireSharedMutationLock() throws on timeout.

* fix(code-reviewer): remove trailing whitespace

* fix(code-reviewer): reliably block built-in teammate spawns

Reject built-in agent types from the teammate spawn path by looking them up in allAgents rather than activeAgents.
This ensures the restriction remains intact even when built-in agents are disabled (e.g. via CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS) and omitted from activeAgents.
Regression test added.

* fix(code-reviewer): preserve original agent identity when resuming a read-only reviewer

Background-agent metadata persisted only agentType, so resuming selected whichever active definition currently had that name. A built-in code-reviewer could be started, followed by a project/SDK definition named code-reviewer taking precedence; resuming the original agent would then use the replacement's ordinary wildcard tool set and grant that reviewer transcript Bash/Edit/Write tools.
Persist the agent definition source and verify it matches the resolved definition on resume, rejecting the resumption if the original was spoofed.

* fix(code-reviewer): address remaining CodeRabbit feedback on test cleanup and metadata source

* fix(code-reviewer): address P1/P2 issues for teammate spawns and resume safety

* docs: make OpenLore prerequisite explicitly optional in AGENTS.md

* docs: fix pinned OpenLore version in AGENTS.md

* Fix review issues

* Revert AGENTS.md changes

* Restore AGENTS.md to match upstream/main

* fix(agent): address maintainer feedback on teammate spawns and resume persistence

* test(agent): add regression coverage for legacy source-less agent resume

* fix(agent): propagation pass — batch fork regression, TeamCreate policy, trailing whitespace, verification gate docs

* fix(batch): allow specific custom agent types while requiring subagent_type

---------

Co-authored-by: Laurent FRANCOISE <lfrancoise@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-19 13:55:48 +08:00
anushka ✩andGitHub 294bd9a1df Add optional Sentry error reporting (env-driven, opt-in) (#2139)
* Add optional Sentry error reporting (env-driven, opt-in)

* Fix Sentry init to use dynamic import instead of require (ESM compatibility)

* Document SENTRY_DSN setup in advanced-setup docs

* Disable Sentry default integrations; document runtime install requirement

* Wire reportErrorToSentry into top-level error handlers; add sentry.test.ts
2026-08-19 13:51:37 +08:00
BogdanandGitHub 09eba26d30 feat(cost): support exact custom model pricing (#2131)
* feat(cost): support exact custom model pricing

* fix(cost): address custom pricing review feedback
2026-08-16 15:55:02 +08:00
BogdanandGitHub c30578819e diagnostics(query): trace interruption causality (#2111)
* diagnostics(issue-1830): trace interruption causality

* test(issue-1830): lock interruption ownership matrix

* fix(codex): preserve stream deadline contract

* fix(diagnostics): harden interruption trace lifecycle

Refs #1830

* fix(diagnostics): harden interruption trace settlement

Refs #1830

* fix(diagnostics): preserve interruption causality

* fix(diagnostics): address interruption trace review

* fix(diagnostics): preserve tracing observer contracts

* fix(diagnostics): preserve interruption trace contracts

* test(permissions): cover interactive hook interrupts
2026-08-16 15:54:29 +08:00
BogdanandGitHub ea655163d3 feat(zai): expand Coding Plan catalog support (#2127)
* feat(zai): expand Coding Plan catalog support

Signed-off-by: chioarub <chioarub@gmail.com>

* fix(zai): use supported low reasoning mode

Signed-off-by: chioarub <chioarub@gmail.com>

---------

Signed-off-by: chioarub <chioarub@gmail.com>
2026-08-15 17:05:49 +08:00
fb9102c422 feat(aimlapi): passwordless onboarding and resumable card top-up (3/3) (#2032)
* feat(aimlapi): add checkout state persistence and sign-in key cache

* fix(aimlapi): cover lock recovery and complete the reset receipt

* test(aimlapi): cover CAS result, reset receipt and sign-in key permissions

* fix(aimlapi): make stale-lock recovery ownership-safe across processes

* test(aimlapi): pass a file URL specifier to the lock workers

* fix(aimlapi): use proper-lockfile and harden checkout-state persistence

* fix(aimlapi): preserve issued keys and survive stale-lock steal races

* fix(aimlapi): surface swallowed lock-retry conditions

* feat(aimlapi): resume interrupted checkouts in the top-up entry points

* fix(aimlapi): keep resumable checkouts through transient and paid states

* fix(aimlapi): surface a lost settled-receipt write to the caller

* fix(aimlapi): harden checkout resume across CLI resume, idempotency and transient errors

* fix(aimlapi): resume settling checkouts and preserve records on ambiguous reads

- Treat 'exchanging' as a paid/resumable status so a run interrupted between
  payment and receipt resumes the exchange instead of opening a second,
  chargeable checkout (matches pollUntilPaid).
- Preserve the recorded checkout on a malformed-but-successful status read
  (AimlapiApiError status 200), alongside the existing transient-error path.
- Print the full recovery key in the receipt-write-failed warning; a masked
  key is useless as a last copy.
- Re-register the real client/providerProfile modules in afterAll so the
  test stubs cannot leak into later files (mock.restore does not undo
  mock.module).
- Bound each lock worker's exit before draining its pipes, and loosen the
  held-lock timeout assertion to any errno (Windows is not always ELOCKED).

* fix(aimlapi): stop mock leak, fail closed on spent sessions, clear GUI receipt

- topup.test.ts: capture the real client/providerProfile modules through a
  cache-busting query so afterAll restores the genuine module instead of the
  corrupted (stub-mutated) reference. Without this the './client.js' stub bled
  past afterAll and failed 25 client.test.ts cases whenever it ran first.
- resolveCheckoutSession: fail closed when a resumed session is already
  'exchanged' but no settled receipt survived locally, matching pollUntilPaid,
  instead of opening a second chargeable checkout for a one-shot key that is
  already gone.
- provisionAimlapiKey: return a clearReceipt closure; ProviderManager now calls
  it only after persistDraft actually saves, so a second GUI top-up opens a
  fresh checkout instead of short-circuiting to a stale key or throwing.
- claimAimlapiTopupState: refuse to replace a stored record that still has an
  open resume token for a different intent, so a changed amount cannot strand a
  still-payable checkout and open a second one.
- Mask the issued key in the CLI receipt-write-failed warning; a paid-for
  credential should not land in scrollback.
- index.ts: note the live CLI/GUI callers and the clear-receipt obligation.
- Regression coverage for exchanged fail-closed, the claim guard, and the GUI
  receipt clear.

* fix(aimlapi): inject the topup transport instead of mocking client.js globally

The prior mock.module('./client.js') stub in topup.test.ts leaked past its
afterAll into client.test.ts (25 failures when this file ran first in CI,
bun 1.3.13). Replace it with a local injection seam:

- topup.ts exposes setAimlapiTopupTestDoubles, and both entry points create
  their client / write their profile through it (defaults unchanged, so
  production behaviour is identical).
- topup.test.ts injects a stub transport through that seam and no longer calls
  mock.module at all, so nothing can bleed OUT to client.test.ts.
- It still loads topup.js through a cache-busting ?ts= query so it stays immune
  to ProviderManager.test.tsx's mock.module('../integrations/aimlapi/index.js'),
  which mock.restore() does not undo and which would otherwise replace the
  shared provisionAimlapiKey binding (verified: without the query the barrel
  stub reaches this file and every topup test fails).

* fix(aimlapi): converge racing checkouts and preserve state on ambiguous reads

- Close a post-claim race: two runs of the same intent converge on one payment
  id, then each can open a session before either records one, and the second
  save overwrote the first's resume token (two payable checkouts). Add
  recordAimlapiCheckoutSession, a compare-and-swap that only records while the
  token is empty; resolveCheckoutSession adopts the winner's session and
  abandons the one it just opened, so pay() converges idempotently on a single
  charge.
- Resume path now preserves the record on any ambiguous getSession error
  (transient, malformed-200, auth/4xx) and retires it only on a definitive
  404/410 gone-session, instead of clearing on every non-transient error.
- pollUntilPaid retries a malformed-but-successful (status 200) body instead of
  aborting, matching the resume path.
- Regression coverage for the peer-records-first race, ambiguous-error
  preserve, 404 replace, and poll-retry-on-200.

* fix(aimlapi): re-validate an adopted peer session before paying it

When two racing runs converge and this one adopts the peer's recorded session,
route that session through the same status classification as the initial
resume: return it only while resumable, fail closed on 'exchanged', and surface
a re-run error on any other terminal status instead of calling pay() on a dead
session. Add regression coverage for an adopted session that is exchanged or
cancelled in the race window.

* test(aimlapi): make abandoned-lock recovery test deterministic

The stale-lock recovery test asserted that all racing claims return the same
payment id. Under a stale-lock steal two recoverers can briefly hold the lock
and mint distinct ids, so that assertion was flaky in CI. A diverged claim is
harmless — it is refused at its next compare-and-swap — so the invariant that
actually matters is that exactly ONE checkout gets established. Drive the
workers through the full claim->record flow and assert a single winner, which
the single-slot store plus the record CAS guarantee deterministically.

* fix(aimlapi): acquire checkout-state off the interactive thread and recover fresh orphans

The interactive top-up flow acquired the checkout-state lock synchronously,
parking the Ink event loop (UI, timers, SIGINT) on Atomics.wait for up to the
5s timeout, and that timeout was shorter than the 30s stale window so a lock
orphaned by an interrupted holder could not be recovered on an immediate resume.

- Add withStateLockAsync + async variants of the state mutators, sharing the
  same inner operations. It acquires with the timer-free lockSync (a sub-ms
  mkdir) but yields via await between retries, so the UI stays live, and its
  longer deadline (15s) covers the stale window so a fresh orphan is reclaimed
  once stale rather than timing out. Shrink the stale window to 8s (sub-ms
  sections never approach it) so recovery is quick.
- Route topup.ts (CLI + GUI) through the async variants; provisioned.clearReceipt
  is now async, and ProviderManager awaits it best-effort so a cleanup failure
  cannot surface an error that invites a retry (a duplicate provider profile).
- Regression coverage for async orphan recovery (mutation-checked: a deadline
  below the stale window fails to recover). Generous per-file test timeout since
  the now-async provisioning yields to a loaded runner's event loop.

* test(aimlapi): cover the async state mutators and speed up the orphan-recovery test

- Add thin contract tests for saveAimlapiTopupStateAsync (CAS accepted/rejected),
  recordAimlapiCheckoutSessionAsync (compare-and-swap on the empty resume token),
  and clearAimlapiTopupStateAsync (ownership-scoped clear); the CLI/GUI flow now
  routes through these and only claimAsync was exercised.
- Back-date the orphaned lock to just inside the stale window so the async
  recovery test reclaims it in ~2s instead of burning the full 8s window.

* fix(aimlapi): fail closed on corrupt state, deliver keys on receipt failure, add a reset action

- Fail closed when the checkout-state file is present but unreadable/schema-
  invalid instead of reading it as absent: a claim would otherwise overwrite an
  open/paid checkout or an exchanged key and open a second chargeable one.
- Never strand a one-shot exchanged key: a receipt-write that throws (lock
  timeout / fs / corrupt state), not just a lost CAS, is caught so the CLI still
  writes the profile and the GUI still returns the key; the post-delivery clear
  is best-effort too.
- Add an explicit discard/reset escape hatch — discardAimlapiCheckoutState, an
  "openclaude aimlapi reset" command, and a GUI "Start over" on the top-up error
  screen — so a terminal checkout (whose resume token blocks a different intent)
  or a corrupt state file can be cleared without editing internal files.
- Surface a failed GUI receipt retirement: retry a few times, then show a
  non-blocking warning instead of swallowing it silently.
- Only chmod a config directory this flow actually created (via mkdirSync's
  return); never tighten a pre-existing OPENCLAUDE_CONFIG_DIR root. The state
  file's own 0600 mode protects the credential.
- Tests for each, incl. corrupt/schema-invalid fail-closed, receipt-write-throw
  key delivery (CLI + GUI), discard semantics, retried/surfaced GUI cleanup, and
  a POSIX check that an existing config dir keeps its mode.

* fix(aimlapi): surface the CLI recovery-receipt clear failure and cover the reset handler

- The CLI finishCliTopup clear failure only went to logForDebugging (debug-only),
  invisible to the user, unlike the loud receipt-write warning and the GUI
  warning. Print a [warn] line pointing at "openclaude aimlapi reset" so a
  stranded receipt that blocks a different top-up is not hidden.
- Add tests for the aimlapiReset CLI handler (discards a stored checkout /
  reports when there is nothing to discard).
- Assert the CLI clear-failure warning is surfaced in the receipt-write-failure
  test (mutation-checked).
- Document "openclaude aimlapi reset" and the GUI Start over recovery in
  docs/aimlapi-setup.md.

* fix(aimlapi): protect a settled receipt from reset and bind method into the checkout identity

- Discard (CLI reset / GUI Start over) no longer deletes a settled receipt — the
  only copy of a paid-for, one-shot key — unless forced. discard now returns
  'discarded' | 'kept-settled' | 'none'; the CLI adds --force and the GUI refuses
  and points back at the recovering retry.
- Add the payment method to AimlapiTopupIntent so a card->crypto (or reverse)
  restart is a different intent and cannot adopt the prior checkout's reused
  idempotency id on the wrong rail; covered by a changed-method resume test.
- Narrow the sign-in-key cache: it is a persistence primitive for the follow-up
  guided passwordless flow with no in-tree consumer, so drop it from the public
  barrel and document the scope (kept in topupState.ts for that follow-up).
- Regression + mutation coverage for the settled-receipt protection and the
  method-scoped intent.

* fix(aimlapi): back off receipt-clear retries, expand the discard API, and test Start over

- Add a 150ms backoff between the GUI clearReceipt() retries so the loop can
  actually ride out a lock-contention window instead of exhausting three
  back-to-back attempts.
- Re-export AimlapiDiscardResult and add resetAimlapiCheckoutSessionAsync so
  barrel consumers can name the discard outcome and use a non-blocking reset,
  matching the other mutators.
- Cover the GUI Start over recovery: it is 'r' (settings:retry) on the top-up
  error screen — the Settings context has no confirm:yes and Enter closes the
  panel; tests drive both the discard and the kept-settled refusal path.
- Rename the test's stale-age constant (LOCK_AGE_WELL_PAST_STALE_MS) so it no
  longer reads as the source's 8s window.

* fix(aimlapi): serialize the one-shot key exchange and recover receipts before login

Address three checkout-state review findings:

- [P1] Serialize the non-idempotent key exchange behind an exchange lease so
  racing same-intent processes mint and record the credential exactly once. The
  elected lease holder exchanges and records the settled receipt; a peer that
  loses the election waits for that receipt and resumes from it instead of
  exchanging in parallel; a lease abandoned by a crashed holder goes stale (past
  the client request timeout) and is reclaimed on a later attempt. The lease is
  released on a failed exchange so a retry is not blocked for the stale window.

- [P1] Recover a settled local receipt BEFORE authenticating in both the CLI and
  guided entry points. A run interrupted after the one-shot exchange but before
  the profile write leaves the paid-for key only in that receipt; requiring a
  fresh login to reach it stranded the key whenever the password had changed or
  the auth service was down. The receipt read is side-effect free and needs no
  token, so it now runs first and only authenticates when a checkout must be
  created/resumed/exchanged.

- [P2] Fix the guided-recovery key in the setup guide: Start over is bound to r,
  not Enter (Enter closes the settings panel).

Covered by state-layer lease tests (acquire/held/stale-steal/settled/gone/
release), a two-process race asserting the exchange runs exactly once with the
loser resuming the receipt, and no-auth-on-settled tests for both entry points.

* test(aimlapi): harden the exchange-lease coverage

Address review follow-ups on the exchange-lease tests (all test-only):

- Gate the two-process race on the loser's own "waiting" status signal instead
  of a fixed sleep, and assert it fired, so the test deterministically exercises
  the held -> wait -> resume path rather than possibly reading settled directly.
- Type seedPersistedState's overrides as Partial<AimlapiPersistedTopup> so a
  misspelled/wrong-typed seed key is a typecheck failure instead of a record
  that silently reroutes the test down another branch.
- Cover re-acquiring your own lease (leaseOwner === owner) so the guard that
  keeps a caller from mistaking its own fresh lease for a live peer's — and
  self-blocking until the stale window — cannot regress unnoticed.

* fix(aimlapi): fence a superseded profile write, protect unreadable state, resume the receipt model

Address three checkout-state review findings:

- [P1] Fence an in-flight checkout before it writes its key to the provider
  profile. If a reset (or a fresh top-up) replaces the stored slot while an
  abandoned flow is awaiting client.exchange(), that flow's settled-receipt CAS
  now misses; previously it still went on to write its stale key and could
  clobber the profile the new top-up created. recordSettledReceipt now reports
  recorded | superseded | errored, and the exchange path aborts (rejecting, and
  pointing the user at rotating the orphaned key) on `superseded` while still
  delivering on a transient `errored` so a paid-for key is never stranded.

- [P1] Do not discard an unreadable/corrupt state file without --force. Such a
  file could be a damaged receipt holding the sole copy of a one-shot key, so
  discardAimlapiCheckoutState now returns `kept-unreadable` and keeps it unless
  forced — matching the safety promise (and existing settled-receipt protection)
  that reset never loses an issued key. The CLI and guided "Start over" surface
  the new outcome and point at `reset --force`; docs updated.

- [P2] Use the settled receipt's model when a peer completed the exchange. The
  settled lease branch dropped lease.state.model, so a loser resuming another
  run's receipt configured its own --model instead of the one actually
  provisioned; it now propagates the receipt's model through both callers.

Covered by: a superseded-mid-exchange fence test, corrupt-discard-needs-force
tests (state layer + CLI + guided GUI), and a two-process race asserting the
loser adopts the winner's provisioned model. All three are mutation-proven.

* test(aimlapi): drop the flaky third guided Start-over drive test

The guided "Start over on a kept-unreadable discard" test added a third
consecutive full Ink mount+drive to ProviderManager.test.tsx. On the CI-pinned
bun (1.3.13) that destabilises the Ink stdin harness (`stdin.ref is not a
function`), so the 'r' keypress never reaches the handler and the awaited
discard call never fires — a timeout unrelated to the code under test (it passes
on local bun 1.3.14). The two-drive configuration is green on CI.

The kept-unreadable behaviour stays covered where it is deterministic: the state
layer (kept-unreadable unforced, discarded on --force) and the CLI handler (the
`reset --force` guidance). The guided keybinding→discard→refusal plumbing is
covered by the identical-structure kept-settled drive test; the kept-unreadable
GUI branch is a trivial mirror of it. A comment records why the third drive is
intentionally omitted.

* wip(aimlapi): converge integration + CLI to the passwordless card-only flow (#1988)

Mid-port checkpoint. Rewrites the AI/ML API integration backend and CLI to the
canonical passwordless, card-only design (target PR #1988), in the current
code style. NOTE: the branch does not yet compile — the GUI (ProviderManager
passwordless rewire) and the aimlapi test suite still reference the removed
API and are the remaining work.

Done (typecheck-clean in these files):
- config.ts: add payBaseUrl + verificationBaseUrl endpoints, buildPartnerReturnUrl.
- topupState.ts: reduce to the #1988 shape — intent keyed on payBaseUrl/
  verificationBaseUrl (no `method`); sync lockfile-based state; drop the
  exchange-lease, discard/reset-command surface, async variants and
  fail-closed-on-corrupt (corrupt reads as absent).
- client.ts: drop the password path (signup/login) and PaymentMethod/crypto;
  pay() is card-only.
- topup.ts: rewrite to the passwordless phase-machine flow (checkAccount ->
  code sign-in / new-account -> provision or by-key top-up; resolveTopupSession;
  pollUntilPaid / pollUntilExchangeSettled / pollUntilByKeyToppedUp). Keeps the
  DI test seam (no cross-file mock.module). Profile env writes AIMLAPI_API_KEY
  mirror + CLAUDE_CODE_PROVIDER_ROUTE_ID.
- onboarding.ts, messages.ts (canonical copy), validation.ts, index.ts (lean
  barrel), providerManagerAimlapi.ts (GUI indirection layer): new.
- CLI: aimlapiCommand.ts (registerAimlapiCommand, --email/--code/--amount, no
  --method/reset), main.tsx wiring, handlers/aimlapi.ts (redacted errors).

Mandatory attribution headers wired on EVERY aimlapi request: X-AIMLAPI-Source
(agent/openclaude) + X-AIMLAPI-Partner-ID — in client.ts request() (auth/
checkout) and the config attribution path (inference/catalog).

* wip(aimlapi): port the passwordless provider-manager GUI (#1988)

Rebase ProviderManager.tsx on the #1988 passwordless aimlapi flow (email ->
6-digit code -> low-balance -> top-up / paste-existing-key -> done), replacing
the old password/method/Start-over flow and importing the aimlapi surface from
providerManagerAimlapi.js. Re-apply the current newer-main, non-aimlapi
`apiFormat: 'auto'` feature that the rebase would otherwise revert (form
metadata, toDraft default, display label, startCreateFromPreset default, the
API-format picker's Automatic option, and persistDraft's selectedApiFormat
'auto' -> undefined branch, kept alongside #1988's deferNavigation/onSaved).
Re-export resolveRouteCredentialValue from integrations/index for the GUI.

All source now typechecks; the aimlapi test suite is the remaining work.

* test(aimlapi): port integration + CLI tests to the passwordless flow

- topupState/topup/onboarding/aimlapiCommand tests: port #1988's coverage,
  adapting the transport to `globalThis.fetch` stubbing and the profile/prompt
  doubles to the module's `setAimlapiTopupTestDoubles` DI seam (no process-global
  mock.module, which leaks across files in this repo).
- client/config tests: keep the current repo's stricter versions (complete
  session contracts), pruning the removed password/crypto cases.
- Add mandatory-attribution-header coverage on all four request classes: the
  client sends X-AIMLAPI-Source + X-AIMLAPI-Partner-ID on auth/checkout, and the
  config attribution path sends both on inference/catalog and strips them for a
  non-canonical proxy endpoint.
- Remove the reset-based CLI handler test (reset no longer exists).

Full aimlapi integration + CLI suite green (67 tests). ProviderManager GUI test
is the remaining piece.

* test(aimlapi): port the passwordless provider-manager GUI tests (#1988)

Rebase ProviderManager.test.tsx on #1988's version for the passwordless aimlapi
GUI tests (email -> code -> low-balance -> top-up / paste-existing-key), which
mock ./providerManagerAimlapi.js. Re-apply HEAD's newer-main apiFormat 'auto'
test cases (API-mode picker, token field, OpenAI/GPT-5/MiniMax presets) since
the source keeps that feature, and restore the current preset list ('LongCat')
in the test's PRESET_ORDER so navigateToPreset indexes match the real presets.

ProviderManager suite green (42 tests).

* docs(aimlapi): rewrite the setup guide for the passwordless card-only flow (#1988)

Describe the passwordless /provider flow (saved-key continue, new-user email +
6-digit code, paste-existing-key, low-balance top-up) and the card-only CLI
`aimlapi topup --email/--code/--amount` (no --method, no reset). Document the
full endpoint override set and the two mandatory attribution headers
(X-AIMLAPI-Source + X-AIMLAPI-Partner-ID) sent on every request, stripped for a
non-canonical proxy endpoint.

* refactor(aimlapi): let tests inject prompt doubles into the top-up flow

* fix(aimlapi): lock the partner id and complete mandatory-header coverage

Lock the partner id to OpenClaude's own attribution id: drop the --partner-id
CLI flag and the AIMLAPI_PARTNER_ID env override so rebate/revenue-share
attribution can never be redirected. resolvePartnerId() now always returns the
built-in id; the mandatory X-AIMLAPI-Partner-ID header itself is unchanged.

Assert the mandatory X-AIMLAPI-Source header on the catalog/discovery and
inference paths (discoveryService, bootstrap, runtimeMetadata) — the header was
already sent, only the test expectations lagged. Refresh stale password-era copy
in the interactive prompt and a top-up comment left over from the removed flow.

* fix(aimlapi): address CodeRabbit review — settlement, key-safety, redaction

- Wait for a resumed sign-in top-up to settle before returning: the account
  non-exchange path now mirrors the by-key flow, so a credited balance is never
  reported while the billing operation is still in flight.
- Preserve a freshly minted sign-in key when the balance read is aborted, so an
  abort cannot orphan a paid credential and mint a second key on the next run.
- Clear the first-run env-key adoption markers when validation fails, so a retry
  re-validates instead of short-circuiting into persisting the unvalidated key.
- Never crash the top-up success screen on an amount parse edge — fall back to
  the raw entered amount after the payment has already cleared.
- Show all four API-format options (visibleOptionCount 3 -> 4).
- Document that guided provisioning requires the canonical inference endpoint.
- Add regression tests: resumed-sign-in settlement, aborted-balance key
  retention, failed-env-key re-validation, and CLI error redaction.

* test(aimlapi): wait for the masked code frame instead of a fixed delay

The AIMLAPI code-screen assertion captured output after a fixed 25ms sleep,
which is too short on a slower CI runner (Node 24) and intermittently missed the
freshly rendered mask characters. Wait for the masked frame instead so the
assertion is deterministic.

* test(aimlapi): harden the provider-manager GUI flows against CI timing

The GUI top-up flow intermittently failed on a loaded CI runner: the final
keystroke on the success screen was sent in the same tick as the render, before
Ink attached the input handler, so it was dropped and the flow stranded on the
done screen. Add the same input settle the other steps already use.

Also raise the shared waitForCondition default timeout (2s -> 5s). The predicate
is polled every 10ms and returns as soon as it is met, so this only adds patience
for a slow runner and never slows a passing wait — keeping the Ink-driven flows
deterministic under CI load.

* fix(aimlapi): gate attribution headers by trusted AI/ML API host

The client sent the mandatory source/partner headers on every request, but the
auth/app/pay/inference base URLs are all env-overridable — so a request pointed
at a user proxy (notably the balance probe against an overridden inference URL)
leaked OpenClaude's partner/source identity. Send them only when the resolved
request host is aimlapi.com (production or staging, over HTTPS), mirroring the
inference/catalog stripping contract in resolveAimlapiAttributionHeaders. Adds
an isTrustedAimlapiRequestUrl predicate plus canonical-sends / proxy-withholds
regression tests.

* test(aimlapi): wait for the done screen to settle before the final keystroke

Replace the fixed 25ms delay before the success-screen keystroke with an
observable frame-stability wait, so a slow CI runner cannot drop the keystroke
before Ink has committed the render and attached its input handler.

* fix(aimlapi): durable receipts and atomic election for concurrent top-ups

- Restore the atomic checkout-session election dropped during the passwordless
  convergence: recordAimlapiCheckoutSession is a first-writer-wins CAS, so two
  concurrent runs of the same intent settle on ONE payable checkout — a loser
  adopts the winner's token and abandons the session it just opened instead of
  leaving two chargeable checkouts. Wired through resolveTopupSession (the create
  branch elects then adopts; the resume branch notifies once) and both the CLI
  and GUI onSession callbacks.
- Persist the settled receipt (apiKey / apiKeyId / model / settled) in the GUI
  BEFORE the profile write, so an interrupted or failed write resumes with the
  paid, one-shot exchanged key instead of stranding it (mirrors the CLI).
- Clear the sign-in key cache with the just-minted key id on a sufficient-balance
  sign-in: persistDraft runs onSaved synchronously, so the aimlapiIssuedKeyId
  state setter has not applied yet — pass the id explicitly.

Adds regression tests for the election (first-writer-wins + loser adoption), the
settled-receipt ordering, and the sufficient-balance cache clear.

* fix(aimlapi): abort on a lost election and keep the receipt write best-effort

- Treat a null recordAimlapiCheckoutSession result as "the slot was cleared by a
  sibling that already completed this top-up" and abort, instead of silently
  proceeding to pay a second, unrecorded checkout (both the CLI persistSession
  and the GUI reportSession). Closes the residual double-charge race.
- Make the GUI settled-receipt write best-effort: the payment already cleared, so
  a receipt-write failure (lock contention, full/read-only disk) must not divert
  the flow into the top-up error path — the profile write is what matters.
- Align the recordAimlapiCheckoutSession test double with the real semantics:
  match on intent + payment id only and return null on a non-matching slot.

Adds a regression test that a sibling clearing the checkout mid-flow aborts
before any /pay call. The sufficient-balance sign-in test now waits for the code
screen to settle before typing (the transition dropped the first keystroke).

* test(aimlapi): settle after each awaited frame so keystrokes aren't dropped

The provider-manager GUI tests type on the line after waitForFrameOutput matches
a new screen, but Ink registers input handlers in an effect that runs after the
render commits. On a loaded CI runner the first post-transition keystroke could
be dropped, stranding the flow and timing out (seen intermittently on Node 22).
Add a short settle after every frame match — returning the same matched frame,
so no assertion changes — which lets the input handler attach before the caller
types. Fixes the class instead of patching individual call sites.

* fix(aimlapi): recover a settled GUI receipt and harden checkout/key edge cases

- Recover a settled checkout receipt in the provider-manager GUI before
  provisioning: if a prior run paid + exchanged the key and saved the receipt but
  was interrupted before the profile write, finish that write with the retained
  key instead of re-entering provisioning against the now-exchanged session
  (which fails in resolveTopupSession and strands the paid credential). Mirrors
  the CLI.
- Reject a non-HTTPS checkout payUrl at the client response boundary (the
  validator required only "openable"), so a session is never retained with an
  address the flow refuses later and then polls with no usable link.
- Do not discard a freshly minted sign-in key when its cache write fails: copy
  the key into memory before persisting and make the sign-in-cache / top-up-state
  writes best-effort, in both the GUI and the CLI, so a lock/permission/disk
  failure can't force a second key on retry.
- Narrow the setup guide: the canonical-inference requirement applies to
  new-account onboarding + key provisioning; the existing-key top-up runs against
  the configured endpoint.

Adds regression tests for the settled-receipt recovery (no re-provision) and the
non-HTTPS payUrl rejection.

* fix(aimlapi): HTTPS checkout callbacks, per-email key cache, safer edges

- Require a credential-free HTTPS base for the checkout return URLs (they embed
  the resumable session token) and for the browser return/landing URL, so a
  cleartext AIMLAPI_PAY_URL/AIMLAPI_RETURN_URL override can't leak the token or
  break the documented HTTPS return-target contract.
- Store sign-in recovery keys as an email-keyed collection instead of a single
  global record, so a concurrent/interrupted sign-in for one account no longer
  evicts another's key (which forced a duplicate mint). Old single-record files
  migrate on read; clear stays per-email ownership-aware.
- Treat post-success receipt cleanup as best-effort in both the CLI (finishProfile)
  and the GUI (resetAimlapiCheckoutIntent): the profile is already saved, so a
  lock/permission/IO failure clearing the receipt must not report failure.
- Reject scientific-notation amounts: parseAimlapiAmountUsd now requires a plain
  decimal with at most two fractional digits, closing the "20.001e0" sub-cent
  bypass that silently rounded to a wrong charge.
- Add the pay/verification/return env vars to the config-test snapshot so a set
  override can't pollute default-endpoint assertions.

Adds regression tests for each.

* fix(aimlapi): async checkout-state clear for the Ink flow + reject malformed bases

- Clear the checkout receipt through an async lock in the provider-manager GUI:
  restore withStateLockAsync + clearAimlapiTopupStateAsync and fire it
  best-effort (unawaited) from the save callback, so a contended lock no longer
  blocks Ink input/timers/SIGINT after the profile is already saved. The CLI keeps
  the sync clear (one-shot command).
- Reject a query string or fragment in the checkout/return base URLs: a base like
  https://pay.aimlapi.com/#x would swallow the appended /checkout?...sessionToken
  into the fragment, so the token never reaches the callback as a query param.
- Surface a non-fatal CLI note when receipt cleanup fails (the profile is already
  saved and the stale receipt reconciles on the next run).

Adds regression tests: async clear ownership, query/fragment rejection, and the
legacy single-record sign-in-cache migration.

* fix(aimlapi): reject bare ?/# delimiters in checkout and return base URLs

url.search / url.hash are empty for a bare delimiter (e.g. https://pay.aimlapi.com/?
or .../#), so those slipped past the query/fragment guard and still corrupted the
appended /checkout?...sessionToken=... . Reject any raw ?/# in the candidate in
both safeHttpsBaseUrl and requireHttpsBaseUrl, and cover the bare-delimiter cases.

* fix(aimlapi): harden checkout recovery — exchange lease, retry modes, payable guard

Addresses a fresh review round on the checkout state machine:

- Restore the cross-process exchange lease (dropped in the passwordless
  convergence): the one-shot key exchange is serialized so two processes resuming
  the same paid sign-up session cannot both exchange and strand the credential —
  the lease winner exchanges, peers wait for its settled receipt.
- Persist the exchange mode in the receipt so a retry that has since become
  sign-in still exchanges the paid session instead of minting an unrelated key
  and clearing the paid checkout (CLI + GUI).
- Recover the checkout URL on a pending_payment resume by re-issuing the
  idempotent pay/top-up (the stable paymentSessionId prevents a double charge)
  instead of polling a session the user can never open.
- Route settled-receipt recovery through persistExistingAimlapi for an existing
  saved profile / AIMLAPI_API_KEY top-up, so it updates the selected profile
  (preserveEnv) rather than minting a new one and copying the env key.
- Confirm before abandoning an already-open checkout: editing amount/auto-top-up
  after a checkout URL was opened now requires an explicit re-submit (the old
  browser tab stays chargeable and no endpoint can cancel it).
- Treat a credentials/query/fragment inference base as non-canonical so a
  `.../v1#x` override cannot be written as OPENAI_BASE_URL and break the shim.

Tests: exchange-lease election + failed-exchange release, retry-exchanges-the-
paid-session, idempotent URL recovery on resume, canonical-gate rejection, and
the re-edit confirmation.

* fix(aimlapi): repair exchange-lease liveness and the re-edit abandon guard

- exchange lease: a peer that finds a live foreign lease now re-attempts on each
  poll instead of only watching for a settled receipt, so it resumes the moment
  the holder settles OR frees the lease (failed/crashed) rather than hanging the
  full 20-minute poll window; folds the wait into the lease loop.
- exchange lease: treat a future-dated exchangeLeaseAt (backwards clock jump or
  an edited state file) as stale and reclaim it, instead of reading a negative
  age as perpetually fresh and deadlocking every peer.
- re-edit guard: reset the abandon acknowledgement when a new checkout opens so a
  further edit to a different amount/auto-top-up is confirmed again instead of
  silently abandoning the freshly-opened chargeable tab; clear the opened-checkout
  tracking once payment settles so a later re-edit never warns about a paid tab.
- tests: lease release is owner-scoped and preserves a settled receipt; a
  future-dated lease is reclaimed; the GUI re-edit warning re-arms after a second
  edit.

* test(aimlapi): sync re-edit test on rendered amount; guard vacuous lease seed

- re-edit GUI test: submit only once the edited amount is reflected in the
  rendered frame instead of after a fixed 25ms delay, so Enter is never processed
  against the stale amount on a slow runner.
- future-dated lease test: assert the seed compare-and-swap actually persisted the
  lease before acquiring, so the reclaim path can never pass vacuously.
- exchange lease: record a swallowed release failure via file-backed debug logging
  (safe on the Ink GUI path) so a lock/permission problem behind a slow takeover is
  diagnosable.

* test(aimlapi): match the complete edited amount in the re-edit frame wait

Prefix matching let "$250" match a stray "$2500" (and "$2500" match "$25000"),
so a wrong-amount input regression could pass unnoticed. Pin the complete value
with a negative lookahead on a trailing digit.

* fix(aimlapi): make the one-shot key exchange crash-durable and per-operation

Three checkout-recovery correctness fixes:

- Persist the exchanged key under the CAS BEFORE returning it. The lease winner
  used to hand the /exchange key to the caller, which wrote the receipt only
  afterward; a crash in between left the checkout exchanged but its only key
  unpersisted, so a retry re-ran (and was rejected by) the spent one-shot
  exchange. exchangeKeyWithLease now records the settled receipt via
  recordAimlapiSettledKeyAsync (merges over the record, clears the lease) as soon
  as the exchange succeeds.

- Use a per-operation exchange-lease owner instead of a module-global id. Two
  overlapping top-ups in the same process shared one owner, which the acquire
  treats as self and immediately reclaims, so both could POST the non-idempotent
  /exchange concurrently. A fresh owner per operation makes the second observe
  the first's lease as foreign and back off; a retry within one operation keeps
  its owner and still reclaims the lease it released.

- Never serialize an empty apiKey/apiKeyId. The existing-key top-up path reports
  apiKeyId: '', which the reader rejects, making the whole settled receipt (and
  the paid key it records) unrecoverable. The save path now coerces an empty
  key/id to absent so the receipt stays readable.

* fix(aimlapi): refuse to overwrite an unfinished checkout when the intent changes

claimAimlapiTopupState backs a single slot, so rerunning with a different amount,
auto-top-up, or endpoint used to unconditionally replace the stored record. When
the prior checkout had opened a session (a resume token — possibly already paid
but not yet exchanged) or held a settled key not yet written to a profile, that
dropped the only handle to a paid session/key and stranded it permanently.

claim now refuses a changed intent while such a record exists, with an actionable
message to finish or cancel the earlier top-up first (re-running the same intent
still resumes it). A never-advanced claim — empty resume token, unsettled, no key
— is still replaced. The CLI surfaces the message directly; the interactive flow
already clears the prior record on edit, so normal re-edits are unaffected.

* fix(aimlapi): never settle a keyless receipt; keep the paid key reaching the profile

Addresses a further review batch:

- recordAimlapiSettledKeyAsync now refuses to mark a receipt settled (and clear
  the lease) when no key resolves from the call or the stored record. A keyless
  settled receipt would make a peer resume from a spent one-shot exchange with no
  credential; the record and its lease now survive so a retry can still exchange.

- The CLI's pre-profile settled-receipt save is now best-effort (try/catch + a dim
  note), matching the earlier saves. A lock/permission/IO failure there no longer
  throws before finishProfile, so the paid, exchanged key still reaches the
  provider profile.

- startCreateFromPreset drops aimlapiPersistedIntentRef on a fresh flow entry
  (in-memory only) so a later resetAimlapiCheckoutIntent can never clear a previous
  flow's on-disk receipt against a stale payment id.

- Prompt copy: "Do you have an aimlapi.com key?" / "I already have an aimlapi.com
  key" (missing article).

Tests: keyless settle is rejected and leaves the lease intact; the CLI forwards
explicit --amount/--model; the settled-receipt recovery renders the top-up (not
"ready") done copy.

* test(aimlapi): assert the exchange lease stays held on a keyless settle attempt

Tighten the keyless-settlement guard test: a "not settled" assertion also passes
if the lease were wrongly cleared (a peer would then see 'acquired'). Assert the
peer acquisition returns 'held' so the test pins that a keyless settle preserves
the lease for a retry.

* fix(aimlapi): poll the checkout token, not the auth bearer, while waiting on a resumed exchange

pollUntilExchangeSettled was called with the passwordless-auth bearer
instead of the partner checkout-session token, so it polled the wrong
resource. A terminal error there clears the recovery receipt, stranding
a paid one-shot sign-up exchange.

* fix(aimlapi): keep the checkout receipt resumable through an unconfirmed amount/auto-top-up edit

Editing the amount or auto-top-up cleared the persisted checkout intent
and durable receipt immediately, before the abandon-ack confirmation
that gates actually starting a new payment session. A user who edits
and backs out (or completes the still-open browser checkout) before
confirming lost the only mapping to that chargeable checkout, so a
later run would open a new one instead of resuming the paid session.

The reset now happens only once the user has explicitly confirmed
abandonment: claimAimlapiTopupState takes an `abandonExisting` option
that atomically overwrites the retained record under the same lock
acquisition, instead of racing a separate async clear against a
synchronous claim.

* test(aimlapi): sync on the rendered email before submitting in the new receipt-resume test

A fixed sleep doesn't guarantee the TextInput has processed the typed
email before Enter is sent; a loaded runner can drop the submit. Wait
for the typed value to actually render, matching the amount-edit sync
already used later in this same test.

* docs(aimlapi): describe checkout retention as durable, not session-scoped

The prior wording ("retained while the provider flow remains open")
undersold what topupState.ts actually does: the payment identity and
any issued key are persisted to disk, so a restart resumes the same
checkout too, and a prior paid+exchanged run finishes the profile
write on the next run instead of re-provisioning.

* fix(aimlapi): unify error-status extraction, drop dead top-up state, tighten wrappers

- Extract aimlapiApiErrorStatus as the one place that reads an HTTP
  status off a caught error, structurally (not `instanceof
  AimlapiApiError`) since some callers surface a duck-typed error with
  a bolted-on `status` instead of the real class; use it at both call
  sites that previously duplicated (and disagreed on) this check.
- Remove aimlapiPaymentSessionId/isAimlapiTopupRunning: both were
  write-only state (declared with a blank destructure slot, never
  read), so every setter call scheduled a render for no observable
  effect.
- Switch providerManagerAimlapi.ts's wrappers to `...args` forwarding
  so an implementation gaining a parameter can't silently get it
  dropped by a wrapper that still names the old ones positionally.
- Stop exporting pollUntilPaid from the aimlapi barrel; nothing
  imports it through there (topup.test.ts imports it directly from
  topup.js), so keep it out of the public surface.
- Normalize the email key while rebuilding the sign-in key store's
  collection branch on read, matching the legacy single-record
  migration branch right above it - a hand-edited or older-build file
  with a mixed-case key would otherwise be invisible to
  loadAimlapiSignInKey and mint a duplicate key.

* test(aimlapi): cover resetAimlapiCheckoutSession, by-key top-up args, and error edges

- resetAimlapiCheckoutSession: refreshes the payment session while
  preserving a minted key, and is a no-op when there's no key to
  preserve.
- ProviderManager: a low-balance saved key that gets topped up charges
  the EXISTING key via topUpAimlapiByApiKey (apiKey, non-empty
  paymentSessionId, empty resumeSessionToken) instead of opening a new
  passwordless-account checkout - previously only exercised through
  the default test mock, with no assertion on the call.
- The three negative assertions in the top-up progress-frame check
  tested strings that don't exist anywhere in this GUI (CLI-only or
  pure invention), so they could never fail; add a check against the
  real failure copy so a regression that silently fails at that point
  is actually caught.
- CLI: pin the --no-open default (false) when the flag is absent, and
  cover the non-Error (thrown string) branch of the handler's
  credential-redaction path - both previously only exercised through
  the Error/AimlapiApiError branches.

* fix(aimlapi): close checkout-state concurrency and exchange-lease races

- saveAimlapiTopupState now merges resumeSessionToken like the other
  retained fields instead of spreading the caller's value verbatim. A
  caller saves this record at points where its in-memory copy is still
  empty (right after sign-in, before a checkout session exists); a
  concurrent peer running the same intent can have already elected and
  recorded a real token in that window, and the unconditional spread
  was overwriting it with "", stranding the peer's chargeable checkout.
- The exchange lease is sized for a single POST (EXCHANGE_LEASE_STALE_MS,
  75s) but a resumed wait-exchange holder can sit in a read-only poll
  for up to POLL_TIMEOUT_MS (20 minutes) before ever reaching that POST.
  Without refreshing, a peer would see the lease go stale mid-wait,
  reclaim it, and risk a second concurrent /exchange on the same
  one-shot session. Add refreshAimlapiExchangeLeaseAsync and call it
  every poll iteration.
- When a peer finishes /exchange and records the settled key WHILE this
  process holds the lease and is polling/exchanging, the poll seeing
  the session flip to 'exchanged' threw a hard failure instead of
  resuming from that peer's settled receipt. Re-check for a settled
  receipt before releasing the lease and rethrowing.
- claimAimlapiTopupState's abandonExisting no longer drops an
  already-minted (but not yet paid) existing-account key when
  overwriting a retained checkout for a different amount/auto-top-up -
  it now merges apiKey/apiKeyId/model in, matching
  resetAimlapiCheckoutSession's retain-key pattern. A fully settled
  (paid + exchanged) credential is refused unconditionally regardless
  of abandonExisting, since that confirms giving up an UNPAID checkout,
  never an already-paid one.

* fix(aimlapi): guard GUI checkout abandonment and receipt recovery

- The abandon-ack gate only armed once a checkout URL surfaced
  (aimlapiOpenedCheckoutRef), but resolveTopupSession can already elect
  and persist a resumeSessionToken before that point. Backing out in
  that window then editing the amount hit claimAimlapiTopupState's
  generic refusal instead of the same confirm-to-abandon flow. Extend
  the gate to also cover a persisted (not yet opened) intent.
- Persist an existing-account key minted at sign-in into the top-up
  receipt itself (mirrors the CLI), not just the separate sign-in-key
  cache, so a restart before settlement can resume from one
  self-contained record instead of depending on two files staying
  consistent.
- reportSession's terminal branch (a cancelled/expired/dead session)
  always fully wiped the receipt; mirror the CLI's persistSession,
  which retains an already-minted key (fresh payment session, dead
  token dropped) and only falls back to a full clear when there's no
  key to keep.
- Submitting the email screen unconditionally reset the whole
  onboarding identity, silently abandoning a chargeable checkout on an
  accidental Esc-back-and-resubmit of the same email. Require the same
  explicit confirmation an amount edit does when a resumable checkout
  exists.
- "Set up a new key or switch account" only cleared in-memory fields,
  leaving a durable receipt from an earlier interrupted top-up (this
  mount's refs were never populated for it, since it may be from an
  earlier process) to hit the same refusal on the next onboarding
  attempt with no way to recover short of deleting the file by hand.
  Force the next claim to override it once.
- existingAimlapiCredential() rejected saved-profile discovery whenever
  the AMBIENT AIMLAPI_INFERENCE_URL wasn't canonical, even for a
  profile that was itself saved against the canonical endpoint. Narrow
  the canonical requirement to what it's actually protecting: reading
  the ambient env key, and sending a saved key to a non-canonical
  endpoint (the existing per-profile check).
- The post-signup success screen claimed a magic link was emailed; this
  flow is passwordless email-code sign-in, no magic link is ever sent.
  Point at the dashboard instead.

* docs(aimlapi): note the interactive/CLI auto-top-up default mismatch

The guided GUI flow pre-selects auto-top-up on; the CLI's --auto-top-up
only enrolls when explicitly passed. Left both defaults as-is (auto-top-up
is a real billing behavior, not something to flip unilaterally) and
documented the asymmetry so it's not a surprise either way.

* test(aimlapi): sync on the settled frame before confirming switch-account

A fixed sleep doesn't prove the Select's focus actually moved to the
second option; on a loaded runner the following Enter could land on
"Continue with your saved API key" instead and assert against the
wrong branch. Wait for the frame to stop changing, matching the
settle-poll pattern already used elsewhere in this file.

* fix(aimlapi): stop the exchange poll when a peer reclaims the lease

The periodic lease refresh added to pollUntilExchangeSettled discarded
its result (`.catch(() => false)`), so a peer reclaiming the lease
mid-wait was silently ignored: the poll kept going, returned normally
once the session left 'exchanging', and the caller walked straight
into the non-idempotent /exchange POST with no ownership check of its
own — racing whatever the peer was doing with the same one-shot
session. The comment claiming this was safe ("resolves on this
function's next outer retry") was simply wrong: there is no outer
retry on the success path, control goes directly to the POST.

Distinguish a thrown refresh (transient lock contention — best-effort,
retry next iteration) from an explicit `false` result (the lease is
definitively no longer ours) and bail out on the latter, so the
caller's existing catch block re-checks for the peer's settled
receipt (or fails the run, requiring a re-run) instead of racing it.

* test(aimlapi): require the settle-wait frame to actually differ from before the keypress

waitForCondition polls every 10ms; on a loaded runner two consecutive
polls can both land before Ink has processed the keypress at all, so
the "stable frame" check was satisfied by the unchanged PRE-keypress
frame, sending Enter before focus ever moved to the second option.
Snapshot the frame before the keypress and require the settled frame
to differ from it, not just be internally stable.

* fix(aimlapi): elect the retained key atomically, stop blocking Ink on claim

- Two concurrent sign-ins for the same intent could each mint their own
  existing-account key before either save landed, and saveAimlapiTopupState
  (last-writer-wins) let whichever saved last silently overwrite the
  other's key on disk while both runs kept using their own in-memory
  copy. Elect apiKey/apiKeyId first-writer-wins (same as
  resumeSessionToken already is), re-check the receipt right before
  minting so a losing run adopts the winner's key instead of minting a
  second, and re-check again after a save that lost the election so the
  run's own in-memory key matches what's actually on disk. Apply the
  same first-writer-wins election to the separate GUI sign-in-key cache
  (saveAimlapiSignInKey), which had the identical last-writer-wins gap.
- The GUI called the sync claimAimlapiTopupState directly from an event
  handler; its lock retry blocks the whole event loop (Atomics.wait) for
  up to LOCK_TIMEOUT_MS on contention, freezing Ink rendering, Esc, and
  SIGINT — exactly what resetAimlapiCheckoutSessionAsync already exists
  to avoid for the same reason. Add claimAimlapiTopupStateAsync (sharing
  the same claim logic via an extracted operation function) and switch
  the GUI to it, making startAimlapiTopup async.

recordAimlapiCheckoutSession (the reportSession/onSession path) has the
same sync-lock exposure but is called from a callback whose return value
AimlapiProvisionOptions.onSession drives synchronous control flow in
several places across both the CLI and GUI provisioning paths; making it
async is a larger, riskier contract change deliberately left out of this
pass.

* fix(aimlapi): never pair a new key with a stale or unrelated apiKeyId

saveAimlapiTopupState's apiKeyId fallback still read current.apiKeyId
even when current.apiKey was empty (the id-without-a-key case) or when
state carried a genuinely new apiKey with its own empty-id sentinel,
letting a fresh key get silently tagged with an unrelated leftover id.
Gate apiKeyId on the same winner apiKey came from instead of falling
back to current independently.

* fix(aimlapi): stop cross-account key leaks, lease key-minting, keep GUI CAS async

- claimAimlapiTopupState's abandonExisting carried a retained apiKey into
  ANY differing intent, including a switch from account A to account B
  (the GUI's forceAbandonExisting path). A B-flow restart before the
  profile write could then initialize from the receipt and call the B
  checkout with A's credential — crediting A while B's flow saves A's
  key. Gate the carry-over on the intent's account/key identity
  (`email`) matching, not just abandonExisting.
- The key-choice screen (I am a new user / I already have a key) reset
  the whole onboarding identity unconditionally on either choice, even
  when Esc had backed all the way out from the amount screen past an
  already-opened, still-chargeable checkout. Apply the same
  abandon-confirmation gate startAimlapiEmailOnboarding already uses.
- POST /v1/keys (minting an existing-account key) had no cross-process
  serialization: two concurrent runs for the same intent could each
  observe no retained key and both mint, orphaning whichever key lost
  the first-writer-wins receipt race. Add a key-mint lease (mirroring
  the exchange lease's acquire/release shape) so exactly one process
  ever mints; a peer backs off and adopts the winner's recorded key.
- The interactive flow already claimed asynchronously, but still called
  the synchronous saveAimlapiTopupState and recordAimlapiCheckoutSession
  directly from an event handler and the onSession callback — either
  could block the whole event loop for up to LOCK_TIMEOUT_MS under lock
  contention, freezing rendering, Esc, and SIGINT while a payment
  session is being created. Add async CAS variants and await them; this
  needed widening AimlapiProvisionOptions.onSession to allow returning a
  promise, since its return value drives resolveTopupSession's session
  election.
- An ambient AIMLAPI_API_KEY takes the by-key route with
  aimlapiExistingUsesEnv, so the eventual profile intentionally stays
  keyless. The settled-receipt save before that write unconditionally
  copied the env value into aimlapi-topup.json regardless, expanding a
  secret's on-disk exposure surface for no recovery benefit (a restart
  re-reads the same env var). Keep an env-backed receipt credential-free.

* fix(aimlapi): preserve the key-mint lease across unrelated CAS writes

saveTopupStateOperation and recordCheckoutSessionOperation merged the
exchange lease but not the key-mint lease added in the previous commit:
AimlapiCheckoutState (what every caller spreads checkoutState from)
carries neither lease pair, so an unrelated write - persisting the
exchange flag, or electing a checkout session - silently dropped an
in-flight peer's key-mint lease. A third process would then see the
slot as free and mint its own key, reopening the exact double-mint race
the lease exists to close. Fall back to the current lease the same way
the exchange lease already does.

Also: add a future-dated key-mint lease reclaim test mirroring the
exchange lease's, and align the default saveAimlapiTopupStateAsync test
mock with the real CAS (match on intent + payment id, keep the first
writer's resumeSessionToken/apiKey) so it no longer accepts a write the
real store would reject.

* test(aimlapi): preserve the key-mint/exchange lease in the mocked GUI CAS writes

saveAimlapiTopupStateAsync's and recordAimlapiCheckoutSessionAsync's
default mocks spread { ...state } as their write's base, same gap as
the real saveTopupStateOperation/recordCheckoutSessionOperation had
before the previous commit: neither lease pair survived a write whose
state didn't carry them (which is every real caller, since
AimlapiCheckoutState exposes neither).

Fixing the merge alone wasn't enough — the same two mocks' "does this
write still belong to this slot" check also compared lease fields as
if they were part of the intent identity, so a write that seeded a
lease value failed to match the just-claimed record and silently
no-op'd instead of persisting anything. Exclude both lease pairs from
that comparison too, matching the real matchingStateOrNull (which only
ever compares INTENT_KEYS + paymentSessionId).

* fix(aimlapi): recover ambiguous key-mint/exchange outcomes before releasing leases

createKey and /exchange are both non-idempotent with no server-side retrieval
path, so a lost response after the request actually committed left three
races: a retry could exchange (or mint) a second time and orphan the first
credential, or the CLI's exchange caller would surface a generic network
error instead of the accurate "already exchanged, rotate the key" guidance.

exchangeKeyWithLease now distinguishes a genuinely ambiguous transport
failure of the /exchange POST itself from other doExchange failures (a
pre-POST bail on a reclaimed lease, or a definite rejection): only the
former re-checks the session status directly, surfaces the already-exchanged
error when confirmed, and otherwise leaves the lease held instead of
releasing it into a race. mintExistingAccountKeyWithLease applies the same
ambiguous/definite split before deciding whether to release its lease.

The GUI sign-in flow had an equivalent gap one step earlier: two concurrent
code-verification races could each see an empty key cache and both mint
before either save elected a winner, so the loser never adopted the winner's
key. completeAimlapiCodeSignIn now serializes the cache lookup and mint
behind a new email-scoped lease in topupState.ts, so a losing process waits
and adopts the winner's cached credential instead of minting its own.

* fix(aimlapi): treat caller-aborted mutations as ambiguous and dedupe the transport helpers

client.request rethrows a caller-driven abort as the raw abort error instead
of wrapping it in AimlapiApiError, so the ambiguous-outcome checks added for
createKey and /exchange missed it: cancelling client-side does not stop a
non-idempotent POST from completing server-side, but the lease was still
released as if the request definitely failed, leaving the door open to a
retry racing a second mint/exchange. All three call sites (the checkout-time
key-mint lease, the exchange lease, and the sign-in key-mint lease) now also
hold the lease when the caller's own signal fired.

Extracted the duplicated abortError/sleep/isAmbiguousTransportApiError
helpers shared between topup.ts and onboarding.ts into transport.ts so the
ambiguity rule can't drift between the CLI and GUI paths. Switched the GUI
sign-in flow's cache save to the async, lock-yielding variant and logged its
lease-release failures for parity with the other leases.

* fix(aimlapi): close six checkout-state races found across the claim, lease, and recovery paths

claimAimlapiTopupState's in-progress check only looked at
resumeSessionToken/settled/apiKey, so a receipt claimed just before its
non-idempotent POST (/v1/keys or /exchange) still looked blank and
replaceable to a different intent. A competing claim could overwrite it
mid-flight, leaving the in-flight request's eventual CAS save with no
matching record to land in and orphaning the credential it was about to
mint or exchange. The claim now also refuses (unconditionally, even under
abandonExisting) while either lease is live.

The sign-in key-mint lease's 75s stale window exactly matched createKey's
worst-case duration (60s) plus the async lock's own timeout (15s) for the
cache write that follows, with zero margin for anything else. A legitimately
still-working holder could lose the lease to a peer moments before its
result was cached. It's now refreshed right after createKey succeeds, giving
the cache-write phase its own fresh window.

ProviderManager's code-verification path called the synchronous
saveAimlapiSignInKey, whose lock retry blocks the whole event loop for up to
five seconds on contention — freezing Ink rendering, timers, Esc, and SIGINT
right after a sign-in. Switched to the async variant, exported through
providerManagerAimlapi.ts alongside the other async cache operations.

The three session polling helpers typed onSession as returning void and
never awaited it, even though ProviderManager's callback is async and starts
receipt cleanup before returning. A terminal session (cancelled/expired/
failed, or a dead session) could let the UI reach the amount screen before
the durable receipt was actually reset, so an immediate retry still saw the
stale resume token and got rejected as "not yet abandoned." Both sides now
await through to completion.

The confirmed email-switch flow cleared the in-memory checkout intent and
fired an un-awaited, error-swallowing state clear, but derived its later
claim's abandonExisting only from refs that clear had just wiped — so a
slow or failed clear left the user's explicit confirmation unenforced at the
claim itself. It now sets the same one-shot force-abandon signal the
"switch account" flow already uses for exactly this kind of on-disk,
this-mount-invisible conflict.

A cached sign-in key that the server had revoked was indistinguishable from
one that was merely unreachable: both collapsed into balanceStatus:
'unknown', which re-cached the same dead key and sent the user to manual-key
entry with no way back into the guided flow short of deleting local state.
A definite 401/403 against a cached (not freshly minted) key now invalidates
the stale cache entry and mints one replacement before falling back to the
generic unknown-balance path; every other (ambiguous) failure still leaves
the cache untouched.

Extracted a shared claim/lease-liveness helper in topupState.ts and added
regression coverage for each race — including two that hold a mocked
createKey/reset call open to prove the competing operation actually waits
instead of just asserting on the end state.

* fix(aimlapi): clear the stale force-abandon flag on a fresh preset entry

aimlapiForceAbandonExistingRef is armed when the user confirms abandoning a
checkout during an email switch, then consumed by the next claim. If that
claim never runs — the switch's own onboarding fails and the user backs all
the way out to preset selection instead of retrying — the flag stayed armed.
Re-entering the aimlapi preset with an unrelated email then passed
abandonExisting: true on its first claim with no confirmation for that flow,
silently overwriting whatever unpaid checkout was still on disk.
startCreateFromPreset now resets the flag alongside the other per-flow refs
it already clears on fresh entry.

Also swapped a fixed 20ms sleep in the cross-intent concurrency test for a
signal fired from the held-open /v1/keys handler, so the test can't flake
under CI load waiting for the run to reach the point it needs to race.

* fix(aimlapi): close the remaining confirmation, cleanup, and lease gaps in checkout state

The API-key-choice screen's own confirm-abandon gate (Enter twice to accept
"a checkout from this account is still pending") reset the onboarding
identity but never armed the force-abandon signal the email-switch and
switch-account flows already use. A contended or failed pre-clear left the
next claim to hit the CAS's unconfirmed-conflict refusal despite the user
having just confirmed abandonment through this exact screen.

reportSession('')'s terminal-session handler discarded the persisted intent
ref before its reset/clear attempt settled, and swallowed any failure as
success. A lock timeout or I/O error then left the durable receipt exactly
as it was, but with no ownership left in memory to retry cleanup or to route
a later conflicting claim through the normal confirmation gate — the CAS
just rejected it outright. Ownership now only drops once the transition
actually commits; a failure is logged and the ref stays populated so the
existing gate covers the next claim.

The sign-in key-mint lease's stale window already had zero margin for its
own refresh call's lock wait (up to 15s) on top of createKey's own worst
case (60s) and the cache save's lock wait (another 15s) — 90s with nothing
left over. Widened it to 150s and lengthened the losing side's patience to
match, and stopped silently ignoring a refresh that reports lost ownership:
it's now logged for diagnosability even though the save itself stays safe
to attempt regardless (first-writer-wins makes a losing write a no-op).

Both onSaved completion paths (persistExistingAimlapi and persistAimlapiKey)
called the synchronous clearAimlapiSignInKey from Ink's synchronous save
callback, whose lock retry blocks the event loop for up to five seconds on
contention — freezing rendering, timers, Esc, and SIGINT right at
completion, the same class of bug already fixed for the sign-in save path.
Re-exported the async variant through providerManagerAimlapi.ts and switched
both call sites to fire-and-forget it instead.

* fix(aimlapi): stop the flow instead of risking a stranded key on a receipt-write failure

/exchange (and the by-key top-up) is a one-shot operation: once it succeeds,
the issued key exists only in memory until a durable copy lands somewhere.
Both the CLI and the GUI wrote the local recovery receipt right after that,
but treated a failure there as best-effort and proceeded straight into the
provider-profile write regardless. If the receipt write failed and the
profile write then also failed — or the process was interrupted between the
two — the key was gone: nothing durable ever recorded it, and a retry can't
re-exchange an already-spent session to get it back.

Both paths now treat the receipt as a required checkpoint rather than an
optional resume aid: a failure here stops the flow with a clear error
pointing at the one real recovery path (rotating the key from the aimlapi.com
dashboard) instead of silently continuing. This shouldn't cost much in
practice — the underlying CAS write already retries substantially on lock
contention before giving up, so a failure this deep signals a real problem
rather than a transient blip a fallback write would likely have hit too.

Added failure-injection coverage for both paths: the CLI test breaks the
config directory (a file where a directory is expected) right as the
exchange response lands, so the post-exchange save fails deterministically
without relying on OS-specific permission semantics; the GUI test mocks the
save to reject directly and asserts the profile write is never reached.

* test(aimlapi): strengthen receipt-write-failure coverage and document the recovery path

Both the CLI and interactive-flow tests for the post-exchange receipt-write
failure only asserted the generic error text, which would still pass if the
issued key id — the actual recovery handle the error exists to surface — got
dropped from the message later. Both now assert the id appears too. The
interactive test also confirms the screen stays usable after the error: a
retry reaches the amount-submission path again instead of the flow being
stuck.

Documented the resulting behavior in the setup guide: since the key exchange
is one-shot, a receipt-write failure after a successful payment now stops
both flows with an error naming the issued key, rather than continuing
silently — recovery is manual, via rotating that key on the aimlapi.com
dashboard.

* test(aimlapi): move to end-of-line before clearing the email field after Esc

Rebasing onto current main picked up the input layer's DEL-coalescing fix,
which now correctly respects the cursor position for a backspace run instead
of dropping it. These three tests backspaced assuming the cursor sat at the
end of the retained email text, but cursorOffset is a single state shared
across every screen's text field and was last set for the amount screen (its
default "25" is 2 chars) — going back via Esc never resets it, so the cursor
was actually stuck mid-string. Sending an explicit end-of-line sequence
before the backspaces makes the clear correct regardless of where the stale
cursor was left.

* fix(aimlapi): close six checkout/onboarding gaps from the latest review pass

Treats a malformed-but-2xx key-mint response as ambiguous (not proof of
failure) so an unusable receipt no longer releases the mint lease and risks
an orphaned credential; fences startAimlapiTopup's cancellation to an
epoch created before the state-lock await so Esc/unmount during that wait
can no longer barge back in; makes the checkout-receipt read fail closed on
a permission/IO/parse/schema failure instead of silently claiming over it;
completes (or reconciles) a settled by-key receipt instead of stranding it
at the post-payment model picker; retires a sign-in mint lease together
with its cache entry so it can't resurface as held once the cache is later
cleared; and adds a --code-stdin path plus a deprecation warning so the
passwordless code no longer has to travel through shell history or argv.

* fix(aimlapi): reconcile env-credential receipts and stop endorsing AIMLAPI_CODE as safe

reconcileSettledAimlapiTopupStateAsync matched a stale settled receipt by
its stored apiKey, but an env-sourced credential's receipt never persists
one, so that path stayed permanently stranded; it now matches on the
absence of a stored key when the caller is reusing an env credential.
Also stops recommending AIMLAPI_CODE as an equivalently safe alternative
to the deprecated --code flag, since typing it inline still lands in
shell history, and adds a lock-release assertion to the fail-closed
receipt-read tests.

* fix(aimlapi): await stale-receipt reconciliation and stop overclaiming --code-stdin's history safety

Reconciling a stale settled receipt matches by the by-key credential's
apiKey, which a genuinely new top-up for that same credential can also
produce — firing the reconcile call without waiting for it left a window
where a fresh settlement landing in that gap could be swept up by it.
Await it before moving on so the two can no longer interleave.

Also narrows the --code-stdin messaging: it only guarantees the code stays
out of this process's argv/`ps` output, not shell history in general,
since that still depends on how the caller feeds stdin.

* fix(aimlapi): keep the configured screen locked until reconciliation finishes

Clearing isAimlapiKeyValidating before the reconcile await let the
aimlapi-configured screen's Select (and its Esc binding) become
interactive while that reconcile was still running in the background —
it carries no abort signal of its own, so aborting the surrounding
controller only stops this flow from acting on the result, not the
reconcile itself. That left a window where the user could start a
competing top-up for the same credential and have its fresh settlement
caught by the still-in-flight reconcile. Both now stay gated on
isAimlapiKeyValidating through the whole wait.

* fix(aimlapi): fail closed on the sign-in cache, bind env receipts by identity, and commit minted keys

Makes the sign-in key cache and its mint lease match the checkout
receipt's fail-closed contract: only ENOENT means no record, so a
permission/IO/parse failure can no longer be mistaken for "nothing
cached, no lease held" and authorize a second createKey call or a
concurrent lease acquisition.

Reworks reconcileSettledAimlapiTopupStateAsync to match on the by-key
checkout intent's non-secret key fingerprint (already carried in the
persisted email field) instead of the raw stored apiKey or its mere
absence — an env-backed receipt never stores its key, so absence alone
couldn't tell two different env credentials' receipts apart, letting one
credential's balance check discard another's still-unrecovered payment.

Treats persisting a freshly minted existing-account key as a commit
point in the CLI's mint-with-lease path: a write failure now stops the
flow with a recovery-oriented error and leaves the lease held, instead
of continuing with the key only in memory where an interruption before
the later checkout/profile save would orphan it once the lease goes
stale.

Adds a shared isValidAimlapiSignInCode check so both the CLI and the
interactive flow reject a malformed passwordless code (empty,
non-numeric, wrong length) before it ever reaches verifySignInCode.

* fix(aimlapi): reject an array-shaped sign-in cache/lease file instead of degrading to empty

typeof [] === 'object' and [] !== null, so readJsonObjectFile's shape
check let a JSON array through as if it were a valid store. Both readers
then found no matching entries and returned {}, exactly the "no cached
key, no live lease" outcome the fail-closed contract exists to prevent —
authorizing a second createKey call or a lease acquisition over a
possibly-live one.

* fix(aimlapi): commit the sign-in key as a checkpoint and retire a completed mint's lease

mintOrAdoptSignInKey swallowed a failed cache commit and returned the
minted key only in memory — the same non-idempotent-mutation gap already
closed for the CLI's mintExistingAccountKeyWithLease. A commit failure
now stops the flow with a recovery-oriented error and leaves the lease
held, instead of risking the key becoming unrecoverable once the lease
ages out and a retry mints a second one.

Also retires the checkout key-mint lease in the same save that elects a
freshly minted key, mirroring how the exchange lease is already cleared
on settle. Without it, backing out of an unpaid checkout and confirming
a different amount right away still hit the "minting or exchanging"
refusal for the full 75s stale window even though the mint had already
completed.

* fix(aimlapi): make key-mint lease retirement owner-checked, preserve error causes

Retiring the checkout key-mint lease on a successful mint (the previous
commit's fix) cleared it unconditionally, with no check that the save
still belonged to the owner that acquired it. createKey has no refresh
mechanism, so a slow response can let the lease go stale and be
reclaimed by a peer before the original owner's save lands — clearing
the lease then would drop that peer's still-live one and let a
differently-amounted claim proceed as though minting were done while
the peer's mint was still genuinely in flight.

Replaces the raw saveAimlapiTopupState call in
mintExistingAccountKeyWithLease with a dedicated
recordAimlapiMintedKeyAsync that takes the acquiring owner and only
retires the lease while it's still theirs — mirroring how
recordAimlapiSettledKeyAsync already handles the exchange lease.

Also adds `cause` to the three recovery-oriented errors thrown on a
receipt-write failure, so the underlying persistence error stays
diagnosable instead of being replaced by the wrapper message alone.

* test(aimlapi): assert a stale owner's minted key is still persisted

The reclaimed-peer-lease regression test only pinned the lease
bookkeeping, so a variant regression that skipped the write entirely for
a non-owning caller (e.g. an early return on lease-owner mismatch) would
still pass while silently discarding a real, non-idempotently minted
key. Asserts the receipt retains it regardless of who currently holds
the lease.

* fix(aimlapi): persist and charge the endpoint a manually-entered key was actually validated against

persistExistingAimlapi's no-existing-profile fallback saved the profile
with resolveEndpoints().inferenceBaseUrl (the current ambient endpoint)
instead of aimlapiInferenceBaseUrl (the endpoint this flow actually
validated the key and will charge against). The two diverge for a
manually-entered key: after "Set up a new key or switch account" resets
aimlapiInferenceBaseUrl to the ambient default, draft.baseUrl (what
validateAndPersistAimlapiKey actually calls the balance/top-up endpoints
with) keeps the OLD profile's endpoint if it differs (e.g. a canonical
saved profile while AIMLAPI_INFERENCE_URL currently points at a proxy).

Now captures the validated endpoint into aimlapiInferenceBaseUrl as soon
as the low-balance branch is reached, and the fallback save uses that
state instead of re-resolving the ambient endpoint — so the top-up
charge and the persisted profile both follow the endpoint that was
actually validated.

* fix(aimlapi): reject stale key-mint results, make the exchange checkpoint mandatory, validate lease pairs

recordAimlapiMintedKeyAsync previously let a stale owner's delayed
createKey result land beside a peer's reclaimed, still-live lease: since
no key was recorded yet, first-writer-wins accepted the stale result
outright, so the reclaiming peer's own (equally real, non-idempotent)
mint got silently discarded once its own save landed — turning one lost
credential into two. It now rejects a result whose ownership was already
lost when nothing is recorded yet to adopt instead, surfacing a
recovery-oriented error naming the issued key id rather than risking a
second orphan. The caller now also returns whichever credential is
actually durably recorded, not always its own.

exchangeKeyWithLease's own settled-receipt commit — the only durable
record of a one-shot /exchange result until the caller's later,
separate save runs — was only logged on failure. A crash in that
window left the paid session exchanged with its key absent from local
recovery state, so a retry could only report the session was already
exchanged with no way to recover automatically. The commit is now a
required checkpoint: its failure stops the flow with the existing
recovery guidance and leaves the exchange lease held, exactly as the
analogous key-mint checkpoint already does.

The receipt schema validated the exchange lease pair but not the newer
key-mint one, and even the exchange check only verified each field in
isolation — a one-sided pair (an owner with no timestamp, or vice versa)
passed either way. A shared validator now enforces both lease pairs are
either fully present or fully absent, so a malformed or partial lease
can no longer be silently accepted as "not currently live" and let a
claim replace it out from under an in-flight mint or exchange.

* fix(aimlapi): fail the exchange when the settled-receipt commit no-ops, not just when it throws

recordAimlapiSettledKeyAsync silently returned without writing whenever the
CAS no longer matched (checkout cleared/reset mid-flight) or no credential
could be resolved to settle with. exchangeKeyWithLease only caught thrown
errors, so both no-op paths let a successfully exchanged key return with no
durable local receipt. The function now returns a boolean, and the caller
treats false exactly like a thrown save error.

Also fixes recordAimlapiMintedKeyAsync returning the raw untrimmed apiKeyId
instead of the trimmed value it actually persisted.

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-08-14 10:12:13 +08:00
575b407275 feat(partners): add ApiSmart, refresh Novita AI logo (#2121)
- Add ApiSmart (https://www.apismart.ai) to the README partners table
  and the web partner strip, with a dark-theme logo variant (near-black
  wordmark recolored to white, white matte removed).
- Replace the Novita AI PNG logo with the new SVG wordmark plus a
  generated dark variant, wired through the same prefers-color-scheme
  <picture> pattern (README) and logoDark field (web).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-13 11:11:43 +08:00
95409464f3 feat(codex): move codexplan default to GPT-5.6 Sol (#2051)
* feat(codex): default codexplan to GPT-5.6 Sol

Preserve existing reasoning and routing behavior while updating the default model, labels, documentation, and focused coverage.

* test(codex): lock fallback and routing behavior

* make credential-step test self-contained

* fix(codex): default unset teammate fallback to GPT-5.6 Sol

The changes to the inert codex config keys are known to have no functional effect, but we updated them to ensure the defaults are correct and consistent across the table.

* fix codexplan gateway defaults after model resolution

* Revert "fix codexplan gateway defaults after model resolution"

This reverts commit 2b5ae791f8.

* fix codexplan custom gateway reasoning default

* fix codexplan reasoning query request routing

* fix(test): unmount CodexCredentialStep render instance before cleanup

---------

Co-authored-by: jatmn <the@jat.mn>
2026-08-10 10:09:48 +08:00
JATMNandGitHub b0cbfe1100 fix(repl): make local interactive max-turns configurable (#2086)
* fix(repl): make interactive max-turns configurable

Wire --max-turns into interactive sessionConfig and honor
OPENCLAUDE_MAX_TURNS / CLAUDE_CODE_MAX_TURNS so long autonomous REPL
sessions can raise the default 50-turn per-prompt cap (fixes #2079).

* fix(repl): forward --max-turns on connect/ssh/remote launches

sessionConfig covered the normal interactive paths; connect, SSH,
assistant, and --remote built REPL props without spreading it, so the
CLI override was dropped despite help advertising interactive support.

* fix(repl): scope interactive max-turns to local query loops

Remote-backed sessions bypass local query(), so forwarding --max-turns
into those REPL props over-claimed enforcement. Clarify help/docs and
match OPENCLAUDE_MAX_RETRIES precedence when OPENCLAUDE_MAX_TURNS is set
but invalid.

* feat(config): add interactive max turns under /config

Expose replMaxTurns in the Config panel (50/100/200/500) and resolve it
after CLI/env so local interactive sessions can raise the per-prompt
cap without restarting. Resolve at query time so mid-session /config
changes apply on the next prompt.

* docs(repl): clarify invalid OPENCLAUDE_MAX_TURNS precedence

Match the OPENCLAUDE_MAX_RETRIES contract: a set-but-invalid primary
env var uses the default and does not fall through to legacy or /config.

* fix(repl): address PR review on max-turns help and web version gate

Share the --max-turns Commander description via an imported constant so
help and tests stay in sync without breaking the CLI bundle, replace
source-only help assertions with Commander behavior coverage, and add
the published 0.27.0 entry so web verify-dist passes.

* fix(repl): typecheck Commander maxTurns opts and warn on invalid env

Avoid TS2339 on untyped Commander opts, log invalid OPENCLAUDE_MAX_TURNS
like MAX_RETRIES, and clarify that /config shows the persisted preference.

* fix(repl): warn when max turns is unlimited

* fix(repl): scope unlimited-turn warning locally

* fix(repl): preserve interactive turn caps across backgrounding

* fix(repl): preserve turn caps when backgrounding

* fix(repl): share turn budget across background handoff

* fix(repl): reserve turns at provider dispatch

* fix(repl): snapshot background handoff transcript

* fix(tasks): avoid phantom background session task

* fix(repl): preserve handoff lifecycle state

* fix(repl): own pending background handoffs

* test(tasks): isolate background session task storage

* fix(repl): refresh background task title and test cleanup

* test(repl): cover max-turn CLI dispatch paths

* test(queue): cover prepend notification and priority

* fix(repl): skip background handoff after foreground query throws

Rebased onto main and gate Ctrl+B continuation on !didThrow so a faulted
foreground turn cannot start a background session from partial state.

* fix(repl): address PR review findings on notifications and handoff

Dedupe background task notifications by embedded task id, gate background
continuation on preflight veto, scope queue removal to main-thread notifications,
and forward maxTurns through all launchRepl entry points.

* fix(repl): resolve latest CodeRabbit inline review findings

Dedupe claimed notification batches by task id, restore notifications on
pre-registration abort, tighten test isolation, and replace remaining brittle
source-text assertions with behavioral coverage.

* fix(repl): keep notification restore active until provider dispatch

Stop clearing notification ownership when preparation succeeds so pre-dispatch
aborts can restore claimed queue items, and commit ownership once the provider
starts. Add regression coverage for the abort path and headless max-turns zero.

* test(repl): cover post-dispatch ownership and headless max-turns 0

Add regression tests for notification restore after provider dispatch commits
ownership, and assert headless --max-turns 0 reaches query() without interactive
resolution stripping the value.

* fix(repl): restore only embeddable notifications on Ctrl+B handoff abort

Track the deduped successor subset when restoring claimed main-thread task
notifications so items already in the settled foreground transcript are not
re-queued. Clarify that agent-scoped notifications intentionally stay on their
owner drain path (issue #2079 scope is interactive turn caps only).

* fix(repl): address review findings on background handoff

Commit notification ownership when background sessions complete without
provider dispatch, forward all task notifications on Ctrl+B again, and
restore deferred max-turn cap attachments when continuation is cancelled.

* fix(repl): guard deferred cap restore and skip remote turn limits

Anchor deferred max-turn restoration to the handed-off transcript tail so a
cancelled Ctrl+B handoff cannot attach the prior prompt's cap to a newer turn.
Apply the interactive turn cap only in local sessions and align remote-session
docs/help wording.

* fix(repl): use messagesRef for deferred cap transcript anchor

persistentMessages is block-scoped inside onQuery try; read the settled
tail from messagesRef in finally so typecheck passes.

* test: harden context fallback warning assertion after max-turns tests

Scope the unknown-model context test to [context] warnings only so unrelated
import-time debug logs do not fail CI, and clear turn env vars in both that
test and replMaxTurnsProp setup to avoid cross-file pollution.

* test: address PR review findings on headless max-turns boundary

Add a runHeadless-to-ask regression that asserts maxTurns 0 is forwarded
through the headless print path, and restore OPENCLAUDE_MAX_TURNS env vars
in context.test.ts after the unknown-model fallback test mutates them.

* test: tidy headless max-turns boundary test and env isolation

Mock headless stdout so runHeadless completes cleanly without leaking
output, restore spies in finally, and centralize turn-env cleanup in
context.test beforeEach.

* fix: address PR review findings for max-turns background handoff

Separate model-request lifecycle from provider dispatch acceptance so
interruption correction arms before async prep, notification ownership
commits only after dispatch, deferred turn caps restore on every abort
path, and foreground work stays blocked while handoff preparation runs.
2026-08-06 20:24:34 +08:00
0xfandomandGitHub 3925f2791c feat(auth): opt-in loopback proxy hosts that keep subscription (OAuth) auth (#2050)
* feat(auth): opt-in loopback proxy hosts that keep OAuth first-party

Pointing ANTHROPIC_BASE_URL at any host other than api.anthropic.com switches
the client to API-key mode, dropping a signed-in subscription session. That
blocks running the CLI through a local transparent proxy (compression,
inspection, caching) that forwards auth headers to Anthropic unchanged.

Add ANTHROPIC_FIRST_PARTY_PROXY_HOSTS: a comma-separated host[:port] allowlist
that extends first-party detection. It is honored only when the base URL points
at a loopback host, and only loopback entries are considered -- both checks are
redundant by design so a misconfigured non-loopback entry can never widen
first-party status to an off-machine host. Default behavior is unchanged.

Closes #2016

* docs(auth): document ANTHROPIC_FIRST_PARTY_PROXY_HOSTS

* fix(auth): harden loopback proxy allowlist matching

Normalize the base URL port to its scheme default (80/443) before
comparing an explicit allowlist port, so a `127.0.0.1:80` entry matches
`http://127.0.0.1`. Reject embedded credentials and non-http(s) schemes
up front so an OAuth session is never attached to a URL carrying userinfo
or a non-proxy scheme.
2026-07-28 22:39:41 +08:00
158bdd0dcf docs(readme): rename Sponsors to Partners, add AI/ML API and Novita AI, new wordmark (#2054)
- Rename the Sponsors section and nav link to Partners
- Add AI/ML API and Novita AI to the partners table with local logo
  assets; AI/ML API ships light/dark SVG variants behind a <picture>
  element so the wordmark stays readable on both GitHub themes
- Replace the green SVG header wordmark with the orange pixel-art
  OPENCLAUDE wordmark PNG

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 13:31:10 +08:00
JATMNandGitHub e3fb051775 feat(kimi): add Kimi K3 context variants (#1989)
* feat(kimi): add K3 context variants

* fix(moonshot): default K3 to 1M

* fix(kimi): restore K3 model name

* fix(kimi): preserve K3 route metadata

* fix(kimi): recommend the selected context variant

* fix(kimi): preserve max shim reasoning effort

* fix(kimi): validate K3 reasoning levels

* fix(kimi): reconcile K3 route metadata

* test(integrations): accept catalog default ids

* fix(kimi): preserve K3 runtime limits and effort aliases

* fix(kimi): preserve override context limits

* fix(kimi): canonicalize K3 reasoning to max

* fix(moonshot): preserve K3 query reasoning

* fix(effort): scope K3 xhigh normalization

* fix(kimi): scope max-only override effort

* docs(kimi): qualify HighSpeed plan claim

* fix(kimi): preserve documented K3 effort levels

* fix(kimi): preserve disabled thinking for K3

* fix(kimi): avoid disabled-thinking fallback

* fix(compact): honor route runtime output limits

* fix(rebase): preserve transport compression routing

* fix(kimi): preserve route defaults and runtime controls

* fix(kimi): retain selected runtime limits for overrides
2026-07-20 22:25:17 +08:00
7674d4d73e feat(aimlapi): provider foundation (1/5) — config, catalog, ambient-key gate (#1995)
* feat(aimlapi): provider foundation — config, catalog, ambient-key gate

First layer of the AI/ML API onboarding split. Self-contained: config
endpoints + partner-header resolution, gateway catalog entry, runtime
metadata and artifact-generator wiring, and the regenerated integration
manifest.

Also lands the P1 security fix in providerProfiles: an ambient AIMLAPI
key is only forwarded when the profile targets the canonical inference
endpoint, so a proxy/staging profile can never leak the key elsewhere.

No client/checkout/UI changes here — those stack in later PRs.

* fix(aimlapi): harden credential gating and restore renamed-preset test path

* fix(aimlapi): harden credential gating and finish renamed-preset path

* fix(aimlapi): finish aimlapi.com rename in provider UI; gate generic proxy credential

* fix(aimlapi): close remaining proxy credential and attribution leaks

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): stop forcing profile credentials onto retargeted endpoints

* fix(aimlapi): withhold ambient custom headers from proxy launches

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-20 10:02:00 +08:00
3808d19da4 fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers (#1940)
* fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers

* test(api): cover Copilot responses fallback deadlines

* fix(api): redact secrets in timeout URL paths

* fix(api): harden Copilot response deadlines

* fix(api): prevent header-timeout request replay

* fix(api): harden timeout cleanup and redaction

* fix(api): redact encoded transport credentials

* fix(api): harden deadline retries and URL redaction

* fix(api): preserve aborted fetch reasons

* fix(api): preserve caller abort reasons

* test(api): clear caller abort timer

* docs(api): clarify API_TIMEOUT_MS transport scope

* docs(api): explain timeout env loading

* fix(api): reset deadline for proxy retries

* fix(api): type deadline fetch adapter

* fix(api): honor abort cleanup and request signals

* fix(api): do not block proxy retries on body cancellation

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-19 09:21:14 +08:00
JATMNandGitHub 46e80568be fix(provider): support custom Anthropic bearer auth (#1929)
* fix(provider): support custom Anthropic bearer auth

* feat(provider): add custom Anthropic profile flow

* fix(provider): restore custom Anthropic tokens on startup

* fix(provider): preserve custom Anthropic env setup

* fix(provider): clear stale custom Anthropic tokens

* fix(provider): preserve custom Anthropic API-key env setup

* fix(provider): cover custom Anthropic auth routing

* test(api): isolate custom Anthropic client routing

* test(api): cache-bust client provider imports

* feat(provider): clarify custom provider presets

* fix(provider): address custom Anthropic review feedback

* fix(provider): preserve custom Anthropic headers

* fix(provider): require custom Anthropic token

* fix(provider): isolate custom Anthropic credentials

* fix(provider): guard custom Anthropic setup

* fix(provider): complete custom Anthropic integration

* fix(provider): classify custom Anthropic proxies

* fix(provider): gate proxy cache extensions

* fix(provider): preserve custom Anthropic isolation

* fix(provider): retain direct proxy model option

* fix(provider): honor custom endpoint boundaries

* fix(provider): keep proxy credentials local

* fix(provider): disable proxy fast mode

* fix(provider): preserve first-party route identity

* fix(provider): isolate custom Anthropic endpoints

* fix(provider): gate remaining first-party features

* fix(provider): isolate custom Anthropic proxy features

* test(web-search): make Brave timeout mock abort-aware

* fix(provider): address custom Anthropic review feedback

* test(provider): cover first-party beta gates

* fix(provider): complete custom Anthropic isolation

* fix(provider): complete custom Anthropic routing

* fix(provider): address custom Anthropic review followups

* fix(provider): close custom Anthropic review gaps

* test(provider): keep custom Anthropic mock helpers isolated

* test(provider): isolate model options gateway mocks

* fix(provider): stabilize custom Anthropic model option display

* fix(provider): address remaining review threads

* fix(provider): synchronize active profile persistence

* fix(provider): preserve custom Anthropic API key auth

* fix(provider): avoid forwarding inherited Anthropic keys

* fix(provider): guard custom auth selection

* fix(provider): require first-party Anthropic port

* test(web-search): avoid duplicate shared lock

* fix(provider): resolve remaining review findings

* fix(provider): harden custom Anthropic routing

* fix(provider): simplify Anthropic thinking gate

* fix(provider): preserve custom proxy routing and secret permissions

* fix(mcp): isolate Claude.ai config cache by provider

* fix(model): keep custom endpoints out of first-party UX

* fix(provider): scope Opus off switch to Anthropic

* fix(provider): disable tool search for custom proxies

* fix(provider): close custom Anthropic review gaps

* fix(provider): reject Anthropic staging custom profiles

* fix(webfetch): classify custom Anthropic endpoints

* fix(provider): block bearer auth at Anthropic origin

* fix(provider): keep custom auth off staging OAuth

* test(provider): strengthen auth regression coverage
2026-07-16 08:14:22 +08:00
a32781537f fix(query): bound per-turn latency growth in long REPL sessions (#1949) (#1952)
* fix(query): bound per-turn latency growth in long REPL sessions (#1949)

Addresses the progressive latency regression where consecutive prompts in a
single session grow non-linearly (2nd prompt ~10s, 3rd 10+ min) due to
unbounded message accumulation with no proactive compaction and no per-prompt
turn cap on the main thread.

- Cap the interactive REPL main thread at 50 turns per prompt (DEFAULT_REPL_MAX_TURNS).
  Headless/print mode and the SDK are unchanged (--max-turns flag / SDK callers
  still control it), preserving the SDK API contract.
- Default maxMessagesCompactionThreshold to '200' so message-count compaction
  runs well before the context window fills, instead of 'off'.
- Lower the auto-compact threshold buffer from 13k -> 30k so compaction fires
  earlier with less accumulated history. The effective-context floor buffer is
  kept at 13k and getAutoCompactThreshold() falls back to it for small-context
  models, so the threshold can never go negative (no #635 regression).

Test updates: isolate the hard-cap override test from the new 200-message
default, and correct an outdated constant reference in the autoCompact test.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(query): repair REPL latency guard

* fix(query): cover resume and default guard paths

* fix(query): enforce cap across interactive paths

* docs(compaction): clarify disabled message limits

* fix(query): retain explicit message thresholds

* fix(query): enforce explicit threshold recovery

* fix(query): honor legacy active-message limit

* fix(doctor): report effective message compaction limit

* fix(config): share message threshold validation

* test(doctor): cover disabled message compaction

* fix(compact): preserve latency guard coverage

* test(repl): exercise turn cap defaults

* fix(compact): honor disabled default message guard

* fix(swarm): honor disabled auto compaction

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 23:06:53 +08:00
14648213a6 Chore/readme cleanup (#1976)
* docs: README cleanup, green wordmark header, Trendshift badges

Header: the startup wordmark (src/constants/brand.ts half-block art)
rendered as a green two-shade SVG (docs/assets/openclaude-wordmark.svg,
textLength-pinned so rows align in any monospace font), with the three
Trendshift badges (daily/monthly/repository) centered beneath it.

Cleanup (536 -> ~430 lines, nothing lost):
- Agent routing, maxSteps limits, and GitHub Copilot sub-agent tuning
  moved to docs/agent-routing.md; headless gRPC server moved to
  docs/grpc-server.md; README keeps linked summaries.
- Build/test/validation commands were repeated in three sections —
  consolidated into one Development section; Contributing links to it.
- New "Meet Your Buddy" section documenting the companion heroes and
  their /buddy commands; added to What Works and Why OpenClaude.
- Star History moved from the header flow down beside Community.
- Setup Guides indexes the new docs pages; fixed a missing blank line
  before Repository Structure and a curly quote.

All relative links, image paths, and internal anchors validated.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* docs: pure-rect wordmark for crisp rendering; drop broken Star History

The wordmark SVG previously drew the half-block art as monospace <text>,
which rendered raggedly (font-dependent glyph stretching and seams).
Regenerated as pure SVG rects computed from the brand.ts wordmark grid —
no font dependence, pixel-crisp at any size, same two-shade green split.

Star History chart removed: the badge endpoint errors and displays a
broken image.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* docs: render the wordmark at full README column width

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-15 22:27:50 +08:00
0xfandomandGitHub eeed68f4fd feat(provider): add Cloudflare Workers AI integration (#1100) (#1178)
* feat(provider): add Cloudflare Workers AI integration

Adds Cloudflare Workers AI as a first-class OpenAI-compatible provider
preset, modeled on the Venice / Xiaomi MiMo descriptors.

- New `src/integrations/vendors/cloudflare.ts` descriptor:
  - `classification: 'openai-compatible'`
  - Default base URL with literal `<ACCOUNT_ID>` placeholder — users
    substitute via `/provider` baseUrl edit, same shape as the Azure
    OpenAI example already in `docs/advanced-setup.md`
  - `CLOUDFLARE_API_TOKEN` env, with `OPENAI_API_KEY` as fallback
  - `removeBodyFields: ['store']` since Workers AI rejects unknown
    OpenAI body fields (mirrors Mistral / Gemini / Cerebras strip)
  - Static catalog with current Workers AI chat models
    (`@cf/meta/llama-3.3-70b-instruct-fp8-fast`,
    `@cf/meta/llama-3.1-8b-instruct`,
    `@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,
    `@cf/qwen/qwen2.5-coder-32b-instruct`)
  - Validation routing on `api.cloudflare.com` /
    `gateway.ai.cloudflare.com` hosts so an env-pasted URL maps back
    to the preset
- Env mirror sites in `src/utils/providerProfiles.ts`: mirror api key
  into `CLOUDFLARE_API_TOKEN` when baseUrl contains a Cloudflare host
  (3 sites: same-env check, openAIProfileEnv build, applyEnv).
- `CLOUDFLARE_API_TOKEN` added to `PROFILE_ENV_KEYS` / `SECRET_ENV_KEYS` /
  `ProfileEnv` / `SecretValueSource` in `src/utils/providerProfile.ts`
  so the profile-clean and secret-redact paths know about it.
- `src/utils/providerFlag.ts` `--provider <name>` startup flag now
  detects a Cloudflare profile from `OPENAI_API_KEY ===
  CLOUDFLARE_API_TOKEN` (mirrors how the other host-key mirrors are
  reverse-mapped to their preset id).
- `bun run scripts/generate-integrations-artifacts.ts` regenerated
  `integrationArtifacts.generated.ts` to include the cloudflare preset
  + route + vendor.
- Tests: `compatibility.test.ts` PRESETS list, new
  `buildProfileSaveMessage` Cloudflare case in `provider.test.tsx`,
  new `applyProviderProfileToProcessEnv` Cloudflare case in
  `providerProfiles.test.ts`.
- Docs: README providers table row + `docs/advanced-setup.md` section
  matching the MiMo / Mistral entries.

- Dedicated AI Gateway integration with `gateway_id` URL templating.
  Today users can still paste a full Gateway URL into `OPENAI_BASE_URL`
  and the preset's `matchBaseUrlHosts` picks `gateway.ai.cloudflare.com`
  up.
- Dynamic `/models` discovery on the Groq #1143 / `mapModel` pattern —
  Cloudflare's `/v1/models` returns the runnable model list and the
  hybrid catalog path drops in cleanly. Left as a separate PR so this
  one stays a focused preset add.

Closes #1100

* fix(cloudflare): narrow route matching to api.cloudflare.com host

`gateway.ai.cloudflare.com` is the shared host for *all* Cloudflare AI
Gateway routes (Workers AI, Anthropic, OpenAI, etc.), so matching it to
the Workers AI preset applied Workers-AI runtime metadata and
credential precedence (CLOUDFLARE_API_TOKEN before OPENAI_API_KEY, body
'store' strip, max_tokens field) to other providers' Gateway URLs.
Drop the shared host from the match list; a dedicated AI Gateway
integration with path-aware routing is the right follow-up.

Refs #1100.

* fix(provider-manager): keep Codex OAuth after DeepSeek when cloudflare added

The picker hardcoded `options.splice(7, 0, …)` to drop the Codex OAuth
entry right after DeepSeek. Adding cloudflare to ORDERED_PROVIDER_PRESETS
bumped DeepSeek to index 7, so the splice now lands Codex OAuth *before*
DeepSeek and breaks the test fixture that drives navigateToPreset by
keypress count.

Switch to a dynamic `findIndex('deepseek') + 1` lookup so any future
preset inserted between Bankr and DeepSeek keeps the established
ordering. Fixture updated to mirror the new picker order.

Caught by CI on 12b3ff… smoke-and-tests: 8 ProviderManager tests
timing out because navigateToPreset overshot/undershot the target.

* fix(cloudflare): exclude the shared AI Gateway host from Cloudflare routing

The profile env/alignment/startup paths mirrored CLOUDFLARE_API_TOKEN whenever
the profile URL merely contained 'gateway.ai.cloudflare.com'. That host is the
shared AI Gateway for all Cloudflare AI routes (Workers AI, OpenAI, Anthropic,
...), so a profile retargeted to /openai or /anthropic Gateway URLs was wrongly
tied to the Cloudflare route and credential precedence.

Add isCloudflareBaseUrl (hostname === api.cloudflare.com, matching the Workers
AI host and the descriptor's matchBaseUrlHosts) and route all three sites
through it, consistent with isXaiBaseUrl/isFireworksBaseUrl. Also restore
CLOUDFLARE_API_TOKEN in the provider profile test cleanup keys.

* fix(cloudflare): don't seed the placeholder base URL from the CLI shortcut

`openclaude --provider cloudflare` fell through the generic OpenAI-compatible
branch and applied the descriptor default base URL verbatim — including the
unresolved `<ACCOUNT_ID>` placeholder — leaving the shortcut 'configured' with
an endpoint that cannot serve a request. Skip seeding any base URL that still
contains a `<...>` placeholder, so the user must supply a real account-scoped
URL (OPENAI_BASE_URL / `/provider` edit) first, matching how the wizard treats
placeholder endpoints.

* test(cloudflare): assert exact null fallback for AI Gateway routes

The shared AI Gateway URL assertions used `.not.toBe('cloudflare')`, which
would also pass for any other non-cloudflare return value. The intended
fallback is null, so assert `.toBe(null)` to lock the regression boundary.

* fix(cloudflare): gate profile token mirroring on base URL host only

applyProviderProfileToProcessEnv mirrored CLOUDFLARE_API_TOKEN whenever
route.routeId === 'cloudflare'. route comes from the saved profile.provider,
so that disjunct is always true for a cloudflare profile, including one
retargeted to the shared gateway.ai.cloudflare.com AI Gateway host. The
sibling sites (isProcessEnvAlignedWithProfile, buildOpenAICompatibleStartupEnv)
already key on isCloudflareBaseUrl only; align this site with them so a
shared-gateway profile no longer leaks the token or stays pinned to the
cloudflare route. Add a regression test for the gateway.ai.cloudflare.com case.

* chore(integrations): regenerate artifacts for the Cloudflare vendor

The rebase took main's generated artifacts at the conflict; regenerate so the
Cloudflare vendor descriptor is registered in VENDOR_DESCRIPTORS and the
manifest alongside the providers main added.

* fix(cloudflare): mirror CLOUDFLARE_API_TOKEN into the OpenAI-compatible auth path

The --provider cloudflare shortcut fell through to the generic
OpenAI-compatible default branch and never copied CLOUDFLARE_API_TOKEN
into OPENAI_API_KEY, so a user who only set the token sent an
unauthenticated request. Add a dedicated cloudflare case that mirrors the
token (and clears a stale generic key when absent), keeping the
placeholder-URL skip.

buildOpenAICompatibleStartupEnv also returned from its strict-env branch
before the fallback CLOUDFLARE_API_TOKEN mirror, so a keyed Cloudflare
profile persisted a startup env that omitted the token and re-detected
inconsistently after relaunch. Mirror it in the strict branch alongside
nearai/fireworks. Add regression coverage for both paths.

* fix(cloudflare): gate token mirroring on a real Cloudflare endpoint

The cloudflare shortcut copied CLOUDFLARE_API_TOKEN into the generic
OPENAI_API_KEY unconditionally. The descriptor default carries an
unresolved `<ACCOUNT_ID>` placeholder and is never seeded, so with
OPENAI_BASE_URL unset (or still pointing at a previous OpenAI-compatible
provider) the token would be attached to the wrong host. Gate the mirror
on isCloudflareBaseUrl(getConfiguredOpenAIBaseUrl()) — only seed
OPENAI_API_KEY once the configured base URL resolves to
api.cloudflare.com, otherwise fail fast and leave it unset. Add
regression coverage for the unconfigured, stale-host, and AI-Gateway-host
cases.

* fix(cloudflare): reject placeholder URL and keep the OPENAI_API_KEY fallback

The token mirror keyed on the api.cloudflare.com host only, so the literal
<ACCOUNT_ID> placeholder URL (same host) passed the gate and copied the
token onto a non-working endpoint. It also deleted any generic
OPENAI_API_KEY when no token was set, breaking the documented
compatibility fallback for users authenticating a real Workers AI URL with
OPENAI_API_KEY. Mirror only on a real (non-placeholder) Cloudflare
endpoint, and preserve an existing generic key there when no dedicated
token is present.

Refs #1100

* refactor(cloudflare): model Workers AI as a gateway, not a vendor

Cloudflare Workers AI is a hosted OpenAI-compatible inference endpoint
reached over the shared openai transport, so it belongs with the gateway
providers (atlas-cloud, groq, together, ...) rather than the transport
vendors. Move it to gateways/cloudflare.ts via defineGateway (category
hosted, vendorId openai), regenerate the integration artifacts, and
allowlist its provider-specific @cf/* catalog ids in the gateway
descriptor check (no shared cross-provider descriptor exists, same as
azure-deployment).

Refs #1100

* fix(cloudflare): key Workers AI detection on the account path, not the host

api.cloudflare.com also serves the general Cloudflare REST API, so matching the
whole host treated unrelated URLs (e.g. /client/v4/user/tokens/verify) as the
Workers AI route and mirrored CLOUDFLARE_API_TOKEN into OPENAI_API_KEY for them.

isCloudflareBaseUrl now requires the Workers AI path
/client/v4/accounts/<account_id>/ai/v1 with a real (non-placeholder) account id,
and resolveRouteIdFromBaseUrl guards its cloudflare hostname match through the
same predicate. Both route detection and token/profile mirroring key on the
actual Workers AI endpoint.

Adds same-host negative regressions (general REST path is not routed and does
not mirror the token; unresolved <ACCOUNT_ID> placeholder is excluded) and
asserts the Cloudflare Workers AI preset appears in the first-run picker.

* fix(cloudflare): honor the Workers AI path boundary in the profile-provider fallback

resolveActiveRouteIdFromEnv returned the saved active-profile provider's route
id before consulting its base URL. For a `cloudflare` profile that had been
retargeted to a non-Workers URL — the shared AI Gateway host, or a general
api.cloudflare.com REST path — this still resolved as `cloudflare`, so the
Workers AI shim config (removeBodyFields: ['store'], Cloudflare model metadata)
and CLOUDFLARE_API_TOKEN mirroring were applied to a generic endpoint, even
though resolveRouteIdFromBaseUrl already excludes those URLs.

Gate the profile-provider shortcut through profileRouteHonorsBaseUrlBoundary,
which requires the path-aware isCloudflareBaseUrl for the cloudflare route (all
other routes are host-scoped by resolveProfileRoute and unaffected). A retargeted
profile now falls through to the generic openai/custom resolution; a genuine
Workers AI profile base URL still resolves as cloudflare.

Adds regressions for both retarget cases (gateway host + REST path) and the
positive Workers AI profile case.

* fix(cloudflare): require HTTPS and honor the Workers AI path in validation

isCloudflareBaseUrl accepted any scheme, so http://api.cloudflare.com/
client/v4/accounts/<id>/ai/v1 resolved as the cloudflare route and mirrored
CLOUDFLARE_API_TOKEN into OPENAI_API_KEY over cleartext. Require url.protocol
=== 'https:'.

Startup validation selected the Cloudflare target on host match alone, so a
non-Workers path like /client/v4/user/tokens/verify demanded Workers AI auth
instead of falling back to generic OpenAI validation. Gate the cloudflare
target on isCloudflareBaseUrl(request.baseUrl), mirroring the runtime route
resolver's path boundary.

* test(cloudflare): lock non-Workers path token boundary; fix stale host-only comments

The apply/persist paths already gate CLOUDFLARE_API_TOKEN mirroring on the
isCloudflareBaseUrl path predicate, but had no coverage for a same-host
non-Workers path (api.cloudflare.com/client/v4/user/tokens/verify) and the
comments beside the mirroring sites still described a host-only boundary.

Add negative apply and persist regressions asserting the token is not mirrored
or persisted for that non-Workers URL, and update the comments to describe the
real Workers AI path predicate instead of host-only matching.

* fix(cloudflare): fall back to a generic route for retargeted profiles

resolveProfileCapabilityRouteId returned the cloudflare capability route id for
any saved cloudflare profile whose base URL no longer resolves — including one
retargeted to gateway.ai.cloudflare.com or another OpenAI-compatible host. That
stripped generic capabilities (apiFormat, custom auth/request headers) from
profile sanitize/apply even though the runtime resolver runs such a profile as
a generic OpenAI-compatible route. Mirror the same isCloudflareBaseUrl boundary:
keep the cloudflare route only for the real Workers AI URL (or the unset
descriptor default) and fall back to 'custom' otherwise. Regression asserts a
retargeted cloudflare profile preserves OPENAI_API_FORMAT.

* test(cloudflare): assert retargeted profile resolves to the custom route

Pin both resolveActiveRouteIdFromEnv assertions for a retargeted cloudflare
profile to .toBe('custom') instead of .not.toBe('cloudflare'), so the test
locks the intended generic OpenAI-compatible fallback rather than merely
excluding the cloudflare route.
2026-07-09 22:48:10 +08:00
JATMNandGitHub e086e8c35a fix(safety): relax over-restrictive safety checks for benign coding tasks (#1897)
* fix(safety): relax over-restrictive safety checks for benign coding tasks (Fixes #1616)

Issue #1616 reports refusals for routine, benign coding tasks. Two layers caused this:

1. Model-level over-refusal: CYBER_RISK_INSTRUCTION and the 'ask before acting' guidance biased the model toward refusing normal work. Reworded to explicitly permit ordinary engineering tasks and dual-use/security-adjacent work in authorized contexts, and to ask a clarifying question rather than refuse when intent is ambiguous.

2. Application-level heuristics that become hard blocks in auto/YOLO/headless mode: the bash command-injection check, the broad DANGEROUS_FILES/DANGEROUS_DIRECTORIES auto-edit guard, and the auto-mode stripping of ordinary interpreter allow-rules (Bash(python:*), npm run:*, etc.).

Added an OPENCLAUDE_SAFETY_LEVEL knob (strict|balanced|permissive, default balanced). In 'permissive' the application-level heuristics above are relaxed while genuine Windows-path/symlink guards remain active. Default behavior is unchanged.

Validation:
- bun run typecheck: clean
- bun run build: succeeds
- bun test safetyLevel.test.ts, bashSecurity.safety.test.ts: pass
- bashSecurity.test.ts, filesystem.test.ts, permissionSetup.test.ts, security-hardening.test.ts: pass (no regressions)

* fix(safety): narrow permissive safety relaxations

* fix(safety): address review follow-ups

* fix(safety): address additional review findings

* refactor(permissions): share rule normalization
2026-07-09 09:07:50 +08:00
0xfandomandGitHub 06e0ae6e0b feat(settings): per-model context_window and max_output_tokens overrides (#1234)
* feat(settings): per-model context_window and max_output_tokens overrides

Adds a `modelLimits` settings.json map so users can declare context window
and max output tokens for OpenAI-compatible models that are not in the
built-in catalog. Resolution order is env var → settings → catalog, so the
existing CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS / _MAX_OUTPUT_TOKENS env vars
keep priority. Keys are matched exactly, then by prefix, with optional
"<host>:<model>" host-qualified forms.

Example:
  "modelLimits": {
    "qwen3.6-plus": { "contextWindow": 1048576, "maxOutputTokens": 32768 }
  }

Closes #478

* fix(model-limits): apply settings overrides in resolveModelRuntimeLimits

The settings.json `modelLimits` fallback was only wired into the scalar
getOpenAIContextWindow / getOpenAIMaxOutputTokens helpers. The runtime
resolution path (resolveModelRuntimeLimits) instead consumes the *Matches
variants, which only returned env-var matches — so a configured modelLimits
override never reached the model whose limits are actually resolved at
runtime, contrary to the documented env → settings → catalog order.

Fold the settings lookup into getOpenAIContextWindowMatches /
getOpenAIMaxOutputTokenMatches as a dedicated `settings` field, and insert it
between the exact env override and the built-in catalog in
resolveModelRuntimeLimits. Declare `modelLimits` on GlobalConfig so the lookup
is typed. Add an integration test that drives resolveModelRuntimeLimits and
asserts settings resolve for exact, host-qualified and prefix keys, and that an
env override still wins.

* fix(model-limits): resolve modelLimits from settings.json, not global config

readSettingsLimits read getGlobalConfig().modelLimits, which is ~/.openclaude.json
— a different file from the settings.json the feature is documented and schema'd
for (SettingsSchema in settings/types.ts). A user adding modelLimits to
settings.json saw no effect. Read it from getInitialSettings() (the merged
settings snapshot, session-cached) instead, and drop the now-unused modelLimits
field from GlobalConfig so the override lives in one place. Rewire both test
suites to a gated-passthrough getInitialSettings mock.

* test(model-limits): capture real settings module at load, not in beforeEach

The settings modelLimits suites re-imported the real settings module with
a cold dynamic import inside beforeEach. That import sat right at Bun's
default 5s hook timeout and intermittently failed the first test. Capture
the genuine module once via a top-level query-string-busted static import
so the cost moves to module load (no hook timeout) and the gated
passthrough still bypasses other suites' settings.js mocks. Also correct
the integration-test comment to name the helpers the runtime path
actually calls (getOpenAIContextWindowMatches / getOpenAIMaxOutputTokenMatches).

* fix(model-limits): keep env-prefix override above settings in runtime resolver

resolveModelRuntimeLimits ordered the settings `modelLimits` value above
the env-prefix match, so a broad env-prefix override (e.g.
`{"my-custom":N}`) was silently overtaken by a more specific settings
entry (`my-custom-deployment`). The scalar getOpenAIContextWindow treats
env (exact ?? prefix) as strictly higher priority than settings; mirror
that in the runtime resolver: env.exact, then env.prefix, then settings,
then catalog/cache/descriptor. Regression covers both contextWindow and
maxOutputTokens.

* docs(model-limits): document the settings.json modelLimits override

Add a user-facing section near the env-var overrides covering the
modelLimits map, JSON shape, exact/prefix/host-qualified key matching, and
the env > settings > catalog > descriptor precedence.

* fix(model-limits): keep catalog above env-prefix in runtime precedence

The previous reorder put the env-prefix match above the catalog value,
which broke the existing invariant that a `:cloud` catalog variant takes
its known catalog limit rather than inheriting a broad base-model env
prefix (deepseek-v4-pro:cloud regressed to 262144). Correct order:
exact env -> catalog/cache -> env prefix -> settings -> descriptor. This
still keeps settings strictly below env-prefix (the original drift fix)
while preserving catalog precedence over prefix. Docs precedence updated
to match.

* docs(advanced-setup): document CLAUDE_CODE_OPENAI_MAX_OUTPUT_TOKENS env var

The modelLimits section referenced the max-output env var but the
Environment Variables table only listed the context-window one, leaving
output-only configuration undocumented. Add the matching table row.

Refs #478

* docs(model): align openaiContextWindows precedence comments with the resolver

The module header and the OpenALimitOverrideMatches.settings comment claimed a
resolution order of "env → settings → catalog", which contradicts
resolveModelRuntimeLimits (exact env → catalog/discovery cache → prefix env →
settings modelLimits → descriptor default). Since this module only produces the
override candidates and does not own the precedence, narrow the comments to say
so and point at runtimeMetadata.ts as the authoritative chain, so this
precedence-sensitive code isn't misread when touched again.

* fix(model): rank host-qualified modelLimits keys above bare model keys

lookupByModel grouped all exact matches (host-qualified and bare) ahead of all
prefix matches, so a bare exact key like `qwen3.6-plus` beat a host-qualified
prefix like `openrouter.ai:qwen3`. That defeated the advertised per-endpoint
disambiguation for versioned model families. A host-qualified key is strictly
more specific than a bare one, so rank both host-qualified forms (exact and
prefix) in the high-priority tier ahead of the bare exact match, leaving only
the bare prefix in the low-priority tier.

* fix(model): keep an exact modelLimits key ahead of any host-qualified prefix

The previous commit ranked host-qualified PREFIX matches above bare exact
matches, which broke the deliberate precedence in context.test.ts: an exact
`gpt-4o` limit was overridden by an unrelated `api.foo.com:gpt-4` prefix that
only matches a shorter, different model name.

Restore the tiering so an exact match (host-qualified or bare) always beats a
prefix, and a host-qualified key beats a bare key WITHIN the same match kind.
The supported way to set a different limit for the same model per endpoint is a
host-qualified EXACT key. Narrow the regression + comment to that behavior.

* docs(model): align modelLimits matching wording with exact-over-prefix rule

Narrow the advanced-setup wording so a host-qualified key only wins over a bare
key within the same match kind (a bare exact key still beats a host-qualified
prefix), matching lookupByModel's exact ?? prefix behavior; per-endpoint limits
for the same model need host-qualified exact keys. Also note modelLimits as part
of the documented user-override layer in the integration add-model and
common-pitfalls guides.

* docs(model): clarify modelLimits host-port key and catalog/cache precedence

Spell out that the host-qualified key uses new URL(baseUrl).host — including the
port when present (localhost:4000:my-model, not localhost:my-model) — and split
the precedence line so the built-in catalog is shown as checked before the
discovery-cache value, matching resolveModelRuntimeLimits.
2026-07-09 06:10:49 +08:00
9a53290588 feat(aimlapi): add guided top-up and key provisioning (#1886)
* fix(aimlapi): send valid rebate partner id (part_62yQ…) instead of literal 'Gitlawb'

* feat(aimlapi): add guided top-up and key provisioning

* fix: restore accidentally removed OpenGateway badge

* fix(aimlapi): restore preset order and harden topup polling/logging

* fix(aimlapi): validate --method choices instead of silently defaulting to card

* feat(aimlapi): guided top-up and API key provisioning

* fix(aimlapi): point non-interactive credential error at existing flags

* docs(aimlapi): document guided top-up alongside the existing-key path

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-08 09:17:17 +08:00
0xfandomandGitHub db01038d5c feat(model-picker): surface inactive provider profiles in /model (#1119 piece 2) (#1164)
* feat(model-picker): surface inactive provider profiles in /model

When a user configures multiple providerProfiles (Kimi + Z.AI + OpenRouter
+ SambaNova in the #1119 repro, but the pattern fits any multi-provider
setup), switching the main session between them currently requires
round-tripping through /provider — /model only shows the active
profile's models.

Make /model the single switcher:

- ModelOption gains an optional `switchToProfileId`. Existing options
  leave it unset and behave exactly as today.
- `getInactiveProviderProfileOptions` enumerates every configured
  profile that isn't the active one and emits a picker entry per model,
  labelled `<model> · <profile.name>` so the user can see the choice
  changes providers, not just models.
- Each option's `value` is encoded with `__switch_profile__:<id>:<model>`
  so the picker's plain-string `value` channel stays the source of truth
  and same-named models under different base URLs (`gpt-4o` on multiple
  OpenAI-compatible endpoints) stay disambiguated.
- /model's handleSelect detects the prefix, calls
  `setActiveProviderProfile` (same path /provider uses — applies env,
  persists active profile, refreshes startup file), then sets
  `mainLoopModel` to the bare model string.

Only surfaces inactive options when `CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED`
is set, so users who haven't opted into the multi-profile workflow at all
don't see the affordance.

Tests cover round-trip encoding (including OpenRouter-style colon-bearing
model strings), the active-filter, the multi-model explosion, and that
`getModelOptions()` 3P path includes the inactive options only when the
profile env is applied. Combined invocation with the rest of
`src/utils/model/` + `src/commands/model/` + `src/utils/providerProfiles.test.ts`
runs clean to guard against mock-leak (per the 2026-04-30 lesson —
spreads `import * as actual` for every `mock.module` factory).

Refs #1119

* fix(model-picker): run fast-mode cleanup on cross-profile switch

The new switch-profile branch returned before reaching the fast-mode
reconciliation, so a user with fastMode latched on Anthropic Opus could
switch to an OpenAI profile and silently keep fastMode on even though
the new model can't support it. Extract the cleanup into a pure helper
`reconcileFastModeForSwitch` and call it from both branches.

Refs #1119.

* fix(model-picker): decode cross-profile values before effort/display lookup

Inactive-profile entries encode the picker value as
`__switch_profile__:<profileId>:<model>`, but `resolveOptionModel`
forwarded the raw string straight to `parseUserSpecifiedModel`. For a
reasoning-capable cross-profile entry such as `gpt-5.4`,
`modelSupportsEffort()` then saw the prefixed string and reported
"Effort not supported", and `handleSelect` dropped the toggled effort
even when the underlying model accepts it.

Run `parseSwitchProfileValue` first; when it matches, hand the bare
target model to `parseUserSpecifiedModel` so effort capability,
default-effort lookup, and display-name resolution all key off the real
model id.

* fix(model-picker): include inactive profiles on local OpenAI-compatible scope

The inactive-profile compute lived after the
`getAdditionalModelOptionsCacheScope()?.startsWith('openai:')` early
return, so users with a local OpenAI-compatible profile active (Ollama,
lm-studio, any localhost endpoint) never saw the cross-profile switcher
in `/model`. They still had to round-trip through `/provider` to change
profile.

Hoist `profileEnvApplied`, the active-profile lookup, and
`getInactiveProviderProfileOptions(activeProfileId)` above the early
return, and append `inactiveProfileOptions` to the local-OpenAI branch
return value. Other branches (Claude.AI, MiMo, MiniMax, ant) were
already either irrelevant or have their own gating.

Test: new regression in modelOptions.crossProfile.test.ts pins
`getAdditionalModelOptionsCacheScope` to an `openai:` value and confirms
the inactive profile still surfaces with a parseable
`__switch_profile__` value.

* fix(model-picker): apply the allowlist to the decoded cross-profile model

filterModelOptionsByAllowlist evaluated cross-profile options by their encoded
__switch_profile__:<id>:<model> value, so an availableModels allowlist that
permits the bare target (e.g. glm-5.1) dropped every inactive-profile entry.
Check the allowlist against parseSwitchProfileValue(value)?.model ?? value, and
cover both the allowed and denied cases.

* fix(model-picker): only surface cross-profile switch options on the /model path

The inactive-profile entries come from the shared getModelOptions() list, but
only the /model command's onSelect decodes __switch_profile__ values and
activates the target profile. The prompt hotkey and Settings pickers wrote the
encoded value straight to mainLoopModel, sending an invalid model string.

Gate these options behind a new allowProfileSwitch prop that only the /model
command sets; inline pickers no longer surface an option they cannot honor.
Also apply the org allowlist to the decoded target model in the /model select
handler.

* test(model-picker): drop flaky cross-profile allowlist case

The decoded-allowlist assertion drove the org allowlist through the shared
session settings cache, which is racy across bun's single-process run and could
leak availableModels into sibling suites (the providerConfig cache-scope tests
went red in CI). The decode itself is a one-line guard already exercised by the
parseSwitchProfileValue round-trip coverage, so remove the unreliable case
rather than ship CI flake.

Also snapshot the real provider/auth modules before mocking so each harness
call rebuilds its mock from a clean base instead of a previous test's overrides
(bun live-repoints the imported namespace to the active mock).

* test(model-picker): stop cross-profile mocks leaking into provider suites

The cross-profile tests mock.module'd ../providerProfiles, ./providers,
../auth and ../../services/api/providerConfig per test. bun's mock.module is
process-wide and mock.restore() does not undo it, so these persisted into later
files — most damagingly the providerConfig mock, which replaced the module with
a single-function stub and stripped resolveProviderRequest /
getAdditionalModelOptionsCacheScope from providerConfig.local's suite (now
adjacent after the rebase onto #1706).

Install each mock once at module load, keep the full export surface, and gate
the overrides on module-level flags cleared in beforeEach/afterEach so the
persisted mocks are transparent passthroughs for every other suite. Same
pattern as the cross-spawn / install-surfaces leak fixes.

* fix(model): reconcile fast mode before activating the switched profile

In the cross-profile /model switch path, reconcileFastModeForSwitch ran after
setActiveProviderProfile. The reconciler gates on isFastModeEnabled(), which
reads the *active* provider — so once the target profile is activated it
reflects the new (fast-mode-less) provider and short-circuits to 'unchanged',
leaving fastMode latched on for a model that can't use it.

Compute the reconciliation before activating the profile, so it evaluates
against the source provider and correctly returns 'off' for an unsupported
target. Add a command-level regression test that drives handleSelect with a
__switch_profile__ value while setActiveProviderProfile flips the fast-mode
state, and asserts fastMode is set to false (it fails if the call order
regresses).

* fix(model): re-check fast mode after activating a switched profile

The pre-activation reconcile gates on the source provider, so its 'on' result
is stale when the target provider cannot run fast mode even though the target
model name passes the source-side support check (e.g. a third-party shim
exposing a claude-opus-* model). Re-evaluate isFastModeEnabled / supported /
available after setActiveProviderProfile and force fastMode off when it is no
longer genuinely supported. Add a command-level regression test for that path
and wrap the cross-profile test cleanup in try/finally so a failing assertion
still unmounts the Ink instance (jatmn review, #1119).

* test(model-picker): cover cross-profile allowlist with isolated settings

Re-add the regression dropped in 06a0c80: filterModelOptionsByAllowlist must
evaluate the allowlist against the decoded target model, not the encoded
__switch_profile__ wrapper. Uses this suite's per-test settings cache (reset in
afterEach) instead of the shared cache that made the earlier version flaky
(jatmn review, #1119).

* test(model-picker): make the cross-profile allowlist test leak-proof

The new allowlist test drove availableModels through setSessionSettingsCache,
but sibling suites (ModelPicker, ProviderManager, ...) mock.module both
settings.js (getSettings_DEPRECATED) and modelAllowlist.js (isModelAllowed)
process-wide, so in the full sequential run the leaked stubs defeated the cache
and the denied option was not filtered (smoke-and-tests red on the full suite,
green in isolation).

Drive the allowlist deterministically from this suite instead: install-once,
gated, passthrough mocks of getSettings_DEPRECATED (the filter gate) and
isModelAllowed (the per-option check), both keyed off a single
activeSettingsOverride and cleared in afterEach. Same gated-passthrough pattern
as the suite's existing providerConfig/providers/auth/profiles mocks and the
agent.test.ts allowlist approach.

* fix(model): keep cross-profile switch options out of the SDK models list

getModelOptions() now returns inactive-profile entries encoded as
__switch_profile__:<id>:<model>. print.ts mapped those straight into the
ModelInfo list returned to SDK/web callers, exposing UI-only values that
are not selectable model ids. Filter them with parseSwitchProfileValue
before building modelInfos. Add ModelPicker coverage for the
allowProfileSwitch filter (hidden inline, shown when allowed) and
document cross-profile /model switching in the provider-profile docs.

* test(model-picker): prove cross-profile switch options never reach SDK models

Extract selectSdkModelOptions as the single gate the SDK modelInfos
builder runs every getModelOptions() entry through, and cover it directly:
an encoded __switch_profile__:<id>:<model> option is dropped while real
model ids pass through. Fails if an inactive-profile affordance ever leaks
into the initialize.models response again (#1119).

* docs(model-picker): clarify the env gate for inactive-profile entries

The inactive-profile models only appear when the provider-profile env
workflow is active (CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED=1), not for
every multi-profile setup. Spell that out and restore the local-only
`--provider ollama` guidance that was folded into the paragraph.

* fix(model-picker): gate SDK option filter on switchToProfileId marker

selectSdkModelOptions filtered on the encoded __switch_profile__ value
prefix, which also reserved that prefix for every custom model id. A real
configured model whose id starts with __switch_profile__: would vanish
from the SDK models response and non-switching pickers. Key the gate on
the explicit switchToProfileId marker, which only synthesized switch
options carry, and add the collision regression.

Refs #1119

* fix(model-picker): reuse switch confirmation for cross-profile selections

The cross-profile branch built its own "Switched to" message and returned
before the regular path appended the selected effort and the
"Billed as extra usage" notice, hiding cost-impacting feedback when a
reasoning/extra-usage target was chosen through an inactive profile.
Append effort and the extra-usage check to the switch confirmation.

Refs #1119

* fix(model-picker): surface inactive profiles on the active Ollama path

The isOllamaProvider() early return ran before the inactive-profile
options were computed, so an active local Ollama profile saw only its own
models and lost the cross-profile switcher, forcing the /provider
round-trip this feature removes. Hoist the inactive-profile compute above
the Ollama branch and append it to the Ollama returns.

Refs #1119

* fix(model): surface inactive profiles on all provider branches; decode only real switch options

Two follow-ups to the #1119 unified /model switcher:

- inactiveProfileOptions was computed before the early-return branches but only
  appended on Ollama / local-scope / PAYG paths. The GitHub Copilot, NVIDIA NIM,
  MiniMax, Xiaomi MiMo, ant, and Claude-subscriber branches returned first, so a
  user with a saved profile active on any of those routes lost the cross-profile
  entries and had to round-trip through /provider. Append the (env-gated, so
  empty unless a profile is applied) inactive options on those branches too.

- filterModelOptionsByAllowlist decoded any value starting with
  `__switch_profile__:` via parseSwitchProfileValue, even a normal custom model
  id that merely shares that prefix, evaluating the allowlist against the wrong
  inner model. Gate the decode on the `switchToProfileId` marker (the type's
  documented contract) so non-switch ids are checked verbatim.

Extends the cross-profile harness with gated getAPIProvider / NVIDIA / subscriber
overrides and adds branch-append + verbatim-allowlist regressions (red-green).

* fix(model): key profile-switch handling on the marker across picker and command

The allowlist/SDK paths already used the switchToProfileId marker, but two
surfaces still keyed on the raw `__switch_profile__:` value prefix:

- ModelPicker's inline-picker filter hid any option whose value started with
  the prefix, so a real custom model id like `__switch_profile__:vendor:gpt-5.4`
  disappeared from prompt/settings pickers. It now filters on
  `switchToProfileId === undefined`.
- the /model command decoded parseSwitchProfileValue(model) for any prefixed
  string and tried to activate the encoded profile id, so selecting such a
  custom model activated a nonexistent profile instead of setting the literal
  model. It now only treats the value as a switch when the decoded profile id
  maps to a real configured provider profile — which every synthesized switch
  option does, and a prefix-colliding custom id does not.

Drops the now-unused SWITCH_PROFILE_VALUE_PREFIX import from ModelPicker. Adds a
picker regression (marked switch hidden, prefixed custom model stays visible) and
completes the cross-profile branch coverage (MiniMax, Xiaomi MiMo, ant) so every
branch that appends inactive-profile options is locked.

* test(model): register target profiles in cross-profile switch tests

The /model command now only treats a `__switch_profile__:` value as a switch
when its decoded profile id maps to a real configured provider profile. The
cross-profile switch tests set up setActiveProviderProfile but left the shared
getProviderProfiles mock empty, so the new guard classified their switch values
as literal models and the fast-mode / effort / extra-usage assertions no longer
ran. Register each test's target profile via getProviderProfiles so the switch
path executes as intended.

* fix(model): gate cross-profile switches on the selected option marker

Selecting a value that merely parses as `__switch_profile__:<profileId>:<model>`
activated the provider whenever <profileId> existed, so a literal custom model
id such as `__switch_profile__:profile_openai:gpt-5-mini` wrongly switched the
active provider instead of being applied verbatim.

Thread the picked option's `switchToProfileId` marker from ModelPicker.onSelect
(selectOptions already carries it) and only activate a profile when the marker
matches the decoded id. The effort/display resolver had the same gap — it
decoded every prefixed value; gate it on a genuine marker-backed switch option
too. Add a regression asserting a marker-less prefixed id is applied literally.

* test(model): cover Max/Team Premium and empty-catalog switch-append paths

The cross-profile branch-coverage suite exercised the populated-catalog returns
but not the Max/Team Premium subscriber early return nor the empty-catalog
fallbacks (NVIDIA/MiniMax/Xiaomi), which are the same paths that previously
dropped the inactive-profile switch options. Lock them so every changed return
that appends `...inactiveProfileOptions` is covered.

* fix(model): keep inactive-profile switch options in /model discovery overrides

The interactive /model command passes an optionsOverride into ModelPicker for
descriptor-backed and legacy OpenAI-compatible discovery contexts, built from
mergeActiveProfileModelOptions which only merges the ACTIVE profile's route
models. Because the picker renders optionsOverride ?? getModelOptions(), the
inactive-profile switch entries getModelOptions() appends never reached those
paths, so the unified switcher vanished for provider-profile routes
(OpenRouter/Kimi/MiniMax, refreshed local profiles). Re-append the same
inactive-profile switch options (allowlist-filtered on the decoded target) to
any override list before handing it to the picker.

* fix(model): base the switch marker on the presented option, treat ties as ambiguous

The picker derived switchToProfileId with selectOptions.find(value===...), and
the effort/display resolver decoded when any getModelOptions() entry with the
same value carried the marker. If a literal custom model id collided with an
encoded switch value, the literal could borrow a different same-value option's
marker and wrongly activate a provider. Add resolveSelectedSwitchProfileId,
which keys on the actual presented option and treats duplicate-value matches as
ambiguous (no switch), and route both the onSelect marker and the decode
decision through it.
2026-07-07 21:53:29 +08:00
2edec9a140 fix(deps): ship a zero-warning, minimal install (#1784)
* fix(deps): ship a zero-warning, minimal install

The published package declared 62 runtime `dependencies`, but `dist/cli.mjs`
is a fully-bundled esbuild output that inlines almost all of them. End users
therefore installed ~476 transitive packages — including three subtrees the
bundle never needs at install time, each emitting an install warning:

  - node-domexception (deprecated) via google-auth-library
  - protobufjs (allow-scripts)     via @grpc/* (already bundled into dist)
  - sharp (allow-scripts)          native image module

The repo's `overrides`/`allowScripts` silence these locally, but those are
root-only npm settings and are ignored when the package is installed as a
dependency — so end users saw the warnings.

Core changes:
  - package.json: runtime dependencies trimmed 62 -> 3 (@orama/orama,
    @orama/plugin-data-persistence, @vscode/ripgrep). Bundled packages, plus
    the optional sharp/google-auth-library, move to devDependencies so they
    are built/tested but not shipped.
  - package.json: @anthropic-ai/sdk, @modelcontextprotocol/sdk, react and
    react-reconciler declared as OPTIONAL peerDependencies — externalized by
    the ./sdk bundle but bundled into the CLI. Optional peers keep the CLI
    install minimal and warning-free while still resolving for ./sdk consumers.
  - externals.ts: sharp, google-auth-library and @anthropic-ai/bedrock-sdk
    marked OPTIONAL_RUNTIME_EXTERNALS (loaded on demand, not shipped).
  - validate-externals.ts: runtime deps validate against externals; bundled
    deps validate against dependencies + devDependencies.
  - client.ts: load @anthropic-ai/bedrock-sdk via the runtime importer so
    esbuild no longer inlines it and hoists its static @aws-sdk import into
    the CLI bundle (that was a startup crash for default installs).

Optional-dependency UX (consistent, actionable errors):
  - New src/utils/optionalRuntimeModule.ts exports importRuntimeModule and
    importOptionalRuntimeModule. The optional variant translates a missing
    package (code === 'ERR_MODULE_NOT_FOUND', specifier present in message)
    into "<feature> requires "<pkg>" ... Run `npm i -g <pkg>`". Generic so
    typed call sites keep their module types.
  - Routed ALL optional-package load sites through it (previously only one
    did): google-auth-library (client.ts, auth.ts, geminiAuth.ts),
    @anthropic-ai/foundry-sdk + @azure/identity (client.ts), and the
    @aws-sdk/* Bedrock paths (model/bedrock.ts, tokenEstimation.ts, aws.ts).
  - imageProcessor.ts: sharp-missing error now says `npm i -g sharp`.
  - docs/advanced-setup.md: new "Optional provider packages" table and a
    Vertex note documenting the on-demand installs.
  - Unit test for the helper (friendly error, success path, specifier match,
    raw passthrough).
  - knip.json: ignore google-auth-library (now loaded via runtime string).

Verified on the current tree:
  - tsc, build/validate-externals, knip, and tests all pass.
  - npm pack + install --omit=dev adds 8 packages, zero deprecation/
    allow-scripts/funding warnings; --version/--help/mcp list run.
  - With packages absent, CLAUDE_CODE_USE_BEDROCK and CLAUDE_CODE_USE_VERTEX
    print the friendly `npm i -g <pkg>` error (verified end-to-end).
  - ./sdk imports once its optional peers are present (24 exports, no warns).
  - Bundled ajv + ajv-formats validate with no ajv installed; no unguarded
    native runtime requires (fsevents absent in chokidar 4; bun:sqlite Bun-only).

Trade-off: image reads, AWS Bedrock, Azure Foundry and GCP/Vertex now prompt
a one-time `npm i -g <pkg>` instead of being shipped to every user.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

Review fixes (CodeRabbit + jatmn):
- validate-externals: the INTENTIONALLY_BUNDLED exemption is now scoped per
  bundle. The CLI exempts every bundled package; the SDK does NOT exempt
  packages declared as peerDependencies (keyed on package.json, an independent
  source of truth) so dropping react/@anthropic-ai/sdk from SDK_EXTERNALS now
  fails validation instead of silently passing. Added an explicit minimal-
  install contract check: bundled packages must be devDependencies-only — never
  in `dependencies`, and only the SDK-external subset may be optional peers.
  Validation logic extracted to scripts/externalsValidation.ts + tests.
- FileReadTool oversized-image fallback now loads via the shared
  getImageProcessor() (not a raw import('sharp')) and re-throws
  ImageProcessorUnavailableError, so a missing processor surfaces the
  `npm i -g sharp` install hint instead of returning an over-budget image.
- optionalRuntimeModule: match the missing specifier as a QUOTED token, not a
  raw substring, so a missing transitive package whose name contains the
  requested one (sharp vs sharp-libvips, @aws-sdk/client-bedrock vs
  @aws-sdk/client-bedrock-runtime) no longer triggers the wrong install hint.
  Predicate extracted to isMissingSpecifierError() with regression tests.
- docs/advanced-setup.md: the Vertex auth section now shows both documented
  paths (gcloud ADC and a GOOGLE_APPLICATION_CREDENTIALS service-account file).

Review fixes (round 2, CodeRabbit):
- validate-externals: assert the optional-peer install contract — every
  peerDependency must be { optional: true } in peerDependenciesMeta
  (validateOptionalPeers), so losing that flag fails the build instead of
  silently reintroducing install warnings.
- validate-externals: hard-check OPTIONAL_RUNTIME_EXTERNALS placement
  (validateOptionalRuntimeexternals). Anything esbuild can see statically must
  stay external in BOTH bundles (dropping sharp/google-auth-library now fails);
  the runtime-indirection-only subset (new RUNTIME_INDIRECTION_ONLY_EXTERNALS)
  must stay OUT of externals so esbuild never re-exposes their static imports.
- Deeper-dig fix: @anthropic-ai/foundry-sdk was misclassified as
  INTENTIONALLY_BUNDLED, but it is loaded only through the Function indirection
  (esbuild never sees it, so it was never actually bundled) — its sole presence
  in dist is the specifier string. Per the PR's own "Azure Foundry now prompts"
  trade-off it is on-demand, so it now lives in OPTIONAL_RUNTIME_EXTERNALS +
  RUNTIME_INDIRECTION_ONLY_EXTERNALS (mirroring bedrock-sdk). sandbox-runtime is
  genuinely statically imported, so it stays bundled.
- Provider-routing coverage (scripts/optionalRuntimeSpecifiers.test.ts): a
  static scan asserts every importOptionalRuntimeModule specifier is a declared
  OPTIONAL_RUNTIME_EXTERNAL and never also INTENTIONALLY_BUNDLED — the
  invariant that keeps a provider's optional package loadable on demand.
- All new validators extracted to scripts/externalsValidation.ts with tests.

Review fixes (round 3, CodeRabbit):
- client.ts: gate the Vertex google-auth-library import behind the non-skip
  branch. CLAUDE_CODE_SKIP_VERTEX_AUTH (proxy/test) uses a mock GoogleAuth and
  must not require the optional package; it was loaded unconditionally before.
- optionalRuntimeModule: drop the hard-coded `npm i -g`. The helper backs both
  the global CLI and project-local ./sdk consumers, so the hint is now
  context-neutral ("npm install <pkg>" / add -g for the global CLI).
- validate-externals: every SDK_ONLY_EXTERNALS entry must STAY a
  peerDependency (a dropped peer leaves runtimeDeps while the SDK still
  externalizes it); and OPTIONAL_RUNTIME_EXTERNALS must never be shipped (fail
  on overlap with dependencies/peerDependencies). Both with tests + live-verified.
- optionalRuntimeSpecifiers.test: pin the EXACT set of optionally-loaded
  specifiers instead of a >=5 count (a count passes even if a provider path
  regresses).
- attachments: extract tryReadEditedImageAttachment() — background watched-file
  image attachments DEGRADE to null on any failure (incl.
  ImageProcessorUnavailableError) so a missing optional package never aborts a
  turn, while the explicit FileReadTool path still surfaces the install hint.
  Deterministic regression test (bad path -> null).
- docs: Bedrock row notes profile-based auth also needs
  @aws-sdk/credential-providers; install-hint wording matches the new message.

Review fixes (round 4, CodeRabbit):
- attachments: stop sending the raw file path through the analytics
  bypass-cast (tengu_watched_file_compression_failed). Send only the safe
  file extension via getFileExtensionForAnalytics, matching the existing
  tengu_file_read_dedup pattern, so no usernames/project paths can leak.
- externals.ts: corrected the OPTIONAL_RUNTIME_EXTERNALS header comment,
  which still claimed all entries "remain in COMMON_EXTERNALS" — no longer
  true since the indirection-only subset (bedrock/foundry) must stay OUT of
  the externals lists.

(Other CodeRabbit comments on this push re-surface items already addressed in
prior commits: the peerDependenciesMeta-optional check (validateOptionalPeers),
the SDK-peers-present and optional-not-shipped validator rules, the
exact-specifier-set test, the attachments degrade contract + test, and the
context-neutral install hint are all present. The "assert every optional
external is a devDependency" suggestion is intentionally NOT applied: @aws-sdk/*
and @azure/identity are transitive devDeps via bedrock-sdk/foundry-sdk, so a
blanket assertion would be incorrect; source resolution is covered by the
build + tests that import these packages.)

Review fixes (round 5, CodeRabbit):
- attachments: stop leaking file paths via logError in the background-image
  degrade path. readImageWithTokenBudget can throw path-bearing messages
  (e.g. "Image file is empty: <path>") and logError persists message/stack, so
  log only the error TYPE name now. (Analytics payload was already sanitized.)
- attachments: tryReadEditedImageAttachment takes an injectable reader so the
  degrade contract is tested for the EXACT error types — ImageProcessorUnavailableError
  and a path-bearing read error both degrade to null (not just ENOENT) — plus a
  success case. No mocking.
- validate-externals: enforce the source-install half of the optional contract.
  Non-transitive OPTIONAL_RUNTIME_EXTERNALS must be devDependencies so `bun
  install` source builds resolve them. The new TRANSITIVE_OPTIONAL_EXTERNALS
  documents the exemption (@aws-sdk/* via @anthropic-ai/bedrock-sdk, @azure/identity
  via @anthropic-ai/foundry-sdk — provided transitively, not direct devDeps). A
  blanket "all optionals are devDeps" check would have wrongly failed on those.
  Tests + live-verified (dropping sharp from devDependencies now fails).

Review fixes (round 6, CodeRabbit + jatmn):
- optionalRuntimeSpecifiers.test: the call-site scan regex missed
  generic-annotated calls (importOptionalRuntimeModule<...>(...)) in
  model/bedrock.ts and tokenEstimation.ts, so the exact-set assertion was
  incomplete. Regex now allows an optional generic; EXPECTED_SPECIFIERS adds
  @aws-sdk/client-bedrock and @aws-sdk/client-bedrock-runtime (7 total).
- importOptionalRuntimeModule default generic is now <T = unknown> (was any),
  so destructured imports are no longer silently any. Every call site now
  supplies its module type — typeof import('<pkg>') where the package is
  type-resolvable (bedrock-sdk, foundry-sdk, @aws-sdk/credential-providers,
  google-auth-library), and a named minimal-shape alias for @azure/identity
  (not a direct devDep, so typeof import can't resolve it). This gives
  compile-time verification of each provider's module contract (export names,
  shapes) — the structural answer to the "cover the provider branches" ask.
- attachments: tryReadEditedImageAttachment takes injectable {read,log,track};
  a new test asserts the sanitized-telemetry contract directly — the logError
  payload is path-free and the analytics payload carries only `ext`, never the
  edited-image path.

* fix(deps): address optional runtime review findings

* test(deps): isolate optional runtime importer mocks

* fix(deps): clarify AWS optional auth labels

* fix(deps): close optional runtime review gaps

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-07 13:19:39 +08:00
fb40d49e68 feat: add repo map codebase intelligence (#1867)
* feat: add Codebase Intelligence — repo map with PageRank-ranked structural summaries

Adds a new module that builds a structural map of the repository by parsing
source files with tree-sitter, building a cross-file reference graph weighted
by IDF, ranking files with PageRank, and rendering a token-budgeted summary
of the most important files and their signatures.

Surface:
- RepoMap tool the model can call on-demand, with focus_files / focus_symbols
- /repomap slash command with --tokens, --focus, --stats, --invalidate
- Auto-injection into session system context, gated by REPO_MAP=1 env var
  (compile-time feature('REPO_MAP') flag stays off in scripts/build.ts)

How it works:
  git ls-files → tree-sitter WASM parse → extract defs/refs →
  IDF-weighted directed graph → PageRank → render top files until token budget

Files imported by many others rank highest. Common symbol names (get, set,
map, value) are down-weighted via IDF. Results cached to disk keyed by
(path, mtime, size) — only changed files are re-parsed.

Supported languages: TypeScript, JavaScript, Python.

Tree-sitter tag queries are inlined as string constants in queries.ts so
they ship inside dist/cli.mjs and work after npm install — the .scm source
files are kept for readability/Aider attribution but are not required at
runtime. A drift-guard test (queries.test.ts) asserts byte-equality between
the inlined strings and the .scm source files.

Dependencies added: web-tree-sitter, tree-sitter-wasms, graphology,
graphology-pagerank, graphology-operators, js-tiktoken.

* fix(repomap): invalidate rendered cache on file edits + Windows test fix

- computeMapHash now folds per-file mtime+size into the cache key so a
  source edit (without changing the file list) no longer returns the
  prior rendered map. Adds a regression test that edits a file and
  confirms the second build reflects the new symbol without manual
  invalidateCache().
- queries.test.ts byte-for-byte drift guard normalizes CRLF -> LF when
  reading the .scm source so Windows checkouts pass. .gitattributes
  also pins *.scm to LF on future checkouts.
- Externals: declare web-tree-sitter, tree-sitter-wasms, graphology*,
  and js-tiktoken in scripts/externals.ts so build validation passes.

* fix(repomap): expand directory focus paths

* fix(repomap): satisfy deadcode check

* Fix repo map review findings

* Resolve remaining repo map review findings

* fix(repomap): address review findings

* fix(repomap): address review findings

* fix(repomap): resolve smoke and review follow-ups

* fix(repomap): preserve cached tag order

* fix(repomap): resolve review follow-ups

* fix(repomap): satisfy query promise lint

* Fix repo map context timeout cleanup

* fix: address repo map review findings

* fix: cancel timed-out repo map context builds

* fix(repomap): preserve git file path whitespace

* fix(repomap): handle graph and parsing edge cases

* fix(repomap): preserve shell token positions

* fix(repomap): respect configured cache home

* fix(repomap): address review findings

- Add explicit 10000ms timeout to the feature-flag-off context test to avoid cold-import flakes.

- Add --focus-symbols flag to /repomap and forward it to buildRepoMap, matching the RepoMap tool.

- Add parsing/command tests and docs coverage for --focus-symbols.

---------

Co-authored-by: gnanam1990 <gnanasekaran.sekareee@gmail.com>
2026-07-07 11:09:41 +08:00
8369f2018e feat(provider): add AI/ML API provider (#863)
* feat(provider): add AI/ML API integration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(provider): preserve bootstrap model fallback

* fix(provider): complete aimlapi env-only routing

* test(provider): keep first-run preset assertions visible

* fix(provider): align aimlapi attribution and setup docs

* fix(provider): use aimlapi rebate attribution headers

* fix(provider): report current integration version

* fix(provider): prioritize dedicated aimlapi credentials

* fix(provider): complete AI/ML API attribution headers

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-07 11:08:50 +08:00
e2bbb0295a feat: smart auto-routing (per-turn simple-vs-strong model selection) (#1734)
* feat(smart-routing): add smartRouting settings schema and reader

* feat(smart-routing): resolve role keys to a SmartRoutingConfig

* feat(smart-routing): wire per-user-turn routing into the query loop

Classify once per user turn (transition===undefined), pin the decision in a
loop-local, and apply the model-only route before the blocking-limit math.
Enforce the org allowlist by calling isModelAllowed directly (coerce disallowed
to strong; disable for the session if strong is also disallowed). Strip thinking
history on a model change only under the provider gate (preserve-reasoning
providers are left untouched). Export stripThinkingBlocksIfProviderAllows.

* feat(smart-routing): add routed-error fallback to the strong model

A simple-routed turn whose model call hits a retryable error retries once on
the strong model, reusing the existing attemptWithFallback retry loop. Aborts
and 4xx client errors propagate. Adds a session routing tally (simple/strong
counts and simple->strong escalations) for the observability surface.

* feat(smart-routing): add /smartroute command and env defaults

/smartroute shows status and sets/toggles the simple and strong roles from
agentModels keys, warning when the simple model is not first-party-cheaper than
the strong one. OPENCLAUDE_SMART_ROUTING(_SIMPLE/_STRONG) provide startup
defaults; an explicit settings block overrides env.

* feat(smart-routing): show routing summary in /cost

Appends a session routing summary (turns simple/strong, simple->strong
escalations) to /cost, with an estimated-savings line gated on first-party
pricing and annotated unavailable for unknown third-party pricing. Per-turn
cost is already attributed to the routed model via the existing per-model
breakdown.

* fix(smart-routing): re-pin to strong after a routed-error fallback

Without this, a turn's later continuation passes re-applied the pinned simple
model after a fallback, re-triggering the same failure each pass. Re-pinning to
strong keeps the rest of the turn on the recovered model.

* fix(review): provider-swap guard, tally reset, notice-storm, env docs

- Add the KTD6 provider-swap guard: drop the per-turn routing pin when a
  mid-turn provider-fallback swap changes the active provider, so the old
  provider's model id is not replayed at the new endpoint (adversarial P1).
- Reset the routing tally in resetCostState() so /cost does not show stale
  cross-session counts.
- Don't emit the disabled-for-session notice on every turn when no sessionId
  is available (suppress instead of storm).
- Document OPENCLAUDE_SMART_ROUTING* in the openaiShim env-var header.
- Add tests: provider-swap-safe pin, undefined-session silence, /smartroute
  strong arm and no-value guard.

* docs(smart-routing): document /smartroute, settings, and env vars

Register /smartroute in the web command catalog, add the smartRouting setting
and OPENCLAUDE_SMART_ROUTING* env vars to the configuration reference, add a
docs/smart-routing.md usage guide, and link it from the README.

* fix(review): clear tally on /login, extract+test swap predicate, cap disabled set

- /login used the raw bootstrap resetCostState, leaking the routing tally
  across an account switch; switch it to the cost-tracker wrapper.
- Extract the provider-swap drop check as a pure, tested
  shouldDropPinForProviderSwap() and use it in the query loop.
- Cap the disabledSessions set so a long-lived host can't grow it unbounded.
- Document the 404/429 retry-by-design rationale; add tests for it.
- Clarify the routedFallbackUsed per-turn scope and the apply-after-guard
  comment; document cross-provider role rejection and the re-enable path.

* test(smart-routing): make allowlist tests robust to cross-file module mocks

The decideTurnModel allowlist tests spied the global settings singleton, which
let another file's leaked mock.module of modelAllowlist (agent.test.ts) flip
isModelAllowed out from under them in the full suite. Spy isModelAllowed
directly and restore it in afterEach so the tests are deterministic regardless
of suite ordering.

* fix(smart-routing): address CodeRabbit review and green CI

- index.test.ts: pin the allowlist in the three happy-path decideTurnModel
  tests so they no longer inherit a leaked cross-file isModelAllowed mock
  (the CI test failure)
- smartroute/index.test.ts: narrow the LocalCommandResult union via an
  expectText helper instead of reading .value off the union (the CI
  typecheck failure)
- conversationRecovery.ts: route deserialize's thinking-strip gate through
  stripThinkingBlocksIfProviderAllows, removing the duplicated provider
  detection
- conversationRecovery.test.ts: replace the two as-any fixtures with a
  shared typed factory

* fix(smart-routing): scope cost claims to first-party reference pricing

Smart routing's savings estimate and "simple isn't cheaper" warning were
derived from the static first-party MODEL_COSTS table via getKnownInputCost,
with no knowledge of the active provider, gateway, or account pricing. For a
multi-provider user whose model ids happen to exist in that table but bill
differently, the /cost summary and /smartroute warning stated a savings figure
as if it reflected what they are actually charged.

Narrow the copy instead of inventing provider-aware pricing the code cannot
verify: the /cost line, the /smartroute warning, and docs/smart-routing.md now
label the numbers as first-party reference pricing and note the active provider
may bill differently. Tests assert the qualifier on every reworded branch so it
cannot silently regress. No routing logic changed.

* fix(smart-routing): clarify simple role wording

* Fix smart routing review findings

* fix(smart-routing): honor env roles and non-text turns

* test(smart-routing): cover non-text skip path

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-07 10:48:52 +08:00
JATMNandGitHub cd13a61537 fix(memory): recover from autocompact overflow failures (#1858)
* fix(memory): recover from autocompact overflow failures

* fix(memory): address autocompact review findings

* fix(memory): close autocompact recovery gaps

* fix(memory): reduce OpenAI conversion pressure

* test(memory): add long-session guard smoke

* fix(memory): add runtime memory guard diagnostics

* fix(memory): surface autocompact failure diagnostics

* fix(memory): reuse hard-cap resolver in diagnostics

* fix(memory): avoid hard-cap diagnostic drift

* fix(memory): clarify hard-cap diagnostics
2026-07-06 08:16:01 +08:00
BogdanandGitHub 5226fb9ee7 fix(query): configure hard max and abort reasons (#1850)
* fix(query): configure hard max and abort reasons

* fix(query): normalize legacy abort reasons

* test(query): dedupe abort classification setup
2026-07-05 11:48:46 +08:00
BogdanandGitHub 8182a46441 feat(report): render task reports as markdown (#1826) 2026-07-01 06:43:05 +08:00
259c7ec27a fix(ollama): preserve chat history with native context (#1805)
* fix(ollama): preserve chat history with native context

Route Ollama chat requests through the native /api/chat endpoint so OpenClaude can send request-level options.num_ctx instead of relying on Ollama's OpenAI-compatible shim.

Default the Ollama request context to 32768 tokens, support OPENCLAUDE_OLLAMA_NUM_CTX and OLLAMA_CONTEXT_LENGTH overrides, and map max tokens/temperature/top_p into native Ollama options.

Adapt native Ollama streaming and non-streaming responses back into the existing OpenAI-shaped conversion pipeline, including usage, text, structured tool calls, and tool_use stop reasons.

Normalize native Ollama request messages for images and historical tool calls, avoiding OpenAI-only image_url/id/type payload fields in /api/chat requests.

Add Ollama context diagnostics, loopback-only ollama ps status checks, regression coverage, and documentation for verifying active context length.

* fix(ollama): address native routing review feedback

* fix(ollama): restrict loopback host matching

* fix(ollama): exclude wildcard bind address

* fix(ollama): keep https localhost proxies on chat completions

---------

Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
2026-06-29 18:04:42 +08:00
BogdanandGitHub a47493342f feat(report): generate deterministic session task reports (#1802)
* feat(report): generate deterministic session task reports

* fix(report): address task report review findings

* fix(report): stabilize task report paths on Windows

* test(report): expect redacted git metadata cwd

* test(report): assert literal redacted git cwd

* fix(report): capture PowerShell and backgrounded validations

* fix(report): detect quoted validation commands

* fix(report): reconcile background validation notifications

* fix(report): keep foreground command statuses authoritative

* test(report): assert command status precedence
2026-06-29 17:57:09 +08:00
JATMNandGitHub cb689cc33a feat(effort): Add model-level reasoning effort routing (#1780)
* Add model-level reasoning effort metadata

Introduce per-model reasoning control metadata on catalog entries and model descriptors so /effort support can be expanded without provider-wide inference.

Resolve /effort through explicit model metadata first, preserve legacy allowlist behavior, and treat supportsReasoning-only entries as capability metadata that does not mutate requests.

Guard OpenAI shim effort serialization with the model-level wire support check and add focused tests for capability-only, explicit metadata, opt-out, and toggle-mode cases.

Document the reasoning metadata contract and provider follow-up workflow.

* Expand reasoning effort routing

Centralize OpenAI shim reasoning request planning so DeepSeek-compatible and Z.AI-compatible controls flow through the effort resolver instead of provider-specific shim helpers.

Add compatibility metadata handling for DeepSeek and Z.AI routes while keeping supportsReasoning-only catalog entries non-controllable until exact wire formats are verified.

Respect route removeBodyFields after compatibility serialization and make provider override support checks use the resolved override route/base URL instead of ambient provider metadata.

Document temporary compatibility rules and add regression coverage for Atlas DeepSeek, Z.AI levels, Groq stripping, providerOverride OpenAI effort, providerOverride Groq stripping, and non-generic metadata opt-out.

Verified: bun test --feature=UNATTENDED_RETRY src/utils/effort.codex.test.ts; bun test --feature=UNATTENDED_RETRY src/services/api/client.test.ts; bun test --feature=UNATTENDED_RETRY src/services/api/openaiShim.test.ts; node .\\node_modules\\typescript\\bin\\tsc --noEmit; bun run build; git diff --check.

* Fix Responses API reasoning effort shape

Send reasoning effort on OpenAI-compatible Responses requests using the nested reasoning object expected by the endpoint instead of flat reasoning_effort/reasoning_summary fields.

Keep chat_completions behavior unchanged so OpenAI-compatible chat endpoints still receive top-level reasoning_effort.

Add a regression test covering the Responses request body shape and verifying the flat fields are omitted.

Validation: bun test --feature=UNATTENDED_RETRY src/services/api/openaiShim.test.ts; node .\\node_modules\\typescript\\bin\\tsc --noEmit; git diff --check.

* Document reasoning effort metadata rules

Make the new reasoning-effort guide discoverable from the integrations overview and reading order.

Update model, gateway, and vendor onboarding docs to explain that supportsReasoning is descriptive only and does not enable /effort request mutation without verified per-model reasoning metadata.

Clarify that gateway and vendor catalogs must annotate reasoning controls per exact route/model rather than provider-wide.

Validation: git diff --check.

* Stabilize effort resolver tests

Add an optional reasoning control context so effort tests can inject provider, catalog, model descriptor, and shim metadata without mocking process-global integration/provider modules.

Update effort.codex tests to use the injected context and restore only the remaining local mocks, preventing mock leakage into later full-suite provider tests.

Validation: bun run check; bun run test:provider; bun run test:provider-recommendation; bun run typecheck:type-tests; bun run integrations:check; python -m pytest -q python/tests; bun run security:pr-scan -- --base upstream/main --head HEAD.

* Address effort PR review findings

Load integration registry before catalog reasoning lookup, isolate provider override route resolution from ambient routes, and carry explicit compat reasoning metadata into the OpenAI shim request planner.

Clarify reasoning metadata documentation and add focused regression coverage for compat metadata and provider override route preference.

* Fix Z.AI metadata high effort serialization

Include high in the Z.AI-compatible metadata gate so high-only reasoning metadata emits reasoning_effort instead of silently dropping the user-selected effort.

Add regression coverage for a high-only zai_compatible catalog entry flowing through the OpenAI shim request planner.

* Fix provider override effort fallback

Allow unrecognized providerOverride OpenAI-compatible routes to fall back to legacy effort support instead of dropping user-selected effort.

Constrain compat metadata levels to wire-faithful high/xhigh values and clarify reserved reasoning wire formats in docs and descriptors.

* Clamp provider override effort by route metadata

Resolve providerOverride effort against the override model and route context before converting it for the OpenAI shim, so stale persisted effort values respect per-model metadata levels.

Add regression coverage for high-only providerOverride metadata and explicit max filtering in compat metadata levels.
2026-06-25 10:10:53 +08:00
JATMNandGitHub dd4c4abc81 feat(api): add OpenAI-compatible credential pool failover (#1706)
* feat(api): rotate OpenAI credential pools

* fix(api): align pooled credential discovery

* fix(cache-probe): preserve GitHub credential precedence

* fix(provider): honor pooled OpenAI fallbacks

* fix(provider): validate pooled profile credential labels

* fix(api): harden OpenAI credential pool handling

Reject placeholder values in pooled OpenAI credentials before requests, discovery, diagnostics, and profile generation can use them.

Normalize pooled credentials to a single usable key for model discovery, runtime cache partitions, cache probing, and NVIDIA NIM cache lookups.

Preserve documented profile precedence by letting live shell credentials override saved pools, carrying OpenCode fallback pools through launch, and redacting individual pool members in profile display.

Add regression coverage for pooled credential validation, profile launch/rebuild behavior, discovery/cache callers, diagnostics, provider autodetect, and shim failover semantics.

* fix(provider): cover pooled key recommendation path

Import the pooled OpenAI credential validator in provider-recommend and split invalid credentials from unset credentials in user guidance.

Add a script-level regression that runs the OpenAI recommendation path with OPENAI_API_KEYS so the ts-nocheck script cannot regress with runtime ReferenceErrors.

Scrub pooled OpenAI keys before xAI OAuth profile env construction and loosen the invalid-pool discovery test to assert auth header absence instead of exact header shape.

* fix(tests): stabilize rebased provider checks

* fix(provider): address pooled credential review findings

* test(api): cover opencode go credential failover

* fix(provider): share OpenAI credential usability checks

* fix(provider): respect pooled credential precedence

* fix(model): preserve pooled discovery credential precedence

* fix(model): fall back from unusable pooled discovery keys
2026-06-23 12:34:55 +08:00
euxaristiaandGitHub 38b0e27333 fix(opencode-go): sync model catalog with opencode.ai/go (#1745)
* fix(opencode-go): sync model catalog with opencode.ai/go

The OpenCode Go subscription page (https://opencode.ai/go) lists 13
models, but the catalog had 20. Remove the 7 models no longer offered:
glm-5, kimi-k2.5, minimax-m2.5, qwen3.5-plus, mimo-v2-pro,
mimo-v2-omni, hy3-preview.

Catalog now matches the page exactly:
- OpenAI-compatible: GLM 5.2, GLM 5.1, Kimi K2.7 Code, Kimi K2.6,
  DeepSeek V4 Pro, DeepSeek V4 Flash, MiMo V2.5 Pro, MiMo V2.5
- Anthropic messages: MiniMax M3, MiniMax M2.7, Qwen3.7 Max,
  Qwen3.7 Plus, Qwen3.6 Plus

Updates gateway catalog, model descriptors, generated artifacts, and
tests.

* fix(opencode-go): reorder model catalog to match opencode.ai/go listing

Reorder both the gateway catalog and model descriptor lists to match the
order models appear on https://opencode.ai/go, so the in-app model picker
mirrors the subscription page. No model added or removed — purely a
reorder.

GLM-5.2 → Qwen3.7 Max → Kimi K2.7 Code → MiMo V2.5 Pro → DeepSeek V4 Pro
→ Qwen3.7 Plus → MiniMax M3 → MiMo V2.5 → DeepSeek V4 Flash → GLM 5.1
→ Kimi K2.6 → Qwen3.6 Plus → MiniMax M2.7

* test(opencode-go): assert exact model set matches opencode.ai/go catalog

Address CodeRabbit review on #1745 — the count-only test wouldn't catch
catalog drift. Add a strict set assertion verifying the 13 expected IDs
are present and no removed/unexpected IDs remain.

* test(opencode-go): update Anthropic Messages route test for refreshed catalog

The direct-env-routing test listed minimax-m2.5 and qwen3.5-plus, which
were removed from the opencode-go catalog. Replace with the five
/messages-endpoint models that remain: minimax-m3, minimax-m2.7,
qwen3.7-max, qwen3.7-plus, qwen3.6-plus.

Unblocks the smoke-and-tests CI check on #1745.

* fix: update OpenCode Go model references to 13 models and add assertions
2026-06-23 09:30:08 +08:00
JATMNandGitHub 5625f4217d fix: preserve provider route context metadata (#1741)
* fix: preserve provider route context metadata

Resolve issue #1732 by keeping active provider-profile routes attached during context limit resolution and making gateway-prefixed model IDs resolve against provider-scoped metadata instead of falling through to global aliases.

Preserve composite provider-path suffixes such as accounts/fireworks/models/... and fireworks/models/... before generic last-segment matching, so wrapped gateway IDs resolve Fireworks-specific limits instead of generic descriptors.

Refresh OpenCode Zen and OpenCode Go catalog metadata, including route-specific context/output limits, regenerate integration artifacts, add DeepSeek V4 Pro on NVIDIA NIM, add the Gemini 3.1 Pro router alias, and update user-facing OpenCode model counts.

Scope OpenCode descriptor default model names to OpenCode routes via providerModelMap so unprefixed vendor lookups are not hijacked by gateway descriptors. Add wrapper-path assertions for the user-facing max output token helper.

Add regression coverage for provider-prefixed gateway models, active-profile route preservation, account-qualified composite paths, and OpenRouter-wrapped fireworks/models/... paths. Harden Windows/full-suite validation by normalizing plugin hook display paths and resetting status-redaction HOME/USERPROFILE state.

Validation: bun install; bun run build; bun run smoke; bun run typecheck; bun run typecheck:type-tests; bun run check (4634 pass, 0 fail); bun run test:provider (857 pass, 0 fail); bun run test:provider-recommendation (91 pass, 0 fail); bun run integrations:check; bun run security:pr-scan -- --base upstream/main; git diff --check. Follow-up validation: bun test src/integrations/runtimeMetadata.test.ts --max-concurrency=1; bun test src/utils/context.test.ts src/integrations/runtimeMetadata.test.ts src/integrations/gateways/opencode.test.ts --max-concurrency=1; bun run test:provider; bun run integrations:check; bun run typecheck; git diff --check.

# Conflicts:
#	src/integrations/gateways/opencode-go.ts
#	src/integrations/models/opencode.ts

* fix: align OpenCode context metadata

Refresh OpenCode Zen and Go descriptor context/output limits against the live OpenCode model lists and Models.dev provider metadata.

Add a regression assertion for provider-specific OpenCode limits so route-scoped metadata does not fall back to generic model budgets.

* fix: remove duplicate Gemini model descriptor

Keep the canonical Gemini 3.1 Pro descriptor and rely on provider-prefixed suffix matching for google/gemini-3.1-pro runtime lookups.

Also separate the OpenCode limit regression test from the following assertion for readability.

* fix: preserve OpenCode Go messages auth metadata

* test: cover OpenCode Go review cases
2026-06-22 13:35:31 +08:00
BogdanandGitHub 5af6f95c46 feat(config): add explicit provider env-file loading (#1668)
* feat(config): add explicit provider env-file loading

* fix(config): handle escaped quotes in provider env files

* fix(config): polish env-file parser review feedback

* fix(config): preserve provider env-file precedence

* test(config): cover provider env-file precedence

* fix(config): preserve provider env-file values

* fix(config): allow documented env-file setup vars

* fix(config): preserve provider flag precedence
2026-06-18 08:51:59 +08:00
BogdanandGitHub 7c034c5a62 feat: add redacted diagnostic issue reports (#1647)
* feat: add redacted diagnostic issue reports

* fix: address diagnostic report review feedback

* fix: report Codex runtime diagnostics accurately
2026-06-16 08:41:18 +08:00
BogdanandGitHub d8dbf274b4 chore(runtime): align Node.js minimum version (#1644)
* chore(runtime): align Node.js runtime requirements

* test(runtime): cover prefixed Node versions

* fix(runtime): check node executable in doctor
2026-06-16 06:55:17 +08:00
Ahmar YaseenandGitHub 661b5ad8cd docs(non-technical-setup): add Getting Help section with support link… (#1631)
* docs(non-technical-setup): add Getting Help section with support links and diagnostic check

The non-technical setup guide ended abruptly after the advanced setup
reference. New users — especially non-technical ones — had no clear path
to follow when they encountered issues beyond the common problems listed.

Add a Getting Help section at the end that includes:
- GitHub Discussions link for Q&A and setup help
- GitHub Issues link for bugs and feature requests
- A quick diagnostic check (openclaude --version) with actionable
  next steps for when the command is not found

This also cross-references the Windows Quick Start guide for npm Path
configuration, reusing existing documentation rather than duplicating
instructions.

Impact: improves first-run experience for the guide's target audience
without changing any code or behavior.

* docs(non-technical-setup): add Getting Help section and restructure around /provider flow

The guide previously directed users to set environment variables before launching the CLI. This misses the simpler onboarding path: users can install, run openclaude, and use /provider for guided setup inside the CLI without any env vars.

Changes:
- Remove required API key from 'Before You Start' prerequisites
- Rewrite 'Fastest Path' to recommend install -> run -> /provider
- Remove env variable step from the fast path
- Add note under provider descriptions that /provider handles setup
- Update 'Invalid API key' troubleshooting to use /provider instead of re-pasting env vars
- Add 'Getting Help' section with support links and diagnostic check

Impact: better aligns the non-technical guide with the current recommended onboarding flow. No code changes.

* docs(non-technical-setup): restore API key prerequisite, restructure Fastest Path around /provider

Re-add the API key to prerequisites. The key is still needed - the /provider flow is just the recommended way to enter it instead of env vars.

Changes:
- Keep API key in prerequisites
- Rewrite Fastest Path to install -> run -> /provider instead of env vars
- Add note under provider descriptions that /provider handles setup
- Update Invalid API key troubleshooting to use /provider
- Add Getting Help section with support links and diagnostic check

Impact: aligns the non-technical guide with the current recommended onboarding flow. No code changes.
2026-06-15 08:49:49 +08:00
JATMNandGitHub de726c43e1 Fix custom provider context discovery (#1620)
* Fix custom provider context discovery

Teach the custom OpenAI-compatible gateway to discover context windows from /v1/models metadata, including LiteLLM model_info context_length and max_input_tokens fields.

Use cached discovery metadata when resolving runtime context and output limits, with sync cache reads kept memoized and partitioned by endpoint, credential, and custom headers.

Add provider-profile maxContextLength env overrides and document LiteLLM context metadata plus the CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS fallback.

Cover startup discovery, runtime cache lookup, custom gateway parsing, profile overrides, and env custom-header cache partitioning with focused tests.

* Fix discovery smoke test isolation

Clear CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC during discovery service test setup so full-suite environment state cannot force startup discovery down the nonessential-traffic skip path.

Verified with:

- bun test ./src/integrations/discoveryService.test.ts

- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 targeted startup discovery test

- bun run smoke

- bun run typecheck

* Partition custom discovery startup test cache

Use a test-only custom header in the startup custom route discovery test so it exercises network discovery even when the full suite has pre-seeded the no-header custom discovery cache key.

Verified with:

- bun test ./src/integrations/discoveryService.test.ts

- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 targeted startup discovery test

- bun run smoke

- bun run typecheck

* Fix profile context override lifecycle

Add CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS to managed profile cleanup so switching profiles clears stale context-window overrides, including same-model OpenAI-compatible switches.

Preserve persisted context-window overrides when rebuilding OpenAI-compatible startup env after restart.

Verified with:

- bun test src/utils/providerProfile.test.ts src/utils/providerProfiles.test.ts

- bun run typecheck

- bun run smoke

* Detect profile context override drift

Include CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS in OpenAI-compatible active-profile env alignment so managed profiles with maxContextLength are re-applied when the override is missing or stale.

Verified with:

- bun test src/utils/providerProfiles.test.ts

- bun run typecheck

- bun run smoke
2026-06-14 20:37:11 +08:00
0b24b60ce9 feat(provider): add Fireworks AI as official OpenAI-compatible provider (#1590)
* feat(provider): add Fireworks AI as official OpenAI-compatible provider

Includes vendor descriptor, brand descriptor (276 models), model
descriptors (full + merged), routing metadata, env auto-detection,
profile support, client defaults, and docs.

* test: add focused regression tests for Fireworks AI auth and routing

- Add 7 env-only routing tests in client.test.ts (shim routing,
  stale model replacement, base URL override, shim option cleanup,
  non-Fireworks override ignored, priority with MiniMax, Bedrock yield)
- Add FIREWORKS_API_KEY auto-detection test in providerAutoDetect.test.ts
- Add profile apply/persistence/env-drift tests in providerProfiles.test.ts
- Fix FIREWORKS_API_KEY propagation in strictEnv early return path

* fix: address reviewer comments on Fireworks integration

- Remove OPENAI_API_KEY exclusion so Fireworks cred wins over stale OpenAI key
- Fix TS type error in test by using String() wrapper
- Add Fireworks to detection priority comment in providerAutoDetect.ts
- Add useFireworksEnvOnlyProvider to shim condition for pattern consistency
- Replace loose .includes('fireworks.ai') with isFireworksBaseUrl() exact hostname check

* fix: add explicit case 'fireworks' in applyProviderFlag for credential precedence

- Add 'fireworks' to PREFERRED_PROVIDER_ORDER
- Add case 'fireworks' with dedicated key winning pattern (mirrors atlas-cloud)
- Add FIREWORKS_API_KEY to copiedOpenAIKeyProvider detection so stale
  keys are cleaned up when switching away from Fireworks

* fix: guard fireworks defaultModel assignment against 'undefined' string coercion

* fix: remove leftover conflict marker in providerProfiles.ts

* docs(fireworks): add JSDoc to Fireworks functions for coderabbit docstring coverage

Adds JSDoc annotations to isFireworksBaseUrl, getFireworksBaseUrlOverride,
hasFireworksEnvOnlyProviderIntent, isFireworksModelName, and
applyFireworksEnvOnlyDefaults.

* fix(fireworks): cross-check NEARAI_API_KEY in env-only intent functions

hasNearaiEnvOnlyProviderIntent and hasFireworksEnvOnlyProviderIntent were
missing mutual cross-checks. When both NEARAI_API_KEY and FIREWORKS_API_KEY
are set, neither excludes the other, and nearai silently wins by ordering.
Adding !hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) to the nearai intent
and !hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) to the fireworks intent
ensures both return false, forcing explicit provider selection.

* fix(fireworks): fix typo in JSDoc — OPENAI_API_API_BASE -> OPENAI_API_BASE

* fix(fireworks): remove merge artifact and preserve no-key auth headers

- src/utils/providerAutoDetect.ts: remove leftover ======= conflict
  marker and stale duplicate priority lines
- src/utils/providerProfiles.ts: preserve apiFormat, authHeader,
  authScheme, authHeaderValue in the no-key OpenAI-compatible
  fallback path so saved Responses mode / custom auth config
  survives restart

* fix: Fireworks env-only startup preservation and MIMO priority comment

- Add FIREWORKS_API_KEY check to hasConcreteProviderSelection() so env-only
  Fireworks setup is not overwritten by Gitlawb Opengateway default
- Add regression test verifying FIREWORKS_API_KEY survives no-profile startup
- Fix providerAutoDetect.ts priority comment to include MIMO_API_KEY (position 8)
  and renumber subsequent entries to match actual detection order

* fix: also preserve env-only NEAR AI startup in hasConcreteProviderSelection()

* fix: remove duplicate Fireworks model descriptor, add FIREWORKS_API_KEY to test env cleanup

* fix: move duplicate model check to generation-time, add OPENAI_AUTH_* env cleanup to test harness

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-13 01:10:19 +08:00
TortesandGitHub 8f92346cf4 fix: avoid file suggestion OOM on large repos (#1074)
* fix: avoid file suggestion OOM on large repos

* fix: handle ignore scope and abort semantics in file suggestions

* test: stabilize rebased branch CI verification

* test: make proxy env cleanup windows-safe

* test: preload file suggestions module in setup

* fix: keep file suggestions lazy on startup

* chore: address final review nits
2026-06-12 23:04:46 +08:00
eacc7d8fac feat: add NEAR AI provider integration (#1594)
* feat: add NEAR AI provider integration

- Create vendor, brand, and model descriptors for NEAR AI (22 models)
- Add NEAR AI to route metadata, client, provider auto-detect, and profiles
- Update compatibility tests and ProviderManager test PRESET_ORDER
- Add README and docs entries for NEAR AI provider
- Update .env.example with NEAR AI configuration

* fix: address CodeRabbit review comments

- Fix .env.example: change 'Option N' to 'Option 11' in quick reference
- Narrow isNearaiModelName to use explicit NearAI model prefixes instead of broad includes('/')
- Add NEARAI_API_KEY propagation in strictEnv startup path

* fix: align NEAR AI validation host matching with wildcard subdomain routing

- Add *.completions.near.ai to matchBaseUrlHosts in vendor descriptor
- Add matchHostnameAgainstRouteHosts helper with wildcard (*.) prefix support
- Use helper in both resolveRouteIdFromBaseUrl and getRuntimeValidationTarget
- Add regression test for qwen35-122b.completions.near.ai TEE endpoint
- Add NEARAI_API_KEY to test env cleanup list

* fix: align Near AI integration with env-only provider best practices

- Replace loose .includes('near.ai') with isNearaiBaseUrl() in providerProfiles.ts
  for exact hostname validation (all 4 instances)
- Add NEARAI_API_KEY to copiedOpenAIKeyProvider detection in providerFlag.ts
- Add case 'nearai' to applyProviderFlag switch with dedicated key precedence
- Add 'nearai' to PREFERRED_PROVIDER_ORDER
- Add useNearaiEnvOnlyProvider to OpenAI shim condition in client.ts
- Remove OPENAI_API_KEY exclusion from hasNearaiEnvOnlyProviderIntent (dedicated
  key wins over stale generic key, consistent with xAI pattern)
- Update detection priority comment in providerAutoDetect.ts to include
  MIMO_API_KEY, XAI_API_KEY, and NEARAI_API_KEY

* fix: add exact completions.near.ai host to isNearaiBaseUrl

* fix: add higher-precedence provider key exclusions to hasNearaiEnvOnlyProviderIntent

* fix: add OPENAI_API_KEY and MINIMAX_API_KEY exclusions to hasNearaiEnvOnlyProviderIntent

* fix(near-ai): don't let stale OPENAI_API_KEY suppress Near AI routing

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-11 12:09:25 +08:00
JATMNandGitHub 14036209cd Add configurable message-count compaction (#1587)
* Add configurable message-count compaction

Add a /config setting for opting into message-count-based compaction thresholds and persist it in global config.

Disable the legacy OPENCLAUDE_MAX_ACTIVE_MESSAGES default unless the new setting is off and the environment variable is explicitly set.

Add a timeout around forked compact summaries using a child abort controller so timeouts and user aborts clean up without affecting the main thread.

Document the diagnostic setting and normalize trailing line endings in Windows alias docs/script.

* Address compaction PR review feedback

Add a shared literal enum and normalizer for message-count compaction thresholds, use it in config, /config UI, and query threshold handling.

Move the compact timeout constant to module scope and mark the /config docs snippet with a text fence.
2026-06-10 13:46:53 +08:00
286d403093 Update(zen-go): add claude-opus-4-8, minimax-m3, mimo-v2.5-free models and proper effort level integration for Zen/Go models (#1505)
* feat(provider): add OpenCode Zen/Go subscription support

Add OpenCode as a first-class provider, enabling users to connect their
Zen (pay-as-you-go) and Go ($10/mo) subscriptions via the /provider command.

New integration descriptors:
- vendors/opencode.ts — OpenCode Zen vendor (41 models)
- gateways/opencode-go.ts — OpenCode Go gateway (12 models)
- brands/opencode.ts — brand descriptor
- models/opencode.ts — full model catalog (GPT, Claude, Gemini, Qwen,
  GLM, Kimi, MiniMax, Grok, DeepSeek, MiMo, Nemotron)

Modified files:
- integrationArtifacts.generated.ts — register descriptors and presets
- providerProfile.ts — add OPENCODE_API_KEY env/secret key, 'opencode'
  profile type, and buildLaunchEnv handler
- providerConfig.ts — add DEFAULT_OPENCODE_BASE_URL constants

Auth: OPENCODE_API_KEY env var or interactive key entry in /provider
Transport: openai-compatible (chat_completions)
Base URLs: https://opencode.ai/zen/v1 (Zen), /zen/go/v1 (Go)

* feat(provider): add [Zen]/[Go] tags to OpenCode preset labels

Add visual tags in the /provider preset selection to distinguish
OpenCode Zen (pay-as-you-go) from OpenCode Go (subscription).

* feat(provider): enable dynamic model discovery for OpenCode

Switch OpenCode vendor and Go gateway from static to hybrid model
catalog with openai-compatible discovery. Models are fetched from
/v1/models on startup and cached for 1 hour. Manual refresh is
supported via the /provider UI.

Static model list is preserved as fallback when discovery fails.

* test(provider): add comprehensive OpenCode Zen/Go test suite

97 tests across 2 files covering:

Integration tests (72 tests):
- Vendor descriptor: id, label, classification, base URL, model, auth,
  transport, preset, validation, catalog, discovery, usage metadata
- Gateway descriptor: id, label, vendorId, category, base URL, model,
  auth, transport, preset, catalog, discovery
- Brand descriptor: id, label, canonicalVendorId, capabilities, modelIds
- Model catalog: registration, vendor/gateway associations, required
  fields, valid classifications, reasoning/coding tags, no duplicates,
  model counts (41 Zen, 12 Go), modelDescriptorId consistency
- Cross-reference: brand↔model, vendor↔model, gateway↔model,
  shared OPENCODE_API_KEY
- Registry validation: no errors, no preset conflicts
- Edge cases: unique ids, unique apiNames, non-empty labels, valid
  contextWindow/maxOutputTokens, valid defaultModel format, validation
  message content, discovery config

Profile tests (25 tests):
- Type guard: isProviderProfile('opencode'), rejects invalid values
- buildLaunchEnv: persisted env, defaults, process env precedence,
  OPENCODE_API_KEY mapping, whitespace/null/undefined/empty handling,
  very long keys, special characters, concurrent access, boundary
  values, no credential leakage

* fix(provider): add per-model endpoint routing (P1)

Add endpointPath field to OpenAIShimTransportConfig so catalog entries
can specify which API path to use per model. This addresses the
maintainer's [P1] finding that all models were routed to
/chat/completions regardless of their upstream endpoint.

Changes:
- descriptors.ts: add endpointPath?: string to OpenAIShimTransportConfig
- openaiShim.ts: buildRequestUrl checks shimConfig.endpointPath first
- vendors/opencode.ts: add transportOverrides to 31 catalog entries
  (GPT→/responses, Claude/Qwen→/messages, Gemini→/models/<id>)
  + switch to source: 'static' to prevent free models from live API
- gateways/opencode-go.ts: add transportOverrides to 4 entries
  (MiniMax/Qwen→/messages) + switch to source: 'static'
- opencode.test.ts: update tests for static source, remove discovery tests

* refactor(opencode): model OpenCode Zen/Go as gateways (P2)

* docs(provider): document OpenCode setup and move badge metadata to descriptors

- Add OpenCode Zen/Go rows to README supported providers table
- Add OpenCode Zen/Go examples and OPENCODE_API_KEY to advanced-setup.md
- Add PresetBadge type to descriptor/manifest with badge propagation in
  artifact generator
- Move 4 hard-coded preset badges ([FREE], [Sponsor], [Zen], [Go]) from
  ProviderManager.tsx into descriptor preset metadata
- Add badge field to providerUiMetadata so UI components read from manifest
- Update integration overview docs to recommend preset.badge for future
  gateways

* fix(provider): match request body to endpoint format for OpenCode /messages and /responses (P1)

Extend the openaiShim transport so that endpointPath overrides select
both the URL and the correct body/response format:

- /responses → OpenAI Responses API body (input, max_output_tokens)
- /messages  → Anthropic Messages API body (content blocks, system, max_tokens)

Also fixes: abort listener leak in SSE passthrough, system prompt
content-block flattening, and removes [Zen]/[Go] badge entries (P3).

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(provider): add Google AI SDK body/response format for OpenCode Zen Gemini models (P1)

The three Gemini models in the OpenCode Zen catalog (gemini-3.5-flash,
gemini-3.1-pro, gemini-3-flash) were sending chat-completions body to
the /models/gemini-* endpoint, which expects Google AI SDK format.

- effectiveTransport now detects /models/gemini- endpointPath → 'gemini'
- buildGeminiBody() converts Anthropic messages → Google contents[]
  with role mapping, systemInstruction, generationConfig, functionDeclarations
- geminiSseToAnthropic() parses Google SSE frames → Anthropic stream events
  with text deltas, functionCall tool_use, finishReason mapping
- _convertGeminiToAnthropicResponse() for non-streaming responses
- Streaming/non-streaming routing via URL detection (/models/gemini-)
- serializeBody(), hasToolsPayload, omitGeminiTools all updated

* fix: prevent OpenCode model descriptors from shadowing canonical limits

P1: Prefix all defaultModel values in opencode.ts with 'opencode-'
so the fallback findModelDescriptorForApiName() doesn't match
canonical model names. The OpenCode descriptors are still found
via catalog entry lookup when the OpenCode route is active.

P2: Add 'OpenCode Go' and 'OpenCode Zen' to PRESET_ORDER in
ProviderManager.test.tsx between 'OpenAI' and 'OpenRouter'
so navigateToPreset() sends the correct number of j keypresses.

* fix: align OpenCode Go descriptor metadata with Zen

- category: 'hosted' → 'aggregating' (both are aggregating gateways)
- add validation block with OPENCODE_API_KEY guidance
- update test assertion from 'hosted' to 'aggregating'

* fix: accept OPENAI_API_KEY as fallback in OpenCode validation

When users set up OpenCode Zen/Go via /provider, the key is saved as
OPENAI_API_KEY (via buildCompatibilityProcessEnv). The validation block
only checked OPENCODE_API_KEY, causing a startup warning even though
the runtime auth header had the key it needed.

Add OPENAI_API_KEY to validation.credentialEnvVars for both gateways,
matching the pattern used by Hicap and Gitlawb Opengateway.

* chore: trigger mergeability recheck

* feat(shim): forward effort/thinking to OpenCode Zen/Go endpoints

- buildResponsesBody: add reasoning_effort + reasoning_summary + include
- buildAnthropicMessagesBody: add thinking config (adaptive/enabled/budget)
- buildGeminiBody: add thinkingConfig with thinkingLevel mapping
- modelSupportsEffort: allow OpenCode Claude and Gemini models
- modelSupportsMaxEffort: add opus-4-7
- getAvailableEffortLevels: show standard levels for OpenCode native models
- opencode-go: add missing validation block

* feat: update OpenCode Zen and Go model counts, add new models, and enhance effort level handling

* feat: implement xhigh effort support for specific models and adjust effort level handling

* fix(effort): address reviewer feedback on xhigh + new models

- docs/advanced-setup.md: bump OpenCode Go count 12 → 13
- openaiShim.ts: include opus-4-8 / opus-4.8 in the adaptive thinking
  detection so the new model uses the adaptive + effort path instead
  of falling back to budgetTokens
- effort.ts: modelUsesOpenAIEffort now also rejects models that include
  'claude-' or 'gemini-' — without this, OpenCode Claude/Gemini
  routes (provider=openai) were misclassified as OpenAI-style and
  could leak xhigh past the new gate
- effort.codex.test.ts: lock in the new exclusion with a regression
  test against the openai provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): address reviewer feedback on xhigh effort + new models

Closes the three P2 findings from PR #1505 review:

1. Settings schema now accepts 'xhigh' so a persisted xhigh survives
   restart instead of being silently dropped by .catch(undefined).
2. ModelPicker /effort cycle is driven by getAvailableEffortLevels(model)
   instead of a boolean includeMax, so models supporting xhigh
   (opus-4-7/4-8, OpenAI/Codex) can actually select it from the picker.
   displayEffort clamp now uses the available levels list, so stale
   xhigh also clamps to high when the focused model doesn't support it.
3. SDK/control metadata uses getAvailableEffortLevels(model) instead of
   the EFFORT_LEVELS fallback that advertised xhigh to every max-capable
   model. SDK schema + generated types extended to include 'xhigh'.

Also fixes a latent generator bug: the array case in generate-sdk-types
now parenthesizes union/intersection elements so the trailing [] binds
the whole type, e.g. ("a"|"b")[] rather than "a"|"b[]. Without this,
the regenerated xhigh levels ended up typed as the single-literal
"xhigh"[] and broke the modelInfo assignability check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(effort): order xhigh before max in EFFORT_LEVELS

EFFORT_LEVELS now matches getAvailableEffortLevels() output order
(['low', 'medium', 'high', 'xhigh', 'max']), and the order asserted by
the existing effort.codex.test.ts tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(effort): order xhigh before max in settings + SDK schemas

Matches the EFFORT_LEVELS / getAvailableEffortLevels order from the
previous commit. The Zod enum order doesn't affect runtime validation,
but keeps the source consistent and avoids confusion if anyone reads
the enum literal to infer display order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): clamp ModelPicker selection and mark xhigh as current

- ModelPicker.handleSelect: clamp the emitted/persisted effort to the
  focused model's available levels so a toggled-but-unsupported level
  (e.g. 'xhigh' on a model that doesn't support it) is never written
  to settings.json or handed to the consumer. Add focusedAvailableLevels
  + focusedDefaultEffort to the memo guard so the function regenerates
  when the focused model changes.
- EffortPicker: compare the xhigh option against the persisted 'xhigh'
  level directly. The 'max' alias path is kept only for legacy
  settings.json values that still hold 'max' from before xhigh was
  introduced.

* docs(effort): fix stale EffortPicker comment about xhigh normalization

openAIEffortToStandard is a type cast that passes 'xhigh' through as a
first-class EffortLevel — the shim only converts to 'max' at the
Anthropic request boundary, not here. Update the comment to match.

* docs(effort): update /effort help to match xhigh support matrix

The /effort --help output still described max as "Opus 4.6 only" and
xhigh as an "alias for max", but this PR promotes xhigh to a first-class
EffortLevel and allows it for OpenCode Claude Opus 4.7/4.8 (with max
also allowed for those Opus variants). Update the help so it matches
the picker/runtime behavior:
- max: "(Opus 4.6+)"
- xhigh: "(OpenAI/Codex and Opus 4.7+)"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sdk): address reviewer P2 — sync xhigh across override union, schemas, CLI

- Add 'xhigh_effort' to ModelCapabilityOverride union so the new
  call at effort.ts:93 typechecks (P2 finding 1).
- Add 'xhigh' to AgentDefinition.effort enum (coreSchemas.ts) and
  control.applied.effort enum (controlSchemas.ts), then regenerate
  coreTypes.generated.ts so the SDK public contract matches the
  first-class effort level (P2 finding 2).
- Add 'xhigh' to the --effort CLI flag allowed list and help text
  (main.tsx:945-951) so users can actually pass --effort xhigh
  instead of hitting "It must be one of: low, medium, high, max".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(effort): narrow allowlist to shim-serialized models; sync max description

Address reviewer findings on PR #1505:

P2: The broad `m.includes('opus-4') || m.includes('sonnet-4')` branch
made older variants (claude-opus-4-1, claude-sonnet-4-5) advertise
effort support, but the Anthropic /messages shim only serializes
low/medium as anthropicBody.effort for the isAdaptive || isOpus45
set (opus-4-5/4-6/4-7/4-8, sonnet-4-6). For other models the shim
only emits thinking for high/max, so low/medium on those models
was silently dropped on the wire. Collapse the two 4-model branches
into one that matches the shim's serialization set; the substring
match still covers prefix variations (claude-, opencode-claude-).

P3: getEffortLevelDescription('max') said "Opus 4.6 only" but
modelSupportsMaxEffort now allows opus-4-6, opus-4-7, opus-4-8.
Update the shared description to "Opus 4.6+" so the picker and
/effort confirmation agree with the new support matrix (matching
the /effort --help text from 3cf5de2).

Add effort.codex.test.ts coverage: assert that opus-4-5/4-6/4-7/4-8
and sonnet-4-6 support effort, while opus-4-1, opus-4-2, and
sonnet-4-5 do not (the latter three were previously true via the
broad substring match and are now correctly excluded).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger CodeRabbit re-review

* fix(effort): gate modelSupportsXHighEffort on modelSupportsEffort

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 08:43:20 +08:00
JoneSSLandGitHub 07c1c56b4f Add Azure / Foundry launch support to VS Code extension (#1365)
* Enhance OpenClaude VS Code extension with Microsoft Foundry / Azure OpenAI support. Added configuration options for Azure API key, endpoint, and deployment settings. Updated README and documentation for new features, including a setup wizard for Azure integration. Improved terminal launch environment handling for Azure compatibility.

* Fix packaged Windows helper runtime references

* Use installed CLI from Windows helper aliases

* Scope Windows helper env overrides to invocation

* Align Windows alias docs with shipped helper
2026-06-09 06:27:18 +08:00
73a2833819 feat(sponsors): add Atlas Cloud sponsor and sponsored tip (#1536)
Add Atlas Cloud (atlascloud.ai) to the README sponsors table with its
banner asset, and add an Atlas Cloud sponsored tip to the tip catalog.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 07:13:33 +08:00
chioarubandGitHub 169d0f737d Add provider-profile model picker modes (#1472)
* fix(model): preserve discovered models for active profiles

* test: isolate attribution settings state

* fix(model): add provider profile picker modes

* docs: document provider profile model picker mode

* Preserve cached legacy model options on empty refresh
2026-06-03 19:27:11 +08:00
JATMNandGitHub 3be54de16b Make OpenGateway the default startup provider (#1493)
Default fresh installs to the Gitlawb OpenGateway profile, keep validation behavior for saved profiles, and mark OpenGateway as the recommended provider in the picker.

Update setup docs and generated integration metadata to reflect the API-key-backed OpenGateway route, and add coverage for the fresh-install startup environment.
2026-06-03 08:45:12 +08:00