@fabric-harness/sdk 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/NOTICE +11 -0
  2. package/dist/agent-definition.d.ts +8 -0
  3. package/dist/agent-definition.d.ts.map +1 -1
  4. package/dist/agent-definition.js +27 -4
  5. package/dist/agent-definition.js.map +1 -1
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +3 -0
  8. package/dist/agent.js.map +1 -1
  9. package/dist/approval-notifier.d.ts +63 -0
  10. package/dist/approval-notifier.d.ts.map +1 -0
  11. package/dist/approval-notifier.js +152 -0
  12. package/dist/approval-notifier.js.map +1 -0
  13. package/dist/attachment-store.d.ts +12 -0
  14. package/dist/attachment-store.d.ts.map +1 -1
  15. package/dist/attachment-store.js +61 -3
  16. package/dist/attachment-store.js.map +1 -1
  17. package/dist/channel-contract.d.ts +29 -0
  18. package/dist/channel-contract.d.ts.map +1 -0
  19. package/dist/channel-contract.js +153 -0
  20. package/dist/channel-contract.js.map +1 -0
  21. package/dist/channel-runtime.d.ts +7 -0
  22. package/dist/channel-runtime.d.ts.map +1 -0
  23. package/dist/channel-runtime.js +6 -0
  24. package/dist/channel-runtime.js.map +1 -0
  25. package/dist/conversation-projection.d.ts.map +1 -1
  26. package/dist/conversation-projection.js +12 -5
  27. package/dist/conversation-projection.js.map +1 -1
  28. package/dist/conversation-stream.d.ts +3 -0
  29. package/dist/conversation-stream.d.ts.map +1 -1
  30. package/dist/conversation-stream.js +14 -0
  31. package/dist/conversation-stream.js.map +1 -1
  32. package/dist/cost-budget.d.ts +5 -0
  33. package/dist/cost-budget.d.ts.map +1 -1
  34. package/dist/cost-budget.js +20 -0
  35. package/dist/cost-budget.js.map +1 -1
  36. package/dist/error.d.ts +16 -4
  37. package/dist/error.d.ts.map +1 -1
  38. package/dist/error.js +30 -5
  39. package/dist/error.js.map +1 -1
  40. package/dist/filesystem.d.ts +3 -3
  41. package/dist/filesystem.d.ts.map +1 -1
  42. package/dist/in-memory-submission-store.d.ts +4 -0
  43. package/dist/in-memory-submission-store.d.ts.map +1 -1
  44. package/dist/in-memory-submission-store.js +45 -0
  45. package/dist/in-memory-submission-store.js.map +1 -1
  46. package/dist/index.d.ts +24 -9
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +11 -4
  49. package/dist/index.js.map +1 -1
  50. package/dist/job-invoke.d.ts +43 -0
  51. package/dist/job-invoke.d.ts.map +1 -0
  52. package/dist/job-invoke.js +56 -0
  53. package/dist/job-invoke.js.map +1 -0
  54. package/dist/mcp.d.ts +53 -0
  55. package/dist/mcp.d.ts.map +1 -1
  56. package/dist/mcp.js +145 -20
  57. package/dist/mcp.js.map +1 -1
  58. package/dist/model.d.ts +22 -8
  59. package/dist/model.d.ts.map +1 -1
  60. package/dist/model.js +465 -230
  61. package/dist/model.js.map +1 -1
  62. package/dist/network-enforcement.d.ts +16 -0
  63. package/dist/network-enforcement.d.ts.map +1 -0
  64. package/dist/network-enforcement.js +36 -0
  65. package/dist/network-enforcement.js.map +1 -0
  66. package/dist/observability.d.ts +36 -0
  67. package/dist/observability.d.ts.map +1 -0
  68. package/dist/observability.js +89 -0
  69. package/dist/observability.js.map +1 -0
  70. package/dist/operational-metrics.d.ts +53 -0
  71. package/dist/operational-metrics.d.ts.map +1 -0
  72. package/dist/operational-metrics.js +119 -0
  73. package/dist/operational-metrics.js.map +1 -0
  74. package/dist/otel-observer.d.ts +3 -0
  75. package/dist/otel-observer.d.ts.map +1 -1
  76. package/dist/otel-observer.js +21 -9
  77. package/dist/otel-observer.js.map +1 -1
  78. package/dist/persistence-bundle-contract.d.ts +7 -0
  79. package/dist/persistence-bundle-contract.d.ts.map +1 -0
  80. package/dist/persistence-bundle-contract.js +86 -0
  81. package/dist/persistence-bundle-contract.js.map +1 -0
  82. package/dist/persistence-bundle.d.ts +56 -0
  83. package/dist/persistence-bundle.d.ts.map +1 -0
  84. package/dist/persistence-bundle.js +2 -0
  85. package/dist/persistence-bundle.js.map +1 -0
  86. package/dist/persistent-agent.d.ts +1 -1
  87. package/dist/persistent-agent.js +1 -1
  88. package/dist/policied-sandbox.d.ts +3 -0
  89. package/dist/policied-sandbox.d.ts.map +1 -1
  90. package/dist/policied-sandbox.js +25 -16
  91. package/dist/policied-sandbox.js.map +1 -1
  92. package/dist/policy.d.ts +8 -1
  93. package/dist/policy.d.ts.map +1 -1
  94. package/dist/policy.js +104 -19
  95. package/dist/policy.js.map +1 -1
  96. package/dist/redaction.js +1 -1
  97. package/dist/redaction.js.map +1 -1
  98. package/dist/sandbox-ref.d.ts +37 -2
  99. package/dist/sandbox-ref.d.ts.map +1 -1
  100. package/dist/sandbox-ref.js +164 -3
  101. package/dist/sandbox-ref.js.map +1 -1
  102. package/dist/sandbox.d.ts +26 -0
  103. package/dist/sandbox.d.ts.map +1 -1
  104. package/dist/sandbox.js +67 -19
  105. package/dist/sandbox.js.map +1 -1
  106. package/dist/secret-provider.d.ts +24 -0
  107. package/dist/secret-provider.d.ts.map +1 -0
  108. package/dist/secret-provider.js +36 -0
  109. package/dist/secret-provider.js.map +1 -0
  110. package/dist/session-submission-executor.d.ts +15 -0
  111. package/dist/session-submission-executor.d.ts.map +1 -0
  112. package/dist/session-submission-executor.js +79 -0
  113. package/dist/session-submission-executor.js.map +1 -0
  114. package/dist/session.d.ts +1 -1
  115. package/dist/session.d.ts.map +1 -1
  116. package/dist/session.js +24 -10
  117. package/dist/session.js.map +1 -1
  118. package/dist/store.d.ts +3 -0
  119. package/dist/store.d.ts.map +1 -1
  120. package/dist/store.js +28 -0
  121. package/dist/store.js.map +1 -1
  122. package/dist/strict.d.ts +20 -7
  123. package/dist/strict.d.ts.map +1 -1
  124. package/dist/strict.js +9 -3
  125. package/dist/strict.js.map +1 -1
  126. package/dist/submission-context.d.ts +5 -0
  127. package/dist/submission-context.d.ts.map +1 -1
  128. package/dist/submission-context.js.map +1 -1
  129. package/dist/submission-runner.d.ts +4 -0
  130. package/dist/submission-runner.d.ts.map +1 -1
  131. package/dist/submission-runner.js +13 -4
  132. package/dist/submission-runner.js.map +1 -1
  133. package/dist/submission-store.d.ts +6 -0
  134. package/dist/submission-store.d.ts.map +1 -1
  135. package/dist/submission-store.js.map +1 -1
  136. package/dist/testing-contracts.d.ts +4 -0
  137. package/dist/testing-contracts.d.ts.map +1 -1
  138. package/dist/testing-contracts.js +2 -0
  139. package/dist/testing-contracts.js.map +1 -1
  140. package/dist/tools.d.ts +4 -0
  141. package/dist/tools.d.ts.map +1 -1
  142. package/dist/tools.js.map +1 -1
  143. package/dist/types.d.ts +4 -2
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/dist/virtual-sandbox.d.ts.map +1 -1
  147. package/dist/virtual-sandbox.js +9 -2
  148. package/dist/virtual-sandbox.js.map +1 -1
  149. package/package.json +12 -6
  150. package/dist/lite.d.ts +0 -19
  151. package/dist/lite.d.ts.map +0 -1
  152. package/dist/lite.js +0 -19
  153. package/dist/lite.js.map +0 -1
