diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 787a2898..cf957ad0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Azure OpenAI Responses and OpenAI Responses error formatting to prefix HTTP status codes onto `errorMessage`, so transient 5xx and 429 errors are correctly matched by the agent-level auto-retry classifier ([#4232](https://github.com/earendil-works/pi/issues/4232)). - Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)). - Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)). diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 5d4bc9c6..918ff9f5 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -40,6 +40,22 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: return mappedDeployment || model.id; } +function formatAzureOpenAIError(error: unknown): string { + if (error instanceof Error) { + const status = (error as Error & { status?: unknown }).status; + const statusCode = typeof status === "number" ? status : undefined; + if (statusCode !== undefined) { + return `Azure OpenAI API error (${statusCode}): ${error.message}`; + } + return error.message; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + // Azure OpenAI Responses-specific options export interface AzureOpenAIResponsesOptions extends StreamOptions { reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -119,7 +135,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" delete (block as { partialJson?: string }).partialJson; } output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + output.errorMessage = formatAzureOpenAIError(error); stream.push({ type: "error", reason: output.stopReason, error: output }); stream.end(); } diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 8cc446d3..c3881a0f 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -51,6 +51,22 @@ function getPromptCacheRetention( return cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined; } +function formatOpenAIResponsesError(error: unknown): string { + if (error instanceof Error) { + const status = (error as Error & { status?: unknown }).status; + const statusCode = typeof status === "number" ? status : undefined; + if (statusCode !== undefined) { + return `OpenAI API error (${statusCode}): ${error.message}`; + } + return error.message; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + // OpenAI Responses-specific options export interface OpenAIResponsesOptions extends StreamOptions { reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -130,7 +146,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes delete (block as { partialJson?: string }).partialJson; } output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + output.errorMessage = formatOpenAIResponsesError(error); stream.push({ type: "error", reason: output.stopReason, error: output }); stream.end(); }