Files
CLIProxyAPI/internal/routing/extractor_test.go
이대희 9299897e04 Implements unified model routing
Migrates the AMP module to a new unified routing system, replacing the fallback handler with a router-based approach.

This change introduces a `ModelRoutingWrapper` that handles model extraction, routing decisions, and proxying based on provider availability and model mappings.
It provides a more flexible and maintainable routing mechanism by centralizing routing logic.

The changes include:
- Introducing new `routing` package with core routing logic.
- Creating characterization tests to capture existing behavior.
- Implementing model extraction and rewriting.
- Updating AMP module routes to utilize the new routing wrapper.
- Deprecating `FallbackHandler` in favor of the new `ModelRoutingWrapper`.
2026-02-01 16:58:32 +09:00

215 lines
5.2 KiB
Go

package routing
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestModelExtractor_ExtractFromJSONBody(t *testing.T) {
extractor := NewModelExtractor()
tests := []struct {
name string
body []byte
want string
wantErr bool
}{
{
name: "extract from JSON body with model field",
body: []byte(`{"model":"gpt-4.1"}`),
want: "gpt-4.1",
},
{
name: "extract claude model from JSON body",
body: []byte(`{"model":"claude-3-5-sonnet-20241022"}`),
want: "claude-3-5-sonnet-20241022",
},
{
name: "extract with additional fields",
body: []byte(`{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}`),
want: "gpt-4",
},
{
name: "empty body returns empty",
body: []byte{},
want: "",
},
{
name: "no model field returns empty",
body: []byte(`{"messages":[]}`),
want: "",
},
{
name: "model is not string returns empty",
body: []byte(`{"model":123}`),
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractor.Extract(tt.body, nil)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestModelExtractor_ExtractFromGeminiActionParam(t *testing.T) {
extractor := NewModelExtractor()
tests := []struct {
name string
body []byte
ginParams map[string]string
want string
}{
{
name: "extract from action parameter - gemini-pro",
body: []byte(`{}`),
ginParams: map[string]string{"action": "gemini-pro:generateContent"},
want: "gemini-pro",
},
{
name: "extract from action parameter - gemini-ultra",
body: []byte(`{}`),
ginParams: map[string]string{"action": "gemini-ultra:chat"},
want: "gemini-ultra",
},
{
name: "empty action returns empty",
body: []byte(`{}`),
ginParams: map[string]string{"action": ""},
want: "",
},
{
name: "action without colon returns full value",
body: []byte(`{}`),
ginParams: map[string]string{"action": "gemini-model"},
want: "gemini-model",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractor.Extract(tt.body, tt.ginParams)
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestModelExtractor_ExtractFromGeminiV1Beta1Path(t *testing.T) {
extractor := NewModelExtractor()
tests := []struct {
name string
body []byte
ginParams map[string]string
want string
}{
{
name: "extract from v1beta1 path - gemini-3-pro",
body: []byte(`{}`),
ginParams: map[string]string{"path": "/publishers/google/models/gemini-3-pro:streamGenerateContent"},
want: "gemini-3-pro",
},
{
name: "extract from v1beta1 path with preview",
body: []byte(`{}`),
ginParams: map[string]string{"path": "/publishers/google/models/gemini-3-pro-preview:generateContent"},
want: "gemini-3-pro-preview",
},
{
name: "path without models segment returns empty",
body: []byte(`{}`),
ginParams: map[string]string{"path": "/publishers/google/gemini-3-pro:streamGenerateContent"},
want: "",
},
{
name: "empty path returns empty",
body: []byte(`{}`),
ginParams: map[string]string{"path": ""},
want: "",
},
{
name: "path with /models/ but no colon returns empty",
body: []byte(`{}`),
ginParams: map[string]string{"path": "/publishers/google/models/gemini-3-pro"},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractor.Extract(tt.body, tt.ginParams)
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestModelExtractor_ExtractPriority(t *testing.T) {
extractor := NewModelExtractor()
// JSON body takes priority over gin params
t.Run("JSON body takes priority over action param", func(t *testing.T) {
body := []byte(`{"model":"gpt-4"}`)
params := map[string]string{"action": "gemini-pro:generateContent"}
got, err := extractor.Extract(body, params)
assert.NoError(t, err)
assert.Equal(t, "gpt-4", got)
})
// Action param takes priority over path param
t.Run("action param takes priority over path param", func(t *testing.T) {
body := []byte(`{}`)
params := map[string]string{
"action": "gemini-action:generate",
"path": "/publishers/google/models/gemini-path:streamGenerateContent",
}
got, err := extractor.Extract(body, params)
assert.NoError(t, err)
assert.Equal(t, "gemini-action", got)
})
}
func TestModelExtractor_NoModelFound(t *testing.T) {
extractor := NewModelExtractor()
tests := []struct {
name string
body []byte
ginParams map[string]string
}{
{
name: "empty body and no params",
body: []byte{},
ginParams: nil,
},
{
name: "body without model and no params",
body: []byte(`{"messages":[]}`),
ginParams: map[string]string{},
},
{
name: "irrelevant params only",
body: []byte(`{}`),
ginParams: map[string]string{"other": "value"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractor.Extract(tt.body, tt.ginParams)
assert.NoError(t, err)
assert.Empty(t, got)
})
}
}