mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:37:52 +08:00
470 lines
14 KiB
Go
470 lines
14 KiB
Go
package service
|
|
|
|
import "testing"
|
|
|
|
func TestResolveOpenAIForwardModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
account *Account
|
|
requestedModel string
|
|
messagesDispatchMappedModel string
|
|
expectedModel string
|
|
}{
|
|
{
|
|
name: "uses messages dispatch model for known claude family",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "claude-opus-4-6",
|
|
messagesDispatchMappedModel: "gpt-4o-mini",
|
|
expectedModel: "gpt-4o-mini",
|
|
},
|
|
{
|
|
name: "uses exact messages dispatch model for unknown claude family",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "claude-fable-5",
|
|
messagesDispatchMappedModel: " gpt-5.6-sol ",
|
|
expectedModel: "gpt-5.6-sol",
|
|
},
|
|
{
|
|
name: "nil account uses messages dispatch model",
|
|
requestedModel: "claude-fable-5",
|
|
messagesDispatchMappedModel: "gpt-5.6-sol",
|
|
expectedModel: "gpt-5.6-sol",
|
|
},
|
|
{
|
|
name: "nil account without messages dispatch keeps requested model",
|
|
requestedModel: "claude-fable-5",
|
|
expectedModel: "claude-fable-5",
|
|
},
|
|
{
|
|
name: "ordinary unknown gpt model has no messages dispatch fallback",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt6",
|
|
expectedModel: "gpt6",
|
|
},
|
|
{
|
|
name: "account exact mapping overrides messages dispatch model",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{
|
|
"claude-fable-5": "gpt-5.5",
|
|
},
|
|
},
|
|
},
|
|
requestedModel: "claude-fable-5",
|
|
messagesDispatchMappedModel: "gpt-5.6-sol",
|
|
expectedModel: "gpt-5.5",
|
|
},
|
|
{
|
|
name: "account wildcard mapping overrides messages dispatch model",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{
|
|
"claude-*": "gpt-5.4",
|
|
},
|
|
},
|
|
},
|
|
requestedModel: "claude-fable-5",
|
|
messagesDispatchMappedModel: "gpt-5.6-sol",
|
|
expectedModel: "gpt-5.4",
|
|
},
|
|
{
|
|
name: "account passthrough mapping overrides messages dispatch model",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{
|
|
"claude-fable-5": "claude-fable-5",
|
|
},
|
|
},
|
|
},
|
|
requestedModel: "claude-fable-5",
|
|
messagesDispatchMappedModel: "gpt-5.6-sol",
|
|
expectedModel: "claude-fable-5",
|
|
},
|
|
{
|
|
name: "ordinary codex spark request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt-5.3-codex-spark",
|
|
expectedModel: "gpt-5.3-codex-spark",
|
|
},
|
|
{
|
|
name: "ordinary gpt-5.5 request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt-5.5",
|
|
expectedModel: "gpt-5.5",
|
|
},
|
|
{
|
|
name: "ordinary gpt-5.5-pro request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt-5.5-pro",
|
|
expectedModel: "gpt-5.5-pro",
|
|
},
|
|
{
|
|
name: "ordinary compact-spelled gpt5.5 request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt5.5",
|
|
expectedModel: "gpt5.5",
|
|
},
|
|
{
|
|
name: "ordinary namespaced gpt-5.5 request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "openai/gpt-5.5",
|
|
expectedModel: "openai/gpt-5.5",
|
|
},
|
|
{
|
|
name: "ordinary compact gpt-5.5 request keeps requested model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt-5.5-openai-compact",
|
|
expectedModel: "gpt-5.5-openai-compact",
|
|
},
|
|
{
|
|
name: "whitespace-only messages dispatch model is ignored",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
requestedModel: "gpt-5.5",
|
|
messagesDispatchMappedModel: " ",
|
|
expectedModel: "gpt-5.5",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := resolveOpenAIForwardModel(tt.account, tt.requestedModel, tt.messagesDispatchMappedModel); got != tt.expectedModel {
|
|
t.Fatalf("resolveOpenAIForwardModel(...) = %q, want %q", got, tt.expectedModel)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveOpenAICompactForwardModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
account *Account
|
|
model string
|
|
expectedModel string
|
|
}{
|
|
{
|
|
name: "nil account keeps original model",
|
|
account: nil,
|
|
model: "gpt-5.4",
|
|
expectedModel: "gpt-5.4",
|
|
},
|
|
{
|
|
name: "missing compact mapping keeps original model",
|
|
account: &Account{
|
|
Credentials: map[string]any{},
|
|
},
|
|
model: "gpt-5.4",
|
|
expectedModel: "gpt-5.4",
|
|
},
|
|
{
|
|
name: "exact compact mapping overrides model",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"compact_model_mapping": map[string]any{
|
|
"gpt-5.4": "gpt-5.4-openai-compact",
|
|
},
|
|
},
|
|
},
|
|
model: "gpt-5.4",
|
|
expectedModel: "gpt-5.4-openai-compact",
|
|
},
|
|
{
|
|
name: "wildcard compact mapping overrides model",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"compact_model_mapping": map[string]any{
|
|
"gpt-5.*": "gpt-5-openai-compact",
|
|
},
|
|
},
|
|
},
|
|
model: "gpt-5.4",
|
|
expectedModel: "gpt-5-openai-compact",
|
|
},
|
|
{
|
|
name: "passthrough compact mapping remains unchanged",
|
|
account: &Account{
|
|
Credentials: map[string]any{
|
|
"compact_model_mapping": map[string]any{
|
|
"gpt-5.4": "gpt-5.4",
|
|
},
|
|
},
|
|
},
|
|
model: "gpt-5.4",
|
|
expectedModel: "gpt-5.4",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := resolveOpenAICompactForwardModel(tt.account, tt.model); got != tt.expectedModel {
|
|
t.Fatalf("resolveOpenAICompactForwardModel(...) = %q, want %q", got, tt.expectedModel)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveOpenAIForwardMappedModels_CompactMappingPrecedence(t *testing.T) {
|
|
conflictingMappings := map[string]any{
|
|
"model_mapping": map[string]any{"gpt-5.5": "gpt-5.4"},
|
|
"compact_model_mapping": map[string]any{"gpt-5.5": "gpt-5.5-openai-compact"},
|
|
}
|
|
mappedOnlyCompact := map[string]any{
|
|
"model_mapping": map[string]any{"gpt-5.5": "gpt-5.4"},
|
|
"compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
account *Account
|
|
requireCompact bool
|
|
wantBilling string
|
|
wantUpstream string
|
|
}{
|
|
{
|
|
name: "compact uses client-visible model before ordinary mapping",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth,
|
|
Credentials: conflictingMappings},
|
|
requireCompact: true,
|
|
wantBilling: "gpt-5.4",
|
|
wantUpstream: "gpt-5.5-openai-compact",
|
|
},
|
|
{
|
|
name: "non-compact uses ordinary mapping",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth,
|
|
Credentials: conflictingMappings},
|
|
wantBilling: "gpt-5.4",
|
|
wantUpstream: "gpt-5.4",
|
|
},
|
|
{
|
|
name: "compact falls back to ordinary mapped model",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth,
|
|
Credentials: mappedOnlyCompact},
|
|
requireCompact: true,
|
|
wantBilling: "gpt-5.4",
|
|
wantUpstream: "gpt-5.4-openai-compact",
|
|
},
|
|
{
|
|
name: "passthrough ignores ordinary mapping",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth,
|
|
Credentials: conflictingMappings, Extra: map[string]any{"openai_passthrough": true}},
|
|
requireCompact: true,
|
|
wantBilling: "gpt-5.5",
|
|
wantUpstream: "gpt-5.5-openai-compact",
|
|
},
|
|
{
|
|
name: "raw chat fallback never applies compact mapping",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
|
|
Credentials: conflictingMappings, Extra: map[string]any{"openai_responses_supported": false}},
|
|
requireCompact: true,
|
|
wantBilling: "gpt-5.4",
|
|
wantUpstream: "gpt-5.4",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
billing, upstream := resolveOpenAIForwardMappedModels(tt.account, "gpt-5.5", tt.requireCompact)
|
|
if billing != tt.wantBilling {
|
|
t.Fatalf("billing model = %q, want %q", billing, tt.wantBilling)
|
|
}
|
|
if upstream != tt.wantUpstream {
|
|
t.Fatalf("upstream model = %q, want %q", upstream, tt.wantUpstream)
|
|
}
|
|
if scheduler := resolveOpenAIAccountUpstreamModelForRequest(tt.account, "gpt-5.5", tt.requireCompact); scheduler != upstream {
|
|
t.Fatalf("scheduler model %q disagrees with Forward model %q", scheduler, upstream)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCanonicalOpenAIAccountSchedulingModelMatchesForwardSemantics(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
account *Account
|
|
model string
|
|
want string
|
|
}{
|
|
{
|
|
name: "OpenAI OAuth applies Codex alias normalization",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
|
|
model: "gpt-5.6",
|
|
want: "gpt-5.6-sol",
|
|
},
|
|
{
|
|
name: "OpenAI passthrough ignores ordinary account mapping",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth,
|
|
Credentials: map[string]any{"model_mapping": map[string]any{"public": "private"}},
|
|
Extra: map[string]any{"openai_passthrough": true}},
|
|
model: "public",
|
|
want: "public",
|
|
},
|
|
{
|
|
name: "Grok OAuth does not inherit OpenAI Codex aliases",
|
|
account: &Account{Platform: PlatformGrok, Type: AccountTypeOAuth},
|
|
model: "gpt-5.6",
|
|
want: "gpt-5.6",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := canonicalOpenAIAccountSchedulingModel(tt.account, tt.model); got != tt.want {
|
|
t.Fatalf("canonical scheduling model = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveOpenAIErrorSchedulingModelPrefersActualUpstreamModel(t *testing.T) {
|
|
if got := resolveOpenAIErrorSchedulingModel("gpt-5.4", "gpt-5.5-openai-compact"); got != "gpt-5.5-openai-compact" {
|
|
t.Fatalf("error scheduling model = %q, want compact upstream model", got)
|
|
}
|
|
if got := resolveOpenAIErrorSchedulingModel("gpt-5.4", ""); got != "gpt-5.4" {
|
|
t.Fatalf("empty upstream fallback = %q, want billing model", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCodexModel(t *testing.T) {
|
|
cases := map[string]string{
|
|
"gpt-5.3-codex-spark": "gpt-5.3-codex-spark",
|
|
"gpt-5.3-codex-spark-high": "gpt-5.3-codex-spark",
|
|
"gpt-5.3-codex-spark-xhigh": "gpt-5.3-codex-spark",
|
|
"gpt-5.3": "gpt-5.3-codex",
|
|
"gpt-image-2": "gpt-image-2",
|
|
"gpt-5.4-nano": "gpt-5.4-nano",
|
|
"gpt-5.4-nano-high": "gpt-5.4-nano",
|
|
"gpt6": "gpt6",
|
|
"claude-opus-4-6": "claude-opus-4-6",
|
|
}
|
|
|
|
for input, expected := range cases {
|
|
if got := normalizeCodexModel(input); got != expected {
|
|
t.Fatalf("normalizeCodexModel(%q) = %q, want %q", input, got, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeOpenAIModelForUpstream(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
account *Account
|
|
model string
|
|
want string
|
|
}{
|
|
{
|
|
name: "oauth routes bare GPT-5.6 alias to Sol",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "gpt-5.6",
|
|
want: "gpt-5.6-sol",
|
|
},
|
|
{
|
|
name: "oauth routes provider-prefixed GPT-5.6 alias to Sol",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "openai/gpt-5.6",
|
|
want: "gpt-5.6-sol",
|
|
},
|
|
{
|
|
name: "oauth preserves unknown non codex model",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "gemini-3-flash-preview",
|
|
want: "gemini-3-flash-preview",
|
|
},
|
|
{
|
|
name: "oauth preserves invalid gpt model",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "gpt6",
|
|
want: "gpt6",
|
|
},
|
|
{
|
|
name: "oauth normalizes known codex alias",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "gpt-5.4-high",
|
|
want: "gpt-5.4",
|
|
},
|
|
{
|
|
name: "oauth preserves GPT-5.5 Pro model",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "openai/gpt-5.5-pro",
|
|
want: "gpt-5.5-pro",
|
|
},
|
|
{
|
|
name: "oauth preserves codex auto review model",
|
|
account: &Account{Type: AccountTypeOAuth},
|
|
model: "codex-auto-review",
|
|
want: "codex-auto-review",
|
|
},
|
|
{
|
|
name: "apikey preserves official bare GPT-5.6 alias",
|
|
account: &Account{Type: AccountTypeAPIKey},
|
|
model: "gpt-5.6",
|
|
want: "gpt-5.6",
|
|
},
|
|
{
|
|
name: "apikey preserves custom compatible model",
|
|
account: &Account{Type: AccountTypeAPIKey},
|
|
model: "gemini-3-flash-preview",
|
|
want: "gemini-3-flash-preview",
|
|
},
|
|
{
|
|
name: "apikey preserves official non codex model",
|
|
account: &Account{Type: AccountTypeAPIKey},
|
|
model: "gpt-4.1",
|
|
want: "gpt-4.1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := normalizeOpenAIModelForUpstream(tt.account, tt.model); got != tt.want {
|
|
t.Fatalf("normalizeOpenAIModelForUpstream(...) = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUsageBillingModelCandidatesPreserveCodexAutoReviewModel(t *testing.T) {
|
|
candidates := usageBillingModelCandidates("codex-auto-review")
|
|
|
|
expected := []string{"codex-auto-review"}
|
|
if len(candidates) != len(expected) {
|
|
t.Fatalf("usageBillingModelCandidates(codex-auto-review) = %#v, want %#v", candidates, expected)
|
|
}
|
|
for i := range expected {
|
|
if candidates[i] != expected[i] {
|
|
t.Fatalf("usageBillingModelCandidates(codex-auto-review) = %#v, want %#v", candidates, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUsageBillingModelCandidatesPreserveGPT55ProModel(t *testing.T) {
|
|
candidates := usageBillingModelCandidates("openai/gpt-5.5-pro")
|
|
|
|
expected := []string{"openai/gpt-5.5-pro", "gpt-5.5-pro"}
|
|
if len(candidates) != len(expected) {
|
|
t.Fatalf("usageBillingModelCandidates(openai/gpt-5.5-pro) = %#v, want %#v", candidates, expected)
|
|
}
|
|
for i := range expected {
|
|
if candidates[i] != expected[i] {
|
|
t.Fatalf("usageBillingModelCandidates(openai/gpt-5.5-pro) = %#v, want %#v", candidates, expected)
|
|
}
|
|
}
|
|
}
|