lynkr 9.7.3 → 9.9.0

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 (74) hide show
  1. package/README.md +29 -19
  2. package/bin/cli.js +11 -0
  3. package/bin/lynkr-init.js +14 -1
  4. package/bin/lynkr-usage.js +78 -0
  5. package/bin/wrap.js +60 -35
  6. package/config/difficulty-anchors.json +22 -0
  7. package/package.json +24 -3
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/calibrate-thresholds.js +38 -157
  10. package/scripts/compact-dictionary.js +204 -0
  11. package/scripts/test-deduplication.js +448 -0
  12. package/scripts/ws7-anchor-replay.js +108 -0
  13. package/skills/lynkr/SKILL.md +195 -0
  14. package/src/api/middleware/loop-guard.js +87 -0
  15. package/src/api/middleware/request-logging.js +5 -64
  16. package/src/api/middleware/session.js +0 -0
  17. package/src/api/openai-router.js +120 -101
  18. package/src/api/providers-handler.js +27 -2
  19. package/src/api/router.js +450 -125
  20. package/src/budget/index.js +2 -19
  21. package/src/cache/semantic.js +9 -0
  22. package/src/clients/databricks.js +455 -142
  23. package/src/clients/gpt-utils.js +11 -105
  24. package/src/clients/openai-format.js +10 -3
  25. package/src/clients/openrouter-utils.js +49 -24
  26. package/src/clients/prompt-cache-injection.js +1 -0
  27. package/src/clients/provider-capabilities.js +1 -1
  28. package/src/clients/responses-format.js +34 -3
  29. package/src/clients/routing.js +15 -0
  30. package/src/config/index.js +36 -2
  31. package/src/context/gcf.js +275 -0
  32. package/src/context/tool-result-compressor.js +51 -9
  33. package/src/dashboard/api.js +1 -0
  34. package/src/logger/index.js +14 -1
  35. package/src/memory/search.js +12 -40
  36. package/src/memory/tools.js +3 -24
  37. package/src/orchestrator/bypass.js +4 -2
  38. package/src/orchestrator/index.js +120 -85
  39. package/src/routing/affinity-store.js +194 -0
  40. package/src/routing/agentic-detector.js +36 -6
  41. package/src/routing/bandit.js +25 -6
  42. package/src/routing/calibration.js +212 -0
  43. package/src/routing/client-profiles.js +292 -0
  44. package/src/routing/complexity-analyzer.js +48 -11
  45. package/src/routing/deescalator.js +148 -0
  46. package/src/routing/degradation.js +109 -0
  47. package/src/routing/feedback.js +157 -0
  48. package/src/routing/index.js +897 -87
  49. package/src/routing/intent-score.js +339 -0
  50. package/src/routing/interaction.js +3 -0
  51. package/src/routing/knn-router.js +70 -21
  52. package/src/routing/model-registry.js +28 -7
  53. package/src/routing/model-tiers.js +25 -2
  54. package/src/routing/reward-pipeline.js +68 -2
  55. package/src/routing/risk-analyzer.js +30 -1
  56. package/src/routing/risk-classifier.js +6 -2
  57. package/src/routing/session-affinity.js +162 -34
  58. package/src/routing/telemetry.js +286 -13
  59. package/src/routing/verifier.js +267 -0
  60. package/src/server.js +66 -21
  61. package/src/sessions/cleanup.js +17 -0
  62. package/src/tools/index.js +1 -15
  63. package/src/tools/smart-selection.js +10 -0
  64. package/src/tools/web-client.js +3 -3
  65. package/.eslintrc.cjs +0 -12
  66. package/benchmark-configs/litellm_config.yaml +0 -86
  67. package/benchmark-configs/lynkr.env +0 -48
  68. package/benchmark-configs/portkey-config.json +0 -60
  69. package/benchmark-configs/portkey-docker.sh +0 -23
  70. package/benchmark-tier-routing.js +0 -449
  71. package/funding.json +0 -110
  72. package/src/api/middleware/validation.js +0 -261
  73. package/src/routing/drift-monitor.js +0 -113
  74. package/src/workers/helpers.js +0 -185
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WS7.2 — replay real telemetry request texts through the anchor classifier
4
+ * and measure whether {trivial, substantive, heavyweight} separate cleanly
5
+ * in cosine space. This is the evidence gate for keeping the three-way
6
+ * blend (vs collapsing to a single cheap-vs-capable threshold).
7
+ *
8
+ * Usage: node scripts/ws7-anchor-replay.js [--db .lynkr/telemetry.db] [--limit N]
9
+ * Requires: local Ollama with the configured embeddings model.
10
+ *
11
+ * Reports:
12
+ * - per-class counts + score distribution
13
+ * - top-2 sim margin per class (median/p25) — the separation metric.
14
+ * Margin >= ~0.05 median = classes separate; < 0.02 = collapse to binary.
15
+ * - tier movement vs the recorded (lexical-era) tier.
16
+ */
17
+ const path = require('path');
18
+ const Database = require('better-sqlite3');
19
+
20
+ const args = process.argv.slice(2);
21
+ const dbPath = args.includes('--db')
22
+ ? args[args.indexOf('--db') + 1]
23
+ : path.join(__dirname, '../.lynkr/telemetry.db');
24
+ const limit = args.includes('--limit') ? Number(args[args.indexOf('--limit') + 1]) : 100000;
25
+
26
+ const MINING_SQL = `
27
+ SELECT DISTINCT request_text, tier, quality_score FROM routing_telemetry
28
+ WHERE request_text IS NOT NULL AND length(request_text)>3
29
+ AND request_text NOT LIKE '%system-reminder%'
30
+ AND request_text NOT LIKE '%SUGGESTION MODE%'
31
+ AND request_text NOT LIKE '%[Lynkr]%'
32
+ AND request_text NOT LIKE 'quota%'
33
+ AND request_text NOT LIKE '<session>%' AND request_text NOT LIKE '<conversation>%'
34
+ LIMIT ?`;
35
+
36
+ function pct(arr, p) {
37
+ if (!arr.length) return null;
38
+ const s = [...arr].sort((a, b) => a - b);
39
+ return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
40
+ }
41
+
42
+ async function main() {
43
+ const { scoreIntent, getDefaultCentroids } = require('../src/routing/intent-score');
44
+ const { getModelTierSelector } = require('../src/routing/model-tiers');
45
+
46
+ const centroids = await getDefaultCentroids();
47
+ if (!centroids) {
48
+ console.error('FATAL: could not build anchor centroids — is Ollama running with the embeddings model?');
49
+ process.exit(1);
50
+ }
51
+
52
+ const db = new Database(dbPath, { readonly: true });
53
+ const rows = db.prepare(MINING_SQL).all(limit);
54
+ console.log(`Mined ${rows.length} distinct request texts from ${dbPath}\n`);
55
+
56
+ const selector = getModelTierSelector();
57
+ const byClass = {};
58
+ const margins = { trivial: [], substantive: [], heavyweight: [] };
59
+ const moves = {};
60
+ let anchorMode = 0;
61
+ let lexicalFallback = 0;
62
+
63
+ for (const row of rows) {
64
+ const r = await scoreIntent({ messages: [{ role: 'user', content: row.request_text }] });
65
+ if (!r) continue;
66
+ if (r.mode !== 'anchor') { lexicalFallback++; continue; }
67
+ anchorMode++;
68
+ const cls = r.class;
69
+ byClass[cls] = byClass[cls] || { n: 0, scores: [], examples: [] };
70
+ byClass[cls].n++;
71
+ byClass[cls].scores.push(r.score);
72
+ if (byClass[cls].examples.length < 4) {
73
+ byClass[cls].examples.push(`${r.score.toString().padStart(2)} ${row.request_text.slice(0, 90).replace(/\n/g, ' ')}`);
74
+ }
75
+ const sims = Object.entries(r.sims).sort((a, b) => b[1] - a[1]);
76
+ margins[cls].push(sims[0][1] - sims[1][1]);
77
+
78
+ const newTier = selector.getTier(r.score);
79
+ const oldTier = row.tier || '??';
80
+ const key = `${oldTier} -> ${newTier}`;
81
+ moves[key] = (moves[key] || 0) + 1;
82
+ }
83
+
84
+ console.log(`Scored: ${anchorMode} anchor-mode, ${lexicalFallback} lexical-fallback\n`);
85
+ console.log('=== Class distribution + score stats ===');
86
+ for (const [cls, d] of Object.entries(byClass)) {
87
+ console.log(`\n${cls}: n=${d.n} score median=${pct(d.scores, 50)} p10=${pct(d.scores, 10)} p90=${pct(d.scores, 90)}`);
88
+ console.log(` top-2 sim margin: median=${pct(margins[cls], 50)?.toFixed(4)} p25=${pct(margins[cls], 25)?.toFixed(4)} min=${Math.min(...margins[cls]).toFixed(4)}`);
89
+ for (const e of d.examples) console.log(` ${e}`);
90
+ }
91
+
92
+ console.log('\n=== Tier movement (recorded lexical-era tier -> anchor tier) ===');
93
+ for (const [k, v] of Object.entries(moves).sort((a, b) => b[1] - a[1])) {
94
+ console.log(` ${k}: ${v}`);
95
+ }
96
+
97
+ const allMargins = Object.values(margins).flat();
98
+ const med = pct(allMargins, 50);
99
+ console.log(`\n=== VERDICT ===`);
100
+ console.log(`Overall top-2 sim margin: median=${med?.toFixed(4)} p25=${pct(allMargins, 25)?.toFixed(4)}`);
101
+ console.log(med >= 0.05
102
+ ? 'Classes separate cleanly (median margin >= 0.05) — three-way blend is supported by the data.'
103
+ : med >= 0.02
104
+ ? 'Borderline separation — keep three-way but watch misroutes; consider more anchors.'
105
+ : 'Classes do NOT separate — collapse to binary (trivial vs rest).');
106
+ }
107
+
108
+ main().catch((err) => { console.error(err); process.exit(1); });
@@ -0,0 +1,195 @@
1
+ ---
2
+ name: lynkr
3
+ display_name: Lynkr AI Routing Proxy
4
+ version: 8.0.1
5
+ description: Universal LLM gateway with intelligent routing, Graphify code intelligence, Distill compression, routing telemetry, Code Mode, and 12+ provider support. 60-80% cost reduction for Claude Code, Cursor, and Codex.
6
+ author: lynkr-ai
7
+ license: MIT
8
+ tags:
9
+ - routing
10
+ - proxy
11
+ - llm
12
+ - multi-provider
13
+ - ollama
14
+ - openai
15
+ - anthropic
16
+ - bedrock
17
+ - cost-optimization
18
+ - graphify
19
+ - distill
20
+ - telemetry
21
+ - code-mode
22
+ - compression
23
+ - memory
24
+ category: infrastructure
25
+ homepage: https://github.com/Fast-Editor/Lynkr
26
+ npm: lynkr
27
+ requires:
28
+ node: ">=20"
29
+ providers:
30
+ - ollama
31
+ - databricks
32
+ - azure-anthropic
33
+ - azure-openai
34
+ - openrouter
35
+ - openai
36
+ - bedrock
37
+ - vertex
38
+ - moonshot
39
+ - zai
40
+ - llamacpp
41
+ - lmstudio
42
+ - codex
43
+ - deepseek
44
+ ---
45
+
46
+ # Lynkr - Universal LLM Gateway
47
+
48
+ Lynkr routes AI coding requests to the optimal model based on task complexity, cost, and provider health. Supports 12+ providers with 60-80% cost reduction through intelligent token optimization.
49
+
50
+ ## Quick Start
51
+
52
+ ```bash
53
+ npm install -g lynkr
54
+ lynkr-setup # Auto-installs Ollama + pulls a model
55
+ lynkr # Start the proxy
56
+ ```
57
+
58
+ Then point your AI coding tool at `http://localhost:8081/v1`.
59
+
60
+ ## How It Works
61
+
62
+ 1. **5-Phase Complexity Analysis** - Scores each request 0-100 using token count, tool usage, code patterns, domain keywords, and Graphify structural analysis (god nodes, community cohesion, blast radius)
63
+ 2. **4-Tier Routing** - Maps score to SIMPLE/MEDIUM/COMPLEX/REASONING, each with a configured provider:model
64
+ 3. **Agentic Detection** - Detects multi-step workflows (tool loops, autonomous agents) and upgrades to higher tiers
65
+ 4. **Cost Optimization** - Picks the cheapest provider that can handle the tier
66
+ 5. **Circuit Breaker + Failover** - Automatic failover with half-open probe recovery
67
+
68
+ ## Key Features (v8.0)
69
+
70
+ ### Intelligent Routing
71
+ - 5-phase complexity scoring with 15-dimension weighted mode
72
+ - Agentic workflow detection (SINGLE_SHOT / TOOL_CHAIN / ITERATIVE / AUTONOMOUS)
73
+ - Graphify knowledge graph integration — god node detection, community cohesion, blast radius
74
+ - Routing telemetry with SQLite store, quality scoring (0-100), latency tracking (P50/P95/P99)
75
+
76
+ ### Token Optimization (60-80% savings)
77
+ - **Smart tool selection** — filters tools by request type
78
+ - **Distill compression** — structural similarity (Jaccard), delta rendering, block dedup
79
+ - **Code Mode** — replaces 100+ MCP tools with 4 meta-tools (~96% token reduction)
80
+ - **History compression** — sliding window with Distill-powered dedup
81
+ - **Prompt caching** — SHA-256 keyed LRU cache
82
+ - **Headroom sidecar** — optional 47-92% compression via Smart Crusher, CCR, LLMLingua
83
+
84
+ ### Production Hardening
85
+ - Circuit breakers with half-open probe recovery
86
+ - Admin hot-reload endpoint (POST /v1/admin/reload) — no restart needed
87
+ - Per-request performance timing (PERF_TIMER=true)
88
+ - Prometheus metrics, structured logging, health checks
89
+ - Rate limiting, load shedding, input validation
90
+
91
+ ### Long-Term Memory (Titans-Inspired)
92
+ - Surprise-based memory storage with decay
93
+ - Semantic search via FTS5
94
+ - Automatic extraction and injection
95
+
96
+ ## Configuration for OpenClaw
97
+
98
+ Set tier routing in your environment:
99
+
100
+ ```env
101
+ MODEL_PROVIDER=ollama
102
+ TIER_SIMPLE=ollama:llama3.2
103
+ TIER_MEDIUM=openrouter:anthropic/claude-sonnet-4
104
+ TIER_COMPLEX=bedrock:anthropic.claude-sonnet-4-20250514-v1:0
105
+ TIER_REASONING=bedrock:anthropic.claude-opus-4-20250514-v1:0
106
+ ```
107
+
108
+ ### OpenClaw Mode
109
+
110
+ When running under OpenClaw, enable model name rewriting:
111
+
112
+ ```env
113
+ OPENCLAW_MODE=true
114
+ ```
115
+
116
+ This replaces the generic `model: "auto"` in responses with the actual `provider/model` that handled the request.
117
+
118
+ ## Provider Registration
119
+
120
+ Add to your `openclaw.json`:
121
+
122
+ ```json
123
+ {
124
+ "models": {
125
+ "providers": [
126
+ {
127
+ "name": "lynkr",
128
+ "type": "openai-compatible",
129
+ "base_url": "http://localhost:8081/v1",
130
+ "api_key": "any-value",
131
+ "models": ["auto"]
132
+ }
133
+ ]
134
+ },
135
+ "agents": {
136
+ "defaults": {
137
+ "models": {
138
+ "primary": "lynkr/auto",
139
+ "fallback": "lynkr/auto"
140
+ }
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## Providers
147
+
148
+ | Provider | Type | Models |
149
+ |----------|------|--------|
150
+ | Ollama | Local (free) | llama3.2, qwen2.5-coder, deepseek-coder, mistral |
151
+ | llama.cpp | Local (free) | Any GGUF model |
152
+ | LM Studio | Local (free) | Any downloaded model |
153
+ | OpenAI | Cloud | gpt-4o, o3, o4-mini |
154
+ | Anthropic | Cloud | claude-opus-4, claude-sonnet-4, claude-haiku-4.5 |
155
+ | Databricks | Cloud | Claude, GPT, Llama via Foundation Model APIs |
156
+ | AWS Bedrock | Cloud | Claude, Titan, Llama, Mistral |
157
+ | Azure OpenAI | Cloud | GPT-4o, o1, o3 |
158
+ | OpenRouter | Cloud | 100+ models |
159
+ | Google Vertex | Cloud | Gemini 2.5 Pro/Flash |
160
+ | Moonshot AI | Cloud | Kimi K2 Thinking/Turbo |
161
+ | Z.AI | Cloud | GLM-4.7 |
162
+ | DeepSeek | Cloud | DeepSeek Reasoner, R1 |
163
+
164
+ ## New in v8.0
165
+
166
+ - **Graphify Integration** — AST-based knowledge graph with 19-language support for blast radius analysis
167
+ - **Distill Compression** — Structural similarity, delta rendering, and smart dedup
168
+ - **Routing Telemetry** — SQLite-backed decision recording with quality scoring
169
+ - **Code Mode** — 4 MCP meta-tools replace 100+ individual definitions
170
+ - **Admin Reload** — Hot-reload config + reset circuit breakers without restart
171
+ - **Performance Timer** — Per-request timing breakdown (PERF_TIMER=true)
172
+ - **Large Payload Passthrough** — Smart cloning skips base64 media that will be discarded
173
+
174
+ ## Response Headers
175
+
176
+ | Header | Description |
177
+ |--------|-------------|
178
+ | `X-Lynkr-Provider` | Provider that handled the request |
179
+ | `X-Lynkr-Model` | Model used |
180
+ | `X-Lynkr-Tier` | Complexity tier (SIMPLE/MEDIUM/COMPLEX/REASONING) |
181
+ | `X-Lynkr-Complexity-Score` | Numeric score 0-100 |
182
+ | `X-Lynkr-Routing-Method` | How the route was decided |
183
+ | `X-Lynkr-Agentic` | Agentic workflow type (if detected) |
184
+ | `X-Lynkr-Cost-Optimized` | Whether cost optimization changed the provider |
185
+
186
+ ## Telemetry Endpoints
187
+
188
+ | Endpoint | Description |
189
+ |----------|-------------|
190
+ | `GET /v1/routing/stats` | Aggregated routing stats with latency percentiles |
191
+ | `GET /v1/routing/stats/:provider` | Per-provider statistics |
192
+ | `GET /v1/routing/telemetry` | Raw telemetry records |
193
+ | `GET /v1/routing/accuracy` | Over/under-provisioned routing detection |
194
+ | `POST /v1/admin/reload` | Hot-reload config + reset circuit breakers |
195
+ | `POST /v1/admin/circuit-breakers/reset` | Reset circuit breakers |
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Loop guard — runaway-agent circuit breaker (WS: multi-agent cost controls).
3
+ *
4
+ * Agent frameworks (LangGraph, CrewAI, AutoGen) resend the full growing
5
+ * conversation every round-trip, so an uncapped tool loop compounds: each
6
+ * turn pays for every previous turn again. Real-world failure mode this
7
+ * guards: an AutoGen deployment ran 40% over budget in its first month from
8
+ * exactly this (uncapped reviewer<->coder loops).
9
+ *
10
+ * The guard is STATELESS — it reads loop depth from the incoming payload
11
+ * itself rather than tracking sessions:
12
+ * - message count ≈ total turns so far
13
+ * - tool_result count ≈ tool-loop iterations so far
14
+ * Both survive proxy restarts and need no session affinity.
15
+ *
16
+ * Disabled by default (caps unset/0). Enable per deployment:
17
+ * LYNKR_MAX_SESSION_TURNS=80 reject when messages[] exceeds 80 entries
18
+ * LYNKR_MAX_TOOL_TURNS=25 reject when tool_result blocks exceed 25
19
+ *
20
+ * Rejections use 429 + a machine-readable error type so agent frameworks
21
+ * surface them as a hard stop instead of retrying blindly.
22
+ */
23
+
24
+ const logger = require("../../logger");
25
+
26
+ function _countToolResults(messages) {
27
+ let n = 0;
28
+ for (const m of messages) {
29
+ if (!Array.isArray(m?.content)) continue;
30
+ for (const block of m.content) {
31
+ if (block?.type === "tool_result") n++;
32
+ }
33
+ }
34
+ return n;
35
+ }
36
+
37
+ function _cap(name) {
38
+ const v = Number(process.env[name]);
39
+ return Number.isFinite(v) && v > 0 ? Math.floor(v) : null;
40
+ }
41
+
42
+ function loopGuard(req, res, next) {
43
+ const maxTurns = _cap("LYNKR_MAX_SESSION_TURNS");
44
+ const maxToolTurns = _cap("LYNKR_MAX_TOOL_TURNS");
45
+ if (!maxTurns && !maxToolTurns) return next();
46
+
47
+ const messages = Array.isArray(req.body?.messages) ? req.body.messages : [];
48
+
49
+ if (maxTurns && messages.length > maxTurns) {
50
+ logger.warn(
51
+ { messageCount: messages.length, cap: maxTurns, path: req.path },
52
+ "[LoopGuard] conversation exceeded turn cap"
53
+ );
54
+ return res.status(429).json({
55
+ error: {
56
+ type: "loop_cap_exceeded",
57
+ message:
58
+ `Conversation has ${messages.length} messages, over the configured ` +
59
+ `cap of ${maxTurns} (LYNKR_MAX_SESSION_TURNS). This usually means an ` +
60
+ `agent loop is not converging — inspect the conversation before raising the cap.`,
61
+ },
62
+ });
63
+ }
64
+
65
+ if (maxToolTurns) {
66
+ const toolResults = _countToolResults(messages);
67
+ if (toolResults > maxToolTurns) {
68
+ logger.warn(
69
+ { toolResults, cap: maxToolTurns, path: req.path },
70
+ "[LoopGuard] conversation exceeded tool-turn cap"
71
+ );
72
+ return res.status(429).json({
73
+ error: {
74
+ type: "loop_cap_exceeded",
75
+ message:
76
+ `Conversation carries ${toolResults} tool results, over the configured ` +
77
+ `cap of ${maxToolTurns} (LYNKR_MAX_TOOL_TURNS). This usually means a ` +
78
+ `tool loop is stuck — inspect the last few tool calls before raising the cap.`,
79
+ },
80
+ });
81
+ }
82
+ }
83
+
84
+ return next();
85
+ }
86
+
87
+ module.exports = { loopGuard, _countToolResults };
@@ -1,14 +1,12 @@
1
1
  const crypto = require("crypto");
2
- const logger = require("../../logger");
3
2
 
4
3
  /**
5
- * Structured request logging middleware
4
+ * Request ID middleware.
6
5
  *
7
- * Features:
8
- * - Request ID tracking (X-Request-ID header)
9
- * - Correlation across services
10
- * - Performance metrics
11
- * - Minimal overhead
6
+ * Assigns req.requestId (honouring an inbound X-Request-ID header) and
7
+ * echoes it on the response. Request/response logging itself is handled
8
+ * by pino-http in middleware/logging.js — don't log here or every
9
+ * request gets logged twice.
12
10
  */
13
11
 
14
12
  function generateRequestId() {
@@ -16,66 +14,9 @@ function generateRequestId() {
16
14
  }
17
15
 
18
16
  function requestLoggingMiddleware(req, res, next) {
19
- const startTime = Date.now();
20
-
21
- // Generate or use existing request ID
22
17
  const requestId = req.headers["x-request-id"] || generateRequestId();
23
18
  req.requestId = requestId;
24
-
25
- // Add to response headers
26
19
  res.setHeader("X-Request-ID", requestId);
27
-
28
- // Log request start
29
- logger.info(
30
- {
31
- requestId,
32
- method: req.method,
33
- path: req.path || req.url,
34
- query: req.query,
35
- ip: req.ip || req.socket.remoteAddress,
36
- userAgent: req.headers["user-agent"],
37
- },
38
- "Request started"
39
- );
40
-
41
- // Capture response finish
42
- const originalSend = res.send;
43
- res.send = function (body) {
44
- const duration = Date.now() - startTime;
45
-
46
- // Log request completion
47
- logger.info(
48
- {
49
- requestId,
50
- method: req.method,
51
- path: req.path || req.url,
52
- status: res.statusCode,
53
- duration,
54
- contentLength: res.getHeader("content-length"),
55
- },
56
- "Request completed"
57
- );
58
-
59
- return originalSend.call(this, body);
60
- };
61
-
62
- // Handle errors
63
- res.on("finish", () => {
64
- if (res.statusCode >= 400) {
65
- const duration = Date.now() - startTime;
66
- logger.warn(
67
- {
68
- requestId,
69
- method: req.method,
70
- path: req.path || req.url,
71
- status: res.statusCode,
72
- duration,
73
- },
74
- "Request failed"
75
- );
76
- }
77
- });
78
-
79
20
  next();
80
21
  }
81
22
 
Binary file