feat(proxy): support claude.base_url for host-side LLM proxies#191
Conversation
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 21s —— View job Code Review
SummaryThe feature design is solid — localhost rewriting, credential mirroring, and the strict-policy allowlist integration are all sensible. There's one bug that would cause the primary use case to silently fail, and a couple of defensive-programming gaps. One bug (critical):
Two defensive gaps:
One minor omission: IPv6 loopback Everything else looks good: the config validation, the |
7736002 to
f5d4d3a
Compare
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 41s —— View job Code Review
SummaryThe feature is well-structured: config validation, proxy credential mirroring, and network policy wiring all follow existing patterns cleanly. One real bug and a few smaller issues. Bug — silent credential injection failure for base URLs with a port (
// line 697 — change .Host to .Hostname()
claude.ConfigureBaseURLProxy(p, anthropicCred, rewrittenURL.Hostname())The existing tests don't catch this because Smaller issues:
|
f5d4d3a to
9e2a0fa
Compare
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 5m 11s —— View job Code Review
SummaryThe relay architecture (new in this revision) is a good design — routing Two bugs, same root cause (
Other issues:
|
Add claude.base_url field to agent.yaml that redirects Claude Code API traffic through a host-side LLM proxy (e.g., Headroom). This enables caching, logging, and telemetry for Anthropic API calls. The implementation handles four problems that prevented using LLM proxies: - Sets ANTHROPIC_BASE_URL inside the container - Rewrites localhost to container-reachable host address - Injects credentials for both api.anthropic.com and the proxy host - Allows the proxy host under strict network policy
9e2a0fa to
60f29ba
Compare
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 7m 27s —— View job Code Review
SummaryThe PR has been significantly redesigned since the last review — the relay pattern ( Two bugs that will cause silent failure for the primary use case (
Security concern:
OAuth correctness:
Smaller issues (some carried over from previous review):
|
Without checking r.URL.Host == "", a proxied request like GET http://anything.com/relay/foo would match the relay path prefix and bypass proxy authentication. Now only direct HTTP requests (r.URL.Host empty) can reach relay endpoints.
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 5m 30s —— View job Code Review
SummaryThe relay architecture is the right approach — routing However, the three bugs flagged in every previous review iteration are still present in this push. Two bugs that cause the feature to silently fail (critical, unfixed):
The tests don't catch either bug because One streaming bug (unfixed):
Smaller issues:
|
ConfigureBaseURLProxy passed host:port (e.g., localhost:8787) to SetCredentialWithGrant, but isValidHost rejects colons, silently dropping the credential. The relay then forwarded Claude Code's placeholder token instead of the real one, causing 401 errors. Fix: strip port in ConfigureBaseURLProxy before registering, and use getCredential/getExtraHeaders/getRemoveHeaders helpers in the relay handler (which handle host:port fallback) instead of raw map lookups.
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 46s —— View job Code Review
SummaryThe two most recent commits ( What was fixed:
Still open:
|
Documentation: - Fix inaccurate "localhost rewriting" claims in docs, example, and code comments — the implementation uses a relay pattern, not host rewriting - Remove incorrect claim about strict network policy auto-allowing the base URL host (not needed with relay pattern) Security & robustness: - Add input validation to AddRelay (reject empty names, slashes, invalid URLs) - Add ResponseHeaderTimeout to relayClient to prevent goroutine leaks - Remove internal host/error details from relay error responses - Log warning on url.Parse failure in manager instead of silent skip - Handle AddRelay error return in manager Testing: - Add 12 relay handler tests: forwarding, credential injection, header removal, unknown relay 404, proxy header filtering, response header copying, unreachable target 502, empty path, body streaming, proxied-request auth bypass guard, and AddRelay validation - Add ConfigureBaseURLProxy test for host without port
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 22s —— View job Code Review
SummaryGood progress since the last iteration — the two critical bugs are now fixed and the test coverage is solid. Three issues remain open from previous reviews. What was fixed:
Three remaining open issues: 1. Streaming not flushed per-chunk (
Fix — replace the single copy with a flushing loop: buf := make([]byte, 4096)
flusher, canFlush := w.(http.Flusher)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, _ = w.Write(buf[:n])
if canFlush {
flusher.Flush()
}
}
if err != nil {
break
}
}2. Silent failure when no grant is active ( When if opts.Config != nil && opts.Config.Claude.BaseURL != "" {
if anthropicCred == nil {
ui.Warn("claude.base_url is set but no anthropic or claude grant is active — ANTHROPIC_BASE_URL will not be set")
} else if baseURL, parseErr := url.Parse(opts.Config.Claude.BaseURL); parseErr == nil {
// ... existing relay setup
}
}3. Grant inconsistency in example file ( The prerequisite comment on line 17 says Minor: security note incomplete ( The comment notes that relays skip auth because direct HTTP requests don't carry |
- Add ui.Warn when claude.base_url is set but no anthropic/claude grant is active (silent misconfiguration) - Add incremental flushing for SSE/streaming responses in relay handler (flush after each read instead of buffering in io.Copy) - Fix example prerequisite comment to match claude grant
|
Claude finished @dpup's task in 3m 46s —— View job Code Review
SummaryThis is a significant improvement over the previous iterations. All critical bugs and most of the smaller issues flagged across six prior review cycles are now resolved. The PR is close to merge-ready. What's fixed:
Two remaining issues: 1. Security comment incomplete ( The comment explains why auth is skipped (direct requests don't carry // Limitation: on Apple containers (0.0.0.0 binding), the relay endpoint is
// reachable from the LAN without the auth token. Relays only forward to
// pre-configured URLs, which limits impact, but is worth documenting.2. Test coverage gap for the host:port lookup path (
// Also test that host:port registration works (port is stripped by SetCredentialWithGrant
// via isValidHost, so getCredential's SplitHostPort fallback must find it)
p.SetCredentialWithGrant(backendHost, "Authorization", "Bearer sk-real-token", "claude")
// ...same request, same expectationMinor:
|
Partial solution for #26
Summary
Add
claude.base_urlfield toagent.yamlfor routing Claude Code API traffic through host-side LLM proxies like Headroom.How it works: Moat sets
ANTHROPIC_BASE_URLto a relay endpoint on the Moat proxy (/relay/anthropic/). The relay forwards requests to the configured URL with credentials injected. This works transparently withlocalhostURLs because the relay runs on the host.Traffic flow:
Claude Code → Moat proxy relay → Headroom (localhost:8787) → api.anthropic.comChanges
claude.base_urlfield with URL validation (http/https only, must have host)internal/proxy/relay.go): GenericAddRelay(name, url)mechanism with name/URL validation, credential injection via existinggetCredential/getExtraHeaders/getRemoveHeadershelpers, proxy header filtering, and response header timeoutANTHROPIC_BASE_URLenv varr.URL.Host == "") to prevent proxied requests from bypassing auth; error responses don't leak internal host infoclaude.base_url, guide section on LLM proxies, workingexamples/llm-proxy/example with HeadroomTest plan
go build ./...go test ./internal/proxy/ ./internal/config/ ./internal/providers/claude/ ./internal/run/make lint— 0 issuesmoat claude examples/llm-proxywith Headroom on port 8787 — Claude Code API traffic routed through Headroom with credentials injected