mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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:
parent
c793eefee9
commit
264b24a462
@ -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'),
|
||||
|
||||
@ -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(/\/+$/, '')
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user