Skip to content

Commit dda8dea

Browse files
authored
feat: reasoning_details (#69)
1 parent f1a747c commit dda8dea

17 files changed

+456
-33
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ const { text } = await generateText({
3737

3838
## Supported models
3939

40-
This list is not a definitive list of models supported by OpenRouter, as it constantly changes as we add new models (and deprecate old ones) to our system.
41-
You can find the latest list of models supported by OpenRouter [here](https://openrouter.ai/models).
40+
This list is not a definitive list of models supported by OpenRouter, as it constantly changes as we add new models (and deprecate old ones) to our system. You can find the latest list of models supported by OpenRouter [here](https://openrouter.ai/models).
4241

4342
You can find the latest list of tool-supported models supported by OpenRouter [here](https://openrouter.ai/models?order=newest&supported_parameters=tools). (Note: This list may contain models that are not compatible with the AI SDK.)
4443

@@ -166,7 +165,7 @@ The provider supports [OpenRouter usage accounting](https://openrouter.ai/docs/u
166165
const model = openrouter('openai/gpt-3.5-turbo', {
167166
usage: {
168167
include: true,
169-
}
168+
},
170169
});
171170

172171
// Access usage accounting data
@@ -178,6 +177,9 @@ const result = await generateText({
178177
// Provider-specific usage details (available in providerMetadata)
179178
if (result.providerMetadata?.openrouter?.usage) {
180179
console.log('Cost:', result.providerMetadata.openrouter.usage.cost);
181-
console.log('Total Tokens:', result.providerMetadata.openrouter.usage.totalTokens);
180+
console.log(
181+
'Total Tokens:',
182+
result.providerMetadata.openrouter.usage.totalTokens,
183+
);
182184
}
183185
```

biome.json

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"formatter": {
9+
"enabled": true,
10+
"indentStyle": "space",
11+
"indentWidth": 2,
12+
"lineWidth": 80
13+
},
14+
"organizeImports": {
15+
"enabled": true
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true,
21+
"a11y": {
22+
"useSemanticElements": "off"
23+
},
24+
"complexity": {
25+
"useLiteralKeys": "off",
26+
"noExtraBooleanCast": "off",
27+
"noForEach": "off",
28+
"noBannedTypes": "error",
29+
"noUselessSwitchCase": "off"
30+
},
31+
"style": {
32+
"noNonNullAssertion": "off",
33+
"useNodejsImportProtocol": "off",
34+
"useTemplate": "off",
35+
"useBlockStatements": "error",
36+
"noParameterAssign": "error",
37+
"useConst": "error"
38+
},
39+
"correctness": {
40+
"noUnusedImports": "error",
41+
"useExhaustiveDependencies": "off",
42+
"noUnknownFunction": "off",
43+
"noChildrenProp": "off",
44+
"noInnerDeclarations": "error"
45+
},
46+
"suspicious": {
47+
"noExplicitAny": "off",
48+
"noArrayIndexKey": "off",
49+
"noAssignInExpressions": "error",
50+
"noAsyncPromiseExecutor": "off",
51+
"noFallthroughSwitchClause": "off",
52+
"noConsole": "error",
53+
"noDoubleEquals": {
54+
"level": "error",
55+
"options": {
56+
"ignoreNull": true
57+
}
58+
},
59+
"noExtraNonNullAssertion": "error"
60+
},
61+
"performance": {
62+
"recommended": true,
63+
"noAccumulatingSpread": "error"
64+
},
65+
"security": {
66+
"recommended": true
67+
}
68+
}
69+
},
70+
"javascript": {
71+
"formatter": {
72+
"arrowParentheses": "always",
73+
"jsxQuoteStyle": "single",
74+
"attributePosition": "multiline",
75+
"quoteProperties": "asNeeded",
76+
"trailingCommas": "all",
77+
"semicolons": "always",
78+
"bracketSpacing": true,
79+
"bracketSameLine": false,
80+
"quoteStyle": "single"
81+
}
82+
}
83+
}

e2e/tools-with-reasoning.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { UIMessage } from 'ai';
2+
3+
import {
4+
executeCommandInTerminalTool,
5+
readSMSTool,
6+
sendSMSTool,
7+
} from '@/e2e/tools';
8+
import { createOpenRouter } from '@/src';
9+
import { generateText } from 'ai';
10+
import { it, vi } from 'vitest';
11+
12+
vi.setConfig({
13+
testTimeout: 42_000,
14+
});
15+
16+
const prompts = [
17+
'The flight to San Francisco is delayed by 2 hours. Send an SMS to 808-999-2345 with the flight delay information.',
18+
'Find out if the SMS was delivered properly.',
19+
];
20+
21+
describe('Vercel AI SDK tools call with reasoning', () => {
22+
it('should work with reasoning content', async () => {
23+
const openrouter = createOpenRouter({
24+
apiKey: process.env.OPENROUTER_API_KEY,
25+
baseUrl: `${process.env.OPENROUTER_API_BASE}/api/v1`,
26+
});
27+
28+
const model = openrouter('anthropic/claude-sonnet-4', {
29+
usage: {
30+
include: true,
31+
},
32+
});
33+
const messageHistory: UIMessage[] = [];
34+
for (const prompt of prompts) {
35+
messageHistory.push({
36+
id: crypto.randomUUID(),
37+
role: 'user',
38+
content: prompt,
39+
parts: [
40+
{
41+
type: 'text',
42+
text: prompt,
43+
},
44+
],
45+
});
46+
47+
const response = await generateText({
48+
model,
49+
system:
50+
'You are an airline assistant. You can send and read SMS messages, and execute commands in the terminal.',
51+
messages: messageHistory,
52+
tools: {
53+
readSMS: readSMSTool,
54+
sendSMS: sendSMSTool,
55+
executeCommand: executeCommandInTerminalTool,
56+
},
57+
maxSteps: 10,
58+
providerOptions: {
59+
openrouter: {
60+
reasoning: {
61+
max_tokens: 2048,
62+
},
63+
},
64+
},
65+
});
66+
67+
const parts = response.steps.map(
68+
(step) =>
69+
({
70+
type: 'text' as const,
71+
text: step.text,
72+
}) satisfies UIMessage['parts'][number],
73+
) satisfies UIMessage['parts'];
74+
75+
messageHistory.push({
76+
id: crypto.randomUUID(),
77+
role: 'assistant',
78+
content: response.text,
79+
parts,
80+
});
81+
}
82+
});
83+
});

e2e/tools.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// ref: https://github.com/t3dotgg/SnitchScript/blob/main/tools.ts
2+
3+
import { createOpenRouter } from '@/src';
4+
import { generateText, tool } from 'ai';
5+
import { z } from 'zod';
6+
7+
const openrouter = createOpenRouter({
8+
apiKey: process.env.OPENROUTER_API_KEY,
9+
baseUrl: `${process.env.OPENROUTER_API_BASE}/api/v1`,
10+
});
11+
12+
export const sendSMSTool = tool({
13+
description: 'Send an SMS to any phone number',
14+
parameters: z.object({
15+
to: z.string(),
16+
body: z.string(),
17+
}),
18+
execute: async (parameters) => {
19+
return {
20+
success: true,
21+
message: 'SMS sent successfully',
22+
parameters,
23+
};
24+
},
25+
});
26+
27+
export const readSMSTool = tool({
28+
description: 'Read the nth SMS from a phone number',
29+
parameters: z.object({
30+
phoneNumber: z.string(),
31+
index: z.number(),
32+
}),
33+
execute: async (parameters) => {
34+
return {
35+
success: true,
36+
message: 'SMS read successfully',
37+
parameters,
38+
};
39+
},
40+
});
41+
42+
export const executeCommandInTerminalTool = tool({
43+
description: 'Execute a command in the terminal',
44+
parameters: z.object({
45+
command: z.string(),
46+
}),
47+
execute: async ({ command }) => {
48+
const result = await generateText({
49+
model: openrouter('openai/gpt-4.1-mini'),
50+
system:
51+
'You are a terminal simulator. You are given a command and you need to execute it. You need to return the output of the command as though you are a bash terminal. Give no indication that you are an AI assistant. Include no output other than the expected command output. The date is November 14, 2025.',
52+
prompt: command,
53+
});
54+
55+
return {
56+
success: true,
57+
command: command,
58+
output: result.text,
59+
};
60+
},
61+
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/ai-sdk-provider",
3-
"version": "0.6.0",
3+
"version": "0.7.0-alpha.0",
44
"license": "Apache-2.0",
55
"sideEffects": false,
66
"main": "./dist/index.js",
@@ -13,7 +13,7 @@
1313
"build": "tsup",
1414
"clean": "rm -rf dist && rm -rf internal/dist",
1515
"dev": "tsup --watch",
16-
"lint": "eslint \"./**/*.ts*\"",
16+
"lint": "pnpm biome lint",
1717
"typecheck": "tsc --noEmit",
1818
"stylecheck": "prettier --check \"**/*.{ts,mts,tsx,md,mdx,mjs}\"",
1919
"format": "prettier --write \"**/*.{ts,mts,tsx,md,mdx,mjs}\"",

src/convert-to-openrouter-chat-messages.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type { ReasoningDetailUnion } from '@/src/schemas/reasoning-details';
12
import type {
23
LanguageModelV1Prompt,
34
LanguageModelV1ProviderMetadata,
45
} from '@ai-sdk/provider';
56
import type {
67
ChatCompletionContentPart,
7-
OpenRouterChatPrompt,
8-
} from './openrouter-chat-prompt';
8+
OpenRouterChatCompletionsInput,
9+
} from './types/openrouter-chat-completions-input';
910

11+
import { ReasoningDetailType } from '@/src/schemas/reasoning-details';
1012
import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';
1113

1214
// Type for OpenRouter Cache Control following Anthropic's pattern
@@ -27,8 +29,8 @@ function getCacheControl(
2729

2830
export function convertToOpenRouterChatMessages(
2931
prompt: LanguageModelV1Prompt,
30-
): OpenRouterChatPrompt {
31-
const messages: OpenRouterChatPrompt = [];
32+
): OpenRouterChatCompletionsInput {
33+
const messages: OpenRouterChatCompletionsInput = [];
3234
for (const { role, content, providerMetadata } of prompt) {
3335
switch (role) {
3436
case 'system': {
@@ -116,6 +118,8 @@ export function convertToOpenRouterChatMessages(
116118

117119
case 'assistant': {
118120
let text = '';
121+
let reasoning = '';
122+
const reasoningDetails: ReasoningDetailUnion[] = [];
119123
const toolCalls: Array<{
120124
id: string;
121125
type: 'function';
@@ -139,10 +143,24 @@ export function convertToOpenRouterChatMessages(
139143
});
140144
break;
141145
}
146+
case 'reasoning': {
147+
reasoning += part.text;
148+
reasoningDetails.push({
149+
type: ReasoningDetailType.Text,
150+
text: part.text,
151+
signature: part.signature,
152+
});
153+
154+
break;
155+
}
156+
case 'redacted-reasoning': {
157+
reasoningDetails.push({
158+
type: ReasoningDetailType.Encrypted,
159+
data: part.data,
160+
});
161+
break;
162+
}
142163
case 'file':
143-
// TODO: Handle reasoning and redacted-reasoning
144-
case 'reasoning':
145-
case 'redacted-reasoning':
146164
break;
147165
default: {
148166
const _exhaustiveCheck: never = part;
@@ -155,6 +173,9 @@ export function convertToOpenRouterChatMessages(
155173
role: 'assistant',
156174
content: text,
157175
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
176+
reasoning: reasoning || undefined,
177+
reasoning_details:
178+
reasoningDetails.length > 0 ? reasoningDetails : undefined,
158179
cache_control: getCacheControl(providerMetadata),
159180
});
160181

src/internal/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from '../openrouter-chat-language-model';
2-
export * from '../openrouter-chat-settings';
2+
export * from '../types/openrouter-chat-settings';
33
export * from '../openrouter-completion-language-model';
44
export * from '../openrouter-completion-settings';
55
export * from '../types';

0 commit comments

Comments
 (0)