@bastani/atomic 0.9.45-alpha.1 → 0.9.46-alpha.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 (166) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +11 -0
  3. package/dist/builtin/intercom/broker/broker.ts +124 -10
  4. package/dist/builtin/intercom/broker/client.ts +177 -25
  5. package/dist/builtin/intercom/broker/multinode-router.ts +228 -27
  6. package/dist/builtin/intercom/broker/multinode-state-store.ts +10 -0
  7. package/dist/builtin/intercom/broker/recipient-receipts.ts +38 -4
  8. package/dist/builtin/intercom/broker/send-handler.ts +9 -2
  9. package/dist/builtin/intercom/broker/sql-outbox-store.ts +29 -4
  10. package/dist/builtin/intercom/group.ts +25 -0
  11. package/dist/builtin/intercom/index.bundle.mjs +177 -35
  12. package/dist/builtin/intercom/package.json +1 -1
  13. package/dist/builtin/intercom/retry-policy.ts +7 -0
  14. package/dist/builtin/intercom/types.ts +21 -0
  15. package/dist/builtin/mcp/package.json +1 -1
  16. package/dist/builtin/subagents/CHANGELOG.md +10 -0
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/src/extension/index.bundle.mjs +150 -54
  19. package/dist/builtin/web-access/package.json +1 -1
  20. package/dist/builtin/workflows/CHANGELOG.md +45 -0
  21. package/dist/builtin/workflows/builtin/{chunk-6901nsd6.js → chunk-6jpwc09k.js} +1 -1
  22. package/dist/builtin/workflows/builtin/{chunk-vwx023ge.js → chunk-qhm35ntx.js} +1 -1
  23. package/dist/builtin/workflows/builtin/{chunk-jvna28mq.js → chunk-zhyqnf5g.js} +331 -68
  24. package/dist/builtin/workflows/builtin/goal.js +2 -2
  25. package/dist/builtin/workflows/builtin/index.js +3 -3
  26. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  27. package/dist/builtin/workflows/package.json +1 -1
  28. package/dist/builtin/workflows/skills/prompt-engineer/SKILL.md +1 -0
  29. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6.md +2 -2
  30. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_sol.md +2 -2
  31. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_terra.md +77 -0
  32. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  33. package/dist/builtin/workflows/src/durable/journal/internal.d.ts +2 -0
  34. package/dist/builtin/workflows/src/extension/index.bundle.mjs +7076 -3386
  35. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +11075 -7261
  36. package/dist/builtin/workflows/src/index.js +6768 -3605
  37. package/dist/builtin/workflows/src/local-worker-entry.mjs +11075 -7261
  38. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +19 -0
  39. package/dist/core/agent-session-boundaries.d.ts.map +1 -1
  40. package/dist/core/agent-session-boundaries.js +1 -0
  41. package/dist/core/agent-session-boundaries.js.map +1 -1
  42. package/dist/core/agent-session-events.d.ts.map +1 -1
  43. package/dist/core/agent-session-events.js +2 -1
  44. package/dist/core/agent-session-events.js.map +1 -1
  45. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  46. package/dist/core/agent-session-extension-bindings.js +15 -2
  47. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  48. package/dist/core/agent-session-methods.d.ts +3 -2
  49. package/dist/core/agent-session-methods.d.ts.map +1 -1
  50. package/dist/core/agent-session-methods.js.map +1 -1
  51. package/dist/core/agent-session-prompt.d.ts +4 -2
  52. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  53. package/dist/core/agent-session-prompt.js +13 -8
  54. package/dist/core/agent-session-prompt.js.map +1 -1
  55. package/dist/core/agent-session-recovery.d.ts.map +1 -1
  56. package/dist/core/agent-session-recovery.js +92 -1
  57. package/dist/core/agent-session-recovery.js.map +1 -1
  58. package/dist/core/agent-session-transfer.d.ts.map +1 -1
  59. package/dist/core/agent-session-transfer.js +5 -0
  60. package/dist/core/agent-session-transfer.js.map +1 -1
  61. package/dist/core/agent-session.d.ts +2 -0
  62. package/dist/core/agent-session.d.ts.map +1 -1
  63. package/dist/core/agent-session.js +2 -0
  64. package/dist/core/agent-session.js.map +1 -1
  65. package/dist/core/copilot-errors.d.ts +6 -0
  66. package/dist/core/copilot-errors.d.ts.map +1 -0
  67. package/dist/core/copilot-errors.js +17 -0
  68. package/dist/core/copilot-errors.js.map +1 -0
  69. package/dist/core/execution-model-router.d.ts.map +1 -1
  70. package/dist/core/execution-model-router.js +12 -2
  71. package/dist/core/execution-model-router.js.map +1 -1
  72. package/dist/core/extensions/context-types.d.ts +7 -0
  73. package/dist/core/extensions/context-types.d.ts.map +1 -1
  74. package/dist/core/extensions/context-types.js.map +1 -1
  75. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  76. package/dist/core/extensions/runner-context.js +6 -0
  77. package/dist/core/extensions/runner-context.js.map +1 -1
  78. package/dist/core/extensions/workflow-activity-hub.d.ts +4 -0
  79. package/dist/core/extensions/workflow-activity-hub.d.ts.map +1 -1
  80. package/dist/core/extensions/workflow-activity-hub.js +26 -5
  81. package/dist/core/extensions/workflow-activity-hub.js.map +1 -1
  82. package/dist/core/extensions/workflow-events.d.ts +5 -0
  83. package/dist/core/extensions/workflow-events.d.ts.map +1 -1
  84. package/dist/core/extensions/workflow-events.js.map +1 -1
  85. package/dist/core/messages.d.ts +5 -0
  86. package/dist/core/messages.d.ts.map +1 -1
  87. package/dist/core/messages.js +1 -1
  88. package/dist/core/messages.js.map +1 -1
  89. package/dist/core/model-fallback-failures.d.ts +1 -1
  90. package/dist/core/model-fallback-failures.d.ts.map +1 -1
  91. package/dist/core/model-fallback-failures.js +37 -3
  92. package/dist/core/model-fallback-failures.js.map +1 -1
  93. package/dist/core/model-runtime-streaming.js +2 -2
  94. package/dist/core/model-runtime-streaming.js.map +1 -1
  95. package/dist/core/provider-composer.d.ts.map +1 -1
  96. package/dist/core/provider-composer.js +19 -11
  97. package/dist/core/provider-composer.js.map +1 -1
  98. package/dist/core/provider-rate-limits.d.ts +55 -6
  99. package/dist/core/provider-rate-limits.d.ts.map +1 -1
  100. package/dist/core/provider-rate-limits.js +182 -29
  101. package/dist/core/provider-rate-limits.js.map +1 -1
  102. package/dist/core/provider-request-admission.d.ts.map +1 -1
  103. package/dist/core/provider-request-admission.js +42 -18
  104. package/dist/core/provider-request-admission.js.map +1 -1
  105. package/dist/core/storage/node-sqlite.d.ts.map +1 -1
  106. package/dist/core/storage/node-sqlite.js +176 -19
  107. package/dist/core/storage/node-sqlite.js.map +1 -1
  108. package/dist/core/storage/provider.d.ts +4 -2
  109. package/dist/core/storage/provider.d.ts.map +1 -1
  110. package/dist/core/storage/provider.js +94 -5
  111. package/dist/core/storage/provider.js.map +1 -1
  112. package/dist/core/structured-output/index.d.ts.map +1 -1
  113. package/dist/core/structured-output/index.js +15 -8
  114. package/dist/core/structured-output/index.js.map +1 -1
  115. package/dist/core/structured-output/provider-failure.d.ts +17 -0
  116. package/dist/core/structured-output/provider-failure.d.ts.map +1 -0
  117. package/dist/core/structured-output/provider-failure.js +28 -0
  118. package/dist/core/structured-output/provider-failure.js.map +1 -0
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js +13 -7
  121. package/dist/core/tools/bash.js.map +1 -1
  122. package/dist/core/tools/shell-task-render.d.ts +1 -0
  123. package/dist/core/tools/shell-task-render.d.ts.map +1 -1
  124. package/dist/core/tools/shell-task-render.js +7 -2
  125. package/dist/core/tools/shell-task-render.js.map +1 -1
  126. package/dist/extensions/tool-search/tool.d.ts +5 -0
  127. package/dist/extensions/tool-search/tool.d.ts.map +1 -1
  128. package/dist/extensions/tool-search/tool.js +38 -5
  129. package/dist/extensions/tool-search/tool.js.map +1 -1
  130. package/dist/index.d.ts +1 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +1 -1
  133. package/dist/index.js.map +1 -1
  134. package/dist/modes/interactive/components/chat-session-host-actions.js +3 -2
  135. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  136. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/login-dialog.js +42 -14
  138. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  139. package/dist/modes/interactive/interactive-extension-context.js +2 -1
  140. package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
  141. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  142. package/dist/modes/interactive/interactive-startup.js +5 -6
  143. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  144. package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
  145. package/dist/modes/rpc/rpc-client-process.js +8 -1
  146. package/dist/modes/rpc/rpc-client-process.js.map +1 -1
  147. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  148. package/dist/modes/rpc/rpc-client.js +7 -1
  149. package/dist/modes/rpc/rpc-client.js.map +1 -1
  150. package/dist/utils/ansi.d.ts +2 -0
  151. package/dist/utils/ansi.d.ts.map +1 -1
  152. package/dist/utils/ansi.js +5 -0
  153. package/dist/utils/ansi.js.map +1 -1
  154. package/docs/codemode.md +1 -1
  155. package/docs/intercom/reference.md +3 -3
  156. package/docs/models/evals.md +31 -12
  157. package/docs/models/reference.md +23 -51
  158. package/docs/models.md +3 -4
  159. package/docs/providers.md +5 -15
  160. package/docs/settings.md +1 -1
  161. package/docs/storage.md +2 -2
  162. package/docs/tools.md +1 -1
  163. package/docs/workflows/api-reference.md +20 -1
  164. package/docs/workflows/operations.md +17 -4
  165. package/npm-shrinkwrap.json +64 -44
  166. package/package.json +4 -4
