mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
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`.
215 lines
5.2 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|