@byte-42/fantastico-sdk 0.0.0-stage → 0.22.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 +484 -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 +742 -0
- package/dist/types.js +27 -0
- package/package.json +48 -6
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { type ToolOriginMap } from "./tool-origin.js";
|
|
2
|
+
import type { ContoBalance, RuntimeKeyHandle, ToolConfig } from "./types.js";
|
|
3
|
+
export interface AuthClientOptions {
|
|
4
|
+
/** La credenziale verso il Cloud: project key `sk-fant-…` o chiave di conto `ck-fant-…`. */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Base url comprensivo di `/api`. */
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
/** La sessione di attività da continuare, se il chiamante ne aveva una. Vedi il campo omonimo. */
|
|
9
|
+
activitySessionId?: string;
|
|
10
|
+
/** Il prodotto del marketplace che questo client integra, mandato a ogni conio. Vedi
|
|
11
|
+
* `FantasticoOptions.product`. */
|
|
12
|
+
product?: string;
|
|
13
|
+
/** Origini pubbliche dei tool → origini interne. Vedi `tool-origin.ts`. */
|
|
14
|
+
toolOriginMap?: ToolOriginMap;
|
|
15
|
+
fetchImpl: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
/** Un tool raggiungibile e la credenziale con cui parlarci, dal conio. */
|
|
18
|
+
export interface ToolGrant {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
endpoint: string;
|
|
22
|
+
/** Cambia quando cambiano i tool: e' la chiave di cache degli schemi. `null` = non dichiarata. */
|
|
23
|
+
schemaVersion: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Il digest di cio' che il control plane ha registrato per questo tool.
|
|
26
|
+
*
|
|
27
|
+
* Arriva col conio, cioe' per una strada diversa da quella con cui arrivera' il documento; e' il
|
|
28
|
+
* confronto fra le due che vale qualcosa. `null` non dovrebbe capitare — un tool senza schema non
|
|
29
|
+
* riceve un token, quindi non produce una concessione — e se capita si tratta come "non
|
|
30
|
+
* verificabile", che ha lo stesso esito di "non corrisponde".
|
|
31
|
+
*/
|
|
32
|
+
schemaHash: string | null;
|
|
33
|
+
/** Il segreto. Torna una volta sola dal conio e non e' recuperabile da nessuna parte. */
|
|
34
|
+
token: string;
|
|
35
|
+
}
|
|
36
|
+
/** Esito di una prenotazione di budget per un tenant. */
|
|
37
|
+
export interface BudgetResult {
|
|
38
|
+
granted: number;
|
|
39
|
+
remaining: number;
|
|
40
|
+
lowBalance: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Client verso il control plane FANTASTICO.
|
|
44
|
+
*
|
|
45
|
+
* Una sola credenziale, e la sceglie chi costruisce: project key `sk-fant-…` (superficie
|
|
46
|
+
* `/api/runtime`) o chiave di conto `ck-fant-…` (superficie `/api/edge`). In nessuno dei due casi
|
|
47
|
+
* entra un segreto di piattaforma nel perimetro del cliente. Tiene le runtime key OpenRouter in
|
|
48
|
+
* memoria con rinnovo prima della scadenza — mai su disco, mai nei log — **una per conto**.
|
|
49
|
+
*/
|
|
50
|
+
export declare class AuthClient {
|
|
51
|
+
private readonly opts;
|
|
52
|
+
/**
|
|
53
|
+
* Una chiave viva **per conto**, non una per client — e la chiave della mappa è la `userKey`
|
|
54
|
+
* del tenant, `""` per il conto che la credenziale sottintende (il proprietario del progetto
|
|
55
|
+
* su `/runtime`, il conto della credenziale su `/edge`).
|
|
56
|
+
*
|
|
57
|
+
* **È il conto che paga, quindi è il conto che conia.** Un conio apre un *hold* sul conto che
|
|
58
|
+
* nomina, quindi ogni tenant ha la propria: una key, una sessione e dei tool token per
|
|
59
|
+
* ciascuno.
|
|
60
|
+
*
|
|
61
|
+
* Non è una cache che cresce senza fine nel senso che preoccupa: una voce tiene una chiave che
|
|
62
|
+
* scade da sola, e il `maxOpenKeys` del piano è **per conto**, quindi un tenant in più non
|
|
63
|
+
* consuma gli slot degli altri.
|
|
64
|
+
*/
|
|
65
|
+
private readonly accounts;
|
|
66
|
+
/** Listino del provider, per {@link outputPricePerToken}. Vedi il TTL li accanto.
|
|
67
|
+
* Uno solo per client: il prezzo di un modello non dipende da chi paga. */
|
|
68
|
+
private models;
|
|
69
|
+
/** Schemi gia' scaricati, per `${tool}@${versione}`: la versione e' il motivo per cui il
|
|
70
|
+
* Cloud la manda, e cambia esattamente quando cambiano i tool. */
|
|
71
|
+
private schemas;
|
|
72
|
+
/**
|
|
73
|
+
* Quale delle due superfici parlare, **dedotta dalla chiave e non ricevuta**.
|
|
74
|
+
*
|
|
75
|
+
* Cambia il prefisso delle rotte e nient'altro: `/runtime/key` e `/edge/key` portano allo stesso
|
|
76
|
+
* `ledger.mint` e rispondono la stessa forma. Quel che cambia davvero è **chi nomina il conto**:
|
|
77
|
+
* su `/runtime` lo nomina il chiamante — la project key vale per tutti i conti del progetto,
|
|
78
|
+
* quindi il conio deve dire quale paga — mentre su `/edge` il conto è quello della credenziale e
|
|
79
|
+
* nel corpo non c'è un campo di identità da riempire, né deve essercene uno.
|
|
80
|
+
*
|
|
81
|
+
* Dedotta qui e non passata dal costruttore perché `surfaceOf` è una funzione pura della stessa
|
|
82
|
+
* stringa: chiamarla due volte dà due volte lo stesso valore, mentre un parametro può portarne
|
|
83
|
+
* uno che non corrisponde alla chiave con cui viaggia.
|
|
84
|
+
*/
|
|
85
|
+
private readonly surface;
|
|
86
|
+
constructor(opts: AuthClientOptions);
|
|
87
|
+
/**
|
|
88
|
+
* Lo stato del conto nominato, creandolo vuoto la prima volta. `undefined` = il conto che la
|
|
89
|
+
* credenziale sottintende.
|
|
90
|
+
*/
|
|
91
|
+
private state;
|
|
92
|
+
private headers;
|
|
93
|
+
/**
|
|
94
|
+
* L'url di una rotta del control plane sulla superficie di questa credenziale.
|
|
95
|
+
*
|
|
96
|
+
* `/runtime` ed `/edge` sono anche i due valori di {@link Surface}, e non è una coincidenza da
|
|
97
|
+
* cui dipendere per caso: le due superfici portano le stesse rotte con lo stesso nome
|
|
98
|
+
* (`/key`, `/keys`, `/models`), e a cambiare è chi nomina il conto. Le poche che esistono solo
|
|
99
|
+
* di qua o solo di là non passano da qui — hanno un metodo loro che dice perché.
|
|
100
|
+
*/
|
|
101
|
+
private url;
|
|
102
|
+
/**
|
|
103
|
+
* Runtime key OpenRouter **del conto indicato**: conia (o riusa quella in memoria) via
|
|
104
|
+
* `POST <superficie>/key`.
|
|
105
|
+
*
|
|
106
|
+
* `userKey` è il tenant a cui la chiamata è attribuita — il campo `user` di
|
|
107
|
+
* `chat.completions.create`. Omesso, è il conto che la credenziale sottintende. La chiave
|
|
108
|
+
* coniata **impegna il conto che nomina**, quindi passarlo o no decide chi paga.
|
|
109
|
+
*/
|
|
110
|
+
getRuntimeKey(userKey?: string): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Butta via la runtime key in mano (es. dopo un 401 da OpenRouter): il prossimo giro ne conia
|
|
113
|
+
* un'altra.
|
|
114
|
+
*
|
|
115
|
+
* **Non c'è niente da dire al Cloud, e prima si diceva.** Qui partiva un `force: true` nel corpo
|
|
116
|
+
* del conio, per saltare una cache di riuso che il vecchio servizio `vending` teneva: senza, il
|
|
117
|
+
* Cloud avrebbe restituito la STESSA chiave appena scartata. Il ledger non ha quella cache —
|
|
118
|
+
* ogni conio apre un hold suo — e `ledger.mint` non dichiara affatto quel parametro, quindi il
|
|
119
|
+
* campo viaggiava e nessuno lo leggeva. Quel che conta è tutto qui: senza la copia locale, il
|
|
120
|
+
* prossimo `getRuntimeKey()` chiede davvero.
|
|
121
|
+
*/
|
|
122
|
+
invalidate(userKey?: string): void;
|
|
123
|
+
/** I tool token in mano per quel conto, gia' con endpoint e versione dello schema. Vuoto
|
|
124
|
+
* prima del conio. */
|
|
125
|
+
getToolGrants(userKey?: string): readonly ToolGrant[];
|
|
126
|
+
/**
|
|
127
|
+
* Il tetto in USD della runtime key in mano per quel conto, o `null` se non se ne è ancora
|
|
128
|
+
* coniata una. Da leggere subito dopo `getRuntimeKey()`, **con lo stesso conto**: due tenant
|
|
129
|
+
* hanno due chiavi e due tetti, e leggere quello sbagliato darebbe un `max_tokens` calcolato
|
|
130
|
+
* su una chiave che non sta per essere usata.
|
|
131
|
+
*/
|
|
132
|
+
getKeyLimitUsd(userKey?: string): number | null;
|
|
133
|
+
/**
|
|
134
|
+
* Cosa si sta usando, senza il segreto: handle, tetto, impegno, scadenza e sessione.
|
|
135
|
+
*
|
|
136
|
+
* `null` finché non si è coniato — e chiamarlo prima del primo conio è la cosa da non fare:
|
|
137
|
+
* `client.ts` lo legge sempre dopo un `getRuntimeKey()`, che è ciò che rende il `null`
|
|
138
|
+
* impossibile al chiamante invece che soltanto improbabile.
|
|
139
|
+
*/
|
|
140
|
+
getHandle(userKey?: string): RuntimeKeyHandle | null;
|
|
141
|
+
/** L'handle che porta questo `recordId`, se è uno di quelli in mano — e il conto a cui
|
|
142
|
+
* appartiene. Serve a `runtimeKeys.revoke`, che nomina una chiave e non un conto. */
|
|
143
|
+
findByRecordId(recordId: string): {
|
|
144
|
+
userKey: string;
|
|
145
|
+
} | null;
|
|
146
|
+
/** La sessione di attività corrente di quel conto, o `null` prima del suo primo conio. Non è
|
|
147
|
+
* una credenziale: non autorizza niente e si può loggare e mostrare. Serve a raggruppare la
|
|
148
|
+
* spesa a valle. */
|
|
149
|
+
getActivitySessionId(userKey?: string): string | null;
|
|
150
|
+
/**
|
|
151
|
+
* Il saldo del proprio conto (`GET /api/edge/balance`) — **solo sulla superficie edge**.
|
|
152
|
+
*
|
|
153
|
+
* È il gemello di `reserveBudget` per una credenziale che non può nominare un conto: la
|
|
154
|
+
* domanda "quanto resta" è la stessa, ma qui non c'è niente da prenotare e niente da regolare,
|
|
155
|
+
* perché non c'è un tenant a cui attribuire. Il blocco vero resta il conio.
|
|
156
|
+
*/
|
|
157
|
+
balance(): Promise<ContoBalance>;
|
|
158
|
+
/**
|
|
159
|
+
* Regola il consumo di un tenant e prenota il budget successivo (`/api/runtime/budget`).
|
|
160
|
+
* Se il tenant o il progetto sono a secco, il Cloud risponde 402 e qui si solleva un
|
|
161
|
+
* FantasticoError **prima** che parta la chiamata LLM.
|
|
162
|
+
*/
|
|
163
|
+
reserveBudget(userKey: string, usedUsd: number, requestUsd: number): Promise<BudgetResult>;
|
|
164
|
+
/**
|
|
165
|
+
* I tool di questa sessione: **uno per concessione**, chiesti a ciascun tool.
|
|
166
|
+
*
|
|
167
|
+
* **Non c'e piu un ripiego**. Sotto c'era `legacyCatalogTools`: un solo
|
|
168
|
+
* "tool" sintetico chiamato `fantastico` che leggeva `GET /api/tools` e instradava ogni
|
|
169
|
+
* esecuzione su `POST /api/tools/execute` con la project key. Quelle due rotte non esistono
|
|
170
|
+
* piu, e il ripiego non era neutro — **saltava la verifica**: il catalogo lo componeva il
|
|
171
|
+
* Cloud, quindi non c'era un documento firmato di cui ricalcolare il digest, e le descrizioni
|
|
172
|
+
* che finivano davanti al modello arrivavano senza che niente ne rispondesse.
|
|
173
|
+
*
|
|
174
|
+
* **Nessuna concessione significa nessun tool, e va detto cosi.** Prima significava "prova
|
|
175
|
+
* dall'altra parte", quindi un conio senza token — un piano che non prezza un tratto di
|
|
176
|
+
* lavoro, un progetto che non ha abilitato niente — si vedeva solo come una chat che
|
|
177
|
+
* funzionava un po' peggio. Ora e un elenco vuoto, che e la verita.
|
|
178
|
+
*
|
|
179
|
+
* **Non conia.** La discovery e' una lettura, e restituisce quello che l'ultimo conio ha
|
|
180
|
+
* portato: chi ha bisogno dei token si assicura la chiave **prima** di chiamare qui, ed e'
|
|
181
|
+
* `client.ts` a farlo in un posto solo (`remoteTools`). Se coniasse anche questo, ci sarebbero
|
|
182
|
+
* due punti da cui parte un conio, e il secondo sarebbe quello che nessuno si aspetta.
|
|
183
|
+
*/
|
|
184
|
+
getTools(userKey?: string): Promise<ToolConfig[]>;
|
|
185
|
+
/**
|
|
186
|
+
* Chiede a UN tool i propri tool, presentando il token emesso per lui.
|
|
187
|
+
*
|
|
188
|
+
* **Lo schema lo dice il tool, non il control plane.** Il ledger tiene endpoint, listino e
|
|
189
|
+
* il *digest* dello schema, mai le descrizioni: sono dati del tool, e una copia nel control
|
|
190
|
+
* plane sarebbe una seconda fonte di verita' per qualcosa che non puo' validare. In piu' una
|
|
191
|
+
* descrizione di tool e' testo che finisce nel contesto di un modello, quindi deve venire da chi
|
|
192
|
+
* ne risponde.
|
|
193
|
+
*
|
|
194
|
+
* **Un tool che non risponde esce dal catalogo di questo giro, e basta.** Il degrado e' lo
|
|
195
|
+
* stesso che questo metodo ha sempre avuto verso il Cloud: N endpoint non devono diventare N modi
|
|
196
|
+
* nuovi di far fallire una chat che senza tool funzionerebbe.
|
|
197
|
+
*/
|
|
198
|
+
private describeTool;
|
|
199
|
+
/**
|
|
200
|
+
* Il prezzo in USD di **un** token in uscita per un modello, dal catalogo del provider.
|
|
201
|
+
*
|
|
202
|
+
* Serve solo a `max-tokens.ts`, ed e' per questo che sta qui accanto al tetto della chiave: i due
|
|
203
|
+
* numeri hanno senso solo insieme, e chiederli a due posti diversi era il motivo per cui il conto
|
|
204
|
+
* non veniva fatto affatto.
|
|
205
|
+
*
|
|
206
|
+
* **Best-effort per costruzione**: se il catalogo non risponde si torna `null`, che a valle
|
|
207
|
+
* significa "non scrivere `max_tokens`" — cioe' esattamente il comportamento di prima. Un
|
|
208
|
+
* arricchimento che fallisce deve togliere un miglioramento, mai una chiamata.
|
|
209
|
+
*
|
|
210
|
+
* Cache in memoria con TTL, come quella che il Cloud tiene davanti alla stessa rotta: il
|
|
211
|
+
* listino non cambia fra due messaggi di una chat, e senza cache questo aggiungerebbe un giro di
|
|
212
|
+
* rete a ogni conio.
|
|
213
|
+
*/
|
|
214
|
+
outputPricePerToken(model: string): Promise<number | null>;
|
|
215
|
+
private mint;
|
|
216
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { resolveToolEndpoint } from "./tool-origin.js";
|
|
2
|
+
import { schemaDigest } from "./schema-digest.js";
|
|
3
|
+
import { wireTool } from "./tool-definition.js";
|
|
4
|
+
import { FantasticoError } from "./types.js";
|
|
5
|
+
import { surfaceOf } from "./surface.js";
|
|
6
|
+
const MODELS_TTL_MS = 5 * 60_000;
|
|
7
|
+
export class AuthClient {
|
|
8
|
+
opts;
|
|
9
|
+
accounts = new Map();
|
|
10
|
+
models = null;
|
|
11
|
+
schemas = new Map();
|
|
12
|
+
surface;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
this.surface = surfaceOf(opts.apiKey);
|
|
16
|
+
this.state().activitySessionId = opts.activitySessionId ?? null;
|
|
17
|
+
}
|
|
18
|
+
state(userKey) {
|
|
19
|
+
const id = userKey ?? "";
|
|
20
|
+
let s = this.accounts.get(id);
|
|
21
|
+
if (s === undefined) {
|
|
22
|
+
s = { runtimeKey: null, mintPromise: null, activitySessionId: null, tools: [] };
|
|
23
|
+
this.accounts.set(id, s);
|
|
24
|
+
}
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
headers(json = false) {
|
|
28
|
+
const h = { Authorization: `Bearer ${this.opts.apiKey}` };
|
|
29
|
+
if (json)
|
|
30
|
+
h["Content-Type"] = "application/json";
|
|
31
|
+
return h;
|
|
32
|
+
}
|
|
33
|
+
url(path) {
|
|
34
|
+
return `${this.opts.baseUrl}/${this.surface}${path}`;
|
|
35
|
+
}
|
|
36
|
+
async getRuntimeKey(userKey) {
|
|
37
|
+
const st = this.state(userKey);
|
|
38
|
+
const skew = 60_000;
|
|
39
|
+
if (st.runtimeKey && st.runtimeKey.expiresAt - Date.now() > skew) {
|
|
40
|
+
return st.runtimeKey.key;
|
|
41
|
+
}
|
|
42
|
+
if (st.mintPromise)
|
|
43
|
+
return (await st.mintPromise).key;
|
|
44
|
+
st.mintPromise = this.mint(userKey).finally(() => {
|
|
45
|
+
st.mintPromise = null;
|
|
46
|
+
});
|
|
47
|
+
const fresh = await st.mintPromise;
|
|
48
|
+
st.runtimeKey = fresh;
|
|
49
|
+
return fresh.key;
|
|
50
|
+
}
|
|
51
|
+
invalidate(userKey) {
|
|
52
|
+
const st = this.state(userKey);
|
|
53
|
+
st.runtimeKey = null;
|
|
54
|
+
st.tools = [];
|
|
55
|
+
}
|
|
56
|
+
getToolGrants(userKey) {
|
|
57
|
+
return this.state(userKey).tools;
|
|
58
|
+
}
|
|
59
|
+
getKeyLimitUsd(userKey) {
|
|
60
|
+
return this.state(userKey).runtimeKey?.limitUsd ?? null;
|
|
61
|
+
}
|
|
62
|
+
getHandle(userKey) {
|
|
63
|
+
const st = this.state(userKey);
|
|
64
|
+
const k = st.runtimeKey;
|
|
65
|
+
if (k === null)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
recordId: k.recordId,
|
|
69
|
+
limitUsd: k.limitUsd,
|
|
70
|
+
costUsd: k.costUsd,
|
|
71
|
+
expiresAt: new Date(k.expiresAt).toISOString(),
|
|
72
|
+
activitySessionId: st.activitySessionId,
|
|
73
|
+
tools: st.tools.map((g) => ({ id: g.id, name: g.name })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
findByRecordId(recordId) {
|
|
77
|
+
for (const [userKey, st] of this.accounts) {
|
|
78
|
+
if (st.runtimeKey?.recordId === recordId)
|
|
79
|
+
return { userKey };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
getActivitySessionId(userKey) {
|
|
84
|
+
return this.state(userKey).activitySessionId;
|
|
85
|
+
}
|
|
86
|
+
async balance() {
|
|
87
|
+
const res = await this.opts.fetchImpl(this.url("/balance"), {
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: this.headers(),
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const body = (await res.json().catch(() => ({})));
|
|
93
|
+
throw new FantasticoError(body.message ?? `Saldo non disponibile (${res.status})`, body.type ?? "BALANCE_FAILED", res.status);
|
|
94
|
+
}
|
|
95
|
+
return (await res.json());
|
|
96
|
+
}
|
|
97
|
+
async reserveBudget(userKey, usedUsd, requestUsd) {
|
|
98
|
+
const res = await this.opts.fetchImpl(`${this.opts.baseUrl}/runtime/budget`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: this.headers(true),
|
|
101
|
+
body: JSON.stringify({ userKey, used: usedUsd, request: requestUsd }),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const body = (await res.json().catch(() => ({})));
|
|
105
|
+
throw new FantasticoError(body.message ?? `Budget negato (${res.status})`, body.type ?? "BUDGET_DENIED", res.status);
|
|
106
|
+
}
|
|
107
|
+
const data = (await res.json());
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
async getTools(userKey) {
|
|
111
|
+
const grants = this.getToolGrants(userKey);
|
|
112
|
+
if (grants.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
const configs = await Promise.all(grants.map((g) => this.describeTool(g)));
|
|
115
|
+
return configs.filter((c) => c !== null);
|
|
116
|
+
}
|
|
117
|
+
async describeTool(grant) {
|
|
118
|
+
const cacheKey = `${grant.id}@${grant.schemaVersion ?? "?"}`;
|
|
119
|
+
const cached = grant.schemaVersion === null ? undefined : this.schemas.get(cacheKey);
|
|
120
|
+
let tools = cached;
|
|
121
|
+
if (tools === undefined) {
|
|
122
|
+
try {
|
|
123
|
+
const res = await this.opts.fetchImpl(resolveToolEndpoint(grant.endpoint, this.opts.toolOriginMap), {
|
|
124
|
+
method: "GET",
|
|
125
|
+
headers: { Authorization: `Bearer ${grant.token}` },
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok)
|
|
128
|
+
return null;
|
|
129
|
+
const body = (await res.json().catch(() => ({})));
|
|
130
|
+
if (body.schema === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
const digest = await schemaDigest(body.schema);
|
|
133
|
+
if (digest === null || grant.schemaHash === null || digest !== grant.schemaHash) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
tools = body.schema.functions ?? [];
|
|
137
|
+
if (grant.schemaVersion !== null)
|
|
138
|
+
this.schemas.set(cacheKey, tools);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (tools.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
name: grant.id,
|
|
148
|
+
url: `${resolveToolEndpoint(grant.endpoint, this.opts.toolOriginMap)}/execute`,
|
|
149
|
+
authToken: grant.token,
|
|
150
|
+
allowedTools: tools.map((t) => t.function.name),
|
|
151
|
+
toolDefinitions: tools.map(wireTool),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async outputPricePerToken(model) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (this.models === null || now - this.models.at > MODELS_TTL_MS) {
|
|
157
|
+
try {
|
|
158
|
+
const res = await this.opts.fetchImpl(this.url("/models"), {
|
|
159
|
+
method: "GET",
|
|
160
|
+
headers: this.headers(),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
return null;
|
|
164
|
+
const body = (await res.json());
|
|
165
|
+
const byId = new Map();
|
|
166
|
+
for (const m of body.models ?? []) {
|
|
167
|
+
const price = Number(m.pricing?.completion);
|
|
168
|
+
if (m.id !== undefined && Number.isFinite(price) && price > 0)
|
|
169
|
+
byId.set(m.id, price);
|
|
170
|
+
}
|
|
171
|
+
this.models = { at: now, byId };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.models.byId.get(model) ?? null;
|
|
178
|
+
}
|
|
179
|
+
async mint(userKey) {
|
|
180
|
+
const st = this.state(userKey);
|
|
181
|
+
const sent = st.activitySessionId;
|
|
182
|
+
const named = this.surface === "runtime" && userKey !== undefined ? { userKey } : {};
|
|
183
|
+
const bought = this.opts.product === undefined ? {} : { product: this.opts.product };
|
|
184
|
+
const res = await this.opts.fetchImpl(this.url("/key"), {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: this.headers(true),
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
...named,
|
|
189
|
+
...bought,
|
|
190
|
+
...(sent === null ? {} : { activitySessionId: sent }),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const body = (await res.json().catch(() => ({})));
|
|
195
|
+
throw new FantasticoError(body.message ?? `Conio runtime key fallito (${res.status})`, body.type ?? "MINT_FAILED", res.status);
|
|
196
|
+
}
|
|
197
|
+
const data = (await res.json());
|
|
198
|
+
if (data.activitySessionId !== undefined) {
|
|
199
|
+
if (sent !== null && sent !== data.activitySessionId) {
|
|
200
|
+
console.warn(`[fantastico] sessione di attività sostituita dal ledger: ${sent} -> ` +
|
|
201
|
+
`${data.activitySessionId}. La spesa precedente resta raggruppata sotto la vecchia.`);
|
|
202
|
+
}
|
|
203
|
+
st.activitySessionId = data.activitySessionId;
|
|
204
|
+
}
|
|
205
|
+
const limitUsd = data.limit == null ? null : Number(data.limit);
|
|
206
|
+
st.tools = (data.tools ?? [])
|
|
207
|
+
.filter((svc) => typeof svc.endpoint === "string")
|
|
208
|
+
.map((svc) => ({
|
|
209
|
+
id: svc.id,
|
|
210
|
+
name: svc.name ?? svc.id,
|
|
211
|
+
endpoint: svc.endpoint,
|
|
212
|
+
schemaVersion: svc.schemaVersion ?? null,
|
|
213
|
+
schemaHash: typeof svc.schemaHash === "string" ? svc.schemaHash : null,
|
|
214
|
+
token: svc.token,
|
|
215
|
+
}));
|
|
216
|
+
return {
|
|
217
|
+
key: data.runtimeKey,
|
|
218
|
+
expiresAt: new Date(data.expiresAt).getTime(),
|
|
219
|
+
recordId: data.recordId,
|
|
220
|
+
limitUsd: limitUsd !== null && Number.isFinite(limitUsd) ? limitUsd : null,
|
|
221
|
+
costUsd: data.cost == null ? null : String(data.cost),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quanto insistere su un errore che passa da solo (429, provider sovraccarico).
|
|
3
|
+
*
|
|
4
|
+
* `false` la spegne del tutto. I default sono pensati per una chiamata dentro una richiesta HTTP:
|
|
5
|
+
* due ritentativi, qualche secondo in tutto, mai più di dieci per singola attesa.
|
|
6
|
+
*/
|
|
7
|
+
export interface RetryOptions {
|
|
8
|
+
/** Quanti **ritentativi** dopo il primo tentativo. `0` equivale a spegnerla. Default 2. */
|
|
9
|
+
attempts?: number;
|
|
10
|
+
/** Base dell'esponenziale quando il provider non dice `Retry-After`. Default 500 ms. */
|
|
11
|
+
baseDelayMs?: number;
|
|
12
|
+
/** Tetto su ogni singola attesa, **compreso** un `Retry-After` esagerato. Default 10 s. */
|
|
13
|
+
maxDelayMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RetryConfig extends RetryOptions {
|
|
16
|
+
/**
|
|
17
|
+
* L'istante oltre il quale non si dorme più.
|
|
18
|
+
*
|
|
19
|
+
* Serve perché l'orchestratore controlla il proprio `budgetMs` solo **in cima a ogni turno**
|
|
20
|
+
* (`orchestrator.ts`): un'attesa dentro la chiamata HTTP gli è invisibile, quindi senza questo
|
|
21
|
+
* limite un backoff potrebbe sforare il budget del chiamante e far emergere un `TIMEOUT` al
|
|
22
|
+
* turno successivo — al posto della causa vera, che era un rate limit. Se non c'è tempo per
|
|
23
|
+
* l'attesa, l'errore del provider si propaga subito: dire "429" è più utile che dire "timeout".
|
|
24
|
+
*/
|
|
25
|
+
deadlineAt?: number;
|
|
26
|
+
/** Iniettabili nei test: un'attesa vera renderebbe la suite lenta e il jitter non asseribile. */
|
|
27
|
+
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
random?: () => number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* L'attesa prima del ritentativo numero `attempt` (1-based).
|
|
32
|
+
*
|
|
33
|
+
* Ordine delle preferenze: **`Retry-After` se c'è** — il provider ha detto il numero, e indovinarne
|
|
34
|
+
* un altro dopo essere stati informati è l'unica opzione chiaramente sbagliata — altrimenti un
|
|
35
|
+
* esponenziale `base * 2^(attempt-1)`. In entrambi i casi si applica il tetto, e in entrambi il
|
|
36
|
+
* **jitter**, che non è un ornamento: molti client diversi condividono lo stesso
|
|
37
|
+
* account presso il provider, quindi prendono il 429 nello stesso istante. Senza jitter ritenterebbero
|
|
38
|
+
* all'unisono e se lo ri-procurerebbero — la ritentata diventerebbe la causa.
|
|
39
|
+
*/
|
|
40
|
+
export declare function delayFor(err: unknown, attempt: number, cfg: RetryConfig): number;
|
|
41
|
+
/**
|
|
42
|
+
* Esegue `run`, e lo rifà se fallisce con un errore transitorio.
|
|
43
|
+
*
|
|
44
|
+
* Sta **attorno alla singola richiesta HTTP**, non attorno all'orchestrazione: lì un ritentativo
|
|
45
|
+
* rigioverebbe la conversazione da capo, ripagando i turni già fatti e rieseguendo i loro tool
|
|
46
|
+
* (vedi la nota su `RemintingLlmClient` in `client.ts`). Qui invece non c'è niente da rieseguire.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withTransientRetry<T>(run: () => Promise<T>, cfg: RetryConfig | false): Promise<T>;
|
package/dist/backoff.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ProviderError, isTransient } from "./provider-error.js";
|
|
2
|
+
const DEFAULTS = { attempts: 2, baseDelayMs: 500, maxDelayMs: 10_000 };
|
|
3
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
export function delayFor(err, attempt, cfg) {
|
|
5
|
+
const base = cfg.baseDelayMs ?? DEFAULTS.baseDelayMs;
|
|
6
|
+
const max = cfg.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
7
|
+
const random = cfg.random ?? Math.random;
|
|
8
|
+
const stated = err instanceof ProviderError ? err.retryAfterMs : null;
|
|
9
|
+
const wanted = stated ?? base * 2 ** (attempt - 1);
|
|
10
|
+
const capped = Math.min(wanted, max);
|
|
11
|
+
return Math.round(capped * (0.5 + random() * 0.5));
|
|
12
|
+
}
|
|
13
|
+
export async function withTransientRetry(run, cfg) {
|
|
14
|
+
if (cfg === false)
|
|
15
|
+
return run();
|
|
16
|
+
const attempts = cfg.attempts ?? DEFAULTS.attempts;
|
|
17
|
+
const sleep = cfg.sleep ?? wait;
|
|
18
|
+
for (let attempt = 0;; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
return await run();
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (attempt >= attempts || !isTransient(err))
|
|
24
|
+
throw err;
|
|
25
|
+
const delay = delayFor(err, attempt + 1, cfg);
|
|
26
|
+
if (cfg.deadlineAt !== undefined && Date.now() + delay > cfg.deadlineAt)
|
|
27
|
+
throw err;
|
|
28
|
+
await sleep(delay);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ClientAction } from "./types.js";
|
|
2
|
+
export type ClientActionHandler = (args: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Esecutore lato client delle azioni consegnate dal server — `ChatCompletion.clientActions`
|
|
5
|
+
* (fire-and-forget) o l'esito da rimandare a `resume()` per un `PendingToolCall` con
|
|
6
|
+
* `reason: "client_action"`. Nessuna dipendenza Node: niente fetch, niente chiavi — gira nel
|
|
7
|
+
* browser, si limita a mappare nome→funzione reale registrata dall'integratore, simmetrico
|
|
8
|
+
* a `{ definition, execute }` lato server ma senza mai vedere una project key.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FantasticoActions {
|
|
11
|
+
private readonly handlers;
|
|
12
|
+
constructor(initial?: Record<string, ClientActionHandler>);
|
|
13
|
+
on(name: string, handler: ClientActionHandler): void;
|
|
14
|
+
/** Esegue una singola azione. Non lancia mai: un handler mancante o che fallisce diventa
|
|
15
|
+
* un esito d'errore, non un'eccezione che spacca il resto del batch. */
|
|
16
|
+
run(action: ClientAction): Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Esegue un elenco di azioni (tipicamente `ChatCompletion.clientActions`) in parallelo,
|
|
22
|
+
* isolando i fallimenti gli uni dagli altri. */
|
|
23
|
+
runAll(actions: ClientAction[]): Promise<Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
result?: unknown;
|
|
26
|
+
error?: string;
|
|
27
|
+
}>>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class FantasticoActions {
|
|
2
|
+
handlers = new Map();
|
|
3
|
+
constructor(initial = {}) {
|
|
4
|
+
for (const [name, handler] of Object.entries(initial))
|
|
5
|
+
this.on(name, handler);
|
|
6
|
+
}
|
|
7
|
+
on(name, handler) {
|
|
8
|
+
this.handlers.set(name, handler);
|
|
9
|
+
}
|
|
10
|
+
async run(action) {
|
|
11
|
+
const handler = this.handlers.get(action.name);
|
|
12
|
+
if (!handler)
|
|
13
|
+
return { id: action.id, error: `Nessun handler registrato per "${action.name}"` };
|
|
14
|
+
try {
|
|
15
|
+
const result = await handler(action.arguments);
|
|
16
|
+
return { id: action.id, result };
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return { id: action.id, error: err.message ?? "Errore azione client" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async runAll(actions) {
|
|
23
|
+
return Promise.all(actions.map((a) => this.run(a)));
|
|
24
|
+
}
|
|
25
|
+
}
|