@desplega.ai/agent-swarm 1.130.0 → 1.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/dist/{actions-73ee723n.js → actions-w53mbns9.js} +7 -7
  3. package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
  4. package/dist/{app-hz5c2xay.js → app-m9gwqsqv.js} +4 -4
  5. package/dist/{assistant-08mr16yp.js → assistant-bp65m6f3.js} +9 -9
  6. package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
  7. package/dist/{boot-reembed-5bgan3cd.js → boot-reembed-62p0at2k.js} +4 -4
  8. package/dist/{boot-reembed-z7ykyeq8.js → boot-reembed-xmfp6wdh.js} +5 -5
  9. package/dist/{boot-scrub-logs-b2rjk8mv.js → boot-scrub-logs-rr8q2dms.js} +3 -3
  10. package/dist/{claude-adapter-qr0k17km.js → claude-adapter-2y47mrkd.js} +6 -6
  11. package/dist/{cli-s21zzx4b.js → cli-0rcyq6d9.js} +447 -183
  12. package/dist/{cli-brebbsen.js → cli-2q72qz2s.js} +9 -6
  13. package/dist/{cli-e5p3es48.js → cli-3kf51j15.js} +1 -1
  14. package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
  15. package/dist/{cli-h3e66g5b.js → cli-5kx36evn.js} +1 -1
  16. package/dist/{cli-n9yxcnvk.js → cli-5n6e5yfn.js} +29 -7
  17. package/dist/{cli-tp519bjz.js → cli-5v4rw6yd.js} +2 -2
  18. package/dist/{cli-dakxgk70.js → cli-5ze9rv97.js} +4 -4
  19. package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
  20. package/dist/{cli-mgerp0vs.js → cli-70xqj0z3.js} +2 -2
  21. package/dist/{cli-7v5dhmc8.js → cli-72q8m8gk.js} +6 -6
  22. package/dist/{cli-yj7z99vp.js → cli-78xwvkhj.js} +2 -2
  23. package/dist/{cli-t5tsq67k.js → cli-9dr5qzc0.js} +39 -4
  24. package/dist/{cli-73bvdyxr.js → cli-a40rnf71.js} +6 -6
  25. package/dist/{cli-s18zzkfp.js → cli-a4jzdpa3.js} +44 -7
  26. package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
  27. package/dist/{cli-hntzdchd.js → cli-bqw0mt1n.js} +1 -1
  28. package/dist/{cli-mhr8tq3v.js → cli-cdeg08wx.js} +1 -1
  29. package/dist/{cli-qcsjdppx.js → cli-cvgjwnh1.js} +7 -7
  30. package/dist/{cli-z9q7pb38.js → cli-d2tfkcga.js} +4 -4
  31. package/dist/{cli-r62me33w.js → cli-d3x3x1c9.js} +2 -2
  32. package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
  33. package/dist/{cli-326083vb.js → cli-e7ckcg4v.js} +12 -12
  34. package/dist/{cli-fs153s3x.js → cli-efxfnq6q.js} +3 -3
  35. package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
  36. package/dist/{cli-2pa5w9b6.js → cli-fywms69x.js} +1 -1
  37. package/dist/{cli-1mj7yz1r.js → cli-g6b8x1h8.js} +1 -1
  38. package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
  39. package/dist/{cli-tfw3c0y1.js → cli-nm5gtp1x.js} +2 -2
  40. package/dist/{cli-q8zwffx0.js → cli-pm6xbh84.js} +128 -35
  41. package/dist/{cli-m8fs5ttq.js → cli-psh3c4va.js} +1 -1
  42. package/dist/{cli-1px8pn1s.js → cli-qv61hp37.js} +2 -2
  43. package/dist/{cli-mf009e0j.js → cli-rzbpmxdr.js} +1 -1
  44. package/dist/{cli-vr2f46zj.js → cli-t1xze5qh.js} +1 -1
  45. package/dist/{cli-qbb2ten8.js → cli-t55yw3a1.js} +2 -2
  46. package/dist/{cli-haanc4qd.js → cli-t76vw5db.js} +22 -22
  47. package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
  48. package/dist/{cli-xdrmxvd4.js → cli-yw8vghx2.js} +46 -10
  49. package/dist/cli.js +22 -22
  50. package/dist/{codex-adapter-6pc50x9q.js → codex-adapter-d24q75wk.js} +4 -4
  51. package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
  52. package/dist/{codex-session-runner-tpg8hyj7.js → codex-session-runner-g50vcsex.js} +4 -4
  53. package/dist/{commands-yfpwt11g.js → commands-c3nrc5hg.js} +3 -3
  54. package/dist/{db-jn499nmp.js → db-nt7fg3tw.js} +3 -3
  55. package/dist/{google-generative-ai-txgg2q58.js → google-generative-ai-hysrd8qx.js} +7 -5
  56. package/dist/{google-vertex-t663s1c3.js → google-vertex-p5arrga0.js} +7 -5
  57. package/dist/{handlers-eev4nng0.js → handlers-fkr6swat.js} +9 -9
  58. package/dist/{hook-sn7pj75w.js → hook-77wpvnjc.js} +4 -4
  59. package/dist/{hook-9fkwkf3m.js → hook-g019yehy.js} +5 -5
  60. package/dist/{http-xdbbtmhx.js → http-jewj0syk.js} +202 -79
  61. package/dist/{index-c0qedjh6.js → index-6gm5he8h.js} +9 -9
  62. package/dist/{index-5fdsemf0.js → index-7n2xpbd4.js} +155 -37
  63. package/dist/{index-9vnkprkh.js → index-hfvggd0z.js} +10 -10
  64. package/dist/{index-fnack5vh.js → index-hp1kdbjj.js} +11 -11
  65. package/dist/{keepalive-fc7a2hw9.js → keepalive-j0fjvqfd.js} +5 -5
  66. package/dist/{lead-1fva34y9.js → lead-1n8x30xe.js} +28 -28
  67. package/dist/{maintenance-0rdb12vf.js → maintenance-gvev11sa.js} +5 -5
  68. package/dist/{mistral-conversations-4swenhmf.js → mistral-conversations-81fwav0j.js} +2 -2
  69. package/dist/{oauth-refresh-sweep-f9rptqbt.js → oauth-refresh-sweep-s9ecsvh0.js} +4 -4
  70. package/dist/{onboard-9zn0fx2m.js → onboard-pvm5b38w.js} +2 -2
  71. package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
  72. package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
  73. package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
  74. package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
  75. package/dist/{otel-impl-thnttmbn.js → otel-impl-p6jvgd9c.js} +1 -1
  76. package/dist/{pi-mono-adapter-bpfrn6d7.js → pi-mono-adapter-t091s27g.js} +17114 -7514
  77. package/dist/{pricing-refresh-n0ve0kh6.js → pricing-refresh-k49rcyed.js} +5 -5
  78. package/dist/{rbac-roles-bbwb4wrm.js → rbac-roles-30pspvy7.js} +5 -5
  79. package/dist/{rbac-roles-w45bhahg.js → rbac-roles-cr80g96t.js} +4 -4
  80. package/dist/{render-v2-d675cp9r.js → render-v2-xm457ng1.js} +4 -4
  81. package/dist/{seed-pricing-47q77nw4.js → seed-pricing-yhpv35fr.js} +4 -4
  82. package/dist/{setup-3vnv2m43.js → setup-nytnmwwr.js} +2 -2
  83. package/dist/{worker-7shsr8hk.js → worker-msytr2cw.js} +28 -28
  84. package/openapi.json +342 -10
  85. package/package.json +6 -6
  86. package/src/apps/store.ts +13 -4
  87. package/src/assets/key.ts +8 -1
  88. package/src/be/asset-key-audit.ts +3 -1
  89. package/src/be/db-queries/mcp-oauth.ts +26 -0
  90. package/src/be/db.ts +42 -1
  91. package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
  92. package/src/be/scripts/db.ts +5 -2
  93. package/src/be/seed-skills/index.ts +7 -0
  94. package/src/http/assets.ts +120 -29
  95. package/src/http/mcp-oauth.ts +69 -1
  96. package/src/http/users.ts +45 -0
  97. package/src/oauth/ensure-mcp-token.ts +7 -1
  98. package/src/oauth/mcp-wrapper.ts +247 -24
  99. package/src/providers/pi-mono-adapter.ts +1 -1
  100. package/src/tests/asset-key-api.test.ts +48 -2
  101. package/src/tests/asset-key-invariants.test.ts +92 -0
  102. package/src/tests/asset-key-migration.test.ts +174 -8
  103. package/src/tests/asset-key-normalization.test.ts +2 -0
  104. package/src/tests/mcp-oauth-dcr-reuse.test.ts +95 -0
  105. package/src/tests/mcp-oauth-ensure-token.test.ts +23 -0
  106. package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
  107. package/src/tests/mcp-oauth-wrapper.test.ts +380 -0
  108. package/src/tests/rbac-admission-e2e.test.ts +68 -0
  109. package/src/tests/script-connections.test.ts +3 -0
  110. package/src/tests/whoami-route.test.ts +127 -0
  111. package/src/tests/workflow-engine-v2.test.ts +80 -7
  112. package/src/tests/workflow-swarm-script.test.ts +104 -0
  113. package/src/tools/workflows/create-workflow.ts +4 -1
  114. package/src/types.ts +18 -5
  115. package/src/utils/template.ts +8 -1
  116. package/src/workflows/engine.ts +27 -4
  117. package/src/workflows/executors/swarm-script.ts +28 -0
  118. package/src/workflows/retry-poller.ts +19 -2
  119. package/templates/skills/asset-namespaces/content.md +2 -2
  120. package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
  121. package/templates/skills/scheduled-task-resilience/config.json +14 -0
  122. package/templates/skills/scheduled-task-resilience/content.md +97 -0
