Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/happy-shrimps-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@openai/agents-extensions': patch
'@openai/agents-core': patch
---

feat: Add support for Anthropic extended thinking
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,9 @@
"verdaccio": "^6.2.1",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.20.0"
"packageManager": "pnpm@10.20.0",
"dependencies": {
"@ai-sdk/openai": "^2.0.62",
"@openai/agents-extensions": "^0.2.1"
}
}
5 changes: 5 additions & 0 deletions packages/agents-core/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,11 @@ export const StreamEventResponseCompleted = SharedBase.extend({
* The output from the model.
*/
output: z.array(OutputModelItem),

/**
* The reasoning/thinking text from the model.
*/
reasoning: z.string().optional(),
}),
});

Expand Down
13 changes: 13 additions & 0 deletions packages/agents-extensions/src/aiSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ export class AiSdkModel implements Model {
: ((result as any).usage?.outputTokens ?? 0)) || 0,
}),
output,
reasoning: (result as any).reasoning ?? undefined,
providerData: result,
} as const;

Expand Down Expand Up @@ -874,6 +875,7 @@ export class AiSdkModel implements Model {
let usageCompletionTokens = 0;
const functionCalls: Record<string, protocol.FunctionCallItem> = {};
let textOutput: protocol.OutputText | undefined;
let reasoningText: string | undefined;

for await (const part of stream) {
if (!started) {
Expand Down Expand Up @@ -922,6 +924,16 @@ export class AiSdkModel implements Model {
: ((part as any).usage?.outputTokens ?? 0);
break;
}
case 'reasoning-delta': {
const reasoningDelta = (part as any).reasoningDelta;
if (reasoningDelta) {
if (!reasoningText) {
reasoningText = '';
}
reasoningText += reasoningDelta;
}
break;
}
case 'error': {
throw part.error;
}
Expand Down Expand Up @@ -953,6 +965,7 @@ export class AiSdkModel implements Model {
totalTokens: usagePromptTokens + usageCompletionTokens,
},
output: outputs,
reasoning: reasoningText,
},
Comment on lines 965 to 969

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reasoning text is not emitted as an output item

The new reasoning support only stores Anthropic thinking in a standalone reasoning field when returning responses (reasoning: (result as any).reasoning and finalEvent.response.reasoning). Downstream code that persists or renders agent output (e.g. RunState.toJSON and guardrails) iterates exclusively over modelResponse.output, which still contains only messages and tool calls. Because no protocol.ReasoningItem is appended to that array, the collected reasoning never reaches the agent history or serialization and is effectively dropped. To keep the reasoning visible and consistent with other providers, push a ReasoningItem into output (or include it in the existing message content) before returning.

Useful? React with 👍 / 👎.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i agree with this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will make improvements based on the feedback. Thank you for your review.

};

Expand Down
109 changes: 109 additions & 0 deletions packages/agents-extensions/test/aiSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,115 @@ describe('AiSdkModel.getResponse', () => {
outputTokensDetails: [],
});
});

test('should store reasoning in response for non-streaming text output', async () => {
const mockProviderResult = {
content: [{ type: 'text', text: 'This is the final answer.' }],
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
providerMetadata: { p: 1 },
response: { id: 'fake-id-123' },
finishReason: 'stop',
warnings: [],
reasoning: '<thinking>I am thinking about the answer.</thinking>',
};

const model = new AiSdkModel(
stubModel({
async doGenerate() {
return mockProviderResult as any;
},
}),
);

const res = await withTrace('t', () =>
model.getResponse({
input: 'hi',
tools: [],
handoffs: [],
modelSettings: {},
outputType: 'text',
tracing: false,
} as any),
);

expect(res.reasoning).toBeDefined();
expect(res.reasoning).toBe(
'<thinking>I am thinking about the answer.</thinking>',
);
expect(res.responseId).toBe('fake-id-123');
});

test('should store reasoning in final response_done event for streaming', async () => {
async function* mockProviderStream() {
yield {
type: 'response-metadata',
id: 'fake-stream-id-456',
};

yield {
type: 'reasoning-delta',
reasoningDelta: '<thinking>Step 1: I am thinking.',
};

yield {
type: 'text-delta',
delta: 'Here is the answer.',
};

yield {
type: 'reasoning-delta',
reasoningDelta: ' Step 2: More thinking.</thinking>',
};

yield {
type: 'finish',
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
};
}

const model = new AiSdkModel(
stubModel({
async doStream() {
return {
stream: mockProviderStream(),
} as any;
},
}),
);

const stream = model.getStreamedResponse({
input: 'hi',
tools: [],
handoffs: [],
modelSettings: {},
outputType: 'text',
tracing: false,
} as any);

const events = [];
for await (const event of stream) {
events.push(event);
}

const finalEvent = events.find((e) => e.type === 'response_done') as
| protocol.StreamEventResponseCompleted
| undefined;

expect(finalEvent).toBeDefined();

expect(finalEvent!.response.reasoning).toBeDefined();
expect(finalEvent!.response.reasoning).toBe(
'<thinking>Step 1: I am thinking. Step 2: More thinking.</thinking>',
);

expect(finalEvent!.response.id).toBe('fake-stream-id-456');
expect(finalEvent!.response.usage.totalTokens).toBe(15);

const textOutput = finalEvent!.response.output.find(
(o) => o.type === 'message' && o.content[0].type === 'output_text',
) as any;
expect(textOutput.content[0].text).toBe('Here is the answer.');
});
});

describe('AiSdkModel.getStreamedResponse', () => {
Expand Down
121 changes: 121 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.