mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00: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:
113
internal/routing/testutil/fake_handler.go
Normal file
113
internal/routing/testutil/fake_handler.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FakeHandlerRecorder records handler invocations for testing.
|
||||
type FakeHandlerRecorder struct {
|
||||
Called bool
|
||||
CallCount int
|
||||
RequestBody []byte
|
||||
RequestHeader http.Header
|
||||
ContextKeys map[string]interface{}
|
||||
ResponseStatus int
|
||||
ResponseBody []byte
|
||||
}
|
||||
|
||||
// NewFakeHandlerRecorder creates a new fake handler recorder.
|
||||
func NewFakeHandlerRecorder() *FakeHandlerRecorder {
|
||||
return &FakeHandlerRecorder{
|
||||
ContextKeys: make(map[string]interface{}),
|
||||
ResponseStatus: http.StatusOK,
|
||||
ResponseBody: []byte(`{"status":"handled"}`),
|
||||
}
|
||||
}
|
||||
|
||||
// GinHandler returns a gin.HandlerFunc that records the invocation.
|
||||
func (f *FakeHandlerRecorder) GinHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
f.record(c)
|
||||
c.Data(f.ResponseStatus, "application/json", f.ResponseBody)
|
||||
}
|
||||
}
|
||||
|
||||
// GinHandlerWithModel returns a gin.HandlerFunc that records the invocation and returns the model from context.
|
||||
// Useful for testing response rewriting in model mapping scenarios.
|
||||
func (f *FakeHandlerRecorder) GinHandlerWithModel() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
f.record(c)
|
||||
// Return a response with the model field that would be in the actual API response
|
||||
// If ResponseBody was explicitly set (not default), use that; otherwise generate from context
|
||||
var body []byte
|
||||
if mappedModel, exists := c.Get("mapped_model"); exists {
|
||||
body = []byte(`{"model":"` + mappedModel.(string) + `","status":"handled"}`)
|
||||
} else {
|
||||
body = f.ResponseBody
|
||||
}
|
||||
c.Data(f.ResponseStatus, "application/json", body)
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPHandler returns an http.HandlerFunc that records the invocation.
|
||||
func (f *FakeHandlerRecorder) HTTPHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
f.Called = true
|
||||
f.CallCount++
|
||||
f.RequestBody = body
|
||||
f.RequestHeader = r.Header.Clone()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(f.ResponseStatus)
|
||||
w.Write(f.ResponseBody)
|
||||
}
|
||||
}
|
||||
|
||||
// record captures the request details from gin context.
|
||||
func (f *FakeHandlerRecorder) record(c *gin.Context) {
|
||||
f.Called = true
|
||||
f.CallCount++
|
||||
|
||||
body, _ := io.ReadAll(c.Request.Body)
|
||||
f.RequestBody = body
|
||||
f.RequestHeader = c.Request.Header.Clone()
|
||||
|
||||
// Capture common context keys used by routing
|
||||
if val, exists := c.Get("mapped_model"); exists {
|
||||
f.ContextKeys["mapped_model"] = val
|
||||
}
|
||||
if val, exists := c.Get("fallback_models"); exists {
|
||||
f.ContextKeys["fallback_models"] = val
|
||||
}
|
||||
if val, exists := c.Get("route_type"); exists {
|
||||
f.ContextKeys["route_type"] = val
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears the recorder state.
|
||||
func (f *FakeHandlerRecorder) Reset() {
|
||||
f.Called = false
|
||||
f.CallCount = 0
|
||||
f.RequestBody = nil
|
||||
f.RequestHeader = nil
|
||||
f.ContextKeys = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// GetContextKey returns a captured context key value.
|
||||
func (f *FakeHandlerRecorder) GetContextKey(key string) (interface{}, bool) {
|
||||
val, ok := f.ContextKeys[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// WasCalled returns true if the handler was called.
|
||||
func (f *FakeHandlerRecorder) WasCalled() bool {
|
||||
return f.Called
|
||||
}
|
||||
|
||||
// GetCallCount returns the number of times the handler was called.
|
||||
func (f *FakeHandlerRecorder) GetCallCount() int {
|
||||
return f.CallCount
|
||||
}
|
||||
83
internal/routing/testutil/fake_proxy.go
Normal file
83
internal/routing/testutil/fake_proxy.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
// CloseNotifierRecorder wraps httptest.ResponseRecorder with CloseNotify support.
|
||||
// This is needed because ReverseProxy requires http.CloseNotifier.
|
||||
type CloseNotifierRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closeChan chan bool
|
||||
}
|
||||
|
||||
// NewCloseNotifierRecorder creates a ResponseRecorder that implements CloseNotifier.
|
||||
func NewCloseNotifierRecorder() *CloseNotifierRecorder {
|
||||
return &CloseNotifierRecorder{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
closeChan: make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
func (c *CloseNotifierRecorder) CloseNotify() <-chan bool {
|
||||
return c.closeChan
|
||||
}
|
||||
|
||||
// FakeProxyRecorder records proxy invocations for testing.
|
||||
type FakeProxyRecorder struct {
|
||||
Called bool
|
||||
CallCount int
|
||||
RequestBody []byte
|
||||
RequestHeaders http.Header
|
||||
ResponseStatus int
|
||||
ResponseBody []byte
|
||||
}
|
||||
|
||||
// NewFakeProxyRecorder creates a new fake proxy recorder.
|
||||
func NewFakeProxyRecorder() *FakeProxyRecorder {
|
||||
return &FakeProxyRecorder{
|
||||
ResponseStatus: http.StatusOK,
|
||||
ResponseBody: []byte(`{"status":"proxied"}`),
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler to act as a reverse proxy.
|
||||
func (f *FakeProxyRecorder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f.Called = true
|
||||
f.CallCount++
|
||||
f.RequestHeaders = r.Header.Clone()
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err == nil {
|
||||
f.RequestBody = body
|
||||
}
|
||||
|
||||
w.WriteHeader(f.ResponseStatus)
|
||||
w.Write(f.ResponseBody)
|
||||
}
|
||||
|
||||
// GetCallCount returns the number of times the proxy was called.
|
||||
func (f *FakeProxyRecorder) GetCallCount() int {
|
||||
return f.CallCount
|
||||
}
|
||||
|
||||
// Reset clears the recorder state.
|
||||
func (f *FakeProxyRecorder) Reset() {
|
||||
f.Called = false
|
||||
f.CallCount = 0
|
||||
f.RequestBody = nil
|
||||
f.RequestHeaders = nil
|
||||
}
|
||||
|
||||
// ToHandler returns the recorder as an http.Handler for use with httptest.
|
||||
func (f *FakeProxyRecorder) ToHandler() http.Handler {
|
||||
return http.HandlerFunc(f.ServeHTTP)
|
||||
}
|
||||
|
||||
// CreateTestServer creates an httptest server with this fake proxy.
|
||||
func (f *FakeProxyRecorder) CreateTestServer() *httptest.Server {
|
||||
return httptest.NewServer(f.ToHandler())
|
||||
}
|
||||
Reference in New Issue
Block a user