@wenathlan/getry 2.0.164 → 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/accounts.ts +2087 -0
- package/antigravity.ts +2171 -0
- package/auth.ts +265 -0
- package/constants.ts +950 -0
- package/db.ts +810 -0
- package/docs/distributionlanes.md +1 -1
- package/docs/getry.md +33 -0
- package/engine.ts +1535 -0
- package/fingerprint.ts +1461 -0
- package/gateway.ts +1306 -0
- package/gatewayprobe.ts +61 -0
- package/gatewaysmoke.ts +64 -0
- package/intelligence.ts +169 -0
- package/json.ts +932 -0
- package/labels.ts +262 -0
- package/llm.ts +2709 -0
- package/models.ts +4017 -0
- package/nodeshim.ts +265 -0
- package/oauth.ts +2201 -0
- package/package.json +580 -215
- package/project.ts +1293 -0
- package/providers.ts +3238 -0
- package/quota.ts +1891 -0
- package/recovery.ts +2434 -0
- package/request.ts +4496 -0
- package/search.ts +1761 -0
- package/server.ts +766 -0
- package/streaming.ts +4398 -0
- package/thinking.ts +416 -0
- package/types.ts +809 -0
- package/utils.ts +263 -0
- package/validate.ts +61 -0
- package/versionregistry.ts +1325 -0
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
|
+
}
|