diff --git a/internal/adapter/provider/cliproxyapi_grok/adapter_test.go b/internal/adapter/provider/cliproxyapi_grok/adapter_test.go index c2c736a6..78bdc72c 100644 --- a/internal/adapter/provider/cliproxyapi_grok/adapter_test.go +++ b/internal/adapter/provider/cliproxyapi_grok/adapter_test.go @@ -3,6 +3,7 @@ package cliproxyapi_grok import ( "context" "errors" + "io" "net/http" "net/http/httptest" "strings" @@ -159,6 +160,81 @@ func TestEnsureOpenAIStreamFinishBeforeDoneDoesNotDuplicateFinishReason(t *testi } } +func TestExecuteOpenAIChatUsesXAIResponsesUpstreamAndReturnsOpenAIShape(t *testing.T) { + var gotPath string + var gotAuth string + var gotAccept string + var gotBody []byte + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotAuth = r.Header.Get("Authorization") + gotAccept = r.Header.Get("Accept") + var err error + gotBody, err = io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read upstream body: %v", err) + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte(`data: {"type":"response.completed","response":{"id":"resp_grok_1","object":"response","created_at":0,"status":"completed","model":"grok-4","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"mock-grok-ok via /responses"}]}],"usage":{"input_tokens":3,"output_tokens":4,"total_tokens":7}}}` + "\n\n")) + })) + defer upstream.Close() + + provider := &domain.Provider{ + ID: 42, + Type: "grok", + Name: "Grok Test", + Config: &domain.ProviderConfig{Grok: &domain.ProviderConfigGrok{ + Type: "xai", + AuthKind: "oauth", + AccessToken: "access-token", + RefreshToken: "refresh-token", + BaseURL: upstream.URL, + }}, + } + adapter, err := NewAdapter(provider) + if err != nil { + t.Fatalf("NewAdapter() error = %v", err) + } + grok := adapter.(*CLIProxyAPIGrokAdapter) + + body := []byte(`{"model":"grok-4","messages":[{"role":"user","content":"say ok"}]}`) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(string(body))) + rec := httptest.NewRecorder() + c := flow.NewCtx(rec, req) + c.Set(flow.KeyClientType, domain.ClientTypeOpenAI) + c.Set(flow.KeyRequestBody, body) + c.Set(flow.KeyRequestModel, "grok-4") + c.Set(flow.KeyMappedModel, "grok-4") + c.Set(flow.KeyIsStream, false) + c.Set(flow.KeyRequestURI, "/v1/chat/completions") + + if err := grok.Execute(c, provider); err != nil { + t.Fatalf("Execute() error = %v", err) + } + got := rec.Body.String() + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, got) + } + if gotPath != "/responses" { + t.Fatalf("upstream path = %q, want /responses", gotPath) + } + if gotAuth != "Bearer access-token" { + t.Fatalf("Authorization = %q, want Bearer access-token", gotAuth) + } + if gotAccept != "text/event-stream" { + t.Fatalf("Accept = %q, want text/event-stream", gotAccept) + } + if strings.Contains(string(gotBody), "chat/completions") { + t.Fatalf("upstream body leaked client chat endpoint: %s", string(gotBody)) + } + if !strings.Contains(string(gotBody), `"model":"grok-4"`) || !strings.Contains(string(gotBody), `"stream":true`) { + t.Fatalf("upstream body was not shaped as xAI Responses payload: %s", string(gotBody)) + } + if !strings.Contains(got, `"object":"chat.completion"`) || !strings.Contains(got, "mock-grok-ok via /responses") { + t.Fatalf("client-visible OpenAI response missing translated content: %s", got) + } +} + func TestExecuteStreamReturnsClientVisibleGrokContentAndFinishReason(t *testing.T) { provider := &domain.Provider{ ID: 42, diff --git a/internal/handler/test_field_model_benchmark.go b/internal/handler/test_field_model_benchmark.go index 80a24624..c978f2e8 100644 --- a/internal/handler/test_field_model_benchmark.go +++ b/internal/handler/test_field_model_benchmark.go @@ -24,7 +24,7 @@ const ( testFieldDefaultConcurrency = 4 testFieldMaxConcurrency = 10 testFieldDefaultTimeout = 30 * time.Second - testFieldMaxModelsPerProv = 50 + testFieldMaxModelsPerProv = 500 testFieldModelCacheTTL = 2 * time.Minute testFieldResultCacheTTL = 5 * time.Minute testFieldFinishedJobRetention = 10 * time.Minute @@ -360,10 +360,7 @@ func (h *AdminHandler) buildTestFieldBenchmarkTargets(ctx context.Context, tenan providerSummaries = append(providerSummaries, summary) continue } - models := modelsResult.Models - if len(models) > minModels { - models = models[:minModels] - } + models := limitTestFieldBenchmarkModels(modelsResult.Models, minModels) summary.Available = true summary.ModelCount = len(modelsResult.Models) summary.TestedCount = len(models) @@ -376,6 +373,13 @@ func (h *AdminHandler) buildTestFieldBenchmarkTargets(ctx context.Context, tenan return providerSummaries, targets } +func limitTestFieldBenchmarkModels(models []string, minModels int) []string { + if minModels <= 0 || len(models) <= minModels { + return models + } + return models[:minModels] +} + func (h *AdminHandler) fetchTestFieldRuntimeModels(ctx context.Context, provider *domain.Provider, reuseCache bool) (providerRuntimeModelsResult, bool) { cacheKey := testFieldProviderCacheKey(provider) if reuseCache { diff --git a/internal/handler/test_field_model_benchmark_test.go b/internal/handler/test_field_model_benchmark_test.go index 3e9156d6..4da01802 100644 --- a/internal/handler/test_field_model_benchmark_test.go +++ b/internal/handler/test_field_model_benchmark_test.go @@ -108,6 +108,38 @@ func TestRunTestFieldBenchmarkTargetsReportsIncrementalCachedResults(t *testing. } } +func TestNormalizeTestFieldBenchmarkRequestAllowsTwoHundredModels(t *testing.T) { + _, _, _, minModels, err := normalizeTestFieldBenchmarkRequest(TestFieldModelBenchmarkRequest{ + ProviderIDs: []uint64{42}, + MinModelsPerProvider: 200, + }) + if err != nil { + t.Fatalf("normalizeTestFieldBenchmarkRequest() error = %v", err) + } + if minModels != 200 { + t.Fatalf("minModels = %d, want 200", minModels) + } +} + +func TestLimitTestFieldBenchmarkModelsUsesAllDiscoveredModelsBelowRequestedMinimum(t *testing.T) { + models := make([]string, 102) + for i := range models { + models[i] = "model" + } + limited := limitTestFieldBenchmarkModels(models, 200) + if len(limited) != 102 { + t.Fatalf("planned models = %d, want all 102 discovered models when requested minimum is 200", len(limited)) + } +} + +func TestLimitTestFieldBenchmarkModelsCapsAboveRequestedMinimum(t *testing.T) { + models := make([]string, 250) + limited := limitTestFieldBenchmarkModels(models, 200) + if len(limited) != 200 { + t.Fatalf("planned models = %d, want requested minimum 200", len(limited)) + } +} + func TestTestFieldOpenAICompatibleEndpointRejectsUnsupportedProvider(t *testing.T) { _, _, ok, errText := testFieldOpenAICompatibleEndpoint(&domain.Provider{Type: "claude", Config: &domain.ProviderConfig{}}, "http://maxx.test") if ok || errText == "" { diff --git a/web/e2e/test-field-grok-benchmark-regression.spec.ts b/web/e2e/test-field-grok-benchmark-regression.spec.ts index f3474ac4..6d33514a 100644 --- a/web/e2e/test-field-grok-benchmark-regression.spec.ts +++ b/web/e2e/test-field-grok-benchmark-regression.spec.ts @@ -102,7 +102,7 @@ async function installTestFieldMocks(page: Page, calls: Call[]) { providerIDs: [42], concurrency: 1, timeoutMs: 5000, - minModelsPerProvider: 2, + minModelsPerProvider: 200, reuseCachedModelLists: true, reuseCachedResults: true, }); @@ -116,7 +116,7 @@ async function installTestFieldMocks(page: Page, calls: Call[]) { prompt: '端到端回归:请返回 mock-grok-ok', concurrency: 1, timeoutMs: 5000, - minModelsPerProvider: 2, + minModelsPerProvider: 200, startedAt: now, finishedAt: new Date(Date.now() + 12).toISOString(), providers: [ @@ -125,8 +125,8 @@ async function installTestFieldMocks(page: Page, calls: Call[]) { providerName: 'Mock Grok OAuth Provider', providerType: 'grok', available: true, - modelCount: 8, - testedCount: 2, + modelCount: 102, + testedCount: 102, cachedModels: false, }, ], @@ -139,7 +139,7 @@ async function installTestFieldMocks(page: Page, calls: Call[]) { available: true, durationMs: 37, statusCode: 200, - response: 'mock-grok-ok via /provider/42/v1/chat/completions', + response: 'mock-grok-ok: client /provider/42/v1/chat/completions -> xAI /responses', startedAt: now, finishedAt: new Date(Date.now() + 37).toISOString(), }, @@ -151,13 +151,13 @@ async function installTestFieldMocks(page: Page, calls: Call[]) { available: true, durationMs: 42, statusCode: 200, - response: 'mock-grok-latest-ok', + response: 'mock-grok-latest-ok via xAI /responses', startedAt: now, finishedAt: new Date(Date.now() + 42).toISOString(), }, ], - totalTargets: 2, - completedTargets: 2, + totalTargets: 102, + completedTargets: 102, cachedResultCount: 0, }, 200, @@ -217,19 +217,20 @@ test('test field runs a Grok provider benchmark without blank-screening', async await page.getByLabel(/^测试问题$/).fill('端到端回归:请返回 mock-grok-ok'); await page.getByLabel(/^并发数$/).fill('1'); await page.getByLabel(/^单模型超时 ms$/).fill('5000'); - await page.getByLabel(/^每个提供商最少测试模型数$/).fill('2'); + await page.getByLabel(/^每个提供商最少测试模型数$/).fill('200'); await attachMockEvidence(page, calls); await page.screenshot({ path: testInfo.outputPath('01-before-run-grok-provider-selected.png'), fullPage: true }); await page.getByRole('button', { name: /Run|运行|开始测试|开始/ }).click(); - await expect(page.getByText(/mock-grok-ok via \/provider\/42\/v1\/chat\/completions/)).toBeVisible({ + await expect(page.getByText(/mock-grok-ok: client \/provider\/42\/v1\/chat\/completions -> xAI \/responses/)).toBeVisible({ timeout: 10_000, }); await expect(page.getByRole('cell', { name: 'grok-4', exact: true })).toBeVisible(); await expect(page.getByRole('cell', { name: 'grok-latest', exact: true })).toBeVisible(); - await expect(page.getByText(/2\/2|completed: 2|已完成/)).toBeVisible(); + await expect(page.getByText(/102\/102|completed: 102|已完成/)).toBeVisible(); + await expect(page.getByText(/计划测试 102 \/ 发现 102 个模型/)).toBeVisible(); expect(calls.some((call) => call.path === '/api/admin/test-field/model-benchmark-jobs' && call.method === 'POST')).toBe(true); expect(calls.some((call) => call.path === '/api/admin/test-field/model-benchmark-jobs/mock-grok-job-1' && call.method === 'GET')).toBe(true); diff --git a/web/src/pages/test-field/index.tsx b/web/src/pages/test-field/index.tsx index 6f7c9881..aa5869d5 100644 --- a/web/src/pages/test-field/index.tsx +++ b/web/src/pages/test-field/index.tsx @@ -380,7 +380,7 @@ export function TestFieldPage() { id="test-field-max-models" type="number" min={1} - max={50} + max={500} value={minModelsPerProvider} onChange={(event) => setMinModelsPerProvider(Number(event.target.value) || 20)} disabled={isRunning}