iop/packages/go/config/usage_attribution_config_test.go
toki 04879f2b43 feat(openai): 실제 provider별 사용량 귀속을 기록한다
요청 종료 계수와 실제 provider 시도 사용량을 분리하고, 직접·pool·retry 경로의 attribution을 보존한다. 관련 계약·스펙과 완료된 task archive 정리도 함께 반영한다.
2026-07-31 20:22:23 +09:00

169 lines
4.1 KiB
Go

package config_test
import (
"os"
"path/filepath"
"strings"
"testing"
"iop/packages/go/config"
)
func loadUsageAttributionConfig(t *testing.T, content string) (*config.EdgeConfig, error) {
t.Helper()
path := filepath.Join(t.TempDir(), "edge.yaml")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
return config.LoadEdge(path)
}
func TestModelUsageAttributionValidationAndDefault(t *testing.T) {
const basePrefix = `
models:
- id: "logical-model"
`
const baseSuffix = `
providers:
provider-a: "served-model"
nodes:
- id: "node-a"
providers:
- id: "provider-a"
type: "vllm"
category: "api"
models:
- "served-model"
`
for _, tc := range []struct {
name string
policyYAML string
want string
wantErrPart string
}{
{name: "omitted defaults to provider", want: config.UsageAttributionProvider},
{name: "provider is accepted", policyYAML: ` usage_attribution: "provider"` + "\n", want: config.UsageAttributionProvider},
{name: "model group is accepted", policyYAML: ` usage_attribution: "model_group"` + "\n", want: config.UsageAttributionModelGroup},
{name: "unknown policy is rejected", policyYAML: ` usage_attribution: "request_model"` + "\n", wantErrPart: "usage_attribution"},
} {
t.Run(tc.name, func(t *testing.T) {
cfg, err := loadUsageAttributionConfig(t, basePrefix+tc.policyYAML+baseSuffix)
if tc.wantErrPart != "" {
if err == nil {
t.Fatal("expected config load error")
}
if !strings.Contains(err.Error(), tc.wantErrPart) {
t.Fatalf("error %q does not contain %q", err, tc.wantErrPart)
}
return
}
if err != nil {
t.Fatalf("load config: %v", err)
}
if got := cfg.Models[0].EffectiveUsageAttribution(); got != tc.want {
t.Fatalf("effective attribution = %q, want %q", got, tc.want)
}
})
}
}
func TestOpenAIDirectProviderBindingValidation(t *testing.T) {
for _, tc := range []struct {
name string
openAIYAML string
wantErrPart string
check func(*testing.T, *config.EdgeConfig)
}{
{
name: "disabled surface does not require binding",
openAIYAML: `
openai:
enabled: false
`,
},
{
name: "top level fallback binding",
openAIYAML: `
openai:
enabled: true
provider_id: "provider-fallback"
adapter: "cli"
target: "codex"
`,
check: func(t *testing.T, cfg *config.EdgeConfig) {
if cfg.OpenAI.ProviderID != "provider-fallback" {
t.Fatalf("provider_id = %q", cfg.OpenAI.ProviderID)
}
},
},
{
name: "route override and fallback binding",
openAIYAML: `
openai:
enabled: true
provider_id: "provider-fallback"
model_routes:
- model: "special"
provider_id: "provider-route"
adapter: "cli"
target: "codex"
- model: "inherited"
provider_id: " "
adapter: "cli"
target: "codex-exec"
`,
check: func(t *testing.T, cfg *config.EdgeConfig) {
if got := cfg.OpenAI.ModelRoutes[0].ProviderID; got != "provider-route" {
t.Fatalf("route provider_id = %q", got)
}
if got := strings.TrimSpace(cfg.OpenAI.ModelRoutes[1].ProviderID); got != "" {
t.Fatalf("blank route provider_id = %q", got)
}
},
},
{
name: "missing fallback binding is rejected",
openAIYAML: `
openai:
enabled: true
adapter: "cli"
target: "codex"
`,
wantErrPart: "openai.provider_id",
},
{
name: "blank fallback binding is rejected",
openAIYAML: `
openai:
enabled: true
provider_id: " "
model_routes:
- model: "special"
provider_id: "provider-route"
adapter: "cli"
target: "codex"
`,
wantErrPart: "openai.provider_id",
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg, err := loadUsageAttributionConfig(t, tc.openAIYAML)
if tc.wantErrPart != "" {
if err == nil {
t.Fatal("expected config load error")
}
if !strings.Contains(err.Error(), tc.wantErrPart) {
t.Fatalf("error %q does not contain %q", err, tc.wantErrPart)
}
return
}
if err != nil {
t.Fatalf("load config: %v", err)
}
if tc.check != nil {
tc.check(t, cfg)
}
})
}
}