Skip to content

Commit be1b070

Browse files
committed
refactor: rename SdkErrorWithStatus to OpenAISdkError and add getOpenAISdkErrorMessage
- Rename interface from SdkErrorWithStatus to OpenAISdkError for clarity - Add nested error.metadata structure to capture upstream provider errors - Add getOpenAISdkErrorMessage() to extract best error message (prioritizes metadata.raw) - Rename type guard from isSdkErrorWithStatus to isOpenAISdkError - Add comprehensive test suite for telemetry error utilities
1 parent 79bb535 commit be1b070

File tree

2 files changed

+176
-8
lines changed

2 files changed

+176
-8
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect } from "vitest"
2+
3+
import {
4+
getErrorStatusCode,
5+
getOpenAISdkErrorMessage,
6+
shouldReportApiErrorToTelemetry,
7+
EXPECTED_API_ERROR_CODES,
8+
} from "../telemetry.js"
9+
10+
describe("telemetry error utilities", () => {
11+
describe("getErrorStatusCode", () => {
12+
it("should return undefined for non-object errors", () => {
13+
expect(getErrorStatusCode(null)).toBeUndefined()
14+
expect(getErrorStatusCode(undefined)).toBeUndefined()
15+
expect(getErrorStatusCode("error string")).toBeUndefined()
16+
expect(getErrorStatusCode(42)).toBeUndefined()
17+
})
18+
19+
it("should return undefined for objects without status property", () => {
20+
expect(getErrorStatusCode({})).toBeUndefined()
21+
expect(getErrorStatusCode({ message: "error" })).toBeUndefined()
22+
expect(getErrorStatusCode({ code: 500 })).toBeUndefined()
23+
})
24+
25+
it("should return undefined for objects with non-numeric status", () => {
26+
expect(getErrorStatusCode({ status: "500" })).toBeUndefined()
27+
expect(getErrorStatusCode({ status: null })).toBeUndefined()
28+
expect(getErrorStatusCode({ status: undefined })).toBeUndefined()
29+
})
30+
31+
it("should return status for OpenAI SDK-like errors", () => {
32+
const error = { status: 429, message: "Rate limit exceeded" }
33+
expect(getErrorStatusCode(error)).toBe(429)
34+
})
35+
36+
it("should return status for errors with additional properties", () => {
37+
const error = {
38+
status: 500,
39+
code: "internal_error",
40+
message: "Internal server error",
41+
error: { message: "Upstream error" },
42+
}
43+
expect(getErrorStatusCode(error)).toBe(500)
44+
})
45+
})
46+
47+
describe("getOpenAISdkErrorMessage", () => {
48+
it("should return undefined for non-OpenAI SDK errors", () => {
49+
expect(getOpenAISdkErrorMessage(null)).toBeUndefined()
50+
expect(getOpenAISdkErrorMessage(undefined)).toBeUndefined()
51+
expect(getOpenAISdkErrorMessage({ message: "error" })).toBeUndefined()
52+
})
53+
54+
it("should return the primary message for simple OpenAI SDK errors", () => {
55+
const error = { status: 400, message: "Bad request" }
56+
expect(getOpenAISdkErrorMessage(error)).toBe("Bad request")
57+
})
58+
59+
it("should prioritize nested error.message over primary message", () => {
60+
const error = {
61+
status: 500,
62+
message: "Request failed",
63+
error: { message: "Upstream provider error" },
64+
}
65+
expect(getOpenAISdkErrorMessage(error)).toBe("Upstream provider error")
66+
})
67+
68+
it("should prioritize metadata.raw over other messages", () => {
69+
const error = {
70+
status: 429,
71+
message: "Request failed",
72+
error: {
73+
message: "Error details",
74+
metadata: { raw: "Rate limit exceeded: free-models-per-day" },
75+
},
76+
}
77+
expect(getOpenAISdkErrorMessage(error)).toBe("Rate limit exceeded: free-models-per-day")
78+
})
79+
80+
it("should fallback to nested error.message when metadata.raw is undefined", () => {
81+
const error = {
82+
status: 400,
83+
message: "Request failed",
84+
error: {
85+
message: "Detailed error message",
86+
metadata: {},
87+
},
88+
}
89+
expect(getOpenAISdkErrorMessage(error)).toBe("Detailed error message")
90+
})
91+
92+
it("should fallback to primary message when no nested messages exist", () => {
93+
const error = {
94+
status: 403,
95+
message: "Forbidden",
96+
error: {},
97+
}
98+
expect(getOpenAISdkErrorMessage(error)).toBe("Forbidden")
99+
})
100+
})
101+
102+
describe("shouldReportApiErrorToTelemetry", () => {
103+
it("should return false for expected error codes", () => {
104+
for (const code of EXPECTED_API_ERROR_CODES) {
105+
expect(shouldReportApiErrorToTelemetry(code)).toBe(false)
106+
}
107+
})
108+
109+
it("should return false for 429 rate limit errors", () => {
110+
expect(shouldReportApiErrorToTelemetry(429)).toBe(false)
111+
expect(shouldReportApiErrorToTelemetry(429, "Rate limit exceeded")).toBe(false)
112+
})
113+
114+
it("should return false for messages starting with 429", () => {
115+
expect(shouldReportApiErrorToTelemetry(undefined, "429 Rate limit exceeded")).toBe(false)
116+
expect(shouldReportApiErrorToTelemetry(undefined, "429: Too many requests")).toBe(false)
117+
})
118+
119+
it("should return false for messages containing 'rate limit' (case insensitive)", () => {
120+
expect(shouldReportApiErrorToTelemetry(undefined, "Rate limit exceeded")).toBe(false)
121+
expect(shouldReportApiErrorToTelemetry(undefined, "RATE LIMIT error")).toBe(false)
122+
expect(shouldReportApiErrorToTelemetry(undefined, "Request failed due to rate limit")).toBe(false)
123+
})
124+
125+
it("should return true for non-rate-limit errors", () => {
126+
expect(shouldReportApiErrorToTelemetry(500)).toBe(true)
127+
expect(shouldReportApiErrorToTelemetry(400, "Bad request")).toBe(true)
128+
expect(shouldReportApiErrorToTelemetry(401, "Unauthorized")).toBe(true)
129+
})
130+
131+
it("should return true when no error code or message is provided", () => {
132+
expect(shouldReportApiErrorToTelemetry()).toBe(true)
133+
expect(shouldReportApiErrorToTelemetry(undefined, undefined)).toBe(true)
134+
})
135+
136+
it("should return true for regular error messages without rate limit keywords", () => {
137+
expect(shouldReportApiErrorToTelemetry(undefined, "Internal server error")).toBe(true)
138+
expect(shouldReportApiErrorToTelemetry(undefined, "Connection timeout")).toBe(true)
139+
})
140+
})
141+
})

packages/types/src/telemetry.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,42 +286,69 @@ const EXPECTED_ERROR_MESSAGE_PATTERNS = [
286286
]
287287

288288
/**
289-
* Interface for SDK errors that have HTTP status information.
289+
* Interface representing the error structure from OpenAI SDK.
290290
* OpenAI SDK errors (APIError, AuthenticationError, RateLimitError, etc.)
291-
* all extend this interface with a numeric status property.
291+
* have a numeric `status` property and may contain nested error metadata.
292+
*
293+
* @see https://github.com/openai/openai-node/blob/master/src/error.ts
292294
*/
293-
interface SdkErrorWithStatus {
295+
interface OpenAISdkError {
296+
/** HTTP status code of the error response */
294297
status: number
298+
/** Optional error code (may be numeric or string) */
295299
code?: number | string
300+
/** Primary error message */
296301
message: string
302+
/** Nested error object containing additional details from the API response */
303+
error?: {
304+
message?: string
305+
metadata?: {
306+
/** Raw error message from upstream provider (e.g., OpenRouter upstream errors) */
307+
raw?: string
308+
}
309+
}
297310
}
298311

299312
/**
300-
* Type guard to check if an error object is an SDK error with status property.
313+
* Type guard to check if an error object is an OpenAI SDK error.
301314
* OpenAI SDK errors (APIError and subclasses) have: status, code, message properties.
302315
*/
303-
function isSdkErrorWithStatus(error: unknown): error is SdkErrorWithStatus {
316+
function isOpenAISdkError(error: unknown): error is OpenAISdkError {
304317
return (
305318
typeof error === "object" &&
306319
error !== null &&
307320
"status" in error &&
308-
typeof (error as SdkErrorWithStatus).status === "number"
321+
typeof (error as OpenAISdkError).status === "number"
309322
)
310323
}
311324

312325
/**
313326
* Extracts the HTTP status code from an error object.
314-
* Supports SDK errors that have a status property (e.g., OpenAI APIError).
327+
* Supports OpenAI SDK errors that have a status property.
315328
* @param error - The error to extract status from
316329
* @returns The status code if available, undefined otherwise
317330
*/
318331
export function getErrorStatusCode(error: unknown): number | undefined {
319-
if (isSdkErrorWithStatus(error)) {
332+
if (isOpenAISdkError(error)) {
320333
return error.status
321334
}
322335
return undefined
323336
}
324337

338+
/**
339+
* Extracts the most descriptive error message from an OpenAI SDK error.
340+
* Prioritizes nested metadata (upstream provider errors) over the standard message.
341+
* @param error - The error to extract message from
342+
* @returns The best available error message, or undefined if not an OpenAI SDK error
343+
*/
344+
export function getOpenAISdkErrorMessage(error: unknown): string | undefined {
345+
if (isOpenAISdkError(error)) {
346+
// Prioritize nested metadata which may contain upstream provider details
347+
return error.error?.metadata?.raw || error.error?.message || error.message
348+
}
349+
return undefined
350+
}
351+
325352
/**
326353
* Helper to check if an API error should be reported to telemetry.
327354
* Filters out expected errors like rate limits by checking both error codes and messages.

0 commit comments

Comments
 (0)