promptlayer 1.0.59 → 1.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/node.js.yml +1 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/index.ts +17 -2
- package/src/run-error-tracking.test.ts +146 -0
- package/src/types.ts +6 -0
- package/src/utils/errors.test.ts +68 -0
- package/src/utils/errors.ts +62 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +6 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptlayer",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.60",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsup-node",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
16
17
|
"release": "npm run build && npm publish"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"openai": "^6.7.0",
|
|
28
29
|
"tsup": "^8.5.1",
|
|
29
30
|
"typescript": "^5.2.2",
|
|
31
|
+
"vitest": "^2.1.9",
|
|
30
32
|
"zod": "^3.25.0"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
utilLogRequest,
|
|
28
28
|
vertexaiRequest,
|
|
29
29
|
} from "@/utils/utils";
|
|
30
|
+
import { categorizeError } from "@/utils/errors";
|
|
30
31
|
import { streamResponse } from "@/utils/streaming";
|
|
31
32
|
import * as opentelemetry from "@opentelemetry/api";
|
|
32
33
|
|
|
@@ -252,8 +253,6 @@ export class PromptLayer {
|
|
|
252
253
|
);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
const response = await request_function(promptBlueprint!, kwargs);
|
|
256
|
-
|
|
257
256
|
const _trackRequest = (body: object) => {
|
|
258
257
|
const request_end_time = new Date().toISOString();
|
|
259
258
|
return trackRequest(
|
|
@@ -280,6 +279,22 @@ export class PromptLayer {
|
|
|
280
279
|
);
|
|
281
280
|
};
|
|
282
281
|
|
|
282
|
+
let response: any;
|
|
283
|
+
try {
|
|
284
|
+
response = await request_function(promptBlueprint!, kwargs);
|
|
285
|
+
} catch (llmError: unknown) {
|
|
286
|
+
const errorType = categorizeError(llmError);
|
|
287
|
+
const errorMessage =
|
|
288
|
+
llmError instanceof Error ? llmError.message : String(llmError);
|
|
289
|
+
await _trackRequest({
|
|
290
|
+
request_response: {},
|
|
291
|
+
status: "ERROR",
|
|
292
|
+
error_type: errorType,
|
|
293
|
+
error_message: errorMessage,
|
|
294
|
+
});
|
|
295
|
+
throw llmError;
|
|
296
|
+
}
|
|
297
|
+
|
|
283
298
|
if (stream)
|
|
284
299
|
return streamResponse(
|
|
285
300
|
response,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock modules before importing the subject
|
|
4
|
+
vi.mock("@/utils/utils", () => ({
|
|
5
|
+
trackRequest: vi.fn().mockResolvedValue({ request_id: 1, prompt_blueprint: {} }),
|
|
6
|
+
configureProviderSettings: vi.fn().mockReturnValue({
|
|
7
|
+
provider_type: "openai",
|
|
8
|
+
kwargs: { model: "gpt-4" },
|
|
9
|
+
}),
|
|
10
|
+
getProviderConfig: vi.fn().mockReturnValue({
|
|
11
|
+
function_name: "openai.chat.completions.create",
|
|
12
|
+
stream_function: null,
|
|
13
|
+
}),
|
|
14
|
+
openaiRequest: vi.fn().mockResolvedValue({ choices: [{ message: { content: "hi" } }] }),
|
|
15
|
+
anthropicRequest: vi.fn(),
|
|
16
|
+
azureOpenAIRequest: vi.fn(),
|
|
17
|
+
googleRequest: vi.fn(),
|
|
18
|
+
mistralRequest: vi.fn(),
|
|
19
|
+
vertexaiRequest: vi.fn(),
|
|
20
|
+
amazonBedrockRequest: vi.fn(),
|
|
21
|
+
anthropicBedrockRequest: vi.fn(),
|
|
22
|
+
readEnv: vi.fn().mockReturnValue("test-api-key"),
|
|
23
|
+
runWorkflowRequest: vi.fn(),
|
|
24
|
+
utilLogRequest: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@/templates", () => {
|
|
28
|
+
return {
|
|
29
|
+
TemplateManager: class {
|
|
30
|
+
get = vi.fn().mockResolvedValue({
|
|
31
|
+
id: 1,
|
|
32
|
+
version: 1,
|
|
33
|
+
prompt_template: { type: "chat", messages: [] },
|
|
34
|
+
metadata: { model: { provider: "openai", name: "gpt-4", parameters: {} } },
|
|
35
|
+
llm_kwargs: { model: "gpt-4" },
|
|
36
|
+
custom_provider: null,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
vi.mock("@/tracing", () => {
|
|
43
|
+
const fakeSpan = {
|
|
44
|
+
setAttribute: vi.fn(),
|
|
45
|
+
setStatus: vi.fn(),
|
|
46
|
+
end: vi.fn(),
|
|
47
|
+
spanContext: () => ({ spanId: "test-span-id" }),
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
getTracer: () => ({
|
|
51
|
+
startActiveSpan: (_name: string, fn: (span: any) => any) => fn(fakeSpan),
|
|
52
|
+
}),
|
|
53
|
+
setupTracing: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
vi.mock("@/groups", () => ({
|
|
58
|
+
GroupManager: class {},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock("@/track", () => ({
|
|
62
|
+
TrackManager: class {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock("@/span-wrapper", () => ({
|
|
66
|
+
wrapWithSpan: vi.fn(),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
vi.mock("@/utils/streaming", () => ({
|
|
70
|
+
streamResponse: vi.fn().mockReturnValue({ async *[Symbol.asyncIterator]() {} }),
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
import { PromptLayer } from "@/index";
|
|
74
|
+
import { trackRequest, openaiRequest } from "@/utils/utils";
|
|
75
|
+
import { RateLimitError } from "openai";
|
|
76
|
+
|
|
77
|
+
describe("run() error tracking", () => {
|
|
78
|
+
let client: PromptLayer;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
client = new PromptLayer({ apiKey: "test-api-key" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("tracks error with UNKNOWN_ERROR type and re-throws when LLM call fails", async () => {
|
|
86
|
+
const llmError = new Error("model overloaded");
|
|
87
|
+
(openaiRequest as Mock).mockRejectedValueOnce(llmError);
|
|
88
|
+
|
|
89
|
+
await expect(
|
|
90
|
+
client.run({ promptName: "test-prompt" })
|
|
91
|
+
).rejects.toThrow("model overloaded");
|
|
92
|
+
|
|
93
|
+
expect(trackRequest).toHaveBeenCalledWith(
|
|
94
|
+
expect.any(String),
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
request_response: {},
|
|
97
|
+
status: "ERROR",
|
|
98
|
+
error_type: "UNKNOWN_ERROR",
|
|
99
|
+
error_message: "model overloaded",
|
|
100
|
+
}),
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("tracks PROVIDER_RATE_LIMIT when LLM throws RateLimitError", async () => {
|
|
106
|
+
const rateLimitError = new RateLimitError(
|
|
107
|
+
429, undefined, "Too Many Requests", undefined
|
|
108
|
+
);
|
|
109
|
+
(openaiRequest as Mock).mockRejectedValueOnce(rateLimitError);
|
|
110
|
+
|
|
111
|
+
await expect(
|
|
112
|
+
client.run({ promptName: "test-prompt" })
|
|
113
|
+
).rejects.toThrow("Too Many Requests");
|
|
114
|
+
|
|
115
|
+
expect(trackRequest).toHaveBeenCalledWith(
|
|
116
|
+
expect.any(String),
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
request_response: {},
|
|
119
|
+
status: "ERROR",
|
|
120
|
+
error_type: "PROVIDER_RATE_LIMIT",
|
|
121
|
+
error_message: expect.stringContaining("Too Many Requests"),
|
|
122
|
+
}),
|
|
123
|
+
true
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("calls trackRequest without error fields on success", async () => {
|
|
128
|
+
const successResponse = { choices: [{ message: { content: "hello" } }] };
|
|
129
|
+
(openaiRequest as Mock).mockResolvedValueOnce(successResponse);
|
|
130
|
+
|
|
131
|
+
await client.run({ promptName: "test-prompt" });
|
|
132
|
+
|
|
133
|
+
expect(trackRequest).toHaveBeenCalledWith(
|
|
134
|
+
expect.any(String),
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
request_response: successResponse,
|
|
137
|
+
}),
|
|
138
|
+
true
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const callArgs = (trackRequest as Mock).mock.calls[0][1];
|
|
142
|
+
expect(callArgs).not.toHaveProperty("error_type");
|
|
143
|
+
expect(callArgs).not.toHaveProperty("error_message");
|
|
144
|
+
expect(callArgs).not.toHaveProperty("status");
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface TrackRequest {
|
|
|
34
34
|
return_data?: boolean;
|
|
35
35
|
group_id?: number;
|
|
36
36
|
span_id?: string;
|
|
37
|
+
status?: "SUCCESS" | "WARNING" | "ERROR";
|
|
38
|
+
error_type?: string;
|
|
39
|
+
error_message?: string;
|
|
37
40
|
[k: string]: unknown;
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -353,6 +356,9 @@ export interface LogRequest {
|
|
|
353
356
|
prompt_id?: number;
|
|
354
357
|
score_name?: string;
|
|
355
358
|
api_type?: string;
|
|
359
|
+
status?: "SUCCESS" | "WARNING" | "ERROR";
|
|
360
|
+
error_type?: string;
|
|
361
|
+
error_message?: string;
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
export interface RequestLog {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { categorizeError, ErrorType } from "@/utils/errors";
|
|
3
|
+
import {
|
|
4
|
+
RateLimitError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
APIConnectionTimeoutError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
InternalServerError,
|
|
9
|
+
} from "openai";
|
|
10
|
+
|
|
11
|
+
describe("categorizeError", () => {
|
|
12
|
+
// Branch: statusCode === 429 (+ className matches /ratelimit/i)
|
|
13
|
+
it("returns PROVIDER_RATE_LIMIT for OpenAI RateLimitError", () => {
|
|
14
|
+
const err = new RateLimitError(429, undefined, "Rate limit exceeded", undefined);
|
|
15
|
+
expect(categorizeError(err)).toBe(ErrorType.PROVIDER_RATE_LIMIT);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Branch: className matches /timeout/i
|
|
19
|
+
it("returns PROVIDER_TIMEOUT for OpenAI APIConnectionTimeoutError", () => {
|
|
20
|
+
const err = new APIConnectionTimeoutError({ message: "Request timed out." });
|
|
21
|
+
expect(categorizeError(err)).toBe(ErrorType.PROVIDER_TIMEOUT);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Branch: statusCode === 401 (+ className matches /authentication/i)
|
|
25
|
+
it("returns PROVIDER_AUTH_ERROR for OpenAI AuthenticationError", () => {
|
|
26
|
+
const err = new AuthenticationError(401, undefined, "Invalid API key", undefined);
|
|
27
|
+
expect(categorizeError(err)).toBe(ErrorType.PROVIDER_AUTH_ERROR);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Branch: message includes "quota"
|
|
31
|
+
it("returns PROVIDER_QUOTA_LIMIT when message contains quota", () => {
|
|
32
|
+
expect(categorizeError(new Error("You exceeded your quota"))).toBe(
|
|
33
|
+
ErrorType.PROVIDER_QUOTA_LIMIT
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Branch: message includes "timeout"
|
|
38
|
+
it("returns PROVIDER_TIMEOUT when message contains timeout", () => {
|
|
39
|
+
expect(categorizeError(new Error("Request timeout"))).toBe(
|
|
40
|
+
ErrorType.PROVIDER_TIMEOUT
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Branch: message includes "timed out"
|
|
45
|
+
it("returns PROVIDER_TIMEOUT when message contains timed out", () => {
|
|
46
|
+
expect(categorizeError(new Error("Connection timed out"))).toBe(
|
|
47
|
+
ErrorType.PROVIDER_TIMEOUT
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Branch: statusCode defined but no earlier match → PROVIDER_ERROR
|
|
52
|
+
it("returns PROVIDER_ERROR for OpenAI InternalServerError", () => {
|
|
53
|
+
const err = new InternalServerError(500, undefined, "Internal server error", undefined);
|
|
54
|
+
expect(categorizeError(err)).toBe(ErrorType.PROVIDER_ERROR);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Branch: fallthrough — no status, no matching class/message
|
|
58
|
+
it("returns UNKNOWN_ERROR for a plain Error", () => {
|
|
59
|
+
expect(categorizeError(new Error("something broke"))).toBe(
|
|
60
|
+
ErrorType.UNKNOWN_ERROR
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Branch: non-Error thrown → String(error) path, getClassName returns ""
|
|
65
|
+
it("returns UNKNOWN_ERROR for a non-Error value", () => {
|
|
66
|
+
expect(categorizeError("oops")).toBe(ErrorType.UNKNOWN_ERROR);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export enum ErrorType {
|
|
2
|
+
PROVIDER_RATE_LIMIT = "PROVIDER_RATE_LIMIT",
|
|
3
|
+
PROVIDER_QUOTA_LIMIT = "PROVIDER_QUOTA_LIMIT",
|
|
4
|
+
PROVIDER_TIMEOUT = "PROVIDER_TIMEOUT",
|
|
5
|
+
PROVIDER_AUTH_ERROR = "PROVIDER_AUTH_ERROR",
|
|
6
|
+
PROVIDER_ERROR = "PROVIDER_ERROR",
|
|
7
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getStatusCode(error: unknown): number | undefined {
|
|
11
|
+
if (
|
|
12
|
+
error &&
|
|
13
|
+
typeof error === "object" &&
|
|
14
|
+
"status" in error &&
|
|
15
|
+
typeof (error as any).status === "number"
|
|
16
|
+
) {
|
|
17
|
+
return (error as any).status;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getClassName(error: unknown): string {
|
|
23
|
+
if (error && typeof error === "object" && error.constructor) {
|
|
24
|
+
return error.constructor.name;
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function categorizeError(error: unknown): ErrorType {
|
|
30
|
+
const statusCode = getStatusCode(error);
|
|
31
|
+
const className = getClassName(error);
|
|
32
|
+
const message =
|
|
33
|
+
error instanceof Error
|
|
34
|
+
? error.message.toLowerCase()
|
|
35
|
+
: String(error).toLowerCase();
|
|
36
|
+
|
|
37
|
+
if (statusCode === 429 || /ratelimit/i.test(className)) {
|
|
38
|
+
return ErrorType.PROVIDER_RATE_LIMIT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (/timeout/i.test(className)) {
|
|
42
|
+
return ErrorType.PROVIDER_TIMEOUT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (statusCode === 401 || /authentication/i.test(className)) {
|
|
46
|
+
return ErrorType.PROVIDER_AUTH_ERROR;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (message.includes("quota")) {
|
|
50
|
+
return ErrorType.PROVIDER_QUOTA_LIMIT;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (message.includes("timeout") || message.includes("timed out")) {
|
|
54
|
+
return ErrorType.PROVIDER_TIMEOUT;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (statusCode !== undefined) {
|
|
58
|
+
return ErrorType.PROVIDER_ERROR;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ErrorType.UNKNOWN_ERROR;
|
|
62
|
+
}
|
package/tsconfig.json
CHANGED