package/dist/model.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createHash, createHmac } from 'node:crypto';
2
- import { applyEstimatedCost } from './model-pricing.js';
1
+ import { createHash, createHmac } from "node:crypto";
2
+ import { applyEstimatedCost } from "./model-pricing.js";
3
3
  export function toolsToModelSchemas(tools) {
4
4
  return [...tools].map((tool) => ({
5
5
  name: tool.name,
@@ -18,7 +18,7 @@ export async function generateWithRuntime(provider, request, options = {}) {
18
18
  // a pending backoff without tearing down an in-flight model call.
19
19
  const retryAbortController = new AbortController();
20
20
  const onCallerAbort = () => retryAbortController.abort(options.signal?.reason);
21
- options.signal?.addEventListener('abort', onCallerAbort, { once: true });
21
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
22
22
  try {
23
23
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
24
24
  if (options.signal?.aborted) {
@@ -30,18 +30,24 @@ export async function generateWithRuntime(provider, request, options = {}) {
30
30
  ? setTimeout(() => controller.abort(new Error(`Model call timed out after ${options.timeoutMs}ms`)), options.timeoutMs)
31
31
  : undefined;
32
32
  const abortListener = () => controller.abort(options.signal?.reason);
33
- options.signal?.addEventListener('abort', abortListener, { once: true });
34
- await options.onAttempt?.({ provider: provider.name, attempt, maxAttempts, status: 'start', ...(request.model !== undefined ? { model: request.model } : {}) });
33
+ options.signal?.addEventListener("abort", abortListener, { once: true });
34
+ await options.onAttempt?.({
35
+ provider: provider.name,
36
+ attempt,
37
+ maxAttempts,
38
+ status: "start",
39
+ ...(request.model !== undefined ? { model: request.model } : {}),
40
+ });
35
41
  try {
36
42
  const response = await provider.generate({ ...request, signal: controller.signal });
37
43
  if (timeout)
38
44
  clearTimeout(timeout);
39
- options.signal?.removeEventListener('abort', abortListener);
45
+ options.signal?.removeEventListener("abort", abortListener);
40
46
  await options.onAttempt?.({
41
47
  provider: provider.name,
42
48
  attempt,
43
49
  maxAttempts,
44
- status: 'success',
50
+ status: "success",
45
51
  durationMs: Date.now() - startedAt,
46
52
  ...(request.model !== undefined ? { model: request.model } : {}),
47
53
  ...(response.usage !== undefined ? { usage: response.usage } : {}),
@@ -51,7 +57,7 @@ export async function generateWithRuntime(provider, request, options = {}) {
51
57
  catch (error) {
52
58
  if (timeout)
53
59
  clearTimeout(timeout);
54
- options.signal?.removeEventListener('abort', abortListener);
60
+ options.signal?.removeEventListener("abort", abortListener);
55
61
  lastError = error;
56
62
  const message = error instanceof Error ? error.message : String(error);
57
63
  const canRetry = attempt < maxAttempts && !options.signal?.aborted && isRetryableModelError(error);
@@ -59,7 +65,7 @@ export async function generateWithRuntime(provider, request, options = {}) {
59
65
  provider: provider.name,
60
66
  attempt,
61
67
  maxAttempts,
62
- status: canRetry ? 'retry' : 'error',
68
+ status: canRetry ? "retry" : "error",
63
69
  durationMs: Date.now() - startedAt,
64
70
  ...(request.model !== undefined ? { model: request.model } : {}),
65
71
  error: message,
@@ -77,26 +83,29 @@ export async function generateWithRuntime(provider, request, options = {}) {
77
83
  }
78
84
  }
79
85
  finally {
80
- options.signal?.removeEventListener('abort', onCallerAbort);
86
+ options.signal?.removeEventListener("abort", onCallerAbort);
81
87
  }
82
88
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
83
89
  }
84
90
  export class MockModelProvider {
85
- name = 'mock';
91
+ name = "mock";
86
92
  async generate(request) {
87
- const latest = [...request.messages].reverse().find((message) => message.role === 'user');
88
- const text = latest?.content ?? '';
89
- const hasToolResults = request.messages.some((message) => message.role === 'tool');
93
+ const latest = [...request.messages].reverse().find((message) => message.role === "user");
94
+ const text = latest?.content ?? "";
95
+ const hasToolResults = request.messages.some((message) => message.role === "tool");
90
96
  const scripted = hasToolResults ? [] : parseScriptedToolCalls(text);
91
97
  if (scripted.length > 0)
92
98
  return { toolCalls: scripted, usage: estimateUsage(request.messages) };
93
99
  const toolSummaries = request.messages
94
- .filter((message) => message.role === 'tool')
95
- .map((message) => `${message.name ?? 'tool'}: ${message.content}`);
100
+ .filter((message) => message.role === "tool")
101
+ .map((message) => `${message.name ?? "tool"}: ${message.content}`);
96
102
  const content = toolSummaries.length > 0
97
- ? `Mock response using tool results:\n${toolSummaries.join('\n')}`
103
+ ? `Mock response using tool results:\n${toolSummaries.join("\n")}`
98
104
  : `Mock response: ${text}`;
99
- return { message: { role: 'assistant', content }, usage: estimateUsage([...request.messages, { role: 'assistant', content }]) };
105
+ return {
106
+ message: { role: "assistant", content },
107
+ usage: estimateUsage([...request.messages, { role: "assistant", content }]),
108
+ };
100
109
  }
101
110
  }
102
111
  // ─── Reasoning effort ────────────────────────────────────────────────────────
@@ -116,53 +125,53 @@ const REASONING_MODEL_PATTERNS = [
116
125
  ];
117
126
  /** Whether a model id (with or without provider prefix) is a known reasoning model. */
118
127
  export function modelSupportsReasoning(model) {
119
- const id = model.includes('/') ? model.slice(model.lastIndexOf('/') + 1) : model;
128
+ const id = model.includes("/") ? model.slice(model.lastIndexOf("/") + 1) : model;
120
129
  return REASONING_MODEL_PATTERNS.some((re) => re.test(id));
121
130
  }
122
131
  /** True when a reasoning control should be applied for this model + requested level. */
123
132
  function shouldApplyReasoning(model, level) {
124
- return level !== undefined && level !== 'off' && modelSupportsReasoning(model);
133
+ return level !== undefined && level !== "off" && modelSupportsReasoning(model);
125
134
  }
126
135
  /** OpenAI / Workers-AI style `reasoning_effort` value. */
127
136
  function openAIReasoningEffort(level) {
128
137
  switch (level) {
129
- case 'minimal':
130
- case 'low':
131
- return 'low';
132
- case 'medium':
133
- return 'medium';
134
- case 'high':
135
- case 'xhigh':
136
- return 'high';
138
+ case "minimal":
139
+ case "low":
140
+ return "low";
141
+ case "medium":
142
+ return "medium";
143
+ case "high":
144
+ case "xhigh":
145
+ return "high";
137
146
  }
138
147
  }
139
148
  /** Anthropic extended-thinking token budget (>= 1024; `max_tokens` must exceed it). */
140
149
  function anthropicThinkingBudget(level) {
141
150
  switch (level) {
142
- case 'minimal':
151
+ case "minimal":
143
152
  return 1024;
144
- case 'low':
153
+ case "low":
145
154
  return 2048;
146
- case 'medium':
155
+ case "medium":
147
156
  return 4096;
148
- case 'high':
157
+ case "high":
149
158
  return 8192;
150
- case 'xhigh':
159
+ case "xhigh":
151
160
  return 16384;
152
161
  }
153
162
  }
154
163
  /** Gemini `thinkingConfig.thinkingBudget` token count. */
155
164
  function geminiThinkingBudget(level) {
156
165
  switch (level) {
157
- case 'minimal':
166
+ case "minimal":
158
167
  return 512;
159
- case 'low':
168
+ case "low":
160
169
  return 1024;
161
- case 'medium':
170
+ case "medium":
162
171
  return 4096;
163
- case 'high':
172
+ case "high":
164
173
  return 12288;
165
- case 'xhigh':
174
+ case "xhigh":
166
175
  return 24576;
167
176
  }
168
177
  }
@@ -175,9 +184,10 @@ export class OpenAICompatibleModelProvider {
175
184
  headers;
176
185
  rateLimiter;
177
186
  rateLimitKey;
187
+ fetchImpl;
178
188
  constructor(options) {
179
- this.name = options.name ?? 'openai-compatible';
180
- this.baseUrl = options.baseUrl.replace(/\/$/, '');
189
+ this.name = options.name ?? "openai-compatible";
190
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
181
191
  this.apiKey = options.apiKey;
182
192
  this.tokenProvider = options.tokenProvider;
183
193
  if (options.defaultModel !== undefined)
@@ -185,11 +195,16 @@ export class OpenAICompatibleModelProvider {
185
195
  this.headers = options.headers ?? {};
186
196
  this.rateLimiter = options.rateLimiter;
187
197
  this.rateLimitKey = `${this.name}:${rateLimitKeyForApiKey(this.apiKey)}`;
198
+ this.fetchImpl = options.fetchImpl ?? fetch;
188
199
  }
189
200
  /** Resolves the bearer token for a request: the rotating `tokenProvider` if set, else `apiKey`. */
190
201
  async authToken() {
191
202
  return this.tokenProvider ? await this.tokenProvider() : this.apiKey;
192
203
  }
204
+ /** Resolve dynamic headers per attempt so request-scoped metadata never becomes stale. */
205
+ async requestHeaders() {
206
+ return typeof this.headers === "function" ? await this.headers() : this.headers;
207
+ }
193
208
  async generate(request) {
194
209
  const model = request.model ?? this.defaultModel;
195
210
  if (!model)
@@ -197,22 +212,25 @@ export class OpenAICompatibleModelProvider {
197
212
  if (this.rateLimiter)
198
213
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
199
214
  const token = await this.authToken();
215
+ const headers = await this.requestHeaders();
200
216
  const init = {
201
- method: 'POST',
217
+ method: "POST",
202
218
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
203
219
  headers: {
204
- 'content-type': 'application/json',
220
+ "content-type": "application/json",
205
221
  authorization: `Bearer ${token}`,
206
- ...this.headers,
222
+ ...headers,
207
223
  },
208
224
  body: JSON.stringify({
209
225
  model,
210
226
  messages: request.messages.map(toOpenAIMessage),
211
227
  ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
212
- ...(shouldApplyReasoning(model, request.thinkingLevel) ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) } : {}),
228
+ ...(shouldApplyReasoning(model, request.thinkingLevel)
229
+ ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) }
230
+ : {}),
213
231
  }),
214
232
  };
215
- const response = await fetch(`${this.baseUrl}/chat/completions`, init);
233
+ const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, init);
216
234
  if (!response.ok) {
217
235
  const body = await response.text();
218
236
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, body, response.headers);
@@ -236,14 +254,15 @@ export class OpenAICompatibleModelProvider {
236
254
  if (this.rateLimiter)
237
255
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
238
256
  const token = await this.authToken();
257
+ const headers = await this.requestHeaders();
239
258
  const init = {
240
- method: 'POST',
259
+ method: "POST",
241
260
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
242
261
  headers: {
243
- 'content-type': 'application/json',
262
+ "content-type": "application/json",
244
263
  authorization: `Bearer ${token}`,
245
- accept: 'text/event-stream',
246
- ...this.headers,
264
+ accept: "text/event-stream",
265
+ ...headers,
247
266
  },
248
267
  body: JSON.stringify({
249
268
  model,
@@ -251,10 +270,12 @@ export class OpenAICompatibleModelProvider {
251
270
  stream_options: { include_usage: true },
252
271
  messages: request.messages.map(toOpenAIMessage),
253
272
  ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
254
- ...(shouldApplyReasoning(model, request.thinkingLevel) ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) } : {}),
273
+ ...(shouldApplyReasoning(model, request.thinkingLevel)
274
+ ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) }
275
+ : {}),
255
276
  }),
256
277
  };
257
- const response = await fetch(`${this.baseUrl}/chat/completions`, init);
278
+ const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, init);
258
279
  if (!response.ok || !response.body) {
259
280
  const body = await response.text();
260
281
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, body, response.headers);
@@ -264,15 +285,15 @@ export class OpenAICompatibleModelProvider {
264
285
  const toolCallBuffers = new Map();
265
286
  let usage;
266
287
  for await (const event of decodeOpenAISSE(response.body)) {
267
- if (event === '[DONE]')
288
+ if (event === "[DONE]")
268
289
  break;
269
290
  const json = safeJsonParse(event);
270
291
  if (!json)
271
292
  continue;
272
293
  const choice = json.choices?.[0];
273
294
  const delta = choice?.delta;
274
- const contentDelta = typeof delta?.content === 'string' ? delta.content : '';
275
- const reasoningDelta = typeof delta?.reasoning_content === 'string' ? delta.reasoning_content : '';
295
+ const contentDelta = typeof delta?.content === "string" ? delta.content : "";
296
+ const reasoningDelta = typeof delta?.reasoning_content === "string" ? delta.reasoning_content : "";
276
297
  if (contentDelta) {
277
298
  text.push(contentDelta);
278
299
  yield { textDelta: contentDelta };
@@ -284,14 +305,14 @@ export class OpenAICompatibleModelProvider {
284
305
  const toolCalls = delta?.tool_calls;
285
306
  if (toolCalls) {
286
307
  for (const tc of toolCalls) {
287
- const idx = typeof tc.index === 'number' ? tc.index : 0;
288
- const buf = toolCallBuffers.get(idx) ?? { args: '' };
289
- if (typeof tc.id === 'string')
308
+ const idx = typeof tc.index === "number" ? tc.index : 0;
309
+ const buf = toolCallBuffers.get(idx) ?? { args: "" };
310
+ if (typeof tc.id === "string")
290
311
  buf.id = tc.id;
291
312
  const fn = tc.function;
292
313
  if (fn?.name)
293
314
  buf.name = fn.name;
294
- if (typeof fn?.arguments === 'string')
315
+ if (typeof fn?.arguments === "string")
295
316
  buf.args += fn.arguments;
296
317
  toolCallBuffers.set(idx, buf);
297
318
  }
@@ -299,22 +320,22 @@ export class OpenAICompatibleModelProvider {
299
320
  const u = json.usage;
300
321
  if (u) {
301
322
  usage = {};
302
- if (typeof u.prompt_tokens === 'number')
323
+ if (typeof u.prompt_tokens === "number")
303
324
  usage.inputTokens = u.prompt_tokens;
304
- if (typeof u.completion_tokens === 'number')
325
+ if (typeof u.completion_tokens === "number")
305
326
  usage.outputTokens = u.completion_tokens;
306
- if (typeof u.total_tokens === 'number')
327
+ if (typeof u.total_tokens === "number")
307
328
  usage.totalTokens = u.total_tokens;
308
329
  }
309
330
  }
310
331
  const done = {
311
- ...(text.length ? { message: { role: 'assistant', content: text.join('') } } : {}),
312
- ...(thinking.length ? { thinking: thinking.join('') } : {}),
332
+ ...(text.length ? { message: { role: "assistant", content: text.join("") } } : {}),
333
+ ...(thinking.length ? { thinking: thinking.join("") } : {}),
313
334
  ...(toolCallBuffers.size > 0
314
335
  ? {
315
336
  toolCalls: [...toolCallBuffers.values()].map((buf, index) => ({
316
337
  id: buf.id ?? `openai-stream-${index + 1}`,
317
- name: buf.name ?? '',
338
+ name: buf.name ?? "",
318
339
  input: buf.args ? JSON.parse(buf.args) : {},
319
340
  })),
320
341
  }
@@ -334,23 +355,26 @@ export class AzureOpenAIModelProvider {
334
355
  apiVersion;
335
356
  headers;
336
357
  constructor(options) {
337
- this.name = options.name ?? 'azure-openai';
338
- this.baseUrl = options.baseUrl.replace(/\/$/, '');
358
+ this.name = options.name ?? "azure-openai";
359
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
339
360
  this.apiKey = options.apiKey;
340
361
  this.deployment = options.deployment;
341
- this.apiVersion = options.apiVersion ?? '2024-10-21';
362
+ this.apiVersion = options.apiVersion ?? "2024-10-21";
342
363
  this.headers = options.headers ?? {};
343
364
  }
344
365
  async generate(request) {
345
366
  const response = await fetch(`${this.baseUrl}/openai/deployments/${encodeURIComponent(this.deployment)}/chat/completions?api-version=${encodeURIComponent(this.apiVersion)}`, {
346
- method: 'POST',
367
+ method: "POST",
347
368
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
348
- headers: { 'content-type': 'application/json', 'api-key': this.apiKey, ...this.headers },
349
- body: JSON.stringify({ messages: request.messages.map(toOpenAIMessage), ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}) }),
369
+ headers: { "content-type": "application/json", "api-key": this.apiKey, ...this.headers },
370
+ body: JSON.stringify({
371
+ messages: request.messages.map(toOpenAIMessage),
372
+ ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
373
+ }),
350
374
  });
351
375
  if (!response.ok)
352
376
  throw formatProviderHttpError(this.name, this.deployment, response.status, response.statusText, await response.text(), response.headers);
353
- const result = openAIChatCompletionToModelResponse(await response.json());
377
+ const result = openAIChatCompletionToModelResponse((await response.json()));
354
378
  applyEstimatedCost(`azure-openai/${this.deployment}`, result.usage);
355
379
  return result;
356
380
  }
@@ -362,8 +386,8 @@ export class GeminiModelProvider {
362
386
  defaultModel;
363
387
  headers;
364
388
  constructor(options) {
365
- this.name = options.name ?? 'gemini';
366
- this.baseUrl = (options.baseUrl ?? 'https://generativelanguage.googleapis.com/v1beta').replace(/\/$/, '');
389
+ this.name = options.name ?? "gemini";
390
+ this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/$/, "");
367
391
  this.apiKey = options.apiKey;
368
392
  if (options.defaultModel !== undefined)
369
393
  this.defaultModel = options.defaultModel;
@@ -374,14 +398,14 @@ export class GeminiModelProvider {
374
398
  if (!model)
375
399
  throw new Error(`${this.name}: model is required.`);
376
400
  const response = await fetch(`${this.baseUrl}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(this.apiKey)}`, {
377
- method: 'POST',
401
+ method: "POST",
378
402
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
379
- headers: { 'content-type': 'application/json', ...this.headers },
403
+ headers: { "content-type": "application/json", ...this.headers },
380
404
  body: JSON.stringify(toGeminiRequest(request, model)),
381
405
  });
382
406
  if (!response.ok)
383
407
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
384
- const result = geminiResponseToModelResponse(await response.json());
408
+ const result = geminiResponseToModelResponse((await response.json()));
385
409
  applyEstimatedCost(`gemini/${model}`, result.usage);
386
410
  return result;
387
411
  }
@@ -394,9 +418,9 @@ export class VertexAIModelProvider {
394
418
  defaultModel;
395
419
  headers;
396
420
  constructor(options) {
397
- this.name = options.name ?? 'vertex-ai';
421
+ this.name = options.name ?? "vertex-ai";
398
422
  this.project = options.project;
399
- this.location = options.location ?? 'us-central1';
423
+ this.location = options.location ?? "us-central1";
400
424
  this.accessToken = options.accessToken;
401
425
  if (options.defaultModel !== undefined)
402
426
  this.defaultModel = options.defaultModel;
@@ -408,14 +432,18 @@ export class VertexAIModelProvider {
408
432
  throw new Error(`${this.name}: model is required.`);
409
433
  const url = `https://${this.location}-aiplatform.googleapis.com/v1/projects/${encodeURIComponent(this.project)}/locations/${encodeURIComponent(this.location)}/publishers/google/models/${encodeURIComponent(model)}:generateContent`;
410
434
  const response = await fetch(url, {
411
- method: 'POST',
435
+ method: "POST",
412
436
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
413
- headers: { 'content-type': 'application/json', authorization: `Bearer ${this.accessToken}`, ...this.headers },
437
+ headers: {
438
+ "content-type": "application/json",
439
+ authorization: `Bearer ${this.accessToken}`,
440
+ ...this.headers,
441
+ },
414
442
  body: JSON.stringify(toGeminiRequest(request, model)),
415
443
  });
416
444
  if (!response.ok)
417
445
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
418
- const result = geminiResponseToModelResponse(await response.json());
446
+ const result = geminiResponseToModelResponse((await response.json()));
419
447
  applyEstimatedCost(`gemini/${model}`, result.usage);
420
448
  return result;
421
449
  }
@@ -429,7 +457,7 @@ export class BedrockModelProvider {
429
457
  defaultModel;
430
458
  headers;
431
459
  constructor(options) {
432
- this.name = options.name ?? 'bedrock';
460
+ this.name = options.name ?? "bedrock";
433
461
  this.region = options.region;
434
462
  this.accessKeyId = options.accessKeyId;
435
463
  this.secretAccessKey = options.secretAccessKey;
@@ -447,21 +475,26 @@ export class BedrockModelProvider {
447
475
  const pathName = `/model/${encodeURIComponent(model)}/converse`;
448
476
  const body = JSON.stringify(toBedrockRequest(request));
449
477
  const headers = signAwsRequest({
450
- method: 'POST',
478
+ method: "POST",
451
479
  host,
452
480
  path: pathName,
453
481
  region: this.region,
454
- service: 'bedrock',
482
+ service: "bedrock",
455
483
  accessKeyId: this.accessKeyId,
456
484
  secretAccessKey: this.secretAccessKey,
457
485
  ...(this.sessionToken ? { sessionToken: this.sessionToken } : {}),
458
486
  body,
459
- headers: { 'content-type': 'application/json', ...this.headers },
487
+ headers: { "content-type": "application/json", ...this.headers },
488
+ });
489
+ const response = await fetch(`https://${host}${pathName}`, {
490
+ method: "POST",
491
+ ...(request.signal !== undefined ? { signal: request.signal } : {}),
492
+ headers,
493
+ body,
460
494
  });
461
- const response = await fetch(`https://${host}${pathName}`, { method: 'POST', ...(request.signal !== undefined ? { signal: request.signal } : {}), headers, body });
462
495
  if (!response.ok)
463
496
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
464
- const result = bedrockResponseToModelResponse(await response.json());
497
+ const result = bedrockResponseToModelResponse((await response.json()));
465
498
  applyEstimatedCost(`bedrock/${model}`, result.usage);
466
499
  return result;
467
500
  }
@@ -473,8 +506,8 @@ export class CohereModelProvider {
473
506
  defaultModel;
474
507
  headers;
475
508
  constructor(options) {
476
- this.name = options.name ?? 'cohere';
477
- this.baseUrl = (options.baseUrl ?? 'https://api.cohere.com/v2').replace(/\/$/, '');
509
+ this.name = options.name ?? "cohere";
510
+ this.baseUrl = (options.baseUrl ?? "https://api.cohere.com/v2").replace(/\/$/, "");
478
511
  this.apiKey = options.apiKey;
479
512
  if (options.defaultModel !== undefined)
480
513
  this.defaultModel = options.defaultModel;
@@ -485,14 +518,18 @@ export class CohereModelProvider {
485
518
  if (!model)
486
519
  throw new Error(`${this.name}: model is required.`);
487
520
  const response = await fetch(`${this.baseUrl}/chat`, {
488
- method: 'POST',
521
+ method: "POST",
489
522
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
490
- headers: { 'content-type': 'application/json', authorization: `Bearer ${this.apiKey}`, ...this.headers },
523
+ headers: {
524
+ "content-type": "application/json",
525
+ authorization: `Bearer ${this.apiKey}`,
526
+ ...this.headers,
527
+ },
491
528
  body: JSON.stringify({ model, messages: request.messages.map(toCohereMessage) }),
492
529
  });
493
530
  if (!response.ok)
494
531
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
495
- const result = cohereResponseToModelResponse(await response.json());
532
+ const result = cohereResponseToModelResponse((await response.json()));
496
533
  applyEstimatedCost(`cohere/${model}`, result.usage);
497
534
  return result;
498
535
  }
@@ -501,9 +538,9 @@ export class FallbackModelProvider {
501
538
  name;
502
539
  providers;
503
540
  constructor(options) {
504
- this.name = options.name ?? 'fallback';
541
+ this.name = options.name ?? "fallback";
505
542
  if (options.providers.length === 0)
506
- throw new Error('FallbackModelProvider requires at least one provider.');
543
+ throw new Error("FallbackModelProvider requires at least one provider.");
507
544
  this.providers = options.providers;
508
545
  }
509
546
  async generate(request) {
@@ -516,7 +553,7 @@ export class FallbackModelProvider {
516
553
  errors.push(`${provider.name}: ${error instanceof Error ? error.message : String(error)}`);
517
554
  }
518
555
  }
519
- throw new Error(`${this.name}: all fallback providers failed. ${errors.join(' | ')}`);
556
+ throw new Error(`${this.name}: all fallback providers failed. ${errors.join(" | ")}`);
520
557
  }
521
558
  }
522
559
  export class AnthropicModelProvider {
@@ -529,8 +566,8 @@ export class AnthropicModelProvider {
529
566
  rateLimiter;
530
567
  rateLimitKey;
531
568
  constructor(options) {
532
- this.name = options.name ?? 'anthropic';
533
- this.baseUrl = (options.baseUrl ?? 'https://api.anthropic.com/v1').replace(/\/$/, '');
569
+ this.name = options.name ?? "anthropic";
570
+ this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
534
571
  this.apiKey = options.apiKey;
535
572
  if (options.defaultModel !== undefined)
536
573
  this.defaultModel = options.defaultModel;
@@ -546,17 +583,35 @@ export class AnthropicModelProvider {
546
583
  if (this.rateLimiter)
547
584
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
548
585
  const { system, messages } = toAnthropicMessages(request.messages);
549
- const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? anthropicThinkingBudget(request.thinkingLevel) : undefined;
550
- const maxTokens = thinkingBudget !== undefined ? Math.max(this.maxTokens, thinkingBudget + 1024) : this.maxTokens;
586
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel)
587
+ ? anthropicThinkingBudget(request.thinkingLevel)
588
+ : undefined;
589
+ const maxTokens = thinkingBudget !== undefined
590
+ ? Math.max(this.maxTokens, thinkingBudget + 1024)
591
+ : this.maxTokens;
551
592
  const response = await fetch(`${this.baseUrl}/messages`, {
552
- method: 'POST',
593
+ method: "POST",
553
594
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
554
- headers: { 'content-type': 'application/json', 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01', ...this.headers },
555
- body: JSON.stringify({ model, max_tokens: maxTokens, ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}), ...(system ? { system } : {}), messages, ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}) }),
595
+ headers: {
596
+ "content-type": "application/json",
597
+ "x-api-key": this.apiKey,
598
+ "anthropic-version": "2023-06-01",
599
+ ...this.headers,
600
+ },
601
+ body: JSON.stringify({
602
+ model,
603
+ max_tokens: maxTokens,
604
+ ...(thinkingBudget !== undefined
605
+ ? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
606
+ : {}),
607
+ ...(system ? { system } : {}),
608
+ messages,
609
+ ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}),
610
+ }),
556
611
  });
557
612
  if (!response.ok)
558
613
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
559
- const result = anthropicResponseToModelResponse(await response.json());
614
+ const result = anthropicResponseToModelResponse((await response.json()));
560
615
  applyEstimatedCost(`anthropic/${model}`, result.usage);
561
616
  return result;
562
617
  }
@@ -567,23 +622,29 @@ export class AnthropicModelProvider {
567
622
  if (this.rateLimiter)
568
623
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
569
624
  const { system, messages } = toAnthropicMessages(request.messages);
570
- const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? anthropicThinkingBudget(request.thinkingLevel) : undefined;
571
- const maxTokens = thinkingBudget !== undefined ? Math.max(this.maxTokens, thinkingBudget + 1024) : this.maxTokens;
625
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel)
626
+ ? anthropicThinkingBudget(request.thinkingLevel)
627
+ : undefined;
628
+ const maxTokens = thinkingBudget !== undefined
629
+ ? Math.max(this.maxTokens, thinkingBudget + 1024)
630
+ : this.maxTokens;
572
631
  const response = await fetch(`${this.baseUrl}/messages`, {
573
- method: 'POST',
632
+ method: "POST",
574
633
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
575
634
  headers: {
576
- 'content-type': 'application/json',
577
- 'x-api-key': this.apiKey,
578
- 'anthropic-version': '2023-06-01',
579
- accept: 'text/event-stream',
635
+ "content-type": "application/json",
636
+ "x-api-key": this.apiKey,
637
+ "anthropic-version": "2023-06-01",
638
+ accept: "text/event-stream",
580
639
  ...this.headers,
581
640
  },
582
641
  body: JSON.stringify({
583
642
  model,
584
643
  stream: true,
585
644
  max_tokens: maxTokens,
586
- ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}),
645
+ ...(thinkingBudget !== undefined
646
+ ? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
647
+ : {}),
587
648
  ...(system ? { system } : {}),
588
649
  messages,
589
650
  ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}),
@@ -596,79 +657,79 @@ export class AnthropicModelProvider {
596
657
  const toolCallBuffers = new Map();
597
658
  let usage;
598
659
  for await (const event of decodeAnthropicSSE(response.body)) {
599
- const type = event.event ?? '';
660
+ const type = event.event ?? "";
600
661
  const data = event.data;
601
662
  if (!data)
602
663
  continue;
603
- if (type === 'content_block_start') {
604
- const idx = typeof data.index === 'number' ? data.index : 0;
664
+ if (type === "content_block_start") {
665
+ const idx = typeof data.index === "number" ? data.index : 0;
605
666
  const block = data.content_block;
606
- if (block?.type === 'tool_use') {
607
- const buf = { args: '' };
608
- if (typeof block.id === 'string')
667
+ if (block?.type === "tool_use") {
668
+ const buf = { args: "" };
669
+ if (typeof block.id === "string")
609
670
  buf.id = block.id;
610
- if (typeof block.name === 'string')
671
+ if (typeof block.name === "string")
611
672
  buf.name = block.name;
612
673
  toolCallBuffers.set(idx, buf);
613
674
  }
614
675
  }
615
- else if (type === 'content_block_delta') {
616
- const idx = typeof data.index === 'number' ? data.index : 0;
676
+ else if (type === "content_block_delta") {
677
+ const idx = typeof data.index === "number" ? data.index : 0;
617
678
  const delta = data.delta;
618
679
  const dt = delta?.type;
619
- if (dt === 'text_delta' && typeof delta?.text === 'string') {
680
+ if (dt === "text_delta" && typeof delta?.text === "string") {
620
681
  text.push(delta.text);
621
682
  yield { textDelta: delta.text };
622
683
  }
623
- else if (dt === 'thinking_delta' && typeof delta?.thinking === 'string') {
684
+ else if (dt === "thinking_delta" && typeof delta?.thinking === "string") {
624
685
  thinking.push(delta.thinking);
625
686
  yield { thinkingDelta: delta.thinking };
626
687
  }
627
- else if (dt === 'input_json_delta' && typeof delta?.partial_json === 'string') {
688
+ else if (dt === "input_json_delta" && typeof delta?.partial_json === "string") {
628
689
  const buf = toolCallBuffers.get(idx);
629
690
  if (buf)
630
691
  buf.args += delta.partial_json;
631
692
  }
632
693
  }
633
- else if (type === 'message_delta') {
694
+ else if (type === "message_delta") {
634
695
  const u = data.usage;
635
696
  if (u) {
636
697
  usage = usage ?? {};
637
- if (typeof u.input_tokens === 'number')
698
+ if (typeof u.input_tokens === "number")
638
699
  usage.inputTokens = u.input_tokens;
639
- if (typeof u.output_tokens === 'number')
700
+ if (typeof u.output_tokens === "number")
640
701
  usage.outputTokens = u.output_tokens;
641
- if (typeof u.cache_read_input_tokens === 'number')
702
+ if (typeof u.cache_read_input_tokens === "number")
642
703
  usage.cachedInputTokens = u.cache_read_input_tokens;
643
- if (typeof u.cache_creation_input_tokens === 'number')
704
+ if (typeof u.cache_creation_input_tokens === "number")
644
705
  usage.cacheWriteTokens = u.cache_creation_input_tokens;
645
706
  if (usage.inputTokens !== undefined || usage.outputTokens !== undefined) {
646
707
  usage.totalTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
647
708
  }
648
709
  }
649
710
  }
650
- else if (type === 'message_start') {
711
+ else if (type === "message_start") {
651
712
  const message = data.message;
652
713
  const u = message?.usage;
653
714
  if (u) {
654
715
  usage = {};
655
- if (typeof u.input_tokens === 'number')
716
+ if (typeof u.input_tokens === "number")
656
717
  usage.inputTokens = u.input_tokens;
657
- if (typeof u.cache_read_input_tokens === 'number')
718
+ if (typeof u.cache_read_input_tokens === "number")
658
719
  usage.cachedInputTokens = u.cache_read_input_tokens;
659
- if (typeof u.cache_creation_input_tokens === 'number')
720
+ if (typeof u.cache_creation_input_tokens === "number")
660
721
  usage.cacheWriteTokens = u.cache_creation_input_tokens;
661
722
  }
662
723
  }
663
724
  }
664
725
  const done = {
665
- ...(text.length ? { message: { role: 'assistant', content: text.join('') } } : {}),
666
- ...(thinking.length ? { thinking: thinking.join('') } : {}),
726
+ ...(text.length ? { message: { role: "assistant", content: text.join("") } } : {}),
727
+ ...(thinking.length ? { thinking: thinking.join("") } : {}),
667
728
  ...(toolCallBuffers.size > 0
668
729
  ? {
669
730
  toolCalls: [...toolCallBuffers.values()].map((buf, index) => ({
670
731
  id: buf.id ?? `anthropic-stream-${index + 1}`,
671
- name: buf.name ?? '',
732
+ name: buf.name ?? "",
672
733
  input: buf.args ? JSON.parse(buf.args) : {},
673
734
  })),
674
735
  }
@@ -682,17 +743,21 @@ export class AnthropicModelProvider {
682
743
  }
683
744
  export const defaultModelProvider = new MockModelProvider();
684
745
  function parseScriptedToolCalls(text) {
685
- const marker = '```fabric-tool-calls';
746
+ const marker = "```fabric-tool-calls";
686
747
  const start = text.indexOf(marker);
687
748
  if (start === -1)
688
749
  return [];
689
- const jsonStart = text.indexOf('\n', start);
690
- const end = text.indexOf('```', jsonStart + 1);
750
+ const jsonStart = text.indexOf("\n", start);
751
+ const end = text.indexOf("```", jsonStart + 1);
691
752
  if (jsonStart === -1 || end === -1)
692
753
  return [];
693
754
  const raw = text.slice(jsonStart + 1, end).trim();
694
755
  const parsed = JSON.parse(raw);
695
- return parsed.map((call, index) => ({ id: call.id ?? `mock-tool-${index + 1}`, name: call.tool, input: call.input }));
756
+ return parsed.map((call, index) => ({
757
+ id: call.id ?? `mock-tool-${index + 1}`,
758
+ name: call.tool,
759
+ input: call.input,
760
+ }));
696
761
  }
697
762
  function estimateUsage(messages) {
698
763
  const chars = messages.reduce((sum, message) => sum + message.content.length, 0);
@@ -706,7 +771,7 @@ function calculateRetryDelay(baseMs, attempt) {
706
771
  function isRetryableModelError(error) {
707
772
  if (error instanceof ProviderHttpError)
708
773
  return error.status === 429 || error.status >= 500;
709
- if (error instanceof Error && error.name === 'AbortError')
774
+ if (error instanceof Error && error.name === "AbortError")
710
775
  return false;
711
776
  const message = error instanceof Error ? error.message : String(error);
712
777
  return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|network.?error|connection.?(?:reset|refused|lost)|socket hang up|fetch failed|timed? out|timeout|terminated/i.test(message);
@@ -719,30 +784,38 @@ function sleepWithAbort(ms, signal) {
719
784
  return Promise.reject(createAbortError(signal.reason));
720
785
  return new Promise((resolve, reject) => {
721
786
  const timer = setTimeout(() => {
722
- signal.removeEventListener('abort', onAbort);
787
+ signal.removeEventListener("abort", onAbort);
723
788
  resolve();
724
789
  }, ms);
725
790
  const onAbort = () => {
726
791
  clearTimeout(timer);
727
- signal.removeEventListener('abort', onAbort);
792
+ signal.removeEventListener("abort", onAbort);
728
793
  reject(createAbortError(signal.reason));
729
794
  };
730
- signal.addEventListener('abort', onAbort, { once: true });
795
+ signal.addEventListener("abort", onAbort, { once: true });
731
796
  });
732
797
  }
733
798
  function createAbortError(reason) {
734
- const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : 'This operation was aborted';
799
+ const message = reason instanceof Error
800
+ ? reason.message
801
+ : typeof reason === "string"
802
+ ? reason
803
+ : "This operation was aborted";
735
804
  const err = new Error(message);
736
- err.name = 'AbortError';
805
+ err.name = "AbortError";
737
806
  return err;
738
807
  }
739
808
  export function toOpenAIMessage(message) {
740
- if (message.role === 'tool') {
741
- return { role: 'tool', tool_call_id: message.toolCallId ?? message.name ?? 'tool', content: message.content };
809
+ if (message.role === "tool") {
810
+ return {
811
+ role: "tool",
812
+ tool_call_id: message.toolCallId ?? message.name ?? "tool",
813
+ content: message.content,
814
+ };
742
815
  }
743
- if (message.role === 'assistant' && message.toolCalls?.length) {
816
+ if (message.role === "assistant" && message.toolCalls?.length) {
744
817
  return {
745
- role: 'assistant',
818
+ role: "assistant",
746
819
  content: message.content || null,
747
820
  tool_calls: message.toolCalls.map(toOpenAIToolCallForMessage),
748
821
  };
@@ -752,33 +825,37 @@ export function toOpenAIMessage(message) {
752
825
  function toOpenAIToolCallForMessage(call) {
753
826
  return {
754
827
  id: call.id,
755
- type: 'function',
828
+ type: "function",
756
829
  function: {
757
830
  name: call.name,
758
- arguments: typeof call.input === 'string' ? call.input : JSON.stringify(call.input),
831
+ arguments: typeof call.input === "string" ? call.input : JSON.stringify(call.input),
759
832
  },
760
833
  };
761
834
  }
762
835
  export function toOpenAITool(tool) {
763
836
  return {
764
- type: 'function',
837
+ type: "function",
765
838
  function: {
766
839
  name: tool.name,
767
- description: tool.description ?? '',
840
+ description: tool.description ?? "",
768
841
  parameters: normalizeOpenAIParameters(tool.inputSchema),
769
842
  },
770
843
  };
771
844
  }
772
845
  function normalizeOpenAIParameters(schema) {
773
- if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
774
- return { type: 'object', properties: {}, required: [], additionalProperties: false };
846
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
847
+ return { type: "object", properties: {}, required: [], additionalProperties: false };
775
848
  }
776
849
  const objectSchema = schema;
777
- if (objectSchema.type !== 'object')
778
- return { type: 'object', properties: {}, required: [], additionalProperties: false };
850
+ if (objectSchema.type !== "object")
851
+ return { type: "object", properties: {}, required: [], additionalProperties: false };
779
852
  return {
780
853
  ...objectSchema,
781
- properties: typeof objectSchema.properties === 'object' && objectSchema.properties !== null && !Array.isArray(objectSchema.properties) ? objectSchema.properties : {},
854
+ properties: typeof objectSchema.properties === "object" &&
855
+ objectSchema.properties !== null &&
856
+ !Array.isArray(objectSchema.properties)
857
+ ? objectSchema.properties
858
+ : {},
782
859
  required: Array.isArray(objectSchema.required) ? objectSchema.required : [],
783
860
  };
784
861
  }
@@ -792,9 +869,12 @@ function fromOpenAIToolCall(call) {
792
869
  export function openAIChatCompletionToModelResponse(json) {
793
870
  const choice = json.choices?.[0];
794
871
  const message = choice?.message;
795
- const reasoning = typeof message?.reasoning_content === 'string' ? message.reasoning_content : undefined;
872
+ const content = openAIMessageText(message?.content);
873
+ const reasoning = typeof message?.reasoning_content === "string"
874
+ ? message.reasoning_content
875
+ : openAIReasoningText(message?.content);
796
876
  const result = {
797
- ...(message?.content ? { message: { role: 'assistant', content: message.content } } : {}),
877
+ ...(content ? { message: { role: "assistant", content } } : {}),
798
878
  ...(reasoning ? { thinking: reasoning } : {}),
799
879
  ...(message?.tool_calls ? { toolCalls: message.tool_calls.map(fromOpenAIToolCall) } : {}),
800
880
  };
@@ -810,53 +890,147 @@ export function openAIChatCompletionToModelResponse(json) {
810
890
  }
811
891
  return result;
812
892
  }
893
+ function openAIMessageText(content) {
894
+ if (typeof content === "string")
895
+ return content;
896
+ const text = content
897
+ ?.flatMap((part) => {
898
+ if ((part.type === "text" || part.type === "output_text") && typeof part.text === "string") {
899
+ return [part.text];
900
+ }
901
+ return [];
902
+ })
903
+ .join("");
904
+ return text || undefined;
905
+ }
906
+ function openAIReasoningText(content) {
907
+ if (!Array.isArray(content))
908
+ return undefined;
909
+ const reasoning = content
910
+ .filter((part) => part.type === "reasoning")
911
+ .flatMap((part) => part.summary ?? [])
912
+ .flatMap((summary) => (typeof summary.text === "string" ? [summary.text] : []))
913
+ .join("\n");
914
+ return reasoning || undefined;
915
+ }
813
916
  function toAnthropicTool(tool) {
814
- return { name: tool.name, description: tool.description ?? '', input_schema: normalizeOpenAIParameters(tool.inputSchema) };
917
+ return {
918
+ name: tool.name,
919
+ description: tool.description ?? "",
920
+ input_schema: normalizeOpenAIParameters(tool.inputSchema),
921
+ };
815
922
  }
816
923
  function toAnthropicMessages(messages) {
817
- const system = messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n') || undefined;
818
- const converted = messages.filter((message) => message.role !== 'system').map((message) => {
819
- if (message.role === 'tool')
820
- return { role: 'user', content: [{ type: 'tool_result', tool_use_id: message.toolCallId ?? message.name ?? 'tool', content: message.content }] };
821
- if (message.role === 'assistant' && message.toolCalls?.length) {
822
- return { role: 'assistant', content: [{ type: 'text', text: message.content || '' }, ...message.toolCalls.map((call) => ({ type: 'tool_use', id: call.id, name: call.name, input: call.input }))] };
924
+ const system = messages
925
+ .filter((message) => message.role === "system")
926
+ .map((message) => message.content)
927
+ .join("\n\n") || undefined;
928
+ const converted = messages
929
+ .filter((message) => message.role !== "system")
930
+ .map((message) => {
931
+ if (message.role === "tool")
932
+ return {
933
+ role: "user",
934
+ content: [
935
+ {
936
+ type: "tool_result",
937
+ tool_use_id: message.toolCallId ?? message.name ?? "tool",
938
+ content: message.content,
939
+ },
940
+ ],
941
+ };
942
+ if (message.role === "assistant" && message.toolCalls?.length) {
943
+ return {
944
+ role: "assistant",
945
+ content: [
946
+ { type: "text", text: message.content || "" },
947
+ ...message.toolCalls.map((call) => ({
948
+ type: "tool_use",
949
+ id: call.id,
950
+ name: call.name,
951
+ input: call.input,
952
+ })),
953
+ ],
954
+ };
823
955
  }
824
- return { role: message.role === 'assistant' ? 'assistant' : 'user', content: message.content };
956
+ return {
957
+ role: message.role === "assistant" ? "assistant" : "user",
958
+ content: message.content,
959
+ };
825
960
  });
826
961
  return { ...(system ? { system } : {}), messages: converted };
827
962
  }
828
963
  function toGeminiRequest(request, model) {
829
- const system = request.messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
830
- const contents = request.messages.filter((message) => message.role !== 'system').map((message) => {
831
- if (message.role === 'tool')
832
- return { role: 'user', parts: [{ functionResponse: { name: message.name ?? 'tool', response: { content: message.content } } }] };
833
- if (message.role === 'assistant' && message.toolCalls?.length)
834
- return { role: 'model', parts: message.toolCalls.map((call) => ({ functionCall: { name: call.name, args: call.input ?? {} } })) };
835
- return { role: message.role === 'assistant' ? 'model' : 'user', parts: [{ text: message.content }] };
964
+ const system = request.messages
965
+ .filter((message) => message.role === "system")
966
+ .map((message) => message.content)
967
+ .join("\n\n");
968
+ const contents = request.messages
969
+ .filter((message) => message.role !== "system")
970
+ .map((message) => {
971
+ if (message.role === "tool")
972
+ return {
973
+ role: "user",
974
+ parts: [
975
+ {
976
+ functionResponse: {
977
+ name: message.name ?? "tool",
978
+ response: { content: message.content },
979
+ },
980
+ },
981
+ ],
982
+ };
983
+ if (message.role === "assistant" && message.toolCalls?.length)
984
+ return {
985
+ role: "model",
986
+ parts: message.toolCalls.map((call) => ({
987
+ functionCall: { name: call.name, args: call.input ?? {} },
988
+ })),
989
+ };
990
+ return {
991
+ role: message.role === "assistant" ? "model" : "user",
992
+ parts: [{ text: message.content }],
993
+ };
836
994
  });
837
- const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? geminiThinkingBudget(request.thinkingLevel) : undefined;
995
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel)
996
+ ? geminiThinkingBudget(request.thinkingLevel)
997
+ : undefined;
838
998
  return {
839
999
  ...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}),
840
1000
  contents,
841
- ...(request.tools?.length ? { tools: [{ functionDeclarations: request.tools.map(toGeminiTool) }] } : {}),
842
- ...(thinkingBudget !== undefined ? { generationConfig: { thinkingConfig: { thinkingBudget } } } : {}),
1001
+ ...(request.tools?.length
1002
+ ? { tools: [{ functionDeclarations: request.tools.map(toGeminiTool) }] }
1003
+ : {}),
1004
+ ...(thinkingBudget !== undefined
1005
+ ? { generationConfig: { thinkingConfig: { thinkingBudget } } }
1006
+ : {}),
843
1007
  };
844
1008
  }
845
1009
  function toGeminiTool(tool) {
846
- return { name: tool.name, description: tool.description ?? '', parameters: normalizeOpenAIParameters(tool.inputSchema) };
1010
+ return {
1011
+ name: tool.name,
1012
+ description: tool.description ?? "",
1013
+ parameters: normalizeOpenAIParameters(tool.inputSchema),
1014
+ };
847
1015
  }
848
1016
  function geminiResponseToModelResponse(json) {
849
1017
  const parts = json.candidates?.[0]?.content?.parts ?? [];
850
1018
  const texts = [];
851
1019
  const toolCalls = [];
852
1020
  for (const [index, part] of parts.entries()) {
853
- if (typeof part.text === 'string')
1021
+ if (typeof part.text === "string")
854
1022
  texts.push(part.text);
855
1023
  if (part.functionCall?.name)
856
- toolCalls.push({ id: `gemini-tool-${index + 1}`, name: part.functionCall.name, input: part.functionCall.args ?? {} });
1024
+ toolCalls.push({
1025
+ id: `gemini-tool-${index + 1}`,
1026
+ name: part.functionCall.name,
1027
+ input: part.functionCall.args ?? {},
1028
+ });
857
1029
  }
858
1030
  const result = {
859
- ...(texts.length > 0 ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
1031
+ ...(texts.length > 0
1032
+ ? { message: { role: "assistant", content: texts.join("\n") } }
1033
+ : {}),
860
1034
  ...(toolCalls.length > 0 ? { toolCalls } : {}),
861
1035
  };
862
1036
  if (json.usageMetadata) {
@@ -872,13 +1046,35 @@ function geminiResponseToModelResponse(json) {
872
1046
  return result;
873
1047
  }
874
1048
  function toBedrockRequest(request) {
875
- const system = request.messages.filter((message) => message.role === 'system').map((message) => ({ text: message.content }));
876
- const messages = request.messages.filter((message) => message.role !== 'system').map((message) => {
877
- if (message.role === 'tool')
878
- return { role: 'user', content: [{ toolResult: { toolUseId: message.toolCallId ?? message.name ?? 'tool', content: [{ text: message.content }] } }] };
879
- if (message.role === 'assistant' && message.toolCalls?.length)
880
- return { role: 'assistant', content: message.toolCalls.map((call) => ({ toolUse: { toolUseId: call.id, name: call.name, input: call.input ?? {} } })) };
881
- return { role: message.role === 'assistant' ? 'assistant' : 'user', content: [{ text: message.content }] };
1049
+ const system = request.messages
1050
+ .filter((message) => message.role === "system")
1051
+ .map((message) => ({ text: message.content }));
1052
+ const messages = request.messages
1053
+ .filter((message) => message.role !== "system")
1054
+ .map((message) => {
1055
+ if (message.role === "tool")
1056
+ return {
1057
+ role: "user",
1058
+ content: [
1059
+ {
1060
+ toolResult: {
1061
+ toolUseId: message.toolCallId ?? message.name ?? "tool",
1062
+ content: [{ text: message.content }],
1063
+ },
1064
+ },
1065
+ ],
1066
+ };
1067
+ if (message.role === "assistant" && message.toolCalls?.length)
1068
+ return {
1069
+ role: "assistant",
1070
+ content: message.toolCalls.map((call) => ({
1071
+ toolUse: { toolUseId: call.id, name: call.name, input: call.input ?? {} },
1072
+ })),
1073
+ };
1074
+ return {
1075
+ role: message.role === "assistant" ? "assistant" : "user",
1076
+ content: [{ text: message.content }],
1077
+ };
882
1078
  });
883
1079
  return {
884
1080
  ...(system.length ? { system } : {}),
@@ -888,20 +1084,30 @@ function toBedrockRequest(request) {
888
1084
  };
889
1085
  }
890
1086
  function toBedrockTool(tool) {
891
- return { toolSpec: { name: tool.name, description: tool.description ?? '', inputSchema: { json: normalizeOpenAIParameters(tool.inputSchema) } } };
1087
+ return {
1088
+ toolSpec: {
1089
+ name: tool.name,
1090
+ description: tool.description ?? "",
1091
+ inputSchema: { json: normalizeOpenAIParameters(tool.inputSchema) },
1092
+ },
1093
+ };
892
1094
  }
893
1095
  function bedrockResponseToModelResponse(json) {
894
1096
  const content = json.output?.message?.content ?? [];
895
1097
  const texts = [];
896
1098
  const toolCalls = [];
897
1099
  for (const part of content) {
898
- if (typeof part.text === 'string')
1100
+ if (typeof part.text === "string")
899
1101
  texts.push(part.text);
900
1102
  if (part.toolUse?.toolUseId && part.toolUse.name)
901
- toolCalls.push({ id: part.toolUse.toolUseId, name: part.toolUse.name, input: part.toolUse.input ?? {} });
1103
+ toolCalls.push({
1104
+ id: part.toolUse.toolUseId,
1105
+ name: part.toolUse.name,
1106
+ input: part.toolUse.input ?? {},
1107
+ });
902
1108
  }
903
1109
  const result = {
904
- ...(texts.length ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
1110
+ ...(texts.length ? { message: { role: "assistant", content: texts.join("\n") } } : {}),
905
1111
  ...(toolCalls.length ? { toolCalls } : {}),
906
1112
  };
907
1113
  if (json.usage) {
@@ -917,12 +1123,21 @@ function bedrockResponseToModelResponse(json) {
917
1123
  return result;
918
1124
  }
919
1125
  function toCohereMessage(message) {
920
- const role = message.role === 'assistant' ? 'assistant' : message.role === 'system' ? 'system' : message.role === 'tool' ? 'tool' : 'user';
1126
+ const role = message.role === "assistant"
1127
+ ? "assistant"
1128
+ : message.role === "system"
1129
+ ? "system"
1130
+ : message.role === "tool"
1131
+ ? "tool"
1132
+ : "user";
921
1133
  return { role, content: message.content };
922
1134
  }
923
1135
  function cohereResponseToModelResponse(json) {
924
- const content = json.message?.content?.map((part) => part.text).filter((text) => typeof text === 'string').join('\n') ?? '';
925
- const result = content ? { message: { role: 'assistant', content } } : {};
1136
+ const content = json.message?.content
1137
+ ?.map((part) => part.text)
1138
+ .filter((text) => typeof text === "string")
1139
+ .join("\n") ?? "";
1140
+ const result = content ? { message: { role: "assistant", content } } : {};
926
1141
  const billed = json.usage?.billed_units;
927
1142
  if (billed) {
928
1143
  const usage = {};
@@ -941,16 +1156,19 @@ function anthropicResponseToModelResponse(json) {
941
1156
  const thinkingChunks = [];
942
1157
  const toolCalls = [];
943
1158
  for (const block of json.content ?? []) {
944
- if (block.type === 'text' && typeof block.text === 'string')
1159
+ if (block.type === "text" && typeof block.text === "string")
945
1160
  texts.push(block.text);
946
- if ((block.type === 'thinking' || block.type === 'redacted_thinking') && typeof block.text === 'string')
1161
+ if ((block.type === "thinking" || block.type === "redacted_thinking") &&
1162
+ typeof block.text === "string")
947
1163
  thinkingChunks.push(block.text);
948
- if (block.type === 'tool_use' && typeof block.id === 'string' && typeof block.name === 'string')
1164
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string")
949
1165
  toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
950
1166
  }
951
1167
  const result = {
952
- ...(texts.length > 0 ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
953
- ...(thinkingChunks.length > 0 ? { thinking: thinkingChunks.join('\n') } : {}),
1168
+ ...(texts.length > 0
1169
+ ? { message: { role: "assistant", content: texts.join("\n") } }
1170
+ : {}),
1171
+ ...(thinkingChunks.length > 0 ? { thinking: thinkingChunks.join("\n") } : {}),
954
1172
  ...(toolCalls.length > 0 ? { toolCalls } : {}),
955
1173
  };
956
1174
  if (json.usage) {
@@ -975,7 +1193,7 @@ function anthropicResponseToModelResponse(json) {
975
1193
  * stable across processes (with the same key) without leaking secrets.
976
1194
  */
977
1195
  function rateLimitKeyForApiKey(apiKey) {
978
- return createHash('sha256').update(apiKey).digest('hex').slice(0, 8);
1196
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 8);
979
1197
  }
980
1198
  function safeJsonParse(value) {
981
1199
  try {
@@ -1001,17 +1219,17 @@ function splitLines(buffer, pendingCR) {
1001
1219
  let pending = false;
1002
1220
  // If previous chunk ended with \r and this one starts with \n, skip the \n
1003
1221
  // as it completes a split CRLF sequence.
1004
- if (pendingCR && buffer.length > 0 && buffer[0] === '\n') {
1222
+ if (pendingCR && buffer.length > 0 && buffer[0] === "\n") {
1005
1223
  start = 1;
1006
1224
  }
1007
1225
  for (let i = start; i < buffer.length; i++) {
1008
1226
  const ch = buffer[i];
1009
- if (ch === '\n') {
1227
+ if (ch === "\n") {
1010
1228
  lines.push(buffer.slice(start, i));
1011
1229
  start = i + 1;
1012
1230
  }
1013
- else if (ch === '\r') {
1014
- if (i + 1 < buffer.length && buffer[i + 1] === '\n') {
1231
+ else if (ch === "\r") {
1232
+ if (i + 1 < buffer.length && buffer[i + 1] === "\n") {
1015
1233
  // CRLF within the same buffer
1016
1234
  lines.push(buffer.slice(start, i));
1017
1235
  i++; // skip the \n
@@ -1034,7 +1252,7 @@ function splitLines(buffer, pendingCR) {
1034
1252
  async function* decodeOpenAISSE(stream) {
1035
1253
  const decoder = new TextDecoder();
1036
1254
  const reader = stream.getReader();
1037
- let buffer = '';
1255
+ let buffer = "";
1038
1256
  let pendingCR = false;
1039
1257
  try {
1040
1258
  while (true) {
@@ -1047,9 +1265,9 @@ async function* decodeOpenAISSE(stream) {
1047
1265
  buffer = remainder;
1048
1266
  for (const line of lines) {
1049
1267
  const trimmed = line.trim();
1050
- if (!trimmed || trimmed.startsWith(':'))
1268
+ if (!trimmed || trimmed.startsWith(":"))
1051
1269
  continue;
1052
- if (trimmed.startsWith('data:')) {
1270
+ if (trimmed.startsWith("data:")) {
1053
1271
  const payload = trimmed.slice(5).trim();
1054
1272
  if (!payload)
1055
1273
  continue;
@@ -1069,7 +1287,7 @@ async function* decodeOpenAISSE(stream) {
1069
1287
  async function* decodeAnthropicSSE(stream) {
1070
1288
  const decoder = new TextDecoder();
1071
1289
  const reader = stream.getReader();
1072
- let buffer = '';
1290
+ let buffer = "";
1073
1291
  let pendingEvent;
1074
1292
  let pendingCR = false;
1075
1293
  try {
@@ -1087,11 +1305,11 @@ async function* decodeAnthropicSSE(stream) {
1087
1305
  pendingEvent = undefined;
1088
1306
  continue;
1089
1307
  }
1090
- if (trimmed.startsWith('event:')) {
1308
+ if (trimmed.startsWith("event:")) {
1091
1309
  pendingEvent = trimmed.slice(6).trim();
1092
1310
  continue;
1093
1311
  }
1094
- if (trimmed.startsWith('data:')) {
1312
+ if (trimmed.startsWith("data:")) {
1095
1313
  const payload = trimmed.slice(5).trim();
1096
1314
  if (!payload)
1097
1315
  continue;
@@ -1123,7 +1341,7 @@ export class ProviderHttpError extends Error {
1123
1341
  this.status = status;
1124
1342
  this.statusText = statusText;
1125
1343
  this.body = body;
1126
- this.name = 'ProviderHttpError';
1344
+ this.name = "ProviderHttpError";
1127
1345
  if (retryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
1128
1346
  this.retryAfterMs = retryAfterMs;
1129
1347
  }
@@ -1154,41 +1372,57 @@ export function parseRetryAfterMs(headerValue) {
1154
1372
  }
1155
1373
  function signAwsRequest(input) {
1156
1374
  const now = new Date();
1157
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
1375
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
1158
1376
  const dateStamp = amzDate.slice(0, 8);
1159
1377
  const headers = {
1160
1378
  ...input.headers,
1161
1379
  host: input.host,
1162
- 'x-amz-date': amzDate,
1163
- ...(input.sessionToken ? { 'x-amz-security-token': input.sessionToken } : {}),
1380
+ "x-amz-date": amzDate,
1381
+ ...(input.sessionToken ? { "x-amz-security-token": input.sessionToken } : {}),
1164
1382
  };
1165
- const signedHeaders = Object.keys(headers).map((key) => key.toLowerCase()).sort();
1166
- const canonicalHeaders = signedHeaders.map((key) => `${key}:${headers[Object.keys(headers).find((candidate) => candidate.toLowerCase() === key) ?? key]?.trim()}\n`).join('');
1383
+ const signedHeaders = Object.keys(headers)
1384
+ .map((key) => key.toLowerCase())
1385
+ .sort();
1386
+ const canonicalHeaders = signedHeaders
1387
+ .map((key) => `${key}:${headers[Object.keys(headers).find((candidate) => candidate.toLowerCase() === key) ?? key]?.trim()}\n`)
1388
+ .join("");
1167
1389
  const payloadHash = sha256Hex(input.body);
1168
- const canonicalRequest = [input.method, input.path, '', canonicalHeaders, signedHeaders.join(';'), payloadHash].join('\n');
1390
+ const canonicalRequest = [
1391
+ input.method,
1392
+ input.path,
1393
+ "",
1394
+ canonicalHeaders,
1395
+ signedHeaders.join(";"),
1396
+ payloadHash,
1397
+ ].join("\n");
1169
1398
  const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
1170
- const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
1399
+ const stringToSign = [
1400
+ "AWS4-HMAC-SHA256",
1401
+ amzDate,
1402
+ credentialScope,
1403
+ sha256Hex(canonicalRequest),
1404
+ ].join("\n");
1171
1405
  const signingKey = awsSigningKey(input.secretAccessKey, dateStamp, input.region, input.service);
1172
1406
  const signature = hmacHex(signingKey, stringToSign);
1173
1407
  return {
1174
1408
  ...headers,
1175
- authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders.join(';')}, Signature=${signature}`,
1409
+ authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders.join(";")}, Signature=${signature}`,
1176
1410
  };
1177
1411
  }
1178
1412
  function sha256Hex(value) {
1179
- return createHash('sha256').update(value).digest('hex');
1413
+ return createHash("sha256").update(value).digest("hex");
1180
1414
  }
1181
1415
  function hmac(key, value) {
1182
- return createHmac('sha256', key).update(value).digest();
1416
+ return createHmac("sha256", key).update(value).digest();
1183
1417
  }
1184
1418
  function hmacHex(key, value) {
1185
- return createHmac('sha256', key).update(value).digest('hex');
1419
+ return createHmac("sha256", key).update(value).digest("hex");
1186
1420
  }
1187
1421
  function awsSigningKey(secret, date, region, service) {
1188
- return hmac(hmac(hmac(hmac(`AWS4${secret}`, date), region), service), 'aws4_request');
1422
+ return hmac(hmac(hmac(hmac(`AWS4${secret}`, date), region), service), "aws4_request");
1189
1423
  }
1190
1424
  function formatProviderHttpError(provider, model, status, statusText, body, headers) {
1191
- const retryAfterMs = headers ? parseRetryAfterMs(headers.get('retry-after')) : undefined;
1425
+ const retryAfterMs = headers ? parseRetryAfterMs(headers.get("retry-after")) : undefined;
1192
1426
  let parsedMessage;
1193
1427
  let code;
1194
1428
  try {
@@ -1199,13 +1433,14 @@ function formatProviderHttpError(provider, model, status, statusText, body, head
1199
1433
  catch { }
1200
1434
  const details = parsedMessage ?? body.slice(0, 500);
1201
1435
  if (status === 401 || status === 403)
1202
- return new ProviderHttpError(`${provider}: authentication/authorization failed for model "${model ?? 'unknown'}" (${status}). Check API key and provider permissions. ${details}`, provider, status, statusText, body);
1436
+ return new ProviderHttpError(`${provider}: authentication/authorization failed for model "${model ?? "unknown"}" (${status}). Check API key and provider permissions. ${details}`, provider, status, statusText, body);
1203
1437
  if (status === 400 && /model/i.test(details))
1204
- return new ProviderHttpError(`${provider}: provider rejected model "${model ?? 'unknown'}". Check FABRIC_MODEL/model access/base URL. ${details}`, provider, status, statusText, body);
1205
- if (status === 400 && (/schema|function|tool/i.test(details) || code === 'invalid_function_parameters'))
1438
+ return new ProviderHttpError(`${provider}: provider rejected model "${model ?? "unknown"}". Check FABRIC_MODEL/model access/base URL. ${details}`, provider, status, statusText, body);
1439
+ if (status === 400 &&
1440
+ (/schema|function|tool/i.test(details) || code === "invalid_function_parameters"))
1206
1441
  return new ProviderHttpError(`${provider}: provider rejected a tool schema. Run fabric-harness doctor --tools. ${details}`, provider, status, statusText, body);
1207
1442
  if (status === 429)
1208
- return new ProviderHttpError(`${provider}: rate limited for model "${model ?? 'unknown'}". ${details}`, provider, status, statusText, body, retryAfterMs);
1443
+ return new ProviderHttpError(`${provider}: rate limited for model "${model ?? "unknown"}". ${details}`, provider, status, statusText, body, retryAfterMs);
1209
1444
  if (status >= 500)
1210
1445
  return new ProviderHttpError(`${provider}: provider unavailable (${status} ${statusText}). ${details}`, provider, status, statusText, body, retryAfterMs);
1211
1446
  return new ProviderHttpError(`${provider}: ${status} ${statusText}: ${details}`, provider, status, statusText, body, retryAfterMs);