@fabric-harness/sdk 2.1.1 → 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.
- package/NOTICE +11 -0
- package/connector-spec/channel.md +61 -0
- package/connector-spec/tooling.md +14 -0
- package/dist/agent-definition.d.ts +8 -0
- package/dist/agent-definition.d.ts.map +1 -1
- package/dist/agent-definition.js +27 -4
- package/dist/agent-definition.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -0
- package/dist/agent.js.map +1 -1
- package/dist/approval-notifier.d.ts +63 -0
- package/dist/approval-notifier.d.ts.map +1 -0
- package/dist/approval-notifier.js +152 -0
- package/dist/approval-notifier.js.map +1 -0
- package/dist/attachment-store.d.ts +12 -0
- package/dist/attachment-store.d.ts.map +1 -1
- package/dist/attachment-store.js +61 -3
- package/dist/attachment-store.js.map +1 -1
- package/dist/channel-contract.d.ts +29 -0
- package/dist/channel-contract.d.ts.map +1 -0
- package/dist/channel-contract.js +153 -0
- package/dist/channel-contract.js.map +1 -0
- package/dist/channel-runtime.d.ts +7 -0
- package/dist/channel-runtime.d.ts.map +1 -0
- package/dist/channel-runtime.js +6 -0
- package/dist/channel-runtime.js.map +1 -0
- package/dist/conversation-projection.d.ts.map +1 -1
- package/dist/conversation-projection.js +12 -5
- package/dist/conversation-projection.js.map +1 -1
- package/dist/conversation-stream.d.ts +3 -0
- package/dist/conversation-stream.d.ts.map +1 -1
- package/dist/conversation-stream.js +14 -0
- package/dist/conversation-stream.js.map +1 -1
- package/dist/cost-budget.d.ts +5 -0
- package/dist/cost-budget.d.ts.map +1 -1
- package/dist/cost-budget.js +20 -0
- package/dist/cost-budget.js.map +1 -1
- package/dist/error.d.ts +16 -4
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +30 -5
- package/dist/error.js.map +1 -1
- package/dist/filesystem.d.ts +3 -3
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/in-memory-submission-store.d.ts +4 -0
- package/dist/in-memory-submission-store.d.ts.map +1 -1
- package/dist/in-memory-submission-store.js +45 -0
- package/dist/in-memory-submission-store.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/job-invoke.d.ts +43 -0
- package/dist/job-invoke.d.ts.map +1 -0
- package/dist/job-invoke.js +56 -0
- package/dist/job-invoke.js.map +1 -0
- package/dist/mcp.d.ts +53 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +145 -20
- package/dist/mcp.js.map +1 -1
- package/dist/model.d.ts +22 -8
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +465 -230
- package/dist/model.js.map +1 -1
- package/dist/network-enforcement.d.ts +16 -0
- package/dist/network-enforcement.d.ts.map +1 -0
- package/dist/network-enforcement.js +36 -0
- package/dist/network-enforcement.js.map +1 -0
- package/dist/observability.d.ts +36 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +89 -0
- package/dist/observability.js.map +1 -0
- package/dist/operational-metrics.d.ts +53 -0
- package/dist/operational-metrics.d.ts.map +1 -0
- package/dist/operational-metrics.js +119 -0
- package/dist/operational-metrics.js.map +1 -0
- package/dist/otel-observer.d.ts +3 -0
- package/dist/otel-observer.d.ts.map +1 -1
- package/dist/otel-observer.js +21 -9
- package/dist/otel-observer.js.map +1 -1
- package/dist/persistence-bundle-contract.d.ts +7 -0
- package/dist/persistence-bundle-contract.d.ts.map +1 -0
- package/dist/persistence-bundle-contract.js +86 -0
- package/dist/persistence-bundle-contract.js.map +1 -0
- package/dist/persistence-bundle.d.ts +56 -0
- package/dist/persistence-bundle.d.ts.map +1 -0
- package/dist/persistence-bundle.js +2 -0
- package/dist/persistence-bundle.js.map +1 -0
- package/dist/persistent-agent.d.ts +1 -1
- package/dist/persistent-agent.js +1 -1
- package/dist/policied-sandbox.d.ts +3 -0
- package/dist/policied-sandbox.d.ts.map +1 -1
- package/dist/policied-sandbox.js +25 -16
- package/dist/policied-sandbox.js.map +1 -1
- package/dist/policy.d.ts +8 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +104 -19
- package/dist/policy.js.map +1 -1
- package/dist/redaction.js +1 -1
- package/dist/redaction.js.map +1 -1
- package/dist/sandbox-ref.d.ts +37 -2
- package/dist/sandbox-ref.d.ts.map +1 -1
- package/dist/sandbox-ref.js +164 -3
- package/dist/sandbox-ref.js.map +1 -1
- package/dist/sandbox.d.ts +26 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +67 -19
- package/dist/sandbox.js.map +1 -1
- package/dist/secret-provider.d.ts +24 -0
- package/dist/secret-provider.d.ts.map +1 -0
- package/dist/secret-provider.js +36 -0
- package/dist/secret-provider.js.map +1 -0
- package/dist/session-submission-executor.d.ts +15 -0
- package/dist/session-submission-executor.d.ts.map +1 -0
- package/dist/session-submission-executor.js +79 -0
- package/dist/session-submission-executor.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +24 -10
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +28 -0
- package/dist/store.js.map +1 -1
- package/dist/strict.d.ts +20 -7
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +9 -3
- package/dist/strict.js.map +1 -1
- package/dist/submission-context.d.ts +5 -0
- package/dist/submission-context.d.ts.map +1 -1
- package/dist/submission-context.js.map +1 -1
- package/dist/submission-runner.d.ts +4 -0
- package/dist/submission-runner.d.ts.map +1 -1
- package/dist/submission-runner.js +13 -4
- package/dist/submission-runner.js.map +1 -1
- package/dist/submission-store.d.ts +6 -0
- package/dist/submission-store.d.ts.map +1 -1
- package/dist/submission-store.js.map +1 -1
- package/dist/testing-contracts.d.ts +4 -0
- package/dist/testing-contracts.d.ts.map +1 -1
- package/dist/testing-contracts.js +2 -0
- package/dist/testing-contracts.js.map +1 -1
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/virtual-sandbox.d.ts.map +1 -1
- package/dist/virtual-sandbox.js +9 -2
- package/dist/virtual-sandbox.js.map +1 -1
- package/package.json +12 -6
- package/dist/lite.d.ts +0 -19
- package/dist/lite.d.ts.map +0 -1
- package/dist/lite.js +0 -19
- package/dist/lite.js.map +0 -1
package/dist/model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash, createHmac } from
|
|
2
|
-
import { applyEstimatedCost } from
|
|
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(
|
|
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(
|
|
34
|
-
await options.onAttempt?.({
|
|
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(
|
|
45
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
40
46
|
await options.onAttempt?.({
|
|
41
47
|
provider: provider.name,
|
|
42
48
|
attempt,
|
|
43
49
|
maxAttempts,
|
|
44
|
-
status:
|
|
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(
|
|
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 ?
|
|
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(
|
|
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 =
|
|
91
|
+
name = "mock";
|
|
86
92
|
async generate(request) {
|
|
87
|
-
const latest = [...request.messages].reverse().find((message) => message.role ===
|
|
88
|
-
const text = latest?.content ??
|
|
89
|
-
const hasToolResults = request.messages.some((message) => message.role ===
|
|
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 ===
|
|
95
|
-
.map((message) => `${message.name ??
|
|
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(
|
|
103
|
+
? `Mock response using tool results:\n${toolSummaries.join("\n")}`
|
|
98
104
|
: `Mock response: ${text}`;
|
|
99
|
-
return {
|
|
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(
|
|
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 !==
|
|
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
|
|
130
|
-
case
|
|
131
|
-
return
|
|
132
|
-
case
|
|
133
|
-
return
|
|
134
|
-
case
|
|
135
|
-
case
|
|
136
|
-
return
|
|
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
|
|
151
|
+
case "minimal":
|
|
143
152
|
return 1024;
|
|
144
|
-
case
|
|
153
|
+
case "low":
|
|
145
154
|
return 2048;
|
|
146
|
-
case
|
|
155
|
+
case "medium":
|
|
147
156
|
return 4096;
|
|
148
|
-
case
|
|
157
|
+
case "high":
|
|
149
158
|
return 8192;
|
|
150
|
-
case
|
|
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
|
|
166
|
+
case "minimal":
|
|
158
167
|
return 512;
|
|
159
|
-
case
|
|
168
|
+
case "low":
|
|
160
169
|
return 1024;
|
|
161
|
-
case
|
|
170
|
+
case "medium":
|
|
162
171
|
return 4096;
|
|
163
|
-
case
|
|
172
|
+
case "high":
|
|
164
173
|
return 12288;
|
|
165
|
-
case
|
|
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 ??
|
|
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:
|
|
217
|
+
method: "POST",
|
|
202
218
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
203
219
|
headers: {
|
|
204
|
-
|
|
220
|
+
"content-type": "application/json",
|
|
205
221
|
authorization: `Bearer ${token}`,
|
|
206
|
-
...
|
|
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)
|
|
228
|
+
...(shouldApplyReasoning(model, request.thinkingLevel)
|
|
229
|
+
? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) }
|
|
230
|
+
: {}),
|
|
213
231
|
}),
|
|
214
232
|
};
|
|
215
|
-
const response = await
|
|
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:
|
|
259
|
+
method: "POST",
|
|
241
260
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
242
261
|
headers: {
|
|
243
|
-
|
|
262
|
+
"content-type": "application/json",
|
|
244
263
|
authorization: `Bearer ${token}`,
|
|
245
|
-
accept:
|
|
246
|
-
...
|
|
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)
|
|
273
|
+
...(shouldApplyReasoning(model, request.thinkingLevel)
|
|
274
|
+
? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) }
|
|
275
|
+
: {}),
|
|
255
276
|
}),
|
|
256
277
|
};
|
|
257
|
-
const response = await
|
|
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 ===
|
|
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 ===
|
|
275
|
-
const reasoningDelta = typeof 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 ===
|
|
288
|
-
const buf = toolCallBuffers.get(idx) ?? { args:
|
|
289
|
-
if (typeof tc.id ===
|
|
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 ===
|
|
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 ===
|
|
323
|
+
if (typeof u.prompt_tokens === "number")
|
|
303
324
|
usage.inputTokens = u.prompt_tokens;
|
|
304
|
-
if (typeof u.completion_tokens ===
|
|
325
|
+
if (typeof u.completion_tokens === "number")
|
|
305
326
|
usage.outputTokens = u.completion_tokens;
|
|
306
|
-
if (typeof u.total_tokens ===
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
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:
|
|
367
|
+
method: "POST",
|
|
347
368
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
348
|
-
headers: {
|
|
349
|
-
body: JSON.stringify({
|
|
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 ??
|
|
366
|
-
this.baseUrl = (options.baseUrl ??
|
|
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:
|
|
401
|
+
method: "POST",
|
|
378
402
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
379
|
-
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 ??
|
|
421
|
+
this.name = options.name ?? "vertex-ai";
|
|
398
422
|
this.project = options.project;
|
|
399
|
-
this.location = options.location ??
|
|
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:
|
|
435
|
+
method: "POST",
|
|
412
436
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
413
|
-
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 ??
|
|
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:
|
|
478
|
+
method: "POST",
|
|
451
479
|
host,
|
|
452
480
|
path: pathName,
|
|
453
481
|
region: this.region,
|
|
454
|
-
service:
|
|
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: {
|
|
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 ??
|
|
477
|
-
this.baseUrl = (options.baseUrl ??
|
|
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:
|
|
521
|
+
method: "POST",
|
|
489
522
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
490
|
-
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 ??
|
|
541
|
+
this.name = options.name ?? "fallback";
|
|
505
542
|
if (options.providers.length === 0)
|
|
506
|
-
throw new Error(
|
|
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 ??
|
|
533
|
-
this.baseUrl = (options.baseUrl ??
|
|
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)
|
|
550
|
-
|
|
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:
|
|
593
|
+
method: "POST",
|
|
553
594
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
554
|
-
headers: {
|
|
555
|
-
|
|
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)
|
|
571
|
-
|
|
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:
|
|
632
|
+
method: "POST",
|
|
574
633
|
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
575
634
|
headers: {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
accept:
|
|
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
|
|
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 ===
|
|
604
|
-
const idx = typeof data.index ===
|
|
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 ===
|
|
607
|
-
const buf = { args:
|
|
608
|
-
if (typeof block.id ===
|
|
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 ===
|
|
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 ===
|
|
616
|
-
const idx = typeof data.index ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
698
|
+
if (typeof u.input_tokens === "number")
|
|
638
699
|
usage.inputTokens = u.input_tokens;
|
|
639
|
-
if (typeof u.output_tokens ===
|
|
700
|
+
if (typeof u.output_tokens === "number")
|
|
640
701
|
usage.outputTokens = u.output_tokens;
|
|
641
|
-
if (typeof u.cache_read_input_tokens ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
716
|
+
if (typeof u.input_tokens === "number")
|
|
656
717
|
usage.inputTokens = u.input_tokens;
|
|
657
|
-
if (typeof u.cache_read_input_tokens ===
|
|
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 ===
|
|
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:
|
|
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 =
|
|
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(
|
|
690
|
-
const end = text.indexOf(
|
|
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) => ({
|
|
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 ===
|
|
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(
|
|
787
|
+
signal.removeEventListener("abort", onAbort);
|
|
723
788
|
resolve();
|
|
724
789
|
}, ms);
|
|
725
790
|
const onAbort = () => {
|
|
726
791
|
clearTimeout(timer);
|
|
727
|
-
signal.removeEventListener(
|
|
792
|
+
signal.removeEventListener("abort", onAbort);
|
|
728
793
|
reject(createAbortError(signal.reason));
|
|
729
794
|
};
|
|
730
|
-
signal.addEventListener(
|
|
795
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
731
796
|
});
|
|
732
797
|
}
|
|
733
798
|
function createAbortError(reason) {
|
|
734
|
-
const message = reason instanceof Error
|
|
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 =
|
|
805
|
+
err.name = "AbortError";
|
|
737
806
|
return err;
|
|
738
807
|
}
|
|
739
808
|
export function toOpenAIMessage(message) {
|
|
740
|
-
if (message.role ===
|
|
741
|
-
return {
|
|
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 ===
|
|
816
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
744
817
|
return {
|
|
745
|
-
role:
|
|
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:
|
|
828
|
+
type: "function",
|
|
756
829
|
function: {
|
|
757
830
|
name: call.name,
|
|
758
|
-
arguments: typeof 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:
|
|
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 !==
|
|
774
|
-
return { type:
|
|
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 !==
|
|
778
|
-
return { type:
|
|
850
|
+
if (objectSchema.type !== "object")
|
|
851
|
+
return { type: "object", properties: {}, required: [], additionalProperties: false };
|
|
779
852
|
return {
|
|
780
853
|
...objectSchema,
|
|
781
|
-
properties: typeof 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
|
|
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
|
-
...(
|
|
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 {
|
|
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
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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 {
|
|
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
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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)
|
|
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
|
|
842
|
-
|
|
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 {
|
|
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 ===
|
|
1021
|
+
if (typeof part.text === "string")
|
|
854
1022
|
texts.push(part.text);
|
|
855
1023
|
if (part.functionCall?.name)
|
|
856
|
-
toolCalls.push({
|
|
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
|
|
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
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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 {
|
|
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 ===
|
|
1100
|
+
if (typeof part.text === "string")
|
|
899
1101
|
texts.push(part.text);
|
|
900
1102
|
if (part.toolUse?.toolUseId && part.toolUse.name)
|
|
901
|
-
toolCalls.push({
|
|
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:
|
|
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 ===
|
|
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
|
|
925
|
-
|
|
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 ===
|
|
1159
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
945
1160
|
texts.push(block.text);
|
|
946
|
-
if ((block.type ===
|
|
1161
|
+
if ((block.type === "thinking" || block.type === "redacted_thinking") &&
|
|
1162
|
+
typeof block.text === "string")
|
|
947
1163
|
thinkingChunks.push(block.text);
|
|
948
|
-
if (block.type ===
|
|
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
|
|
953
|
-
|
|
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(
|
|
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] ===
|
|
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 ===
|
|
1227
|
+
if (ch === "\n") {
|
|
1010
1228
|
lines.push(buffer.slice(start, i));
|
|
1011
1229
|
start = i + 1;
|
|
1012
1230
|
}
|
|
1013
|
-
else if (ch ===
|
|
1014
|
-
if (i + 1 < buffer.length && buffer[i + 1] ===
|
|
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(
|
|
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(
|
|
1308
|
+
if (trimmed.startsWith("event:")) {
|
|
1091
1309
|
pendingEvent = trimmed.slice(6).trim();
|
|
1092
1310
|
continue;
|
|
1093
1311
|
}
|
|
1094
|
-
if (trimmed.startsWith(
|
|
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 =
|
|
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
|
-
|
|
1163
|
-
...(input.sessionToken ? {
|
|
1380
|
+
"x-amz-date": amzDate,
|
|
1381
|
+
...(input.sessionToken ? { "x-amz-security-token": input.sessionToken } : {}),
|
|
1164
1382
|
};
|
|
1165
|
-
const signedHeaders = Object.keys(headers)
|
|
1166
|
-
|
|
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 = [
|
|
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 = [
|
|
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(
|
|
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(
|
|
1413
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1180
1414
|
}
|
|
1181
1415
|
function hmac(key, value) {
|
|
1182
|
-
return createHmac(
|
|
1416
|
+
return createHmac("sha256", key).update(value).digest();
|
|
1183
1417
|
}
|
|
1184
1418
|
function hmacHex(key, value) {
|
|
1185
|
-
return createHmac(
|
|
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),
|
|
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(
|
|
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 ??
|
|
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 ??
|
|
1205
|
-
if (status === 400 &&
|
|
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 ??
|
|
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);
|