Last active
March 26, 2026 17:49
-
-
Save ccarse/d88095dbc02ac096cfbb03d7af12a44b to your computer and use it in GitHub Desktop.
Minimal repro: OpenAI Responses API server_error during streaming with web_search tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env npx tsx | |
| /** | |
| * Minimal reproduction: OpenAI Responses API server_error during long-running | |
| * streams with server-executed web_search. | |
| * | |
| * The error surfaces as a stream event mid-response: | |
| * {"type":"error","sequence_number":N,"error":{"type":"server_error",...}} | |
| * | |
| * Root cause: a single Responses API call with many server-executed | |
| * web_search tool calls eventually hits server_error. No tool loop, no | |
| * context accumulation — just one long-running request. | |
| * | |
| * Observed at sequence_numbers: 235, 315, 460 (prod), 1488 (this script). | |
| * Frequency: intermittent, ~1 in 10-15 runs. | |
| * | |
| * Request IDs for OpenAI investigation: | |
| * req_efc5957215ea459d9b79599c14421820 (repro, 2026-03-26) | |
| * req_ccd3232a3f6d4df681d605f13b43900f (prod, 2026-03-26) | |
| * req_60d48e93f8384981a3a98107cd986ad3 (prod, 2026-03-25) | |
| * req_62a1708d38f3431ca534205106f592e5 (prod, 2026-03-23) | |
| * | |
| * Usage: | |
| * OPENAI_API_KEY=sk-... npx tsx scripts/repro-server-error.ts | |
| * | |
| * Requires: @ai-sdk/openai, ai (already in agent-zero deps) | |
| */ | |
| import { openai } from '@ai-sdk/openai'; | |
| import { convertToModelMessages, streamText, stepCountIs, type UIMessage } from 'ai'; | |
| const model = openai('gpt-5.2'); | |
| const tools = { | |
| web_search: openai.tools.webSearch({}), | |
| }; | |
| const SYSTEM_PROMPT = `You are a product research assistant with access to web_search. | |
| Your task: research each product thoroughly. For EVERY product listed by the user: | |
| - Use web_search to find current pricing, specs, and availability | |
| - Search for each product individually — do not batch or skip any | |
| Be thorough — do not skip any products. Research each one individually.`; | |
| const USER_MESSAGE = `Research the following products and compile a detailed comparison report with pricing, specs, and availability for each: | |
| 1. Milwaukee M18 FUEL hammer drill | |
| 2. DeWalt 20V MAX XR impact driver | |
| 3. Makita 18V LXT circular saw | |
| 4. Bosch 12V Max drill/driver kit | |
| 5. Hilti TE 6-A22 rotary hammer | |
| 6. Festool TID 18 impact driver | |
| 7. Metabo HPT 36V MultiVolt circular saw | |
| 8. Ridgid 18V brushless router | |
| 9. Ryobi ONE+ HP compact router | |
| 10. Milwaukee M12 FUEL installation drill | |
| 11. DeWalt ATOMIC 20V MAX oscillating tool | |
| 12. Makita 40V Max XGT reciprocating saw | |
| 13. Bosch 18V brushless jigsaw | |
| 14. Hilti SF 6H-A22 drill driver | |
| 15. Milwaukee M18 FUEL Sawzall | |
| For each product provide: current retail price, specs, weight, battery compatibility, and where to buy.`; | |
| async function main() { | |
| console.log(`Starting long-running tool-loop stream at ${new Date().toISOString()}`); | |
| console.log('Model: gpt-5.2 (Responses API)'); | |
| console.log('Tools: web_search'); | |
| console.log('Message path: UIMessage[] → convertToModelMessages() → streamText()'); | |
| console.log('Expecting server_error intermittently (~1 in 10-15 runs).\n'); | |
| const startTime = Date.now(); | |
| let stepCount = 0; | |
| const messages: UIMessage[] = [ | |
| { | |
| id: 'msg_user_1', | |
| role: 'user', | |
| parts: [{ type: 'text', text: USER_MESSAGE }], | |
| }, | |
| ]; | |
| const modelMessages = await convertToModelMessages(messages, { | |
| tools, | |
| ignoreIncompleteToolCalls: true, | |
| }); | |
| try { | |
| const result = streamText({ | |
| model, | |
| system: SYSTEM_PROMPT, | |
| messages: modelMessages, | |
| tools, | |
| stopWhen: stepCountIs(80), | |
| providerOptions: { | |
| openai: { | |
| reasoningSummary: 'auto', | |
| reasoningEffort: 'low', | |
| textVerbosity: 'low', | |
| }, | |
| }, | |
| onStepFinish: ({ stepNumber, finishReason, toolCalls }) => { | |
| stepCount = stepNumber; | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| const toolNames = toolCalls?.map((tc) => tc.toolName).join(', ') || 'none'; | |
| console.log(` [step ${stepNumber}] finishReason=${finishReason} tools=[${toolNames}] elapsed=${elapsed}s`); | |
| }, | |
| onError: ({ error }) => { | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| console.error(`\n*** STREAM ERROR at ${elapsed}s (step ~${stepCount}) ***`); | |
| console.error(JSON.stringify(error, null, 2)); | |
| }, | |
| }); | |
| // Ring buffer of last 20 events for debugging | |
| const recentEvents: { type: string; summary: string }[] = []; | |
| const RING_SIZE = 20; | |
| for await (const part of result.fullStream) { | |
| // Summarize each event compactly | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const p = part as any; | |
| const summary = | |
| p.type === 'text-delta' | |
| ? `"${String(p.textDelta).slice(0, 40)}…"` | |
| : p.type === 'tool-call' | |
| ? `toolName=${p.toolName}` | |
| : p.type === 'tool-result' | |
| ? `toolName=${p.toolName}` | |
| : p.type === 'step-finish' | |
| ? `finishReason=${p.finishReason}` | |
| : ''; | |
| recentEvents.push({ type: p.type, summary }); | |
| if (recentEvents.length > RING_SIZE) recentEvents.shift(); | |
| if (part.type === 'error') { | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| console.error(`\n*** ERROR PART at ${elapsed}s ***`); | |
| console.error(JSON.stringify(part.error, null, 2)); | |
| console.error(`\nLast ${RING_SIZE} stream events before error:`); | |
| for (const evt of recentEvents) { | |
| console.error(` ${evt.type}${evt.summary ? ` — ${evt.summary}` : ''}`); | |
| } | |
| } | |
| } | |
| const finishReason = await result.finishReason; | |
| const usage = await result.totalUsage; | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| console.log(`\nStream completed in ${elapsed}s`); | |
| console.log(` finishReason: ${finishReason}`); | |
| console.log(` steps: ${stepCount}`); | |
| console.log(` totalUsage: ${JSON.stringify(usage)}`); | |
| } catch (error) { | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| console.error(`\n*** THROWN ERROR at ${elapsed}s (step ~${stepCount}) ***`); | |
| if (error instanceof Error) { | |
| console.error(` ${error.name}: ${error.message}`); | |
| } else { | |
| console.error(JSON.stringify(error, null, 2)); | |
| } | |
| process.exit(1); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment