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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "promptlayer",
3
3
  "license": "MIT",
4
- "version": "1.0.59",
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
@@ -110,5 +110,6 @@
110
110
  "paths": {
111
111
  "@/*": ["*"]
112
112
  }
113
- }
113
+ },
114
+ "exclude": ["**/*.test.ts"]
114
115
  }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import path from "path";
3
+
4
+ export default defineConfig({
5
+ resolve: { alias: { "@": path.resolve(__dirname, "src") } },
6
+ });