@dogpile/sdk 0.3.0 → 0.3.1

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/browser/index.js +784 -562
  3. package/dist/browser/index.js.map +1 -1
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/runtime/broadcast.d.ts.map +1 -1
  9. package/dist/runtime/broadcast.js +1 -13
  10. package/dist/runtime/broadcast.js.map +1 -1
  11. package/dist/runtime/coordinator.d.ts.map +1 -1
  12. package/dist/runtime/coordinator.js +1 -13
  13. package/dist/runtime/coordinator.js.map +1 -1
  14. package/dist/runtime/ids.d.ts +19 -0
  15. package/dist/runtime/ids.d.ts.map +1 -0
  16. package/dist/runtime/ids.js +36 -0
  17. package/dist/runtime/ids.js.map +1 -0
  18. package/dist/runtime/logger.d.ts +61 -0
  19. package/dist/runtime/logger.d.ts.map +1 -0
  20. package/dist/runtime/logger.js +114 -0
  21. package/dist/runtime/logger.js.map +1 -0
  22. package/dist/runtime/retry.d.ts +99 -0
  23. package/dist/runtime/retry.d.ts.map +1 -0
  24. package/dist/runtime/retry.js +181 -0
  25. package/dist/runtime/retry.js.map +1 -0
  26. package/dist/runtime/sequential.d.ts.map +1 -1
  27. package/dist/runtime/sequential.js +1 -10
  28. package/dist/runtime/sequential.js.map +1 -1
  29. package/dist/runtime/shared.d.ts.map +1 -1
  30. package/dist/runtime/shared.js +1 -13
  31. package/dist/runtime/shared.js.map +1 -1
  32. package/dist/runtime/tools/built-in.d.ts +99 -0
  33. package/dist/runtime/tools/built-in.d.ts.map +1 -0
  34. package/dist/runtime/tools/built-in.js +577 -0
  35. package/dist/runtime/tools/built-in.js.map +1 -0
  36. package/dist/runtime/tools/vercel-ai.d.ts +67 -0
  37. package/dist/runtime/tools/vercel-ai.d.ts.map +1 -0
  38. package/dist/runtime/tools/vercel-ai.js +148 -0
  39. package/dist/runtime/tools/vercel-ai.js.map +1 -0
  40. package/dist/runtime/tools.d.ts +5 -268
  41. package/dist/runtime/tools.d.ts.map +1 -1
  42. package/dist/runtime/tools.js +7 -770
  43. package/dist/runtime/tools.js.map +1 -1
  44. package/dist/types/benchmark.d.ts +276 -0
  45. package/dist/types/benchmark.d.ts.map +1 -0
  46. package/dist/types/benchmark.js +2 -0
  47. package/dist/types/benchmark.js.map +1 -0
  48. package/dist/types/events.d.ts +495 -0
  49. package/dist/types/events.d.ts.map +1 -0
  50. package/dist/types/events.js +2 -0
  51. package/dist/types/events.js.map +1 -0
  52. package/dist/types/replay.d.ts +169 -0
  53. package/dist/types/replay.d.ts.map +1 -0
  54. package/dist/types/replay.js +2 -0
  55. package/dist/types/replay.js.map +1 -0
  56. package/dist/types.d.ts +6 -935
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +27 -1
  59. package/src/index.ts +4 -0
  60. package/src/runtime/broadcast.ts +1 -16
  61. package/src/runtime/coordinator.ts +1 -16
  62. package/src/runtime/ids.ts +41 -0
  63. package/src/runtime/logger.ts +152 -0
  64. package/src/runtime/retry.ts +270 -0
  65. package/src/runtime/sequential.ts +1 -12
  66. package/src/runtime/shared.ts +1 -16
  67. package/src/runtime/tools/built-in.ts +875 -0
  68. package/src/runtime/tools/vercel-ai.ts +269 -0
  69. package/src/runtime/tools.ts +60 -1255
  70. package/src/types/benchmark.ts +300 -0
  71. package/src/types/events.ts +544 -0
  72. package/src/types/replay.ts +201 -0
  73. package/src/types.ts +104 -994
