@arcote.tech/arc-ai 0.5.1 → 0.5.2
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 +3 -3
- package/src/aggregates/credit-ledger.ts +178 -0
- package/src/ai-builder.ts +19 -36
- package/src/billing/index.ts +5 -0
- package/src/billing/strategies/fixed-markup.ts +32 -0
- package/src/billing/strategies/flat-rate.ts +19 -0
- package/src/billing/strategies/index.ts +3 -0
- package/src/billing/strategies/token-pass.ts +15 -0
- package/src/billing/types.ts +24 -0
- package/src/billing/wrap-provider.ts +52 -0
- package/src/index.ts +13 -7
- package/src/tool/tool.ts +139 -53
- package/src/types.ts +3 -9
- package/src/aggregates/budget.ts +0 -137
- package/src/aggregates/completion.ts +0 -235
- package/src/routes/stream-completion.ts +0 -94
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.
|
|
4
|
+
"version": "0.5.2",
|
|
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.
|
|
14
|
-
"@arcote.tech/arc-auth": "^0.5.
|
|
13
|
+
"@arcote.tech/arc": "^0.5.2",
|
|
14
|
+
"@arcote.tech/arc-auth": "^0.5.2",
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
} from "./
|
|
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
|
-
|
|
13
|
+
billing?: {
|
|
14
|
+
ledger: CreditLedgerConfig;
|
|
15
|
+
strategy: BillingStrategy;
|
|
16
|
+
};
|
|
19
17
|
}) {
|
|
20
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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,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 {
|
|
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
|
-
// ---
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
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 {
|
|
@@ -28,5 +35,4 @@ export type {
|
|
|
28
35
|
ToolResult,
|
|
29
36
|
TokenUsage,
|
|
30
37
|
FinishReason,
|
|
31
|
-
PricingConfig,
|
|
32
38
|
} from "./types";
|
package/src/tool/tool.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
arcFunctionWithCtx,
|
|
3
|
+
type ArcFunction,
|
|
4
|
+
type ArcId,
|
|
5
5
|
type ArcRawShape,
|
|
6
|
-
type $type,
|
|
7
6
|
type ArcContextElement,
|
|
8
7
|
type ArcFunctionData,
|
|
8
|
+
type ArcObjectAny,
|
|
9
|
+
type FnContext,
|
|
10
|
+
type Merge,
|
|
11
|
+
type $type,
|
|
12
|
+
type DefaultFunctionData,
|
|
13
|
+
ArcObject,
|
|
9
14
|
} from "@arcote.tech/arc";
|
|
15
|
+
import type { ComponentType } from "react";
|
|
10
16
|
|
|
11
17
|
// ─── JSON Schema Tool Definition (sent to LLM) ──────────────────
|
|
12
18
|
|
|
@@ -16,68 +22,153 @@ export interface JsonSchemaToolDef {
|
|
|
16
22
|
parameters: Record<string, unknown>;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
// ───
|
|
25
|
+
// ─── View Props ─────────────────────────────────────────────────
|
|
20
26
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
export type ServerToolViewProps<P, R> = {
|
|
28
|
+
params: P;
|
|
29
|
+
result: R | undefined;
|
|
30
|
+
calling: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type InteractiveToolViewProps<P, R> = {
|
|
35
|
+
params: P;
|
|
36
|
+
respond: (result: R) => void;
|
|
37
|
+
calling: boolean;
|
|
38
|
+
};
|
|
26
39
|
|
|
27
40
|
// ─── ArcTool ─────────────────────────────────────────────────────
|
|
28
41
|
|
|
29
42
|
export class ArcTool<
|
|
30
43
|
const Name extends string = string,
|
|
31
|
-
|
|
44
|
+
Data extends ArcFunctionData = DefaultFunctionData,
|
|
45
|
+
IdentifyBy = string,
|
|
32
46
|
> {
|
|
33
47
|
readonly name: Name;
|
|
34
|
-
#fn: ArcFunction<
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
48
|
+
readonly #fn: ArcFunction<Data, { identifyBy: IdentifyBy }>;
|
|
49
|
+
readonly #view?: ComponentType<any>;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
name: Name,
|
|
53
|
+
fn?: ArcFunction<any, any>,
|
|
54
|
+
view?: ComponentType<any>,
|
|
55
|
+
) {
|
|
38
56
|
this.name = name;
|
|
39
|
-
this.#fn = fn ??
|
|
57
|
+
this.#fn = fn ?? (arcFunctionWithCtx<{ identifyBy: IdentifyBy }>() as any);
|
|
58
|
+
this.#view = view;
|
|
40
59
|
}
|
|
41
60
|
|
|
61
|
+
// ─── Builder methods ──────────────────────────────────────────
|
|
62
|
+
|
|
42
63
|
description<const D extends string>(desc: D) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
const nextFn = this.#fn.description(desc);
|
|
65
|
+
return new ArcTool<Name, Merge<Data, { description: D }>, IdentifyBy>(
|
|
66
|
+
this.name, nextFn as any, this.#view,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
withParams<const S extends ArcRawShape>(shape: S) {
|
|
71
|
+
const nextFn = this.#fn.withParams(shape);
|
|
72
|
+
return new ArcTool<Name, Merge<Data, { params: ArcObject<S> }>, IdentifyBy>(
|
|
73
|
+
this.name, nextFn as any, this.#view,
|
|
74
|
+
);
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
return
|
|
77
|
+
withResult<const S extends ArcRawShape>(shape: S) {
|
|
78
|
+
const nextFn = this.#fn.withResult(shape);
|
|
79
|
+
return new ArcTool<Name, Merge<Data, { result: ArcObject<S> }>, IdentifyBy>(
|
|
80
|
+
this.name, nextFn as any, this.#view,
|
|
81
|
+
);
|
|
51
82
|
}
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
query<const Elements extends ArcContextElement<any>[]>(elements: Elements) {
|
|
85
|
+
const nextFn = this.#fn.query(elements);
|
|
86
|
+
return new ArcTool<Name, Merge<Data, { queryElements: Elements }>, IdentifyBy>(
|
|
87
|
+
this.name, nextFn as any, this.#view,
|
|
88
|
+
);
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
mutate<const Elements extends ArcContextElement<any>[]>(elements: Elements) {
|
|
92
|
+
const nextFn = this.#fn.mutate(elements);
|
|
93
|
+
return new ArcTool<Name, Merge<Data, { mutationElements: Elements }>, IdentifyBy>(
|
|
94
|
+
this.name, nextFn as any, this.#view,
|
|
95
|
+
);
|
|
63
96
|
}
|
|
64
97
|
|
|
98
|
+
identifyBy<Id extends ArcId<any>>(_id: Id) {
|
|
99
|
+
return new ArcTool<Name, Data, $type<Id>>(
|
|
100
|
+
this.name, this.#fn as any, this.#view,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handle<
|
|
105
|
+
Handler extends
|
|
106
|
+
| ((
|
|
107
|
+
ctx: FnContext<Data, { identifyBy: IdentifyBy }>,
|
|
108
|
+
...args: Data["params"] extends ArcObjectAny
|
|
109
|
+
? [$type<Data["params"]>]
|
|
110
|
+
: []
|
|
111
|
+
) => Promise<any>)
|
|
112
|
+
| false,
|
|
113
|
+
>(handler: Handler) {
|
|
114
|
+
const nextFn = this.#fn.handle(handler as any);
|
|
115
|
+
return new ArcTool<Name, Merge<Data, { handler: Handler }>, IdentifyBy>(
|
|
116
|
+
this.name, nextFn as any, this.#view,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
view(
|
|
121
|
+
component: ComponentType<
|
|
122
|
+
Data["handler"] extends Function
|
|
123
|
+
? ServerToolViewProps<
|
|
124
|
+
Data["params"] extends ArcObjectAny ? $type<Data["params"]> : {},
|
|
125
|
+
Data["result"] extends ArcObjectAny
|
|
126
|
+
? $type<Data["result"]>
|
|
127
|
+
: unknown
|
|
128
|
+
>
|
|
129
|
+
: InteractiveToolViewProps<
|
|
130
|
+
Data["params"] extends ArcObjectAny ? $type<Data["params"]> : {},
|
|
131
|
+
Data["result"] extends ArcObjectAny
|
|
132
|
+
? $type<Data["result"]>
|
|
133
|
+
: unknown
|
|
134
|
+
>
|
|
135
|
+
>,
|
|
136
|
+
) {
|
|
137
|
+
return new ArcTool<Name, Data, IdentifyBy>(this.name, this.#fn as any, component);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Getters ──────────────────────────────────────────────────
|
|
141
|
+
|
|
65
142
|
get hasHandler(): boolean {
|
|
66
|
-
return !!this.#handler;
|
|
143
|
+
return !!this.#fn.handler;
|
|
67
144
|
}
|
|
68
145
|
|
|
69
146
|
get isServerTool(): boolean {
|
|
70
147
|
return this.hasHandler;
|
|
71
148
|
}
|
|
72
149
|
|
|
73
|
-
get
|
|
74
|
-
return !this.hasHandler;
|
|
150
|
+
get isInteractiveTool(): boolean {
|
|
151
|
+
return !this.hasHandler && !!this.#view;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get hasView(): boolean {
|
|
155
|
+
return !!this.#view;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get viewComponent(): ComponentType<any> | undefined {
|
|
159
|
+
return this.#view;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get queryElements(): ArcContextElement<any>[] {
|
|
163
|
+
return this.#fn.data.queryElements || [];
|
|
75
164
|
}
|
|
76
165
|
|
|
77
166
|
get mutationElements(): ArcContextElement<any>[] {
|
|
78
167
|
return this.#fn.data.mutationElements || [];
|
|
79
168
|
}
|
|
80
169
|
|
|
170
|
+
// ─── JSON Schema (for LLM) ───────────────────────────────────
|
|
171
|
+
|
|
81
172
|
toJsonSchema(): JsonSchemaToolDef {
|
|
82
173
|
const fnSchema = this.#fn.toJsonSchema();
|
|
83
174
|
return {
|
|
@@ -87,28 +178,23 @@ export class ArcTool<
|
|
|
87
178
|
};
|
|
88
179
|
}
|
|
89
180
|
|
|
90
|
-
|
|
91
|
-
if (!this.#handler) {
|
|
92
|
-
throw new Error(`Tool "${this.name}" has no handler`);
|
|
93
|
-
}
|
|
94
|
-
return this.#handler(ctx, params as $type<ReturnType<typeof object<S>>>);
|
|
95
|
-
}
|
|
181
|
+
// ─── Execution (called by listener) ──────────────────────────
|
|
96
182
|
|
|
97
|
-
async
|
|
98
|
-
|
|
183
|
+
async executeWithContext(
|
|
184
|
+
params: Record<string, unknown>,
|
|
185
|
+
listenerCtx: any,
|
|
186
|
+
identifyByValue: string,
|
|
187
|
+
): Promise<string> {
|
|
188
|
+
const handler = this.#fn.handler as Function | false | null;
|
|
189
|
+
if (!handler) {
|
|
99
190
|
throw new Error(`Tool "${this.name}" has no handler`);
|
|
100
191
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
mutate: () => { throw new Error("No mutation context available"); },
|
|
108
|
-
query: () => { throw new Error("No query context available"); },
|
|
109
|
-
identifyBy: "",
|
|
110
|
-
};
|
|
111
|
-
return this.#handler(emptyCtx, params as $type<ReturnType<typeof object<S>>>);
|
|
192
|
+
|
|
193
|
+
const ctx = { ...listenerCtx, identifyBy: identifyByValue };
|
|
194
|
+
const result = await (handler as Function)(ctx, params);
|
|
195
|
+
|
|
196
|
+
if (typeof result === "string") return result;
|
|
197
|
+
return JSON.stringify(result);
|
|
112
198
|
}
|
|
113
199
|
}
|
|
114
200
|
|
|
@@ -118,4 +204,4 @@ export function tool<const Name extends string>(name: Name) {
|
|
|
118
204
|
return new ArcTool(name);
|
|
119
205
|
}
|
|
120
206
|
|
|
121
|
-
export type ArcToolAny = ArcTool<string,
|
|
207
|
+
export type ArcToolAny = ArcTool<string, any, any>;
|
package/src/types.ts
CHANGED
|
@@ -48,6 +48,7 @@ export interface CompletionRequest {
|
|
|
48
48
|
model: string;
|
|
49
49
|
messages: Message[];
|
|
50
50
|
tools?: JsonSchemaToolDef[];
|
|
51
|
+
toolChoice?: "auto" | "required" | { type: "function"; name: string };
|
|
51
52
|
webSearch?: boolean;
|
|
52
53
|
temperature?: number;
|
|
53
54
|
maxTokens?: number;
|
|
@@ -92,6 +93,7 @@ export interface LLMProvider {
|
|
|
92
93
|
request: CompletionRequest,
|
|
93
94
|
onChunk: (chunk: StreamChunk) => void,
|
|
94
95
|
): Promise<CompletionResult>;
|
|
96
|
+
getPricing?(model: string): import("./billing/types").AdapterPricing | undefined;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
// ─── Chat Stream (SSE events for chat streaming) ────────────────
|
|
@@ -100,7 +102,7 @@ export type ChatStreamEventType =
|
|
|
100
102
|
| "content_delta"
|
|
101
103
|
| "server_tool_start"
|
|
102
104
|
| "server_tool_result"
|
|
103
|
-
| "
|
|
105
|
+
| "interactive_tool_request"
|
|
104
106
|
| "usage_update"
|
|
105
107
|
| "done"
|
|
106
108
|
| "error";
|
|
@@ -118,11 +120,3 @@ export interface ChatStreamEvent {
|
|
|
118
120
|
error?: string;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
// ─── Pricing ─────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
export interface PricingConfig {
|
|
124
|
-
inputPer1M: number;
|
|
125
|
-
outputPer1M: number;
|
|
126
|
-
cachedInputPer1M?: number;
|
|
127
|
-
reasoningPer1M?: number;
|
|
128
|
-
}
|
package/src/aggregates/budget.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/// <reference path="../arc.d.ts" />
|
|
2
|
-
import {
|
|
3
|
-
aggregate,
|
|
4
|
-
boolean,
|
|
5
|
-
id,
|
|
6
|
-
number,
|
|
7
|
-
string,
|
|
8
|
-
type ArcId,
|
|
9
|
-
} from "@arcote.tech/arc";
|
|
10
|
-
import type { Token } from "@arcote.tech/arc-auth";
|
|
11
|
-
import type { createCompletionAggregate } from "./completion";
|
|
12
|
-
|
|
13
|
-
// ─── ID ──────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export const createBudgetId = <const Name extends string>(data: {
|
|
16
|
-
name: Name;
|
|
17
|
-
}) => id(`${data.name}Budget`);
|
|
18
|
-
|
|
19
|
-
export type BudgetId<Name extends string = string> = ReturnType<
|
|
20
|
-
typeof createBudgetId<Name>
|
|
21
|
-
>;
|
|
22
|
-
|
|
23
|
-
// ─── Aggregate ───────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export type BudgetAggregateData = {
|
|
26
|
-
name: string;
|
|
27
|
-
budgetId: ArcId<any>;
|
|
28
|
-
accountId: ArcId<any>;
|
|
29
|
-
userToken: Token;
|
|
30
|
-
CompletionAggregate: ReturnType<typeof createCompletionAggregate>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const createBudgetAggregate = <
|
|
34
|
-
const Data extends BudgetAggregateData,
|
|
35
|
-
>(
|
|
36
|
-
data: Data,
|
|
37
|
-
) => {
|
|
38
|
-
const { budgetId, accountId, userToken, CompletionAggregate } = data;
|
|
39
|
-
|
|
40
|
-
const completionFinishedEvent =
|
|
41
|
-
CompletionAggregate.getEvent("completionFinished");
|
|
42
|
-
|
|
43
|
-
return aggregate(`${data.name}Budgets`, budgetId, {
|
|
44
|
-
accountId,
|
|
45
|
-
budgetType: string(),
|
|
46
|
-
limitCents: number(),
|
|
47
|
-
usedCents: number(),
|
|
48
|
-
period: string().optional(),
|
|
49
|
-
isExceeded: boolean(),
|
|
50
|
-
})
|
|
51
|
-
.publicEvent(
|
|
52
|
-
"budgetSet",
|
|
53
|
-
{
|
|
54
|
-
budgetId,
|
|
55
|
-
accountId,
|
|
56
|
-
budgetType: string(),
|
|
57
|
-
limitCents: number(),
|
|
58
|
-
period: string().optional(),
|
|
59
|
-
},
|
|
60
|
-
async (ctx, event) => {
|
|
61
|
-
const p = event.payload;
|
|
62
|
-
await ctx.set(p.budgetId, {
|
|
63
|
-
accountId: p.accountId,
|
|
64
|
-
budgetType: p.budgetType,
|
|
65
|
-
limitCents: p.limitCents,
|
|
66
|
-
usedCents: 0,
|
|
67
|
-
period: p.period,
|
|
68
|
-
isExceeded: false,
|
|
69
|
-
});
|
|
70
|
-
},
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
.publicEvent(
|
|
74
|
-
"budgetUsageRecorded",
|
|
75
|
-
{
|
|
76
|
-
budgetId,
|
|
77
|
-
amountCents: number(),
|
|
78
|
-
},
|
|
79
|
-
async (ctx, event) => {
|
|
80
|
-
const existing = await ctx.findOne({ _id: event.payload.budgetId });
|
|
81
|
-
if (!existing) return;
|
|
82
|
-
|
|
83
|
-
const newUsed = existing.usedCents + event.payload.amountCents;
|
|
84
|
-
await ctx.modify(event.payload.budgetId, {
|
|
85
|
-
usedCents: newUsed,
|
|
86
|
-
isExceeded: newUsed >= existing.limitCents,
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
.handleEvent(completionFinishedEvent, async (ctx, event) => {
|
|
92
|
-
// Record usage from completion cost on all matching budgets
|
|
93
|
-
const budgets = await ctx.find({});
|
|
94
|
-
for (const budget of budgets) {
|
|
95
|
-
const newUsed = budget.usedCents + event.payload.costCents;
|
|
96
|
-
await ctx.modify(budget._id, {
|
|
97
|
-
usedCents: newUsed,
|
|
98
|
-
isExceeded: newUsed >= budget.limitCents,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
.mutateMethod(
|
|
104
|
-
"setBudget",
|
|
105
|
-
(fn) => fn.withParams({
|
|
106
|
-
budgetType: string(),
|
|
107
|
-
limitCents: number(),
|
|
108
|
-
period: string().optional(),
|
|
109
|
-
}).handle(
|
|
110
|
-
ONLY_SERVER &&
|
|
111
|
-
(async (ctx, params) => {
|
|
112
|
-
const bId = budgetId.generate();
|
|
113
|
-
const aId = ctx.$auth.params.accountId;
|
|
114
|
-
|
|
115
|
-
await ctx.budgetSet.emit({
|
|
116
|
-
budgetId: bId,
|
|
117
|
-
accountId: accountId.parse(aId),
|
|
118
|
-
budgetType: params.budgetType,
|
|
119
|
-
limitCents: params.limitCents,
|
|
120
|
-
period: params.period,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return { budgetId: bId };
|
|
124
|
-
}),
|
|
125
|
-
))
|
|
126
|
-
|
|
127
|
-
.clientQuery(
|
|
128
|
-
"getByAccount",
|
|
129
|
-
(fn) => fn.handle(async (ctx) =>
|
|
130
|
-
ctx.$query.find({ where: { accountId: ctx.$auth.params.accountId } }),
|
|
131
|
-
))
|
|
132
|
-
|
|
133
|
-
.protectBy(userToken, (p) => ({ accountId: p.accountId }))
|
|
134
|
-
;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export type BudgetAggregate = ReturnType<typeof createBudgetAggregate>;
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/// <reference path="../arc.d.ts" />
|
|
2
|
-
import {
|
|
3
|
-
aggregate,
|
|
4
|
-
date,
|
|
5
|
-
id,
|
|
6
|
-
number,
|
|
7
|
-
string,
|
|
8
|
-
type ArcId,
|
|
9
|
-
} from "@arcote.tech/arc";
|
|
10
|
-
import type { Token } from "@arcote.tech/arc-auth";
|
|
11
|
-
import type { LLMProvider, PricingConfig, TokenUsage } from "../types";
|
|
12
|
-
|
|
13
|
-
// ─── ID ──────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export const createCompletionId = <const Name extends string>(data: {
|
|
16
|
-
name: Name;
|
|
17
|
-
}) => id(`${data.name}Completion`);
|
|
18
|
-
|
|
19
|
-
export type CompletionId<Name extends string = string> = ReturnType<
|
|
20
|
-
typeof createCompletionId<Name>
|
|
21
|
-
>;
|
|
22
|
-
|
|
23
|
-
// ─── Aggregate ───────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export type CompletionAggregateData = {
|
|
26
|
-
name: string;
|
|
27
|
-
completionId: ArcId<any>;
|
|
28
|
-
accountId: ArcId<any>;
|
|
29
|
-
userToken: Token;
|
|
30
|
-
providers: LLMProvider[];
|
|
31
|
-
pricing?: Record<string, PricingConfig>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const createCompletionAggregate = <
|
|
35
|
-
const Data extends CompletionAggregateData,
|
|
36
|
-
>(
|
|
37
|
-
data: Data,
|
|
38
|
-
) => {
|
|
39
|
-
const { completionId, accountId, userToken, providers, pricing } = data;
|
|
40
|
-
|
|
41
|
-
function resolveProvider(model: string): LLMProvider | undefined {
|
|
42
|
-
return providers.find((p) => p.models.includes(model));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function calculateCostCents(
|
|
46
|
-
model: string,
|
|
47
|
-
usage: TokenUsage,
|
|
48
|
-
): number {
|
|
49
|
-
const config = pricing?.[model];
|
|
50
|
-
if (!config) return 0;
|
|
51
|
-
|
|
52
|
-
const inputCost =
|
|
53
|
-
(usage.inputTokens / 1_000_000) * config.inputPer1M;
|
|
54
|
-
const outputCost =
|
|
55
|
-
(usage.outputTokens / 1_000_000) * config.outputPer1M;
|
|
56
|
-
const cachedCost = config.cachedInputPer1M
|
|
57
|
-
? (usage.cachedTokens / 1_000_000) * config.cachedInputPer1M
|
|
58
|
-
: 0;
|
|
59
|
-
const reasoningCost = config.reasoningPer1M
|
|
60
|
-
? (usage.reasoningTokens / 1_000_000) * config.reasoningPer1M
|
|
61
|
-
: 0;
|
|
62
|
-
|
|
63
|
-
return Math.round((inputCost + outputCost + cachedCost + reasoningCost) * 100);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return aggregate(
|
|
67
|
-
`${data.name}Completions`,
|
|
68
|
-
completionId,
|
|
69
|
-
{
|
|
70
|
-
accountId,
|
|
71
|
-
model: string(),
|
|
72
|
-
provider: string(),
|
|
73
|
-
status: string(),
|
|
74
|
-
promptTokens: number(),
|
|
75
|
-
completionTokens: number(),
|
|
76
|
-
totalTokens: number(),
|
|
77
|
-
costCents: number(),
|
|
78
|
-
finishReason: string().optional(),
|
|
79
|
-
errorMessage: string().optional(),
|
|
80
|
-
requestedAt: date(),
|
|
81
|
-
completedAt: date().optional(),
|
|
82
|
-
metadata: string().optional(),
|
|
83
|
-
},
|
|
84
|
-
)
|
|
85
|
-
.publicEvent(
|
|
86
|
-
"completionRequested",
|
|
87
|
-
{
|
|
88
|
-
completionId,
|
|
89
|
-
accountId,
|
|
90
|
-
model: string(),
|
|
91
|
-
provider: string(),
|
|
92
|
-
metadata: string().optional(),
|
|
93
|
-
},
|
|
94
|
-
async (ctx, event) => {
|
|
95
|
-
const { completionId: cId, accountId: aId, model, provider, metadata } =
|
|
96
|
-
event.payload;
|
|
97
|
-
await ctx.set(cId, {
|
|
98
|
-
accountId: aId,
|
|
99
|
-
model,
|
|
100
|
-
provider,
|
|
101
|
-
status: "pending",
|
|
102
|
-
promptTokens: 0,
|
|
103
|
-
completionTokens: 0,
|
|
104
|
-
totalTokens: 0,
|
|
105
|
-
costCents: 0,
|
|
106
|
-
requestedAt: event.createdAt,
|
|
107
|
-
metadata,
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
.publicEvent(
|
|
113
|
-
"completionFinished",
|
|
114
|
-
{
|
|
115
|
-
completionId,
|
|
116
|
-
promptTokens: number(),
|
|
117
|
-
completionTokens: number(),
|
|
118
|
-
totalTokens: number(),
|
|
119
|
-
costCents: number(),
|
|
120
|
-
finishReason: string(),
|
|
121
|
-
},
|
|
122
|
-
async (ctx, event) => {
|
|
123
|
-
const p = event.payload;
|
|
124
|
-
await ctx.modify(p.completionId, {
|
|
125
|
-
status: "completed",
|
|
126
|
-
promptTokens: p.promptTokens,
|
|
127
|
-
completionTokens: p.completionTokens,
|
|
128
|
-
totalTokens: p.totalTokens,
|
|
129
|
-
costCents: p.costCents,
|
|
130
|
-
finishReason: p.finishReason,
|
|
131
|
-
completedAt: event.createdAt,
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
.publicEvent(
|
|
137
|
-
"completionFailed",
|
|
138
|
-
{
|
|
139
|
-
completionId,
|
|
140
|
-
errorMessage: string(),
|
|
141
|
-
},
|
|
142
|
-
async (ctx, event) => {
|
|
143
|
-
await ctx.modify(event.payload.completionId, {
|
|
144
|
-
status: "error",
|
|
145
|
-
errorMessage: event.payload.errorMessage,
|
|
146
|
-
completedAt: event.createdAt,
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
.mutateMethod(
|
|
152
|
-
"requestCompletion",
|
|
153
|
-
(fn) => fn.withParams({
|
|
154
|
-
model: string(),
|
|
155
|
-
messages: string(),
|
|
156
|
-
tools: string().optional(),
|
|
157
|
-
webSearch: string().optional(),
|
|
158
|
-
metadata: string().optional(),
|
|
159
|
-
}).handle(
|
|
160
|
-
ONLY_SERVER &&
|
|
161
|
-
(async (ctx, params) => {
|
|
162
|
-
const provider = resolveProvider(params.model);
|
|
163
|
-
if (!provider) {
|
|
164
|
-
return { error: "PROVIDER_NOT_FOUND" as const };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const cId = completionId.generate();
|
|
168
|
-
const aId = ctx.$auth.params.accountId;
|
|
169
|
-
|
|
170
|
-
await ctx.completionRequested.emit({
|
|
171
|
-
completionId: cId,
|
|
172
|
-
accountId: accountId.parse(aId),
|
|
173
|
-
model: params.model,
|
|
174
|
-
provider: provider.name,
|
|
175
|
-
metadata: params.metadata,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const messages = JSON.parse(params.messages);
|
|
180
|
-
const webSearch = params.webSearch === "true";
|
|
181
|
-
|
|
182
|
-
const toolDefs = params.tools
|
|
183
|
-
? JSON.parse(params.tools) as { name: string; description: string; parameters: Record<string, unknown> }[]
|
|
184
|
-
: undefined;
|
|
185
|
-
|
|
186
|
-
const result = await provider.complete({
|
|
187
|
-
model: params.model,
|
|
188
|
-
messages,
|
|
189
|
-
tools: toolDefs,
|
|
190
|
-
webSearch,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const costCents = calculateCostCents(params.model, result.usage);
|
|
194
|
-
|
|
195
|
-
await ctx.completionFinished.emit({
|
|
196
|
-
completionId: cId,
|
|
197
|
-
promptTokens: result.usage.inputTokens,
|
|
198
|
-
completionTokens: result.usage.outputTokens,
|
|
199
|
-
totalTokens: result.usage.totalTokens,
|
|
200
|
-
costCents,
|
|
201
|
-
finishReason: result.finishReason,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
completionId: cId,
|
|
206
|
-
content: result.content,
|
|
207
|
-
toolCalls: result.toolCalls,
|
|
208
|
-
finishReason: result.finishReason,
|
|
209
|
-
};
|
|
210
|
-
} catch (error) {
|
|
211
|
-
const errorMessage =
|
|
212
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
213
|
-
|
|
214
|
-
await ctx.completionFailed.emit({
|
|
215
|
-
completionId: cId,
|
|
216
|
-
errorMessage,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
return { error: errorMessage };
|
|
220
|
-
}
|
|
221
|
-
}),
|
|
222
|
-
))
|
|
223
|
-
|
|
224
|
-
.clientQuery("getAll", (fn) => fn.handle(async (ctx) => ctx.$query.find({})))
|
|
225
|
-
.clientQuery(
|
|
226
|
-
"getByAccount",
|
|
227
|
-
(fn) => fn.handle(async (ctx) =>
|
|
228
|
-
ctx.$query.find({ where: { accountId: ctx.$auth.params.accountId } }),
|
|
229
|
-
))
|
|
230
|
-
|
|
231
|
-
.protectBy(userToken, (p) => ({ accountId: p.accountId }))
|
|
232
|
-
;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
export type CompletionAggregate = ReturnType<typeof createCompletionAggregate>;
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { route, type ArcAggregateElement } from "@arcote.tech/arc";
|
|
2
|
-
import type { Token } from "@arcote.tech/arc-auth";
|
|
3
|
-
import type { LLMProvider, Message, PricingConfig, StreamChunk } from "../types";
|
|
4
|
-
import type { ArcToolAny } from "../tool/tool";
|
|
5
|
-
|
|
6
|
-
export type StreamCompletionRouteData = {
|
|
7
|
-
name: string;
|
|
8
|
-
completionElement: ArcAggregateElement;
|
|
9
|
-
userToken: Token;
|
|
10
|
-
providers: LLMProvider[];
|
|
11
|
-
pricing?: Record<string, PricingConfig>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const createStreamCompletionRoute = <
|
|
15
|
-
const Data extends StreamCompletionRouteData,
|
|
16
|
-
>(
|
|
17
|
-
data: Data,
|
|
18
|
-
) => {
|
|
19
|
-
const { completionElement, userToken, providers, pricing } = data;
|
|
20
|
-
|
|
21
|
-
function resolveProvider(model: string): LLMProvider | undefined {
|
|
22
|
-
return providers.find((p) => p.models.includes(model));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return route(`${data.name}AiStream`)
|
|
26
|
-
.path(`/ai/${data.name}/stream/:completionId`)
|
|
27
|
-
.protectBy(userToken, () => true)
|
|
28
|
-
.mutate([completionElement])
|
|
29
|
-
.handle({
|
|
30
|
-
POST: async (ctx, req) => {
|
|
31
|
-
const body = await req.json() as {
|
|
32
|
-
model: string;
|
|
33
|
-
messages: Message[];
|
|
34
|
-
tools?: { name: string; description: string; parameters: Record<string, unknown> }[];
|
|
35
|
-
webSearch?: boolean;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const provider = resolveProvider(body.model);
|
|
39
|
-
if (!provider) {
|
|
40
|
-
return new Response(
|
|
41
|
-
JSON.stringify({ error: "PROVIDER_NOT_FOUND" }),
|
|
42
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const encoder = new TextEncoder();
|
|
47
|
-
|
|
48
|
-
const stream = new ReadableStream({
|
|
49
|
-
async start(controller) {
|
|
50
|
-
const sendEvent = (chunk: StreamChunk) => {
|
|
51
|
-
controller.enqueue(
|
|
52
|
-
encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`),
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const result = await provider.streamComplete(
|
|
58
|
-
{
|
|
59
|
-
model: body.model,
|
|
60
|
-
messages: body.messages,
|
|
61
|
-
tools: body.tools,
|
|
62
|
-
webSearch: body.webSearch,
|
|
63
|
-
},
|
|
64
|
-
sendEvent,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Send final done event with usage
|
|
68
|
-
sendEvent({
|
|
69
|
-
type: "done",
|
|
70
|
-
usage: result.usage,
|
|
71
|
-
finishReason: result.finishReason,
|
|
72
|
-
});
|
|
73
|
-
} catch (error) {
|
|
74
|
-
sendEvent({
|
|
75
|
-
type: "error",
|
|
76
|
-
content:
|
|
77
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
78
|
-
});
|
|
79
|
-
} finally {
|
|
80
|
-
controller.close();
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return new Response(stream, {
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "text/event-stream",
|
|
88
|
-
"Cache-Control": "no-cache",
|
|
89
|
-
Connection: "keep-alive",
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
};
|