mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
docs(config): preserve active developer contracts
This commit is contained in:
1 parent
deb1ed0e67
commit
fd0d4362b3
2 files changed
+91
-569
No files matched your search
+58
-202
@@ -1,217 +1,73 @@
|
|||||||
# Codex Auth Profile Isolation (`ccsx auth`)
|
# Codex Auth Developer Contract
|
||||||
|
|
||||||
Run two Codex accounts simultaneously — one per terminal — with full auth isolation.
|
The canonical [Codex Adapter guide](https://docs.ccs.kaitran.ca/features/workflow/codex-adapter)
|
||||||
|
owns user setup. This local contract documents active `ccsx auth` invariants
|
||||||
|
that contributors and operators must preserve.
|
||||||
|
|
||||||
## Why
|
## Command Surface
|
||||||
|
|
||||||
Codex stores its OAuth credentials in a single directory (`~/.codex/`). When you run two
|
`ccsx auth` owns `create`, `login`, `switch`, `use`, `show`, `remove`, and
|
||||||
`codex` sessions in separate terminals, they both write to the same `auth.json`. A token
|
`import-default`. Keep syntax and option changes sourced from
|
||||||
refresh in one session overwrites the other's credentials.
|
[`src/codex-auth/codex-auth-help.ts`](../src/codex-auth/codex-auth-help.ts)
|
||||||
|
rather than copying a long command reference here.
|
||||||
|
|
||||||
`ccsx auth` solves this by giving each account its own profile directory under
|
- `create <name>` is idempotent and starts native `codex login` for new
|
||||||
`~/.ccs/codex-instances/<name>/`. Each profile holds its own `auth.json` and
|
profiles. `--force` repairs shared resources without replacing `auth.json`.
|
||||||
`history.jsonl`, plus its own session data. Shared `config.toml`, `agents/`, `skills/`,
|
- `switch <name>` changes the persistent registry default.
|
||||||
and plugin cache resources come from `~/.codex/` so configuration and installed plugin
|
- `use <name>` emits only shell-evaluable `CODEX_HOME` and
|
||||||
skills stay in sync across profiles.
|
`CCS_CODEX_PROFILE` assignments to stdout. It affects the current shell after
|
||||||
|
`eval`/`source`; diagnostics stay on stderr.
|
||||||
|
- `ccsx <name>` launches a named profile directly without changing the
|
||||||
|
persistent default.
|
||||||
|
|
||||||
## Quick start (4 commands)
|
## Import Safety
|
||||||
|
|
||||||
```bash
|
`import-default <name>` imports native `~/.codex/auth.json` without deleting the
|
||||||
# Create and authenticate two profiles
|
source. The implementation in
|
||||||
ccsx auth create work # creates ~/.ccs/codex-instances/work/ and prompts for login
|
[`import-default-command.ts`](../src/codex-auth/commands/import-default-command.ts)
|
||||||
ccsx auth create personal # same for personal account
|
must continue to:
|
||||||
|
|
||||||
# Activate per terminal (ephemeral — only this shell)
|
- refuse import while a current-user Codex process may be refreshing tokens,
|
||||||
# Terminal A:
|
unless the operator explicitly accepts the race with
|
||||||
eval "$(ccsx auth use work)"
|
`--force-while-running`;
|
||||||
codex
|
- retry and validate JSON/JWT shape, reject CLIProxy auth-file formats, and fail
|
||||||
|
without registering a profile when a torn write persists;
|
||||||
|
- write the destination atomically with private permissions;
|
||||||
|
- omit history and sessions unless `--with-history` is requested;
|
||||||
|
- refuse an existing profile unless `--force` is used, and preserve its current
|
||||||
|
`auth.json` as `auth.json.bak-<timestamp>` before overwrite.
|
||||||
|
|
||||||
# Terminal B:
|
## Storage And Cross-Platform Fallback
|
||||||
eval "$(ccsx auth use personal)"
|
|
||||||
codex
|
|
||||||
|
|
||||||
# Or launch a named profile directly through ccsx
|
```text
|
||||||
ccsx work
|
|
||||||
```
|
|
||||||
|
|
||||||
## Two-terminal example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Terminal A — work account
|
|
||||||
eval "$(ccsx auth use work)"
|
|
||||||
codex # runs with CODEX_HOME=~/.ccs/codex-instances/work
|
|
||||||
|
|
||||||
# Terminal B — personal account (simultaneously)
|
|
||||||
eval "$(ccsx auth use personal)"
|
|
||||||
codex # runs with CODEX_HOME=~/.ccs/codex-instances/personal
|
|
||||||
|
|
||||||
# No token clobbering. Each session refreshes its own auth.json only.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command reference
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `ccsx auth create <name>` | Create profile dir + auto-login |
|
|
||||||
| `ccsx <name>` | Launch a named Codex auth profile |
|
|
||||||
| `ccsx auth login <name>` | (Re-)authenticate an existing profile |
|
|
||||||
| `ccsx auth switch <name>` | Set the persistent default profile for future `ccsx` launches |
|
|
||||||
| `ccsx auth use <name>` | Emit shell exports for this shell only (use with `eval`) |
|
|
||||||
| `ccsx auth show [name]` | List all profiles or show details for one |
|
|
||||||
| `ccsx auth remove <name>` | Delete profile dir + registry entry |
|
|
||||||
| `ccsx auth import-default <name>` | Migrate legacy `~/.codex/auth.json` into a new profile |
|
|
||||||
|
|
||||||
## Persistent vs ephemeral switching
|
|
||||||
|
|
||||||
| Method | Scope | How |
|
|
||||||
|--------|-------|-----|
|
|
||||||
| `ccsx <name>` | One launch | Resolves `<name>` from the Codex profile registry |
|
|
||||||
| `ccsx auth switch <name>` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` |
|
|
||||||
| `eval "$(ccsx auth use <name>)"` | Current shell only | Sets `CODEX_HOME` + `CCS_CODEX_PROFILE` in your shell |
|
|
||||||
|
|
||||||
Native `codex` shells only see the persistent default when launched through the `ccsx`
|
|
||||||
Codex runtime. For an already-open shell or a plain native `codex` binary, use `auth use`.
|
|
||||||
|
|
||||||
Do not use `ccs persist codex` for Claude Code or the Claude Code Extension. That path
|
|
||||||
would persist Claude settings that send Claude traffic through the Codex translator. CCS
|
|
||||||
blocks Codex CLIProxy profiles from Claude extension setup; use `ccsxp` or
|
|
||||||
`ccs codex --target codex` for ChatGPT/Codex subscriptions. If old settings were already
|
|
||||||
persisted, clear them with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs persist default --yes
|
|
||||||
```
|
|
||||||
|
|
||||||
The command prints a config receipt after writing settings: cleared managed keys,
|
|
||||||
written managed keys, whether and where any `/api/provider/codex` translator URL
|
|
||||||
remains, and the native Codex targets to use next.
|
|
||||||
|
|
||||||
Shell syntax for `use`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# bash / zsh
|
|
||||||
eval "$(ccsx auth use work)"
|
|
||||||
|
|
||||||
# fish
|
|
||||||
ccsx auth use work | source
|
|
||||||
|
|
||||||
# PowerShell
|
|
||||||
ccsx auth use work | Invoke-Expression
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration from `~/.codex`
|
|
||||||
|
|
||||||
If you already have a logged-in session in `~/.codex/auth.json`, import it without
|
|
||||||
disturbing the original:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Auth only (default — recommended)
|
|
||||||
ccsx auth import-default legacy
|
|
||||||
|
|
||||||
# Auth + history + sessions (opt-in)
|
|
||||||
ccsx auth import-default legacy --with-history
|
|
||||||
|
|
||||||
# Make it the default
|
|
||||||
ccsx auth switch legacy
|
|
||||||
```
|
|
||||||
|
|
||||||
The source `~/.codex/` directory is **never modified**. If `import-default` is not run,
|
|
||||||
`codex` continues to work exactly as before.
|
|
||||||
|
|
||||||
### Torn-write safety
|
|
||||||
|
|
||||||
Codex writes `auth.json` with truncate+write (not atomic rename). Running
|
|
||||||
`import-default` while a token refresh is in flight can produce a corrupt copy.
|
|
||||||
The command detects a running `codex` process via `pgrep` and refuses unless you
|
|
||||||
pass `--force-while-running`. The safest approach is to quit Codex before
|
|
||||||
importing.
|
|
||||||
|
|
||||||
## Dashboard
|
|
||||||
|
|
||||||
The CCS dashboard shows active profile metadata at the **Auth Profiles** tab on the
|
|
||||||
Codex page:
|
|
||||||
|
|
||||||
- Profile name and whether it is the current default
|
|
||||||
- Decoded email address (from `id_token` — no signature verification; display only)
|
|
||||||
- Plan tier (Plus, Pro, Free) when present in the token
|
|
||||||
- Last-used timestamp
|
|
||||||
|
|
||||||
No OAuth tokens are ever returned by the API endpoint or shown in the UI.
|
|
||||||
|
|
||||||
## Profile disk layout
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.ccs/
|
~/.ccs/
|
||||||
├── codex-profiles.yaml # Registry: version, default, profiles metadata
|
├── codex-profiles.yaml
|
||||||
└── codex-instances/
|
└── codex-instances/<name>/
|
||||||
└── <name>/
|
├── auth.json, history.jsonl, sessions/ # profile-local
|
||||||
├── auth.json # OAuth credentials (Codex writes here)
|
├── config.toml -> ~/.codex/config.toml
|
||||||
├── history.jsonl # Per-profile prompt history (optional)
|
├── agents/ -> ~/.codex/agents/
|
||||||
├── sessions/ # Per-profile chat session dirs (optional)
|
├── skills/ -> ~/.codex/skills/
|
||||||
├── config.toml -> ~/.codex/config.toml (symlink — shared)
|
└── plugins/
|
||||||
├── agents/ -> ~/.codex/agents/ (symlink — shared)
|
└── cache/ -> ~/.codex/plugins/cache/
|
||||||
├── skills/ -> ~/.codex/skills/ (symlink — shared)
|
|
||||||
└── plugins/ # Profile-local parent; may hold local metadata
|
|
||||||
└── cache/ -> ~/.codex/plugins/cache/ (symlink — shared)
|
|
||||||
|
|
||||||
~/.codex/
|
|
||||||
├── config.toml # Single shared model/provider config
|
|
||||||
├── agents/ # Shared Codex agent role config files
|
|
||||||
├── skills/ # Shared Codex skills
|
|
||||||
└── plugins/
|
|
||||||
└── cache/ # Shared installed plugin payloads
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Only `plugins/cache/` is shared. The profile's parent `plugins/` directory remains a
|
The `plugins/` parent stays profile-local. Shared config, resources, and plugin
|
||||||
real local directory so Codex can keep profile-specific plugin metadata beside the
|
cache repair must preserve existing profile-local content. On Windows or other
|
||||||
shared cache.
|
systems where symlinks are unavailable, CCS copies missing shared content into
|
||||||
|
the profile and warns that later upstream edits will not propagate
|
||||||
|
automatically. See
|
||||||
|
[`codex-config-symlink.ts`](../src/codex-auth/codex-config-symlink.ts),
|
||||||
|
[`codex-profile-resources.ts`](../src/codex-auth/codex-profile-resources.ts),
|
||||||
|
and
|
||||||
|
[`codex-profile-plugin-cache.ts`](../src/codex-auth/codex-profile-plugin-cache.ts).
|
||||||
|
|
||||||
`ccsx auth create <name>` and direct `ccsx <name>` launches repair these links
|
## `ccsx` And `ccsxp` Isolation
|
||||||
idempotently before Codex starts. This keeps relative entries such as
|
|
||||||
`agents/foo.toml` valid and prevents stale first-launch skill warnings after a plugin
|
|
||||||
install or update changes the cache.
|
|
||||||
|
|
||||||
## Caveats
|
`ccsx auth` applies only to native Codex profiles. `ccsxp` ignores
|
||||||
|
`CCS_CODEX_PROFILE`, uses native `~/.codex` history by default, and routes
|
||||||
|
through its separate CLIProxy Codex pool. `CCSXP_CODEX_HOME` is its explicit
|
||||||
|
home override. Never make a `ccsx auth switch` silently redirect `ccsxp`, merge
|
||||||
|
their auth stores, or consume `ccsx` import backups as pool credentials.
|
||||||
|
|
||||||
### Windows symlinks
|
Behavior locks live under `tests/unit/codex-auth/` and
|
||||||
|
`tests/integration/codex-auth/`.
|
||||||
On Windows, creating symlinks requires Developer Mode or elevated privileges.
|
|
||||||
If symlink creation fails, CCS falls back to copying `config.toml`, `agents/`,
|
|
||||||
`skills/`, and the current `plugins/cache/` snapshot. Copies do not update live with
|
|
||||||
`~/.codex/`; after a plugin update, another profile launch or
|
|
||||||
`ccsx auth create <name> --force` repair copies newly missing cache entries. Existing
|
|
||||||
profile-local cache files are preserved.
|
|
||||||
|
|
||||||
### Native Codex project-local config warnings
|
|
||||||
|
|
||||||
`ccsx` preserves your current working directory. If you launch from your home directory,
|
|
||||||
native Codex can also see `~/.codex/config.toml` as `./.codex/config.toml`, a
|
|
||||||
project-local config file. Codex rejects user-level-only keys such as `model_providers`
|
|
||||||
and `notify` in project-local config. That warning comes from native Codex config
|
|
||||||
layering, not from the `ccsx auth` profile resource links. Launch from a project
|
|
||||||
directory or move project-local Codex config out of `$HOME/.codex/config.toml` if the
|
|
||||||
warning is noisy.
|
|
||||||
|
|
||||||
### `ccsx` vs `ccsxp`
|
|
||||||
|
|
||||||
`ccsx auth` profiles apply only to the **native `codex`** CLI. They have no effect on
|
|
||||||
`ccsxp` (the CLIProxy round-robin pool). `ccsxp` unconditionally sets its own
|
|
||||||
`CODEX_HOME` on startup and ignores `CCS_CODEX_PROFILE`.
|
|
||||||
|
|
||||||
If you run `eval "$(ccsx auth use work)"` and then invoke `ccsxp`, a notice is emitted
|
|
||||||
to stderr:
|
|
||||||
|
|
||||||
```
|
|
||||||
[i] CCS_CODEX_PROFILE is ignored by ccsxp; profile applies to native 'codex' only
|
|
||||||
```
|
|
||||||
|
|
||||||
### cmd.exe
|
|
||||||
|
|
||||||
`ccsx auth use` emits `set FOO=bar` syntax for cmd.exe. Native `eval` is not available
|
|
||||||
in legacy cmd — use PowerShell (`Invoke-Expression`) instead.
|
|
||||||
|
|
||||||
### Backup files from `--force`
|
|
||||||
|
|
||||||
When re-importing with `--force`, the existing `auth.json` is backed up as
|
|
||||||
`auth.json.bak-<timestamp>` in the profile directory. These accumulate over time; remove
|
|
||||||
them manually when no longer needed.
|
|
||||||
@@ -1,378 +1,44 @@
|
|||||||
# OpenAI-Compatible Provider Routing
|
# OpenAI-Compatible Proxy Developer Contract
|
||||||
|
|
||||||
CCS can route Claude Code traffic through a local Anthropic-compatible proxy when
|
The canonical
|
||||||
your API profile points at an OpenAI-compatible chat completions endpoint.
|
[OpenAI-Compatible Provider Routing guide](https://docs.ccs.kaitran.ca/features/proxy/openai-compatible-providers)
|
||||||
|
owns user setup and workflows. This local contract retains runtime and
|
||||||
|
configuration invariants used by source, tests, and operators.
|
||||||
|
|
||||||
This is useful for providers such as:
|
## Profile-Scoped Insecure TLS
|
||||||
|
|
||||||
- Hugging Face Inference Providers
|
`CCS_OPENAI_PROXY_INSECURE` is read from an OpenAI-compatible profile env.
|
||||||
- Tuning Engines
|
Truthy values are `1`, `true`, `yes`, and `on` (case-insensitive). When enabled,
|
||||||
- OpenRouter
|
the local proxy disables upstream certificate verification for that profile,
|
||||||
- Ollama
|
including request-time routing to another insecure profile.
|
||||||
- llama.cpp servers
|
|
||||||
- OpenAI-compatible self-hosted gateways
|
|
||||||
|
|
||||||
## Related Project: claude-code-router
|
This flag weakens TLS verification. Keep it explicit and profile-scoped; never
|
||||||
|
make it a global default or infer it from a failed certificate check. The live
|
||||||
|
resolution and dispatcher contracts are in
|
||||||
|
[`profile-router.ts`](../src/proxy/profile-router.ts) and
|
||||||
|
[`proxy-server.ts`](../src/proxy/server/proxy-server.ts).
|
||||||
|
|
||||||
[claude-code-router](https://github.com/musistudio/claude-code-router) is the
|
## Request Timeout
|
||||||
main external reference that informed this CCS work. Their Anthropic/OpenAI
|
|
||||||
transformer design helped shape the routing approach here.
|
|
||||||
|
|
||||||
When to use CCR:
|
`CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS` controls the upstream request timeout in
|
||||||
|
milliseconds:
|
||||||
|
|
||||||
- you want a standalone router without CCS profile integration
|
- default: `600000` (10 minutes);
|
||||||
- you do not need CCS account/runtime management around the request flow
|
- accepted: values whose `Number.parseInt` result is positive;
|
||||||
|
- missing, invalid, zero, or negative values: fall back to the default.
|
||||||
|
|
||||||
When to use CCS:
|
Upstream Undici header/body timeouts must stay above the request timeout so they
|
||||||
|
do not terminate slow self-hosted inference first. The current implementation
|
||||||
|
adds a 30-second grace ceiling. Source of truth:
|
||||||
|
[`messages-route.ts`](../src/proxy/server/messages-route.ts).
|
||||||
|
|
||||||
- you already use CCS API profiles or runtime bridges
|
## Compatibility Boundary
|
||||||
- you want the proxy flow available through `ccs <profile>` and `ccs proxy ...`
|
|
||||||
- you want the routing behavior documented and tested inside the CCS workflow
|
|
||||||
|
|
||||||
## What CCS Does
|
These variables configure the local Anthropic-to-OpenAI proxy. They do not
|
||||||
|
convert a non-compatible profile into a compatible one, bypass local proxy
|
||||||
|
authentication, or authorize remote binding. Keep profile detection, adaptive
|
||||||
|
port selection, passthrough mode, request-time routing, and scenario routing
|
||||||
|
documented in the public guide.
|
||||||
|
|
||||||
When you launch a compatible settings profile with the Claude target, CCS now:
|
Focused behavior locks live in `tests/unit/proxy/` and
|
||||||
|
`tests/integration/proxy/`.
|
||||||
1. Starts a local proxy on `127.0.0.1` using the resolved local port for that profile
|
|
||||||
2. Accepts Anthropic `/v1/messages` traffic from Claude Code
|
|
||||||
3. Translates requests into OpenAI chat-completions format
|
|
||||||
4. Forwards them to your configured upstream provider
|
|
||||||
5. Translates streaming responses back into Anthropic SSE
|
|
||||||
|
|
||||||
You do not need to rewrite your profile by hand each time.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
Create or reuse an API profile that points at an OpenAI-compatible endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs api create --preset hf
|
|
||||||
```
|
|
||||||
|
|
||||||
For Tuning Engines:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs api create --preset te
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use the profile directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs hf
|
|
||||||
# or
|
|
||||||
ccs te
|
|
||||||
```
|
|
||||||
|
|
||||||
CCS detects that the profile is OpenAI-compatible and auto-routes Claude Code
|
|
||||||
through the local proxy.
|
|
||||||
|
|
||||||
## Manual Proxy Lifecycle
|
|
||||||
|
|
||||||
If you want to manage the proxy explicitly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs proxy start hf
|
|
||||||
eval "$(ccs proxy activate)"
|
|
||||||
ccs proxy status
|
|
||||||
ccs proxy stop
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful variants:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs proxy start hf --host 127.0.0.1
|
|
||||||
ccs proxy start hf --port 3460
|
|
||||||
ccs proxy activate hf
|
|
||||||
ccs proxy activate --fish
|
|
||||||
ccs proxy status hf
|
|
||||||
ccs proxy stop hf
|
|
||||||
```
|
|
||||||
|
|
||||||
Port selection precedence is:
|
|
||||||
|
|
||||||
1. CLI `--port` for an exact one-off pin
|
|
||||||
2. `proxy.profile_ports[profile]` for an exact per-profile pin
|
|
||||||
3. `proxy.port` for a shared preferred starting port
|
|
||||||
4. adaptive per-profile fallback when nothing is pinned
|
|
||||||
|
|
||||||
Legacy shared `proxy.port: 3456` values are treated as unset so older configs
|
|
||||||
move onto the adaptive path instead of staying on the hot legacy default. If
|
|
||||||
you need an exact `3456` binding now, pin it via `--port` or `proxy.profile_ports`.
|
|
||||||
|
|
||||||
`ccs proxy activate` now prints the full local runtime contract:
|
|
||||||
|
|
||||||
- `ANTHROPIC_BASE_URL`
|
|
||||||
- `ANTHROPIC_AUTH_TOKEN`
|
|
||||||
- `ANTHROPIC_MODEL` plus tier defaults when present
|
|
||||||
- `DISABLE_TELEMETRY`
|
|
||||||
- `DISABLE_COST_WARNINGS`
|
|
||||||
- `API_TIMEOUT_MS`
|
|
||||||
- `NO_PROXY`
|
|
||||||
|
|
||||||
## Multiple Active Proxy Profiles
|
|
||||||
|
|
||||||
CCS now stores OpenAI-compatible proxy state per profile instead of treating the
|
|
||||||
runtime as a singleton.
|
|
||||||
|
|
||||||
- Different compatible profiles can run at the same time on separate local ports
|
|
||||||
- `ccs proxy activate` without a profile stays convenient when only one proxy is
|
|
||||||
running
|
|
||||||
- When multiple proxies are running, pass the profile explicitly to
|
|
||||||
`activate`, `status`, or `stop`
|
|
||||||
- `status` and `activate` always reflect the actual running port instead of an
|
|
||||||
assumed default
|
|
||||||
|
|
||||||
If you want to pin or guide ports explicitly, configure them in `~/.ccs/config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
proxy:
|
|
||||||
port: 45000
|
|
||||||
profile_ports:
|
|
||||||
hf: 3460
|
|
||||||
openai: 3461
|
|
||||||
```
|
|
||||||
|
|
||||||
## Request-Time Routing
|
|
||||||
|
|
||||||
The proxy is no longer limited to the startup profile's default model.
|
|
||||||
|
|
||||||
Supported request-time selectors:
|
|
||||||
|
|
||||||
- `profile:model`
|
|
||||||
Example: `deepseek:deepseek-reasoner`
|
|
||||||
- `profile`
|
|
||||||
Example: `openrouter`
|
|
||||||
- plain model ids
|
|
||||||
Example: `deepseek-chat`
|
|
||||||
|
|
||||||
Plain model ids use exact string equality against the configured profile model
|
|
||||||
slots (`model`, `opusModel`, `sonnetModel`, `haikuModel`). CCS does not apply
|
|
||||||
fuzzy matching or prefix matching here. If no exact match is found, the request
|
|
||||||
stays on the active profile with the requested model id unchanged.
|
|
||||||
|
|
||||||
Routing behavior:
|
|
||||||
|
|
||||||
1. `profile:model` wins immediately.
|
|
||||||
2. Scenario routing may override the active profile when configured.
|
|
||||||
3. Plain model ids are matched against the configured OpenAI-compatible
|
|
||||||
profiles before falling back to the active profile.
|
|
||||||
|
|
||||||
This means a Claude session launched through one compatible profile can still
|
|
||||||
request another compatible profile/model when the proxy can resolve it safely.
|
|
||||||
|
|
||||||
## Scenario Routing
|
|
||||||
|
|
||||||
Scenario routing is now supported through `proxy.routing` in your CCS config.
|
|
||||||
|
|
||||||
Example `~/.ccs/config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
proxy:
|
|
||||||
routing:
|
|
||||||
default: "deepseek:deepseek-chat"
|
|
||||||
background: "ollama:qwen2.5-coder:0.5b"
|
|
||||||
think: "deepseek:deepseek-reasoner"
|
|
||||||
longContext: "openrouter:google/gemini-2.5-pro"
|
|
||||||
longContextThreshold: 60000
|
|
||||||
webSearch: "openrouter:perplexity/sonar-pro"
|
|
||||||
```
|
|
||||||
|
|
||||||
Current scenario detection:
|
|
||||||
|
|
||||||
- `background`: requested model contains `haiku`
|
|
||||||
- `think`: Anthropic `thinking` is enabled
|
|
||||||
- `longContext`: estimated request tokens exceed `longContextThreshold`
|
|
||||||
- `webSearch`: tool list includes `web_search`
|
|
||||||
- `default`: fallback selector when the above do not apply
|
|
||||||
|
|
||||||
Routing decisions are logged through CCS structured logs.
|
|
||||||
|
|
||||||
`longContextThreshold` uses an intentionally approximate token estimate based on
|
|
||||||
message characters, tool payload size, and a `chars / 4` heuristic. Tune the
|
|
||||||
threshold conservatively if your routing decision needs a sharper cutoff near
|
|
||||||
the boundary.
|
|
||||||
|
|
||||||
## How Profile Detection Works
|
|
||||||
|
|
||||||
CCS keeps these profiles in the normal API/settings-profile flow.
|
|
||||||
|
|
||||||
Anthropic-compatible endpoints such as:
|
|
||||||
|
|
||||||
- `https://api.anthropic.com`
|
|
||||||
- `https://api.z.ai/api/anthropic`
|
|
||||||
- `https://api.deepseek.com/anthropic`
|
|
||||||
|
|
||||||
continue to launch directly.
|
|
||||||
|
|
||||||
OpenAI-compatible endpoints such as:
|
|
||||||
|
|
||||||
- `https://router.huggingface.co/v1`
|
|
||||||
- `https://api.openai.com/v1`
|
|
||||||
- `http://localhost:11434`
|
|
||||||
|
|
||||||
are routed through the local proxy for Claude-target launches.
|
|
||||||
|
|
||||||
## Provider Setup
|
|
||||||
|
|
||||||
### DeepSeek
|
|
||||||
|
|
||||||
Use a settings profile whose env looks like:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/v1",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-...",
|
|
||||||
"ANTHROPIC_MODEL": "deepseek-chat",
|
|
||||||
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Typical override target:
|
|
||||||
|
|
||||||
- `deepseek:deepseek-reasoner`
|
|
||||||
|
|
||||||
### OpenRouter
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api/v1",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-or-...",
|
|
||||||
"ANTHROPIC_MODEL": "openai/gpt-4.1-mini",
|
|
||||||
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful when you want:
|
|
||||||
|
|
||||||
- model fan-out behind one provider profile
|
|
||||||
- long-context or web-search scenario targets
|
|
||||||
|
|
||||||
### Ollama / Local Gateways
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:11434",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "ollama",
|
|
||||||
"ANTHROPIC_MODEL": "qwen3-coder",
|
|
||||||
"CCS_DROID_PROVIDER": "generic-chat-completion-api"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For self-signed HTTPS gateways, add `CCS_OPENAI_PROXY_INSECURE=1`.
|
|
||||||
|
|
||||||
### DashScope / Qwen Compatible Mode
|
|
||||||
|
|
||||||
DashScope's compatible endpoint works even when older settings files still
|
|
||||||
carry a stale Anthropic-style provider hint:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://dashscope-us.aliyuncs.com/compatible-mode/v1",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-...",
|
|
||||||
"ANTHROPIC_MODEL": "qwen3.6-plus",
|
|
||||||
"CCS_DROID_PROVIDER": "anthropic"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
CCS now infers the OpenAI-compatible route from the base URL and does not let
|
|
||||||
that stale provider hint block proxy routing.
|
|
||||||
|
|
||||||
## Self-Signed TLS
|
|
||||||
|
|
||||||
If your upstream gateway uses a self-signed or privately issued certificate,
|
|
||||||
set this in the profile settings JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"CCS_OPENAI_PROXY_INSECURE": "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
That flag is respected by both:
|
|
||||||
|
|
||||||
- `ccs <profile>` auto-routing
|
|
||||||
- `ccs proxy start <profile>`
|
|
||||||
|
|
||||||
## Supported Runtime Paths
|
|
||||||
|
|
||||||
- `ccs <profile>` with Claude target: auto-starts the local proxy when needed
|
|
||||||
- `ccs proxy start <profile>`: starts the proxy explicitly
|
|
||||||
- `GET /`: proxy info and bound profile details
|
|
||||||
- `GET /health`: proxy liveness check
|
|
||||||
- `GET /v1/models`: local view of the configured model mapping
|
|
||||||
- `POST /v1/messages`: Anthropic-compatible request entrypoint
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Missing or invalid local proxy token
|
|
||||||
|
|
||||||
- Re-run `eval "$(ccs proxy activate)"`
|
|
||||||
- Check `ccs proxy status` and confirm the expected profile is running
|
|
||||||
|
|
||||||
### Self-signed or private CA upstream
|
|
||||||
|
|
||||||
- Add `CCS_OPENAI_PROXY_INSECURE=1` to the profile settings
|
|
||||||
- Restart the proxy after changing the setting
|
|
||||||
|
|
||||||
### Need to pin or verify the local port
|
|
||||||
|
|
||||||
- Check the active binding with `ccs proxy status hf`
|
|
||||||
- Pin a one-off port with `ccs proxy start hf --port 3460`
|
|
||||||
- Reserve a stable profile port with `proxy.profile_ports`
|
|
||||||
- Re-run `ccs proxy activate hf` after changing the port
|
|
||||||
|
|
||||||
### Provider returns `429` or empty upstream output
|
|
||||||
|
|
||||||
- CCS now preserves upstream rate-limit errors and retry headers
|
|
||||||
- Empty or malformed provider JSON is returned as Anthropic-style `api_error`
|
|
||||||
|
|
||||||
### Slow upstreams: `socket connection was closed unexpectedly`
|
|
||||||
|
|
||||||
- Long-running upstreams (self-hosted LLMs with queue/prefill phases) can stay
|
|
||||||
silent for minutes before the first or next token. The proxy already allows up
|
|
||||||
to 10 minutes per request; set `CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS` in the
|
|
||||||
profile settings to raise or lower that ceiling.
|
|
||||||
- Restart the proxy after changing the setting.
|
|
||||||
|
|
||||||
### Requests route to the wrong model/profile
|
|
||||||
|
|
||||||
- Use an explicit selector such as `profile:model`
|
|
||||||
- Review `proxy.routing` if scenario routing is enabled
|
|
||||||
- Check CCS structured logs in `~/.ccs/logs/current.jsonl` for routing decisions
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
The shipped coverage includes:
|
|
||||||
|
|
||||||
- unit tests for OpenAI-compatible profile detection
|
|
||||||
- unit tests for Anthropic -> OpenAI request translation
|
|
||||||
- unit tests for request-time profile/model routing and scenario routing
|
|
||||||
- unit tests for multi-line SSE parsing
|
|
||||||
- integration tests for `/v1/messages` request/response translation
|
|
||||||
- integration tests for rate limits, empty upstream responses, timeout handling,
|
|
||||||
thinking/tool-call chunk streaming, and request-time routing
|
|
||||||
- integration tests for daemon lifecycle and `/health` / `/v1/models`
|
|
||||||
- e2e tests for `ccs proxy` lifecycle
|
|
||||||
- e2e tests for `ccs <profile>` auto-routing through a mock upstream
|
|
||||||
|
|
||||||
Focused verification command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun test tests/e2e/proxy-command.e2e.test.ts tests/integration/proxy/request-routing.test.ts --coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
Pre-merge gate:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run validate
|
|
||||||
```
|
|
||||||
Reference in new issue
Block a user