@argentic/chest-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/README.md +213 -23
- package/client/index.ts +7 -5
- package/client/src/ai.ts +374 -0
- package/client/src/api.ts +7 -3
- package/client/src/chest.ts +80 -0
- package/client/src/errors.ts +44 -0
- package/client/src/member.ts +26 -5
- package/client/src/members.ts +3 -3
- package/client/src/testing.ts +167 -20
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/src/ai.d.ts +131 -0
- package/dist/src/ai.d.ts.map +1 -0
- package/dist/src/ai.js +290 -0
- package/dist/src/ai.js.map +1 -0
- package/dist/src/api.d.ts +2 -0
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +7 -2
- package/dist/src/api.js.map +1 -1
- package/dist/src/chest.d.ts +10 -0
- package/dist/src/chest.d.ts.map +1 -0
- package/dist/src/chest.js +49 -0
- package/dist/src/chest.js.map +1 -0
- package/dist/src/errors.d.ts +16 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +36 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/member.d.ts +4 -0
- package/dist/src/member.d.ts.map +1 -1
- package/dist/src/member.js +17 -5
- package/dist/src/member.js.map +1 -1
- package/dist/src/members.d.ts.map +1 -1
- package/dist/src/members.js +3 -3
- package/dist/src/members.js.map +1 -1
- package/dist/src/testing.d.ts +38 -7
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +148 -15
- package/dist/src/testing.js.map +1 -1
- package/package.json +15 -3
package/client/src/testing.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
2
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
3
|
import type { AddressInfo } from "node:net";
|
|
4
|
+
import type { Alias, Provider } from "./ai.js";
|
|
5
|
+
import type { AiUnavailableReason } from "./errors.js";
|
|
4
6
|
import type { ChestEvent } from "./events.js";
|
|
5
7
|
import { groupIdPattern, memberIdPattern, type Member } from "./member.js";
|
|
6
8
|
import { forget } from "./members.js";
|
|
@@ -8,9 +10,11 @@ import { forget } from "./members.js";
|
|
|
8
10
|
// For a tool's own tests, never imported by its production code: a member's
|
|
9
11
|
// assertion signed as the Chest signs it, and a Chest's API in the test's
|
|
10
12
|
// process that answers members, groups, files (stat, move, links; an upload
|
|
11
|
-
// it authorises but does not receive), badges, notifications
|
|
12
|
-
//
|
|
13
|
-
// and
|
|
13
|
+
// it authorises but does not receive), badges, notifications, AI (chat,
|
|
14
|
+
// streamed or not, embeddings, models, usage: deterministic answers, no
|
|
15
|
+
// provider) and the acknowledgment of an erasure with the Chest's bounds,
|
|
16
|
+
// quotas and errors; and that delivers an event to the tool, signed as the
|
|
17
|
+
// Chest signs it.
|
|
14
18
|
//
|
|
15
19
|
// import { fakeChest, withMember } from "@argentic/chest-sdk/testing";
|
|
16
20
|
// const chest = await fakeChest({ members: [camille], capabilities: ["members", "files", "notifications"] });
|
|
@@ -28,13 +32,32 @@ export type FakeFile = { data: Uint8Array; type: string; updated: string };
|
|
|
28
32
|
// cleaned as the Chest cleans it, its path (/chest when not said) and its
|
|
29
33
|
// key.
|
|
30
34
|
export type FakeNotification = { member: string; title: string; body?: string; path: string; key?: string };
|
|
35
|
+
// An alias a fake Chest maps: the model behind it, its provider (openrouter
|
|
36
|
+
// by default) and its prices in US dollars per million tokens (1 and 2 by
|
|
37
|
+
// default).
|
|
38
|
+
export type FakeAiModel = { alias: Alias; model: string; provider?: Provider; input?: number; output?: number };
|
|
39
|
+
// What a fake model answers: text, or text and tool calls (an id of call_…
|
|
40
|
+
// by default; arguments are JSON text).
|
|
41
|
+
export type FakeAiReply = string | { text?: string; toolCalls?: { name: string; arguments: string; id?: string }[] };
|
|
42
|
+
// The AI of a fake Chest: the aliases the tool declared, each mapped (all
|
|
43
|
+
// four by default: fake-default, fake-fast, fake-smart, fake-embedding); what
|
|
44
|
+
// its models answer to a chat, given the request as the tool sent it (the
|
|
45
|
+
// last user message's text, echoed, by default); the tool's monthly cap in
|
|
46
|
+
// euros (5 by default; a call is refused cap_reached once the spending
|
|
47
|
+
// reaches it, 0 refuses at once); and a reason that makes chat and
|
|
48
|
+
// embeddings unavailable.
|
|
49
|
+
export type FakeAi = { models?: FakeAiModel[]; reply?: (request: Record<string, unknown>) => FakeAiReply; cap?: number; unavailable?: AiUnavailableReason };
|
|
50
|
+
// A call of the tool to the AI of a fake Chest: its path and its body as
|
|
51
|
+
// sent (null for a GET).
|
|
52
|
+
export type FakeAiCall = { path: string; body: unknown };
|
|
31
53
|
|
|
32
54
|
// What a fake Chest is given: the members who have the tool, those who left
|
|
33
55
|
// it (erased: their data was erased, the name gone), its groups, the
|
|
34
56
|
// capabilities its version holds (a capability left out answers 403;
|
|
35
|
-
// members, files and
|
|
57
|
+
// members, files, notifications and ai by default, members.email to read the
|
|
36
58
|
// addresses), the events it receives (["member.*"] by default, [] to answer
|
|
37
|
-
// an acknowledgment 403)
|
|
59
|
+
// an acknowledgment 403), the files it keeps, its AI, and what the Chest is
|
|
60
|
+
// (the chest module: "Test organization", UTC and English by default).
|
|
38
61
|
export type FakeChestOptions = {
|
|
39
62
|
members?: Member[];
|
|
40
63
|
former?: { id: string; name?: string; erased?: boolean }[];
|
|
@@ -42,6 +65,8 @@ export type FakeChestOptions = {
|
|
|
42
65
|
capabilities?: string[];
|
|
43
66
|
receives?: string[];
|
|
44
67
|
files?: Record<string, { data: Uint8Array | string; type?: string }>;
|
|
68
|
+
ai?: FakeAi;
|
|
69
|
+
chest?: { organization?: string; timeZone?: string; language?: string };
|
|
45
70
|
};
|
|
46
71
|
|
|
47
72
|
// An event for emit: its type and data; its id (a new evt_… by default) and
|
|
@@ -53,7 +78,7 @@ export type FakeEvent = { [K in ChestEvent["type"]]: { type: K; data: Extract<Ch
|
|
|
53
78
|
// set in the environment, what it keeps (members, groups and files a test
|
|
54
79
|
// changes or reads; the notifications the tool sent, in the order sent, a
|
|
55
80
|
// replaced one last; each member's badge; the erasures the tool
|
|
56
|
-
// acknowledged), emit, which delivers an event to the tool — POST
|
|
81
|
+
// acknowledged; its calls to AI, in order), emit, which delivers an event to the tool — POST
|
|
57
82
|
// /chest-events of its address, or a handler of Web Requests — and says the
|
|
58
83
|
// status it answered, and close, which stops it and restores the
|
|
59
84
|
// environment. Its members are those who have the tool: the others are
|
|
@@ -68,16 +93,24 @@ export type FakeChest = {
|
|
|
68
93
|
notifications: FakeNotification[];
|
|
69
94
|
badges: Map<string, number>;
|
|
70
95
|
acknowledged: string[];
|
|
96
|
+
ai: FakeAiCall[];
|
|
71
97
|
emit(event: FakeEvent, to: string | ((request: Request) => Response | Promise<Response>)): Promise<number>;
|
|
72
98
|
close(): Promise<void>;
|
|
73
99
|
};
|
|
74
100
|
|
|
75
101
|
const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
76
102
|
|
|
103
|
+
// What an assertion is signed with and says besides the member: the token
|
|
104
|
+
// (CHEST_TOKEN by default) and the tool (CHEST_TOOL by default) it is for,
|
|
105
|
+
// and when it is issued (now by default). The member is signed as given: a
|
|
106
|
+
// language or a zone the Chest would never send makes member() refuse the
|
|
107
|
+
// assertion, as it refuses the Chest's.
|
|
108
|
+
export type AssertionOptions = { token?: string; tool?: string; now?: Date };
|
|
109
|
+
|
|
77
110
|
// signAssertion is the Chest-Member value the Chest's front would send for
|
|
78
|
-
// that member: HS256 under the key of the token
|
|
79
|
-
//
|
|
80
|
-
export function signAssertion(member: Member, options:
|
|
111
|
+
// that member: HS256 under the key of the token, for the tool, valid 60
|
|
112
|
+
// seconds from when it is issued.
|
|
113
|
+
export function signAssertion(member: Member, options: AssertionOptions = {}): string {
|
|
81
114
|
const token = options.token ?? process.env["CHEST_TOKEN"];
|
|
82
115
|
const tool = options.tool ?? process.env["CHEST_TOOL"];
|
|
83
116
|
if (!token || !tool) throw new Error("signAssertion needs a token and a tool: start a fakeChest, or name them");
|
|
@@ -86,7 +119,8 @@ export function signAssertion(member: Member, options: { token?: string; tool?:
|
|
|
86
119
|
const body = encode({ alg: "HS256", typ: "JWT" }) + "." + encode({
|
|
87
120
|
iss: `https://${tool}-chest.chest.test`, aud: tool, iat, exp: iat + 60, sub: member.id,
|
|
88
121
|
given_name: member.firstName, family_name: member.lastName, name: member.name, picture: member.photo ?? "", role: member.role ?? "",
|
|
89
|
-
admin: member.isAdmin, builder: member.isBuilder, groups: member.groups, ...(member.email === undefined ? {} : { email: member.email }),
|
|
122
|
+
admin: member.isAdmin, builder: member.isBuilder, groups: member.groups, time_zone: member.timeZone, ...(member.email === undefined ? {} : { email: member.email }),
|
|
123
|
+
language: member.language,
|
|
90
124
|
});
|
|
91
125
|
// The key as the Chest derives it, and member() reads it: HMAC-SHA256 of
|
|
92
126
|
// the label of the assertion's shape under the text of the token.
|
|
@@ -104,9 +138,10 @@ function signEvent(id: string, body: string, options: { token: string; tool: str
|
|
|
104
138
|
return signed + "." + createHmac("sha256", key).update(signed).digest("base64url");
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
// withMember is the request carrying that member's assertion
|
|
108
|
-
// Request, or the same Node request
|
|
109
|
-
|
|
141
|
+
// withMember is the request carrying that member's assertion, signed with
|
|
142
|
+
// the options of signAssertion: a new Web Request, or the same Node request
|
|
143
|
+
// with its header set.
|
|
144
|
+
export function withMember<R extends Request | IncomingMessage>(request: R, member: Member, options: AssertionOptions = {}): R {
|
|
110
145
|
const assertion = signAssertion(member, options);
|
|
111
146
|
if (request instanceof Request) {
|
|
112
147
|
const headers = new Headers(request.headers);
|
|
@@ -130,6 +165,22 @@ const cleanTitle = (s: string): string => s.replace(/[\t\r\n]/gu, " ").replace(/
|
|
|
130
165
|
const cleanText = (s: string): string => s.replace(/\r\n?/gu, "\n").replace(/\t/gu, " ").replace(/[^\P{Cc}\n]/gu, "").replace(reordering, "").trim();
|
|
131
166
|
const isPath = (p: unknown): boolean => typeof p === "string" && p.length <= maxPath && /^\/chest([/?#][\x21-\x5b\x5d-\x7e]*)?$/u.test(p) && !p.includes("//") && !p.split(/[?#]/u)[0]!.split("/").some(x => /^(\.|%2e){1,2}$/iu.test(x));
|
|
132
167
|
|
|
168
|
+
// Its AI: aliases in order, bounds of a request, requests a minute.
|
|
169
|
+
const aliasOrder: readonly Alias[] = ["default", "fast", "smart", "embedding"];
|
|
170
|
+
const chatKeys = ["model", "messages", "max_tokens", "stream", "temperature", "top_p", "stop", "tools", "tool_choice", "response_format", "parallel_tool_calls", "seed", "reasoning_effort", "member"];
|
|
171
|
+
const maxAiBody = 10 << 20, maxAiOutput = 128000, maxInputs = 256, aiPerMinute = 60, fakeDimensions = 8;
|
|
172
|
+
// A fake count of tokens: one per 4 characters.
|
|
173
|
+
const tokensOf = (text: string): number => Math.ceil(text.length / 4);
|
|
174
|
+
// A vector of a text, the same for the same text: unit length.
|
|
175
|
+
function vectorOf(text: string, dimensions: number): number[] {
|
|
176
|
+
const values: number[] = [];
|
|
177
|
+
for (let block = 0; values.length < dimensions; block++) {
|
|
178
|
+
for (const b of createHash("sha256").update(block + ":" + text).digest()) values.push(b / 127.5 - 1);
|
|
179
|
+
}
|
|
180
|
+
const kept = values.slice(0, dimensions), norm = Math.hypot(...kept) || 1;
|
|
181
|
+
return kept.map(x => x / norm);
|
|
182
|
+
}
|
|
183
|
+
|
|
133
184
|
const fold = (s: string): string => s.normalize("NFD").replace(/\p{Mn}/gu, "").toLowerCase();
|
|
134
185
|
|
|
135
186
|
function send(response: ServerResponse, status: number, value?: unknown, headers: Record<string, string> = {}): void {
|
|
@@ -150,10 +201,11 @@ async function body(request: IncomingMessage, limit: number): Promise<Buffer | n
|
|
|
150
201
|
|
|
151
202
|
// fakeChest starts a Chest's API on 127.0.0.1 and points the environment at
|
|
152
203
|
// it: CHEST_API, CHEST_TOKEN (a new one) and CHEST_TOOL ("tool" unless the
|
|
153
|
-
// environment names one)
|
|
154
|
-
//
|
|
204
|
+
// environment names one), and what the Chest is (CHEST_ORGANIZATION,
|
|
205
|
+
// CHEST_TIME_ZONE, CHEST_LANGUAGE, as options.chest says). What member(),
|
|
206
|
+
// chest and the modules of the SDK read is then this Chest's.
|
|
155
207
|
export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChest> {
|
|
156
|
-
const capabilities = new Set(options.capabilities ?? ["members", "files", "notifications"]);
|
|
208
|
+
const capabilities = new Set(options.capabilities ?? ["members", "files", "notifications", "ai"]);
|
|
157
209
|
const email = capabilities.has("members.email");
|
|
158
210
|
const tool = process.env["CHEST_TOOL"] || "tool";
|
|
159
211
|
const token = randomBytes(32).toString("base64url");
|
|
@@ -164,10 +216,10 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
|
|
|
164
216
|
const receives = options.receives ?? ["member.*"];
|
|
165
217
|
// The erasures the tool was told of, by emit: those it may acknowledge.
|
|
166
218
|
const erasures = new Set<string>();
|
|
167
|
-
const chest: FakeChest = { api: "", token, tool, members: [...(options.members ?? [])], groups: [...(options.groups ?? [])], files, notifications: [], badges: new Map(), acknowledged: [], emit: async () => 0, close: async () => {} };
|
|
219
|
+
const chest: FakeChest = { api: "", token, tool, members: [...(options.members ?? [])], groups: [...(options.groups ?? [])], files, notifications: [], badges: new Map(), acknowledged: [], ai: [], emit: async () => 0, close: async () => {} };
|
|
168
220
|
const former = [...(options.former ?? [])];
|
|
169
221
|
let window = 0, calls = 0;
|
|
170
|
-
const shown = (m: Member) => ({ id: m.id, first_name: m.firstName, last_name: m.lastName, name: m.name, photo: m.photo, role: m.role, admin: m.isAdmin, builder: m.isBuilder, groups: m.groups, ...(email && m.email !== undefined ? { email: m.email } : {}) });
|
|
222
|
+
const shown = (m: Member) => ({ id: m.id, first_name: m.firstName, last_name: m.lastName, name: m.name, photo: m.photo, role: m.role, admin: m.isAdmin, builder: m.isBuilder, groups: m.groups, language: m.language, time_zone: m.timeZone, ...(email && m.email !== undefined ? { email: m.email } : {}) });
|
|
171
223
|
const key = (m: Member) => fold(m.name) + "\u0000" + m.id;
|
|
172
224
|
const described = (name: string, f: FakeFile) => ({ name, type: f.type, size: f.data.byteLength, updated: f.updated });
|
|
173
225
|
|
|
@@ -369,6 +421,100 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
|
|
|
369
421
|
send(response, 200, { delivered: kept, skipped: ids.filter(id => !access(id)) });
|
|
370
422
|
}
|
|
371
423
|
|
|
424
|
+
// Its AI: the declared aliases, the spending this month, the requests this
|
|
425
|
+
// minute.
|
|
426
|
+
const mapped = (options.ai?.models ?? aliasOrder.map((alias): FakeAiModel => ({ alias, model: "fake-" + alias }))).map(m => ({ alias: m.alias, model: m.model, provider: m.provider ?? "openrouter", input: m.input ?? 1, output: m.output ?? 2 }));
|
|
427
|
+
const cap = options.ai?.cap ?? 5;
|
|
428
|
+
const aiMinute: Window = { start: 0, count: 0 };
|
|
429
|
+
let spent = 0;
|
|
430
|
+
const month = () => new Date().toISOString().slice(0, 7);
|
|
431
|
+
const resets = () => {
|
|
432
|
+
const now = new Date();
|
|
433
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString().replace(".000Z", "Z");
|
|
434
|
+
};
|
|
435
|
+
const spend = (m: { input: number; output: number }, input: number, output: number): number => {
|
|
436
|
+
const cost = Math.round((input * m.input + output * m.output) / 1e6 * 1e6) / 1e6;
|
|
437
|
+
spent = Math.round((spent + cost) * 1e6) / 1e6;
|
|
438
|
+
return cost;
|
|
439
|
+
};
|
|
440
|
+
// echo is the text of the last user message: its text, or its text parts.
|
|
441
|
+
const echo = (request: Record<string, unknown>): string => {
|
|
442
|
+
const last = [...request["messages"] as { role?: unknown; content?: unknown }[]].reverse().find(m => m?.role === "user")?.content;
|
|
443
|
+
return typeof last === "string" ? last : Array.isArray(last) ? last.map(p => (p as { text?: unknown })?.text).filter(t => typeof t === "string").join("") : "";
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
async function aiRoute(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {
|
|
447
|
+
const raw = request.method === "POST" ? await body(request, maxAiBody) : null;
|
|
448
|
+
let command: Record<string, unknown> | null = null;
|
|
449
|
+
try {
|
|
450
|
+
const value = JSON.parse(raw?.toString() ?? "") as unknown;
|
|
451
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) command = value as Record<string, unknown>;
|
|
452
|
+
} catch {
|
|
453
|
+
command = null;
|
|
454
|
+
}
|
|
455
|
+
chest.ai.push({ path: url.pathname, body: command });
|
|
456
|
+
if (!capabilities.has("ai")) return send(response, 403, { error: "capability_not_granted" });
|
|
457
|
+
if (request.method === "GET" && url.pathname === "/ai/models") return send(response, 200, { models: [...mapped].sort((a, b) => aliasOrder.indexOf(a.alias) - aliasOrder.indexOf(b.alias)) });
|
|
458
|
+
if (request.method === "GET" && url.pathname === "/ai/usage") return send(response, 200, { month: month(), spent, cap, resets: resets() });
|
|
459
|
+
if (request.method !== "POST" || (url.pathname !== "/ai/chat" && url.pathname !== "/ai/embeddings")) return send(response, 404, { error: "not_found" });
|
|
460
|
+
if (raw === null) return send(response, 413, { error: "too_large" });
|
|
461
|
+
const chat = url.pathname === "/ai/chat";
|
|
462
|
+
if (!command || !Object.keys(command).every(k => chat ? chatKeys.includes(k) : ["model", "input", "dimensions", "member"].includes(k))) return send(response, 400, { error: "invalid_body" });
|
|
463
|
+
const model = mapped.find(m => m.alias === command!["model"]);
|
|
464
|
+
if (!model) return send(response, 403, { error: "model_not_allowed" });
|
|
465
|
+
const member = command["member"];
|
|
466
|
+
if (member !== undefined && (typeof member !== "string" || !memberIdPattern.test(member))) return send(response, 400, { error: "invalid_body" });
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
// refused says the refusal of a valid request, if any: the rate, the
|
|
469
|
+
// provider, the cap.
|
|
470
|
+
const refused = (): boolean => {
|
|
471
|
+
const busy = live(aiMinute, 60_000, now) && aiMinute.count >= aiPerMinute;
|
|
472
|
+
if (busy) send(response, 429, { error: "rate_limited" }, wait(aiMinute, 60_000, now));
|
|
473
|
+
else {
|
|
474
|
+
count(aiMinute, 60_000, now, 1);
|
|
475
|
+
if (options.ai?.unavailable) send(response, options.ai.unavailable === "provider_key_invalid" ? 502 : 503, { error: options.ai.unavailable });
|
|
476
|
+
else if (spent >= cap) send(response, 402, { error: "cap_reached", scope: "tool", resets: resets() });
|
|
477
|
+
}
|
|
478
|
+
return response.headersSent;
|
|
479
|
+
};
|
|
480
|
+
if (!chat) {
|
|
481
|
+
const input = command["input"], texts = typeof input === "string" ? [input] : input, dimensions = command["dimensions"] ?? fakeDimensions;
|
|
482
|
+
if (!Array.isArray(texts) || texts.length < 1 || texts.length > maxInputs || !texts.every(t => typeof t === "string") || typeof dimensions !== "number" || !Number.isInteger(dimensions) || dimensions < 1 || dimensions > 4096) return send(response, 400, { error: "invalid_body" });
|
|
483
|
+
if (refused()) return;
|
|
484
|
+
const used = texts.reduce((sum: number, t: string) => sum + tokensOf(t), 0);
|
|
485
|
+
const cost = spend(model, used, 0);
|
|
486
|
+
return send(response, 200, { object: "list", data: texts.map((t: string, index) => ({ object: "embedding", index, embedding: vectorOf(t, dimensions) })), model: model.model, usage: { prompt_tokens: used, total_tokens: used, cost } });
|
|
487
|
+
}
|
|
488
|
+
const messages = command["messages"], limit = command["max_tokens"] ?? 4096;
|
|
489
|
+
if (!Array.isArray(messages) || messages.length < 1 || !messages.every(m => m !== null && typeof m === "object" && typeof (m as { role?: unknown }).role === "string") || typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > maxAiOutput || !(command["stream"] === undefined || typeof command["stream"] === "boolean")) return send(response, 400, { error: "invalid_body" });
|
|
490
|
+
if (refused()) return;
|
|
491
|
+
const given = options.ai?.reply ? options.ai.reply(command) : echo(command);
|
|
492
|
+
const text = typeof given === "string" ? given : given.text ?? "";
|
|
493
|
+
const calls = (typeof given === "string" ? [] : given.toolCalls ?? []).map((c, i) => ({ id: c.id ?? `call_${i + 1}`, type: "function" as const, function: { name: c.name, arguments: c.arguments } }));
|
|
494
|
+
const input = tokensOf(JSON.stringify(messages)), output = tokensOf(text + calls.map(c => c.function.name + c.function.arguments).join(""));
|
|
495
|
+
const usage = { prompt_tokens: input, completion_tokens: output, total_tokens: input + output, prompt_tokens_details: { cached_tokens: 0 }, cost: spend(model, input, output) };
|
|
496
|
+
const finish = calls.length ? "tool_calls" : "stop";
|
|
497
|
+
const head = { id: "chatcmpl-fake", created: Math.floor(now / 1000), model: model.model };
|
|
498
|
+
if (command["stream"] !== true) {
|
|
499
|
+
return send(response, 200, { ...head, object: "chat.completion", choices: [{ index: 0, message: { role: "assistant", content: calls.length && !text ? null : text, ...(calls.length ? { tool_calls: calls } : {}) }, finish_reason: finish }], usage });
|
|
500
|
+
}
|
|
501
|
+
// Streamed: the role, the text word by word, each call's name then its
|
|
502
|
+
// arguments in two halves, the finish reason, the usage, [DONE].
|
|
503
|
+
const chunk = (choices: unknown[], extra: Record<string, unknown> = {}) => `data: ${JSON.stringify({ ...head, object: "chat.completion.chunk", choices, ...extra })}\n\n`;
|
|
504
|
+
const delta = (d: Record<string, unknown>, finishReason: string | null = null) => chunk([{ index: 0, delta: d, finish_reason: finishReason }]);
|
|
505
|
+
const parts = [delta({ role: "assistant", content: "" })];
|
|
506
|
+
for (const word of text.match(/\S+\s*|\s+/gu) ?? []) parts.push(delta({ content: word }));
|
|
507
|
+
calls.forEach((c, index) => {
|
|
508
|
+
const half = Math.ceil(c.function.arguments.length / 2);
|
|
509
|
+
parts.push(delta({ tool_calls: [{ index, id: c.id, type: "function", function: { name: c.function.name, arguments: "" } }] }));
|
|
510
|
+
for (const piece of [c.function.arguments.slice(0, half), c.function.arguments.slice(half)]) if (piece) parts.push(delta({ tool_calls: [{ index, function: { arguments: piece } }] }));
|
|
511
|
+
});
|
|
512
|
+
parts.push(delta({}, finish), chunk([], { usage }), "data: [DONE]\n\n");
|
|
513
|
+
response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
514
|
+
for (const part of parts) response.write(part);
|
|
515
|
+
response.end();
|
|
516
|
+
}
|
|
517
|
+
|
|
372
518
|
// The acknowledgment of an erasure the tool was told of (emit).
|
|
373
519
|
async function erasuresRoute(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {
|
|
374
520
|
if (!receives.includes("member.*")) return send(response, 403, { error: "capability_not_granted" });
|
|
@@ -384,6 +530,7 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
|
|
|
384
530
|
const server = createServer((request, response) => {
|
|
385
531
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
386
532
|
const route = url.pathname.startsWith("/erasures/") ? erasuresRoute
|
|
533
|
+
: url.pathname.startsWith("/ai/") ? aiRoute
|
|
387
534
|
: url.pathname === "/members" || url.pathname.startsWith("/members/") || url.pathname === "/groups" ? members
|
|
388
535
|
: url.pathname === "/files" || url.pathname.startsWith("/files/") ? filesRoute
|
|
389
536
|
: url.pathname === "/badges" || url.pathname.startsWith("/badges/") || url.pathname.startsWith("/notifications") ? notificationsRoute : null;
|
|
@@ -391,9 +538,9 @@ export async function fakeChest(options: FakeChestOptions = {}): Promise<FakeChe
|
|
|
391
538
|
route(request, response, url).catch(() => { if (!response.headersSent) send(response, 503, { error: "unavailable" }); else response.destroy(); });
|
|
392
539
|
});
|
|
393
540
|
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
|
394
|
-
const saved = Object.fromEntries(["CHEST_API", "CHEST_TOKEN", "CHEST_TOOL"].map(name => [name, process.env[name]]));
|
|
541
|
+
const saved = Object.fromEntries(["CHEST_API", "CHEST_TOKEN", "CHEST_TOOL", "CHEST_ORGANIZATION", "CHEST_TIME_ZONE", "CHEST_LANGUAGE"].map(name => [name, process.env[name]]));
|
|
395
542
|
chest.api = "http://127.0.0.1:" + (server.address() as AddressInfo).port;
|
|
396
|
-
Object.assign(process.env, { CHEST_API: chest.api, CHEST_TOKEN: token, CHEST_TOOL: tool });
|
|
543
|
+
Object.assign(process.env, { CHEST_API: chest.api, CHEST_TOKEN: token, CHEST_TOOL: tool, CHEST_ORGANIZATION: options.chest?.organization ?? "Test organization", CHEST_TIME_ZONE: options.chest?.timeZone ?? "UTC", CHEST_LANGUAGE: options.chest?.language ?? "en" });
|
|
397
544
|
forget();
|
|
398
545
|
chest.emit = async (event, to) => {
|
|
399
546
|
const id = event.id ?? "evt_" + Array.from(randomBytes(26), b => "abcdefghijklmnopqrstuvwxyz234567"[b & 31]).join("");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from "./src/errors.js";
|
|
2
2
|
export * from "./src/member.js";
|
|
3
|
+
export * from "./src/chest.js";
|
|
3
4
|
export * from "./src/database.js";
|
|
4
5
|
export * as files from "./src/files.js";
|
|
5
6
|
export * as members from "./src/members.js";
|
|
6
7
|
export * as notifications from "./src/notifications.js";
|
|
7
8
|
export * as events from "./src/events.js";
|
|
9
|
+
export * as ai from "./src/ai.js";
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAOA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAOA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// The package root: every published module a tool's code uses (tool contract
|
|
2
|
-
// v2). Each one is also its own subpath (@argentic/chest-sdk/member,
|
|
3
|
-
// /database, /files, /members, /notifications, /events, /errors), which
|
|
4
|
-
// pulls in nothing else. The files, members, notifications and
|
|
5
|
-
// are namespaces here, as their names (get, list, stat, move, notify,
|
|
6
|
-
// verify…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
|
|
2
|
+
// v2). Each one is also its own subpath (@argentic/chest-sdk/member, /chest,
|
|
3
|
+
// /database, /files, /members, /notifications, /events, /ai, /errors), which
|
|
4
|
+
// pulls in nothing else. The files, members, notifications, events and ai
|
|
5
|
+
// APIs are namespaces here, as their names (get, list, stat, move, notify,
|
|
6
|
+
// verify, chat…) are too plain to stand alone. @argentic/chest-sdk/testing is for a tool's tests only, and
|
|
7
7
|
// is not here.
|
|
8
8
|
export * from "./src/errors.js";
|
|
9
9
|
export * from "./src/member.js";
|
|
10
|
+
export * from "./src/chest.js";
|
|
10
11
|
export * from "./src/database.js";
|
|
11
12
|
export * as files from "./src/files.js";
|
|
12
13
|
export * as members from "./src/members.js";
|
|
13
14
|
export * as notifications from "./src/notifications.js";
|
|
14
15
|
export * as events from "./src/events.js";
|
|
16
|
+
export * as ai from "./src/ai.js";
|
|
15
17
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,0EAA0E;AAC1E,2EAA2E;AAC3E,2GAA2G;AAC3G,eAAe;AACf,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/src/ai.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type Alias = "default" | "fast" | "smart" | "embedding";
|
|
2
|
+
export type Provider = "openrouter";
|
|
3
|
+
export type ChatMessage = {
|
|
4
|
+
role: "system" | "developer" | "user" | "assistant" | "tool";
|
|
5
|
+
content?: string | unknown[] | null;
|
|
6
|
+
name?: string;
|
|
7
|
+
tool_calls?: {
|
|
8
|
+
id: string;
|
|
9
|
+
type: "function";
|
|
10
|
+
function: {
|
|
11
|
+
name: string;
|
|
12
|
+
arguments: string;
|
|
13
|
+
};
|
|
14
|
+
}[];
|
|
15
|
+
tool_call_id?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ChatTool = {
|
|
18
|
+
type: "function";
|
|
19
|
+
function: {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
parameters?: Record<string, unknown>;
|
|
23
|
+
strict?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type ToolChoice = "auto" | "none" | "required" | {
|
|
27
|
+
type: "function";
|
|
28
|
+
function: {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type ResponseFormat = {
|
|
33
|
+
type: "text";
|
|
34
|
+
} | {
|
|
35
|
+
type: "json_object";
|
|
36
|
+
} | {
|
|
37
|
+
type: "json_schema";
|
|
38
|
+
json_schema: {
|
|
39
|
+
name: string;
|
|
40
|
+
schema: Record<string, unknown>;
|
|
41
|
+
strict?: boolean;
|
|
42
|
+
description?: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export type ChatOptions = {
|
|
46
|
+
model: Alias;
|
|
47
|
+
messages: ChatMessage[];
|
|
48
|
+
maxTokens?: number;
|
|
49
|
+
temperature?: number;
|
|
50
|
+
topP?: number;
|
|
51
|
+
stop?: string | string[];
|
|
52
|
+
tools?: ChatTool[];
|
|
53
|
+
toolChoice?: ToolChoice;
|
|
54
|
+
responseFormat?: ResponseFormat;
|
|
55
|
+
parallelToolCalls?: boolean;
|
|
56
|
+
seed?: number;
|
|
57
|
+
reasoningEffort?: string;
|
|
58
|
+
member?: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
};
|
|
61
|
+
export type ToolCall = {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
arguments: string;
|
|
65
|
+
};
|
|
66
|
+
export type ToolCallDelta = {
|
|
67
|
+
index: number;
|
|
68
|
+
id?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
arguments?: string;
|
|
71
|
+
};
|
|
72
|
+
export type Usage = {
|
|
73
|
+
input: number;
|
|
74
|
+
output: number;
|
|
75
|
+
cached: number;
|
|
76
|
+
cost: number;
|
|
77
|
+
};
|
|
78
|
+
export type ChatResult = {
|
|
79
|
+
text: string;
|
|
80
|
+
message: ChatMessage;
|
|
81
|
+
toolCalls: ToolCall[];
|
|
82
|
+
finishReason: string;
|
|
83
|
+
model: string;
|
|
84
|
+
usage: Usage;
|
|
85
|
+
};
|
|
86
|
+
export type ChatChunk = {
|
|
87
|
+
text: string;
|
|
88
|
+
toolCalls?: ToolCallDelta[];
|
|
89
|
+
finishReason?: string;
|
|
90
|
+
usage?: Usage;
|
|
91
|
+
};
|
|
92
|
+
export type EmbedOptions = {
|
|
93
|
+
model: Alias;
|
|
94
|
+
input: string | string[];
|
|
95
|
+
dimensions?: number;
|
|
96
|
+
member?: string;
|
|
97
|
+
};
|
|
98
|
+
export type Embeddings = {
|
|
99
|
+
embeddings: number[][];
|
|
100
|
+
model: string;
|
|
101
|
+
usage: {
|
|
102
|
+
input: number;
|
|
103
|
+
cost: number;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
export type AiModel = {
|
|
107
|
+
alias: Alias;
|
|
108
|
+
model: string;
|
|
109
|
+
provider: Provider;
|
|
110
|
+
input: number;
|
|
111
|
+
output: number;
|
|
112
|
+
};
|
|
113
|
+
export type AiUsage = {
|
|
114
|
+
month: string;
|
|
115
|
+
spent: number;
|
|
116
|
+
cap: number;
|
|
117
|
+
resetsAt: Date;
|
|
118
|
+
};
|
|
119
|
+
export declare function chat(options: ChatOptions & {
|
|
120
|
+
stream: true;
|
|
121
|
+
}): AsyncIterable<ChatChunk>;
|
|
122
|
+
export declare function chat(options: ChatOptions & {
|
|
123
|
+
stream?: false;
|
|
124
|
+
}): Promise<ChatResult>;
|
|
125
|
+
export declare function chat(options: ChatOptions & {
|
|
126
|
+
stream?: boolean;
|
|
127
|
+
}): Promise<ChatResult> | AsyncIterable<ChatChunk>;
|
|
128
|
+
export declare function embed(options: EmbedOptions): Promise<Embeddings>;
|
|
129
|
+
export declare function models(): Promise<AiModel[]>;
|
|
130
|
+
export declare function usage(): Promise<AiUsage>;
|
|
131
|
+
//# sourceMappingURL=ai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../client/src/ai.ts"],"names":[],"mappings":"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"}
|