Commit Graph
6 Commits
Author SHA1 Message Date
lukaszraczylo 2c8a32c77a fix(media): rewrite prefixed alias in multipart replay, strip alias key, multipart validation 2026-08-20 19:03:54 +01:00
lukaszraczylo 93b289c1c8 feat: unified image and audio endpoints with gemini image translation 2026-08-20 18:48:02 +01:00
lukaszraczylo 722e6f3d29 fix(retry): retries-after-first semantics, ctx-aware backoff, hardened classification
Critical: attempts now counts retries performed AFTER the first try
(default 1 -> two tries total), not the total try count. The prior
reading made retry:{enabled:true}'s own documented default perform zero
retries. (*retryPolicy).totalTries() is the single conversion point,
clamping any attempts<1 to 1 so do()'s loop bound can never compute <=0
and skip calling call() entirely (no (nil,nil) return path).

Backoff waits are now ctx-aware: sleepFn func(time.Duration) is replaced
by waitFn func(context.Context, time.Duration) bool, whose production
implementation (defaultWaitFn) selects between time.After(d) and
ctx.Done(). A context canceled mid-wait aborts the remaining retries
immediately instead of blocking out the full backoff.

Also: classify a request-build failure (invalid method/URL) as
non-transient via errRequestBuildFailed, since retrying an identically
malformed request only wastes attempts; drop the drain cap to a
dedicated 64KiB constant instead of the 32MiB response cap; fix a
t.Fatal call from a non-test goroutine in the mid-stream-death test;
drop the misleading omitempty on Config.Retry (a struct value is never
'empty' to encoding/json); document the Retry-After >2s-falls-back-to-
backoff behavior and cover it with a direct table test.

README: add the retry config table, a RetryConfig field reference, and
a Retry section documenting the semantics. examples/kubernetes.yaml
gains a retry block alongside its other optional sections.

Gate: go test -race, gofmt, vet, yaegi-check, golangci-lint (incl a
fieldalignment fix on two new test structs) all clean. 295 subtests
pass, 0 failed. integration/ untouched.
2026-08-20 17:34:52 +01:00
lukaszraczylo c9ae14adf7 docs: spec limit semantics, pricing caveats, manifest hedge
I2 (controller ruling): docs/superpowers/specs/2026-08-19-traefik-llmgateway-design.md
described limits as "user overrides group" / "user limit if set, else
group limit, else unlimited" — stale relative to the actual, already-correct
implementation (buildLimitScopes/checkAndCount evaluate every configured
scope and return the first violation). Amended §2 and §7 to state both
scopes are enforced together and the tighter bound wins.

I4 (manifest hedge): .traefik.yml's groups.default was a bare "{}",
the shape empirically reproduced as a decode failure outside this
repo's own (necessarily simplified) yaegi-check harness. Changed to
"providers: []" — same zero semantics, no longer a bare empty object.
manifest_test.go's JSON mirror and drift-check token updated to match;
make yaegi-check still passes (its YAML-subset parser already handles
an inline "[]" under a nested map key).

M9 (pricing provenance): pricing.go's built-in table comment replaced
"verify before release" with a real source note (each provider's own
published list prices, recorded 2026-08, approximate). README's limits
section gained the matching caveat and points to the "pricing" config
override for billing-grade accuracy.

Doc nits: README's Kubernetes section now says the example manifest
applies cleanly once Traefik's CRDs (Middleware, IngressRoute) are
installed, rather than claiming kubectl apply -f works unconditionally.
README's Security notes section no longer claims "no auth-event logging
at all" (see the paired fix: commit) — it now describes what is logged
and that key material never appears.

This commit and its sibling fix: commit are the final wave closing out
the whole-branch review findings for docs/superpowers/sdd/2026-08-20-
traefik-llmgateway.

Gates: go test ./... -count=1 (-race) green, gofmt -l . empty, go vet
clean, make yaegi-check OK, golangci-lint 0 issues.
2026-08-20 14:59:57 +01:00
lukaszraczylo 5a11cc85be docs: correct security claims, pipeline diagram, k8s example completeness
Fixes from review, verified against source before rewriting:

- README.md Security notes: "auth events are logged" was false — grepped
  auth.go, zero logging calls exist anywhere in it. A lookup miss is a
  plain 401 envelope with no log line at all, not "logged without key
  material". Also corrected the adjacent providerHTTPError claim: the
  struct retains the upstream response body as a field (surfaced
  deliberately to the client under error.upstream); only its Error()
  string omits the body, and nothing in this codebase currently logs a
  providerHTTPError via a bare %v anyway.
