@upstash/qstash 2.8.4 → 2.9.0

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