mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
Review response (5 Important + 3 folded):
1. README's cacheTTL row/bullet implied per-group scoping; cacheTTL sets
the TTL written when a group's own request populates a shared cache
entry, it never scopes the entry itself. Reworded only, cacheKey
unchanged.
2. README's fallback-counter claim ("used only when redis is not
configured") was wrong: limits.go also falls back during a Redis
outage whenever failOpen is true (the default). Reworded against
limiter.storeIncrBy/storeGet.
3. Dropped the unevidenced "in-flight requests finish on the old
instance" claim; replaced with what TestConfigHotReload actually
observes (zero non-200s across the rewrite window, every run).
4. Real bug: cleanup's settle poll checked for the injected alias to
disappear, which trivially no-ops on the skip path (the alias was
never live, so "absent" was already true on the first check) —
exactly the unguarded window that caused the earlier 404 cascade.
Replaced with settleToBaseline: captures the baseline /v1/models id
set up front, then polls unconditionally (both paths) for 3
consecutive matching polls, bounded 20s. Verified against a live run
that actually hit the skip path.
5. The ~20rps probe loop shared aliceKey/"eng" (1000/min) with most of
this suite's other tests; a budget-exhaustion 429 would look identical
to a reload blip. Added a dedicated hotreload group
(requestsPerMinute: 100000) and hotreload-probe user to
dynamic.yml.tmpl; every request TestConfigHotReload issues now uses
that key.
6. examples/docker-compose.yml still shipped providers.file.filename,
contradicting the new guidance; switched to providers.file.directory.
Verified by actually running the demo stack.
7. waitForModelAliasBounded and the new settle poll now use
doJSONNonFatal (returns an error instead of calling t.Fatalf), so a
transient transport error resets that poll's retry state instead of
aborting the whole test.
8. Reworded the skip-path Skip message: zero failures there proves only
that the stack survived an unreloaded rewrite attempt, not that a
successful reload is blip-free. Re-added the old single-file path to
.gitignore alongside the new directory entry.
Gates: go build/vet/gofmt clean on integration/; root go test -race,
gofmt, vet, yaegi-check all clean; make integration run cold twice, both
exit 0, plus a verbose run that landed on the skip path (the exact
scenario item 4 fixes) with the full suite, including
TestUsersFileHotReload, passing immediately after.
180 lines
7.6 KiB
Cheetah
180 lines
7.6 KiB
Cheetah
http:
|
|
routers:
|
|
llmgateway-router:
|
|
rule: "PathPrefix(`/`)"
|
|
entryPoints: ["web"]
|
|
service: dummy-service
|
|
middlewares: ["llmgateway"]
|
|
priority: 1
|
|
|
|
llmgateway-real-router:
|
|
rule: "Host(`real.localhost`)"
|
|
entryPoints: ["web"]
|
|
service: dummy-service
|
|
middlewares: ["llmgateway-real"]
|
|
priority: 100
|
|
|
|
services:
|
|
# Placeholder upstream. Every route the plugin recognizes is
|
|
# short-circuited by the middleware itself; this backend is never
|
|
# actually reached by the requests the integration suite sends.
|
|
dummy-service:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://mockopenai:8080"
|
|
|
|
middlewares:
|
|
llmgateway:
|
|
plugin:
|
|
llmgateway:
|
|
providers:
|
|
openai:
|
|
type: openai
|
|
baseUrl: "http://mockopenai:8080"
|
|
apiKey: "mock-openai-key"
|
|
discovery: true
|
|
models: ["gpt-mock"]
|
|
anthropic:
|
|
type: anthropic
|
|
baseUrl: "http://mockanthropic:8080"
|
|
apiKey: "mock-anthropic-key"
|
|
models: ["claude-mock"]
|
|
gemini:
|
|
type: gemini
|
|
baseUrl: "http://mockgemini:8080"
|
|
apiKey: "mock-gemini-key"
|
|
models: ["gemini-mock"]
|
|
# Backs task 5's retry coverage only: baseUrl points at the
|
|
# SAME mockopenai container, under its dedicated /flaky prefix
|
|
# (see integration/mock/main.go's handleFlakyOpenAIChat), so
|
|
# a.baseURL+"/v1/chat/completions" (provider_openai.go)
|
|
# resolves to /flaky/v1/chat/completions, and this provider's
|
|
# own native passthrough route (/flaky/reset) reaches
|
|
# handleFlakyReset the same way. discovery is left off — GET
|
|
# /flaky/v1/models isn't served — so the explicit "flaky-mock"
|
|
# model id below is the only way this provider's model is
|
|
# known.
|
|
flaky:
|
|
type: openai
|
|
baseUrl: "http://mockopenai:8080/flaky"
|
|
apiKey: "mock-openai-key"
|
|
models: ["flaky-mock"]
|
|
groups:
|
|
eng:
|
|
limits:
|
|
requestsPerMinute: 1000
|
|
limited:
|
|
limits:
|
|
requestsPerMinute: 3
|
|
# Dedicated to TestConfigHotReload's sustained ~20rps probe loop
|
|
# (integration/config_hot_reload_test.go), isolated from "eng"'s
|
|
# shared 1000/min bucket: that loop alone can run several
|
|
# hundred requests in one test, and "eng" is also driven by
|
|
# every other alice-keyed test in this suite. Sharing "eng"
|
|
# measurably risked exhausting its per-minute budget, which
|
|
# would surface as a 429 in the probe loop — indistinguishable
|
|
# from a real reload-caused blip to that test's own "zero
|
|
# non-200s" assertion, and a false failure signal for every
|
|
# OTHER "eng" test running around the same time. 100000/min is
|
|
# deliberately far above anything this suite could ever send.
|
|
hotreload:
|
|
limits:
|
|
requestsPerMinute: 100000
|
|
users:
|
|
inline:
|
|
# limits.requestsPerDay is set (generously — every test in
|
|
# this suite together never approaches it) purely so
|
|
# buildLimitScopes (routes_unified.go/routes_media.go/
|
|
# admin.go) builds a "user" counter scope for alice at all:
|
|
# a nil per-user Limits means only the group's own counters
|
|
# ever increment, which would make the admin usage test's
|
|
# "alice's own request counters are > 0" assertion always
|
|
# read zero regardless of how much traffic she generated.
|
|
- name: alice
|
|
group: eng
|
|
apiKey: "sk-int-alice"
|
|
limits:
|
|
requestsPerDay: 1000000
|
|
# Admin-flagged user for the admin dashboard integration
|
|
# coverage (spec §4, v0.2): same "eng" group as alice, so
|
|
# polling /admin/api/* repeatedly never hits a rate limit
|
|
# meant for a different scenario. limits.requestsPerDay is
|
|
# set for the same reason as alice's above.
|
|
- name: admin
|
|
group: eng
|
|
apiKey: "sk-int-admin"
|
|
admin: true
|
|
limits:
|
|
requestsPerDay: 1000000
|
|
# TestConfigHotReload's dedicated identity — see the
|
|
# "hotreload" group's own comment above for why it is not
|
|
# alice/eng. No per-user limits set: the group's
|
|
# requestsPerMinute alone (100000) already covers this user.
|
|
- name: hotreload-probe
|
|
group: hotreload
|
|
apiKey: "sk-int-hotreload"
|
|
file: "/users/users.json"
|
|
redis:
|
|
address: "redis:6379"
|
|
mcpServers:
|
|
tool:
|
|
url: "http://mocktool:8080"
|
|
# v0.2 features (spec §1, §2, §4, §5), all opt-in and off by
|
|
# default in v0.1: enabled here so the integration suite
|
|
# exercises every one under real Traefik+Yaegi, not just go
|
|
# test.
|
|
retry:
|
|
enabled: true
|
|
attempts: 1 # one retry after the first try — matches handleFlakyOpenAIChat's single forced 503
|
|
cache:
|
|
enabled: true
|
|
ttl: "2m"
|
|
admin:
|
|
enabled: true
|
|
# Task 6 (spec §5): one alias per mock provider type, so the
|
|
# integration suite proves the echo-behavior asymmetry under
|
|
# real Traefik+Yaegi — openai-type forwards the upstream body
|
|
# verbatim (its "model" field carries "gpt-mock", not the
|
|
# alias); anthropic-type builds its own envelope and echoes the
|
|
# client's exact alias string back.
|
|
modelAliases:
|
|
aliased/mock: "gpt-mock"
|
|
aliased/claude: "claude-mock"
|
|
|
|
# Second, separate middleware instance carrying only the optional
|
|
# real-upstream smoke test's "uni" provider (task 7, INTEGRATION_REAL=1
|
|
# gated). Kept isolated from the mock-backed "llmgateway" middleware
|
|
# above so the always-on tests never depend on network egress.
|
|
llmgateway-real:
|
|
plugin:
|
|
llmgateway:
|
|
providers:
|
|
real:
|
|
type: openai
|
|
baseUrl: "__INTEGRATION_REAL_BASEURL__"
|
|
apiKey: ""
|
|
# Explicit model AND discovery: the explicit entry keeps
|
|
# resolution working offline (discovery failures are non-fatal
|
|
# by design - log + retry next interval), while discovery
|
|
# populates /v1/models with the gateway's full live catalog
|
|
# when the upstream is reachable (INTEGRATION_REAL=1 runs).
|
|
#
|
|
# The provider is deliberately NOT named "uni": the upstream
|
|
# gateway's own model ids carry a "uni/" prefix, and a
|
|
# provider of that name would make the plugin strip it
|
|
# (provider-prefix resolution) before proxying. With a
|
|
# non-colliding name the whole slash-bearing id below is a
|
|
# bare model id and is forwarded verbatim.
|
|
models: ["uni/deepseek-v4-flash-0731"]
|
|
discovery: true
|
|
discoveryInterval: "5m"
|
|
groups:
|
|
eng:
|
|
limits:
|
|
requestsPerMinute: 1000
|
|
users:
|
|
inline:
|
|
- name: alice-real
|
|
group: eng
|
|
apiKey: "sk-int-real"
|