diff --git a/.changeset/six-schools-enjoy.md b/.changeset/six-schools-enjoy.md new file mode 100644 index 000000000000..8fde45fe3f64 --- /dev/null +++ b/.changeset/six-schools-enjoy.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/xai': patch +--- + +fix reasoning text extraction from content in responses doGenerate diff --git a/examples/ai-functions/src/generate-text/xai/responses-reasoning.ts b/examples/ai-functions/src/generate-text/xai/responses-reasoning.ts new file mode 100644 index 000000000000..337d66ea1af1 --- /dev/null +++ b/examples/ai-functions/src/generate-text/xai/responses-reasoning.ts @@ -0,0 +1,26 @@ +import { xai } from '@ai-sdk/xai'; +import { generateText } from 'ai'; +import { run } from '../../lib/run'; + +run(async () => { + const result = await generateText({ + model: xai.responses('grok-3-mini-latest'), + prompt: 'How many "r"s are in the word "strawberry"?', + }); + + for (const part of result.content) { + switch (part.type) { + case 'reasoning': { + console.log('--- reasoning ---'); + console.log(part.text); + break; + } + case 'text': { + console.log('--- text ---'); + console.log(part.text); + break; + } + } + console.log(); + } +}); diff --git a/packages/xai/src/responses/xai-responses-api.ts b/packages/xai/src/responses/xai-responses-api.ts index 70483ea4f7c3..3ce35c9f91ea 100644 --- a/packages/xai/src/responses/xai-responses-api.ts +++ b/packages/xai/src/responses/xai-responses-api.ts @@ -225,6 +225,9 @@ const outputItemSchema = z.discriminatedUnion('type', [ type: z.literal('reasoning'), id: z.string(), summary: z.array(reasoningSummaryPartSchema), + content: z + .array(z.object({ type: z.string(), text: z.string() })) + .nullish(), status: z.string(), encrypted_content: z.string().nullish(), }), diff --git a/packages/xai/src/responses/xai-responses-language-model.test.ts b/packages/xai/src/responses/xai-responses-language-model.test.ts index aa374cbaa6f8..c997a83112f9 100644 --- a/packages/xai/src/responses/xai-responses-language-model.test.ts +++ b/packages/xai/src/responses/xai-responses-language-model.test.ts @@ -407,6 +407,68 @@ describe('XaiResponsesLanguageModel', () => { ] `); }); + + it('should extract reasoning from content when summary is empty', async () => { + prepareJsonResponse({ + id: 'resp_123', + object: 'response', + status: 'completed', + model: 'grok-3-mini', + output: [ + { + type: 'reasoning', + id: 'rs_456', + status: 'completed', + summary: [], + content: [ + { + type: 'reasoning_text', + text: 'Let me think step by step.', + }, + ], + }, + { + type: 'message', + id: 'msg_123', + status: 'completed', + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'The answer is 444.', + annotations: [], + }, + ], + }, + ], + usage: { + input_tokens: 10, + output_tokens: 15, + }, + }); + + const result = await createModel('grok-3-mini').doGenerate({ + prompt: TEST_PROMPT, + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "providerMetadata": { + "xai": { + "itemId": "rs_456", + }, + }, + "text": "Let me think step by step.", + "type": "reasoning", + }, + { + "text": "The answer is 444.", + "type": "text", + }, + ] + `); + }); }); describe('settings and options', () => { diff --git a/packages/xai/src/responses/xai-responses-language-model.ts b/packages/xai/src/responses/xai-responses-language-model.ts index 2cad3f87ecf8..d7ba39cc2b4f 100644 --- a/packages/xai/src/responses/xai-responses-language-model.ts +++ b/packages/xai/src/responses/xai-responses-language-model.ts @@ -367,11 +367,14 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { } case 'reasoning': { - const summaryTexts = part.summary - .map(s => s.text) - .filter(text => text && text.length > 0); - - const reasoningText = summaryTexts.join(''); + const texts = + part.summary.length > 0 + ? part.summary.map(s => s.text) + : (part.content ?? []).map(c => c.text); + + const reasoningText = texts + .filter(text => text && text.length > 0) + .join(''); // condition changed here since encrypted content can now come with empty reasoning text if (reasoningText || part.encrypted_content) {