mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
362 lines
9.9 KiB
Markdown
362 lines
9.9 KiB
Markdown
# OpenAI-Compatible Provider Routing
|
|
|
|
CCS can route Claude Code traffic through a local Anthropic-compatible proxy when
|
|
your API profile points at an OpenAI-compatible chat completions endpoint.
|
|
|
|
This is useful for providers such as:
|
|
|
|
- Hugging Face Inference Providers
|
|
- OpenRouter
|
|
- Ollama
|
|
- llama.cpp servers
|
|
- OpenAI-compatible self-hosted gateways
|
|
|
|
## Related Project: claude-code-router
|
|
|
|
[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.
|
|
|
|
When to use CCR:
|
|
|
|
- you want a standalone router without CCS profile integration
|
|
- you do not need CCS account/runtime management around the request flow
|
|
|
|
When to use CCS:
|
|
|
|
- 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
|
|
|
|
## What CCS Does
|
|
|
|
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
|
|
```
|
|
|
|
Then you can use the profile directly:
|
|
|
|
```bash
|
|
ccs hf
|
|
```
|
|
|
|
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`
|
|
|
|
### 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
|
|
```
|