@upstash/qstash 2.8.4 → 2.9.0-rc

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/workflow.js CHANGED
@@ -43,120 +43,154 @@ module.exports = __toCommonJS(workflow_exports);
43
43
  // src/receiver.ts
44
44
  var jose = __toESM(require("jose"));
45
45
  var import_crypto_js = __toESM(require("crypto-js"));
46
- var SignatureError = class extends Error {
47
- constructor(message) {
48
- super(message);
49
- this.name = "SignatureError";
46
+
47
+ // src/client/api/base.ts
48
+ var BaseProvider = class {
49
+ baseUrl;
50
+ token;
51
+ owner;
52
+ constructor(baseUrl, token, owner) {
53
+ this.baseUrl = baseUrl;
54
+ this.token = token;
55
+ this.owner = owner;
56
+ }
57
+ getUrl() {
58
+ return `${this.baseUrl}/${this.getRoute().join("/")}`;
50
59
  }
51
60
  };
52
- var Receiver = class {
53
- currentSigningKey;
54
- nextSigningKey;
55
- constructor(config) {
56
- this.currentSigningKey = config.currentSigningKey;
57
- this.nextSigningKey = config.nextSigningKey;
61
+
62
+ // src/client/api/llm.ts
63
+ var LLMProvider = class extends BaseProvider {
64
+ apiKind = "llm";
65
+ organization;
66
+ method = "POST";
67
+ constructor(baseUrl, token, owner, organization) {
68
+ super(baseUrl, token, owner);
69
+ this.organization = organization;
58
70
  }
59
- /**
60
- * Verify the signature of a request.
61
- *
62
- * Tries to verify the signature with the current signing key.
63
- * If that fails, maybe because you have rotated the keys recently, it will
64
- * try to verify the signature with the next signing key.
65
- *
66
- * If that fails, the signature is invalid and a `SignatureError` is thrown.
67
- */
68
- async verify(request) {
69
- let payload;
70
- try {
71
- payload = await this.verifyWithKey(this.currentSigningKey, request);
72
- } catch {
73
- payload = await this.verifyWithKey(this.nextSigningKey, request);
71
+ getRoute() {
72
+ return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"];
73
+ }
74
+ getHeaders(options) {
75
+ if (this.owner === "upstash" && !options.analytics) {
76
+ return { "content-type": "application/json" };
74
77
  }
75
- this.verifyBodyAndUrl(payload, request);
76
- return true;
78
+ const header = this.owner === "anthropic" ? "x-api-key" : "authorization";
79
+ const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`;
80
+ const headers = {
81
+ [header]: headerValue,
82
+ "content-type": "application/json"
83
+ };
84
+ if (this.owner === "openai" && this.organization) {
85
+ headers["OpenAI-Organization"] = this.organization;
86
+ }
87
+ if (this.owner === "anthropic") {
88
+ headers["anthropic-version"] = "2023-06-01";
89
+ }
90
+ return headers;
77
91
  }
78
92
  /**
79
- * Verify signature with a specific signing key
93
+ * Checks if callback exists and adds analytics in place if it's set.
94
+ *
95
+ * @param request
96
+ * @param options
80
97
  */
81
- async verifyWithKey(key, request) {
82
- const jwt = await jose.jwtVerify(request.signature, new TextEncoder().encode(key), {
83
- issuer: "Upstash",
84
- clockTolerance: request.clockTolerance
85
- }).catch((error) => {
86
- throw new SignatureError(error.message);
87
- });
88
- return jwt.payload;
89
- }
90
- verifyBodyAndUrl(payload, request) {
91
- const p = payload;
92
- if (request.url !== void 0 && p.sub !== request.url) {
93
- throw new SignatureError(`invalid subject: ${p.sub}, want: ${request.url}`);
94
- }
95
- const bodyHash = import_crypto_js.default.SHA256(request.body).toString(import_crypto_js.default.enc.Base64url);
96
- const padding = new RegExp(/=+$/);
97
- if (p.body.replace(padding, "") !== bodyHash.replace(padding, "")) {
98
- throw new SignatureError(`body hash does not match, want: ${p.body}, got: ${bodyHash}`);
98
+ onFinish(providerInfo, options) {
99
+ if (options.analytics) {
100
+ return updateWithAnalytics(providerInfo, options.analytics);
99
101
  }
102
+ return providerInfo;
100
103
  }
101
104
  };
105
+ var upstash = () => {
106
+ return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash");
107
+ };
102
108
 
103
- // src/client/dlq.ts
104
- var DLQ = class {
105
- http;
106
- constructor(http) {
107
- this.http = http;
109
+ // src/client/api/utils.ts
110
+ var getProviderInfo = (api, upstashToken) => {
111
+ const { name, provider, ...parameters } = api;
112
+ const finalProvider = provider ?? upstash();
113
+ if (finalProvider.owner === "upstash" && !finalProvider.token) {
114
+ finalProvider.token = upstashToken;
108
115
  }
109
- /**
110
- * List messages in the dlq
111
- */
112
- async listMessages(options) {
113
- const filterPayload = {
114
- ...options?.filter,
115
- topicName: options?.filter?.urlGroup
116
+ if (!finalProvider.baseUrl)
117
+ throw new TypeError("baseUrl cannot be empty or undefined!");
118
+ if (!finalProvider.token)
119
+ throw new TypeError("token cannot be empty or undefined!");
120
+ if (finalProvider.apiKind !== name) {
121
+ throw new TypeError(
122
+ `Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}`
123
+ );
124
+ }
125
+ const providerInfo = {
126
+ url: finalProvider.getUrl(),
127
+ baseUrl: finalProvider.baseUrl,
128
+ route: finalProvider.getRoute(),
129
+ appendHeaders: finalProvider.getHeaders(parameters),
130
+ owner: finalProvider.owner,
131
+ method: finalProvider.method
132
+ };
133
+ return finalProvider.onFinish(providerInfo, parameters);
134
+ };
135
+ var safeJoinHeaders = (headers, record) => {
136
+ const joinedHeaders = new Headers(record);
137
+ for (const [header, value] of headers.entries()) {
138
+ joinedHeaders.set(header, value);
139
+ }
140
+ return joinedHeaders;
141
+ };
142
+ var processApi = (request, headers, upstashToken) => {
143
+ if (!request.api) {
144
+ request.headers = headers;
145
+ return request;
146
+ }
147
+ const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken);
148
+ if (request.api.name === "llm") {
149
+ const callback = request.callback;
150
+ if (!callback) {
151
+ throw new TypeError("Callback cannot be undefined when using LLM api.");
152
+ }
153
+ return {
154
+ ...request,
155
+ method: request.method ?? method,
156
+ headers: safeJoinHeaders(headers, appendHeaders),
157
+ ...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 }
116
158
  };
117
- const messagesPayload = await this.http.request({
118
- method: "GET",
119
- path: ["v2", "dlq"],
120
- query: {
121
- cursor: options?.cursor,
122
- count: options?.count,
123
- ...filterPayload
124
- }
125
- });
159
+ } else {
126
160
  return {
127
- messages: messagesPayload.messages.map((message) => {
128
- return {
129
- ...message,
130
- urlGroup: message.topicName,
131
- ratePerSecond: "rate" in message ? message.rate : void 0
132
- };
133
- }),
134
- cursor: messagesPayload.cursor
161
+ ...request,
162
+ method: request.method ?? method,
163
+ headers: safeJoinHeaders(headers, appendHeaders),
164
+ url,
165
+ api: void 0
135
166
  };
136
167
  }
137
- /**
138
- * Remove a message from the dlq using it's `dlqId`
139
- */
140
- async delete(dlqMessageId) {
141
- return await this.http.request({
142
- method: "DELETE",
143
- path: ["v2", "dlq", dlqMessageId],
144
- parseResponseAsJson: false
145
- // there is no response
146
- });
147
- }
148
- /**
149
- * Remove multiple messages from the dlq using their `dlqId`s
150
- */
151
- async deleteMany(request) {
152
- return await this.http.request({
153
- method: "DELETE",
154
- path: ["v2", "dlq"],
155
- headers: { "Content-Type": "application/json" },
156
- body: JSON.stringify({ dlqIds: request.dlqIds })
157
- });
158
- }
159
168
  };
169
+ function updateWithAnalytics(providerInfo, analytics) {
170
+ switch (analytics.name) {
171
+ case "helicone": {
172
+ providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`;
173
+ if (providerInfo.owner === "upstash") {
174
+ updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [
175
+ "llm",
176
+ ...providerInfo.route
177
+ ]);
178
+ } else {
179
+ providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl;
180
+ updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route);
181
+ }
182
+ return providerInfo;
183
+ }
184
+ default: {
185
+ throw new Error("Unknown analytics provider");
186
+ }
187
+ }
188
+ }
189
+ function updateProviderInfo(providerInfo, baseUrl, route) {
190
+ providerInfo.baseUrl = baseUrl;
191
+ providerInfo.route = route;
192
+ providerInfo.url = `${baseUrl}/${route.join("/")}`;
193
+ }
160
194
 
161
195
  // src/client/error.ts
162
196
  var RATELIMIT_STATUS = 429;
@@ -238,644 +272,764 @@ var formatWorkflowError = (error) => {
238
272
  };
239
273
  };
240
274
 
241
- // src/client/http.ts
242
- var HttpClient = class {
243
- baseUrl;
244
- authorization;
245
- options;
246
- retry;
247
- headers;
248
- telemetryHeaders;
249
- constructor(config) {
250
- this.baseUrl = config.baseUrl.replace(/\/$/, "");
251
- this.authorization = config.authorization;
252
- this.retry = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
253
- typeof config.retry === "boolean" && !config.retry ? {
254
- attempts: 1,
255
- backoff: () => 0
256
- } : {
257
- attempts: config.retry?.retries ?? 5,
258
- backoff: config.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50)
259
- };
260
- this.headers = config.headers;
261
- this.telemetryHeaders = config.telemetryHeaders;
262
- }
263
- async request(request) {
264
- const { response } = await this.requestWithBackoff(request);
265
- if (request.parseResponseAsJson === false) {
266
- return void 0;
267
- }
268
- return await response.json();
269
- }
270
- async *requestStream(request) {
271
- const { response } = await this.requestWithBackoff(request);
272
- if (!response.body) {
273
- throw new Error("No response body");
275
+ // src/client/utils.ts
276
+ var isIgnoredHeader = (header) => {
277
+ const lowerCaseHeader = header.toLowerCase();
278
+ return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
279
+ };
280
+ function prefixHeaders(headers) {
281
+ const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
282
+ for (const key of keysToBePrefixed) {
283
+ const value = headers.get(key);
284
+ if (value !== null) {
285
+ headers.set(`Upstash-Forward-${key}`, value);
274
286
  }
275
- const body = response.body;
276
- const reader = body.getReader();
277
- const decoder = new TextDecoder();
278
- try {
279
- while (true) {
280
- const { done, value } = await reader.read();
281
- if (done) {
282
- break;
283
- }
284
- const chunkText = decoder.decode(value, { stream: true });
285
- const chunks = chunkText.split("\n").filter(Boolean);
286
- for (const chunk of chunks) {
287
- if (chunk.startsWith("data: ")) {
288
- const data = chunk.slice(6);
289
- if (data === "[DONE]") {
290
- break;
291
- }
292
- yield JSON.parse(data);
293
- }
294
- }
295
- }
296
- } finally {
297
- await reader.cancel();
287
+ headers.delete(key);
288
+ }
289
+ return headers;
290
+ }
291
+ function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) {
292
+ if (!globalHeaders) {
293
+ return headers;
294
+ }
295
+ const finalHeaders = new Headers(globalHeaders);
296
+ headers.forEach((value, key) => {
297
+ finalHeaders.set(key, value);
298
+ });
299
+ telemetryHeaders?.forEach((value, key) => {
300
+ if (!value)
301
+ return;
302
+ finalHeaders.append(key, value);
303
+ });
304
+ return finalHeaders;
305
+ }
306
+ function processHeaders(request) {
307
+ const headers = prefixHeaders(new Headers(request.headers));
308
+ headers.set("Upstash-Method", request.method ?? "POST");
309
+ if (request.delay !== void 0) {
310
+ if (typeof request.delay === "string") {
311
+ headers.set("Upstash-Delay", request.delay);
312
+ } else {
313
+ headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
298
314
  }
299
315
  }
300
- requestWithBackoff = async (request) => {
301
- const [url, requestOptions] = this.processRequest(request);
302
- let response = void 0;
303
- let error = void 0;
304
- for (let index = 0; index <= this.retry.attempts; index++) {
305
- try {
306
- response = await fetch(url.toString(), requestOptions);
307
- break;
308
- } catch (error_) {
309
- error = error_;
310
- if (index < this.retry.attempts) {
311
- await new Promise((r) => setTimeout(r, this.retry.backoff(index)));
312
- }
313
- }
316
+ if (request.notBefore !== void 0) {
317
+ headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
318
+ }
319
+ if (request.deduplicationId !== void 0) {
320
+ headers.set("Upstash-Deduplication-Id", request.deduplicationId);
321
+ }
322
+ if (request.contentBasedDeduplication) {
323
+ headers.set("Upstash-Content-Based-Deduplication", "true");
324
+ }
325
+ if (request.retries !== void 0) {
326
+ headers.set("Upstash-Retries", request.retries.toFixed(0));
327
+ }
328
+ if (request.retryDelay !== void 0) {
329
+ headers.set("Upstash-Retry-Delay", request.retryDelay);
330
+ }
331
+ if (request.callback !== void 0) {
332
+ headers.set("Upstash-Callback", request.callback);
333
+ }
334
+ if (request.failureCallback !== void 0) {
335
+ headers.set("Upstash-Failure-Callback", request.failureCallback);
336
+ }
337
+ if (request.timeout !== void 0) {
338
+ if (typeof request.timeout === "string") {
339
+ headers.set("Upstash-Timeout", request.timeout);
340
+ } else {
341
+ headers.set("Upstash-Timeout", `${request.timeout}s`);
314
342
  }
315
- if (!response) {
316
- throw error ?? new Error("Exhausted all retries");
343
+ }
344
+ if (request.flowControl?.key) {
345
+ const parallelism = request.flowControl.parallelism?.toString();
346
+ const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString();
347
+ const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period;
348
+ const controlValue = [
349
+ parallelism ? `parallelism=${parallelism}` : void 0,
350
+ rate ? `rate=${rate}` : void 0,
351
+ period ? `period=${period}` : void 0
352
+ ].filter(Boolean);
353
+ if (controlValue.length === 0) {
354
+ throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl");
317
355
  }
318
- await this.checkResponse(response);
319
- return {
320
- response,
321
- error
322
- };
323
- };
324
- processRequest = (request) => {
325
- const headers = new Headers(request.headers);
326
- if (!headers.has("Authorization")) {
327
- headers.set("Authorization", this.authorization);
356
+ headers.set("Upstash-Flow-Control-Key", request.flowControl.key);
357
+ headers.set("Upstash-Flow-Control-Value", controlValue.join(", "));
358
+ }
359
+ if (request.label !== void 0) {
360
+ headers.set("Upstash-Label", request.label);
361
+ }
362
+ return headers;
363
+ }
364
+ function getRequestPath(request) {
365
+ const nonApiPath = request.url ?? request.urlGroup ?? request.topic;
366
+ if (nonApiPath)
367
+ return nonApiPath;
368
+ if (request.api?.name === "llm")
369
+ return `api/llm`;
370
+ if (request.api?.name === "email") {
371
+ const providerInfo = getProviderInfo(request.api, "not-needed");
372
+ return providerInfo.baseUrl;
373
+ }
374
+ throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`);
375
+ }
376
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
377
+ var NANOID_LENGTH = 21;
378
+ function nanoid() {
379
+ return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
380
+ }
381
+ function decodeBase64(base64) {
382
+ try {
383
+ const binString = atob(base64);
384
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
385
+ return new TextDecoder().decode(intArray);
386
+ } catch (error) {
387
+ try {
388
+ const result = atob(base64);
389
+ console.warn(
390
+ `Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
391
+ );
392
+ return result;
393
+ } catch (error2) {
394
+ console.warn(
395
+ `Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}`
396
+ );
397
+ return base64;
328
398
  }
329
- const requestOptions = {
330
- method: request.method,
331
- headers,
332
- body: request.body,
333
- keepalive: request.keepalive
399
+ }
400
+ }
401
+ function getRuntime() {
402
+ if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun)
403
+ return `bun@${process.versions.bun}`;
404
+ if (typeof EdgeRuntime === "string")
405
+ return "edge-light";
406
+ else if (typeof process === "object" && typeof process.version === "string")
407
+ return `node@${process.version}`;
408
+ return "";
409
+ }
410
+ function getSafeEnvironment() {
411
+ return typeof process === "undefined" ? {} : process.env;
412
+ }
413
+
414
+ // src/client/multi-region/utils.ts
415
+ var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
416
+ var DEFAULT_QSTASH_URL = "https://qstash.upstash.io";
417
+ var getRegionFromEnvironment = (environment) => {
418
+ const region = environment.QSTASH_REGION;
419
+ return normalizeRegionHeader(region);
420
+ };
421
+ function readEnvironmentVariables(environmentVariables, environment, region) {
422
+ const result = {};
423
+ for (const variable of environmentVariables) {
424
+ const key = region ? `${region}_${variable}` : variable;
425
+ result[variable] = environment[key];
426
+ }
427
+ return result;
428
+ }
429
+ function readClientEnvironmentVariables(environment, region) {
430
+ return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
431
+ }
432
+ function readReceiverEnvironmentVariables(environment, region) {
433
+ return readEnvironmentVariables(
434
+ ["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
435
+ environment,
436
+ region
437
+ );
438
+ }
439
+ function normalizeRegionHeader(region) {
440
+ if (!region) {
441
+ return void 0;
442
+ }
443
+ region = region.replaceAll("-", "_").toUpperCase();
444
+ if (VALID_REGIONS.includes(region)) {
445
+ return region;
446
+ }
447
+ console.warn(
448
+ `[Upstash QStash] Invalid UPSTASH_REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
449
+ ", "
450
+ )}.`
451
+ );
452
+ return void 0;
453
+ }
454
+
455
+ // src/client/multi-region/incoming.ts
456
+ var getReceiverSigningKeys = ({
457
+ environment,
458
+ regionFromHeader,
459
+ config
460
+ }) => {
461
+ if (config?.currentSigningKey && config.nextSigningKey) {
462
+ return {
463
+ currentSigningKey: config.currentSigningKey,
464
+ nextSigningKey: config.nextSigningKey
334
465
  };
335
- const url = new URL([request.baseUrl ?? this.baseUrl, ...request.path].join("/"));
336
- if (request.query) {
337
- for (const [key, value] of Object.entries(request.query)) {
338
- if (value !== void 0) {
339
- url.searchParams.set(key, value.toString());
340
- }
341
- }
342
- }
343
- return [url.toString(), requestOptions];
344
- };
345
- async checkResponse(response) {
346
- if (response.status === 429) {
347
- if (response.headers.get("x-ratelimit-limit-requests")) {
348
- throw new QstashChatRatelimitError({
349
- "limit-requests": response.headers.get("x-ratelimit-limit-requests"),
350
- "limit-tokens": response.headers.get("x-ratelimit-limit-tokens"),
351
- "remaining-requests": response.headers.get("x-ratelimit-remaining-requests"),
352
- "remaining-tokens": response.headers.get("x-ratelimit-remaining-tokens"),
353
- "reset-requests": response.headers.get("x-ratelimit-reset-requests"),
354
- "reset-tokens": response.headers.get("x-ratelimit-reset-tokens")
355
- });
356
- } else if (response.headers.get("RateLimit-Limit")) {
357
- throw new QstashDailyRatelimitError({
358
- limit: response.headers.get("RateLimit-Limit"),
359
- remaining: response.headers.get("RateLimit-Remaining"),
360
- reset: response.headers.get("RateLimit-Reset")
361
- });
466
+ }
467
+ const regionEnvironment = getRegionFromEnvironment(environment);
468
+ if (regionEnvironment) {
469
+ const regionHeader = normalizeRegionHeader(regionFromHeader);
470
+ if (regionHeader) {
471
+ const regionCreds = readReceiverEnvironmentVariables(environment, regionHeader);
472
+ if (regionCreds.QSTASH_CURRENT_SIGNING_KEY && regionCreds.QSTASH_NEXT_SIGNING_KEY) {
473
+ return {
474
+ currentSigningKey: regionCreds.QSTASH_CURRENT_SIGNING_KEY,
475
+ nextSigningKey: regionCreds.QSTASH_NEXT_SIGNING_KEY,
476
+ region: regionHeader
477
+ };
478
+ } else {
479
+ console.warn(
480
+ `[Upstash QStash] Signing keys not found for region "${regionHeader}". Falling back to default signing keys.`
481
+ );
362
482
  }
363
- throw new QstashRatelimitError({
364
- limit: response.headers.get("Burst-RateLimit-Limit"),
365
- remaining: response.headers.get("Burst-RateLimit-Remaining"),
366
- reset: response.headers.get("Burst-RateLimit-Reset")
367
- });
368
- }
369
- if (response.status < 200 || response.status >= 300) {
370
- const body = await response.text();
371
- throw new QstashError(
372
- body.length > 0 ? body : `Error: status=${response.status}`,
373
- response.status
483
+ } else {
484
+ console.warn(
485
+ `[Upstash QStash] Invalid UPSTASH_REGION header value: "${regionFromHeader}". Expected one of: EU-CENTRAL-1, US-EAST-1. Falling back to default signing keys.`
374
486
  );
375
487
  }
376
488
  }
489
+ const defaultCreds = readReceiverEnvironmentVariables(environment);
490
+ if (defaultCreds.QSTASH_CURRENT_SIGNING_KEY && defaultCreds.QSTASH_NEXT_SIGNING_KEY) {
491
+ return {
492
+ currentSigningKey: defaultCreds.QSTASH_CURRENT_SIGNING_KEY,
493
+ nextSigningKey: defaultCreds.QSTASH_NEXT_SIGNING_KEY
494
+ };
495
+ }
377
496
  };
378
497
 
379
- // src/client/llm/providers.ts
380
- var setupAnalytics = (analytics, providerApiKey, providerBaseUrl, provider) => {
381
- if (!analytics)
382
- return {};
383
- switch (analytics.name) {
384
- case "helicone": {
385
- switch (provider) {
386
- case "upstash": {
387
- return {
388
- baseURL: "https://qstash.helicone.ai/llm/v1/chat/completions",
389
- defaultHeaders: {
390
- "Helicone-Auth": `Bearer ${analytics.token}`,
391
- Authorization: `Bearer ${providerApiKey}`
392
- }
393
- };
394
- }
395
- default: {
396
- return {
397
- baseURL: "https://gateway.helicone.ai/v1/chat/completions",
398
- defaultHeaders: {
399
- "Helicone-Auth": `Bearer ${analytics.token}`,
400
- "Helicone-Target-Url": providerBaseUrl,
401
- Authorization: `Bearer ${providerApiKey}`
402
- }
403
- };
404
- }
405
- }
406
- }
407
- default: {
408
- throw new Error("Unknown analytics provider");
498
+ // src/client/multi-region/outgoing.ts
499
+ var getClientCredentials = (clientCredentialConfig) => {
500
+ const credentials = resolveCredentials(clientCredentialConfig);
501
+ return verifyCredentials(credentials);
502
+ };
503
+ var resolveCredentials = ({
504
+ environment,
505
+ config
506
+ }) => {
507
+ if (config?.baseUrl && config.token) {
508
+ return {
509
+ baseUrl: config.baseUrl,
510
+ token: config.token
511
+ };
512
+ }
513
+ const region = getRegionFromEnvironment(environment);
514
+ if (region) {
515
+ const regionCreds = readClientEnvironmentVariables(environment, region);
516
+ if (regionCreds.QSTASH_URL && regionCreds.QSTASH_TOKEN) {
517
+ return {
518
+ baseUrl: regionCreds.QSTASH_URL,
519
+ token: regionCreds.QSTASH_TOKEN,
520
+ region
521
+ };
522
+ } else {
523
+ console.warn(
524
+ `[Upstash QStash] QSTASH_REGION is set to "${region}" but credentials are missing. Expected ${region}_QSTASH_URL and ${region}_QSTASH_TOKEN. Falling back to default credentials.`
525
+ );
409
526
  }
410
527
  }
528
+ const defaultCreds = readClientEnvironmentVariables(environment);
529
+ return {
530
+ baseUrl: config?.baseUrl ?? defaultCreds.QSTASH_URL ?? DEFAULT_QSTASH_URL,
531
+ token: config?.token ?? defaultCreds.QSTASH_TOKEN ?? ""
532
+ };
533
+ };
534
+ var verifyCredentials = (credentials) => {
535
+ const token = credentials.token;
536
+ let baseUrl = credentials.baseUrl;
537
+ baseUrl = baseUrl.replace(/\/$/, "");
538
+ if (baseUrl === "https://qstash.upstash.io/v2/publish") {
539
+ baseUrl = DEFAULT_QSTASH_URL;
540
+ }
541
+ if (!token) {
542
+ console.warn(
543
+ "[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable."
544
+ );
545
+ }
546
+ return { baseUrl, token };
411
547
  };
412
548
 
413
- // src/client/llm/chat.ts
414
- var Chat = class _Chat {
415
- http;
416
- token;
417
- constructor(http, token) {
418
- this.http = http;
419
- this.token = token;
549
+ // src/receiver.ts
550
+ var SignatureError = class extends Error {
551
+ constructor(message) {
552
+ super(message);
553
+ this.name = "SignatureError";
420
554
  }
421
- static toChatRequest(request) {
422
- const messages = [];
423
- messages.push(
424
- { role: "system", content: request.system },
425
- { role: "user", content: request.user }
426
- );
427
- const chatRequest = { ...request, messages };
428
- return chatRequest;
555
+ };
556
+ var Receiver = class {
557
+ currentSigningKey;
558
+ nextSigningKey;
559
+ constructor(config) {
560
+ this.currentSigningKey = config?.currentSigningKey;
561
+ this.nextSigningKey = config?.nextSigningKey;
429
562
  }
430
563
  /**
431
- * Calls the Upstash completions api given a ChatRequest.
564
+ * Verify the signature of a request.
432
565
  *
433
- * Returns a ChatCompletion or a stream of ChatCompletionChunks
434
- * if stream is enabled.
566
+ * Tries to verify the signature with the current signing key.
567
+ * If that fails, maybe because you have rotated the keys recently, it will
568
+ * try to verify the signature with the next signing key.
435
569
  *
436
- * @param request ChatRequest with messages
437
- * @returns Chat completion or stream
570
+ * If that fails, the signature is invalid and a `SignatureError` is thrown.
438
571
  */
439
- create = async (request) => {
440
- if (request.provider.owner != "upstash")
441
- return this.createThirdParty(request);
442
- const body = JSON.stringify(request);
443
- let baseUrl = void 0;
444
- let headers = {
445
- "Content-Type": "application/json",
446
- Authorization: `Bearer ${this.token}`,
447
- ..."stream" in request && request.stream ? {
448
- Connection: "keep-alive",
449
- Accept: "text/event-stream",
450
- "Cache-Control": "no-cache"
451
- } : {}
452
- };
453
- if (request.analytics) {
454
- const { baseURL, defaultHeaders } = setupAnalytics(
455
- { name: "helicone", token: request.analytics.token },
456
- this.getAuthorizationToken(),
457
- request.provider.baseUrl,
458
- "upstash"
572
+ async verify(request) {
573
+ const environment = getSafeEnvironment();
574
+ const signingKeys = getReceiverSigningKeys({
575
+ environment,
576
+ regionFromHeader: request.upstashRegion,
577
+ config: {
578
+ currentSigningKey: this.currentSigningKey,
579
+ nextSigningKey: this.nextSigningKey
580
+ }
581
+ });
582
+ if (!signingKeys) {
583
+ throw new Error(
584
+ "[Upstash QStash] No signing keys available for verification. See the warning above for more details."
459
585
  );
460
- headers = { ...headers, ...defaultHeaders };
461
- baseUrl = baseURL;
462
586
  }
463
- const path = request.analytics ? [] : ["llm", "v1", "chat", "completions"];
464
- return "stream" in request && request.stream ? this.http.requestStream({
465
- path,
466
- method: "POST",
467
- headers,
468
- baseUrl,
469
- body
470
- }) : this.http.request({
471
- path,
472
- method: "POST",
473
- headers,
474
- baseUrl,
475
- body
476
- });
477
- };
587
+ let payload;
588
+ try {
589
+ payload = await this.verifyWithKey(signingKeys.currentSigningKey, request);
590
+ } catch {
591
+ payload = await this.verifyWithKey(signingKeys.nextSigningKey, request);
592
+ }
593
+ this.verifyBodyAndUrl(payload, request);
594
+ return true;
595
+ }
478
596
  /**
479
- * Calls the Upstash completions api given a ChatRequest.
480
- *
481
- * Returns a ChatCompletion or a stream of ChatCompletionChunks
482
- * if stream is enabled.
483
- *
484
- * @param request ChatRequest with messages
485
- * @returns Chat completion or stream
597
+ * Verify signature with a specific signing key
486
598
  */
487
- createThirdParty = async (request) => {
488
- const { baseUrl, token, owner, organization } = request.provider;
489
- if (owner === "upstash")
490
- throw new Error("Upstash is not 3rd party provider!");
491
- delete request.provider;
492
- delete request.system;
493
- const analytics = request.analytics;
494
- delete request.analytics;
495
- const body = JSON.stringify(request);
496
- const isAnalyticsEnabled = analytics?.name && analytics.token;
497
- const analyticsConfig = analytics?.name && analytics.token ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, owner) : { defaultHeaders: void 0, baseURL: baseUrl };
498
- const isStream = "stream" in request && request.stream;
499
- const headers = {
500
- "Content-Type": "application/json",
501
- Authorization: `Bearer ${token}`,
502
- ...organization ? {
503
- "OpenAI-Organization": organization
504
- } : {},
505
- ...isStream ? {
506
- Connection: "keep-alive",
507
- Accept: "text/event-stream",
508
- "Cache-Control": "no-cache"
509
- } : {},
510
- ...analyticsConfig.defaultHeaders
511
- };
512
- const response = await this.http[isStream ? "requestStream" : "request"]({
513
- path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"],
514
- method: "POST",
515
- headers,
516
- body,
517
- baseUrl: analyticsConfig.baseURL
599
+ async verifyWithKey(key, request) {
600
+ const jwt = await jose.jwtVerify(request.signature, new TextEncoder().encode(key), {
601
+ issuer: "Upstash",
602
+ clockTolerance: request.clockTolerance
603
+ }).catch((error) => {
604
+ throw new SignatureError(error.message);
518
605
  });
519
- return response;
520
- };
521
- // Helper method to get the authorization token
522
- getAuthorizationToken() {
523
- const authHeader = String(this.http.authorization);
524
- const match = /Bearer (.+)/.exec(authHeader);
525
- if (!match) {
526
- throw new Error("Invalid authorization header format");
606
+ return jwt.payload;
607
+ }
608
+ verifyBodyAndUrl(payload, request) {
609
+ const p = payload;
610
+ if (request.url !== void 0 && p.sub !== request.url) {
611
+ throw new SignatureError(`invalid subject: ${p.sub}, want: ${request.url}`);
612
+ }
613
+ const bodyHash = import_crypto_js.default.SHA256(request.body).toString(import_crypto_js.default.enc.Base64url);
614
+ const padding = new RegExp(/=+$/);
615
+ if (p.body.replace(padding, "") !== bodyHash.replace(padding, "")) {
616
+ throw new SignatureError(`body hash does not match, want: ${p.body}, got: ${bodyHash}`);
527
617
  }
528
- return match[1];
529
618
  }
530
- /**
531
- * Calls the Upstash completions api given a PromptRequest.
532
- *
533
- * Returns a ChatCompletion or a stream of ChatCompletionChunks
534
- * if stream is enabled.
535
- *
536
- * @param request PromptRequest with system and user messages.
537
- * Note that system parameter shouldn't be passed in the case of
538
- * mistralai/Mistral-7B-Instruct-v0.2 model.
539
- * @returns Chat completion or stream
540
- */
541
- prompt = async (request) => {
542
- const chatRequest = _Chat.toChatRequest(request);
543
- return this.create(chatRequest);
544
- };
545
619
  };
546
620
 
547
- // src/client/messages.ts
548
- var Messages = class {
621
+ // src/client/dlq.ts
622
+ var DLQ = class {
549
623
  http;
550
624
  constructor(http) {
551
625
  this.http = http;
552
626
  }
553
627
  /**
554
- * Get a message
628
+ * List messages in the dlq
555
629
  */
556
- async get(messageId) {
557
- const messagePayload = await this.http.request({
630
+ async listMessages(options) {
631
+ const filterPayload = {
632
+ ...options?.filter,
633
+ topicName: options?.filter?.urlGroup
634
+ };
635
+ const messagesPayload = await this.http.request({
558
636
  method: "GET",
559
- path: ["v2", "messages", messageId]
637
+ path: ["v2", "dlq"],
638
+ query: {
639
+ cursor: options?.cursor,
640
+ count: options?.count,
641
+ ...filterPayload
642
+ }
560
643
  });
561
- const message = {
562
- ...messagePayload,
563
- urlGroup: messagePayload.topicName,
564
- ratePerSecond: "rate" in messagePayload ? messagePayload.rate : void 0
644
+ return {
645
+ messages: messagesPayload.messages.map((message) => {
646
+ return {
647
+ ...message,
648
+ urlGroup: message.topicName,
649
+ ratePerSecond: "rate" in message ? message.rate : void 0
650
+ };
651
+ }),
652
+ cursor: messagesPayload.cursor
565
653
  };
566
- return message;
567
654
  }
568
655
  /**
569
- * Cancel a message
656
+ * Remove a message from the dlq using it's `dlqId`
570
657
  */
571
- async delete(messageId) {
658
+ async delete(dlqMessageId) {
572
659
  return await this.http.request({
573
660
  method: "DELETE",
574
- path: ["v2", "messages", messageId],
661
+ path: ["v2", "dlq", dlqMessageId],
575
662
  parseResponseAsJson: false
663
+ // there is no response
576
664
  });
577
665
  }
578
- async deleteMany(messageIds) {
579
- const result = await this.http.request({
666
+ /**
667
+ * Remove multiple messages from the dlq using their `dlqId`s
668
+ */
669
+ async deleteMany(request) {
670
+ return await this.http.request({
580
671
  method: "DELETE",
581
- path: ["v2", "messages"],
672
+ path: ["v2", "dlq"],
582
673
  headers: { "Content-Type": "application/json" },
583
- body: JSON.stringify({ messageIds })
674
+ body: JSON.stringify({ dlqIds: request.dlqIds })
584
675
  });
585
- return result.cancelled;
586
- }
587
- async deleteAll() {
588
- const result = await this.http.request({
589
- method: "DELETE",
590
- path: ["v2", "messages"]
591
- });
592
- return result.cancelled;
593
676
  }
594
677
  };
595
678
 
596
- // src/client/api/base.ts
597
- var BaseProvider = class {
679
+ // src/client/http.ts
680
+ var HttpClient = class {
598
681
  baseUrl;
599
- token;
600
- owner;
601
- constructor(baseUrl, token, owner) {
602
- this.baseUrl = baseUrl;
603
- this.token = token;
604
- this.owner = owner;
605
- }
606
- getUrl() {
607
- return `${this.baseUrl}/${this.getRoute().join("/")}`;
608
- }
609
- };
610
-
611
- // src/client/api/llm.ts
612
- var LLMProvider = class extends BaseProvider {
613
- apiKind = "llm";
614
- organization;
615
- method = "POST";
616
- constructor(baseUrl, token, owner, organization) {
617
- super(baseUrl, token, owner);
618
- this.organization = organization;
619
- }
620
- getRoute() {
621
- return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"];
682
+ authorization;
683
+ options;
684
+ retry;
685
+ headers;
686
+ telemetryHeaders;
687
+ constructor(config) {
688
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
689
+ this.authorization = config.authorization;
690
+ this.retry = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
691
+ typeof config.retry === "boolean" && !config.retry ? {
692
+ attempts: 1,
693
+ backoff: () => 0
694
+ } : {
695
+ attempts: config.retry?.retries ?? 5,
696
+ backoff: config.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50)
697
+ };
698
+ this.headers = config.headers;
699
+ this.telemetryHeaders = config.telemetryHeaders;
622
700
  }
623
- getHeaders(options) {
624
- if (this.owner === "upstash" && !options.analytics) {
625
- return { "content-type": "application/json" };
701
+ async request(request) {
702
+ const { response } = await this.requestWithBackoff(request);
703
+ if (request.parseResponseAsJson === false) {
704
+ return void 0;
626
705
  }
627
- const header = this.owner === "anthropic" ? "x-api-key" : "authorization";
628
- const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`;
629
- const headers = {
630
- [header]: headerValue,
631
- "content-type": "application/json"
632
- };
633
- if (this.owner === "openai" && this.organization) {
634
- headers["OpenAI-Organization"] = this.organization;
706
+ return await response.json();
707
+ }
708
+ async *requestStream(request) {
709
+ const { response } = await this.requestWithBackoff(request);
710
+ if (!response.body) {
711
+ throw new Error("No response body");
635
712
  }
636
- if (this.owner === "anthropic") {
637
- headers["anthropic-version"] = "2023-06-01";
713
+ const body = response.body;
714
+ const reader = body.getReader();
715
+ const decoder = new TextDecoder();
716
+ try {
717
+ while (true) {
718
+ const { done, value } = await reader.read();
719
+ if (done) {
720
+ break;
721
+ }
722
+ const chunkText = decoder.decode(value, { stream: true });
723
+ const chunks = chunkText.split("\n").filter(Boolean);
724
+ for (const chunk of chunks) {
725
+ if (chunk.startsWith("data: ")) {
726
+ const data = chunk.slice(6);
727
+ if (data === "[DONE]") {
728
+ break;
729
+ }
730
+ yield JSON.parse(data);
731
+ }
732
+ }
733
+ }
734
+ } finally {
735
+ await reader.cancel();
638
736
  }
639
- return headers;
640
737
  }
641
- /**
642
- * Checks if callback exists and adds analytics in place if it's set.
643
- *
644
- * @param request
645
- * @param options
646
- */
647
- onFinish(providerInfo, options) {
648
- if (options.analytics) {
649
- return updateWithAnalytics(providerInfo, options.analytics);
738
+ requestWithBackoff = async (request) => {
739
+ const [url, requestOptions] = this.processRequest(request);
740
+ let response = void 0;
741
+ let error = void 0;
742
+ for (let index = 0; index <= this.retry.attempts; index++) {
743
+ try {
744
+ response = await fetch(url.toString(), requestOptions);
745
+ break;
746
+ } catch (error_) {
747
+ error = error_;
748
+ if (index < this.retry.attempts) {
749
+ await new Promise((r) => setTimeout(r, this.retry.backoff(index)));
750
+ }
751
+ }
650
752
  }
651
- return providerInfo;
652
- }
653
- };
654
- var upstash = () => {
655
- return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash");
656
- };
657
-
658
- // src/client/api/utils.ts
659
- var getProviderInfo = (api, upstashToken) => {
660
- const { name, provider, ...parameters } = api;
661
- const finalProvider = provider ?? upstash();
662
- if (finalProvider.owner === "upstash" && !finalProvider.token) {
663
- finalProvider.token = upstashToken;
664
- }
665
- if (!finalProvider.baseUrl)
666
- throw new TypeError("baseUrl cannot be empty or undefined!");
667
- if (!finalProvider.token)
668
- throw new TypeError("token cannot be empty or undefined!");
669
- if (finalProvider.apiKind !== name) {
670
- throw new TypeError(
671
- `Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}`
672
- );
673
- }
674
- const providerInfo = {
675
- url: finalProvider.getUrl(),
676
- baseUrl: finalProvider.baseUrl,
677
- route: finalProvider.getRoute(),
678
- appendHeaders: finalProvider.getHeaders(parameters),
679
- owner: finalProvider.owner,
680
- method: finalProvider.method
681
- };
682
- return finalProvider.onFinish(providerInfo, parameters);
683
- };
684
- var safeJoinHeaders = (headers, record) => {
685
- const joinedHeaders = new Headers(record);
686
- for (const [header, value] of headers.entries()) {
687
- joinedHeaders.set(header, value);
688
- }
689
- return joinedHeaders;
690
- };
691
- var processApi = (request, headers, upstashToken) => {
692
- if (!request.api) {
693
- request.headers = headers;
694
- return request;
695
- }
696
- const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken);
697
- if (request.api.name === "llm") {
698
- const callback = request.callback;
699
- if (!callback) {
700
- throw new TypeError("Callback cannot be undefined when using LLM api.");
753
+ if (!response) {
754
+ throw error ?? new Error("Exhausted all retries");
701
755
  }
756
+ await this.checkResponse(response);
702
757
  return {
703
- ...request,
704
- method: request.method ?? method,
705
- headers: safeJoinHeaders(headers, appendHeaders),
706
- ...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 }
758
+ response,
759
+ error
707
760
  };
708
- } else {
709
- return {
710
- ...request,
711
- method: request.method ?? method,
712
- headers: safeJoinHeaders(headers, appendHeaders),
713
- url,
714
- api: void 0
761
+ };
762
+ processRequest = (request) => {
763
+ const headers = new Headers(request.headers);
764
+ if (!headers.has("Authorization")) {
765
+ headers.set("Authorization", this.authorization);
766
+ }
767
+ const requestOptions = {
768
+ method: request.method,
769
+ headers,
770
+ body: request.body,
771
+ keepalive: request.keepalive
715
772
  };
716
- }
717
- };
718
- function updateWithAnalytics(providerInfo, analytics) {
719
- switch (analytics.name) {
720
- case "helicone": {
721
- providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`;
722
- if (providerInfo.owner === "upstash") {
723
- updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [
724
- "llm",
725
- ...providerInfo.route
726
- ]);
727
- } else {
728
- providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl;
729
- updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route);
773
+ const url = new URL([request.baseUrl ?? this.baseUrl, ...request.path].join("/"));
774
+ if (request.query) {
775
+ for (const [key, value] of Object.entries(request.query)) {
776
+ if (value !== void 0) {
777
+ url.searchParams.set(key, value.toString());
778
+ }
730
779
  }
731
- return providerInfo;
732
780
  }
733
- default: {
734
- throw new Error("Unknown analytics provider");
735
- }
736
- }
737
- }
738
- function updateProviderInfo(providerInfo, baseUrl, route) {
739
- providerInfo.baseUrl = baseUrl;
740
- providerInfo.route = route;
741
- providerInfo.url = `${baseUrl}/${route.join("/")}`;
742
- }
743
-
744
- // src/client/utils.ts
745
- var isIgnoredHeader = (header) => {
746
- const lowerCaseHeader = header.toLowerCase();
747
- return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
748
- };
749
- function prefixHeaders(headers) {
750
- const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
751
- for (const key of keysToBePrefixed) {
752
- const value = headers.get(key);
753
- if (value !== null) {
754
- headers.set(`Upstash-Forward-${key}`, value);
781
+ return [url.toString(), requestOptions];
782
+ };
783
+ async checkResponse(response) {
784
+ if (response.status === 429) {
785
+ if (response.headers.get("x-ratelimit-limit-requests")) {
786
+ throw new QstashChatRatelimitError({
787
+ "limit-requests": response.headers.get("x-ratelimit-limit-requests"),
788
+ "limit-tokens": response.headers.get("x-ratelimit-limit-tokens"),
789
+ "remaining-requests": response.headers.get("x-ratelimit-remaining-requests"),
790
+ "remaining-tokens": response.headers.get("x-ratelimit-remaining-tokens"),
791
+ "reset-requests": response.headers.get("x-ratelimit-reset-requests"),
792
+ "reset-tokens": response.headers.get("x-ratelimit-reset-tokens")
793
+ });
794
+ } else if (response.headers.get("RateLimit-Limit")) {
795
+ throw new QstashDailyRatelimitError({
796
+ limit: response.headers.get("RateLimit-Limit"),
797
+ remaining: response.headers.get("RateLimit-Remaining"),
798
+ reset: response.headers.get("RateLimit-Reset")
799
+ });
800
+ }
801
+ throw new QstashRatelimitError({
802
+ limit: response.headers.get("Burst-RateLimit-Limit"),
803
+ remaining: response.headers.get("Burst-RateLimit-Remaining"),
804
+ reset: response.headers.get("Burst-RateLimit-Reset")
805
+ });
755
806
  }
756
- headers.delete(key);
757
- }
758
- return headers;
759
- }
760
- function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) {
761
- if (!globalHeaders) {
762
- return headers;
763
- }
764
- const finalHeaders = new Headers(globalHeaders);
765
- headers.forEach((value, key) => {
766
- finalHeaders.set(key, value);
767
- });
768
- telemetryHeaders?.forEach((value, key) => {
769
- if (!value)
770
- return;
771
- finalHeaders.append(key, value);
772
- });
773
- return finalHeaders;
774
- }
775
- function processHeaders(request) {
776
- const headers = prefixHeaders(new Headers(request.headers));
777
- headers.set("Upstash-Method", request.method ?? "POST");
778
- if (request.delay !== void 0) {
779
- if (typeof request.delay === "string") {
780
- headers.set("Upstash-Delay", request.delay);
781
- } else {
782
- headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
807
+ if (response.status < 200 || response.status >= 300) {
808
+ const body = await response.text();
809
+ throw new QstashError(
810
+ body.length > 0 ? body : `Error: status=${response.status}`,
811
+ response.status
812
+ );
783
813
  }
784
814
  }
785
- if (request.notBefore !== void 0) {
786
- headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
787
- }
788
- if (request.deduplicationId !== void 0) {
789
- headers.set("Upstash-Deduplication-Id", request.deduplicationId);
790
- }
791
- if (request.contentBasedDeduplication) {
792
- headers.set("Upstash-Content-Based-Deduplication", "true");
793
- }
794
- if (request.retries !== void 0) {
795
- headers.set("Upstash-Retries", request.retries.toFixed(0));
796
- }
797
- if (request.retryDelay !== void 0) {
798
- headers.set("Upstash-Retry-Delay", request.retryDelay);
815
+ };
816
+
817
+ // src/client/llm/providers.ts
818
+ var setupAnalytics = (analytics, providerApiKey, providerBaseUrl, provider) => {
819
+ if (!analytics)
820
+ return {};
821
+ switch (analytics.name) {
822
+ case "helicone": {
823
+ switch (provider) {
824
+ case "upstash": {
825
+ return {
826
+ baseURL: "https://qstash.helicone.ai/llm/v1/chat/completions",
827
+ defaultHeaders: {
828
+ "Helicone-Auth": `Bearer ${analytics.token}`,
829
+ Authorization: `Bearer ${providerApiKey}`
830
+ }
831
+ };
832
+ }
833
+ default: {
834
+ return {
835
+ baseURL: "https://gateway.helicone.ai/v1/chat/completions",
836
+ defaultHeaders: {
837
+ "Helicone-Auth": `Bearer ${analytics.token}`,
838
+ "Helicone-Target-Url": providerBaseUrl,
839
+ Authorization: `Bearer ${providerApiKey}`
840
+ }
841
+ };
842
+ }
843
+ }
844
+ }
845
+ default: {
846
+ throw new Error("Unknown analytics provider");
847
+ }
799
848
  }
800
- if (request.callback !== void 0) {
801
- headers.set("Upstash-Callback", request.callback);
849
+ };
850
+
851
+ // src/client/llm/chat.ts
852
+ var Chat = class _Chat {
853
+ http;
854
+ token;
855
+ constructor(http, token) {
856
+ this.http = http;
857
+ this.token = token;
802
858
  }
803
- if (request.failureCallback !== void 0) {
804
- headers.set("Upstash-Failure-Callback", request.failureCallback);
859
+ static toChatRequest(request) {
860
+ const messages = [];
861
+ messages.push(
862
+ { role: "system", content: request.system },
863
+ { role: "user", content: request.user }
864
+ );
865
+ const chatRequest = { ...request, messages };
866
+ return chatRequest;
805
867
  }
806
- if (request.timeout !== void 0) {
807
- if (typeof request.timeout === "string") {
808
- headers.set("Upstash-Timeout", request.timeout);
809
- } else {
810
- headers.set("Upstash-Timeout", `${request.timeout}s`);
868
+ /**
869
+ * Calls the Upstash completions api given a ChatRequest.
870
+ *
871
+ * Returns a ChatCompletion or a stream of ChatCompletionChunks
872
+ * if stream is enabled.
873
+ *
874
+ * @param request ChatRequest with messages
875
+ * @returns Chat completion or stream
876
+ */
877
+ create = async (request) => {
878
+ if (request.provider.owner != "upstash")
879
+ return this.createThirdParty(request);
880
+ const body = JSON.stringify(request);
881
+ let baseUrl = void 0;
882
+ let headers = {
883
+ "Content-Type": "application/json",
884
+ Authorization: `Bearer ${this.token}`,
885
+ ..."stream" in request && request.stream ? {
886
+ Connection: "keep-alive",
887
+ Accept: "text/event-stream",
888
+ "Cache-Control": "no-cache"
889
+ } : {}
890
+ };
891
+ if (request.analytics) {
892
+ const { baseURL, defaultHeaders } = setupAnalytics(
893
+ { name: "helicone", token: request.analytics.token },
894
+ this.getAuthorizationToken(),
895
+ request.provider.baseUrl,
896
+ "upstash"
897
+ );
898
+ headers = { ...headers, ...defaultHeaders };
899
+ baseUrl = baseURL;
811
900
  }
812
- }
813
- if (request.flowControl?.key) {
814
- const parallelism = request.flowControl.parallelism?.toString();
815
- const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString();
816
- const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period;
817
- const controlValue = [
818
- parallelism ? `parallelism=${parallelism}` : void 0,
819
- rate ? `rate=${rate}` : void 0,
820
- period ? `period=${period}` : void 0
821
- ].filter(Boolean);
822
- if (controlValue.length === 0) {
823
- throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl");
901
+ const path = request.analytics ? [] : ["llm", "v1", "chat", "completions"];
902
+ return "stream" in request && request.stream ? this.http.requestStream({
903
+ path,
904
+ method: "POST",
905
+ headers,
906
+ baseUrl,
907
+ body
908
+ }) : this.http.request({
909
+ path,
910
+ method: "POST",
911
+ headers,
912
+ baseUrl,
913
+ body
914
+ });
915
+ };
916
+ /**
917
+ * Calls the Upstash completions api given a ChatRequest.
918
+ *
919
+ * Returns a ChatCompletion or a stream of ChatCompletionChunks
920
+ * if stream is enabled.
921
+ *
922
+ * @param request ChatRequest with messages
923
+ * @returns Chat completion or stream
924
+ */
925
+ createThirdParty = async (request) => {
926
+ const { baseUrl, token, owner, organization } = request.provider;
927
+ if (owner === "upstash")
928
+ throw new Error("Upstash is not 3rd party provider!");
929
+ delete request.provider;
930
+ delete request.system;
931
+ const analytics = request.analytics;
932
+ delete request.analytics;
933
+ const body = JSON.stringify(request);
934
+ const isAnalyticsEnabled = analytics?.name && analytics.token;
935
+ const analyticsConfig = analytics?.name && analytics.token ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, owner) : { defaultHeaders: void 0, baseURL: baseUrl };
936
+ const isStream = "stream" in request && request.stream;
937
+ const headers = {
938
+ "Content-Type": "application/json",
939
+ Authorization: `Bearer ${token}`,
940
+ ...organization ? {
941
+ "OpenAI-Organization": organization
942
+ } : {},
943
+ ...isStream ? {
944
+ Connection: "keep-alive",
945
+ Accept: "text/event-stream",
946
+ "Cache-Control": "no-cache"
947
+ } : {},
948
+ ...analyticsConfig.defaultHeaders
949
+ };
950
+ const response = await this.http[isStream ? "requestStream" : "request"]({
951
+ path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"],
952
+ method: "POST",
953
+ headers,
954
+ body,
955
+ baseUrl: analyticsConfig.baseURL
956
+ });
957
+ return response;
958
+ };
959
+ // Helper method to get the authorization token
960
+ getAuthorizationToken() {
961
+ const authHeader = String(this.http.authorization);
962
+ const match = /Bearer (.+)/.exec(authHeader);
963
+ if (!match) {
964
+ throw new Error("Invalid authorization header format");
824
965
  }
825
- headers.set("Upstash-Flow-Control-Key", request.flowControl.key);
826
- headers.set("Upstash-Flow-Control-Value", controlValue.join(", "));
966
+ return match[1];
827
967
  }
828
- if (request.label !== void 0) {
829
- headers.set("Upstash-Label", request.label);
968
+ /**
969
+ * Calls the Upstash completions api given a PromptRequest.
970
+ *
971
+ * Returns a ChatCompletion or a stream of ChatCompletionChunks
972
+ * if stream is enabled.
973
+ *
974
+ * @param request PromptRequest with system and user messages.
975
+ * Note that system parameter shouldn't be passed in the case of
976
+ * mistralai/Mistral-7B-Instruct-v0.2 model.
977
+ * @returns Chat completion or stream
978
+ */
979
+ prompt = async (request) => {
980
+ const chatRequest = _Chat.toChatRequest(request);
981
+ return this.create(chatRequest);
982
+ };
983
+ };
984
+
985
+ // src/client/messages.ts
986
+ var Messages = class {
987
+ http;
988
+ constructor(http) {
989
+ this.http = http;
830
990
  }
831
- return headers;
832
- }
833
- function getRequestPath(request) {
834
- const nonApiPath = request.url ?? request.urlGroup ?? request.topic;
835
- if (nonApiPath)
836
- return nonApiPath;
837
- if (request.api?.name === "llm")
838
- return `api/llm`;
839
- if (request.api?.name === "email") {
840
- const providerInfo = getProviderInfo(request.api, "not-needed");
841
- return providerInfo.baseUrl;
991
+ /**
992
+ * Get a message
993
+ */
994
+ async get(messageId) {
995
+ const messagePayload = await this.http.request({
996
+ method: "GET",
997
+ path: ["v2", "messages", messageId]
998
+ });
999
+ const message = {
1000
+ ...messagePayload,
1001
+ urlGroup: messagePayload.topicName,
1002
+ ratePerSecond: "rate" in messagePayload ? messagePayload.rate : void 0
1003
+ };
1004
+ return message;
842
1005
  }
843
- throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`);
844
- }
845
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
846
- var NANOID_LENGTH = 21;
847
- function nanoid() {
848
- return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
849
- }
850
- function decodeBase64(base64) {
851
- try {
852
- const binString = atob(base64);
853
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
854
- return new TextDecoder().decode(intArray);
855
- } catch (error) {
856
- try {
857
- const result = atob(base64);
858
- console.warn(
859
- `Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
860
- );
861
- return result;
862
- } catch (error2) {
863
- console.warn(
864
- `Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}`
865
- );
866
- return base64;
867
- }
1006
+ /**
1007
+ * Cancel a message
1008
+ */
1009
+ async delete(messageId) {
1010
+ return await this.http.request({
1011
+ method: "DELETE",
1012
+ path: ["v2", "messages", messageId],
1013
+ parseResponseAsJson: false
1014
+ });
868
1015
  }
869
- }
870
- function getRuntime() {
871
- if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun)
872
- return `bun@${process.versions.bun}`;
873
- if (typeof EdgeRuntime === "string")
874
- return "edge-light";
875
- else if (typeof process === "object" && typeof process.version === "string")
876
- return `node@${process.version}`;
877
- return "";
878
- }
1016
+ async deleteMany(messageIds) {
1017
+ const result = await this.http.request({
1018
+ method: "DELETE",
1019
+ path: ["v2", "messages"],
1020
+ headers: { "Content-Type": "application/json" },
1021
+ body: JSON.stringify({ messageIds })
1022
+ });
1023
+ return result.cancelled;
1024
+ }
1025
+ async deleteAll() {
1026
+ const result = await this.http.request({
1027
+ method: "DELETE",
1028
+ path: ["v2", "messages"]
1029
+ });
1030
+ return result.cancelled;
1031
+ }
1032
+ };
879
1033
 
880
1034
  // src/client/queue.ts
881
1035
  var Queue = class {
@@ -1206,19 +1360,15 @@ var UrlGroups = class {
1206
1360
  };
1207
1361
 
1208
1362
  // version.ts
1209
- var VERSION = "v2.8.4";
1363
+ var VERSION = "v2.9.0-rc";
1210
1364
 
1211
1365
  // src/client/client.ts
1212
1366
  var Client = class {
1213
1367
  http;
1214
1368
  token;
1215
1369
  constructor(config) {
1216
- const environment = typeof process === "undefined" ? {} : process.env;
1217
- let baseUrl = (config?.baseUrl ?? environment.QSTASH_URL ?? "https://qstash.upstash.io").replace(/\/$/, "");
1218
- if (baseUrl === "https://qstash.upstash.io/v2/publish") {
1219
- baseUrl = "https://qstash.upstash.io";
1220
- }
1221
- const token = config?.token ?? environment.QSTASH_TOKEN;
1370
+ const environment = getSafeEnvironment();
1371
+ const { baseUrl, token } = getClientCredentials({ environment, config });
1222
1372
  const enableTelemetry = environment.UPSTASH_DISABLE_TELEMETRY ? false : config?.enableTelemetry ?? true;
1223
1373
  const isCloudflare = typeof caches !== "undefined" && "default" in caches;
1224
1374
  const telemetryHeaders = new Headers(
@@ -1237,11 +1387,6 @@ var Client = class {
1237
1387
  //@ts-expect-error caused by undici and bunjs type overlap
1238
1388
  telemetryHeaders
1239
1389
  });
1240
- if (!token) {
1241
- console.warn(
1242
- "[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable."
1243
- );
1244
- }
1245
1390
  this.token = token;
1246
1391
  }
1247
1392
  /**