From fd0d4362b388e29dc394e5092bfd3af74d859977 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Jul 2026 09:35:42 -0400 Subject: [PATCH] docs(config): preserve active developer contracts --- docs/codex-auth.md | 260 ++++-------------- docs/openai-compatible-providers.md | 400 +++------------------------- 2 files changed, 91 insertions(+), 569 deletions(-) diff --git a/docs/codex-auth.md b/docs/codex-auth.md index d25420ba..827493c4 100644 --- a/docs/codex-auth.md +++ b/docs/codex-auth.md @@ -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//`. 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 ` is idempotent and starts native `codex login` for new + profiles. `--force` repairs shared resources without replacing `auth.json`. +- `switch ` changes the persistent registry default. +- `use ` 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 ` 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 ` 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-` 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 ` | Create profile dir + auto-login | -| `ccsx ` | Launch a named Codex auth profile | -| `ccsx auth login ` | (Re-)authenticate an existing profile | -| `ccsx auth switch ` | Set the persistent default profile for future `ccsx` launches | -| `ccsx auth use ` | 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 ` | Delete profile dir + registry entry | -| `ccsx auth import-default ` | Migrate legacy `~/.codex/auth.json` into a new profile | - -## Persistent vs ephemeral switching - -| Method | Scope | How | -|--------|-------|-----| -| `ccsx ` | One launch | Resolves `` from the Codex profile registry | -| `ccsx auth switch ` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` | -| `eval "$(ccsx auth use )"` | 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/ - └── / - ├── 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// + ├── 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 ` and direct `ccsx ` 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 --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-` 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/`. diff --git a/docs/openai-compatible-providers.md b/docs/openai-compatible-providers.md index 3a507bbe..6c776eda 100644 --- a/docs/openai-compatible-providers.md +++ b/docs/openai-compatible-providers.md @@ -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 ` 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 ` auto-routing -- `ccs proxy start ` - -## Supported Runtime Paths - -- `ccs ` with Claude target: auto-starts the local proxy when needed -- `ccs proxy start `: 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 ` 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/`.