iop/packages/go/config/protocol_profile_test.go
toki e5869a53ae fix(edge): provider 호출 정규화 계층을 추가한다
caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
2026-08-13 23:06:50 +09:00

1015 lines
36 KiB
Go

package config_test
import (
"os"
"path/filepath"
"strings"
"testing"
"iop/packages/go/config"
)
// --- S01: Overlay resolution, rejection, and legacy alias normalization ---
func TestProtocolProfileOverlayResolution(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"base": {
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://api.example.com/v1",
Operations: map[string]string{
"models": "/models",
},
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat"},
},
"overlay": {
Base: "base",
Operations: map[string]string{
"chat_completions": "/chat/completions",
},
Capabilities: []string{"models", "chat", "streaming"},
},
}
resolved, err := config.ResolveProtocolProfile("overlay", "", catalog)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if resolved.ID != "overlay" {
t.Errorf("ID = %q, want %q", resolved.ID, "overlay")
}
if resolved.Driver != config.ProtocolDriverOpenAIChat {
t.Errorf("Driver = %q, want %q", resolved.Driver, config.ProtocolDriverOpenAIChat)
}
if resolved.BaseURL != "https://api.example.com/v1" {
t.Errorf("BaseURL = %q, want inherited value", resolved.BaseURL)
}
if _, ok := resolved.Operations["models"]; !ok {
t.Errorf("expected inherited operation models")
}
if _, ok := resolved.Operations["chat_completions"]; !ok {
t.Errorf("expected overlay operation chat_completions")
}
if len(resolved.Capabilities) != 3 {
t.Errorf("Capabilities = %v, want 3", resolved.Capabilities)
}
}
func TestProtocolProfileOverlayDeepCopyImmutability(t *testing.T) {
baseOps := map[string]string{"models": "/models"}
catalog := map[string]config.ProtocolProfileConf{
"base": {
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://api.example.com/v1",
Operations: baseOps,
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat"},
},
"overlay": {
Base: "base",
Operations: map[string]string{"chat_completions": "/chat/completions"},
},
}
resolved, err := config.ResolveProtocolProfile("overlay", "", catalog)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
// Mutate the resolved snapshot and verify the catalog is unaffected.
resolved.Operations["injected"] = "/injected"
if _, ok := catalog["base"].Operations["injected"]; ok {
t.Error("mutating resolved profile affected catalog base")
}
if _, ok := catalog["overlay"].Operations["injected"]; ok {
t.Error("mutating resolved profile affected catalog overlay")
}
}
func TestProtocolProfileReasoningEffortNormalization(t *testing.T) {
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if got, ok := profile.MapReasoningEffort(config.OperationResponses, "xhigh", true); !ok || got != "xhigh" {
t.Fatalf("Responses xhigh with tools = %q,%t; want xhigh,true", got, ok)
}
if got, ok := profile.MapReasoningEffort(config.OperationChatCompletions, "high", true); ok || got != "" {
t.Fatalf("Chat high with tools = %q,%t; want unsupported", got, ok)
}
if got, ok := profile.MapReasoningEffort(config.OperationChatCompletions, "high", false); !ok || got != "high" {
t.Fatalf("Chat high without tools = %q,%t; want high,true", got, ok)
}
}
func TestProtocolProfileEffortNormalizationOverlayAndValidation(t *testing.T) {
catalog := config.BuiltInProtocolProfileCatalog()
catalog["four-grade-openai"] = config.ProtocolProfileConf{
Base: "openai",
Normalization: config.ProtocolNormalizationConf{Effort: map[string]config.ProtocolEffortMappingConf{
string(config.OperationResponses): {
WithTools: true,
Wire: config.ProtocolEffortWireOpenAIResponses,
Levels: map[string]string{
"none": "none", "low": "low", "medium": "medium",
"high": "high", "xhigh": "xhigh",
},
},
}},
}
profile, err := config.ResolveProtocolProfile("four-grade-openai", "", catalog)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if got, ok := profile.MapReasoningEffort(config.OperationResponses, "max", true); !ok || got != "xhigh" {
t.Fatalf("mapped max = %q,%t; want xhigh,true", got, ok)
}
bad := config.BuiltInProtocolProfileCatalog()
bad["bad-effort"] = config.ProtocolProfileConf{
Base: "openai",
Normalization: config.ProtocolNormalizationConf{Effort: map[string]config.ProtocolEffortMappingConf{
"messages": {Wire: config.ProtocolEffortWireAnthropicMessage, Levels: map[string]string{"high": "high"}},
}},
}
if _, err := config.ResolveProtocolProfile("bad-effort", "", bad); err == nil || !strings.Contains(err.Error(), "not declared") {
t.Fatalf("expected undeclared effort operation error, got %v", err)
}
badUpgrade := config.BuiltInProtocolProfileCatalog()
badUpgrade["bad-upgrade"] = config.ProtocolProfileConf{
Base: "openai",
Normalization: config.ProtocolNormalizationConf{Effort: map[string]config.ProtocolEffortMappingConf{
string(config.OperationResponses): {
WithTools: true,
Wire: config.ProtocolEffortWireOpenAIResponses,
Levels: map[string]string{"high": "max"},
},
}},
}
if _, err := config.ResolveProtocolProfile("bad-upgrade", "", badUpgrade); err == nil || !strings.Contains(err.Error(), "upgrades") {
t.Fatalf("expected effort upgrade error, got %v", err)
}
}
func TestProtocolProfileOverlayRejected(t *testing.T) {
t.Run("cycle", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"a": {Base: "b", Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m"}, Capabilities: []string{"chat"}},
"b": {Base: "a", Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://b/v1", Operations: map[string]string{"models": "/m"}, Capabilities: []string{"chat"}},
}
_, err := config.ResolveProtocolProfile("a", "", catalog)
if err == nil || !strings.Contains(err.Error(), "cycle") {
t.Fatalf("expected cycle error, got %v", err)
}
})
t.Run("unknown_base", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"overlay": {Base: "nonexistent", Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m"}, Capabilities: []string{"chat"}},
}
_, err := config.ResolveProtocolProfile("overlay", "", catalog)
if err == nil || !strings.Contains(err.Error(), "unknown") {
t.Fatalf("expected unknown base error, got %v", err)
}
})
t.Run("conflicting_driver", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"base": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m"}, Capabilities: []string{"chat"}},
"overlay": {Base: "base", Driver: config.ProtocolDriverAnthropicMessages, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m", "messages": "/msg"}, Capabilities: []string{"messages"}},
}
_, err := config.ResolveProtocolProfile("overlay", "", catalog)
if err == nil || !strings.Contains(err.Error(), "driver") {
t.Fatalf("expected driver conflict error, got %v", err)
}
})
t.Run("invalid_operation", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"messages": "/msg"}, Capabilities: []string{"chat"}},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "messages") {
t.Fatalf("expected invalid operation error, got %v", err)
}
})
t.Run("invalid_auth_header", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m", "chat_completions": "/chat"}, Auth: config.ProtocolAuthConf{Header: ""}, Capabilities: []string{"models", "chat"}},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "auth") {
t.Fatalf("expected auth validation error, got %v", err)
}
})
t.Run("empty_capabilities", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "/m"}, Capabilities: []string{}},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "capabilities") {
t.Fatalf("expected capabilities error, got %v", err)
}
})
t.Run("malformed_absolute_url", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "://not-a-url"}, Capabilities: []string{"chat"}},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "operation") {
t.Fatalf("expected URL validation error, got %v", err)
}
})
t.Run("absolute_url_without_host", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://a/v1",
Operations: map[string]string{"chat_completions": "https:///v1/chat/completions"},
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"chat"},
},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "absolute http(s) URL") {
t.Fatalf("expected absolute URL host validation error, got %v", err)
}
})
t.Run("relative_path_no_leading_slash", func(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"bad": {Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://a/v1", Operations: map[string]string{"models": "models"}, Capabilities: []string{"chat"}},
}
_, err := config.ResolveProtocolProfile("bad", "", catalog)
if err == nil || !strings.Contains(err.Error(), "leading") {
t.Fatalf("expected leading slash error, got %v", err)
}
})
}
func TestLegacyProviderAliasProfileNormalization(t *testing.T) {
cases := []struct {
legacyType string
wantID string
}{
{"vllm", "openai"},
{"openai_api", "openai"},
{"openai_compat", "openai"},
{"vllm-mlx", "openai"},
{"lemonade", "openai"},
{"sglang", "openai"},
{"seulgivibe_claude", "seulgi_messages"},
{"seulgivibe_openai", "seulgi_chat"},
{"ollama", ""},
{"unknown_type", ""},
}
for _, c := range cases {
got := config.LegacyProviderTypeToProfile(c.legacyType)
if got != c.wantID {
t.Errorf("LegacyProviderTypeToProfile(%q) = %q, want %q", c.legacyType, got, c.wantID)
}
}
}
func TestLegacyProviderAliasResolvesToBuiltIn(t *testing.T) {
// No explicit selector: legacy type should normalize to the built-in.
resolved, err := config.ResolveProtocolProfile("", "vllm", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if resolved.ID != "openai" {
t.Errorf("ID = %q, want %q", resolved.ID, "openai")
}
if resolved.Driver != config.ProtocolDriverOpenAIChat {
t.Errorf("Driver = %q, want %q", resolved.Driver, config.ProtocolDriverOpenAIChat)
}
}
func TestExplicitProfilePrecedenceOverLegacyType(t *testing.T) {
// Explicit selector should win over legacy type mapping.
resolved, err := config.ResolveProtocolProfile("anthropic", "vllm", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if resolved.ID != "anthropic" {
t.Errorf("ID = %q, want %q", resolved.ID, "anthropic")
}
if resolved.Driver != config.ProtocolDriverAnthropicMessages {
t.Errorf("Driver = %q, want %q", resolved.Driver, config.ProtocolDriverAnthropicMessages)
}
}
func TestProtocolProfileMultiLevelOverlay(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"l1": {
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://l1/v1",
Operations: map[string]string{"models": "/models"},
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat"},
},
"l2": {
Base: "l1",
BaseURL: "https://l2/v1",
Operations: map[string]string{"chat_completions": "/chat/completions"},
},
"l3": {
Base: "l2",
Capabilities: []string{"models", "chat", "streaming"},
},
}
resolved, err := config.ResolveProtocolProfile("l3", "", catalog)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if resolved.BaseURL != "https://l2/v1" {
t.Errorf("BaseURL = %q, want %q", resolved.BaseURL, "https://l2/v1")
}
if _, ok := resolved.Operations["models"]; !ok {
t.Error("expected inherited operation models")
}
if _, ok := resolved.Operations["chat_completions"]; !ok {
t.Error("expected inherited operation chat_completions")
}
if len(resolved.Capabilities) != 3 {
t.Errorf("Capabilities = %v, want 3", resolved.Capabilities)
}
}
func TestProtocolProfileCustomOverlay(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"openai": config.BuiltInProtocolProfiles["openai"],
"my-proxy": {
Base: "openai",
BaseURL: "https://my-proxy.example.invalid/v1",
},
}
resolved, err := config.ResolveProtocolProfile("my-proxy", "", catalog)
if err != nil {
t.Fatalf("ResolveProtocolProfile: %v", err)
}
if resolved.BaseURL != "https://my-proxy.example.invalid/v1" {
t.Errorf("BaseURL = %q, want overridden value", resolved.BaseURL)
}
if resolved.ID != "my-proxy" {
t.Errorf("ID = %q, want %q", resolved.ID, "my-proxy")
}
}
// --- S03: Built-in catalog admission ---
func TestBuiltInProtocolProfileCatalog(t *testing.T) {
expectedIDs := []string{
"anthropic", "gemini", "glm", "glm_coding", "grok", "kimi", "minimax_chat", "minimax_messages",
"mimo_chat", "mimo_messages",
"openai", "seulgi_chat", "seulgi_messages",
}
for _, id := range expectedIDs {
if _, ok := config.BuiltInProtocolProfiles[id]; !ok {
t.Fatalf("built-in profile %q not found", id)
}
resolved, err := config.ResolveProtocolProfile(id, "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatalf("ResolveProtocolProfile(%q): %v", id, err)
}
if resolved.ID != id {
t.Errorf("profile %q: ID = %q, want %q", id, resolved.ID, id)
}
if resolved.Base != "" {
t.Errorf("profile %q: Base should be empty in concrete snapshot, got %q", id, resolved.Base)
}
}
// Verify all expected IDs are present
for _, id := range expectedIDs {
if _, ok := config.BuiltInProtocolProfiles[id]; !ok {
t.Errorf("expected built-in profile %q", id)
}
}
}
func TestBuiltInProtocolProfileOperations(t *testing.T) {
t.Run("openai_has_chat_completions", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
if _, ok := r.Operations["chat_completions"]; !ok {
t.Error("openai should declare chat_completions")
}
if _, ok := r.Operations["responses"]; !ok {
t.Error("openai should declare responses")
}
})
t.Run("anthropic_has_messages", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
if _, ok := r.Operations["messages"]; !ok {
t.Error("anthropic should declare messages")
}
if _, ok := r.Operations["chat_completions"]; ok {
t.Error("anthropic should NOT declare chat_completions")
}
})
t.Run("seulgi_messages_has_messages", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("seulgi_messages", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
if _, ok := r.Operations["messages"]; !ok {
t.Error("seulgi_messages should declare messages")
}
})
t.Run("seulgi_chat_has_chat_completions", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("seulgi_chat", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
if _, ok := r.Operations["chat_completions"]; !ok {
t.Error("seulgi_chat should declare chat_completions")
}
})
}
func TestBuiltInProtocolProfileURLs(t *testing.T) {
t.Run("anthropic_absolute_path", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("messages")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(url, "/v1/messages") {
t.Errorf("expected /v1/messages suffix, got %q", url)
}
})
t.Run("gemini_v1beta_openai", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("chat_completions")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(url, "/v1beta/openai/chat/completions") {
t.Errorf("expected /v1beta/openai/chat/completions suffix, got %q", url)
}
})
t.Run("glm_api_paas_v4", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("glm", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("chat_completions")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(url, "/api/paas/v4/chat/completions") {
t.Errorf("expected /api/paas/v4/chat/completions suffix, got %q", url)
}
})
t.Run("glm_coding_api_coding_paas_v4", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("glm_coding", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("chat_completions")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(url, "/api/coding/paas/v4/chat/completions") {
t.Errorf("expected /api/coding/paas/v4/chat/completions suffix, got %q", url)
}
})
t.Run("glm_coding_models_url", func(t *testing.T) {
r, err := config.ResolveProtocolProfile("glm_coding", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("models")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(url, "/api/coding/paas/v4/models") {
t.Errorf("expected /api/coding/paas/v4/models suffix, got %q", url)
}
})
}
func TestProtocolProfileUnsupportedOperationRejected(t *testing.T) {
r, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
_, err = r.ResolveOperationURL("chat_completions")
if err == nil || !strings.Contains(err.Error(), "not supported") {
t.Fatalf("expected unsupported operation error, got %v", err)
}
}
func TestProtocolProfileResolveOperationURLAbsolute(t *testing.T) {
catalog := map[string]config.ProtocolProfileConf{
"custom": {
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://base.example.com/v1",
Operations: map[string]string{
"models": "https://override.example.com/v1/models",
"chat_completions": "/chat/completions",
},
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat"},
},
}
r, err := config.ResolveProtocolProfile("custom", "", catalog)
if err != nil {
t.Fatal(err)
}
url, err := r.ResolveOperationURL("models")
if err != nil {
t.Fatal(err)
}
if url != "https://override.example.com/v1/models" {
t.Errorf("expected absolute URL unchanged, got %q", url)
}
}
func TestProtocolProfileClone(t *testing.T) {
r, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
clone := r.Clone()
if clone.ID != r.ID {
t.Errorf("ID mismatch: %q vs %q", clone.ID, r.ID)
}
clone.Operations["injected"] = "/injected"
if _, ok := r.Operations["injected"]; ok {
t.Error("clone mutation affected original")
}
}
func TestProtocolProfileProviderEndpointOverlay(t *testing.T) {
cfg, err := loadProtocolProfileYAML(t, `
nodes:
- id: node-profile
providers:
- id: provider-profile
type: vllm
category: api
endpoint: http://127.0.0.1:18080/v1/
base_url: http://127.0.0.1:18081/ignored
models: [upstream-model]
models:
- id: canonical-model
providers:
provider-profile: upstream-model
`)
if err != nil {
t.Fatalf("LoadEdge: %v", err)
}
profile := cfg.Nodes[0].Providers[0].RuntimeProfile
if profile == nil || profile.BaseURL != "http://127.0.0.1:18080/v1" {
t.Fatalf("runtime profile base_url = %#v", profile)
}
url, err := profile.ResolveOperationURL(string(config.OperationChatCompletions))
if err != nil || url != "http://127.0.0.1:18080/v1/chat/completions" {
t.Fatalf("resolved chat URL = %q, err=%v", url, err)
}
fresh := config.BuiltInProtocolProfileCatalog()["openai"]
if fresh.BaseURL != "https://api.openai.com/v1" {
t.Fatalf("provider endpoint mutated built-in: %q", fresh.BaseURL)
}
rootCfg, err := loadProtocolProfileYAML(t, `
nodes:
- id: node-root
providers:
- id: provider-root
type: lemonade
category: api
endpoint: http://127.0.0.1:18082
models: [upstream-model]
models:
- id: root-model
providers: {provider-root: upstream-model}
`)
if err != nil {
t.Fatalf("LoadEdge root endpoint: %v", err)
}
rootProfile := rootCfg.Nodes[0].Providers[0].RuntimeProfile
if rootProfile == nil || rootProfile.BaseURL != "http://127.0.0.1:18082/v1" {
t.Fatalf("root endpoint did not retain profile prefix: %#v", rootProfile)
}
}
func TestProtocolProfileCatalogCollisionRejected(t *testing.T) {
_, err := loadProtocolProfileYAML(t, `
protocol_profiles:
openai:
driver: openai_chat
base_url: https://collision.example.invalid/v1
operations: {chat_completions: /chat/completions}
auth: {header: Authorization, scheme: Bearer}
capabilities: [chat]
`)
if err == nil || !strings.Contains(err.Error(), "reserved") {
t.Fatalf("expected reserved built-in collision error, got %v", err)
}
}
func TestProtocolProfileCapabilityMatrixRejected(t *testing.T) {
base := config.ProtocolProfileConf{
Driver: config.ProtocolDriverOpenAIChat,
BaseURL: "https://matrix.example.invalid/v1",
Operations: map[string]string{"chat_completions": "/chat/completions"},
Auth: config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"chat"},
}
for _, tc := range []struct {
name string
edit func(*config.ProtocolProfileConf)
want string
}{
{name: "unknown_capability", edit: func(p *config.ProtocolProfileConf) { p.Capabilities = append(p.Capabilities, "telepathy") }, want: "not recognized"},
{name: "operation_without_capability", edit: func(p *config.ProtocolProfileConf) { p.Operations["models"] = "/models" }, want: "requires capability"},
{name: "wrong_driver_operation", edit: func(p *config.ProtocolProfileConf) { p.Operations["messages"] = "/messages" }, want: "not supported"},
{name: "raw_header_with_scheme", edit: func(p *config.ProtocolProfileConf) {
p.Auth = config.ProtocolAuthConf{Header: "X-Api-Key", Scheme: "Bearer"}
}, want: "raw credential"},
{name: "authorization_without_bearer", edit: func(p *config.ProtocolProfileConf) { p.Auth.Scheme = "Token" }, want: "Bearer"},
} {
t.Run(tc.name, func(t *testing.T) {
profile := base
profile.Operations = map[string]string{"chat_completions": "/chat/completions"}
profile.Capabilities = append([]string(nil), base.Capabilities...)
tc.edit(&profile)
_, err := config.ResolveProtocolProfile("bad", "", map[string]config.ProtocolProfileConf{"bad": profile})
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("expected %q error, got %v", tc.want, err)
}
})
}
}
func TestProtocolProfileChatOnlyTokenCounter(t *testing.T) {
base := func(profile, counter string) string {
return `
nodes:
- id: node-counter
providers:
- id: provider-counter
type: openai_api
category: api
profile: ` + profile + `
endpoint: http://127.0.0.1:18080/v1
models: [upstream-model]
models:
- id: canonical-model
providers:
provider-counter: upstream-model
token_counter:
` + counter
}
if _, err := loadProtocolProfileYAML(t, base("openai", " mode: deterministic\n")); err != nil {
t.Fatalf("Chat deterministic counter: %v", err)
}
for _, tc := range []struct {
name, profile, counter, want string
}{
{name: "messages", profile: "anthropic", counter: " mode: deterministic\n", want: "Chat-only"},
{name: "estimate_range", profile: "openai", counter: " mode: estimate\n per_1k_input: 1001\n", want: "between 1 and 1000"},
{name: "deterministic_parameter", profile: "openai", counter: " mode: deterministic\n per_1k_input: 250\n", want: "must be 0"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := loadProtocolProfileYAML(t, base(tc.profile, tc.counter))
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("expected %q error, got %v", tc.want, err)
}
})
}
_, err := loadProtocolProfileYAML(t, `
protocol_profiles:
responses_custom:
driver: openai_responses
base_url: https://responses.example.invalid/v1
operations: {responses: /responses}
auth: {header: Authorization, scheme: Bearer}
capabilities: [responses]
nodes:
- id: node-responses-counter
providers:
- id: responses-counter
type: openai_api
category: api
profile: responses_custom
models: [upstream-response]
models:
- id: canonical-response
providers: {responses-counter: upstream-response}
token_counter: {mode: deterministic}
`)
if err == nil || !strings.Contains(err.Error(), "Chat-only") {
t.Fatalf("expected Responses profile token-counter rejection, got %v", err)
}
}
func TestBuiltInProtocolProfileCatalogImmutable(t *testing.T) {
first := config.BuiltInProtocolProfileCatalog()
openai := first["openai"]
openai.Operations[string(config.OperationChatCompletions)] = "/mutated"
openai.Capabilities[0] = "mutated"
first["openai"] = openai
fresh := config.BuiltInProtocolProfileCatalog()["openai"]
if fresh.Operations[string(config.OperationChatCompletions)] != "/chat/completions" || fresh.Capabilities[0] == "mutated" {
t.Fatalf("built-in catalog mutation leaked: %+v", fresh)
}
}
// TestProtocolProfileHeaderValidation covers the RFC 9110 field-name token
// grammar independently from Go's permissive MIME canonicalization helpers.
func TestProtocolProfileHeaderValidation(t *testing.T) {
validProfile := func(header string) config.ProtocolProfileConf {
return config.ProtocolProfileConf{
Driver: config.ProtocolDriverOpenAIChat, BaseURL: "https://example.invalid/v1",
Operations: map[string]string{"chat_completions": "/chat/completions"},
Auth: config.ProtocolAuthConf{Header: header}, Capabilities: []string{"chat"},
}
}
for _, tc := range []struct {
name, header string
valid bool
}{
{name: "token_punctuation", header: "X!#$%&'*+-.^_`|~Key", valid: true},
{name: "empty", header: ""},
{name: "leading_space", header: " X-Key"},
{name: "trailing_space", header: "X-Key "},
{name: "embedded_space", header: "X Key"},
{name: "colon", header: "X:Key"},
{name: "tab", header: "X\tKey"},
{name: "control", header: "X\x01Key"},
{name: "non_ascii", header: "X-Kéy"},
{name: "separator", header: "X(Key)"},
} {
t.Run(tc.name, func(t *testing.T) {
profile := validProfile(tc.header)
_, err := config.ResolveProtocolProfile("header-test", "", map[string]config.ProtocolProfileConf{"header-test": profile})
if tc.valid && err != nil {
t.Fatalf("valid header rejected: %v", err)
}
if !tc.valid && (err == nil || !strings.Contains(err.Error(), "auth")) {
t.Fatalf("invalid header %q was not rejected: %v", tc.header, err)
}
})
}
}
// TestProtocolProfileBuiltInAuthMatrix is intentionally literal so catalog
// drift cannot update the expected values by sharing production data. MiniMax
// and MiMo Anthropic-compatible auth was rechecked against official provider
// documentation on 2026-08-01:
// https://platform.minimax.io/docs/token-plan/other-tools
// https://mimo.mi.com/docs/en-US/quick-start/summary/first-api-call
func TestProtocolProfileBuiltInAuthMatrix(t *testing.T) {
type wantAuth struct{ header, scheme string }
want := map[string]wantAuth{
"openai": {"Authorization", "Bearer"}, "gemini": {"Authorization", "Bearer"},
"anthropic": {"x-api-key", ""}, "glm": {"Authorization", "Bearer"},
"glm_coding": {"Authorization", "Bearer"},
"kimi": {"Authorization", "Bearer"}, "minimax_chat": {"Authorization", "Bearer"},
"minimax_messages": {"Authorization", "Bearer"}, "mimo_chat": {"Authorization", "Bearer"},
"mimo_messages": {"api-key", ""}, "grok": {"Authorization", "Bearer"},
"seulgi_chat": {"Authorization", "Bearer"}, "seulgi_messages": {"x-api-key", ""},
}
if len(config.BuiltInProtocolProfiles) != len(want) {
t.Fatalf("built-in profile count = %d, want %d", len(config.BuiltInProtocolProfiles), len(want))
}
for id, expected := range want {
resolved, err := config.ResolveProtocolProfile(id, "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatalf("ResolveProtocolProfile(%q): %v", id, err)
}
if resolved.Auth.Header != expected.header || resolved.Auth.Scheme != expected.scheme {
t.Errorf("%s auth = {%q %q}, want {%q %q}", id, resolved.Auth.Header, resolved.Auth.Scheme, expected.header, expected.scheme)
}
}
}
// TestBuiltInGLMProtocolProfiles asserts that both GLM profiles carry the
// expected capabilities, lack Responses, and expose only models+chat operations.
// Official docs rechecked on 2026-08-02:
// https://docs.z.ai/api-reference/introduction
// https://docs.z.ai/api-reference/llm/chat-completion
// https://docs.z.ai/scenario-example/develop-tools/others
func TestBuiltInGLMProtocolProfiles(t *testing.T) {
for _, id := range []string{"glm", "glm_coding"} {
t.Run(id, func(t *testing.T) {
r, err := config.ResolveProtocolProfile(id, "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"models", "chat", "streaming", "tool_calling"} {
if !r.HasCapability(want) {
t.Errorf("%s: missing capability %q", id, want)
}
}
if r.HasCapability("responses") {
t.Errorf("%s: must not advertise responses capability", id)
}
if _, ok := r.Operations[string(config.OperationResponses)]; ok {
t.Errorf("%s: must not declare responses operation", id)
}
for _, op := range []config.ProtocolOperation{config.OperationModels, config.OperationChatCompletions} {
if _, ok := r.Operations[string(op)]; !ok {
t.Errorf("%s: missing operation %q", id, op)
}
}
})
}
}
// TestGLMProfilesRemainDistinctThroughProviderPoolConfig loads a provider-pool
// mapping with two distinct external model IDs that route to two distinct
// provider IDs and two distinct GLM profiles, then asserts no cross-mapping
// and both map to the same upstream model.
// Official endpoint distinction rechecked on 2026-08-02:
// https://docs.z.ai/scenario-example/develop-tools/others
func TestGLMProfilesRemainDistinctThroughProviderPoolConfig(t *testing.T) {
tmpDir := t.TempDir()
yaml := `
models:
- id: "glm-5.1-api"
providers:
glm-api: "glm-5.1"
- id: "glm-5.1-coding"
providers:
glm-coding: "glm-5.1"
nodes:
- id: "node-glm"
providers:
- id: "glm-api"
type: "openai_api"
category: "api"
profile: "glm"
endpoint: "https://api.z.ai/api/paas/v4"
models: ["glm-5.1"]
capacity: 1
- id: "glm-coding"
type: "openai_api"
category: "api"
profile: "glm_coding"
endpoint: "https://api.z.ai/api/coding/paas/v4"
models: ["glm-5.1"]
capacity: 1
`
path := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
cfg, err := config.LoadEdge(path)
if err != nil {
t.Fatalf("LoadEdge: %v", err)
}
if len(cfg.Models) != 2 {
t.Fatalf("expected 2 models, got %d", len(cfg.Models))
}
if len(cfg.Nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
}
if len(cfg.Nodes[0].Providers) != 2 {
t.Fatalf("expected 2 providers, got %d", len(cfg.Nodes[0].Providers))
}
var apiProv, codingProv *config.NodeProviderConf
for i := range cfg.Nodes[0].Providers {
p := &cfg.Nodes[0].Providers[i]
if p.ID == "glm-api" {
apiProv = p
} else if p.ID == "glm-coding" {
codingProv = p
}
}
if apiProv == nil {
t.Fatal("glm-api provider missing")
}
if codingProv == nil {
t.Fatal("glm-coding provider missing")
}
if apiProv.Profile != "glm" {
t.Errorf("glm-api profile = %q, want %q", apiProv.Profile, "glm")
}
if codingProv.Profile != "glm_coding" {
t.Errorf("glm-coding profile = %q, want %q", codingProv.Profile, "glm_coding")
}
if len(cfg.Models[0].Providers) != 1 {
t.Fatalf("glm-5.1-api: expected 1 provider mapping, got %d", len(cfg.Models[0].Providers))
}
if len(cfg.Models[1].Providers) != 1 {
t.Fatalf("glm-5.1-coding: expected 1 provider mapping, got %d", len(cfg.Models[1].Providers))
}
if cfg.Models[0].Providers["glm-api"] != "glm-5.1" {
t.Errorf("glm-5.1-api provider served = %q, want %q", cfg.Models[0].Providers["glm-api"], "glm-5.1")
}
if cfg.Models[1].Providers["glm-coding"] != "glm-5.1" {
t.Errorf("glm-5.1-coding provider served = %q, want %q", cfg.Models[1].Providers["glm-coding"], "glm-5.1")
}
// Resolved runtime profiles must be distinct.
apiProfile, err := config.ResolveProtocolProfile("glm", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
codingProfile, err := config.ResolveProtocolProfile("glm_coding", "", config.BuiltInProtocolProfiles)
if err != nil {
t.Fatal(err)
}
if apiProfile.BaseURL == codingProfile.BaseURL {
t.Errorf("glm and glm_coding must resolve to different base URLs, both got %q", apiProfile.BaseURL)
}
}
func TestProtocolProfileAdapterBackingResolution(t *testing.T) {
cfg, err := loadProtocolProfileYAML(t, `
nodes:
- id: node-backed
adapters:
openai_compat_instances:
- name: upstream
enabled: true
provider: lemonade
endpoint: http://127.0.0.1:18080
providers:
- id: profile-provider
type: lemonade
category: api
adapter: upstream
profile: openai
models: [served]
capacity: 1
models:
- id: canonical
providers: {profile-provider: served}
`)
if err != nil {
t.Fatalf("LoadEdge: %v", err)
}
profile := cfg.Nodes[0].Providers[0].RuntimeProfile
if profile == nil || profile.BaseURL != "http://127.0.0.1:18080/v1" {
t.Fatalf("backing endpoint was not applied: %#v", profile)
}
}
func TestProtocolProfileAdapterBackingRejected(t *testing.T) {
for _, tc := range []struct {
name, adapters, ref, want string
}{
{name: "missing", ref: "missing", adapters: " openai_compat_instances: []", want: "missing"},
{name: "disabled", ref: "backing", adapters: " openai_compat_instances:\n - {name: backing, enabled: false, endpoint: http://127.0.0.1:1}", want: "disabled"},
{name: "ambiguous", ref: "backing", adapters: " vllm_instances:\n - {name: backing, enabled: true, endpoint: http://127.0.0.1:1}\n openai_compat_instances:\n - {name: backing, enabled: true, endpoint: http://127.0.0.1:2}", want: "ambiguous"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := loadProtocolProfileYAML(t, `
nodes:
- id: node-bad-backing
adapters:
`+tc.adapters+`
providers:
- id: profile-provider
type: openai_compat
category: api
adapter: `+tc.ref+`
profile: openai
models: [served]
capacity: 1
`)
if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "profile-provider") || !strings.Contains(err.Error(), tc.ref) {
t.Fatalf("expected contextual %q error, got %v", tc.want, err)
}
})
}
}
func loadProtocolProfileYAML(t *testing.T, contents string) (*config.EdgeConfig, error) {
t.Helper()
path := filepath.Join(t.TempDir(), "edge.yaml")
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
return config.LoadEdge(path)
}