1166 Commits
Author SHA1 Message Date
Tam Nhu Tran 283e8843ac test(openrouter): refine regression test using existing profile-writer harness (#1728) 2026-09-09 15:41:42 -06:00
Tam Nhu Tran 375a3468b8 fix(openrouter): use /api endpoint root to prevent duplicated /v1 (#1728) 2026-09-09 15:38:22 -06:00
Kenneth Wong 7ae7a5eba6 fix(extended-context): manage ANTHROPIC_DEFAULT_MODEL and keep the overlay in sync
Claude Code reads ANTHROPIC_DEFAULT_MODEL as its lowest-priority startup
model (after --model, ANTHROPIC_MODEL and the settings `model` field). It is
a plain model id, so behind a proxy a bare value is clamped to 200k exactly
like the tier keys. Add it to EXTRA_EXTENDED_CONTEXT_MODEL_ENV_KEYS so
--1m/--no-1m and the saved preference toggle it with the rest.

The `--settings` launch overlay only carried the routing keys plus the five
Anthropic tier keys. Claude Code applies the settings `env` block on top of
the process environment, so a bare CLAUDE_CODE_SUBAGENT_MODEL or
ANTHROPIC_DEFAULT_MODEL persisted on disk silently undid the resolved
--1m result (and a saved [1m] undid --no-1m). Overlay the extended-context
extra keys too.

Verified with a stub Claude binary reading the overlay file:

  ccs claude --no-1m -> ANTHROPIC_DEFAULT_MODEL=claude-opus-5, CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-5
  ccs claude --1m    -> both keys suffixed with [1m]
2026-09-04 08:34:33 +08:00
Kenneth Wong 02c45e088a fix(extended-context): suffix the Fable tier and default it on 1M launches
The previous commit assumed Claude Code's fable resolver strips [1m] and
therefore never wrote the suffix into ANTHROPIC_DEFAULT_FABLE_MODEL. That
premise only holds when ANTHROPIC_BASE_URL is unset or points at
api.anthropic.com. Behind a proxy (CLIProxy, headroom, ...) Claude Code
2.1.259 does the opposite:

- the fable alias resolver passes the env value through untouched, so a
  saved `claude-fable-5-1[1m]` reaches the model picker as-is;
- the context-window resolver grants 1M unconditionally when the model id
  carries [1m], and otherwise trusts a natively-1M model only when the base
  URL is first-party. Bare `claude-fable-5-1` via 127.0.0.1 is clamped to
  200k even though CLIProxy's /v1/models advertises max_input_tokens 1M.

So the suffix is the only thing that turns the long window on for CCS, and
stripping it from the Fable key is what kept `ccs claude --model fable` at
200k. A headroom settings profile with the suffix saved on that key showed
the 1M window in the same Claude Code build, which is how the inversion was
caught.

Changes:

- Drop the suffix-stripping key guard; ANTHROPIC_DEFAULT_FABLE_MODEL now
  receives and keeps [1m] exactly like the opus/sonnet tiers.
- On a long-context launch (explicit --1m, or a saved [1m] on any Anthropic
  tier key) fill a missing Fable tier with the catalog Fable model plus
  [1m]. The model-neutral claude profile pins no Fable model, so `--model
  fable` used to fall back to Claude Code's bare default and lose the window
  even when every other tier asked for 1M. An explicit mapping always wins,
  --no-1m never fills, and providers without a catalog Fable model are left
  alone.
- Add getDefaultFableTierModel() to the model catalog for that default.

Verified with a stub Claude binary that dumps its environment:

  ccs claude --model fable          -> ANTHROPIC_DEFAULT_FABLE_MODEL=claude-fable-5-1[1m]
  ccs claude --1m                   -> ANTHROPIC_DEFAULT_FABLE_MODEL=claude-fable-5-1[1m]
  ccs claude --no-1m --model fable  -> no Fable tier written, ANTHROPIC_MODEL stripped
2026-09-04 02:48:15 +08:00
Kenneth Wong 2b780ceae3 fix(extended-context): never write [1m] into the Fable tier key
Claude Code resolves ANTHROPIC_DEFAULT_FABLE_MODEL through a resolver that
strips the [1m] suffix before use, unlike the opus/sonnet resolvers which
pass their env value through untouched. The stripped value is then treated
as an env-supplied default and held to the standard 200k window instead of
the model's native 1M, so writing [1m] into that key costs the long context
window rather than granting it.

Measured against a Claude Code 2.1.259 session (`/context` on a live proxy
endpoint):

  ANTHROPIC_MODEL=claude-fable-5-1[1m]               -> 1m window
  ANTHROPIC_MODEL=claude-fable-5-1                   -> 200k window
  ANTHROPIC_DEFAULT_FABLE_MODEL=claude-fable-5-1[1m] -> 200k window

The fix is key-scoped rather than model-scoped, since the suffix is what
grants 1M on every other key. Adds envKeyAcceptsExtendedContextSuffix() as
the single place recording which keys reject the suffix, enforces it as a
floor inside applyExtendedContextPreferenceToAnthropicModels() so no caller
can bypass it, and strips a previously saved suffix from that key on the
next launch through the auto path.

Also extends the preference to CLAUDE_CODE_SUBAGENT_MODEL. Subagent windows
come from the same resolver as the main loop (the Explore inheritCap bounds
the model tier, not the context window), so a bare subagent model id sits at
200k while the suffixed form gets 1M. The new key list is kept separate from
ANTHROPIC_MODEL_ENV_KEYS, which also drives routing, model-id normalization
and profile validation.

Claude-Session: https://claude.ai/code/session_01NPkafQjVf4pSwPBwBistGk
2026-09-03 17:08:23 +08:00
Kenneth Wong 3583b544d0 feat(models): add Claude Fable 5.1 and correct Sonnet 5 pricing
Register `claude-fable-5-1` in the CLIProxy model catalog, the dashboard
catalog, and the usage pricing registry.

Pricing is taken from Anthropic's official pricing page:

- Fable 5.1 base rates are $10/$50 per MTok with a $12.50 5m cache write,
  matching Fable 5.
- Cache hits bill at 0.025x base input ($0.25/MTok) rather than the
  standard 0.1x multiplier. Anthropic applies that reduced rate only to
  Fable 5.1 and Mythos 5.1, so this entry cannot derive its cache rates
  from CACHE_READ_MULTIPLIER.

This also corrects Claude Sonnet 5 from $3/$15 to $2/$10 (cache write
$2.50, cache read $0.20). The launch introductory rate became the
standard price and the increase scheduled for 2026-09-01 was cancelled,
so the previous entry over-reported Sonnet 5 usage cost by 50%.

Thinking on Fable 5.1 is always on and can only be steered through
effort levels, so the catalog entry exposes the same `low`..`max` level
surface as Fable 5 and Opus 5 instead of a manual token budget.

The GitHub Copilot catalog is deliberately left unchanged, since Copilot
availability for Fable 5.1 is not verified; surfacing it there would
offer a model the backend may reject.

Regenerates docs/reports/hardening-inventory.{json,md} so the ci-parity
gate matches the source tree.
2026-09-03 16:26:41 +08:00
Tam Nhu Tran eddc404f24 docs: refresh hardening inventory and add 75% quota test 2026-08-25 16:13:41 -04:00
Tam Nhu Tran 745cb74318 fix(cliproxy): make formatQuotaBar pure ASCII and add doctor quota tests 2026-08-25 16:04:33 -04:00
Kai (Tam Nhu) Tran 8f87937e8a Merge pull request #1721 from kaitranntt/kai/fix/1716-websearch-probe-perf
fix(websearch): filter disabled legacy CLI probes and skip unused version fetching (#1716)
2026-08-25 16:02:43 -04:00
Tam Nhu Tran c0c36991d6 fix(websearch): format status.ts 2026-08-25 15:56:12 -04:00
Tam Nhu Tran f4911694e3 fix(websearch): filter disabled legacy CLI probes and skip unused version fetching
Closes #1716
2026-08-25 15:35:54 -04:00
Tam Nhu Tran eca266ad30 feat(cliproxy): clarify quota percentages are remaining in CLI output
Closes #1714
2026-08-25 15:35:23 -04:00
Sergey Galuza fa3fd8eb3a refactor(shared-manager): tighten canonical identity capture
Build the canonical identity from the stat getCanonicalFile already
takes, instead of a second lstat of the same inode. One syscall less,
and mode, mtime and identity now describe the same moment rather than
two adjacent ones.

Assert in the adoption race tests that the foreign writer never fired.
It writes only when the canonical path is observed empty, so a zero
count states the invariant the fix establishes - the path is never left
without a regular file - instead of only checking the final content.

Built [OnSteroids](https://onsteroids.ai)
2026-08-23 08:32:39 +02:00
Sergey Galuza 00a4dceb94 fix(shared-manager): publish adopted settings by replacement
Adoption moved the canonical settings.json aside with rename() and left
the path empty until publication, roughly 100 ms later. Claude Code or a
second `ccs` starting inside that window found no file and seeded an
empty placeholder; publication then failed with EEXIST because link() is
no-replace, and the rollback published a backup and unlinked the claim,
destroying the only remaining copy of the user's settings. Recovering
meant digging through sidecar files by hand.

Publish by replacement instead: write a temp file next to the canonical
inode and rename() it over the target, so the path always holds a regular
file and no placeholder can be seeded. A compare-and-swap guard on
(ino, mtime, size) runs immediately before the rename and refuses to
publish when the canonical inode changed since it was read, so a writer
that got there first is still never clobbered. The pre-image backup is
published before the replacement, keeping the old content recoverable if
publication is interrupted.

Drops the canonical claim entirely along with restoreCanonicalClaim, and
folds the two identical sidecar publishers into one helper.
recoverOrphanedCanonicalClaim stays, since claims written by older
versions may still be on disk.

New tests cover both writers seen in the incident: Claude Code seeding
`{}` with a trailing newline, and a second `ccs` seeding the 2-byte
variant from shared-dir-linker. Four tests that pinned the claim-based
design were rewritten, among them `preserves a canonical write that
lands during no-replace publication`, whose intent is now enforced by
the CAS guard instead of by an EEXIST from a no-replace link.

Built [OnSteroids](https://onsteroids.ai)
2026-08-23 08:32:26 +02:00
Kai (Tam Nhu) Tran ae1559aeb0 fix(ci): publish dev releases with public access for scoped package (#1715) 2026-08-20 12:14:14 -04:00
Kai (Tam Nhu) Tran 95faef5960 Merge pull request #1710 from kaitranntt/kai/fix/1703-image-analysis-original-backend-route
fix(image-analysis): route original backend at CLIProxy root
2026-08-19 17:43:27 -04:00
Tam Nhu Tran c43cd6caf5 fix(update): detect pnpm v9+ global store layouts
Fixes #1706
2026-08-19 16:43:15 -04:00
Tam Nhu Tran afa663b5b2 fix(image-analysis): route original backend at CLIProxy root
Fixes #1703
2026-08-19 16:43:15 -04:00
Kai (Tam Nhu) Tran 3997dbd258 Merge pull request #1708 from aaron-tsar/fix/image-analysis-profile-backends-launch
fix(image-analysis): honor configured profile_backends at launch
2026-08-19 16:35:08 -04:00
Aaron VuandClaude fe3447f487 fix(image-analysis): honor configured profile_backends at launch
resolveImageAnalysisRuntimeStatus defaulted to DEFAULT_IMAGE_ANALYSIS_CONFIG
when callers omitted the config argument. That constant ships empty
profile_backends and a gemini fallback_backend, so the launch paths that call
it without a config (settings profile dispatch and headless delegation) never
saw user-configured mappings.

A profile mapped to another backend still resolved to gemini, failed the
Gemini auth check, and silently dropped to native Read. Profiles whose model
has no vision support could not read images at all, even with a reachable
CLIProxy and an authenticated backend.

getImageAnalysisHookEnv already reads the saved config, so the launch env and
the runtime status disagreed on the same launch: CCS_IMAGE_ANALYSIS_BACKEND_ID
carried the mapped backend while the status object reported native-read.

Default to getImageAnalysisConfig() so both read the same source. Callers that
pass an explicit config keep their existing behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 15:27:02 +07:00
Marc-oss-hubandClaude ea7906b975 feat(api): add OrcaRouter provider preset
Add a named 'orcarouter' preset to the shared provider catalog mirroring
the existing OpenRouter entry: OpenAI-compatible base URL
https://api.orcarouter.ai/v1, default model openai/gpt-5.5, sk-orca-...
key placeholder, and a dashboard icon. Regenerate the throw-error
baseline for the line shift in provider-preset-catalog.ts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 22:29:17 +08:00
Tam Nhu Tran 54c3e86e89 fix(docker): target live cliproxy compose stack 2026-08-10 22:13:29 -04:00
Tam Nhu Tran fc56ecaac4 fix(cliproxy): finish concurrent update recovery 2026-08-10 22:13:29 -04:00
Tam Nhu Tran 4502e5d503 fix(cliproxy): harden update recovery paths 2026-08-10 22:13:29 -04:00
Tam Nhu Tran 8e4def4713 fix(cliproxy): keep proxy available during updates 2026-08-10 22:13:29 -04:00
Tam Nhu Tran 54efb82055 fix(auth): guard shared linking against instance replacement 2026-08-08 23:06:29 -04:00
Tam Nhu Tran 5f9db033e7 chore: merge origin/dev into issue #1688
# Conflicts:
#	docs/reports/hardening-inventory.json
#	docs/reports/hardening-inventory.md
2026-08-08 22:29:46 -04:00
Tam Nhu Tran 0ddeb09955 chore(dev): merge v8.8.1-dev.20 for issue 1686 2026-08-08 22:09:27 -04:00
Tam Nhu Tran ce8ad269f0 chore(merge): sync dev release v8.8.1-dev.19 2026-08-08 21:48:56 -04:00
Tam Nhu Tran 4d5449a493 Merge remote-tracking branch 'origin/dev' into kai/review/pr-1691
# Conflicts:
#	docs/reports/hardening-inventory.json
#	docs/reports/hardening-inventory.md
2026-08-08 21:31:32 -04:00
Tam Nhu Tran 1a80717f99 fix(bar): harden lifecycle process handling 2026-08-08 21:17:51 -04:00
Tam Nhu Tran 6841025bb4 feat(auth): share canonical Claude memory
Refs #1688
2026-08-08 21:17:32 -04:00
Tam Nhu Tran d8210cf011 fix(proxy): stabilize upstream fetch transport
Refs #1686
2026-08-08 21:17:13 -04:00
Tam Nhu Tran 6721d47502 fix(auth): preserve explicit resume session continuity
Refs #1685
2026-08-08 21:17:01 -04:00
Tam Nhu Tran 59eec74f40 chore: merge released dev into PR #1690 2026-08-08 21:14:50 -04:00
Tam Nhu Tran da2de60015 fix(bar): bound native credential and quota waits 2026-08-08 21:12:28 -04:00
Tam Nhu Tran c621241a2e test(proxy): harden system message ordering
Cover supported late-system and tool-result ordering invariants.

Refs #1687
2026-08-08 20:59:37 -04:00
poomscandClaude Fable 5 d27b53f235 fix(bar): keep sticky port across ccs bar stop via launch.json fallback
`ccs bar stop` deletes bar.json, so a bar.json-only sticky port is lost on
every stop/start cycle: the next launch reverted to 3000 and the probe could
no longer find a server still running on the previously chosen port.
resolveBarPort now falls back to the --port recorded in launch.json (written
by launch, not deleted by stop), which restores both the sticky port and
probe discovery after a stop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:50:18 +07:00
poomscandClaude Fable 5 833473c5f6 fix(bar): treat cached quota rows as stale once their reset passes
A cached row whose next_reset has passed describes the previous quota
window — the quota snapped back at the boundary, so serving it for the rest
of the 10-min TTL shows wrong percentages and an already-elapsed reset time
in the bar. Both the per-profile TTL short-circuit and the rotating-slot
eligibility now mark such rows stale. Guarded by cachedAt < resetAt so a
post-reset payload that still reports a past reset cannot refetch-loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:43:52 +07:00
poomscandClaude Fable 5 a5a5b742e4 fix(bar): stop false re-auth on non-default native subscription profiles
Fixes the two collector-side root causes of #1601:

1. Claude per-profile credential reads were file-only, but on macOS Claude
   Code stores the OAuth token for an isolated CLAUDE_CONFIG_DIR in a
   per-directory Keychain item ("Claude Code-credentials-<sha256(dir)[0..8]>").
   The .credentials.json file never exists, so every isolated profile was
   parked with needsReauth:true forever. The reader now falls back to that
   Keychain item (file-first, same security-CLI read the shipped global
   fallback already performs; TTL-gated so it is not on every /summary).

2. Non-default profiles were cache-only forever, so they could never leave
   the parked state even with valid credentials. getNativeAccountRows now
   gives each surface ONE rotating live slot: the stalest eligible
   non-default profile is refreshed per pass, skipping profiles inside
   breaker/reauth cooldowns. Every account converges to real quota within a
   few polls while the per-pass upstream budget stays constant (<= 2 calls
   per surface regardless of profile count). Codex named profiles with valid
   auth but sparse payloads now yield an active quota-less row instead of a
   false needsReauth row.

Non-default rows keep paused:true (dimmed) even when freshly refreshed so
only the default renders active and rows do not flicker between polls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:18:00 +07:00
poomscandClaude Fable 5 34608ce291 feat(bar): support --port for ccs bar with sticky port persistence
ccs bar always forced the dashboard onto port 3000 (first free of a
hardcoded candidate list), which collides with other local dev servers, and
bar.json was rewritten to 3000 on every launch.

- `ccs bar [launch] --port N` runs the server on exactly N: reuses a live
  server already on N, moves a running server from another port (SIGTERM via
  server.pid, wait for exit), errors clearly when N is busy or the value is
  invalid.
- The chosen port is persisted into launch.json args, so the Swift app
  self-starts the server on the same port.
- Without --port, launch and serve now try the port recorded in bar.json
  first (sticky), so the server keeps coming back on the port the user last
  chose instead of reverting to 3000.
- Bare flags (`ccs bar --port N`) route to the launch subcommand; --port is
  documented in `ccs bar --help`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:09:58 +07:00
sn4p.dev b720231077 fix(proxy): hoist duplicate system messages before coalescing
Claude Code sends the system prompt as the top-level `system` field and,
separately, sends skill/plugin listings as `role: "system"` entries inside
`messages` (#1459 made the transformer accept those). `transform()`
unconditionally prepends the top-level field, so once both are present the
OpenAI-compat payload ends up with two `system` messages that are not
adjacent. `coalesceMessages` only merges consecutive same-role messages and
explicitly skips `system`, so it cannot fix this.

Strict OpenAI-compatible backends (LiteLLM among them) reject that shape with:
  400 A 'system' message can only appear at index 0 of the messages array.

Add `hoistSystemMessages`, run before `coalesceMessages`, which extracts every
`system` message in encounter order and reinserts a single merged one at
index 0. Content-preserving, no behavior change when at most one system
message is present.
2026-08-06 11:28:18 +02:00
Tam Nhu Tran 7ae617f0ee fix(shared-manager): preserve diverged files safely 2026-08-02 20:21:09 -04:00
Nelson Melo fa6b27f159 fix(shared-manager): adopt diverged plugin registry files in linkInstancePlugins
The same atomic-rename divergence occurs in the plugins subtree: a
plugin install inside a session rewrites plugins/installed_plugins.json,
replacing the instance-level symlink with a regular file. The per-launch
relink then discarded it, so the plugin was effectively uninstalled on
every relaunch while settings.json still marked it enabled — sessions
then fail with 'Unknown skill: <plugin>:<skill>'.

Move adoptDivergedFileContent to fs-helpers (avoids a circular import)
and apply it to file-type plugin entries before re-linking.
2026-07-31 13:40:23 -04:00
Nelson Melo 481f013ec2 fix(shared-manager): adopt diverged settings.json content before re-linking
Claude Code saves settings.json atomically (temp file + rename), which
replaces the managed shared symlink with a regular file holding the
user's latest changes (see #57). The launch-time relink then deleted
that file without reading it, silently reverting plugin enables and any
other in-session settings change on every profile relaunch.

Adopt the diverged file's content into the canonical ~/.claude file
(with a .bak-ccs-adopt backup) before restoring the symlink, at both
the shared-level and instance-level reconciliation points.

Fixes #1681
2026-07-31 12:14:29 -04:00
Tam Nhu Tran 26c1393c16 ci(review): configure explicit reasoning effort 2026-07-31 10:32:32 -04:00
Tam Nhu Tran c107a6752c test(cliproxy): cover auto compact windows 2026-07-29 13:46:53 -04:00
Kai (Tam Nhu) Tran 236471100c Merge pull request #1676 from kaitranntt/kai/fix/1648-disabled-websearch-steering
fix(websearch): honor disabled steering per launch
2026-07-29 13:44:56 -04:00
Tam Nhu Tran 92fb66c47b test(cliproxy): cover Claude quota probe 429s 2026-07-29 13:22:21 -04:00
Tam Nhu Tran 9f5706c275 test(websearch): cover disabled launch snapshots 2026-07-29 13:22:21 -04:00