docs(config): preserve active developer contracts

This commit is contained in:
Tam Nhu Tran committed 2026-07-26 09:35:42 -04:00
1 parent deb1ed0e67
commit fd0d4362b3
2 files changed
+91 -569

No files matched your search

+58 -202
View File
@@ -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
`codex` sessions in separate terminals, they both write to the same `auth.json`. A token
refresh in one session overwrites the other's credentials.
`ccsx auth` owns `create`, `login`, `switch`, `use`, `show`, `remove`, and
`import-default`. Keep syntax and option changes sourced from
[`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
`~/.ccs/codex-instances/<name>/`. Each profile holds its own `auth.json` and
`history.jsonl`, plus its own session data. Shared `config.toml`, `agents/`, `skills/`,
and plugin cache resources come from `~/.codex/` so configuration and installed plugin
skills stay in sync across profiles.
- `create <name>` is idempotent and starts native `codex login` for new
profiles. `--force` repairs shared resources without replacing `auth.json`.
- `switch <name>` changes the persistent registry default.
- `use <name>` emits only shell-evaluable `CODEX_HOME` and
`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
# Create and authenticate two profiles
ccsx auth create work # creates ~/.ccs/codex-instances/work/ and prompts for login
ccsx auth create personal # same for personal account
`import-default <name>` imports native `~/.codex/auth.json` without deleting the
source. The implementation in
[`import-default-command.ts`](../src/codex-auth/commands/import-default-command.ts)
must continue to:
# Activate per terminal (ephemeral — only this shell)
# Terminal A:
eval "$(ccsx auth use work)"
codex
- refuse import while a current-user Codex process may be refreshing tokens,
unless the operator explicitly accepts the race with
`--force-while-running`;
- 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:
eval "$(ccsx auth use personal)"
codex
## Storage And Cross-Platform Fallback
# Or launch a named profile directly through ccsx
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
```
```text
~/.ccs/
├── codex-profiles.yaml # Registry: version, default, profiles metadata
└── codex-instances/
└── <name>/
├── auth.json # OAuth credentials (Codex writes here)
├── history.jsonl # Per-profile prompt history (optional)
├── sessions/ # Per-profile chat session dirs (optional)
├── config.toml -> ~/.codex/config.toml (symlink — shared)
├── agents/ -> ~/.codex/agents/ (symlink — shared)
├── 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
├── codex-profiles.yaml
└── codex-instances/<name>/
├── auth.json, history.jsonl, sessions/ # profile-local
├── config.toml -> ~/.codex/config.toml
├── agents/ -> ~/.codex/agents/
├── skills/ -> ~/.codex/skills/
└── plugins/
└── cache/ -> ~/.codex/plugins/cache/
```
Only `plugins/cache/` is shared. The profile's parent `plugins/` directory remains a
real local directory so Codex can keep profile-specific plugin metadata beside the
shared cache.
The `plugins/` parent stays profile-local. Shared config, resources, and plugin
cache repair must preserve existing profile-local content. On Windows or other
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
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.
## `ccsx` And `ccsxp` Isolation
## 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
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.
Behavior locks live under `tests/unit/codex-auth/` and
`tests/integration/codex-auth/`.
+33 -367
View File
@@ -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
your API profile points at an OpenAI-compatible chat completions endpoint.
The canonical
[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
- Tuning Engines
- OpenRouter
- Ollama
- llama.cpp servers
- OpenAI-compatible self-hosted gateways
`CCS_OPENAI_PROXY_INSECURE` is read from an OpenAI-compatible profile env.
Truthy values are `1`, `true`, `yes`, and `on` (case-insensitive). When enabled,
the local proxy disables upstream certificate verification for that profile,
including request-time routing to another insecure profile.
## 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
main external reference that informed this CCS work. Their Anthropic/OpenAI
transformer design helped shape the routing approach here.
## Request Timeout
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
- you do not need CCS account/runtime management around the request flow
- default: `600000` (10 minutes);
- 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
- you want the proxy flow available through `ccs <profile>` and `ccs proxy ...`
- you want the routing behavior documented and tested inside the CCS workflow
## Compatibility Boundary
## 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:
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
```
Focused behavior locks live in `tests/unit/proxy/` and
`tests/integration/proxy/`.