@jambonz/llm 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 +21 -0
- package/README.md +20 -0
- package/dist/index.cjs +2194 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +2152 -0
- package/dist/index.js.map +1 -0
- package/dist/test-kit/index.cjs +2645 -0
- package/dist/test-kit/index.cjs.map +1 -0
- package/dist/test-kit/index.d.cts +162 -0
- package/dist/test-kit/index.d.ts +162 -0
- package/dist/test-kit/index.js +2608 -0
- package/dist/test-kit/index.js.map +1 -0
- package/dist/types-D8rgMCbi.d.cts +280 -0
- package/dist/types-D8rgMCbi.d.ts +280 -0
- package/package.json +91 -0
|
@@ -0,0 +1,2608 @@
|
|
|
1
|
+
// src/test-kit/runner.ts
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/registry.ts
|
|
5
|
+
var registry = /* @__PURE__ */ new Map();
|
|
6
|
+
function registerAdapter(factory) {
|
|
7
|
+
if (registry.has(factory.vendor)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Adapter already registered for vendor '${factory.vendor}'. Use replaceAdapter() if you intended to override.`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
if (factory.vendor !== factory.manifest.vendor) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Adapter factory vendor '${factory.vendor}' does not match manifest.vendor '${factory.manifest.vendor}'.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
registry.set(factory.vendor, factory);
|
|
18
|
+
}
|
|
19
|
+
function getAdapterFactory(vendor) {
|
|
20
|
+
const factory = registry.get(vendor);
|
|
21
|
+
if (!factory) {
|
|
22
|
+
const known = Array.from(registry.keys()).sort().join(", ") || "(none)";
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Unknown vendor '${vendor}'. Registered vendors: ${known}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return factory;
|
|
28
|
+
}
|
|
29
|
+
function _resetRegistryForTests() {
|
|
30
|
+
registry.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/adapters/openai/adapter.ts
|
|
34
|
+
import OpenAI from "openai";
|
|
35
|
+
|
|
36
|
+
// src/validate.ts
|
|
37
|
+
function assertValidRequest(req) {
|
|
38
|
+
if (!req || typeof req !== "object") {
|
|
39
|
+
throw new Error("PromptRequest must be an object");
|
|
40
|
+
}
|
|
41
|
+
if (typeof req.model !== "string" || req.model.length === 0) {
|
|
42
|
+
throw new Error("PromptRequest.model must be a non-empty string");
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(req.messages) || req.messages.length === 0) {
|
|
45
|
+
throw new Error("PromptRequest.messages must be a non-empty array");
|
|
46
|
+
}
|
|
47
|
+
if (req.messages.some((m) => m.role === "system")) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"PromptRequest.messages must not contain role: 'system'. Pass the system prompt via PromptRequest.system instead."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/adapters/openai/_streaming.ts
|
|
55
|
+
async function* streamFromOpenAI(client, req, options) {
|
|
56
|
+
assertValidRequest(req);
|
|
57
|
+
if (req.signal?.aborted) {
|
|
58
|
+
yield { type: "end", finishReason: "aborted" };
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const capabilities = capabilitiesFor(req.model, options.knownModels);
|
|
62
|
+
const useTools = req.tools && req.tools.length > 0 && capabilities.tools;
|
|
63
|
+
const messages = buildWireMessages(req);
|
|
64
|
+
const tools = useTools ? formatTools(req.tools) : void 0;
|
|
65
|
+
const body = {
|
|
66
|
+
model: req.model,
|
|
67
|
+
messages,
|
|
68
|
+
stream: true,
|
|
69
|
+
stream_options: { include_usage: true }
|
|
70
|
+
};
|
|
71
|
+
if (tools) {
|
|
72
|
+
body.tools = tools;
|
|
73
|
+
body.tool_choice = "auto";
|
|
74
|
+
}
|
|
75
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
76
|
+
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
77
|
+
let stream;
|
|
78
|
+
try {
|
|
79
|
+
stream = await client.chat.completions.create(body, { signal: req.signal });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (isAbortError(err)) {
|
|
82
|
+
yield { type: "end", finishReason: "aborted" };
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
const pending = {};
|
|
88
|
+
const startedIds = /* @__PURE__ */ new Set();
|
|
89
|
+
const toolCallsEmitted = /* @__PURE__ */ new Set();
|
|
90
|
+
let usage;
|
|
91
|
+
let finalFinishReason = null;
|
|
92
|
+
let finalRawReason;
|
|
93
|
+
try {
|
|
94
|
+
for await (const chunk of stream) {
|
|
95
|
+
if (req.signal?.aborted) {
|
|
96
|
+
yield { type: "end", finishReason: "aborted" };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (chunk.usage) {
|
|
100
|
+
usage = {
|
|
101
|
+
...chunk.usage.prompt_tokens !== void 0 ? { inputTokens: chunk.usage.prompt_tokens } : {},
|
|
102
|
+
...chunk.usage.completion_tokens !== void 0 ? { outputTokens: chunk.usage.completion_tokens } : {},
|
|
103
|
+
...chunk.usage.total_tokens !== void 0 ? { totalTokens: chunk.usage.total_tokens } : {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const choice = chunk.choices?.[0];
|
|
107
|
+
if (!choice) continue;
|
|
108
|
+
const delta = choice.delta;
|
|
109
|
+
const finishReason = choice.finish_reason;
|
|
110
|
+
if (delta?.content) {
|
|
111
|
+
yield { type: "token", text: delta.content };
|
|
112
|
+
}
|
|
113
|
+
if (delta?.tool_calls) {
|
|
114
|
+
for (const tc of delta.tool_calls) {
|
|
115
|
+
const idx = tc.index;
|
|
116
|
+
if (!pending[idx]) {
|
|
117
|
+
pending[idx] = { id: "", name: "", arguments: "" };
|
|
118
|
+
}
|
|
119
|
+
if (tc.id) pending[idx].id += tc.id;
|
|
120
|
+
if (tc.function?.name) pending[idx].name += tc.function.name;
|
|
121
|
+
if (tc.function?.arguments) pending[idx].arguments += tc.function.arguments;
|
|
122
|
+
if (pending[idx].id && pending[idx].name && !startedIds.has(pending[idx].id)) {
|
|
123
|
+
startedIds.add(pending[idx].id);
|
|
124
|
+
yield {
|
|
125
|
+
type: "toolCallStart",
|
|
126
|
+
id: pending[idx].id,
|
|
127
|
+
name: pending[idx].name
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (finishReason === "tool_calls") {
|
|
133
|
+
if (req.signal?.aborted) {
|
|
134
|
+
yield { type: "end", finishReason: "aborted" };
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const tc of Object.values(pending)) {
|
|
138
|
+
if (!tc.id || !tc.name) continue;
|
|
139
|
+
if (toolCallsEmitted.has(tc.id)) continue;
|
|
140
|
+
toolCallsEmitted.add(tc.id);
|
|
141
|
+
let args = {};
|
|
142
|
+
try {
|
|
143
|
+
args = tc.arguments ? JSON.parse(tc.arguments) : {};
|
|
144
|
+
} catch {
|
|
145
|
+
args = {};
|
|
146
|
+
}
|
|
147
|
+
yield {
|
|
148
|
+
type: "toolCall",
|
|
149
|
+
id: tc.id,
|
|
150
|
+
name: tc.name,
|
|
151
|
+
arguments: args
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
finalFinishReason = "tool";
|
|
155
|
+
finalRawReason = finishReason;
|
|
156
|
+
} else if (finishReason) {
|
|
157
|
+
finalFinishReason = mapFinishReason(finishReason);
|
|
158
|
+
finalRawReason = finishReason;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (isAbortError(err)) {
|
|
163
|
+
yield { type: "end", finishReason: "aborted" };
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
const endEvent = {
|
|
169
|
+
type: "end",
|
|
170
|
+
finishReason: finalFinishReason ?? "stop",
|
|
171
|
+
...usage ? { usage } : {},
|
|
172
|
+
...finalRawReason ? { rawReason: finalRawReason } : {}
|
|
173
|
+
};
|
|
174
|
+
yield endEvent;
|
|
175
|
+
}
|
|
176
|
+
function appendOpenAIToolResult(history, toolCallId, result) {
|
|
177
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
178
|
+
const wireMessage = {
|
|
179
|
+
role: "tool",
|
|
180
|
+
tool_call_id: toolCallId,
|
|
181
|
+
content
|
|
182
|
+
};
|
|
183
|
+
return [
|
|
184
|
+
...history,
|
|
185
|
+
{
|
|
186
|
+
role: "tool",
|
|
187
|
+
content,
|
|
188
|
+
vendorRaw: wireMessage
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
async function listOpenAICompatibleModels(client, knownModels) {
|
|
193
|
+
const result = await client.models.list();
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const m of result.data) {
|
|
196
|
+
const known = knownModels.find((k) => k.id === m.id);
|
|
197
|
+
if (known) {
|
|
198
|
+
out.push(known);
|
|
199
|
+
} else {
|
|
200
|
+
out.push({
|
|
201
|
+
id: m.id,
|
|
202
|
+
capabilities: defaultCapabilitiesForUnknown(m.id)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function buildWireMessages(req) {
|
|
209
|
+
const out = [];
|
|
210
|
+
if (req.system) {
|
|
211
|
+
out.push({ role: "system", content: req.system });
|
|
212
|
+
}
|
|
213
|
+
for (const msg of req.messages) {
|
|
214
|
+
if (msg.vendorRaw && typeof msg.vendorRaw === "object") {
|
|
215
|
+
out.push(msg.vendorRaw);
|
|
216
|
+
} else {
|
|
217
|
+
out.push({ role: msg.role, content: msg.content });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function formatTools(tools) {
|
|
223
|
+
return tools.map((t) => ({
|
|
224
|
+
type: "function",
|
|
225
|
+
function: {
|
|
226
|
+
name: t.name,
|
|
227
|
+
description: t.description ?? "",
|
|
228
|
+
parameters: t.parameters ?? { type: "object", properties: {} }
|
|
229
|
+
}
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
function capabilitiesFor(modelId, knownModels) {
|
|
233
|
+
const known = knownModels.find((k) => k.id === modelId);
|
|
234
|
+
if (known) return known.capabilities;
|
|
235
|
+
return defaultCapabilitiesForUnknown(modelId);
|
|
236
|
+
}
|
|
237
|
+
function defaultCapabilitiesForUnknown(modelId) {
|
|
238
|
+
const isReasoning = /^o[13]([-/]|$)/i.test(modelId);
|
|
239
|
+
if (isReasoning) {
|
|
240
|
+
return {
|
|
241
|
+
streaming: false,
|
|
242
|
+
tools: false,
|
|
243
|
+
vision: false,
|
|
244
|
+
systemPrompt: false
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
streaming: true,
|
|
249
|
+
tools: true,
|
|
250
|
+
vision: false,
|
|
251
|
+
systemPrompt: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function mapFinishReason(r) {
|
|
255
|
+
switch (r) {
|
|
256
|
+
case "stop":
|
|
257
|
+
return "stop";
|
|
258
|
+
case "tool_calls":
|
|
259
|
+
return "tool";
|
|
260
|
+
case "function_call":
|
|
261
|
+
return "tool";
|
|
262
|
+
case "length":
|
|
263
|
+
return "length";
|
|
264
|
+
case "content_filter":
|
|
265
|
+
return "filtered";
|
|
266
|
+
default:
|
|
267
|
+
return "stop";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function isAbortError(err) {
|
|
271
|
+
if (!err || typeof err !== "object") return false;
|
|
272
|
+
const e = err;
|
|
273
|
+
return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ABORT_ERR" || typeof e.message === "string" && /aborted/i.test(e.message);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/adapters/openai/manifest.ts
|
|
277
|
+
var OPENAI_KNOWN_MODELS = [
|
|
278
|
+
{
|
|
279
|
+
id: "gpt-4o",
|
|
280
|
+
displayName: "GPT-4o",
|
|
281
|
+
capabilities: {
|
|
282
|
+
streaming: true,
|
|
283
|
+
tools: true,
|
|
284
|
+
vision: true,
|
|
285
|
+
systemPrompt: true,
|
|
286
|
+
maxContextTokens: 128e3
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "gpt-4o-mini",
|
|
291
|
+
displayName: "GPT-4o mini",
|
|
292
|
+
capabilities: {
|
|
293
|
+
streaming: true,
|
|
294
|
+
tools: true,
|
|
295
|
+
vision: true,
|
|
296
|
+
systemPrompt: true,
|
|
297
|
+
maxContextTokens: 128e3
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: "gpt-4-turbo",
|
|
302
|
+
displayName: "GPT-4 Turbo",
|
|
303
|
+
capabilities: {
|
|
304
|
+
streaming: true,
|
|
305
|
+
tools: true,
|
|
306
|
+
vision: true,
|
|
307
|
+
systemPrompt: true,
|
|
308
|
+
maxContextTokens: 128e3
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "gpt-3.5-turbo",
|
|
313
|
+
displayName: "GPT-3.5 Turbo",
|
|
314
|
+
capabilities: {
|
|
315
|
+
streaming: true,
|
|
316
|
+
tools: true,
|
|
317
|
+
vision: false,
|
|
318
|
+
systemPrompt: true,
|
|
319
|
+
maxContextTokens: 16385
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: "o1-preview",
|
|
324
|
+
displayName: "o1 preview (reasoning)",
|
|
325
|
+
capabilities: {
|
|
326
|
+
streaming: false,
|
|
327
|
+
tools: false,
|
|
328
|
+
vision: false,
|
|
329
|
+
systemPrompt: false,
|
|
330
|
+
maxContextTokens: 128e3
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: "o1-mini",
|
|
335
|
+
displayName: "o1 mini (reasoning)",
|
|
336
|
+
capabilities: {
|
|
337
|
+
streaming: false,
|
|
338
|
+
tools: false,
|
|
339
|
+
vision: false,
|
|
340
|
+
systemPrompt: false,
|
|
341
|
+
maxContextTokens: 128e3
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
var openAIManifest = {
|
|
346
|
+
vendor: "openai",
|
|
347
|
+
displayName: "OpenAI",
|
|
348
|
+
authKinds: [
|
|
349
|
+
{
|
|
350
|
+
kind: "apiKey",
|
|
351
|
+
displayName: "API Key",
|
|
352
|
+
fields: [
|
|
353
|
+
{
|
|
354
|
+
name: "apiKey",
|
|
355
|
+
label: "API Key",
|
|
356
|
+
type: "password",
|
|
357
|
+
required: true,
|
|
358
|
+
help: "Get an API key from https://platform.openai.com/api-keys"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "baseURL",
|
|
362
|
+
label: "Base URL",
|
|
363
|
+
type: "url",
|
|
364
|
+
required: false,
|
|
365
|
+
default: "https://api.openai.com/v1",
|
|
366
|
+
help: "Override for OpenAI-compatible endpoints (LM Studio, Ollama, vLLM, proxies)."
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
],
|
|
371
|
+
knownModels: OPENAI_KNOWN_MODELS,
|
|
372
|
+
supportsModelListing: true,
|
|
373
|
+
docsUrl: "https://platform.openai.com/docs/api-reference"
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/adapters/openai/adapter.ts
|
|
377
|
+
var OpenAIAdapter = class {
|
|
378
|
+
vendor = openAIManifest.vendor;
|
|
379
|
+
acceptedAuth = ["apiKey"];
|
|
380
|
+
client;
|
|
381
|
+
init(auth, client) {
|
|
382
|
+
if (!auth.apiKey) {
|
|
383
|
+
throw new Error("OpenAIAdapter: apiKey is required");
|
|
384
|
+
}
|
|
385
|
+
this.client = new OpenAI({
|
|
386
|
+
apiKey: auth.apiKey,
|
|
387
|
+
...auth.baseURL ? { baseURL: auth.baseURL } : {},
|
|
388
|
+
...client?.timeout !== void 0 ? { timeout: client.timeout } : {},
|
|
389
|
+
...client?.maxRetries !== void 0 ? { maxRetries: client.maxRetries } : {}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
stream(req) {
|
|
393
|
+
return streamFromOpenAI(this.ensureClient(), req, {
|
|
394
|
+
knownModels: openAIManifest.knownModels
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
appendToolResult(history, toolCallId, result) {
|
|
398
|
+
return appendOpenAIToolResult(history, toolCallId, result);
|
|
399
|
+
}
|
|
400
|
+
async listAvailableModels() {
|
|
401
|
+
return listOpenAICompatibleModels(this.ensureClient(), openAIManifest.knownModels);
|
|
402
|
+
}
|
|
403
|
+
async warmup() {
|
|
404
|
+
const client = this.ensureClient();
|
|
405
|
+
await client.models.list().catch(() => void 0);
|
|
406
|
+
}
|
|
407
|
+
ensureClient() {
|
|
408
|
+
if (!this.client) {
|
|
409
|
+
throw new Error("OpenAIAdapter: init() must be called before stream()/listAvailableModels()");
|
|
410
|
+
}
|
|
411
|
+
return this.client;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/adapters/openai/factory.ts
|
|
416
|
+
var openAIFactory = {
|
|
417
|
+
vendor: openAIManifest.vendor,
|
|
418
|
+
manifest: openAIManifest,
|
|
419
|
+
create: () => new OpenAIAdapter()
|
|
420
|
+
};
|
|
421
|
+
var deepseekManifest = {
|
|
422
|
+
vendor: "deepseek",
|
|
423
|
+
displayName: "DeepSeek",
|
|
424
|
+
authKinds: [
|
|
425
|
+
{
|
|
426
|
+
kind: "apiKey",
|
|
427
|
+
displayName: "API Key",
|
|
428
|
+
fields: [
|
|
429
|
+
{
|
|
430
|
+
name: "apiKey",
|
|
431
|
+
label: "API Key",
|
|
432
|
+
type: "password",
|
|
433
|
+
required: true,
|
|
434
|
+
help: "Get an API key from https://platform.deepseek.com/api_keys"
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "baseURL",
|
|
438
|
+
label: "Base URL",
|
|
439
|
+
type: "url",
|
|
440
|
+
required: false,
|
|
441
|
+
default: "https://api.deepseek.com/v1",
|
|
442
|
+
help: "Override for self-hosted DeepSeek or proxy endpoints."
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
],
|
|
447
|
+
knownModels: [
|
|
448
|
+
{
|
|
449
|
+
id: "deepseek-chat",
|
|
450
|
+
displayName: "DeepSeek Chat",
|
|
451
|
+
capabilities: {
|
|
452
|
+
streaming: true,
|
|
453
|
+
tools: true,
|
|
454
|
+
vision: false,
|
|
455
|
+
systemPrompt: true,
|
|
456
|
+
maxContextTokens: 64e3
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: "deepseek-reasoner",
|
|
461
|
+
displayName: "DeepSeek Reasoner",
|
|
462
|
+
capabilities: {
|
|
463
|
+
streaming: true,
|
|
464
|
+
tools: false,
|
|
465
|
+
vision: false,
|
|
466
|
+
systemPrompt: true,
|
|
467
|
+
maxContextTokens: 64e3
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
],
|
|
471
|
+
supportsModelListing: true,
|
|
472
|
+
docsUrl: "https://api-docs.deepseek.com/"
|
|
473
|
+
};
|
|
474
|
+
var DeepSeekAdapter = class extends OpenAIAdapter {
|
|
475
|
+
vendor = deepseekManifest.vendor;
|
|
476
|
+
init(auth, client) {
|
|
477
|
+
super.init(
|
|
478
|
+
{
|
|
479
|
+
...auth,
|
|
480
|
+
baseURL: auth.baseURL ?? "https://api.deepseek.com/v1"
|
|
481
|
+
},
|
|
482
|
+
client
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var deepseekFactory = {
|
|
487
|
+
vendor: deepseekManifest.vendor,
|
|
488
|
+
manifest: deepseekManifest,
|
|
489
|
+
create: () => new DeepSeekAdapter()
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/adapters/anthropic/adapter.ts
|
|
493
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
494
|
+
|
|
495
|
+
// src/adapters/anthropic/manifest.ts
|
|
496
|
+
var ANTHROPIC_KNOWN_MODELS = [
|
|
497
|
+
{
|
|
498
|
+
id: "claude-opus-4-7",
|
|
499
|
+
displayName: "Claude Opus 4.7",
|
|
500
|
+
capabilities: {
|
|
501
|
+
streaming: true,
|
|
502
|
+
tools: true,
|
|
503
|
+
vision: true,
|
|
504
|
+
systemPrompt: true,
|
|
505
|
+
maxContextTokens: 2e5
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: "claude-sonnet-4-6",
|
|
510
|
+
displayName: "Claude Sonnet 4.6",
|
|
511
|
+
capabilities: {
|
|
512
|
+
streaming: true,
|
|
513
|
+
tools: true,
|
|
514
|
+
vision: true,
|
|
515
|
+
systemPrompt: true,
|
|
516
|
+
maxContextTokens: 2e5
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: "claude-haiku-4-5-20251001",
|
|
521
|
+
displayName: "Claude Haiku 4.5",
|
|
522
|
+
capabilities: {
|
|
523
|
+
streaming: true,
|
|
524
|
+
tools: true,
|
|
525
|
+
vision: true,
|
|
526
|
+
systemPrompt: true,
|
|
527
|
+
maxContextTokens: 2e5
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
id: "claude-3-7-sonnet-20250219",
|
|
532
|
+
displayName: "Claude 3.7 Sonnet",
|
|
533
|
+
capabilities: {
|
|
534
|
+
streaming: true,
|
|
535
|
+
tools: true,
|
|
536
|
+
vision: true,
|
|
537
|
+
systemPrompt: true,
|
|
538
|
+
maxContextTokens: 2e5
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "claude-3-5-sonnet-20241022",
|
|
543
|
+
displayName: "Claude 3.5 Sonnet",
|
|
544
|
+
capabilities: {
|
|
545
|
+
streaming: true,
|
|
546
|
+
tools: true,
|
|
547
|
+
vision: true,
|
|
548
|
+
systemPrompt: true,
|
|
549
|
+
maxContextTokens: 2e5
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: "claude-3-5-haiku-20241022",
|
|
554
|
+
displayName: "Claude 3.5 Haiku",
|
|
555
|
+
capabilities: {
|
|
556
|
+
streaming: true,
|
|
557
|
+
tools: true,
|
|
558
|
+
vision: false,
|
|
559
|
+
systemPrompt: true,
|
|
560
|
+
maxContextTokens: 2e5
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
];
|
|
564
|
+
var anthropicManifest = {
|
|
565
|
+
vendor: "anthropic",
|
|
566
|
+
displayName: "Anthropic Claude",
|
|
567
|
+
authKinds: [
|
|
568
|
+
{
|
|
569
|
+
kind: "apiKey",
|
|
570
|
+
displayName: "API Key",
|
|
571
|
+
fields: [
|
|
572
|
+
{
|
|
573
|
+
name: "apiKey",
|
|
574
|
+
label: "API Key",
|
|
575
|
+
type: "password",
|
|
576
|
+
required: true,
|
|
577
|
+
help: "Get an API key from https://console.anthropic.com/settings/keys"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "baseURL",
|
|
581
|
+
label: "Base URL",
|
|
582
|
+
type: "url",
|
|
583
|
+
required: false,
|
|
584
|
+
default: "https://api.anthropic.com",
|
|
585
|
+
help: "Override for proxies or enterprise deployments."
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
],
|
|
590
|
+
knownModels: ANTHROPIC_KNOWN_MODELS,
|
|
591
|
+
supportsModelListing: true,
|
|
592
|
+
docsUrl: "https://docs.anthropic.com/en/api/overview"
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/adapters/anthropic/adapter.ts
|
|
596
|
+
var AnthropicAdapter = class {
|
|
597
|
+
vendor = anthropicManifest.vendor;
|
|
598
|
+
acceptedAuth = ["apiKey"];
|
|
599
|
+
client;
|
|
600
|
+
init(auth, client) {
|
|
601
|
+
if (!auth.apiKey) {
|
|
602
|
+
throw new Error("AnthropicAdapter: apiKey is required");
|
|
603
|
+
}
|
|
604
|
+
this.client = new Anthropic({
|
|
605
|
+
apiKey: auth.apiKey,
|
|
606
|
+
...auth.baseURL ? { baseURL: auth.baseURL } : {},
|
|
607
|
+
...client?.timeout !== void 0 ? { timeout: client.timeout } : {},
|
|
608
|
+
...client?.maxRetries !== void 0 ? { maxRetries: client.maxRetries } : {}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async *stream(req) {
|
|
612
|
+
assertValidRequest(req);
|
|
613
|
+
const client = this.ensureClient();
|
|
614
|
+
if (req.signal?.aborted) {
|
|
615
|
+
yield { type: "end", finishReason: "aborted" };
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const messages = buildWireMessages2(req);
|
|
619
|
+
const tools = req.tools && req.tools.length > 0 ? formatTools2(req.tools) : void 0;
|
|
620
|
+
const body = {
|
|
621
|
+
model: req.model,
|
|
622
|
+
messages,
|
|
623
|
+
max_tokens: req.maxTokens ?? 4096,
|
|
624
|
+
stream: true
|
|
625
|
+
};
|
|
626
|
+
if (req.system) body.system = req.system;
|
|
627
|
+
if (tools) body.tools = tools;
|
|
628
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
629
|
+
let stream;
|
|
630
|
+
try {
|
|
631
|
+
stream = await client.messages.create(
|
|
632
|
+
body,
|
|
633
|
+
{ signal: req.signal }
|
|
634
|
+
);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (isAbortError2(err)) {
|
|
637
|
+
yield { type: "end", finishReason: "aborted" };
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
642
|
+
let currentToolUse = null;
|
|
643
|
+
const completedToolCalls = [];
|
|
644
|
+
let stopReason;
|
|
645
|
+
let inputTokens;
|
|
646
|
+
let outputTokens;
|
|
647
|
+
try {
|
|
648
|
+
for await (const event of stream) {
|
|
649
|
+
if (req.signal?.aborted) {
|
|
650
|
+
yield { type: "end", finishReason: "aborted" };
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
switch (event.type) {
|
|
654
|
+
case "message_start":
|
|
655
|
+
inputTokens = event.message?.usage?.input_tokens;
|
|
656
|
+
break;
|
|
657
|
+
case "content_block_start": {
|
|
658
|
+
const block = event.content_block;
|
|
659
|
+
if (block?.type === "tool_use") {
|
|
660
|
+
currentToolUse = {
|
|
661
|
+
id: block.id ?? "",
|
|
662
|
+
name: block.name ?? "",
|
|
663
|
+
argumentsText: ""
|
|
664
|
+
};
|
|
665
|
+
if (currentToolUse.id && currentToolUse.name) {
|
|
666
|
+
yield {
|
|
667
|
+
type: "toolCallStart",
|
|
668
|
+
id: currentToolUse.id,
|
|
669
|
+
name: currentToolUse.name
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case "content_block_delta": {
|
|
676
|
+
const delta = event.delta;
|
|
677
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
678
|
+
yield { type: "token", text: delta.text };
|
|
679
|
+
} else if (delta?.type === "input_json_delta" && currentToolUse && delta.partial_json) {
|
|
680
|
+
currentToolUse.argumentsText += delta.partial_json;
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case "content_block_stop":
|
|
685
|
+
if (currentToolUse && currentToolUse.id && currentToolUse.name) {
|
|
686
|
+
let parsed = {};
|
|
687
|
+
try {
|
|
688
|
+
parsed = currentToolUse.argumentsText ? JSON.parse(currentToolUse.argumentsText) : {};
|
|
689
|
+
} catch {
|
|
690
|
+
parsed = {};
|
|
691
|
+
}
|
|
692
|
+
completedToolCalls.push({
|
|
693
|
+
id: currentToolUse.id,
|
|
694
|
+
name: currentToolUse.name,
|
|
695
|
+
arguments: parsed
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
currentToolUse = null;
|
|
699
|
+
break;
|
|
700
|
+
case "message_delta":
|
|
701
|
+
if (event.delta?.stop_reason) {
|
|
702
|
+
stopReason = event.delta.stop_reason;
|
|
703
|
+
}
|
|
704
|
+
if (event.usage?.output_tokens !== void 0) {
|
|
705
|
+
outputTokens = event.usage.output_tokens;
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
case "message_stop":
|
|
709
|
+
break;
|
|
710
|
+
default:
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
if (isAbortError2(err)) {
|
|
716
|
+
yield { type: "end", finishReason: "aborted" };
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
if (req.signal?.aborted) {
|
|
722
|
+
yield { type: "end", finishReason: "aborted" };
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
for (const tc of completedToolCalls) {
|
|
726
|
+
yield {
|
|
727
|
+
type: "toolCall",
|
|
728
|
+
id: tc.id,
|
|
729
|
+
name: tc.name,
|
|
730
|
+
arguments: tc.arguments
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const finishReason = mapStopReason(stopReason, completedToolCalls.length > 0);
|
|
734
|
+
const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
735
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
736
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
737
|
+
...inputTokens !== void 0 && outputTokens !== void 0 ? { totalTokens: inputTokens + outputTokens } : {}
|
|
738
|
+
} : void 0;
|
|
739
|
+
const endEvent = {
|
|
740
|
+
type: "end",
|
|
741
|
+
finishReason,
|
|
742
|
+
...usage ? { usage } : {},
|
|
743
|
+
...stopReason ? { rawReason: stopReason } : {}
|
|
744
|
+
};
|
|
745
|
+
yield endEvent;
|
|
746
|
+
}
|
|
747
|
+
appendToolResult(history, toolCallId, result) {
|
|
748
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
749
|
+
const wireMessage = {
|
|
750
|
+
role: "user",
|
|
751
|
+
content: [
|
|
752
|
+
{
|
|
753
|
+
type: "tool_result",
|
|
754
|
+
tool_use_id: toolCallId,
|
|
755
|
+
content
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
};
|
|
759
|
+
return [
|
|
760
|
+
...history,
|
|
761
|
+
{
|
|
762
|
+
role: "tool",
|
|
763
|
+
content,
|
|
764
|
+
vendorRaw: wireMessage
|
|
765
|
+
}
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
async listAvailableModels() {
|
|
769
|
+
const client = this.ensureClient();
|
|
770
|
+
const result = await client.models.list();
|
|
771
|
+
const models = [];
|
|
772
|
+
for (const m of result.data) {
|
|
773
|
+
const known = anthropicManifest.knownModels.find((k) => k.id === m.id);
|
|
774
|
+
if (known) {
|
|
775
|
+
models.push(known);
|
|
776
|
+
} else {
|
|
777
|
+
models.push({
|
|
778
|
+
id: m.id,
|
|
779
|
+
displayName: m.display_name ?? void 0,
|
|
780
|
+
capabilities: defaultClaudeCapabilities()
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return models;
|
|
785
|
+
}
|
|
786
|
+
async warmup() {
|
|
787
|
+
const client = this.ensureClient();
|
|
788
|
+
await client.models.list().catch(() => void 0);
|
|
789
|
+
}
|
|
790
|
+
ensureClient() {
|
|
791
|
+
if (!this.client) {
|
|
792
|
+
throw new Error("AnthropicAdapter: init() must be called before stream()/listAvailableModels()");
|
|
793
|
+
}
|
|
794
|
+
return this.client;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
function buildWireMessages2(req) {
|
|
798
|
+
const out = [];
|
|
799
|
+
for (const msg of req.messages) {
|
|
800
|
+
if (msg.vendorRaw && typeof msg.vendorRaw === "object") {
|
|
801
|
+
out.push(msg.vendorRaw);
|
|
802
|
+
} else {
|
|
803
|
+
out.push({ role: msg.role, content: msg.content });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
function formatTools2(tools) {
|
|
809
|
+
return tools.map((t) => ({
|
|
810
|
+
name: t.name,
|
|
811
|
+
description: t.description ?? "",
|
|
812
|
+
input_schema: t.parameters ?? { type: "object", properties: {} }
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
function mapStopReason(r, hadToolCalls) {
|
|
816
|
+
if (hadToolCalls) return "tool";
|
|
817
|
+
switch (r) {
|
|
818
|
+
case "end_turn":
|
|
819
|
+
case "stop_sequence":
|
|
820
|
+
return "stop";
|
|
821
|
+
case "tool_use":
|
|
822
|
+
return "tool";
|
|
823
|
+
case "max_tokens":
|
|
824
|
+
return "length";
|
|
825
|
+
default:
|
|
826
|
+
return "stop";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function defaultClaudeCapabilities() {
|
|
830
|
+
return {
|
|
831
|
+
streaming: true,
|
|
832
|
+
tools: true,
|
|
833
|
+
vision: true,
|
|
834
|
+
systemPrompt: true
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function isAbortError2(err) {
|
|
838
|
+
if (!err || typeof err !== "object") return false;
|
|
839
|
+
const e = err;
|
|
840
|
+
return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ABORT_ERR" || typeof e.message === "string" && /aborted/i.test(e.message);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/adapters/anthropic/factory.ts
|
|
844
|
+
var anthropicFactory = {
|
|
845
|
+
vendor: anthropicManifest.vendor,
|
|
846
|
+
manifest: anthropicManifest,
|
|
847
|
+
create: () => new AnthropicAdapter()
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// src/adapters/google/adapter.ts
|
|
851
|
+
import { GoogleGenAI } from "@google/genai";
|
|
852
|
+
|
|
853
|
+
// src/adapters/google/_streaming.ts
|
|
854
|
+
async function* streamFromGemini(client, req) {
|
|
855
|
+
assertValidRequest(req);
|
|
856
|
+
if (req.signal?.aborted) {
|
|
857
|
+
yield { type: "end", finishReason: "aborted" };
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const contents = buildWireContents(req);
|
|
861
|
+
const tools = req.tools && req.tools.length > 0 ? formatTools3(req.tools) : void 0;
|
|
862
|
+
const config = {};
|
|
863
|
+
if (req.system) {
|
|
864
|
+
config.systemInstruction = { parts: [{ text: req.system }] };
|
|
865
|
+
}
|
|
866
|
+
if (tools) config.tools = tools;
|
|
867
|
+
if (req.temperature !== void 0) config.temperature = req.temperature;
|
|
868
|
+
if (req.maxTokens !== void 0) config.maxOutputTokens = req.maxTokens;
|
|
869
|
+
if (req.signal) config.abortSignal = req.signal;
|
|
870
|
+
let stream;
|
|
871
|
+
try {
|
|
872
|
+
stream = await client.models.generateContentStream({
|
|
873
|
+
model: req.model,
|
|
874
|
+
contents,
|
|
875
|
+
config
|
|
876
|
+
});
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (isAbortError3(err)) {
|
|
879
|
+
yield { type: "end", finishReason: "aborted" };
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
throw err;
|
|
883
|
+
}
|
|
884
|
+
const toolCallCounter = /* @__PURE__ */ new Map();
|
|
885
|
+
const completedToolCalls = [];
|
|
886
|
+
let stopReason;
|
|
887
|
+
let inputTokens;
|
|
888
|
+
let outputTokens;
|
|
889
|
+
let totalTokens;
|
|
890
|
+
try {
|
|
891
|
+
for await (const chunk of stream) {
|
|
892
|
+
if (req.signal?.aborted) {
|
|
893
|
+
yield { type: "end", finishReason: "aborted" };
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const usage2 = chunk.usageMetadata;
|
|
897
|
+
if (usage2) {
|
|
898
|
+
if (usage2.promptTokenCount !== void 0) inputTokens = usage2.promptTokenCount;
|
|
899
|
+
if (usage2.candidatesTokenCount !== void 0) outputTokens = usage2.candidatesTokenCount;
|
|
900
|
+
if (usage2.totalTokenCount !== void 0) totalTokens = usage2.totalTokenCount;
|
|
901
|
+
}
|
|
902
|
+
const candidate = chunk.candidates?.[0];
|
|
903
|
+
if (!candidate?.content?.parts) {
|
|
904
|
+
if (candidate?.finishReason) stopReason = candidate.finishReason;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
for (const part of candidate.content.parts) {
|
|
908
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
909
|
+
yield { type: "token", text: part.text };
|
|
910
|
+
}
|
|
911
|
+
if (part.functionCall) {
|
|
912
|
+
const name = part.functionCall.name ?? "";
|
|
913
|
+
if (!name) continue;
|
|
914
|
+
const count = toolCallCounter.get(name) ?? 0;
|
|
915
|
+
toolCallCounter.set(name, count + 1);
|
|
916
|
+
const id = count === 0 ? name : `${name}#${count}`;
|
|
917
|
+
yield { type: "toolCallStart", id, name };
|
|
918
|
+
completedToolCalls.push({
|
|
919
|
+
id,
|
|
920
|
+
name,
|
|
921
|
+
arguments: part.functionCall.args ?? {}
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (candidate.finishReason) {
|
|
926
|
+
stopReason = candidate.finishReason;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
} catch (err) {
|
|
930
|
+
if (isAbortError3(err)) {
|
|
931
|
+
yield { type: "end", finishReason: "aborted" };
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
throw err;
|
|
935
|
+
}
|
|
936
|
+
if (req.signal?.aborted) {
|
|
937
|
+
yield { type: "end", finishReason: "aborted" };
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
for (const tc of completedToolCalls) {
|
|
941
|
+
yield {
|
|
942
|
+
type: "toolCall",
|
|
943
|
+
id: tc.id,
|
|
944
|
+
name: tc.name,
|
|
945
|
+
arguments: tc.arguments
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
const finishReason = mapFinishReason2(stopReason, completedToolCalls.length > 0);
|
|
949
|
+
const usage = inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 ? {
|
|
950
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
951
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
952
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
953
|
+
} : void 0;
|
|
954
|
+
const endEvent = {
|
|
955
|
+
type: "end",
|
|
956
|
+
finishReason,
|
|
957
|
+
...usage ? { usage } : {},
|
|
958
|
+
...stopReason ? { rawReason: stopReason } : {}
|
|
959
|
+
};
|
|
960
|
+
yield endEvent;
|
|
961
|
+
}
|
|
962
|
+
function appendGeminiToolResult(history, toolCallId, result) {
|
|
963
|
+
const name = parseToolName(toolCallId);
|
|
964
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
965
|
+
const wireMessage = {
|
|
966
|
+
role: "user",
|
|
967
|
+
parts: [
|
|
968
|
+
{
|
|
969
|
+
functionResponse: {
|
|
970
|
+
name,
|
|
971
|
+
response: { result: content }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
]
|
|
975
|
+
};
|
|
976
|
+
return [
|
|
977
|
+
...history,
|
|
978
|
+
{
|
|
979
|
+
role: "tool",
|
|
980
|
+
content,
|
|
981
|
+
vendorRaw: wireMessage
|
|
982
|
+
}
|
|
983
|
+
];
|
|
984
|
+
}
|
|
985
|
+
async function listGeminiModels(client, knownModels) {
|
|
986
|
+
const out = [];
|
|
987
|
+
const pager = await client.models.list();
|
|
988
|
+
for await (const m of pager) {
|
|
989
|
+
const id = stripModelPrefix(m.name ?? "");
|
|
990
|
+
if (!id) continue;
|
|
991
|
+
const known = knownModels.find((k) => k.id === id);
|
|
992
|
+
if (known) {
|
|
993
|
+
out.push(known);
|
|
994
|
+
} else {
|
|
995
|
+
out.push({
|
|
996
|
+
id,
|
|
997
|
+
displayName: m.displayName ?? void 0,
|
|
998
|
+
capabilities: defaultGeminiCapabilities()
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
1004
|
+
function buildWireContents(req) {
|
|
1005
|
+
const out = [];
|
|
1006
|
+
for (const msg of req.messages) {
|
|
1007
|
+
if (msg.vendorRaw && typeof msg.vendorRaw === "object") {
|
|
1008
|
+
out.push(msg.vendorRaw);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
1012
|
+
out.push({
|
|
1013
|
+
role: toGeminiRole(msg.role),
|
|
1014
|
+
parts: [{ text }]
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return out;
|
|
1018
|
+
}
|
|
1019
|
+
function toGeminiRole(role) {
|
|
1020
|
+
return role === "assistant" || role === "model" ? "model" : "user";
|
|
1021
|
+
}
|
|
1022
|
+
function formatTools3(tools) {
|
|
1023
|
+
return [
|
|
1024
|
+
{
|
|
1025
|
+
functionDeclarations: tools.map((t) => ({
|
|
1026
|
+
name: t.name,
|
|
1027
|
+
description: t.description ?? "",
|
|
1028
|
+
parameters: t.parameters ?? { type: "object", properties: {} }
|
|
1029
|
+
}))
|
|
1030
|
+
}
|
|
1031
|
+
];
|
|
1032
|
+
}
|
|
1033
|
+
function mapFinishReason2(r, hadToolCalls) {
|
|
1034
|
+
if (hadToolCalls && (r === "STOP" || r === "TOOL_CALLS" || !r)) return "tool";
|
|
1035
|
+
switch (r) {
|
|
1036
|
+
case "STOP":
|
|
1037
|
+
return "stop";
|
|
1038
|
+
case "MAX_TOKENS":
|
|
1039
|
+
return "length";
|
|
1040
|
+
case "SAFETY":
|
|
1041
|
+
case "RECITATION":
|
|
1042
|
+
return "filtered";
|
|
1043
|
+
case "TOOL_CALLS":
|
|
1044
|
+
return "tool";
|
|
1045
|
+
default:
|
|
1046
|
+
return "stop";
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function parseToolName(id) {
|
|
1050
|
+
const hashIdx = id.lastIndexOf("#");
|
|
1051
|
+
if (hashIdx === -1) return id;
|
|
1052
|
+
const suffix = id.slice(hashIdx + 1);
|
|
1053
|
+
if (/^\d+$/.test(suffix)) return id.slice(0, hashIdx);
|
|
1054
|
+
return id;
|
|
1055
|
+
}
|
|
1056
|
+
function stripModelPrefix(name) {
|
|
1057
|
+
return name.startsWith("models/") ? name.slice("models/".length) : name;
|
|
1058
|
+
}
|
|
1059
|
+
function defaultGeminiCapabilities() {
|
|
1060
|
+
return {
|
|
1061
|
+
streaming: true,
|
|
1062
|
+
tools: true,
|
|
1063
|
+
vision: true,
|
|
1064
|
+
systemPrompt: true
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function isAbortError3(err) {
|
|
1068
|
+
if (!err || typeof err !== "object") return false;
|
|
1069
|
+
const e = err;
|
|
1070
|
+
return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ABORT_ERR" || typeof e.message === "string" && /aborted/i.test(e.message);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/adapters/google/manifest.ts
|
|
1074
|
+
var GOOGLE_KNOWN_MODELS = [
|
|
1075
|
+
{
|
|
1076
|
+
id: "gemini-2.5-pro",
|
|
1077
|
+
displayName: "Gemini 2.5 Pro",
|
|
1078
|
+
capabilities: {
|
|
1079
|
+
streaming: true,
|
|
1080
|
+
tools: true,
|
|
1081
|
+
vision: true,
|
|
1082
|
+
systemPrompt: true,
|
|
1083
|
+
maxContextTokens: 1e6
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
id: "gemini-2.5-flash",
|
|
1088
|
+
displayName: "Gemini 2.5 Flash",
|
|
1089
|
+
capabilities: {
|
|
1090
|
+
streaming: true,
|
|
1091
|
+
tools: true,
|
|
1092
|
+
vision: true,
|
|
1093
|
+
systemPrompt: true,
|
|
1094
|
+
maxContextTokens: 1e6
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
id: "gemini-2.0-flash",
|
|
1099
|
+
displayName: "Gemini 2.0 Flash",
|
|
1100
|
+
capabilities: {
|
|
1101
|
+
streaming: true,
|
|
1102
|
+
tools: true,
|
|
1103
|
+
vision: true,
|
|
1104
|
+
systemPrompt: true,
|
|
1105
|
+
maxContextTokens: 1e6
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
id: "gemini-2.0-flash-lite",
|
|
1110
|
+
displayName: "Gemini 2.0 Flash Lite",
|
|
1111
|
+
capabilities: {
|
|
1112
|
+
streaming: true,
|
|
1113
|
+
tools: true,
|
|
1114
|
+
vision: false,
|
|
1115
|
+
systemPrompt: true,
|
|
1116
|
+
maxContextTokens: 1e6
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
id: "gemini-1.5-pro",
|
|
1121
|
+
displayName: "Gemini 1.5 Pro",
|
|
1122
|
+
capabilities: {
|
|
1123
|
+
streaming: true,
|
|
1124
|
+
tools: true,
|
|
1125
|
+
vision: true,
|
|
1126
|
+
systemPrompt: true,
|
|
1127
|
+
maxContextTokens: 2e6
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
id: "gemini-1.5-flash",
|
|
1132
|
+
displayName: "Gemini 1.5 Flash",
|
|
1133
|
+
capabilities: {
|
|
1134
|
+
streaming: true,
|
|
1135
|
+
tools: true,
|
|
1136
|
+
vision: true,
|
|
1137
|
+
systemPrompt: true,
|
|
1138
|
+
maxContextTokens: 1e6
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
];
|
|
1142
|
+
var googleManifest = {
|
|
1143
|
+
vendor: "google",
|
|
1144
|
+
displayName: "Google Gemini",
|
|
1145
|
+
authKinds: [
|
|
1146
|
+
{
|
|
1147
|
+
kind: "googleApiKey",
|
|
1148
|
+
displayName: "API Key",
|
|
1149
|
+
fields: [
|
|
1150
|
+
{
|
|
1151
|
+
name: "apiKey",
|
|
1152
|
+
label: "API Key",
|
|
1153
|
+
type: "password",
|
|
1154
|
+
required: true,
|
|
1155
|
+
help: "Get an API key from https://aistudio.google.com/apikey"
|
|
1156
|
+
}
|
|
1157
|
+
]
|
|
1158
|
+
}
|
|
1159
|
+
],
|
|
1160
|
+
knownModels: GOOGLE_KNOWN_MODELS,
|
|
1161
|
+
supportsModelListing: true,
|
|
1162
|
+
docsUrl: "https://ai.google.dev/gemini-api/docs"
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/adapters/google/adapter.ts
|
|
1166
|
+
var GoogleAdapter = class {
|
|
1167
|
+
vendor = googleManifest.vendor;
|
|
1168
|
+
acceptedAuth = ["googleApiKey"];
|
|
1169
|
+
client;
|
|
1170
|
+
init(auth, _client) {
|
|
1171
|
+
if (auth.kind !== "googleApiKey") {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`GoogleAdapter: unsupported auth kind '${auth.kind}'`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (!auth.apiKey) {
|
|
1177
|
+
throw new Error("GoogleAdapter: apiKey is required");
|
|
1178
|
+
}
|
|
1179
|
+
this.client = new GoogleGenAI({ apiKey: auth.apiKey });
|
|
1180
|
+
}
|
|
1181
|
+
stream(req) {
|
|
1182
|
+
return streamFromGemini(this.ensureClient(), req);
|
|
1183
|
+
}
|
|
1184
|
+
appendToolResult(history, toolCallId, result) {
|
|
1185
|
+
return appendGeminiToolResult(history, toolCallId, result);
|
|
1186
|
+
}
|
|
1187
|
+
async listAvailableModels() {
|
|
1188
|
+
return listGeminiModels(this.ensureClient(), googleManifest.knownModels);
|
|
1189
|
+
}
|
|
1190
|
+
async warmup() {
|
|
1191
|
+
}
|
|
1192
|
+
ensureClient() {
|
|
1193
|
+
if (!this.client) {
|
|
1194
|
+
throw new Error("GoogleAdapter: init() must be called before stream()/listAvailableModels()");
|
|
1195
|
+
}
|
|
1196
|
+
return this.client;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// src/adapters/google/factory.ts
|
|
1201
|
+
var googleFactory = {
|
|
1202
|
+
vendor: googleManifest.vendor,
|
|
1203
|
+
manifest: googleManifest,
|
|
1204
|
+
create: () => new GoogleAdapter()
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// src/adapters/vertex-gemini/adapter.ts
|
|
1208
|
+
import { GoogleGenAI as GoogleGenAI2 } from "@google/genai";
|
|
1209
|
+
|
|
1210
|
+
// src/adapters/vertex-gemini/manifest.ts
|
|
1211
|
+
var VERTEX_GEMINI_KNOWN_MODELS = [
|
|
1212
|
+
{
|
|
1213
|
+
id: "gemini-2.5-pro",
|
|
1214
|
+
displayName: "Gemini 2.5 Pro",
|
|
1215
|
+
capabilities: {
|
|
1216
|
+
streaming: true,
|
|
1217
|
+
tools: true,
|
|
1218
|
+
vision: true,
|
|
1219
|
+
systemPrompt: true,
|
|
1220
|
+
maxContextTokens: 1e6
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: "gemini-2.5-flash",
|
|
1225
|
+
displayName: "Gemini 2.5 Flash",
|
|
1226
|
+
capabilities: {
|
|
1227
|
+
streaming: true,
|
|
1228
|
+
tools: true,
|
|
1229
|
+
vision: true,
|
|
1230
|
+
systemPrompt: true,
|
|
1231
|
+
maxContextTokens: 1e6
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
id: "gemini-2.0-flash",
|
|
1236
|
+
displayName: "Gemini 2.0 Flash",
|
|
1237
|
+
capabilities: {
|
|
1238
|
+
streaming: true,
|
|
1239
|
+
tools: true,
|
|
1240
|
+
vision: true,
|
|
1241
|
+
systemPrompt: true,
|
|
1242
|
+
maxContextTokens: 1e6
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
id: "gemini-1.5-pro",
|
|
1247
|
+
displayName: "Gemini 1.5 Pro",
|
|
1248
|
+
capabilities: {
|
|
1249
|
+
streaming: true,
|
|
1250
|
+
tools: true,
|
|
1251
|
+
vision: true,
|
|
1252
|
+
systemPrompt: true,
|
|
1253
|
+
maxContextTokens: 2e6
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
id: "gemini-1.5-flash",
|
|
1258
|
+
displayName: "Gemini 1.5 Flash",
|
|
1259
|
+
capabilities: {
|
|
1260
|
+
streaming: true,
|
|
1261
|
+
tools: true,
|
|
1262
|
+
vision: true,
|
|
1263
|
+
systemPrompt: true,
|
|
1264
|
+
maxContextTokens: 1e6
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
];
|
|
1268
|
+
var vertexGeminiManifest = {
|
|
1269
|
+
vendor: "vertex-gemini",
|
|
1270
|
+
displayName: "Vertex AI \u2014 Gemini",
|
|
1271
|
+
authKinds: [
|
|
1272
|
+
{
|
|
1273
|
+
kind: "vertexServiceAccount",
|
|
1274
|
+
displayName: "Service Account JSON",
|
|
1275
|
+
fields: [
|
|
1276
|
+
{
|
|
1277
|
+
name: "credentials",
|
|
1278
|
+
label: "Service Account Key (JSON)",
|
|
1279
|
+
type: "json-file",
|
|
1280
|
+
required: true,
|
|
1281
|
+
help: "Upload the service account key JSON from Google Cloud Console."
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
name: "projectId",
|
|
1285
|
+
label: "Project ID",
|
|
1286
|
+
type: "text",
|
|
1287
|
+
required: true,
|
|
1288
|
+
help: "GCP project that hosts the Vertex AI deployment."
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
name: "location",
|
|
1292
|
+
label: "Region",
|
|
1293
|
+
type: "select",
|
|
1294
|
+
required: true,
|
|
1295
|
+
default: "us-central1",
|
|
1296
|
+
options: [
|
|
1297
|
+
{ value: "us-central1", label: "us-central1" },
|
|
1298
|
+
{ value: "us-east1", label: "us-east1" },
|
|
1299
|
+
{ value: "us-east4", label: "us-east4" },
|
|
1300
|
+
{ value: "us-west1", label: "us-west1" },
|
|
1301
|
+
{ value: "us-west4", label: "us-west4" },
|
|
1302
|
+
{ value: "europe-west1", label: "europe-west1" },
|
|
1303
|
+
{ value: "europe-west2", label: "europe-west2" },
|
|
1304
|
+
{ value: "europe-west3", label: "europe-west3" },
|
|
1305
|
+
{ value: "europe-west4", label: "europe-west4" },
|
|
1306
|
+
{ value: "asia-east1", label: "asia-east1" },
|
|
1307
|
+
{ value: "asia-northeast1", label: "asia-northeast1" },
|
|
1308
|
+
{ value: "asia-southeast1", label: "asia-southeast1" }
|
|
1309
|
+
]
|
|
1310
|
+
}
|
|
1311
|
+
]
|
|
1312
|
+
}
|
|
1313
|
+
],
|
|
1314
|
+
knownModels: VERTEX_GEMINI_KNOWN_MODELS,
|
|
1315
|
+
supportsModelListing: true,
|
|
1316
|
+
docsUrl: "https://cloud.google.com/vertex-ai/generative-ai/docs"
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
// src/adapters/vertex-gemini/adapter.ts
|
|
1320
|
+
var VertexGeminiAdapter = class {
|
|
1321
|
+
vendor = vertexGeminiManifest.vendor;
|
|
1322
|
+
acceptedAuth = ["vertexServiceAccount"];
|
|
1323
|
+
client;
|
|
1324
|
+
init(auth, _client) {
|
|
1325
|
+
if (auth.kind !== "vertexServiceAccount") {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`VertexGeminiAdapter: unsupported auth kind '${auth.kind}'`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (!auth.credentials) {
|
|
1331
|
+
throw new Error("VertexGeminiAdapter: service-account credentials are required");
|
|
1332
|
+
}
|
|
1333
|
+
if (!auth.projectId) {
|
|
1334
|
+
throw new Error("VertexGeminiAdapter: projectId is required");
|
|
1335
|
+
}
|
|
1336
|
+
if (!auth.location) {
|
|
1337
|
+
throw new Error("VertexGeminiAdapter: location is required");
|
|
1338
|
+
}
|
|
1339
|
+
this.client = new GoogleGenAI2({
|
|
1340
|
+
vertexai: true,
|
|
1341
|
+
project: auth.projectId,
|
|
1342
|
+
location: auth.location,
|
|
1343
|
+
googleAuthOptions: {
|
|
1344
|
+
credentials: auth.credentials
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
stream(req) {
|
|
1349
|
+
return streamFromGemini(this.ensureClient(), req);
|
|
1350
|
+
}
|
|
1351
|
+
appendToolResult(history, toolCallId, result) {
|
|
1352
|
+
return appendGeminiToolResult(history, toolCallId, result);
|
|
1353
|
+
}
|
|
1354
|
+
async listAvailableModels() {
|
|
1355
|
+
return listGeminiModels(this.ensureClient(), vertexGeminiManifest.knownModels);
|
|
1356
|
+
}
|
|
1357
|
+
async warmup() {
|
|
1358
|
+
}
|
|
1359
|
+
ensureClient() {
|
|
1360
|
+
if (!this.client) {
|
|
1361
|
+
throw new Error(
|
|
1362
|
+
"VertexGeminiAdapter: init() must be called before stream()/listAvailableModels()"
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
return this.client;
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/adapters/vertex-gemini/factory.ts
|
|
1370
|
+
var vertexGeminiFactory = {
|
|
1371
|
+
vendor: vertexGeminiManifest.vendor,
|
|
1372
|
+
manifest: vertexGeminiManifest,
|
|
1373
|
+
create: () => new VertexGeminiAdapter()
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/adapters/vertex-openai/adapter.ts
|
|
1377
|
+
import OpenAI2 from "openai";
|
|
1378
|
+
import { GoogleAuth } from "google-auth-library";
|
|
1379
|
+
|
|
1380
|
+
// src/adapters/vertex-openai/manifest.ts
|
|
1381
|
+
var VERTEX_OPENAI_KNOWN_MODELS = [
|
|
1382
|
+
{
|
|
1383
|
+
id: "mistral-large",
|
|
1384
|
+
displayName: "Mistral Large (on Vertex)",
|
|
1385
|
+
capabilities: {
|
|
1386
|
+
streaming: true,
|
|
1387
|
+
tools: true,
|
|
1388
|
+
vision: false,
|
|
1389
|
+
systemPrompt: true,
|
|
1390
|
+
maxContextTokens: 128e3
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
id: "mistral-small",
|
|
1395
|
+
displayName: "Mistral Small (on Vertex)",
|
|
1396
|
+
capabilities: {
|
|
1397
|
+
streaming: true,
|
|
1398
|
+
tools: true,
|
|
1399
|
+
vision: false,
|
|
1400
|
+
systemPrompt: true,
|
|
1401
|
+
maxContextTokens: 32e3
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
id: "meta/llama-3.3-70b-instruct-maas",
|
|
1406
|
+
displayName: "Llama 3.3 70B Instruct (on Vertex)",
|
|
1407
|
+
capabilities: {
|
|
1408
|
+
streaming: true,
|
|
1409
|
+
tools: true,
|
|
1410
|
+
vision: false,
|
|
1411
|
+
systemPrompt: true,
|
|
1412
|
+
maxContextTokens: 128e3
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
id: "meta/llama-3.1-405b-instruct-maas",
|
|
1417
|
+
displayName: "Llama 3.1 405B Instruct (on Vertex)",
|
|
1418
|
+
capabilities: {
|
|
1419
|
+
streaming: true,
|
|
1420
|
+
tools: true,
|
|
1421
|
+
vision: false,
|
|
1422
|
+
systemPrompt: true,
|
|
1423
|
+
maxContextTokens: 128e3
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
];
|
|
1427
|
+
var vertexOpenAIManifest = {
|
|
1428
|
+
vendor: "vertex-openai",
|
|
1429
|
+
displayName: "Vertex AI \u2014 OpenAI-compatible",
|
|
1430
|
+
authKinds: [
|
|
1431
|
+
{
|
|
1432
|
+
kind: "vertexServiceAccount",
|
|
1433
|
+
displayName: "Service Account JSON",
|
|
1434
|
+
fields: [
|
|
1435
|
+
{
|
|
1436
|
+
name: "credentials",
|
|
1437
|
+
label: "Service Account Key (JSON)",
|
|
1438
|
+
type: "json-file",
|
|
1439
|
+
required: true,
|
|
1440
|
+
help: "Upload the service account key JSON from Google Cloud Console."
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "projectId",
|
|
1444
|
+
label: "Project ID",
|
|
1445
|
+
type: "text",
|
|
1446
|
+
required: true,
|
|
1447
|
+
help: "GCP project that hosts the Vertex AI deployment."
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
name: "location",
|
|
1451
|
+
label: "Region",
|
|
1452
|
+
type: "select",
|
|
1453
|
+
required: true,
|
|
1454
|
+
default: "us-central1",
|
|
1455
|
+
options: [
|
|
1456
|
+
{ value: "us-central1", label: "us-central1" },
|
|
1457
|
+
{ value: "us-east4", label: "us-east4" },
|
|
1458
|
+
{ value: "europe-west1", label: "europe-west1" },
|
|
1459
|
+
{ value: "europe-west4", label: "europe-west4" },
|
|
1460
|
+
{ value: "asia-southeast1", label: "asia-southeast1" }
|
|
1461
|
+
]
|
|
1462
|
+
}
|
|
1463
|
+
]
|
|
1464
|
+
}
|
|
1465
|
+
],
|
|
1466
|
+
knownModels: VERTEX_OPENAI_KNOWN_MODELS,
|
|
1467
|
+
// The OpenAI-compatible endpoint on Vertex does not expose a /models list.
|
|
1468
|
+
supportsModelListing: false,
|
|
1469
|
+
docsUrl: "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-openai"
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/adapters/vertex-openai/adapter.ts
|
|
1473
|
+
var VertexOpenAIAdapter = class {
|
|
1474
|
+
vendor = vertexOpenAIManifest.vendor;
|
|
1475
|
+
acceptedAuth = ["vertexServiceAccount"];
|
|
1476
|
+
client;
|
|
1477
|
+
init(auth, client) {
|
|
1478
|
+
if (auth.kind !== "vertexServiceAccount") {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
`VertexOpenAIAdapter: unsupported auth kind '${auth.kind}'`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (!auth.credentials) {
|
|
1484
|
+
throw new Error("VertexOpenAIAdapter: service-account credentials are required");
|
|
1485
|
+
}
|
|
1486
|
+
if (!auth.projectId) {
|
|
1487
|
+
throw new Error("VertexOpenAIAdapter: projectId is required");
|
|
1488
|
+
}
|
|
1489
|
+
if (!auth.location) {
|
|
1490
|
+
throw new Error("VertexOpenAIAdapter: location is required");
|
|
1491
|
+
}
|
|
1492
|
+
const baseURL = `https://${auth.location}-aiplatform.googleapis.com/v1/projects/${auth.projectId}/locations/${auth.location}/endpoints/openapi`;
|
|
1493
|
+
const googleAuth = new GoogleAuth({
|
|
1494
|
+
credentials: auth.credentials,
|
|
1495
|
+
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
|
1496
|
+
});
|
|
1497
|
+
const authedFetch = async (input, init2) => {
|
|
1498
|
+
const googleClient = await googleAuth.getClient();
|
|
1499
|
+
const token = await googleClient.getAccessToken();
|
|
1500
|
+
const headers = new Headers(init2?.headers);
|
|
1501
|
+
headers.set("Authorization", `Bearer ${token.token ?? ""}`);
|
|
1502
|
+
return fetch(input, { ...init2, headers });
|
|
1503
|
+
};
|
|
1504
|
+
this.client = new OpenAI2({
|
|
1505
|
+
apiKey: "vertex-ai",
|
|
1506
|
+
baseURL,
|
|
1507
|
+
fetch: authedFetch,
|
|
1508
|
+
...client?.timeout !== void 0 ? { timeout: client.timeout } : {},
|
|
1509
|
+
...client?.maxRetries !== void 0 ? { maxRetries: client.maxRetries } : {}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
stream(req) {
|
|
1513
|
+
return streamFromOpenAI(this.ensureClient(), req, {
|
|
1514
|
+
knownModels: vertexOpenAIManifest.knownModels
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
appendToolResult(history, toolCallId, result) {
|
|
1518
|
+
return appendOpenAIToolResult(history, toolCallId, result);
|
|
1519
|
+
}
|
|
1520
|
+
async listAvailableModels() {
|
|
1521
|
+
return [...vertexOpenAIManifest.knownModels];
|
|
1522
|
+
}
|
|
1523
|
+
async warmup() {
|
|
1524
|
+
}
|
|
1525
|
+
ensureClient() {
|
|
1526
|
+
if (!this.client) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
"VertexOpenAIAdapter: init() must be called before stream()/listAvailableModels()"
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
return this.client;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/adapters/vertex-openai/factory.ts
|
|
1536
|
+
var vertexOpenAIFactory = {
|
|
1537
|
+
vendor: vertexOpenAIManifest.vendor,
|
|
1538
|
+
manifest: vertexOpenAIManifest,
|
|
1539
|
+
create: () => new VertexOpenAIAdapter()
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
// src/adapters/bedrock/adapter.ts
|
|
1543
|
+
import {
|
|
1544
|
+
BedrockRuntimeClient,
|
|
1545
|
+
ConverseStreamCommand
|
|
1546
|
+
} from "@aws-sdk/client-bedrock-runtime";
|
|
1547
|
+
|
|
1548
|
+
// src/adapters/bedrock/manifest.ts
|
|
1549
|
+
var BEDROCK_KNOWN_MODELS = [
|
|
1550
|
+
{
|
|
1551
|
+
id: "anthropic.claude-opus-4-v1:0",
|
|
1552
|
+
displayName: "Claude Opus 4 (on Bedrock)",
|
|
1553
|
+
capabilities: {
|
|
1554
|
+
streaming: true,
|
|
1555
|
+
tools: true,
|
|
1556
|
+
vision: true,
|
|
1557
|
+
systemPrompt: true,
|
|
1558
|
+
maxContextTokens: 2e5
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
id: "anthropic.claude-sonnet-4-v1:0",
|
|
1563
|
+
displayName: "Claude Sonnet 4 (on Bedrock)",
|
|
1564
|
+
capabilities: {
|
|
1565
|
+
streaming: true,
|
|
1566
|
+
tools: true,
|
|
1567
|
+
vision: true,
|
|
1568
|
+
systemPrompt: true,
|
|
1569
|
+
maxContextTokens: 2e5
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
|
1574
|
+
displayName: "Claude 3.7 Sonnet (on Bedrock)",
|
|
1575
|
+
capabilities: {
|
|
1576
|
+
streaming: true,
|
|
1577
|
+
tools: true,
|
|
1578
|
+
vision: true,
|
|
1579
|
+
systemPrompt: true,
|
|
1580
|
+
maxContextTokens: 2e5
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
1585
|
+
displayName: "Claude 3.5 Sonnet v2 (on Bedrock)",
|
|
1586
|
+
capabilities: {
|
|
1587
|
+
streaming: true,
|
|
1588
|
+
tools: true,
|
|
1589
|
+
vision: true,
|
|
1590
|
+
systemPrompt: true,
|
|
1591
|
+
maxContextTokens: 2e5
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
id: "anthropic.claude-3-5-haiku-20241022-v1:0",
|
|
1596
|
+
displayName: "Claude 3.5 Haiku (on Bedrock)",
|
|
1597
|
+
capabilities: {
|
|
1598
|
+
streaming: true,
|
|
1599
|
+
tools: true,
|
|
1600
|
+
vision: false,
|
|
1601
|
+
systemPrompt: true,
|
|
1602
|
+
maxContextTokens: 2e5
|
|
1603
|
+
}
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
id: "meta.llama3-3-70b-instruct-v1:0",
|
|
1607
|
+
displayName: "Llama 3.3 70B Instruct (on Bedrock)",
|
|
1608
|
+
capabilities: {
|
|
1609
|
+
streaming: true,
|
|
1610
|
+
tools: true,
|
|
1611
|
+
vision: false,
|
|
1612
|
+
systemPrompt: true,
|
|
1613
|
+
maxContextTokens: 128e3
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
id: "mistral.mistral-large-2407-v1:0",
|
|
1618
|
+
displayName: "Mistral Large (on Bedrock)",
|
|
1619
|
+
capabilities: {
|
|
1620
|
+
streaming: true,
|
|
1621
|
+
tools: true,
|
|
1622
|
+
vision: false,
|
|
1623
|
+
systemPrompt: true,
|
|
1624
|
+
maxContextTokens: 128e3
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
id: "amazon.nova-pro-v1:0",
|
|
1629
|
+
displayName: "Amazon Nova Pro",
|
|
1630
|
+
capabilities: {
|
|
1631
|
+
streaming: true,
|
|
1632
|
+
tools: true,
|
|
1633
|
+
vision: true,
|
|
1634
|
+
systemPrompt: true,
|
|
1635
|
+
maxContextTokens: 3e5
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
id: "amazon.nova-lite-v1:0",
|
|
1640
|
+
displayName: "Amazon Nova Lite",
|
|
1641
|
+
capabilities: {
|
|
1642
|
+
streaming: true,
|
|
1643
|
+
tools: true,
|
|
1644
|
+
vision: true,
|
|
1645
|
+
systemPrompt: true,
|
|
1646
|
+
maxContextTokens: 3e5
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
id: "amazon.nova-micro-v1:0",
|
|
1651
|
+
displayName: "Amazon Nova Micro",
|
|
1652
|
+
capabilities: {
|
|
1653
|
+
streaming: true,
|
|
1654
|
+
tools: true,
|
|
1655
|
+
vision: false,
|
|
1656
|
+
systemPrompt: true,
|
|
1657
|
+
maxContextTokens: 128e3
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
];
|
|
1661
|
+
var BEDROCK_REGIONS = [
|
|
1662
|
+
{ value: "us-east-1", label: "us-east-1 (N. Virginia)" },
|
|
1663
|
+
{ value: "us-east-2", label: "us-east-2 (Ohio)" },
|
|
1664
|
+
{ value: "us-west-2", label: "us-west-2 (Oregon)" },
|
|
1665
|
+
{ value: "eu-west-1", label: "eu-west-1 (Ireland)" },
|
|
1666
|
+
{ value: "eu-west-3", label: "eu-west-3 (Paris)" },
|
|
1667
|
+
{ value: "eu-central-1", label: "eu-central-1 (Frankfurt)" },
|
|
1668
|
+
{ value: "ap-northeast-1", label: "ap-northeast-1 (Tokyo)" },
|
|
1669
|
+
{ value: "ap-south-1", label: "ap-south-1 (Mumbai)" },
|
|
1670
|
+
{ value: "ap-southeast-1", label: "ap-southeast-1 (Singapore)" },
|
|
1671
|
+
{ value: "ap-southeast-2", label: "ap-southeast-2 (Sydney)" },
|
|
1672
|
+
{ value: "ca-central-1", label: "ca-central-1 (Canada)" }
|
|
1673
|
+
];
|
|
1674
|
+
var bedrockManifest = {
|
|
1675
|
+
vendor: "bedrock",
|
|
1676
|
+
displayName: "AWS Bedrock",
|
|
1677
|
+
authKinds: [
|
|
1678
|
+
{
|
|
1679
|
+
kind: "bedrockApiKey",
|
|
1680
|
+
displayName: "API Key",
|
|
1681
|
+
fields: [
|
|
1682
|
+
{
|
|
1683
|
+
name: "apiKey",
|
|
1684
|
+
label: "API Key",
|
|
1685
|
+
type: "password",
|
|
1686
|
+
required: true,
|
|
1687
|
+
help: "Bedrock API key (bearer token). Create one in the AWS console under Amazon Bedrock \u2192 API keys."
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
name: "region",
|
|
1691
|
+
label: "Region",
|
|
1692
|
+
type: "select",
|
|
1693
|
+
required: true,
|
|
1694
|
+
default: "us-east-1",
|
|
1695
|
+
options: BEDROCK_REGIONS
|
|
1696
|
+
}
|
|
1697
|
+
]
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
kind: "bedrockIam",
|
|
1701
|
+
displayName: "IAM Credentials",
|
|
1702
|
+
fields: [
|
|
1703
|
+
{
|
|
1704
|
+
name: "accessKeyId",
|
|
1705
|
+
label: "Access Key ID",
|
|
1706
|
+
type: "text",
|
|
1707
|
+
required: true
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
name: "secretAccessKey",
|
|
1711
|
+
label: "Secret Access Key",
|
|
1712
|
+
type: "password",
|
|
1713
|
+
required: true
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
name: "sessionToken",
|
|
1717
|
+
label: "Session Token",
|
|
1718
|
+
type: "password",
|
|
1719
|
+
required: false,
|
|
1720
|
+
help: "Only required when using temporary credentials (e.g., STS)."
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
name: "region",
|
|
1724
|
+
label: "Region",
|
|
1725
|
+
type: "select",
|
|
1726
|
+
required: true,
|
|
1727
|
+
default: "us-east-1",
|
|
1728
|
+
options: BEDROCK_REGIONS
|
|
1729
|
+
}
|
|
1730
|
+
]
|
|
1731
|
+
}
|
|
1732
|
+
],
|
|
1733
|
+
knownModels: BEDROCK_KNOWN_MODELS,
|
|
1734
|
+
// The Bedrock runtime client does not expose a `/models` listing — model
|
|
1735
|
+
// enumeration lives in `@aws-sdk/client-bedrock` (control plane), which we
|
|
1736
|
+
// don't pull in. Return the curated set from the manifest.
|
|
1737
|
+
supportsModelListing: false,
|
|
1738
|
+
docsUrl: "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html"
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
// src/adapters/bedrock/adapter.ts
|
|
1742
|
+
var BedrockAdapter = class {
|
|
1743
|
+
vendor = bedrockManifest.vendor;
|
|
1744
|
+
acceptedAuth = ["bedrockApiKey", "bedrockIam"];
|
|
1745
|
+
client;
|
|
1746
|
+
init(auth, client) {
|
|
1747
|
+
if (!auth.region) {
|
|
1748
|
+
throw new Error("BedrockAdapter: region is required");
|
|
1749
|
+
}
|
|
1750
|
+
const config = {
|
|
1751
|
+
region: auth.region,
|
|
1752
|
+
...client?.endpoint ? { endpoint: client.endpoint } : {},
|
|
1753
|
+
...client?.maxRetries !== void 0 ? { maxAttempts: client.maxRetries + 1 } : {}
|
|
1754
|
+
};
|
|
1755
|
+
if (auth.kind === "bedrockApiKey") {
|
|
1756
|
+
if (!auth.apiKey) {
|
|
1757
|
+
throw new Error("BedrockAdapter: apiKey is required for bedrockApiKey auth");
|
|
1758
|
+
}
|
|
1759
|
+
config.token = { token: auth.apiKey };
|
|
1760
|
+
config.authSchemePreference = ["httpBearerAuth"];
|
|
1761
|
+
} else if (auth.kind === "bedrockIam") {
|
|
1762
|
+
if (!auth.accessKeyId || !auth.secretAccessKey) {
|
|
1763
|
+
throw new Error("BedrockAdapter: accessKeyId and secretAccessKey required for bedrockIam auth");
|
|
1764
|
+
}
|
|
1765
|
+
config.credentials = {
|
|
1766
|
+
accessKeyId: auth.accessKeyId,
|
|
1767
|
+
secretAccessKey: auth.secretAccessKey,
|
|
1768
|
+
...auth.sessionToken ? { sessionToken: auth.sessionToken } : {}
|
|
1769
|
+
};
|
|
1770
|
+
} else {
|
|
1771
|
+
throw new Error(
|
|
1772
|
+
`BedrockAdapter: unsupported auth kind '${auth.kind}'`
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
this.client = new BedrockRuntimeClient(config);
|
|
1776
|
+
}
|
|
1777
|
+
async *stream(req) {
|
|
1778
|
+
assertValidRequest(req);
|
|
1779
|
+
const client = this.ensureClient();
|
|
1780
|
+
if (req.signal?.aborted) {
|
|
1781
|
+
yield { type: "end", finishReason: "aborted" };
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const messages = buildWireMessages3(req);
|
|
1785
|
+
const tools = req.tools && req.tools.length > 0 ? { tools: formatTools4(req.tools) } : void 0;
|
|
1786
|
+
const inferenceConfig = {};
|
|
1787
|
+
if (req.maxTokens !== void 0) inferenceConfig.maxTokens = req.maxTokens;
|
|
1788
|
+
if (req.temperature !== void 0) inferenceConfig.temperature = req.temperature;
|
|
1789
|
+
const commandInput = {
|
|
1790
|
+
modelId: req.model,
|
|
1791
|
+
messages
|
|
1792
|
+
};
|
|
1793
|
+
if (req.system) commandInput.system = [{ text: req.system }];
|
|
1794
|
+
if (tools) commandInput.toolConfig = tools;
|
|
1795
|
+
if (Object.keys(inferenceConfig).length > 0) {
|
|
1796
|
+
commandInput.inferenceConfig = inferenceConfig;
|
|
1797
|
+
}
|
|
1798
|
+
const command = new ConverseStreamCommand(
|
|
1799
|
+
commandInput
|
|
1800
|
+
);
|
|
1801
|
+
let response;
|
|
1802
|
+
try {
|
|
1803
|
+
response = await client.send(command, { abortSignal: req.signal });
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
if (isAbortError4(err)) {
|
|
1806
|
+
yield { type: "end", finishReason: "aborted" };
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
throw err;
|
|
1810
|
+
}
|
|
1811
|
+
if (!response.stream) {
|
|
1812
|
+
yield { type: "end", finishReason: "stop" };
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
let currentToolUse = null;
|
|
1816
|
+
const completedToolCalls = [];
|
|
1817
|
+
let stopReason;
|
|
1818
|
+
let inputTokens;
|
|
1819
|
+
let outputTokens;
|
|
1820
|
+
let totalTokens;
|
|
1821
|
+
try {
|
|
1822
|
+
for await (const event of response.stream) {
|
|
1823
|
+
if (req.signal?.aborted) {
|
|
1824
|
+
yield { type: "end", finishReason: "aborted" };
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (event.contentBlockStart) {
|
|
1828
|
+
const start = event.contentBlockStart.start;
|
|
1829
|
+
if (start?.toolUse) {
|
|
1830
|
+
currentToolUse = {
|
|
1831
|
+
id: start.toolUse.toolUseId ?? "",
|
|
1832
|
+
name: start.toolUse.name ?? "",
|
|
1833
|
+
inputText: ""
|
|
1834
|
+
};
|
|
1835
|
+
if (currentToolUse.id && currentToolUse.name) {
|
|
1836
|
+
yield {
|
|
1837
|
+
type: "toolCallStart",
|
|
1838
|
+
id: currentToolUse.id,
|
|
1839
|
+
name: currentToolUse.name
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
} else if (event.contentBlockDelta) {
|
|
1844
|
+
const delta = event.contentBlockDelta.delta;
|
|
1845
|
+
if (delta?.text) {
|
|
1846
|
+
yield { type: "token", text: delta.text };
|
|
1847
|
+
}
|
|
1848
|
+
if (delta?.toolUse && currentToolUse && typeof delta.toolUse.input === "string") {
|
|
1849
|
+
currentToolUse.inputText += delta.toolUse.input;
|
|
1850
|
+
}
|
|
1851
|
+
} else if (event.contentBlockStop) {
|
|
1852
|
+
if (currentToolUse && currentToolUse.id && currentToolUse.name) {
|
|
1853
|
+
let parsed = {};
|
|
1854
|
+
try {
|
|
1855
|
+
parsed = currentToolUse.inputText ? JSON.parse(currentToolUse.inputText) : {};
|
|
1856
|
+
} catch {
|
|
1857
|
+
parsed = {};
|
|
1858
|
+
}
|
|
1859
|
+
completedToolCalls.push({
|
|
1860
|
+
id: currentToolUse.id,
|
|
1861
|
+
name: currentToolUse.name,
|
|
1862
|
+
arguments: parsed
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
currentToolUse = null;
|
|
1866
|
+
} else if (event.messageStop) {
|
|
1867
|
+
stopReason = event.messageStop.stopReason;
|
|
1868
|
+
} else if (event.metadata) {
|
|
1869
|
+
if (event.metadata.usage) {
|
|
1870
|
+
inputTokens = event.metadata.usage.inputTokens;
|
|
1871
|
+
outputTokens = event.metadata.usage.outputTokens;
|
|
1872
|
+
totalTokens = event.metadata.usage.totalTokens;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
if (isAbortError4(err)) {
|
|
1878
|
+
yield { type: "end", finishReason: "aborted" };
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
throw err;
|
|
1882
|
+
}
|
|
1883
|
+
if (req.signal?.aborted) {
|
|
1884
|
+
yield { type: "end", finishReason: "aborted" };
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
for (const tc of completedToolCalls) {
|
|
1888
|
+
yield {
|
|
1889
|
+
type: "toolCall",
|
|
1890
|
+
id: tc.id,
|
|
1891
|
+
name: tc.name,
|
|
1892
|
+
arguments: tc.arguments
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
const finishReason = mapStopReason2(stopReason, completedToolCalls.length > 0);
|
|
1896
|
+
const usage = inputTokens !== void 0 || outputTokens !== void 0 || totalTokens !== void 0 ? {
|
|
1897
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
1898
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
1899
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1900
|
+
} : void 0;
|
|
1901
|
+
const endEvent = {
|
|
1902
|
+
type: "end",
|
|
1903
|
+
finishReason,
|
|
1904
|
+
...usage ? { usage } : {},
|
|
1905
|
+
...stopReason ? { rawReason: stopReason } : {}
|
|
1906
|
+
};
|
|
1907
|
+
yield endEvent;
|
|
1908
|
+
}
|
|
1909
|
+
appendToolResult(history, toolCallId, result) {
|
|
1910
|
+
const text = typeof result === "string" ? result : JSON.stringify(result);
|
|
1911
|
+
const wireMessage = {
|
|
1912
|
+
role: "user",
|
|
1913
|
+
content: [
|
|
1914
|
+
{
|
|
1915
|
+
toolResult: {
|
|
1916
|
+
toolUseId: toolCallId,
|
|
1917
|
+
content: [{ text }]
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
]
|
|
1921
|
+
};
|
|
1922
|
+
return [
|
|
1923
|
+
...history,
|
|
1924
|
+
{
|
|
1925
|
+
role: "tool",
|
|
1926
|
+
content: text,
|
|
1927
|
+
vendorRaw: wireMessage
|
|
1928
|
+
}
|
|
1929
|
+
];
|
|
1930
|
+
}
|
|
1931
|
+
async listAvailableModels() {
|
|
1932
|
+
return [...bedrockManifest.knownModels];
|
|
1933
|
+
}
|
|
1934
|
+
async warmup() {
|
|
1935
|
+
}
|
|
1936
|
+
ensureClient() {
|
|
1937
|
+
if (!this.client) {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
"BedrockAdapter: init() must be called before stream()/listAvailableModels()"
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
return this.client;
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
function buildWireMessages3(req) {
|
|
1946
|
+
const out = [];
|
|
1947
|
+
for (const msg of req.messages) {
|
|
1948
|
+
if (msg.vendorRaw && typeof msg.vendorRaw === "object") {
|
|
1949
|
+
out.push(msg.vendorRaw);
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
1953
|
+
const role = msg.role === "tool" ? "user" : msg.role;
|
|
1954
|
+
out.push({
|
|
1955
|
+
role,
|
|
1956
|
+
content: [{ text }]
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
return out;
|
|
1960
|
+
}
|
|
1961
|
+
function formatTools4(tools) {
|
|
1962
|
+
return tools.map((t) => ({
|
|
1963
|
+
toolSpec: {
|
|
1964
|
+
name: t.name,
|
|
1965
|
+
// Bedrock rejects empty-string descriptions; fall back to a single space.
|
|
1966
|
+
description: t.description && t.description.length > 0 ? t.description : " ",
|
|
1967
|
+
inputSchema: {
|
|
1968
|
+
json: t.parameters ?? { type: "object", properties: {} }
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}));
|
|
1972
|
+
}
|
|
1973
|
+
function mapStopReason2(r, hadToolCalls) {
|
|
1974
|
+
if (hadToolCalls) return "tool";
|
|
1975
|
+
switch (r) {
|
|
1976
|
+
case "end_turn":
|
|
1977
|
+
case "stop_sequence":
|
|
1978
|
+
return "stop";
|
|
1979
|
+
case "tool_use":
|
|
1980
|
+
return "tool";
|
|
1981
|
+
case "max_tokens":
|
|
1982
|
+
return "length";
|
|
1983
|
+
case "content_filtered":
|
|
1984
|
+
case "guardrail_intervened":
|
|
1985
|
+
return "filtered";
|
|
1986
|
+
default:
|
|
1987
|
+
return "stop";
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function isAbortError4(err) {
|
|
1991
|
+
if (!err || typeof err !== "object") return false;
|
|
1992
|
+
const e = err;
|
|
1993
|
+
return e.name === "AbortError" || e.name === "APIUserAbortError" || e.code === "ABORT_ERR" || typeof e.message === "string" && /aborted/i.test(e.message);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/adapters/bedrock/factory.ts
|
|
1997
|
+
var bedrockFactory = {
|
|
1998
|
+
vendor: bedrockManifest.vendor,
|
|
1999
|
+
manifest: bedrockManifest,
|
|
2000
|
+
create: () => new BedrockAdapter()
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
// src/adapters/index.ts
|
|
2004
|
+
registerAdapter(openAIFactory);
|
|
2005
|
+
registerAdapter(deepseekFactory);
|
|
2006
|
+
registerAdapter(anthropicFactory);
|
|
2007
|
+
registerAdapter(googleFactory);
|
|
2008
|
+
registerAdapter(vertexGeminiFactory);
|
|
2009
|
+
registerAdapter(vertexOpenAIFactory);
|
|
2010
|
+
registerAdapter(bedrockFactory);
|
|
2011
|
+
|
|
2012
|
+
// src/index.ts
|
|
2013
|
+
async function createLlm(args) {
|
|
2014
|
+
const factory = getAdapterFactory(args.vendor);
|
|
2015
|
+
if (!factory.manifest.authKinds.some((k) => k.kind === args.auth.kind)) {
|
|
2016
|
+
const accepted = factory.manifest.authKinds.map((k) => k.kind).join(", ");
|
|
2017
|
+
throw new Error(
|
|
2018
|
+
`Vendor '${args.vendor}' does not accept auth kind '${args.auth.kind}'. Accepted kinds: ${accepted}`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
const instance = factory.create();
|
|
2022
|
+
await Promise.resolve(instance.init(args.auth, args.client));
|
|
2023
|
+
return instance;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/test-kit/runner.ts
|
|
2027
|
+
var VALID_FINISH_REASONS = [
|
|
2028
|
+
"stop",
|
|
2029
|
+
"tool",
|
|
2030
|
+
"length",
|
|
2031
|
+
"filtered",
|
|
2032
|
+
"error",
|
|
2033
|
+
"aborted"
|
|
2034
|
+
];
|
|
2035
|
+
var VALID_FORM_FIELD_TYPES = ["text", "password", "url", "json-file", "select"];
|
|
2036
|
+
function runContractTests(harness) {
|
|
2037
|
+
describe(`contract: ${harness.vendor}`, () => {
|
|
2038
|
+
beforeEach(() => {
|
|
2039
|
+
_resetRegistryForTests();
|
|
2040
|
+
registerAdapter(harness.factory);
|
|
2041
|
+
});
|
|
2042
|
+
afterEach(async () => {
|
|
2043
|
+
await harness.cleanup();
|
|
2044
|
+
_resetRegistryForTests();
|
|
2045
|
+
});
|
|
2046
|
+
it("[1] manifest is well-formed", () => {
|
|
2047
|
+
const m = harness.factory.manifest;
|
|
2048
|
+
expect(m.vendor).toMatch(/^[a-z][a-z0-9-]*$/);
|
|
2049
|
+
expect(m.displayName.length).toBeGreaterThan(0);
|
|
2050
|
+
expect(m.authKinds.length).toBeGreaterThan(0);
|
|
2051
|
+
for (const ak of m.authKinds) {
|
|
2052
|
+
expect(ak.displayName.length).toBeGreaterThan(0);
|
|
2053
|
+
for (const field of ak.fields) {
|
|
2054
|
+
expect(field.name.length).toBeGreaterThan(0);
|
|
2055
|
+
expect(field.label.length).toBeGreaterThan(0);
|
|
2056
|
+
expect(VALID_FORM_FIELD_TYPES).toContain(field.type);
|
|
2057
|
+
if (field.type === "select") {
|
|
2058
|
+
expect(field.options?.length ?? 0).toBeGreaterThan(0);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
expect(m.knownModels.length).toBeGreaterThan(0);
|
|
2063
|
+
for (const model of m.knownModels) {
|
|
2064
|
+
expect(model.id.length).toBeGreaterThan(0);
|
|
2065
|
+
expect(typeof model.capabilities.streaming).toBe("boolean");
|
|
2066
|
+
expect(typeof model.capabilities.tools).toBe("boolean");
|
|
2067
|
+
expect(typeof model.capabilities.vision).toBe("boolean");
|
|
2068
|
+
expect(typeof model.capabilities.systemPrompt).toBe("boolean");
|
|
2069
|
+
}
|
|
2070
|
+
expect(typeof m.supportsModelListing).toBe("boolean");
|
|
2071
|
+
});
|
|
2072
|
+
it("[2] createLlm returns an adapter with matching vendor id", async () => {
|
|
2073
|
+
const firstKind = harness.factory.manifest.authKinds[0].kind;
|
|
2074
|
+
const adapter = await createLlm({
|
|
2075
|
+
vendor: harness.vendor,
|
|
2076
|
+
auth: harness.authFor(firstKind)
|
|
2077
|
+
});
|
|
2078
|
+
expect(adapter.vendor).toBe(harness.vendor);
|
|
2079
|
+
});
|
|
2080
|
+
it("[3] init() accepts each declared auth kind", async () => {
|
|
2081
|
+
for (const ak of harness.factory.manifest.authKinds) {
|
|
2082
|
+
await expect(
|
|
2083
|
+
createLlm({ vendor: harness.vendor, auth: harness.authFor(ak.kind) })
|
|
2084
|
+
).resolves.toBeDefined();
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
if (harness.unsupportedAuth) {
|
|
2088
|
+
it("[4] init() rejects an unsupported auth kind", async () => {
|
|
2089
|
+
await expect(
|
|
2090
|
+
createLlm({
|
|
2091
|
+
vendor: harness.vendor,
|
|
2092
|
+
auth: harness.unsupportedAuth
|
|
2093
|
+
})
|
|
2094
|
+
).rejects.toThrow();
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
it("[18] empty messages array is rejected with a clear error", async () => {
|
|
2098
|
+
const adapter = await init(harness);
|
|
2099
|
+
await expect(
|
|
2100
|
+
drainStream(adapter, {
|
|
2101
|
+
model: harness.toolCapableModel,
|
|
2102
|
+
messages: []
|
|
2103
|
+
})
|
|
2104
|
+
).rejects.toThrowError(/messages/i);
|
|
2105
|
+
});
|
|
2106
|
+
it("[5,6,7] stream emits exactly one end event, as the last event, with a valid finishReason", async () => {
|
|
2107
|
+
await harness.mockScenario("simple-stream");
|
|
2108
|
+
const adapter = await init(harness);
|
|
2109
|
+
const events = await drainStream(adapter, {
|
|
2110
|
+
model: harness.toolCapableModel,
|
|
2111
|
+
messages: [{ role: "user", content: "hello" }]
|
|
2112
|
+
});
|
|
2113
|
+
const endIndexes = events.map((e, i) => e.type === "end" ? i : -1).filter((i) => i >= 0);
|
|
2114
|
+
expect(endIndexes).toHaveLength(1);
|
|
2115
|
+
expect(endIndexes[0]).toBe(events.length - 1);
|
|
2116
|
+
const end = events[events.length - 1];
|
|
2117
|
+
expect(VALID_FINISH_REASONS).toContain(end.finishReason);
|
|
2118
|
+
});
|
|
2119
|
+
it("[8] tokens stream incrementally (at least two chunks)", async () => {
|
|
2120
|
+
await harness.mockScenario("simple-stream");
|
|
2121
|
+
const adapter = await init(harness);
|
|
2122
|
+
const events = await drainStream(adapter, {
|
|
2123
|
+
model: harness.toolCapableModel,
|
|
2124
|
+
messages: [{ role: "user", content: "hello" }]
|
|
2125
|
+
});
|
|
2126
|
+
const tokens = events.filter((e) => e.type === "token");
|
|
2127
|
+
expect(tokens.length).toBeGreaterThanOrEqual(2);
|
|
2128
|
+
});
|
|
2129
|
+
it("[9] pre-fired AbortSignal yields end(aborted) within 500ms", async () => {
|
|
2130
|
+
await harness.mockScenario("simple-stream");
|
|
2131
|
+
const adapter = await init(harness);
|
|
2132
|
+
const controller = new AbortController();
|
|
2133
|
+
controller.abort();
|
|
2134
|
+
const start = Date.now();
|
|
2135
|
+
const events = await drainStream(adapter, {
|
|
2136
|
+
model: harness.toolCapableModel,
|
|
2137
|
+
messages: [{ role: "user", content: "hello" }],
|
|
2138
|
+
signal: controller.signal
|
|
2139
|
+
});
|
|
2140
|
+
const elapsed = Date.now() - start;
|
|
2141
|
+
expect(elapsed).toBeLessThan(500);
|
|
2142
|
+
const end = events[events.length - 1];
|
|
2143
|
+
expect(end.type).toBe("end");
|
|
2144
|
+
expect(end.finishReason).toBe("aborted");
|
|
2145
|
+
expect(events.filter((e) => e.type === "token").length).toBe(0);
|
|
2146
|
+
});
|
|
2147
|
+
it("[10] mid-stream abort prevents any pending toolCall from firing", async () => {
|
|
2148
|
+
await harness.mockScenario("long-stream-with-pending-tool");
|
|
2149
|
+
const adapter = await init(harness);
|
|
2150
|
+
const controller = new AbortController();
|
|
2151
|
+
const events = [];
|
|
2152
|
+
let aborted = false;
|
|
2153
|
+
for await (const evt of adapter.stream({
|
|
2154
|
+
model: harness.toolCapableModel,
|
|
2155
|
+
messages: [{ role: "user", content: "go" }],
|
|
2156
|
+
signal: controller.signal
|
|
2157
|
+
})) {
|
|
2158
|
+
events.push(evt);
|
|
2159
|
+
if (!aborted && evt.type === "token") {
|
|
2160
|
+
controller.abort();
|
|
2161
|
+
aborted = true;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const end = events[events.length - 1];
|
|
2165
|
+
expect(end.type).toBe("end");
|
|
2166
|
+
expect(end.finishReason).toBe("aborted");
|
|
2167
|
+
expect(events.filter((e) => e.type === "toolCall")).toHaveLength(0);
|
|
2168
|
+
});
|
|
2169
|
+
it("[11] tool-capable request yields a well-formed toolCall event", async () => {
|
|
2170
|
+
await harness.mockScenario("tool-call");
|
|
2171
|
+
const adapter = await init(harness);
|
|
2172
|
+
const events = await drainStream(adapter, {
|
|
2173
|
+
model: harness.toolCapableModel,
|
|
2174
|
+
messages: [{ role: "user", content: "call the tool" }],
|
|
2175
|
+
tools: [
|
|
2176
|
+
{
|
|
2177
|
+
name: "test_tool",
|
|
2178
|
+
description: "A test tool",
|
|
2179
|
+
parameters: { type: "object", properties: {} }
|
|
2180
|
+
}
|
|
2181
|
+
]
|
|
2182
|
+
});
|
|
2183
|
+
const toolCalls = events.filter(
|
|
2184
|
+
(e) => e.type === "toolCall"
|
|
2185
|
+
);
|
|
2186
|
+
expect(toolCalls.length).toBeGreaterThanOrEqual(1);
|
|
2187
|
+
const tc = toolCalls[0];
|
|
2188
|
+
expect(typeof tc.id).toBe("string");
|
|
2189
|
+
expect(tc.id.length).toBeGreaterThan(0);
|
|
2190
|
+
expect(typeof tc.name).toBe("string");
|
|
2191
|
+
expect(tc.name.length).toBeGreaterThan(0);
|
|
2192
|
+
expect(tc.arguments).toBeDefined();
|
|
2193
|
+
});
|
|
2194
|
+
if (harness.emitsToolCallStart) {
|
|
2195
|
+
it("[12] every toolCallStart precedes its matching toolCall with the same id", async () => {
|
|
2196
|
+
await harness.mockScenario("tool-call-after-tokens");
|
|
2197
|
+
const adapter = await init(harness);
|
|
2198
|
+
const events = await drainStream(adapter, {
|
|
2199
|
+
model: harness.toolCapableModel,
|
|
2200
|
+
messages: [{ role: "user", content: "hi" }],
|
|
2201
|
+
tools: [
|
|
2202
|
+
{
|
|
2203
|
+
name: "test_tool",
|
|
2204
|
+
description: "A test tool",
|
|
2205
|
+
parameters: { type: "object", properties: {} }
|
|
2206
|
+
}
|
|
2207
|
+
]
|
|
2208
|
+
});
|
|
2209
|
+
const startIndexById = /* @__PURE__ */ new Map();
|
|
2210
|
+
for (let i = 0; i < events.length; i++) {
|
|
2211
|
+
const evt = events[i];
|
|
2212
|
+
if (evt.type === "toolCallStart") {
|
|
2213
|
+
startIndexById.set(evt.id, i);
|
|
2214
|
+
} else if (evt.type === "toolCall") {
|
|
2215
|
+
const startIndex = startIndexById.get(evt.id);
|
|
2216
|
+
expect(startIndex, `missing toolCallStart for id ${evt.id}`).toBeDefined();
|
|
2217
|
+
expect(startIndex).toBeLessThan(i);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
it("[13] appendToolResult produces a history that re-streams without throwing", async () => {
|
|
2223
|
+
await harness.mockScenario("tool-call");
|
|
2224
|
+
const adapter = await init(harness);
|
|
2225
|
+
const events = await drainStream(adapter, {
|
|
2226
|
+
model: harness.toolCapableModel,
|
|
2227
|
+
messages: [{ role: "user", content: "call it" }],
|
|
2228
|
+
tools: [
|
|
2229
|
+
{ name: "test_tool", description: "x", parameters: { type: "object" } }
|
|
2230
|
+
]
|
|
2231
|
+
});
|
|
2232
|
+
const tc = events.find(
|
|
2233
|
+
(e) => e.type === "toolCall"
|
|
2234
|
+
);
|
|
2235
|
+
expect(tc).toBeDefined();
|
|
2236
|
+
const history = [
|
|
2237
|
+
{ role: "user", content: "call it" },
|
|
2238
|
+
harness.buildAssistantWithToolCall(tc)
|
|
2239
|
+
];
|
|
2240
|
+
const withResult = adapter.appendToolResult(history, tc.id, { ok: true });
|
|
2241
|
+
expect(withResult.length).toBe(history.length + 1);
|
|
2242
|
+
await harness.mockScenario("simple-stream");
|
|
2243
|
+
await expect(
|
|
2244
|
+
drainStream(adapter, {
|
|
2245
|
+
model: harness.toolCapableModel,
|
|
2246
|
+
messages: withResult
|
|
2247
|
+
})
|
|
2248
|
+
).resolves.toBeDefined();
|
|
2249
|
+
});
|
|
2250
|
+
it("[14] appendToolResult is deterministic for the same inputs", async () => {
|
|
2251
|
+
const adapter = await init(harness);
|
|
2252
|
+
const history = [{ role: "user", content: "hi" }];
|
|
2253
|
+
const a = adapter.appendToolResult(history, "tc_1", { a: 1 });
|
|
2254
|
+
const b = adapter.appendToolResult(history, "tc_1", { a: 1 });
|
|
2255
|
+
expect(JSON.stringify(a)).toBe(JSON.stringify(b));
|
|
2256
|
+
});
|
|
2257
|
+
it("[15] listAvailableModels returns a non-empty list", async () => {
|
|
2258
|
+
await harness.mockScenario("list-models");
|
|
2259
|
+
const adapter = await init(harness);
|
|
2260
|
+
const models = await adapter.listAvailableModels();
|
|
2261
|
+
expect(models.length).toBeGreaterThan(0);
|
|
2262
|
+
for (const m of models) {
|
|
2263
|
+
expect(m.id.length).toBeGreaterThan(0);
|
|
2264
|
+
expect(typeof m.capabilities.tools).toBe("boolean");
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
if (harness.nonToolCapableModel) {
|
|
2268
|
+
it("[16] non-tool-capable model with tools declared either fails fast or runs without tool calls", async () => {
|
|
2269
|
+
await harness.mockScenario("simple-stream");
|
|
2270
|
+
const adapter = await init(harness);
|
|
2271
|
+
const run = async () => drainStream(adapter, {
|
|
2272
|
+
model: harness.nonToolCapableModel,
|
|
2273
|
+
messages: [{ role: "user", content: "hi" }],
|
|
2274
|
+
tools: [
|
|
2275
|
+
{ name: "test_tool", description: "x", parameters: { type: "object" } }
|
|
2276
|
+
]
|
|
2277
|
+
});
|
|
2278
|
+
try {
|
|
2279
|
+
const events = await run();
|
|
2280
|
+
expect(events.filter((e) => e.type === "toolCall")).toHaveLength(0);
|
|
2281
|
+
const end = events[events.length - 1];
|
|
2282
|
+
expect(end.finishReason).not.toBe("error");
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
expect(err).toBeInstanceOf(Error);
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
it("[17] system prompt is present in the upstream request", async () => {
|
|
2289
|
+
await harness.mockScenario("simple-stream");
|
|
2290
|
+
const adapter = await init(harness);
|
|
2291
|
+
await drainStream(adapter, {
|
|
2292
|
+
model: harness.toolCapableModel,
|
|
2293
|
+
system: "You are a test agent.",
|
|
2294
|
+
messages: [{ role: "user", content: "hi" }]
|
|
2295
|
+
});
|
|
2296
|
+
const captured = harness.getCapturedRequest();
|
|
2297
|
+
expect(captured).not.toBeNull();
|
|
2298
|
+
expect(captured.system).toBe("You are a test agent.");
|
|
2299
|
+
});
|
|
2300
|
+
it("[19] multi-turn conversation is preserved in the upstream request", async () => {
|
|
2301
|
+
await harness.mockScenario("simple-stream");
|
|
2302
|
+
const adapter = await init(harness);
|
|
2303
|
+
const messages = [
|
|
2304
|
+
{ role: "user", content: "first" },
|
|
2305
|
+
{ role: "assistant", content: "one" },
|
|
2306
|
+
{ role: "user", content: "second" },
|
|
2307
|
+
{ role: "assistant", content: "two" },
|
|
2308
|
+
{ role: "user", content: "third" }
|
|
2309
|
+
];
|
|
2310
|
+
await drainStream(adapter, { model: harness.toolCapableModel, messages });
|
|
2311
|
+
const captured = harness.getCapturedRequest();
|
|
2312
|
+
expect(captured).not.toBeNull();
|
|
2313
|
+
expect(captured.messageCount).toBe(messages.length);
|
|
2314
|
+
});
|
|
2315
|
+
it("[20] vendorRaw on an assistant message round-trips through the next request", async () => {
|
|
2316
|
+
await harness.mockScenario("tool-call");
|
|
2317
|
+
const adapter = await init(harness);
|
|
2318
|
+
const first = await drainStream(adapter, {
|
|
2319
|
+
model: harness.toolCapableModel,
|
|
2320
|
+
messages: [{ role: "user", content: "go" }],
|
|
2321
|
+
tools: [
|
|
2322
|
+
{ name: "test_tool", description: "x", parameters: { type: "object" } }
|
|
2323
|
+
]
|
|
2324
|
+
});
|
|
2325
|
+
const tc = first.find(
|
|
2326
|
+
(e) => e.type === "toolCall"
|
|
2327
|
+
);
|
|
2328
|
+
expect(tc).toBeDefined();
|
|
2329
|
+
const history = [
|
|
2330
|
+
{ role: "user", content: "go" },
|
|
2331
|
+
harness.buildAssistantWithToolCall(tc)
|
|
2332
|
+
];
|
|
2333
|
+
const withResult = adapter.appendToolResult(history, tc.id, { ok: true });
|
|
2334
|
+
await harness.mockScenario("simple-stream");
|
|
2335
|
+
await drainStream(adapter, {
|
|
2336
|
+
model: harness.toolCapableModel,
|
|
2337
|
+
messages: withResult
|
|
2338
|
+
});
|
|
2339
|
+
const captured = harness.getCapturedRequest();
|
|
2340
|
+
expect(captured).not.toBeNull();
|
|
2341
|
+
expect(captured.vendorRawHonored).toBe(true);
|
|
2342
|
+
});
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
async function init(harness) {
|
|
2346
|
+
const kind = harness.factory.manifest.authKinds[0].kind;
|
|
2347
|
+
return createLlm({ vendor: harness.vendor, auth: harness.authFor(kind) });
|
|
2348
|
+
}
|
|
2349
|
+
async function drainStream(adapter, req) {
|
|
2350
|
+
const events = [];
|
|
2351
|
+
for await (const evt of adapter.stream(req)) {
|
|
2352
|
+
events.push(evt);
|
|
2353
|
+
}
|
|
2354
|
+
return events;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/test-kit/fake-adapter.ts
|
|
2358
|
+
var FakeAdapter = class {
|
|
2359
|
+
constructor(state) {
|
|
2360
|
+
this.state = state;
|
|
2361
|
+
}
|
|
2362
|
+
state;
|
|
2363
|
+
vendor = fakeManifest.vendor;
|
|
2364
|
+
acceptedAuth = ["apiKey"];
|
|
2365
|
+
initialized = false;
|
|
2366
|
+
init(auth, _client) {
|
|
2367
|
+
if (auth.kind !== "apiKey") {
|
|
2368
|
+
throw new Error(`FakeAdapter: unsupported auth kind '${auth.kind}'`);
|
|
2369
|
+
}
|
|
2370
|
+
if (!auth.apiKey) {
|
|
2371
|
+
throw new Error("FakeAdapter: apiKey is required");
|
|
2372
|
+
}
|
|
2373
|
+
this.initialized = true;
|
|
2374
|
+
}
|
|
2375
|
+
async *stream(req) {
|
|
2376
|
+
this.ensureInitialized();
|
|
2377
|
+
assertValidRequest(req);
|
|
2378
|
+
this.capture(req);
|
|
2379
|
+
const scenario = this.state.scenario ?? "simple-stream";
|
|
2380
|
+
if (req.signal?.aborted) {
|
|
2381
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
switch (scenario) {
|
|
2385
|
+
case "simple-stream":
|
|
2386
|
+
yield* simpleStream(req.signal);
|
|
2387
|
+
return;
|
|
2388
|
+
case "tool-call":
|
|
2389
|
+
yield* toolCallOnly(req.signal);
|
|
2390
|
+
return;
|
|
2391
|
+
case "tool-call-after-tokens":
|
|
2392
|
+
yield* toolCallAfterTokens(req.signal);
|
|
2393
|
+
return;
|
|
2394
|
+
case "long-stream":
|
|
2395
|
+
yield* longStream(req.signal);
|
|
2396
|
+
return;
|
|
2397
|
+
case "long-stream-with-pending-tool":
|
|
2398
|
+
yield* longStreamWithPendingTool(req.signal);
|
|
2399
|
+
return;
|
|
2400
|
+
case "list-models":
|
|
2401
|
+
yield* simpleStream(req.signal);
|
|
2402
|
+
return;
|
|
2403
|
+
default:
|
|
2404
|
+
throw new Error(`FakeAdapter: unknown scenario '${scenario}'`);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
appendToolResult(history, toolCallId, result) {
|
|
2408
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
2409
|
+
return [
|
|
2410
|
+
...history,
|
|
2411
|
+
{
|
|
2412
|
+
role: "tool",
|
|
2413
|
+
content,
|
|
2414
|
+
vendorRaw: { tool_call_id: toolCallId }
|
|
2415
|
+
}
|
|
2416
|
+
];
|
|
2417
|
+
}
|
|
2418
|
+
async listAvailableModels() {
|
|
2419
|
+
this.ensureInitialized();
|
|
2420
|
+
return fakeManifest.knownModels;
|
|
2421
|
+
}
|
|
2422
|
+
ensureInitialized() {
|
|
2423
|
+
if (!this.initialized) {
|
|
2424
|
+
throw new Error("FakeAdapter: init() must be called before stream()/listAvailableModels()");
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
capture(req) {
|
|
2428
|
+
this.state.captured = {
|
|
2429
|
+
messageCount: req.messages.length,
|
|
2430
|
+
vendorRawHonored: req.messages.some((m) => m.vendorRaw !== void 0),
|
|
2431
|
+
...req.system !== void 0 ? { system: req.system } : {}
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
async function* simpleStream(signal) {
|
|
2436
|
+
const chunks = ["Hello", ", ", "world", "!"];
|
|
2437
|
+
for (const text of chunks) {
|
|
2438
|
+
if (signal?.aborted) {
|
|
2439
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
yield { type: "token", text };
|
|
2443
|
+
}
|
|
2444
|
+
yield { type: "end", finishReason: "stop", usage: { inputTokens: 5, outputTokens: 4 } };
|
|
2445
|
+
}
|
|
2446
|
+
async function* toolCallOnly(signal) {
|
|
2447
|
+
if (signal?.aborted) {
|
|
2448
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
yield { type: "toolCallStart", id: "call_fake_1", name: "test_tool" };
|
|
2452
|
+
yield {
|
|
2453
|
+
type: "toolCall",
|
|
2454
|
+
id: "call_fake_1",
|
|
2455
|
+
name: "test_tool",
|
|
2456
|
+
arguments: { foo: "bar" }
|
|
2457
|
+
};
|
|
2458
|
+
yield { type: "end", finishReason: "tool" };
|
|
2459
|
+
}
|
|
2460
|
+
async function* toolCallAfterTokens(signal) {
|
|
2461
|
+
const tokens = ["Let ", "me ", "check"];
|
|
2462
|
+
for (const text of tokens) {
|
|
2463
|
+
if (signal?.aborted) {
|
|
2464
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
yield { type: "token", text };
|
|
2468
|
+
}
|
|
2469
|
+
if (signal?.aborted) {
|
|
2470
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
yield { type: "toolCallStart", id: "call_fake_2", name: "test_tool" };
|
|
2474
|
+
yield {
|
|
2475
|
+
type: "toolCall",
|
|
2476
|
+
id: "call_fake_2",
|
|
2477
|
+
name: "test_tool",
|
|
2478
|
+
arguments: { q: "hello" }
|
|
2479
|
+
};
|
|
2480
|
+
yield { type: "end", finishReason: "tool" };
|
|
2481
|
+
}
|
|
2482
|
+
async function* longStream(signal) {
|
|
2483
|
+
for (let i = 0; i < 12; i++) {
|
|
2484
|
+
if (signal?.aborted) {
|
|
2485
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
yield { type: "token", text: `chunk-${i} ` };
|
|
2489
|
+
await sleep(20);
|
|
2490
|
+
}
|
|
2491
|
+
yield { type: "end", finishReason: "stop" };
|
|
2492
|
+
}
|
|
2493
|
+
async function* longStreamWithPendingTool(signal) {
|
|
2494
|
+
for (let i = 0; i < 5; i++) {
|
|
2495
|
+
if (signal?.aborted) {
|
|
2496
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
yield { type: "token", text: `chunk-${i} ` };
|
|
2500
|
+
await sleep(25);
|
|
2501
|
+
}
|
|
2502
|
+
if (signal?.aborted) {
|
|
2503
|
+
yield { type: "end", finishReason: "aborted" };
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
yield { type: "toolCallStart", id: "call_fake_3", name: "test_tool" };
|
|
2507
|
+
yield {
|
|
2508
|
+
type: "toolCall",
|
|
2509
|
+
id: "call_fake_3",
|
|
2510
|
+
name: "test_tool",
|
|
2511
|
+
arguments: { q: "never-reached" }
|
|
2512
|
+
};
|
|
2513
|
+
yield { type: "end", finishReason: "tool" };
|
|
2514
|
+
}
|
|
2515
|
+
function sleep(ms) {
|
|
2516
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2517
|
+
}
|
|
2518
|
+
var fakeManifest = {
|
|
2519
|
+
vendor: "fake",
|
|
2520
|
+
displayName: "Fake (test-kit reference)",
|
|
2521
|
+
authKinds: [
|
|
2522
|
+
{
|
|
2523
|
+
kind: "apiKey",
|
|
2524
|
+
displayName: "API Key",
|
|
2525
|
+
fields: [
|
|
2526
|
+
{ name: "apiKey", label: "API Key", type: "password", required: true }
|
|
2527
|
+
]
|
|
2528
|
+
}
|
|
2529
|
+
],
|
|
2530
|
+
knownModels: [
|
|
2531
|
+
{
|
|
2532
|
+
id: "fake-tool-model",
|
|
2533
|
+
displayName: "Fake Tool-Capable Model",
|
|
2534
|
+
capabilities: {
|
|
2535
|
+
streaming: true,
|
|
2536
|
+
tools: true,
|
|
2537
|
+
vision: false,
|
|
2538
|
+
systemPrompt: true,
|
|
2539
|
+
maxContextTokens: 8e3
|
|
2540
|
+
}
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
id: "fake-plain-model",
|
|
2544
|
+
displayName: "Fake Plain Model (no tools)",
|
|
2545
|
+
capabilities: {
|
|
2546
|
+
streaming: true,
|
|
2547
|
+
tools: false,
|
|
2548
|
+
vision: false,
|
|
2549
|
+
systemPrompt: true,
|
|
2550
|
+
maxContextTokens: 4e3
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
],
|
|
2554
|
+
supportsModelListing: true
|
|
2555
|
+
};
|
|
2556
|
+
function createFakeHarness() {
|
|
2557
|
+
const state = { scenario: null, captured: null };
|
|
2558
|
+
const factory = {
|
|
2559
|
+
vendor: fakeManifest.vendor,
|
|
2560
|
+
manifest: fakeManifest,
|
|
2561
|
+
create: () => new FakeAdapter(state)
|
|
2562
|
+
};
|
|
2563
|
+
return {
|
|
2564
|
+
vendor: fakeManifest.vendor,
|
|
2565
|
+
factory,
|
|
2566
|
+
authFor: (kind) => {
|
|
2567
|
+
if (kind === "apiKey") return { kind: "apiKey", apiKey: "sk-fake" };
|
|
2568
|
+
throw new Error(`FakeHarness: asked for unsupported authFor kind '${kind}'`);
|
|
2569
|
+
},
|
|
2570
|
+
unsupportedAuth: {
|
|
2571
|
+
kind: "bedrockIam",
|
|
2572
|
+
accessKeyId: "x",
|
|
2573
|
+
secretAccessKey: "y",
|
|
2574
|
+
region: "us-east-1"
|
|
2575
|
+
},
|
|
2576
|
+
mockScenario(scenario) {
|
|
2577
|
+
state.scenario = scenario;
|
|
2578
|
+
state.captured = null;
|
|
2579
|
+
},
|
|
2580
|
+
cleanup() {
|
|
2581
|
+
state.scenario = null;
|
|
2582
|
+
state.captured = null;
|
|
2583
|
+
},
|
|
2584
|
+
getCapturedRequest() {
|
|
2585
|
+
return state.captured;
|
|
2586
|
+
},
|
|
2587
|
+
toolCapableModel: "fake-tool-model",
|
|
2588
|
+
nonToolCapableModel: "fake-plain-model",
|
|
2589
|
+
emitsToolCallStart: true,
|
|
2590
|
+
buildAssistantWithToolCall(tc) {
|
|
2591
|
+
return {
|
|
2592
|
+
role: "assistant",
|
|
2593
|
+
content: "",
|
|
2594
|
+
vendorRaw: {
|
|
2595
|
+
role: "assistant",
|
|
2596
|
+
toolCall: { id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
export {
|
|
2603
|
+
FakeAdapter,
|
|
2604
|
+
createFakeHarness,
|
|
2605
|
+
fakeManifest,
|
|
2606
|
+
runContractTests
|
|
2607
|
+
};
|
|
2608
|
+
//# sourceMappingURL=index.js.map
|