From 671099edaa367c03c25f4262fbcd1150c2f750ef Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:36:12 +0000 Subject: [PATCH 1/3] fix(provider): keep bounded provider error summaries in telemetry JUNIOR-8T collapsed stream failures to AI provider error: network with no safe cause text, so gateway HTTP 200 + stopReason error spikes were hard to attribute. Attach a redacted summary on terminal, retry, and chat-span telemetry without raw provider payloads. Co-Authored-By: David Cramer Co-Authored-By: David Cramer --- TELEMETRY.md | 1 + packages/junior/src/chat/agent/index.ts | 13 ++++- packages/junior/src/chat/pi/traced-stream.ts | 16 +++++- .../src/chat/services/provider-error.ts | 44 +++++++++++++++ .../src/chat/services/provider-retry.ts | 27 ++++++++- .../unit/services/provider-retry.test.ts | 56 ++++++++++++++++--- .../services/turn-failure-response.test.ts | 4 ++ 7 files changed, 146 insertions(+), 15 deletions(-) diff --git a/TELEMETRY.md b/TELEMETRY.md index f27e546e1a..92b56e26cb 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -189,6 +189,7 @@ Attributes: `gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.usage.output_tokens`, `gen_ai.usage.input_tokens.cached`, `gen_ai.usage.input_tokens.cache_write`, `app.ai.reasoning_tokens`, `app.ai.empty_output.attempt`, `app.ai.provider_error.kind`, +`app.ai.provider_error.summary`, `app.ai.provider_error.retry_attempt`, `app.guardian.review_attempt`, `app.ai.cost.input_usd`, `app.ai.cost.output_usd`, `app.ai.cost.cache_read_usd`, `app.ai.cost.cache_write_usd`, diff --git a/packages/junior/src/chat/agent/index.ts b/packages/junior/src/chat/agent/index.ts index 4015ad7537..85503bfc2c 100644 --- a/packages/junior/src/chat/agent/index.ts +++ b/packages/junior/src/chat/agent/index.ts @@ -83,7 +83,10 @@ import { getProviderErrorAttributes, isProviderRetryError, } from "@/chat/services/provider-error"; -import { nextProviderRetry } from "@/chat/services/provider-retry"; +import { + getProviderRetryAttributes, + nextProviderRetry, +} from "@/chat/services/provider-retry"; import { nextEmptyOutputContinuation } from "@/chat/services/empty-output-continuation"; import { getDiscardedRetryUsage } from "@/chat/agent/retry-usage"; import { projectTimedOutToolResult } from "@/chat/tool-support/timed-out-tool-result"; @@ -1604,7 +1607,13 @@ async function executeAgentRunInPrivacyContext( providerRetryAttempt += 1; await prepareRetry(providerRetry.messages); - logWarn("agent.turn.provider.retrying"); + logWarn( + "agent.turn.provider.retrying", + getProviderRetryAttributes({ + attempt: providerRetryAttempt, + providerError: providerRetry.providerError, + }), + ); await sleep(providerRetry.delayMs, signal); run = agent!.continue(); } diff --git a/packages/junior/src/chat/pi/traced-stream.ts b/packages/junior/src/chat/pi/traced-stream.ts index c58bb6abd3..cb6a548ad0 100644 --- a/packages/junior/src/chat/pi/traced-stream.ts +++ b/packages/junior/src/chat/pi/traced-stream.ts @@ -25,6 +25,7 @@ import { toGenAiMessagesTraceAttributes, } from "@/chat/conversation-privacy"; import { hasCompactedConversationContext } from "@/chat/services/context-compaction-marker"; +import { summarizeProviderErrorMessage } from "@/chat/services/provider-error"; type GenAiAttributeMode = "content" | "metadata"; type TraceAttributeValue = string | number | boolean | string[]; @@ -126,6 +127,13 @@ function buildChatEndAttributes( attributes["gen_ai.response.model"] = message.model; } + if (message.stopReason === "error") { + const summary = summarizeProviderErrorMessage(message.errorMessage ?? ""); + if (summary) { + attributes["app.ai.provider_error.summary"] = summary; + } + } + return attributes; } @@ -191,7 +199,13 @@ export function createTracedStreamFn( } if (finalMessage.stopReason === "error") { span.setAttribute("error.type", "provider_error"); - span.setStatus({ code: 2, message: "LLM stream failed" }); + const summary = summarizeProviderErrorMessage( + finalMessage.errorMessage ?? "", + ); + span.setStatus({ + code: 2, + message: summary ?? "LLM stream failed", + }); } } finally { span.end(); diff --git a/packages/junior/src/chat/services/provider-error.ts b/packages/junior/src/chat/services/provider-error.ts index 146b7a913b..c92c9d78d6 100644 --- a/packages/junior/src/chat/services/provider-error.ts +++ b/packages/junior/src/chat/services/provider-error.ts @@ -116,6 +116,48 @@ function providerMessage(error: unknown): string { return (error instanceof Error ? error.message : String(error)).trim(); } +const PROVIDER_ERROR_SUMMARY_MAX_CHARS = 240; + +/** + * Build a short provider-boundary summary for telemetry. + * + * Keep the leading diagnostic text. Drop large JSON bodies that often follow + * gateway status lines so Sentry stays free of provider payloads. + */ +export function summarizeProviderErrorMessage(message: string): string | undefined { + const normalized = message.trim().replace(/\s+/g, " "); + if (!normalized) return undefined; + + // Strip nested JSON/array bodies from gateway status lines without keeping + // payload keys. Plain transport messages stay intact. + let withoutJsonBodies = normalized; + while (true) { + const next = withoutJsonBodies + .replace(/\{[^{}]*\}/g, " ") + .replace(/\[[^\[\]]*\]/g, " ") + .replace(/\s+/g, " ") + .trim(); + if (next === withoutJsonBodies) break; + withoutJsonBodies = next; + } + const summary = withoutJsonBodies || normalized; + if (!summary) return undefined; + return summary.length > PROVIDER_ERROR_SUMMARY_MAX_CHARS + ? `${summary.slice(0, PROVIDER_ERROR_SUMMARY_MAX_CHARS)}...` + : summary; +} + +function providerErrorCauseMessage(error: ProviderError): string | undefined { + const cause = error.cause; + if (cause instanceof Error) { + return summarizeProviderErrorMessage(cause.message); + } + if (typeof cause === "string") { + return summarizeProviderErrorMessage(cause); + } + return undefined; +} + function extractTransportKind( error: unknown, depth = 0, @@ -284,6 +326,7 @@ export function getProviderErrorUserMessage(error: ProviderError): string { export function getProviderErrorAttributes( error: ProviderError, ): Record { + const summary = providerErrorCauseMessage(error); return { "app.ai.provider_error.kind": error.kind, "app.ai.provider_error.retryable": error.retryable, @@ -293,6 +336,7 @@ export function getProviderErrorAttributes( ...(error.retryAfterMs !== undefined ? { "app.ai.provider_error.retry_after_ms": error.retryAfterMs } : undefined), + ...(summary ? { "app.ai.provider_error.summary": summary } : undefined), ...(error.modelId ? { "gen_ai.request.model": error.modelId } : undefined), }; } diff --git a/packages/junior/src/chat/services/provider-retry.ts b/packages/junior/src/chat/services/provider-retry.ts index 1c0098fd7f..ca1a1987c5 100644 --- a/packages/junior/src/chat/services/provider-retry.ts +++ b/packages/junior/src/chat/services/provider-retry.ts @@ -3,7 +3,11 @@ import { type AssistantMessage, } from "@earendil-works/pi-ai"; import type { PiMessage } from "@/chat/pi/messages"; -import { createProviderError } from "@/chat/services/provider-error"; +import { + createProviderError, + getProviderErrorAttributes, + type ProviderError, +} from "@/chat/services/provider-error"; import { getPiMessageRole, trimTrailingAssistantMessages, @@ -17,7 +21,13 @@ export function nextProviderRetry(args: { attempt: number; failure?: AssistantMessage; messages: PiMessage[]; -}): { delayMs: number; messages: PiMessage[] } | undefined { +}): + | { + delayMs: number; + messages: PiMessage[]; + providerError: ProviderError; + } + | undefined { const backoffMs = PROVIDER_RETRY_DELAYS_MS[args.attempt]; const errorMessage = args.failure?.errorMessage; if (backoffMs === undefined || !args.failure || !errorMessage) { @@ -47,5 +57,16 @@ export function nextProviderRetry(args: { return undefined; } - return { delayMs, messages }; + return { delayMs, messages, providerError }; +} + +/** Safe attributes for one provider-boundary retry attempt. */ +export function getProviderRetryAttributes(args: { + attempt: number; + providerError: ProviderError; +}): Record { + return { + ...getProviderErrorAttributes(args.providerError), + "app.ai.provider_error.retry_attempt": args.attempt, + }; } diff --git a/packages/junior/tests/unit/services/provider-retry.test.ts b/packages/junior/tests/unit/services/provider-retry.test.ts index 7a8164da10..b3a9aa3518 100644 --- a/packages/junior/tests/unit/services/provider-retry.test.ts +++ b/packages/junior/tests/unit/services/provider-retry.test.ts @@ -5,11 +5,16 @@ import type { PiMessage } from "@/chat/pi/messages"; import { createProviderError, findProviderError, + getProviderErrorAttributes, getProviderErrorUserMessage, isProviderRetryError, ProviderError, + summarizeProviderErrorMessage, } from "@/chat/services/provider-error"; -import { nextProviderRetry } from "@/chat/services/provider-retry"; +import { + getProviderRetryAttributes, + nextProviderRetry, +} from "@/chat/services/provider-retry"; function assistantError(errorMessage: string | undefined): AssistantMessage { return fauxAssistantMessage([], { @@ -34,6 +39,11 @@ describe("provider retry helpers", () => { kind: "network", retryable: true, }); + expect(getProviderErrorAttributes(error)).toMatchObject({ + "app.ai.provider_error.kind": "network", + "app.ai.provider_error.summary": + "Anthropic stream ended before message_stop", + }); expect(isProviderRetryError(error)).toBe(true); expect(isProviderRetryError(createProviderError("invalid_api_key"))).toBe( false, @@ -42,6 +52,20 @@ describe("provider retry helpers", () => { expect(isProviderRetryError(new Error(error.message))).toBe(false); }); + it("keeps a bounded provider summary without gateway JSON payloads", () => { + expect(summarizeProviderErrorMessage(XAI_SERVICE_UNAVAILABLE)).toBe("503"); + expect( + getProviderErrorAttributes(createProviderError(XAI_SERVICE_UNAVAILABLE, { + modelId: "xai/grok-4.5", + })), + ).toMatchObject({ + "app.ai.provider_error.kind": "server", + "app.ai.provider_error.status": 503, + "app.ai.provider_error.summary": "503", + "gen_ai.request.model": "xai/grok-4.5", + }); + }); + it("finds provider errors preserved by domain wrappers", () => { const providerError = createProviderError("No object generated", { kind: "invalid_response", @@ -114,13 +138,27 @@ describe("provider retry helpers", () => { "Anthropic stream ended before message_stop", ); + const retry = nextProviderRetry({ + attempt: 0, + failure: failedAssistant, + messages: [user, failedAssistant], + }); + expect(retry).toMatchObject({ delayMs: 2_000, messages: [user] }); + expect(retry?.providerError).toMatchObject({ + kind: "network", + retryable: true, + }); expect( - nextProviderRetry({ - attempt: 0, - failure: failedAssistant, - messages: [user, failedAssistant], + getProviderRetryAttributes({ + attempt: 1, + providerError: retry!.providerError, }), - ).toEqual({ delayMs: 2_000, messages: [user] }); + ).toMatchObject({ + "app.ai.provider_error.kind": "network", + "app.ai.provider_error.retry_attempt": 1, + "app.ai.provider_error.summary": + "Anthropic stream ended before message_stop", + }); }); it("retries a structured xAI 503 despite gateway credential metadata", () => { @@ -147,7 +185,7 @@ describe("provider retry helpers", () => { failure: failedAssistant, messages: [user, failedAssistant], }), - ).toEqual({ delayMs: 2_000, messages: [user] }); + ).toMatchObject({ delayMs: 2_000, messages: [user] }); }); it("honors bounded rate-limit hints", () => { @@ -175,7 +213,7 @@ describe("provider retry helpers", () => { failure: failedAssistant, messages: [user, failedAssistant], }), - ).toEqual({ delayMs: 60_000, messages: [user] }); + ).toMatchObject({ delayMs: 60_000, messages: [user] }); }); it("classifies HTTP request timeouts without overriding permanent signals", () => { @@ -232,7 +270,7 @@ describe("provider retry helpers", () => { failure, messages: [user, failure], }), - ).toEqual({ delayMs: 2_000, messages: [user] }); + ).toMatchObject({ delayMs: 2_000, messages: [user] }); }); it("keeps explicit permanent request failures terminal", () => { diff --git a/packages/junior/tests/unit/services/turn-failure-response.test.ts b/packages/junior/tests/unit/services/turn-failure-response.test.ts index 4346fab2a6..86132ca9aa 100644 --- a/packages/junior/tests/unit/services/turn-failure-response.test.ts +++ b/packages/junior/tests/unit/services/turn-failure-response.test.ts @@ -72,10 +72,14 @@ describe("finalizeFailedTurnReply", () => { "app.ai.provider_error.kind": "server", "app.ai.provider_error.retryable": true, "app.ai.provider_error.status": 503, + "app.ai.provider_error.summary": "503", "app.ai.failure_reason": "server", "gen_ai.request.model": "xai/grok-4.5", }); expect(attributes).not.toHaveProperty("exception.message"); + expect(String(attributes?.["app.ai.provider_error.summary"] ?? "")).not.toContain( + "providerMetadata", + ); }); it("classifies empty execution failures without raw exception text", () => { From 2c1b31186ecbc02f8f203f7b90b4fa38ce44ca60 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:36:37 +0000 Subject: [PATCH 2/3] fix(provider): align stream error spans with otel recording-errors Use low-cardinality error.type from ProviderError.kind, keep a bounded status description, and drop the oxlint character-class escape warning. Co-Authored-By: David Cramer --- packages/junior/src/chat/pi/traced-stream.ts | 20 +++++++++++++------ .../src/chat/services/provider-error.ts | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/junior/src/chat/pi/traced-stream.ts b/packages/junior/src/chat/pi/traced-stream.ts index cb6a548ad0..e39221aeef 100644 --- a/packages/junior/src/chat/pi/traced-stream.ts +++ b/packages/junior/src/chat/pi/traced-stream.ts @@ -25,7 +25,11 @@ import { toGenAiMessagesTraceAttributes, } from "@/chat/conversation-privacy"; import { hasCompactedConversationContext } from "@/chat/services/context-compaction-marker"; -import { summarizeProviderErrorMessage } from "@/chat/services/provider-error"; +import { + createProviderError, + getProviderErrorAttributes, + summarizeProviderErrorMessage, +} from "@/chat/services/provider-error"; type GenAiAttributeMode = "content" | "metadata"; type TraceAttributeValue = string | number | boolean | string[]; @@ -128,10 +132,9 @@ function buildChatEndAttributes( } if (message.stopReason === "error") { - const summary = summarizeProviderErrorMessage(message.errorMessage ?? ""); - if (summary) { - attributes["app.ai.provider_error.summary"] = summary; - } + const providerError = createProviderError(message.errorMessage ?? ""); + Object.assign(attributes, getProviderErrorAttributes(providerError)); + attributes["error.type"] = providerError.kind; } return attributes; @@ -198,7 +201,12 @@ export function createTracedStreamFn( span.setAttribute(key, value); } if (finalMessage.stopReason === "error") { - span.setAttribute("error.type", "provider_error"); + const providerError = createProviderError( + finalMessage.errorMessage ?? "", + ); + // OTEL recording-errors: error.type is the class; status + // description may carry the non-sensitive exception message. + span.setAttribute("error.type", providerError.kind); const summary = summarizeProviderErrorMessage( finalMessage.errorMessage ?? "", ); diff --git a/packages/junior/src/chat/services/provider-error.ts b/packages/junior/src/chat/services/provider-error.ts index c92c9d78d6..0d360b9c13 100644 --- a/packages/junior/src/chat/services/provider-error.ts +++ b/packages/junior/src/chat/services/provider-error.ts @@ -131,10 +131,10 @@ export function summarizeProviderErrorMessage(message: string): string | undefin // Strip nested JSON/array bodies from gateway status lines without keeping // payload keys. Plain transport messages stay intact. let withoutJsonBodies = normalized; - while (true) { + for (let pass = 0; pass < 8; pass += 1) { const next = withoutJsonBodies .replace(/\{[^{}]*\}/g, " ") - .replace(/\[[^\[\]]*\]/g, " ") + .replace(/\[[^[\]]*\]/g, " ") .replace(/\s+/g, " ") .trim(); if (next === withoutJsonBodies) break; From 1dd7564b677e85efa2b7c2aeee1ef45fd40df826 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:50:24 +0000 Subject: [PATCH 3/3] ref(provider): slim provider error summary telemetry surface Drop the retry-attributes wrapper and public summarizer export. Keep one bounded summary path through getProviderErrorAttributes, reuse the already classified ProviderError on retries, and avoid double createProviderError on chat spans. Co-Authored-By: David Cramer --- .../eval-ai-gateway-dispatcher.test.ts | 2 +- packages/junior/src/chat/agent/index.ts | 16 ++---- packages/junior/src/chat/pi/traced-stream.ts | 19 ++----- .../src/chat/services/provider-error.ts | 49 +++++++------------ .../src/chat/services/provider-retry.ts | 12 ----- .../unit/services/provider-retry.test.ts | 31 +++++------- 6 files changed, 39 insertions(+), 90 deletions(-) diff --git a/packages/junior-evals/tests/integration/eval-ai-gateway-dispatcher.test.ts b/packages/junior-evals/tests/integration/eval-ai-gateway-dispatcher.test.ts index 740ffc1eff..af44285339 100644 --- a/packages/junior-evals/tests/integration/eval-ai-gateway-dispatcher.test.ts +++ b/packages/junior-evals/tests/integration/eval-ai-gateway-dispatcher.test.ts @@ -89,7 +89,7 @@ describe("eval AI Gateway dispatcher", () => { failure: failedAssistant, messages: [userMessage as PiMessage, failedAssistant as PiMessage], }), - ).toEqual({ delayMs: 2_000, messages: [userMessage as PiMessage] }); + ).toMatchObject({ delayMs: 2_000, messages: [userMessage as PiMessage] }); } finally { await restore(); } diff --git a/packages/junior/src/chat/agent/index.ts b/packages/junior/src/chat/agent/index.ts index 85503bfc2c..4f3fe0c68d 100644 --- a/packages/junior/src/chat/agent/index.ts +++ b/packages/junior/src/chat/agent/index.ts @@ -83,10 +83,7 @@ import { getProviderErrorAttributes, isProviderRetryError, } from "@/chat/services/provider-error"; -import { - getProviderRetryAttributes, - nextProviderRetry, -} from "@/chat/services/provider-retry"; +import { nextProviderRetry } from "@/chat/services/provider-retry"; import { nextEmptyOutputContinuation } from "@/chat/services/empty-output-continuation"; import { getDiscardedRetryUsage } from "@/chat/agent/retry-usage"; import { projectTimedOutToolResult } from "@/chat/tool-support/timed-out-tool-result"; @@ -1607,13 +1604,10 @@ async function executeAgentRunInPrivacyContext( providerRetryAttempt += 1; await prepareRetry(providerRetry.messages); - logWarn( - "agent.turn.provider.retrying", - getProviderRetryAttributes({ - attempt: providerRetryAttempt, - providerError: providerRetry.providerError, - }), - ); + logWarn("agent.turn.provider.retrying", { + ...getProviderErrorAttributes(providerRetry.providerError), + "app.ai.provider_error.retry_attempt": providerRetryAttempt, + }); await sleep(providerRetry.delayMs, signal); run = agent!.continue(); } diff --git a/packages/junior/src/chat/pi/traced-stream.ts b/packages/junior/src/chat/pi/traced-stream.ts index e39221aeef..a7907b7ad6 100644 --- a/packages/junior/src/chat/pi/traced-stream.ts +++ b/packages/junior/src/chat/pi/traced-stream.ts @@ -28,7 +28,6 @@ import { hasCompactedConversationContext } from "@/chat/services/context-compact import { createProviderError, getProviderErrorAttributes, - summarizeProviderErrorMessage, } from "@/chat/services/provider-error"; type GenAiAttributeMode = "content" | "metadata"; @@ -195,24 +194,16 @@ export function createTracedStreamFn( .then( (finalMessage: AssistantMessage) => { try { - for (const [key, value] of Object.entries( - buildChatEndAttributes(finalMessage, mode), - )) { + const endAttributes = buildChatEndAttributes(finalMessage, mode); + for (const [key, value] of Object.entries(endAttributes)) { span.setAttribute(key, value); } if (finalMessage.stopReason === "error") { - const providerError = createProviderError( - finalMessage.errorMessage ?? "", - ); - // OTEL recording-errors: error.type is the class; status - // description may carry the non-sensitive exception message. - span.setAttribute("error.type", providerError.kind); - const summary = summarizeProviderErrorMessage( - finalMessage.errorMessage ?? "", - ); + const summary = endAttributes["app.ai.provider_error.summary"]; span.setStatus({ code: 2, - message: summary ?? "LLM stream failed", + message: + typeof summary === "string" ? summary : "LLM stream failed", }); } } finally { diff --git a/packages/junior/src/chat/services/provider-error.ts b/packages/junior/src/chat/services/provider-error.ts index 0d360b9c13..337b9f7a71 100644 --- a/packages/junior/src/chat/services/provider-error.ts +++ b/packages/junior/src/chat/services/provider-error.ts @@ -118,46 +118,22 @@ function providerMessage(error: unknown): string { const PROVIDER_ERROR_SUMMARY_MAX_CHARS = 240; -/** - * Build a short provider-boundary summary for telemetry. - * - * Keep the leading diagnostic text. Drop large JSON bodies that often follow - * gateway status lines so Sentry stays free of provider payloads. - */ -export function summarizeProviderErrorMessage(message: string): string | undefined { +/** Short provider-boundary text for telemetry. Drop trailing JSON bodies. */ +function summarizeProviderErrorMessage(message: string): string | undefined { const normalized = message.trim().replace(/\s+/g, " "); if (!normalized) return undefined; - // Strip nested JSON/array bodies from gateway status lines without keeping - // payload keys. Plain transport messages stay intact. - let withoutJsonBodies = normalized; - for (let pass = 0; pass < 8; pass += 1) { - const next = withoutJsonBodies - .replace(/\{[^{}]*\}/g, " ") - .replace(/\[[^[\]]*\]/g, " ") - .replace(/\s+/g, " ") - .trim(); - if (next === withoutJsonBodies) break; - withoutJsonBodies = next; - } - const summary = withoutJsonBodies || normalized; + // Gateway errors often look like `503 {"error":...}`. Keep the lead text. + const jsonStart = normalized.indexOf("{"); + const summary = ( + jsonStart >= 0 ? normalized.slice(0, jsonStart) : normalized + ).trim(); if (!summary) return undefined; return summary.length > PROVIDER_ERROR_SUMMARY_MAX_CHARS ? `${summary.slice(0, PROVIDER_ERROR_SUMMARY_MAX_CHARS)}...` : summary; } -function providerErrorCauseMessage(error: ProviderError): string | undefined { - const cause = error.cause; - if (cause instanceof Error) { - return summarizeProviderErrorMessage(cause.message); - } - if (typeof cause === "string") { - return summarizeProviderErrorMessage(cause); - } - return undefined; -} - function extractTransportKind( error: unknown, depth = 0, @@ -326,7 +302,16 @@ export function getProviderErrorUserMessage(error: ProviderError): string { export function getProviderErrorAttributes( error: ProviderError, ): Record { - const summary = providerErrorCauseMessage(error); + const cause = error.cause; + const causeMessage = + cause instanceof Error + ? cause.message + : typeof cause === "string" + ? cause + : undefined; + const summary = causeMessage + ? summarizeProviderErrorMessage(causeMessage) + : undefined; return { "app.ai.provider_error.kind": error.kind, "app.ai.provider_error.retryable": error.retryable, diff --git a/packages/junior/src/chat/services/provider-retry.ts b/packages/junior/src/chat/services/provider-retry.ts index ca1a1987c5..e68b674c32 100644 --- a/packages/junior/src/chat/services/provider-retry.ts +++ b/packages/junior/src/chat/services/provider-retry.ts @@ -5,7 +5,6 @@ import { import type { PiMessage } from "@/chat/pi/messages"; import { createProviderError, - getProviderErrorAttributes, type ProviderError, } from "@/chat/services/provider-error"; import { @@ -59,14 +58,3 @@ export function nextProviderRetry(args: { return { delayMs, messages, providerError }; } - -/** Safe attributes for one provider-boundary retry attempt. */ -export function getProviderRetryAttributes(args: { - attempt: number; - providerError: ProviderError; -}): Record { - return { - ...getProviderErrorAttributes(args.providerError), - "app.ai.provider_error.retry_attempt": args.attempt, - }; -} diff --git a/packages/junior/tests/unit/services/provider-retry.test.ts b/packages/junior/tests/unit/services/provider-retry.test.ts index b3a9aa3518..559f9645ed 100644 --- a/packages/junior/tests/unit/services/provider-retry.test.ts +++ b/packages/junior/tests/unit/services/provider-retry.test.ts @@ -9,12 +9,8 @@ import { getProviderErrorUserMessage, isProviderRetryError, ProviderError, - summarizeProviderErrorMessage, } from "@/chat/services/provider-error"; -import { - getProviderRetryAttributes, - nextProviderRetry, -} from "@/chat/services/provider-retry"; +import { nextProviderRetry } from "@/chat/services/provider-retry"; function assistantError(errorMessage: string | undefined): AssistantMessage { return fauxAssistantMessage([], { @@ -53,11 +49,12 @@ describe("provider retry helpers", () => { }); it("keeps a bounded provider summary without gateway JSON payloads", () => { - expect(summarizeProviderErrorMessage(XAI_SERVICE_UNAVAILABLE)).toBe("503"); expect( - getProviderErrorAttributes(createProviderError(XAI_SERVICE_UNAVAILABLE, { - modelId: "xai/grok-4.5", - })), + getProviderErrorAttributes( + createProviderError(XAI_SERVICE_UNAVAILABLE, { + modelId: "xai/grok-4.5", + }), + ), ).toMatchObject({ "app.ai.provider_error.kind": "server", "app.ai.provider_error.status": 503, @@ -143,19 +140,13 @@ describe("provider retry helpers", () => { failure: failedAssistant, messages: [user, failedAssistant], }); - expect(retry).toMatchObject({ delayMs: 2_000, messages: [user] }); - expect(retry?.providerError).toMatchObject({ - kind: "network", - retryable: true, + expect(retry).toMatchObject({ + delayMs: 2_000, + messages: [user], + providerError: { kind: "network", retryable: true }, }); - expect( - getProviderRetryAttributes({ - attempt: 1, - providerError: retry!.providerError, - }), - ).toMatchObject({ + expect(getProviderErrorAttributes(retry!.providerError)).toMatchObject({ "app.ai.provider_error.kind": "network", - "app.ai.provider_error.retry_attempt": 1, "app.ai.provider_error.summary": "Anthropic stream ended before message_stop", });