Skip to content

Commit 71c285b

Browse files
committed
web: Pass open router errors through
1 parent 9f9f464 commit 71c285b

File tree

2 files changed

+97
-11
lines changed

2 files changed

+97
-11
lines changed

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import {
1313
handleOpenRouterNonStream,
1414
handleOpenRouterStream,
15+
OpenRouterError,
1516
} from '@/llm-api/openrouter'
1617
import { extractApiKeyFromHeader } from '@/util/auth'
1718

@@ -339,6 +340,12 @@ export async function postChatCompletions(params: {
339340
},
340341
logger,
341342
})
343+
344+
// Pass through OpenRouter provider-specific errors
345+
if (error instanceof OpenRouterError) {
346+
return NextResponse.json(error.toJSON(), { status: error.statusCode })
347+
}
348+
342349
return NextResponse.json(
343350
{ error: 'Failed to process request' },
344351
{ status: 500 },

web/src/llm-api/openrouter.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import {
66
extractRequestMetadata,
77
insertMessageToBigQuery,
88
} from './helpers'
9-
import { OpenRouterStreamChatCompletionChunkSchema } from './type/openrouter'
9+
import {
10+
OpenRouterErrorResponseSchema,
11+
OpenRouterStreamChatCompletionChunkSchema,
12+
} from './type/openrouter'
1013

1114
import type { UsageData } from './helpers'
1215
import type { OpenRouterStreamChatCompletionChunk } from './type/openrouter'
1316
import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/bigquery'
1417
import type { Logger } from '@codebuff/common/types/contracts/logger'
1518

1619
type StreamState = { responseText: string; reasoningText: string }
17-
1820
function createOpenRouterRequest(params: {
1921
body: any
2022
openrouterApiKey: string | null
@@ -93,9 +95,9 @@ export async function handleOpenRouterNonStream({
9395

9496
const responses = await Promise.all(requests)
9597
if (responses.every((r) => !r.ok)) {
96-
throw new Error(
97-
`Failed to make all ${n} requests: ${responses.map((r) => r.statusText).join(', ')}`,
98-
)
98+
// Return provider-specific error from the first failed response
99+
const firstFailedResponse = responses[0]
100+
throw await parseOpenRouterError(firstFailedResponse)
99101
}
100102
const allData = await Promise.all(responses.map((r) => r.json()))
101103

@@ -183,9 +185,7 @@ export async function handleOpenRouterNonStream({
183185
})
184186

185187
if (!response.ok) {
186-
throw new Error(
187-
`OpenRouter API error (${response.statusText}): ${await response.text()}`,
188-
)
188+
throw await parseOpenRouterError(response)
189189
}
190190

191191
const data = await response.json()
@@ -261,9 +261,7 @@ export async function handleOpenRouterStream({
261261
})
262262

263263
if (!response.ok) {
264-
throw new Error(
265-
`OpenRouter API error (${response.statusText}): ${await response.text()}`,
266-
)
264+
throw await parseOpenRouterError(response)
267265
}
268266

269267
const reader = response.body?.getReader()
@@ -532,3 +530,84 @@ async function handleStreamChunk({
532530
state.reasoningText += choice.delta?.reasoning ?? ''
533531
return state
534532
}
533+
534+
/**
535+
* Custom error class for OpenRouter API errors that preserves provider-specific details.
536+
*/
537+
export class OpenRouterError extends Error {
538+
constructor(
539+
public readonly statusCode: number,
540+
public readonly statusText: string,
541+
public readonly errorBody: {
542+
error: {
543+
message: string
544+
code: string | number | null
545+
type?: string | null
546+
param?: unknown
547+
metadata?: {
548+
raw?: string
549+
provider_name?: string
550+
}
551+
}
552+
},
553+
) {
554+
super(errorBody.error.message)
555+
this.name = 'OpenRouterError'
556+
}
557+
558+
/**
559+
* Returns the error in a format suitable for API responses.
560+
*/
561+
toJSON() {
562+
return {
563+
error: {
564+
message: this.errorBody.error.message,
565+
code: this.errorBody.error.code,
566+
type: this.errorBody.error.type,
567+
param: this.errorBody.error.param,
568+
metadata: this.errorBody.error.metadata,
569+
},
570+
}
571+
}
572+
}
573+
574+
/**
575+
* Parses an error response from OpenRouter and returns an OpenRouterError.
576+
*/
577+
async function parseOpenRouterError(
578+
response: Response,
579+
): Promise<OpenRouterError> {
580+
const errorText = await response.text()
581+
let errorBody: OpenRouterError['errorBody']
582+
try {
583+
const parsed = JSON.parse(errorText)
584+
const validated = OpenRouterErrorResponseSchema.safeParse(parsed)
585+
if (validated.success) {
586+
errorBody = {
587+
error: {
588+
message: validated.data.error.message,
589+
code: validated.data.error.code ?? null,
590+
type: validated.data.error.type,
591+
param: validated.data.error.param,
592+
// metadata is not in the schema but OpenRouter includes it for provider errors
593+
metadata: (parsed as any).error?.metadata,
594+
},
595+
}
596+
} else {
597+
errorBody = {
598+
error: {
599+
message: errorText || response.statusText,
600+
code: response.status,
601+
},
602+
}
603+
}
604+
} catch {
605+
errorBody = {
606+
error: {
607+
message: errorText || response.statusText,
608+
code: response.status,
609+
},
610+
}
611+
}
612+
return new OpenRouterError(response.status, response.statusText, errorBody)
613+
}

0 commit comments

Comments
 (0)