@byte-42/fantastico-sdk 0.0.0-stage → 0.21.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/LICENSE +21 -0
- package/README.md +470 -3
- package/dist/activity.d.ts +72 -0
- package/dist/activity.js +22 -0
- package/dist/auth.d.ts +216 -0
- package/dist/auth.js +224 -0
- package/dist/backoff.d.ts +48 -0
- package/dist/backoff.js +31 -0
- package/dist/browser-actions.d.ts +28 -0
- package/dist/browser-actions.js +25 -0
- package/dist/client.d.ts +410 -0
- package/dist/client.js +410 -0
- package/dist/decisions.d.ts +149 -0
- package/dist/decisions.js +71 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/llm.d.ts +40 -0
- package/dist/llm.js +74 -0
- package/dist/local-tools.d.ts +72 -0
- package/dist/local-tools.js +55 -0
- package/dist/max-tokens.d.ts +27 -0
- package/dist/max-tokens.js +16 -0
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.js +55 -0
- package/dist/orchestrator.d.ts +105 -0
- package/dist/orchestrator.js +296 -0
- package/dist/provider-error.d.ts +165 -0
- package/dist/provider-error.js +156 -0
- package/dist/raw.d.ts +48 -0
- package/dist/raw.js +72 -0
- package/dist/schema-digest.d.ts +25 -0
- package/dist/schema-digest.js +20 -0
- package/dist/speech.d.ts +107 -0
- package/dist/speech.js +48 -0
- package/dist/surface.d.ts +35 -0
- package/dist/surface.js +13 -0
- package/dist/tool-definition.d.ts +22 -0
- package/dist/tool-definition.js +10 -0
- package/dist/tool-origin.d.ts +32 -0
- package/dist/tool-origin.js +65 -0
- package/dist/types.d.ts +734 -0
- package/dist/types.js +27 -0
- package/package.json +48 -6
package/dist/client.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { normalizeToolOriginMap, toolOriginMapFromEnv } from "./tool-origin.js";
|
|
2
|
+
import { FantasticoError } from "./types.js";
|
|
3
|
+
import { surfaceOf, unsupportedOnSurface } from "./surface.js";
|
|
4
|
+
import { AuthClient } from "./auth.js";
|
|
5
|
+
import { createLlmClient } from "./llm.js";
|
|
6
|
+
import { isRemintable, providerFailure, ProviderError } from "./provider-error.js";
|
|
7
|
+
import { affordableMaxTokens } from "./max-tokens.js";
|
|
8
|
+
import { orchestrate, resumeOrchestration, DEFAULT_BUDGET_MS, } from "./orchestrator.js";
|
|
9
|
+
import { createMcpExecutor } from "./mcp.js";
|
|
10
|
+
import { callOpenRouterRaw, normalizeRawUsage, normalizeFinishReason } from "./raw.js";
|
|
11
|
+
import { LocalToolRegistry } from "./local-tools.js";
|
|
12
|
+
import { callOpenRouterDecision, createDecisionTool, } from "./decisions.js";
|
|
13
|
+
import { callOpenRouterSpeech, } from "./speech.js";
|
|
14
|
+
function upstreamOf(err) {
|
|
15
|
+
return err instanceof ProviderError ? err.upstream : null;
|
|
16
|
+
}
|
|
17
|
+
export class Fantastico {
|
|
18
|
+
opts;
|
|
19
|
+
auth;
|
|
20
|
+
fetchImpl;
|
|
21
|
+
mcpExecutor;
|
|
22
|
+
localTools;
|
|
23
|
+
unsettled = new Map();
|
|
24
|
+
name;
|
|
25
|
+
surface;
|
|
26
|
+
chat;
|
|
27
|
+
tools;
|
|
28
|
+
models;
|
|
29
|
+
accounts;
|
|
30
|
+
runtimeKeys;
|
|
31
|
+
decisions;
|
|
32
|
+
audio;
|
|
33
|
+
balance;
|
|
34
|
+
usage;
|
|
35
|
+
get activitySessionId() {
|
|
36
|
+
return this.auth.getActivitySessionId();
|
|
37
|
+
}
|
|
38
|
+
constructor(options) {
|
|
39
|
+
if (!options.apiKey)
|
|
40
|
+
throw new Error("apiKey è obbligatoria");
|
|
41
|
+
this.surface = surfaceOf(options.apiKey);
|
|
42
|
+
this.opts = {
|
|
43
|
+
baseUrl: "https://fantastico.cc/api",
|
|
44
|
+
defaultModel: "anthropic/claude-sonnet-5",
|
|
45
|
+
maxTurns: 8,
|
|
46
|
+
budgetMs: 90_000,
|
|
47
|
+
mcpTimeoutMs: 180_000,
|
|
48
|
+
reserveUsd: 0.1,
|
|
49
|
+
...options,
|
|
50
|
+
};
|
|
51
|
+
this.name = this.opts.name;
|
|
52
|
+
this.fetchImpl = this.opts.fetchImpl ?? fetch;
|
|
53
|
+
const toolOrigins = this.opts.toolOriginMap ? normalizeToolOriginMap(this.opts.toolOriginMap) : toolOriginMapFromEnv();
|
|
54
|
+
this.auth = new AuthClient({
|
|
55
|
+
apiKey: this.opts.apiKey,
|
|
56
|
+
baseUrl: this.opts.baseUrl,
|
|
57
|
+
...(this.opts.activitySessionId === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { activitySessionId: this.opts.activitySessionId }),
|
|
60
|
+
...(this.opts.product === undefined ? {} : { product: this.opts.product }),
|
|
61
|
+
...(toolOrigins === undefined ? {} : { toolOriginMap: toolOrigins }),
|
|
62
|
+
fetchImpl: this.fetchImpl,
|
|
63
|
+
});
|
|
64
|
+
this.mcpExecutor = createMcpExecutor({
|
|
65
|
+
timeoutMs: this.opts.mcpTimeoutMs,
|
|
66
|
+
fetchImpl: this.fetchImpl,
|
|
67
|
+
});
|
|
68
|
+
this.localTools = new LocalToolRegistry(this.opts.tools ?? []);
|
|
69
|
+
this.chat = {
|
|
70
|
+
completions: {
|
|
71
|
+
create: (params, options) => this.createCompletion(params, options),
|
|
72
|
+
resume: (state, decisions, context, options) => this.resumeCompletion(state, decisions, context, options),
|
|
73
|
+
createRaw: (params) => this.createRawCompletion(params),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
this.tools = {
|
|
77
|
+
list: async (user) => toolsToDefinitions(await this.remoteTools(this.tenantOf(user))),
|
|
78
|
+
enabled: async () => (await this.api("GET", `/${this.surface}/tools`)).tools,
|
|
79
|
+
execute: async (name, args, user) => {
|
|
80
|
+
const tools = await this.remoteTools(this.tenantOf(user));
|
|
81
|
+
const tool = tools.find((s) => s.allowedTools.includes(name));
|
|
82
|
+
if (!tool)
|
|
83
|
+
throw new FantasticoError(`Tool sconosciuto: ${name}`, "UNKNOWN_TOOL", 404);
|
|
84
|
+
return this.mcpExecutor(tool, name, args);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
this.models = {
|
|
88
|
+
list: async () => (await this.api("GET", `/${this.surface}/models`)).models,
|
|
89
|
+
};
|
|
90
|
+
const conto = (userKey, suffix = "") => `/conti/${encodeURIComponent(userKey)}${suffix}`;
|
|
91
|
+
this.accounts = {
|
|
92
|
+
create: (params) => this.conti("POST", "/conti", params),
|
|
93
|
+
list: () => this.conti("GET", "/conti"),
|
|
94
|
+
get: (userKey) => this.conti("GET", conto(userKey)),
|
|
95
|
+
update: (userKey, params) => this.conti("PATCH", conto(userKey), params),
|
|
96
|
+
upgrade: (userKey) => this.conti("POST", conto(userKey, "/upgrade"), {}),
|
|
97
|
+
keys: {
|
|
98
|
+
list: () => this.conti("GET", "/conti/keys"),
|
|
99
|
+
issue: (userKey, label) => this.conti("POST", conto(userKey, "/key"), label ? { label } : {}),
|
|
100
|
+
rotate: (userKey) => this.conti("POST", conto(userKey, "/key/rotate"), {}),
|
|
101
|
+
revoke: (userKey) => this.conti("DELETE", conto(userKey, "/key")),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const forConto = (userKey) => {
|
|
105
|
+
if (userKey === undefined)
|
|
106
|
+
return "";
|
|
107
|
+
if (this.surface !== "runtime") {
|
|
108
|
+
throw unsupportedOnSurface("Nominare un conto sulle proprie runtime key", "runtime");
|
|
109
|
+
}
|
|
110
|
+
return `?userKey=${encodeURIComponent(userKey)}`;
|
|
111
|
+
};
|
|
112
|
+
this.runtimeKeys = {
|
|
113
|
+
ensure: async (user) => {
|
|
114
|
+
const userKey = this.tenantOf(user);
|
|
115
|
+
await this.auth.getRuntimeKey(userKey);
|
|
116
|
+
const handle = this.auth.getHandle(userKey);
|
|
117
|
+
if (handle === null)
|
|
118
|
+
throw new FantasticoError("Conio non riuscito", "MINT_FAILED", 402);
|
|
119
|
+
return handle;
|
|
120
|
+
},
|
|
121
|
+
current: (user) => this.auth.getHandle(this.tenantOf(user)),
|
|
122
|
+
list: async (userKey) => this.api("GET", `/${this.surface}/keys${forConto(userKey)}`),
|
|
123
|
+
revoke: async (recordId, userKey) => {
|
|
124
|
+
const result = await this.api("DELETE", `/${this.surface}/keys/${encodeURIComponent(recordId)}${forConto(userKey)}`);
|
|
125
|
+
const mine = this.auth.findByRecordId(recordId);
|
|
126
|
+
if (mine !== null)
|
|
127
|
+
this.auth.invalidate(mine.userKey);
|
|
128
|
+
return result;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
this.balance = async () => {
|
|
132
|
+
if (this.surface !== "edge") {
|
|
133
|
+
throw unsupportedOnSurface("Il saldo del proprio conto (con una project key i conti sono molti: `accounts.get`)", "edge");
|
|
134
|
+
}
|
|
135
|
+
return this.auth.balance();
|
|
136
|
+
};
|
|
137
|
+
this.decisions = {
|
|
138
|
+
evaluate: (params) => this.evaluateDecision(params),
|
|
139
|
+
tool: (config) => createDecisionTool(config, (params) => this.evaluateDecision(params)),
|
|
140
|
+
};
|
|
141
|
+
this.audio = {
|
|
142
|
+
speech: {
|
|
143
|
+
create: (params) => this.createSpeech(params),
|
|
144
|
+
models: async () => (await this.api("GET", `/${this.surface}/models/speech`)).models,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
this.usage = {
|
|
148
|
+
record: async (params) => ({
|
|
149
|
+
service: params.service,
|
|
150
|
+
unit: params.unit,
|
|
151
|
+
quantity: params.quantity,
|
|
152
|
+
amount: 0,
|
|
153
|
+
deduped: false,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
tenantOf(user) {
|
|
158
|
+
if (user !== undefined && this.surface !== "runtime") {
|
|
159
|
+
throw unsupportedOnSurface("Attribuire la chiamata a un conto con `user` (una chiave di conto ne vale già uno solo)", "runtime");
|
|
160
|
+
}
|
|
161
|
+
return user;
|
|
162
|
+
}
|
|
163
|
+
async conti(method, path, body) {
|
|
164
|
+
if (this.surface !== "runtime") {
|
|
165
|
+
throw unsupportedOnSurface("L'anagrafica dei conti è di un progetto", "runtime");
|
|
166
|
+
}
|
|
167
|
+
return this.api(method, path, body);
|
|
168
|
+
}
|
|
169
|
+
addTool(tool) {
|
|
170
|
+
this.localTools.add(tool);
|
|
171
|
+
}
|
|
172
|
+
async defaultMaxTokens(model, explicit, userKey) {
|
|
173
|
+
if (explicit != null)
|
|
174
|
+
return explicit;
|
|
175
|
+
const limit = this.auth.getKeyLimitUsd(userKey);
|
|
176
|
+
if (limit === null)
|
|
177
|
+
return undefined;
|
|
178
|
+
return affordableMaxTokens(limit, await this.auth.outputPricePerToken(model));
|
|
179
|
+
}
|
|
180
|
+
asFantasticoError(err, msg) {
|
|
181
|
+
const named = providerFailure(err);
|
|
182
|
+
const detail = upstreamOf(err);
|
|
183
|
+
if (named !== null)
|
|
184
|
+
return new FantasticoError(msg, named.code, named.status, detail);
|
|
185
|
+
return new FantasticoError(msg || "Errore interno", "INTERNAL_ERROR", 500, detail);
|
|
186
|
+
}
|
|
187
|
+
retryConfig(deadlineAt) {
|
|
188
|
+
const opt = this.opts.retry;
|
|
189
|
+
if (opt === false)
|
|
190
|
+
return false;
|
|
191
|
+
return { ...(opt ?? {}), deadlineAt };
|
|
192
|
+
}
|
|
193
|
+
remintingLlm(model, deadlineAt, userKey) {
|
|
194
|
+
const retry = this.retryConfig(deadlineAt);
|
|
195
|
+
return {
|
|
196
|
+
chatCompletion: (params) => this.withProviderRetry(async (runtimeKey) => {
|
|
197
|
+
const llm = createLlmClient({ providerApiKey: runtimeKey, fetchImpl: this.fetchImpl, retry });
|
|
198
|
+
return llm.chatCompletion({
|
|
199
|
+
...params,
|
|
200
|
+
max_tokens: await this.defaultMaxTokens(model, params.max_tokens, userKey),
|
|
201
|
+
});
|
|
202
|
+
}, userKey),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async withProviderRetry(run, userKey) {
|
|
206
|
+
const mintKey = async () => {
|
|
207
|
+
try {
|
|
208
|
+
return await this.auth.getRuntimeKey(userKey);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (err instanceof FantasticoError)
|
|
212
|
+
throw err;
|
|
213
|
+
throw new FantasticoError(err.message, "MINT_FAILED", 402);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const runtimeKey = await mintKey();
|
|
217
|
+
try {
|
|
218
|
+
return await run(runtimeKey);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (err instanceof FantasticoError)
|
|
222
|
+
throw err;
|
|
223
|
+
const msg = err.message ?? "";
|
|
224
|
+
if (!isRemintable(err))
|
|
225
|
+
throw this.asFantasticoError(err, msg);
|
|
226
|
+
this.auth.invalidate(userKey);
|
|
227
|
+
const freshKey = await mintKey();
|
|
228
|
+
try {
|
|
229
|
+
return await run(freshKey);
|
|
230
|
+
}
|
|
231
|
+
catch (second) {
|
|
232
|
+
const named = providerFailure(second);
|
|
233
|
+
if (named !== null && !isRemintable(second)) {
|
|
234
|
+
throw new FantasticoError(second.message, named.code, named.status, upstreamOf(second));
|
|
235
|
+
}
|
|
236
|
+
throw new FantasticoError("Errore nella comunicazione con il provider LLM", "LLM_PROVIDER_ERROR", 502, upstreamOf(second));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async createCompletion(params, options) {
|
|
241
|
+
const model = params.model ?? this.opts.defaultModel;
|
|
242
|
+
const userKey = this.tenantOf(params.user);
|
|
243
|
+
if (userKey) {
|
|
244
|
+
const used = this.unsettled.get(userKey) ?? 0;
|
|
245
|
+
await this.auth.reserveBudget(userKey, used, this.opts.reserveUsd);
|
|
246
|
+
this.unsettled.set(userKey, 0);
|
|
247
|
+
}
|
|
248
|
+
const granted = await this.remoteTools(userKey);
|
|
249
|
+
const only = params.marketplaceTools;
|
|
250
|
+
const remoteTools = only === undefined ? granted : granted.filter((t) => only.includes(t.name));
|
|
251
|
+
const messages = params.messages.map((m) => ({ role: m.role, content: m.content }));
|
|
252
|
+
if (this.opts.defaultSystemPrompt && messages[0]?.role !== "system") {
|
|
253
|
+
messages.unshift({ role: "system", content: this.opts.defaultSystemPrompt });
|
|
254
|
+
}
|
|
255
|
+
const context = params.context ?? this.opts.context;
|
|
256
|
+
const budgetMs = this.opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
257
|
+
const result = await orchestrate(messages, this.remintingLlm(model, Date.now() + budgetMs, userKey), {
|
|
258
|
+
model,
|
|
259
|
+
remoteTools,
|
|
260
|
+
localTools: options?.localTools?.length ? this.localTools.with(options.localTools) : this.localTools,
|
|
261
|
+
context,
|
|
262
|
+
mcpExecutor: this.mcpExecutor,
|
|
263
|
+
maxTurns: this.opts.maxTurns,
|
|
264
|
+
budgetMs,
|
|
265
|
+
temperature: params.temperature,
|
|
266
|
+
reasoning: params.reasoning ?? this.opts.reasoning,
|
|
267
|
+
maxTokens: params.max_tokens,
|
|
268
|
+
onEvent: options?.onEvent,
|
|
269
|
+
});
|
|
270
|
+
if (userKey && result.status === "done") {
|
|
271
|
+
this.unsettled.set(userKey, (this.unsettled.get(userKey) ?? 0) + result.usage.cost_usd);
|
|
272
|
+
}
|
|
273
|
+
return stampTenant(result, userKey);
|
|
274
|
+
}
|
|
275
|
+
async remoteTools(userKey) {
|
|
276
|
+
try {
|
|
277
|
+
await this.auth.getRuntimeKey(userKey);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return await this.auth.getTools(userKey);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async resumeCompletion(rawState, decisions, context, options) {
|
|
289
|
+
const model = rawState?.model ?? this.opts.defaultModel;
|
|
290
|
+
const userKey = this.tenantOf(tenantFromState(rawState));
|
|
291
|
+
const remoteTools = await this.remoteTools(userKey);
|
|
292
|
+
const budgetMs = this.opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
293
|
+
const result = await resumeOrchestration(rawState, decisions, this.remintingLlm(model, Date.now() + budgetMs, userKey), {
|
|
294
|
+
model,
|
|
295
|
+
remoteTools,
|
|
296
|
+
localTools: options?.localTools?.length ? this.localTools.with(options.localTools) : this.localTools,
|
|
297
|
+
context: context ?? this.opts.context,
|
|
298
|
+
mcpExecutor: this.mcpExecutor,
|
|
299
|
+
maxTurns: this.opts.maxTurns,
|
|
300
|
+
budgetMs,
|
|
301
|
+
onEvent: options?.onEvent,
|
|
302
|
+
});
|
|
303
|
+
return stampTenant(result, userKey);
|
|
304
|
+
}
|
|
305
|
+
async createRawCompletion(params) {
|
|
306
|
+
const model = params.model ?? this.opts.defaultModel;
|
|
307
|
+
const userKey = this.tenantOf(params.user);
|
|
308
|
+
if (userKey) {
|
|
309
|
+
const used = this.unsettled.get(userKey) ?? 0;
|
|
310
|
+
await this.auth.reserveBudget(userKey, used, this.opts.reserveUsd);
|
|
311
|
+
this.unsettled.set(userKey, 0);
|
|
312
|
+
}
|
|
313
|
+
const deadlineAt = Date.now() + (this.opts.budgetMs ?? DEFAULT_BUDGET_MS);
|
|
314
|
+
const raw = await this.withProviderRetry(async (runtimeKey) => callOpenRouterRaw(runtimeKey, {
|
|
315
|
+
...params,
|
|
316
|
+
model,
|
|
317
|
+
max_tokens: await this.defaultMaxTokens(model, params.max_tokens, userKey),
|
|
318
|
+
reasoning: params.reasoning ?? this.opts.reasoning,
|
|
319
|
+
}, this.fetchImpl, this.retryConfig(deadlineAt)), userKey);
|
|
320
|
+
const usage = normalizeRawUsage(raw.usage);
|
|
321
|
+
if (userKey) {
|
|
322
|
+
this.unsettled.set(userKey, (this.unsettled.get(userKey) ?? 0) + usage.cost_usd);
|
|
323
|
+
}
|
|
324
|
+
const choice = raw.choices[0];
|
|
325
|
+
return {
|
|
326
|
+
id: raw.id,
|
|
327
|
+
model: raw.model,
|
|
328
|
+
choices: [{
|
|
329
|
+
index: 0,
|
|
330
|
+
message: choice?.message ?? { role: "assistant", content: "" },
|
|
331
|
+
finish_reason: normalizeFinishReason(choice?.finish_reason),
|
|
332
|
+
}],
|
|
333
|
+
usage,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async evaluateDecision(params) {
|
|
337
|
+
const userKey = this.tenantOf(params.user);
|
|
338
|
+
if (userKey) {
|
|
339
|
+
const used = this.unsettled.get(userKey) ?? 0;
|
|
340
|
+
await this.auth.reserveBudget(userKey, used, this.opts.reserveUsd);
|
|
341
|
+
this.unsettled.set(userKey, 0);
|
|
342
|
+
}
|
|
343
|
+
const deadlineAt = Date.now() + (this.opts.budgetMs ?? DEFAULT_BUDGET_MS);
|
|
344
|
+
const result = await this.withProviderRetry((runtimeKey) => callOpenRouterDecision(runtimeKey, params, this.fetchImpl, this.retryConfig(deadlineAt)), userKey);
|
|
345
|
+
if (userKey) {
|
|
346
|
+
this.unsettled.set(userKey, (this.unsettled.get(userKey) ?? 0) + result.usage.cost_usd);
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
async createSpeech(params) {
|
|
351
|
+
const userKey = this.tenantOf(params.user);
|
|
352
|
+
if (userKey) {
|
|
353
|
+
const used = this.unsettled.get(userKey) ?? 0;
|
|
354
|
+
await this.auth.reserveBudget(userKey, used, this.opts.reserveUsd);
|
|
355
|
+
this.unsettled.set(userKey, 0);
|
|
356
|
+
}
|
|
357
|
+
const deadlineAt = Date.now() + (this.opts.budgetMs ?? DEFAULT_BUDGET_MS);
|
|
358
|
+
return this.withProviderRetry((runtimeKey) => callOpenRouterSpeech(runtimeKey, params, this.fetchImpl, this.retryConfig(deadlineAt)), userKey);
|
|
359
|
+
}
|
|
360
|
+
async api(method, path, body) {
|
|
361
|
+
const res = await this.fetchImpl(`${this.opts.baseUrl}${path}`, {
|
|
362
|
+
method,
|
|
363
|
+
headers: {
|
|
364
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
365
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
366
|
+
},
|
|
367
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
368
|
+
});
|
|
369
|
+
if (!res.ok) {
|
|
370
|
+
const err = (await res.json().catch(() => ({})));
|
|
371
|
+
throw new FantasticoError(err.message ?? `${method} ${path} fallita (${res.status})`, err.type ?? "REQUEST_FAILED", res.status);
|
|
372
|
+
}
|
|
373
|
+
return (await res.json());
|
|
374
|
+
}
|
|
375
|
+
async flushBudget(userKey) {
|
|
376
|
+
const used = this.unsettled.get(userKey) ?? 0;
|
|
377
|
+
if (used <= 0)
|
|
378
|
+
return;
|
|
379
|
+
await this.auth.reserveBudget(userKey, used, 0);
|
|
380
|
+
this.unsettled.set(userKey, 0);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const TENANT_ON_STATE = "__fantasticoUser";
|
|
384
|
+
function tenantFromState(rawState) {
|
|
385
|
+
const user = rawState?.[TENANT_ON_STATE];
|
|
386
|
+
return typeof user === "string" && user.length > 0 ? user : undefined;
|
|
387
|
+
}
|
|
388
|
+
function stampTenant(result, userKey) {
|
|
389
|
+
if (userKey === undefined || result.status !== "pending")
|
|
390
|
+
return result;
|
|
391
|
+
const state = result.state;
|
|
392
|
+
if (state !== null && typeof state === "object") {
|
|
393
|
+
state[TENANT_ON_STATE] = userKey;
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
function toolsToDefinitions(tools) {
|
|
398
|
+
const defs = [];
|
|
399
|
+
for (const svc of tools) {
|
|
400
|
+
if (svc.toolDefinitions) {
|
|
401
|
+
defs.push(...svc.toolDefinitions);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
for (const name of svc.allowedTools) {
|
|
405
|
+
defs.push({ type: "function", function: { name, description: `Tool ${svc.name}`, parameters: { type: "object", properties: {} } } });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return defs;
|
|
410
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { LocalToolDefinition } from "./local-tools.js";
|
|
2
|
+
import { type RetryConfig } from "./backoff.js";
|
|
3
|
+
/**
|
|
4
|
+
* Modelli "System One" (Jev di TypeSafe e affini, in alpha su OpenRouter a metà settembre 2026):
|
|
5
|
+
* non generano testo, restituiscono una **decisione tipizzata con probabilità** a partire da uno
|
|
6
|
+
* stato applicativo. Su OpenRouter non passano dalla `/chat/completions` che usa il resto
|
|
7
|
+
* dell'SDK (`llm.ts`, `raw.ts`): hanno un endpoint dedicato — `POST /api/alpha/decisions` — con un
|
|
8
|
+
* corpo diverso (`state` + `questions`, non `messages`) e non compaiono nel catalogo modelli
|
|
9
|
+
* standard (`ai.models.list()`). Questo file è il trasporto verso quell'endpoint; l'integrazione
|
|
10
|
+
* nel loop di tool-use di un LLM classico è `createDecisionTool` più sotto.
|
|
11
|
+
*
|
|
12
|
+
* **Verificato sul campo (2026-09-21, 0.15.1).** La 0.15.0 nasceva dalla documentazione pubblica e
|
|
13
|
+
* aveva la forma sbagliata: `bool` invece di `noul`, criteri di `choice` come array invece che come
|
|
14
|
+
* mappa, e una risposta `{ value, probability }` che l'endpoint non manda mai — `normalize…`
|
|
15
|
+
* scartava i campi veri e restituiva risposte vuote. Ora tipi e normalizzazione seguono le risposte
|
|
16
|
+
* reali di `typesafe/jev-1.13`. Resta un endpoint alpha: la forma può cambiare senza preavviso, e
|
|
17
|
+
* una risposta di tipo sconosciuto passa com'è invece di essere scartata.
|
|
18
|
+
*
|
|
19
|
+
* Id modello: `typesafe/jev-1.13` (versione fissata) o l'alias mobile `~typesafe/jev-latest`, con
|
|
20
|
+
* la tilde — `typesafe/jev-latest` non esiste.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Una domanda. `type` è un'unione CHIUSA, e di proposito: la 0.15.0 la teneva aperta e per questo
|
|
24
|
+
* nessun compilatore poteva accorgersi che `bool` non esiste (l'endpoint risponde 400).
|
|
25
|
+
*
|
|
26
|
+
* - `noul` — sì/no: la risposta è la probabilità che sia SÌ. I criteri sono opzionali.
|
|
27
|
+
* - `choice` — una scelta fra etichette. I criteri sono OBBLIGATORI, come mappa etichetta → descrizione.
|
|
28
|
+
* - `score` — una posizione su una scala ordinata. I criteri sono un array, dal livello più basso.
|
|
29
|
+
*/
|
|
30
|
+
export type DecisionQuestion = {
|
|
31
|
+
type: "noul";
|
|
32
|
+
instructions: string;
|
|
33
|
+
criteria?: {
|
|
34
|
+
true?: string;
|
|
35
|
+
false?: string;
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
type: "choice";
|
|
39
|
+
instructions: string;
|
|
40
|
+
criteria: Record<string, string>;
|
|
41
|
+
} | {
|
|
42
|
+
type: "score";
|
|
43
|
+
instructions: string;
|
|
44
|
+
criteria: string[];
|
|
45
|
+
};
|
|
46
|
+
/** Risposta a una domanda `noul`: `noul` è P(sì), da 0 a 1. */
|
|
47
|
+
export interface NoulAnswer {
|
|
48
|
+
type: "noul";
|
|
49
|
+
noul: number;
|
|
50
|
+
}
|
|
51
|
+
/** Risposta a una `choice`: l'etichetta più probabile e le probabilità di tutte. */
|
|
52
|
+
export interface ChoiceAnswer {
|
|
53
|
+
type: "choice";
|
|
54
|
+
choice: string;
|
|
55
|
+
probabilities: Record<string, number>;
|
|
56
|
+
confidence?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Risposta a uno `score`: continuo (media pesata sui livelli), non un intero — arrotondare prima di una soglia. */
|
|
59
|
+
export interface ScoreAnswer {
|
|
60
|
+
type: "score";
|
|
61
|
+
score: number;
|
|
62
|
+
probabilities: Record<string, number>;
|
|
63
|
+
confidence?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Il verdetto per una singola domanda: valore tipizzato con la sua probabilità, mai testo libero.
|
|
67
|
+
* Si distingue per `type`. Un tipo che l'SDK non conosce ancora passa com'è (`{ type: string, … }`).
|
|
68
|
+
*/
|
|
69
|
+
export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer | ({
|
|
70
|
+
type: string;
|
|
71
|
+
} & Record<string, unknown>);
|
|
72
|
+
export interface DecisionUsage {
|
|
73
|
+
/** Token in ingresso/uscita — l'endpoint li chiama `input_tokens`/`output_tokens`. */
|
|
74
|
+
prompt_tokens?: number;
|
|
75
|
+
completion_tokens?: number;
|
|
76
|
+
/** Costo reale in USD, come lo riporta OpenRouter — stessa fonte di `cost_usd` altrove
|
|
77
|
+
* nell'SDK, mai stimato lato client. */
|
|
78
|
+
cost_usd: number;
|
|
79
|
+
}
|
|
80
|
+
/** Risposta dell'endpoint Decisions: una per domanda posta, nessuna scelta implicita fra loro. */
|
|
81
|
+
export interface DecisionResult {
|
|
82
|
+
id: string;
|
|
83
|
+
/** Il modello che ha risposto, con la data di versione (`typesafe/jev-1.13-20260917`). */
|
|
84
|
+
model: string;
|
|
85
|
+
provider?: string;
|
|
86
|
+
answers: Record<string, DecisionAnswer>;
|
|
87
|
+
usage: DecisionUsage;
|
|
88
|
+
}
|
|
89
|
+
/** Richiesta a un modello System One: nessun `messages`, nessun `max_tokens` — non sono concetti
|
|
90
|
+
* che questa famiglia di modelli conosce. */
|
|
91
|
+
export interface EvaluateDecisionParams {
|
|
92
|
+
/** Fisso per questo tipo di modello: non esiste un default sensato come per la chat, la
|
|
93
|
+
* famiglia "System One" è volutamente ristretta e chi la chiama la sceglie a mano. */
|
|
94
|
+
model: string;
|
|
95
|
+
/** Lo stato applicativo su cui decidere — dati, non prompt: nessuna istruzione va qui dentro,
|
|
96
|
+
* le istruzioni stanno in {@link DecisionQuestion.instructions}. */
|
|
97
|
+
state: Record<string, unknown>;
|
|
98
|
+
questions: Record<string, DecisionQuestion>;
|
|
99
|
+
/** Identità del tenant finale, come `user` in `chat.completions.create()`: se presente, il
|
|
100
|
+
* costo di questa decisione è attribuito e limitato per quel conto. */
|
|
101
|
+
user?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Una singola chiamata all'endpoint Decisions — pass-through quasi totale, stessa forma di
|
|
105
|
+
* `callOpenRouterRaw` in `raw.ts`: nessuna orchestrazione, un ritentativo sui soli errori
|
|
106
|
+
* transitori (`backoff.ts`), un errore tipizzato (`provider-error.ts`) che `client.ts` può usare
|
|
107
|
+
* per decidere se ri-coniare.
|
|
108
|
+
*/
|
|
109
|
+
export declare function callOpenRouterDecision(runtimeKey: string, params: EvaluateDecisionParams, fetchImpl: typeof fetch, retry?: RetryConfig | false): Promise<DecisionResult>;
|
|
110
|
+
/**
|
|
111
|
+
* Configura un tool locale che, chiamato dal modello **conversazionale** che sta orchestrando
|
|
112
|
+
* (Claude, GPT, ... via `chat.completions.create()`), delega la decisione a un modello System
|
|
113
|
+
* One come Jev — restando dentro lo stesso loop e la stessa runtime key/conto.
|
|
114
|
+
*
|
|
115
|
+
* **`questions` e `model` sono fissi per questo tool, non li sceglie il modello che lo chiama.**
|
|
116
|
+
* Il modello conversazionale può decidere *quando* chiamare il tool e *quali fatti* passargli
|
|
117
|
+
* (`parameters`), non *come* Jev deve rispondere: la forma del verdetto (bool/choice/score) è
|
|
118
|
+
* decisa da chi integra, in anticipo — esattamente come un `LocalToolDefinition` normale non
|
|
119
|
+
* lascia il modello scegliere quale funzione locale gira dietro un nome.
|
|
120
|
+
*
|
|
121
|
+
* **`state` non sono gli argomenti del modello per default fidato: è una scelta esplicita.**
|
|
122
|
+
* Il default (nessun `state` passato) manda a Jev esattamente gli argomenti che il modello ha
|
|
123
|
+
* generato — comodo per prototipare, ma un modello può sbagliare un campo o ometterne uno. Chi
|
|
124
|
+
* ha dati fidati da aggiungere (un ruolo utente, un flag di piano) li inietta qui dal `context`,
|
|
125
|
+
* mai generato dal modello — stessa regola di `LocalToolDefinition.execute`.
|
|
126
|
+
*/
|
|
127
|
+
export interface DecisionToolConfig<TContext = unknown> {
|
|
128
|
+
name: string;
|
|
129
|
+
description: string;
|
|
130
|
+
model: string;
|
|
131
|
+
questions: Record<string, DecisionQuestion>;
|
|
132
|
+
/** JSON Schema (function-calling) degli argomenti che il modello conversazionale fornisce. */
|
|
133
|
+
parameters: Record<string, unknown>;
|
|
134
|
+
/** Costruisce lo `state` da mandare a Jev. Default: gli argomenti del modello, invariati. */
|
|
135
|
+
state?: (args: Record<string, unknown>, context: TContext) => Record<string, unknown>;
|
|
136
|
+
/** Il conto a cui attribuire il costo di questa decisione. Default: nessuno (conto del
|
|
137
|
+
* proprietario, come ogni chiamata senza `user`). */
|
|
138
|
+
user?: (context: TContext) => string | undefined;
|
|
139
|
+
sequential?: boolean;
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
available?: (context: TContext) => boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Costruisce il `LocalToolDefinition` da registrare con `ai.addTool(...)` o nel costruttore
|
|
145
|
+
* (`tools: [...]`). `evaluate` è l'unico punto di contatto col client FANTASTICO: chi chiama
|
|
146
|
+
* questa funzione non sa nulla del conio o della runtime key, solo che `evaluate` restituisce un
|
|
147
|
+
* {@link DecisionResult} pagato sul conto giusto — è `client.ts` a passare quella chiusura.
|
|
148
|
+
*/
|
|
149
|
+
export declare function createDecisionTool<TContext = unknown>(config: DecisionToolConfig<TContext>, evaluate: (params: EvaluateDecisionParams) => Promise<DecisionResult>): LocalToolDefinition<TContext>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { providerErrorFromResponse, errorInBody } from "./provider-error.js";
|
|
2
|
+
import { withTransientRetry } from "./backoff.js";
|
|
3
|
+
export async function callOpenRouterDecision(runtimeKey, params, fetchImpl, retry = {}) {
|
|
4
|
+
const body = {
|
|
5
|
+
model: params.model,
|
|
6
|
+
state: params.state,
|
|
7
|
+
questions: params.questions,
|
|
8
|
+
};
|
|
9
|
+
return withTransientRetry(async () => {
|
|
10
|
+
const res = await fetchImpl("https://openrouter.ai/api/alpha/decisions", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${runtimeKey}`,
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"HTTP-Referer": "https://fantastico.cc",
|
|
16
|
+
"X-Title": "FANTASTICO SDK",
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
throw await providerErrorFromResponse(res, "OpenRouter Decisions");
|
|
22
|
+
const payload = await res.json();
|
|
23
|
+
const embedded = errorInBody(payload, "OpenRouter Decisions");
|
|
24
|
+
if (embedded)
|
|
25
|
+
throw embedded;
|
|
26
|
+
return normalizeDecisionResponse(payload, params.model);
|
|
27
|
+
}, retry);
|
|
28
|
+
}
|
|
29
|
+
function normalizeDecisionResponse(payload, requestedModel) {
|
|
30
|
+
const p = payload;
|
|
31
|
+
const answers = {};
|
|
32
|
+
for (const [name, a] of Object.entries(p.answers ?? {})) {
|
|
33
|
+
if (a && typeof a === "object" && typeof a.type === "string") {
|
|
34
|
+
answers[name] = a;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id: p.id ?? "",
|
|
39
|
+
model: p.model ?? requestedModel,
|
|
40
|
+
...(p.provider ? { provider: p.provider } : {}),
|
|
41
|
+
answers,
|
|
42
|
+
usage: {
|
|
43
|
+
prompt_tokens: p.usage?.input_tokens ?? p.usage?.prompt_tokens,
|
|
44
|
+
completion_tokens: p.usage?.output_tokens ?? p.usage?.completion_tokens,
|
|
45
|
+
cost_usd: p.usage?.cost ?? 0,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function createDecisionTool(config, evaluate) {
|
|
50
|
+
return {
|
|
51
|
+
type: "function",
|
|
52
|
+
function: {
|
|
53
|
+
name: config.name,
|
|
54
|
+
description: config.description,
|
|
55
|
+
parameters: config.parameters,
|
|
56
|
+
},
|
|
57
|
+
sequential: config.sequential,
|
|
58
|
+
timeoutMs: config.timeoutMs,
|
|
59
|
+
available: config.available,
|
|
60
|
+
execute: async (args, context) => {
|
|
61
|
+
const state = config.state ? config.state(args, context) : args;
|
|
62
|
+
const result = await evaluate({
|
|
63
|
+
model: config.model,
|
|
64
|
+
state,
|
|
65
|
+
questions: config.questions,
|
|
66
|
+
user: config.user?.(context),
|
|
67
|
+
});
|
|
68
|
+
return result.answers;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Fantastico } from "./client.js";
|
|
2
|
+
export { parseToolOriginMap, resolveToolEndpoint, toolOriginMapFromEnv, type ToolOriginMap } from "./tool-origin.js";
|
|
3
|
+
export { FantasticoError, EmptyCompletionError } from "./types.js";
|
|
4
|
+
export { ProviderError } from "./provider-error.js";
|
|
5
|
+
export type { FantasticoOptions, ChatCompletionCreateParams, ReasoningConfig, ReasoningEffort, ChatCompletion, ChatCompletionResult, ChatMessage, CompletionUsage, ChatCompletionChoice, ToolDefinition, ToolCall, ChatContentPart, RawChatMessage, RawCompletionCreateParams, RawChatCompletion, AccountCreateParams, AccountUpdateParams, AccountInfo, AccountKeyInfo, IssuedAccountKey, RuntimeKeyInfo, RuntimeKeyHandle, ContoBalance, ClientAction, PendingToolCall, PendingCompletion, ResumeDecision, OpenRouterModel, } from "./types.js";
|
|
6
|
+
export type { LocalToolDefinition, ClientActionToolDefinition, RegisteredTool } from "./local-tools.js";
|
|
7
|
+
export { LocalToolRegistry } from "./local-tools.js";
|
|
8
|
+
export type { DecisionQuestion, DecisionAnswer, NoulAnswer, ChoiceAnswer, ScoreAnswer, DecisionUsage, DecisionResult, EvaluateDecisionParams, DecisionToolConfig, } from "./decisions.js";
|
|
9
|
+
export type { SpeechCreateParams, SpeechTurn, SpeechResult, SpeechModel } from "./speech.js";
|
|
10
|
+
export { FantasticoActions } from "./browser-actions.js";
|
|
11
|
+
export { writeActivityStream, ACTIVITY_STREAM_CONTENT_TYPE } from "./activity.js";
|
|
12
|
+
export type { ChatActivityEvent, ChatActivityListener, ChatActivityOptions } from "./activity.js";
|
|
13
|
+
export type { Surface } from "./surface.js";
|
|
14
|
+
export type { ClientActionHandler } from "./browser-actions.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Fantastico } from "./client.js";
|
|
2
|
+
export { parseToolOriginMap, resolveToolEndpoint, toolOriginMapFromEnv } from "./tool-origin.js";
|
|
3
|
+
export { FantasticoError, EmptyCompletionError } from "./types.js";
|
|
4
|
+
export { ProviderError } from "./provider-error.js";
|
|
5
|
+
export { LocalToolRegistry } from "./local-tools.js";
|
|
6
|
+
export { FantasticoActions } from "./browser-actions.js";
|
|
7
|
+
export { writeActivityStream, ACTIVITY_STREAM_CONTENT_TYPE } from "./activity.js";
|