Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -425,5 +425,86 @@ describe('DefaultOpenAICompatibleProvider', () => {
expect(result.max_tokens).toBe(8000); // GPT-4 has 16K limit, min(16K, 8K) = 8K
expect(result).not.toHaveProperty('custom_param');
});

it('mirrors reasoning_content into reasoning for Qwen3 assistant history turns without mutating the source request', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] All 21 existing tests in this file use the 'should ...' convention (e.g., 'should merge extra_body into the request'). The 3 new tests break this pattern with present tense ('mirrors ...', 'does not overwrite ...', 'does not mirror ...'). Consider prefixing with 'should' for consistency:

Suggested change
it('mirrors reasoning_content into reasoning for Qwen3 assistant history turns without mutating the source request', () => {
it('should mirror reasoning_content into reasoning for Qwen3 assistant history turns without mutating the source request', () => {

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'Qwen/Qwen3.6-35B-A3B',
messages: [
{ role: 'user', content: 'First turn' },
{
role: 'assistant',
content: 'Visible answer',
reasoning_content: 'Preserved chain of thought',
} as OpenAI.Chat.ChatCompletionAssistantMessageParam & {
reasoning_content: string;
reasoning?: string;
},
{ role: 'user', content: 'Second turn' },
],
};

const result = provider.buildRequest(originalRequest, 'prompt-id');
const assistant = result.messages?.[1] as {
reasoning_content?: string;
reasoning?: string;
};

expect(assistant.reasoning_content).toBe('Preserved chain of thought');
expect(assistant.reasoning).toBe('Preserved chain of thought');
expect(
(originalRequest.messages[1] as { reasoning?: string }).reasoning,
).toBeUndefined();
});

it('does not overwrite an explicit reasoning field on Qwen3 assistant history turns', () => {
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'Qwen3-32B',
messages: [
{
role: 'assistant',
content: 'Visible answer',
reasoning_content: 'Legacy reasoning field',
reasoning: 'Canonical reasoning field',
} as OpenAI.Chat.ChatCompletionAssistantMessageParam & {
reasoning_content: string;
reasoning: string;
},
],
};

const result = provider.buildRequest(originalRequest, 'prompt-id');
const assistant = result.messages?.[0] as {
reasoning_content?: string;
reasoning?: string;
};

expect(assistant.reasoning).toBe('Canonical reasoning field');
expect(assistant.reasoning_content).toBe('Legacy reasoning field');
});

it('does not mirror reasoning_content for non-Qwen3 OpenAI-compatible models', () => {
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'gpt-4o',
messages: [
{
role: 'assistant',
content: 'Visible answer',
reasoning_content: 'Preserved chain of thought',
} as OpenAI.Chat.ChatCompletionAssistantMessageParam & {
reasoning_content: string;
reasoning?: string;
},
],
};

const result = provider.buildRequest(originalRequest, 'prompt-id');
const assistant = result.messages?.[0] as {
reasoning_content?: string;
reasoning?: string;
};

expect(assistant.reasoning_content).toBe('Preserved chain of thought');
expect(assistant.reasoning).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ import {
hasExplicitOutputLimit,
} from '../../tokenLimits.js';

type AssistantMessageWithReasoningFields =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] AssistantMessageWithReasoningFields duplicates the existing ExtendedChatCompletionAssistantMessageParam from converter.ts:41-44, which already declares reasoning_content?: string | null on the same base type. deepseek.ts:10 already imports and extends that interface. Two parallel type definitions for the same concept will drift over time.

Suggested change
type AssistantMessageWithReasoningFields =
type AssistantMessageWithReasoningFields =
ExtendedChatCompletionAssistantMessageParam & {
reasoning?: string | null;
};

This requires importing the existing type:

import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js';

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

OpenAI.Chat.ChatCompletionAssistantMessageParam & {
reasoning_content?: string | null;
reasoning?: string | null;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The functions here describe what they do but not why. Compare with deepseek.ts:220-224 which explains the DeepSeek reasoning_content contract with issue links. Future maintainers will wonder: which API layers require reasoning vs reasoning_content? Is this a vLLM requirement? When can this code be safely removed?

Consider adding a brief comment explaining that newer vLLM builds consume message.reasoning and may drop reasoning_content before the chat template renders, and linking to issue #4285.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

function shouldMirrorReasoningContentForQwen3(model: string): boolean {
return model.toLowerCase().includes('qwen3');
}

function mirrorReasoningContentToReasoning(
message: OpenAI.Chat.ChatCompletionMessageParam,
): OpenAI.Chat.ChatCompletionMessageParam {
if (message.role !== 'assistant') {
return message;
}

const assistant = message as AssistantMessageWithReasoningFields;
if (
typeof assistant.reasoning_content !== 'string' ||
assistant.reasoning_content.length === 0 ||
typeof assistant.reasoning === 'string'
) {
return message;
}

return {
...assistant,
reasoning: assistant.reasoning_content,
} as OpenAI.Chat.ChatCompletionMessageParam;
}

/**
* Default provider for standard OpenAI-compatible APIs
*/
Expand Down Expand Up @@ -75,9 +107,13 @@ export class DefaultOpenAICompatibleProvider
// Apply output token limits to ensure max_tokens is set appropriately
// This prevents occupying too much context window with output reservation
const requestWithTokenLimits = this.applyOutputTokenLimit(request);
const messages = shouldMirrorReasoningContentForQwen3(request.model)
? requestWithTokenLimits.messages.map(mirrorReasoningContentToReasoning)
: requestWithTokenLimits.messages;

return {
...requestWithTokenLimits,
messages,
...(extraBody ? extraBody : {}),
};
}
Expand Down
Loading