@wenathlan/getry 2.0.165 → 2.0.166

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/thinking.ts ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * thinking.ts — the thinking seat of getry (root layer): the registerable
3
+ * reasoning ladder plus the routing the rungs drive.
4
+ *
5
+ * the reasoning ladder the gateway accepts on every chat route: named
6
+ * levels, each bound to the thinking budget the provider receives. the
7
+ * ladder is registered, never fixed: the seven rungs below are the family
8
+ * seed only — a registration (the GETRY_THINKING_LEVELS surface at boot,
9
+ * or registerthinkinglevels from the host) replaces the whole ladder with
10
+ * whatever the person registered: any count, any names, any budgets. no
11
+ * ceiling lives in this engine — a plan that wants fewer rungs blocks in
12
+ * the devthink system layer, never here.
13
+ *
14
+ * beside the ladder the seat owns the routing the level selects: the model
15
+ * tier the rung targets (tier routing), the token budget clamped to the
16
+ * model output ceiling, the two-calls pattern verdict, the ordered
17
+ * fallback chain over the provider catalog, and the session/stream lanes a
18
+ * thinking request rides. every routing function below is pure,
19
+ * dependency-free math over structural rows — the gateway modules (engine,
20
+ * gateway) and the family consumers (devthink and siblings) wire the real
21
+ * catalog rows in, never a second copy of the math.
22
+ *
23
+ * the logicfold3 fold retired the thinkingtypes.ts mirror this seat once
24
+ * carried (the declaration surface that repeated every export below as an
25
+ * `export declare` tombstone — zero import sites repo-wide) — every
26
+ * declaration it mirrored answers from this implementation alone.
27
+ */
28
+
29
+ /** the name of one thinking level — the seven seed rungs stay literate for
30
+ * completion, and any registered name rides beside them: the vocabulary is
31
+ * what the person registers, never a closed union. */
32
+ export type ThinkingLevelName = (string & {}) | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
33
+
34
+ /** one rung of the reasoning ladder. */
35
+ export type ThinkingLevel = {
36
+ level: ThinkingLevelName;
37
+ budget: number;
38
+ desc: string;
39
+ };
40
+
41
+ /** the family seed ladder — the seven rungs the self-hosted build answers
42
+ * until a registration replaces them. the seed is a default, never a
43
+ * ceiling: registerthinkinglevels accepts any count of rungs. */
44
+ export const THINKINGLEVELS: ThinkingLevel[] = [
45
+ { level: "none", budget: 0, desc: "no thinking" },
46
+ { level: "minimal", budget: 1400, desc: "quick reasoning" },
47
+ { level: "low", budget: 5500, desc: "light reasoning" },
48
+ { level: "medium", budget: 17000, desc: "balanced" },
49
+ { level: "high", budget: 68000, desc: "deep reasoning default" },
50
+ { level: "xhigh", budget: 68000, desc: "extra deep" },
51
+ { level: "max", budget: 68000, desc: "maximum" },
52
+ ];
53
+
54
+ /** the seed rung the gateway resolves when the request names none — the
55
+ * registered default (GETRY_THINKING_DEFAULT or registerthinkinglevels)
56
+ * overrides it at runtime through defaultthinkinglevel(). */
57
+ export const DEFAULTTHINKINGLEVEL: ThinkingLevelName = "high";
58
+
59
+ /** the registration surface: GETRY_THINKING_LEVELS — a JSON array of rung
60
+ * objects ({"level","budget","desc"}) the person registers at boot; any
61
+ * count rides. invalid json answers the family seed untouched. */
62
+ const LEVELS_ENV = "GETRY_THINKING_LEVELS";
63
+
64
+ /** the registration surface for the default rung: GETRY_THINKING_DEFAULT —
65
+ * a level name that must name a rung of the registered ladder. */
66
+ const DEFAULT_ENV = "GETRY_THINKING_DEFAULT";
67
+
68
+ /** parses one registration payload into a ladder — the honest rungs only:
69
+ * named, finite-budgeted, deduped (last wins). no count cap applies. */
70
+ function parseRegisteredLevels(raw: string): ThinkingLevel[] | null {
71
+ let parsed: unknown;
72
+ try {
73
+ parsed = JSON.parse(raw);
74
+ } catch {
75
+ return null;
76
+ }
77
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
78
+ const rungs = new Map<string, ThinkingLevel>();
79
+ for (const entry of parsed) {
80
+ if (typeof entry !== "object" || entry === null) continue;
81
+ const { level, budget, desc } = entry as Record<string, unknown>;
82
+ if (typeof level !== "string" || level.trim() === "") continue;
83
+ const name = level.toLowerCase().trim();
84
+ const tokens = typeof budget === "number" && Number.isFinite(budget) && budget >= 0 ? Math.floor(budget) : 0;
85
+ rungs.set(name, {
86
+ level: name,
87
+ budget: tokens,
88
+ desc: typeof desc === "string" && desc !== "" ? desc : `registered level ${name}`,
89
+ });
90
+ }
91
+ return rungs.size === 0 ? null : [...rungs.values()];
92
+ }
93
+
94
+ /** the live ladder: the registered rungs, or the family seed when no
95
+ * registration ever landed. resolved once at import from the environment
96
+ * so the load-time budget maps capture the registered ladder too. */
97
+ let ladder: ThinkingLevel[] = parseRegisteredLevels(process.env[LEVELS_ENV] ?? "") ?? THINKINGLEVELS;
98
+
99
+ /** the live default rung: a registered default that names a real rung, or
100
+ * the family seed default. */
101
+ let ladderDefault: ThinkingLevelName | null = (() => {
102
+ const raw = process.env[DEFAULT_ENV];
103
+ if (typeof raw !== "string" || raw.trim() === "") return null;
104
+ const name = raw.toLowerCase().trim();
105
+ return ladder.some((rung) => rung.level === name) ? name : null;
106
+ })();
107
+
108
+ /**
109
+ * registers the thinking ladder — the owner's word is the law: any count of
110
+ * rungs, any names, any budgets (no ceiling in this engine). the
111
+ * registration replaces the whole ladder; pass the seed back to restore the
112
+ * family default. invalid entries are skipped, duplicates keep the last
113
+ * write, and an empty registration answers the seed untouched.
114
+ *
115
+ * @law levels depend on what the person registers — never a fixed 7
116
+ * @param levels the rungs to register.
117
+ * @param options the default rung of the registration (must name a rung).
118
+ * @returns the live ladder after the registration.
119
+ */
120
+ export function registerthinkinglevels(
121
+ levels: readonly ThinkingLevel[],
122
+ options?: { default?: ThinkingLevelName },
123
+ ): ThinkingLevel[] {
124
+ const rungs = parseRegisteredLevels(JSON.stringify(levels));
125
+ if (rungs === null) return ladder;
126
+ ladder = rungs;
127
+ const wanted = options?.default;
128
+ if (typeof wanted === "string" && wanted.trim() !== "" && islevel(wanted)) {
129
+ ladderDefault = wanted.toLowerCase().trim();
130
+ } else if (wanted === undefined) {
131
+ ladderDefault = ladder.some((rung) => rung.level === DEFAULTTHINKINGLEVEL) ? DEFAULTTHINKINGLEVEL : null;
132
+ }
133
+ return ladder;
134
+ }
135
+
136
+ /**
137
+ * lists the live ladder in serve order (the registration order, none first
138
+ * when the seed rides).
139
+ *
140
+ * @returns the thinking levels the engine accepts right now.
141
+ */
142
+ export function listlevels(): ThinkingLevel[] {
143
+ return ladder;
144
+ }
145
+
146
+ /** the live default rung — the registered default or the family seed. */
147
+ export function defaultthinkinglevel(): ThinkingLevelName {
148
+ return ladderDefault ?? DEFAULTTHINKINGLEVEL;
149
+ }
150
+
151
+ /**
152
+ * resolves the budget of one named level against the live ladder.
153
+ *
154
+ * @param level the level name to resolve.
155
+ * @returns the thinking budget in tokens — 0 when the rung never registered.
156
+ */
157
+ export function budgetof(level: ThinkingLevelName): number {
158
+ return ladder.find((rung) => rung.level === (typeof level === "string" ? level.toLowerCase().trim() : level))
159
+ ?.budget ?? 0;
160
+ }
161
+
162
+ /**
163
+ * checks whether a request value names a real rung of the live ladder.
164
+ *
165
+ * @param candidate the raw value from the request.
166
+ * @returns true when the value is a registered level.
167
+ */
168
+ export function islevel(candidate: string): candidate is ThinkingLevelName {
169
+ const name = candidate.toLowerCase().trim();
170
+ return ladder.some((rung) => rung.level === name);
171
+ }
172
+
173
+ /**
174
+ * formats a budget the way the theme renders it (grouped thousands).
175
+ *
176
+ * @param budget the budget in tokens.
177
+ * @returns the formatted label.
178
+ */
179
+ export function formatbudget(budget: number): string {
180
+ return budget.toLocaleString("en-US");
181
+ }
182
+
183
+ /* ===== block: tier routing ===== */
184
+
185
+ /** the model tier vocabulary the provider catalog rows carry (the
186
+ * intelligencerank tier of the gateway types): 1 the flagship tier, 2 the
187
+ * strong tier, 3 the mid tier, 4 the economy tier. */
188
+ export type thinkingmodeltier = 1 | 2 | 3 | 4;
189
+
190
+ /**
191
+ * resolves the model tier one thinking level targets — the rung POSITION
192
+ * on the live ladder selects the tier, never the name: the fraction of the
193
+ * ladder the rung sits at walks the tiers bottom-up (at or below a quarter
194
+ * the economy tier 4, at or below a half the mid tier 3, at or below three
195
+ * quarters the strong tier 2, above it the flagship tier 1), so the family
196
+ * seed routes none/minimal to 4, low/medium to 3, high to 2 and xhigh/max
197
+ * to the flagship 1 — and a registered ladder of any length routes the same
198
+ * way. a single-rung registration answers the ceiling the rung carries (a
199
+ * zero-budget rung targets the economy tier, every other single rung the
200
+ * flagship). a level that names no rung of the live ladder routes the
201
+ * default rung's tier, the same honest fallback budgetof answers.
202
+ *
203
+ * @law the level selects the model tier — the tier ladder never hardcodes
204
+ * the seven seed names
205
+ * @param level the level name to route.
206
+ * @returns the model tier the level targets.
207
+ */
208
+ export function targettier(level: ThinkingLevelName): thinkingmodeltier {
209
+ const rungs = ladder;
210
+ const named = typeof level === "string" ? level.toLowerCase().trim() : level;
211
+ let index = rungs.findIndex((rung) => rung.level === named);
212
+ if (index < 0) index = rungs.findIndex((rung) => rung.level === defaultthinkinglevel());
213
+ if (index < 0) return 2;
214
+ if (rungs.length <= 1) return rungs[index].budget === 0 ? 4 : 1;
215
+ const fraction = index / (rungs.length - 1);
216
+ if (fraction <= 0.25) return 4;
217
+ if (fraction <= 0.5) return 3;
218
+ if (fraction <= 0.75) return 2;
219
+ return 1;
220
+ }
221
+
222
+ /* ===== block: token budgets ===== */
223
+
224
+ /** the two-calls threshold the family seed pins (98304 tokens — the glm
225
+ * max output window the v1 lane rides); a config threshold always wins. */
226
+ export const TWOCALLSTHRESHOLD = 98304;
227
+
228
+ /**
229
+ * clamps the budget of one level to a model's output ceiling — the gateway
230
+ * never promises the provider more thinking tokens than the model can
231
+ * answer with (the clamp discipline the bodybuild config names).
232
+ *
233
+ * @param level the level whose budget rides.
234
+ * @param ceiling the model max output tokens (no clamp when absent).
235
+ * @returns the budget in tokens, clamped down to the ceiling.
236
+ */
237
+ export function clampbudget(level: ThinkingLevelName, ceiling?: number): number {
238
+ const budget = budgetof(level);
239
+ if (ceiling === undefined || !Number.isFinite(ceiling) || ceiling <= 0) return budget;
240
+ return Math.min(budget, Math.floor(ceiling));
241
+ }
242
+
243
+ /**
244
+ * decides whether a request rides the two-calls pattern (call 1 thinking
245
+ * enabled, call 2 a fresh response when the guarantee rides): the pattern
246
+ * fires when the thinking budget plus the response ceiling crosses the
247
+ * threshold, or whenever the level thinks at all — the exact math the
248
+ * engine's zai sdk lane always carried, lifted here so every consumer
249
+ * answers the same verdict.
250
+ *
251
+ * @param level the thinking level of the request.
252
+ * @param responsemax the response ceiling in tokens (max_tokens or the
253
+ * metamodel output window).
254
+ * @param threshold the two-calls trigger threshold (the family default
255
+ * 98304 when absent).
256
+ * @param budget the effective thinking budget when a config map overrides
257
+ * the ladder (the live ladder answers when absent).
258
+ * @returns true when the request rides the two-calls pattern.
259
+ */
260
+ export function twocallspattern(input: {
261
+ level: ThinkingLevelName;
262
+ responsemax: number;
263
+ threshold?: number;
264
+ budget?: number;
265
+ }): boolean {
266
+ const budget = input.budget ?? budgetof(input.level);
267
+ const threshold = input.threshold ?? TWOCALLSTHRESHOLD;
268
+ return budget + input.responsemax > threshold || input.level !== "none";
269
+ }
270
+
271
+ /* ===== block: fallback chains ===== */
272
+
273
+ /** one model row of the provider catalog the seat routes over — the
274
+ * structural slice of the modeldef kind the gateway types own (id,
275
+ * reasoning flag, output ceiling, leaderboard rank): the seat never
276
+ * imports the gateway graph, so the rows ride as plain shapes. */
277
+ export type thinkingmodelrow = {
278
+ id: string;
279
+ reasoning?: boolean;
280
+ maxoutput?: number;
281
+ rank?: { score?: number; tier?: thinkingmodeltier };
282
+ };
283
+
284
+ /**
285
+ * checks whether one catalog model can serve a thinking level: a model
286
+ * that carries no reasoning flag counts as capable (the family convention
287
+ * — an unknown capability answers true the way the engine's
288
+ * supports_thinking projection does), a model that declares reasoning
289
+ * false never carries a thinking budget.
290
+ *
291
+ * @param model the catalog row to check.
292
+ * @param level the level the request rides.
293
+ * @returns true when the model may serve the level.
294
+ */
295
+ export function modelcapable(model: thinkingmodelrow, level: ThinkingLevelName): boolean {
296
+ if (model.reasoning === false) return level === "none";
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * builds the ordered fallback chain of one provider's catalog for a level:
302
+ * the capable models ordered by tier distance from the level's target
303
+ * tier (exact tier first, adjacent tiers outward), then by whether the
304
+ * output ceiling holds the full budget unclamped, then by leaderboard
305
+ * score, then by id — a stable, honest order the caller walks top to
306
+ * bottom on failure. unranked models ride after every ranked tier.
307
+ *
308
+ * @param models the provider catalog rows.
309
+ * @param level the level the request rides.
310
+ * @returns the ordered capable rows.
311
+ */
312
+ export function fallbackchain(models: readonly thinkingmodelrow[], level: ThinkingLevelName): thinkingmodelrow[] {
313
+ const target = targettier(level);
314
+ const budget = budgetof(level);
315
+ const capable = models.filter((model) => modelcapable(model, level));
316
+ const distance = (model: thinkingmodelrow): number => {
317
+ const tier = model.rank?.tier;
318
+ if (tier === undefined) return Number.MAX_SAFE_INTEGER;
319
+ return Math.abs(tier - target);
320
+ };
321
+ const fullfit = (model: thinkingmodelrow): boolean =>
322
+ model.maxoutput === undefined || model.maxoutput >= budget;
323
+ return capable.sort((left, right) => {
324
+ const bydistance = distance(left) - distance(right);
325
+ if (bydistance !== 0) return bydistance;
326
+ const byfit = Number(fullfit(right)) - Number(fullfit(left));
327
+ if (byfit !== 0) return byfit;
328
+ const byscore = (right.rank?.score ?? -Infinity) - (left.rank?.score ?? -Infinity);
329
+ if (byscore !== 0) return byscore;
330
+ return left.id.localeCompare(right.id);
331
+ });
332
+ }
333
+
334
+ /* ===== block: session and stream lanes ===== */
335
+
336
+ /** the lane a thinking request rides: "session" pins the per-session
337
+ * rotation context (the rotation cursor and the thinking budget bind to
338
+ * the (sessionid, provider) row the sessions store owns), "stream" rides
339
+ * the sse emission contract (reasoning deltas before content, the parser
340
+ * pattern the streaming routes follow). a request may ride both lanes at
341
+ * once — a session-scoped stream. */
342
+ export type thinkinglane = "session" | "stream";
343
+
344
+ /**
345
+ * resolves the lanes one thinking request rides, in bind order: the
346
+ * session lane first (the rotation context binds before any byte leaves
347
+ * the gateway), the stream lane second (the emission order of the
348
+ * answer). a request that names neither lane rides no named lane — a
349
+ * plain one-shot completion.
350
+ *
351
+ * @param input the request shape: stream true asks the sse contract,
352
+ * sessionid binds the session rotation context.
353
+ * @returns the lanes in bind order (empty when the request is a plain
354
+ * one-shot completion).
355
+ */
356
+ export function lanesof(input: { stream?: boolean; sessionid?: string | null }): thinkinglane[] {
357
+ const lanes: thinkinglane[] = [];
358
+ if (typeof input.sessionid === "string" && input.sessionid.trim() !== "") lanes.push("session");
359
+ if (input.stream === true) lanes.push("stream");
360
+ return lanes;
361
+ }
362
+
363
+ /* ===== block: provider catalog ===== */
364
+
365
+ /** one provider row of the catalog the seat routes over — the structural
366
+ * slice of the gateway version rows (provider name, lifecycle badge, the
367
+ * model catalog). */
368
+ export type thinkingproviderrow = {
369
+ provider: string;
370
+ badge?: "live" | "paused";
371
+ models: readonly thinkingmodelrow[];
372
+ };
373
+
374
+ /**
375
+ * lists the providers whose catalog serves a level (at least one capable
376
+ * model), live providers first — a paused provider still serves the list
377
+ * as a fallback seat the way the paused v2 lane falls back to v1, it just
378
+ * never leads the chain.
379
+ *
380
+ * @param catalog the provider rows.
381
+ * @param level the level the request rides.
382
+ * @returns the serving providers, live first, input order stable within
383
+ * each badge group.
384
+ */
385
+ export function providersserving(catalog: readonly thinkingproviderrow[], level: ThinkingLevelName): thinkingproviderrow[] {
386
+ return catalog
387
+ .filter((provider) => provider.models.some((model) => modelcapable(model, level)))
388
+ .sort((left, right) => Number(left.badge !== "live") - Number(right.badge !== "live"));
389
+ }
390
+
391
+ /** one provider/model candidate of the cross-provider chain. */
392
+ export type thinkingcandidate = { provider: string; model: thinkingmodelrow };
393
+
394
+ /**
395
+ * builds the cross-provider fallback chain for a level: every serving
396
+ * provider in live-first order, each contributing its own ordered
397
+ * fallbackchain — the single honest walk order a consumer retries through
398
+ * on failure (the tier-exact candidate of the live providers first, the
399
+ * paused providers' candidates last).
400
+ *
401
+ * @param catalog the provider rows.
402
+ * @param level the level the request rides.
403
+ * @returns the ordered provider/model candidates.
404
+ */
405
+ export function providerchain(
406
+ catalog: readonly thinkingproviderrow[],
407
+ level: ThinkingLevelName,
408
+ ): thinkingcandidate[] {
409
+ const chain: thinkingcandidate[] = [];
410
+ for (const provider of providersserving(catalog, level)) {
411
+ for (const model of fallbackchain(provider.models, level)) {
412
+ chain.push({ provider: provider.provider, model });
413
+ }
414
+ }
415
+ return chain;
416
+ }