mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(ai): preserve system role for Qwen chat completions
This commit is contained in:
1 parent
14c9defdf1
commit
2e385bd401
3 files changed
+62
-4
No files matched your search
@@ -121,7 +121,7 @@ describe('buildPiModel', () => {
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
})
|
||||
assert.equal(model.reasoning, true)
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen' })
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen', supportsDeveloperRole: false })
|
||||
})
|
||||
|
||||
it('auto-infers reasoning=true and compat.thinkingFormat=qwen for custom self-hosted qwen3 model', () => {
|
||||
@@ -131,7 +131,7 @@ describe('buildPiModel', () => {
|
||||
{ findModelFn: () => null }
|
||||
)
|
||||
assert.equal(model.reasoning, true)
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen' })
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen', supportsDeveloperRole: false })
|
||||
})
|
||||
|
||||
it('auto-infers reasoning=true for custom model saved with only capabilities:["chat"]', () => {
|
||||
@@ -155,7 +155,7 @@ describe('buildPiModel', () => {
|
||||
}
|
||||
)
|
||||
assert.equal(model.reasoning, true)
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen' })
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen', supportsDeveloperRole: false })
|
||||
})
|
||||
|
||||
it('auto-infers reasoning=false and compat=undefined for non-reasoning Anthropic model', () => {
|
||||
|
||||
@@ -156,6 +156,10 @@ export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOption
|
||||
: baseUrl
|
||||
|
||||
const { reasoning, compat, thinkingLevelMap } = inferReasoning(config.provider, modelId, modelDef)
|
||||
// Qwen chat-completions endpoints reject developer messages, even for reasoning models.
|
||||
// Override Pi's default for official and custom-hosted Qwen models without disabling thinking.
|
||||
const requiresSystemRole =
|
||||
apiFormat === 'openai-completions' && (config.provider.toLowerCase() === 'qwen' || /\bqwen|qwq/i.test(modelId))
|
||||
|
||||
return {
|
||||
id: modelId,
|
||||
@@ -171,6 +175,6 @@ export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOption
|
||||
contextWindow,
|
||||
// Reasoning and visible text share the output budget on compatible APIs.
|
||||
maxTokens: config.maxTokens ?? (reasoning ? 16_384 : 4096),
|
||||
compat,
|
||||
compat: requiresSystemRole ? { ...compat, supportsDeveloperRole: false } : compat,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,62 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import type { AIServiceConfig } from '../../ai'
|
||||
import { completeSimple } from '../../ai/pi-runtime'
|
||||
import { createChatTopicModelClient } from './model-client'
|
||||
|
||||
// Prevent topic requests from failing with HTTP 400 on Qwen endpoints that reject the developer role.
|
||||
for (const { provider, model, expectedRole } of [
|
||||
{ provider: 'qwen', model: 'qwen3.8', expectedRole: 'system' },
|
||||
{ provider: 'openai-compatible', model: 'qwen3.8', expectedRole: 'system' },
|
||||
{ provider: 'openai-compatible', model: 'Qwen/QwQ-32B', expectedRole: 'system' },
|
||||
{ provider: 'openai', model: 'o3', expectedRole: 'developer' },
|
||||
]) {
|
||||
test(`topic requests preserve the supported prompt role for ${provider}/${model}`, async () => {
|
||||
let messages: Array<{ role: string; content: unknown }> | undefined
|
||||
let reasoning: boolean | undefined
|
||||
const client = createChatTopicModelClient(
|
||||
{
|
||||
id: 'synthetic',
|
||||
name: 'Synthetic',
|
||||
provider,
|
||||
model,
|
||||
baseUrl: 'https://example.invalid/v1',
|
||||
apiKey: 'test-key',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
{
|
||||
completeSimple: (model, context, options) => {
|
||||
reasoning = model.reasoning
|
||||
return completeSimple(model, context, {
|
||||
...options,
|
||||
onPayload: async (payload, model) => {
|
||||
const normalized = (await options?.onPayload?.(payload, model)) ?? payload
|
||||
messages = (normalized as { messages: typeof messages }).messages
|
||||
// Capture the real SDK serialization, then stop before any network request.
|
||||
throw new Error('Captured topic request before network')
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
await assert.rejects(
|
||||
() =>
|
||||
client.complete(
|
||||
{ systemPrompt: 'Synthetic system prompt', userPrompt: 'Synthetic user prompt' },
|
||||
{ signal: new AbortController().signal, sessionId: 'synthetic' }
|
||||
),
|
||||
/Captured topic request before network/
|
||||
)
|
||||
assert.equal(reasoning, true)
|
||||
assert.deepEqual(
|
||||
messages?.map((message) => message.role),
|
||||
[expectedRole, 'user']
|
||||
)
|
||||
assert.equal(messages?.[0].content, 'Synthetic system prompt')
|
||||
})
|
||||
}
|
||||
|
||||
test('topic model calls disable reasoning and constrain the DeepSeek payload', async () => {
|
||||
let capturedOptions: Parameters<typeof import('../../ai').completeSimple>[2]
|
||||
let normalizedPayload: unknown
|
||||
|
||||
Reference in new issue
Block a user