@@ -42,11 +42,28 @@ function lazyStream(model, setup) {
42
42
  });
43
43
  return outer;
44
44
  }
45
- function lazyApi(load) {
46
- return {
45
+ function lazyApi(load, capabilities) {
46
+ const api = {
47
47
  stream: (model, context, options) => lazyStream(model, async () => (await load()).stream(model, context, options)),
48
48
  streamSimple: (model, context, options) => lazyStream(model, async () => (await load()).streamSimple(model, context, options))
49
49
  };
50
+ if (capabilities?.fetchDeferred) {
51
+ api.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
52
+ const implementation = await load();
53
+ if (!implementation.fetchDeferred)
54
+ throw new Error("API does not support deferred responses");
55
+ return implementation.fetchDeferred(model, handle, options);
56
+ });
57
+ }
58
+ if (capabilities?.cancelDeferred) {
59
+ api.cancelDeferred = async (model, handle, options) => {
60
+ const implementation = await load();
61
+ if (!implementation.cancelDeferred)
62
+ throw new Error("API cannot cancel deferred responses");
63
+ await implementation.cancelDeferred(model, handle, options);
64
+ };
65
+ }
66
+ return api;
50
67
  }
51
68
 
52
69
  // node_modules/@earendil-works/pi-ai/dist/auth/context.js
