feat: improve model catalog aggregation

This commit is contained in:
2026-05-11 17:44:57 +08:00
parent ec87816c95
commit 0431cb8157
41 changed files with 4745 additions and 550 deletions
+19 -15
View File
@@ -127,15 +127,16 @@ func TestOpenAIClientChatContract(t *testing.T) {
Model: "openai:gpt-4o-mini",
Body: map[string]any{"model": "openai:gpt-4o-mini", "messages": []any{map[string]any{"role": "user", "content": "ping"}}},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gpt-4o-mini",
Credentials: map[string]any{"apiKey": "test-key"},
BaseURL: server.URL,
ModelName: "gpt-4o-mini",
ProviderModelName: "openai-compatible-gpt-4o-mini",
Credentials: map[string]any{"apiKey": "test-key"},
},
})
if err != nil {
t.Fatalf("run openai client: %v", err)
}
if gotPath != "/chat/completions" || gotAuth != "Bearer test-key" || gotModel != "gpt-4o-mini" {
if gotPath != "/chat/completions" || gotAuth != "Bearer test-key" || gotModel != "openai-compatible-gpt-4o-mini" {
t.Fatalf("unexpected request path=%s auth=%s model=%s", gotPath, gotAuth, gotModel)
}
if response.Usage.TotalTokens != 5 || response.Result["id"] != "chatcmpl-test" {
@@ -231,16 +232,17 @@ func TestGeminiClientChatContract(t *testing.T) {
"messages": []any{map[string]any{"role": "user", "content": "ping"}},
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gemini-2.5-flash",
ModelType: "chat",
Credentials: map[string]any{"apiKey": "gemini-key"},
BaseURL: server.URL,
ModelName: "gemini-2.5-flash",
ProviderModelName: "gemini-compatible-2.5-flash",
ModelType: "chat",
Credentials: map[string]any{"apiKey": "gemini-key"},
},
})
if err != nil {
t.Fatalf("run gemini client: %v", err)
}
if gotPath != "/v1beta/models/gemini-2.5-flash:generateContent" || gotKey != "gemini-key" || gotText != "ping" {
if gotPath != "/v1beta/models/gemini-compatible-2.5-flash:generateContent" || gotKey != "gemini-key" || gotText != "ping" {
t.Fatalf("unexpected request path=%s key=%s text=%s", gotPath, gotKey, gotText)
}
if response.Usage.TotalTokens != 10 || extractText(response.Result) != "gemini ok" {
@@ -290,9 +292,10 @@ func TestVolcesClientImageEditUsesGenerationEndpoint(t *testing.T) {
"image": "https://example.com/source.png",
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "doubao-seedream-4-0-250828",
Credentials: map[string]any{"apiKey": "volces-key"},
BaseURL: server.URL,
ModelName: "豆包Seedream-4.0",
ProviderModelName: "doubao-seedream-4-0-250828",
Credentials: map[string]any{"apiKey": "volces-key"},
Capabilities: map[string]any{
"image_edit": map[string]any{"output_multiple_images": true},
},
@@ -366,9 +369,10 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
"aspect_ratio": "16:9",
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "doubao-seedance-2-0-260128",
Credentials: map[string]any{"apiKey": "volces-key"},
BaseURL: server.URL,
ModelName: "豆包Seedance-2.0",
ProviderModelName: "doubao-seedance-2-0-260128",
Credentials: map[string]any{"apiKey": "volces-key"},
PlatformConfig: map[string]any{
"volcesPollIntervalMs": 100,
"volcesPollTimeoutSeconds": 1,
+1 -1
View File
@@ -22,7 +22,7 @@ func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error
}
body := geminiBody(request)
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, geminiURL(request.Candidate.BaseURL, request.Candidate.ModelName, apiKey), bytes.NewReader(raw))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, geminiURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey), bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
return Response{}, &ClientError{Code: "unsupported_kind", Message: "unsupported openai request kind", Retryable: false}
}
body := cloneBody(request.Body)
body["model"] = request.Candidate.ModelName
body["model"] = upstreamModelName(request.Candidate)
stream := request.Stream || boolValue(body, "stream")
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(request.Candidate.BaseURL, endpoint), bytes.NewReader(raw))
+8
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"strings"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
@@ -72,6 +73,13 @@ func IsRetryable(err error) bool {
return errors.As(err, &clientErr) && clientErr.Retryable
}
func upstreamModelName(candidate store.RuntimeModelCandidate) string {
if name := strings.TrimSpace(candidate.ProviderModelName); name != "" {
return name
}
return candidate.ModelName
}
func ErrorCode(err error) string {
var clientErr *ClientError
if errors.As(err, &clientErr) && clientErr.Code != "" {
+3 -3
View File
@@ -177,7 +177,7 @@ func (c VolcesClient) getJSON(ctx context.Context, baseURL string, path string,
func volcesImageBody(request Request) map[string]any {
body := cleanProviderBody(request.Body)
body["model"] = request.Candidate.ModelName
body["model"] = upstreamModelName(request.Candidate)
if _, ok := body["watermark"]; !ok {
body["watermark"] = false
}
@@ -200,7 +200,7 @@ func volcesImageBody(request Request) map[string]any {
func volcesVideoBody(request Request) map[string]any {
body := cleanProviderBody(request.Body)
body["model"] = request.Candidate.ModelName
body["model"] = upstreamModelName(request.Candidate)
content := contentItems(body["content"])
if len(content) == 0 {
content = buildVolcesContentFromBody(body)
@@ -515,7 +515,7 @@ func volcesVideoSuccessResult(request Request, upstreamTaskID string, raw map[st
"id": upstreamTaskID,
"object": "video.generation",
"created": created,
"model": request.Candidate.ModelName,
"model": upstreamModelName(request.Candidate),
"status": "succeeded",
"upstream_task_id": upstreamTaskID,
"data": data,