mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
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`.
This commit is contained in:
214
internal/routing/extractor_test.go
Normal file
214
internal/routing/extractor_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user