Skip to content

Commit 64aa48f

Browse files
authored
feat(provider/azure): Responses API web search preview is enabled (#10370)
## Background Web Search Preview has become available on Microsoft Azure OpenAI. We enabled the use of `web_search_preview` from `@ai-sdk/openai` within `@ai-sdk/azure`. [https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/web-search?view=foundry-classic](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/web-search?view=foundry-classic) ## Summary Using Next.js, it is now possible to perform web searches with both `useChat` and `streamText`. * We enabled Azure’s webSearchPreview tool by routing through the OpenAI internal implementation. * In `doGenerate`, the Zod type definition for `url_citation` was updated to support annotations. * In `doStream`, there were some missing pieces when switching between `web_search` and `web_search_preview`. * add description `web_search_preview` in azure provider web page. ## Manual Verification * Added verification for `web_search_preview` in Azure’s CI/CD tests. `packages/azure/src/__fixtures__/azure-web-search-preview-tool.1.chunks.txt` `packages/azure/src/__fixtures__/azure-web-search-preview-tool.1.json` * Added tests for `generateText` and `streamText` in `examples/ai-core`. `generateText` correctly includes the `url_citation` annotation, while `streamText` currently does not output it. This is expected to be resolved once PR #10253 is merged. `examples/ai-core/src/generate-text/azure-responses-web-search-preview.ts' `examples/ai-core/src/stream-text/azure-responses-web-search-preview.ts' * Added a working Next.js example demonstrating web search in `examples/next-openai`. `http://localhost:3000/test-azure-web-search-preview` `examples/next-openai/app/test-azure-web-search-preview/page.tsx` <img width="908" height="1032" alt="image" src="https://github.com/user-attachments/assets/b51bf4b8-ffb2-4106-9859-a3b017128aae" /> ## Future Work Since `streamText` does not currently output annotations, we need to confirm that this is resolved once PR #10253 is merged. ## Related Issues #10253 --------- Co-authored-by: tsuzaki430 <[email protected]>
1 parent 86b6c0a commit 64aa48f

File tree

16 files changed

+2767
-3
lines changed

16 files changed

+2767
-3
lines changed

.changeset/small-balloons-eat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/openai': patch
3+
'@ai-sdk/azure': patch
4+
---
5+
6+
Azure OpenAI enabled web-search-preview

content/providers/01-ai-sdk-providers/04-azure.mdx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,49 @@ The following OpenAI-specific metadata is returned:
340340
not supported when using 'azure.chat' or 'azure.completion'
341341
</Note>
342342

343+
#### Web Search Tool
344+
345+
The Azure OpenAI responses API supports web search(preview) through the `azure.tools.webSearchPreview` tool.
346+
347+
```ts
348+
const result = await generateText({
349+
model: azure('gpt-4.1-mini'),
350+
prompt: 'What happened in San Francisco last week?',
351+
tools: {
352+
web_search_preview: azure.tools.webSearchPreview({
353+
// optional configuration:
354+
searchContextSize: 'low',
355+
userLocation: {
356+
type: 'approximate',
357+
city: 'San Francisco',
358+
region: 'California',
359+
},
360+
}),
361+
},
362+
// Force web search tool (optional):
363+
toolChoice: { type: 'tool', toolName: 'web_search_preview' },
364+
});
365+
366+
console.log(result.text);
367+
368+
// URL sources directly from `results`
369+
const sources = result.sources;
370+
for (const source of sources) {
371+
console.log('source:', source);
372+
}
373+
```
374+
375+
<Note>
376+
The tool must be named `web_search_preview` when using Azure OpenAI's web
377+
search(preview) functionality. This name is required by Azure OpenAI's API
378+
specification and cannot be customized.
379+
</Note>
380+
381+
<Note>
382+
The 'web_search_preview' tool is only supported with the default responses
383+
API, and is not supported when using 'azure.chat' or 'azure.completion'
384+
</Note>
385+
343386
#### File Search Tool
344387

345388
The Azure OpenAI provider supports file search through the `azure.tools.fileSearch` tool.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { azure } from '@ai-sdk/azure';
2+
import { generateText } from 'ai';
3+
import 'dotenv/config';
4+
5+
/**
6+
* prepare
7+
* Please add parameters in your .env file for initialize Azure OpenAI..
8+
* AZURE_RESOURCE_NAME="<your_resource_name>"
9+
* AZURE_API_KEY="<your_api_key>"
10+
*/
11+
12+
async function main() {
13+
// Basic text generation
14+
const basicResult = await generateText({
15+
model: azure.responses('gpt-4.1-mini'),
16+
prompt: 'Summarize three major news stories from today.',
17+
tools: {
18+
web_search_preview: azure.tools.webSearchPreview({
19+
searchContextSize: 'low',
20+
}),
21+
},
22+
});
23+
24+
console.log('\n=== Basic Text Generation ===');
25+
console.log(basicResult.text);
26+
console.log('\n=== Other Outputs ===');
27+
console.dir(basicResult.toolCalls, { depth: Infinity });
28+
console.dir(basicResult.toolResults, { depth: Infinity });
29+
console.log('\n=== Web Search Preview Annotations ===');
30+
for (const part of basicResult.content) {
31+
if (part.type === 'text') {
32+
const annotations = part.providerMetadata?.openai?.annotations;
33+
if (annotations) {
34+
console.dir(annotations);
35+
}
36+
}
37+
}
38+
for (const step of basicResult.steps) {
39+
if (step.warnings) {
40+
console.log(step.warnings);
41+
}
42+
}
43+
}
44+
45+
main().catch(console.error);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { azure } from '@ai-sdk/azure';
2+
import { streamText } from 'ai';
3+
import 'dotenv/config';
4+
5+
/**
6+
* prepare
7+
* Please add parameters in your .env file for initialize Azure OpenAI..
8+
* AZURE_RESOURCE_NAME="<your_resource_name>"
9+
* AZURE_API_KEY="<your_api_key>"
10+
*/
11+
12+
async function main() {
13+
// Basic text generation
14+
const result = streamText({
15+
model: azure.responses('gpt-4.1-mini'), // use your own deployment
16+
prompt: 'Summarize three major news stories from today.',
17+
tools: {
18+
web_search_preview: azure.tools.webSearchPreview({
19+
searchContextSize: 'low',
20+
}),
21+
},
22+
});
23+
24+
console.log('\n=== Basic Text Generation ===');
25+
for await (const textPart of result.textStream) {
26+
process.stdout.write(textPart);
27+
}
28+
console.log('\n=== Other Outputs ===');
29+
console.log(await result.toolCalls);
30+
console.log(await result.toolResults);
31+
console.log('\n=== Web Search Preview Annotations ===');
32+
for await (const part of result.fullStream) {
33+
switch (part.type) {
34+
case 'text-end':
35+
{
36+
const annotations = part.providerMetadata?.openai?.annotations;
37+
if (annotations) {
38+
console.dir(annotations);
39+
}
40+
}
41+
break;
42+
43+
case 'source':
44+
if (part.sourceType === 'url') {
45+
console.log(`\n[source: ${part.url}]`);
46+
}
47+
break;
48+
49+
case 'error':
50+
console.log('error');
51+
console.error(part.error);
52+
break;
53+
}
54+
}
55+
}
56+
57+
main().catch(console.error);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { azure } from '@ai-sdk/azure';
2+
import {
3+
convertToModelMessages,
4+
InferUITools,
5+
streamText,
6+
ToolSet,
7+
UIDataTypes,
8+
UIMessage,
9+
} from 'ai';
10+
11+
const tools = {
12+
web_search_preview: azure.tools.webSearchPreview({}),
13+
} satisfies ToolSet;
14+
15+
export type AzureWebSearchPreviewMessage = UIMessage<
16+
never,
17+
UIDataTypes,
18+
InferUITools<typeof tools>
19+
>;
20+
21+
export async function POST(req: Request) {
22+
const { messages }: { messages: AzureWebSearchPreviewMessage[] } =
23+
await req.json();
24+
25+
const prompt = convertToModelMessages(messages);
26+
27+
const result = streamText({
28+
model: azure.responses('gpt-4.1-mini'),
29+
prompt,
30+
tools: {
31+
web_search_preview: azure.tools.webSearchPreview({
32+
searchContextSize: 'low',
33+
}),
34+
},
35+
});
36+
37+
return result.toUIMessageStreamResponse({
38+
sendSources: true,
39+
});
40+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client';
2+
3+
import { Response } from '@/components/ai-elements/response';
4+
import ChatInput from '@/components/chat-input';
5+
import { ReasoningView } from '@/components/reasoning-view';
6+
import SourcesView from '@/components/sources-view';
7+
import { useChat } from '@ai-sdk/react';
8+
import { DefaultChatTransport } from 'ai';
9+
import { AzureWebSearchPreviewMessage } from '@/app/api/chat-azure-web-search-preview/route';
10+
import AzureWebSearchPreviewView from '@/components/tool/azure-web-search-preview-view';
11+
12+
export default function TestOpenAIWebSearch() {
13+
const { error, status, sendMessage, messages, regenerate } =
14+
useChat<AzureWebSearchPreviewMessage>({
15+
transport: new DefaultChatTransport({
16+
api: '/api/chat-azure-web-search-preview',
17+
}),
18+
onError: error => {
19+
console.error('Chat error:', error);
20+
},
21+
});
22+
23+
return (
24+
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
25+
<h1 className="mb-4 text-xl font-bold">
26+
Azure OpenAI Web Search Preview
27+
</h1>
28+
29+
{messages.map(message => (
30+
<div key={message.id} className="whitespace-pre-wrap">
31+
{message.role === 'user' ? 'User: ' : 'AI: '}
32+
{message.parts.map((part, index) => {
33+
switch (part.type) {
34+
case 'text': {
35+
return <Response key={index}>{part.text}</Response>;
36+
}
37+
case 'reasoning': {
38+
return <ReasoningView part={part} key={index} />;
39+
}
40+
case 'tool-web_search_preview': {
41+
return (
42+
<AzureWebSearchPreviewView invocation={part} key={index} />
43+
);
44+
}
45+
}
46+
})}
47+
48+
<SourcesView
49+
sources={message.parts.filter(part => part.type === 'source-url')}
50+
/>
51+
</div>
52+
))}
53+
54+
{error && (
55+
<div className="mt-4">
56+
<div className="text-red-500">An error occurred.</div>
57+
<button
58+
type="button"
59+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
60+
onClick={() => regenerate()}
61+
>
62+
Retry
63+
</button>
64+
</div>
65+
)}
66+
67+
<ChatInput status={status} onSubmit={text => sendMessage({ text })} />
68+
</div>
69+
);
70+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { azure } from '@ai-sdk/azure';
2+
import { UIToolInvocation } from 'ai';
3+
4+
export default function AzureWebSearchPreviewView({
5+
invocation,
6+
}: {
7+
invocation: UIToolInvocation<ReturnType<typeof azure.tools.webSearchPreview>>;
8+
}) {
9+
switch (invocation.state) {
10+
case 'input-available': {
11+
return (
12+
<div className="flex flex-col gap-2 p-3 bg-blue-50 rounded border-l-4 border-blue-400 shadow">
13+
<div className="flex items-center font-semibold text-blue-700">
14+
<span className="inline-block mr-2 bg-blue-200 text-blue-900 rounded px-2 py-0.5 text-xs font-mono tracking-wider">
15+
SEARCH
16+
</span>
17+
Searching the web...
18+
</div>
19+
</div>
20+
);
21+
}
22+
case 'output-available': {
23+
const output = invocation.output;
24+
25+
switch (output.action?.type) {
26+
case 'search':
27+
return (
28+
<div className="flex flex-col gap-2 p-3 bg-blue-50 rounded border-l-4 border-blue-400 shadow">
29+
<div className="flex items-center font-semibold text-blue-700">
30+
<span className="inline-block mr-2 bg-blue-200 text-blue-900 rounded px-2 py-0.5 text-xs font-mono tracking-wider">
31+
SEARCH
32+
</span>
33+
Searched the web
34+
</div>
35+
<div className="pl-5 text-sm text-blue-800">
36+
<span className="font-semibold">Query:</span>{' '}
37+
<span className="inline-block bg-white border border-blue-100 rounded px-2 py-0.5 font-mono">
38+
{output.action.query}
39+
</span>
40+
</div>
41+
</div>
42+
);
43+
case 'openPage':
44+
return (
45+
<div className="flex flex-col gap-2 p-3 bg-green-50 rounded border-l-4 border-green-500 shadow">
46+
<div className="flex items-center font-semibold text-green-800">
47+
<span className="inline-block mr-2 bg-green-200 text-green-900 rounded px-2 py-0.5 text-xs font-mono tracking-wider">
48+
OPEN PAGE
49+
</span>
50+
Opened a page
51+
</div>
52+
<div className="pl-5 text-sm text-green-900 break-all">
53+
<span className="font-semibold">URL:</span>{' '}
54+
<a
55+
href={output.action.url}
56+
target="_blank"
57+
rel="noopener noreferrer"
58+
className="underline hover:text-green-700"
59+
>
60+
{output.action.url}
61+
</a>
62+
</div>
63+
</div>
64+
);
65+
case 'find':
66+
return (
67+
<div className="flex flex-col gap-2 p-3 bg-yellow-50 rounded border-l-4 border-yellow-500 shadow">
68+
<div className="flex items-center font-semibold text-yellow-800">
69+
<span className="inline-block mr-2 bg-yellow-200 text-yellow-900 rounded px-2 py-0.5 text-xs font-mono tracking-wider">
70+
FIND
71+
</span>
72+
Searched for pattern in page
73+
</div>
74+
<div className="pl-5 text-sm text-yellow-900">
75+
<span className="font-semibold">Pattern:</span>{' '}
76+
<span className="inline-block bg-white border border-yellow-100 rounded px-2 py-0.5 font-mono">
77+
{output.action.pattern}
78+
</span>
79+
</div>
80+
<div className="pl-5 text-sm text-yellow-900 break-all">
81+
<span className="font-semibold">In URL:</span>{' '}
82+
<a
83+
href={output.action.url}
84+
target="_blank"
85+
rel="noopener noreferrer"
86+
className="underline hover:text-yellow-700"
87+
>
88+
{output.action.url}
89+
</a>
90+
</div>
91+
</div>
92+
);
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)