@@ -86,38 +103,93 @@ function defaultProviderAuthContext() {
86
103
  };
87
104
  }
88
105
 
106
+ // node_modules/@earendil-works/pi-ai/dist/utils/abort.js
107
+ function abortReason(signal) {
108
+ if (signal.reason !== undefined)
109
+ return signal.reason;
110
+ const error = new Error("The operation was aborted");
111
+ error.name = "AbortError";
112
+ return error;
113
+ }
114
+ function operationSignal(signal) {
115
+ return signal ?? new AbortController().signal;
116
+ }
117
+ function raceWithAbortSignal(operation, signal) {
118
+ if (signal.aborted) {
119
+ operation.catch(() => {});
120
+ return Promise.reject(abortReason(signal));
121
+ }
122
+ return new Promise((resolve, reject) => {
123
+ let settled = false;
124
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
125
+ const onAbort = () => {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ cleanup();
130
+ reject(abortReason(signal));
131
+ };
132
+ signal.addEventListener("abort", onAbort, { once: true });
133
+ operation.then((value) => {
134
+ if (settled)
135
+ return;
136
+ settled = true;
137
+ cleanup();
138
+ resolve(value);
139
+ }, (error) => {
140
+ if (settled)
141
+ return;
142
+ settled = true;
143
+ cleanup();
144
+ reject(error);
145
+ });
146
+ if (signal.aborted)
147
+ onAbort();
148
+ });
149
+ }
150
+
89
151
  // node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js
90
152
  class InMemoryCredentialStore {
91
153
  credentials = new Map;
92
154
  chains = new Map;
93
- enqueue(providerId, task) {
155
+ enqueue(providerId, task, options) {
156
+ const signal = operationSignal(options?.signal);
94
157
  const previous = this.chains.get(providerId) ?? Promise.resolve();
95
- const next = (async () => {
158
+ const queued = (async () => {
96
159
  await previous.catch(() => {});
160
+ signal.throwIfAborted();
97
161
  return task();
98
162
  })();
99
- this.chains.set(providerId, next.catch(() => {}));
100
- return next;
163
+ const tail = queued.catch(() => {});
164
+ this.chains.set(providerId, tail);
165
+ tail.then(() => {
166
+ if (this.chains.get(providerId) === tail)
167
+ this.chains.delete(providerId);
168
+ });
169
+ return raceWithAbortSignal(queued, signal);
101
170
  }
102
- async read(providerId) {
171
+ async read(providerId, options) {
172
+ options?.signal?.throwIfAborted();
103
173
  return this.credentials.get(providerId);
104
174
  }
105
- async list() {
175
+ async list(options) {
176
+ options?.signal?.throwIfAborted();
106
177
  return [...this.credentials].map(([providerId, credential]) => ({ providerId, type: credential.type }));
107
178
  }
108
- modify(providerId, fn) {
179
+ modify(providerId, fn, options) {
109
180
  return this.enqueue(providerId, async () => {
110
181
  const current = this.credentials.get(providerId);
111
182
  const next = await fn(current);
183
+ options?.signal?.throwIfAborted();
112
184
  if (next !== undefined)
113
185
  this.credentials.set(providerId, next);
114
186
  return next ?? current;
115
- });
187
+ }, options);
116
188
  }
117
- delete(providerId) {
189
+ delete(providerId, options) {
118
190
  return this.enqueue(providerId, async () => {
119
191
  this.credentials.delete(providerId);
120
- });
192
+ }, options);
121
193
  }
122
194
  }
123
195
 
@@ -138,27 +210,32 @@ function withCauseDetail(message, cause) {
138
210
  return message;
139
211
  return `${message}: ${detail}`;
140
212
  }
141
- async function resolveProviderAuth(provider, credentials, authContext, overrides) {
213
+ function resolveProviderAuth(provider, credentials, authContext, overrides) {
214
+ const signal = operationSignal(overrides?.signal);
215
+ return raceWithAbortSignal(resolveProviderAuthWithSignal(provider, credentials, authContext, overrides, signal), signal);
216
+ }
217
+ async function resolveProviderAuthWithSignal(provider, credentials, authContext, overrides, signal) {
218
+ signal.throwIfAborted();
142
219
  const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext;
143
220
  if (overrides?.apiKey !== undefined && provider.auth.apiKey) {
144
221
  return resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, {
145
222
  type: "api_key",
146
223
  key: overrides.apiKey,
147
224
  env: overrides.env
148
- });
225
+ }, signal);
149
226
  }
150
- const stored = await readCredential(credentials, provider.id);
227
+ const stored = await readCredential(credentials, provider.id, signal);
151
228
  if (stored) {
152
229
  if (stored.type === "oauth" && provider.auth.oauth) {
153
- return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored, overrides?.minOAuthValidityMs);
230
+ return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored, signal, overrides?.minOAuthValidityMs);
154
231
  }
155
232
  if (stored.type === "api_key" && provider.auth.apiKey) {
156
233
  const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored;
157
- return resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, credential);
234
+ return resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, credential, signal);
158
235
  }
