# LLM proxy Hankweave can route its Anthropic API traffic through an optional local HTTP proxy that sits between the runtime and the upstream API. The proxy is disabled by default (`withoutProxy: true`); when you enable it, requests pass through a middleware pipeline on the way out and responses pass back through it on the way in. Its only supported mode is `passthrough` – the module has no caching, load balancing, or authentication layer, and it is not intended to grow one. This page is the reference for that behavior: how to enable the proxy, which traffic actually traverses it, how the request/response pipeline works, and what the middleware classes do. The command-line flags that turn it on are documented in [reference/cli](/0.10.0/files/reference/cli); here we cover what happens after you pass them. ## What the LLM proxy is A passthrough proxy applies request middleware, forwards the request to its configured upstream, applies response middleware, and returns the response. The word "mode" should not suggest a choice among routing or caching strategies: `ProxyRunner.start()` accepts only `passthrough`, and no other value exists to select. ## Enabling and configuring the proxy Use `--proxy` to enable the local proxy. A second flag, `--without-proxy`, sets `withoutProxy: true` for backward compatibility; it is redundant because the proxy is off by default, and it is not advertised in `--help`. The complete flag contracts belong to [reference/cli](/0.10.0/files/reference/cli). Two settings in `hankweave.json` relate to the proxy but do different jobs. The configuration schema (the machine-readable field contract) lists them as: | field | type | default | required | constraints | description | | ------------------ | --------- | ------- | -------- | ----------- | ----------------------------------------------------- | | `withoutProxy` | `boolean` | | no | | Bypass internal LLM proxy | | `anthropicBaseUrl` | `string` | | no | | Custom Anthropic API base URL (for corporate proxies) | `withoutProxy` controls whether the proxy runs at all; `anthropicBaseUrl` selects where it sends traffic. Both have environment-variable counterparts – `HANKWEAVE_RUNTIME_WITHOUT_PROXY` and `HANKWEAVE_RUNTIME_ANTHROPIC_BASE_URL` – mapped through the generic `HANKWEAVE_RUNTIME_*` loader. For the boolean variable, only `"true"` and `"1"` parse as `true`; every other non-empty value, including `"false"`, parses as `false`. This produces a genuinely confusing combination: `HANKWEAVE_RUNTIME_WITHOUT_PROXY=false` *enables* the proxy, while the default `withoutProxy: true` disables it. Configuration layering is described in [concepts/hanks](/0.10.0/files/concepts/hanks), and the generated environment-variable entries are in [reference/environment-variables](/0.10.0/files/reference/environment-variables). The base-URL setting has its own trap. `anthropicBaseUrl`, `HANKWEAVE_RUNTIME_ANTHROPIC_BASE_URL`, and `--anthropic-base-url` select the proxy's upstream – the default upstream is `https://api.anthropic.com` – but none of them enable the proxy. When the proxy is disabled, `--anthropic-base-url` is not consumed for codon traffic (model work performed by a Hankweave task). If you set a custom upstream intending it for the internal proxy, enable the proxy separately with `--proxy`. > **Pitfall:** The upstream URL setting is not a universal provider gateway. Pi-harness codons bypass this proxy, and `--without-proxy` is a compatibility flag rather than the primary way to configure it. ## Ports and lifecycle Once enabled, the proxy starts after the WebSocket server (the persistent client-connection service) binds. Its preferred port is the actual WebSocket port plus one; because the WebSocket port is OS-assigned by default, a port such as `7778` is illustrative, not a fixed default. If that preferred port fails with an address-in-use-class error, the proxy retries with an OS-assigned dynamic port. The `+1` value is an adjacent-port convention; no rationale for it is documented. Startup leaves three traces you can use to find the running proxy. The startup console box includes `Proxy: http://localhost:`. The execution lock file is updated with `proxyPort`; lock-file discovery belongs to [integrate/deployment-model](/0.10.0/files/integrate/deployment-model). And since 0.4.1, the connected client also receives an optional `proxyPort` in `server.ready`; that event is not journaled, and its contract is in [reference/events](/0.10.0/files/reference/events). The proxy stops when the server shuts down. Two paths bypass the middleware pipeline entirely. `GET /health` and `GET /` return status `200`, content type `text/plain`, and the body `Hankweave Proxy OK`, so you can probe a running proxy directly: ```sh curl http://localhost:7778/health # Hankweave Proxy OK ``` The port in this example is illustrative; use the port shown at startup or exposed as `proxyPort`. Every other path enters the middleware pipeline described below. One lifecycle caveat concerns idle timeouts. The help text says `--idle-timeout` applies to WebSocket and proxy servers, but at this tag the configured value is not passed to `ProxyRunner`, so the proxy uses its constructor default of `0` (no timeout). ## Which codon traffic goes through the proxy Enabling the proxy does not route all of Hankweave's model traffic through it. The proxy URL has exactly one consumer: codon dispatch. When the proxy runs, codons dispatched to the Claude Agent SDK harness receive `ANTHROPIC_BASE_URL` set to the local proxy URL, overriding any ambient value. ![When enabled, the proxy handles Anthropic API traffic from SDK codons, not every provider call.](/content-assets/cf45dff5691c48c0/diagrams/reference-llm-proxy/1.svg) When enabled, the proxy handles Anthropic API traffic from SDK codons, not every provider call.
Diagram as text ```text With the local proxy enabled: Claude Agent SDK codons (Anthropic API requests) | | ANTHROPIC_BASE_URL v local LLM proxy -- HttpTransport --> configured Anthropic upstream Pi harness codons -----------+ +-- bypass the proxy --> model provider's API sentinel calls / self-tests -+ ```
Everything else resolves its endpoint elsewhere. Codon self-tests and sentinel model calls do not receive the proxy URL. Sentinel calls resolve through the provider registry; the Anthropic provider factory supplies an API key but no base URL, so those calls use the SDK-default Anthropic endpoint regardless of the proxy. Codons on the Pi harness never traverse the proxy at all. The Pi manager receives no proxy base URL, and Pi does not use environment base URLs for this routing. Conversely, with the proxy disabled, an ambient `ANTHROPIC_BASE_URL` is passed through to the Claude Agent SDK unchanged – which is why the base-URL setting in the previous section only matters when the proxy is on. ## The request/response pipeline For traffic that does enter the proxy, the pipeline runs in a fixed order. The proxy extracts request headers and body, attempts to parse the body as JSON matching `claudeApiRequestSchema.passthrough()`, applies request middleware in registration order, forwards through the transport, applies response middleware in order, and returns the final `Response`. A failed parse only produces a debug log entry; the request still forwards with its raw body. Middleware receives the request without `body` and receives the body separately, a split that matters when you read the middleware source in the next section. When `claudeRequestData` is present, the proxy serializes that structured value back into the body; otherwise it keeps the original body. `HttpTransport` rewrites `Host` for the target host and removes `Content-Length` so it can be recalculated after a body change. Error and streaming behavior is deliberately plain. A non-200 upstream response is logged as an error and forwarded with its original status and body. Responses marked `text/event-stream` or `chunked` are forwarded as streams; middleware that reads a stream consumes it. Any exception during processing returns status `500` with body `Proxy Error`. ## When middleware is source-internal The middleware classes are not a public extension API. `LLMProxyMiddleware`, `LoggingMiddleware`, and `HttpTransport` are not exported from the module, and the published package exports only `.`, `./schemas`, and `./types`. The source module's exported symbols include `LLMProxyRequest`, `LLMProxyResponse`, `LLMTransport`, `DoubleMaxTokens`, `LLMProxy`, `createPassthroughProxy`, and `ProxyRunner`; these are contributor building blocks, not package-reachable middleware APIs. `LLMProxy.addMiddleware` and `removeMiddleware` exist, but registration is source-level: there is no user-facing registration API, configuration key, or plugin path. The request and response shapes below are shown for contributors reading or modifying the source: ```typescript export interface LLMProxyRequest { /** HTTP method (GET, POST, etc.) */ method: string; /** Request URL path and query parameters */ url: string; /** HTTP headers as key-value pairs */ headers: Record; /** Request body content */ body?: string; /** Parsed Claude API request data if the body contains a valid Claude request */ claudeRequestData?: ClaudeApiRequest; } /** * LLMProxyRequest without the body field, used in middleware processing. Body is removed to avoid temptation to modify it directly. */ type LLMProxyRequestWithoutBody = Omit; /** * Represents a response from the LLM proxy */ export interface LLMProxyResponse { /** HTTP status code */ status: number; /** Response headers as key-value pairs */ headers: Record; /** Response body, either as string or streaming data */ body?: string | ReadableStream; } ``` Note the relationship between these types and the pipeline described above. The hooks use `handleRequest(request: LLMProxyRequestWithoutBody, body?: string)` and `handleResponse(response: LLMProxyResponse)`. The transcluded demonstration annotates its parameter as `LLMProxyRequest`, but processing supplies the body-less request and separate body argument. A middleware changes structured `claudeRequestData` rather than assigning the raw body directly. In a default run, `createPassthroughProxy` installs exactly one middleware, `LoggingMiddleware`. It logs method and URL; for Claude requests it logs `model`, `messages`, `max_tokens`, and `stream`, while other bodies are truncated at 500 characters. Proxy activity is written to `.hankweave/logs/server.log` in the execution directory with `[LLM-PROXY]`, `[PROXY-HTTP-TRANSPORT]`, and `[LOGGING-MIDDLEWARE]` tags. Log inspection belongs to [operate/observe-and-debug](/0.10.0/files/operate/observe-and-debug). A typical exchange looks like this: ``` [LOGGING-MIDDLEWARE] Received request POST /v1/messages [LOGGING-MIDDLEWARE] Claude request - model=claude-haiku-4-5, messages=5, max_tokens=8192, stream=true --- [LOGGING-MIDDLEWARE] Response status: 200 --- ``` The source also ships `DoubleMaxTokens`, an exported demonstration middleware. It doubles `max_tokens` when a Claude request includes it, but it is not installed by default and is not importable from the published package: ```typescript export class DoubleMaxTokens extends LLMProxyMiddleware { /** * Modify the request to double the max_tokens parameter if present * @param req - The request to modify * @returns Promise resolving to the modified request */ override async handleRequest(req: LLMProxyRequest): Promise { if (req.claudeRequestData?.max_tokens) { const originalMaxTokens = req.claudeRequestData.max_tokens; req.claudeRequestData.max_tokens = originalMaxTokens * 2; } return req; } } // -------------= ``` Treat it as an example of the hook contract, not an available feature. Source-level middleware changes require contributor work; see [contribute/runtime-architecture](/0.10.0/files/contribute/runtime-architecture). ## Limitations Structured `claudeRequestData` exists only for bodies matching the Anthropic Messages API shape. Other traffic remains raw-body passthrough, and middleware cannot edit that raw body directly. The proxy is intentionally limited to passthrough behavior: it does not provide caching, load balancing, or an authentication layer. For the owning references, see [reference/cli](/0.10.0/files/reference/cli) for flags, [reference/hankweave-json](/0.10.0/files/reference/hankweave-json) for configuration keys, [reference/environment-variables](/0.10.0/files/reference/environment-variables) for `HANKWEAVE_RUNTIME_*` variables, [reference/events](/0.10.0/files/reference/events) for `server.ready.proxyPort`, [integrate/deployment-model](/0.10.0/files/integrate/deployment-model) for lock-file discovery, [contribute/runtime-architecture](/0.10.0/files/contribute/runtime-architecture) for internals, and [operate/authentication-and-models](/0.10.0/files/operate/authentication-and-models) for harness selection.