Files
cloudpods/pkg/aiproxy/models/chat_upstream_test.go
Zexi Li 3fd882452b feat(llm): route MCP agent via aiproxy virtual key and routing id (#25667)
Support X-Ai-Routing-Id to pin ai_routing, treat empty model_pattern as non-wildcard, and wire MCP agent to aiproxy credentials instead of direct LLM api_key.
2026-09-11 10:14:08 +08:00

148 lines
4.6 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"strings"
"testing"
)
func TestParseClientModelRef(t *testing.T) {
cases := []struct {
in string
routeKey string
catalogPart string
hierarchical bool
}{
{"flat-alias", "", "flat-alias", false},
{"my-route/gpt-4", "my-route", "gpt-4", true},
{"my-route/claude-sonnet-4-6", "my-route", "claude-sonnet-4-6", true},
{"/gpt-4", "", "/gpt-4", false},
{"my-route/", "my-route", "", true},
}
for _, tc := range cases {
ref := parseClientModelRef(tc.in)
if ref.routeKey != tc.routeKey || ref.catalogPart != tc.catalogPart || ref.hierarchical != tc.hierarchical {
t.Fatalf("parseClientModelRef(%q) = %#v, want routeKey=%q catalogPart=%q hierarchical=%v",
tc.in, ref, tc.routeKey, tc.catalogPart, tc.hierarchical)
}
}
}
func TestNormalizeAiRoutingModelKeyRejectsSlash(t *testing.T) {
_, err := normalizeAiRoutingModelKey("my/route")
if err == nil {
t.Fatal("expected error for model_key containing slash")
}
if _, err := normalizeAiRoutingModelKey("my-route"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
}
func TestPickRoutingForRequestHierarchical(t *testing.T) {
routings := []SAiRouting{
{Priority: 10, ModelKey: "claude", ModelPattern: "claude-*"},
{Priority: 20, ModelKey: "other"},
}
picked, err := pickRoutingForRequest(routings, "claude/claude-sonnet-4-6", "primary")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if picked == nil || picked.ModelKey != "claude" {
t.Fatalf("expected claude routing, got %#v", picked)
}
}
func TestPickRoutingForRequestHierarchicalSkipsPattern(t *testing.T) {
routings := []SAiRouting{
{Priority: 10, ModelPattern: "claude-*"},
}
picked, err := pickRoutingForRequest(routings, "claude/claude-sonnet-4-6", "primary")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if picked != nil {
t.Fatalf("hierarchical request should not match pattern-only routing, got %#v", picked)
}
}
func TestCatalogRequestPart(t *testing.T) {
ref := parseClientModelRef("claude/claude-sonnet-4-6")
if got := catalogRequestPart(ref, false); got != "claude-sonnet-4-6" {
t.Fatalf("expected catalog part, got %q", got)
}
ref = parseClientModelRef("claude")
if got := catalogRequestPart(ref, true); got != "" {
t.Fatalf("flat model_key match should use empty catalog part, got %q", got)
}
if got := catalogRequestPart(ref, false); got != "claude" {
t.Fatalf("flat pattern match should use raw model, got %q", got)
}
}
func TestRouterRequestModel(t *testing.T) {
ref := parseClientModelRef("claude/claude-sonnet-4-6")
if got := routerRequestModel(ref); got != "claude-sonnet-4-6" {
t.Fatalf("expected catalog part for router, got %q", got)
}
ref = parseClientModelRef("claude")
if got := routerRequestModel(ref); got != "claude" {
t.Fatalf("expected raw model for flat router request, got %q", got)
}
}
func TestPickRoutingByIdHitsPreferred(t *testing.T) {
qwen := SAiRouting{ModelKey: "qwen38-Qwen3.8-27B-NVFP4"}
qwen.Id = "rid-qwen"
qwen.Name = "qwen"
ds := SAiRouting{ModelKey: "deepseek-v4-flash"}
ds.Id = "rid-ds"
ds.Name = "deepseek"
routings := []SAiRouting{qwen, ds}
picked, err := pickRoutingById(routings, "rid-qwen", "primary")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if picked == nil || picked.Id != "rid-qwen" {
t.Fatalf("expected pinned qwen routing, got %#v", picked)
}
}
func TestPickRoutingByIdNotInList(t *testing.T) {
ds := SAiRouting{ModelKey: "deepseek-v4-flash"}
ds.Id = "rid-ds"
picked, err := pickRoutingById([]SAiRouting{ds}, "rid-missing", "primary")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if picked != nil {
t.Fatalf("expected nil when routing id is not visible, got %#v", picked)
}
}
func TestPickRoutingByIdBoundElsewhere(t *testing.T) {
r := SAiRouting{ModelKey: "qwen", AiProxyNodeId: "node-b"}
r.Id = "rid-qwen"
r.Name = "qwen"
_, err := pickRoutingById([]SAiRouting{r}, "rid-qwen", "node-a")
if err == nil {
t.Fatal("expected forbidden error")
}
if !strings.Contains(err.Error(), "bound to ai_proxy_node") {
t.Fatalf("expected node binding error, got %v", err)
}
}