@spendgraph/llms 0.2.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 +354 -0
- package/dist/adapters/anthropic.d.ts +2 -0
- package/dist/adapters/anthropic.js +101 -0
- package/dist/adapters/bedrock.d.ts +2 -0
- package/dist/adapters/bedrock.js +88 -0
- package/dist/adapters/gemini.d.ts +2 -0
- package/dist/adapters/gemini.js +87 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +36 -0
- package/dist/adapters/openai.d.ts +2 -0
- package/dist/adapters/openai.js +71 -0
- package/dist/adapters/perplexity.d.ts +2 -0
- package/dist/adapters/perplexity.js +92 -0
- package/dist/adapters/responses.d.ts +2 -0
- package/dist/adapters/responses.js +100 -0
- package/dist/affordable.d.ts +44 -0
- package/dist/affordable.js +87 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/llm/drivers.d.ts +36 -0
- package/dist/llm/drivers.js +121 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.js +1 -0
- package/dist/llm/invoke.d.ts +3 -0
- package/dist/llm/invoke.js +24 -0
- package/dist/llm/llm.d.ts +50 -0
- package/dist/llm/llm.js +238 -0
- package/dist/llm/schema/index.d.ts +1 -0
- package/dist/llm/schema/index.js +1 -0
- package/dist/llm/schema/schema.d.ts +45 -0
- package/dist/llm/schema/schema.js +80 -0
- package/dist/llm/types.d.ts +62 -0
- package/dist/llm/types.js +1 -0
- package/dist/model.d.ts +7 -0
- package/dist/model.js +22 -0
- package/dist/read.d.ts +10 -0
- package/dist/read.js +18 -0
- package/dist/report/index.d.ts +2 -0
- package/dist/report/index.js +1 -0
- package/dist/report/ingest.d.ts +15 -0
- package/dist/report/ingest.js +29 -0
- package/dist/report/types.d.ts +30 -0
- package/dist/report/types.js +1 -0
- package/dist/request.d.ts +26 -0
- package/dist/request.js +32 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +71 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +22 -0
- package/dist/usage.js +76 -0
- package/package.json +52 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { pricingId } from "../model.js";
|
|
2
|
+
import { count, parseArgs, reply, setUsage } from "../usage.js";
|
|
3
|
+
function usageOf(raw) {
|
|
4
|
+
const details = raw?.prompt_tokens_details;
|
|
5
|
+
return {
|
|
6
|
+
inputTokens: count(raw?.prompt_tokens),
|
|
7
|
+
outputTokens: count(raw?.completion_tokens),
|
|
8
|
+
cacheReadTokens: count(details?.cached_tokens),
|
|
9
|
+
cacheWriteTokens: count(details?.cache_creation_tokens),
|
|
10
|
+
citationTokens: 0,
|
|
11
|
+
reasoningTokens: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function toolCallsOf(calls) {
|
|
15
|
+
return (calls ?? []).map((c) => ({
|
|
16
|
+
id: c.id ?? "",
|
|
17
|
+
name: c.function?.name ?? "",
|
|
18
|
+
args: parseArgs(c.function?.arguments),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export const openai = {
|
|
22
|
+
provider: "openai",
|
|
23
|
+
detect(value) {
|
|
24
|
+
const raw = value;
|
|
25
|
+
return Boolean(raw &&
|
|
26
|
+
Array.isArray(raw.choices) &&
|
|
27
|
+
raw.choices.every((c) => "message" in c || "delta" in c || "finish_reason" in c));
|
|
28
|
+
},
|
|
29
|
+
read(value, opts = {}) {
|
|
30
|
+
const raw = value;
|
|
31
|
+
const choice = raw.choices?.[0];
|
|
32
|
+
const refused = choice?.finish_reason === "content_filter";
|
|
33
|
+
return reply({
|
|
34
|
+
output: choice?.message?.content ?? "",
|
|
35
|
+
model: opts.model ?? pricingId(opts.provider ?? "openai", raw.model ?? ""),
|
|
36
|
+
usage: usageOf(raw.usage),
|
|
37
|
+
toolCalls: toolCallsOf(choice?.message?.tool_calls),
|
|
38
|
+
stopReason: choice?.finish_reason ?? undefined,
|
|
39
|
+
status: refused ? "failed" : "completed",
|
|
40
|
+
error: refused ? "declined by a content filter" : undefined,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
chunk(event, state) {
|
|
44
|
+
const raw = event;
|
|
45
|
+
if (raw.model)
|
|
46
|
+
state.model = raw.model;
|
|
47
|
+
if (raw.usage)
|
|
48
|
+
setUsage(state.usage, usageOf(raw.usage));
|
|
49
|
+
const choice = raw.choices?.[0];
|
|
50
|
+
if (!choice)
|
|
51
|
+
return "";
|
|
52
|
+
if (choice.finish_reason)
|
|
53
|
+
state.stopReason = choice.finish_reason;
|
|
54
|
+
for (const call of choice.delta?.tool_calls ?? []) {
|
|
55
|
+
const key = String(call.index ?? call.id ?? state.tools.size);
|
|
56
|
+
const pending = state.tools.get(key) ?? { id: "", name: "", json: "" };
|
|
57
|
+
if (call.id)
|
|
58
|
+
pending.id = call.id;
|
|
59
|
+
if (call.function?.name)
|
|
60
|
+
pending.name = call.function.name;
|
|
61
|
+
if (call.function?.arguments)
|
|
62
|
+
pending.json += call.function.arguments;
|
|
63
|
+
state.tools.set(key, pending);
|
|
64
|
+
}
|
|
65
|
+
const text = choice.delta?.content;
|
|
66
|
+
if (typeof text !== "string" || !text)
|
|
67
|
+
return "";
|
|
68
|
+
state.text += text;
|
|
69
|
+
return text;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { pricingId } from "../model.js";
|
|
2
|
+
import { count, parseArgs, reply, setUsage } from "../usage.js";
|
|
3
|
+
function usageOf(raw) {
|
|
4
|
+
return {
|
|
5
|
+
inputTokens: count(raw?.prompt_tokens),
|
|
6
|
+
outputTokens: count(raw?.completion_tokens),
|
|
7
|
+
cacheReadTokens: 0,
|
|
8
|
+
cacheWriteTokens: 0,
|
|
9
|
+
citationTokens: count(raw?.citation_tokens),
|
|
10
|
+
reasoningTokens: count(raw?.reasoning_tokens),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function citationsOf(raw) {
|
|
14
|
+
const urls = [];
|
|
15
|
+
for (const result of raw.search_results ?? []) {
|
|
16
|
+
if (typeof result.url === "string" && result.url)
|
|
17
|
+
urls.push(result.url);
|
|
18
|
+
}
|
|
19
|
+
if (urls.length)
|
|
20
|
+
return urls;
|
|
21
|
+
for (const url of raw.citations ?? []) {
|
|
22
|
+
if (typeof url === "string" && url)
|
|
23
|
+
urls.push(url);
|
|
24
|
+
}
|
|
25
|
+
return urls;
|
|
26
|
+
}
|
|
27
|
+
function toolCallsOf(calls) {
|
|
28
|
+
const parsed = [];
|
|
29
|
+
for (const call of calls ?? []) {
|
|
30
|
+
parsed.push({
|
|
31
|
+
id: call.id ?? "",
|
|
32
|
+
name: call.function?.name ?? "",
|
|
33
|
+
args: parseArgs(call.function?.arguments),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
export const perplexity = {
|
|
39
|
+
provider: "perplexity",
|
|
40
|
+
detect(value) {
|
|
41
|
+
const raw = value;
|
|
42
|
+
if (!raw || !Array.isArray(raw.choices))
|
|
43
|
+
return false;
|
|
44
|
+
return Array.isArray(raw.search_results) || Array.isArray(raw.citations);
|
|
45
|
+
},
|
|
46
|
+
read(value, opts = {}) {
|
|
47
|
+
const raw = value;
|
|
48
|
+
const choice = raw.choices?.[0];
|
|
49
|
+
const refused = choice?.finish_reason === "content_filter";
|
|
50
|
+
return reply({
|
|
51
|
+
output: choice?.message?.content ?? "",
|
|
52
|
+
model: opts.model ?? pricingId(opts.provider ?? "perplexity", raw.model ?? ""),
|
|
53
|
+
usage: usageOf(raw.usage),
|
|
54
|
+
toolCalls: toolCallsOf(choice?.message?.tool_calls),
|
|
55
|
+
citations: citationsOf(raw),
|
|
56
|
+
stopReason: choice?.finish_reason ?? undefined,
|
|
57
|
+
status: refused ? "failed" : "completed",
|
|
58
|
+
error: refused ? "declined by a content filter" : undefined,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
chunk(event, state) {
|
|
62
|
+
const raw = event;
|
|
63
|
+
if (raw.model)
|
|
64
|
+
state.model = raw.model;
|
|
65
|
+
if (raw.usage)
|
|
66
|
+
setUsage(state.usage, usageOf(raw.usage));
|
|
67
|
+
const found = citationsOf(raw);
|
|
68
|
+
if (found.length)
|
|
69
|
+
state.citations = found;
|
|
70
|
+
const choice = raw.choices?.[0];
|
|
71
|
+
if (!choice)
|
|
72
|
+
return "";
|
|
73
|
+
if (choice.finish_reason)
|
|
74
|
+
state.stopReason = choice.finish_reason;
|
|
75
|
+
for (const call of choice.delta?.tool_calls ?? []) {
|
|
76
|
+
const key = String(call.index ?? call.id ?? state.tools.size);
|
|
77
|
+
const pending = state.tools.get(key) ?? { id: "", name: "", json: "" };
|
|
78
|
+
if (call.id)
|
|
79
|
+
pending.id = call.id;
|
|
80
|
+
if (call.function?.name)
|
|
81
|
+
pending.name = call.function.name;
|
|
82
|
+
if (call.function?.arguments)
|
|
83
|
+
pending.json += call.function.arguments;
|
|
84
|
+
state.tools.set(key, pending);
|
|
85
|
+
}
|
|
86
|
+
const text = choice.delta?.content;
|
|
87
|
+
if (typeof text !== "string" || !text)
|
|
88
|
+
return "";
|
|
89
|
+
state.text += text;
|
|
90
|
+
return text;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { pricingId } from "../model.js";
|
|
2
|
+
import { count, parseArgs, reply, setUsage } from "../usage.js";
|
|
3
|
+
function usageOf(raw) {
|
|
4
|
+
const input = raw?.input_tokens_details;
|
|
5
|
+
return {
|
|
6
|
+
inputTokens: count(raw?.input_tokens),
|
|
7
|
+
outputTokens: count(raw?.output_tokens),
|
|
8
|
+
cacheReadTokens: count(input?.cached_tokens),
|
|
9
|
+
cacheWriteTokens: 0,
|
|
10
|
+
citationTokens: 0,
|
|
11
|
+
reasoningTokens: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function textOf(raw) {
|
|
15
|
+
if (typeof raw.output_text === "string")
|
|
16
|
+
return raw.output_text;
|
|
17
|
+
let text = "";
|
|
18
|
+
for (const item of raw.output ?? []) {
|
|
19
|
+
if (item.type !== "message")
|
|
20
|
+
continue;
|
|
21
|
+
for (const part of item.content ?? []) {
|
|
22
|
+
if (part.type === "output_text" && typeof part.text === "string")
|
|
23
|
+
text += part.text;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
function toolCallsOf(raw) {
|
|
29
|
+
const calls = [];
|
|
30
|
+
for (const item of raw.output ?? []) {
|
|
31
|
+
if (item.type !== "function_call")
|
|
32
|
+
continue;
|
|
33
|
+
calls.push({
|
|
34
|
+
id: item.call_id ?? item.id ?? "",
|
|
35
|
+
name: item.name ?? "",
|
|
36
|
+
args: parseArgs(item.arguments),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return calls;
|
|
40
|
+
}
|
|
41
|
+
export const responses = {
|
|
42
|
+
provider: "openai",
|
|
43
|
+
detect(value) {
|
|
44
|
+
const raw = value;
|
|
45
|
+
if (!raw)
|
|
46
|
+
return false;
|
|
47
|
+
if (raw.object === "response")
|
|
48
|
+
return true;
|
|
49
|
+
return Array.isArray(raw.output) && typeof raw.status === "string";
|
|
50
|
+
},
|
|
51
|
+
read(value, opts = {}) {
|
|
52
|
+
const raw = value;
|
|
53
|
+
const failed = raw.status === "failed" || raw.status === "incomplete";
|
|
54
|
+
return reply({
|
|
55
|
+
output: textOf(raw),
|
|
56
|
+
model: opts.model ?? pricingId(opts.provider ?? "openai", raw.model ?? ""),
|
|
57
|
+
usage: usageOf(raw.usage),
|
|
58
|
+
toolCalls: toolCallsOf(raw),
|
|
59
|
+
stopReason: raw.status,
|
|
60
|
+
status: failed ? "failed" : "completed",
|
|
61
|
+
error: failed ? (raw.incomplete_details?.reason ?? raw.status) : undefined,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
chunk(event, state) {
|
|
65
|
+
const e = event;
|
|
66
|
+
switch (e.type) {
|
|
67
|
+
case "response.created":
|
|
68
|
+
case "response.in_progress":
|
|
69
|
+
state.model = e.response?.model ?? state.model;
|
|
70
|
+
return "";
|
|
71
|
+
case "response.output_text.delta": {
|
|
72
|
+
const text = typeof e.delta === "string" ? e.delta : "";
|
|
73
|
+
state.text += text;
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
case "response.output_item.done": {
|
|
77
|
+
const item = e.item;
|
|
78
|
+
if (item?.type !== "function_call")
|
|
79
|
+
return "";
|
|
80
|
+
state.tools.set(item.call_id ?? item.id ?? String(state.tools.size), {
|
|
81
|
+
id: item.call_id ?? item.id ?? "",
|
|
82
|
+
name: item.name ?? "",
|
|
83
|
+
json: item.arguments ?? "",
|
|
84
|
+
});
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
case "response.completed":
|
|
88
|
+
case "response.incomplete":
|
|
89
|
+
case "response.failed": {
|
|
90
|
+
const done = e.response;
|
|
91
|
+
state.model = done?.model ?? state.model;
|
|
92
|
+
state.stopReason = done?.status ?? state.stopReason;
|
|
93
|
+
setUsage(state.usage, usageOf(done?.usage));
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface AffordableOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Refuse to retry below this ceiling. Default 0, which accepts any positive one.
|
|
4
|
+
*
|
|
5
|
+
* Worth setting where the reply is a forced tool call: a schema truncated
|
|
6
|
+
* mid-arguments parses to nothing, which is a worse answer than the refusal it
|
|
7
|
+
* replaced. A caller that would rather fail than be cut off says so here.
|
|
8
|
+
*/
|
|
9
|
+
floor?: number;
|
|
10
|
+
/** Attempts after the first. Default 1. */
|
|
11
|
+
retries?: number;
|
|
12
|
+
/** Recognises the refusal and says what is affordable. Replaces the built-in reading. */
|
|
13
|
+
reads?: (err: unknown) => number | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What a provider says it can afford, read off a refusal.
|
|
17
|
+
*
|
|
18
|
+
* `402` alone is not enough to act on — it is also what an empty account
|
|
19
|
+
* returns, and shrinking a ceiling will not fix that. The number is the signal:
|
|
20
|
+
* a provider that names one is telling you the request would have been accepted
|
|
21
|
+
* smaller.
|
|
22
|
+
*/
|
|
23
|
+
export declare function affordableCeiling(err: unknown): number | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* A provider client that lowers its ceiling rather than failing over it.
|
|
26
|
+
*
|
|
27
|
+
* `max_tokens` is a request for headroom, not a bill — output is charged on what
|
|
28
|
+
* is written. A provider that reserves the ceiling against a prepaid balance
|
|
29
|
+
* refuses the whole call over headroom that was never going to be used:
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* 402 You requested up to 4096 tokens, but can only afford 4000.
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* This retries that once at the number the provider named. It is a decorator
|
|
36
|
+
* rather than something inside `Llm` because how a client copes with a
|
|
37
|
+
* provider's billing is the client's business, and because it then works for
|
|
38
|
+
* every shape — the wrapper mirrors whatever it is given, so `driverFor` still
|
|
39
|
+
* recognises an Anthropic, OpenAI or Gemini client through it.
|
|
40
|
+
*
|
|
41
|
+
* A `402` that names no number is passed through untouched. That one is an
|
|
42
|
+
* empty account, and no ceiling will fix it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function affordable<T>(client: T, opts?: AffordableOptions): T;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Every field a provider carries the output ceiling in. */
|
|
2
|
+
const CEILINGS = ["max_tokens", "max_completion_tokens", "maxOutputTokens"];
|
|
3
|
+
const AFFORDS = /can only afford\s+(\d[\d,]*)/i;
|
|
4
|
+
/**
|
|
5
|
+
* What a provider says it can afford, read off a refusal.
|
|
6
|
+
*
|
|
7
|
+
* `402` alone is not enough to act on — it is also what an empty account
|
|
8
|
+
* returns, and shrinking a ceiling will not fix that. The number is the signal:
|
|
9
|
+
* a provider that names one is telling you the request would have been accepted
|
|
10
|
+
* smaller.
|
|
11
|
+
*/
|
|
12
|
+
export function affordableCeiling(err) {
|
|
13
|
+
const status = err?.status;
|
|
14
|
+
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
15
|
+
if (status !== 402 && !/\b402\b/.test(message))
|
|
16
|
+
return undefined;
|
|
17
|
+
const found = AFFORDS.exec(message);
|
|
18
|
+
return found ? Number(found[1].replace(/,/g, "")) : undefined;
|
|
19
|
+
}
|
|
20
|
+
function shrink(body, to) {
|
|
21
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
22
|
+
return undefined;
|
|
23
|
+
const source = body;
|
|
24
|
+
for (const key of CEILINGS) {
|
|
25
|
+
const current = source[key];
|
|
26
|
+
if (typeof current === "number" && to < current)
|
|
27
|
+
return { ...source, [key]: to };
|
|
28
|
+
}
|
|
29
|
+
for (const [key, value] of Object.entries(source)) {
|
|
30
|
+
const nested = value && typeof value === "object" ? shrink(value, to) : undefined;
|
|
31
|
+
if (nested)
|
|
32
|
+
return { ...source, [key]: nested };
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A provider client that lowers its ceiling rather than failing over it.
|
|
38
|
+
*
|
|
39
|
+
* `max_tokens` is a request for headroom, not a bill — output is charged on what
|
|
40
|
+
* is written. A provider that reserves the ceiling against a prepaid balance
|
|
41
|
+
* refuses the whole call over headroom that was never going to be used:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* 402 You requested up to 4096 tokens, but can only afford 4000.
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* This retries that once at the number the provider named. It is a decorator
|
|
48
|
+
* rather than something inside `Llm` because how a client copes with a
|
|
49
|
+
* provider's billing is the client's business, and because it then works for
|
|
50
|
+
* every shape — the wrapper mirrors whatever it is given, so `driverFor` still
|
|
51
|
+
* recognises an Anthropic, OpenAI or Gemini client through it.
|
|
52
|
+
*
|
|
53
|
+
* A `402` that names no number is passed through untouched. That one is an
|
|
54
|
+
* empty account, and no ceiling will fix it.
|
|
55
|
+
*/
|
|
56
|
+
export function affordable(client, opts = {}) {
|
|
57
|
+
const floor = opts.floor ?? 0;
|
|
58
|
+
const retries = opts.retries ?? 1;
|
|
59
|
+
const reads = opts.reads ?? affordableCeiling;
|
|
60
|
+
const call = async (fn, owner, args) => {
|
|
61
|
+
let attempt = 0;
|
|
62
|
+
let current = args;
|
|
63
|
+
for (;;) {
|
|
64
|
+
try {
|
|
65
|
+
return await fn.apply(owner, current);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const to = reads(err);
|
|
69
|
+
const smaller = to === undefined ? undefined : shrink(current[0], to);
|
|
70
|
+
if (attempt >= retries || to === undefined || to < floor || !smaller)
|
|
71
|
+
throw err;
|
|
72
|
+
current = [smaller, ...current.slice(1)];
|
|
73
|
+
attempt++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const wrap = (target) => new Proxy(target, {
|
|
78
|
+
get(node, prop) {
|
|
79
|
+
const value = Reflect.get(node, prop);
|
|
80
|
+
if (typeof value === "function") {
|
|
81
|
+
return (...args) => call(value, node, args);
|
|
82
|
+
}
|
|
83
|
+
return value && typeof value === "object" ? wrap(value) : value;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return wrap(client);
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { affordable, affordableCeiling, type AffordableOptions } from "./affordable.js";
|
|
2
|
+
export type { Shape } from "./adapters/index.js";
|
|
3
|
+
export type { CallOptions, LlmOptions, ToolBus, ToolResult, } from "./llm/index.js";
|
|
4
|
+
export { Llm, UnsupportedClientError } from "./llm/index.js";
|
|
5
|
+
export type { JsonSchema, StructuredOutput } from "./llm/schema/index.js";
|
|
6
|
+
export { StructuredOutputError } from "./llm/schema/index.js";
|
|
7
|
+
export { read, UnknownReplyError } from "./read.js";
|
|
8
|
+
export type { Ingest, SpendgraphOptions, Tag, UsageEvent, } from "./report/index.js";
|
|
9
|
+
export { MissingCredentialsError } from "./report/index.js";
|
|
10
|
+
export { anthropicMessages, geminiMessages, openaiMessages, } from "./request.js";
|
|
11
|
+
export type { LlmStream, StreamOptions } from "./stream.js";
|
|
12
|
+
export { collect, stream } from "./stream.js";
|
|
13
|
+
export type { CallOutcome, LlmReply, LlmToolCall, LlmUsage, PromptMessage, Provider, ReadOptions, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { affordable, affordableCeiling } from "./affordable.js";
|
|
2
|
+
export { Llm, UnsupportedClientError } from "./llm/index.js";
|
|
3
|
+
export { StructuredOutputError } from "./llm/schema/index.js";
|
|
4
|
+
export { read, UnknownReplyError } from "./read.js";
|
|
5
|
+
export { MissingCredentialsError } from "./report/index.js";
|
|
6
|
+
export { anthropicMessages, geminiMessages, openaiMessages, } from "./request.js";
|
|
7
|
+
export { collect, stream } from "./stream.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Shape } from "../adapters/index.js";
|
|
2
|
+
import type { LlmToolCall, PromptMessage } from "../types.js";
|
|
3
|
+
import { type StructuredOutput } from "./schema/index.js";
|
|
4
|
+
import type { ToolBus, ToolResult } from "./types.js";
|
|
5
|
+
export interface Conversation {
|
|
6
|
+
system?: unknown;
|
|
7
|
+
messages: unknown[];
|
|
8
|
+
}
|
|
9
|
+
export interface Body {
|
|
10
|
+
model: string;
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
tools?: unknown[];
|
|
13
|
+
/** A shape the reply must take. Sent the way each provider wants it. */
|
|
14
|
+
structured?: StructuredOutput;
|
|
15
|
+
params: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface Completed {
|
|
18
|
+
call: LlmToolCall;
|
|
19
|
+
result: ToolResult;
|
|
20
|
+
}
|
|
21
|
+
export interface Driver {
|
|
22
|
+
shape: Shape;
|
|
23
|
+
supportsTools: boolean;
|
|
24
|
+
detect(client: unknown): boolean;
|
|
25
|
+
start(messages: PromptMessage[]): Conversation;
|
|
26
|
+
create(client: unknown, convo: Conversation, body: Body): Promise<unknown>;
|
|
27
|
+
stream(client: unknown, convo: Conversation, body: Body): Promise<AsyncIterable<unknown>>;
|
|
28
|
+
declarations(tools: ToolBus): unknown[];
|
|
29
|
+
pushReply(convo: Conversation, raw: unknown): void;
|
|
30
|
+
pushResults(convo: Conversation, done: Completed[]): void;
|
|
31
|
+
}
|
|
32
|
+
export declare const anthropicDriver: Driver;
|
|
33
|
+
export declare const openaiDriver: Driver;
|
|
34
|
+
export declare const geminiDriver: Driver;
|
|
35
|
+
export declare const DRIVERS: Driver[];
|
|
36
|
+
export declare function driverFor(client: unknown): Driver | undefined;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { anthropicMessages, geminiMessages, openaiMessages } from "../request.js";
|
|
2
|
+
import { has, invoke } from "./invoke.js";
|
|
3
|
+
import { anthropicStructured, geminiStructured, openaiStructured, } from "./schema/index.js";
|
|
4
|
+
const answer = (result) => result.output || result.error || "";
|
|
5
|
+
function anthropicBody(convo, body) {
|
|
6
|
+
return {
|
|
7
|
+
model: body.model,
|
|
8
|
+
max_tokens: body.maxTokens ?? 16_000,
|
|
9
|
+
...(convo.system ? { system: convo.system } : {}),
|
|
10
|
+
messages: convo.messages,
|
|
11
|
+
...(body.tools?.length ? { tools: body.tools } : {}),
|
|
12
|
+
...(body.structured ? anthropicStructured(body.structured) : {}),
|
|
13
|
+
...body.params,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export const anthropicDriver = {
|
|
17
|
+
shape: "anthropic",
|
|
18
|
+
supportsTools: true,
|
|
19
|
+
detect: (client) => has(client, ["messages", "create"]),
|
|
20
|
+
start(messages) {
|
|
21
|
+
const { system, messages: turns } = anthropicMessages(messages);
|
|
22
|
+
return { system, messages: turns };
|
|
23
|
+
},
|
|
24
|
+
create: (client, convo, body) => invoke(client, ["messages", "create"], anthropicBody(convo, body)),
|
|
25
|
+
stream: (client, convo, body) => invoke(client, ["messages", "stream"], anthropicBody(convo, body)),
|
|
26
|
+
declarations: (tools) => tools.anthropic(),
|
|
27
|
+
pushReply(convo, raw) {
|
|
28
|
+
const { content } = (raw ?? {});
|
|
29
|
+
convo.messages.push({ role: "assistant", content });
|
|
30
|
+
},
|
|
31
|
+
pushResults(convo, done) {
|
|
32
|
+
convo.messages.push({
|
|
33
|
+
role: "user",
|
|
34
|
+
content: done.map(({ call, result }) => ({
|
|
35
|
+
type: "tool_result",
|
|
36
|
+
tool_use_id: call.id,
|
|
37
|
+
content: answer(result),
|
|
38
|
+
is_error: result.status === "failed",
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function openaiBody(convo, body) {
|
|
44
|
+
return {
|
|
45
|
+
model: body.model,
|
|
46
|
+
messages: convo.messages,
|
|
47
|
+
...(body.maxTokens ? { max_completion_tokens: body.maxTokens } : {}),
|
|
48
|
+
...(body.tools?.length ? { tools: body.tools } : {}),
|
|
49
|
+
...(body.structured ? openaiStructured(body.structured) : {}),
|
|
50
|
+
...body.params,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export const openaiDriver = {
|
|
54
|
+
shape: "openai",
|
|
55
|
+
supportsTools: true,
|
|
56
|
+
detect: (client) => has(client, ["chat", "completions", "create"]),
|
|
57
|
+
start: (messages) => ({ messages: openaiMessages(messages).messages }),
|
|
58
|
+
create: (client, convo, body) => invoke(client, ["chat", "completions", "create"], openaiBody(convo, body)),
|
|
59
|
+
stream: (client, convo, body) => invoke(client, ["chat", "completions", "create"], {
|
|
60
|
+
...openaiBody(convo, body),
|
|
61
|
+
stream: true,
|
|
62
|
+
stream_options: { include_usage: true },
|
|
63
|
+
}),
|
|
64
|
+
declarations: (tools) => tools.openai(),
|
|
65
|
+
pushReply(convo, raw) {
|
|
66
|
+
const { choices } = (raw ?? {});
|
|
67
|
+
const message = choices?.[0]?.message;
|
|
68
|
+
if (message)
|
|
69
|
+
convo.messages.push(message);
|
|
70
|
+
},
|
|
71
|
+
pushResults(convo, done) {
|
|
72
|
+
for (const { call, result } of done) {
|
|
73
|
+
convo.messages.push({
|
|
74
|
+
role: "tool",
|
|
75
|
+
tool_call_id: call.id,
|
|
76
|
+
content: answer(result),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
function geminiBody(convo, body) {
|
|
82
|
+
const { generationConfig, ...rest } = body.params;
|
|
83
|
+
const structured = body.structured ? geminiStructured(body.structured) : {};
|
|
84
|
+
const config = body.maxTokens || body.structured
|
|
85
|
+
? {
|
|
86
|
+
...(body.maxTokens ? { maxOutputTokens: body.maxTokens } : {}),
|
|
87
|
+
...structured,
|
|
88
|
+
...generationConfig,
|
|
89
|
+
}
|
|
90
|
+
: generationConfig;
|
|
91
|
+
return {
|
|
92
|
+
model: body.model,
|
|
93
|
+
contents: convo.messages,
|
|
94
|
+
...(convo.system ? { systemInstruction: convo.system } : {}),
|
|
95
|
+
...rest,
|
|
96
|
+
...(config ? { generationConfig: config } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export const geminiDriver = {
|
|
100
|
+
shape: "gemini",
|
|
101
|
+
supportsTools: false,
|
|
102
|
+
detect: (client) => has(client, ["models", "generateContent"]),
|
|
103
|
+
start(messages) {
|
|
104
|
+
const { systemInstruction, contents } = geminiMessages(messages);
|
|
105
|
+
return { system: systemInstruction, messages: contents };
|
|
106
|
+
},
|
|
107
|
+
create: (client, convo, body) => invoke(client, ["models", "generateContent"], geminiBody(convo, body)),
|
|
108
|
+
stream: (client, convo, body) => invoke(client, ["models", "generateContentStream"], geminiBody(convo, body)),
|
|
109
|
+
declarations: () => [],
|
|
110
|
+
pushReply(convo, raw) {
|
|
111
|
+
const { candidates } = (raw ?? {});
|
|
112
|
+
convo.messages.push(candidates?.[0]?.content ?? { role: "model", parts: [] });
|
|
113
|
+
},
|
|
114
|
+
pushResults() {
|
|
115
|
+
throw new Error("Llm does not drive Gemini tool loops. Use read() and drive it yourself.");
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
export const DRIVERS = [anthropicDriver, openaiDriver, geminiDriver];
|
|
119
|
+
export function driverFor(client) {
|
|
120
|
+
return DRIVERS.find((driver) => driver.detect(client));
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Llm, UnsupportedClientError } from "./llm.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function walk(client, keys) {
|
|
2
|
+
let node = client;
|
|
3
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
4
|
+
if (!node || typeof node !== "object")
|
|
5
|
+
return undefined;
|
|
6
|
+
node = node[keys[i]];
|
|
7
|
+
}
|
|
8
|
+
if (!node || typeof node !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const owner = node;
|
|
11
|
+
return { owner, fn: owner[keys[keys.length - 1]] };
|
|
12
|
+
}
|
|
13
|
+
export function has(client, path) {
|
|
14
|
+
return typeof walk(client, path)?.fn === "function";
|
|
15
|
+
}
|
|
16
|
+
/** Calls `client.a.b.c(body)` with `client.a.b` as the receiver. */
|
|
17
|
+
export async function invoke(client, path, body) {
|
|
18
|
+
const found = walk(client, path);
|
|
19
|
+
if (typeof found?.fn !== "function") {
|
|
20
|
+
throw new TypeError(`This client has no ${path.join(".")}().`);
|
|
21
|
+
}
|
|
22
|
+
const call = found.fn;
|
|
23
|
+
return call.call(found.owner, body);
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LlmReply, PromptMessage } from "../types.js";
|
|
2
|
+
import type { CallOptions, LlmOptions } from "./types.js";
|
|
3
|
+
export declare class UnsupportedClientError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A provider client and its defaults. Calls the model, normalises the reply,
|
|
8
|
+
* and records what it consumed.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Llm {
|
|
11
|
+
private readonly opts;
|
|
12
|
+
private readonly driver;
|
|
13
|
+
private readonly ingest?;
|
|
14
|
+
constructor(opts: LlmOptions);
|
|
15
|
+
get provider(): string;
|
|
16
|
+
get model(): string;
|
|
17
|
+
/** The same client with different defaults — another model, tools, no tracing. */
|
|
18
|
+
with(overrides: Partial<LlmOptions>): Llm;
|
|
19
|
+
/** Calls the model, looping over tools when a bus is attached. */
|
|
20
|
+
call(messages: PromptMessage[], opts?: CallOptions): Promise<LlmReply>;
|
|
21
|
+
/**
|
|
22
|
+
* The call, then each fallback model in turn.
|
|
23
|
+
*
|
|
24
|
+
* The last error is thrown rather than the first: a caller reading the stack
|
|
25
|
+
* wants to know how it finished, and the first failure is the one that is
|
|
26
|
+
* already in the `onFallback` log.
|
|
27
|
+
*/
|
|
28
|
+
private attempt;
|
|
29
|
+
/** The same, streamed. `onText` receives each delta as it arrives. */
|
|
30
|
+
stream(messages: PromptMessage[], opts?: CallOptions): Promise<LlmReply>;
|
|
31
|
+
private once;
|
|
32
|
+
private loop;
|
|
33
|
+
private record;
|
|
34
|
+
private settings;
|
|
35
|
+
private body;
|
|
36
|
+
/**
|
|
37
|
+
* Attaches the object a `schema` asked for.
|
|
38
|
+
*
|
|
39
|
+
* Anthropic answers a forced tool call, so the value is already parsed in the
|
|
40
|
+
* call's arguments; the JSON modes answer as text. A reply that satisfies
|
|
41
|
+
* neither throws rather than coming back with `data` quietly missing.
|
|
42
|
+
*
|
|
43
|
+
* A forced call leaves no text block, so `output` is filled from the object:
|
|
44
|
+
* it is what a recorded rollout stores as the reply, and blank there reads as
|
|
45
|
+
* a model that said nothing.
|
|
46
|
+
*/
|
|
47
|
+
private withData;
|
|
48
|
+
private read;
|
|
49
|
+
private adapterFor;
|
|
50
|
+
}
|