From 9c2408db527439b3640887ee8180c8c24dbeda35 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 18 May 2026 18:02:10 +0800 Subject: [PATCH 1/2] fix(core): handle MiMo tool-result media --- .../src/core/openaiContentGenerator/index.ts | 6 + .../openaiContentGenerator/pipeline.test.ts | 42 ++++ .../core/openaiContentGenerator/pipeline.ts | 7 +- .../openaiContentGenerator/provider/index.ts | 1 + .../provider/mimo.test.ts | 184 ++++++++++++++++++ .../openaiContentGenerator/provider/mimo.ts | 87 +++++++++ .../openaiContentGenerator/provider/types.ts | 5 + 7 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts create mode 100644 packages/core/src/core/openaiContentGenerator/provider/mimo.ts diff --git a/packages/core/src/core/openaiContentGenerator/index.ts b/packages/core/src/core/openaiContentGenerator/index.ts index d22bff03d1..06df838065 100644 --- a/packages/core/src/core/openaiContentGenerator/index.ts +++ b/packages/core/src/core/openaiContentGenerator/index.ts @@ -14,6 +14,7 @@ import { DashScopeOpenAICompatibleProvider, DeepSeekOpenAICompatibleProvider, ModelScopeOpenAICompatibleProvider, + MiMoOpenAICompatibleProvider, MiniMaxOpenAICompatibleProvider, MistralOpenAICompatibleProvider, OpenRouterOpenAICompatibleProvider, @@ -29,6 +30,7 @@ export { type OpenAICompatibleProvider, DashScopeOpenAICompatibleProvider, DeepSeekOpenAICompatibleProvider, + MiMoOpenAICompatibleProvider, MiniMaxOpenAICompatibleProvider, MistralOpenAICompatibleProvider, OpenRouterOpenAICompatibleProvider, @@ -76,6 +78,10 @@ export function determineProvider( ); } + if (MiMoOpenAICompatibleProvider.isMiMoProvider(config)) { + return new MiMoOpenAICompatibleProvider(contentGeneratorConfig, cliConfig); + } + // Check for OpenRouter provider if (OpenRouterOpenAICompatibleProvider.isOpenRouterProvider(config)) { return new OpenRouterOpenAICompatibleProvider( diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index e16921c0c3..b47cdcd536 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -216,6 +216,48 @@ describe('ContentGenerationPipeline', () => { ); }); + it('should apply provider request context overrides', async () => { + const request: GenerateContentParameters = { + model: 'test-model', + contents: [{ parts: [{ text: 'Hello' }], role: 'user' }], + }; + const userPromptId = 'test-prompt-id'; + const mockMessages = [ + { role: 'user', content: 'Hello' }, + ] as OpenAI.Chat.ChatCompletionMessageParam[]; + const mockOpenAIResponse = { + id: 'response-id', + choices: [ + { message: { content: 'Hello response' }, finish_reason: 'stop' }, + ], + created: Date.now(), + model: 'test-model', + } as OpenAI.Chat.ChatCompletion; + const mockGeminiResponse = new GenerateContentResponse(); + + mockProvider.getRequestContextOverrides = vi.fn().mockReturnValue({ + splitToolMedia: true, + }); + (mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue( + mockMessages, + ); + (mockConverter.convertOpenAIResponseToGemini as Mock).mockReturnValue( + mockGeminiResponse, + ); + (mockClient.chat.completions.create as Mock).mockResolvedValue( + mockOpenAIResponse, + ); + + await pipeline.execute(request, userPromptId); + + expect(mockConverter.convertGeminiRequestToOpenAI).toHaveBeenCalledWith( + request, + expect.objectContaining({ + splitToolMedia: true, + }), + ); + }); + it('should fall back to configured model when request.model is empty', async () => { // Arrange — empty model string is falsy, should fall back to contentGeneratorConfig.model const request: GenerateContentParameters = { diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index c814527d61..52b61b34ea 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -544,6 +544,8 @@ export class ContentGenerationPipeline { isStreaming: boolean, ): RequestContext { const effectiveModel = request.model || this.contentGeneratorConfig.model; + const providerOverrides = + this.config.provider.getRequestContextOverrides?.() ?? {}; const toolCallParser = isStreaming ? new StreamingToolCallParser() : undefined; @@ -558,7 +560,10 @@ export class ContentGenerationPipeline { model: effectiveModel, modalities: this.contentGeneratorConfig.modalities ?? {}, startTime: Date.now(), - splitToolMedia: this.contentGeneratorConfig.splitToolMedia ?? false, + splitToolMedia: + providerOverrides.splitToolMedia ?? + this.contentGeneratorConfig.splitToolMedia ?? + false, ...(toolCallParser ? { toolCallParser } : {}), ...(responseParsingOptions ? { responseParsingOptions } : {}), ...(taggedThinkingParser ? { taggedThinkingParser } : {}), diff --git a/packages/core/src/core/openaiContentGenerator/provider/index.ts b/packages/core/src/core/openaiContentGenerator/provider/index.ts index f5c02ca434..3b87596eba 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/index.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/index.ts @@ -4,6 +4,7 @@ export { DeepSeekOpenAICompatibleProvider } from './deepseek.js'; export { OpenRouterOpenAICompatibleProvider } from './openrouter.js'; export { MiniMaxOpenAICompatibleProvider } from './minimax.js'; export { MistralOpenAICompatibleProvider } from './mistral.js'; +export { MiMoOpenAICompatibleProvider } from './mimo.js'; export { DefaultOpenAICompatibleProvider } from './default.js'; export type { OpenAICompatibleProvider, diff --git a/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts b/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts new file mode 100644 index 0000000000..236b3b3e3e --- /dev/null +++ b/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from 'vitest'; +import type OpenAI from 'openai'; +import type { Config } from '../../../config/config.js'; +import type { ContentGeneratorConfig } from '../../contentGenerator.js'; +import { determineProvider } from '../index.js'; +import { MiMoOpenAICompatibleProvider } from './mimo.js'; + +function createCliConfig(): Config { + return { + getCliVersion: vi.fn().mockReturnValue('1.0.0'), + getProxy: vi.fn().mockReturnValue(undefined), + } as unknown as Config; +} + +function createProviderConfig( + overrides: Partial = {}, +): ContentGeneratorConfig { + return { + apiKey: 'test-api-key', + baseUrl: 'https://token-plan-cn.xiaomimimo.com/v1', + model: 'mimo-v2.5-pro', + ...overrides, + } as ContentGeneratorConfig; +} + +describe('MiMoOpenAICompatibleProvider', () => { + it('is selected for Xiaomi MiMo hostnames', () => { + const provider = determineProvider( + createProviderConfig(), + createCliConfig(), + ); + + expect(provider).toBeInstanceOf(MiMoOpenAICompatibleProvider); + }); + + it('is selected for official API hostnames', () => { + const provider = determineProvider( + createProviderConfig({ + baseUrl: 'https://api.xiaomimimo.com/v1', + model: 'custom-model-alias', + }), + createCliConfig(), + ); + + expect(provider).toBeInstanceOf(MiMoOpenAICompatibleProvider); + }); + + it('is selected for MiMo model names behind custom gateways', () => { + const provider = determineProvider( + createProviderConfig({ + baseUrl: 'https://gateway.example.com/v1', + model: 'MiMo-V2.5-Pro', + }), + createCliConfig(), + ); + + expect(provider).toBeInstanceOf(MiMoOpenAICompatibleProvider); + }); + + it('does not match hostile hostnames containing xiaomimimo.com', () => { + const provider = determineProvider( + createProviderConfig({ + baseUrl: 'https://api.xiaomimimo.com.evil.example/v1', + model: 'gpt-4o', + }), + createCliConfig(), + ); + + expect(provider).not.toBeInstanceOf(MiMoOpenAICompatibleProvider); + }); + + it('splits tool-result media by default for strict MiMo chat requests', () => { + const provider = determineProvider( + createProviderConfig(), + createCliConfig(), + ); + + expect(provider.getRequestContextOverrides?.()).toEqual({ + splitToolMedia: true, + }); + }); + + it('preserves an explicit splitToolMedia override', () => { + const provider = determineProvider( + createProviderConfig({ splitToolMedia: false }), + createCliConfig(), + ); + + expect(provider.getRequestContextOverrides?.()).toEqual({ + splitToolMedia: false, + }); + }); + + it('injects empty reasoning_content on tool-calling assistant turns missing it', () => { + const provider = determineProvider( + createProviderConfig(), + createCliConfig(), + ); + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user', content: 'list markdown files' }, + { + role: 'assistant', + content: '', + tool_calls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'glob', arguments: '{"pattern":"**/*.md"}' }, + }, + ], + }, + { role: 'tool', tool_call_id: 'call_1', content: 'Found 2 files' }, + ], + }; + + const result = provider.buildRequest(request, 'prompt-123'); + const assistant = result.messages?.[1] as { + reasoning_content?: string; + }; + + expect(assistant.reasoning_content).toBe(''); + }); + + it('preserves existing reasoning_content on tool-calling assistant turns', () => { + const provider = determineProvider( + createProviderConfig(), + createCliConfig(), + ); + const request = { + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user' as const, content: 'list markdown files' }, + { + role: 'assistant' as const, + content: '', + reasoning_content: 'I should search the repository.', + tool_calls: [ + { + id: 'call_1', + type: 'function' as const, + function: { name: 'glob', arguments: '{"pattern":"**/*.md"}' }, + }, + ], + }, + ], + } as unknown as OpenAI.Chat.ChatCompletionCreateParams; + + const result = provider.buildRequest(request, 'prompt-123'); + const assistant = result.messages?.[1] as { + reasoning_content?: string; + }; + + expect(assistant.reasoning_content).toBe('I should search the repository.'); + }); + + it('does not add reasoning_content to assistant turns without tool calls', () => { + const provider = determineProvider( + createProviderConfig(), + createCliConfig(), + ); + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user', content: 'hi' }, + { role: 'assistant', content: 'hello' }, + ], + }; + + const result = provider.buildRequest(request, 'prompt-123'); + const assistant = result.messages?.[1] as { + reasoning_content?: string; + }; + + expect(assistant.reasoning_content).toBeUndefined(); + }); +}); diff --git a/packages/core/src/core/openaiContentGenerator/provider/mimo.ts b/packages/core/src/core/openaiContentGenerator/provider/mimo.ts new file mode 100644 index 0000000000..a7fef8b005 --- /dev/null +++ b/packages/core/src/core/openaiContentGenerator/provider/mimo.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import type OpenAI from 'openai'; +import type { Config } from '../../../config/config.js'; +import type { ContentGeneratorConfig } from '../../contentGenerator.js'; +import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js'; +import { DefaultOpenAICompatibleProvider } from './default.js'; + +export function isMiMoProvider( + contentGeneratorConfig: ContentGeneratorConfig, +): boolean { + const baseUrl = contentGeneratorConfig.baseUrl ?? ''; + if (baseUrl) { + try { + const hostname = new URL(baseUrl).hostname.toLowerCase(); + if ( + hostname === 'xiaomimimo.com' || + hostname.endsWith('.xiaomimimo.com') + ) { + return true; + } + } catch { + // Invalid custom URLs fall through to model-name detection. + } + } + + const model = contentGeneratorConfig.model ?? ''; + return model.toLowerCase().startsWith('mimo-'); +} + +export class MiMoOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider { + constructor( + contentGeneratorConfig: ContentGeneratorConfig, + cliConfig: Config, + ) { + super(contentGeneratorConfig, cliConfig); + } + + static isMiMoProvider = isMiMoProvider; + + override buildRequest( + request: OpenAI.Chat.ChatCompletionCreateParams, + userPromptId: string, + ): OpenAI.Chat.ChatCompletionCreateParams { + const baseRequest = super.buildRequest(request, userPromptId); + if (!baseRequest.messages?.length) { + return baseRequest; + } + + return { + ...baseRequest, + messages: baseRequest.messages.map(ensureReasoningContentOnToolCalls), + }; + } + + getRequestContextOverrides(): { splitToolMedia?: boolean } { + return { + splitToolMedia: this.contentGeneratorConfig.splitToolMedia ?? true, + }; + } +} + +function ensureReasoningContentOnToolCalls( + message: OpenAI.Chat.ChatCompletionMessageParam, +): OpenAI.Chat.ChatCompletionMessageParam { + if (message.role !== 'assistant') { + return message; + } + + const assistant = message as ExtendedChatCompletionAssistantMessageParam; + if (!assistant.tool_calls?.length) { + return message; + } + + if (typeof assistant.reasoning_content === 'string') { + return message; + } + + return { + ...assistant, + reasoning_content: '', + } as OpenAI.Chat.ChatCompletionMessageParam; +} diff --git a/packages/core/src/core/openaiContentGenerator/provider/types.ts b/packages/core/src/core/openaiContentGenerator/provider/types.ts index 3f6eb138c6..c990516790 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/types.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/types.ts @@ -2,6 +2,10 @@ import type { GenerateContentConfig } from '@google/genai'; import type OpenAI from 'openai'; import type { OpenAIResponseParsingOptions } from '../responseParsingOptions.js'; +export type OpenAIRequestContextOverrides = { + splitToolMedia?: boolean; +}; + // Extended types to support cache_control for DashScope export interface ChatCompletionContentPartTextWithCache extends OpenAI.Chat.ChatCompletionContentPartText { @@ -26,6 +30,7 @@ export interface OpenAICompatibleProvider { ): OpenAI.Chat.ChatCompletionCreateParams; getDefaultGenerationConfig(): GenerateContentConfig; getResponseParsingOptions?(): OpenAIResponseParsingOptions; + getRequestContextOverrides?(): OpenAIRequestContextOverrides; } export type DashScopeRequestMetadata = { From b70a0516a4b65d981a1e4f351ef3a70e9cb51d5b Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Wed, 20 May 2026 11:17:57 +0800 Subject: [PATCH 2/2] fix(core): replay reasoning content for MiMo turns --- .../openaiContentGenerator/pipeline.test.ts | 43 +++++++++++++++++++ .../provider/deepseek.ts | 28 +----------- .../provider/mimo.test.ts | 4 +- .../openaiContentGenerator/provider/mimo.ts | 34 ++++----------- .../openaiContentGenerator/provider/utils.ts | 29 +++++++++++++ 5 files changed, 84 insertions(+), 54 deletions(-) create mode 100644 packages/core/src/core/openaiContentGenerator/provider/utils.ts diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index b47cdcd536..3504071ae3 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -258,6 +258,49 @@ describe('ContentGenerationPipeline', () => { ); }); + it('should let provider request context overrides take precedence over content generator config', async () => { + const request: GenerateContentParameters = { + model: 'test-model', + contents: [{ parts: [{ text: 'Hello' }], role: 'user' }], + }; + const userPromptId = 'test-prompt-id'; + const mockMessages = [ + { role: 'user', content: 'Hello' }, + ] as OpenAI.Chat.ChatCompletionMessageParam[]; + const mockOpenAIResponse = { + id: 'response-id', + choices: [ + { message: { content: 'Hello response' }, finish_reason: 'stop' }, + ], + created: Date.now(), + model: 'test-model', + } as OpenAI.Chat.ChatCompletion; + const mockGeminiResponse = new GenerateContentResponse(); + + mockContentGeneratorConfig.splitToolMedia = true; + mockProvider.getRequestContextOverrides = vi.fn().mockReturnValue({ + splitToolMedia: false, + }); + (mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue( + mockMessages, + ); + (mockConverter.convertOpenAIResponseToGemini as Mock).mockReturnValue( + mockGeminiResponse, + ); + (mockClient.chat.completions.create as Mock).mockResolvedValue( + mockOpenAIResponse, + ); + + await pipeline.execute(request, userPromptId); + + expect(mockConverter.convertGeminiRequestToOpenAI).toHaveBeenCalledWith( + request, + expect.objectContaining({ + splitToolMedia: false, + }), + ); + }); + it('should fall back to configured model when request.model is empty', async () => { // Arrange — empty model string is falsy, should fall back to contentGeneratorConfig.model const request: GenerateContentParameters = { diff --git a/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts b/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts index 72f7ece999..d601575807 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/deepseek.ts @@ -7,9 +7,9 @@ import type OpenAI from 'openai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; -import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js'; import { DefaultOpenAICompatibleProvider } from './default.js'; import type { GenerateContentConfig } from '@google/genai'; +import { ensureReasoningContentOnAssistantMessage } from './utils.js'; /** * Hostname-only check used to decide whether `reasoning.effort` should be @@ -108,7 +108,7 @@ export class DeepSeekOpenAICompatibleProvider extends DefaultOpenAICompatiblePro const messages = reshaped.messages.map((message) => { const flattened = flattenContentParts(message); - return ensureReasoningContentOnToolCalls(flattened); + return ensureReasoningContentOnAssistantMessage(flattened); }); return { @@ -216,27 +216,3 @@ function translateReasoningEffort( } return next as unknown as OpenAI.Chat.ChatCompletionCreateParams; } - -// DeepSeek's thinking mode requires reasoning_content to be replayed on every -// prior assistant turn, including ones without tool_calls. The model may -// legitimately return a turn without reasoning text, so the field can be -// missing when we rebuild the request. Send an empty string in that case so -// the API contract is satisfied. https://github.com/QwenLM/qwen-code/issues/3695 -function ensureReasoningContentOnToolCalls( - message: OpenAI.Chat.ChatCompletionMessageParam, -): OpenAI.Chat.ChatCompletionMessageParam { - if (message.role !== 'assistant') { - return message; - } - const extended = message as ExtendedChatCompletionAssistantMessageParam; - if ( - typeof extended.reasoning_content === 'string' && - extended.reasoning_content.length > 0 - ) { - return message; - } - return { - ...extended, - reasoning_content: '', - } as OpenAI.Chat.ChatCompletionMessageParam; -} diff --git a/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts b/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts index 236b3b3e3e..cc508086e3 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/mimo.test.ts @@ -161,7 +161,7 @@ describe('MiMoOpenAICompatibleProvider', () => { expect(assistant.reasoning_content).toBe('I should search the repository.'); }); - it('does not add reasoning_content to assistant turns without tool calls', () => { + it('injects empty reasoning_content on assistant turns without tool calls', () => { const provider = determineProvider( createProviderConfig(), createCliConfig(), @@ -179,6 +179,6 @@ describe('MiMoOpenAICompatibleProvider', () => { reasoning_content?: string; }; - expect(assistant.reasoning_content).toBeUndefined(); + expect(assistant.reasoning_content).toBe(''); }); }); diff --git a/packages/core/src/core/openaiContentGenerator/provider/mimo.ts b/packages/core/src/core/openaiContentGenerator/provider/mimo.ts index a7fef8b005..f369ca9509 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/mimo.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/mimo.ts @@ -7,8 +7,9 @@ import type OpenAI from 'openai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; -import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js'; import { DefaultOpenAICompatibleProvider } from './default.js'; +import type { OpenAIRequestContextOverrides } from './types.js'; +import { ensureReasoningContentOnAssistantMessage } from './utils.js'; export function isMiMoProvider( contentGeneratorConfig: ContentGeneratorConfig, @@ -24,7 +25,7 @@ export function isMiMoProvider( return true; } } catch { - // Invalid custom URLs fall through to model-name detection. + // Non-MiMo URLs fall through to model-name detection. } } @@ -53,35 +54,16 @@ export class MiMoOpenAICompatibleProvider extends DefaultOpenAICompatibleProvide return { ...baseRequest, - messages: baseRequest.messages.map(ensureReasoningContentOnToolCalls), + messages: baseRequest.messages.map( + ensureReasoningContentOnAssistantMessage, + ), }; } - getRequestContextOverrides(): { splitToolMedia?: boolean } { + getRequestContextOverrides(): OpenAIRequestContextOverrides { + // Respect explicit user configuration; default to true for MiMo compatibility. return { splitToolMedia: this.contentGeneratorConfig.splitToolMedia ?? true, }; } } - -function ensureReasoningContentOnToolCalls( - message: OpenAI.Chat.ChatCompletionMessageParam, -): OpenAI.Chat.ChatCompletionMessageParam { - if (message.role !== 'assistant') { - return message; - } - - const assistant = message as ExtendedChatCompletionAssistantMessageParam; - if (!assistant.tool_calls?.length) { - return message; - } - - if (typeof assistant.reasoning_content === 'string') { - return message; - } - - return { - ...assistant, - reasoning_content: '', - } as OpenAI.Chat.ChatCompletionMessageParam; -} diff --git a/packages/core/src/core/openaiContentGenerator/provider/utils.ts b/packages/core/src/core/openaiContentGenerator/provider/utils.ts new file mode 100644 index 0000000000..a09435e3af --- /dev/null +++ b/packages/core/src/core/openaiContentGenerator/provider/utils.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import type OpenAI from 'openai'; +import type { ExtendedChatCompletionAssistantMessageParam } from '../converter.js'; + +// Some thinking-mode OpenAI-compatible APIs require `reasoning_content` to be +// replayed on every prior assistant turn, even when the model returned no +// visible reasoning text for that turn. +export function ensureReasoningContentOnAssistantMessage( + message: OpenAI.Chat.ChatCompletionMessageParam, +): OpenAI.Chat.ChatCompletionMessageParam { + if (message.role !== 'assistant') { + return message; + } + + const assistant = message as ExtendedChatCompletionAssistantMessageParam; + if (typeof assistant.reasoning_content === 'string') { + return message; + } + + return { + ...assistant, + reasoning_content: '', + } as OpenAI.Chat.ChatCompletionMessageParam; +}