@@ -7,16 +7,65 @@ export interface ProviderRateLimitSnapshot {
7
7
  readonly rateLimitCount: number;
8
8
  readonly cooldownUntil: number;
9
9
  }
10
+ /** The limit a failure was recorded against: the whole provider or one of its models. */
11
+ export interface ProviderLimitScope {
12
+ readonly key: string;
13
+ }
14
+ /**
15
+ * A limit one process recorded, in a form other processes on the same host can apply: the provider, the
16
+ * model when the limit covers only that model, and the wall-clock time the limit ends.
17
+ */
18
+ export interface ProviderRateLimitObservation {
19
+ readonly provider: string;
20
+ readonly model?: string;
21
+ /** `cooldown` holds requests back; `unavailable` (an exhausted quota) makes the fallback walk skip the account. */
22
+ readonly kind: "cooldown" | "unavailable";
23
+ readonly untilEpochMs: number;
24
+ }
10
25
  export declare function configureProviderRequestBudget(provider: string, budget: ProviderRequestBudget | undefined): void;
11
26
  export declare function providerRateLimitSnapshot(provider: string): ProviderRateLimitSnapshot | undefined;
12
27
  export declare function providerIdForModel(model: string | object | undefined): string | undefined;
13
- export declare function providerCooldownMs(provider: string | undefined): number;
14
- export declare function providerAdmissionDelayMs(provider: string): number;
15
- export declare function tryAdmitProviderRequest(provider: string): number;
28
+ /** Model ID within its provider, from a `provider/model` reference or a model object. */
29
+ export declare function modelIdForModel(model: string | object | undefined): string | undefined;
30
+ /**
31
+ * Time until requests to the provider (and, when given, this model) are admitted again. Pass `now` to compare
32
+ * several limits against one clock reading.
33
+ */
34
+ export declare function providerCooldownMs(provider: string | undefined, model?: string, now?: number): number;
35
+ /** Time until an exhausted quota on the provider (and, when given, this model) resets. */
36
+ export declare function providerUnavailableMs(provider: string | undefined, model?: string, now?: number): number;
37
+ export declare function providerAdmissionDelayMs(provider: string, model?: string): number;
38
+ export declare function tryAdmitProviderRequest(provider: string, model?: string): number;
16
39
  export declare function isProviderRateLimit(error: unknown): boolean;
17
- export declare function markProviderRateLimitObserved(error: object): void;
18
- export declare function recordProviderRateLimit(provider: string | undefined, error: unknown): number | undefined;
19
- export declare function recordProviderSuccess(provider: string): void;
40
+ /** A rate limit or an exhausted quota: either makes the account unusable until the server's reset. */
41
+ export declare function isProviderLimitFailure(error: unknown): boolean;
42
+ export declare function markProviderRateLimitObserved(error: object, scope: ProviderLimitScope): void;
43
+ /**
44
+ * Record a rate limit or exhausted quota and return how long the account stays limited. The limit covers
45
+ * only `model` when the failure names that model, and the whole provider otherwise.
46
+ */
47
+ export declare function recordProviderRateLimit(provider: string | undefined, error: unknown, model?: string): number | undefined;
48
+ /** The limit a failure belongs to: its model when the failure names that model, else the whole provider. */
49
+ export declare function providerLimitScope(provider: string, error: unknown, model?: string): ProviderLimitScope;
50
+ /**
51
+ * Merge a limit another process observed. It only ever extends the limit of the same provider or model
52
+ * scope, ignores limits that already ended, and counts no failure here. Returns whether the limit grew.
53
+ * Observers are not told: the observation came from elsewhere, so sending it on would echo it back.
54
+ */
55
+ export declare function applyProviderRateLimitObservation(observation: ProviderRateLimitObservation): boolean;
56
+ /** Every limit of this process that has not ended yet, as observations another process can apply. */
57
+ export declare function providerRateLimitObservations(): ProviderRateLimitObservation[];
58
+ /**
59
+ * Be told each time this process records or extends a limit, but not when it applies another process's
60
+ * observation. Only rate-limited and quota-exhausted responses notify, never ordinary requests.
61
+ */
62
+ export declare function observeProviderRateLimits(observer: (observation: ProviderRateLimitObservation) => void): () => void;
63
+ /**
64
+ * Apply the server retry hint of a failure that was already recorded against `scope`, without counting it
65
+ * again. A limit only ever grows, so a guess made before the hint was known cannot shorten it.
66
+ */
67
+ export declare function recordProviderRetryHint(provider: string | undefined, error: unknown, scope: ProviderLimitScope): void;
68
+ export declare function recordProviderSuccess(provider: string, model?: string): void;
20
69
  export declare function providerRetryHint(error: unknown): {
21
70
  retryAfterMs?: number;
22
71
  };