- README.md mermaid diagram: the previous version showed a single global
  Auth gate ahead of routing. ServeHTTP (llmgateway.go) actually dispatches
  on path+method first and calls identify() separately inside each
  recognized-route branch; an unrecognized route (passthroughUnknown/404
  fallback) never calls identify() at all. Redrawn Route -> per-branch
  Auth, with the unauthenticated fallback branching directly off Route.
  Also fixed the 401 node's error type label from the nonexistent
  "invalid_api_key" to the actual envelope type, "authentication_error"
  (llmgateway.go, routes_unified.go). Re-rendered with mmdc before and
  after the fix.
- README.md + examples/kubernetes.yaml: narrowed the bare-{} empty-group
  claim. Re-verified this session with an isolated, minimal repro
  (Traefik file provider only, no docker-compose configs: mechanism
  involved) that a bare `default: {}` still fails plugin construction
  with the file provider specifically: "expected a map, got 'string'".
  Scoped the claim to what is actually verified (file provider, twice)
  and explicitly flagged Docker/ECS label providers and the Kubernetes
  CRD provider as untested, rather than asserting the file provider is
  unaffected or that the CRD provider shares its decode path — the
  latter was an unverified inference in the prior text, now hedged.
- examples/kubernetes.yaml: the IngressRoute referenced an undefined
  Service (llmgateway-dummy-backend), so the manifest did not actually
  apply as advertised. Added the missing Deployment + Service (traefik/
  whoami, the same image already exercised end-to-end in
  examples/docker-compose.yml this session). Re-validated: 6-document
  YAML parses, Middleware's plugin config still round-trips through the
  real Config struct and New() (throwaway test, deleted after).
- README.md Known limitations: dropped the standalone path-traversal
  bullet — it duplicated content already stated in the "request rejected
  before it reaches a provider" bullet immediately above it.

NOTE for the reviewer: the ruling on the bare-{} finding stated "file
provider observed working" and named Docker/ECS label providers as the
affected ones. I re-tested this directly (twice, including a standalone
repro outside the compose demo) and the file provider fails on a bare
{} exactly as originally reported — I could not reproduce "file provider
unaffected" and have not tested the label providers at all. Narrowed the
doc wording to only what I verified rather than asserting either
provider's behaviour without evidence; flagging the discrepancy in case
the review had independent grounds I don't have visibility into.

Gates: go test ./... -count=1, gofmt -l ., go vet ./..., make
yaegi-check — all clean. Also re-ran go test -race, golangci-lint (incl.
gosec), and fieldalignment, matching the standard this task already set;
all clean.
2026-08-20 14:30:32 +01:00
lukaszraczylo 08dca6ad98 docs: README, kubernetes and compose examples, MIT license
- README.md: what-it-is, quickstart (localPlugins today, catalog form
  once published), full config reference table for every Config/
  ProviderConfig/GroupConfig/LimitsConfig/UsersConfig/UserConfig/
  RedisConfig/ModelPricing/TargetConfig/AgentConfig field, model routing,
  limits and accounting semantics, unified vs passthrough differences,
  MCP/A2A, security notes, known limitations (streaming buffering under
  Yaegi, quota-on-reject nuances, identity-encoding, path-traversal
  rejects), Kubernetes deployment notes, and development/catalog-
  submission status. Every claim grounded against the actual source read
  this session; two draft claims (user-limits "replace" group limits, an
  unqualified "go-redis is documented as unsuitable in resp.go/
  providers.go") were caught and corrected against the real
  buildLimitScopes semantics and an actual grep before committing.
- examples/kubernetes.yaml: Secret (users.json) + Middleware CR +
  IngressRoute + a Helm values snippet. The Middleware's plugin config
  round-trips through the real Config struct and New() (verified via a
  throwaway, since-deleted test), not just YAML-syntax-checked.
- examples/docker-compose.yml: minimal localPlugins-based demo (traefik +
  whoami + inline file-provider config via compose's inline configs.content).
  Brought up and exercised end-to-end against real OpenAI this session:
  auth 401/200, /v1/models, /v1/chat/completions (upstream 401 envelope
  with a fake key, proving passthrough+key-injection+wrapping all work),
  and the unknown-route 404 envelope. Found and fixed a real bug in the
  process: a bare "{}" empty group value fails Traefik's own dynamic-
  config decode with "expected a map, got 'string'"; fixed to an
  explicit empty list and documented the gotcha in both example files and
  the README.
- LICENSE: MIT, copyright 2026 Lukasz Raczylo.
2026-08-20 14:12:05 +01:00