feat(routing): add formal chat route endpoint

This commit is contained in:
phamnazage-jpg
2026-05-29 13:17:56 +08:00
parent 09b43ce2d8
commit ecdeedb103
4 changed files with 325 additions and 3 deletions

View File

@@ -70,6 +70,68 @@ func TestAPIProxyRouteChatCompletionsReturnsResolveAndForward(t *testing.T) {
assertJSONContains(t, response.Body().Bytes(), "forward.upstream_status", float64(200))
}
func TestAPIRouteChatCompletionsReturnsFormalResult(t *testing.T) {
t.Parallel()
handler := NewAPIHandler("secret-token", ActionSet{
RouteChatCompletions: func(_ context.Context, req RouteChatCompletionsRequest) (RouteChatCompletionsResult, error) {
if req.LogicalGroupID != "gpt-shared" {
t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
}
if req.Model != "gpt-5.4" {
t.Fatalf("Model = %q, want gpt-5.4", req.Model)
}
return RouteChatCompletionsResult{
RequestID: "req-route-1",
Backend: "memory",
LogicalGroupID: req.LogicalGroupID,
Model: req.Model,
Scope: req.Scope,
SubjectID: req.SubjectID,
StickyKey: "lg:gpt-shared:m:gpt-5.4:conv:conv-1",
StickyHit: false,
StickyAction: "bind",
FallbackUsed: true,
SelectedRoute: RouteChatCompletionsRouteInfo{
RouteID: "asxs",
RouteName: "ASXS",
ShadowHostID: "remote43",
ShadowGroupID: "9",
ShadowModel: "gpt-5.4",
Priority: 10,
Weight: 100,
},
Forward: RouteChatCompletionsForwardInfo{
OK: true,
HostID: "remote43",
HostBaseURL: "https://sub2api.example.com",
ShadowGroupID: "9",
ShadowModel: "gpt-5.4",
UpstreamPath: "/v1/chat/completions",
UpstreamStatus: 200,
LatencyMS: 12,
ContentType: "application/json",
},
}, nil
},
})
request := httptestRequest(t, http.MethodPost, "/api/routing/chat/completions", map[string]any{
"logical_group_id": "gpt-shared",
"model": "gpt-5.4",
"scope": "conversation",
"subject_id": "conv-1",
"gateway_api_key": "gateway-key",
"sync": true,
}, "secret-token")
response := httptestRecorder(handler, request)
assertStatusCode(t, response, http.StatusOK)
assertJSONContains(t, response.Body().Bytes(), "selected_route.route_id", "asxs")
assertJSONContains(t, response.Body().Bytes(), "selected_route.shadow_model", "gpt-5.4")
assertJSONContains(t, response.Body().Bytes(), "fallback_used", true)
assertJSONContains(t, response.Body().Bytes(), "forward.upstream_status", float64(200))
}
func TestNewActionSetProxyRouteChatCompletionsFlow(t *testing.T) {
t.Parallel()
@@ -225,6 +287,144 @@ func TestNewActionSetProxyRouteChatCompletionsFlow(t *testing.T) {
}
}
func TestNewActionSetRouteChatCompletionsFlow(t *testing.T) {
t.Parallel()
var (
gotAuthHeader string
gotModel string
gotPrompt string
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/chat/completions" {
t.Fatalf("URL.Path = %q, want /v1/chat/completions", r.URL.Path)
}
gotAuthHeader = r.Header.Get("Authorization")
var payload struct {
Model string `json:"model"`
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("json.Decode() error = %v", err)
}
gotModel = payload.Model
if len(payload.Messages) > 0 {
gotPrompt = payload.Messages[0].Content
}
writeJSON(w, http.StatusOK, map[string]any{
"id": "chatcmpl_route",
"object": "chat.completion",
"choices": []map[string]any{
{
"index": 0,
"message": map[string]any{
"role": "assistant",
"content": "pong-route",
},
},
},
})
}))
defer server.Close()
dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-formal.db")) + "?_busy_timeout=5000"
actions := NewActionSet(dsn)
ctx := context.Background()
store, err := sqlite.Open(ctx, dsn)
if err != nil {
t.Fatalf("sqlite.Open() error = %v", err)
}
defer store.Close()
if _, err := store.Hosts().Create(ctx, sqlite.Host{
HostID: "remote43",
BaseURL: server.URL,
HostVersion: "0.1.126",
AuthType: "apikey",
AuthToken: "host-admin-token",
}); err != nil {
t.Fatalf("Hosts().Create() error = %v", err)
}
if _, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
LogicalGroupID: "gpt-shared",
DisplayName: "GPT Shared",
Status: "active",
RoutePolicy: "priority",
StickyMode: "conversation_preferred",
ConversationTTLSeconds: 1200,
UserModelTTLSeconds: 600,
FailoverThreshold: 2,
CooldownSeconds: 300,
}); err != nil {
t.Fatalf("CreateLogicalGroup() error = %v", err)
}
if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
LogicalGroupID: "gpt-shared",
PublicModel: "gpt-5.4",
Status: "active",
}); err != nil {
t.Fatalf("CreateLogicalGroupModel() error = %v", err)
}
if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
LogicalGroupID: "gpt-shared",
RouteID: "asxs",
Name: "ASXS",
Status: "active",
Priority: 10,
ShadowGroupID: "gpt-shared__asxs",
ShadowHostID: "remote43",
UpstreamBaseURLHint: "https://api.asxs.top/v1",
}); err != nil {
t.Fatalf("CreateLogicalGroupRoute() error = %v", err)
}
if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
LogicalGroupID: "gpt-shared",
RouteID: "asxs",
PublicModel: "gpt-5.4",
ShadowModel: "gpt-5.4",
Status: "active",
}); err != nil {
t.Fatalf("CreateLogicalGroupRouteModel() error = %v", err)
}
result, err := actions.RouteChatCompletions(ctx, RouteChatCompletionsRequest{
RequestID: "req-route-1",
LogicalGroupID: "gpt-shared",
Model: "gpt-5.4",
Scope: "conversation",
SubjectID: "conv-1",
GatewayAPIKey: "gateway-key",
Sync: true,
})
if err != nil {
t.Fatalf("RouteChatCompletions() error = %v", err)
}
if gotAuthHeader != "Bearer gateway-key" {
t.Fatalf("Authorization header = %q, want Bearer gateway-key", gotAuthHeader)
}
if gotModel != "gpt-5.4" {
t.Fatalf("forwarded model = %q, want gpt-5.4", gotModel)
}
if gotPrompt != "ping" {
t.Fatalf("forwarded prompt = %q, want ping", gotPrompt)
}
if result.SelectedRoute.RouteID != "asxs" || result.SelectedRoute.ShadowModel != "gpt-5.4" {
t.Fatalf("SelectedRoute = %+v, want asxs route with shadow model gpt-5.4", result.SelectedRoute)
}
if !result.Forward.OK || result.Forward.UpstreamStatus != http.StatusOK {
t.Fatalf("Forward = %+v, want successful 200 forward", result.Forward)
}
if result.Model != "gpt-5.4" || result.RequestID != "req-route-1" {
t.Fatalf("RouteChatCompletions() = %+v, want model gpt-5.4 and request id req-route-1", result)
}
}
func TestNewActionSetProxyRouteChatCompletionsManagedSubscriptionFlow(t *testing.T) {
t.Parallel()