@arcote.tech/arc-ai 0.5.1 → 0.5.5

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-ai",
3
3
  "type": "module",
4
- "version": "0.5.1",
4
+ "version": "0.5.5",
5
5
  "private": false,
6
6
  "description": "AI provider abstraction, completion tracking, and budget management for Arc framework",
7
7
  "main": "./src/index.ts",
@@ -10,8 +10,8 @@
10
10
  "type-check": "tsc --noEmit"
11
11
  },
12
12
  "peerDependencies": {
13
- "@arcote.tech/arc": "^0.5.1",
14
- "@arcote.tech/arc-auth": "^0.5.1",
13
+ "@arcote.tech/arc": "^0.5.5",
14
+ "@arcote.tech/arc-auth": "^0.5.5",
15
15
  "typescript": "^5.0.0"
16
16
  },
17
17
  "devDependencies": {
@@ -0,0 +1,178 @@
1
+ /// <reference path="../arc.d.ts" />
2
+ import {
3
+ aggregate,
4
+ context,
5
+ id,
6
+ number,
7
+ string,
8
+ type ArcId,
9
+ } from "@arcote.tech/arc";
10
+ import type { Token } from "@arcote.tech/arc-auth";
11
+
12
+ // ─── ID ──────────────────────────────────────────────────────────
13
+
14
+ export const createLedgerId = <const Name extends string>(data: {
15
+ name: Name;
16
+ }) => id(`${data.name}Ledger`);
17
+
18
+ // ─── Factory ────────────────────────────────────────────────────
19
+
20
+ export function creditLedger(config: {
21
+ name: string;
22
+ scopeId: ArcId<any>;
23
+ accountId: ArcId<any>;
24
+ protectBy: Token;
25
+ }) {
26
+ const { name, scopeId, protectBy } = config;
27
+ const ledgerId = createLedgerId({ name });
28
+
29
+ const Ledger = aggregate(`${name}Ledger`, ledgerId, {
30
+ scopeId,
31
+ balance: number(),
32
+ totalCredited: number(),
33
+ totalDebited: number(),
34
+ })
35
+
36
+ // ─── credited — top up balance ──────────────────────────────
37
+ .publicEvent(
38
+ "credited",
39
+ {
40
+ ledgerId,
41
+ scopeId,
42
+ amount: number(),
43
+ reason: string(),
44
+ metadata: string().optional(),
45
+ },
46
+ async (ctx, event) => {
47
+ const p = event.payload;
48
+ const existing = await ctx.findOne({ scopeId: p.scopeId });
49
+
50
+ if (existing) {
51
+ await ctx.set(existing._id, {
52
+ ...existing,
53
+ balance: (existing.balance ?? 0) + p.amount,
54
+ totalCredited: (existing.totalCredited ?? 0) + p.amount,
55
+ });
56
+ } else {
57
+ await ctx.set(p.ledgerId, {
58
+ scopeId: p.scopeId,
59
+ balance: p.amount,
60
+ totalCredited: p.amount,
61
+ totalDebited: 0,
62
+ });
63
+ }
64
+ },
65
+ )
66
+
67
+ // ─── debited — deduct from balance ──────────────────────────
68
+ .publicEvent(
69
+ "debited",
70
+ {
71
+ ledgerId,
72
+ scopeId,
73
+ amount: number(),
74
+ reason: string(),
75
+ model: string().optional(),
76
+ metadata: string().optional(),
77
+ },
78
+ async (ctx, event) => {
79
+ const p = event.payload;
80
+ const existing = await ctx.findOne({ scopeId: p.scopeId });
81
+
82
+ if (existing) {
83
+ await ctx.set(existing._id, {
84
+ ...existing,
85
+ balance: (existing.balance ?? 0) - p.amount,
86
+ totalDebited: (existing.totalDebited ?? 0) + p.amount,
87
+ });
88
+ } else {
89
+ await ctx.set(p.ledgerId, {
90
+ scopeId: p.scopeId,
91
+ balance: -p.amount,
92
+ totalCredited: 0,
93
+ totalDebited: p.amount,
94
+ });
95
+ }
96
+ },
97
+ )
98
+
99
+ // ─── topUp — add credits ────────────────────────────────────
100
+ .mutateMethod(
101
+ "topUp",
102
+ (fn) => fn.withParams({
103
+ scopeId,
104
+ amount: number(),
105
+ reason: string(),
106
+ metadata: string().optional(),
107
+ }).handle(
108
+ ONLY_SERVER &&
109
+ (async (ctx, params) => {
110
+ const entryId = ledgerId.generate();
111
+ await ctx.credited.emit({
112
+ ledgerId: entryId,
113
+ scopeId: params.scopeId,
114
+ amount: params.amount,
115
+ reason: params.reason,
116
+ metadata: params.metadata,
117
+ });
118
+ return { ok: true };
119
+ }),
120
+ ),
121
+ )
122
+
123
+ // ─── deduct — remove credits ────────────────────────────────
124
+ .mutateMethod(
125
+ "deduct",
126
+ (fn) => fn.withParams({
127
+ scopeId,
128
+ amount: number(),
129
+ reason: string(),
130
+ model: string().optional(),
131
+ metadata: string().optional(),
132
+ }).handle(
133
+ ONLY_SERVER &&
134
+ (async (ctx, params) => {
135
+ const entryId = ledgerId.generate();
136
+ await ctx.debited.emit({
137
+ ledgerId: entryId,
138
+ scopeId: params.scopeId,
139
+ amount: params.amount,
140
+ reason: params.reason,
141
+ model: params.model,
142
+ metadata: params.metadata,
143
+ });
144
+ return { ok: true };
145
+ }),
146
+ ),
147
+ )
148
+
149
+ // ─── checkBalance — query current balance ───────────────────
150
+ .clientQuery(
151
+ "checkBalance",
152
+ (fn) => fn
153
+ .withParams({ scopeId: string() })
154
+ .handle(async (ctx, params) => {
155
+ const entry = await ctx.$query.findOne({ scopeId: params.scopeId });
156
+ const balance = entry?.balance ?? 0;
157
+ return {
158
+ balance,
159
+ canUse: balance > 0,
160
+ totalCredited: entry?.totalCredited ?? 0,
161
+ totalDebited: entry?.totalDebited ?? 0,
162
+ };
163
+ }),
164
+ )
165
+
166
+ .protectBy(protectBy, (p: any) => ({ scopeId: p.workspaceId ?? p.accountId }));
167
+
168
+ const elements = [Ledger];
169
+
170
+ return {
171
+ context: context(elements),
172
+ elements,
173
+ ledgerId,
174
+ Ledger,
175
+ };
176
+ }
177
+
178
+ export type CreditLedgerConfig = ReturnType<typeof creditLedger>;
package/src/ai-builder.ts CHANGED
@@ -1,54 +1,37 @@
1
1
  import { context, type ArcContextElement } from "@arcote.tech/arc";
2
2
  import type { AccountId, Token } from "@arcote.tech/arc-auth";
3
- import {
4
- createCompletionId,
5
- createCompletionAggregate,
6
- } from "./aggregates/completion";
7
- import {
8
- createBudgetId,
9
- createBudgetAggregate,
10
- } from "./aggregates/budget";
11
- import type { LLMProvider, PricingConfig } from "./types";
3
+ import type { CreditLedgerConfig } from "./aggregates/credit-ledger";
4
+ import type { BillingStrategy } from "./billing/types";
5
+ import { wrapProviderWithBilling } from "./billing/wrap-provider";
6
+ import type { LLMProvider } from "./types";
12
7
 
13
8
  export function ai(config: {
14
9
  name: string;
15
10
  accountId: AccountId;
16
11
  userToken: Token;
17
12
  providers: LLMProvider[];
18
- pricing?: Record<string, PricingConfig>;
13
+ billing?: {
14
+ ledger: CreditLedgerConfig;
15
+ strategy: BillingStrategy;
16
+ };
19
17
  }) {
20
- const completionId = createCompletionId({ name: config.name });
21
- const budgetId = createBudgetId({ name: config.name });
22
-
23
- const Completion = createCompletionAggregate({
24
- name: config.name,
25
- completionId,
26
- accountId: config.accountId,
27
- userToken: config.userToken,
28
- providers: config.providers,
29
- pricing: config.pricing,
30
- });
31
-
32
- const Budget = createBudgetAggregate({
33
- name: config.name,
34
- budgetId,
35
- accountId: config.accountId,
36
- userToken: config.userToken,
37
- CompletionAggregate: Completion,
38
- });
18
+ const elements: ArcContextElement<any>[] = [];
39
19
 
40
- const elements: ArcContextElement<any>[] = [Completion, Budget];
20
+ if (config.billing) {
21
+ elements.push(...config.billing.ledger.elements);
22
+ }
41
23
 
42
24
  return {
43
25
  context: context(elements),
44
26
  elements,
45
- completionId,
46
- budgetId,
47
- Completion,
48
- Budget,
49
27
  providers: config.providers,
50
- resolveProvider: (model: string) =>
51
- config.providers.find((p) => p.models.includes(model)),
28
+ billing: config.billing,
29
+ resolveProvider: (model: string, scopeId?: string): LLMProvider | undefined => {
30
+ const provider = config.providers.find((p) => p.models.includes(model));
31
+ if (!provider) return undefined;
32
+ if (!config.billing || !scopeId) return provider;
33
+ return wrapProviderWithBilling(provider, config.billing, scopeId);
34
+ },
52
35
  };
53
36
  }
54
37
 
@@ -0,0 +1,5 @@
1
+ export type { BillingStrategy, AdapterPricing } from "./types";
2
+ export { fixedMarkupStrategy } from "./strategies/fixed-markup";
3
+ export { flatRateStrategy } from "./strategies/flat-rate";
4
+ export { tokenPassStrategy } from "./strategies/token-pass";
5
+ export { wrapProviderWithBilling } from "./wrap-provider";
@@ -0,0 +1,32 @@
1
+ import type { BillingStrategy, AdapterPricing } from "../types";
2
+ import type { TokenUsage } from "../../types";
3
+
4
+ export function fixedMarkupStrategy(config: {
5
+ multiplier: number;
6
+ }): BillingStrategy {
7
+ const { multiplier } = config;
8
+
9
+ return {
10
+ name: "fixedMarkup",
11
+
12
+ calculateCost({ usage, adapterPricing }) {
13
+ if (!adapterPricing) return 0;
14
+
15
+ const inputCost = (usage.inputTokens / 1_000_000) * adapterPricing.inputPer1M;
16
+ const outputCost = (usage.outputTokens / 1_000_000) * adapterPricing.outputPer1M;
17
+ const cachedCost = adapterPricing.cachedInputPer1M
18
+ ? (usage.cachedTokens / 1_000_000) * adapterPricing.cachedInputPer1M
19
+ : 0;
20
+ const reasoningCost = adapterPricing.reasoningPer1M
21
+ ? (usage.reasoningTokens / 1_000_000) * adapterPricing.reasoningPer1M
22
+ : 0;
23
+
24
+ const baseCost = inputCost + outputCost + cachedCost + reasoningCost;
25
+ return Math.round(baseCost * multiplier * 100) / 100;
26
+ },
27
+
28
+ canExecute(balance) {
29
+ return balance > 0;
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,19 @@
1
+ import type { BillingStrategy } from "../types";
2
+
3
+ export function flatRateStrategy(config: {
4
+ creditsPerRequest: number;
5
+ }): BillingStrategy {
6
+ const { creditsPerRequest } = config;
7
+
8
+ return {
9
+ name: "flatRate",
10
+
11
+ calculateCost() {
12
+ return creditsPerRequest;
13
+ },
14
+
15
+ canExecute(balance) {
16
+ return balance > 0;
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,3 @@
1
+ export { fixedMarkupStrategy } from "./fixed-markup";
2
+ export { flatRateStrategy } from "./flat-rate";
3
+ export { tokenPassStrategy } from "./token-pass";
@@ -0,0 +1,15 @@
1
+ import type { BillingStrategy } from "../types";
2
+
3
+ export function tokenPassStrategy(): BillingStrategy {
4
+ return {
5
+ name: "tokenPass",
6
+
7
+ calculateCost({ usage }) {
8
+ return usage.totalTokens;
9
+ },
10
+
11
+ canExecute(balance) {
12
+ return balance > 0;
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,24 @@
1
+ import type { TokenUsage } from "../types";
2
+
3
+ // ─── Adapter Pricing ────────────────────────────────────────────
4
+
5
+ export interface AdapterPricing {
6
+ inputPer1M: number;
7
+ outputPer1M: number;
8
+ cachedInputPer1M?: number;
9
+ reasoningPer1M?: number;
10
+ }
11
+
12
+ // ─── Billing Strategy ───────────────────────────────────────────
13
+
14
+ export interface BillingStrategy {
15
+ readonly name: string;
16
+
17
+ calculateCost(params: {
18
+ model: string;
19
+ usage: TokenUsage;
20
+ adapterPricing?: AdapterPricing;
21
+ }): number;
22
+
23
+ canExecute(balance: number): boolean;
24
+ }
@@ -0,0 +1,52 @@
1
+ import type { LLMProvider, CompletionRequest, CompletionResult, StreamChunk } from "../types";
2
+ import type { CreditLedgerConfig } from "../aggregates/credit-ledger";
3
+ import type { BillingStrategy } from "./types";
4
+
5
+ export function wrapProviderWithBilling(
6
+ provider: LLMProvider,
7
+ billing: { ledger: CreditLedgerConfig; strategy: BillingStrategy },
8
+ scopeId: string,
9
+ ): LLMProvider {
10
+ const { ledger, strategy } = billing;
11
+
12
+ async function checkBalanceOrThrow() {
13
+ const result = await (ledger.Ledger as any).checkBalance({ scopeId });
14
+ if (!strategy.canExecute(result?.balance ?? 0)) {
15
+ throw new Error("Insufficient credits");
16
+ }
17
+ }
18
+
19
+ async function deductUsage(model: string, usage: any) {
20
+ const adapterPricing = provider.getPricing?.(model);
21
+ const cost = strategy.calculateCost({ model, usage, adapterPricing });
22
+ if (cost > 0) {
23
+ await (ledger.Ledger as any).deduct({
24
+ scopeId,
25
+ amount: cost,
26
+ reason: `AI: ${model}`,
27
+ model,
28
+ });
29
+ }
30
+ }
31
+
32
+ return {
33
+ ...provider,
34
+
35
+ async complete(request: CompletionRequest): Promise<CompletionResult> {
36
+ await checkBalanceOrThrow();
37
+ const result = await provider.complete(request);
38
+ await deductUsage(request.model, result.usage);
39
+ return result;
40
+ },
41
+
42
+ async streamComplete(
43
+ request: CompletionRequest,
44
+ onChunk: (chunk: StreamChunk) => void,
45
+ ): Promise<CompletionResult> {
46
+ await checkBalanceOrThrow();
47
+ const result = await provider.streamComplete(request, onChunk);
48
+ await deductUsage(request.model, result.usage);
49
+ return result;
50
+ },
51
+ };
52
+ }
package/src/index.ts CHANGED
@@ -4,13 +4,20 @@ export type { AIConfig } from "./ai-builder";
4
4
 
5
5
  // --- Tool helper ---
6
6
  export { tool, ArcTool } from "./tool/tool";
7
- export type { ArcToolAny, JsonSchemaToolDef, ToolContext } from "./tool/tool";
7
+ export type {
8
+ ArcToolAny,
9
+ JsonSchemaToolDef,
10
+ ServerToolViewProps,
11
+ InteractiveToolViewProps,
12
+ } from "./tool/tool";
13
+
14
+ // --- Credit Ledger ---
15
+ export { creditLedger } from "./aggregates/credit-ledger";
16
+ export type { CreditLedgerConfig } from "./aggregates/credit-ledger";
8
17
 
9
- // --- Aggregate factories & types ---
10
- export { createCompletionAggregate, createCompletionId } from "./aggregates/completion";
11
- export type { CompletionAggregate, CompletionId } from "./aggregates/completion";
12
- export { createBudgetAggregate, createBudgetId } from "./aggregates/budget";
13
- export type { BudgetAggregate, BudgetId } from "./aggregates/budget";
18
+ // --- Billing strategies ---
19
+ export { fixedMarkupStrategy, flatRateStrategy, tokenPassStrategy } from "./billing";
20
+ export type { BillingStrategy, AdapterPricing } from "./billing";
14
21
 
15
22
  // --- Provider types ---
16
23
  export type {
@@ -18,15 +25,20 @@ export type {
18
25
  ProviderName,
19
26
  CompletionRequest,
20
27
  CompletionResult,
28
+ Conversation,
29
+ ConversationTurn,
30
+ UserTurn,
31
+ AssistantTurn,
32
+ ToolResultTurn,
33
+ AssistantContentBlock,
34
+ TextBlock,
35
+ ToolCallBlock,
21
36
  StreamChunk,
22
37
  StreamEventType,
23
38
  ChatStreamEvent,
24
39
  ChatStreamEventType,
25
- Message,
26
- MessageRole,
27
40
  ToolCall,
28
41
  ToolResult,
29
42
  TokenUsage,
30
43
  FinishReason,
31
- PricingConfig,
32
44
  } from "./types";