Skip to content

Commit 10af3a4

Browse files
fry69louisgv
andauthored
fix: return reasoning content with generateText() (#116)
* fix: (WIP) return reasoning content with generateText() * fix: downgrade to Zod v3 * chore: fix formatting * fix: relax peer depdencies for Zod v4 * Use types derived from schema * Remove unnecessary types * chore: update beta version --------- Co-authored-by: lab <[email protected]>
1 parent afcfed1 commit 10af3a4

File tree

4 files changed

+166
-72
lines changed

4 files changed

+166
-72
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/ai-sdk-provider",
3-
"version": "1.0.0-beta.4",
3+
"version": "1.0.0-beta.5",
44
"license": "Apache-2.0",
55
"sideEffects": false,
66
"main": "./dist/index.js",
@@ -49,11 +49,11 @@
4949
"typescript": "5.8.3",
5050
"vite-tsconfig-paths": "5.1.4",
5151
"vitest": "3.2.4",
52-
"zod": "^4.0.5"
52+
"zod": "3.25.76"
5353
},
5454
"peerDependencies": {
5555
"ai": "^5.0.0-beta.12",
56-
"zod": "^4.0.5"
56+
"zod": "^3.24.1 || ^v4"
5757
},
5858
"engines": {
5959
"node": ">=18"

pnpm-lock.yaml

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/chat/index.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { LanguageModelV2Prompt } from '@ai-sdk/provider';
2+
import type { ReasoningDetailUnion } from '../schemas/reasoning-details';
23

34
import {
45
convertReadableStreamToArray,
56
createTestServer,
67
} from '@ai-sdk/provider-utils/test';
78
import { createOpenRouter } from '../provider';
9+
import { ReasoningDetailType } from '../schemas/reasoning-details';
810

911
const TEST_PROMPT: LanguageModelV2Prompt = [
1012
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
@@ -121,6 +123,8 @@ describe('doGenerate', () => {
121123

122124
function prepareJsonResponse({
123125
content = '',
126+
reasoning,
127+
reasoning_details,
124128
usage = {
125129
prompt_tokens: 4,
126130
total_tokens: 34,
@@ -130,6 +134,8 @@ describe('doGenerate', () => {
130134
finish_reason = 'stop',
131135
}: {
132136
content?: string;
137+
reasoning?: string;
138+
reasoning_details?: Array<ReasoningDetailUnion>;
133139
usage?: {
134140
prompt_tokens: number;
135141
total_tokens: number;
@@ -159,6 +165,8 @@ describe('doGenerate', () => {
159165
message: {
160166
role: 'assistant',
161167
content,
168+
reasoning,
169+
reasoning_details,
162170
},
163171
logprobs,
164172
finish_reason,
@@ -238,6 +246,91 @@ describe('doGenerate', () => {
238246
expect(response.finishReason).toStrictEqual('unknown');
239247
});
240248

249+
it('should extract reasoning content from reasoning field', async () => {
250+
prepareJsonResponse({
251+
content: 'Hello!',
252+
reasoning:
253+
'I need to think about this... The user said hello, so I should respond with a greeting.',
254+
});
255+
256+
const result = await model.doGenerate({
257+
prompt: TEST_PROMPT,
258+
});
259+
260+
expect(result.content).toStrictEqual([
261+
{
262+
type: 'reasoning',
263+
text: 'I need to think about this... The user said hello, so I should respond with a greeting.',
264+
},
265+
{
266+
type: 'text',
267+
text: 'Hello!',
268+
},
269+
]);
270+
});
271+
272+
it('should extract reasoning content from reasoning_details', async () => {
273+
prepareJsonResponse({
274+
content: 'Hello!',
275+
reasoning_details: [
276+
{
277+
type: ReasoningDetailType.Text,
278+
text: 'Let me analyze this request...',
279+
},
280+
{
281+
type: ReasoningDetailType.Summary,
282+
summary: 'The user wants a greeting response.',
283+
},
284+
],
285+
});
286+
287+
const result = await model.doGenerate({
288+
prompt: TEST_PROMPT,
289+
});
290+
291+
expect(result.content).toStrictEqual([
292+
{
293+
type: 'reasoning',
294+
text: 'Let me analyze this request...',
295+
},
296+
{
297+
type: 'reasoning',
298+
text: 'The user wants a greeting response.',
299+
},
300+
{
301+
type: 'text',
302+
text: 'Hello!',
303+
},
304+
]);
305+
});
306+
307+
it('should handle encrypted reasoning details', async () => {
308+
prepareJsonResponse({
309+
content: 'Hello!',
310+
reasoning_details: [
311+
{
312+
type: ReasoningDetailType.Encrypted,
313+
data: 'encrypted_reasoning_data_here',
314+
},
315+
],
316+
});
317+
318+
const result = await model.doGenerate({
319+
prompt: TEST_PROMPT,
320+
});
321+
322+
expect(result.content).toStrictEqual([
323+
{
324+
type: 'reasoning',
325+
text: '[REDACTED]',
326+
},
327+
{
328+
type: 'text',
329+
text: 'Hello!',
330+
},
331+
]);
332+
});
333+
241334
it('should pass the model and the messages', async () => {
242335
prepareJsonResponse({ content: '' });
243336

src/chat/index.ts

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { ReasoningDetailUnion } from '@/src/schemas/reasoning-details';
2-
import type { OpenRouterUsageAccounting } from '@/src/types/index';
31
import type {
42
LanguageModelV2,
53
LanguageModelV2CallOptions,
@@ -14,12 +12,12 @@ import type {
1412
import type { ParseResult } from '@ai-sdk/provider-utils';
1513
import type { FinishReason } from 'ai';
1614
import type { z } from 'zod/v4';
15+
import type { OpenRouterUsageAccounting } from '@/src/types/index';
1716
import type {
1817
OpenRouterChatModelId,
1918
OpenRouterChatSettings,
2019
} from '../types/openrouter-chat-settings';
2120

22-
import { ReasoningDetailType } from '@/src/schemas/reasoning-details';
2321
import { InvalidResponseDataError } from '@ai-sdk/provider';
2422
import {
2523
combineHeaders,
@@ -29,6 +27,7 @@ import {
2927
isParsableJson,
3028
postJsonToApi,
3129
} from '@ai-sdk/provider-utils';
30+
import { ReasoningDetailType } from '@/src/schemas/reasoning-details';
3231
import { openrouterFailedResponseHandler } from '../schemas/error-response';
3332
import { mapOpenRouterFinishReason } from '../utils/map-finish-reason';
3433
import { convertToOpenRouterChatMessages } from './convert-to-openrouter-chat-messages';
@@ -235,60 +234,62 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
235234
cachedInputTokens: 0,
236235
};
237236

238-
const reasoningDetails = (choice.message.reasoning_details ??
239-
[]) as ReasoningDetailUnion[];
240-
241-
// const reasoning: any[] =
242-
reasoningDetails.length > 0
243-
? reasoningDetails
244-
.map((detail) => {
245-
switch (detail.type) {
246-
case ReasoningDetailType.Text: {
247-
if (detail.text) {
248-
return {
249-
type: 'text' as const,
250-
text: detail.text,
251-
signature: detail.signature ?? undefined,
252-
};
237+
const reasoningDetails = choice.message.reasoning_details ?? [];
238+
239+
const reasoning: Array<LanguageModelV2Content> =
240+
reasoningDetails.length > 0
241+
? reasoningDetails
242+
.map((detail) => {
243+
switch (detail.type) {
244+
case ReasoningDetailType.Text: {
245+
if (detail.text) {
246+
return {
247+
type: 'reasoning' as const,
248+
text: detail.text,
249+
};
250+
}
251+
break;
253252
}
254-
break;
255-
}
256-
case ReasoningDetailType.Summary: {
257-
if (detail.summary) {
258-
return {
259-
type: 'text' as const,
260-
text: detail.summary,
261-
};
253+
case ReasoningDetailType.Summary: {
254+
if (detail.summary) {
255+
return {
256+
type: 'reasoning' as const,
257+
text: detail.summary,
258+
};
259+
}
260+
break;
262261
}
263-
break;
264-
}
265-
case ReasoningDetailType.Encrypted: {
266-
if (detail.data) {
267-
return {
268-
type: 'redacted' as const,
269-
data: detail.data,
270-
};
262+
case ReasoningDetailType.Encrypted: {
263+
// For encrypted reasoning, we include a redacted placeholder
264+
if (detail.data) {
265+
return {
266+
type: 'reasoning' as const,
267+
text: '[REDACTED]',
268+
};
269+
}
270+
break;
271+
}
272+
default: {
273+
detail satisfies never;
271274
}
272-
break;
273-
}
274-
default: {
275-
detail satisfies never;
276275
}
277-
}
278-
return null;
279-
})
280-
.filter((p) => p !== null)
281-
: choice.message.reasoning
282-
? [
283-
{
284-
type: 'text' as const,
285-
text: choice.message.reasoning,
286-
},
287-
]
288-
: [];
276+
return null;
277+
})
278+
.filter((p) => p !== null)
279+
: choice.message.reasoning
280+
? [
281+
{
282+
type: 'reasoning' as const,
283+
text: choice.message.reasoning,
284+
},
285+
]
286+
: [];
289287

290288
const content: Array<LanguageModelV2Content> = [];
291289

290+
// Add reasoning content first
291+
content.push(...reasoning);
292+
292293
if (choice.message.content) {
293294
content.push({
294295
type: 'text' as const,

0 commit comments

Comments
 (0)