159
236
  return;
160
237
  }
161
- return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, undefined) : undefined;
238
+ return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, undefined, signal) : undefined;
162
239
  }
163
240
  function overlayEnvAuthContext(base, env) {
164
241
  return {
@@ -167,7 +244,8 @@ function overlayEnvAuthContext(base, env) {
167
244
  };
168
245
  }
169
246
  var DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1000;
170
- async function resolveStoredOAuth(credentials, providerId, oauth, stored, minOAuthValidityMs) {
247
+ var DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 15000;
248
+ async function resolveStoredOAuth(credentials, providerId, oauth, stored, signal, minOAuthValidityMs) {
171
249
  const minimumValidityMs = Math.max(DEFAULT_OAUTH_MINIMUM_VALIDITY_MS, minOAuthValidityMs ?? 0);
172
250
  const expiresSoon = (credential2) => Date.now() + minimumValidityMs >= credential2.expires;
173
251
  let credential = stored;
@@ -180,11 +258,15 @@ async function resolveStoredOAuth(credentials, providerId, oauth, stored, minOAu
180
258
  if (!expiresSoon(current))
181
259
  return;
182
260
  try {
183
- return await oauth.refresh(current);
261
+ const refreshSignal = AbortSignal.any([
262
+ signal,
263
+ AbortSignal.timeout(DEFAULT_OAUTH_REFRESH_TIMEOUT_MS)
264
+ ]);
265
+ return await oauth.refresh(current, refreshSignal);
184
266
  } catch (error) {
185
267
  throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error });
186
268
  }
187
- });
269
+ }, { signal });
188
270
  } catch (error) {
189
271
  if (error instanceof ModelsError)
190
272
  throw error;
@@ -203,16 +285,16 @@ async function resolveStoredOAuth(credentials, providerId, oauth, stored, minOAu
203
285
  throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error });
204
286
  }
205
287
  }
