@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/utils.ts ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * utils — shared helpers for v1 v5 gateway replica
3
+ * all identifiers lowercase no underscore no emoji per skill
4
+ * related logics grouped in this one file 0 to 95 logics
5
+ */
6
+
7
+ import { randomInt } from "node:crypto";
8
+ import {
9
+ THINKINGLEVELS,
10
+ budgetof,
11
+ defaultthinkinglevel,
12
+ islevel,
13
+ listlevels,
14
+ type ThinkingLevel,
15
+ type ThinkingLevelName,
16
+ } from "@wenathlan/getry/thinking";
17
+
18
+ /** cors headers — open cors for all gateway routes */
19
+ export const corsheaders: Record<string, string> = {
20
+ "access-control-allow-origin": "*",
21
+ "access-control-allow-methods":
22
+ "get,post,put,patch,delete,head,options,connect,trace,propfind,proppatch,mkcol,copy,move,lock,unlock,search,purge,link,unlink,report,checkout,checkin,checkout,version-control,label,merge,baseline-control,mkactivity,mkworkspace,update,subscribe,unsubscribe,notify,poll,bind,rebind,unbind,reindex",
23
+ "access-control-allow-headers":
24
+ "content-type,authorization,x-token,x-chat-id,x-user-id,x-session-id,x-request-id,x-thinking-level,x-model,x-tools,accept",
25
+ "access-control-max-age": "86400",
26
+ "access-control-expose-headers": "x-request-id,x-session-id",
27
+ };
28
+
29
+ /** content type constants */
30
+ export const ctjson = "application/json";
31
+ export const ctsse = "text/event-stream";
32
+ export const ctplain = "text/plain";
33
+ export const ctndjson = "application/x-ndjson";
34
+ export const ctoctet = "application/octet-stream";
35
+
36
+ /** max duration — near infinite 2 power 31 minus 1 */
37
+ export const maxdurationconst = 2147483647;
38
+
39
+ /** default max tokens — conservative per request 32768 */
40
+ export const defaultmaxtokens = 32768;
41
+
42
+ /** randombase36 — cryptographically secure base36 string of n chars
43
+ * node crypto randomint keeps every character uniformly distributed
44
+ * code scanning js insecure randomness safe — never math random */
45
+ export function randombase36(n: number): string {
46
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
47
+ let out = "";
48
+ for (let i = 0; i < n; i += 1) out += alphabet[randomInt(alphabet.length)];
49
+ return out;
50
+ }
51
+
52
+ /** securerandom — cryptographically secure uniform float in 0 1
53
+ * replacement for math random in every security sensitive path
54
+ * jitter backoff session ids key selection all use this */
55
+ export function securerandom(): number {
56
+ return randomInt(1, 2 ** 48) / 2 ** 48;
57
+ }
58
+
59
+ /** genid — generate a unique chatcmpl id
60
+ * timestamp plus 8 chars of secure base36 entropy */
61
+ export function genid(prefix = "chatcmpl"): string {
62
+ return `${prefix}-${Date.now().toString(36)}${randombase36(8)}`;
63
+ }
64
+
65
+ /** getip — extract client ip from request headers */
66
+ export function getip(req: Request): string {
67
+ const headers = req.headers;
68
+ return (
69
+ headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
70
+ headers.get("x-real-ip") ||
71
+ headers.get("cf-connecting-ip") ||
72
+ headers.get("x-client-ip") ||
73
+ "unknown"
74
+ );
75
+ }
76
+
77
+ /** handlecors — handle options preflight returns response or null */
78
+ export function handlecors(req: Request): Response | null {
79
+ if (req.method.toUpperCase() === "OPTIONS") {
80
+ return new Response(null, { status: 204, headers: corsheaders });
81
+ }
82
+ return null;
83
+ }
84
+
85
+ /** json headers */
86
+ export function jsonheaders(): Record<string, string> {
87
+ return { ...corsheaders, "content-type": ctjson };
88
+ }
89
+
90
+ /** detectcontenttype — read accept header returns format */
91
+ export function detectcontenttype(req: Request): "sse" | "ndjson" | "json" | "plain" | "octet" {
92
+ const accept = (req.headers.get("accept") || "").toLowerCase();
93
+ if (accept.includes(ctndjson)) return "ndjson";
94
+ if (accept.includes("text/event-stream")) return "sse";
95
+ if (accept.includes(ctplain)) return "plain";
96
+ if (accept.includes(ctoctet)) return "octet";
97
+ return "json";
98
+ }
99
+
100
+ /** clamp — bound a number between min and max
101
+ * only nan falls back: infinity is a boundable value (plus infinity clamps
102
+ * to max minus infinity to min) — treating it like nan silently replaced
103
+ * extreme inputs with the fallback */
104
+ export function clamp(n: unknown, min: number, max: number, fallback = 0): number {
105
+ const v = typeof n === "string" ? parseFloat(n) : typeof n === "number" ? n : fallback;
106
+ if (Number.isNaN(v)) return fallback;
107
+ if (!Number.isFinite(v)) return v > 0 ? max : min;
108
+ return Math.max(min, Math.min(max, v));
109
+ }
110
+
111
+ /** automaxtokens — clamp max tokens 1 to 98304 */
112
+ export function automaxtokens(n: unknown, fallback = defaultmaxtokens): number {
113
+ const v = typeof n === "string" ? parseInt(n, 10) : typeof n === "number" ? n : fallback;
114
+ if (!Number.isFinite(v) || v === 0) return fallback;
115
+ return clamp(v, 1, 98304, fallback);
116
+ }
117
+
118
+ /** autotemp — clamp temperature 0 to 2 */
119
+ export function autotemp(n: unknown, fallback = 0.7): number {
120
+ return clamp(n, 0, 2, fallback);
121
+ }
122
+
123
+ /** autotopp — clamp top p 0 to 1 */
124
+ export function autotopp(n: unknown, fallback = 0.9): number {
125
+ return clamp(n, 0, 1, fallback);
126
+ }
127
+
128
+ /** autofrequencypenalty */
129
+ export function autofrequencypenalty(n: unknown, fallback = 0): number {
130
+ return clamp(n, -2, 2, fallback);
131
+ }
132
+
133
+ /** autopresencepenalty */
134
+ export function autopresencepenalty(n: unknown, fallback = 0): number {
135
+ return clamp(n, -2, 2, fallback);
136
+ }
137
+
138
+ /** auton — clamp n 1 to 10 */
139
+ export function auton(n: unknown, fallback = 1): number {
140
+ return clamp(n, 1, 10, fallback);
141
+ }
142
+
143
+ /** autoseed — nonnegative int bounded
144
+ * absolute value floors toward zero: abs before floor — floor before abs
145
+ * rounded negatives away from zero (-12.9 became 13 while 12.9 was 12) */
146
+ export function autoseed(n: unknown, fallback = 0): number {
147
+ const v = typeof n === "string" ? parseInt(n, 10) : typeof n === "number" ? n : fallback;
148
+ if (!Number.isFinite(v)) return fallback;
149
+ return clamp(Math.floor(Math.abs(v)), 0, 2147483647, fallback);
150
+ }
151
+
152
+ /** autothinking — normalize thinking level string to the live ladder owned
153
+ * by the getry engine (pr16, the registerable fold): the rung vocabulary
154
+ * answers the owner's registered levels (any count — never a fixed 7) and
155
+ * the default rung answers the owner's defaultthinkinglevel — never a
156
+ * second declaration. */
157
+ export function autothinking(level?: unknown): ThinkingLevelName {
158
+ if (!level || typeof level !== "string") return defaultthinkinglevel();
159
+ const l = String(level).toLowerCase().trim();
160
+ if (islevel(l)) return l;
161
+ if (l === "disabled" || l === "off" || l === "false") return "none";
162
+ if (l === "minimum") return "minimal";
163
+ if (l === "med" || l === "mid") return "medium";
164
+ if (l === "extra" || l === "ultra") return "xhigh";
165
+ if (l === "maximum" || l === "full" || l === "all") return "max";
166
+ return defaultthinkinglevel();
167
+ }
168
+
169
+ /** thinking budgets — the token budgets of the live ladder derived from
170
+ * the getry engine (the one owner of the reasoning ladder, pr16): never a
171
+ * local copy, the map answers the owner's registered rungs at load time
172
+ * and budgetof answers the live truth at call time. */
173
+ export const thinkingbudgets: Record<string, number> = Object.fromEntries(
174
+ listlevels().map((rung) => [rung.level, rung.budget]),
175
+ );
176
+
177
+ /** the seed ladder re-exported for the hosts that inspect the family
178
+ * default — the live truth answers listlevels() on the engine. */
179
+ export const thinkingseed: ThinkingLevel[] = THINKINGLEVELS;
180
+
181
+ /** thinkingbudget — get token budget for a level */
182
+ export function thinkingbudget(level?: string): number {
183
+ return budgetof(autothinking(level));
184
+ }
185
+
186
+ /** makethinker — create a thinking budget object */
187
+ export function makethinker(level?: string) {
188
+ const lvl = autothinking(level);
189
+ if (lvl === "none") return { type: "disabled" as const };
190
+ return { type: "enabled" as const, budget: budgetof(lvl) };
191
+ }
192
+
193
+ /** safestringify — json stringify that never throws */
194
+ export function safestringify(obj: unknown): string {
195
+ try {
196
+ return JSON.stringify(obj);
197
+ } catch {
198
+ return String(obj);
199
+ }
200
+ }
201
+
202
+ /** safejsonparse — json parse that never throws returns null on failure */
203
+ export function safejsonparse<T = unknown>(s: string): T | null {
204
+ try {
205
+ return JSON.parse(s) as T;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /** levenshtein — fuzzy string distance for parameter name matching */
212
+ export function levenshtein(a: string, b: string): number {
213
+ const m = a.length;
214
+ const n = b.length;
215
+ if (m === 0) return n;
216
+ if (n === 0) return m;
217
+ const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
218
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
219
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
220
+ for (let i = 1; i <= m; i++) {
221
+ for (let j = 1; j <= n; j++) {
222
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
223
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
224
+ }
225
+ }
226
+ return dp[m][n];
227
+ }
228
+
229
+ /** esttokens — cjk aware token estimate */
230
+ export function esttokens(text: string): number {
231
+ if (!text) return 0;
232
+ let ascii = 0;
233
+ let cjk = 0;
234
+ for (const ch of text) {
235
+ const code = ch.codePointAt(0) || 0;
236
+ if (code >= 0x3000 && code <= 0x9fff) cjk++;
237
+ else if (code >= 0x4e00 && code <= 0x9fff) cjk++;
238
+ else if (code >= 0xac00 && code <= 0xd7af) cjk++;
239
+ else ascii++;
240
+ }
241
+ return Math.ceil(ascii / 4 + cjk / 2);
242
+ }
243
+
244
+ /** truncatemessages — truncate message history to fit context window */
245
+ export function truncatemessages(messages: unknown[], maxtokens: number): unknown[] {
246
+ const result: unknown[] = [];
247
+ let total = 0;
248
+ for (let i = messages.length - 1; i >= 0; i--) {
249
+ const msg = messages[i] as Record<string, unknown>;
250
+ const content = typeof msg.content === "string" ? msg.content : safestringify(msg.content);
251
+ const tokens = esttokens(content) + 4;
252
+ if (total + tokens > maxtokens) break;
253
+ result.unshift(msg);
254
+ total += tokens;
255
+ }
256
+ return result;
257
+ }
258
+
259
+ // the devthink meta-model identity and the intelligencerank interface
260
+ // previously lived here as helpers for the deleted gatewayvN.ts files —
261
+ // that context now lives exactly once: the type surface in types.ts
262
+ // (intelligencerank, modeldef.rank) and the user definitions in
263
+ // config.mjs (one context one file — nothing duplicated in utils)
package/validate.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * validate — the pure gateway definition validator of the getry family.
3
+ *
4
+ * The ownership move (wave/ownership.gateway step 3) folded the whole
5
+ * configuration loader family into the server owner, and the pure
6
+ * validator landed beside the hono transport it never needed. This module
7
+ * is the corrected seat: the checks are pure logic over the definition
8
+ * object (no transport, no filesystem, no server imports), so every
9
+ * consumer of the family — the server owner that re-exports it, the
10
+ * devthink config bridge the browser graph rides, the configloader test
11
+ * lane — resolves the same single owner without ever dragging the
12
+ * transport stack into a module graph that must stay browser-pure.
13
+ *
14
+ * The contract is verbatim the server face: one definition in, one list
15
+ * of human readable problems out — empty when valid, synchronous always.
16
+ */
17
+ import type { gatewaydefinition } from "./types.js";
18
+
19
+ /** validateconfig — check a gateway definition for common errors
20
+ * returns a list of human readable problems — empty when valid */
21
+ export function validateconfig(def: gatewaydefinition): string[] {
22
+ const problems: string[] = [];
23
+ if (!def.versions || Object.keys(def.versions).length === 0) {
24
+ problems.push("no versions defined — add at least one version to versions map");
25
+ return problems;
26
+ }
27
+ for (const [id, cfg] of Object.entries(def.versions)) {
28
+ const prefix = `version ${id}:`;
29
+ if (!cfg.id) problems.push(`${prefix} missing id`);
30
+ if (cfg.id !== id) problems.push(`${prefix} id mismatch — map key is ${id} but id is ${cfg.id}`);
31
+ if (!cfg.providername) problems.push(`${prefix} missing providername`);
32
+ if (!cfg.upstreams || cfg.upstreams.length === 0) problems.push(`${prefix} no upstreams configured`);
33
+ if (!cfg.auth) problems.push(`${prefix} missing auth config`);
34
+ if (!cfg.models || cfg.models.length === 0) problems.push(`${prefix} no models configured`);
35
+ if (!cfg.metamodel?.id) problems.push(`${prefix} missing metamodel id`);
36
+ // rotation models must exist in the catalog — the catalog may itself be
37
+ // missing (already reported above) so the lookup guards against it
38
+ if (cfg.rotation?.models) {
39
+ const ids = new Set((cfg.models ?? []).map((m) => m.id));
40
+ for (const rm of cfg.rotation.models) {
41
+ if (!ids.has(rm)) problems.push(`${prefix} rotation model ${rm} not in models catalog`);
42
+ }
43
+ }
44
+ // defaultmodel must exist
45
+ if (
46
+ cfg.defaultmodel &&
47
+ !(cfg.models ?? []).find((m) => m.id === cfg.defaultmodel) &&
48
+ cfg.defaultmodel !== cfg.metamodel?.id
49
+ ) {
50
+ problems.push(`${prefix} defaultmodel ${cfg.defaultmodel} not in models catalog`);
51
+ }
52
+ // auth env var must be set when required from env — a version with a
53
+ // missing auth block already carries its problem above; the dereference
54
+ // below used to crash the whole validation with a type error instead of
55
+ // reporting the remaining problems
56
+ if (cfg.auth?.required && cfg.auth?.keysources?.includes("env") && !cfg.auth?.envvar) {
57
+ problems.push(`${prefix} auth requires env keys but envvar is not set`);
58
+ }
59
+ }
60
+ return problems;
61
+ }