Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/anthropic-extended-thinking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-extensions': patch
---

feat(agents-extensions): #628 add Anthropic extended thinking support
78 changes: 78 additions & 0 deletions packages/agents-extensions/src/aiSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,23 @@ export class AiSdkModel implements Model {
const output: ModelResponse['output'] = [];

const resultContent = (result as any).content ?? [];

// Extract and add reasoning items FIRST (required by Anthropic: thinking blocks must precede tool_use blocks)
const reasoningParts = resultContent.filter(
(c: any) => c && c.type === 'reasoning',
);
for (const reasoningPart of reasoningParts) {
const reasoningText =
typeof reasoningPart.text === 'string' ? reasoningPart.text : '';
output.push({
type: 'reasoning',
content: [{ type: 'input_text', text: reasoningText }],
rawContent: [{ type: 'reasoning_text', text: reasoningText }],
// Preserve provider-specific metadata (including signature for Anthropic extended thinking)
providerData: reasoningPart.providerMetadata ?? undefined,
});
}

const toolCalls = resultContent.filter(
(c: any) => c && c.type === 'tool-call',
);
Expand Down Expand Up @@ -908,6 +925,15 @@ export class AiSdkModel implements Model {
const functionCalls: Record<string, protocol.FunctionCallItem> = {};
let textOutput: protocol.OutputText | undefined;

// State for tracking reasoning blocks (for Anthropic extended thinking)
const reasoningBlocks: Record<
string,
{
text: string;
providerMetadata?: Record<string, any>;
}
> = {};

for await (const part of stream) {
if (!started) {
started = true;
Expand All @@ -925,6 +951,40 @@ export class AiSdkModel implements Model {
yield { type: 'output_text_delta', delta: (part as any).delta };
break;
}
case 'reasoning-start': {
// Start tracking a new reasoning block
const reasoningId = (part as any).id ?? 'default';
reasoningBlocks[reasoningId] = {
text: '',
providerMetadata: (part as any).providerMetadata,
};
break;
}
case 'reasoning-delta': {
// Accumulate reasoning text
const reasoningId = (part as any).id ?? 'default';
if (!reasoningBlocks[reasoningId]) {
reasoningBlocks[reasoningId] = {
text: '',
providerMetadata: (part as any).providerMetadata,
};
}
reasoningBlocks[reasoningId].text += (part as any).delta ?? '';
break;
}
case 'reasoning-end': {
// Capture final provider metadata (may contain signature)
const reasoningId = (part as any).id ?? 'default';
if (
reasoningBlocks[reasoningId] &&
(part as any).providerMetadata
) {
reasoningBlocks[reasoningId].providerMetadata = (
part as any
).providerMetadata;
}
break;
}
case 'tool-call': {
const toolCallId = (part as any).toolCallId;
if (toolCallId) {
Expand Down Expand Up @@ -967,6 +1027,24 @@ export class AiSdkModel implements Model {
}

const outputs: protocol.OutputModelItem[] = [];

// Add reasoning items FIRST (required by Anthropic: thinking blocks must precede tool_use blocks)
// Emit reasoning item even when text is empty to preserve signature in providerData for redacted thinking streams
for (const [reasoningId, reasoningBlock] of Object.entries(
reasoningBlocks,
)) {
if (reasoningBlock.text || reasoningBlock.providerMetadata) {
outputs.push({
type: 'reasoning',
id: reasoningId !== 'default' ? reasoningId : undefined,
content: [{ type: 'input_text', text: reasoningBlock.text }],
rawContent: [{ type: 'reasoning_text', text: reasoningBlock.text }],
// Preserve provider-specific metadata (including signature for Anthropic extended thinking)
providerData: reasoningBlock.providerMetadata ?? undefined,
});
}
}

if (textOutput) {
outputs.push({
type: 'message',
Expand Down
Loading