@broberg/ai-sdk 0.2.0 → 0.3.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/dist/index.d.ts +65 -3
- package/dist/index.js +419 -13
- 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,9 +319,7 @@ 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) {
|
|
@@ -295,6 +338,16 @@ function anthropicAdapter(config = {}) {
|
|
|
295
338
|
if (system.length > 0) body.system = system.join("\n");
|
|
296
339
|
if (req.tools) body.tools = toProviderTools(req.tools, "anthropic");
|
|
297
340
|
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
341
|
+
return body;
|
|
342
|
+
}
|
|
343
|
+
function apiKeyOrThrow() {
|
|
344
|
+
const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
345
|
+
if (!apiKey) throw new Error("anthropic adapter: API key not set (env ANTHROPIC_API_KEY)");
|
|
346
|
+
return apiKey;
|
|
347
|
+
}
|
|
348
|
+
async function chatHttp(req) {
|
|
349
|
+
const apiKey = apiKeyOrThrow();
|
|
350
|
+
const body = buildBody(req);
|
|
298
351
|
const res = await httpTransport({
|
|
299
352
|
spec: req.spec,
|
|
300
353
|
http: {
|
|
@@ -345,7 +398,110 @@ function anthropicAdapter(config = {}) {
|
|
|
345
398
|
async function chat(req) {
|
|
346
399
|
return req.spec.transport === "subprocess" ? chatSubprocess(req) : chatHttp(req);
|
|
347
400
|
}
|
|
348
|
-
|
|
401
|
+
async function* chatStream(req) {
|
|
402
|
+
if (req.spec.transport === "subprocess") {
|
|
403
|
+
throw new Error("anthropic adapter: streaming is not supported over the subprocess transport");
|
|
404
|
+
}
|
|
405
|
+
const apiKey = apiKeyOrThrow();
|
|
406
|
+
const body = { ...buildBody(req), stream: true };
|
|
407
|
+
const stream = streamTransport({
|
|
408
|
+
spec: req.spec,
|
|
409
|
+
fetch: config.fetch,
|
|
410
|
+
http: {
|
|
411
|
+
url: `${baseUrl}/v1/messages`,
|
|
412
|
+
headers: {
|
|
413
|
+
"content-type": "application/json",
|
|
414
|
+
"x-api-key": apiKey,
|
|
415
|
+
"anthropic-version": version
|
|
416
|
+
},
|
|
417
|
+
body
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
let inputTokens = 0;
|
|
421
|
+
let outputTokens = 0;
|
|
422
|
+
let cacheReadTokens = 0;
|
|
423
|
+
let cacheCreationTokens = 0;
|
|
424
|
+
let stopReason = null;
|
|
425
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
426
|
+
for await (const data of stream) {
|
|
427
|
+
let ev;
|
|
428
|
+
try {
|
|
429
|
+
ev = JSON.parse(data);
|
|
430
|
+
} catch {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
switch (ev.type) {
|
|
434
|
+
case "message_start": {
|
|
435
|
+
const u = ev.message?.usage;
|
|
436
|
+
inputTokens = u?.input_tokens ?? 0;
|
|
437
|
+
cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
438
|
+
cacheCreationTokens = u?.cache_creation_input_tokens ?? 0;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "content_block_start": {
|
|
442
|
+
if (ev.content_block?.type === "tool_use" && ev.index !== void 0) {
|
|
443
|
+
toolBlocks.set(ev.index, {
|
|
444
|
+
id: ev.content_block.id ?? "",
|
|
445
|
+
name: ev.content_block.name ?? "",
|
|
446
|
+
json: ""
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "content_block_delta": {
|
|
452
|
+
const d = ev.delta;
|
|
453
|
+
if (d?.type === "text_delta" && d.text) {
|
|
454
|
+
yield { type: "text", delta: d.text };
|
|
455
|
+
} else if (d?.type === "input_json_delta" && d.partial_json && ev.index !== void 0) {
|
|
456
|
+
const b = toolBlocks.get(ev.index);
|
|
457
|
+
if (b) b.json += d.partial_json;
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "message_delta": {
|
|
462
|
+
if (ev.delta?.stop_reason) stopReason = ev.delta.stop_reason;
|
|
463
|
+
if (ev.usage?.output_tokens !== void 0) outputTokens = ev.usage.output_tokens;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
default:
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const [, b] of [...toolBlocks.entries()].sort((a, c) => a[0] - c[0])) {
|
|
471
|
+
let args = {};
|
|
472
|
+
try {
|
|
473
|
+
args = b.json ? JSON.parse(b.json) : {};
|
|
474
|
+
} catch {
|
|
475
|
+
args = {};
|
|
476
|
+
}
|
|
477
|
+
yield { type: "tool_call", id: b.id, name: b.name, args };
|
|
478
|
+
}
|
|
479
|
+
const usage = freshUsage({
|
|
480
|
+
provider: "anthropic",
|
|
481
|
+
model: req.spec.model,
|
|
482
|
+
transport: "http",
|
|
483
|
+
capability: "chat",
|
|
484
|
+
inputTokens,
|
|
485
|
+
outputTokens,
|
|
486
|
+
cacheReadTokens,
|
|
487
|
+
cacheCreationTokens
|
|
488
|
+
});
|
|
489
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
490
|
+
yield { type: "finish", reason: mapAnthropicStop(stopReason) };
|
|
491
|
+
}
|
|
492
|
+
return { name: "anthropic", chat, chatStream, vision: chat };
|
|
493
|
+
}
|
|
494
|
+
function mapAnthropicStop(reason) {
|
|
495
|
+
switch (reason) {
|
|
496
|
+
case "tool_use":
|
|
497
|
+
return "tool_calls";
|
|
498
|
+
case "max_tokens":
|
|
499
|
+
return "length";
|
|
500
|
+
case "stop_sequence":
|
|
501
|
+
return "stop";
|
|
502
|
+
default:
|
|
503
|
+
return "end_turn";
|
|
504
|
+
}
|
|
349
505
|
}
|
|
350
506
|
|
|
351
507
|
// src/providers/openai-compatible.ts
|
|
@@ -353,6 +509,13 @@ function toOpenAIMessage(m) {
|
|
|
353
509
|
if (typeof m.content === "string") {
|
|
354
510
|
const base = { role: m.role, content: m.content };
|
|
355
511
|
if (m.toolCallId) base.tool_call_id = m.toolCallId;
|
|
512
|
+
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
513
|
+
base.tool_calls = m.toolCalls.map((tc) => ({
|
|
514
|
+
id: tc.id,
|
|
515
|
+
type: "function",
|
|
516
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
356
519
|
return base;
|
|
357
520
|
}
|
|
358
521
|
const content = m.content.map((p) => {
|
|
@@ -375,6 +538,8 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
375
538
|
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
376
539
|
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
377
540
|
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
541
|
+
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
542
|
+
if (config.costFromResponseField) body.usage = { include: true };
|
|
378
543
|
const res = await httpTransport({
|
|
379
544
|
spec: req.spec,
|
|
380
545
|
http: {
|
|
@@ -404,17 +569,113 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
404
569
|
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
405
570
|
outputTokens: data.usage?.completion_tokens ?? 0
|
|
406
571
|
});
|
|
572
|
+
if (config.costFromResponseField && typeof data.usage?.cost === "number") {
|
|
573
|
+
usage.costUsd = data.usage.cost;
|
|
574
|
+
}
|
|
407
575
|
const result = { text, usage };
|
|
408
576
|
if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
409
577
|
return result;
|
|
410
578
|
}
|
|
579
|
+
async function* chatStream(req) {
|
|
580
|
+
const apiKey = config.apiKey ?? process.env[`${config.name.toUpperCase()}_API_KEY`];
|
|
581
|
+
if (!apiKey) {
|
|
582
|
+
throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
|
|
583
|
+
}
|
|
584
|
+
const body = {
|
|
585
|
+
model: req.spec.model,
|
|
586
|
+
messages: req.messages.map(toOpenAIMessage),
|
|
587
|
+
stream: true,
|
|
588
|
+
stream_options: { include_usage: true }
|
|
589
|
+
};
|
|
590
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
591
|
+
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
592
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
593
|
+
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
594
|
+
if (config.costFromResponseField) body.usage = { include: true };
|
|
595
|
+
const stream = streamTransport({
|
|
596
|
+
spec: req.spec,
|
|
597
|
+
fetch: config.fetch,
|
|
598
|
+
http: {
|
|
599
|
+
url: `${config.baseUrl}/chat/completions`,
|
|
600
|
+
headers: {
|
|
601
|
+
"content-type": "application/json",
|
|
602
|
+
Authorization: `Bearer ${apiKey}`,
|
|
603
|
+
...config.extraHeaders
|
|
604
|
+
},
|
|
605
|
+
body
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
const toolAcc = /* @__PURE__ */ new Map();
|
|
609
|
+
let finishReason = null;
|
|
610
|
+
for await (const data of stream) {
|
|
611
|
+
let chunk;
|
|
612
|
+
try {
|
|
613
|
+
chunk = JSON.parse(data);
|
|
614
|
+
} catch {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const choice = chunk.choices?.[0];
|
|
618
|
+
if (choice) {
|
|
619
|
+
const delta = choice.delta ?? {};
|
|
620
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
621
|
+
yield { type: "text", delta: delta.content };
|
|
622
|
+
}
|
|
623
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
624
|
+
const idx = tc.index ?? 0;
|
|
625
|
+
const cur = toolAcc.get(idx) ?? { id: "", name: "", args: "" };
|
|
626
|
+
if (tc.id) cur.id = tc.id;
|
|
627
|
+
if (tc.function?.name) cur.name = tc.function.name;
|
|
628
|
+
if (tc.function?.arguments) cur.args += tc.function.arguments;
|
|
629
|
+
toolAcc.set(idx, cur);
|
|
630
|
+
}
|
|
631
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
632
|
+
}
|
|
633
|
+
if (chunk.usage) {
|
|
634
|
+
const usage = freshUsage({
|
|
635
|
+
provider: config.name,
|
|
636
|
+
model: req.spec.model,
|
|
637
|
+
transport: "http",
|
|
638
|
+
capability: "chat",
|
|
639
|
+
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
640
|
+
outputTokens: chunk.usage.completion_tokens ?? 0
|
|
641
|
+
});
|
|
642
|
+
if (config.costFromResponseField && typeof chunk.usage.cost === "number") {
|
|
643
|
+
usage.costUsd = chunk.usage.cost;
|
|
644
|
+
}
|
|
645
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
for (const [, t] of [...toolAcc.entries()].sort((a, b) => a[0] - b[0])) {
|
|
649
|
+
let args = {};
|
|
650
|
+
try {
|
|
651
|
+
args = t.args ? JSON.parse(t.args) : {};
|
|
652
|
+
} catch {
|
|
653
|
+
args = {};
|
|
654
|
+
}
|
|
655
|
+
yield { type: "tool_call", id: t.id, name: t.name, args };
|
|
656
|
+
}
|
|
657
|
+
yield { type: "finish", reason: mapFinishReason(finishReason) };
|
|
658
|
+
}
|
|
411
659
|
return {
|
|
412
660
|
name: config.name,
|
|
413
661
|
chat,
|
|
662
|
+
chatStream,
|
|
414
663
|
// gpt-4o-class models are multimodal — vision shares the chat path.
|
|
415
664
|
vision: chat
|
|
416
665
|
};
|
|
417
666
|
}
|
|
667
|
+
function mapFinishReason(reason) {
|
|
668
|
+
switch (reason) {
|
|
669
|
+
case "tool_calls":
|
|
670
|
+
return "tool_calls";
|
|
671
|
+
case "length":
|
|
672
|
+
return "length";
|
|
673
|
+
case "stop":
|
|
674
|
+
return "stop";
|
|
675
|
+
default:
|
|
676
|
+
return "end_turn";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
418
679
|
|
|
419
680
|
// src/providers/openai.ts
|
|
420
681
|
var WHISPER_PRICE_PER_MIN = {
|
|
@@ -495,11 +756,12 @@ function partsFrom(content) {
|
|
|
495
756
|
}
|
|
496
757
|
function geminiAdapter(config = {}) {
|
|
497
758
|
const baseUrl = config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
498
|
-
|
|
759
|
+
function resolveKey() {
|
|
499
760
|
const apiKey = config.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
|
|
500
|
-
if (!apiKey)
|
|
501
|
-
|
|
502
|
-
|
|
761
|
+
if (!apiKey) throw new Error("gemini adapter: API key not set (env GOOGLE_API_KEY)");
|
|
762
|
+
return apiKey;
|
|
763
|
+
}
|
|
764
|
+
function buildBody(req) {
|
|
503
765
|
const systemParts = [];
|
|
504
766
|
const contents = [];
|
|
505
767
|
for (const m of req.messages) {
|
|
@@ -519,6 +781,11 @@ function geminiAdapter(config = {}) {
|
|
|
519
781
|
if (req.maxTokens !== void 0) genConfig.maxOutputTokens = req.maxTokens;
|
|
520
782
|
if (req.temperature !== void 0) genConfig.temperature = req.temperature;
|
|
521
783
|
if (Object.keys(genConfig).length > 0) body.generationConfig = genConfig;
|
|
784
|
+
return body;
|
|
785
|
+
}
|
|
786
|
+
async function chat(req) {
|
|
787
|
+
const apiKey = resolveKey();
|
|
788
|
+
const body = buildBody(req);
|
|
522
789
|
const res = await httpTransport({
|
|
523
790
|
spec: req.spec,
|
|
524
791
|
http: {
|
|
@@ -546,7 +813,71 @@ function geminiAdapter(config = {}) {
|
|
|
546
813
|
if (toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
547
814
|
return result;
|
|
548
815
|
}
|
|
549
|
-
|
|
816
|
+
async function* chatStream(req) {
|
|
817
|
+
const apiKey = resolveKey();
|
|
818
|
+
const body = buildBody(req);
|
|
819
|
+
const stream = streamTransport({
|
|
820
|
+
spec: req.spec,
|
|
821
|
+
fetch: config.fetch,
|
|
822
|
+
http: {
|
|
823
|
+
url: `${baseUrl}/models/${req.spec.model}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`,
|
|
824
|
+
headers: { "content-type": "application/json" },
|
|
825
|
+
body
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
const toolCalls = [];
|
|
829
|
+
let inputTokens = 0;
|
|
830
|
+
let outputTokens = 0;
|
|
831
|
+
let finishReason = null;
|
|
832
|
+
for await (const data of stream) {
|
|
833
|
+
let chunk;
|
|
834
|
+
try {
|
|
835
|
+
chunk = JSON.parse(data);
|
|
836
|
+
} catch {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const candidate = chunk.candidates?.[0];
|
|
840
|
+
for (const p of candidate?.content?.parts ?? []) {
|
|
841
|
+
if (typeof p.text === "string" && p.text.length > 0) {
|
|
842
|
+
yield { type: "text", delta: p.text };
|
|
843
|
+
} else if (p.functionCall) {
|
|
844
|
+
toolCalls.push(fromProviderToolCall({ functionCall: p.functionCall }, "gemini"));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (candidate?.finishReason) finishReason = candidate.finishReason;
|
|
848
|
+
if (chunk.usageMetadata) {
|
|
849
|
+
inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens;
|
|
850
|
+
outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const tc of toolCalls) {
|
|
854
|
+
yield { type: "tool_call", id: tc.id, name: tc.name, args: tc.arguments };
|
|
855
|
+
}
|
|
856
|
+
const usage = freshUsage({
|
|
857
|
+
provider: "gemini",
|
|
858
|
+
model: req.spec.model,
|
|
859
|
+
transport: "http",
|
|
860
|
+
capability: "chat",
|
|
861
|
+
inputTokens,
|
|
862
|
+
outputTokens
|
|
863
|
+
});
|
|
864
|
+
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
865
|
+
yield {
|
|
866
|
+
type: "finish",
|
|
867
|
+
reason: toolCalls.length > 0 ? "tool_calls" : mapGeminiFinish(finishReason)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return { name: "gemini", chat, chatStream, vision: chat };
|
|
871
|
+
}
|
|
872
|
+
function mapGeminiFinish(reason) {
|
|
873
|
+
switch (reason) {
|
|
874
|
+
case "MAX_TOKENS":
|
|
875
|
+
return "length";
|
|
876
|
+
case "STOP":
|
|
877
|
+
return "end_turn";
|
|
878
|
+
default:
|
|
879
|
+
return reason ? "stop" : "end_turn";
|
|
880
|
+
}
|
|
550
881
|
}
|
|
551
882
|
|
|
552
883
|
// src/providers/deepinfra.ts
|
|
@@ -567,7 +898,10 @@ function openrouterAdapter(config = {}) {
|
|
|
567
898
|
extraHeaders: {
|
|
568
899
|
"HTTP-Referer": config.referer ?? "https://broberg.ai",
|
|
569
900
|
"X-Title": config.title ?? "@broberg/ai-sdk"
|
|
570
|
-
}
|
|
901
|
+
},
|
|
902
|
+
// OpenRouter returns ground-truth usage.cost (USD) when usage:{include:true}
|
|
903
|
+
// is set — use it over the local pricing-table estimate (F010).
|
|
904
|
+
costFromResponseField: true
|
|
571
905
|
});
|
|
572
906
|
}
|
|
573
907
|
|
|
@@ -911,6 +1245,8 @@ var chatInputSchema = z.object({
|
|
|
911
1245
|
tools: z.array(toolSchema).optional(),
|
|
912
1246
|
maxTokens: z.number().int().positive().optional(),
|
|
913
1247
|
temperature: z.number().min(0).max(2).optional(),
|
|
1248
|
+
/** "json" requests JSON-object output (OpenAI-compatible response_format). */
|
|
1249
|
+
responseFormat: z.enum(["json", "text"]).optional(),
|
|
914
1250
|
...callOptions
|
|
915
1251
|
});
|
|
916
1252
|
var visionInputSchema = z.object({
|
|
@@ -1029,6 +1365,73 @@ function createAI(config = {}) {
|
|
|
1029
1365
|
}
|
|
1030
1366
|
throw lastErr;
|
|
1031
1367
|
}
|
|
1368
|
+
function eligibleForFallback(e) {
|
|
1369
|
+
const status = e?.status;
|
|
1370
|
+
if (status === void 0) return true;
|
|
1371
|
+
return status === 429 || status >= 500;
|
|
1372
|
+
}
|
|
1373
|
+
function errorEvent(e) {
|
|
1374
|
+
const ev = {
|
|
1375
|
+
type: "error",
|
|
1376
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1377
|
+
};
|
|
1378
|
+
const status = e?.status;
|
|
1379
|
+
if (status !== void 0) ev.status = status;
|
|
1380
|
+
return ev;
|
|
1381
|
+
}
|
|
1382
|
+
async function* chatStreamImpl(input) {
|
|
1383
|
+
input = chatInputSchema.parse(input);
|
|
1384
|
+
const tier = input.tier ?? "smart";
|
|
1385
|
+
const messages = toMessages(input);
|
|
1386
|
+
const estIn = messages.reduce(
|
|
1387
|
+
(n, m) => n + estTokens(typeof m.content === "string" ? m.content : JSON.stringify(m.content)),
|
|
1388
|
+
0
|
|
1389
|
+
);
|
|
1390
|
+
const estOut = input.maxTokens ?? 512;
|
|
1391
|
+
const routes = [
|
|
1392
|
+
resolveTier(tier, input.override, cfg.defaults),
|
|
1393
|
+
...(input.fallback ?? []).map(
|
|
1394
|
+
(f) => typeof f === "string" ? resolveTier(f, void 0, cfg.defaults) : f
|
|
1395
|
+
)
|
|
1396
|
+
];
|
|
1397
|
+
let lastErr;
|
|
1398
|
+
for (let i = 0; i < routes.length; i++) {
|
|
1399
|
+
const spec = routes[i];
|
|
1400
|
+
await preflight(spec, estIn, estOut);
|
|
1401
|
+
const adapter = pickProvider(spec.provider);
|
|
1402
|
+
if (!adapter.chatStream) {
|
|
1403
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support streaming`);
|
|
1404
|
+
}
|
|
1405
|
+
const t0 = performance.now();
|
|
1406
|
+
let emitted = false;
|
|
1407
|
+
try {
|
|
1408
|
+
for await (const ev of adapter.chatStream({
|
|
1409
|
+
messages,
|
|
1410
|
+
spec,
|
|
1411
|
+
tools: input.tools,
|
|
1412
|
+
maxTokens: input.maxTokens,
|
|
1413
|
+
temperature: input.temperature,
|
|
1414
|
+
responseFormat: input.responseFormat
|
|
1415
|
+
})) {
|
|
1416
|
+
if (ev.type === "text" || ev.type === "tool_call") emitted = true;
|
|
1417
|
+
if (ev.type === "usage") {
|
|
1418
|
+
enrich(ev.usage, "chat", i === 0 ? tier : void 0, input.purpose, performance.now() - t0);
|
|
1419
|
+
await settle(ev.usage);
|
|
1420
|
+
await report(ev.usage);
|
|
1421
|
+
}
|
|
1422
|
+
yield ev;
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
} catch (e) {
|
|
1426
|
+
lastErr = e;
|
|
1427
|
+
if (emitted || !eligibleForFallback(e)) {
|
|
1428
|
+
yield errorEvent(e);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
yield errorEvent(lastErr);
|
|
1434
|
+
}
|
|
1032
1435
|
const client = {
|
|
1033
1436
|
async chat(input) {
|
|
1034
1437
|
input = chatInputSchema.parse(input);
|
|
@@ -1049,10 +1452,11 @@ function createAI(config = {}) {
|
|
|
1049
1452
|
invoke: async (spec) => {
|
|
1050
1453
|
const adapter = pickProvider(spec.provider);
|
|
1051
1454
|
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 });
|
|
1455
|
+
return adapter.chat({ messages, spec, tools: input.tools, maxTokens: input.maxTokens, temperature: input.temperature, responseFormat: input.responseFormat });
|
|
1053
1456
|
}
|
|
1054
1457
|
});
|
|
1055
1458
|
},
|
|
1459
|
+
chatStream: chatStreamImpl,
|
|
1056
1460
|
async vision(input) {
|
|
1057
1461
|
input = visionInputSchema.parse(input);
|
|
1058
1462
|
const tier = input.tier ?? VISION_DEFAULT_TIER;
|
|
@@ -1235,8 +1639,8 @@ var stubProviders = {
|
|
|
1235
1639
|
};
|
|
1236
1640
|
|
|
1237
1641
|
// src/version.ts
|
|
1238
|
-
var VERSION = "0.
|
|
1239
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
1642
|
+
var VERSION = "0.3.0";
|
|
1643
|
+
var SDK_TAG = "@broberg/ai-sdk@0.3.0";
|
|
1240
1644
|
|
|
1241
1645
|
// src/cost/budget-store.ts
|
|
1242
1646
|
function sqliteBudgetStore(config) {
|
|
@@ -1465,6 +1869,7 @@ export {
|
|
|
1465
1869
|
BudgetGuard,
|
|
1466
1870
|
DEFAULT_TIER_MAP,
|
|
1467
1871
|
SDK_TAG,
|
|
1872
|
+
StreamHttpError,
|
|
1468
1873
|
VERSION,
|
|
1469
1874
|
aiConfigSchema,
|
|
1470
1875
|
anthropicAdapter,
|
|
@@ -1499,6 +1904,7 @@ export {
|
|
|
1499
1904
|
resolveTier,
|
|
1500
1905
|
sqliteBudgetStore,
|
|
1501
1906
|
sqliteSink,
|
|
1907
|
+
streamTransport,
|
|
1502
1908
|
stubProviders,
|
|
1503
1909
|
subprocessTransport,
|
|
1504
1910
|
tierSpecSchema,
|