@@ -1 +1 @@
1
- {"version":3,"file":"provider-rate-limits.d.ts","sourceRoot":"","sources":["../../src/core/provider-rate-limits.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAgCD,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,CAchH;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAGjG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEvE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYjE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAYD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAYxG;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG5D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAM3E"}
1
+ {"version":3,"file":"provider-rate-limits.d.ts","sourceRoot":"","sources":["../../src/core/provider-rate-limits.ts"],"names":[],"mappings":"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"}
@@ -1,20 +1,30 @@
1
1
  import { getProviderRetryAfterMs } from "@bastani/pi-ai/utils/retry-hints";
2
- import { normalizeModelFailureSignal } from "./model-fallback-failures.js";
2
+ import { errorMessage, normalizeModelFailureSignal } from "./model-fallback-failures.js";
3
3
  const stateKey = Symbol.for("atomic-coding-agent/provider-rate-limits@1");
4
4
  const globals = globalThis;
5
5
  const state = globals[stateKey] ?? {
6
6
  providers: new Map(),
7
- observedFailures: new WeakSet(),
7
+ observedFailures: new WeakMap(),
8
8
  };
9
9
  globals[stateKey] = state;
10
- function providerLimit(provider) {
11
- let limit = state.providers.get(provider);
10
+ function providerLimit(key) {
11
+ let limit = state.providers.get(key);
12
12
  if (!limit) {
13
- limit = { cooldownUntil: 0, rateLimitCount: 0, consecutiveLimits: 0, requests: [] };
14
- state.providers.set(provider, limit);
13
+ limit = { cooldownUntil: 0, unavailableUntil: 0, rateLimitCount: 0, consecutiveLimits: 0, requests: [] };
14
+ state.providers.set(key, limit);
15
15
  }
16
16
  return limit;
17
17
  }
18
+ /** Key for limits scoped to one model; provider IDs never contain "/", so it cannot collide with a provider key. */
19
+ function modelLimitKey(provider, model) {
20
+ return `${provider}/${model}`;
21
+ }
22
+ function scopedLimits(provider, model) {
23
+ const limits = [state.providers.get(provider)];
24
+ if (model !== undefined)
25
+ limits.push(state.providers.get(modelLimitKey(provider, model)));
26
+ return limits.filter((limit) => limit !== undefined);
27
+ }
18
28
  export function configureProviderRequestBudget(provider, budget) {
19
29
  if (!provider.trim())
20
30
  throw new Error("Provider request budget requires a provider ID");
@@ -41,16 +51,36 @@ export function providerIdForModel(model) {
41
51
  return model.provider;
42
52
  return undefined;
43
53
  }
44
- export function providerCooldownMs(provider) {
45
- return provider === undefined ? 0 : Math.max(0, (state.providers.get(provider)?.cooldownUntil ?? 0) - Date.now());
54
+ /** Model ID within its provider, from a `provider/model` reference or a model object. */
55
+ export function modelIdForModel(model) {
56
+ if (typeof model === "string")
57
+ return model.includes("/") ? model.slice(model.indexOf("/") + 1) : undefined;
58
+ if (model && "id" in model && typeof model.id === "string")
59
+ return model.id;
60
+ return undefined;
46
61
  }
47
- export function providerAdmissionDelayMs(provider) {
48
- const limit = state.providers.get(provider);
49
- if (!limit)
62
+ /**
63
+ * Time until requests to the provider (and, when given, this model) are admitted again. Pass `now` to compare
64
+ * several limits against one clock reading.
65
+ */
66
+ export function providerCooldownMs(provider, model, now = Date.now()) {
67
+ if (provider === undefined)
68
+ return 0;
69
+ const until = Math.max(0, ...scopedLimits(provider, model).map((limit) => limit.cooldownUntil));
70
+ return Math.max(0, until - now);
71
+ }
72
+ /** Time until an exhausted quota on the provider (and, when given, this model) resets. */
73
+ export function providerUnavailableMs(provider, model, now = Date.now()) {
74
+ if (provider === undefined)
50
75
  return 0;
76
+ const until = Math.max(0, ...scopedLimits(provider, model).map((limit) => limit.unavailableUntil));
77
+ return Math.max(0, until - now);
78
+ }
79
+ export function providerAdmissionDelayMs(provider, model) {
80
+ const limit = state.providers.get(provider);
51
81
  const now = Date.now();
52
- const cooldown = Math.max(0, limit.cooldownUntil - now);
53
- if (!limit.budget)
82
+ const cooldown = providerCooldownMs(provider, model);
83
+ if (!limit?.budget)
54
84
  return cooldown;
55
85
  limit.requests = limit.requests.filter((timestamp) => timestamp > now - limit.budget.intervalMs);
56
86
  const budgetDelay = limit.requests.length < limit.budget.maxRequests
@@ -58,8 +88,8 @@ export function providerAdmissionDelayMs(provider) {
58
88
  : limit.requests[limit.requests.length - limit.budget.maxRequests] + limit.budget.intervalMs - now;
59
89
  return Math.max(cooldown, budgetDelay);
60
90
  }
61
- export function tryAdmitProviderRequest(provider) {
62
- const delay = providerAdmissionDelayMs(provider);
91
+ export function tryAdmitProviderRequest(provider, model) {
92
+ const delay = providerAdmissionDelayMs(provider, model);
63
93
  if (delay === 0) {
64
94
  const limit = state.providers.get(provider);
65
95
  if (limit?.budget)
@@ -70,6 +100,26 @@ export function tryAdmitProviderRequest(provider) {
70
100
  export function isProviderRateLimit(error) {
71
101
  return normalizeModelFailureSignal(error)?.kind === "rate_limit";
72
102
  }
103
+ /** A rate limit or an exhausted quota: either makes the account unusable until the server's reset. */
104
+ export function isProviderLimitFailure(error) {
105
+ const kind = normalizeModelFailureSignal(error)?.kind;
106
+ return kind === "rate_limit" || kind === "quota_exhausted";
107
+ }
108
+ function escapeRegExp(value) {
109
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ }
111
+ /**
112
+ * Whether the failure names the model that hit the limit. Providers that limit per model say so in the
113
+ * error ("rate limit reached for <model>"); without that, the limit may cover the whole account.
114
+ */
115
+ function failureNamesModel(error, model) {
116
+ if (!model.trim())
117
+ return false;
118
+ const text = failureObjects(error)
119
+ .map((value) => errorMessage(value))
120
+ .join("\n");
121
+ return new RegExp(`(?<![\\w.-])${escapeRegExp(model)}(?![\\w-]|\\.\\w)`, "i").test(text);
122
+ }
73
123
  function failureObjects(error) {
74
124
  const seen = new Set();
75
125
  let current = error;
@@ -79,27 +129,130 @@ function failureObjects(error) {
79
129
  }
80
130
  return [...seen];
81
131
  }
82
- export function markProviderRateLimitObserved(error) {
83
- state.observedFailures.add(error);
132
+ export function markProviderRateLimitObserved(error, scope) {
133
+ state.observedFailures.set(error, scope);
84
134
  }
85
- export function recordProviderRateLimit(provider, error) {
86
- if (provider === undefined || !isProviderRateLimit(error))
135
+ /**
136
+ * Record a rate limit or exhausted quota and return how long the account stays limited. The limit covers
137
+ * only `model` when the failure names that model, and the whole provider otherwise.
138
+ */
139
+ export function recordProviderRateLimit(provider, error, model) {
140
+ if (provider === undefined)
141
+ return undefined;
142
+ const kind = normalizeModelFailureSignal(error)?.kind;
143
+ if (kind !== "rate_limit" && kind !== "quota_exhausted")
87
144
  return undefined;
145
+ const remaining = () => kind === "quota_exhausted" ? providerUnavailableMs(provider, model) : providerCooldownMs(provider, model);
88
146
  for (const value of failureObjects(error)) {
89
- if (state.observedFailures.delete(value))
90
- return providerCooldownMs(provider);
147
+ const scope = state.observedFailures.get(value);
148
+ if (scope !== undefined) {
149
+ state.observedFailures.delete(value);
150
+ // Already counted; a server hint on this later view of the failure can still extend the limit.
151
+ recordProviderRetryHint(provider, error, scope);
152
+ return remaining();
153
+ }
91
154
  }
92
- const limit = providerLimit(provider);
93
- limit.rateLimitCount += 1;
155
+ const providerEntry = providerLimit(provider);
156
+ const key = providerLimitScope(provider, error, model).key;
157
+ const limit = providerLimit(key);
158
+ providerEntry.rateLimitCount += 1;
94
159
  limit.consecutiveLimits += 1;
95
160
  const backoff = Math.min(60_000, 1000 * 2 ** Math.min(limit.consecutiveLimits - 1, 6));
96
- const delay = providerRetryHint(error).retryAfterMs ?? backoff * (0.5 + Math.random() * 0.5);
97
- limit.cooldownUntil = Math.max(limit.cooldownUntil, Math.ceil(Date.now() + Math.max(1, delay)));
98
- return providerCooldownMs(provider);
161
+ extendLimit(key, kind, providerRetryHint(error).retryAfterMs ?? backoff * (0.5 + Math.random() * 0.5));
162
+ return remaining();
99
163
  }
100
- export function recordProviderSuccess(provider) {
101
- const limit = state.providers.get(provider);
102
- if (limit)
164
+ /** The limit a failure belongs to: its model when the failure names that model, else the whole provider. */
165
+ export function providerLimitScope(provider, error, model) {
166
+ return { key: model !== undefined && failureNamesModel(error, model) ? modelLimitKey(provider, model) : provider };
167
+ }
168
+ function extendLimit(key, kind, delay) {
169
+ const until = Math.ceil(Date.now() + Math.max(1, delay));
170
+ const observation = raiseLimit(key, kind === "quota_exhausted" ? "unavailable" : "cooldown", until);
171
+ if (observation === undefined || state.observers === undefined)
172
+ return;
173
+ for (const observer of state.observers) {
174
+ // An observer must never change how the request that hit the limit is handled, nor starve the others.
175
+ try {
176
+ observer(observation);
177
+ }
178
+ catch (error) {
179
+ if (!observerFailureReported) {
180
+ observerFailureReported = true;
181
+ console.debug("Provider rate-limit observer failed", error);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ /** Observer failures are reported once per process. */
187
+ let observerFailureReported = false;
188
+ /** Raise one limit to `until`; returns the limit as an observation when it grew, and nothing otherwise. */
189
+ function raiseLimit(key, kind, until) {
190
+ const limit = providerLimit(key);
191
+ const field = kind === "unavailable" ? "unavailableUntil" : "cooldownUntil";
192
+ if (until <= limit[field])
193
+ return undefined;
194
+ limit[field] = until;
195
+ const slash = key.indexOf("/");
196
+ return slash < 0
197
+ ? { provider: key, kind, untilEpochMs: until }
198
+ : { provider: key.slice(0, slash), model: key.slice(slash + 1), kind, untilEpochMs: until };
199
+ }
200
+ /**
201
+ * Merge a limit another process observed. It only ever extends the limit of the same provider or model
202
+ * scope, ignores limits that already ended, and counts no failure here. Returns whether the limit grew.
203
+ * Observers are not told: the observation came from elsewhere, so sending it on would echo it back.
204
+ */
205
+ export function applyProviderRateLimitObservation(observation) {
206
+ const { provider, model, kind, untilEpochMs } = observation;
207
+ if (!provider || provider.includes("/") || (kind !== "cooldown" && kind !== "unavailable"))
208
+ return false;
209
+ if (!Number.isFinite(untilEpochMs) || untilEpochMs <= Date.now())
210
+ return false;
211
+ const key = model === undefined ? provider : modelLimitKey(provider, model);
212
+ return raiseLimit(key, kind, Math.ceil(untilEpochMs)) !== undefined;
213
+ }
214
+ /** Every limit of this process that has not ended yet, as observations another process can apply. */
215
+ export function providerRateLimitObservations() {
216
+ const now = Date.now();
217
+ const observations = [];
218
+ for (const [key, limit] of state.providers) {
219
+ const slash = key.indexOf("/");
220
+ const scope = slash < 0 ? { provider: key } : { provider: key.slice(0, slash), model: key.slice(slash + 1) };
221
+ if (limit.cooldownUntil > now)
222
+ observations.push({ ...scope, kind: "cooldown", untilEpochMs: limit.cooldownUntil });
223
+ if (limit.unavailableUntil > now)
224
+ observations.push({ ...scope, kind: "unavailable", untilEpochMs: limit.unavailableUntil });
225
+ }
226
+ return observations;
227
+ }
228
+ /**
229
+ * Be told each time this process records or extends a limit, but not when it applies another process's
230
+ * observation. Only rate-limited and quota-exhausted responses notify, never ordinary requests.
231
+ */
232
+ export function observeProviderRateLimits(observer) {
233
+ state.observers ??= new Set();
234
+ const observers = state.observers;
235
+ observers.add(observer);
236
+ return () => {
237
+ observers.delete(observer);
238
+ };
239
+ }
240
+ /**
241
+ * Apply the server retry hint of a failure that was already recorded against `scope`, without counting it
242
+ * again. A limit only ever grows, so a guess made before the hint was known cannot shorten it.
243
+ */
244
+ export function recordProviderRetryHint(provider, error, scope) {
245
+ if (provider === undefined)
246
+ return;
247
+ const kind = normalizeModelFailureSignal(error)?.kind;
248
+ if (kind !== "rate_limit" && kind !== "quota_exhausted")
249
+ return;
250
+ const retryAfterMs = providerRetryHint(error).retryAfterMs;
251
+ if (retryAfterMs !== undefined)
252
+ extendLimit(scope.key, kind, retryAfterMs);
253
+ }
254
+ export function recordProviderSuccess(provider, model) {
255
+ for (const limit of scopedLimits(provider, model))
103
256
  limit.consecutiveLimits = 0;
104
257
  }
105
258
  export function providerRetryHint(error) {
@@ -1 +1 @@
1
- {"version":3,"file":"provider-rate-limits.js","sourceRoot":"","sources":["../../src/core/provider-rate-limits.ts"],"names":[],"mappings":"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","sourcesContent":["import { getProviderRetryAfterMs } from \"@bastani/pi-ai/utils/retry-hints\";\nimport { normalizeModelFailureSignal } from \"./model-fallback-failures.ts\";\n\nexport interface ProviderRequestBudget {\n\treadonly maxRequests: number;\n\treadonly intervalMs: number;\n}\n\nexport interface ProviderRateLimitSnapshot {\n\treadonly provider: string;\n\treadonly rateLimitCount: number;\n\treadonly cooldownUntil: number;\n}\n\ninterface ProviderLimit {\n\tcooldownUntil: number;\n\trateLimitCount: number;\n\tconsecutiveLimits: number;\n\tbudget?: ProviderRequestBudget;\n\trequests: number[];\n}\n\ninterface ProviderLimitState {\n\treadonly providers: Map<string, ProviderLimit>;\n\treadonly observedFailures: WeakSet<object>;\n}\n\nconst stateKey = Symbol.for(\"atomic-coding-agent/provider-rate-limits@1\");\nconst globals = globalThis as typeof globalThis & { [stateKey]?: ProviderLimitState };\nconst state = globals[stateKey] ?? {\n\tproviders: new Map<string, ProviderLimit>(),\n\tobservedFailures: new WeakSet<object>(),\n};\nglobals[stateKey] = state;\n\nfunction providerLimit(provider: string): ProviderLimit {\n\tlet limit = state.providers.get(provider);\n\tif (!limit) {\n\t\tlimit = { cooldownUntil: 0, rateLimitCount: 0, consecutiveLimits: 0, requests: [] };\n\t\tstate.providers.set(provider, limit);\n\t}\n\treturn limit;\n}\n\nexport function configureProviderRequestBudget(provider: string, budget: ProviderRequestBudget | undefined): void {\n\tif (!provider.trim()) throw new Error(\"Provider request budget requires a provider ID\");\n\tif (\n\t\tbudget &&\n\t\t(!Number.isSafeInteger(budget.maxRequests) ||\n\t\t\tbudget.maxRequests <= 0 ||\n\t\t\t!Number.isSafeInteger(budget.intervalMs) ||\n\t\t\tbudget.intervalMs <= 0)\n\t) {\n\t\tthrow new Error(\"Provider request budget requires positive integer maxRequests and intervalMs\");\n\t}\n\tconst limit = providerLimit(provider);\n\tlimit.budget = budget ? { ...budget } : undefined;\n\tif (!budget) limit.requests = [];\n}\n\nexport function providerRateLimitSnapshot(provider: string): ProviderRateLimitSnapshot | undefined {\n\tconst limit = state.providers.get(provider);\n\treturn limit ? { provider, rateLimitCount: limit.rateLimitCount, cooldownUntil: limit.cooldownUntil } : undefined;\n}\n\nexport function providerIdForModel(model: string | object | undefined): string | undefined {\n\tif (typeof model === \"string\") return model.includes(\"/\") ? model.slice(0, model.indexOf(\"/\")) : undefined;\n\tif (model && \"provider\" in model && typeof model.provider === \"string\") return model.provider;\n\treturn undefined;\n}\n\nexport function providerCooldownMs(provider: string | undefined): number {\n\treturn provider === undefined ? 0 : Math.max(0, (state.providers.get(provider)?.cooldownUntil ?? 0) - Date.now());\n}\n\nexport function providerAdmissionDelayMs(provider: string): number {\n\tconst limit = state.providers.get(provider);\n\tif (!limit) return 0;\n\tconst now = Date.now();\n\tconst cooldown = Math.max(0, limit.cooldownUntil - now);\n\tif (!limit.budget) return cooldown;\n\tlimit.requests = limit.requests.filter((timestamp) => timestamp > now - limit.budget!.intervalMs);\n\tconst budgetDelay =\n\t\tlimit.requests.length < limit.budget.maxRequests\n\t\t\t? 0\n\t\t\t: limit.requests[limit.requests.length - limit.budget.maxRequests]! + limit.budget.intervalMs - now;\n\treturn Math.max(cooldown, budgetDelay);\n}\n\nexport function tryAdmitProviderRequest(provider: string): number {\n\tconst delay = providerAdmissionDelayMs(provider);\n\tif (delay === 0) {\n\t\tconst limit = state.providers.get(provider);\n\t\tif (limit?.budget) limit.requests.push(Date.now());\n\t}\n\treturn delay;\n}\n\nexport function isProviderRateLimit(error: unknown): boolean {\n\treturn normalizeModelFailureSignal(error)?.kind === \"rate_limit\";\n}\n\nfunction failureObjects(error: unknown): object[] {\n\tconst seen = new Set<object>();\n\tlet current = error;\n\twhile (current !== null && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tcurrent = \"cause\" in current ? current.cause : undefined;\n\t}\n\treturn [...seen];\n}\n\nexport function markProviderRateLimitObserved(error: object): void {\n\tstate.observedFailures.add(error);\n}\n\nexport function recordProviderRateLimit(provider: string | undefined, error: unknown): number | undefined {\n\tif (provider === undefined || !isProviderRateLimit(error)) return undefined;\n\tfor (const value of failureObjects(error)) {\n\t\tif (state.observedFailures.delete(value)) return providerCooldownMs(provider);\n\t}\n\tconst limit = providerLimit(provider);\n\tlimit.rateLimitCount += 1;\n\tlimit.consecutiveLimits += 1;\n\tconst backoff = Math.min(60_000, 1000 * 2 ** Math.min(limit.consecutiveLimits - 1, 6));\n\tconst delay = providerRetryHint(error).retryAfterMs ?? backoff * (0.5 + Math.random() * 0.5);\n\tlimit.cooldownUntil = Math.max(limit.cooldownUntil, Math.ceil(Date.now() + Math.max(1, delay)));\n\treturn providerCooldownMs(provider);\n}\n\nexport function recordProviderSuccess(provider: string): void {\n\tconst limit = state.providers.get(provider);\n\tif (limit) limit.consecutiveLimits = 0;\n}\n\nexport function providerRetryHint(error: unknown): { retryAfterMs?: number } {\n\tfor (const value of failureObjects(error)) {\n\t\tconst retryAfterMs = getProviderRetryAfterMs(value);\n\t\tif (retryAfterMs !== undefined) return { retryAfterMs };\n\t}\n\treturn {};\n}\n"]}
1
+ {"version":3,"file":"provider-rate-limits.js","sourceRoot":"","sources":["../../src/core/provider-rate-limits.ts"],"names":[],"mappings":"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","sourcesContent":["import { getProviderRetryAfterMs } from \"@bastani/pi-ai/utils/retry-hints\";\nimport { errorMessage, normalizeModelFailureSignal } from \"./model-fallback-failures.ts\";\n\nexport interface ProviderRequestBudget {\n\treadonly maxRequests: number;\n\treadonly intervalMs: number;\n}\n\nexport interface ProviderRateLimitSnapshot {\n\treadonly provider: string;\n\treadonly rateLimitCount: number;\n\treadonly cooldownUntil: number;\n}\n\ninterface ProviderLimit {\n\tcooldownUntil: number;\n\t/** Quota exhaustion: the walk skips this account until then, but requests are not held back. */\n\tunavailableUntil: number;\n\trateLimitCount: number;\n\tconsecutiveLimits: number;\n\tbudget?: ProviderRequestBudget;\n\trequests: number[];\n}\n\n/** The limit a failure was recorded against: the whole provider or one of its models. */\nexport interface ProviderLimitScope {\n\treadonly key: string;\n}\n\ninterface ProviderLimitState {\n\treadonly providers: Map<string, ProviderLimit>;\n\t/** Failures already recorded, with the limit (provider or model) they were recorded against. */\n\treadonly observedFailures: WeakMap<object, ProviderLimitScope>;\n\t/** Listeners told when this process records or extends a limit; absent on state created by an older copy. */\n\tobservers?: Set<(observation: ProviderRateLimitObservation) => void>;\n}\n\n/**\n * A limit one process recorded, in a form other processes on the same host can apply: the provider, the\n * model when the limit covers only that model, and the wall-clock time the limit ends.\n */\nexport interface ProviderRateLimitObservation {\n\treadonly provider: string;\n\treadonly model?: string;\n\t/** `cooldown` holds requests back; `unavailable` (an exhausted quota) makes the fallback walk skip the account. */\n\treadonly kind: \"cooldown\" | \"unavailable\";\n\treadonly untilEpochMs: number;\n}\n\nconst stateKey = Symbol.for(\"atomic-coding-agent/provider-rate-limits@1\");\nconst globals = globalThis as typeof globalThis & { [stateKey]?: ProviderLimitState };\nconst state: ProviderLimitState = globals[stateKey] ?? {\n\tproviders: new Map<string, ProviderLimit>(),\n\tobservedFailures: new WeakMap<object, ProviderLimitScope>(),\n};\nglobals[stateKey] = state;\n\nfunction providerLimit(key: string): ProviderLimit {\n\tlet limit = state.providers.get(key);\n\tif (!limit) {\n\t\tlimit = { cooldownUntil: 0, unavailableUntil: 0, rateLimitCount: 0, consecutiveLimits: 0, requests: [] };\n\t\tstate.providers.set(key, limit);\n\t}\n\treturn limit;\n}\n\n/** Key for limits scoped to one model; provider IDs never contain \"/\", so it cannot collide with a provider key. */\nfunction modelLimitKey(provider: string, model: string): string {\n\treturn `${provider}/${model}`;\n}\n\nfunction scopedLimits(provider: string, model: string | undefined): ProviderLimit[] {\n\tconst limits = [state.providers.get(provider)];\n\tif (model !== undefined) limits.push(state.providers.get(modelLimitKey(provider, model)));\n\treturn limits.filter((limit): limit is ProviderLimit => limit !== undefined);\n}\n\nexport function configureProviderRequestBudget(provider: string, budget: ProviderRequestBudget | undefined): void {\n\tif (!provider.trim()) throw new Error(\"Provider request budget requires a provider ID\");\n\tif (\n\t\tbudget &&\n\t\t(!Number.isSafeInteger(budget.maxRequests) ||\n\t\t\tbudget.maxRequests <= 0 ||\n\t\t\t!Number.isSafeInteger(budget.intervalMs) ||\n\t\t\tbudget.intervalMs <= 0)\n\t) {\n\t\tthrow new Error(\"Provider request budget requires positive integer maxRequests and intervalMs\");\n\t}\n\tconst limit = providerLimit(provider);\n\tlimit.budget = budget ? { ...budget } : undefined;\n\tif (!budget) limit.requests = [];\n}\n\nexport function providerRateLimitSnapshot(provider: string): ProviderRateLimitSnapshot | undefined {\n\tconst limit = state.providers.get(provider);\n\treturn limit ? { provider, rateLimitCount: limit.rateLimitCount, cooldownUntil: limit.cooldownUntil } : undefined;\n}\n\nexport function providerIdForModel(model: string | object | undefined): string | undefined {\n\tif (typeof model === \"string\") return model.includes(\"/\") ? model.slice(0, model.indexOf(\"/\")) : undefined;\n\tif (model && \"provider\" in model && typeof model.provider === \"string\") return model.provider;\n\treturn undefined;\n}\n\n/** Model ID within its provider, from a `provider/model` reference or a model object. */\nexport function modelIdForModel(model: string | object | undefined): string | undefined {\n\tif (typeof model === \"string\") return model.includes(\"/\") ? model.slice(model.indexOf(\"/\") + 1) : undefined;\n\tif (model && \"id\" in model && typeof model.id === \"string\") return model.id;\n\treturn undefined;\n}\n\n/**\n * Time until requests to the provider (and, when given, this model) are admitted again. Pass `now` to compare\n * several limits against one clock reading.\n */\nexport function providerCooldownMs(provider: string | undefined, model?: string, now = Date.now()): number {\n\tif (provider === undefined) return 0;\n\tconst until = Math.max(0, ...scopedLimits(provider, model).map((limit) => limit.cooldownUntil));\n\treturn Math.max(0, until - now);\n}\n\n/** Time until an exhausted quota on the provider (and, when given, this model) resets. */\nexport function providerUnavailableMs(provider: string | undefined, model?: string, now = Date.now()): number {\n\tif (provider === undefined) return 0;\n\tconst until = Math.max(0, ...scopedLimits(provider, model).map((limit) => limit.unavailableUntil));\n\treturn Math.max(0, until - now);\n}\n\nexport function providerAdmissionDelayMs(provider: string, model?: string): number {\n\tconst limit = state.providers.get(provider);\n\tconst now = Date.now();\n\tconst cooldown = providerCooldownMs(provider, model);\n\tif (!limit?.budget) return cooldown;\n\tlimit.requests = limit.requests.filter((timestamp) => timestamp > now - limit.budget!.intervalMs);\n\tconst budgetDelay =\n\t\tlimit.requests.length < limit.budget.maxRequests\n\t\t\t? 0\n\t\t\t: limit.requests[limit.requests.length - limit.budget.maxRequests]! + limit.budget.intervalMs - now;\n\treturn Math.max(cooldown, budgetDelay);\n}\n\nexport function tryAdmitProviderRequest(provider: string, model?: string): number {\n\tconst delay = providerAdmissionDelayMs(provider, model);\n\tif (delay === 0) {\n\t\tconst limit = state.providers.get(provider);\n\t\tif (limit?.budget) limit.requests.push(Date.now());\n\t}\n\treturn delay;\n}\n\nexport function isProviderRateLimit(error: unknown): boolean {\n\treturn normalizeModelFailureSignal(error)?.kind === \"rate_limit\";\n}\n\n/** A rate limit or an exhausted quota: either makes the account unusable until the server's reset. */\nexport function isProviderLimitFailure(error: unknown): boolean {\n\tconst kind = normalizeModelFailureSignal(error)?.kind;\n\treturn kind === \"rate_limit\" || kind === \"quota_exhausted\";\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Whether the failure names the model that hit the limit. Providers that limit per model say so in the\n * error (\"rate limit reached for <model>\"); without that, the limit may cover the whole account.\n */\nfunction failureNamesModel(error: unknown, model: string): boolean {\n\tif (!model.trim()) return false;\n\tconst text = failureObjects(error)\n\t\t.map((value) => errorMessage(value))\n\t\t.join(\"\\n\");\n\treturn new RegExp(`(?<![\\\\w.-])${escapeRegExp(model)}(?![\\\\w-]|\\\\.\\\\w)`, \"i\").test(text);\n}\n\nfunction failureObjects(error: unknown): object[] {\n\tconst seen = new Set<object>();\n\tlet current = error;\n\twhile (current !== null && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tcurrent = \"cause\" in current ? current.cause : undefined;\n\t}\n\treturn [...seen];\n}\n\nexport function markProviderRateLimitObserved(error: object, scope: ProviderLimitScope): void {\n\tstate.observedFailures.set(error, scope);\n}\n\n/**\n * Record a rate limit or exhausted quota and return how long the account stays limited. The limit covers\n * only `model` when the failure names that model, and the whole provider otherwise.\n */\nexport function recordProviderRateLimit(\n\tprovider: string | undefined,\n\terror: unknown,\n\tmodel?: string,\n): number | undefined {\n\tif (provider === undefined) return undefined;\n\tconst kind = normalizeModelFailureSignal(error)?.kind;\n\tif (kind !== \"rate_limit\" && kind !== \"quota_exhausted\") return undefined;\n\tconst remaining = () =>\n\t\tkind === \"quota_exhausted\" ? providerUnavailableMs(provider, model) : providerCooldownMs(provider, model);\n\tfor (const value of failureObjects(error)) {\n\t\tconst scope = state.observedFailures.get(value);\n\t\tif (scope !== undefined) {\n\t\t\tstate.observedFailures.delete(value);\n\t\t\t// Already counted; a server hint on this later view of the failure can still extend the limit.\n\t\t\trecordProviderRetryHint(provider, error, scope);\n\t\t\treturn remaining();\n\t\t}\n\t}\n\tconst providerEntry = providerLimit(provider);\n\tconst key = providerLimitScope(provider, error, model).key;\n\tconst limit = providerLimit(key);\n\tproviderEntry.rateLimitCount += 1;\n\tlimit.consecutiveLimits += 1;\n\tconst backoff = Math.min(60_000, 1000 * 2 ** Math.min(limit.consecutiveLimits - 1, 6));\n\textendLimit(key, kind, providerRetryHint(error).retryAfterMs ?? backoff * (0.5 + Math.random() * 0.5));\n\treturn remaining();\n}\n\n/** The limit a failure belongs to: its model when the failure names that model, else the whole provider. */\nexport function providerLimitScope(provider: string, error: unknown, model?: string): ProviderLimitScope {\n\treturn { key: model !== undefined && failureNamesModel(error, model) ? modelLimitKey(provider, model) : provider };\n}\n\nfunction extendLimit(key: string, kind: \"rate_limit\" | \"quota_exhausted\", delay: number): void {\n\tconst until = Math.ceil(Date.now() + Math.max(1, delay));\n\tconst observation = raiseLimit(key, kind === \"quota_exhausted\" ? \"unavailable\" : \"cooldown\", until);\n\tif (observation === undefined || state.observers === undefined) return;\n\tfor (const observer of state.observers) {\n\t\t// An observer must never change how the request that hit the limit is handled, nor starve the others.\n\t\ttry {\n\t\t\tobserver(observation);\n\t\t} catch (error) {\n\t\t\tif (!observerFailureReported) {\n\t\t\t\tobserverFailureReported = true;\n\t\t\t\tconsole.debug(\"Provider rate-limit observer failed\", error);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Observer failures are reported once per process. */\nlet observerFailureReported = false;\n\n/** Raise one limit to `until`; returns the limit as an observation when it grew, and nothing otherwise. */\nfunction raiseLimit(\n\tkey: string,\n\tkind: ProviderRateLimitObservation[\"kind\"],\n\tuntil: number,\n): ProviderRateLimitObservation | undefined {\n\tconst limit = providerLimit(key);\n\tconst field = kind === \"unavailable\" ? \"unavailableUntil\" : \"cooldownUntil\";\n\tif (until <= limit[field]) return undefined;\n\tlimit[field] = until;\n\tconst slash = key.indexOf(\"/\");\n\treturn slash < 0\n\t\t? { provider: key, kind, untilEpochMs: until }\n\t\t: { provider: key.slice(0, slash), model: key.slice(slash + 1), kind, untilEpochMs: until };\n}\n\n/**\n * Merge a limit another process observed. It only ever extends the limit of the same provider or model\n * scope, ignores limits that already ended, and counts no failure here. Returns whether the limit grew.\n * Observers are not told: the observation came from elsewhere, so sending it on would echo it back.\n */\nexport function applyProviderRateLimitObservation(observation: ProviderRateLimitObservation): boolean {\n\tconst { provider, model, kind, untilEpochMs } = observation;\n\tif (!provider || provider.includes(\"/\") || (kind !== \"cooldown\" && kind !== \"unavailable\")) return false;\n\tif (!Number.isFinite(untilEpochMs) || untilEpochMs <= Date.now()) return false;\n\tconst key = model === undefined ? provider : modelLimitKey(provider, model);\n\treturn raiseLimit(key, kind, Math.ceil(untilEpochMs)) !== undefined;\n}\n\n/** Every limit of this process that has not ended yet, as observations another process can apply. */\nexport function providerRateLimitObservations(): ProviderRateLimitObservation[] {\n\tconst now = Date.now();\n\tconst observations: ProviderRateLimitObservation[] = [];\n\tfor (const [key, limit] of state.providers) {\n\t\tconst slash = key.indexOf(\"/\");\n\t\tconst scope = slash < 0 ? { provider: key } : { provider: key.slice(0, slash), model: key.slice(slash + 1) };\n\t\tif (limit.cooldownUntil > now)\n\t\t\tobservations.push({ ...scope, kind: \"cooldown\", untilEpochMs: limit.cooldownUntil });\n\t\tif (limit.unavailableUntil > now)\n\t\t\tobservations.push({ ...scope, kind: \"unavailable\", untilEpochMs: limit.unavailableUntil });\n\t}\n\treturn observations;\n}\n\n/**\n * Be told each time this process records or extends a limit, but not when it applies another process's\n * observation. Only rate-limited and quota-exhausted responses notify, never ordinary requests.\n */\nexport function observeProviderRateLimits(observer: (observation: ProviderRateLimitObservation) => void): () => void {\n\tstate.observers ??= new Set();\n\tconst observers = state.observers;\n\tobservers.add(observer);\n\treturn () => {\n\t\tobservers.delete(observer);\n\t};\n}\n\n/**\n * Apply the server retry hint of a failure that was already recorded against `scope`, without counting it\n * again. A limit only ever grows, so a guess made before the hint was known cannot shorten it.\n */\nexport function recordProviderRetryHint(provider: string | undefined, error: unknown, scope: ProviderLimitScope): void {\n\tif (provider === undefined) return;\n\tconst kind = normalizeModelFailureSignal(error)?.kind;\n\tif (kind !== \"rate_limit\" && kind !== \"quota_exhausted\") return;\n\tconst retryAfterMs = providerRetryHint(error).retryAfterMs;\n\tif (retryAfterMs !== undefined) extendLimit(scope.key, kind, retryAfterMs);\n}\n\nexport function recordProviderSuccess(provider: string, model?: string): void {\n\tfor (const limit of scopedLimits(provider, model)) limit.consecutiveLimits = 0;\n}\n\nexport function providerRetryHint(error: unknown): { retryAfterMs?: number } {\n\tfor (const value of failureObjects(error)) {\n\t\tconst retryAfterMs = getProviderRetryAfterMs(value);\n\t\tif (retryAfterMs !== undefined) return { retryAfterMs };\n\t}\n\treturn {};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"provider-request-admission.d.ts","sourceRoot":"","sources":["../../src/core/provider-request-admission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AA2ElF,wBAAsB,mBAAmB,CACxC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,aAAa,EACtB,aAAa,EAAE,OAAO,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAAC,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;CAAE,CAAC,CAsBhH"}
1
+ {"version":3,"file":"provider-request-admission.d.ts","sourceRoot":"","sources":["../../src/core/provider-request-admission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAoGlF,wBAAsB,mBAAmB,CACxC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,aAAa,EACtB,aAAa,EAAE,OAAO,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAAC,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;CAAE,CAAC,CA0BhH"}
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { createFetchWithAdmission } from "@bastani/pi-ai/utils/request-admission";
3
- import { isProviderRateLimit, markProviderRateLimitObserved, recordProviderRateLimit, recordProviderSuccess, tryAdmitProviderRequest, } from "./provider-rate-limits.js";
3
+ import { isProviderLimitFailure, isProviderRateLimit, markProviderRateLimitObserved, providerLimitScope, recordProviderRateLimit, recordProviderRetryHint, recordProviderSuccess, tryAdmitProviderRequest, } from "./provider-rate-limits.js";
4
4
  const FETCH_APIS = new Set([
5
5
  "anthropic-messages",
6
6
  "openai-completions",
@@ -11,31 +11,50 @@ const FETCH_APIS = new Set([
11
11
  ]);
12
12
  const streamAdmission = new AsyncLocalStorage();
13
13
  const admittedFetches = new WeakMap();
14
- function providerFetch(provider, originalFetch) {
14
+ async function responseText(response) {
15
+ try {
16
+ return await response.clone().text();
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ function providerFetch(provider, model, originalFetch) {
15
23
  let providers = admittedFetches.get(originalFetch);
16
24
  if (!providers) {
17
25
  providers = new Map();
18
26
  admittedFetches.set(originalFetch, providers);
19
27
  }
20
- const cached = providers.get(provider);
28
+ const key = `${provider}/${model}`;
29
+ const cached = providers.get(key);
21
30
  if (cached)
22
31
  return cached;
23
32
  const observedFetch = Object.assign(async (input, init) => {
24
33
  const state = streamAdmission.getStore();
25
- if (state)
34
+ if (state) {
26
35
  state.observedRateLimit = false;
36
+ state.observedQuotaExhaustion = false;
37
+ state.observedScope = undefined;
38
+ }
27
39
  const response = await originalFetch(input, init);
28
- if (response.status === 429) {
29
- recordProviderRateLimit(provider, { status: 429, headers: response.headers });
40
+ if (response.status === 429 || response.status === 402) {
41
+ // The body can name the model whose limit was hit, which scopes the cooldown to that model.
42
+ const body = await responseText(response);
43
+ const failure = { status: response.status, headers: response.headers, message: body, body };
44
+ recordProviderRateLimit(provider, failure, model);
30
45
  if (state)
46
+ state.observedScope = providerLimitScope(provider, failure, model);
47
+ if (state && response.status === 429)
31
48
  state.observedRateLimit = true;
49
+ if (state && response.status === 402)
50
+ state.observedQuotaExhaustion = true;
32
51
  }
33
52
  else if (response.ok)
34
- recordProviderSuccess(provider);
53
+ recordProviderSuccess(provider, model);
35
54
  return response;
36
55
  }, originalFetch);
37
- const admitted = createFetchWithAdmission(observedFetch, (signal) => waitForAdmission(provider, signal));
38
- providers.set(provider, admitted);
56
+ const admitted = createFetchWithAdmission(observedFetch, (signal) => waitForAdmission(provider, model, signal));
57
+ providers.set(key, admitted);
39
58
  return admitted;
40
59
  }
41
60
  function sleepForAdmission(ms, signal) {
@@ -54,24 +73,24 @@ function sleepForAdmission(ms, signal) {
54
73
  signal?.addEventListener("abort", abort, { once: true });
55
74
  });
56
75
  }
57
- async function waitForAdmission(provider, signal) {
76
+ async function waitForAdmission(provider, model, signal) {
58
77
  while (true) {
59
78
  signal?.throwIfAborted();
60
- const delay = tryAdmitProviderRequest(provider);
79
+ const delay = tryAdmitProviderRequest(provider, model);
61
80
  if (delay === 0)
62
81
  return;
63
82
  await sleepForAdmission(Math.min(delay, 60_000), signal);
64
83
  }
65
84
  }
66
85
  export async function admitProviderStream(model, options, ownsTransport) {
67
- const state = { observedRateLimit: false };
86
+ const state = { observedRateLimit: false, observedQuotaExhaustion: false };
68
87
  const usesFetch = !ownsTransport &&
69
88
  (FETCH_APIS.has(model.api) || (model.api === "openai-codex-responses" && options.transport === "sse"));
70
89
  if (usesFetch) {
71
- options = { ...options, fetch: providerFetch(model.provider, options.fetch ?? globalThis.fetch) };
90
+ options = { ...options, fetch: providerFetch(model.provider, model.id, options.fetch ?? globalThis.fetch) };
72
91
  }
73
92
  else {
74
- await waitForAdmission(model.provider, options.signal);
93
+ await waitForAdmission(model.provider, model.id, options.signal);
75
94
  }
76
95
  return {
77
96
  options,
@@ -80,11 +99,16 @@ export async function admitProviderStream(model, options, ownsTransport) {
80
99
  if (state.observedRateLimit && error.stopReason === "error" && !isProviderRateLimit(error)) {
81
100
  error.errorMessage = `429 Too Many Requests: ${error.errorMessage ?? "provider rate limited the request"}`;
82
101
  }
83
- if (!isProviderRateLimit(error))
102
+ if (!isProviderLimitFailure(error))
84
103
  return;
85
- if (!state.observedRateLimit)
86
- recordProviderRateLimit(model.provider, error);
87
- markProviderRateLimitObserved(error);
104
+ let scope = state.observedScope;
105
+ if (scope === undefined) {
106
+ recordProviderRateLimit(model.provider, error, model.id);
107
+ scope = providerLimitScope(model.provider, error, model.id);
108
+ }
109
+ else
110
+ recordProviderRetryHint(model.provider, error, scope);
111
+ markProviderRateLimitObserved(error, scope);
88
112
  },
89
113
  };
90
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"provider-request-admission.js","sourceRoot":"","sources":["../../src/core/provider-request-admission.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { Api, AssistantMessage, Model, StreamOptions } from \"@bastani/pi-ai\";\nimport { createFetchWithAdmission } from \"@bastani/pi-ai/utils/request-admission\";\nimport {\n\tisProviderRateLimit,\n\tmarkProviderRateLimitObserved,\n\trecordProviderRateLimit,\n\trecordProviderSuccess,\n\ttryAdmitProviderRequest,\n} from \"./provider-rate-limits.js\";\n\nconst FETCH_APIS = new Set([\n\t\"anthropic-messages\",\n\t\"openai-completions\",\n\t\"openai-responses\",\n\t\"azure-openai-responses\",\n\t\"mistral-conversations\",\n\t\"pi-messages\",\n]);\n\ninterface StreamAdmissionState {\n\tobservedRateLimit: boolean;\n}\n\nconst streamAdmission = new AsyncLocalStorage<StreamAdmissionState>();\nconst admittedFetches = new WeakMap<typeof fetch, Map<string, typeof fetch>>();\n\nfunction providerFetch(provider: string, originalFetch: typeof fetch): typeof fetch {\n\tlet providers = admittedFetches.get(originalFetch);\n\tif (!providers) {\n\t\tproviders = new Map();\n\t\tadmittedFetches.set(originalFetch, providers);\n\t}\n\tconst cached = providers.get(provider);\n\tif (cached) return cached;\n\tconst observedFetch: typeof fetch = Object.assign(async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {\n\t\tconst state = streamAdmission.getStore();\n\t\tif (state) state.observedRateLimit = false;\n\t\tconst response = await originalFetch(input, init);\n\t\tif (response.status === 429) {\n\t\t\trecordProviderRateLimit(provider, { status: 429, headers: response.headers });\n\t\t\tif (state) state.observedRateLimit = true;\n\t\t} else if (response.ok) recordProviderSuccess(provider);\n\t\treturn response;\n\t}, originalFetch);\n\tconst admitted = createFetchWithAdmission(observedFetch, (signal) => waitForAdmission(provider, signal));\n\tproviders.set(provider, admitted);\n\treturn admitted;\n}\n\nfunction sleepForAdmission(ms: number, signal?: AbortSignal): Promise<void> {\n\tsignal?.throwIfAborted();\n\treturn new Promise((resolve, reject) => {\n\t\tconst cleanup = () => signal?.removeEventListener(\"abort\", abort);\n\t\tconst timer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t}, ms);\n\t\tconst abort = () => {\n\t\t\tclearTimeout(timer);\n\t\t\tcleanup();\n\t\t\treject(signal?.reason ?? new Error(\"Provider request aborted\"));\n\t\t};\n\t\tsignal?.addEventListener(\"abort\", abort, { once: true });\n\t});\n}\n\nasync function waitForAdmission(provider: string, signal?: AbortSignal): Promise<void> {\n\twhile (true) {\n\t\tsignal?.throwIfAborted();\n\t\tconst delay = tryAdmitProviderRequest(provider);\n\t\tif (delay === 0) return;\n\t\tawait sleepForAdmission(Math.min(delay, 60_000), signal);\n\t}\n}\n\nexport async function admitProviderStream(\n\tmodel: Model<Api>,\n\toptions: StreamOptions,\n\townsTransport: boolean,\n): Promise<{ options: StreamOptions; run<T>(invoke: () => T): T; observeFailure(error: AssistantMessage): void }> {\n\tconst state: StreamAdmissionState = { observedRateLimit: false };\n\tconst usesFetch =\n\t\t!ownsTransport &&\n\t\t(FETCH_APIS.has(model.api) || (model.api === \"openai-codex-responses\" && options.transport === \"sse\"));\n\tif (usesFetch) {\n\t\toptions = { ...options, fetch: providerFetch(model.provider, options.fetch ?? globalThis.fetch) };\n\t} else {\n\t\tawait waitForAdmission(model.provider, options.signal);\n\t}\n\treturn {\n\t\toptions,\n\t\trun: (invoke) => streamAdmission.run(state, invoke),\n\t\tobserveFailure(error) {\n\t\t\tif (state.observedRateLimit && error.stopReason === \"error\" && !isProviderRateLimit(error)) {\n\t\t\t\terror.errorMessage = `429 Too Many Requests: ${error.errorMessage ?? \"provider rate limited the request\"}`;\n\t\t\t}\n\t\t\tif (!isProviderRateLimit(error)) return;\n\t\t\tif (!state.observedRateLimit) recordProviderRateLimit(model.provider, error);\n\t\t\tmarkProviderRateLimitObserved(error);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"provider-request-admission.js","sourceRoot":"","sources":["../../src/core/provider-request-admission.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { Api, AssistantMessage, Model, StreamOptions } from \"@bastani/pi-ai\";\nimport { createFetchWithAdmission } from \"@bastani/pi-ai/utils/request-admission\";\nimport {\n\tisProviderLimitFailure,\n\tisProviderRateLimit,\n\tmarkProviderRateLimitObserved,\n\ttype ProviderLimitScope,\n\tproviderLimitScope,\n\trecordProviderRateLimit,\n\trecordProviderRetryHint,\n\trecordProviderSuccess,\n\ttryAdmitProviderRequest,\n} from \"./provider-rate-limits.js\";\n\nconst FETCH_APIS = new Set([\n\t\"anthropic-messages\",\n\t\"openai-completions\",\n\t\"openai-responses\",\n\t\"azure-openai-responses\",\n\t\"mistral-conversations\",\n\t\"pi-messages\",\n]);\n\ninterface StreamAdmissionState {\n\tobservedRateLimit: boolean;\n\tobservedQuotaExhaustion: boolean;\n\t/** The limit the observed 429/402 response was recorded against. */\n\tobservedScope?: ProviderLimitScope;\n}\n\nconst streamAdmission = new AsyncLocalStorage<StreamAdmissionState>();\nconst admittedFetches = new WeakMap<typeof fetch, Map<string, typeof fetch>>();\n\nasync function responseText(response: Response): Promise<string> {\n\ttry {\n\t\treturn await response.clone().text();\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\nfunction providerFetch(provider: string, model: string, originalFetch: typeof fetch): typeof fetch {\n\tlet providers = admittedFetches.get(originalFetch);\n\tif (!providers) {\n\t\tproviders = new Map();\n\t\tadmittedFetches.set(originalFetch, providers);\n\t}\n\tconst key = `${provider}/${model}`;\n\tconst cached = providers.get(key);\n\tif (cached) return cached;\n\tconst observedFetch: typeof fetch = Object.assign(async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {\n\t\tconst state = streamAdmission.getStore();\n\t\tif (state) {\n\t\t\tstate.observedRateLimit = false;\n\t\t\tstate.observedQuotaExhaustion = false;\n\t\t\tstate.observedScope = undefined;\n\t\t}\n\t\tconst response = await originalFetch(input, init);\n\t\tif (response.status === 429 || response.status === 402) {\n\t\t\t// The body can name the model whose limit was hit, which scopes the cooldown to that model.\n\t\t\tconst body = await responseText(response);\n\t\t\tconst failure = { status: response.status, headers: response.headers, message: body, body };\n\t\t\trecordProviderRateLimit(provider, failure, model);\n\t\t\tif (state) state.observedScope = providerLimitScope(provider, failure, model);\n\t\t\tif (state && response.status === 429) state.observedRateLimit = true;\n\t\t\tif (state && response.status === 402) state.observedQuotaExhaustion = true;\n\t\t} else if (response.ok) recordProviderSuccess(provider, model);\n\t\treturn response;\n\t}, originalFetch);\n\tconst admitted = createFetchWithAdmission(observedFetch, (signal) => waitForAdmission(provider, model, signal));\n\tproviders.set(key, admitted);\n\treturn admitted;\n}\n\nfunction sleepForAdmission(ms: number, signal?: AbortSignal): Promise<void> {\n\tsignal?.throwIfAborted();\n\treturn new Promise((resolve, reject) => {\n\t\tconst cleanup = () => signal?.removeEventListener(\"abort\", abort);\n\t\tconst timer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t}, ms);\n\t\tconst abort = () => {\n\t\t\tclearTimeout(timer);\n\t\t\tcleanup();\n\t\t\treject(signal?.reason ?? new Error(\"Provider request aborted\"));\n\t\t};\n\t\tsignal?.addEventListener(\"abort\", abort, { once: true });\n\t});\n}\n\nasync function waitForAdmission(provider: string, model: string, signal?: AbortSignal): Promise<void> {\n\twhile (true) {\n\t\tsignal?.throwIfAborted();\n\t\tconst delay = tryAdmitProviderRequest(provider, model);\n\t\tif (delay === 0) return;\n\t\tawait sleepForAdmission(Math.min(delay, 60_000), signal);\n\t}\n}\n\nexport async function admitProviderStream(\n\tmodel: Model<Api>,\n\toptions: StreamOptions,\n\townsTransport: boolean,\n): Promise<{ options: StreamOptions; run<T>(invoke: () => T): T; observeFailure(error: AssistantMessage): void }> {\n\tconst state: StreamAdmissionState = { observedRateLimit: false, observedQuotaExhaustion: false };\n\tconst usesFetch =\n\t\t!ownsTransport &&\n\t\t(FETCH_APIS.has(model.api) || (model.api === \"openai-codex-responses\" && options.transport === \"sse\"));\n\tif (usesFetch) {\n\t\toptions = { ...options, fetch: providerFetch(model.provider, model.id, options.fetch ?? globalThis.fetch) };\n\t} else {\n\t\tawait waitForAdmission(model.provider, model.id, options.signal);\n\t}\n\treturn {\n\t\toptions,\n\t\trun: (invoke) => streamAdmission.run(state, invoke),\n\t\tobserveFailure(error) {\n\t\t\tif (state.observedRateLimit && error.stopReason === \"error\" && !isProviderRateLimit(error)) {\n\t\t\t\terror.errorMessage = `429 Too Many Requests: ${error.errorMessage ?? \"provider rate limited the request\"}`;\n\t\t\t}\n\t\t\tif (!isProviderLimitFailure(error)) return;\n\t\t\tlet scope = state.observedScope;\n\t\t\tif (scope === undefined) {\n\t\t\t\trecordProviderRateLimit(model.provider, error, model.id);\n\t\t\t\tscope = providerLimitScope(model.provider, error, model.id);\n\t\t\t} else recordProviderRetryHint(model.provider, error, scope);\n\t\t\tmarkProviderRateLimitObserved(error, scope);\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"node-sqlite.d.ts","sourceRoot":"","sources":["../../../src/core/storage/node-sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAA6D,MAAM,eAAe,CAAC;AA2Z/G,wBAAsB,sBAAsB,CAC3C,IAAI,EAAE,MAAM,EACZ,EACC,aAAoB,EACpB,UAA6C,EAC7C,QAAgB,GAChB,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACzE,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,YAAY,GAAG,cAAc,CAW/E"}
1
+ {"version":3,"file":"node-sqlite.d.ts","sourceRoot":"","sources":["../../../src/core/storage/node-sqlite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAA6D,MAAM,eAAe,CAAC;AAyjB/G,wBAAsB,sBAAsB,CAC3C,IAAI,EAAE,MAAM,EACZ,EACC,aAAoB,EACpB,UAA6C,EAC7C,QAAgB,GAChB,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACzE,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,YAAY,GAAG,cAAc,CAW/E"}