@fabric-harness/sdk 0.1.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/LICENSE +202 -0
- package/README.md +73 -0
- package/dist/agent-definition.d.ts +61 -0
- package/dist/agent-definition.d.ts.map +1 -0
- package/dist/agent-definition.js +52 -0
- package/dist/agent-definition.js.map +1 -0
- package/dist/agent.d.ts +13 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +99 -0
- package/dist/agent.js.map +1 -0
- package/dist/context-budget.d.ts +20 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +27 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +48 -0
- package/dist/error.js.map +1 -0
- package/dist/filesystem-source.d.ts +69 -0
- package/dist/filesystem-source.d.ts.map +1 -0
- package/dist/filesystem-source.js +109 -0
- package/dist/filesystem-source.js.map +1 -0
- package/dist/history.d.ts +16 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +118 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.d.ts +46 -0
- package/dist/lite.d.ts.map +1 -0
- package/dist/lite.js +38 -0
- package/dist/lite.js.map +1 -0
- package/dist/loop.d.ts +41 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +94 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-stdio.d.ts +28 -0
- package/dist/mcp-stdio.d.ts.map +1 -0
- package/dist/mcp-stdio.js +102 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +153 -0
- package/dist/mcp.js.map +1 -0
- package/dist/model.d.ts +204 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +661 -0
- package/dist/model.js.map +1 -0
- package/dist/otel.d.ts +17 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +25 -0
- package/dist/otel.js.map +1 -0
- package/dist/policy.d.ts +54 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +148 -0
- package/dist/policy.js.map +1 -0
- package/dist/providers.d.ts +37 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +265 -0
- package/dist/providers.js.map +1 -0
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +41 -0
- package/dist/redaction.js.map +1 -0
- package/dist/result.d.ts +7 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +105 -0
- package/dist/result.js.map +1 -0
- package/dist/runtime-mode.d.ts +30 -0
- package/dist/runtime-mode.d.ts.map +1 -0
- package/dist/runtime-mode.js +42 -0
- package/dist/runtime-mode.js.map +1 -0
- package/dist/sandbox.d.ts +247 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +922 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema.d.ts +37 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +121 -0
- package/dist/schema.js.map +1 -0
- package/dist/session.d.ts +60 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +762 -0
- package/dist/session.js.map +1 -0
- package/dist/store.d.ts +48 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +348 -0
- package/dist/store.js.map +1 -0
- package/dist/telemetry.d.ts +15 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +51 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tools.d.ts +131 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +235 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +71 -0
package/dist/model.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
export function toolsToModelSchemas(tools) {
|
|
3
|
+
return [...tools].map((tool) => ({
|
|
4
|
+
name: tool.name,
|
|
5
|
+
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
6
|
+
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
export async function generateWithRuntime(provider, request, options = {}) {
|
|
10
|
+
const maxAttempts = (options.retries ?? 0) + 1;
|
|
11
|
+
let lastError;
|
|
12
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
13
|
+
const startedAt = Date.now();
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeout = options.timeoutMs
|
|
16
|
+
? setTimeout(() => controller.abort(new Error(`Model call timed out after ${options.timeoutMs}ms`)), options.timeoutMs)
|
|
17
|
+
: undefined;
|
|
18
|
+
const abortListener = () => controller.abort(options.signal?.reason);
|
|
19
|
+
options.signal?.addEventListener('abort', abortListener, { once: true });
|
|
20
|
+
await options.onAttempt?.({ provider: provider.name, attempt, maxAttempts, status: 'start' });
|
|
21
|
+
try {
|
|
22
|
+
const response = await provider.generate({ ...request, signal: controller.signal });
|
|
23
|
+
if (timeout)
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
options.signal?.removeEventListener('abort', abortListener);
|
|
26
|
+
await options.onAttempt?.({
|
|
27
|
+
provider: provider.name,
|
|
28
|
+
attempt,
|
|
29
|
+
maxAttempts,
|
|
30
|
+
status: 'success',
|
|
31
|
+
durationMs: Date.now() - startedAt,
|
|
32
|
+
...(response.usage !== undefined ? { usage: response.usage } : {}),
|
|
33
|
+
});
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (timeout)
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
options.signal?.removeEventListener('abort', abortListener);
|
|
40
|
+
lastError = error;
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
const canRetry = attempt < maxAttempts && !options.signal?.aborted && isRetryableModelError(error);
|
|
43
|
+
await options.onAttempt?.({
|
|
44
|
+
provider: provider.name,
|
|
45
|
+
attempt,
|
|
46
|
+
maxAttempts,
|
|
47
|
+
status: canRetry ? 'retry' : 'error',
|
|
48
|
+
durationMs: Date.now() - startedAt,
|
|
49
|
+
error: message,
|
|
50
|
+
});
|
|
51
|
+
if (!canRetry)
|
|
52
|
+
break;
|
|
53
|
+
await delay(retryDelay(options.retryDelayMs ?? 250, attempt));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
57
|
+
}
|
|
58
|
+
export class MockModelProvider {
|
|
59
|
+
name = 'mock';
|
|
60
|
+
async generate(request) {
|
|
61
|
+
const latest = [...request.messages].reverse().find((message) => message.role === 'user');
|
|
62
|
+
const text = latest?.content ?? '';
|
|
63
|
+
const hasToolResults = request.messages.some((message) => message.role === 'tool');
|
|
64
|
+
const scripted = hasToolResults ? [] : parseScriptedToolCalls(text);
|
|
65
|
+
if (scripted.length > 0)
|
|
66
|
+
return { toolCalls: scripted, usage: estimateUsage(request.messages) };
|
|
67
|
+
const toolSummaries = request.messages
|
|
68
|
+
.filter((message) => message.role === 'tool')
|
|
69
|
+
.map((message) => `${message.name ?? 'tool'}: ${message.content}`);
|
|
70
|
+
const content = toolSummaries.length > 0
|
|
71
|
+
? `Mock response using tool results:\n${toolSummaries.join('\n')}`
|
|
72
|
+
: `Mock response: ${text}`;
|
|
73
|
+
return { message: { role: 'assistant', content }, usage: estimateUsage([...request.messages, { role: 'assistant', content }]) };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class OpenAICompatibleModelProvider {
|
|
77
|
+
name;
|
|
78
|
+
baseUrl;
|
|
79
|
+
apiKey;
|
|
80
|
+
defaultModel;
|
|
81
|
+
headers;
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.name = options.name ?? 'openai-compatible';
|
|
84
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
85
|
+
this.apiKey = options.apiKey;
|
|
86
|
+
if (options.defaultModel !== undefined)
|
|
87
|
+
this.defaultModel = options.defaultModel;
|
|
88
|
+
this.headers = options.headers ?? {};
|
|
89
|
+
}
|
|
90
|
+
async generate(request) {
|
|
91
|
+
const model = request.model ?? this.defaultModel;
|
|
92
|
+
if (!model)
|
|
93
|
+
throw new Error(`${this.name}: model is required.`);
|
|
94
|
+
const init = {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
97
|
+
headers: {
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
100
|
+
...this.headers,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
model,
|
|
104
|
+
messages: request.messages.map(toOpenAIMessage),
|
|
105
|
+
...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, init);
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const body = await response.text();
|
|
111
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, body);
|
|
112
|
+
}
|
|
113
|
+
let json;
|
|
114
|
+
try {
|
|
115
|
+
json = (await response.json());
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
throw new Error(`${this.name}: failed to parse response as JSON: ${message}`);
|
|
120
|
+
}
|
|
121
|
+
return openAIChatCompletionToModelResponse(json);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export class AzureOpenAIModelProvider {
|
|
125
|
+
name;
|
|
126
|
+
baseUrl;
|
|
127
|
+
apiKey;
|
|
128
|
+
deployment;
|
|
129
|
+
apiVersion;
|
|
130
|
+
headers;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
this.name = options.name ?? 'azure-openai';
|
|
133
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
134
|
+
this.apiKey = options.apiKey;
|
|
135
|
+
this.deployment = options.deployment;
|
|
136
|
+
this.apiVersion = options.apiVersion ?? '2024-10-21';
|
|
137
|
+
this.headers = options.headers ?? {};
|
|
138
|
+
}
|
|
139
|
+
async generate(request) {
|
|
140
|
+
const response = await fetch(`${this.baseUrl}/openai/deployments/${encodeURIComponent(this.deployment)}/chat/completions?api-version=${encodeURIComponent(this.apiVersion)}`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
143
|
+
headers: { 'content-type': 'application/json', 'api-key': this.apiKey, ...this.headers },
|
|
144
|
+
body: JSON.stringify({ messages: request.messages.map(toOpenAIMessage), ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}) }),
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
throw formatProviderHttpError(this.name, this.deployment, response.status, response.statusText, await response.text());
|
|
148
|
+
return openAIChatCompletionToModelResponse(await response.json());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export class GeminiModelProvider {
|
|
152
|
+
name;
|
|
153
|
+
baseUrl;
|
|
154
|
+
apiKey;
|
|
155
|
+
defaultModel;
|
|
156
|
+
headers;
|
|
157
|
+
constructor(options) {
|
|
158
|
+
this.name = options.name ?? 'gemini';
|
|
159
|
+
this.baseUrl = (options.baseUrl ?? 'https://generativelanguage.googleapis.com/v1beta').replace(/\/$/, '');
|
|
160
|
+
this.apiKey = options.apiKey;
|
|
161
|
+
if (options.defaultModel !== undefined)
|
|
162
|
+
this.defaultModel = options.defaultModel;
|
|
163
|
+
this.headers = options.headers ?? {};
|
|
164
|
+
}
|
|
165
|
+
async generate(request) {
|
|
166
|
+
const model = request.model ?? this.defaultModel;
|
|
167
|
+
if (!model)
|
|
168
|
+
throw new Error(`${this.name}: model is required.`);
|
|
169
|
+
const response = await fetch(`${this.baseUrl}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(this.apiKey)}`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
172
|
+
headers: { 'content-type': 'application/json', ...this.headers },
|
|
173
|
+
body: JSON.stringify(toGeminiRequest(request)),
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok)
|
|
176
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text());
|
|
177
|
+
return geminiResponseToModelResponse(await response.json());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export class VertexAIModelProvider {
|
|
181
|
+
name;
|
|
182
|
+
project;
|
|
183
|
+
location;
|
|
184
|
+
accessToken;
|
|
185
|
+
defaultModel;
|
|
186
|
+
headers;
|
|
187
|
+
constructor(options) {
|
|
188
|
+
this.name = options.name ?? 'vertex-ai';
|
|
189
|
+
this.project = options.project;
|
|
190
|
+
this.location = options.location ?? 'us-central1';
|
|
191
|
+
this.accessToken = options.accessToken;
|
|
192
|
+
if (options.defaultModel !== undefined)
|
|
193
|
+
this.defaultModel = options.defaultModel;
|
|
194
|
+
this.headers = options.headers ?? {};
|
|
195
|
+
}
|
|
196
|
+
async generate(request) {
|
|
197
|
+
const model = request.model ?? this.defaultModel;
|
|
198
|
+
if (!model)
|
|
199
|
+
throw new Error(`${this.name}: model is required.`);
|
|
200
|
+
const url = `https://${this.location}-aiplatform.googleapis.com/v1/projects/${encodeURIComponent(this.project)}/locations/${encodeURIComponent(this.location)}/publishers/google/models/${encodeURIComponent(model)}:generateContent`;
|
|
201
|
+
const response = await fetch(url, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
204
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${this.accessToken}`, ...this.headers },
|
|
205
|
+
body: JSON.stringify(toGeminiRequest(request)),
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok)
|
|
208
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text());
|
|
209
|
+
return geminiResponseToModelResponse(await response.json());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export class BedrockModelProvider {
|
|
213
|
+
name;
|
|
214
|
+
region;
|
|
215
|
+
accessKeyId;
|
|
216
|
+
secretAccessKey;
|
|
217
|
+
sessionToken;
|
|
218
|
+
defaultModel;
|
|
219
|
+
headers;
|
|
220
|
+
constructor(options) {
|
|
221
|
+
this.name = options.name ?? 'bedrock';
|
|
222
|
+
this.region = options.region;
|
|
223
|
+
this.accessKeyId = options.accessKeyId;
|
|
224
|
+
this.secretAccessKey = options.secretAccessKey;
|
|
225
|
+
if (options.sessionToken !== undefined)
|
|
226
|
+
this.sessionToken = options.sessionToken;
|
|
227
|
+
if (options.defaultModel !== undefined)
|
|
228
|
+
this.defaultModel = options.defaultModel;
|
|
229
|
+
this.headers = options.headers ?? {};
|
|
230
|
+
}
|
|
231
|
+
async generate(request) {
|
|
232
|
+
const model = request.model ?? this.defaultModel;
|
|
233
|
+
if (!model)
|
|
234
|
+
throw new Error(`${this.name}: model is required.`);
|
|
235
|
+
const host = `bedrock-runtime.${this.region}.amazonaws.com`;
|
|
236
|
+
const pathName = `/model/${encodeURIComponent(model)}/converse`;
|
|
237
|
+
const body = JSON.stringify(toBedrockRequest(request));
|
|
238
|
+
const headers = signAwsRequest({
|
|
239
|
+
method: 'POST',
|
|
240
|
+
host,
|
|
241
|
+
path: pathName,
|
|
242
|
+
region: this.region,
|
|
243
|
+
service: 'bedrock',
|
|
244
|
+
accessKeyId: this.accessKeyId,
|
|
245
|
+
secretAccessKey: this.secretAccessKey,
|
|
246
|
+
...(this.sessionToken ? { sessionToken: this.sessionToken } : {}),
|
|
247
|
+
body,
|
|
248
|
+
headers: { 'content-type': 'application/json', ...this.headers },
|
|
249
|
+
});
|
|
250
|
+
const response = await fetch(`https://${host}${pathName}`, { method: 'POST', ...(request.signal !== undefined ? { signal: request.signal } : {}), headers, body });
|
|
251
|
+
if (!response.ok)
|
|
252
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text());
|
|
253
|
+
return bedrockResponseToModelResponse(await response.json());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export class CohereModelProvider {
|
|
257
|
+
name;
|
|
258
|
+
baseUrl;
|
|
259
|
+
apiKey;
|
|
260
|
+
defaultModel;
|
|
261
|
+
headers;
|
|
262
|
+
constructor(options) {
|
|
263
|
+
this.name = options.name ?? 'cohere';
|
|
264
|
+
this.baseUrl = (options.baseUrl ?? 'https://api.cohere.com/v2').replace(/\/$/, '');
|
|
265
|
+
this.apiKey = options.apiKey;
|
|
266
|
+
if (options.defaultModel !== undefined)
|
|
267
|
+
this.defaultModel = options.defaultModel;
|
|
268
|
+
this.headers = options.headers ?? {};
|
|
269
|
+
}
|
|
270
|
+
async generate(request) {
|
|
271
|
+
const model = request.model ?? this.defaultModel;
|
|
272
|
+
if (!model)
|
|
273
|
+
throw new Error(`${this.name}: model is required.`);
|
|
274
|
+
const response = await fetch(`${this.baseUrl}/chat`, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
277
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${this.apiKey}`, ...this.headers },
|
|
278
|
+
body: JSON.stringify({ model, messages: request.messages.map(toCohereMessage) }),
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok)
|
|
281
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text());
|
|
282
|
+
return cohereResponseToModelResponse(await response.json());
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export class FallbackModelProvider {
|
|
286
|
+
name;
|
|
287
|
+
providers;
|
|
288
|
+
constructor(options) {
|
|
289
|
+
this.name = options.name ?? 'fallback';
|
|
290
|
+
if (options.providers.length === 0)
|
|
291
|
+
throw new Error('FallbackModelProvider requires at least one provider.');
|
|
292
|
+
this.providers = options.providers;
|
|
293
|
+
}
|
|
294
|
+
async generate(request) {
|
|
295
|
+
const errors = [];
|
|
296
|
+
for (const provider of this.providers) {
|
|
297
|
+
try {
|
|
298
|
+
return await provider.generate(request);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
errors.push(`${provider.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`${this.name}: all fallback providers failed. ${errors.join(' | ')}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export class AnthropicModelProvider {
|
|
308
|
+
name;
|
|
309
|
+
baseUrl;
|
|
310
|
+
apiKey;
|
|
311
|
+
defaultModel;
|
|
312
|
+
headers;
|
|
313
|
+
maxTokens;
|
|
314
|
+
constructor(options) {
|
|
315
|
+
this.name = options.name ?? 'anthropic';
|
|
316
|
+
this.baseUrl = (options.baseUrl ?? 'https://api.anthropic.com/v1').replace(/\/$/, '');
|
|
317
|
+
this.apiKey = options.apiKey;
|
|
318
|
+
if (options.defaultModel !== undefined)
|
|
319
|
+
this.defaultModel = options.defaultModel;
|
|
320
|
+
this.headers = options.headers ?? {};
|
|
321
|
+
this.maxTokens = options.maxTokens ?? 1024;
|
|
322
|
+
}
|
|
323
|
+
async generate(request) {
|
|
324
|
+
const model = request.model ?? this.defaultModel;
|
|
325
|
+
if (!model)
|
|
326
|
+
throw new Error(`${this.name}: model is required.`);
|
|
327
|
+
const { system, messages } = toAnthropicMessages(request.messages);
|
|
328
|
+
const response = await fetch(`${this.baseUrl}/messages`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
331
|
+
headers: { 'content-type': 'application/json', 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01', ...this.headers },
|
|
332
|
+
body: JSON.stringify({ model, max_tokens: this.maxTokens, ...(system ? { system } : {}), messages, ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}) }),
|
|
333
|
+
});
|
|
334
|
+
if (!response.ok)
|
|
335
|
+
throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text());
|
|
336
|
+
return anthropicResponseToModelResponse(await response.json());
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export const defaultModelProvider = new MockModelProvider();
|
|
340
|
+
function parseScriptedToolCalls(text) {
|
|
341
|
+
const marker = '```fabric-tool-calls';
|
|
342
|
+
const start = text.indexOf(marker);
|
|
343
|
+
if (start === -1)
|
|
344
|
+
return [];
|
|
345
|
+
const jsonStart = text.indexOf('\n', start);
|
|
346
|
+
const end = text.indexOf('```', jsonStart + 1);
|
|
347
|
+
if (jsonStart === -1 || end === -1)
|
|
348
|
+
return [];
|
|
349
|
+
const raw = text.slice(jsonStart + 1, end).trim();
|
|
350
|
+
const parsed = JSON.parse(raw);
|
|
351
|
+
return parsed.map((call, index) => ({ id: call.id ?? `mock-tool-${index + 1}`, name: call.tool, input: call.input }));
|
|
352
|
+
}
|
|
353
|
+
function estimateUsage(messages) {
|
|
354
|
+
const chars = messages.reduce((sum, message) => sum + message.content.length, 0);
|
|
355
|
+
return { totalTokens: Math.ceil(chars / 4) };
|
|
356
|
+
}
|
|
357
|
+
function retryDelay(baseMs, attempt) {
|
|
358
|
+
return Math.min(30_000, Math.round(baseMs * 2 ** Math.max(0, attempt - 1)));
|
|
359
|
+
}
|
|
360
|
+
function isRetryableModelError(error) {
|
|
361
|
+
if (error instanceof ProviderHttpError)
|
|
362
|
+
return error.status === 429 || error.status >= 500;
|
|
363
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
364
|
+
return false;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
function delay(ms) {
|
|
368
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
369
|
+
}
|
|
370
|
+
function toOpenAIMessage(message) {
|
|
371
|
+
if (message.role === 'tool') {
|
|
372
|
+
return { role: 'tool', tool_call_id: message.toolCallId ?? message.name ?? 'tool', content: message.content };
|
|
373
|
+
}
|
|
374
|
+
if (message.role === 'assistant' && message.toolCalls?.length) {
|
|
375
|
+
return {
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
content: message.content || null,
|
|
378
|
+
tool_calls: message.toolCalls.map(toOpenAIToolCallForMessage),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { role: message.role, content: message.content };
|
|
382
|
+
}
|
|
383
|
+
function toOpenAIToolCallForMessage(call) {
|
|
384
|
+
return {
|
|
385
|
+
id: call.id,
|
|
386
|
+
type: 'function',
|
|
387
|
+
function: {
|
|
388
|
+
name: call.name,
|
|
389
|
+
arguments: typeof call.input === 'string' ? call.input : JSON.stringify(call.input),
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function toOpenAITool(tool) {
|
|
394
|
+
return {
|
|
395
|
+
type: 'function',
|
|
396
|
+
function: {
|
|
397
|
+
name: tool.name,
|
|
398
|
+
description: tool.description ?? '',
|
|
399
|
+
parameters: normalizeOpenAIParameters(tool.inputSchema),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function normalizeOpenAIParameters(schema) {
|
|
404
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
405
|
+
return { type: 'object', properties: {}, required: [], additionalProperties: false };
|
|
406
|
+
}
|
|
407
|
+
const objectSchema = schema;
|
|
408
|
+
if (objectSchema.type !== 'object')
|
|
409
|
+
return { type: 'object', properties: {}, required: [], additionalProperties: false };
|
|
410
|
+
return {
|
|
411
|
+
...objectSchema,
|
|
412
|
+
properties: typeof objectSchema.properties === 'object' && objectSchema.properties !== null && !Array.isArray(objectSchema.properties) ? objectSchema.properties : {},
|
|
413
|
+
required: Array.isArray(objectSchema.required) ? objectSchema.required : [],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function fromOpenAIToolCall(call) {
|
|
417
|
+
return {
|
|
418
|
+
id: call.id,
|
|
419
|
+
name: call.function.name,
|
|
420
|
+
input: call.function.arguments ? JSON.parse(call.function.arguments) : {},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function openAIChatCompletionToModelResponse(json) {
|
|
424
|
+
const choice = json.choices?.[0];
|
|
425
|
+
const message = choice?.message;
|
|
426
|
+
const result = {
|
|
427
|
+
...(message?.content ? { message: { role: 'assistant', content: message.content } } : {}),
|
|
428
|
+
...(message?.tool_calls ? { toolCalls: message.tool_calls.map(fromOpenAIToolCall) } : {}),
|
|
429
|
+
};
|
|
430
|
+
if (json.usage) {
|
|
431
|
+
const usage = {};
|
|
432
|
+
if (json.usage.prompt_tokens !== undefined)
|
|
433
|
+
usage.inputTokens = json.usage.prompt_tokens;
|
|
434
|
+
if (json.usage.completion_tokens !== undefined)
|
|
435
|
+
usage.outputTokens = json.usage.completion_tokens;
|
|
436
|
+
if (json.usage.total_tokens !== undefined)
|
|
437
|
+
usage.totalTokens = json.usage.total_tokens;
|
|
438
|
+
result.usage = usage;
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
function toAnthropicTool(tool) {
|
|
443
|
+
return { name: tool.name, description: tool.description ?? '', input_schema: normalizeOpenAIParameters(tool.inputSchema) };
|
|
444
|
+
}
|
|
445
|
+
function toAnthropicMessages(messages) {
|
|
446
|
+
const system = messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n') || undefined;
|
|
447
|
+
const converted = messages.filter((message) => message.role !== 'system').map((message) => {
|
|
448
|
+
if (message.role === 'tool')
|
|
449
|
+
return { role: 'user', content: [{ type: 'tool_result', tool_use_id: message.toolCallId ?? message.name ?? 'tool', content: message.content }] };
|
|
450
|
+
if (message.role === 'assistant' && message.toolCalls?.length) {
|
|
451
|
+
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 }))] };
|
|
452
|
+
}
|
|
453
|
+
return { role: message.role === 'assistant' ? 'assistant' : 'user', content: message.content };
|
|
454
|
+
});
|
|
455
|
+
return { ...(system ? { system } : {}), messages: converted };
|
|
456
|
+
}
|
|
457
|
+
function toGeminiRequest(request) {
|
|
458
|
+
const system = request.messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
|
|
459
|
+
const contents = request.messages.filter((message) => message.role !== 'system').map((message) => {
|
|
460
|
+
if (message.role === 'tool')
|
|
461
|
+
return { role: 'user', parts: [{ functionResponse: { name: message.name ?? 'tool', response: { content: message.content } } }] };
|
|
462
|
+
if (message.role === 'assistant' && message.toolCalls?.length)
|
|
463
|
+
return { role: 'model', parts: message.toolCalls.map((call) => ({ functionCall: { name: call.name, args: call.input ?? {} } })) };
|
|
464
|
+
return { role: message.role === 'assistant' ? 'model' : 'user', parts: [{ text: message.content }] };
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}),
|
|
468
|
+
contents,
|
|
469
|
+
...(request.tools?.length ? { tools: [{ functionDeclarations: request.tools.map(toGeminiTool) }] } : {}),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function toGeminiTool(tool) {
|
|
473
|
+
return { name: tool.name, description: tool.description ?? '', parameters: normalizeOpenAIParameters(tool.inputSchema) };
|
|
474
|
+
}
|
|
475
|
+
function geminiResponseToModelResponse(json) {
|
|
476
|
+
const parts = json.candidates?.[0]?.content?.parts ?? [];
|
|
477
|
+
const texts = [];
|
|
478
|
+
const toolCalls = [];
|
|
479
|
+
for (const [index, part] of parts.entries()) {
|
|
480
|
+
if (typeof part.text === 'string')
|
|
481
|
+
texts.push(part.text);
|
|
482
|
+
if (part.functionCall?.name)
|
|
483
|
+
toolCalls.push({ id: `gemini-tool-${index + 1}`, name: part.functionCall.name, input: part.functionCall.args ?? {} });
|
|
484
|
+
}
|
|
485
|
+
const result = {
|
|
486
|
+
...(texts.length > 0 ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
|
|
487
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
488
|
+
};
|
|
489
|
+
if (json.usageMetadata) {
|
|
490
|
+
const usage = {};
|
|
491
|
+
if (json.usageMetadata.promptTokenCount !== undefined)
|
|
492
|
+
usage.inputTokens = json.usageMetadata.promptTokenCount;
|
|
493
|
+
if (json.usageMetadata.candidatesTokenCount !== undefined)
|
|
494
|
+
usage.outputTokens = json.usageMetadata.candidatesTokenCount;
|
|
495
|
+
if (json.usageMetadata.totalTokenCount !== undefined)
|
|
496
|
+
usage.totalTokens = json.usageMetadata.totalTokenCount;
|
|
497
|
+
result.usage = usage;
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
function toBedrockRequest(request) {
|
|
502
|
+
const system = request.messages.filter((message) => message.role === 'system').map((message) => ({ text: message.content }));
|
|
503
|
+
const messages = request.messages.filter((message) => message.role !== 'system').map((message) => {
|
|
504
|
+
if (message.role === 'tool')
|
|
505
|
+
return { role: 'user', content: [{ toolResult: { toolUseId: message.toolCallId ?? message.name ?? 'tool', content: [{ text: message.content }] } }] };
|
|
506
|
+
if (message.role === 'assistant' && message.toolCalls?.length)
|
|
507
|
+
return { role: 'assistant', content: message.toolCalls.map((call) => ({ toolUse: { toolUseId: call.id, name: call.name, input: call.input ?? {} } })) };
|
|
508
|
+
return { role: message.role === 'assistant' ? 'assistant' : 'user', content: [{ text: message.content }] };
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
...(system.length ? { system } : {}),
|
|
512
|
+
messages,
|
|
513
|
+
...(request.tools?.length ? { toolConfig: { tools: request.tools.map(toBedrockTool) } } : {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function toBedrockTool(tool) {
|
|
517
|
+
return { toolSpec: { name: tool.name, description: tool.description ?? '', inputSchema: { json: normalizeOpenAIParameters(tool.inputSchema) } } };
|
|
518
|
+
}
|
|
519
|
+
function bedrockResponseToModelResponse(json) {
|
|
520
|
+
const content = json.output?.message?.content ?? [];
|
|
521
|
+
const texts = [];
|
|
522
|
+
const toolCalls = [];
|
|
523
|
+
for (const part of content) {
|
|
524
|
+
if (typeof part.text === 'string')
|
|
525
|
+
texts.push(part.text);
|
|
526
|
+
if (part.toolUse?.toolUseId && part.toolUse.name)
|
|
527
|
+
toolCalls.push({ id: part.toolUse.toolUseId, name: part.toolUse.name, input: part.toolUse.input ?? {} });
|
|
528
|
+
}
|
|
529
|
+
const result = {
|
|
530
|
+
...(texts.length ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
|
|
531
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
532
|
+
};
|
|
533
|
+
if (json.usage) {
|
|
534
|
+
const usage = {};
|
|
535
|
+
if (json.usage.inputTokens !== undefined)
|
|
536
|
+
usage.inputTokens = json.usage.inputTokens;
|
|
537
|
+
if (json.usage.outputTokens !== undefined)
|
|
538
|
+
usage.outputTokens = json.usage.outputTokens;
|
|
539
|
+
if (json.usage.totalTokens !== undefined)
|
|
540
|
+
usage.totalTokens = json.usage.totalTokens;
|
|
541
|
+
result.usage = usage;
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
function toCohereMessage(message) {
|
|
546
|
+
const role = message.role === 'assistant' ? 'assistant' : message.role === 'system' ? 'system' : message.role === 'tool' ? 'tool' : 'user';
|
|
547
|
+
return { role, content: message.content };
|
|
548
|
+
}
|
|
549
|
+
function cohereResponseToModelResponse(json) {
|
|
550
|
+
const content = json.message?.content?.map((part) => part.text).filter((text) => typeof text === 'string').join('\n') ?? '';
|
|
551
|
+
const result = content ? { message: { role: 'assistant', content } } : {};
|
|
552
|
+
const billed = json.usage?.billed_units;
|
|
553
|
+
if (billed) {
|
|
554
|
+
const usage = {};
|
|
555
|
+
if (billed.input_tokens !== undefined)
|
|
556
|
+
usage.inputTokens = billed.input_tokens;
|
|
557
|
+
if (billed.output_tokens !== undefined)
|
|
558
|
+
usage.outputTokens = billed.output_tokens;
|
|
559
|
+
if (usage.inputTokens !== undefined || usage.outputTokens !== undefined)
|
|
560
|
+
usage.totalTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
561
|
+
result.usage = usage;
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
function anthropicResponseToModelResponse(json) {
|
|
566
|
+
const texts = [];
|
|
567
|
+
const toolCalls = [];
|
|
568
|
+
for (const block of json.content ?? []) {
|
|
569
|
+
if (block.type === 'text' && typeof block.text === 'string')
|
|
570
|
+
texts.push(block.text);
|
|
571
|
+
if (block.type === 'tool_use' && typeof block.id === 'string' && typeof block.name === 'string')
|
|
572
|
+
toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
|
|
573
|
+
}
|
|
574
|
+
const result = {
|
|
575
|
+
...(texts.length > 0 ? { message: { role: 'assistant', content: texts.join('\n') } } : {}),
|
|
576
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
577
|
+
};
|
|
578
|
+
if (json.usage) {
|
|
579
|
+
const usage = {};
|
|
580
|
+
if (json.usage.input_tokens !== undefined)
|
|
581
|
+
usage.inputTokens = json.usage.input_tokens;
|
|
582
|
+
if (json.usage.output_tokens !== undefined)
|
|
583
|
+
usage.outputTokens = json.usage.output_tokens;
|
|
584
|
+
if (usage.inputTokens !== undefined || usage.outputTokens !== undefined)
|
|
585
|
+
usage.totalTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
586
|
+
result.usage = usage;
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
export class ProviderHttpError extends Error {
|
|
591
|
+
provider;
|
|
592
|
+
status;
|
|
593
|
+
statusText;
|
|
594
|
+
body;
|
|
595
|
+
constructor(message, provider, status, statusText, body) {
|
|
596
|
+
super(message);
|
|
597
|
+
this.provider = provider;
|
|
598
|
+
this.status = status;
|
|
599
|
+
this.statusText = statusText;
|
|
600
|
+
this.body = body;
|
|
601
|
+
this.name = 'ProviderHttpError';
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function signAwsRequest(input) {
|
|
605
|
+
const now = new Date();
|
|
606
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
|
607
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
608
|
+
const headers = {
|
|
609
|
+
...input.headers,
|
|
610
|
+
host: input.host,
|
|
611
|
+
'x-amz-date': amzDate,
|
|
612
|
+
...(input.sessionToken ? { 'x-amz-security-token': input.sessionToken } : {}),
|
|
613
|
+
};
|
|
614
|
+
const signedHeaders = Object.keys(headers).map((key) => key.toLowerCase()).sort();
|
|
615
|
+
const canonicalHeaders = signedHeaders.map((key) => `${key}:${headers[Object.keys(headers).find((candidate) => candidate.toLowerCase() === key) ?? key]?.trim()}\n`).join('');
|
|
616
|
+
const payloadHash = sha256Hex(input.body);
|
|
617
|
+
const canonicalRequest = [input.method, input.path, '', canonicalHeaders, signedHeaders.join(';'), payloadHash].join('\n');
|
|
618
|
+
const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
|
|
619
|
+
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
|
|
620
|
+
const signingKey = awsSigningKey(input.secretAccessKey, dateStamp, input.region, input.service);
|
|
621
|
+
const signature = hmacHex(signingKey, stringToSign);
|
|
622
|
+
return {
|
|
623
|
+
...headers,
|
|
624
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders.join(';')}, Signature=${signature}`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function sha256Hex(value) {
|
|
628
|
+
return createHash('sha256').update(value).digest('hex');
|
|
629
|
+
}
|
|
630
|
+
function hmac(key, value) {
|
|
631
|
+
return createHmac('sha256', key).update(value).digest();
|
|
632
|
+
}
|
|
633
|
+
function hmacHex(key, value) {
|
|
634
|
+
return createHmac('sha256', key).update(value).digest('hex');
|
|
635
|
+
}
|
|
636
|
+
function awsSigningKey(secret, date, region, service) {
|
|
637
|
+
return hmac(hmac(hmac(hmac(`AWS4${secret}`, date), region), service), 'aws4_request');
|
|
638
|
+
}
|
|
639
|
+
function formatProviderHttpError(provider, model, status, statusText, body) {
|
|
640
|
+
let parsedMessage;
|
|
641
|
+
let code;
|
|
642
|
+
try {
|
|
643
|
+
const parsed = JSON.parse(body);
|
|
644
|
+
parsedMessage = parsed.error?.message;
|
|
645
|
+
code = parsed.error?.code ?? parsed.error?.type;
|
|
646
|
+
}
|
|
647
|
+
catch { }
|
|
648
|
+
const details = parsedMessage ?? body.slice(0, 500);
|
|
649
|
+
if (status === 401 || status === 403)
|
|
650
|
+
return new ProviderHttpError(`${provider}: authentication/authorization failed for model "${model ?? 'unknown'}" (${status}). Check API key and provider permissions. ${details}`, provider, status, statusText, body);
|
|
651
|
+
if (status === 400 && /model/i.test(details))
|
|
652
|
+
return new ProviderHttpError(`${provider}: provider rejected model "${model ?? 'unknown'}". Check FABRIC_MODEL/model access/base URL. ${details}`, provider, status, statusText, body);
|
|
653
|
+
if (status === 400 && (/schema|function|tool/i.test(details) || code === 'invalid_function_parameters'))
|
|
654
|
+
return new ProviderHttpError(`${provider}: provider rejected a tool schema. Run fabric-harness doctor --tools. ${details}`, provider, status, statusText, body);
|
|
655
|
+
if (status === 429)
|
|
656
|
+
return new ProviderHttpError(`${provider}: rate limited for model "${model ?? 'unknown'}". ${details}`, provider, status, statusText, body);
|
|
657
|
+
if (status >= 500)
|
|
658
|
+
return new ProviderHttpError(`${provider}: provider unavailable (${status} ${statusText}). ${details}`, provider, status, statusText, body);
|
|
659
|
+
return new ProviderHttpError(`${provider}: ${status} ${statusText}: ${details}`, provider, status, statusText, body);
|
|
660
|
+
}
|
|
661
|
+
//# sourceMappingURL=model.js.map
|