11import { Anthropic } from "@anthropic-ai/sdk"
22import OpenAI from "openai"
3+ import { z } from "zod"
34
45import {
56 openRouterDefaultModelId ,
@@ -42,13 +43,77 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
4243 reasoning ?: OpenRouterReasoningParams
4344}
4445
45- // OpenRouter error structure that may include metadata.raw with actual upstream error
46+ // Zod schema for OpenRouter error response structure (for caught exceptions)
47+ const OpenRouterErrorResponseSchema = z . object ( {
48+ error : z
49+ . object ( {
50+ message : z . string ( ) . optional ( ) ,
51+ code : z . number ( ) . optional ( ) ,
52+ metadata : z
53+ . object ( {
54+ raw : z . string ( ) . optional ( ) ,
55+ } )
56+ . optional ( ) ,
57+ } )
58+ . optional ( ) ,
59+ } )
60+
61+ // OpenRouter error structure that may include error.metadata.raw with actual upstream error
62+ // This is for caught exceptions which have the error wrapped in an "error" property
4663interface OpenRouterErrorResponse {
64+ error ?: {
65+ message ?: string
66+ code ?: number
67+ metadata ?: { raw ?: string }
68+ }
69+ }
70+
71+ // Direct error object structure (for streaming errors passed directly)
72+ interface OpenRouterError {
4773 message ?: string
4874 code ?: number
4975 metadata ?: { raw ?: string }
5076}
5177
78+ /**
79+ * Helper function to parse and extract error message from metadata.raw
80+ * metadata.raw is often a JSON encoded string that may contain .message or .error fields
81+ * Example structures:
82+ * - {"message": "Error text"}
83+ * - {"error": "Error text"}
84+ * - {"error": {"message": "Error text"}}
85+ * - {"type":"error","error":{"type":"invalid_request_error","message":"tools: Tool names must be unique."}}
86+ */
87+ function extractErrorFromMetadataRaw ( raw : string | undefined ) : string | undefined {
88+ if ( ! raw ) {
89+ return undefined
90+ }
91+
92+ try {
93+ const parsed = JSON . parse ( raw )
94+ // Check for common error message fields
95+ if ( typeof parsed === "object" && parsed !== null ) {
96+ // Check for direct message field
97+ if ( typeof parsed . message === "string" ) {
98+ return parsed . message
99+ }
100+ // Check for nested error.message field (e.g., Anthropic error format)
101+ if ( typeof parsed . error === "object" && parsed . error !== null && typeof parsed . error . message === "string" ) {
102+ return parsed . error . message
103+ }
104+ // Check for error as a string
105+ if ( typeof parsed . error === "string" ) {
106+ return parsed . error
107+ }
108+ }
109+ // If we can't extract a specific field, return the raw string
110+ return raw
111+ } catch {
112+ // If it's not valid JSON, return as-is
113+ return raw
114+ }
115+ }
116+
52117// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
53118// `CompletionsAPI.CompletionUsage`
54119// See also: https://openrouter.ai/docs/use-cases/usage-accounting
@@ -119,19 +184,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
119184 /**
120185 * Handle OpenRouter streaming error response and report to telemetry.
121186 * OpenRouter may include metadata.raw with the actual upstream provider error.
187+ * @param error The error object (not wrapped - receives the error directly)
122188 */
123- private handleStreamingError ( error : OpenRouterErrorResponse , modelId : string , operation : string ) : never {
124- const rawErrorMessage = error ?. metadata ?. raw || error ?. message
189+ private handleStreamingError ( error : OpenRouterError , modelId : string , operation : string ) : never {
190+ const rawString = error ?. metadata ?. raw
191+ const parsedError = extractErrorFromMetadataRaw ( rawString )
192+ const rawErrorMessage = parsedError || error ?. message || "Unknown error"
125193
126194 const apiError = Object . assign (
127- new ApiProviderError (
128- rawErrorMessage ?? "Unknown error" ,
129- this . providerName ,
130- modelId ,
131- operation ,
132- error ?. code ,
133- ) ,
134- { status : error ?. code , error : { message : error ?. message , metadata : error ?. metadata } } ,
195+ new ApiProviderError ( rawErrorMessage , this . providerName , modelId , operation , error ?. code ) ,
196+ { status : error ?. code , error } ,
135197 )
136198
137199 TelemetryService . instance . captureException ( apiError )
@@ -256,10 +318,38 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
256318 try {
257319 stream = await this . client . chat . completions . create ( completionParams , requestOptions )
258320 } catch ( error ) {
259- const errorMessage = error instanceof Error ? error . message : String ( error )
260- const apiError = new ApiProviderError ( errorMessage , this . providerName , modelId , "createMessage" )
261- TelemetryService . instance . captureException ( apiError )
262- throw handleOpenAIError ( error , this . providerName )
321+ // Try to parse as OpenRouter error structure using Zod
322+ const parseResult = OpenRouterErrorResponseSchema . safeParse ( error )
323+
324+ if ( parseResult . success && parseResult . data . error ) {
325+ const openRouterError = parseResult . data
326+ const rawString = openRouterError . error ?. metadata ?. raw
327+ const parsedError = extractErrorFromMetadataRaw ( rawString )
328+ const rawErrorMessage = parsedError || openRouterError . error ?. message || "Unknown error"
329+
330+ const apiError = Object . assign (
331+ new ApiProviderError (
332+ rawErrorMessage ,
333+ this . providerName ,
334+ modelId ,
335+ "createMessage" ,
336+ openRouterError . error ?. code ,
337+ ) ,
338+ {
339+ status : openRouterError . error ?. code ,
340+ error : openRouterError . error ,
341+ } ,
342+ )
343+
344+ TelemetryService . instance . captureException ( apiError )
345+ throw handleOpenAIError ( error , this . providerName )
346+ } else {
347+ // Fallback for non-OpenRouter errors
348+ const errorMessage = error instanceof Error ? error . message : String ( error )
349+ const apiError = new ApiProviderError ( errorMessage , this . providerName , modelId , "createMessage" )
350+ TelemetryService . instance . captureException ( apiError )
351+ throw handleOpenAIError ( error , this . providerName )
352+ }
263353 }
264354
265355 let lastUsage : CompletionUsage | undefined = undefined
@@ -281,7 +371,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
281371 for await ( const chunk of stream ) {
282372 // OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
283373 if ( "error" in chunk ) {
284- this . handleStreamingError ( chunk . error as OpenRouterErrorResponse , modelId , "createMessage" )
374+ this . handleStreamingError ( chunk . error as OpenRouterError , modelId , "createMessage" )
285375 }
286376
287377 const delta = chunk . choices [ 0 ] ?. delta
@@ -486,14 +576,42 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
486576 try {
487577 response = await this . client . chat . completions . create ( completionParams , requestOptions )
488578 } catch ( error ) {
489- const errorMessage = error instanceof Error ? error . message : String ( error )
490- const apiError = new ApiProviderError ( errorMessage , this . providerName , modelId , "completePrompt" )
491- TelemetryService . instance . captureException ( apiError )
492- throw handleOpenAIError ( error , this . providerName )
579+ // Try to parse as OpenRouter error structure using Zod
580+ const parseResult = OpenRouterErrorResponseSchema . safeParse ( error )
581+
582+ if ( parseResult . success && parseResult . data . error ) {
583+ const openRouterError = parseResult . data
584+ const rawString = openRouterError . error ?. metadata ?. raw
585+ const parsedError = extractErrorFromMetadataRaw ( rawString )
586+ const rawErrorMessage = parsedError || openRouterError . error ?. message || "Unknown error"
587+
588+ const apiError = Object . assign (
589+ new ApiProviderError (
590+ rawErrorMessage ,
591+ this . providerName ,
592+ modelId ,
593+ "completePrompt" ,
594+ openRouterError . error ?. code ,
595+ ) ,
596+ {
597+ status : openRouterError . error ?. code ,
598+ error : openRouterError . error ,
599+ } ,
600+ )
601+
602+ TelemetryService . instance . captureException ( apiError )
603+ throw handleOpenAIError ( error , this . providerName )
604+ } else {
605+ // Fallback for non-OpenRouter errors
606+ const errorMessage = error instanceof Error ? error . message : String ( error )
607+ const apiError = new ApiProviderError ( errorMessage , this . providerName , modelId , "completePrompt" )
608+ TelemetryService . instance . captureException ( apiError )
609+ throw handleOpenAIError ( error , this . providerName )
610+ }
493611 }
494612
495613 if ( "error" in response ) {
496- this . handleStreamingError ( response . error as OpenRouterErrorResponse , modelId , "completePrompt" )
614+ this . handleStreamingError ( response . error as OpenRouterError , modelId , "completePrompt" )
497615 }
498616
499617 const completion = response as OpenAI . Chat . ChatCompletion
0 commit comments