Skip to content

Credential Proxy

Extensions run in two contexts: extension host (Node, trusted) and webview (browser sandbox, less trusted). Without a proxy, API keys would need to reach the webview for direct LLM calls. The credential proxy keeps keys in the extension host and only exposes a localhost URL + session token to the client.

sequenceDiagram participant WV as Webview participant Proxy as Proxy 127.0.0.1:random participant KS as OS keychain participant UP as Upstream provider WV->>Proxy: POST /anthropic/v1/messages<br/>x-codebuddy-proxy-token: ... Proxy->>Proxy: Validate token Proxy->>Proxy: Rate-limit check (token bucket) Proxy->>KS: Fetch key for "anthropic" KS-->>Proxy: sk-ant-... Proxy->>Proxy: Strip sensitive headers<br/>Inject x-api-key + anthropic-version Proxy->>UP: POST /v1/messages UP-->>Proxy: 200 (streaming) Proxy-->>WV: 200 (streaming passthrough) Proxy->>Proxy: Ring-buffer audit entry
ProviderUpstreamAuth headerFormat
Anthropichttps://api.anthropic.comx-api-keyRaw key
OpenAIhttps://api.openai.comAuthorizationBearer {key}
Groqhttps://api.groq.com/openaiAuthorizationBearer {key}
DeepSeekhttps://api.deepseek.comAuthorizationBearer {key}
Qwenhttps://dashscope-intl.aliyuncs.com/compatible-modeAuthorizationBearer {key}
GLMhttps://open.bigmodel.cn/api/paasAuthorizationBearer {key}
Grokhttps://api.x.aiAuthorizationBearer {key}
Tavilyhttps://api.tavily.comAuthorizationBearer {key}
Localhttp://localhost:11434AuthorizationBearer {key}

Anthropic also gets anthropic-version: 2023-06-01 on every request.

Generated on every proxy start via crypto.randomBytes(32). Header name: x-codebuddy-proxy-token. Missing or wrong token → 403 Forbidden. Prevents other processes on the machine from using the proxy.

Per-provider token bucket:

  • Each provider has its own bucket with maxTokens + refillRate.
  • Local providers exempt (no upstream rate caps).
  • Empty bucket → 429 Too Many Requests with Retry-After.
  • Live-reloads on config change (buckets reset).

Localhost-only binding. srv.listen(0, "127.0.0.1", cb) — never exposed to the network.

Header stripping — sensitive headers removed from the client request before forwarding:

  • authorization
  • x-api-key
  • x-goog-api-key
  • host
  • connection, keep-alive, transfer-encoding

Then the proxy injects the correct credentials from the keychain.

Body limits:

  • Max body: 10 MB → 413 Payload Too Large
  • Client idle: 30 s between body chunks → drop
  • Upstream: 5 min timeout (streaming responses)

Error mapping — no internal details leaked:

UpstreamProxyMessage
ECONNREFUSED502Upstream refused connection
ECONNRESET502Upstream reset connection
ETIMEDOUT504Upstream connection timed out
ENOTFOUND502Upstream host not found
ECONNABORTED504Request timed out

Ring buffer, 1000-entry cap, O(1) writes:

{
"timestamp": 1711612800000,
"provider": "anthropic",
"method": "POST",
"path": "/v1/messages",
"statusCode": 200,
"latencyMs": 1523
}

Oldest entries evicted when full — bounded memory.

stateDiagram-v2 idle --> starting : start() starting --> running : Listening starting --> stopped : Startup error running --> draining : dispose() draining --> stopped : Connections closed stopped --> idle : restart
  • start() is promise-coalesced — concurrent calls share one promise.
  • dispose() drains: stop accepting → force-close tracked sockets after grace period → config watcher disposed → stopped.
SettingDefaultPurpose
codebuddy.credentialProxy.enabledfalseEnable the proxy
codebuddy.credentialProxy.rateLimits{"anthropic":60,"openai":60,"groq":30,"deepseek":60,"qwen":60,"glm":60,"grok":60}Per-provider requests/min (override individual providers)

With the proxy enabled, getAPIKeyAndModel() returns a proxy URL instead of the real upstream. LangChain wrappers (and completion providers) hit http://127.0.0.1:{port}/{provider}/... — the proxy injects credentials and forwards. Client-side code never sees the real key.

  • Security — the broader security model
  • Providers — provider selection + failover