@telemetry-dev/sdk 0.1.11 → 0.2.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/README.md +24 -17
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +31 -4
- package/package.json +2 -2
- package/src/config.ts +1 -1
- package/src/prompts.ts +44 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pure TypeScript SDK for [telemetry.dev](https://telemetry.dev): add LLM traces, logs, and GenAI
|
|
4
4
|
metrics to any app. Built on OpenTelemetry — spans use the `gen_ai.*` semantic conventions and
|
|
5
|
-
ship as OTLP protobuf to `https://
|
|
5
|
+
ship as OTLP protobuf to `https://api.telemetry.dev`. Works on Node >= 20.19 and edge runtimes
|
|
6
6
|
(Vercel Edge, and Cloudflare Workers with Node.js compatibility, which is on by default for
|
|
7
7
|
compatibility dates of 2026-08-04 or later; earlier dates need the `nodejs_compat` flag). ESM-only;
|
|
8
8
|
CJS consumers can `require()` it on Node >= 20.19.
|
|
@@ -59,12 +59,16 @@ Without an API key every call is a silent no-op — the SDK never throws into yo
|
|
|
59
59
|
|
|
60
60
|
## Configuration
|
|
61
61
|
|
|
62
|
-
| Option | Env var | Default
|
|
63
|
-
| ------------- | --------------------------- |
|
|
64
|
-
| `apiKey` | `TELEMETRY_DEV_API_KEY` | — (absent ⇒ no-op)
|
|
65
|
-
| `baseUrl` | `TELEMETRY_DEV_BASE_URL` | `https://
|
|
66
|
-
| `environment` | `TELEMETRY_DEV_ENVIRONMENT` | `production`
|
|
67
|
-
| `serviceName` | `OTEL_SERVICE_NAME` | `unknown_service`
|
|
62
|
+
| Option | Env var | Default |
|
|
63
|
+
| ------------- | --------------------------- | --------------------------- |
|
|
64
|
+
| `apiKey` | `TELEMETRY_DEV_API_KEY` | — (absent ⇒ no-op) |
|
|
65
|
+
| `baseUrl` | `TELEMETRY_DEV_BASE_URL` | `https://api.telemetry.dev` |
|
|
66
|
+
| `environment` | `TELEMETRY_DEV_ENVIRONMENT` | `production` |
|
|
67
|
+
| `serviceName` | `OTEL_SERVICE_NAME` | `unknown_service` |
|
|
68
|
+
|
|
69
|
+
Earlier versions sent data to `https://ingest.telemetry.dev` by default. That hostname reaches the
|
|
70
|
+
same service and stays supported: if a firewall allows only it, set `TELEMETRY_DEV_BASE_URL` or
|
|
71
|
+
`baseUrl` to it.
|
|
68
72
|
|
|
69
73
|
Other `init()` options: `enabled` (kill switch), `registerGlobal`, `exportMode: "batched" |
|
|
70
74
|
"immediate"`, `captureInput` / `captureOutput`, `mask(value, { key })` redaction hook,
|
|
@@ -151,11 +155,13 @@ const pinned = await getPrompt("support/reply", { version: 13 });
|
|
|
151
155
|
`timeout`. A response with a different status and no API error gives `http_<status>`, for
|
|
152
156
|
example `http_302` for a redirect. getPrompt does not send the request to the location of a
|
|
153
157
|
redirect, thus your key does not go to a different host. With no copy, the calls of the next 10
|
|
154
|
-
seconds (or the TTL, if it is shorter) get the same result without a fetch.
|
|
155
|
-
|
|
158
|
+
seconds (or the TTL, if it is shorter) get the same result without a fetch. After a 429 or 503
|
|
159
|
+
response with a `Retry-After` header, getPrompt does not fetch that prompt again before that time
|
|
160
|
+
(one hour at most). This is the one SDK function that can reject: your app cannot run without
|
|
161
|
+
its prompt.
|
|
156
162
|
- getPrompt uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY` and
|
|
157
163
|
`TELEMETRY_DEV_BASE_URL`, or its own `apiKey`, `baseUrl`, and `fetch` options. It reads the same
|
|
158
|
-
host that telemetry goes to, `https://
|
|
164
|
+
host that telemetry goes to, `https://api.telemetry.dev` by default. The key needs the read
|
|
159
165
|
scope for prompts.
|
|
160
166
|
- When a label sends part of the traffic to a second version, each session keeps one version: the
|
|
161
167
|
choice uses the session id of `propagateAttributes` (or of `sessionMode: "process"`). Without a
|
|
@@ -167,12 +173,13 @@ When input capture is on, `td.prompt.variables` holds the JSON of the variables
|
|
|
167
173
|
`compile()` of that prompt object before `withPrompt` starts, or of the last `compile()` inside
|
|
168
174
|
`fn`, after `mask` and truncation. If tasks that run at the same time share one prompt object, call
|
|
169
175
|
`compile()` in `withPrompt`, or call getPrompt in each task. getPrompt returns a new object from
|
|
170
|
-
the cache. If you do not, a task can record the variables of a different task.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
the cache. If you do not, a task can record the variables of a different task. Tasks that run at the
|
|
177
|
+
same time inside one `withPrompt` share its variables, so call `withPrompt` in each task that
|
|
178
|
+
compiles its own. Model calls are the spans of type `generation` (operations `chat`,
|
|
179
|
+
`text_completion`, and `generate_content`), including the spans of the provider integrations that
|
|
180
|
+
use this SDK: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, and OpenRouter. Spans from the AI SDK
|
|
181
|
+
and TanStack AI integrations, and from other OpenTelemetry tracers, do not get the attributes.
|
|
182
|
+
Messages from `compile()` that you pass to a model call outside `withPrompt` are not recorded.
|
|
176
183
|
|
|
177
184
|
## API
|
|
178
185
|
|
|
@@ -265,7 +272,7 @@ Alternatively `init({ registerGlobal: true })` makes the SDK's provider the app'
|
|
|
265
272
|
that mode only this SDK's spans are exported by default.
|
|
266
273
|
|
|
267
274
|
Any vanilla OTel app can also skip the SDK entirely: point a stock OTLP/HTTP protobuf exporter at
|
|
268
|
-
`https://
|
|
275
|
+
`https://api.telemetry.dev/v1/traces` with an `Authorization: Bearer td_live_...` header.
|
|
269
276
|
|
|
270
277
|
### Native Claude SDK spans
|
|
271
278
|
|
package/dist/index.d.mts
CHANGED
|
@@ -16,7 +16,7 @@ type MaskFn<T = unknown> = (value: T, ctx: {
|
|
|
16
16
|
interface TelemetryOptions {
|
|
17
17
|
/** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
|
|
18
18
|
apiKey?: string;
|
|
19
|
-
/** Base URL for telemetry export and getPrompt. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://
|
|
19
|
+
/** Base URL for telemetry export and getPrompt. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://api.telemetry.dev`. */
|
|
20
20
|
baseUrl?: string;
|
|
21
21
|
/** Deployment environment label. Falls back to `TELEMETRY_DEV_ENVIRONMENT`, then `production`. */
|
|
22
22
|
environment?: string;
|
|
@@ -355,7 +355,7 @@ interface GetPromptOptionsBase {
|
|
|
355
355
|
cacheTtlSeconds?: number;
|
|
356
356
|
/** API key with the prompts:read scope. Falls back to the initialized client, then `TELEMETRY_DEV_API_KEY`. */
|
|
357
357
|
apiKey?: string;
|
|
358
|
-
/** Falls back to the initialized client, then `TELEMETRY_DEV_BASE_URL`, then `https://
|
|
358
|
+
/** Falls back to the initialized client, then `TELEMETRY_DEV_BASE_URL`, then `https://api.telemetry.dev`. */
|
|
359
359
|
baseUrl?: string;
|
|
360
360
|
/** Falls back to the initialized client's `fetch`, then `globalThis.fetch`. */
|
|
361
361
|
fetch?: typeof fetch;
|
|
@@ -406,7 +406,8 @@ declare function getPrompt(name: string, options?: GetChatPromptOptions): Promis
|
|
|
406
406
|
* it: the spans get `td.prompt.name`, `td.prompt.version`, `td.prompt.label`, and, when input
|
|
407
407
|
* capture is on, `td.prompt.variables`. The variables are those of the last `compile()` of this
|
|
408
408
|
* prompt object before withPrompt starts, or of the last `compile()` inside `fn`, so a compile in
|
|
409
|
-
* another task does not change them once withPrompt runs.
|
|
409
|
+
* another task does not change them once withPrompt runs. Concurrent tasks in `fn` share the
|
|
410
|
+
* variables, so call withPrompt in each task that compiles its own.
|
|
410
411
|
*/
|
|
411
412
|
declare function withPrompt<T>(prompt: Prompt, fn: () => T): T;
|
|
412
413
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -563,9 +563,13 @@ var PromptError = class extends Error {
|
|
|
563
563
|
}
|
|
564
564
|
};
|
|
565
565
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
566
|
+
/** The longest Retry-After that getPrompt honors, so that a bad header cannot stop fetches. */
|
|
567
|
+
const MAX_RETRY_AFTER_MS = 36e5;
|
|
566
568
|
/** How long a failed fetch with no copy answers calls, so that they do not each fetch again. */
|
|
567
569
|
const ERROR_TTL_MS = 1e4;
|
|
568
570
|
const MAX_CACHE_ENTRIES = 1e3;
|
|
571
|
+
/** IMF-fixdate, the HTTP date that servers send: `Sun, 06 Nov 1994 08:49:37 GMT`. */
|
|
572
|
+
const HTTP_DATE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
569
573
|
const VARIABLE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
570
574
|
const MODEL_CALL_OPERATIONS = new Set([
|
|
571
575
|
"chat",
|
|
@@ -575,6 +579,8 @@ const MODEL_CALL_OPERATIONS = new Set([
|
|
|
575
579
|
const PROMPT_KEY = createContextKey("telemetry.dev prompt");
|
|
576
580
|
const cache = /* @__PURE__ */ new Map();
|
|
577
581
|
const compiledVariables = /* @__PURE__ */ new WeakMap();
|
|
582
|
+
/** The Retry-After of the response that failed with this error, in milliseconds. */
|
|
583
|
+
const retryAfter = /* @__PURE__ */ new WeakMap();
|
|
578
584
|
async function getPrompt(name, options = {}) {
|
|
579
585
|
const type = options.type ?? "chat";
|
|
580
586
|
checkOptions(name, options, type);
|
|
@@ -635,7 +641,8 @@ async function getPrompt(name, options = {}) {
|
|
|
635
641
|
* it: the spans get `td.prompt.name`, `td.prompt.version`, `td.prompt.label`, and, when input
|
|
636
642
|
* capture is on, `td.prompt.variables`. The variables are those of the last `compile()` of this
|
|
637
643
|
* prompt object before withPrompt starts, or of the last `compile()` inside `fn`, so a compile in
|
|
638
|
-
* another task does not change them once withPrompt runs.
|
|
644
|
+
* another task does not change them once withPrompt runs. Concurrent tasks in `fn` share the
|
|
645
|
+
* variables, so call withPrompt in each task that compiles its own.
|
|
639
646
|
*/
|
|
640
647
|
function withPrompt(prompt, fn) {
|
|
641
648
|
const scope = {
|
|
@@ -675,10 +682,13 @@ async function load(key, ttlMs, fetchResponse, onError) {
|
|
|
675
682
|
let entry = cache.get(key);
|
|
676
683
|
if (!entry) {
|
|
677
684
|
if (cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value);
|
|
678
|
-
entry = {
|
|
685
|
+
entry = {
|
|
686
|
+
checkedAt: 0,
|
|
687
|
+
retryAt: 0
|
|
688
|
+
};
|
|
679
689
|
cache.set(key, entry);
|
|
680
690
|
}
|
|
681
|
-
const fresh = Date.now() < entry.checkedAt + (entry.response ? ttlMs : Math.min(ttlMs, ERROR_TTL_MS));
|
|
691
|
+
const fresh = Date.now() < Math.max(entry.checkedAt + (entry.response ? ttlMs : Math.min(ttlMs, ERROR_TTL_MS)), entry.retryAt);
|
|
682
692
|
if (entry.response) {
|
|
683
693
|
if (!fresh) entry.pending ??= refresh(entry, fetchResponse, onError);
|
|
684
694
|
return entry.response;
|
|
@@ -689,8 +699,11 @@ async function load(key, ttlMs, fetchResponse, onError) {
|
|
|
689
699
|
function refresh(entry, fetchResponse, onError) {
|
|
690
700
|
return fetchResponse().then((response) => {
|
|
691
701
|
entry.response = response;
|
|
702
|
+
entry.retryAt = 0;
|
|
692
703
|
return response;
|
|
693
704
|
}, (error) => {
|
|
705
|
+
const delay = error instanceof PromptError ? retryAfter.get(error) : void 0;
|
|
706
|
+
entry.retryAt = delay === void 0 ? 0 : Date.now() + delay;
|
|
694
707
|
if (!entry.response) {
|
|
695
708
|
entry.error = error;
|
|
696
709
|
throw error;
|
|
@@ -715,7 +728,10 @@ async function fetchPrompt(url, headers, fetchImpl) {
|
|
|
715
728
|
const body = await res.json().catch(() => void 0);
|
|
716
729
|
if (!res.ok) {
|
|
717
730
|
const error = isRecord(body) && isRecord(body.error) ? body.error : {};
|
|
718
|
-
|
|
731
|
+
const failure = new PromptError(typeof error.code === "string" ? error.code : `http_${res.status}`, typeof error.message === "string" ? error.message : `telemetry.dev returned ${res.status}`, res.status);
|
|
732
|
+
const delay = retryAfterMs(res);
|
|
733
|
+
if (delay !== void 0) retryAfter.set(failure, delay);
|
|
734
|
+
throw failure;
|
|
719
735
|
}
|
|
720
736
|
const response = parseResponse(body);
|
|
721
737
|
if (!response) throw new PromptError("invalid_response", "telemetry.dev returned a prompt it cannot read");
|
|
@@ -783,6 +799,17 @@ function keyFingerprint(apiKey) {
|
|
|
783
799
|
const digest = sha256(new TextEncoder().encode(apiKey)).subarray(0, 16);
|
|
784
800
|
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
785
801
|
}
|
|
802
|
+
/** The Retry-After of a 429 or 503 response, as seconds or an IMF-fixdate HTTP date, capped. */
|
|
803
|
+
function retryAfterMs(res) {
|
|
804
|
+
if (res.status !== 429 && res.status !== 503) return void 0;
|
|
805
|
+
const value = res.headers.get("retry-after")?.trim();
|
|
806
|
+
if (!value) return void 0;
|
|
807
|
+
let ms;
|
|
808
|
+
if (/^\d+$/.test(value)) ms = Number(value) * 1e3;
|
|
809
|
+
else if (HTTP_DATE.test(value)) ms = Date.parse(value) - Date.now();
|
|
810
|
+
else return void 0;
|
|
811
|
+
return ms > 0 ? Math.min(ms, MAX_RETRY_AFTER_MS) : void 0;
|
|
812
|
+
}
|
|
786
813
|
function parseResponse(body) {
|
|
787
814
|
if (!isRecord(body)) return void 0;
|
|
788
815
|
const { name, type, label, version, rollout } = body;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pure TypeScript SDK for telemetry.dev: LLM traces, logs, and GenAI metrics on OpenTelemetry, for Node and edge runtimes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"genai",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@opentelemetry/sdk-logs": "^0.223.0",
|
|
43
43
|
"@opentelemetry/sdk-metrics": "^2.12.0",
|
|
44
44
|
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
45
|
-
"@telemetry-dev/otel": "^0.
|
|
45
|
+
"@telemetry-dev/otel": "^0.2.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.1",
|
package/src/config.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
|
|
|
22
22
|
export interface TelemetryOptions {
|
|
23
23
|
/** telemetry.dev ingest key (`td_live_…`). Falls back to `TELEMETRY_DEV_API_KEY`. When absent the SDK is a no-op. */
|
|
24
24
|
apiKey?: string;
|
|
25
|
-
/** Base URL for telemetry export and getPrompt. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://
|
|
25
|
+
/** Base URL for telemetry export and getPrompt. Falls back to `TELEMETRY_DEV_BASE_URL`, then `https://api.telemetry.dev`. */
|
|
26
26
|
baseUrl?: string;
|
|
27
27
|
/** Deployment environment label. Falls back to `TELEMETRY_DEV_ENVIRONMENT`, then `production`. */
|
|
28
28
|
environment?: string;
|
package/src/prompts.ts
CHANGED
|
@@ -118,7 +118,7 @@ interface GetPromptOptionsBase {
|
|
|
118
118
|
cacheTtlSeconds?: number;
|
|
119
119
|
/** API key with the prompts:read scope. Falls back to the initialized client, then `TELEMETRY_DEV_API_KEY`. */
|
|
120
120
|
apiKey?: string;
|
|
121
|
-
/** Falls back to the initialized client, then `TELEMETRY_DEV_BASE_URL`, then `https://
|
|
121
|
+
/** Falls back to the initialized client, then `TELEMETRY_DEV_BASE_URL`, then `https://api.telemetry.dev`. */
|
|
122
122
|
baseUrl?: string;
|
|
123
123
|
/** Falls back to the initialized client's `fetch`, then `globalThis.fetch`. */
|
|
124
124
|
fetch?: typeof fetch;
|
|
@@ -182,6 +182,8 @@ interface CacheEntry {
|
|
|
182
182
|
error?: unknown;
|
|
183
183
|
/** When the last fetch ended. */
|
|
184
184
|
checkedAt: number;
|
|
185
|
+
/** No fetch before this time: the Retry-After of the last 429 or 503 response. */
|
|
186
|
+
retryAt: number;
|
|
185
187
|
pending?: Promise<PromptResponse>;
|
|
186
188
|
}
|
|
187
189
|
|
|
@@ -192,15 +194,21 @@ interface PromptScope {
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
197
|
+
/** The longest Retry-After that getPrompt honors, so that a bad header cannot stop fetches. */
|
|
198
|
+
const MAX_RETRY_AFTER_MS = 3_600_000;
|
|
195
199
|
/** How long a failed fetch with no copy answers calls, so that they do not each fetch again. */
|
|
196
200
|
const ERROR_TTL_MS = 10_000;
|
|
197
201
|
const MAX_CACHE_ENTRIES = 1_000;
|
|
202
|
+
/** IMF-fixdate, the HTTP date that servers send: `Sun, 06 Nov 1994 08:49:37 GMT`. */
|
|
203
|
+
const HTTP_DATE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
198
204
|
const VARIABLE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
199
205
|
const MODEL_CALL_OPERATIONS = new Set<unknown>(["chat", "text_completion", "generate_content"]);
|
|
200
206
|
const PROMPT_KEY = createContextKey("telemetry.dev prompt");
|
|
201
207
|
|
|
202
208
|
const cache = new Map<string, CacheEntry>();
|
|
203
209
|
const compiledVariables = new WeakMap<Prompt, object>();
|
|
210
|
+
/** The Retry-After of the response that failed with this error, in milliseconds. */
|
|
211
|
+
const retryAfter = new WeakMap<PromptError, number>();
|
|
204
212
|
|
|
205
213
|
/**
|
|
206
214
|
* Fetches a prompt version from telemetry.dev and keeps it. When the copy is older than
|
|
@@ -314,7 +322,8 @@ export async function getPrompt(name: string, options: GetPromptOptions = {}): P
|
|
|
314
322
|
* it: the spans get `td.prompt.name`, `td.prompt.version`, `td.prompt.label`, and, when input
|
|
315
323
|
* capture is on, `td.prompt.variables`. The variables are those of the last `compile()` of this
|
|
316
324
|
* prompt object before withPrompt starts, or of the last `compile()` inside `fn`, so a compile in
|
|
317
|
-
* another task does not change them once withPrompt runs.
|
|
325
|
+
* another task does not change them once withPrompt runs. Concurrent tasks in `fn` share the
|
|
326
|
+
* variables, so call withPrompt in each task that compiles its own.
|
|
318
327
|
*/
|
|
319
328
|
export function withPrompt<T>(prompt: Prompt, fn: () => T): T {
|
|
320
329
|
const scope: PromptScope = { prompt, variables: compiledVariables.get(prompt) };
|
|
@@ -397,12 +406,16 @@ async function load(
|
|
|
397
406
|
|
|
398
407
|
if (!entry) {
|
|
399
408
|
if (cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value!);
|
|
400
|
-
entry = { checkedAt: 0 };
|
|
409
|
+
entry = { checkedAt: 0, retryAt: 0 };
|
|
401
410
|
cache.set(key, entry);
|
|
402
411
|
}
|
|
403
412
|
|
|
404
413
|
const fresh =
|
|
405
|
-
Date.now() <
|
|
414
|
+
Date.now() <
|
|
415
|
+
Math.max(
|
|
416
|
+
entry.checkedAt + (entry.response ? ttlMs : Math.min(ttlMs, ERROR_TTL_MS)),
|
|
417
|
+
entry.retryAt,
|
|
418
|
+
);
|
|
406
419
|
|
|
407
420
|
if (entry.response) {
|
|
408
421
|
if (!fresh) entry.pending ??= refresh(entry, fetchResponse, onError);
|
|
@@ -424,10 +437,14 @@ function refresh(
|
|
|
424
437
|
.then(
|
|
425
438
|
(response) => {
|
|
426
439
|
entry.response = response;
|
|
440
|
+
entry.retryAt = 0;
|
|
427
441
|
|
|
428
442
|
return response;
|
|
429
443
|
},
|
|
430
444
|
(error: unknown) => {
|
|
445
|
+
const delay = error instanceof PromptError ? retryAfter.get(error) : undefined;
|
|
446
|
+
entry.retryAt = delay === undefined ? 0 : Date.now() + delay;
|
|
447
|
+
|
|
431
448
|
if (!entry.response) {
|
|
432
449
|
entry.error = error;
|
|
433
450
|
throw error;
|
|
@@ -468,11 +485,16 @@ async function fetchPrompt(
|
|
|
468
485
|
if (!res.ok) {
|
|
469
486
|
const error = isRecord(body) && isRecord(body.error) ? body.error : {};
|
|
470
487
|
|
|
471
|
-
|
|
488
|
+
const failure = new PromptError(
|
|
472
489
|
typeof error.code === "string" ? error.code : `http_${res.status}`,
|
|
473
490
|
typeof error.message === "string" ? error.message : `telemetry.dev returned ${res.status}`,
|
|
474
491
|
res.status,
|
|
475
492
|
);
|
|
493
|
+
|
|
494
|
+
const delay = retryAfterMs(res);
|
|
495
|
+
if (delay !== undefined) retryAfter.set(failure, delay);
|
|
496
|
+
|
|
497
|
+
throw failure;
|
|
476
498
|
}
|
|
477
499
|
|
|
478
500
|
const response = parseResponse(body);
|
|
@@ -594,6 +616,23 @@ function keyFingerprint(apiKey: string): string {
|
|
|
594
616
|
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
595
617
|
}
|
|
596
618
|
|
|
619
|
+
/** The Retry-After of a 429 or 503 response, as seconds or an IMF-fixdate HTTP date, capped. */
|
|
620
|
+
function retryAfterMs(res: Response): number | undefined {
|
|
621
|
+
if (res.status !== 429 && res.status !== 503) return undefined;
|
|
622
|
+
|
|
623
|
+
const value = res.headers.get("retry-after")?.trim();
|
|
624
|
+
|
|
625
|
+
if (!value) return undefined;
|
|
626
|
+
|
|
627
|
+
let ms: number;
|
|
628
|
+
|
|
629
|
+
if (/^\d+$/.test(value)) ms = Number(value) * 1000;
|
|
630
|
+
else if (HTTP_DATE.test(value)) ms = Date.parse(value) - Date.now();
|
|
631
|
+
else return undefined;
|
|
632
|
+
|
|
633
|
+
return ms > 0 ? Math.min(ms, MAX_RETRY_AFTER_MS) : undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
597
636
|
function parseResponse(body: unknown): PromptResponse | undefined {
|
|
598
637
|
if (!isRecord(body)) return undefined;
|
|
599
638
|
const { name, type, label, version, rollout } = body;
|