mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
revert(core): revert GLM tagged thinking parsing (#6248)
This commit is contained in:
parent
183ad54b11
commit
e3076e2990
8 changed files with 46 additions and 638 deletions
|
|
@ -3816,97 +3816,6 @@ describe('OpenAIContentConverter', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should convert GLM inline <think> content to thought parts', () => {
|
||||
const response = converter.convertOpenAIResponseToGemini(
|
||||
{
|
||||
object: 'chat.completion',
|
||||
id: 'chatcmpl-glm-1',
|
||||
created: 123,
|
||||
model: 'glm-5.2',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: '',
|
||||
content:
|
||||
'<think>The user is asking a simple logic puzzle.</think>LEAK_CHECK_FINAL: prize is in B',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
} as unknown as OpenAI.Chat.ChatCompletion,
|
||||
withTaggedThinkingOptions(),
|
||||
);
|
||||
|
||||
expect(response.candidates?.[0]?.content?.parts).toEqual([
|
||||
{
|
||||
text: 'The user is asking a simple logic puzzle.',
|
||||
thought: true,
|
||||
},
|
||||
{ text: 'LEAK_CHECK_FINAL: prize is in B' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve reasoning_content when tagged parsing finds no thinking tags', () => {
|
||||
const response = converter.convertOpenAIResponseToGemini(
|
||||
{
|
||||
object: 'chat.completion',
|
||||
id: 'chatcmpl-glm-2',
|
||||
created: 123,
|
||||
model: 'glm-5.2',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'separate reasoning channel',
|
||||
content: 'final answer',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
} as unknown as OpenAI.Chat.ChatCompletion,
|
||||
withTaggedThinkingOptions(),
|
||||
);
|
||||
|
||||
expect(response.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'separate reasoning channel', thought: true },
|
||||
{ text: 'final answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not duplicate reasoning_content when content already has tagged thinking', () => {
|
||||
const response = converter.convertOpenAIResponseToGemini(
|
||||
{
|
||||
object: 'chat.completion',
|
||||
id: 'chatcmpl-glm-3',
|
||||
created: 123,
|
||||
model: 'glm-5.2',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: 'duplicate reasoning channel',
|
||||
content: '<think>tagged reasoning</think>final answer',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
} as unknown as OpenAI.Chat.ChatCompletion,
|
||||
withTaggedThinkingOptions(),
|
||||
);
|
||||
|
||||
expect(response.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'tagged reasoning', thought: true },
|
||||
{ text: 'final answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve ordering around <thinking> blocks', () => {
|
||||
const response = converter.convertOpenAIResponseToGemini(
|
||||
{
|
||||
|
|
@ -4085,335 +3994,6 @@ describe('OpenAIContentConverter', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should suppress reasoning_content when the same streaming chunk has tagged thinking content', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const chunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-dual-tagged',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: 'duplicate reasoning channel',
|
||||
content: '<think>tagged reasoning</think>final answer',
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(chunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'tagged reasoning', thought: true },
|
||||
{ text: 'final answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should suppress late reasoning_content after streaming tagged thinking content', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-late-reasoning-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: '<think>tagged reasoning</think>' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const secondChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-late-reasoning-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: 'late reasoning' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'tagged reasoning', thought: true },
|
||||
]);
|
||||
expect(secondChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
});
|
||||
|
||||
it('should suppress buffered reasoning_content when later streaming content has tagged thinking', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-reasoning-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: 'duplicate reasoning channel' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const secondChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-reasoning-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: '<think>tagged reasoning</think>' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const finalChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-reasoning-3',
|
||||
created: 458,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: 'final answer' },
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
expect(secondChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'tagged reasoning', thought: true },
|
||||
]);
|
||||
expect(finalChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'final answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flush buffered content before later tagged thinking content', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-content-before-tag-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: 'duplicate reasoning channel',
|
||||
content: 'early visible ',
|
||||
},
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const secondChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-content-before-tag-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: '<think>tagged reasoning</think>final answer' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
expect(secondChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'early visible ' },
|
||||
{ text: 'tagged reasoning', thought: true },
|
||||
{ text: 'final answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flush buffered content before current content when reasoning flushes on finish', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-content-order-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: 'step 1',
|
||||
content: 'hello ',
|
||||
},
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const finalChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-buffered-content-order-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: 'world' },
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
expect(finalChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'step 1', thought: true },
|
||||
{ text: 'hello ' },
|
||||
{ text: 'world' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flush buffered reasoning_content when tagged streaming content has no thinking tags', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-reasoning-only-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: 'separate reasoning channel',
|
||||
content: 'final ',
|
||||
},
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const finalChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-reasoning-only-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: 'answer' },
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
expect(finalChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'separate reasoning channel', thought: true },
|
||||
{ text: 'final ' },
|
||||
{ text: 'answer' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flush reasoning-only chunks when tagged streaming content has no thinking tags', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
const firstChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-reasoning-only-no-content-1',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: 'step 1' },
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
const finalChunk = converter.convertOpenAIChunkToGemini(
|
||||
{
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'chunk-glm-reasoning-only-no-content-2',
|
||||
created: 457,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: 'stop',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
model: 'glm-5.2',
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk,
|
||||
context,
|
||||
);
|
||||
|
||||
expect(firstChunk.candidates?.[0]?.content?.parts).toEqual([]);
|
||||
expect(finalChunk.candidates?.[0]?.content?.parts).toEqual([
|
||||
{ text: 'step 1', thought: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should flush unclosed streaming thinking content on finish', () => {
|
||||
const context = withTaggedThinkingStreamParser();
|
||||
|
||||
|
|
|
|||
|
|
@ -1081,10 +1081,6 @@ function convertOpenAITextToParts(
|
|||
return parseTaggedThinkingText(text);
|
||||
}
|
||||
|
||||
function hasThoughtPart(parts: Part[]): boolean {
|
||||
return parts.some((part) => part.thought === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAI response to Gemini format.
|
||||
*/
|
||||
|
|
@ -1097,23 +1093,26 @@ export function convertOpenAIResponseToGemini(
|
|||
|
||||
if (choice) {
|
||||
const parts: Part[] = [];
|
||||
const textParts = choice.message.content
|
||||
? convertOpenAITextToParts(choice.message.content, requestContext)
|
||||
: [];
|
||||
|
||||
// Handle reasoning content (thoughts).
|
||||
// Tagged thinking providers may put thoughts in content, while other
|
||||
// responses still use reasoning_content. Preserve the separate reasoning
|
||||
// channel unless content parsing already produced thought parts.
|
||||
const reasoningText =
|
||||
(choice.message as ExtendedCompletionMessage).reasoning_content ??
|
||||
(choice.message as ExtendedCompletionMessage).reasoning;
|
||||
if (reasoningText && !hasThoughtPart(textParts)) {
|
||||
parts.push({ text: reasoningText, thought: true });
|
||||
// When taggedThinkingTags is enabled, thought content is already
|
||||
// extracted from the text content via convertOpenAITextToParts.
|
||||
// Skip reasoning_content extraction to avoid duplicating thought parts.
|
||||
if (!requestContext.responseParsingOptions?.taggedThinkingTags) {
|
||||
const reasoningText =
|
||||
(choice.message as ExtendedCompletionMessage).reasoning_content ??
|
||||
(choice.message as ExtendedCompletionMessage).reasoning;
|
||||
if (reasoningText) {
|
||||
parts.push({ text: reasoningText, thought: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text content
|
||||
parts.push(...textParts);
|
||||
if (choice.message.content) {
|
||||
parts.push(
|
||||
...convertOpenAITextToParts(choice.message.content, requestContext),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
if (choice.message.tool_calls) {
|
||||
|
|
@ -1225,12 +1224,29 @@ export function convertOpenAIChunkToGemini(
|
|||
|
||||
if (choice) {
|
||||
const parts: Part[] = [];
|
||||
let contentParts: Part[] = [];
|
||||
|
||||
// Handle reasoning content (thoughts).
|
||||
const reasoningText =
|
||||
(choice.delta as ExtendedCompletionChunkDelta)?.reasoning_content ??
|
||||
(choice.delta as ExtendedCompletionChunkDelta)?.reasoning;
|
||||
// When taggedThinkingTags is enabled, thought content is already
|
||||
// extracted from the text content via convertOpenAITextToParts.
|
||||
// Skip reasoning_content extraction to avoid duplicating thought parts.
|
||||
if (!requestContext.responseParsingOptions?.taggedThinkingTags) {
|
||||
const reasoningText =
|
||||
(choice.delta as ExtendedCompletionChunkDelta)?.reasoning_content ??
|
||||
(choice.delta as ExtendedCompletionChunkDelta)?.reasoning;
|
||||
if (reasoningText) {
|
||||
const normalizedReasoningText = normalizeStreamingTextDelta(
|
||||
reasoningText,
|
||||
(requestContext.reasoningDeltaState ??= {
|
||||
emittedText: '',
|
||||
emittedLength: 0,
|
||||
cumulativeMode: false,
|
||||
}),
|
||||
);
|
||||
if (normalizedReasoningText) {
|
||||
parts.push({ text: normalizedReasoningText, thought: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text content
|
||||
if (typeof choice.delta?.content === 'string') {
|
||||
|
|
@ -1245,99 +1261,19 @@ export function convertOpenAIChunkToGemini(
|
|||
// Skip empty-string push mid-stream; still call on finish_reason to
|
||||
// flush any buffered tagged-thinking content.
|
||||
if (normalizedContent || choice.finish_reason) {
|
||||
contentParts = convertOpenAITextToParts(
|
||||
normalizedContent,
|
||||
requestContext,
|
||||
Boolean(choice.finish_reason),
|
||||
parts.push(
|
||||
...convertOpenAITextToParts(
|
||||
normalizedContent,
|
||||
requestContext,
|
||||
Boolean(choice.finish_reason),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (choice.finish_reason) {
|
||||
// Flush any buffered tagged-thinking content on stream end
|
||||
contentParts = convertOpenAITextToParts('', requestContext, true);
|
||||
parts.push(...convertOpenAITextToParts('', requestContext, true));
|
||||
}
|
||||
|
||||
if (hasThoughtPart(contentParts)) {
|
||||
requestContext.hasTaggedThinkingThought = true;
|
||||
requestContext.pendingReasoningText = undefined;
|
||||
debugLogger.debug(
|
||||
'convertOpenAIChunkToGemini: tagged thinking content emitted a thought; dropping buffered reasoning',
|
||||
);
|
||||
if (requestContext.pendingContentParts?.length) {
|
||||
debugLogger.debug(
|
||||
`convertOpenAIChunkToGemini: flushing ${requestContext.pendingContentParts.length} buffered content part(s) before tagged content`,
|
||||
);
|
||||
parts.push(...requestContext.pendingContentParts);
|
||||
requestContext.pendingContentParts = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
reasoningText &&
|
||||
(!requestContext.responseParsingOptions?.taggedThinkingTags ||
|
||||
!requestContext.hasTaggedThinkingThought)
|
||||
) {
|
||||
const normalizedReasoningText = normalizeStreamingTextDelta(
|
||||
reasoningText,
|
||||
(requestContext.reasoningDeltaState ??= {
|
||||
emittedText: '',
|
||||
emittedLength: 0,
|
||||
cumulativeMode: false,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
normalizedReasoningText &&
|
||||
!requestContext.responseParsingOptions?.taggedThinkingTags
|
||||
) {
|
||||
parts.push({ text: normalizedReasoningText, thought: true });
|
||||
} else if (
|
||||
normalizedReasoningText &&
|
||||
!requestContext.hasTaggedThinkingThought
|
||||
) {
|
||||
requestContext.pendingReasoningText =
|
||||
(requestContext.pendingReasoningText ?? '') + normalizedReasoningText;
|
||||
debugLogger.debug(
|
||||
`convertOpenAIChunkToGemini: buffered reasoning text (${requestContext.pendingReasoningText.length} chars) for tagged stream`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
requestContext.responseParsingOptions?.taggedThinkingTags &&
|
||||
!requestContext.hasTaggedThinkingThought &&
|
||||
requestContext.pendingReasoningText &&
|
||||
contentParts.length
|
||||
) {
|
||||
requestContext.pendingContentParts = [
|
||||
...(requestContext.pendingContentParts ?? []),
|
||||
...contentParts,
|
||||
];
|
||||
debugLogger.debug(
|
||||
`convertOpenAIChunkToGemini: buffered ${contentParts.length} content part(s) behind pending reasoning`,
|
||||
);
|
||||
contentParts = [];
|
||||
}
|
||||
|
||||
if (
|
||||
choice.finish_reason &&
|
||||
requestContext.responseParsingOptions?.taggedThinkingTags &&
|
||||
!requestContext.hasTaggedThinkingThought &&
|
||||
requestContext.pendingReasoningText
|
||||
) {
|
||||
debugLogger.debug(
|
||||
'convertOpenAIChunkToGemini: flushing buffered reasoning for tagged stream with no tagged thought',
|
||||
);
|
||||
parts.push({ text: requestContext.pendingReasoningText, thought: true });
|
||||
requestContext.pendingReasoningText = undefined;
|
||||
}
|
||||
if (choice.finish_reason && requestContext.pendingContentParts?.length) {
|
||||
debugLogger.debug(
|
||||
`convertOpenAIChunkToGemini: flushing ${requestContext.pendingContentParts.length} buffered content part(s) on stream finish`,
|
||||
);
|
||||
parts.push(...requestContext.pendingContentParts);
|
||||
requestContext.pendingContentParts = undefined;
|
||||
}
|
||||
parts.push(...contentParts);
|
||||
|
||||
// Handle tool calls using the stream-local parser
|
||||
if (choice.delta?.tool_calls) {
|
||||
for (const toolCall of choice.delta.tool_calls) {
|
||||
|
|
|
|||
|
|
@ -268,44 +268,6 @@ describe('ContentGenerationPipeline', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should request provider parsing options for the effective request model', async () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'glm-5.2',
|
||||
contents: [{ parts: [{ text: 'Hello' }], role: 'user' }],
|
||||
};
|
||||
const mockOpenAIResponse = {
|
||||
id: 'response-id',
|
||||
choices: [{ message: { content: 'response' }, finish_reason: 'stop' }],
|
||||
created: Date.now(),
|
||||
model: 'glm-5.2',
|
||||
} as OpenAI.Chat.ChatCompletion;
|
||||
const mockGeminiResponse = new GenerateContentResponse();
|
||||
|
||||
mockProvider.getResponseParsingOptions = vi.fn().mockReturnValue({
|
||||
taggedThinkingTags: true,
|
||||
});
|
||||
(mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue([]);
|
||||
(mockConverter.convertOpenAIResponseToGemini as Mock).mockReturnValue(
|
||||
mockGeminiResponse,
|
||||
);
|
||||
(mockClient.chat.completions.create as Mock).mockResolvedValue(
|
||||
mockOpenAIResponse,
|
||||
);
|
||||
|
||||
await pipeline.execute(request, 'prompt-id');
|
||||
|
||||
expect(mockProvider.getResponseParsingOptions).toHaveBeenCalledWith(
|
||||
'glm-5.2',
|
||||
);
|
||||
expect(mockConverter.convertOpenAIResponseToGemini).toHaveBeenCalledWith(
|
||||
mockOpenAIResponse,
|
||||
expect.objectContaining({
|
||||
model: 'glm-5.2',
|
||||
responseParsingOptions: { taggedThinkingTags: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should let provider request context overrides take precedence over content generator config', async () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'test-model',
|
||||
|
|
|
|||
|
|
@ -781,7 +781,7 @@ export class ContentGenerationPipeline {
|
|||
? new StreamingToolCallParser()
|
||||
: undefined;
|
||||
const responseParsingOptions =
|
||||
this.config.provider.getResponseParsingOptions?.(effectiveModel);
|
||||
this.config.provider.getResponseParsingOptions?.();
|
||||
const taggedThinkingParser =
|
||||
isStreaming && responseParsingOptions?.taggedThinkingTags
|
||||
? new TaggedThinkingParser()
|
||||
|
|
|
|||
|
|
@ -108,49 +108,6 @@ describe('DashScopeOpenAICompatibleProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getResponseParsingOptions', () => {
|
||||
it('should enable tagged thinking parsing for GLM models', () => {
|
||||
const glmProvider = new DashScopeOpenAICompatibleProvider(
|
||||
{ ...mockContentGeneratorConfig, model: 'glm-5.2' },
|
||||
mockCliConfig,
|
||||
);
|
||||
|
||||
expect(glmProvider.getResponseParsingOptions()).toEqual({
|
||||
taggedThinkingTags: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match GLM models case-insensitively', () => {
|
||||
const glmProvider = new DashScopeOpenAICompatibleProvider(
|
||||
{ ...mockContentGeneratorConfig, model: 'GLM-5.1' },
|
||||
mockCliConfig,
|
||||
);
|
||||
|
||||
expect(glmProvider.getResponseParsingOptions()).toEqual({
|
||||
taggedThinkingTags: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the request model override when provided', () => {
|
||||
expect(provider.getResponseParsingOptions('glm-5.2')).toEqual({
|
||||
taggedThinkingTags: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should let a non-GLM request model override a configured GLM model', () => {
|
||||
const glmProvider = new DashScopeOpenAICompatibleProvider(
|
||||
{ ...mockContentGeneratorConfig, model: 'glm-5.2' },
|
||||
mockCliConfig,
|
||||
);
|
||||
|
||||
expect(glmProvider.getResponseParsingOptions('qwen-max')).toEqual({});
|
||||
});
|
||||
|
||||
it('should not enable tagged thinking parsing for non-GLM models', () => {
|
||||
expect(provider.getResponseParsingOptions()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDashScopeProvider', () => {
|
||||
it('should return true for QWEN_OAUTH auth type', () => {
|
||||
const config = {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import type {
|
|||
ChatCompletionContentPartWithCache,
|
||||
ChatCompletionToolWithCache,
|
||||
} from './types.js';
|
||||
import type { OpenAIResponseParsingOptions } from '../responseParsingOptions.js';
|
||||
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
|
||||
import { createDebugLogger } from '../../../utils/debugLogger.js';
|
||||
import { DefaultOpenAICompatibleProvider } from './default.js';
|
||||
|
|
@ -314,13 +313,6 @@ export class DashScopeOpenAICompatibleProvider extends DefaultOpenAICompatiblePr
|
|||
return {};
|
||||
}
|
||||
|
||||
getResponseParsingOptions(model?: string): OpenAIResponseParsingOptions {
|
||||
if (this.isGlmModel(model ?? this.contentGeneratorConfig.model)) {
|
||||
return { taggedThinkingTags: true };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add cache control flag to specified message(s) for DashScope providers
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export interface OpenAICompatibleProvider {
|
|||
userPromptId: string,
|
||||
): OpenAI.Chat.ChatCompletionCreateParams;
|
||||
getDefaultGenerationConfig(): GenerateContentConfig;
|
||||
getResponseParsingOptions?(model?: string): OpenAIResponseParsingOptions;
|
||||
getResponseParsingOptions?(): OpenAIResponseParsingOptions;
|
||||
getRequestContextOverrides?(): OpenAIRequestContextOverrides;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GenerateContentParameters, Part } from '@google/genai';
|
||||
import type { GenerateContentParameters } from '@google/genai';
|
||||
import type { Config } from '../../config/config.js';
|
||||
import type {
|
||||
ContentGeneratorConfig,
|
||||
|
|
@ -63,25 +63,6 @@ export interface RequestContext {
|
|||
* channel are deduplicated correctly.
|
||||
*/
|
||||
reasoningDeltaState?: StreamingTextDeltaState;
|
||||
/**
|
||||
* Set once tagged content parsing emits a thought in this stream. Used to
|
||||
* suppress duplicate provider reasoning-channel output that arrives in a
|
||||
* different chunk.
|
||||
*/
|
||||
hasTaggedThinkingThought?: boolean;
|
||||
/**
|
||||
* Reasoning-channel text buffered while a tagged-thinking stream is still
|
||||
* open. It is emitted only if the stream finishes without tagged content
|
||||
* producing any thought parts. If a stream ends abnormally before a finish
|
||||
* chunk, buffered reasoning is best-effort and may be lost.
|
||||
*/
|
||||
pendingReasoningText?: string;
|
||||
/**
|
||||
* Visible content buffered behind pending reasoning-channel text while a
|
||||
* tagged-thinking stream is still ambiguous. This preserves thought-before-
|
||||
* answer ordering when the provider uses reasoning_content without tags.
|
||||
*/
|
||||
pendingContentParts?: Part[];
|
||||
}
|
||||
|
||||
export interface ErrorHandler {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue