@broberg/ai-sdk 0.2.0 → 0.3.1
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/dist/index.d.ts +65 -3
- package/dist/index.js +447 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,6 +81,51 @@ async function subprocessTransport(req) {
|
|
|
81
81
|
return parseClaudeCliJson(stdout);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// src/transport/stream.ts
|
|
85
|
+
var StreamHttpError = class extends Error {
|
|
86
|
+
status;
|
|
87
|
+
constructor(message, status) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "StreamHttpError";
|
|
90
|
+
this.status = status;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
async function* streamTransport(req) {
|
|
94
|
+
if (!req.http) throw new Error("streamTransport: req.http is required for http transport");
|
|
95
|
+
const { url, method = "POST", headers, body } = req.http;
|
|
96
|
+
const fetchImpl = req.fetch ?? fetch;
|
|
97
|
+
const res = await fetchImpl(url, {
|
|
98
|
+
method,
|
|
99
|
+
headers,
|
|
100
|
+
body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body)
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok || !res.body) {
|
|
103
|
+
const text = await res.text().catch(() => "");
|
|
104
|
+
throw new StreamHttpError(`stream ${res.status}: ${text.slice(0, 300)}`, res.status);
|
|
105
|
+
}
|
|
106
|
+
const reader = res.body.getReader();
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
let buffer = "";
|
|
109
|
+
try {
|
|
110
|
+
for (; ; ) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
let nl;
|
|
115
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
116
|
+
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
117
|
+
buffer = buffer.slice(nl + 1);
|
|
118
|
+
if (!line.startsWith("data:")) continue;
|
|
119
|
+
const data = line.slice(5).trim();
|
|
120
|
+
if (data === "[DONE]") return;
|
|
121
|
+
if (data) yield data;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
84
129
|
// src/providers/tools.ts
|
|
85
130
|
function family(provider) {
|
|
86
131
|
if (provider === "gemini" || provider === "google") return "gemini";
|
|
@@ -274,17 +319,41 @@ function flattenForSubprocess(messages) {
|
|
|
274
319
|
function anthropicAdapter(config = {}) {
|
|
275
320
|
const baseUrl = config.baseUrl ?? "https://api.anthropic.com";
|
|
276
321
|
const version = config.anthropicVersion ?? "2023-06-01";
|
|
277
|
-
|
|
278
|
-
const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
279
|
-
if (!apiKey) throw new Error("anthropic adapter: API key not set (env ANTHROPIC_API_KEY)");
|
|
322
|
+
function buildBody(req) {
|
|
280
323
|
const system = [];
|
|
281
324
|
const messages = [];
|
|
282
325
|
for (const m of req.messages) {
|
|
283
326
|
if (m.role === "system") {
|
|
284
327
|
system.push(typeof m.content === "string" ? m.content : "");
|
|
285
|
-
|
|
286
|
-
messages.push({ role: m.role === "assistant" ? "assistant" : "user", content: contentBlocks(m.content) });
|
|
328
|
+
continue;
|
|
287
329
|
}
|
|
330
|
+
if (m.role === "tool") {
|
|
331
|
+
messages.push({
|
|
332
|
+
role: "user",
|
|
333
|
+
content: [
|
|
334
|
+
{
|
|
335
|
+
type: "tool_result",
|
|
336
|
+
tool_use_id: m.toolCallId ?? "",
|
|
337
|
+
content: typeof m.content === "string" ? m.content : ""
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
|
|
344
|
+
const blocks = [];
|
|
345
|
+
if (typeof m.content === "string") {
|
|
346
|
+
if (m.content.length > 0) blocks.push({ type: "text", text: m.content });
|
|
347
|
+
} else {
|
|
348
|
+
blocks.push(...contentBlocks(m.content));
|
|
349
|
+
}
|
|
350
|
+
for (const tc of m.toolCalls) {
|
|
351
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
|
|
352
|
+
}
|
|
353
|
+
messages.push({ role: "assistant", content: blocks });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
messages.push({ role: m.role === "assistant" ? "assistant" : "user", content: contentBlocks(m.content) });
|
|
288
357
|
}
|
|
289
358
|
const body = {
|
|
290
359
|
model: req.spec.model,
|
|
@@ -295,6 +364,16 @@ function anthropicAdapter(config = {}) {
|
|
|
295
364
|
if (system.length > 0) body.system = system.join("\n");
|
|
296
365
|
if (req.tools) body.tools = toProviderTools(req.tools, "anthropic");
|
|
297
366
|
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
367
|
+
return body;
|
|
368
|
+
}
|
|
369
|
+
function apiKeyOrThrow() {
|
|
370
|
+
const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
371
|
+
if (!apiKey) throw new Error("anthropic adapter: API key not set (env ANTHROPIC_API_KEY)");
|
|
372
|
+
return apiKey;
|
|
373
|
+
}
|
|
374
|
+
async function chatHttp(req) {
|
|
375
|
+
const apiKey = apiKeyOrThrow();
|
|
376
|
+
const body = buildBody(req);
|
|
298
377
|
const res = await httpTransport({
|
|
299
378
|
spec: req.spec,
|
|
300
379
|
http: {
|
|
@@ -345,7 +424,110 @@ function anthropicAdapter(config = {}) {
|
|
|
345
424
|
async function chat(req) {
|
|
346
425
|
return req.spec.transport === "subprocess" ? chatSubprocess(req) : chatHttp(req);
|
|
347
426
|
}
|
|
348
|
-
|
|
427
|
+
async function* chatStream(req) {
|
|
428
|
+
if (req.spec.transport === "subprocess") {
|
|
429
|
+
throw new Error("anthropic adapter: streaming is not supported over the subprocess transport");
|
|
430
|
+
}
|
|
431
|
+
const apiKey = apiKeyOrThrow();
|
|
432
|
+
const body = { ...buildBody(req), stream: true };
|
|
433
|
+
const stream = streamTransport({
|
|
434
|
+
spec: req.spec,
|
|
435
|
+
fetch: config.fetch,
|
|
436
|
+
http: {
|
|
437
|
+
url: `${baseUrl}/v1/messages`,
|
|
438
|
+
headers: {
|
|
439
|
+
"content-type": "application/json",
|
|
440
|
+
"x-api-key": apiKey,
|
|
441
|
+
"anthropic-version": version
|
|
442
|
+
},
|
|
443
|
+
body
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
let inputTokens = 0;
|
|
447
|
+
let outputTokens = 0;
|
|
448
|
+
let cacheReadTokens = 0;
|
|
449
|
+
let cacheCreationTokens = 0;
|
|
450
|
+
let stopReason = null;
|
|
451
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
452
|
+
for await (const data of stream) {
|
|
453
|
+
let ev;
|
|
454
|
+
try {
|
|
455
|
+
ev = JSON.parse(data);
|
|
456
|
+
} catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
switch (ev.type) {
|
|
460
|
+
case "message_start": {
|
|
461
|
+
const u = ev.message?.usage;
|
|
462
|
+
inputTokens = u?.input_tokens ?? 0;
|
|
463
|
+
cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
464
|
+
cacheCreationTokens = u?.cache_creation_input_tokens ?? 0;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "content_block_start": {
|
|
468
|
+
if (ev.content_block?.type === "tool_use" && ev.index !== void 0) {
|
|
469
|
+
toolBlocks.set(ev.index, {
|
|
470
|
+
id: ev.content_block.id ?? "",
|
|
471
|
+
name: ev.content_block.name ?? "",
|
|
472
|
+
json: ""
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case "content_block_delta": {
|
|
478
|
+
const d = ev.delta;
|
|
479
|
+
if (d?.type === "text_delta" && d.text) {
|
|
480
|
+
yield { type: "text", delta: d.text };
|
|
481
|
+
} else if (d?.type === "input_json_delta" && d.partial_json && ev.index !== void 0) {
|
|
482
|
+
const b = toolBlocks.get(ev.index);
|
|
483
|
+
if (b) b.json += d.partial_json;
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case "message_delta": {
|
|
488
|
+
if (ev.delta?.stop_reason) stopReason = ev.delta.stop_reason;
|
|
489
|
+
if (ev.usage?.output_tokens !== void 0) outputTokens = ev.usage.output_tokens;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
default:
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const [, b] of [...toolBlocks.entries()].sort((a, c) => a[0] - c[0])) {
|
|
497
|
+
let args = {};
|
|
498
|
+
try {
|
|
499
|
+
args = b.json ? JSON.parse(b.json) : {};
|
|
500
|
+
} catch {
|
|
501
|
+
args = {};
|
|
502
|
+
}
|
|
503
|
+
yield { type: "tool_call", id: b.id, name: b.name, args };
|
|
504
|
+
}
|
|
505
|
+
const usage = freshUsage({
|
|
506
|
+
provider: "anthropic",
|
|
507
|
+
model: req.spec.model,
|
|
508
|
+
transport: "http",
|
|
509
|
+
capability: "chat",
|
|
510
|
+
inputTokens,
|
|
511
|
+
outputTokens,
|
|
512
|
+
cacheReadTokens,
|
|
513
|
+
cacheCreationTokens
|
|
514
|
+
});
|
|
515
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
516
|
+
yield { type: "finish", reason: mapAnthropicStop(stopReason) };
|
|
517
|
+
}
|
|
518
|
+
return { name: "anthropic", chat, chatStream, vision: chat };
|
|
519
|
+
}
|
|
520
|
+
function mapAnthropicStop(reason) {
|
|
521
|
+
switch (reason) {
|
|
522
|
+
case "tool_use":
|
|
523
|
+
return "tool_calls";
|
|
524
|
+
case "max_tokens":
|
|
525
|
+
return "length";
|
|
526
|
+
case "stop_sequence":
|
|
527
|
+
return "stop";
|
|
528
|
+
default:
|
|
529
|
+
return "end_turn";
|
|
530
|
+
}
|
|
349
531
|
}
|
|
350
532
|
|
|
351
533
|
// src/providers/openai-compatible.ts
|
|
@@ -353,6 +535,13 @@ function toOpenAIMessage(m) {
|
|
|
353
535
|
if (typeof m.content === "string") {
|
|
354
536
|
const base = { role: m.role, content: m.content };
|
|
355
537
|
if (m.toolCallId) base.tool_call_id = m.toolCallId;
|
|
538
|
+
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
539
|
+
base.tool_calls = m.toolCalls.map((tc) => ({
|
|
540
|
+
id: tc.id,
|
|
541
|
+
type: "function",
|
|
542
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
356
545
|
return base;
|
|
357
546
|
}
|
|
358
547
|
const content = m.content.map((p) => {
|
|
@@ -375,6 +564,8 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
375
564
|
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
376
565
|
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
377
566
|
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
567
|
+
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
568
|
+
if (config.costFromResponseField) body.usage = { include: true };
|
|
378
569
|
const res = await httpTransport({
|
|
379
570
|
spec: req.spec,
|
|
380
571
|
http: {
|
|
@@ -404,17 +595,113 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
404
595
|
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
405
596
|
outputTokens: data.usage?.completion_tokens ?? 0
|
|
406
597
|
});
|
|
598
|
+
if (config.costFromResponseField && typeof data.usage?.cost === "number") {
|
|
599
|
+
usage.costUsd = data.usage.cost;
|
|
600
|
+
}
|
|
407
601
|
const result = { text, usage };
|
|
408
602
|
if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
409
603
|
return result;
|
|
410
604
|
}
|
|
605
|
+
async function* chatStream(req) {
|
|
606
|
+
const apiKey = config.apiKey ?? process.env[`${config.name.toUpperCase()}_API_KEY`];
|
|
607
|
+
if (!apiKey) {
|
|
608
|
+
throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
|
|
609
|
+
}
|
|
610
|
+
const body = {
|
|
611
|
+
model: req.spec.model,
|
|
612
|
+
messages: req.messages.map(toOpenAIMessage),
|
|
613
|
+
stream: true,
|
|
614
|
+
stream_options: { include_usage: true }
|
|
615
|
+
};
|
|
616
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
617
|
+
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
618
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
619
|
+
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
620
|
+
if (config.costFromResponseField) body.usage = { include: true };
|
|
621
|
+
const stream = streamTransport({
|
|
622
|
+
spec: req.spec,
|
|
623
|
+
fetch: config.fetch,
|
|
624
|
+
http: {
|
|
625
|
+
url: `${config.baseUrl}/chat/completions`,
|
|
626
|
+
headers: {
|
|
627
|
+
"content-type": "application/json",
|
|
628
|
+
Authorization: `Bearer ${apiKey}`,
|
|
629
|
+
...config.extraHeaders
|
|
630
|
+
},
|
|
631
|
+
body
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
const toolAcc = /* @__PURE__ */ new Map();
|
|
635
|
+
let finishReason = null;
|
|
636
|
+
for await (const data of stream) {
|
|
637
|
+
let chunk;
|
|
638
|
+
try {
|
|
639
|
+
chunk = JSON.parse(data);
|
|
640
|
+
} catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const choice = chunk.choices?.[0];
|
|
644
|
+
if (choice) {
|
|
645
|
+
const delta = choice.delta ?? {};
|
|
646
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
647
|
+
yield { type: "text", delta: delta.content };
|
|
648
|
+
}
|
|
649
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
650
|
+
const idx = tc.index ?? 0;
|
|
651
|
+
const cur = toolAcc.get(idx) ?? { id: "", name: "", args: "" };
|
|
652
|
+
if (tc.id) cur.id = tc.id;
|
|
653
|
+
if (tc.function?.name) cur.name = tc.function.name;
|
|
654
|
+
if (tc.function?.arguments) cur.args += tc.function.arguments;
|
|
655
|
+
toolAcc.set(idx, cur);
|
|
656
|
+
}
|
|
657
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
658
|
+
}
|
|
659
|
+
if (chunk.usage) {
|
|
660
|
+
const usage = freshUsage({
|
|
661
|
+
provider: config.name,
|
|
662
|
+
model: req.spec.model,
|
|
663
|
+
transport: "http",
|
|
664
|
+
capability: "chat",
|
|
665
|
+
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
666
|
+
outputTokens: chunk.usage.completion_tokens ?? 0
|
|
667
|
+
});
|
|
668
|
+
if (config.costFromResponseField && typeof chunk.usage.cost === "number") {
|
|
669
|
+
usage.costUsd = chunk.usage.cost;
|
|
670
|
+
}
|
|
671
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const [, t] of [...toolAcc.entries()].sort((a, b) => a[0] - b[0])) {
|
|
675
|
+
let args = {};
|
|
676
|
+
try {
|
|
677
|
+
args = t.args ? JSON.parse(t.args) : {};
|
|
678
|
+
} catch {
|
|
679
|
+
args = {};
|
|
680
|
+
}
|
|
681
|
+
yield { type: "tool_call", id: t.id, name: t.name, args };
|
|
682
|
+
}
|
|
683
|
+
yield { type: "finish", reason: mapFinishReason(finishReason) };
|
|
684
|
+
}
|
|
411
685
|
return {
|
|
412
686
|
name: config.name,
|
|
413
687
|
chat,
|
|
688
|
+
chatStream,
|
|
414
689
|
// gpt-4o-class models are multimodal — vision shares the chat path.
|
|
415
690
|
vision: chat
|
|
416
691
|
};
|
|
417
692
|
}
|
|
693
|
+
function mapFinishReason(reason) {
|
|
694
|
+
switch (reason) {
|
|
695
|
+
case "tool_calls":
|
|
696
|
+
return "tool_calls";
|
|
697
|
+
case "length":
|
|
698
|
+
return "length";
|
|
699
|
+
case "stop":
|
|
700
|
+
return "stop";
|
|
701
|
+
default:
|
|
702
|
+
return "end_turn";
|
|
703
|
+
}
|
|
704
|
+
}
|
|
418
705
|
|
|
419
706
|
// src/providers/openai.ts
|
|
420
707
|
var WHISPER_PRICE_PER_MIN = {
|
|
@@ -495,11 +782,12 @@ function partsFrom(content) {
|
|
|
495
782
|
}
|
|
496
783
|
function geminiAdapter(config = {}) {
|
|
497
784
|
const baseUrl = config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
498
|
-
|
|
785
|
+
function resolveKey() {
|
|
499
786
|
const apiKey = config.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
|
|
500
|
-
if (!apiKey)
|
|
501
|
-
|
|
502
|
-
|
|
787
|
+
if (!apiKey) throw new Error("gemini adapter: API key not set (env GOOGLE_API_KEY)");
|
|
788
|
+
return apiKey;
|
|
789
|
+
}
|
|
790
|
+
function buildBody(req) {
|
|
503
791
|
const systemParts = [];
|
|
504
792
|
const contents = [];
|
|
505
793
|
for (const m of req.messages) {
|
|
@@ -519,6 +807,11 @@ function geminiAdapter(config = {}) {
|
|
|
519
807
|
if (req.maxTokens !== void 0) genConfig.maxOutputTokens = req.maxTokens;
|
|
520
808
|
if (req.temperature !== void 0) genConfig.temperature = req.temperature;
|
|
521
809
|
if (Object.keys(genConfig).length > 0) body.generationConfig = genConfig;
|
|
810
|
+
return body;
|
|
811
|
+
}
|
|
812
|
+
async function chat(req) {
|
|
813
|
+
const apiKey = resolveKey();
|
|
814
|
+
const body = buildBody(req);
|
|
522
815
|
const res = await httpTransport({
|
|
523
816
|
spec: req.spec,
|
|
524
817
|
http: {
|
|
@@ -546,7 +839,71 @@ function geminiAdapter(config = {}) {
|
|
|
546
839
|
if (toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
547
840
|
return result;
|
|
548
841
|
}
|
|
549
|
-
|
|
842
|
+
async function* chatStream(req) {
|
|
843
|
+
const apiKey = resolveKey();
|
|
844
|
+
const body = buildBody(req);
|
|
845
|
+
const stream = streamTransport({
|
|
846
|
+
spec: req.spec,
|
|
847
|
+
fetch: config.fetch,
|
|
848
|
+
http: {
|
|
849
|
+
url: `${baseUrl}/models/${req.spec.model}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`,
|
|
850
|
+
headers: { "content-type": "application/json" },
|
|
851
|
+
body
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
const toolCalls = [];
|
|
855
|
+
let inputTokens = 0;
|
|
856
|
+
let outputTokens = 0;
|
|
857
|
+
let finishReason = null;
|
|
858
|
+
for await (const data of stream) {
|
|
859
|
+
let chunk;
|
|
860
|
+
try {
|
|
861
|
+
chunk = JSON.parse(data);
|
|
862
|
+
} catch {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const candidate = chunk.candidates?.[0];
|
|
866
|
+
for (const p of candidate?.content?.parts ?? []) {
|
|
867
|
+
if (typeof p.text === "string" && p.text.length > 0) {
|
|
868
|
+
yield { type: "text", delta: p.text };
|
|
869
|
+
} else if (p.functionCall) {
|
|
870
|
+
toolCalls.push(fromProviderToolCall({ functionCall: p.functionCall }, "gemini"));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (candidate?.finishReason) finishReason = candidate.finishReason;
|
|
874
|
+
if (chunk.usageMetadata) {
|
|
875
|
+
inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens;
|
|
876
|
+
outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const tc of toolCalls) {
|
|
880
|
+
yield { type: "tool_call", id: tc.id, name: tc.name, args: tc.arguments };
|
|
881
|
+
}
|
|
882
|
+
const usage = freshUsage({
|
|
883
|
+
provider: "gemini",
|
|
884
|
+
model: req.spec.model,
|
|
885
|
+
transport: "http",
|
|
886
|
+
capability: "chat",
|
|
887
|
+
inputTokens,
|
|
888
|
+
outputTokens
|
|
889
|
+
});
|
|
890
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
891
|
+
yield {
|
|
892
|
+
type: "finish",
|
|
893
|
+
reason: toolCalls.length > 0 ? "tool_calls" : mapGeminiFinish(finishReason)
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
return { name: "gemini", chat, chatStream, vision: chat };
|
|
897
|
+
}
|
|
898
|
+
function mapGeminiFinish(reason) {
|
|
899
|
+
switch (reason) {
|
|
900
|
+
case "MAX_TOKENS":
|
|
901
|
+
return "length";
|
|
902
|
+
case "STOP":
|
|
903
|
+
return "end_turn";
|
|
904
|
+
default:
|
|
905
|
+
return reason ? "stop" : "end_turn";
|
|
906
|
+
}
|
|
550
907
|
}
|
|
551
908
|
|
|
552
909
|
// src/providers/deepinfra.ts
|
|
@@ -567,7 +924,10 @@ function openrouterAdapter(config = {}) {
|
|
|
567
924
|
extraHeaders: {
|
|
568
925
|
"HTTP-Referer": config.referer ?? "https://broberg.ai",
|
|
569
926
|
"X-Title": config.title ?? "@broberg/ai-sdk"
|
|
570
|
-
}
|
|
927
|
+
},
|
|
928
|
+
// OpenRouter returns ground-truth usage.cost (USD) when usage:{include:true}
|
|
929
|
+
// is set — use it over the local pricing-table estimate (F010).
|
|
930
|
+
costFromResponseField: true
|
|
571
931
|
});
|
|
572
932
|
}
|
|
573
933
|
|
|
@@ -911,6 +1271,8 @@ var chatInputSchema = z.object({
|
|
|
911
1271
|
tools: z.array(toolSchema).optional(),
|
|
912
1272
|
maxTokens: z.number().int().positive().optional(),
|
|
913
1273
|
temperature: z.number().min(0).max(2).optional(),
|
|
1274
|
+
/** "json" requests JSON-object output (OpenAI-compatible response_format). */
|
|
1275
|
+
responseFormat: z.enum(["json", "text"]).optional(),
|
|
914
1276
|
...callOptions
|
|
915
1277
|
});
|
|
916
1278
|
var visionInputSchema = z.object({
|
|
@@ -1029,6 +1391,73 @@ function createAI(config = {}) {
|
|
|
1029
1391
|
}
|
|
1030
1392
|
throw lastErr;
|
|
1031
1393
|
}
|
|
1394
|
+
function eligibleForFallback(e) {
|
|
1395
|
+
const status = e?.status;
|
|
1396
|
+
if (status === void 0) return true;
|
|
1397
|
+
return status === 429 || status >= 500;
|
|
1398
|
+
}
|
|
1399
|
+
function errorEvent(e) {
|
|
1400
|
+
const ev = {
|
|
1401
|
+
type: "error",
|
|
1402
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1403
|
+
};
|
|
1404
|
+
const status = e?.status;
|
|
1405
|
+
if (status !== void 0) ev.status = status;
|
|
1406
|
+
return ev;
|
|
1407
|
+
}
|
|
1408
|
+
async function* chatStreamImpl(input) {
|
|
1409
|
+
input = chatInputSchema.parse(input);
|
|
1410
|
+
const tier = input.tier ?? "smart";
|
|
1411
|
+
const messages = toMessages(input);
|
|
1412
|
+
const estIn = messages.reduce(
|
|
1413
|
+
(n, m) => n + estTokens(typeof m.content === "string" ? m.content : JSON.stringify(m.content)),
|
|
1414
|
+
0
|
|
1415
|
+
);
|
|
1416
|
+
const estOut = input.maxTokens ?? 512;
|
|
1417
|
+
const routes = [
|
|
1418
|
+
resolveTier(tier, input.override, cfg.defaults),
|
|
1419
|
+
...(input.fallback ?? []).map(
|
|
1420
|
+
(f) => typeof f === "string" ? resolveTier(f, void 0, cfg.defaults) : f
|
|
1421
|
+
)
|
|
1422
|
+
];
|
|
1423
|
+
let lastErr;
|
|
1424
|
+
for (let i = 0; i < routes.length; i++) {
|
|
1425
|
+
const spec = routes[i];
|
|
1426
|
+
await preflight(spec, estIn, estOut);
|
|
1427
|
+
const adapter = pickProvider(spec.provider);
|
|
1428
|
+
if (!adapter.chatStream) {
|
|
1429
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support streaming`);
|
|
1430
|
+
}
|
|
1431
|
+
const t0 = performance.now();
|
|
1432
|
+
let emitted = false;
|
|
1433
|
+
try {
|
|
1434
|
+
for await (const ev of adapter.chatStream({
|
|
1435
|
+
messages,
|
|
1436
|
+
spec,
|
|
1437
|
+
tools: input.tools,
|
|
1438
|
+
maxTokens: input.maxTokens,
|
|
1439
|
+
temperature: input.temperature,
|
|
1440
|
+
responseFormat: input.responseFormat
|
|
1441
|
+
})) {
|
|
1442
|
+
if (ev.type === "text" || ev.type === "tool_call") emitted = true;
|
|
1443
|
+
if (ev.type === "usage") {
|
|
1444
|
+
enrich(ev.usage, "chat", i === 0 ? tier : void 0, input.purpose, performance.now() - t0);
|
|
1445
|
+
await settle(ev.usage);
|
|
1446
|
+
await report(ev.usage);
|
|
1447
|
+
}
|
|
1448
|
+
yield ev;
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
lastErr = e;
|
|
1453
|
+
if (emitted || !eligibleForFallback(e)) {
|
|
1454
|
+
yield errorEvent(e);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
yield errorEvent(lastErr);
|
|
1460
|
+
}
|
|
1032
1461
|
const client = {
|
|
1033
1462
|
async chat(input) {
|
|
1034
1463
|
input = chatInputSchema.parse(input);
|
|
@@ -1049,10 +1478,11 @@ function createAI(config = {}) {
|
|
|
1049
1478
|
invoke: async (spec) => {
|
|
1050
1479
|
const adapter = pickProvider(spec.provider);
|
|
1051
1480
|
if (!adapter.chat) throw new Error(`createAI: provider "${spec.provider}" does not support chat`);
|
|
1052
|
-
return adapter.chat({ messages, spec, tools: input.tools, maxTokens: input.maxTokens, temperature: input.temperature });
|
|
1481
|
+
return adapter.chat({ messages, spec, tools: input.tools, maxTokens: input.maxTokens, temperature: input.temperature, responseFormat: input.responseFormat });
|
|
1053
1482
|
}
|
|
1054
1483
|
});
|
|
1055
1484
|
},
|
|
1485
|
+
chatStream: chatStreamImpl,
|
|
1056
1486
|
async vision(input) {
|
|
1057
1487
|
input = visionInputSchema.parse(input);
|
|
1058
1488
|
const tier = input.tier ?? VISION_DEFAULT_TIER;
|
|
@@ -1235,8 +1665,8 @@ var stubProviders = {
|
|
|
1235
1665
|
};
|
|
1236
1666
|
|
|
1237
1667
|
// src/version.ts
|
|
1238
|
-
var VERSION = "0.
|
|
1239
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
1668
|
+
var VERSION = "0.3.1";
|
|
1669
|
+
var SDK_TAG = "@broberg/ai-sdk@0.3.1";
|
|
1240
1670
|
|
|
1241
1671
|
// src/cost/budget-store.ts
|
|
1242
1672
|
function sqliteBudgetStore(config) {
|
|
@@ -1465,6 +1895,7 @@ export {
|
|
|
1465
1895
|
BudgetGuard,
|
|
1466
1896
|
DEFAULT_TIER_MAP,
|
|
1467
1897
|
SDK_TAG,
|
|
1898
|
+
StreamHttpError,
|
|
1468
1899
|
VERSION,
|
|
1469
1900
|
aiConfigSchema,
|
|
1470
1901
|
anthropicAdapter,
|
|
@@ -1499,6 +1930,7 @@ export {
|
|
|
1499
1930
|
resolveTier,
|
|
1500
1931
|
sqliteBudgetStore,
|
|
1501
1932
|
sqliteSink,
|
|
1933
|
+
streamTransport,
|
|
1502
1934
|
stubProviders,
|
|
1503
1935
|
subprocessTransport,
|
|
1504
1936
|
tierSpecSchema,
|