|
|
@@ -1,5 +1,6 @@
|
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
|
import OpenAI from "openai"
|
|
|
+import { z } from "zod"
|
|
|
|
|
|
import {
|
|
|
openRouterDefaultModelId,
|
|
|
@@ -42,13 +43,77 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
|
|
|
reasoning?: OpenRouterReasoningParams
|
|
|
}
|
|
|
|
|
|
-// OpenRouter error structure that may include metadata.raw with actual upstream error
|
|
|
+// Zod schema for OpenRouter error response structure (for caught exceptions)
|
|
|
+const OpenRouterErrorResponseSchema = z.object({
|
|
|
+ error: z
|
|
|
+ .object({
|
|
|
+ message: z.string().optional(),
|
|
|
+ code: z.number().optional(),
|
|
|
+ metadata: z
|
|
|
+ .object({
|
|
|
+ raw: z.string().optional(),
|
|
|
+ })
|
|
|
+ .optional(),
|
|
|
+ })
|
|
|
+ .optional(),
|
|
|
+})
|
|
|
+
|
|
|
+// OpenRouter error structure that may include error.metadata.raw with actual upstream error
|
|
|
+// This is for caught exceptions which have the error wrapped in an "error" property
|
|
|
interface OpenRouterErrorResponse {
|
|
|
+ error?: {
|
|
|
+ message?: string
|
|
|
+ code?: number
|
|
|
+ metadata?: { raw?: string }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Direct error object structure (for streaming errors passed directly)
|
|
|
+interface OpenRouterError {
|
|
|
message?: string
|
|
|
code?: number
|
|
|
metadata?: { raw?: string }
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Helper function to parse and extract error message from metadata.raw
|
|
|
+ * metadata.raw is often a JSON encoded string that may contain .message or .error fields
|
|
|
+ * Example structures:
|
|
|
+ * - {"message": "Error text"}
|
|
|
+ * - {"error": "Error text"}
|
|
|
+ * - {"error": {"message": "Error text"}}
|
|
|
+ * - {"type":"error","error":{"type":"invalid_request_error","message":"tools: Tool names must be unique."}}
|
|
|
+ */
|
|
|
+function extractErrorFromMetadataRaw(raw: string | undefined): string | undefined {
|
|
|
+ if (!raw) {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(raw)
|
|
|
+ // Check for common error message fields
|
|
|
+ if (typeof parsed === "object" && parsed !== null) {
|
|
|
+ // Check for direct message field
|
|
|
+ if (typeof parsed.message === "string") {
|
|
|
+ return parsed.message
|
|
|
+ }
|
|
|
+ // Check for nested error.message field (e.g., Anthropic error format)
|
|
|
+ if (typeof parsed.error === "object" && parsed.error !== null && typeof parsed.error.message === "string") {
|
|
|
+ return parsed.error.message
|
|
|
+ }
|
|
|
+ // Check for error as a string
|
|
|
+ if (typeof parsed.error === "string") {
|
|
|
+ return parsed.error
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // If we can't extract a specific field, return the raw string
|
|
|
+ return raw
|
|
|
+ } catch {
|
|
|
+ // If it's not valid JSON, return as-is
|
|
|
+ return raw
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
|
|
|
// `CompletionsAPI.CompletionUsage`
|
|
|
// See also: https://openrouter.ai/docs/use-cases/usage-accounting
|
|
|
@@ -119,19 +184,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|
|
/**
|
|
|
* Handle OpenRouter streaming error response and report to telemetry.
|
|
|
* OpenRouter may include metadata.raw with the actual upstream provider error.
|
|
|
+ * @param error The error object (not wrapped - receives the error directly)
|
|
|
*/
|
|
|
- private handleStreamingError(error: OpenRouterErrorResponse, modelId: string, operation: string): never {
|
|
|
- const rawErrorMessage = error?.metadata?.raw || error?.message
|
|
|
+ private handleStreamingError(error: OpenRouterError, modelId: string, operation: string): never {
|
|
|
+ const rawString = error?.metadata?.raw
|
|
|
+ const parsedError = extractErrorFromMetadataRaw(rawString)
|
|
|
+ const rawErrorMessage = parsedError || error?.message || "Unknown error"
|
|
|
|
|
|
const apiError = Object.assign(
|
|
|
- new ApiProviderError(
|
|
|
- rawErrorMessage ?? "Unknown error",
|
|
|
- this.providerName,
|
|
|
- modelId,
|
|
|
- operation,
|
|
|
- error?.code,
|
|
|
- ),
|
|
|
- { status: error?.code, error: { message: error?.message, metadata: error?.metadata } },
|
|
|
+ new ApiProviderError(rawErrorMessage, this.providerName, modelId, operation, error?.code),
|
|
|
+ { status: error?.code, error },
|
|
|
)
|
|
|
|
|
|
TelemetryService.instance.captureException(apiError)
|
|
|
@@ -256,10 +318,38 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|
|
try {
|
|
|
stream = await this.client.chat.completions.create(completionParams, requestOptions)
|
|
|
} catch (error) {
|
|
|
- const errorMessage = error instanceof Error ? error.message : String(error)
|
|
|
- const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
|
|
|
- TelemetryService.instance.captureException(apiError)
|
|
|
- throw handleOpenAIError(error, this.providerName)
|
|
|
+ // Try to parse as OpenRouter error structure using Zod
|
|
|
+ const parseResult = OpenRouterErrorResponseSchema.safeParse(error)
|
|
|
+
|
|
|
+ if (parseResult.success && parseResult.data.error) {
|
|
|
+ const openRouterError = parseResult.data
|
|
|
+ const rawString = openRouterError.error?.metadata?.raw
|
|
|
+ const parsedError = extractErrorFromMetadataRaw(rawString)
|
|
|
+ const rawErrorMessage = parsedError || openRouterError.error?.message || "Unknown error"
|
|
|
+
|
|
|
+ const apiError = Object.assign(
|
|
|
+ new ApiProviderError(
|
|
|
+ rawErrorMessage,
|
|
|
+ this.providerName,
|
|
|
+ modelId,
|
|
|
+ "createMessage",
|
|
|
+ openRouterError.error?.code,
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ status: openRouterError.error?.code,
|
|
|
+ error: openRouterError.error,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ TelemetryService.instance.captureException(apiError)
|
|
|
+ throw handleOpenAIError(error, this.providerName)
|
|
|
+ } else {
|
|
|
+ // Fallback for non-OpenRouter errors
|
|
|
+ const errorMessage = error instanceof Error ? error.message : String(error)
|
|
|
+ const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
|
|
|
+ TelemetryService.instance.captureException(apiError)
|
|
|
+ throw handleOpenAIError(error, this.providerName)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
let lastUsage: CompletionUsage | undefined = undefined
|
|
|
@@ -281,7 +371,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|
|
for await (const chunk of stream) {
|
|
|
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
|
|
|
if ("error" in chunk) {
|
|
|
- this.handleStreamingError(chunk.error as OpenRouterErrorResponse, modelId, "createMessage")
|
|
|
+ this.handleStreamingError(chunk.error as OpenRouterError, modelId, "createMessage")
|
|
|
}
|
|
|
|
|
|
const delta = chunk.choices[0]?.delta
|
|
|
@@ -486,14 +576,42 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|
|
try {
|
|
|
response = await this.client.chat.completions.create(completionParams, requestOptions)
|
|
|
} catch (error) {
|
|
|
- const errorMessage = error instanceof Error ? error.message : String(error)
|
|
|
- const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
|
|
|
- TelemetryService.instance.captureException(apiError)
|
|
|
- throw handleOpenAIError(error, this.providerName)
|
|
|
+ // Try to parse as OpenRouter error structure using Zod
|
|
|
+ const parseResult = OpenRouterErrorResponseSchema.safeParse(error)
|
|
|
+
|
|
|
+ if (parseResult.success && parseResult.data.error) {
|
|
|
+ const openRouterError = parseResult.data
|
|
|
+ const rawString = openRouterError.error?.metadata?.raw
|
|
|
+ const parsedError = extractErrorFromMetadataRaw(rawString)
|
|
|
+ const rawErrorMessage = parsedError || openRouterError.error?.message || "Unknown error"
|
|
|
+
|
|
|
+ const apiError = Object.assign(
|
|
|
+ new ApiProviderError(
|
|
|
+ rawErrorMessage,
|
|
|
+ this.providerName,
|
|
|
+ modelId,
|
|
|
+ "completePrompt",
|
|
|
+ openRouterError.error?.code,
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ status: openRouterError.error?.code,
|
|
|
+ error: openRouterError.error,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ TelemetryService.instance.captureException(apiError)
|
|
|
+ throw handleOpenAIError(error, this.providerName)
|
|
|
+ } else {
|
|
|
+ // Fallback for non-OpenRouter errors
|
|
|
+ const errorMessage = error instanceof Error ? error.message : String(error)
|
|
|
+ const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
|
|
|
+ TelemetryService.instance.captureException(apiError)
|
|
|
+ throw handleOpenAIError(error, this.providerName)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
if ("error" in response) {
|
|
|
- this.handleStreamingError(response.error as OpenRouterErrorResponse, modelId, "completePrompt")
|
|
|
+ this.handleStreamingError(response.error as OpenRouterError, modelId, "completePrompt")
|
|
|
}
|
|
|
|
|
|
const completion = response as OpenAI.Chat.ChatCompletion
|