lynkr 9.14.1 → 9.14.3

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.
Files changed (96) hide show
  1. package/README.md +5 -1
  2. package/bin/cli.js +14 -6
  3. package/bin/lynkr-desktop-token.js +108 -0
  4. package/bin/lynkr-init.js +13 -1
  5. package/bin/lynkr-restart.js +125 -0
  6. package/bin/opencode-setup.js +175 -0
  7. package/bin/run.js +92 -0
  8. package/config/model-capabilities.json +20 -0
  9. package/config/model-capability-seeds.json +75 -0
  10. package/config/model-tiers.json +13 -1
  11. package/package.json +16 -22
  12. package/scripts/claude-desktop.js +212 -0
  13. package/scripts/ope-report.js +78 -0
  14. package/scripts/seed-capabilities.js +230 -0
  15. package/src/api/claude-desktop-gateway.js +123 -0
  16. package/src/api/health.js +0 -1
  17. package/src/api/mcp-broker.js +149 -0
  18. package/src/api/middleware/budget-enforcer.js +77 -7
  19. package/src/api/middleware/budget.js +36 -2
  20. package/src/api/middleware/session.js +0 -0
  21. package/src/api/openai-router.js +210 -15
  22. package/src/api/providers-handler.js +16 -1
  23. package/src/api/router.js +73 -106
  24. package/src/auth-mode.js +40 -4
  25. package/src/budget/index.js +86 -0
  26. package/src/cache/embeddings.js +125 -35
  27. package/src/cache/onnx-embedder.js +92 -0
  28. package/src/cache/prompt.js +0 -7
  29. package/src/clients/bedrock-utils.js +2 -1
  30. package/src/clients/codex-process.js +1 -1
  31. package/src/clients/codex-utils.js +0 -2
  32. package/src/clients/databricks.js +337 -46
  33. package/src/clients/gpt-utils.js +84 -8
  34. package/src/clients/health-probe.js +157 -0
  35. package/src/clients/openai-format.js +73 -23
  36. package/src/clients/provider-capabilities.js +27 -2
  37. package/src/clients/resilience.js +0 -1
  38. package/src/clients/responses-format.js +0 -56
  39. package/src/clients/routing.js +1 -1
  40. package/src/config/index.js +13 -2
  41. package/src/context/budget.js +1 -1
  42. package/src/context/compression.js +127 -19
  43. package/src/context/tool-result-compressor.js +3 -5
  44. package/src/dashboard/api.js +1 -1
  45. package/src/logger/oversized-error-stream.js +1 -1
  46. package/src/memory/extractor.js +3 -3
  47. package/src/memory/search.js +2 -2
  48. package/src/memory/store.js +9 -24
  49. package/src/memory/tools.js +14 -0
  50. package/src/observability/metrics.js +52 -3
  51. package/src/observability/otel.js +180 -0
  52. package/src/orchestrator/azure-responses-sse.js +223 -0
  53. package/src/orchestrator/index.js +216 -212
  54. package/src/orchestrator/passthrough-stream.js +7 -1
  55. package/src/orchestrator/sse-transformer.js +130 -13
  56. package/src/policy/safe-commands.js +0 -1
  57. package/src/routing/bandit.js +39 -0
  58. package/src/routing/calibration.js +21 -1
  59. package/src/routing/capabilities.js +88 -0
  60. package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
  61. package/src/routing/capability-seeds/family-heuristics.js +77 -0
  62. package/src/routing/capability-seeds/family.js +105 -0
  63. package/src/routing/capability-seeds/models-dev.js +64 -0
  64. package/src/routing/capability-seeds/normalize.js +160 -0
  65. package/src/routing/capability-seeds/registry.js +125 -0
  66. package/src/routing/capability-seeds/sources.js +69 -0
  67. package/src/routing/capability-seeds/swebench.js +66 -0
  68. package/src/routing/client-profiles.js +26 -0
  69. package/src/routing/complexity-analyzer.js +5 -8
  70. package/src/routing/confidence-scorer.js +2 -2
  71. package/src/routing/cost-optimizer.js +1 -2
  72. package/src/routing/decide.js +149 -0
  73. package/src/routing/deescalator.js +38 -1
  74. package/src/routing/embedding-cache.js +0 -1
  75. package/src/routing/index.js +202 -86
  76. package/src/routing/intent-score.js +0 -1
  77. package/src/routing/knn-router.js +27 -5
  78. package/src/routing/latency-tracker.js +1 -1
  79. package/src/routing/model-registry.js +25 -0
  80. package/src/routing/model-slots.js +65 -0
  81. package/src/routing/model-tiers.js +2 -0
  82. package/src/routing/ope.js +255 -0
  83. package/src/routing/openai-model-slots.js +82 -0
  84. package/src/routing/regret-estimator.js +1 -1
  85. package/src/routing/shortfall.js +290 -0
  86. package/src/routing/stuck-detector.js +127 -0
  87. package/src/routing/telemetry.js +11 -2
  88. package/src/server.js +41 -3
  89. package/src/stores/file-store.js +5 -1
  90. package/src/tools/web-search-exec.js +182 -0
  91. package/src/training/trajectory-compressor.js +1 -1
  92. package/src/workers/pool.js +1 -1
  93. package/src/workers/worker.js +1 -1
  94. package/scripts/convert-routellm.py +0 -105
  95. package/src/diff/comments.js +0 -153
  96. package/src/policy/web-fallback.js +0 -33
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Server-side web_search / web_fetch execution — narrowly scoped resurrection.
3
+ *
4
+ * Context: src/tools/web.js used to implement this (SearXNG-backed search,
5
+ * host-allowlisted fetch) and was deleted 2026-07-22 in commit b32e988,
6
+ * "Remove server-mode tool execution: Lynkr always forwards tool calls to
7
+ * the client." That was a deliberate architecture pivot, not a bug — Lynkr
8
+ * stopped executing ANY tool itself.
9
+ *
10
+ * This does NOT reintroduce general server-mode tool execution. It only
11
+ * auto-resolves `web_search`/`web_fetch` calls, and ONLY when the calling
12
+ * client is unrecognized by src/routing/client-profiles.js's detectClient()
13
+ * — i.e. Lynkr has no signal the caller can fulfill the call itself.
14
+ * Known harnesses (Claude Code / Claude Desktop's shared agent-sdk — both
15
+ * present as `claude-cli/...`, Cursor, goose, Codex) already execute these
16
+ * client-side today and are never routed through this module; see the call
17
+ * site in src/orchestrator/index.js's runAgentLoop.
18
+ *
19
+ * @module tools/web-search-exec
20
+ */
21
+
22
+ const config = require("../config");
23
+ const logger = require("../logger");
24
+
25
+ const AUTO_RESOLVABLE_NAMES = new Set(["web_search", "websearch", "web_fetch", "webfetch"]);
26
+
27
+ /**
28
+ * @param {string} name
29
+ * @returns {boolean}
30
+ */
31
+ function isAutoResolvable(name) {
32
+ return AUTO_RESOLVABLE_NAMES.has(String(name || "").toLowerCase());
33
+ }
34
+
35
+ /**
36
+ * True only when every call in this batch is one we can resolve ourselves —
37
+ * a mixed batch (one auto-resolvable + one arbitrary client tool) falls
38
+ * through to the normal "forward to client" path untouched, since we can't
39
+ * partially resolve a batch the model expects answered together.
40
+ *
41
+ * @param {Array} toolCalls - normalized {id, function:{name}} or {name} calls
42
+ * @returns {boolean}
43
+ */
44
+ function canAutoResolveAll(toolCalls) {
45
+ return Array.isArray(toolCalls) && toolCalls.length > 0
46
+ && toolCalls.every((tc) => isAutoResolvable(tc?.function?.name ?? tc?.name));
47
+ }
48
+
49
+ function isHostAllowed(urlStr) {
50
+ if (config.webSearch?.allowAllHosts) return true;
51
+ try {
52
+ const host = new URL(urlStr).hostname.toLowerCase();
53
+ const allowed = config.webSearch?.allowedHosts;
54
+ return Array.isArray(allowed) && allowed.includes(host);
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ async function _withTimeout(fn, timeoutMs) {
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
63
+ try {
64
+ return await fn(controller.signal);
65
+ } finally {
66
+ clearTimeout(timer);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * SearXNG-backed search. Retries transient failures per config.webSearch
72
+ * (retryEnabled/maxRetries) — mirrors the retry policy the deleted web.js
73
+ * had, since a single dropped connection to a local sidecar shouldn't fail
74
+ * the whole tool call.
75
+ *
76
+ * @param {string} query
77
+ * @returns {Promise<Object>} { query, results: [{title,url,snippet}] } or { error }
78
+ */
79
+ async function searchWeb(query) {
80
+ const endpoint = config.webSearch?.endpoint || "http://localhost:8888/search";
81
+ const timeoutMs = config.webSearch?.timeoutMs || 10000;
82
+ const maxRetries = config.webSearch?.retryEnabled ? (config.webSearch?.maxRetries ?? 2) : 0;
83
+ const url = `${endpoint}?q=${encodeURIComponent(query || "")}&format=json`;
84
+
85
+ let lastErr = null;
86
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
87
+ try {
88
+ const res = await _withTimeout(
89
+ (signal) => fetch(url, { signal }),
90
+ timeoutMs,
91
+ );
92
+ if (!res.ok) {
93
+ lastErr = `search backend returned HTTP ${res.status}`;
94
+ continue;
95
+ }
96
+ const json = await res.json();
97
+ const results = (Array.isArray(json.results) ? json.results : [])
98
+ .slice(0, 8)
99
+ .map((r) => ({ title: r.title, url: r.url, snippet: r.content }));
100
+ return { query, results };
101
+ } catch (err) {
102
+ lastErr = err.message;
103
+ }
104
+ }
105
+ logger.warn({ query, endpoint, err: lastErr }, "[web-search-exec] search failed after retries");
106
+ return { query, error: `search failed: ${lastErr}` };
107
+ }
108
+
109
+ /**
110
+ * Generic URL fetch, host-allowlisted per config.webSearch (allowAllHosts /
111
+ * allowedHosts), truncated per config.webSearch.bodyPreviewMax.
112
+ *
113
+ * @param {string} targetUrl
114
+ * @returns {Promise<Object>}
115
+ */
116
+ async function fetchUrl(targetUrl) {
117
+ if (!targetUrl) return { error: "no url provided" };
118
+ if (!isHostAllowed(targetUrl)) {
119
+ logger.warn({ url: targetUrl }, "[web-search-exec] fetch blocked — host not allowed");
120
+ return { url: targetUrl, error: "host not in allowedHosts (WEB_SEARCH_ALLOW_ALL=false)" };
121
+ }
122
+ const maxPreview = config.webSearch?.bodyPreviewMax || 10000;
123
+ const timeoutMs = config.webSearch?.timeoutMs || 10000;
124
+ try {
125
+ const res = await _withTimeout(
126
+ (signal) => fetch(targetUrl, { signal }),
127
+ timeoutMs,
128
+ );
129
+ const text = await res.text();
130
+ return {
131
+ url: targetUrl,
132
+ status: res.status,
133
+ content: text.slice(0, maxPreview),
134
+ truncated: text.length > maxPreview,
135
+ };
136
+ } catch (err) {
137
+ logger.warn({ url: targetUrl, err: err.message }, "[web-search-exec] fetch failed");
138
+ return { url: targetUrl, error: `fetch failed: ${err.message}` };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Execute every call in this (pre-checked via canAutoResolveAll) batch and
144
+ * append the matching assistant tool_use + user tool_result turns to
145
+ * `messages`, mirroring exactly what a real client round-trip would send
146
+ * back — so the next agent-loop iteration behaves identically to a normal
147
+ * client-fulfilled tool exchange, and the client never sees the exchange.
148
+ *
149
+ * @param {Array} toolCalls
150
+ * @param {Array} messages - mutated in place (push only, never rewritten)
151
+ */
152
+ async function autoResolve(toolCalls, messages) {
153
+ const toolUseBlocks = [];
154
+ const toolResultBlocks = [];
155
+
156
+ for (const tc of toolCalls) {
157
+ const rawName = tc.function?.name ?? tc.name ?? "";
158
+ const name = String(rawName).toLowerCase();
159
+ let input = {};
160
+ try {
161
+ const raw = tc.function?.arguments ?? tc.input ?? {};
162
+ input = typeof raw === "string" ? JSON.parse(raw) : (raw || {});
163
+ } catch { /* leave input empty — result will just reflect an empty query/url */ }
164
+
165
+ toolUseBlocks.push({ type: "tool_use", id: tc.id, name: rawName, input });
166
+
167
+ const result = (name === "web_search" || name === "websearch")
168
+ ? await searchWeb(input.query || input.q || "")
169
+ : await fetchUrl(input.url || "");
170
+
171
+ toolResultBlocks.push({
172
+ type: "tool_result",
173
+ tool_use_id: tc.id,
174
+ content: JSON.stringify(result),
175
+ });
176
+ }
177
+
178
+ messages.push({ role: "assistant", content: toolUseBlocks });
179
+ messages.push({ role: "user", content: toolResultBlocks });
180
+ }
181
+
182
+ module.exports = { canAutoResolveAll, autoResolve, searchWeb, fetchUrl, isAutoResolvable };
@@ -38,7 +38,7 @@ const telemetry = require("../routing/telemetry");
38
38
  const ANONYMIZE_PATTERNS = [
39
39
  // API keys and bearer tokens
40
40
  [/sk-[A-Za-z0-9_-]{20,}/g, "<API_KEY>"],
41
- [/Bearer\s+[A-Za-z0-9._\-]+/gi, "Bearer <REDACTED>"],
41
+ [/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer <REDACTED>"],
42
42
  [/dapi_[A-Za-z0-9_-]+/g, "<DATABRICKS_KEY>"],
43
43
  [/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "<JWT>"],
44
44
  // AWS keys
@@ -310,7 +310,7 @@ class WorkerPool {
310
310
  logger.info('[WorkerPool] Shutting down...');
311
311
 
312
312
  // Reject all pending tasks
313
- for (const [taskId, pending] of this.pendingTasks) {
313
+ for (const [_taskId, pending] of this.pendingTasks) {
314
314
  clearTimeout(pending.timeout);
315
315
  pending.reject(new Error('Worker pool shutting down'));
316
316
  }
@@ -65,7 +65,7 @@ const handlers = {
65
65
  let tokensReduced = 0;
66
66
  const seenToolResults = new Map(); // For deduplication
67
67
 
68
- const transformed = messages.map((msg, idx) => {
68
+ const transformed = messages.map((msg) => {
69
69
  if (!msg) return msg;
70
70
 
71
71
  // Clone to avoid mutating original
@@ -1,105 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Convert routellm/gpt4_dataset (train.jsonl) → Lynkr kNN bootstrap JSONL.
4
-
5
- Stream-downloads from HuggingFace and maps mixtral_score (1-5) to your
6
- TIER_* model config so the kNN index learns to recommend YOUR real models,
7
- not RouteLLM's pool (mixtral / gpt-4).
8
-
9
- Output format matches scripts/build-knn-index.js bootstrap input:
10
- {"query": "...", "provider": "ollama", "model": "minimax-m2.5:cloud",
11
- "quality": 95, "cost": 0.001, "latency": 200, "tier": "SIMPLE"}
12
-
13
- Usage:
14
- .venv/bin/python scripts/convert-routellm.py [--limit 30000] -o routellm.jsonl
15
- """
16
- import argparse
17
- import json
18
- import sys
19
- import urllib.request
20
- import ssl
21
-
22
- # Map mixtral_score → user's tier config.
23
- # Spread is intentionally aggressive: only score=5 (Mixtral truly nailed it)
24
- # stays SIMPLE; score=4 escalates to MEDIUM so the kNN index has enough MEDIUM
25
- # examples to actually recommend non-SIMPLE tiers (vs the previous mapping
26
- # where 86% of records were SIMPLE and kNN never disagreed with the heuristic).
27
- SCORE_TO_ROUTE = {
28
- 5: {"tier": "SIMPLE", "provider": "ollama", "model": "minimax-m2.5:cloud", "quality": 95, "cost": 0.001, "latency": 200},
29
- 4: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 80, "cost": 0.003, "latency": 1500},
30
- 3: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 70, "cost": 0.003, "latency": 1500},
31
- 2: {"tier": "COMPLEX", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 55, "cost": 0.003, "latency": 1500},
32
- 1: {"tier": "REASONING", "provider": "azure-anthropic", "model": "claude-opus-4-8", "quality": 40, "cost": 0.015, "latency": 3500},
33
- }
34
-
35
- URL = "https://huggingface.co/datasets/routellm/gpt4_dataset/resolve/main/train.jsonl"
36
-
37
-
38
- def main():
39
- p = argparse.ArgumentParser()
40
- p.add_argument("--output", "-o", default="data/routellm-bootstrap.jsonl")
41
- p.add_argument("--limit", type=int, default=None,
42
- help="Only convert first N rows")
43
- args = p.parse_args()
44
-
45
- print(f"Streaming {URL} …", file=sys.stderr)
46
- # Some macOS Python builds have SSL issues with huggingface.co — fall back
47
- # to unverified context if needed. The data is public, no auth.
48
- ctx = ssl.create_default_context()
49
- try:
50
- with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
51
- _stream(resp, args.output, args.limit)
52
- except (ssl.SSLError, OSError) as e:
53
- print(f"SSL strict failed ({e}); retrying with unverified context", file=sys.stderr)
54
- ctx = ssl._create_unverified_context()
55
- with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
56
- _stream(resp, args.output, args.limit)
57
-
58
-
59
- def _stream(resp, out_path, limit):
60
- written = 0
61
- by_score = {s: 0 for s in SCORE_TO_ROUTE}
62
- skipped = 0
63
- with open(out_path, "w") as out:
64
- for line in resp:
65
- if limit and written >= limit:
66
- break
67
- try:
68
- row = json.loads(line)
69
- except Exception:
70
- skipped += 1
71
- continue
72
- score = row.get("mixtral_score")
73
- if score not in SCORE_TO_ROUTE:
74
- skipped += 1
75
- continue
76
- route = SCORE_TO_ROUTE[score]
77
- rec = {
78
- "query": row.get("prompt", ""),
79
- "provider": route["provider"],
80
- "model": route["model"],
81
- "quality": route["quality"],
82
- "cost": route["cost"],
83
- "latency": route["latency"],
84
- "tier": route["tier"],
85
- }
86
- if not rec["query"]:
87
- skipped += 1
88
- continue
89
- out.write(json.dumps(rec) + "\n")
90
- written += 1
91
- by_score[score] += 1
92
- if written % 5000 == 0:
93
- print(f" {written} written…", file=sys.stderr)
94
-
95
- print(f"\nWrote {written} records to {out_path} ({skipped} skipped)", file=sys.stderr)
96
- print(f"By mixtral_score: {by_score}", file=sys.stderr)
97
- print(f"\nTier distribution:", file=sys.stderr)
98
- for score, route in SCORE_TO_ROUTE.items():
99
- n = by_score[score]
100
- if n:
101
- print(f" {route['tier']:>10} {route['provider']}:{route['model']:30s} n={n}", file=sys.stderr)
102
-
103
-
104
- if __name__ == "__main__":
105
- main()
@@ -1,153 +0,0 @@
1
- const crypto = require("crypto");
2
- const db = require("../db");
3
- const logger = require("../logger");
4
-
5
- const insertCommentStmt = db.prepare(
6
- `INSERT INTO diff_comments (
7
- thread_id,
8
- session_id,
9
- file_path,
10
- hunk,
11
- line,
12
- comment,
13
- author,
14
- created_at,
15
- metadata
16
- ) VALUES (
17
- @thread_id,
18
- @session_id,
19
- @file_path,
20
- @hunk,
21
- @line,
22
- @comment,
23
- @author,
24
- @created_at,
25
- @metadata
26
- )`,
27
- );
28
-
29
- const deleteCommentStmt = db.prepare("DELETE FROM diff_comments WHERE id = ?");
30
-
31
- function buildSelectCommentsQuery({ filePath, threadId }) {
32
- let sql = `SELECT id,
33
- thread_id,
34
- session_id,
35
- file_path,
36
- hunk,
37
- line,
38
- comment,
39
- author,
40
- created_at,
41
- metadata
42
- FROM diff_comments`;
43
- const conditions = [];
44
- const params = [];
45
- if (filePath) {
46
- conditions.push("file_path = ?");
47
- params.push(filePath);
48
- }
49
- if (threadId) {
50
- conditions.push("thread_id = ?");
51
- params.push(threadId);
52
- }
53
- if (conditions.length) {
54
- sql += ` WHERE ${conditions.join(" AND ")}`;
55
- }
56
- sql += " ORDER BY created_at ASC, id ASC";
57
- return { sql, params };
58
- }
59
-
60
- function normaliseCommentRow(row) {
61
- return {
62
- id: row.id,
63
- threadId: row.thread_id ?? null,
64
- sessionId: row.session_id ?? null,
65
- filePath: row.file_path,
66
- hunk: row.hunk ?? null,
67
- line: typeof row.line === "number" ? row.line : null,
68
- comment: row.comment,
69
- author: row.author ?? null,
70
- createdAt: row.created_at,
71
- metadata: row.metadata ? JSON.parse(row.metadata) : null,
72
- };
73
- }
74
-
75
- function addDiffComment({
76
- threadId,
77
- sessionId,
78
- filePath,
79
- line,
80
- hunk,
81
- comment,
82
- author,
83
- metadata,
84
- }) {
85
- if (typeof filePath !== "string" || filePath.trim().length === 0) {
86
- throw new Error("Diff comment requires a file path.");
87
- }
88
- if (typeof comment !== "string" || comment.trim().length === 0) {
89
- throw new Error("Diff comment requires non-empty comment text.");
90
- }
91
-
92
- const createdAt = Date.now();
93
- const thread = threadId ?? crypto.randomUUID();
94
-
95
- const params = {
96
- thread_id: thread,
97
- session_id: sessionId ?? null,
98
- file_path: filePath,
99
- hunk: hunk ?? null,
100
- line: typeof line === "number" ? line : null,
101
- comment,
102
- author: author ?? null,
103
- created_at: createdAt,
104
- metadata: metadata ? JSON.stringify(metadata) : null,
105
- };
106
-
107
- const result = insertCommentStmt.run(params);
108
-
109
- logger.debug(
110
- {
111
- id: result.lastInsertRowid,
112
- thread,
113
- filePath,
114
- line,
115
- },
116
- "Recorded diff comment",
117
- );
118
-
119
- return normaliseCommentRow({
120
- id: result.lastInsertRowid,
121
- thread_id: thread,
122
- session_id: sessionId ?? null,
123
- file_path: filePath,
124
- hunk: hunk ?? null,
125
- line: typeof line === "number" ? line : null,
126
- comment,
127
- author: author ?? null,
128
- created_at: createdAt,
129
- metadata: metadata ? JSON.stringify(metadata) : null,
130
- });
131
- }
132
-
133
- function listDiffComments({ filePath, threadId } = {}) {
134
- const { sql, params } = buildSelectCommentsQuery({ filePath, threadId });
135
- return db
136
- .prepare(sql)
137
- .all(...params)
138
- .map(normaliseCommentRow);
139
- }
140
-
141
- function deleteDiffComment({ id }) {
142
- if (!id) {
143
- throw new Error("diff_comment_delete requires an id.");
144
- }
145
- const result = deleteCommentStmt.run(id);
146
- return result.changes > 0;
147
- }
148
-
149
- module.exports = {
150
- addDiffComment,
151
- listDiffComments,
152
- deleteDiffComment,
153
- };
@@ -1,33 +0,0 @@
1
- const BROWSING_FALLBACK_PATTERNS = [
2
- /i (do|don't|cannot) have (browser|browsing|internet) (capability|access)/i,
3
- /cannot look up information/i,
4
- /no web browsing capability/i,
5
- /can'?t (access|reach) the internet/i,
6
- /(do not|don't) have access to .*web (?:browsing|browser|internet)/i,
7
- /(do not|don't) have .*browser/i,
8
- /web(fetch|_fetch| search).*(not available|disabled|unavailable)/i,
9
- /tool.*(not available|disabled|unavailable)/i,
10
- /don't have access to real-time/i,
11
- ];
12
-
13
- function needsWebFallback(text) {
14
- if (typeof text !== "string") return false;
15
- const trimmed = text.trim();
16
- if (!trimmed) return false;
17
-
18
- // If the response already includes concrete financial data, skip fallback.
19
- if (
20
- /\bclosed at \$\d[\d.,]*/i.test(trimmed) ||
21
- /\bprevious close\b/i.test(trimmed) ||
22
- /\bday'?s range\b/i.test(trimmed) ||
23
- /\btrading volume\b/i.test(trimmed)
24
- ) {
25
- return false;
26
- }
27
-
28
- return BROWSING_FALLBACK_PATTERNS.some((regex) => regex.test(trimmed));
29
- }
30
-
31
- module.exports = {
32
- needsWebFallback,
33
- };