From 264b24a462c60fae77501a0894beeba61e99c087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 1 Jun 2026 20:03:40 +0800 Subject: [PATCH] fix: make provider tests match chat model normalization (#620) Provider connectivity checks and proxy validation are API boundaries, but they kept the user-facing context suffix in the model id while the chat runtime already strips it. Normalize these paths so MiMo-style models with [1m] test the same model that sessions send. Constraint: [1m]/[2m] are client-side context markers and upstream APIs may reject them Rejected: Strip suffixes when saving provider config | would change the user-visible model selection instead of only the API boundary Confidence: high Scope-risk: narrow Directive: Keep all provider API boundaries aligned with normalizeModelStringForAPI before sending requests upstream Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "normalizes context-window suffixes" Tested: bun test src/server/__tests__/providers.test.ts Tested: bun run check:server Tested: git diff --check Not-tested: Live MiMo API call; no provider credential was used Related: #620 --- src/server/__tests__/providers.test.ts | 112 +++++++++++++++++++++++++ src/server/proxy/handler.ts | 6 +- src/server/services/providerService.ts | 6 +- 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index 6ed3ecae..7381db61 100644 --- a/src/server/__tests__/providers.test.ts +++ b/src/server/__tests__/providers.test.ts @@ -1054,6 +1054,47 @@ describe('ProviderService', () => { globalThis.fetch = originalFetch } }) + + test('normalizes context-window suffixes before forwarding OpenAI Chat proxy requests', async () => { + const originalFetch = globalThis.fetch + const calls: Array<{ body: Record }> = [] + globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => { + calls.push({ body: JSON.parse(String(init?.body)) as Record }) + return new Response(JSON.stringify({ + id: 'chatcmpl-1', + object: 'chat.completion', + created: 0, + model: 'mimo-v2.5-pro', + choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }) as typeof fetch + + try { + const svc = new ProviderService() + const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' })) + await svc.activateProvider(provider.id) + + const req = new Request('http://localhost:3456/proxy/v1/messages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'mimo-v2.5-pro[1m]', + max_tokens: 64, + messages: [{ role: 'user', content: 'hello from proxy' }], + }), + }) + + const res = await handleProxyRequest(req, new URL(req.url)) + expect(res.status).toBe(200) + expect(calls[0].body.model).toBe('mimo-v2.5-pro') + } finally { + globalThis.fetch = originalFetch + } + }) }) describe('testProvider', () => { @@ -1151,6 +1192,77 @@ describe('ProviderService', () => { } }) + test('normalizes context-window suffixes for Anthropic-compatible connectivity tests', async () => { + const originalFetch = globalThis.fetch + const calls: Array<{ body: Record }> = [] + globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => { + calls.push({ body: JSON.parse(String(init?.body)) as Record }) + return new Response(JSON.stringify({ + type: 'message', + model: 'mimo-v2.5-pro', + content: [], + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }) as typeof fetch + + try { + const svc = new ProviderService() + const result = await svc.testProviderConfig({ + baseUrl: 'https://api.xiaomimimo.com/anthropic', + apiKey: 'sk-api', + modelId: 'mimo-v2.5-pro[1m]', + authStrategy: 'auth_token', + apiFormat: 'anthropic', + }) + + expect(result.connectivity.success).toBe(true) + expect(result.connectivity.modelUsed).toBe('mimo-v2.5-pro') + expect(calls[0].body.model).toBe('mimo-v2.5-pro') + } finally { + globalThis.fetch = originalFetch + } + }) + + test('normalizes context-window suffixes for provider proxy pipeline tests', async () => { + const originalFetch = globalThis.fetch + const calls: Array<{ body: Record }> = [] + globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => { + calls.push({ body: JSON.parse(String(init?.body)) as Record }) + return new Response(JSON.stringify({ + id: 'chatcmpl-1', + object: 'chat.completion', + created: 0, + model: 'mimo-v2.5-pro', + choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }) as typeof fetch + + try { + const svc = new ProviderService() + const result = await svc.testProviderConfig({ + baseUrl: 'https://api.example.com', + apiKey: 'sk-api', + modelId: 'mimo-v2.5-pro[1m]', + authStrategy: 'api_key', + apiFormat: 'openai_chat', + }) + + expect(result.connectivity.success).toBe(true) + expect(result.proxy?.success).toBe(true) + expect(result.connectivity.modelUsed).toBe('mimo-v2.5-pro') + expect(result.proxy?.modelUsed).toBe('mimo-v2.5-pro') + expect(calls.map((call) => call.body.model)).toEqual(['mimo-v2.5-pro', 'mimo-v2.5-pro']) + } finally { + globalThis.fetch = originalFetch + } + }) + test('should use configured network timeout for provider tests', async () => { await fs.writeFile( path.join(tmpDir, 'settings.json'), diff --git a/src/server/proxy/handler.ts b/src/server/proxy/handler.ts index 5a8112de..2a1583ac 100644 --- a/src/server/proxy/handler.ts +++ b/src/server/proxy/handler.ts @@ -21,6 +21,7 @@ import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStr import type { AnthropicRequest } from './transform/types.js' import { getProxyFetchOptions } from '../../utils/proxy.js' import { getManualNetworkProxyUrl, loadNetworkSettings } from '../services/networkSettings.js' +import { normalizeModelStringForAPI } from '../../utils/model/model.js' const providerService = new ProviderService() @@ -127,7 +128,10 @@ export async function handleProxyRequest(req: Request, url: URL): Promise