206
- async function resolveApiKey(authContext, apiKey, providerId, credential) {
288
+ async function resolveApiKey(authContext, apiKey, providerId, credential, signal) {
207
289
  try {
208
- return await apiKey.resolve({ ctx: authContext, credential });
290
+ return await apiKey.resolve({ ctx: authContext, credential, signal });
209
291
  } catch (error) {
210
292
  throw new ModelsError("auth", `API key auth failed for provider ${providerId}`, { cause: error });
211
293
  }
212
294
  }
213
- async function readCredential(credentials, providerId) {
295
+ async function readCredential(credentials, providerId, signal) {
214
296
  try {
215
- return await credentials.read(providerId);
297
+ return await credentials.read(providerId, { signal });
216
298
  } catch (error) {
217
299
  throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error });
218
300
  }
@@ -221,14 +303,17 @@ async function readCredential(credentials, providerId) {
221
303
  // node_modules/@earendil-works/pi-ai/dist/models-store.js
222
304
  class InMemoryModelsStore {
223
305
  entries = new Map;
224
- async read(providerId) {
306
+ async read(providerId, options) {
307
+ options?.signal?.throwIfAborted();
225
308
  const entry = this.entries.get(providerId);
226
309
  return entry ? structuredClone(entry) : undefined;
227
310
  }
228
- async write(providerId, entry) {
311
+ async write(providerId, entry, options) {
312
+ options?.signal?.throwIfAborted();
229
313
  this.entries.set(providerId, structuredClone(entry));
230
314
  }
231
- async delete(providerId) {
315
+ async delete(providerId, options) {
316
+ options?.signal?.throwIfAborted();
232
317
  this.entries.delete(providerId);
233
318
  }
234
319
  }
@@ -254,18 +339,26 @@ class ModelsImpl {
254
339
  credentials;
255
340
  modelsStore;
256
341
  authContext;
342
+ refreshGenerations = new Map;
343
+ refreshControllers = new Map;
344
+ publicationChains = new Map;
257
345
  constructor(options) {
258
346
  this.credentials = options?.credentials ?? new InMemoryCredentialStore;
259
347
  this.modelsStore = options?.modelsStore ?? new InMemoryModelsStore;
260
348
  this.authContext = options?.authContext ?? defaultProviderAuthContext();
261
349
  }
262
350
  setProvider(provider) {
351
+ this.supersedeProviderRefresh(provider.id);
263
352
  this.providers.set(provider.id, provider);
264
353
  }
265
354
  deleteProvider(id) {
355
+ this.supersedeProviderRefresh(id);
266
356
  this.providers.delete(id);
267
357
  }
268
358
  clearProviders() {
359
+ for (const id of new Set([...this.providers.keys(), ...this.refreshControllers.keys()])) {
360
+ this.supersedeProviderRefresh(id);
361
+ }
269
362
  this.providers.clear();
270
363
  }
271
364
  getProviders() {
@@ -296,80 +389,139 @@ class ModelsImpl {
296
389
  getModel(provider, id) {
297
390
  return this.getModels(provider).find((model) => model.id === id);
298
391
  }
392
+ supersedeProviderRefresh(providerId) {
393
+ const generation = (this.refreshGenerations.get(providerId) ?? 0) + 1;
394
+ this.refreshGenerations.set(providerId, generation);
395
+ const previous = this.refreshControllers.get(providerId);
396
+ if (previous) {
397
+ this.refreshControllers.delete(providerId);
398
+ previous.abort();
399
+ }
400
+ return generation;
401
+ }
402
+ beginProviderRefresh(providerId) {
403
+ const generation = this.supersedeProviderRefresh(providerId);
404
+ const controller = new AbortController;
405
+ this.refreshControllers.set(providerId, controller);
406
+ return { generation, controller };
407
+ }
408
+ publishProviderModels(providerId, generation, signal, publication) {
409
+ const previous = this.publicationChains.get(providerId) ?? Promise.resolve();
410
+ const queued = (async () => {
411
+ await previous.catch(() => {});
412
+ if (signal.aborted || this.refreshGenerations.get(providerId) !== generation)
413
+ return false;
414
+ if (publication.persist === null) {
415
+ await this.modelsStore.delete(providerId, { signal });
416
+ } else if (publication.persist !== undefined) {
417
+ await this.modelsStore.write(providerId, structuredClone(publication.persist), { signal });
418
+ }
419
+ if (signal.aborted || this.refreshGenerations.get(providerId) !== generation)
420
+ return false;
421
+ publication.update?.();
422
+ return true;
423
+ })();
424
+ const tail = queued.catch(() => {});
425
+ this.publicationChains.set(providerId, tail);
426
+ tail.then(() => {
427
+ if (this.publicationChains.get(providerId) === tail)
428
+ this.publicationChains.delete(providerId);
429
+ });
430
+ return raceWithAbortSignal(queued, signal);
431
+ }
432
+ async runProviderRefreshPhase(provider, credential, allowNetwork, force, generation, signal) {
433
+ const stored = await this.modelsStore.read(provider.id, { signal });
434
+ await provider.refreshModels({
435
+ credential,
436
+ stored: stored ? structuredClone(stored) : undefined,
437
+ publish: (publication) => this.publishProviderModels(provider.id, generation, signal, publication),
438
+ allowNetwork,
439
+ force: allowNetwork ? force : undefined,
440
+ signal
441
+ });
442
+ }
299
443
  async refresh(options = {}) {
300
444
  const allowNetwork = options.allowNetwork ?? true;
445
+ const callerSignal = operationSignal(options.signal);
301
446
  const errors = new Map;
302
- const refreshable = Array.from(this.providers.values()).filter((provider) => provider.refreshModels !== undefined);
303
- await Promise.all(refreshable.map(async (provider) => {
304
- if (options.signal?.aborted)
305
- return;
306
- const store = {
307
- read: () => this.modelsStore.read(provider.id),
308
- write: (entry) => this.modelsStore.write(provider.id, entry),
309
- delete: () => this.modelsStore.delete(provider.id)
310
- };
311
- let stored;
312
- try {
313
- stored = await this.readCredential(provider.id);
314
- const credential = await this.resolveRefreshCredential(provider, stored, allowNetwork, options.signal);
447
+ if (callerSignal.aborted)
448
+ return { aborted: true, errors };
449
+ const selected = options.providers ? new Set(options.providers) : undefined;
450
+ const refreshable = Array.from(this.providers.values()).filter((provider) => provider.refreshModels !== undefined && (!selected || selected.has(provider.id)));
451
+ const refresh = Promise.all(refreshable.map(async (provider) => {
452
+ const { generation, controller } = this.beginProviderRefresh(provider.id);
453
+ const signal = AbortSignal.any([callerSignal, controller.signal]);
454
+ const operation = (async () => {
455
+ let storedCredential;
456
+ let credentialError;
457
+ try {
458
+ storedCredential = await this.readCredential(provider.id, signal);
459
+ } catch (error) {
460
+ credentialError = error;
461
+ }
462
+ await this.runProviderRefreshPhase(provider, storedCredential, false, undefined, generation, signal);
463
+ if (credentialError !== undefined)
464
+ throw credentialError;
465
+ if (!allowNetwork || signal.aborted)
466
+ return;
467
+ const credential = await this.resolveRefreshCredential(provider, storedCredential, signal);
315
468
  if (!credential)
316
469
  return;
317
- await provider.refreshModels({
318
- credential,
319
- store,
320
- allowNetwork,
321
- force: options.force,
322
- signal: options.signal
323
- });
470
+ await this.runProviderRefreshPhase(provider, credential, true, options.force, generation, signal);
471
+ })();
472
+ try {
473
+ await raceWithAbortSignal(operation, signal);
324
474
  } catch (error) {
325
- if (!options.signal?.aborted) {
475
+ if (!signal.aborted) {
326
476
  errors.set(provider.id, error instanceof Error ? error : new ModelsError("model_source", `Model refresh failed for ${provider.id}`, { cause: error }));
327
477
  }
328
- try {
329
- await provider.refreshModels({
330
- credential: stored,
331
- store,
332
- allowNetwork: false,
333
- signal: options.signal
334
- });
335
- } catch {}
478
+ } finally {
479
+ if (this.refreshControllers.get(provider.id) === controller) {
480
+ this.refreshControllers.delete(provider.id);
481
+ }
336
482
  }
337
483
  }));
338
- return { aborted: options.signal?.aborted ?? false, errors };
484
+ try {
485
+ await raceWithAbortSignal(refresh, callerSignal);
486
+ } catch (error) {
487
+ if (!callerSignal.aborted)
488
+ throw error;
489
+ }
490
+ return { aborted: callerSignal.aborted, errors: new Map(errors) };
339
491
  }
340
- async resolveRefreshCredential(provider, stored, allowNetwork, signal) {
492
+ async resolveRefreshCredential(provider, stored, signal) {
341
493
  if (stored?.type === "oauth") {
342
494
  const oauth = provider.auth.oauth;
343
495
  if (!oauth)
344
496
  return;
345
- if (!allowNetwork || Date.now() < stored.expires)
497
+ if (Date.now() < stored.expires)
346
498
  return stored;
347
- if (signal?.aborted)
499
+ if (signal.aborted)
348
500
  return;
349
501
  const post = await this.credentials.modify(provider.id, async (current) => {
350
502
  if (current?.type !== "oauth" || Date.now() < current.expires)
351
503
  return;
352
504
  return oauth.refresh(current, signal);
353
- });
505
+ }, { signal });
354
506
  return post?.type === "oauth" ? post : undefined;
355
507
  }
356
508
  const apiKey = provider.auth.apiKey;
357
509
  if (!apiKey)
358
510
  return;
359
511
  const credential = stored?.type === "api_key" ? stored : undefined;
360
- const result = await apiKey.resolve({ ctx: this.authContext, credential });
512
+ const result = await apiKey.resolve({ ctx: this.authContext, credential, signal });
361
513
  if (!result)
362
514
  return;
363
515
  return { type: "api_key", key: result.auth.apiKey, env: result.env };
364
516
  }
365
- async readCredential(providerId) {
517
+ async readCredential(providerId, signal) {
366
518
  try {
367
- return await this.credentials.read(providerId);
519
+ return await this.credentials.read(providerId, { signal });
368
520
  } catch (error) {
369
521
  throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error });
370
522
  }
371
523
  }
372
- async checkProviderAuth(provider, credential) {
524
+ async checkProviderAuth(provider, credential, signal) {
373
525
  if (credential?.type === "oauth") {
374
526
  return provider.auth.oauth ? { source: "OAuth", type: "oauth" } : undefined;
375
527
  }
@@ -380,40 +532,52 @@ class ModelsImpl {
380
532
  try {
381
533
  return await apiKey.check({
382
534
  ctx: this.authContext,
383
- credential: credential?.type === "api_key" ? credential : undefined
535
+ credential: credential?.type === "api_key" ? credential : undefined,
536
+ signal
384
537
  });
385
538
  } catch (error) {
386
539
  throw new ModelsError("auth", `API key auth check failed for provider ${provider.id}`, { cause: error });
387
540
  }
388
541
  }
389
- const resolution = await resolveProviderAuth(provider, this.credentials, this.authContext);
542
+ const resolution = await resolveProviderAuth(provider, this.credentials, this.authContext, { signal });
390
543
  return resolution ? { source: resolution.source, type: "api_key" } : undefined;
391
544
  }
392
- async checkAuth(providerId) {
393
- const provider = this.providers.get(providerId);
394
- if (!provider)
395
- return;
396
- return this.checkProviderAuth(provider, await this.readCredential(providerId));
397
- }
398
- async getAvailable(providerId) {
399
- const providers = providerId ? [this.providers.get(providerId)].filter((entry) => entry !== undefined) : this.getProviders();
400
- const checks = await Promise.all(providers.map(async (provider) => {
401
- const credential = await this.readCredential(provider.id);
402
- return { provider, credential, auth: await this.checkProviderAuth(provider, credential) };
403
- }));
404
- return checks.flatMap(({ provider, credential, auth }) => {
405
- if (!auth)
406
- return [];
407
- const models = provider.getModels();
408
- return provider.filterModels?.(models, credential) ?? models;
409
- });
545
+ checkAuth(providerId, options) {
546
+ const signal = operationSignal(options?.signal);
547
+ const check = (async () => {
548
+ signal.throwIfAborted();
549
+ const provider = this.providers.get(providerId);
550
+ if (!provider)
551
+ return;
552
+ return this.checkProviderAuth(provider, await this.readCredential(providerId, signal), signal);
553
+ })();
554
+ return raceWithAbortSignal(check, signal);
555
+ }
556
+ getAvailable(providerId, options) {
557
+ const signal = operationSignal(options?.signal);
558
+ const available = (async () => {
559
+ signal.throwIfAborted();
560
+ const providers = providerId ? [this.providers.get(providerId)].filter((entry) => entry !== undefined) : this.getProviders();
561
+ const checks = await Promise.all(providers.map(async (provider) => {
562
+ const credential = await this.readCredential(provider.id, signal);
563
+ return { provider, credential, auth: await this.checkProviderAuth(provider, credential, signal) };
564
+ }));
565
+ return checks.flatMap(({ provider, credential, auth }) => {
566
+ if (!auth)
567
+ return [];
568
+ const models = provider.getModels();
569
+ return provider.filterModels?.(models, credential) ?? models;
570
+ });
571
+ })();
572
+ return raceWithAbortSignal(available, signal);
410
573
  }
411
574
  async getAuth(providerOrModel, overrides) {
575
+ const signal = operationSignal(overrides?.signal);
412
576
  const providerId = typeof providerOrModel === "string" ? providerOrModel : providerOrModel.provider;
413
577
  const provider = this.providers.get(providerId);
414
578
  if (!provider)
415
579
  return;
416
- const result = await resolveProviderAuth(provider, this.credentials, this.authContext, overrides);
580
+ const result = await resolveProviderAuth(provider, this.credentials, this.authContext, { ...overrides, signal });
417
581
  if (!result || typeof providerOrModel === "string" || !providerOrModel.headers)
418
582
  return result;
419
583
  return {
@@ -425,6 +589,8 @@ class ModelsImpl {
425
589
  };
426
590
  }
427
591
  async login(providerId, type, interaction) {
592
+ const signal = operationSignal(interaction.signal);
593
+ signal.throwIfAborted();
428
594
  const provider = this.providers.get(providerId);
429
595
  if (!provider)
430
596
  throw new ModelsError("provider", `Unknown provider: ${providerId}`);
@@ -432,18 +598,50 @@ class ModelsImpl {
432
598
  if (!method?.login) {
433
599
  throw new ModelsError("auth", `${provider.name} does not support ${type} login`);
434
600
  }
435
- const credential = await method.login(interaction);
601
+ const loginOperation = method.login({ ...interaction, signal });
602
+ const credential = await raceWithAbortSignal(loginOperation, signal);
603
+ let mutationStarted = false;
604
+ let markMutationStarted;
605
+ const started = new Promise((resolve) => {
606
+ markMutationStarted = resolve;
607
+ });
608
+ const mutation = this.credentials.modify(providerId, async () => {
609
+ mutationStarted = true;
610
+ markMutationStarted?.();
611
+ return credential;
612
+ }, { signal });
613
+ mutation.catch(() => {});
436
614
  try {
437
- await this.credentials.modify(providerId, async () => credential);
615
+ await new Promise((resolve, reject) => {
616
+ const onAbort = () => {
617
+ if (!mutationStarted)
618
+ reject(signal.reason);
619
+ };
620
+ signal.addEventListener("abort", onAbort, { once: true });
621
+ Promise.race([started, mutation]).then(() => {
622
+ signal.removeEventListener("abort", onAbort);
623
+ resolve();
624
+ }, (error) => {
625
+ signal.removeEventListener("abort", onAbort);
626
+ reject(error);
627
+ });
628
+ if (signal.aborted)
629
+ onAbort();
630
+ });
631
+ await mutation;
438
632
  } catch (error) {
633
+ signal.throwIfAborted();
439
634
  throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error });
440
635
  }
441
636
  return credential;
442
637
  }
443
- async logout(providerId) {
638
+ async logout(providerId, options) {
639
+ const signal = operationSignal(options?.signal);
640
+ signal.throwIfAborted();
444
641
  try {
445
- await this.credentials.delete(providerId);
642
+ await this.credentials.delete(providerId, { signal });
446
643
  } catch (error) {
644
+ signal.throwIfAborted();
447
645
  throw new ModelsError("auth", `Credential store delete failed for ${providerId}`, { cause: error });
448
646
  }
449
647
  }
@@ -458,7 +656,8 @@ class ModelsImpl {
458
656
  this.requireProvider(model);
459
657
  const resolution = await this.getAuth(model, {
460
658
  apiKey: options?.apiKey,
461
- env: options?.env
659
+ env: options?.env,
660
+ signal: options?.signal
462
661
  });
463
662
  if (!resolution) {
464
663
  throw new ModelsError("auth", `Provider is not configured: ${model.provider}`);
@@ -494,6 +693,24 @@ class ModelsImpl {
494
693
  async completeSimple(model, context, options) {
495
694
  return this.streamSimple(model, context, options).result();
496
695
  }
696
+ async fetchDeferred(model, handle, options) {
697
+ return lazyStream(model, async () => {
698
+ const provider = this.requireProvider(model);
699
+ if (!provider.fetchDeferred) {
700
+ throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
701
+ }
702
+ const { requestModel, requestOptions } = await this.applyAuth(model, options);
703
+ return provider.fetchDeferred(requestModel, handle, requestOptions);
704
+ }).result();
705
+ }
706
+ async cancelDeferred(model, handle, options) {
707
+ const provider = this.requireProvider(model);
708
+ if (!provider.cancelDeferred) {
709
+ throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
710
+ }
711
+ const { requestModel, requestOptions } = await this.applyAuth(model, options);
712
+ await provider.cancelDeferred(requestModel, handle, requestOptions);
713
+ }
497
714
  }
498
715
  function createModels(options) {
499
716
  return new ModelsImpl(options);
@@ -501,7 +718,6 @@ function createModels(options) {
501
718
  function createProvider(input) {
502
719
  const baselineModels = input.models;
503
720
  let dynamicModels = [];
504
- let inflightRefresh;
505
721
  const fetchModels = input.fetchModels;
506
722
  const currentModels = () => {
507
723
  const merged = [...baselineModels];
@@ -518,45 +734,68 @@ function createProvider(input) {
518
734
  const byApi = single ? undefined : input.api;
519
735
  const apiFor = (model) => single ?? byApi?.[model.api];
520
736
  const dispatch = (model, run) => {
521
- const streams = apiFor(model);
522
- if (!streams) {
737
+ const streams2 = apiFor(model);
738
+ if (!streams2) {
523
739
  return lazyStream(model, async () => {
524
740
  throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`);
525
741
  });
526
742
  }
527
- return run(streams);
743
+ return run(streams2);
528
744
  };
529
- return {
745
+ const provider = {
530
746
  id: input.id,
531
747
  name: input.name ?? input.id,
532
748
  baseUrl: input.baseUrl,
533
749
  headers: input.headers,
534
750
  auth: input.auth,
535
751
  getModels: currentModels,
536
- refreshModels: fetchModels ? (context) => {
537
- inflightRefresh ??= (async () => {
538
- try {
539
- const stored = await context.store.read();
540
- if (stored) {
541
- dynamicModels = stored.models.filter((model) => model.provider === input.id).map((model) => model);
752
+ refreshModels: fetchModels ? async (context) => {
753
+ if (context.stored) {
754
+ const restored = context.stored.models.filter((model) => model.provider === input.id).map((model) => model);
755
+ if (!await context.publish({
756
+ update: () => {
757
+ dynamicModels = restored;
542
758
  }
543
- if (!context.allowNetwork || context.signal?.aborted)
544
- return;
545
- const refreshed = await fetchModels(context);
546
- if (context.signal?.aborted)
547
- return;
759
+ })) {
760
+ return;
761
+ }
762
+ }
763
+ if (!context.allowNetwork || context.signal.aborted)
764
+ return;
765
+ const refreshed = await fetchModels(context);
766
+ if (context.signal.aborted)
767
+ return;
768
+ await context.publish({
769
+ persist: { models: refreshed, checkedAt: Date.now() },
770
+ update: () => {
548
771
  dynamicModels = refreshed;
549
- await context.store.write({ models: refreshed, checkedAt: Date.now() });
550
- } finally {
551
- inflightRefresh = undefined;
552
772
  }
553
- })();
554
- return inflightRefresh;
773
+ });
555
774
  } : undefined,
556
775
  filterModels: input.filterModels,
557
- stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)),
558
- streamSimple: (model, context, options) => dispatch(model, (streams) => streams.streamSimple(model, context, options))
776
+ stream: (model, context, options) => dispatch(model, (streams2) => streams2.stream(model, context, options)),
777
+ streamSimple: (model, context, options) => dispatch(model, (streams2) => streams2.streamSimple(model, context, options))
559
778
  };
779
+ const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== undefined);
780
+ if (streams.some((entry) => entry.fetchDeferred !== undefined)) {
781
+ provider.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
782
+ const implementation = apiFor(model);
783
+ if (!implementation?.fetchDeferred) {
784
+ throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model.api}"`);
785
+ }
786
+ return implementation.fetchDeferred(model, handle, options);
787
+ });
788
+ }
789
+ if (streams.some((entry) => entry.cancelDeferred !== undefined)) {
790
+ provider.cancelDeferred = async (model, handle, options) => {
791
+ const implementation = apiFor(model);
792
+ if (!implementation?.cancelDeferred) {
793
+ throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model.api}"`);
794
+ }
795
+ await implementation.cancelDeferred(model, handle, options);
796
+ };
797
+ }
798
+ return provider;
560
799
  }
561
800
  function hasApi(model, api) {
562
801
  return model.api === api;