@@ -0,0 +1,270 @@
1
+ import {
2
+ DogpileError,
3
+ type ConfiguredModelProvider,
4
+ type DogpileErrorCode,
5
+ type ModelOutputChunk,
6
+ type ModelRequest,
7
+ type ModelResponse
8
+ } from "../types.js";
9
+
10
+ /**
11
+ * Default DogpileError codes that `withRetry` retries when no `retryOn`
12
+ * predicate is supplied. These map to the transient provider failures listed
13
+ * in `docs/developer-usage.md`.
14
+ */
15
+ export const DEFAULT_RETRYABLE_DOGPILE_CODES: readonly DogpileErrorCode[] = [
16
+ "provider-rate-limited",
17
+ "provider-timeout",
18
+ "provider-unavailable"
19
+ ];
20
+
21
+ /** Reason passed to `onRetry` and used to drive jitter selection. */
22
+ export type RetryJitterMode = "full" | "none";
23
+
24
+ /**
25
+ * Information about a single retry attempt that has just failed and is about
26
+ * to sleep before the next attempt.
27
+ */
28
+ export interface RetryAttemptInfo {
29
+ /** 1-based index of the attempt that just failed. */
30
+ readonly attempt: number;
31
+ /** Maximum number of attempts the policy will make before giving up. */
32
+ readonly maxAttempts: number;
33
+ /** Sleep duration before the next attempt, in milliseconds. */
34
+ readonly delayMs: number;
35
+ /** The error thrown by the failing attempt. */
36
+ readonly error: unknown;
37
+ /** Provider id of the wrapped provider. */
38
+ readonly providerId: string;
39
+ }
40
+
41
+ /**
42
+ * Caller-supplied retry policy for `withRetry`.
43
+ *
44
+ * The defaults match the conservative, neutrality-preserving recipe in
45
+ * `docs/developer-usage.md`. A caller that wants per-error custom logic
46
+ * (e.g. honor a custom `Retry-After` header from a non-Dogpile error shape)
47
+ * should pass `retryOn` and `delayForError`.
48
+ */
49
+ export interface RetryPolicy {
50
+ /** Maximum total attempts including the first call. Default: 3. */
51
+ readonly maxAttempts?: number;
52
+ /** Initial backoff delay in milliseconds. Default: 250. */
53
+ readonly baseDelayMs?: number;
54
+ /** Cap on the per-attempt backoff delay. Default: 4000. */
55
+ readonly maxDelayMs?: number;
56
+ /** Jitter strategy. `"full"` uses uniform jitter, `"none"` is deterministic. Default: "full". */
57
+ readonly jitter?: RetryJitterMode;
58
+ /**
59
+ * Predicate deciding whether an error is retryable. Receives the raw error
60
+ * thrown by the wrapped provider; returns `true` to retry, `false` to
61
+ * propagate immediately. Default: matches `DEFAULT_RETRYABLE_DOGPILE_CODES`
62
+ * for `DogpileError`, and treats `AbortError` / `DOMException(AbortError)` /
63
+ * `DogpileError({ code: "aborted" })` as non-retryable.
64
+ */
65
+ readonly retryOn?: (error: unknown) => boolean;
66
+ /**
67
+ * Optional delay override for a specific error. Return a non-negative number
68
+ * (ms) to override the computed backoff for the next attempt — used to
69
+ * honor server-supplied `Retry-After` semantics. Returning `undefined`
70
+ * keeps the computed backoff.
71
+ */
72
+ readonly delayForError?: (error: unknown) => number | undefined;
73
+ /**
74
+ * Side-effect callback invoked after each failing attempt that will be
75
+ * retried. Useful for surfacing retries to a logger or metrics system
76
+ * without wrapping the whole event stream.
77
+ */
78
+ readonly onRetry?: (info: RetryAttemptInfo) => void;
79
+ /**
80
+ * Random source for jitter, primarily for deterministic tests. Must return
81
+ * a value in `[0, 1)`. Default: `Math.random`.
82
+ */
83
+ readonly random?: () => number;
84
+ /**
85
+ * Sleep implementation, primarily for deterministic tests. Default: a
86
+ * `setTimeout`-backed promise that respects `AbortSignal`.
87
+ */
88
+ readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
89
+ }
90
+
91
+ const DEFAULTS = {
92
+ maxAttempts: 3,
93
+ baseDelayMs: 250,
94
+ maxDelayMs: 4_000,
95
+ jitter: "full" as RetryJitterMode
96
+ };
97
+
98
+ /**
99
+ * Wrap a `ConfiguredModelProvider` with a retry policy. The wrapper:
100
+ *
101
+ * - Preserves the provider `id` so traces remain stable.
102
+ * - Retries `generate()` calls when the policy says the error is retryable.
103
+ * - Propagates `AbortSignal` cancellation immediately — never retries after
104
+ * the caller cancels.
105
+ * - Honors a `Retry-After`-style hint exposed via `error.detail.retryAfterMs`
106
+ * when present and the policy did not provide its own `delayForError`.
107
+ * - Forwards `stream()` calls through unchanged — streaming retries cannot be
108
+ * safely automated because partial output may have already been observed.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const robustProvider = withRetry(rawProvider, {
113
+ * maxAttempts: 4,
114
+ * baseDelayMs: 500,
115
+ * onRetry: ({ attempt, delayMs, error }) => {
116
+ * logger.warn("provider retry", { attempt, delayMs, error });
117
+ * }
118
+ * });
119
+ * ```
120
+ */
121
+ export function withRetry(
122
+ provider: ConfiguredModelProvider,
123
+ policy: RetryPolicy = {}
124
+ ): ConfiguredModelProvider {
125
+ const settings = {
126
+ maxAttempts: policy.maxAttempts ?? DEFAULTS.maxAttempts,
127
+ baseDelayMs: policy.baseDelayMs ?? DEFAULTS.baseDelayMs,
128
+ maxDelayMs: policy.maxDelayMs ?? DEFAULTS.maxDelayMs,
129
+ jitter: policy.jitter ?? DEFAULTS.jitter,
130
+ retryOn: policy.retryOn ?? defaultRetryOn,
131
+ random: policy.random ?? Math.random,
132
+ sleep: policy.sleep ?? defaultSleep
133
+ };
134
+ if (settings.maxAttempts < 1) {
135
+ throw new DogpileError({
136
+ code: "invalid-configuration",
137
+ message: "withRetry: maxAttempts must be >= 1.",
138
+ detail: { maxAttempts: settings.maxAttempts }
139
+ });
140
+ }
141
+ if (settings.baseDelayMs < 0 || settings.maxDelayMs < 0) {
142
+ throw new DogpileError({
143
+ code: "invalid-configuration",
144
+ message: "withRetry: delay fields must be non-negative.",
145
+ detail: { baseDelayMs: settings.baseDelayMs, maxDelayMs: settings.maxDelayMs }
146
+ });
147
+ }
148
+
149
+ const wrapped: ConfiguredModelProvider = {
150
+ id: provider.id,
151
+ async generate(request: ModelRequest): Promise<ModelResponse> {
152
+ let lastError: unknown;
153
+ for (let attempt = 1; attempt <= settings.maxAttempts; attempt++) {
154
+ if (request.signal?.aborted) {
155
+ throw abortReason(request.signal);
156
+ }
157
+ try {
158
+ return await provider.generate(request);
159
+ } catch (error) {
160
+ lastError = error;
161
+ if (isAbortError(error) || request.signal?.aborted) {
162
+ throw error;
163
+ }
164
+ const isLastAttempt = attempt >= settings.maxAttempts;
165
+ if (isLastAttempt || !settings.retryOn(error)) {
166
+ throw error;
167
+ }
168
+ const delayMs = chooseDelay({ attempt, error, settings, policy });
169
+ policy.onRetry?.({
170
+ attempt,
171
+ maxAttempts: settings.maxAttempts,
172
+ delayMs,
173
+ error,
174
+ providerId: provider.id
175
+ });
176
+ await settings.sleep(delayMs, request.signal);
177
+ }
178
+ }
179
+ // Unreachable in practice — the loop either returns or throws — but TS
180
+ // needs an explicit fallthrough.
181
+ throw lastError ?? new DogpileError({
182
+ code: "unknown",
183
+ message: "withRetry: exhausted attempts without throwing or returning."
184
+ });
185
+ }
186
+ };
187
+
188
+ if (typeof provider.stream === "function") {
189
+ const upstreamStream = provider.stream.bind(provider);
190
+ wrapped.stream = (request: ModelRequest): AsyncIterable<ModelOutputChunk> =>
191
+ upstreamStream(request);
192
+ }
193
+
194
+ return wrapped;
195
+ }
196
+
197
+ function chooseDelay(args: {
198
+ attempt: number;
199
+ error: unknown;
200
+ settings: { baseDelayMs: number; maxDelayMs: number; jitter: RetryJitterMode; random: () => number };
201
+ policy: RetryPolicy;
202
+ }): number {
203
+ const override = args.policy.delayForError?.(args.error) ?? retryAfterFromError(args.error);
204
+ if (override !== undefined && Number.isFinite(override) && override >= 0) {
205
+ return Math.min(args.settings.maxDelayMs, override);
206
+ }
207
+ const exponential = args.settings.baseDelayMs * 2 ** (args.attempt - 1);
208
+ const capped = Math.min(args.settings.maxDelayMs, exponential);
209
+ if (args.settings.jitter === "none") {
210
+ return capped;
211
+ }
212
+ return Math.floor(capped * args.settings.random());
213
+ }
214
+
215
+ function defaultRetryOn(error: unknown): boolean {
216
+ if (isAbortError(error)) return false;
217
+ if (DogpileError.isInstance(error)) {
218
+ if (error.code === "aborted" || error.code === "invalid-configuration") {
219
+ return false;
220
+ }
221
+ return DEFAULT_RETRYABLE_DOGPILE_CODES.includes(error.code);
222
+ }
223
+ // Treat generic network/transient errors as retryable. Most fetch errors
224
+ // surface as `TypeError` with messages like "fetch failed" / "network".
225
+ if (error instanceof TypeError) return true;
226
+ return false;
227
+ }
228
+
229
+ function isAbortError(error: unknown): boolean {
230
+ if (DogpileError.isInstance(error) && error.code === "aborted") return true;
231
+ if (typeof error === "object" && error !== null) {
232
+ const name = (error as { name?: unknown }).name;
233
+ if (name === "AbortError") return true;
234
+ }
235
+ return false;
236
+ }
237
+
238
+ function abortReason(signal: AbortSignal): unknown {
239
+ return signal.reason ?? new DogpileError({ code: "aborted", message: "Request aborted." });
240
+ }
241
+
242
+ function retryAfterFromError(error: unknown): number | undefined {
243
+ if (!DogpileError.isInstance(error)) return undefined;
244
+ const detail = error.detail;
245
+ if (!detail || typeof detail !== "object") return undefined;
246
+ const candidate = (detail as { retryAfterMs?: unknown }).retryAfterMs;
247
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0) {
248
+ return candidate;
249
+ }
250
+ return undefined;
251
+ }
252
+
253
+ function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {
254
+ if (ms <= 0) return Promise.resolve();
255
+ return new Promise((resolve, reject) => {
256
+ if (signal?.aborted) {
257
+ reject(abortReason(signal));
258
+ return;
259
+ }
260
+ const timer = setTimeout(() => {
261
+ signal?.removeEventListener("abort", onAbort);
262
+ resolve();
263
+ }, ms);
264
+ const onAbort = (): void => {
265
+ clearTimeout(timer);
266
+ reject(abortReason(signal!));
267
+ };
268
+ signal?.addEventListener("abort", onAbort, { once: true });
269
+ });
270
+ }
@@ -18,6 +18,7 @@ import type {
18
18
  Tier,
19
19
  TranscriptEntry
20
20
  } from "../types.js";
