lynkr 8.0.1 → 9.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "8.0.1",
3
+ "version": "9.0.1",
4
4
  "description": "Self-hosted Claude Code & Cursor proxy with Databricks,AWS BedRock,Azure adapters, openrouter, Ollama,llamacpp,LM Studio, workspace tooling, and MCP integration.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,13 +8,13 @@
8
8
  "lynkr-setup": "./scripts/setup.js"
9
9
  },
10
10
  "scripts": {
11
- "prestart": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom up -d headroom 2>/dev/null || echo 'Headroom skipped (disabled or Docker not running)'",
11
+ "prestart": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom up -d --build headroom 2>/dev/null || echo 'Headroom skipped (disabled or Docker not running)'",
12
12
  "start": "node index.js 2>&1 | npx pino-pretty --sync",
13
13
  "stop": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom down || echo 'Headroom skipped (disabled or Docker not running)'",
14
14
  "dev": "nodemon index.js",
15
15
  "lint": "eslint src index.js",
16
16
  "test": "npm run test:unit && npm run test:performance",
17
- "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
17
+ "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js",
18
18
  "test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
19
19
  "test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
20
20
  "test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
@@ -48,6 +48,7 @@
48
48
  "@babel/parser": "^7.29.0",
49
49
  "@babel/traverse": "^7.29.0",
50
50
  "@toon-format/toon": "^2.1.0",
51
+ "cockatiel": "^3.2.1",
51
52
  "compression": "^1.7.4",
52
53
  "diff": "^5.2.0",
53
54
  "dotenv": "^16.4.5",
@@ -27,6 +27,23 @@ const { IDE_SAFE_TOOLS } = require("../clients/standard-tools");
27
27
 
28
28
  const router = express.Router();
29
29
 
30
+ /**
31
+ * Resolve the model name for OpenAI responses.
32
+ * In OpenClaw mode, returns the actual provider/model from routing metadata.
33
+ */
34
+ function resolveResponseModel(resultBody, requestModel) {
35
+ if (config.openclaw?.enabled && resultBody?._routingMeta) {
36
+ const meta = resultBody._routingMeta;
37
+ if (meta.provider && meta.model) {
38
+ return `${meta.provider}/${meta.model}`;
39
+ }
40
+ if (meta.provider) {
41
+ return meta.provider;
42
+ }
43
+ }
44
+ return requestModel;
45
+ }
46
+
30
47
  /**
31
48
  * Client detection - identifies which AI coding tool is making the request
32
49
  * @param {Object} headers - Request headers
@@ -499,7 +516,8 @@ router.post("/chat/completions", async (req, res) => {
499
516
  }
500
517
 
501
518
  // Convert to OpenAI format
502
- const openaiResponse = convertAnthropicToOpenAI(result.body, req.body.model);
519
+ const streamModel = resolveResponseModel(result.body, req.body.model);
520
+ const openaiResponse = convertAnthropicToOpenAI(result.body, streamModel);
503
521
 
504
522
  // Debug: Log what we're about to stream
505
523
  logger.debug({
@@ -539,7 +557,7 @@ router.post("/chat/completions", async (req, res) => {
539
557
  id: openaiResponse.id,
540
558
  object: "chat.completion.chunk",
541
559
  created: openaiResponse.created,
542
- model: req.body.model,
560
+ model: streamModel,
543
561
  system_fingerprint: "fp_lynkr",
544
562
  choices: [{
545
563
  index: 0,
@@ -561,7 +579,7 @@ router.post("/chat/completions", async (req, res) => {
561
579
  id: openaiResponse.id,
562
580
  object: "chat.completion.chunk",
563
581
  created: openaiResponse.created,
564
- model: req.body.model,
582
+ model: streamModel,
565
583
  system_fingerprint: "fp_lynkr",
566
584
  choices: [{
567
585
  index: 0,
@@ -581,7 +599,7 @@ router.post("/chat/completions", async (req, res) => {
581
599
  id: openaiResponse.id,
582
600
  object: "chat.completion.chunk",
583
601
  created: openaiResponse.created,
584
- model: req.body.model,
602
+ model: streamModel,
585
603
  choices: [{
586
604
  index: 0,
587
605
  delta: {
@@ -608,7 +626,7 @@ router.post("/chat/completions", async (req, res) => {
608
626
  id: openaiResponse.id,
609
627
  object: "chat.completion.chunk",
610
628
  created: openaiResponse.created,
611
- model: req.body.model,
629
+ model: streamModel,
612
630
  system_fingerprint: "fp_lynkr",
613
631
  choices: [{
614
632
  index: 0,
@@ -679,7 +697,7 @@ router.post("/chat/completions", async (req, res) => {
679
697
  }, "Orchestrator result structure");
680
698
 
681
699
  // Convert Anthropic response to OpenAI format
682
- const openaiResponse = convertAnthropicToOpenAI(result.body, req.body.model);
700
+ const openaiResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
683
701
 
684
702
  // Map tool names for known IDE clients
685
703
  if (clientType !== "unknown" && openaiResponse.choices?.[0]?.message?.tool_calls?.length > 0) {
@@ -1535,7 +1553,8 @@ router.post("/responses", async (req, res) => {
1535
1553
  }, "=== ORCHESTRATOR RESULT FOR RESPONSES API ===");
1536
1554
 
1537
1555
  // Convert back: Anthropic → OpenAI → Responses
1538
- const chatResponse = convertAnthropicToOpenAI(result.body, req.body.model);
1556
+ const responsesModel = resolveResponseModel(result.body, req.body.model);
1557
+ const chatResponse = convertAnthropicToOpenAI(result.body, responsesModel);
1539
1558
 
1540
1559
  logger.debug({
1541
1560
  chatContent: chatResponse.choices?.[0]?.message?.content?.substring(0, 200),
@@ -1597,7 +1616,7 @@ router.post("/responses", async (req, res) => {
1597
1616
  object: "response",
1598
1617
  status: "in_progress",
1599
1618
  created_at: createdAt,
1600
- model: req.body.model,
1619
+ model: responsesModel,
1601
1620
  output: [],
1602
1621
  usage: null
1603
1622
  },
@@ -1614,7 +1633,7 @@ router.post("/responses", async (req, res) => {
1614
1633
  object: "response",
1615
1634
  status: "in_progress",
1616
1635
  created_at: createdAt,
1617
- model: req.body.model,
1636
+ model: responsesModel,
1618
1637
  output: [],
1619
1638
  usage: null
1620
1639
  },
@@ -1793,7 +1812,7 @@ router.post("/responses", async (req, res) => {
1793
1812
  object: "response",
1794
1813
  status: "completed",
1795
1814
  created_at: createdAt,
1796
- model: req.body.model,
1815
+ model: responsesModel,
1797
1816
  output: outputItems,
1798
1817
  usage: {
1799
1818
  input_tokens: responsesResponse.usage?.prompt_tokens || 0,
@@ -1846,7 +1865,7 @@ router.post("/responses", async (req, res) => {
1846
1865
  });
1847
1866
 
1848
1867
  // Convert back: Anthropic → OpenAI → Responses
1849
- const chatResponse = convertAnthropicToOpenAI(result.body, req.body.model);
1868
+ const chatResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
1850
1869
  const responsesResponse = convertChatToResponses(chatResponse);
1851
1870
 
1852
1871
  logger.info({
@@ -415,8 +415,8 @@ router.get("/health/providers", (req, res) => {
415
415
  // Get circuit breaker states
416
416
  const circuitBreakerStates = {};
417
417
  const allBreakers = registry.getAll();
418
- for (const [name, breaker] of Object.entries(allBreakers)) {
419
- circuitBreakerStates[name] = breaker.state;
418
+ for (const breaker of allBreakers) {
419
+ circuitBreakerStates[breaker.name] = breaker.state;
420
420
  }
421
421
 
422
422
  // Get all provider health
@@ -452,7 +452,8 @@ router.get("/health/providers/:name", (req, res) => {
452
452
 
453
453
  // Get circuit breaker state for this provider
454
454
  const allBreakers = registry.getAll();
455
- const circuitState = allBreakers[providerName]?.state || "CLOSED";
455
+ const breakerState = allBreakers.find((b) => b.name === providerName);
456
+ const circuitState = breakerState?.state || "CLOSED";
456
457
 
457
458
  // Get detailed metrics
458
459
  const metrics = healthTracker.getProviderMetrics(providerName);
@@ -477,4 +478,171 @@ router.get("/health/providers/:name", (req, res) => {
477
478
  }
478
479
  });
479
480
 
481
+ // ============================================================================
482
+ // Routing Telemetry Endpoints
483
+ // ============================================================================
484
+
485
+ const telemetry = require("../routing/telemetry");
486
+ const { getLatencyTracker } = require("../routing/latency-tracker");
487
+
488
+ /**
489
+ * GET /v1/routing/stats
490
+ *
491
+ * Aggregated routing telemetry statistics.
492
+ */
493
+ router.get("/routing/stats", (req, res) => {
494
+ try {
495
+ const since = req.query.since ? Number(req.query.since) : undefined;
496
+ const until = req.query.until ? Number(req.query.until) : undefined;
497
+ const stats = telemetry.getStats({ since, until });
498
+
499
+ if (!stats) {
500
+ return res.json({ object: "routing_stats", data: null, message: "No telemetry data available" });
501
+ }
502
+
503
+ // Merge latency tracker percentiles
504
+ const latencyTracker = getLatencyTracker();
505
+ const latencyStats = {};
506
+ for (const [provider, pStats] of latencyTracker.getAllStats()) {
507
+ latencyStats[provider] = pStats;
508
+ }
509
+
510
+ res.json({
511
+ object: "routing_stats",
512
+ data: { ...stats, latencyPercentiles: latencyStats },
513
+ timestamp: new Date().toISOString(),
514
+ });
515
+ } catch (error) {
516
+ logger.error({ error: error.message }, "Error getting routing stats");
517
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
518
+ }
519
+ });
520
+
521
+ /**
522
+ * GET /v1/routing/stats/:provider
523
+ *
524
+ * Per-provider routing telemetry.
525
+ */
526
+ router.get("/routing/stats/:provider", (req, res) => {
527
+ try {
528
+ const provider = req.params.provider.toLowerCase();
529
+ const since = req.query.since ? Number(req.query.since) : undefined;
530
+ const stats = telemetry.getProviderStats(provider, { since });
531
+
532
+ if (!stats) {
533
+ return res.json({ object: "provider_routing_stats", data: null, message: `No data for ${provider}` });
534
+ }
535
+
536
+ const latencyTracker = getLatencyTracker();
537
+ const latency = latencyTracker.getStats(provider);
538
+
539
+ res.json({
540
+ object: "provider_routing_stats",
541
+ provider,
542
+ data: { ...stats, latency },
543
+ timestamp: new Date().toISOString(),
544
+ });
545
+ } catch (error) {
546
+ logger.error({ error: error.message }, "Error getting provider routing stats");
547
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
548
+ }
549
+ });
550
+
551
+ /**
552
+ * GET /v1/routing/telemetry
553
+ *
554
+ * Raw telemetry records (most recent first).
555
+ */
556
+ router.get("/routing/telemetry", (req, res) => {
557
+ try {
558
+ const filters = {
559
+ provider: req.query.provider,
560
+ tier: req.query.tier,
561
+ since: req.query.since ? Number(req.query.since) : undefined,
562
+ limit: req.query.limit ? Number(req.query.limit) : 100,
563
+ };
564
+ const records = telemetry.query(filters);
565
+
566
+ res.json({ object: "telemetry_list", data: records, count: records.length });
567
+ } catch (error) {
568
+ logger.error({ error: error.message }, "Error querying telemetry");
569
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
570
+ }
571
+ });
572
+
573
+ /**
574
+ * GET /v1/routing/accuracy
575
+ *
576
+ * Routing accuracy analysis (over/under-provisioned percentages).
577
+ */
578
+ router.get("/routing/accuracy", (req, res) => {
579
+ try {
580
+ const since = req.query.since ? Number(req.query.since) : undefined;
581
+ const accuracy = telemetry.getRoutingAccuracy({ since });
582
+
583
+ res.json({
584
+ object: "routing_accuracy",
585
+ data: accuracy,
586
+ timestamp: new Date().toISOString(),
587
+ });
588
+ } catch (error) {
589
+ logger.error({ error: error.message }, "Error getting routing accuracy");
590
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
591
+ }
592
+ });
593
+
594
+ // ── Admin: Hot Reload Config + Reset Circuit Breakers ─────────────────
595
+
596
+ router.post("/admin/reload", (req, res) => {
597
+ try {
598
+ config.reloadConfig();
599
+ const registry = getCircuitBreakerRegistry();
600
+ const states = registry.getAll();
601
+ res.json({
602
+ object: "admin_reload",
603
+ status: "ok",
604
+ reloaded: [
605
+ "modelTiers",
606
+ "apiKeys",
607
+ "providerSettings",
608
+ "circuitBreakers",
609
+ ],
610
+ circuitBreakers: states.map(s => ({ name: s.name, state: s.state })),
611
+ timestamp: new Date().toISOString(),
612
+ });
613
+ } catch (error) {
614
+ logger.error({ error: error.message }, "Admin reload failed");
615
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
616
+ }
617
+ });
618
+
619
+ router.post("/admin/circuit-breakers/reset", (req, res) => {
620
+ try {
621
+ const provider = req.query.provider || req.body?.provider;
622
+ const registry = getCircuitBreakerRegistry();
623
+
624
+ if (provider) {
625
+ const breaker = registry.breakers?.get(provider);
626
+ if (breaker) {
627
+ breaker.reset();
628
+ res.json({ object: "circuit_breaker_reset", provider, status: "reset" });
629
+ } else {
630
+ res.status(404).json({ error: { message: `No circuit breaker for provider: ${provider}` } });
631
+ }
632
+ } else {
633
+ registry.resetAll();
634
+ const states = registry.getAll();
635
+ res.json({
636
+ object: "circuit_breaker_reset",
637
+ provider: "all",
638
+ status: "reset",
639
+ breakers: states.map(s => ({ name: s.name, state: s.state })),
640
+ });
641
+ }
642
+ } catch (error) {
643
+ logger.error({ error: error.message }, "Circuit breaker reset failed");
644
+ res.status(500).json({ error: { type: "server_error", message: error.message } });
645
+ }
646
+ });
647
+
480
648
  module.exports = router;
package/src/api/router.js CHANGED
@@ -136,7 +136,7 @@ router.post("/routing/analyze", async (req, res) => {
136
136
  const { getModelTierSelector } = require("../routing/model-tiers");
137
137
  const { getModelRegistry } = require("../routing/model-registry");
138
138
 
139
- const analysis = analyzeComplexity(req.body, { weighted: req.query.weighted === "true" });
139
+ const analysis = await analyzeComplexity(req.body, { weighted: req.query.weighted === "true" });
140
140
  const agentic = getAgenticDetector().detect(req.body);
141
141
  const selector = getModelTierSelector();
142
142
  const tier = selector.getTier(analysis.score);
@@ -210,13 +210,17 @@ router.post("/api/event_logging/batch", (req, res) => {
210
210
 
211
211
  router.post("/v1/messages", rateLimiter, async (req, res, next) => {
212
212
  try {
213
+ const { createTimer } = require("../utils/perf-timer");
214
+ const timer = createTimer("POST /v1/messages");
213
215
  metrics.recordRequest();
214
216
  // Support both query parameter (?stream=true) and body parameter ({"stream": true})
215
217
  const wantsStream = Boolean(req.query?.stream === 'true' || req.body?.stream);
216
218
  const hasTools = Array.isArray(req.body?.tools) && req.body.tools.length > 0;
219
+ timer.mark("parseRequest");
217
220
 
218
221
  // Analyze complexity for routing headers (Phase 3)
219
- const complexity = analyzeComplexity(req.body);
222
+ const complexity = await analyzeComplexity(req.body);
223
+ timer.mark("analyzeComplexity");
220
224
  let preRouteProvider = 'cloud';
221
225
  if (complexity.recommendation === 'local') {
222
226
  // Use tier config to determine actual provider instead of hardcoding 'ollama'
@@ -430,6 +434,7 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
430
434
  }
431
435
 
432
436
  // Non-streaming or tool-based requests (buffered path)
437
+ timer.mark("preProcessMessage");
433
438
  const result = await processMessage({
434
439
  payload: req.body,
435
440
  headers: req.headers,
@@ -440,6 +445,8 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
440
445
  maxDurationMs: req.body?.max_duration_ms,
441
446
  },
442
447
  });
448
+ timer.mark("processMessage");
449
+ timer.done();
443
450
 
444
451
  // Legacy streaming wrapper (for tool-based requests that requested streaming)
445
452
  if (wantsStream && hasTools) {
@@ -1,253 +1,16 @@
1
- const logger = require("../logger");
2
-
3
1
  /**
4
- * Circuit Breaker Pattern
5
- *
6
- * States:
7
- * - CLOSED: Normal operation, requests pass through
8
- * - OPEN: Failures exceeded threshold, requests fail fast
9
- * - HALF_OPEN: Testing if service recovered
2
+ * Circuit Breaker Pattern — backed by Cockatiel
10
3
  *
11
- * Performance:
12
- * - Fail fast instead of waiting for timeouts
13
- * - Automatic recovery testing
14
- * - Minimal overhead in CLOSED state
15
- */
16
-
17
- const STATE = {
18
- CLOSED: "CLOSED",
19
- OPEN: "OPEN",
20
- HALF_OPEN: "HALF_OPEN",
21
- };
22
-
23
- class CircuitBreaker {
24
- constructor(name, options = {}) {
25
- this.name = name;
26
-
27
- // Configuration
28
- this.failureThreshold = options.failureThreshold || 5; // failures before opening
29
- this.successThreshold = options.successThreshold || 2; // successes to close from half-open
30
- this.timeout = options.timeout || 60000; // time to wait before trying again (60s)
31
- this.resetTimeout = options.resetTimeout || 30000; // time in half-open before resetting (30s)
32
-
33
- // State
34
- this.state = STATE.CLOSED;
35
- this.failureCount = 0;
36
- this.successCount = 0;
37
- this.nextAttempt = Date.now();
38
- this.lastStateChange = Date.now();
39
-
40
- // Metrics
41
- this.stats = {
42
- totalRequests: 0,
43
- totalFailures: 0,
44
- totalSuccesses: 0,
45
- totalRejected: 0,
46
- };
47
- }
48
-
49
- /**
50
- * Execute function with circuit breaker protection
51
- */
52
- async execute(fn) {
53
- this.stats.totalRequests++;
54
-
55
- // Check circuit state
56
- if (this.state === STATE.OPEN) {
57
- if (Date.now() < this.nextAttempt) {
58
- this.stats.totalRejected++;
59
- throw new CircuitBreakerError(
60
- `Circuit breaker ${this.name} is OPEN`,
61
- this.nextAttempt - Date.now()
62
- );
63
- }
64
-
65
- // Try half-open
66
- this.transitionTo(STATE.HALF_OPEN);
67
- }
68
-
69
- try {
70
- const result = await fn();
71
-
72
- // Success
73
- this.onSuccess();
74
- return result;
75
- } catch (err) {
76
- // Failure
77
- this.onFailure();
78
- throw err;
79
- }
80
- }
81
-
82
- /**
83
- * Handle successful request
84
- */
85
- onSuccess() {
86
- this.stats.totalSuccesses++;
87
- this.failureCount = 0;
88
-
89
- if (this.state === STATE.HALF_OPEN) {
90
- this.successCount++;
91
-
92
- if (this.successCount >= this.successThreshold) {
93
- this.transitionTo(STATE.CLOSED);
94
- }
95
- }
96
- }
97
-
98
- /**
99
- * Handle failed request
100
- */
101
- onFailure() {
102
- this.stats.totalFailures++;
103
- this.failureCount++;
104
- this.successCount = 0;
105
-
106
- if (this.failureCount >= this.failureThreshold) {
107
- this.transitionTo(STATE.OPEN);
108
- }
109
- }
110
-
111
- /**
112
- * Transition to new state
113
- */
114
- transitionTo(newState) {
115
- const oldState = this.state;
116
-
117
- if (oldState === newState) {
118
- return;
119
- }
120
-
121
- this.state = newState;
122
- this.lastStateChange = Date.now();
123
-
124
- logger.info(
125
- {
126
- circuitBreaker: this.name,
127
- oldState,
128
- newState,
129
- failureCount: this.failureCount,
130
- successCount: this.successCount,
131
- },
132
- "Circuit breaker state change"
133
- );
134
-
135
- // Set next attempt time when opening
136
- if (newState === STATE.OPEN) {
137
- this.nextAttempt = Date.now() + this.timeout;
138
- logger.warn(
139
- {
140
- circuitBreaker: this.name,
141
- retryAfter: this.timeout,
142
- },
143
- "Circuit breaker opened - failing fast"
144
- );
145
- }
146
-
147
- // Reset counters
148
- if (newState === STATE.CLOSED) {
149
- this.failureCount = 0;
150
- this.successCount = 0;
151
- logger.info(
152
- {
153
- circuitBreaker: this.name,
154
- },
155
- "Circuit breaker closed - normal operation resumed"
156
- );
157
- }
158
-
159
- if (newState === STATE.HALF_OPEN) {
160
- this.successCount = 0;
161
- logger.info(
162
- {
163
- circuitBreaker: this.name,
164
- },
165
- "Circuit breaker half-open - testing service recovery"
166
- );
167
- }
168
- }
169
-
170
- /**
171
- * Get current state
172
- */
173
- getState() {
174
- return {
175
- name: this.name,
176
- state: this.state,
177
- failureCount: this.failureCount,
178
- successCount: this.successCount,
179
- nextAttempt: this.nextAttempt,
180
- lastStateChange: this.lastStateChange,
181
- stats: this.stats,
182
- };
183
- }
184
-
185
- /**
186
- * Manually reset circuit breaker
187
- */
188
- reset() {
189
- this.transitionTo(STATE.CLOSED);
190
- this.failureCount = 0;
191
- this.successCount = 0;
192
- }
193
- }
194
-
195
- /**
196
- * Circuit breaker error
4
+ * This module re-exports Cockatiel-backed adapters from resilience.js
5
+ * while preserving the same API surface for all consumers.
197
6
  */
198
- class CircuitBreakerError extends Error {
199
- constructor(message, retryAfter) {
200
- super(message);
201
- this.name = "CircuitBreakerError";
202
- this.retryAfter = retryAfter;
203
- this.code = "circuit_breaker_open";
204
- }
205
- }
206
-
207
- /**
208
- * Circuit breaker registry
209
- */
210
- class CircuitBreakerRegistry {
211
- constructor() {
212
- this.breakers = new Map();
213
- }
214
-
215
- /**
216
- * Get or create circuit breaker
217
- */
218
- get(name, options) {
219
- if (!this.breakers.has(name)) {
220
- this.breakers.set(name, new CircuitBreaker(name, options));
221
- }
222
- return this.breakers.get(name);
223
- }
224
-
225
- /**
226
- * Get all circuit breakers
227
- */
228
- getAll() {
229
- return Array.from(this.breakers.values()).map((breaker) => breaker.getState());
230
- }
231
-
232
- /**
233
- * Reset all circuit breakers
234
- */
235
- resetAll() {
236
- for (const breaker of this.breakers.values()) {
237
- breaker.reset();
238
- }
239
- }
240
- }
241
-
242
- // Singleton registry
243
- let registry = null;
244
-
245
- function getCircuitBreakerRegistry() {
246
- if (!registry) {
247
- registry = new CircuitBreakerRegistry();
248
- }
249
- return registry;
250
- }
7
+ const {
8
+ CockatielCircuitBreaker: CircuitBreaker,
9
+ CircuitBreakerError,
10
+ CockatielRegistry: CircuitBreakerRegistry,
11
+ getCockatielRegistry: getCircuitBreakerRegistry,
12
+ STATE,
13
+ } = require("./resilience");
251
14
 
252
15
  module.exports = {
253
16
  CircuitBreaker,