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
This commit is contained in:
程序员阿江(Relakkes) 2026-06-01 20:03:40 +08:00
parent c793eefee9
commit 264b24a462
3 changed files with 121 additions and 3 deletions

View File

@ -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<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
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<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
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<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
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'),

View File

@ -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<Respon
)
}
body = ensureClaudeCodeAttribution(body)
body = ensureClaudeCodeAttribution({
...body,
model: normalizeModelStringForAPI(body.model),
})
const isStream = body.stream === true
const baseUrl = config.baseUrl.replace(/\/+$/, '')

View File

@ -41,6 +41,7 @@ import {
loadNetworkSettings,
type NetworkSettings,
} from './networkSettings.js'
import { normalizeModelStringForAPI } from '../../utils/model/model.js'
import type {
SavedProvider,
ProvidersIndex,
@ -445,11 +446,12 @@ export class ProviderService {
const format: ApiFormat = input.apiFormat ?? 'anthropic'
const authStrategy = input.authStrategy ?? 'api_key'
const base = input.baseUrl.replace(/\/+$/, '')
const modelId = normalizeModelStringForAPI(input.modelId)
const networkSettings = await loadNetworkSettings()
// ── Step 1: Basic connectivity ───────────────────────────
// Directly call the upstream API to verify URL, key, and model.
const step1 = await this.testConnectivity(base, input.apiKey, input.modelId, format, authStrategy, networkSettings)
const step1 = await this.testConnectivity(base, input.apiKey, modelId, format, authStrategy, networkSettings)
// If connectivity failed, no point running step 2
if (!step1.success) {
@ -463,7 +465,7 @@ export class ProviderService {
// ── Step 2: Full proxy pipeline ──────────────────────────
// Anthropic request → transform → upstream → transform back → validate
const step2 = await this.testProxyPipeline(base, input.apiKey, input.modelId, format, networkSettings)
const step2 = await this.testProxyPipeline(base, input.apiKey, modelId, format, networkSettings)
return { connectivity: step1, proxy: step2 }
}