21
+ import { createRunId, elapsedMs, nowMs } from "./ids.js";
21
22
  import {
22
23
  addCost,
23
24
  createReplayTraceBudget,
@@ -369,15 +370,3 @@ function responseCost(response: ModelResponse): CostSummary {
369
370
  };
370
371
  }
371
372
 
372
- function createRunId(): string {
373
- const random = globalThis.crypto?.randomUUID?.();
374
- return random ?? `run-${Date.now().toString(36)}`;
375
- }
376
-
377
- function nowMs(): number {
378
- return globalThis.performance?.now() ?? Date.now();
379
- }
380
-
381
- function elapsedMs(startedAtMs: number): number {
382
- return Math.max(0, nowMs() - startedAtMs);
383
- }
@@ -18,6 +18,7 @@ import type {
18
18
  Tier,
19
19
  TranscriptEntry
20
20
  } from "../types.js";
21
+ import { createRunId, elapsedMs, nowMs, providerCallIdFor } from "./ids.js";
21
22
  import {
22
23
  addCost,
23
24
  createReplayTraceBudget,
@@ -375,19 +376,3 @@ function responseCost(response: ModelResponse): CostSummary {
375
376
  };
376
377
  }
377
378
 
378
- function createRunId(): string {
379
- const random = globalThis.crypto?.randomUUID?.();
380
- return random ?? `run-${Date.now().toString(36)}`;
381
- }
382
-
383
- function nowMs(): number {
384
- return globalThis.performance?.now() ?? Date.now();
385
- }
386
-
387
- function elapsedMs(startedAtMs: number): number {
388
- return Math.max(0, nowMs() - startedAtMs);
389
- }
390
-
391
- function providerCallIdFor(runId: string, oneBasedIndex: number): string {
392
- return `${runId}:provider-call:${oneBasedIndex}`;
393
- }