{"record":{"id":"e4d256f0f750a4c7","repo":"can1357/oh-my-pi","slug":"anthropic-messages-upstream-message-stopreason","errorCode":null,"errorMessage":"anthropic-messages: upstream ${message.stopReason}","messagePattern":"anthropic-messages: upstream (.+?)","errorType":"exception","errorClass":"ProviderResponseError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/anthropic-messages-server.ts","lineNumber":500,"sourceCode":"\t\t\t\tblocks.push({ type: \"tool_use\", id: c.id, name: c.name, input: c.arguments ?? {} });\n\t\t\t\tbreak;\n\t\t}\n\t}\n\treturn blocks;\n}\n\nfunction encodeUsage(message: AssistantMessage): Record<string, unknown> {\n\treturn {\n\t\tinput_tokens: message.usage.input,\n\t\toutput_tokens: message.usage.output,\n\t\tcache_read_input_tokens: message.usage.cacheRead,\n\t\tcache_creation_input_tokens: message.usage.cacheWrite,\n\t};\n}\n\nexport function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown> {\n\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\tthrow new AIError.ProviderResponseError(\n\t\t\tmessage.errorMessage ?? `anthropic-messages: upstream ${message.stopReason}`,\n\t\t\t{\n\t\t\t\tprovider: \"anthropic\",\n\t\t\t\tkind: \"output\",\n\t\t\t},\n\t\t);\n\t}\n\treturn {\n\t\tid: message.responseId ?? newMessageId(),\n\t\ttype: \"message\",\n\t\trole: \"assistant\",\n\t\tmodel: requestedModelId,\n\t\tcontent: encodeContentBlocks(message),\n\t\tstop_reason: mapStopReasonOut(message.stopReason),\n\t\t// TODO: surface the matched stop sequence once pi-ai's\n\t\t// `AssistantMessage.stopReason` carries the matched string. Intentionally\n\t\t// `null` for now (Anthropic schema allows it).\n\t\tstop_sequence: null,","sourceCodeStart":482,"sourceCodeEnd":518,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/anthropic-messages-server.ts#L482-L518","documentation":"encodeResponse converts an internal AssistantMessage back into an Anthropic Messages API response. If the upstream run ended with stopReason 'error' or 'aborted', it throws ProviderResponseError using the recorded errorMessage, or the fallback 'anthropic-messages: upstream error/aborted'. The server therefore surfaces upstream provider failures to the API client as an error response rather than silently returning an empty/malformed completion.","triggerScenarios":"A request proxied through the anthropic-messages server whose downstream provider call produced an AssistantMessage with stopReason 'error' or 'aborted' — provider outage, upstream auth failure, or the original caller aborting mid-stream.","commonSituations":"The downstream provider API key expired or is invalid; upstream provider is rate-limiting or down; the end client disconnected and the run was aborted; streaming interrupted between server and upstream provider.","solutions":["Inspect message.errorMessage (preferred) over the fallback text — it carries the actual upstream failure reason","Check upstream provider credentials and quota (the usual cause of stopReason 'error')","Treat 'upstream aborted' as client cancellation: verify the caller isn't closing the connection prematurely","Retry transient upstream errors with backoff at the client of this server","Add health checks/alerting on the upstream provider if these errors appear in clusters"],"exampleFix":"// before: no handling of upstream failure responses\nconst res = await serverClient.post(\"/v1/messages\", body);\nreturn res.json();\n// after\nconst res = await serverClient.post(\"/v1/messages\", body);\nif (!res.ok) {\n  const err = await res.json();\n  if (isTransientUpstream(err)) return retryWithBackoff(() => post(body));\n  throw new Error(err.error?.message ?? \"upstream provider failure\");\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { AIError } from \"@oh-my-pi/pi-ai\";\nfunction isUpstreamFailure(e: unknown): e is AIError.ProviderResponseError {\n  return e instanceof AIError.ProviderResponseError;\n}","tryCatchPattern":"try {\n  return encodeResponse(message, modelId);\n} catch (err) {\n  if (isUpstreamFailure(err)) {\n    // stopReason was error/aborted upstream; prefer err's contextual info and the\n    // original message.errorMessage over the generic fallback text\n    if (message.stopReason === \"aborted\") return clientCancelledResponse();\n    return upstreamBadGatewayResponse(message.errorMessage ?? err.message);\n  }\n  throw err;\n}","preventionTips":["Monitor downstream provider health; most 'error' stop reasons are upstream auth/quota/outage","Carry message.errorMessage end-to-end instead of letting the fallback text mask the cause","Treat 'aborted' as client cancellation and don't retry those","Retry only classified-transient upstream errors with backoff"],"tags":["anthropic","api-server","upstream-error","proxy"],"backgroundTag":"upstream-provider-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}