cline-proxy is a small Rust gateway that exposes OpenAI Chat Completions,
OpenAI Responses, and Anthropic Messages APIs over one or more Cline API
keys. Its primary use is running Claude Code or Grok Build against Cline
while filling one configured key at a time.
Routing invariant: ONLY an effective upstream HTTP 429 may trigger Cline API-key switching. This includes a direct HTTP 429 and a high-confidence proxy wrapper that explicitly reports an upstream HTTP 429. Generic 5xx, network failures, timeouts, ambiguous error text, and interrupted streams never switch keys or replay a request.
The binary contains five intentionally small subsystems:
- Axum serves
/v1/messages,/v1/messages/count_tokens,/v1/chat/completions,/v1/responses,/v1/models,/healthz, and/readyz. - One long-lived rustls
reqwest::Clientprovides HTTP/2, gzip, pooling, keep-alive, connect timeout, and read-inactivity timeout behavior. - A concurrency-safe pool holds the sticky active key, per-key effective-429 cooldown metadata (Healthy → Cooling → HalfOpen with single-flight probing), and runtime counters. No lock is held over a network await.
- A debounced writer persists key cooldowns and the active key to a local JSON state file (name-keyed, wall-clock deadlines, atomic rename) so restarts never re-probe keys with known quota cooldowns.
- The Anthropic adapter converts structured messages, images, tool use/results, reasoning, usage, stop reasons, and stateful OpenAI SSE into Anthropic SSE.
- Central error and redaction paths remove configured secrets, Bearer values, API-key fields, cookies, and JWT-like values.
There is no OAuth, credential refresh, database, Redis, web UI, or balance
poller. Configuration is read once from JSON at startup; /admin/status is a
read-only, authenticated operational snapshot.
Rust 1.88 or newer is required by the locked dependency graph.
cargo build --release
install -m 0755 target/release/cline-proxy ~/.local/bin/cline-proxyThe binary can also be run directly with cargo run --release.
Safe local overrides (the binary reads the config file; flags never echo secrets):
./target/release/cline-proxy \
--config /path/to/config.json \
--bind 127.0.0.1:8799 \
--state-file /tmp/cline-proxy-state.json \
--log-directory /tmp/cline-proxy-logs \
--no-color--color auto|always|never controls ANSI on stderr. Default auto: color
only when stderr is a terminal and NO_COLOR is unset.
Copy the example and replace every placeholder locally:
cp config.example.json config.json
chmod 600 config.jsonThe default path is ./config.json. Override it with --config PATH or
CLINE_PROXY_CONFIG=PATH.
The key list is ordered. Disabled entries remain configured but are not loaded:
"cline_api_keys": [
{ "name": "cline-1", "api_key": "YOUR_CLINE_API_KEY_1", "enabled": true },
{ "name": "cline-2", "api_key": "YOUR_CLINE_API_KEY_2", "enabled": true },
{ "name": "spare", "api_key": "YOUR_CLINE_API_KEY_3", "enabled": false }
]Startup rejects an empty or non-header-safe gateway key, zero enabled Cline keys, empty or duplicate key entries, an invalid bind address or URL, embedded URL credentials, zero timeout/size limits, invalid tracing filters, and malformed or reserved upstream headers. Validation errors identify a key by index only and never print its value.
upstream.headers deliberately describes the Cline client identity. The
example sends:
HTTP-Referer: https://cline.bot
User-Agent: Cline/4.1.16
X-Client-Type: cline-vscode
X-Client-Version: 4.1.16
X-Core-Version: 4.1.16
X-Platform: vscode
X-Platform-Version: 1.106.0
X-Title: Cline
These are HTTP semantic compatibility settings, not a claim that rustls reproduces a Chromium, Node, or VS Code TLS ClientHello fingerprint.
The gateway owns Authorization, Host, Content-Length,
Transfer-Encoding, Connection, Accept, Content-Type,
Accept-Encoding, Cookie, x-api-key, x-task-id, and x-request-id;
configuration cannot override them (x-task-id is set dynamically per
session, x-request-id per logical request). Client headers are not blindly
copied upstream. Each upstream request is
constructed with Authorization: Bearer <selected Cline key> after all other
headers have been selected.
models.default plus alias names and targets form the deterministic local
/v1/models result. Aliases are applied consistently to OpenAI chat,
Anthropic Messages, and token-count request accounting. For example:
"models": {
"default": "z-ai/glm-5.3-flash",
"aliases": {
"claude-sonnet-4-6": "z-ai/glm-5.3-flash"
}
}No live model-list request is needed for readiness or model discovery.
Cline runs rotating free-model promotions served live from
GET /api/v1/ai/cline/recommended-models (free[].id — currently
cline-free/deepseek-v4.1-flash, cline-free/muse-spark-1.3-contributor,
z-ai/glm-5.3-flash, cline-free/solar-pro4, poolside/laguna-s-2.1:free).
The gateway fetches that list lazily with a 6-hour TTL cache (bounded to 64
entries, single in-flight refresh, failures degrade silently to the static
list) and advertises the union in /v1/models. Any free model id can be used
in requests directly — non-GLM ids ride the passthrough path with no GLM
policy applied, so the upstream body keeps the requested model verbatim.
models.aliases still takes precedence if you map a client name onto a free
id.
Free-quota exhaustion is per account: the gateway returns 429 with body text
free limit reached on model … try again in <time> and the existing
effective-429 state machine cools that key (parsed retry window, classified
free_quota) and fails over to the next account key.
Upstream requests carry a Cline VSCode client identity. The default profile
tracks the current extension release (4.1.18): http-referer: https://cline.bot,
user-agent: Cline/4.1.18, x-client-type: cline-vscode,
x-client-version/x-core-version 4.1.18, x-platform: vscode,
x-title: Cline. Every value is overridable per deployment through
upstream.headers. Configs still carrying the exact old 4.1.16 default set
are migrated to the current profile automatically at load; user-customized
header sets are never touched. There is no public evidence that
api.cline.bot inspects User-Agent or TLS fingerprints; treat this profile as
a best-effort client match, not a bypass.
Requests are optimized for coding-agent workloads before they reach Cline.
Full rationale and evidence: docs/GLM53_FLASH.md and
docs/adr/0004-glm53-bounded-reasoning-and-context-policy.md.
"glm53": {
"reasoning": {
"default_effort": "high",
"adaptive_effort": "high",
"strip_historical_thinking": true,
"expose_thinking": "requested_only"
},
"limits": { "max_output_tokens": 16384 },
"context": { "safe_compaction": true },
"telemetry": {
"exact_input_tokens": true,
"max_concurrent_token_counts": 1
}
}- Explicit reasoning effort, always. The official GLM template coerces
unset effort to
max; cline-proxy never sends unset. Without explicit client controls the effort ishigh(strong analysis/planning without multi-minutemaxrunaways).disabled->low,adaptive->high, smallbudget_tokens(<8192)->low, large->high, and only an explicitoutput_config.effort: "max"producesmax. Precedence: explicitoutput_config.effort> explicitthinking> proxy default. - Historical thinking is stripped per reasoning epoch. The epoch
boundary is the newest human user message — tool results continue the
current epoch rather than starting one. Reasoning from previous epochs
is removed; the current epoch keeps its in-turn reasoning continuity
across the tool loop. Text, tool calls, call ids, and order are always
untouched. This is the reliable local equivalent of GLM's
clear_thinking, refined by real tool-loop semantics (ADR 0006). - Reasoning shadow store (
shadow_current_turn, default on): when the client did not request thinking, the proxy briefly keeps the in-turn reasoning that issued tool calls and restores it onto the matching assistant turn of the next request in the same epoch — tool loops keep their reasoning continuity without Claude Code ever storing or replaying reasoning. Memory-only, bounded (256 sessions / 64 MiB / 1 MiB per entry / 10-minute TTL), never truncated, never logged, never persisted; disabled automatically when no stable session identity exists. - Thinking exposure is
requested_only: upstream reasoning is surfaced to the client as Anthropic thinking blocks only when the request explicitly carriesthinking. This prevents Claude Code from storing and re-sending reasoning (the main multi-turn amplification source). - Output is capped:
effective_max_tokens = min(client, 16384)by default; a request without a bound gets the cap. - Safe compaction only: lossless structural normalization (single text
block -> string, empty blocks dropped, Anthropic-only
metadatadropped). Tool results are never truncated and tool schemas are never edited; Claude Code keeps full ownership of context compaction. - Model-scoped policy. The GLM semantics above apply only when the
resolved upstream model is a GLM id (
glm*segment detection). Every other OpenAI-compatible model passes through byte-for-byte: no injectedreasoning_effort, no output cap, no message rewriting, no metadata removal. Unknown models fail safe toward compatibility, never toward GLM semantics. - Telemetry: per-request byte breakdown and policy decisions
(
request optimizationlog, including the model family), per-stream reasoning/text/tool-call byte accounting with first-tool-call latency, upstream usage tokens (prompt/completion/cached/reasoning) when provided, and an exact GLM token count of the optimized request. The tokenizer is CPU-bound, so the count runs on the blocking pool under a semaphore (max_concurrent_token_counts, default 1) — never on a Tokio worker and never queued unboundedly; when the slot is busy the count is skipped and logged. Logs contain sizes/counts only, never prompt content.
One request = one summary. A request-local trace aggregates counters,
timings, tokens, and routing in memory; on completion exactly one compact
console line and one JSONL record are emitted through a bounded queue
→ dedicated writer thread (disk IO never runs on a Tokio worker, a
full queue drops the record instead of ever slowing a request). Detailed
lifecycle events live in a request-local RAM flight recorder and are
attached only to anomalous requests (errors, 429, transport failures,
TTFT/duration over logging.slow_ttft_ms/slow_duration_ms).
File logging has a hard disk quota (max_total_size_mb, default 1 GB)
with 85% cleanup watermark and 64 MB rotation; there is no fsync;
failures degrade file logging to disabled — the proxy never fails a
request because of logs. Logs carry names, counts, sizes, timings, and
fingerprints only, never prompts, reasoning, tool output, or raw session
ids. See docs/OBSERVABILITY.md, docs/PERFORMANCE.md, and ADR 0007.
Upstream prompt caches key on byte-exact prefixes, so per-turn byte drift in an otherwise identical conversation wastes prefill. cline-proxy addresses the drift sources it can (issue #8, ADR 0005):
- Volatile billing header: Claude Code prepends an
x-anthropic-billing-header: ...line to the system text whose attribution metadata changes between requests. A leading line of exactly that shape is stripped (LF/CRLF/CR aware); a header mentioned later in the text is never touched. Applied in Anthropic system normalization so the wire body,/v1/messages/count_tokens, and telemetry all see the same normalized system. - Canonical tool-argument JSON: historical assistant
tool_calls[].function.argumentsstrings are re-serialized with deterministic key order so equivalent arguments are byte-identical across turns. Arrays keep order; malformed strings, plain-text tool results, shell output, and source code are never rewritten. - Stable prefix telemetry: each request logs
prefix_hashandprefix_bytes(hash of normalized system + messages + tools). Equal hashes prove local byte stability, not an upstream cache hit. - Session fingerprint: when Claude Code supplies a session identity
(
metadata.user_id/session_id), a 16-hex-char HMAC fingerprint is logged (session=...). Raw ids are never logged; without identity the field isunstableand no session-scoped behavior is attempted. - Upstream X-Task-ID (credential-scoped): the proxy maintains one
internal session fingerprint across credential failover (used for the
reasoning shadow store and local telemetry). The upstream
X-Task-IDis derived from that internal identity AND the selected Cline API key: the same Claude task gets a stable X-Task-ID while using one Cline key, and a different X-Task-ID after switching to a different key. Raw session ids are never forwarded; raw Cline API keys are used only forAuthorizationand as internal HMAC input, and are never embedded inX-Task-IDor logged. Without session identity the header is not sent and affinity is never guessed from connection, IP, key, or recent requests. This is privacy minimization, not unlinkability — Cline server-sideX-Task-IDsemantics are undocumented, and correlation through body, timing, IP, or account ownership remains possible. - Cache ratios:
cache_hit_ratioandreasoning_ratioare logged from upstream usage. Verified against live traffic: Cline reportscached_tokens(subset ofprompt_tokens), and a real ~300 K-token Claude Code session sustained 99.8-100.0% cache hit ratios across consecutive turns with these stability mechanisms enabled (docs/DEVELOPMENT_STATE.md).prompt_cache_keyis not sent — high cache hits are achieved without it.
Claude Code's WebSearch is executed by the gateway, not the client.
web_search_20250305 is fully supported. web_search_20260209 and
web_search_20260318 are accepted only for basic direct search
(allowed_callers: ["direct"]); dynamic filtering, code-execution callers,
encrypted Anthropic citations, and WebFetch are not implemented.
Searches go to Cline POST {base_url}/search/websearch using the same HTTP
client, credential, headers, X-Task-ID, and SOCKS/direct route as chat.
Domain allow/block lists and max_uses (hard cap 5) are enforced locally.
Stream and non-stream /v1/messages both keep one client response across
internal search rounds. Details: docs/WEB_SEARCH.md.
Routing is strict sticky sequential. The active key is used for every request until it confirms an effective upstream HTTP 429; then it enters cooldown and the next configured key becomes active until its quota is exhausted. Successes, 5xx, timeouts, connection resets, TLS/DNS errors, and stream interruptions never rotate the key, and a recovered key whose cooldown expired never steals the active role back from a healthy key.
Key cooldowns and the active selection survive process restarts:
runtime.state_file(default./runtime-state.json; set tonullor""to disable) stores one versioned JSON snapshot.- The file is keyed by configured key name (never by position), so reordering the key list cannot cool the wrong key.
- Deadlines are wall-clock Unix milliseconds; nothing secret is ever written: no API keys, no authorization values, no raw upstream error text.
- Writes are atomic (temp file + rename) and debounced off the request path; healthy requests never touch the disk. A final flush runs during graceful shutdown.
- A missing, truncated, or unreadable state file is non-fatal: the gateway
logs a sanitized warning and starts with empty state. Persistence health is
visible in
/admin/status. - On startup, expired cooldowns are restored as probe-eligible (never as active cooldowns), and the persisted active key is restored if it still exists and is enabled — so a restart no longer re-probes keys that are hours away from quota recovery.
A confirmed effective 429 is further classified as daily_quota, transient,
or unknown (rate_limit_kind in /admin/status). Classification happens
only after the 429 is confirmed and never widens failover conditions.
cargo run --release -- --config config.json
# or
./target/release/cline-proxy --config config.jsonUpstream stall detection (issue #24) is separate from the Reqwest read
timeout (upstream.timeout_secs, default 600, last-resort inactivity
backstop). Defaults:
first_event_timeout_secs = 180 # headers → first SSE/data
first_semantic_timeout_secs = 180 # headers → reasoning/text/tool
stream_idle_timeout_secs = 120 # max gap between upstream bytes
semantic_idle_timeout_secs = 180 # max gap between semantic deltas
0 disables an individual timer. Downstream Anthropic pings do not reset
these. A stalled committed stream emits one Anthropic error and is never
replayed. Optional glm53.context.upstream_context_window_tokens is unset
by default — do not guess a Cline serving limit.
Outbound Cline traffic uses one shared rustls HTTP/2 client. upstream.proxy
is optional:
null / omitted → direct (environment HTTP(S)_PROXY is ignored)
socks5://host:port → SOCKS5, local DNS
socks5h://host:port → SOCKS5, proxy DNS
Override without copying the config: --upstream-proxy socks5://127.0.0.1:10888
or --upstream-proxy direct. Proxy credentials, if present, are never logged.
SOCKS/connect failures do not rotate Cline keys and never replay a committed
stream.
SIGINT and SIGTERM stop new accepts and allow active requests to drain for
runtime.shutdown_timeout_secs before remaining connections are aborted.
Set RUST_LOG to override runtime.log_level. runtime.log_format accepts
pretty or json.
API routes require the gateway's server.api_key, supplied in either form:
Authorization: Bearer <gateway key>
x-api-key: <gateway key>
Health endpoints are intentionally unauthenticated. The gateway key is never forwarded. The Cline keys are never returned to clients.
GET /admin/status (same gateway authentication) returns a read-only
operational snapshot: version, uptime, the active key and its age, and per-key
state (healthy / cooling / half_open / probing), cooldown deadlines,
rate-limit kind, model scope, and request counters. It never returns key
material or raw upstream error text.
Point Claude Code's Anthropic base URL at the local gateway. Provide
CLINE_PROXY_GATEWAY_KEY through your normal secret-management mechanism; it
must equal server.api_key:
POST /v1/messages/count_tokens returns the exact GLM-5.3-Flash prompt
token count, computed in-process with the official tokenizer and official
chat template (zai-org/GLM-5.3-Flash, revision pinned in
docs/GLM53_FLASH.md — no Python, no network at runtime). Responses carry
x-cline-proxy-token-count: exact_glm53_optimized (counting the optimized request the gateway would actually send; the oracle-faithful count is used when the strip policy is disabled). See
docs/adr/0003-glm53-exact-tokenizer.md for the parity guarantees, the
reasoning-effort mapping, and the two documented exclusions (non-text
documents are rejected rather than undercounted; images count as their
template placeholder).
export ANTHROPIC_BASE_URL=http://127.0.0.1:8788
export ANTHROPIC_AUTH_TOKEN="${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}"
export ANTHROPIC_MODEL=claude-sonnet-4-6
export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-sonnet-4-6
export ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-sonnet-4-6
export API_TIMEOUT_MS=600000
claudeIf a Claude Code version uses ANTHROPIC_API_KEY instead, set it to the same
gateway key. The gateway accepts both Bearer and x-api-key authentication.
Messages compatibility includes system and mid-conversation system content,
text, base64/URL images, documents where the upstream supports OpenAI file
parts, assistant tool_use, user tool_result, tools/input schemas,
tool choice, parallel tools, sampling and stop fields, response usage, and
reasoning fields that can be represented as Anthropic thinking blocks.
Streaming uses a bounded, stateful SSE decoder. It emits message_start,
content block start/delta/stop, message_delta, and message_stop; tool
arguments can be split at arbitrary chunk boundaries and multiple call indexes
can be interleaved. Malformed or truncated upstream streams produce one
Anthropic error event and are never replayed. Dropping the downstream body
drops the reqwest body so abandoned streaming work is cancelled upstream.
POST /v1/responses implements the OpenAI Responses wire schema as consumed
by Grok Build (api_backend = "responses"). Point Grok Build's OpenAI base
URL at the gateway and set the gateway key through your normal
secret-management mechanism:
export OPENAI_BASE_URL=http://127.0.0.1:8788/v1
export OPENAI_API_KEY="${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}"
grokRequest support: string input and the full item array (message items with
input_text/output_text/refusal parts, roles system/developer/user/
assistant), function_call history replayed with the original call_id,
function_call_output → tool results, flat function tools with byte-identical
schemas, tool_choice, max_output_tokens, temperature/top_p, and
reasoning.effort (none/minimal/low → low; medium/high/xhigh/max → high).
reasoning.summary requests reasoning exposure (requested_only gate, like
Anthropic thinking). Historical reasoning input items are never replayed
onto the wire; the proxy's reasoning shadow store keeps in-turn continuity
instead.
Backend-hosted web_search declarations convert to the same upstream
web_search function the Anthropic frontend offers: the gateway executes the
search against Cline's /search/websearch endpoint and answers the model,
then streams response.web_search_call.in_progress + output_item.done
frames (action.search with query and source URLs) before the grounded
continuation. The terminal response reconstructs those items, and replayed
web_search_call input items are accepted and rebuilt into the upstream
tool-call chain. Other hosted backend tools (x_search, code_interpreter,
MCP, …) are dropped, not forwarded — this proxy cannot execute them and never
fabricates results. item_reference inputs are rejected (the proxy is
stateless).
Streaming emits the exact typed Responses event lifecycle with monotonic
sequence_number: response.created → item/content/argument
added/delta/done frames → a terminal response.completed,
response.incomplete (length), or response.failed (upstream error; never a
replay). response.output_item.added for a function_call always precedes
its argument deltas (Grok Build requires this ordering). Keepalives are SSE
comments (: ping), never synthetic event frames. Non-stream requests use
one upstream streaming generation aggregated locally.
prompt_cache_key (or x-grok-conv-id / conv-id headers) is used as the
session identity after a domain-separated HMAC fingerprint — the raw value is
never logged or stored, and Responses fingerprints cannot collide with
Anthropic session fingerprints. The GLM-5.3-Flash request policy (explicit
reasoning effort, bounded output, historical-thinking strip, safe compaction,
canonical tool JSON) applies unchanged, and the effective-429 key-failover
invariants are identical to the other frontends.
Set CLINE_PROXY_GATEWAY_KEY to server.api_key through your normal
secret-management mechanism. These commands do not use or reveal a Cline key.
Model list:
curl -sS http://127.0.0.1:8788/v1/models \
-H "Authorization: Bearer ${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}"OpenAI non-stream:
curl -sS http://127.0.0.1:8788/v1/chat/completions \
-H "Authorization: Bearer ${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}" \
-H 'Content-Type: application/json' \
-d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with OK"}]}'OpenAI stream:
curl -N http://127.0.0.1:8788/v1/chat/completions \
-H "Authorization: Bearer ${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}" \
-H 'Content-Type: application/json' \
-d '{"model":"claude-sonnet-4-6","stream":true,"messages":[{"role":"user","content":"Count to three"}]}'OpenAI Responses (Grok Build):
curl -N http://127.0.0.1:8788/v1/responses \
-H "Authorization: Bearer ${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}" \
-H 'Content-Type: application/json' \
-d '{"model":"claude-sonnet-4-6","input":"Count to three","stream":true}'Anthropic stream:
curl -N http://127.0.0.1:8788/v1/messages \
-H "x-api-key: ${CLINE_PROXY_GATEWAY_KEY:?set CLINE_PROXY_GATEWAY_KEY}" \
-H 'anthropic-version: 2023-06-01' \
-H 'Content-Type: application/json' \
-d '{"model":"claude-sonnet-4-6","max_tokens":128,"stream":true,"messages":[{"role":"user","content":"Reply with OK"}]}'Health checks:
curl -fsS http://127.0.0.1:8788/healthz
curl -fsS http://127.0.0.1:8788/readyzSelection is sticky and sequential. With keys 1, 2, and 3, normal requests continue using key 1. If and only if key 1 produces an effective HTTP 429, that key enters cooldown and the same logical request may try key 2. Later requests stay on key 2 until it is rate-limited. When the current key is limited, selection scans forward and wraps to any older key whose cooldown has expired.
An effective 429 is either the configured upstream's direct HTTP 429 response,
or an outer HTTP 5xx whose bounded error body provides high-confidence proxy
evidence. Structured wrappers recognize explicit upstream_status,
upstreamStatus, status_code, statusCode, http_status, and httpStatus
fields; error.status is also accepted inside an error object. Text wrappers
recognize upstream returned [HTTP] 429, upstream status 429, upstream response status: 429, and upstream error: 429 only when the same body also
contains rate-limit or quota semantics. Merely containing the number 429 is
never sufficient.
For one request, every enabled key is tried at most once. The maximum number of
upstream attempts is therefore the enabled key count. If all are cooling or all
return 429, the client receives HTTP 429 with all Cline API keys are currently rate-limited and, when known, an approximate earliest Retry-After.
Cooldown precedence is:
Retry-Afterdelta-seconds or HTTP date.- Clear structured JSON retry fields.
- Text such as
Try again in 2h 30m,Retry in 3h, orRetry after 30ms. upstream.fallback_429_cooldown_secs.
The parser accepts d, h, m, s, and ms, combines components, checks
overflow, caps unreasonable values, handles invalid UTF-8 lossily, and never
panics on malformed input.
Every response not classified as an effective 429—including 400, 401, 402, 403, 404, 408, 409, 422 and generic 5xx responses—is sanitized and returned without switching keys. DNS, TLS, connection, reset, timeout, body-read, invalid JSON, and SSE errors also return without switching. Transport-error strings are never inspected for status codes. There are no hidden generic 5xx retries or automatic auth retries.
Failover classification happens on the initial HTTP error response before any response body is exposed downstream. Error inspection is capped at 64 KiB and the same buffered, sanitized body is used if the error is returned to the client. After an OpenAI or Anthropic stream body exists, no retry path is reachable. This prevents duplicate text, tool calls, commands, file edits, or patches.
Logs include request ID, protocol, model/alias, streaming mode, selected key name/index, status, duration, attempt/failover count, cooldown source and duration, first Anthropic event, periodic stream counters, completion, and sanitized error class. Rate-limit logs preserve both the proxy's outer status and the classified effective status. Prompts and full request headers are not logged.
Configured Cline and gateway key strings are removed from upstream error bodies in addition to structural redaction of Authorization, API keys, access/refresh tokens, cookies, Bearer values, and JWT-like strings. Key names are operational labels and may appear in logs; do not put secret material in a key name.
Protect config.json as a credential file, bind to loopback unless remote
access is deliberately secured, use a long random gateway key, and put TLS in
front of the service before exposing it beyond a trusted host. This gateway
does not encrypt the config file or provide credential management.
- Startup
server.api_key must not be empty: choose a local gateway key; it is separate from all Cline keys. all Cline API keys are currently rate-limited: wait for the earliest cooldown or add another enabled key and restart.- A 401/403 does not move to another key by design. Correct or replace that configured Cline key, then restart.
could not connect to upstream: check DNS, TLS roots, firewall/proxy policy, andupstream.base_url. The request is not retried on another key.upstream stream ended unexpectedly: the provider closed without a final stop reason; the gateway intentionally did not replay generation.- Token counts differ from provider billing: this endpoint is documented as a local approximation because no Cline tokenizer endpoint is assumed.
The test suite is offline and uses deterministic loopback mock upstreams. It covers normal OpenAI/Anthropic requests, SSE and cancellation, reasoning, usage, tool loops and fragmented parallel tools, direct and proxy-wrapped 429 routing, false-positive wrapper rejection, cooldowns, bounded attempts, concurrency, header ownership, and secret redaction.
cargo fmt --all -- --check
cargo check --all-targets
cargo test --all-targets
cargo clippy --all-targets --all-features -- -D warningsSome restricted sandboxes require permission for cargo test to bind
ephemeral 127.0.0.1 ports. No test contacts api.cline.bot.