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"