65 Commits
Author SHA1 Message Date
Kevin CodexandOpenClaude 50d4058861 feat: publish as @twigpine/openclaude (#2269)
The npm package moves from the @gitlawb scope to @twigpine. Everything that
names the package now uses the new name, and the release workflow can create
the new package and close out the old one.

Rename
- package.json, bun.lock, release-please config, docs, website, VS Code
  extension and tests use @twigpine/openclaude.
- scripts/build.ts takes the baked-in package name from package.json instead
  of repeating it; scripts/verify-clean-install.ts does the same.

One final release under the old name, then nothing
- Shipped builds have no working auto-updater (the no-telemetry plugin stubs
  it out), so people update by re-running the install command. Anyone still
  typing the old name lands on whatever is latest there.
- release.yml publishes this release once more as @gitlawb/openclaude. That
  build shows a startup notice, and `openclaude update` and `/update` print
  the same note and stop, saying the package moved and how to switch.
- legacy-scope-gate limits this to a single release. It only runs while the
  old name's latest version is still 0.31.0.
- There is no in-place migration. Both packages own the `openclaude` bin, so
  npm refuses to install one over the other, and a forced install is undone
  by uninstalling the old package. The docs give the safe order.

Tokenless releases (OIDC only)
- Both publish steps authenticate through npm trusted publishing and attach
  provenance; no npm token is used anywhere. npm only attaches a trusted
  publisher to a package that already exists, so @twigpine/openclaude is
  created once by hand as an empty 0.0.0 placeholder before its first release.
- Publish steps skip a version that is already on the registry, publish the
  verified dist/ with --ignore-scripts, and report provenance and whether a
  trusted publisher was used in the run summary
  (scripts/npm-publish-report.ts).
- The legacy-named build is built and install-verified before anything is
  published.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-10-12 11:21:36 +08:00
GravireiandGravirei afa71563b6 chore(rebrand): update Gitlawb references to Twigpine (#2246) (#2248)
* chore(rebrand): update Gitlawb references to Twigpine (#2246)

The GitHub org has been renamed to Twigpine, so point repo URLs, badges,
CI repo-identity guards, and user-facing sponsor/partner labels at the new
name.

Scope is deliberately limited to references that are safe to change today:

- GitHub URLs (github.com, api.github.com, raw.githubusercontent.com),
  including the skills registry and release-please release notes
- The release-please release trigger, which gates on the repo slug and
  would otherwise stop publishing
- User-visible display strings: gateway label/name, AI/ML API partner
  name, sponsored-tip sponsor, and docs/site prose

Left on the old name because the rename has not landed for them:
- `@gitlawb/openclaude` npm scope (the package does not exist on npm)
- opengateway/ads/docs subdomains, which resolve only under gitlawb.com
- The `gitlawb-opengateway` preset id, tip ids, and gateway file names,
  which are persisted in user config and sent on the wire

* fix(aimlapi): stop matching topup intent on partnerName (#2246)

partnerName is display metadata that travels alongside the partner id, not
part of the intent identity. It is now excluded from INTENT_KEYS, so
matchesIntent no longer requires it to be unchanged.

Without this, renaming the partner on the Gitlawb -> Twigpine rebrand would
have stranded any in-flight topup: a receipt persisted under the old name
fails the strict equality check, so loadAimlapiTopupState returns null and
the user restarts checkout. partnerId remains the identity field, so
receipts are still scoped to the correct partner.

* fix(web): use current Twigpine artwork for the partner card (#2246)

The partner asset was a byte-identical rename, so the roster row rendered
the former GitLawb key/branch symbol under the Twigpine name while the
README pointed at the official tree mark. Vendors the current artwork from
twigpine.com/logo.png, resized to the roster's 256x256 convention, keeping
the self-hosting contract in web/src/data/partners.ts.

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-10-12 09:15:34 +08:00
Sakiandliuyang 5bc160fb8b feat: add AnySearch as an explicit web search backend (#2261)
* feat: add AnySearch web search backend

* fix(anysearch): preserve non-json error details

---------

Co-authored-by: liuyang <liuy.web@lingyu.work>
2026-10-12 08:39:56 +08:00
rome-xi 3ed1c5a191 docs: clarify sharp opt-in for local image decode vs passthrough (#2226)
Maintainer confirmed sharp stays intentionally opt-in on published
installs. Document that clipboard paste / local FileRead decode need
`npm i -g sharp`, while pre-encoded URL/base64 passthrough does not.
2026-10-06 10:42:56 +08:00
Kevin CodexandOpenClaude 790c009ae9 feat(search): make Exa the default web search backend and add /search (#2257)
* feat(search): make Exa the default web search backend and add /search

Exa now leads the auto chain: keyed Exa (EXA_API_KEY) runs first, other
keyed backends next, then a new keyless Exa free-tier adapter (Exa's hosted
MCP endpoint), with DuckDuckGo as the last-resort fallback. Built-in search
for Anthropic/Vertex/Foundry/Codex is unchanged. WEB_SEARCH_PROVIDER=exa
uses the keyed API when a key is set, otherwise the free tier.

New Exa options: EXA_SEARCH_TYPE, EXA_NUM_RESULTS, EXA_FREE_TIER (opt-out).
The free-tier adapter handles all three rate-limit reply shapes, spaces
calls to Exa's default QPS, backs off for five minutes after a limit, and
parses Exa's plain-text result blocks defensively.

New /search command: pick a backend, add or remove an API key via a masked
input, show status, run a test search. Choices are saved to the env block
of ~/.openclaude.json (0600) and applied immediately; settings.json env
entries that override them are reported instead of silently losing.

Security: isSensitive arg redaction now also covers local-jsx commands
(/ads, /search), which previously recorded raw inline args in the
transcript and model context.

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

* fix(search): fall through when domain filters empty the Exa free-tier hits

The keyless Exa endpoint has no server-side domain filters, so when the
client-side filter removed every hit, runSearch returned a successful empty
result and never tried DuckDuckGo, which can honor the filter. Providers can
now set `fallbackInAuto` on their output; auto mode skips to the next
backend on that signal, and returns the filtered-empty result only if no
later backend does better. Genuine no-results replies and explicit `exa`
mode are unchanged.

Also adds doc comments to the touched functions that lacked them.

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

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-10-06 10:37:04 +08:00
JATMN 70d97fd137 feat(web-search): add Ollama backend (#2234)
* feat(web-search): add Ollama backend

* fix(web-search): harden Ollama routing

* fix(web-search): preserve Ollama profile identity

* fix(web-search): align Ollama diagnostics

* fix(web-search): align Ollama endpoint diagnostics

* fix(web-search): honor Ollama endpoint aliases

* fix(web-search): reject Ollama key placeholders

* fix(web-search): complete Ollama configuration guidance
2026-09-21 09:10:23 +08:00
JATMN 876fb7924a feat(query): configure idle timeout (#2233)
* feat(query): configure idle timeout

* docs(query): clarify idle timeout limits

* feat(settings): configure query idle timeout

* test(query): exercise idle timeout integration

* fix(query): apply live idle timeout overrides
2026-09-21 09:09:50 +08:00
0xfandom 35d7edc2f6 fix(api): validate API_TIMEOUT_MS in the native Anthropic clients (#2169)
* fix(api): validate API_TIMEOUT_MS in the native Anthropic clients

The native Anthropic client path parsed API_TIMEOUT_MS with a raw
parseInt: `parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10)`
in client.ts and `parseInt(process.env.API_TIMEOUT_MS || '', 10)` in the
non-streaming fallback. Because `||` only substitutes the default for an
empty/unset value, any non-empty malformed value ("abc", "25ms") is truthy
and parses to NaN, and a negative value passes straight through. A NaN or
negative timeout on the SDK client is undefined behavior, and an oversized
value overflows the 32-bit setTimeout ceiling into a tiny/immediate delay.

The OpenAI-compatible shim already routes API_TIMEOUT_MS through
getApiTimeoutMs, which rejects non-digit/negative input, falls back to a
default, and clamps to the 32-bit ceiling. Reuse that single parser in the
native clients too. getApiTimeoutMs gains an optional fallbackMs argument so
the non-streaming path keeps its context-specific default (120s on remote,
300s otherwise) while still validating and clamping the override; existing
shim callers are unaffected (the default remains 600s).

Tests cover the fallback argument in the parser and assert the constructed
native client's timeout for malformed, negative, zero, unset, valid, and
oversized values.

* test(api): cover the non-streaming fallback timeout and pin route env

Add direct coverage for getNonstreamingFallbackTimeoutMs, which the previous
tests only exercised indirectly through the shared parser. It now asserts the
context-specific defaults survive the change (300s local, 120s remote) for
missing and invalid API_TIMEOUT_MS, that a valid override is honored in both
contexts, and that an oversized one is clamped. The function is exported for
this; test files are knip entry points, so it does not read as dead code.

Also clear every provider route-selection variable explicitly in the native
client timeout test. The suite's afterEach already restores them, but pinning
them up front keeps an inherited CLAUDE_CODE_USE_OPENAI (or another route
flag) from diverting getAnthropicClient into the shim and measuring the wrong
client's timeout.

* test(api): pin the native client path and document the timeout contract

The native timeout test cleared many provider variables but left
NEARAI_API_KEY untouched, and the suite never snapshotted it at all.
NEARAI_API_KEY alone selects the env-only Near AI route, and clearing the
competing vendor keys removed the very credentials whose presence suppresses
it, so an inherited value routed getAnthropicClient to the OpenAI shim. The
shim is returned as a cast object with no SDK timeout, so the assertions read
undefined and the regression silently stopped being covered. CLINE_API_KEY had
the same gap.

Snapshot, clear, and restore both alongside the other route-selection inputs,
and assert the constructed object is an Anthropic instance before reading its
timeout, so a stray variable or a future env-only route fails loudly instead
of weakening the test. Verified by injecting NEARAI_API_KEY: the guard now
fails on the shim object rather than passing on undefined.

Also refresh the API_TIMEOUT_MS reference, which still said the Anthropic SDK
kept its own timeout handling. It now describes both consumers: the
headers-deadline semantics for OpenAI-compatible paths, and the native
Anthropic SDK request timeout plus the non-streaming fallback's own defaults
(120s remote, 300s otherwise), under the same validation and clamping.
2026-09-21 09:07:40 +08:00
JATMN 055e5d551a feat(launcher): support --max-old-space-size-percentage (#2219)
* feat(launcher): support --max-old-space-size-percentage heap sizing

Convert Node's percentage heap flag into --max-old-space-size from
constrained or total RAM so small containers and large workstations can
avoid the fixed 8192 MB cap on every supported Node version.

Fixes #2213

* fix(launcher): reject unconstrained memory sentinels in heap percentage

Ignore libuv UINT64_MAX constrainedMemory values, clamp sub-megabyte
percentage results to 1 MB, and keep spaced heap-flag values on
background Node launches.

* fix(launcher): report unresolved heap percentage instead of silent MB fallback

A valid percentage with unknown available memory no longer looks like an
unrelated megabyte env or default success; the launcher warns once, docs
state full precedence, and child NODE_OPTIONS tests call the real helper.

* fix(launcher): strip launcher heap flags when Node already has a cap

Commander rejected --max-old-space-size-percentage on native heap+gc
and disable-relaunch paths. Strip those argv tokens before import, and
do not treat a following non-percentage word as the flag value.

* fix(launcher): propagate execArgv heap caps to subprocess NODE_OPTIONS

Child tools inherit NODE_OPTIONS, not the parent process.execArgv. When the
CLI already started with a numeric --max-old-space-size cap there, copy that
value into NODE_OPTIONS instead of defaulting to 8192 MB. Also strip
launcher-only heap flags on the OPENCLAUDE_HEAP_RELAUNCHED early-return path
and harden launcher integration tests with Node 22-safe execArgv fixtures.
2026-09-12 08:22:10 +08:00
fancivezandfancivez 0b68e579b9 fix(profiles): apply context limit to all profile models (#2201)
* fix(profiles): apply context limit to all models

* fix(profiles): include saved model in context limits

* test(profiles): cover saved model context lifecycle

* fix(profiles): align context limit keys with runtime models

---------

Co-authored-by: fancivez <fancive@gmail.com>
2026-09-08 07:51:10 +08:00
JATMN 3609d751c0 fix: identify OpenCode Go requests (#2203)
* fix: identify OpenCode Go requests

* fix: harden OpenCode Go identity headers

* fix: pin OpenCode session across retries
2026-09-06 08:29:28 +08:00
euxaristia 3451187aed fix(ctx): let explicit overrides beat discovered context windows (#2082)
* fix(ctx): prefer known model limits over generic 128k discovery

Proxies such as OmniRoute often report context_length 128000 when per-model
metadata is missing. That discovery value was beating known descriptors and
settings.modelLimits, so 1M models auto-compacted against a 128k budget.

Prefer larger known descriptors over the generic 128k discovery default, and
let settings/env overrides beat discovery. Document the override path in
/set-context-window help.

Refs #2081

* test(ctx): cover OmniRoute + gpt-5.6-sol (user-reported gpt sol)

The Discord report used GPT-5.6 Sol, not gpt-5.4. Regression now pins the
catalog-less custom-route rescue to the 272k descriptor for gpt-5.6-sol.

Refs #2081

* fix(ctx): let explicit overrides beat discovered context windows

Addresses both review findings on #2082.

P1 — drop the generic-128k rescue from the previous commit. The custom gateway
discovery mapper copies an endpoint's own context_length / context_window into
ModelCatalogEntry.contextWindow, and nothing synthesizes 128k on our side, so
the numeric value carries no provenance: a flat 128000 is indistinguishable
from a real per-deployment or tenant cap. Budgeting a known-larger descriptor
over that cap traded an early auto-compact for a late context-window API
failure.

Discovery is authoritative over the model descriptor again. What remains is the
safe half of the fix: an env prefix override and settings.json modelLimits now
sit above discovery, so a user on a gateway that advertises the wrong window
can pin the real one. Docs and /set-context-window help point at that path.
Tests cover a gateway-advertised 128k and 200k window staying put for a model
the catalog knows is larger, plus env and modelLimits overrides beating it.

P3 — isolate the discovery-cache fixtures. getClaudeConfigHomeDir() ignores
CLAUDE_CONFIG_DIR by design, so both the new modelLimits test and the
pre-existing withTempConfigDir helper were writing fixtures into the caller's
real config dir. Both now use setClaudeConfigHomeDirForTesting with save and
restore, and clear the discovery cache on teardown so the in-memory sync
snapshot cannot leak into a later suite. The modelLimits test now asserts the
isolated cache resolves on its own before asserting the settings pin wins.

Refs #2081

* test(ctx): cover env prefix overrides over discovery cache

CodeRabbit on #2082 asked for a regression where CLAUDE_CODE_OPENAI_*
prefix keys beat a seeded discovery cache for both contextWindow and
maxOutputTokens, not just exact keys and settings.modelLimits.

Refs #2081

* docs(ctx): Clarify permanent overrides for catalogued models

Keep the documented workaround aligned with resolver precedence so users do not expect modelLimits to override built-in catalog entries.

Refs #2081
2026-09-04 07:33:41 +08:00
anushka ✩ 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
Bogdan 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
Bogdan 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
Lucas Molinariandjatmn 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
JATMN 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
0xfandom 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
Bogdanandjatmn 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
JATMN 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
JATMNandClaude 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
0xfandom 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
JATMN 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
0xfandom 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
0xfandom 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
Kevin Codexandjatmn 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
JATMN 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
Bogdan 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
Bogdan 8182a46441 feat(report): render task reports as markdown (#1826) 2026-07-01 06:43:05 +08:00
JATMNandjatmn 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
Bogdan 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
JATMN 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
euxaristia 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
JATMN 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
Bogdan 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
Bogdan 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
JATMN 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
GravireiandGravirei 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
GravireiandGravirei 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
JATMN 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
JoneSSL 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
chioarub 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
JATMN 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
chioarub f3d41c6161 fix(release): verify npm latest tag and document @latest install (#1378)
* fix(release): verify npm latest tag and document @latest install

* fix(auto-updater): use @latest for global installs
2026-06-01 06:08:17 +08:00
5a22d604f8 feat(provider): add OpenCode Zen/Go subscription support (#1350)
* 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

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
2026-05-30 14:45:08 +08:00
stamsam 2e3f7467f9 docs(vertex): clarify Claude on Vertex setup (#1273)
* docs(vertex): clarify Claude on Vertex setup

* docs(vertex): point region overrides at env utils

* docs(vertex): use cli model selector
2026-05-30 00:08:12 +08:00
JATMNandJATMN d02c10b399 feat: configure API retry backoff (#370) (#1095)
* feat: configure API retry backoff

Add OpenClaude-branded retry controls for retryable API failures.

- Replace the old CLAUDE_CODE_MAX_RETRIES config with OPENCLAUDE_MAX_RETRIES

- Allow OPENCLAUDE_MAX_RETRIES=0 to disable retries after the initial request

- Cap retry attempts at 100 and invalid values fall back to the default of 10

- Add OPENCLAUDE_RETRY_DELAY_MS to configure the exponential backoff base for APIs that omit Retry-After

- Keep Retry-After precedence over configured retry delay

- Document both settings in .env.example and advanced setup docs

- Add focused retry configuration tests for defaults, invalid values, caps, zero retries, configured delay, and Retry-After precedence

Validation:

- bun test src/services/api/withRetry.test.ts

- bun run build

* Honor legacy max retries env var

Add compatibility fallback from CLAUDE_CODE_MAX_RETRIES when OPENCLAUDE_MAX_RETRIES is unset.

Document the deprecated fallback and cover precedence behavior in retry configuration tests.

---------

Co-authored-by: JATMN <12479882+jatmn@users.noreply.github.com>
2026-05-25 19:27:07 +08:00
Kevin Codex 13a090162f fix(gemini): preserve tool calls through opengateway (#1204) 2026-05-17 05:12:07 +08:00
4d04f5bf4f feat(opengateway): add Gemini 3.1 Flash Lite + GLM 5.1 FP8 to catalog (#1194)
* feat(opengateway): add Gemini 3.1 Flash Lite + GLM 5.1 FP8 to catalog

Opengateway now routes non-Xiaomi models through GMI Cloud (configured
in opengateway/src/providers.ts via modelIds:
  - google/gemini-3.1-flash-lite-preview
  - zai-org/GLM-5.1-FP8

Adding both as catalog entries on the gitlawb-opengateway gateway
descriptor so openclaude users see them in the model picker when the
Opengateway preset is active. Each catalog entry reuses the existing
upstream model descriptor (`gemini-3.1-flash-lite-preview`, `GLM-5.1`)
for capability metadata; the apiName uses the full vendor-prefixed
form the gateway routes on.

No new model/brand/vendor descriptors needed — only the gateway
catalog gets the new IDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* update opengateway

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:20:30 +08:00