Review fixes for Task 12:
1. (CRITICAL) runUnified now calls g.limiter.account BEFORE handling an
adapter error, not after returning on it. All three adapters'
forwardStream deliberately return captured usage alongside a mid-stream
error, so a client that aborts right after the usage frame arrives was
getting tokens delivered but never billed - repeated aborts bypassed
every token/cost budget. A non-streaming error path always carries zero
usage by contract, so accounting it is a no-op (account skips writes
once both total tokens and cost are zero). The zero-usage-stream logf
and the zero-usage-non-stream estimate now only fire when callErr is
nil, so a genuine error no longer gets a fabricated token estimate.
New test: TestHandleChat_MidStreamDropAfterUsageChunk_AccountsPartialUsage
(hijack+close after the usage chunk; asserts the limiter's counter shows
the captured tokens). Verified this test fails against the pre-fix
ordering and the two new alias tests below fail when their guarded
delete() call is removed, before restoring and re-running clean.
2. Added TestHandleChat_OpenAI_AliasKeyStrippedFromUpstreamBody and
TestHandleEmbeddings_OpenAI_AliasKeyStrippedFromUpstreamBody: the
openai-type adapter marshals req verbatim as the upstream wire body, so
it is the one path where a leftover gatewayAliasKey entry actually
reaches a real provider. The only existing alias-leak assertion covered
anthropic, which rebuilds the body field-by-field and was never at risk.
3. Reworded the stale comment on limiter.evaluateScope (limits.go): it
described a nil-limits scope's req counters as a normal, expected case.
Ruling (e), implemented in routes_unified.go's buildLimitScopes, means
the unified route never builds a nil-limits scope in the first place -
the nil check remains a defensive no-op, not a path any current caller
exercises.
Gates: gofmt, go vet, fieldalignment, golangci-lint (incl. gosec) all
clean; go test ./... -race -count=1 passes (full suite, including all
Task 12 tests).
Implements Task 12: POST /v1/chat/completions and POST /v1/embeddings
through a shared pipeline (routes_unified.go: handleChat, handleEmbeddings,
runUnified) wired into ServeHTTP's dispatch in llmgateway.go.
Pipeline: decode (10MiB cap) -> resolve model -> enforce user/group
limits -> call adapter -> account usage, translating every failure mode
into its HTTP envelope per the task brief's carried rulings:
- ALIAS ECHO (ruling a): the client-requested model id is threaded to the
adapter via the internal gatewayAliasKey convention and echoed back in
anthropic/gemini's translated response model field; every adapter
strips the key before building its upstream wire body so it never
leaks to a real provider.
- Limit violations (ruling b): storeDown -> 503 server_error; otherwise
429 rate_limit_error with Retry-After when the violation names a
window.
- Stream-safe error handling (ruling c): statusTrackingWriter.wroteHeader
gates whether an adapter error still gets an envelope; providerHTTPError
wraps into {error:{message,type,code,upstream}}; translateError maps to
400/501; context.Canceled logs quietly; anything else is a 502.
- Usage/accounting (ruling d): zero non-streaming usage gets a
ceil(len(body)/4) prompt-token estimate; zero streaming usage logs once
and accounts the request only. unifiedCostMicros extends costMicros'
single-id lookup to canonical-then-bare without changing its signature.
- Scopes (ruling e): user scope only when u.limits is set, group scope
only when grp.limits is set, user first.
routes_unified_test.go drives every case end-to-end through
Gateway.ServeHTTP against httptest fake upstreams (openai- and
anthropic-shaped): happy path with counters asserted via the limiter's
own store, incremental stream flushing, 401/403/404/429/503, embeddings,
alias echo through a provider-prefixed anthropic request, wrapped
upstream error envelopes, and a mid-stream drop that must not append a
second envelope to an already-committed response.
Gates: gofmt, go vet, fieldalignment, golangci-lint (incl. gosec) all
clean; go test ./... -race -count=1 passes.