mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
feat: operator-defined model aliases
Spec §5 (v0.2): Config.ModelAliases map[string]string, alias-first resolution precedence ahead of provider-prefix splitting, alias-OR-target authorization, lazy target resolution with a 404 naming both ids, /v1/models listing, admin overview alias table (JSON + dashboard), and construction-time validation (shape, prefix-shadow, explicit-model collision, chain, empty target). routes_unified.go/routes_media.go needed no resolution-call-site changes - both already pass the client's raw requested id through to registry.resolve, so aliases inherit the __alias echo and multipart rewrite machinery for free. cache.go needed no changes either - cacheKey already hashes requestedModel, so two aliases resolving to the same target already produce distinct cache entries. Adds registry_test.go validation table + precedence/authz/lazy-404/ listing coverage, a dedicated cache-key split test, chat/embeddings/ images/audio-transcriptions end-to-end tests, an extended admin overview test, and an integration test covering the openai-type vs anthropic-type echo asymmetry under real Traefik+Yaegi.
This commit is contained in:
@@ -239,6 +239,55 @@ func TestUnifiedChatAllProviders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestModelAliases covers v0.2 integration coverage: task 6 (spec §5).
|
||||
// dynamic.yml.tmpl configures two operator-defined aliases —
|
||||
// "aliased/mock" -> "gpt-mock" (openai-type) and "aliased/claude" ->
|
||||
// "claude-mock" (anthropic-type) — proving alias resolution runs under
|
||||
// real Traefik+Yaegi, not just go test, including the documented
|
||||
// echo-behavior asymmetry between provider types (see
|
||||
// registry_test.go's Gateway-level equivalents for the same assertion
|
||||
// under go test).
|
||||
func TestModelAliases(t *testing.T) {
|
||||
// openai-type: the upstream response is forwarded verbatim
|
||||
// (provider_openai.go), so its "model" field carries whatever the
|
||||
// mock echoes back — the resolved upstream id "gpt-mock", not the
|
||||
// alias. Correctness here is 200 + real content + the cache header
|
||||
// unified routes always set, not the "model" field (documented v0.1
|
||||
// passthrough asymmetry, unchanged by aliasing).
|
||||
openaiReq := map[string]any{
|
||||
"model": "aliased/mock",
|
||||
"messages": []map[string]any{{"role": "user", "content": "hi"}},
|
||||
}
|
||||
resp, body := doJSON(t, http.MethodPost, traefik1URL+"/v1/chat/completions", aliceKey, openaiReq)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("aliased openai chat: status = %d, body=%#v", resp.StatusCode, body)
|
||||
}
|
||||
if content := firstChoiceContent(t, body); content != "mock openai response" {
|
||||
t.Errorf("aliased openai chat: content = %q, want %q", content, "mock openai response")
|
||||
}
|
||||
if got := resp.Header.Get("X-Llmgw-Cache"); got == "" {
|
||||
t.Error("aliased openai chat: X-Llmgw-Cache header missing, want \"miss\" or \"hit\"")
|
||||
}
|
||||
|
||||
// anthropic-type: translate_anthropic.go builds its own response
|
||||
// envelope and echoes the client's exact requested id (the alias)
|
||||
// back into "model" — this DOES surface the alias.
|
||||
anthropicReq := map[string]any{
|
||||
"model": "aliased/claude",
|
||||
"messages": []map[string]any{{"role": "user", "content": "hi"}},
|
||||
}
|
||||
resp2, body2 := doJSON(t, http.MethodPost, traefik1URL+"/v1/chat/completions", aliceKey, anthropicReq)
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("aliased anthropic chat: status = %d, body=%#v", resp2.StatusCode, body2)
|
||||
}
|
||||
if content := firstChoiceContent(t, body2); content != "mock anthropic response" {
|
||||
t.Errorf("aliased anthropic chat: content = %q, want %q", content, "mock anthropic response")
|
||||
}
|
||||
if model, _ := body2["model"].(string); model != "aliased/claude" {
|
||||
t.Errorf("aliased anthropic chat: response model = %q, want the client-requested alias %q echoed back", model, "aliased/claude")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamingIncrementalityProbe covers integration test 3.
|
||||
//
|
||||
// Content correctness always runs, via assertStreamingContent: a host-side
|
||||
|
||||
@@ -98,9 +98,10 @@ http:
|
||||
mcpServers:
|
||||
tool:
|
||||
url: "http://mocktool:8080"
|
||||
# v0.2 features (spec §1, §2, §4), 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.
|
||||
# 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
|
||||
@@ -109,6 +110,15 @@ http:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user