Commit Graph
5 Commits
Author SHA1 Message Date
lukaszraczylo 93b289c1c8 feat: unified image and audio endpoints with gemini image translation 2026-08-20 18:48:02 +01:00
lukaszraczylo 34968e27b2 feat: same-provider retry for transient upstream failures
Adds spec §1's opt-in retry engine: RetryConfig (top-level, zero value
disabled) and retryPolicy (retry.go), wired into upstreamJSON — the
single HTTP chokepoint every adapter's chat/embeddings/listModels call
already funneled through. A transient failure (net error, 429, 5xx)
retries up to Attempts times with exponential backoff capped at 2s,
honoring a qualifying Retry-After header on 429, draining and closing
each failed attempt's body before retrying. Never retries a non-429 4xx,
a translate error, or after context cancellation.

Because upstreamJSON returns before any adapter forwards a byte to the
client (forwardStream/forwardJSON run only after it returns), the
'zero bytes reached client' safety rule and the 'streaming retries only
the initial exchange' rule both hold by construction — no adapter
streaming logic needed to change.

buildAdapters(cfg) keeps its v0.1 signature: it derives the retryPolicy
from cfg.Retry internally rather than taking it as a parameter, so
every existing direct call (15+ across provider_openai_test.go) keeps
compiling unmodified. Every v0.1 test passes byte-identically, without
edits.
2026-08-20 17:11:40 +01:00
lukaszraczylo 8d470b173b feat: native provider passthrough with key injection and best-effort accounting 2026-08-20 11:24:24 +01:00
lukaszraczylo dcfcfb7e48 feat: unified chat and embeddings endpoints with limits and accounting
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.
2026-08-20 10:45:12 +01:00
lukaszraczylo fb7162ab71 feat: anthropic adapter with bidirectional translation incl streaming
Implements Task 9: the anthropic-type providerAdapter and its
request/response/stream translation to and from OpenAI's wire shape.

- translate_anthropic.go: anthropicRequestFromOpenAI (system messages
  joined, content blocks incl. base64 data-URI images, tool_calls ->
  tool_use, tool messages -> tool_result, max_tokens defaulting,
  stop/stop_sequences, tool_choice incl. the none-drops-tools rule,
  n>1/logit_bias/logprobs rejected), openAIResponseFromAnthropic
  (content blocks joined/tool_calls re-marshaled, stop_reason mapping,
  usage), and anthropicStreamState.translate mapping each SSE event to
  zero or more OpenAI chat.completion.chunk payloads. translateError
  carries a notSupported flag so the caller (Task 12) can tell a 400
  from a 501.
- provider_anthropic.go: anthropicAdapter implementing providerAdapter.
  newAnthropicAdapter errors on an empty API key per ruling (c), unlike
  openai-type's keyless ruling. Headers: x-api-key, anthropic-version.
  embeddings always returns a notSupported translateError without
  calling upstream (Anthropic has no embeddings endpoint). listModels
  GETs /v1/models and returns any non-2xx (incl. 404) as-is.
- providers.go: wires the anthropic buildAdapters case to
  newAnthropicAdapter, propagating its constructor error.
- provider_openai_test.go: replaces the now-stale placeholder
  TestBuildAdapters subtest with one asserting a keyed anthropic
  provider builds successfully and a keyless one errors.
- translate_anthropic_test.go: golden-file harness walking
  testdata/anthropic/{chat,resp}_*_in.json against sibling _want.json
  (success) or _wanterr.json (*translateError) fixtures, plus a stream
  harness driving each *.sse fixture through anthropicStreamState and
  asserting the chunk list and usage().
- provider_anthropic_test.go: httptest-backed adapter wiring tests
  (streaming + non-streaming), asserting OpenAI-shaped output on the
  wire, per-event flushing, and the required auth headers.

Gate: go test ./... -count=1 and -race both green; gofmt, go vet,
fieldalignment, golangci-lint (incl. gosec) all clean.
2026-08-20 03:37:15 +01:00