promptlayer 1.0.57 → 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.57",
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",
@@ -11,8 +11,9 @@
11
11
  "url": "https://github.com/MagnivOrg/prompt-layer-js"
12
12
  },
13
13
  "scripts": {
14
- "build": "tsup-node src/index.ts --format cjs,esm --minify --dts-resolve --clean --sourcemap --legacy-output",
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
+ });
@@ -1,4 +1,4 @@
1
- import { fetchWithRetry } from "@/utils/utils";
1
+ import { fetchWithRetry, getCommonHeaders } from "@/utils/utils";
2
2
  import { Attributes, SpanKind, SpanStatusCode } from "@opentelemetry/api";
3
3
  import { ExportResultCode } from "@opentelemetry/core";
4
4
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
@@ -91,6 +91,7 @@ class PromptLayerSpanExporter implements SpanExporter {
91
91
  headers: {
92
92
  "Content-Type": "application/json",
93
93
  "X-API-KEY": this.apiKey || "",
94
+ ...getCommonHeaders(),
94
95
  },
95
96
  body: JSON.stringify({
96
97
  spans: requestData,
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
 
@@ -352,6 +355,10 @@ export interface LogRequest {
352
355
  score?: number;
353
356
  prompt_id?: number;
354
357
  score_name?: string;
358
+ api_type?: string;
359
+ status?: "SUCCESS" | "WARNING" | "ERROR";
360
+ error_type?: string;
361
+ error_message?: string;
355
362
  }
356
363
 
357
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
+ }
@@ -28,6 +28,31 @@ import {
28
28
  STREAMING_PROVIDERS_WITH_USAGE,
29
29
  } from "./streaming";
30
30
 
31
+ // SDK version - injected at build time from package.json
32
+ declare const __SDK_VERSION__: string;
33
+ export const SDK_VERSION = __SDK_VERSION__;
34
+
35
+ // Get Node.js version (major.minor format)
36
+ const getNodeVersion = (): string => {
37
+ if (typeof process !== "undefined" && process.versions?.node) {
38
+ const parts = process.versions.node.split(".");
39
+ return `${parts[0]}.${parts[1]}`;
40
+ }
41
+ return "unknown";
42
+ };
43
+
44
+ const _NODE_VERSION = getNodeVersion();
45
+ const _PROMPTLAYER_USER_AGENT = `promptlayer-js/${SDK_VERSION} (node ${_NODE_VERSION})`;
46
+
47
+ /**
48
+ * Returns common headers to be included in all PromptLayer API requests.
49
+ * Includes the SDK version and user agent for tracking and debugging purposes.
50
+ */
51
+ export const getCommonHeaders = (): Record<string, string> => ({
52
+ "User-Agent": _PROMPTLAYER_USER_AGENT,
53
+ "X-SDK-Version": SDK_VERSION,
54
+ });
55
+
31
56
  export const SET_WORKFLOW_COMPLETE_MESSAGE = "SET_WORKFLOW_COMPLETE";
32
57
 
33
58
  export enum FinalOutputCode {
@@ -43,7 +68,7 @@ async function getFinalOutput(
43
68
  ): Promise<any> {
44
69
  const response = await fetchWithRetry(
45
70
  `${baseURL}/workflow-version-execution-results?workflow_version_execution_id=${executionId}&return_all_outputs=${returnAllOutputs}`,
46
- { headers }
71
+ { headers, ...getCommonHeaders() }
47
72
  );
48
73
  if (!response.ok) {
49
74
  throw new Error("Failed to fetch final output");
@@ -223,9 +248,7 @@ const promptLayerApiRequest = async (
223
248
  try {
224
249
  const response = await fetchWithRetry(`${baseURL}/track-request`, {
225
250
  method: "POST",
226
- headers: {
227
- "Content-Type": "application/json",
228
- },
251
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
229
252
  body: JSON.stringify(body),
230
253
  });
231
254
  const data = await response.json();
@@ -264,9 +287,7 @@ const promptLayerTrackMetadata = async (
264
287
  try {
265
288
  const response = await fetchWithRetry(`${baseURL}/library-track-metadata`, {
266
289
  method: "POST",
267
- headers: {
268
- "Content-Type": "application/json",
269
- },
290
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
270
291
  body: JSON.stringify({
271
292
  ...body,
272
293
  api_key: apiKey,
@@ -307,9 +328,7 @@ const promptLayerTrackScore = async (
307
328
  try {
308
329
  const response = await fetchWithRetry(`${baseURL}/library-track-score`, {
309
330
  method: "POST",
310
- headers: {
311
- "Content-Type": "application/json",
312
- },
331
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
313
332
  body: JSON.stringify({
314
333
  ...body,
315
334
  api_key: apiKey,
@@ -350,9 +369,7 @@ const promptLayerTrackPrompt = async (
350
369
  try {
351
370
  const response = await fetchWithRetry(`${baseURL}/library-track-prompt`, {
352
371
  method: "POST",
353
- headers: {
354
- "Content-Type": "application/json",
355
- },
372
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
356
373
  body: JSON.stringify({
357
374
  ...body,
358
375
  api_key: apiKey,
@@ -393,9 +410,7 @@ const promptLayerTrackGroup = async (
393
410
  try {
394
411
  const response = await fetchWithRetry(`${baseURL}/track-group`, {
395
412
  method: "POST",
396
- headers: {
397
- "Content-Type": "application/json",
398
- },
413
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
399
414
  body: JSON.stringify({
400
415
  ...body,
401
416
  api_key: apiKey,
@@ -435,9 +450,7 @@ const promptLayerCreateGroup = async (
435
450
  try {
436
451
  const response = await fetchWithRetry(`${baseURL}/create-group`, {
437
452
  method: "POST",
438
- headers: {
439
- "Content-Type": "application/json",
440
- },
453
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
441
454
  body: JSON.stringify({
442
455
  api_key: apiKey,
443
456
  }),
@@ -482,6 +495,7 @@ const getPromptTemplate = async (
482
495
  headers: {
483
496
  "Content-Type": "application/json",
484
497
  "X-API-KEY": apiKey,
498
+ ...getCommonHeaders(),
485
499
  },
486
500
  body: JSON.stringify(params),
487
501
  });
@@ -523,10 +537,7 @@ const publishPromptTemplate = async (
523
537
  ): Promise<PublishPromptTemplateResponse> => {
524
538
  const response = await fetchWithRetry(`${baseURL}/rest/prompt-templates`, {
525
539
  method: "POST",
526
- headers: {
527
- "Content-Type": "application/json",
528
- "X-API-KEY": apiKey,
529
- },
540
+ headers: { "Content-Type": "application/json", "X-API-KEY": apiKey, ...getCommonHeaders() },
530
541
  body: JSON.stringify({
531
542
  prompt_template: { ...body },
532
543
  prompt_version: { ...body },
@@ -560,10 +571,7 @@ const getAllPromptTemplates = async (
560
571
  url.searchParams.append(key, value.toString())
561
572
  );
562
573
  const response = await fetchWithRetry(url, {
563
- headers: {
564
- "Content-Type": "application/json",
565
- "X-API-KEY": apiKey,
566
- },
574
+ headers: { "Content-Type": "application/json", "X-API-KEY": apiKey, ...getCommonHeaders() },
567
575
  });
568
576
  const data = await response.json();
569
577
  if (response.status !== 200) {
@@ -658,10 +666,7 @@ export const runWorkflowRequest = async ({
658
666
  return_all_outputs,
659
667
  };
660
668
 
661
- const headers = {
662
- "X-API-KEY": api_key,
663
- "Content-Type": "application/json",
664
- };
669
+ const headers = { "Content-Type": "application/json", "X-API-KEY": api_key, ...getCommonHeaders() };
665
670
 
666
671
  try {
667
672
  const response = await fetchWithRetry(
@@ -775,9 +780,7 @@ const trackRequest = async (
775
780
  try {
776
781
  const response = await fetchWithRetry(`${baseURL}/track-request`, {
777
782
  method: "POST",
778
- headers: {
779
- "Content-Type": "application/json",
780
- },
783
+ headers: { "Content-Type": "application/json", ...getCommonHeaders() },
781
784
  body: JSON.stringify(body),
782
785
  });
783
786
  const data = await response.json();
@@ -907,10 +910,7 @@ const utilLogRequest = async (
907
910
  try {
908
911
  const response = await fetchWithRetry(`${baseURL}/log-request`, {
909
912
  method: "POST",
910
- headers: {
911
- "X-API-KEY": apiKey,
912
- "Content-Type": "application/json",
913
- },
913
+ headers: { "Content-Type": "application/json", "X-API-KEY": apiKey, ...getCommonHeaders() },
914
914
  body: JSON.stringify(body),
915
915
  });
916
916
  const data = await response.json();
package/tsconfig.json CHANGED
@@ -110,5 +110,6 @@
110
110
  "paths": {
111
111
  "@/*": ["*"]
112
112
  }
113
- }
113
+ },
114
+ "exclude": ["**/*.test.ts"]
114
115
  }
package/tsup.config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "tsup";
2
+ import { readFileSync } from "fs";
3
+
4
+ const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
5
+
6
+ export default defineConfig({
7
+ entry: ["src/index.ts"],
8
+ format: ["cjs", "esm"],
9
+ dts: true,
10
+ clean: true,
11
+ sourcemap: true,
12
+ minify: true,
13
+ legacyOutput: true,
14
+ define: {
15
+ __SDK_VERSION__: JSON.stringify(pkg.version),
16
+ },
17
+ });
@@ -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
+ });