Skip to content

Commit 3d3eeab

Browse files
vercel-ai-sdk[bot]aayush-kapoor
andauthored
Backport: feat(open-responses): add option to pass reasoning summary for OpenResponses (#14208)
This is an automated backport of #14115 to the release-v6.0 branch. FYI @aayush-kapoor This backport has conflicts that need to be resolved manually. ### `git cherry-pick` output ``` Auto-merging packages/open-responses/src/open-responses-provider.ts Auto-merging packages/open-responses/src/responses/open-responses-language-model.test.ts CONFLICT (content): Merge conflict in packages/open-responses/src/responses/open-responses-language-model.test.ts Auto-merging packages/open-responses/src/responses/open-responses-language-model.ts CONFLICT (content): Merge conflict in packages/open-responses/src/responses/open-responses-language-model.ts error: could not apply e69a836... feat(open-responses): add option to pass reasoning summary for OpenResponses (#14115) hint: After resolving the conflicts, mark them with hint: "git add/rm <pathspec>", then run hint: "git cherry-pick --continue". hint: You can instead skip this commit with "git cherry-pick --skip". hint: To abort and get back to the state before "git cherry-pick", hint: run "git cherry-pick --abort". hint: Disable this message with "git config set advice.mergeConflict false" ``` --------- Co-authored-by: Aayush Kapoor <83492835+aayush-kapoor@users.noreply.github.com> Co-authored-by: Aayush Kapoor <aayushkapoor34@gmail.com>
1 parent db0df72 commit 3d3eeab

8 files changed

Lines changed: 242 additions & 0 deletions

File tree

.changeset/stale-berries-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ai-sdk/open-responses": patch
3+
---
4+
5+
feat(open-responses): add option to pass reasoning summary for OpenResponses
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { createOpenResponses } from '@ai-sdk/open-responses';
2+
import { generateText } from 'ai';
3+
import { run } from '../../lib/run';
4+
5+
const lmstudio = createOpenResponses({
6+
name: 'lmstudio',
7+
url: 'https://api.openai.com/v1/responses',
8+
apiKey: process.env.OPENAI_API_KEY,
9+
});
10+
11+
run(async () => {
12+
const result = await generateText({
13+
model: lmstudio('gpt-5'),
14+
prompt: 'Invent a new holiday and describe its traditions.',
15+
maxOutputTokens: 100,
16+
providerOptions: {
17+
lmstudio: {
18+
reasoningEffort: 'high',
19+
reasoningSummary: 'detailed',
20+
},
21+
},
22+
});
23+
24+
console.log(JSON.stringify(result.response.body, null, 2));
25+
26+
console.log('Reasoning:', result.reasoning);
27+
console.log(result.text);
28+
console.log();
29+
console.log('Token usage:', result.usage);
30+
console.log('Finish reason:', result.finishReason);
31+
console.log('Request:', JSON.stringify(result.request, null, 2));
32+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { VERSION } from './version';
22
export { createOpenResponses } from './open-responses-provider';
3+
export type { OpenResponsesOptions } from './responses/open-responses-options';

packages/open-responses/src/open-responses-provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function createOpenResponses(
6565
const createResponsesModel = (modelId: string) => {
6666
return new OpenResponsesLanguageModel(modelId, {
6767
provider: `${providerName}.responses`,
68+
providerOptionsName: providerName,
6869
headers: getHeaders,
6970
url: options.url,
7071
fetch: options.fetch,

packages/open-responses/src/responses/open-responses-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils';
22

33
export type OpenResponsesConfig = {
44
provider: string;
5+
providerOptionsName: string;
56
url: string;
67
headers: () => Record<string, string | undefined>;
78
fetch?: FetchFunction;

packages/open-responses/src/responses/open-responses-language-model.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('OpenResponsesLanguageModel', () => {
2525
function createModel(modelId: string = 'gemma-7b-it') {
2626
return new OpenResponsesLanguageModel(modelId, {
2727
provider: 'lmstudio',
28+
providerOptionsName: 'lmstudio',
2829
url: URL,
2930
headers: () => ({}),
3031
generateId: mockId(),
@@ -150,6 +151,166 @@ describe('OpenResponsesLanguageModel', () => {
150151
});
151152
});
152153

154+
describe('providerOptions reasoning', () => {
155+
beforeEach(() => {
156+
prepareJsonFixtureResponse('lmstudio-basic.1');
157+
});
158+
159+
it('should send reasoning.effort via providerOptions', async () => {
160+
await createModel().doGenerate({
161+
prompt: TEST_PROMPT,
162+
providerOptions: {
163+
lmstudio: { reasoningEffort: 'high' },
164+
},
165+
});
166+
167+
expect((await server.calls[0].requestBodyJson).reasoning).toStrictEqual(
168+
{ effort: 'high' },
169+
);
170+
});
171+
172+
it('should send reasoning.effort none via providerOptions', async () => {
173+
await createModel().doGenerate({
174+
prompt: TEST_PROMPT,
175+
providerOptions: {
176+
lmstudio: { reasoningEffort: 'none' },
177+
},
178+
});
179+
180+
expect((await server.calls[0].requestBodyJson).reasoning).toStrictEqual(
181+
{ effort: 'none' },
182+
);
183+
});
184+
185+
it('should send reasoning.summary via providerOptions', async () => {
186+
await createModel().doGenerate({
187+
prompt: TEST_PROMPT,
188+
providerOptions: {
189+
lmstudio: { reasoningSummary: 'detailed' },
190+
},
191+
});
192+
193+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
194+
{
195+
"input": [
196+
{
197+
"content": [
198+
{
199+
"text": "Hello",
200+
"type": "input_text",
201+
},
202+
],
203+
"role": "user",
204+
"type": "message",
205+
},
206+
],
207+
"model": "gemma-7b-it",
208+
"reasoning": {
209+
"summary": "detailed",
210+
},
211+
}
212+
`);
213+
});
214+
215+
it('should send reasoning.summary concise via providerOptions', async () => {
216+
await createModel().doGenerate({
217+
prompt: TEST_PROMPT,
218+
providerOptions: {
219+
lmstudio: { reasoningSummary: 'concise' },
220+
},
221+
});
222+
223+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
224+
{
225+
"input": [
226+
{
227+
"content": [
228+
{
229+
"text": "Hello",
230+
"type": "input_text",
231+
},
232+
],
233+
"role": "user",
234+
"type": "message",
235+
},
236+
],
237+
"model": "gemma-7b-it",
238+
"reasoning": {
239+
"summary": "concise",
240+
},
241+
}
242+
`);
243+
});
244+
245+
it('should combine reasoning effort and summary from providerOptions', async () => {
246+
await createModel().doGenerate({
247+
prompt: TEST_PROMPT,
248+
providerOptions: {
249+
lmstudio: { reasoningEffort: 'high', reasoningSummary: 'auto' },
250+
},
251+
});
252+
253+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
254+
{
255+
"input": [
256+
{
257+
"content": [
258+
{
259+
"text": "Hello",
260+
"type": "input_text",
261+
},
262+
],
263+
"role": "user",
264+
"type": "message",
265+
},
266+
],
267+
"model": "gemma-7b-it",
268+
"reasoning": {
269+
"effort": "high",
270+
"summary": "auto",
271+
},
272+
}
273+
`);
274+
});
275+
276+
it('should not set reasoning when providerOptions has no reasoning fields', async () => {
277+
await createModel().doGenerate({
278+
prompt: TEST_PROMPT,
279+
providerOptions: {
280+
lmstudio: {},
281+
},
282+
});
283+
284+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
285+
{
286+
"input": [
287+
{
288+
"content": [
289+
{
290+
"text": "Hello",
291+
"type": "input_text",
292+
},
293+
],
294+
"role": "user",
295+
"type": "message",
296+
},
297+
],
298+
"model": "gemma-7b-it",
299+
}
300+
`);
301+
});
302+
303+
it('should not set reasoning when not specified', async () => {
304+
await createModel().doGenerate({
305+
prompt: TEST_PROMPT,
306+
});
307+
308+
expect(
309+
(await server.calls[0].requestBodyJson).reasoning,
310+
).toBeUndefined();
311+
});
312+
});
313+
153314
describe('tool call parsing', () => {
154315
let result: LanguageModelV3GenerateResult;
155316

packages/open-responses/src/responses/open-responses-language-model.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
createJsonErrorResponseHandler,
1616
createJsonResponseHandler,
1717
jsonSchema,
18+
parseProviderOptions,
1819
ParseResult,
1920
postJsonToApi,
2021
} from '@ai-sdk/provider-utils';
@@ -30,6 +31,7 @@ import {
3031
} from './open-responses-api';
3132
import { mapOpenResponsesFinishReason } from './map-open-responses-finish-reason';
3233
import { OpenResponsesConfig } from './open-responses-config';
34+
import { openResponsesOptionsSchema } from './open-responses-options';
3335

3436
export class OpenResponsesLanguageModel implements LanguageModelV3 {
3537
readonly specificationVersion = 'v3';
@@ -127,6 +129,12 @@ export class OpenResponsesLanguageModel implements LanguageModelV3 {
127129
}
128130
: undefined;
129131

132+
const openResponsesOptions = await parseProviderOptions({
133+
provider: this.config.providerOptionsName,
134+
providerOptions,
135+
schema: openResponsesOptionsSchema,
136+
});
137+
130138
return {
131139
body: {
132140
model: this.modelId,
@@ -137,6 +145,18 @@ export class OpenResponsesLanguageModel implements LanguageModelV3 {
137145
top_p: topP,
138146
presence_penalty: presencePenalty,
139147
frequency_penalty: frequencyPenalty,
148+
reasoning:
149+
openResponsesOptions?.reasoningEffort != null ||
150+
openResponsesOptions?.reasoningSummary != null
151+
? {
152+
...(openResponsesOptions?.reasoningEffort != null && {
153+
effort: openResponsesOptions.reasoningEffort,
154+
}),
155+
...(openResponsesOptions?.reasoningSummary != null && {
156+
summary: openResponsesOptions.reasoningSummary,
157+
}),
158+
}
159+
: undefined,
140160
tools: functionTools?.length ? functionTools : undefined,
141161
tool_choice: convertedToolChoice,
142162
...(textFormat != null && { text: { format: textFormat } }),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { InferSchema, lazySchema, zodSchema } from '@ai-sdk/provider-utils';
2+
import { z } from 'zod/v4';
3+
4+
export const openResponsesOptionsSchema = lazySchema(() =>
5+
zodSchema(
6+
z.object({
7+
reasoningEffort: z
8+
.enum(['none', 'low', 'medium', 'high', 'xhigh'])
9+
.nullish(),
10+
/**
11+
* Controls reasoning summary output from the model.
12+
* Valid values: 'concise', 'detailed', 'auto'.
13+
*/
14+
reasoningSummary: z.enum(['concise', 'detailed', 'auto']).nullish(),
15+
}),
16+
),
17+
);
18+
19+
export type OpenResponsesOptions = InferSchema<
20+
typeof openResponsesOptionsSchema
21+
>;

0 commit comments

Comments
 (0)