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
6 changes: 6 additions & 0 deletions .changeset/light-apes-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@ai-sdk/openai': patch
'@ai-sdk/azure': patch
---

Set the annotations from the Responses API to doStream
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import 'dotenv/config';
async function main() {
// Basic text generation
const result = streamText({
model: azure.responses('gpt-5-mini'), // use your own deployment
model: azure.responses('gpt-4.1-mini'), // use your own deployment
prompt:
'Create a program that generates five random numbers between 1 and 100 with two decimal places, and show me the execution results.',
'Create a program that generates five random numbers between 1 and 100 with two decimal places, and show me the execution results. Also save the result to a file.',
tools: {
code_interpreter: azure.tools.codeInterpreter(),
},
Expand All @@ -27,6 +27,15 @@ async function main() {
console.log('\n=== Other Outputs ===');
console.log(await result.toolCalls);
console.log(await result.toolResults);
console.log('\n=== Code Interpreter Annotations ===');
for await (const part of result.fullStream) {
if (part.type === 'text-end') {
const annotations = part.providerMetadata?.openai?.annotations;
if (annotations) {
console.dir(annotations);
}
}
}
}

main().catch(console.error);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async function main() {
const result = streamText({
model: openai.responses('gpt-4.1-mini'),
prompt:
'Create a program that generates five random numbers between 1 and 100 with two decimal places, and show me the execution results.',
'Create a program that generates five random numbers between 1 and 100 with two decimal places, and show me the execution results. Also save the result to a file.',
tools: {
code_interpreter: openai.tools.codeInterpreter({}),
},
Expand All @@ -20,6 +20,15 @@ async function main() {
console.log('\n=== Other Outputs ===');
console.log(await result.toolCalls);
console.log(await result.toolResults);
console.log('\n=== Code Interpreter Annotations ===');
for await (const part of result.fullStream) {
if (part.type === 'text-end') {
const annotations = part.providerMetadata?.openai?.annotations;
if (annotations) {
console.dir(annotations);
}
}
}
}

main().catch(console.error);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DefaultChatTransport } from 'ai';
import ChatInput from '@/components/chat-input';
import { OpenAICodeInterpreterMessage } from '@/app/api/chat-openai-code-interpreter/route';
import CodeInterpreterView from '@/components/tool/openai-code-interpreter-view';
import { OpenaiResponsesText } from '@/components/tool/openai-responses-text';

export default function TestOpenAIWebSearch() {
const { status, sendMessage, messages } =
Expand All @@ -24,7 +25,7 @@ export default function TestOpenAIWebSearch() {
{message.parts.map((part, index) => {
switch (part.type) {
case 'text':
return <div key={index}>{part.text}</div>;
return <OpenaiResponsesText key={index} part={part} />;
case 'tool-code_interpreter':
return <CodeInterpreterView key={index} invocation={part} />;
}
Expand Down
82 changes: 82 additions & 0 deletions examples/next-openai/components/tool/openai-responses-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client';

import { Response } from '@/components/ai-elements/response';
import { TextUIPart } from 'ai';
import { z } from 'zod/v4';

export const openaiResponsesTextUIPartProviderMetadataSchema = z.object({
openai: z.object({
itemId: z.string(),
annotations: z
.array(
z.discriminatedUnion('type', [
z.object({
type: z.literal('url_citation'),
url: z.string(),
title: z.string(),
start_index: z.number(),
end_index: z.number(),
}),
z.object({
type: z.literal('file_citation'),
file_id: z.string(),
filename: z.string(),
index: z.number(),
quote: z.string().nullish(),
}),
z.object({
type: z.literal('container_file_citation'),
container_id: z.string(),
file_id: z.string(),
filename: z.string(),
start_index: z.number(),
end_index: z.number(),
}),
]),
)
.optional(),
}),
});

export function OpenaiResponsesText({ part }: { part: TextUIPart }) {
if (!part.providerMetadata) return <Response>{part.text}</Response>;

const providerMetadataParsed =
openaiResponsesTextUIPartProviderMetadataSchema.safeParse(
part.providerMetadata,
);

if (!providerMetadataParsed.success) return <Response>{part.text}</Response>;

const { annotations } = providerMetadataParsed.data.openai;
if (!annotations) return <Response>{part.text}</Response>;

const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';

// Sort annotations by start_index in descending order to process from end to start.
// This ensures that string modifications don't invalidate indices of earlier annotations.
const sortedAnnotations = [...annotations].sort((a, b) => {
const aStart = 'start_index' in a ? a.start_index : -1;
const bStart = 'start_index' in b ? b.start_index : -1;
return bStart - aStart;
});

const text = sortedAnnotations.reduce<string>((acc, cur) => {
const text = (() => {
switch (cur.type) {
case 'container_file_citation':
if (cur.start_index === 0 && cur.end_index === 0) return acc;
return (
acc.slice(0, cur.start_index) +
`${baseUrl}/api/download-container-file?container_id=${encodeURIComponent(cur.container_id)}&file_id=${encodeURIComponent(cur.file_id)}&filename=${encodeURIComponent(cur.filename)}` +
acc.slice(cur.end_index)
);
default:
return acc;
}
})();
return text;
}, part.text);

return <Response>{text}</Response>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2059,6 +2059,11 @@ Generated numbers (formatted to 2 decimal places): 88.99, 51.21, 89.85, 88.97, 7
},
{
"id": "msg_0ad69c3c5fcb01f60068eba7956c948193be6ab573376051d6",
"providerMetadata": {
"openai": {
"itemId": "msg_0ad69c3c5fcb01f60068eba7956c948193be6ab573376051d6",
},
},
"type": "text-end",
},
{
Expand Down Expand Up @@ -2603,6 +2608,19 @@ providers and models, and which ones are available in the AI SDK.",
},
{
"id": "msg_06456cb9918b63780068cacd7c922081a1ae15f2672a51980f",
"providerMetadata": {
"openai": {
"annotations": [
{
"file_id": "file-Ebzhf8H4DPGPr9pUhr7n7v",
"filename": "ai.pdf",
"index": 379,
"type": "file_citation",
},
],
"itemId": "msg_06456cb9918b63780068cacd7c922081a1ae15f2672a51980f",
},
},
"type": "text-end",
},
{
Expand Down Expand Up @@ -3109,6 +3127,25 @@ exports[`responses > file search tool > should stream file search results withou
},
{
"id": "msg_0459517ad68504ad0068cabfc6b5c48192a15ac773668537f1",
"providerMetadata": {
"openai": {
"annotations": [
{
"file_id": "file-Ebzhf8H4DPGPr9pUhr7n7v",
"filename": "ai.pdf",
"index": 154,
"type": "file_citation",
},
{
"file_id": "file-Ebzhf8H4DPGPr9pUhr7n7v",
"filename": "ai.pdf",
"index": 382,
"type": "file_citation",
},
],
"itemId": "msg_0459517ad68504ad0068cabfc6b5c48192a15ac773668537f1",
},
},
"type": "text-end",
},
{
Expand Down Expand Up @@ -4976,6 +5013,20 @@ exports[`responses > web search preview tool > should stream web search preview
},
{
"id": "msg_0dcf1a118189f28100691daa5a28488194b14fd6fa1d2895be",
"providerMetadata": {
"openai": {
"annotations": [
{
"end_index": 577,
"start_index": 458,
"title": "While You Were Sleeping: 5 stories you might have missed, Nov 19, 2025",
"type": "url_citation",
"url": "https://www.straitstimes.com/world/while-you-were-sleeping-5-stories-you-might-have-missed-nov-19-2025",
},
],
"itemId": "msg_0dcf1a118189f28100691daa5a28488194b14fd6fa1d2895be",
},
},
"type": "text-end",
},
{
Expand Down
24 changes: 24 additions & 0 deletions packages/azure/src/azure-openai-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,11 @@ describe('responses', () => {
},
{
"id": "msg_67c9a8787f4c8190b49c858d4c1cf20c",
"providerMetadata": {
"openai": {
"itemId": "msg_67c9a8787f4c8190b49c858d4c1cf20c",
},
},
"type": "text-end",
},
{
Expand Down Expand Up @@ -1559,6 +1564,25 @@ describe('responses', () => {
},
{
"id": "msg_456",
"providerMetadata": {
"openai": {
"annotations": [
{
"file_id": "assistant-YRcoCqn3Fo2K4JgraG",
"filename": "resource1.json",
"index": 145,
"type": "file_citation",
},
{
"file_id": "assistant-YRcoCqn3Fo2K4JgraG",
"filename": "resource1.json",
"index": 192,
"type": "file_citation",
},
],
"itemId": "msg_456",
},
},
"type": "text-end",
},
{
Expand Down
Loading
Loading