@broberg/ai-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/index.d.ts +1146 -0
- package/dist/index.js +1416 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
// src/routing/tier-map.ts
|
|
2
|
+
var DEFAULT_TIER_MAP = {
|
|
3
|
+
fast: { provider: "anthropic", model: "claude-haiku-4-5", transport: "http" },
|
|
4
|
+
smart: { provider: "anthropic", model: "claude-sonnet-4-6", transport: "http" },
|
|
5
|
+
powerful: { provider: "anthropic", model: "claude-opus-4-8", transport: "http" },
|
|
6
|
+
cheap: { provider: "anthropic", model: "claude-haiku-4-5", transport: "subprocess" },
|
|
7
|
+
vision: { provider: "anthropic", model: "claude-sonnet-4-6", transport: "http" },
|
|
8
|
+
embedding: { provider: "openai", model: "text-embedding-3-small", transport: "http" }
|
|
9
|
+
};
|
|
10
|
+
function resolveTier(tier, override, configMap) {
|
|
11
|
+
const base = configMap?.[tier] ?? DEFAULT_TIER_MAP[tier];
|
|
12
|
+
return { ...base, ...override };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/transport/http.ts
|
|
16
|
+
async function httpTransport(req) {
|
|
17
|
+
if (!req.http) {
|
|
18
|
+
throw new Error("httpTransport: req.http is required for http transport");
|
|
19
|
+
}
|
|
20
|
+
const { url, method = "POST", headers, body } = req.http;
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body)
|
|
25
|
+
});
|
|
26
|
+
const json = await res.json().catch(() => void 0);
|
|
27
|
+
return { ok: res.ok, status: res.status, json };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/transport/subprocess.ts
|
|
31
|
+
function parseClaudeCliJson(raw) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`subprocessTransport: could not parse claude -p JSON output: ${raw.slice(0, 200)}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const u = parsed.usage ?? {};
|
|
41
|
+
return {
|
|
42
|
+
text: parsed.result ?? "",
|
|
43
|
+
inputTokens: u.input_tokens ?? 0,
|
|
44
|
+
outputTokens: u.output_tokens ?? 0,
|
|
45
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
46
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
47
|
+
costUsd: 0,
|
|
48
|
+
subprocess: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function subprocessTransport(req) {
|
|
52
|
+
if (!req.subprocess) {
|
|
53
|
+
throw new Error("subprocessTransport: req.subprocess is required for subprocess transport");
|
|
54
|
+
}
|
|
55
|
+
const { prompt, systemPrompt } = req.subprocess;
|
|
56
|
+
const cmd = ["claude", "-p", "--output-format", "json", "--model", req.spec.model];
|
|
57
|
+
if (systemPrompt) cmd.push("--system-prompt", systemPrompt);
|
|
58
|
+
const proc = (() => {
|
|
59
|
+
try {
|
|
60
|
+
return Bun.spawn(cmd, {
|
|
61
|
+
stdin: new Blob([prompt]),
|
|
62
|
+
stdout: "pipe",
|
|
63
|
+
stderr: "pipe"
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`subprocessTransport: failed to spawn 'claude' \u2014 is the CLI installed and on PATH? (${String(err)})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
72
|
+
new Response(proc.stdout).text(),
|
|
73
|
+
new Response(proc.stderr).text(),
|
|
74
|
+
proc.exited
|
|
75
|
+
]);
|
|
76
|
+
if (exitCode !== 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`subprocessTransport: claude -p exited ${exitCode}: ${stderr.slice(0, 300) || stdout.slice(0, 300)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return parseClaudeCliJson(stdout);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/providers/tools.ts
|
|
85
|
+
function family(provider) {
|
|
86
|
+
if (provider === "gemini" || provider === "google") return "gemini";
|
|
87
|
+
if (provider === "anthropic") return "anthropic";
|
|
88
|
+
return "openai";
|
|
89
|
+
}
|
|
90
|
+
function toProviderTools(tools, provider) {
|
|
91
|
+
switch (family(provider)) {
|
|
92
|
+
case "openai":
|
|
93
|
+
return tools.map((t) => ({
|
|
94
|
+
type: "function",
|
|
95
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
96
|
+
}));
|
|
97
|
+
case "gemini":
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
functionDeclarations: tools.map((t) => ({
|
|
101
|
+
name: t.name,
|
|
102
|
+
description: t.description,
|
|
103
|
+
parameters: t.parameters
|
|
104
|
+
}))
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
case "anthropic":
|
|
108
|
+
return tools.map((t) => ({
|
|
109
|
+
name: t.name,
|
|
110
|
+
description: t.description,
|
|
111
|
+
input_schema: t.parameters
|
|
112
|
+
}));
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`toProviderTools: unsupported provider family for "${provider}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function fromProviderToolCall(raw, provider) {
|
|
118
|
+
const r = raw;
|
|
119
|
+
switch (family(provider)) {
|
|
120
|
+
case "openai": {
|
|
121
|
+
const fn = r.function ?? {};
|
|
122
|
+
return {
|
|
123
|
+
id: typeof r.id === "string" ? r.id : "",
|
|
124
|
+
name: fn.name ?? "",
|
|
125
|
+
arguments: parseArgs(fn.arguments)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
case "gemini": {
|
|
129
|
+
const fc = r.functionCall ?? r;
|
|
130
|
+
return {
|
|
131
|
+
id: "",
|
|
132
|
+
// Gemini function calls have no id
|
|
133
|
+
name: fc.name ?? "",
|
|
134
|
+
arguments: fc.args ?? {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "anthropic": {
|
|
138
|
+
return {
|
|
139
|
+
id: typeof r.id === "string" ? r.id : "",
|
|
140
|
+
name: typeof r.name === "string" ? r.name : "",
|
|
141
|
+
arguments: r.input ?? {}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
throw new Error(`fromProviderToolCall: unsupported provider family for "${provider}"`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function parseArgs(raw) {
|
|
149
|
+
if (raw === void 0 || raw === null) return {};
|
|
150
|
+
if (typeof raw === "object") return raw;
|
|
151
|
+
if (typeof raw === "string") {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/cost/pricing.ts
|
|
162
|
+
var V = "2026-06-02";
|
|
163
|
+
var PRICING = {
|
|
164
|
+
// Anthropic (direct API). DEFAULT_TIER_MAP: fast/cheap=haiku, smart/vision=sonnet, powerful=opus.
|
|
165
|
+
"anthropic:claude-haiku-4-5": {
|
|
166
|
+
inputPer1M: 0.8,
|
|
167
|
+
outputPer1M: 4,
|
|
168
|
+
cacheReadPer1M: 0.08,
|
|
169
|
+
cacheWritePer1M: 1,
|
|
170
|
+
version: V
|
|
171
|
+
},
|
|
172
|
+
"anthropic:claude-sonnet-4-6": {
|
|
173
|
+
inputPer1M: 3,
|
|
174
|
+
outputPer1M: 15,
|
|
175
|
+
cacheReadPer1M: 0.3,
|
|
176
|
+
cacheWritePer1M: 3.75,
|
|
177
|
+
version: V
|
|
178
|
+
},
|
|
179
|
+
"anthropic:claude-opus-4-8": {
|
|
180
|
+
inputPer1M: 15,
|
|
181
|
+
outputPer1M: 75,
|
|
182
|
+
cacheReadPer1M: 1.5,
|
|
183
|
+
cacheWritePer1M: 18.75,
|
|
184
|
+
version: V
|
|
185
|
+
},
|
|
186
|
+
// OpenAI. embedding default tier = text-embedding-3-small (no output tokens).
|
|
187
|
+
"openai:text-embedding-3-small": { inputPer1M: 0.02, outputPer1M: 0, version: V },
|
|
188
|
+
"openai:text-embedding-3-large": { inputPer1M: 0.13, outputPer1M: 0, version: V },
|
|
189
|
+
"openai:gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, version: V },
|
|
190
|
+
"openai:gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, version: V },
|
|
191
|
+
// Whisper is priced per minute, not per token — not representable here; transcribe
|
|
192
|
+
// (F5.6) computes its own cost. Listed as 0 so token-based compute never charges it.
|
|
193
|
+
"openai:whisper-1": { inputPer1M: 0, outputPer1M: 0, version: V },
|
|
194
|
+
// OpenRouter (meta-router — model slugs include the upstream vendor).
|
|
195
|
+
"openrouter:anthropic/claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, version: V },
|
|
196
|
+
"openrouter:anthropic/claude-haiku-4-5": { inputPer1M: 0.8, outputPer1M: 4, version: V },
|
|
197
|
+
"openrouter:google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },
|
|
198
|
+
"openrouter:minimax/minimax-m2.7": {
|
|
199
|
+
inputPer1M: 0.3,
|
|
200
|
+
outputPer1M: 1.2,
|
|
201
|
+
version: `${V}-estimate`
|
|
202
|
+
},
|
|
203
|
+
// Google Gemini (direct). Image-gen model used by cms.
|
|
204
|
+
"google:gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V }
|
|
205
|
+
};
|
|
206
|
+
function getPrice(provider, model) {
|
|
207
|
+
return PRICING[`${provider}:${model}`];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/cost/usage.ts
|
|
211
|
+
function computeCost(provider, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheCreationTokens = 0) {
|
|
212
|
+
const price = getPrice(provider, model);
|
|
213
|
+
if (!price) return 0;
|
|
214
|
+
const perToken = (per1M) => per1M / 1e6;
|
|
215
|
+
const inRate = perToken(price.inputPer1M);
|
|
216
|
+
const outRate = perToken(price.outputPer1M);
|
|
217
|
+
const cacheReadRate = price.cacheReadPer1M !== void 0 ? perToken(price.cacheReadPer1M) : inRate;
|
|
218
|
+
const cacheWriteRate = price.cacheWritePer1M !== void 0 ? perToken(price.cacheWritePer1M) : inRate;
|
|
219
|
+
return inputTokens * inRate + outputTokens * outRate + cacheReadTokens * cacheReadRate + cacheCreationTokens * cacheWriteRate;
|
|
220
|
+
}
|
|
221
|
+
function freshUsage(args) {
|
|
222
|
+
const cacheReadTokens = args.cacheReadTokens ?? 0;
|
|
223
|
+
const cacheCreationTokens = args.cacheCreationTokens ?? 0;
|
|
224
|
+
const costUsd = args.subprocess ? 0 : computeCost(
|
|
225
|
+
args.provider,
|
|
226
|
+
args.model,
|
|
227
|
+
args.inputTokens,
|
|
228
|
+
args.outputTokens,
|
|
229
|
+
cacheReadTokens,
|
|
230
|
+
cacheCreationTokens
|
|
231
|
+
);
|
|
232
|
+
const usage = {
|
|
233
|
+
provider: args.provider,
|
|
234
|
+
model: args.model,
|
|
235
|
+
transport: args.transport,
|
|
236
|
+
inputTokens: args.inputTokens,
|
|
237
|
+
outputTokens: args.outputTokens,
|
|
238
|
+
cacheReadTokens,
|
|
239
|
+
cacheCreationTokens,
|
|
240
|
+
costUsd,
|
|
241
|
+
latencyMs: 0,
|
|
242
|
+
capability: args.capability,
|
|
243
|
+
ts: ""
|
|
244
|
+
};
|
|
245
|
+
if (args.subprocess) usage.subprocess = true;
|
|
246
|
+
return usage;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/providers/anthropic.ts
|
|
250
|
+
function contentBlocks(content) {
|
|
251
|
+
if (typeof content === "string") return content;
|
|
252
|
+
return content.map((p) => {
|
|
253
|
+
if (p.type === "text") return { type: "text", text: p.text };
|
|
254
|
+
if (typeof p.image === "string" && /^https?:\/\//.test(p.image)) {
|
|
255
|
+
return { type: "image", source: { type: "url", url: p.image } };
|
|
256
|
+
}
|
|
257
|
+
const data = typeof p.image === "string" ? p.image.replace(/^data:[^;]+;base64,/, "") : Buffer.from(p.image).toString("base64");
|
|
258
|
+
return {
|
|
259
|
+
type: "image",
|
|
260
|
+
source: { type: "base64", media_type: p.mimeType ?? "image/png", data }
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function flattenForSubprocess(messages) {
|
|
265
|
+
const sys = [];
|
|
266
|
+
const turns = [];
|
|
267
|
+
for (const m of messages) {
|
|
268
|
+
const text = typeof m.content === "string" ? m.content : m.content.map((p) => p.type === "text" ? p.text : "[image]").join(" ");
|
|
269
|
+
if (m.role === "system") sys.push(text);
|
|
270
|
+
else turns.push(`${m.role}: ${text}`);
|
|
271
|
+
}
|
|
272
|
+
return { prompt: turns.join("\n\n"), system: sys.length ? sys.join("\n") : void 0 };
|
|
273
|
+
}
|
|
274
|
+
function anthropicAdapter(config = {}) {
|
|
275
|
+
const baseUrl = config.baseUrl ?? "https://api.anthropic.com";
|
|
276
|
+
const version = config.anthropicVersion ?? "2023-06-01";
|
|
277
|
+
async function chatHttp(req) {
|
|
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)");
|
|
280
|
+
const system = [];
|
|
281
|
+
const messages = [];
|
|
282
|
+
for (const m of req.messages) {
|
|
283
|
+
if (m.role === "system") {
|
|
284
|
+
system.push(typeof m.content === "string" ? m.content : "");
|
|
285
|
+
} else {
|
|
286
|
+
messages.push({ role: m.role === "assistant" ? "assistant" : "user", content: contentBlocks(m.content) });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const body = {
|
|
290
|
+
model: req.spec.model,
|
|
291
|
+
max_tokens: req.maxTokens ?? 1024,
|
|
292
|
+
// Anthropic requires max_tokens
|
|
293
|
+
messages
|
|
294
|
+
};
|
|
295
|
+
if (system.length > 0) body.system = system.join("\n");
|
|
296
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "anthropic");
|
|
297
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
298
|
+
const res = await httpTransport({
|
|
299
|
+
spec: req.spec,
|
|
300
|
+
http: {
|
|
301
|
+
url: `${baseUrl}/v1/messages`,
|
|
302
|
+
headers: {
|
|
303
|
+
"content-type": "application/json",
|
|
304
|
+
"x-api-key": apiKey,
|
|
305
|
+
"anthropic-version": version
|
|
306
|
+
},
|
|
307
|
+
body
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
if (!res.ok) throw new Error(`anthropic ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
|
|
311
|
+
const data = res.json;
|
|
312
|
+
const blocks = data.content ?? [];
|
|
313
|
+
const text = blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
314
|
+
const toolCalls = blocks.filter((b) => b.type === "tool_use").map((b) => fromProviderToolCall(b, "anthropic"));
|
|
315
|
+
const usage = freshUsage({
|
|
316
|
+
provider: "anthropic",
|
|
317
|
+
model: req.spec.model,
|
|
318
|
+
transport: "http",
|
|
319
|
+
capability: "chat",
|
|
320
|
+
inputTokens: data.usage?.input_tokens ?? 0,
|
|
321
|
+
outputTokens: data.usage?.output_tokens ?? 0,
|
|
322
|
+
cacheReadTokens: data.usage?.cache_read_input_tokens ?? 0,
|
|
323
|
+
cacheCreationTokens: data.usage?.cache_creation_input_tokens ?? 0
|
|
324
|
+
});
|
|
325
|
+
const result = { text, usage };
|
|
326
|
+
if (toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
async function chatSubprocess(req) {
|
|
330
|
+
const { prompt, system } = flattenForSubprocess(req.messages);
|
|
331
|
+
const r = await subprocessTransport({ spec: req.spec, subprocess: { prompt, systemPrompt: system } });
|
|
332
|
+
const usage = freshUsage({
|
|
333
|
+
provider: "anthropic",
|
|
334
|
+
model: req.spec.model,
|
|
335
|
+
transport: "subprocess",
|
|
336
|
+
capability: "chat",
|
|
337
|
+
inputTokens: r.inputTokens,
|
|
338
|
+
outputTokens: r.outputTokens,
|
|
339
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
340
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
341
|
+
subprocess: true
|
|
342
|
+
});
|
|
343
|
+
return { text: r.text, usage };
|
|
344
|
+
}
|
|
345
|
+
async function chat(req) {
|
|
346
|
+
return req.spec.transport === "subprocess" ? chatSubprocess(req) : chatHttp(req);
|
|
347
|
+
}
|
|
348
|
+
return { name: "anthropic", chat, vision: chat };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/providers/openai-compatible.ts
|
|
352
|
+
function toOpenAIMessage(m) {
|
|
353
|
+
if (typeof m.content === "string") {
|
|
354
|
+
const base = { role: m.role, content: m.content };
|
|
355
|
+
if (m.toolCallId) base.tool_call_id = m.toolCallId;
|
|
356
|
+
return base;
|
|
357
|
+
}
|
|
358
|
+
const content = m.content.map((p) => {
|
|
359
|
+
if (p.type === "text") return { type: "text", text: p.text };
|
|
360
|
+
const url = typeof p.image === "string" ? p.image : `data:${p.mimeType ?? "image/png"};base64,${Buffer.from(p.image).toString("base64")}`;
|
|
361
|
+
return { type: "image_url", image_url: { url } };
|
|
362
|
+
});
|
|
363
|
+
return { role: m.role, content };
|
|
364
|
+
}
|
|
365
|
+
function makeOpenAICompatibleAdapter(config) {
|
|
366
|
+
async function chat(req) {
|
|
367
|
+
const apiKey = config.apiKey ?? process.env[`${config.name.toUpperCase()}_API_KEY`];
|
|
368
|
+
if (!apiKey) {
|
|
369
|
+
throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
|
|
370
|
+
}
|
|
371
|
+
const body = {
|
|
372
|
+
model: req.spec.model,
|
|
373
|
+
messages: req.messages.map(toOpenAIMessage)
|
|
374
|
+
};
|
|
375
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
376
|
+
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
377
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
378
|
+
const res = await httpTransport({
|
|
379
|
+
spec: req.spec,
|
|
380
|
+
http: {
|
|
381
|
+
url: `${config.baseUrl}/chat/completions`,
|
|
382
|
+
headers: {
|
|
383
|
+
"content-type": "application/json",
|
|
384
|
+
Authorization: `Bearer ${apiKey}`,
|
|
385
|
+
...config.extraHeaders
|
|
386
|
+
},
|
|
387
|
+
body
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
throw new Error(`${config.name} ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
|
|
392
|
+
}
|
|
393
|
+
const data = res.json;
|
|
394
|
+
const msg = data.choices?.[0]?.message;
|
|
395
|
+
const text = msg?.content ?? "";
|
|
396
|
+
const toolCalls = msg?.tool_calls?.map(
|
|
397
|
+
(tc) => fromProviderToolCall(tc, "openai")
|
|
398
|
+
);
|
|
399
|
+
const usage = freshUsage({
|
|
400
|
+
provider: config.name,
|
|
401
|
+
model: req.spec.model,
|
|
402
|
+
transport: "http",
|
|
403
|
+
capability: "chat",
|
|
404
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
405
|
+
outputTokens: data.usage?.completion_tokens ?? 0
|
|
406
|
+
});
|
|
407
|
+
const result = { text, usage };
|
|
408
|
+
if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
name: config.name,
|
|
413
|
+
chat,
|
|
414
|
+
// gpt-4o-class models are multimodal — vision shares the chat path.
|
|
415
|
+
vision: chat
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/providers/openai.ts
|
|
420
|
+
function openaiAdapter(config = {}) {
|
|
421
|
+
const baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
|
|
422
|
+
const base = makeOpenAICompatibleAdapter({ name: "openai", baseUrl, apiKey: config.apiKey });
|
|
423
|
+
async function embedding(req) {
|
|
424
|
+
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
|
|
425
|
+
if (!apiKey) throw new Error("openai adapter: API key not set (env OPENAI_API_KEY)");
|
|
426
|
+
const res = await httpTransport({
|
|
427
|
+
spec: req.spec,
|
|
428
|
+
http: {
|
|
429
|
+
url: `${baseUrl}/embeddings`,
|
|
430
|
+
headers: { "content-type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
431
|
+
body: { model: req.spec.model, input: req.input }
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
throw new Error(`openai ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
|
|
436
|
+
}
|
|
437
|
+
const data = res.json;
|
|
438
|
+
const vectors = (data.data ?? []).map((d) => d.embedding);
|
|
439
|
+
const usage = freshUsage({
|
|
440
|
+
provider: "openai",
|
|
441
|
+
model: req.spec.model,
|
|
442
|
+
transport: "http",
|
|
443
|
+
capability: "embedding",
|
|
444
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
445
|
+
outputTokens: 0
|
|
446
|
+
});
|
|
447
|
+
return { vectors, usage };
|
|
448
|
+
}
|
|
449
|
+
async function transcribe(req) {
|
|
450
|
+
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
|
|
451
|
+
if (!apiKey) throw new Error("openai adapter: API key not set (env OPENAI_API_KEY)");
|
|
452
|
+
const form = new FormData();
|
|
453
|
+
form.append("file", new Blob([req.audio]), "audio");
|
|
454
|
+
form.append("model", req.spec.model);
|
|
455
|
+
if (req.language) form.append("language", req.language);
|
|
456
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
457
|
+
const res = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
460
|
+
body: form
|
|
461
|
+
});
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
throw new Error(`openai ${res.status}: ${(await res.text().catch(() => "")).slice(0, 200)}`);
|
|
464
|
+
}
|
|
465
|
+
const data = await res.json();
|
|
466
|
+
const usage = freshUsage({
|
|
467
|
+
provider: "openai",
|
|
468
|
+
model: req.spec.model,
|
|
469
|
+
transport: "http",
|
|
470
|
+
capability: "transcribe",
|
|
471
|
+
inputTokens: 0,
|
|
472
|
+
outputTokens: 0
|
|
473
|
+
// Whisper is per-minute, not token-priced; cost stays 0 for v1.
|
|
474
|
+
});
|
|
475
|
+
return { text: data.text ?? "", usage };
|
|
476
|
+
}
|
|
477
|
+
return { ...base, embedding, transcribe };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/providers/gemini.ts
|
|
481
|
+
function partsFrom(content) {
|
|
482
|
+
if (typeof content === "string") return [{ text: content }];
|
|
483
|
+
return content.map((p) => {
|
|
484
|
+
if (p.type === "text") return { text: p.text };
|
|
485
|
+
const data = typeof p.image === "string" ? p.image.replace(/^data:[^;]+;base64,/, "") : Buffer.from(p.image).toString("base64");
|
|
486
|
+
return { inlineData: { mimeType: p.mimeType ?? "image/png", data } };
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function geminiAdapter(config = {}) {
|
|
490
|
+
const baseUrl = config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
491
|
+
async function chat(req) {
|
|
492
|
+
const apiKey = config.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
|
|
493
|
+
if (!apiKey) {
|
|
494
|
+
throw new Error("gemini adapter: API key not set (env GOOGLE_API_KEY)");
|
|
495
|
+
}
|
|
496
|
+
const systemParts = [];
|
|
497
|
+
const contents = [];
|
|
498
|
+
for (const m of req.messages) {
|
|
499
|
+
if (m.role === "system") {
|
|
500
|
+
systemParts.push(...partsFrom(m.content));
|
|
501
|
+
} else {
|
|
502
|
+
contents.push({
|
|
503
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
504
|
+
parts: partsFrom(m.content)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const body = { contents };
|
|
509
|
+
if (systemParts.length > 0) body.systemInstruction = { parts: systemParts };
|
|
510
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "gemini");
|
|
511
|
+
const genConfig = {};
|
|
512
|
+
if (req.maxTokens !== void 0) genConfig.maxOutputTokens = req.maxTokens;
|
|
513
|
+
if (req.temperature !== void 0) genConfig.temperature = req.temperature;
|
|
514
|
+
if (Object.keys(genConfig).length > 0) body.generationConfig = genConfig;
|
|
515
|
+
const res = await httpTransport({
|
|
516
|
+
spec: req.spec,
|
|
517
|
+
http: {
|
|
518
|
+
url: `${baseUrl}/models/${req.spec.model}:generateContent?key=${encodeURIComponent(apiKey)}`,
|
|
519
|
+
headers: { "content-type": "application/json" },
|
|
520
|
+
body
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
if (!res.ok) {
|
|
524
|
+
throw new Error(`gemini ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`);
|
|
525
|
+
}
|
|
526
|
+
const data = res.json;
|
|
527
|
+
const parts = data.candidates?.[0]?.content?.parts ?? [];
|
|
528
|
+
const text = parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
529
|
+
const toolCalls = parts.filter((p) => p.functionCall).map((p) => fromProviderToolCall({ functionCall: p.functionCall }, "gemini"));
|
|
530
|
+
const usage = freshUsage({
|
|
531
|
+
provider: "gemini",
|
|
532
|
+
model: req.spec.model,
|
|
533
|
+
transport: "http",
|
|
534
|
+
capability: "chat",
|
|
535
|
+
inputTokens: data.usageMetadata?.promptTokenCount ?? 0,
|
|
536
|
+
outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0
|
|
537
|
+
});
|
|
538
|
+
const result = { text, usage };
|
|
539
|
+
if (toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
return { name: "gemini", chat, vision: chat };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/providers/deepinfra.ts
|
|
546
|
+
function deepinfraAdapter(config = {}) {
|
|
547
|
+
return makeOpenAICompatibleAdapter({
|
|
548
|
+
name: "deepinfra",
|
|
549
|
+
baseUrl: config.baseUrl ?? "https://api.deepinfra.com/v1/openai",
|
|
550
|
+
apiKey: config.apiKey
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/providers/openrouter.ts
|
|
555
|
+
function openrouterAdapter(config = {}) {
|
|
556
|
+
return makeOpenAICompatibleAdapter({
|
|
557
|
+
name: "openrouter",
|
|
558
|
+
baseUrl: config.baseUrl ?? "https://openrouter.ai/api/v1",
|
|
559
|
+
apiKey: config.apiKey,
|
|
560
|
+
extraHeaders: {
|
|
561
|
+
"HTTP-Referer": config.referer ?? "https://broberg.ai",
|
|
562
|
+
"X-Title": config.title ?? "@broberg/ai-sdk"
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/providers/fal.ts
|
|
568
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
569
|
+
function falAdapter(config = {}) {
|
|
570
|
+
const doFetch = config.fetch ?? fetch;
|
|
571
|
+
const syncBase = config.syncBaseUrl ?? "https://fal.run";
|
|
572
|
+
const queueBase = config.queueBaseUrl ?? "https://queue.fal.run";
|
|
573
|
+
const pollIntervalMs = config.pollIntervalMs ?? 2e3;
|
|
574
|
+
const timeoutMs = config.timeoutMs ?? 6e4;
|
|
575
|
+
async function image(req) {
|
|
576
|
+
const apiKey = config.apiKey ?? process.env.FAL_KEY;
|
|
577
|
+
if (!apiKey) throw new Error("fal adapter: FAL_KEY not set");
|
|
578
|
+
const headers = { "content-type": "application/json", Authorization: `Key ${apiKey}` };
|
|
579
|
+
const body = { prompt: req.prompt };
|
|
580
|
+
if (req.width !== void 0 && req.height !== void 0) {
|
|
581
|
+
body.image_size = { width: req.width, height: req.height };
|
|
582
|
+
}
|
|
583
|
+
const mode = config.mode ?? "sync";
|
|
584
|
+
const url = await (mode === "sync" ? runSync(req.spec.model, headers, body) : runQueue(req.spec.model, headers, body));
|
|
585
|
+
const usage = freshUsage({
|
|
586
|
+
provider: "fal",
|
|
587
|
+
model: req.spec.model,
|
|
588
|
+
transport: "http",
|
|
589
|
+
capability: "image",
|
|
590
|
+
inputTokens: 0,
|
|
591
|
+
outputTokens: 0
|
|
592
|
+
});
|
|
593
|
+
return { url, usage };
|
|
594
|
+
}
|
|
595
|
+
async function runSync(model, headers, body) {
|
|
596
|
+
const res = await doFetch(`${syncBase}/${model}`, {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers,
|
|
599
|
+
body: JSON.stringify(body)
|
|
600
|
+
});
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
throw new Error(`fal ${res.status}: ${(await res.text().catch(() => "")).slice(0, 200)}`);
|
|
603
|
+
}
|
|
604
|
+
const data = await res.json();
|
|
605
|
+
const out = data.images?.[0]?.url;
|
|
606
|
+
if (!out) throw new Error(`fal: no image url in response`);
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
async function runQueue(model, headers, body) {
|
|
610
|
+
const submitRes = await doFetch(`${queueBase}/${model}`, {
|
|
611
|
+
method: "POST",
|
|
612
|
+
headers,
|
|
613
|
+
body: JSON.stringify(body)
|
|
614
|
+
});
|
|
615
|
+
if (!submitRes.ok) {
|
|
616
|
+
throw new Error(`fal queue submit ${submitRes.status}`);
|
|
617
|
+
}
|
|
618
|
+
const submit = await submitRes.json();
|
|
619
|
+
const statusUrl = submit.status_url;
|
|
620
|
+
const responseUrl = submit.response_url;
|
|
621
|
+
if (!statusUrl || !responseUrl) throw new Error("fal queue: missing status/response url");
|
|
622
|
+
const deadline = Date.now() + timeoutMs;
|
|
623
|
+
for (; ; ) {
|
|
624
|
+
const statusRes = await doFetch(statusUrl, { headers });
|
|
625
|
+
const status = await statusRes.json();
|
|
626
|
+
if (status.status === "COMPLETED") break;
|
|
627
|
+
if (status.status === "FAILED") throw new Error("fal queue: generation FAILED");
|
|
628
|
+
if (Date.now() >= deadline) throw new Error(`fal queue: timed out after ${timeoutMs}ms`);
|
|
629
|
+
await sleep(pollIntervalMs);
|
|
630
|
+
}
|
|
631
|
+
const resultRes = await doFetch(responseUrl, { headers });
|
|
632
|
+
const result = await resultRes.json();
|
|
633
|
+
const out = result.images?.[0]?.url;
|
|
634
|
+
if (!out) throw new Error("fal queue: no image url in result");
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
return { name: "fal", image };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/providers/registry.ts
|
|
641
|
+
var defaultProviders = {
|
|
642
|
+
anthropic: anthropicAdapter(),
|
|
643
|
+
openai: openaiAdapter(),
|
|
644
|
+
gemini: geminiAdapter(),
|
|
645
|
+
deepinfra: deepinfraAdapter(),
|
|
646
|
+
openrouter: openrouterAdapter(),
|
|
647
|
+
fal: falAdapter()
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/cost/budget.ts
|
|
651
|
+
var BudgetExceededError = class extends Error {
|
|
652
|
+
kind;
|
|
653
|
+
limit;
|
|
654
|
+
spent;
|
|
655
|
+
requested;
|
|
656
|
+
constructor(kind, limit, spent, requested) {
|
|
657
|
+
super(
|
|
658
|
+
`Budget exceeded (${kind}): this call's estimated $${requested.toFixed(6)} ` + (kind === "rolling" ? `+ $${spent.toFixed(6)} already spent exceeds the $${limit.toFixed(6)} rolling ceiling` : `exceeds the $${limit.toFixed(6)} per-call ceiling`)
|
|
659
|
+
);
|
|
660
|
+
this.name = "BudgetExceededError";
|
|
661
|
+
this.kind = kind;
|
|
662
|
+
this.limit = limit;
|
|
663
|
+
this.spent = spent;
|
|
664
|
+
this.requested = requested;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
var BudgetGuard = class {
|
|
668
|
+
constructor(config) {
|
|
669
|
+
this.config = config;
|
|
670
|
+
}
|
|
671
|
+
config;
|
|
672
|
+
spentUsd = 0;
|
|
673
|
+
/** Throws BudgetExceededError if `requested` would breach the per-call ceiling
|
|
674
|
+
* or push the rolling total past its ceiling. Call before firing the request. */
|
|
675
|
+
check(requested) {
|
|
676
|
+
const { perCallUsd, rollingUsd } = this.config;
|
|
677
|
+
if (perCallUsd !== void 0 && requested > perCallUsd) {
|
|
678
|
+
throw new BudgetExceededError("per-call", perCallUsd, this.spentUsd, requested);
|
|
679
|
+
}
|
|
680
|
+
if (rollingUsd !== void 0 && this.spentUsd + requested > rollingUsd) {
|
|
681
|
+
throw new BudgetExceededError("rolling", rollingUsd, this.spentUsd, requested);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** Add an actual cost to the running total (after a successful call). */
|
|
685
|
+
record(actual) {
|
|
686
|
+
this.spentUsd += actual;
|
|
687
|
+
}
|
|
688
|
+
get totalSpent() {
|
|
689
|
+
return this.spentUsd;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/capabilities/vision.ts
|
|
694
|
+
var VISION_DEFAULT_TIER = "vision";
|
|
695
|
+
function buildVisionMessages(input) {
|
|
696
|
+
return [
|
|
697
|
+
{
|
|
698
|
+
role: "user",
|
|
699
|
+
content: [
|
|
700
|
+
{ type: "text", text: input.prompt },
|
|
701
|
+
{ type: "image", image: input.image, mimeType: input.mimeType }
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/capabilities/translate.ts
|
|
708
|
+
var TRANSLATE_DEFAULT_TIER = "fast";
|
|
709
|
+
var TRANSLATE_SYSTEM = "You are a translation engine. Translate the user's text only. Return the translation and nothing else \u2014 no preamble, no quotes.";
|
|
710
|
+
function buildTranslateMessages(input) {
|
|
711
|
+
const fromClause = input.from ? ` from ${input.from}` : "";
|
|
712
|
+
return [
|
|
713
|
+
{ role: "system", content: TRANSLATE_SYSTEM },
|
|
714
|
+
{ role: "user", content: `Translate${fromClause} to ${input.to}:
|
|
715
|
+
|
|
716
|
+
${input.text}` }
|
|
717
|
+
];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/capabilities/embedding.ts
|
|
721
|
+
var EMBEDDING_DEFAULT_TIER = "embedding";
|
|
722
|
+
|
|
723
|
+
// src/capabilities/transcribe.ts
|
|
724
|
+
var DEFAULT_TRANSCRIBE_SPEC = {
|
|
725
|
+
provider: "openai",
|
|
726
|
+
model: "whisper-1",
|
|
727
|
+
transport: "http"
|
|
728
|
+
};
|
|
729
|
+
async function resolveAudio(audio, fetchImpl = fetch) {
|
|
730
|
+
if (typeof audio !== "string") return audio;
|
|
731
|
+
if (!/^https?:\/\//.test(audio)) {
|
|
732
|
+
throw new Error("transcribe: string audio must be an http(s) URL (or pass raw bytes)");
|
|
733
|
+
}
|
|
734
|
+
const res = await fetchImpl(audio);
|
|
735
|
+
if (!res.ok) throw new Error(`transcribe: failed to fetch audio (${res.status})`);
|
|
736
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/capabilities/contracts/index.ts
|
|
740
|
+
function parseJsonLoose(text) {
|
|
741
|
+
const fenced = text.replace(/```(?:json)?/gi, "").trim();
|
|
742
|
+
const start = fenced.search(/[[{]/);
|
|
743
|
+
if (start === -1) throw new Error("no JSON found in model output");
|
|
744
|
+
const slice = fenced.slice(start);
|
|
745
|
+
const lastObj = slice.lastIndexOf("}");
|
|
746
|
+
const lastArr = slice.lastIndexOf("]");
|
|
747
|
+
const end = Math.max(lastObj, lastArr);
|
|
748
|
+
return JSON.parse(slice.slice(0, end + 1));
|
|
749
|
+
}
|
|
750
|
+
function makeContracts(client) {
|
|
751
|
+
return {
|
|
752
|
+
async mockup(input) {
|
|
753
|
+
const constraints = input.constraints ? `
|
|
754
|
+
|
|
755
|
+
Constraints:
|
|
756
|
+
${input.constraints}` : "";
|
|
757
|
+
const res = await client.chat({
|
|
758
|
+
system: "You are a UI mockup generator. Output a single self-contained HTML document using Tailwind CSS utility classes. Return ONLY the HTML \u2014 no markdown, no prose.",
|
|
759
|
+
prompt: `Build a UI mockup for:
|
|
760
|
+
${input.description}${constraints}`,
|
|
761
|
+
tier: input.tier ?? "smart",
|
|
762
|
+
purpose: input.purpose ?? "contract:mockup"
|
|
763
|
+
});
|
|
764
|
+
return { html: res.text, usage: res.usage };
|
|
765
|
+
},
|
|
766
|
+
async design(input) {
|
|
767
|
+
const res = await client.vision({
|
|
768
|
+
image: input.screenshot,
|
|
769
|
+
prompt: `You are a design-iteration engine. Given this screenshot, apply the instructions and return a single self-contained HTML document (Tailwind), ONLY the HTML.
|
|
770
|
+
|
|
771
|
+
Instructions:
|
|
772
|
+
${input.instructions}`,
|
|
773
|
+
tier: input.tier ?? "powerful",
|
|
774
|
+
purpose: input.purpose ?? "contract:design"
|
|
775
|
+
});
|
|
776
|
+
return { html: res.text, usage: res.usage };
|
|
777
|
+
},
|
|
778
|
+
async extract(input) {
|
|
779
|
+
const base = "You are a structured-data extractor. Extract the requested data from the text and return ONLY valid JSON \u2014 no markdown, no prose." + (input.instructions ? `
|
|
780
|
+
|
|
781
|
+
${input.instructions}` : "");
|
|
782
|
+
const run = async (reinforce) => {
|
|
783
|
+
const res2 = await client.chat({
|
|
784
|
+
system: reinforce ? `${base}
|
|
785
|
+
|
|
786
|
+
Your previous output was not valid JSON. Return ONLY parseable JSON.` : base,
|
|
787
|
+
prompt: input.text,
|
|
788
|
+
tier: input.tier ?? "smart",
|
|
789
|
+
purpose: input.purpose ?? "contract:extract"
|
|
790
|
+
});
|
|
791
|
+
return res2;
|
|
792
|
+
};
|
|
793
|
+
let res = await run(false);
|
|
794
|
+
try {
|
|
795
|
+
return { data: input.schema.parse(parseJsonLoose(res.text)), usage: res.usage };
|
|
796
|
+
} catch {
|
|
797
|
+
res = await run(true);
|
|
798
|
+
return { data: input.schema.parse(parseJsonLoose(res.text)), usage: res.usage };
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
async classify(input) {
|
|
802
|
+
const res = await client.chat({
|
|
803
|
+
system: 'You are a zero-shot classifier. Choose exactly one label from the provided list. Return ONLY JSON: {"label": "<one of the labels>", "confidence": <0..1>}.',
|
|
804
|
+
prompt: `Labels: ${JSON.stringify(input.labels)}
|
|
805
|
+
|
|
806
|
+
Text:
|
|
807
|
+
${input.text}`,
|
|
808
|
+
tier: input.tier ?? "cheap",
|
|
809
|
+
purpose: input.purpose ?? "contract:classify"
|
|
810
|
+
});
|
|
811
|
+
const parsed = parseJsonLoose(res.text);
|
|
812
|
+
const label = input.labels.includes(parsed.label ?? "") ? parsed.label : input.labels[0] ?? "";
|
|
813
|
+
const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 0;
|
|
814
|
+
return { label, confidence, usage: res.usage };
|
|
815
|
+
},
|
|
816
|
+
async rerank(input) {
|
|
817
|
+
const res = await client.chat({
|
|
818
|
+
system: 'You are a relevance reranker. Score each item 0..1 for relevance to the query and return ONLY JSON: [{"item": "<verbatim item>", "score": <0..1>}], ordered by score desc.',
|
|
819
|
+
prompt: `Query: ${input.query}
|
|
820
|
+
|
|
821
|
+
Items:
|
|
822
|
+
${JSON.stringify(input.items)}`,
|
|
823
|
+
tier: input.tier ?? "fast",
|
|
824
|
+
purpose: input.purpose ?? "contract:rerank"
|
|
825
|
+
});
|
|
826
|
+
const raw = parseJsonLoose(res.text);
|
|
827
|
+
const ranked = (Array.isArray(raw) ? raw : []).map((r) => ({ item: String(r.item ?? ""), score: typeof r.score === "number" ? r.score : 0 })).sort((a, b) => b.score - a.score);
|
|
828
|
+
return { ranked, usage: res.usage };
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/schema/inputs.ts
|
|
834
|
+
import { z } from "zod";
|
|
835
|
+
var transportSchema = z.enum(["http", "subprocess"]);
|
|
836
|
+
var tierSchema = z.enum([
|
|
837
|
+
"fast",
|
|
838
|
+
"smart",
|
|
839
|
+
"powerful",
|
|
840
|
+
"cheap",
|
|
841
|
+
"vision",
|
|
842
|
+
"embedding"
|
|
843
|
+
]);
|
|
844
|
+
var tierSpecSchema = z.object({
|
|
845
|
+
provider: z.string(),
|
|
846
|
+
model: z.string(),
|
|
847
|
+
transport: transportSchema
|
|
848
|
+
});
|
|
849
|
+
var toolSchema = z.object({
|
|
850
|
+
name: z.string(),
|
|
851
|
+
description: z.string(),
|
|
852
|
+
parameters: z.record(z.unknown())
|
|
853
|
+
});
|
|
854
|
+
var toolCallSchema = z.object({
|
|
855
|
+
id: z.string(),
|
|
856
|
+
name: z.string(),
|
|
857
|
+
arguments: z.record(z.unknown())
|
|
858
|
+
});
|
|
859
|
+
var contentPartSchema = z.union([
|
|
860
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
861
|
+
z.object({
|
|
862
|
+
type: z.literal("image"),
|
|
863
|
+
image: z.union([z.string(), z.instanceof(Uint8Array)]),
|
|
864
|
+
mimeType: z.string().optional()
|
|
865
|
+
})
|
|
866
|
+
]);
|
|
867
|
+
var messageSchema = z.object({
|
|
868
|
+
role: z.enum(["system", "user", "assistant", "tool"]),
|
|
869
|
+
content: z.union([z.string(), z.array(contentPartSchema)]),
|
|
870
|
+
toolCalls: z.array(toolCallSchema).optional(),
|
|
871
|
+
toolCallId: z.string().optional()
|
|
872
|
+
});
|
|
873
|
+
var callOptions = {
|
|
874
|
+
tier: tierSchema.optional(),
|
|
875
|
+
override: tierSpecSchema.partial().optional(),
|
|
876
|
+
fallback: z.array(z.union([tierSchema, tierSpecSchema])).optional(),
|
|
877
|
+
purpose: z.string().optional()
|
|
878
|
+
};
|
|
879
|
+
var chatInputSchema = z.object({
|
|
880
|
+
prompt: z.string().optional(),
|
|
881
|
+
messages: z.array(messageSchema).optional(),
|
|
882
|
+
system: z.string().optional(),
|
|
883
|
+
tools: z.array(toolSchema).optional(),
|
|
884
|
+
maxTokens: z.number().int().positive().optional(),
|
|
885
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
886
|
+
...callOptions
|
|
887
|
+
});
|
|
888
|
+
var visionInputSchema = z.object({
|
|
889
|
+
image: z.union([z.string(), z.instanceof(Uint8Array)]),
|
|
890
|
+
prompt: z.string(),
|
|
891
|
+
mimeType: z.string().optional(),
|
|
892
|
+
...callOptions
|
|
893
|
+
});
|
|
894
|
+
var translateInputSchema = z.object({
|
|
895
|
+
text: z.string(),
|
|
896
|
+
to: z.string(),
|
|
897
|
+
from: z.string().optional(),
|
|
898
|
+
...callOptions
|
|
899
|
+
});
|
|
900
|
+
var imageInputSchema = z.object({
|
|
901
|
+
prompt: z.string(),
|
|
902
|
+
width: z.number().int().positive().optional(),
|
|
903
|
+
height: z.number().int().positive().optional(),
|
|
904
|
+
...callOptions
|
|
905
|
+
});
|
|
906
|
+
var embeddingInputSchema = z.object({
|
|
907
|
+
text: z.union([z.string(), z.array(z.string())]),
|
|
908
|
+
...callOptions
|
|
909
|
+
});
|
|
910
|
+
var transcribeInputSchema = z.object({
|
|
911
|
+
/** Audio URL or raw bytes. */
|
|
912
|
+
audio: z.union([z.string(), z.instanceof(Uint8Array)]),
|
|
913
|
+
language: z.string().optional(),
|
|
914
|
+
...callOptions
|
|
915
|
+
});
|
|
916
|
+
var budgetSchema = z.object({
|
|
917
|
+
perCallUsd: z.number().positive().optional(),
|
|
918
|
+
rollingUsd: z.number().positive().optional()
|
|
919
|
+
});
|
|
920
|
+
var aiConfigSchema = z.object({
|
|
921
|
+
defaults: z.record(tierSchema, tierSpecSchema).optional(),
|
|
922
|
+
// Functions can't be deeply validated — z.custom asserts the TS type and
|
|
923
|
+
// passes the value through untouched.
|
|
924
|
+
providers: z.record(z.string(), z.custom()).optional(),
|
|
925
|
+
costSink: z.custom().optional(),
|
|
926
|
+
budget: budgetSchema.optional()
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// src/client.ts
|
|
930
|
+
var DEFAULT_IMAGE_SPEC = {
|
|
931
|
+
provider: "fal",
|
|
932
|
+
model: "fal-ai/flux/schnell",
|
|
933
|
+
transport: "http"
|
|
934
|
+
};
|
|
935
|
+
function createAI(config = {}) {
|
|
936
|
+
const cfg = aiConfigSchema.parse(config);
|
|
937
|
+
const providers = cfg.providers ?? defaultProviders;
|
|
938
|
+
const budget = cfg.budget ? new BudgetGuard(cfg.budget) : void 0;
|
|
939
|
+
const estTokens = (s) => Math.ceil(s.length / 4);
|
|
940
|
+
function preflight(spec, estInTokens, estOutTokens) {
|
|
941
|
+
if (!budget) return;
|
|
942
|
+
budget.check(computeCost(spec.provider, spec.model, estInTokens, estOutTokens));
|
|
943
|
+
}
|
|
944
|
+
function settle(usage) {
|
|
945
|
+
if (budget) budget.record(usage.costUsd);
|
|
946
|
+
}
|
|
947
|
+
function pickProvider(name) {
|
|
948
|
+
const adapter = providers[name];
|
|
949
|
+
if (!adapter) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`createAI: no provider adapter registered for "${name}". Registered: ${Object.keys(providers).join(", ") || "(none)"}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return adapter;
|
|
955
|
+
}
|
|
956
|
+
function enrich(usage, capability, tier, purpose, latencyMs) {
|
|
957
|
+
usage.capability = capability;
|
|
958
|
+
if (tier) usage.tier = tier;
|
|
959
|
+
if (purpose) usage.purpose = purpose;
|
|
960
|
+
usage.latencyMs = Math.round(latencyMs);
|
|
961
|
+
if (!usage.ts) usage.ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
962
|
+
return usage;
|
|
963
|
+
}
|
|
964
|
+
async function report(usage) {
|
|
965
|
+
if (!cfg.costSink) return;
|
|
966
|
+
try {
|
|
967
|
+
await cfg.costSink.record(usage);
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function toMessages(input) {
|
|
972
|
+
if (input.messages && input.messages.length > 0) return input.messages;
|
|
973
|
+
const msgs = [];
|
|
974
|
+
if (input.system) msgs.push({ role: "system", content: input.system });
|
|
975
|
+
msgs.push({ role: "user", content: input.prompt ?? "" });
|
|
976
|
+
return msgs;
|
|
977
|
+
}
|
|
978
|
+
const client = {
|
|
979
|
+
async chat(input) {
|
|
980
|
+
input = chatInputSchema.parse(input);
|
|
981
|
+
const spec = resolveTier(input.tier ?? "smart", input.override, cfg.defaults);
|
|
982
|
+
const adapter = pickProvider(spec.provider);
|
|
983
|
+
if (!adapter.chat) {
|
|
984
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support chat`);
|
|
985
|
+
}
|
|
986
|
+
const messages = toMessages(input);
|
|
987
|
+
const estIn = messages.reduce(
|
|
988
|
+
(n, m) => n + estTokens(typeof m.content === "string" ? m.content : JSON.stringify(m.content)),
|
|
989
|
+
0
|
|
990
|
+
);
|
|
991
|
+
preflight(spec, estIn, input.maxTokens ?? 512);
|
|
992
|
+
const t0 = performance.now();
|
|
993
|
+
const res = await adapter.chat({
|
|
994
|
+
messages,
|
|
995
|
+
spec,
|
|
996
|
+
tools: input.tools,
|
|
997
|
+
maxTokens: input.maxTokens,
|
|
998
|
+
temperature: input.temperature
|
|
999
|
+
});
|
|
1000
|
+
enrich(res.usage, "chat", input.tier ?? "smart", input.purpose, performance.now() - t0);
|
|
1001
|
+
settle(res.usage);
|
|
1002
|
+
await report(res.usage);
|
|
1003
|
+
return res;
|
|
1004
|
+
},
|
|
1005
|
+
async vision(input) {
|
|
1006
|
+
input = visionInputSchema.parse(input);
|
|
1007
|
+
const spec = resolveTier(input.tier ?? VISION_DEFAULT_TIER, input.override, cfg.defaults);
|
|
1008
|
+
const adapter = pickProvider(spec.provider);
|
|
1009
|
+
if (!adapter.vision) {
|
|
1010
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support vision`);
|
|
1011
|
+
}
|
|
1012
|
+
const messages = buildVisionMessages(input);
|
|
1013
|
+
preflight(spec, estTokens(input.prompt) + 1e3, 512);
|
|
1014
|
+
const t0 = performance.now();
|
|
1015
|
+
const res = await adapter.vision({ messages, spec });
|
|
1016
|
+
enrich(res.usage, "vision", input.tier ?? VISION_DEFAULT_TIER, input.purpose, performance.now() - t0);
|
|
1017
|
+
settle(res.usage);
|
|
1018
|
+
await report(res.usage);
|
|
1019
|
+
return res;
|
|
1020
|
+
},
|
|
1021
|
+
async translate(input) {
|
|
1022
|
+
input = translateInputSchema.parse(input);
|
|
1023
|
+
const spec = resolveTier(input.tier ?? TRANSLATE_DEFAULT_TIER, input.override, cfg.defaults);
|
|
1024
|
+
const adapter = pickProvider(spec.provider);
|
|
1025
|
+
if (!adapter.chat) {
|
|
1026
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support chat (translate routes through chat)`);
|
|
1027
|
+
}
|
|
1028
|
+
const messages = buildTranslateMessages(input);
|
|
1029
|
+
const estIn = estTokens(input.text) + 40;
|
|
1030
|
+
preflight(spec, estIn, estIn);
|
|
1031
|
+
const t0 = performance.now();
|
|
1032
|
+
const res = await adapter.chat({ messages, spec });
|
|
1033
|
+
enrich(res.usage, "translate", input.tier ?? TRANSLATE_DEFAULT_TIER, input.purpose, performance.now() - t0);
|
|
1034
|
+
settle(res.usage);
|
|
1035
|
+
await report(res.usage);
|
|
1036
|
+
return { text: res.text, usage: res.usage };
|
|
1037
|
+
},
|
|
1038
|
+
async image(input) {
|
|
1039
|
+
input = imageInputSchema.parse(input);
|
|
1040
|
+
const spec = { ...DEFAULT_IMAGE_SPEC, ...input.override };
|
|
1041
|
+
const adapter = pickProvider(spec.provider);
|
|
1042
|
+
if (!adapter.image) {
|
|
1043
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support image`);
|
|
1044
|
+
}
|
|
1045
|
+
preflight(spec, 0, 0);
|
|
1046
|
+
const t0 = performance.now();
|
|
1047
|
+
const res = await adapter.image({
|
|
1048
|
+
prompt: input.prompt,
|
|
1049
|
+
spec,
|
|
1050
|
+
width: input.width,
|
|
1051
|
+
height: input.height
|
|
1052
|
+
});
|
|
1053
|
+
enrich(res.usage, "image", void 0, input.purpose, performance.now() - t0);
|
|
1054
|
+
settle(res.usage);
|
|
1055
|
+
await report(res.usage);
|
|
1056
|
+
return res;
|
|
1057
|
+
},
|
|
1058
|
+
async embedding(input) {
|
|
1059
|
+
input = embeddingInputSchema.parse(input);
|
|
1060
|
+
const spec = resolveTier(input.tier ?? EMBEDDING_DEFAULT_TIER, input.override, cfg.defaults);
|
|
1061
|
+
const adapter = pickProvider(spec.provider);
|
|
1062
|
+
if (!adapter.embedding) {
|
|
1063
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support embedding`);
|
|
1064
|
+
}
|
|
1065
|
+
const text = Array.isArray(input.text) ? input.text : [input.text];
|
|
1066
|
+
preflight(spec, text.reduce((n, t) => n + estTokens(t), 0), 0);
|
|
1067
|
+
const t0 = performance.now();
|
|
1068
|
+
const res = await adapter.embedding({ input: text, spec });
|
|
1069
|
+
enrich(res.usage, "embedding", input.tier ?? EMBEDDING_DEFAULT_TIER, input.purpose, performance.now() - t0);
|
|
1070
|
+
settle(res.usage);
|
|
1071
|
+
await report(res.usage);
|
|
1072
|
+
return res;
|
|
1073
|
+
},
|
|
1074
|
+
async transcribe(input) {
|
|
1075
|
+
input = transcribeInputSchema.parse(input);
|
|
1076
|
+
const spec = { ...DEFAULT_TRANSCRIBE_SPEC, ...input.override };
|
|
1077
|
+
const adapter = pickProvider(spec.provider);
|
|
1078
|
+
if (!adapter.transcribe) {
|
|
1079
|
+
throw new Error(`createAI: provider "${spec.provider}" does not support transcribe`);
|
|
1080
|
+
}
|
|
1081
|
+
const audio = await resolveAudio(input.audio);
|
|
1082
|
+
preflight(spec, 0, 0);
|
|
1083
|
+
const t0 = performance.now();
|
|
1084
|
+
const res = await adapter.transcribe({ audio, language: input.language, spec });
|
|
1085
|
+
enrich(res.usage, "transcribe", void 0, input.purpose, performance.now() - t0);
|
|
1086
|
+
settle(res.usage);
|
|
1087
|
+
await report(res.usage);
|
|
1088
|
+
return res;
|
|
1089
|
+
},
|
|
1090
|
+
// Replaced below with the real prompt-contracts (needs the client itself).
|
|
1091
|
+
contracts: void 0
|
|
1092
|
+
};
|
|
1093
|
+
client.contracts = makeContracts(client);
|
|
1094
|
+
return client;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/providers/stub.ts
|
|
1098
|
+
function stubUsage(provider, model, transport, capability) {
|
|
1099
|
+
return {
|
|
1100
|
+
provider,
|
|
1101
|
+
model,
|
|
1102
|
+
transport,
|
|
1103
|
+
inputTokens: 0,
|
|
1104
|
+
outputTokens: 0,
|
|
1105
|
+
cacheReadTokens: 0,
|
|
1106
|
+
cacheCreationTokens: 0,
|
|
1107
|
+
costUsd: 0,
|
|
1108
|
+
latencyMs: 0,
|
|
1109
|
+
capability,
|
|
1110
|
+
// ts is supplied by the caller-side at real call time; stub uses a fixed marker
|
|
1111
|
+
// ('' avoids Date.now() — keeps the stub pure/deterministic for tests).
|
|
1112
|
+
ts: ""
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function lastUserText(req) {
|
|
1116
|
+
for (let i = req.messages.length - 1; i >= 0; i--) {
|
|
1117
|
+
const m = req.messages[i];
|
|
1118
|
+
if (m && m.role === "user") {
|
|
1119
|
+
return typeof m.content === "string" ? m.content : m.content.map((p) => p.type === "text" ? p.text : "[image]").join(" ");
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return "";
|
|
1123
|
+
}
|
|
1124
|
+
var anthropicApiAdapter = {
|
|
1125
|
+
name: "anthropic",
|
|
1126
|
+
async chat(req) {
|
|
1127
|
+
return {
|
|
1128
|
+
text: `[stub:anthropic-api] ${lastUserText(req)}`,
|
|
1129
|
+
usage: stubUsage("anthropic", req.spec.model, "http", "chat")
|
|
1130
|
+
};
|
|
1131
|
+
},
|
|
1132
|
+
async vision(req) {
|
|
1133
|
+
return {
|
|
1134
|
+
text: `[stub:anthropic-api:vision] ${lastUserText(req)}`,
|
|
1135
|
+
usage: stubUsage("anthropic", req.spec.model, "http", "vision")
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
var anthropicSubprocessAdapter = {
|
|
1140
|
+
name: "anthropic",
|
|
1141
|
+
async chat(req) {
|
|
1142
|
+
const usage = stubUsage("anthropic", req.spec.model, "subprocess", "chat");
|
|
1143
|
+
usage.subprocess = true;
|
|
1144
|
+
return { text: `[stub:anthropic-subprocess] ${lastUserText(req)}`, usage };
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
var openaiStubAdapter = {
|
|
1148
|
+
name: "openai",
|
|
1149
|
+
async chat(req) {
|
|
1150
|
+
return {
|
|
1151
|
+
text: `[stub:openai] ${lastUserText(req)}`,
|
|
1152
|
+
usage: stubUsage("openai", req.spec.model, "http", "chat")
|
|
1153
|
+
};
|
|
1154
|
+
},
|
|
1155
|
+
async embedding(req) {
|
|
1156
|
+
return {
|
|
1157
|
+
vectors: req.input.map(() => [0, 0, 0]),
|
|
1158
|
+
usage: stubUsage("openai", req.spec.model, "http", "embedding")
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
var falStubAdapter = {
|
|
1163
|
+
name: "fal",
|
|
1164
|
+
async image(req) {
|
|
1165
|
+
return {
|
|
1166
|
+
url: `https://stub.fal/${encodeURIComponent(req.prompt).slice(0, 32)}.png`,
|
|
1167
|
+
usage: stubUsage("fal", req.spec.model, "http", "image")
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
var stubProviders = {
|
|
1172
|
+
anthropic: anthropicApiAdapter,
|
|
1173
|
+
openai: openaiStubAdapter,
|
|
1174
|
+
fal: falStubAdapter
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// src/version.ts
|
|
1178
|
+
var VERSION = "0.1.0";
|
|
1179
|
+
var SDK_TAG = "@broberg/ai-sdk@0.1.0";
|
|
1180
|
+
|
|
1181
|
+
// src/cost/sinks/noop.ts
|
|
1182
|
+
var noopSink = {
|
|
1183
|
+
record() {
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// src/cost/sinks/multi.ts
|
|
1188
|
+
function multiSink(sinks) {
|
|
1189
|
+
return {
|
|
1190
|
+
async record(usage) {
|
|
1191
|
+
await Promise.allSettled(sinks.map(async (s) => s.record(usage)));
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/cost/sinks/upmetrics.ts
|
|
1197
|
+
function upmetricsSink(config) {
|
|
1198
|
+
const doFetch = config.fetch ?? fetch;
|
|
1199
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/agent`;
|
|
1200
|
+
return {
|
|
1201
|
+
async record(usage) {
|
|
1202
|
+
try {
|
|
1203
|
+
const startedAt = usage.ts || (/* @__PURE__ */ new Date()).toISOString();
|
|
1204
|
+
const endedAt = new Date(
|
|
1205
|
+
new Date(startedAt).getTime() + (usage.latencyMs || 0)
|
|
1206
|
+
).toISOString();
|
|
1207
|
+
const agentKind = config.agentKind ?? (usage.capability === "embedding" ? "embedding" : "chatbot");
|
|
1208
|
+
const body = {
|
|
1209
|
+
mode: "record",
|
|
1210
|
+
agent_kind: agentKind,
|
|
1211
|
+
agent_name: config.agentName,
|
|
1212
|
+
provider: usage.provider,
|
|
1213
|
+
model: usage.model,
|
|
1214
|
+
status: "success",
|
|
1215
|
+
input_tokens: usage.inputTokens,
|
|
1216
|
+
output_tokens: usage.outputTokens,
|
|
1217
|
+
cache_read_tokens: usage.cacheReadTokens,
|
|
1218
|
+
cache_creation_tokens: usage.cacheCreationTokens,
|
|
1219
|
+
cost_usd: usage.costUsd,
|
|
1220
|
+
duration_ms: usage.latencyMs,
|
|
1221
|
+
started_at: startedAt,
|
|
1222
|
+
ended_at: endedAt,
|
|
1223
|
+
tags: {
|
|
1224
|
+
capability: usage.capability,
|
|
1225
|
+
transport: usage.transport,
|
|
1226
|
+
sdk: SDK_TAG
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
if (usage.tier !== void 0) body.tier = usage.tier;
|
|
1230
|
+
if (usage.purpose !== void 0) body.purpose = usage.purpose;
|
|
1231
|
+
if (usage.toolCalls) {
|
|
1232
|
+
body.tool_calls = usage.toolCalls.map((t) => ({
|
|
1233
|
+
name: t.name,
|
|
1234
|
+
count: t.count,
|
|
1235
|
+
error_count: t.errorCount ?? 0
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
void config.complianceMode;
|
|
1239
|
+
const res = await doFetch(url, {
|
|
1240
|
+
method: "POST",
|
|
1241
|
+
headers: {
|
|
1242
|
+
"content-type": "application/json",
|
|
1243
|
+
"X-Upmetrics-Key": config.apiKey
|
|
1244
|
+
},
|
|
1245
|
+
body: JSON.stringify(body)
|
|
1246
|
+
});
|
|
1247
|
+
if (!res.ok) {
|
|
1248
|
+
const text = await res.text().catch(() => "");
|
|
1249
|
+
config.onError?.(
|
|
1250
|
+
new Error(`upmetricsSink: ingest returned ${res.status}: ${text.slice(0, 200)}`)
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
config.onError?.(err);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/cost/sinks/discord.ts
|
|
1261
|
+
function discordSink(config) {
|
|
1262
|
+
const doFetch = config.fetch ?? fetch;
|
|
1263
|
+
const minUsd = config.minUsd ?? 0;
|
|
1264
|
+
return {
|
|
1265
|
+
async record(usage) {
|
|
1266
|
+
try {
|
|
1267
|
+
if (!usage.subprocess && usage.costUsd < minUsd) return;
|
|
1268
|
+
const costLabel = usage.subprocess ? "Max plan (free)" : `$${usage.costUsd.toFixed(6)}`;
|
|
1269
|
+
const embed = {
|
|
1270
|
+
title: `AI call \u2014 ${usage.capability}`,
|
|
1271
|
+
fields: [
|
|
1272
|
+
{ name: "Provider", value: usage.provider, inline: true },
|
|
1273
|
+
{ name: "Model", value: usage.model, inline: true },
|
|
1274
|
+
{ name: "Transport", value: usage.transport, inline: true },
|
|
1275
|
+
{ name: "Cost", value: costLabel, inline: true },
|
|
1276
|
+
{
|
|
1277
|
+
name: "Tokens",
|
|
1278
|
+
value: `${usage.inputTokens} in / ${usage.outputTokens} out`,
|
|
1279
|
+
inline: true
|
|
1280
|
+
},
|
|
1281
|
+
{ name: "Latency", value: `${usage.latencyMs} ms`, inline: true }
|
|
1282
|
+
],
|
|
1283
|
+
timestamp: usage.ts || (/* @__PURE__ */ new Date()).toISOString()
|
|
1284
|
+
};
|
|
1285
|
+
const res = await doFetch(config.webhookUrl, {
|
|
1286
|
+
method: "POST",
|
|
1287
|
+
headers: { "content-type": "application/json" },
|
|
1288
|
+
body: JSON.stringify({ embeds: [embed] })
|
|
1289
|
+
});
|
|
1290
|
+
if (!res.ok) {
|
|
1291
|
+
config.onError?.(new Error(`discordSink: webhook returned ${res.status}`));
|
|
1292
|
+
}
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
config.onError?.(err);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/cost/sinks/sqlite.ts
|
|
1301
|
+
import { Database } from "bun:sqlite";
|
|
1302
|
+
var CREATE_TABLE = `
|
|
1303
|
+
CREATE TABLE IF NOT EXISTS ai_usage (
|
|
1304
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1305
|
+
ts TEXT NOT NULL,
|
|
1306
|
+
provider TEXT NOT NULL,
|
|
1307
|
+
model TEXT NOT NULL,
|
|
1308
|
+
tier TEXT,
|
|
1309
|
+
transport TEXT NOT NULL,
|
|
1310
|
+
capability TEXT NOT NULL,
|
|
1311
|
+
purpose TEXT,
|
|
1312
|
+
input_tokens INTEGER NOT NULL,
|
|
1313
|
+
output_tokens INTEGER NOT NULL,
|
|
1314
|
+
cache_read_tokens INTEGER NOT NULL,
|
|
1315
|
+
cache_creation_tokens INTEGER NOT NULL,
|
|
1316
|
+
cost_usd REAL NOT NULL,
|
|
1317
|
+
latency_ms INTEGER NOT NULL,
|
|
1318
|
+
subprocess INTEGER NOT NULL DEFAULT 0
|
|
1319
|
+
)`;
|
|
1320
|
+
function sqliteSink(config) {
|
|
1321
|
+
const db = new Database(config.dbPath);
|
|
1322
|
+
db.run(CREATE_TABLE);
|
|
1323
|
+
const insert = db.prepare(
|
|
1324
|
+
`INSERT INTO ai_usage
|
|
1325
|
+
(ts, provider, model, tier, transport, capability, purpose,
|
|
1326
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
|
1327
|
+
cost_usd, latency_ms, subprocess)
|
|
1328
|
+
VALUES ($ts, $provider, $model, $tier, $transport, $capability, $purpose,
|
|
1329
|
+
$input, $output, $cacheRead, $cacheCreation, $cost, $latency, $subprocess)`
|
|
1330
|
+
);
|
|
1331
|
+
return {
|
|
1332
|
+
record(usage) {
|
|
1333
|
+
insert.run({
|
|
1334
|
+
$ts: usage.ts || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1335
|
+
$provider: usage.provider,
|
|
1336
|
+
$model: usage.model,
|
|
1337
|
+
$tier: usage.tier ?? null,
|
|
1338
|
+
$transport: usage.transport,
|
|
1339
|
+
$capability: usage.capability,
|
|
1340
|
+
$purpose: usage.purpose ?? null,
|
|
1341
|
+
$input: usage.inputTokens,
|
|
1342
|
+
$output: usage.outputTokens,
|
|
1343
|
+
$cacheRead: usage.cacheReadTokens,
|
|
1344
|
+
$cacheCreation: usage.cacheCreationTokens,
|
|
1345
|
+
$cost: usage.costUsd,
|
|
1346
|
+
$latency: usage.latencyMs,
|
|
1347
|
+
$subprocess: usage.subprocess ? 1 : 0
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function getCostSummary(dbPath) {
|
|
1353
|
+
const db = new Database(dbPath, { readonly: true });
|
|
1354
|
+
const total = db.query(`SELECT SUM(cost_usd) AS total FROM ai_usage`).get();
|
|
1355
|
+
const byProvider = {};
|
|
1356
|
+
for (const row of db.query(
|
|
1357
|
+
`SELECT provider, SUM(cost_usd) AS sum FROM ai_usage GROUP BY provider`
|
|
1358
|
+
).all()) {
|
|
1359
|
+
byProvider[row.provider] = row.sum;
|
|
1360
|
+
}
|
|
1361
|
+
const byCapability = {};
|
|
1362
|
+
for (const row of db.query(
|
|
1363
|
+
`SELECT capability, SUM(cost_usd) AS sum FROM ai_usage GROUP BY capability`
|
|
1364
|
+
).all()) {
|
|
1365
|
+
byCapability[row.capability] = row.sum;
|
|
1366
|
+
}
|
|
1367
|
+
return { totalUsd: total?.total ?? 0, byProvider, byCapability };
|
|
1368
|
+
}
|
|
1369
|
+
export {
|
|
1370
|
+
BudgetExceededError,
|
|
1371
|
+
BudgetGuard,
|
|
1372
|
+
DEFAULT_TIER_MAP,
|
|
1373
|
+
SDK_TAG,
|
|
1374
|
+
VERSION,
|
|
1375
|
+
aiConfigSchema,
|
|
1376
|
+
anthropicAdapter,
|
|
1377
|
+
anthropicApiAdapter,
|
|
1378
|
+
anthropicSubprocessAdapter,
|
|
1379
|
+
chatInputSchema,
|
|
1380
|
+
computeCost,
|
|
1381
|
+
createAI,
|
|
1382
|
+
deepinfraAdapter,
|
|
1383
|
+
defaultProviders,
|
|
1384
|
+
discordSink,
|
|
1385
|
+
embeddingInputSchema,
|
|
1386
|
+
falAdapter,
|
|
1387
|
+
falStubAdapter,
|
|
1388
|
+
freshUsage,
|
|
1389
|
+
fromProviderToolCall,
|
|
1390
|
+
geminiAdapter,
|
|
1391
|
+
getCostSummary,
|
|
1392
|
+
getPrice,
|
|
1393
|
+
httpTransport,
|
|
1394
|
+
imageInputSchema,
|
|
1395
|
+
makeContracts,
|
|
1396
|
+
makeOpenAICompatibleAdapter,
|
|
1397
|
+
messageSchema,
|
|
1398
|
+
multiSink,
|
|
1399
|
+
noopSink,
|
|
1400
|
+
openaiAdapter,
|
|
1401
|
+
openaiStubAdapter,
|
|
1402
|
+
openrouterAdapter,
|
|
1403
|
+
parseClaudeCliJson,
|
|
1404
|
+
parseJsonLoose,
|
|
1405
|
+
resolveTier,
|
|
1406
|
+
sqliteSink,
|
|
1407
|
+
stubProviders,
|
|
1408
|
+
subprocessTransport,
|
|
1409
|
+
tierSpecSchema,
|
|
1410
|
+
toProviderTools,
|
|
1411
|
+
toolSchema,
|
|
1412
|
+
translateInputSchema,
|
|
1413
|
+
upmetricsSink,
|
|
1414
|
+
visionInputSchema
|
|
1415
|
+
};
|
|
1416
|
+
//# sourceMappingURL=index.js.map
|