@arcote.tech/arc-chat 0.7.11 → 0.7.12
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-chat",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.12",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Chat module with AI integration for Arc framework",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@arcote.tech/arc": "^0.7.
|
|
14
|
-
"@arcote.tech/arc-ai": "^0.7.
|
|
15
|
-
"@arcote.tech/arc-ai-voice": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-auth": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
18
|
-
"@arcote.tech/platform": "^0.7.
|
|
13
|
+
"@arcote.tech/arc": "^0.7.12",
|
|
14
|
+
"@arcote.tech/arc-ai": "^0.7.12",
|
|
15
|
+
"@arcote.tech/arc-ai-voice": "^0.7.12",
|
|
16
|
+
"@arcote.tech/arc-auth": "^0.7.12",
|
|
17
|
+
"@arcote.tech/arc-ds": "^0.7.12",
|
|
18
|
+
"@arcote.tech/platform": "^0.7.12",
|
|
19
19
|
"lucide-react": ">=0.400.0",
|
|
20
20
|
"react": ">=18.0.0",
|
|
21
21
|
"typescript": "^5.0.0"
|
package/src/chat-builder.ts
CHANGED
|
@@ -55,6 +55,22 @@ export interface ChatReactComponentOptions {
|
|
|
55
55
|
|
|
56
56
|
// ─── Chat Data ──────────────────────────────────────────────────
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Map snapshot token params (decoded payload of the token that protects this
|
|
60
|
+
* chat) to the scopeId we charge for an AI call. Consumers wire this via
|
|
61
|
+
* `.billTo(fn)`. Examples for typical setups:
|
|
62
|
+
*
|
|
63
|
+
* .billTo(p => p.accountId) // per-user billing
|
|
64
|
+
* .billTo(p => p.workspaceId) // per-workspace billing
|
|
65
|
+
*
|
|
66
|
+
* Called inside the ai-generation-listener with `ctx.$auth.params` (which
|
|
67
|
+
* is the decoded payload of the token snapshotted at `messageSent` emit
|
|
68
|
+
* time — i.e. whatever token the chat's `.protectBy(...)` is configured
|
|
69
|
+
* with). The returned string becomes the `_id` of the ledger row that gets
|
|
70
|
+
* debited.
|
|
71
|
+
*/
|
|
72
|
+
export type BillToFn = (tokenParams: Record<string, any>) => string;
|
|
73
|
+
|
|
58
74
|
export interface ArcChatData {
|
|
59
75
|
name: string;
|
|
60
76
|
identifyBy: ArcId<any> | null;
|
|
@@ -67,6 +83,8 @@ export interface ArcChatData {
|
|
|
67
83
|
tools: ArcToolAny[];
|
|
68
84
|
maxExecutionCount: number;
|
|
69
85
|
toolChoice: "auto" | "required" | { type: "function"; name: string };
|
|
86
|
+
alias: string | null;
|
|
87
|
+
billTo: BillToFn | null;
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
const defaultChatData = {
|
|
@@ -81,6 +99,8 @@ const defaultChatData = {
|
|
|
81
99
|
tools: [],
|
|
82
100
|
maxExecutionCount: 10,
|
|
83
101
|
toolChoice: "auto" as const,
|
|
102
|
+
alias: null,
|
|
103
|
+
billTo: null,
|
|
84
104
|
} as const satisfies ArcChatData;
|
|
85
105
|
|
|
86
106
|
type DefaultChatData = typeof defaultChatData;
|
|
@@ -164,6 +184,41 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
164
184
|
} as any);
|
|
165
185
|
}
|
|
166
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Billing alias for this chat — written to the `usageRecorded` event
|
|
189
|
+
* payload (see `@arcote.tech/arc-ai`). Defaults to chat `name`. Override
|
|
190
|
+
* when you want consistent reporting across renames or to group multiple
|
|
191
|
+
* chats under one alias for admin SQL reports.
|
|
192
|
+
*
|
|
193
|
+
* chat("identityConsultation").alias("chat-identity")...
|
|
194
|
+
*/
|
|
195
|
+
alias<const A extends string>(alias: A) {
|
|
196
|
+
return new ArcChat<Merge<Data, { alias: A }>>({
|
|
197
|
+
...this.data,
|
|
198
|
+
alias,
|
|
199
|
+
} as any);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Decide which scopeId to bill for an AI call made from this chat.
|
|
204
|
+
*
|
|
205
|
+
* Called with the decoded params of the token snapshotted at `messageSent`
|
|
206
|
+
* emit time (i.e. the token used in `.protectBy(...)`). The returned string
|
|
207
|
+
* becomes the `_id` of the row in `creditLedger` that gets debited.
|
|
208
|
+
*
|
|
209
|
+
* .billTo(p => p.accountId) // per-user billing
|
|
210
|
+
* .billTo(p => p.workspaceId) // per-workspace billing
|
|
211
|
+
*
|
|
212
|
+
* Required when `.ai(...)` config has billing wired — `build()` throws
|
|
213
|
+
* otherwise. Without billing, this method is a no-op.
|
|
214
|
+
*/
|
|
215
|
+
billTo(fn: BillToFn) {
|
|
216
|
+
return new ArcChat<Merge<Data, { billTo: BillToFn }>>({
|
|
217
|
+
...this.data,
|
|
218
|
+
billTo: fn,
|
|
219
|
+
} as any);
|
|
220
|
+
}
|
|
221
|
+
|
|
167
222
|
createTool<const N extends string>(name: N) {
|
|
168
223
|
type IdType = Data["identifyBy"] extends ArcId<any>
|
|
169
224
|
? $type<Data["identifyBy"]>
|
|
@@ -195,6 +250,8 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
195
250
|
tools,
|
|
196
251
|
maxExecutionCount,
|
|
197
252
|
toolChoice,
|
|
253
|
+
alias: aliasOverride,
|
|
254
|
+
billTo,
|
|
198
255
|
} = this.data;
|
|
199
256
|
|
|
200
257
|
if (!name) throw new Error("ArcChat: name is required");
|
|
@@ -202,6 +259,13 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
202
259
|
if (!accountId) throw new Error("ArcChat: accountId is required");
|
|
203
260
|
if (!userToken) throw new Error("ArcChat: userToken is required");
|
|
204
261
|
if (!aiConfig) throw new Error("ArcChat: ai is required");
|
|
262
|
+
// Billing wired but no `.billTo(...)` would silently skip recordUsage —
|
|
263
|
+
// forbid that. Consumer must explicitly decide which scopeId to charge.
|
|
264
|
+
if (aiConfig.recordUsage && !billTo) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`ArcChat "${name}": ai() factory has billing wired but chat is missing .billTo(...) — declare how to map snapshot token params to a billing scope, e.g. .billTo(p => p.accountId).`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
205
269
|
|
|
206
270
|
const messageId = createMessageId({ name });
|
|
207
271
|
|
|
@@ -244,13 +308,14 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
244
308
|
const serverTools = tools.filter((t) => t.isServerTool);
|
|
245
309
|
const interactiveTools = tools.filter((t) => t.isInteractiveTool);
|
|
246
310
|
|
|
247
|
-
// Add
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
311
|
+
// Add usage-registry aggregate to listener mutate deps so `recordUsage`
|
|
312
|
+
// (which calls `ctx.mutate(registry).recordUsage(...)`) compiles and runs.
|
|
313
|
+
// Ledger view is a read-only projection — consumer queries it from React,
|
|
314
|
+
// listener never writes to it directly.
|
|
315
|
+
if (aiConfig.usageRegistry) {
|
|
316
|
+
for (const el of aiConfig.usageRegistry.elements) {
|
|
251
317
|
if (!allMutationElements.includes(el)) allMutationElements.push(el);
|
|
252
318
|
if (!allQueryElements.includes(el)) allQueryElements.push(el);
|
|
253
|
-
billingElements.push(el);
|
|
254
319
|
}
|
|
255
320
|
}
|
|
256
321
|
|
|
@@ -265,6 +330,9 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
265
330
|
allMutationElements,
|
|
266
331
|
maxExecutionCount,
|
|
267
332
|
toolChoice: toolChoice !== "auto" ? toolChoice : undefined,
|
|
333
|
+
alias: aliasOverride ?? name,
|
|
334
|
+
recordUsage: aiConfig.recordUsage,
|
|
335
|
+
billTo: billTo ?? undefined,
|
|
268
336
|
};
|
|
269
337
|
|
|
270
338
|
const aiListener = createAiGenerationListener(listenerConfig);
|
|
@@ -28,6 +28,37 @@ export interface AiGenerationListenerConfig {
|
|
|
28
28
|
allMutationElements: ArcContextElement<any>[];
|
|
29
29
|
maxExecutionCount: number;
|
|
30
30
|
toolChoice?: "auto" | "required" | { type: "function"; name: string };
|
|
31
|
+
/**
|
|
32
|
+
* Billing alias for this chat — written to the `usageRecorded` event
|
|
33
|
+
* payload so admin reports can attribute cost back to which chat (e.g.
|
|
34
|
+
* `"chat-identity"`, `"chat-create-content"`). Defaults to `name` if the
|
|
35
|
+
* builder didn't override via `.alias(...)`.
|
|
36
|
+
*/
|
|
37
|
+
alias?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Optional billing hook from `ai()` factory. Called after every
|
|
40
|
+
* `completeAssistantTurn` (each turn, including those that close with
|
|
41
|
+
* tool calls) so the credit ledger view sees consistent usage events.
|
|
42
|
+
* No-op when undefined (ai() built without `billing` config).
|
|
43
|
+
*/
|
|
44
|
+
recordUsage?: (
|
|
45
|
+
ctx: any,
|
|
46
|
+
params: {
|
|
47
|
+
scopeId: string;
|
|
48
|
+
alias: string;
|
|
49
|
+
model: string;
|
|
50
|
+
usage: import("@arcote.tech/arc-ai").TokenUsage;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
},
|
|
53
|
+
) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Consumer-supplied function from chat-builder's `.billTo(...)` — maps
|
|
56
|
+
* decoded params of the chat's protection token (snapshotted at
|
|
57
|
+
* `messageSent` emit time) to the ledger scopeId we charge. Required when
|
|
58
|
+
* `recordUsage` is set (chat-builder enforces this at build time), so the
|
|
59
|
+
* listener can treat the pair as always-present in the call site.
|
|
60
|
+
*/
|
|
61
|
+
billTo?: (tokenParams: Record<string, any>) => string;
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
// ─── History reconstruction ─────────────────────────────────────
|
|
@@ -195,6 +226,13 @@ interface RunLoopConfig {
|
|
|
195
226
|
* lecieć. Następne iteracje (multi-turn po server tool exec) tworzą fresh
|
|
196
227
|
* rows. */
|
|
197
228
|
preCreatedAssistantMessageId?: string;
|
|
229
|
+
/** Billing alias — written to `usageRecorded` event payload. Optional;
|
|
230
|
+
* no-op when paired `recordUsage` is undefined. */
|
|
231
|
+
alias?: string;
|
|
232
|
+
/** Billing hook — see `AiGenerationListenerConfig.recordUsage`. */
|
|
233
|
+
recordUsage?: AiGenerationListenerConfig["recordUsage"];
|
|
234
|
+
/** Token-params → scopeId mapper from chat-builder `.billTo(...)`. */
|
|
235
|
+
billTo?: AiGenerationListenerConfig["billTo"];
|
|
198
236
|
}
|
|
199
237
|
|
|
200
238
|
async function runGenerationLoop(config: RunLoopConfig) {
|
|
@@ -339,15 +377,46 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
339
377
|
|
|
340
378
|
// Close the turn row — same row that was opened above. Final blocks
|
|
341
379
|
// are the SINGLE persistent write of message content in this turn.
|
|
342
|
-
//
|
|
343
|
-
//
|
|
380
|
+
// Usage zapisywane ZAWSZE (intermediate tool turns też zużywają tokeny
|
|
381
|
+
// i muszą być rozliczone w `recordUsage` poniżej; klient dostaje pełen
|
|
382
|
+
// history of cost per turn from message rows).
|
|
344
383
|
await ctx.mutate(messageElement).completeAssistantTurn({
|
|
345
384
|
messageId: currentTurnId!,
|
|
346
385
|
blocks: JSON.stringify(result.blocks),
|
|
347
386
|
previousResponseId: result.responseId,
|
|
348
|
-
usage:
|
|
387
|
+
usage: JSON.stringify(result.usage),
|
|
349
388
|
});
|
|
350
389
|
|
|
390
|
+
// Billing hook — emit usageRecorded event for the credit ledger view.
|
|
391
|
+
// Called after `completeAssistantTurn` so the message row exists in DB
|
|
392
|
+
// before its cost is attributed.
|
|
393
|
+
//
|
|
394
|
+
// chat-builder enforces `.billTo()` is present whenever `recordUsage`
|
|
395
|
+
// is wired, so the only legitimate "skip" path is "billing not
|
|
396
|
+
// configured at all" — both undefined. We still guard defensively.
|
|
397
|
+
if (config.recordUsage && config.alias && config.billTo) {
|
|
398
|
+
const billingScopeId = config.billTo(
|
|
399
|
+
((ctx as any).$auth?.params as Record<string, any>) ?? {},
|
|
400
|
+
);
|
|
401
|
+
try {
|
|
402
|
+
await config.recordUsage(ctx, {
|
|
403
|
+
scopeId: billingScopeId,
|
|
404
|
+
alias: config.alias,
|
|
405
|
+
model,
|
|
406
|
+
usage: result.usage,
|
|
407
|
+
metadata: {
|
|
408
|
+
messageId: currentTurnId!,
|
|
409
|
+
sessionId,
|
|
410
|
+
turnIndex: executionCount,
|
|
411
|
+
chatScopeId: scopeId,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
} catch (err) {
|
|
415
|
+
// Best-effort: billing failure shouldn't break generation.
|
|
416
|
+
console.error("[arc-chat] recordUsage failed:", err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
351
420
|
// Tear down the in-memory stream: broadcast `done` do subscriberów,
|
|
352
421
|
// close controllery, drop registry entry po grace window. Klient z
|
|
353
422
|
// `done` flippuje isStreaming=false i renderuje final blocks z DB.
|
|
@@ -516,6 +585,9 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
516
585
|
maxExecutionCount,
|
|
517
586
|
toolChoice: config.toolChoice,
|
|
518
587
|
instruction,
|
|
588
|
+
alias: config.alias,
|
|
589
|
+
recordUsage: config.recordUsage,
|
|
590
|
+
billTo: config.billTo,
|
|
519
591
|
preCreatedAssistantMessageId: (
|
|
520
592
|
event.payload as { assistantMessageId?: string }
|
|
521
593
|
).assistantMessageId,
|
|
@@ -596,6 +668,9 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
596
668
|
maxExecutionCount,
|
|
597
669
|
toolChoice: config.toolChoice,
|
|
598
670
|
instruction,
|
|
671
|
+
alias: config.alias,
|
|
672
|
+
recordUsage: config.recordUsage,
|
|
673
|
+
billTo: config.billTo,
|
|
599
674
|
preCreatedAssistantMessageId: (
|
|
600
675
|
event.payload as { assistantMessageId?: string }
|
|
601
676
|
).assistantMessageId,
|
|
@@ -688,6 +763,9 @@ export function createAiRetryListener(config: AiGenerationListenerConfig) {
|
|
|
688
763
|
maxExecutionCount,
|
|
689
764
|
toolChoice: config.toolChoice,
|
|
690
765
|
instruction,
|
|
766
|
+
alias: config.alias,
|
|
767
|
+
recordUsage: config.recordUsage,
|
|
768
|
+
billTo: config.billTo,
|
|
691
769
|
preCreatedAssistantMessageId: assistantMsgId,
|
|
692
770
|
});
|
|
693
771
|
});
|