Repository navigation
Expand file tree
/
Copy pathllmRouter.ts
More file actions
350 lines (310 loc) · 12 KB
/
Copy pathllmRouter.ts
File metadata and controls
350 lines (310 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/**
* Cheapest-sufficient LLM routing for HSP AI column / transmitter.
*
* Order:
* 1. TinyModel (+ transmitter context) when retrieve/route quality is enough
* 2. Vercel AI Gateway (mini → frontier by complexity)
* 3. Direct OpenAI (same mini/frontier split)
*/
import type { TinyModelEnrichment, TinyModelEnrichmentMeta } from "./tinymodel.js";
export type LlmBackend = "tinymodel" | "vercel_gateway" | "openai";
export type LlmTier = "none" | "mini" | "frontier";
export type LlmRouteDecision = {
backend: LlmBackend;
tier: LlmTier;
/** Provider model id (gateway uses `provider/model`). */
model: string;
reason: string;
};
const GATEWAY_BASE = "https://ai-gateway.vercel.sh/v1";
/** Strong enough retrieve hit to answer from corpus without a frontier LLM. */
const TINY_RETRIEVE_SCORE_MIN = 0.28;
const TINY_TOP_LABEL_PROB_MIN = 0.45;
/** Pick complexity class — light prompts stay cheap; complex ones use frontier. */
export function selectSmartChatModel(input: string): string {
const t = input.trim();
const len = t.length;
const complex =
len > 280 ||
/\b(analy[sz]e|compare|architect|debug|refactor|prove|derive|code|sql|contract|security|plan)\b/i.test(
t,
) ||
(t.match(/\?/g) ?? []).length >= 2;
return complex ? "gpt-5.2" : "gpt-4.1-mini";
}
export function isVercelGatewayConfigured(): boolean {
const key =
process.env.AI_GATEWAY_API_KEY?.trim() ||
process.env.VERCEL_OIDC_TOKEN?.trim() ||
"";
return key.length > 0;
}
export function isOpenAiConfigured(): boolean {
return Boolean(process.env.OPENAI?.trim());
}
export function gatewayAuthKey(): string {
return (
process.env.AI_GATEWAY_API_KEY?.trim() ||
process.env.VERCEL_OIDC_TOKEN?.trim() ||
""
);
}
export function gatewayBaseUrl(): string {
return (process.env.AI_GATEWAY_BASE_URL?.trim() || GATEWAY_BASE).replace(/\/$/, "");
}
/** Map HSP complexity to Gateway model ids (cheap mini first). */
export function gatewayModelForInput(input: string, preferFrontier: boolean): string {
if (preferFrontier || selectSmartChatModel(input) === "gpt-5.2") {
return process.env.AI_GATEWAY_FRONTIER_MODEL?.trim() || "openai/gpt-4.1";
}
return process.env.AI_GATEWAY_MINI_MODEL?.trim() || "openai/gpt-4.1-mini";
}
/** Direct OpenAI model ids. */
export function openAiModelForInput(input: string, preferFrontier: boolean): string {
if (preferFrontier) return "gpt-5.2";
return selectSmartChatModel(input);
}
/**
* Prefer Universal Brain in Auto for ordinary general chat (not frontier-hard).
* Hard analysis/code/long prompts stay on Gateway/OpenAI.
*/
export function shouldUseUniversalBrainInAuto(input: string): boolean {
const trimmed = input.trim();
if (!trimmed) return false;
if (trimmed.length > 900) return false;
if (selectSmartChatModel(trimmed) === "gpt-5.2") return false;
return true;
}
/**
* True when TinyModel RAG / route hints can satisfy the user without an LLM call.
* Keeps quality bar: factual HSP help, navigation, or high-confidence retrieve.
*/
export function canAnswerWithTinyModel(
input: string,
meta?: TinyModelEnrichmentMeta | null,
): boolean {
if (!meta) return false;
const trimmed = input.trim();
if (!trimmed) return false;
// Multi-step / analysis / code → need a real LLM.
if (selectSmartChatModel(trimmed) === "gpt-5.2") return false;
if (trimmed.length > 400) return false;
// Model-identity questions must not be swallowed by TinyModel product RAG.
if (
/\bwhat(?:'s| is| are)?\s+(?:the\s+)?(?:ai\s+)?(?:model|llm|version)\b/i.test(trimmed) ||
/какая\s+(?:у\s+тебя\s+|твоя\s+)?(?:версия|модель)/i.test(trimmed) ||
/че\s+ты\s+за\s+модел/i.test(trimmed) ||
/что\s+ты\s+за\s+(?:ии|ai|модел)/i.test(trimmed) ||
/название\s+своей\s+модел/i.test(trimmed)
) {
return false;
}
// Local corpus / route-only answers (no TinyModel sidecar) still count when strong.
if (meta.route && (meta.configured || meta.local_corpus)) return true;
if (!meta.configured && !meta.local_corpus) return false;
if (meta.health_ok === false && !meta.local_corpus) return false;
const hits = meta.retrieve_hits ?? [];
if (hits.length === 0) return false;
const best = Math.max(...hits.map((h) => h.score));
const scoreMin = meta.local_corpus ? 0.18 : TINY_RETRIEVE_SCORE_MIN;
if (best < scoreMin) return false;
const labelOk =
meta.top_label_prob == null || meta.top_label_prob >= TINY_TOP_LABEL_PROB_MIN;
const looksLikeProductHelp =
/\b(how|what|where|when|why|can i|does|feature|pro|swap|wallet|telegram|ai|price|cost|subscribe)\b/i.test(
trimmed,
) || trimmed.length < 160;
return labelOk && looksLikeProductHelp;
}
/** Build a user-facing reply from TinyModel retrieve hits (no LLM). */
export function buildTinyModelOnlyAnswer(
input: string,
enrichment: TinyModelEnrichment,
): string {
const hits = enrichment.meta.retrieve_hits ?? [];
const route = enrichment.meta.route;
const lines: string[] = [];
if (route?.startsWith("navigate:")) {
const path = route.slice("navigate:".length);
lines.push(
`I can take you to **${path}**. Use the suggested action, or open it from the menu.`,
);
} else if (route?.startsWith("feature:")) {
const feature = route.slice("feature:".length).replace(/_/g, " ");
if (feature === "company") {
lines.push(
"**Hyperlinks Space** builds **Hyperlinks Space Program** at https://program.hyperlinks.space/ — wallets, swaps, messaging, and AI & Search.",
);
} else if (feature === "dllr") {
lines.push(
"**DLLR** (Dollars) is the program dollar. In Hyperlinks Space Program it is presented at about **3T+ USD capitalization** ($3 trillion+), with a $1 reference rate.",
);
} else {
lines.push(`That relates to **${feature}** in Hyperlinks Space Program.`);
}
}
if (enrichment.contextBlock) {
const parts = enrichment.contextBlock.split(/\[Program excerpt \d+\]\n/);
const excerpts = parts
.slice(1)
.map((p) => p.trim())
.filter(Boolean)
.slice(0, 2);
if (excerpts.length > 0) {
if (lines.length > 0) lines.push("");
lines.push(excerpts.join("\n\n"));
}
} else if (hits.length > 0) {
if (lines.length > 0) lines.push("");
lines.push(hits.map((h) => h.title).filter(Boolean).join("\n\n"));
}
if (lines.length === 0) {
lines.push(
"I found related Hyperlinks Space Program context, but not a full answer. Try rephrasing, or ask again for a deeper reply.",
);
}
lines.push("");
lines.push("_Quick answer from program knowledge._");
return lines.join("\n").trim();
}
export type LlmRoutePreference = {
/** auto = smart cheapest-sufficient; tinymodel = RAG only; model = fixed id. */
modelMode?: "auto" | "tinymodel" | "model";
/** Gateway (`provider/model`) or OpenAI model id when modelMode is `model`. */
modelId?: string | null;
};
/** True when the user pinned a specific external (non–Tiny Model) id in AI tools. */
export function isExplicitExternalModelPreference(
preference?: LlmRoutePreference | null,
): boolean {
return (
preference?.modelMode === "model" &&
Boolean(typeof preference.modelId === "string" && preference.modelId.trim())
);
}
/**
* Pick backend + model. Prefer TinyModel-only when capable; else Gateway then OpenAI.
* Explicit `model` preference never resolves to TinyModel — only the pinned id.
*/
export function resolveLlmRoute(
input: string,
tinymodel?: TinyModelEnrichmentMeta | null,
opts?: {
preferFrontier?: boolean;
allowTinyOnly?: boolean;
preference?: LlmRoutePreference | null;
},
): LlmRouteDecision | { error: string } {
const allowTiny = opts?.allowTinyOnly !== false;
const preferFrontier = opts?.preferFrontier === true;
const pref = opts?.preference;
const mode = pref?.modelMode ?? "auto";
const forcedId = typeof pref?.modelId === "string" ? pref.modelId.trim() : "";
if (mode === "tinymodel") {
return {
backend: "tinymodel",
tier: "none",
model: "tinymodel/universal-brain",
reason: "user_tinymodel_only",
};
}
if (mode === "model") {
if (!forcedId) {
return {
error: "Selected model id is missing. Pick a model again in AI tools.",
};
}
const looksGateway = forcedId.includes("/");
const provider = looksGateway
? (forcedId.split("/")[0] || "").toLowerCase()
: "openai";
const isOpenAiFamily = !looksGateway || provider === "openai";
// Prefer Gateway for provider/model ids (anthropic/*, google/*, openai/*, …).
if (looksGateway && isVercelGatewayConfigured()) {
return {
backend: "vercel_gateway",
tier: forcedId.includes("mini") ? "mini" : "frontier",
model: forcedId,
reason: "user_fixed_gateway_model",
};
}
// Direct OpenAI only for bare ids or openai/* — never strip anthropic/google/etc.
if (isOpenAiConfigured() && isOpenAiFamily) {
const direct = looksGateway ? forcedId.split("/").pop() || forcedId : forcedId;
return {
backend: "openai",
tier: direct.includes("mini") ? "mini" : "frontier",
model: direct,
reason: "user_fixed_openai_model",
};
}
// Bare OpenAI id with Gateway only.
if (!looksGateway && isVercelGatewayConfigured()) {
const gw = `openai/${forcedId}`;
return {
backend: "vercel_gateway",
tier: gw.includes("mini") ? "mini" : "frontier",
model: gw,
reason: "user_fixed_gateway_fallback",
};
}
return {
error:
"Selected model needs AI_GATEWAY_API_KEY or OPENAI configured on the server.",
};
}
if (allowTiny && canAnswerWithTinyModel(input, tinymodel)) {
return {
backend: "tinymodel",
tier: "none",
model: "tinymodel/rag",
reason: "strong_retrieve_or_route",
};
}
if (isVercelGatewayConfigured()) {
const model = gatewayModelForInput(input, preferFrontier);
return {
backend: "vercel_gateway",
tier: model.includes("mini") ? "mini" : "frontier",
model,
reason: "gateway_available",
};
}
if (isOpenAiConfigured()) {
const model = openAiModelForInput(input, preferFrontier);
return {
backend: "openai",
tier: model.includes("mini") ? "mini" : "frontier",
model,
reason: "openai_direct",
};
}
return {
error:
"No AI provider configured. Set AI_GATEWAY_API_KEY (Vercel AI Gateway), or OPENAI, and optionally TINYMODEL_API_URL for free program answers.",
};
}
/** Curated models offered in the AI tools dialog (Gateway + OpenAI). */
export const AI_TOOLS_MODEL_OPTIONS: Array<{
id: string;
label: string;
backend: "vercel_gateway" | "openai";
}> = [
// Vercel AI Gateway — popular, well-known models for messenger, market, and in-app actions
// https://vercel.com/docs/ai-gateway · https://ai-gateway.vercel.sh/v1/models
{ id: "openai/gpt-5.6-sol", label: "GPT-5.6 Sol", backend: "vercel_gateway" },
{ id: "openai/gpt-5.2", label: "GPT-5.2", backend: "vercel_gateway" },
{ id: "openai/gpt-6-astra", label: "GPT-6 Astra", backend: "vercel_gateway" },
{ id: "anthropic/claude-sonnet-5", label: "Claude Sonnet 5", backend: "vercel_gateway" },
{ id: "anthropic/claude-sonnet-4.5", label: "Claude Sonnet 4.5", backend: "vercel_gateway" },
{ id: "google/gemini-3.8-flash", label: "Gemini 3.8 Flash", backend: "vercel_gateway" },
{ id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro", backend: "vercel_gateway" },
{ id: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", backend: "vercel_gateway" },
{ id: "alibaba/qwen3.8-max-0902", label: "Qwen3.8 Max", backend: "vercel_gateway" },
{ id: "zai/glm-5.3", label: "GLM-5.3", backend: "vercel_gateway" },
{ id: "spacexai/grok-4.3", label: "Grok 4.3", backend: "vercel_gateway" },
{ id: "mistral/mistral-large-3", label: "Mistral Large 3", backend: "vercel_gateway" },
// Direct OpenAI fallbacks when Gateway is unavailable
{ id: "gpt-5.2", label: "GPT-5.2 (OpenAI)", backend: "openai" },
{ id: "gpt-4.1-mini", label: "GPT-4.1 Mini (OpenAI)", backend: "openai" },
];