@telemetry-dev/sdk 0.1.11 → 0.1.12
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 +11 -8
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +31 -4
- package/package.json +1 -1
- package/src/prompts.ts +43 -4
package/README.md
CHANGED
|
@@ -151,8 +151,10 @@ const pinned = await getPrompt("support/reply", { version: 13 });
|
|
|
151
151
|
`timeout`. A response with a different status and no API error gives `http_<status>`, for
|
|
152
152
|
example `http_302` for a redirect. getPrompt does not send the request to the location of a
|
|
153
153
|
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
|
-
|
|
154
|
+
seconds (or the TTL, if it is shorter) get the same result without a fetch. After a 429 or 503
|
|
155
|
+
response with a `Retry-After` header, getPrompt does not fetch that prompt again before that time
|
|
156
|
+
(one hour at most). This is the one SDK function that can reject: your app cannot run without
|
|
157
|
+
its prompt.
|
|
156
158
|
- getPrompt uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY` and
|
|
157
159
|
`TELEMETRY_DEV_BASE_URL`, or its own `apiKey`, `baseUrl`, and `fetch` options. It reads the same
|
|
158
160
|
host that telemetry goes to, `https://ingest.telemetry.dev` by default. The key needs the read
|
|
@@ -167,12 +169,13 @@ When input capture is on, `td.prompt.variables` holds the JSON of the variables
|
|
|
167
169
|
`compile()` of that prompt object before `withPrompt` starts, or of the last `compile()` inside
|
|
168
170
|
`fn`, after `mask` and truncation. If tasks that run at the same time share one prompt object, call
|
|
169
171
|
`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
|
-
|
|
172
|
+
the cache. If you do not, a task can record the variables of a different task. Tasks that run at the
|
|
173
|
+
same time inside one `withPrompt` share its variables, so call `withPrompt` in each task that
|
|
174
|
+
compiles its own. Model calls are the spans of type `generation` (operations `chat`,
|
|
175
|
+
`text_completion`, and `generate_content`), including the spans of the provider integrations that
|
|
176
|
+
use this SDK: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, and OpenRouter. Spans from the AI SDK
|
|
177
|
+
and TanStack AI integrations, and from other OpenTelemetry tracers, do not get the attributes.
|
|
178
|
+
Messages from `compile()` that you pass to a model call outside `withPrompt` are not recorded.
|
|
176
179
|
|
|
177
180
|
## API
|
|
178
181
|
|
package/dist/index.d.mts
CHANGED
|
@@ -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
package/src/prompts.ts
CHANGED
|
@@ -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;
|