create-byan-agent 2.9.4 → 2.9.5
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/install/bin/byan-cleanup.js +156 -0
- package/install/bin/byan-kanban.js +159 -0
- package/install/bin/byan-ledger.js +45 -0
- package/install/lib/cleanup/detector.js +154 -0
- package/install/lib/cleanup/executor.js +72 -0
- package/install/lib/subagent-generator.js +208 -0
- package/install/lib/token-ledger.js +131 -0
- package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
- package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
- package/install/templates/.claude/agents/bmad-byan.md +152 -0
- package/install/templates/.claude/agents/bmad-carmack.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
- package/install/templates/.claude/agents/bmad-claude.md +26 -0
- package/install/templates/.claude/agents/bmad-codex.md +26 -0
- package/install/templates/.claude/agents/bmad-compliance.md +68 -0
- package/install/templates/.claude/agents/bmad-drawio.md +25 -0
- package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
- package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
- package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
- package/install/templates/.claude/agents/bmad-hermes.md +59 -0
- package/install/templates/.claude/agents/bmad-marc.md +25 -0
- package/install/templates/.claude/agents/bmad-patnote.md +26 -0
- package/install/templates/.claude/agents/bmad-rachid.md +25 -0
- package/install/templates/.claude/agents/bmad-tao.md +14 -0
- package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
- package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
- package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
- package/install/templates/.claude/hooks/fd-response-check.js +92 -0
- package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
- package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
- package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
- package/install/templates/.claude/hooks/tool-transparency.js +4 -0
- package/install/templates/.claude/settings.json +23 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
- package/install/templates/.githooks/pre-commit +75 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
- package/install/templates/detector.js +154 -0
- package/package.json +6 -7
- package/src/loadbalancer/capability-matrix.js +157 -0
- package/src/loadbalancer/config.js +141 -0
- package/src/loadbalancer/graceful-degradation.js +212 -0
- package/src/loadbalancer/health-probe.js +151 -0
- package/src/loadbalancer/hooks/claude-hooks.js +53 -0
- package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
- package/src/loadbalancer/index.js +81 -0
- package/src/loadbalancer/loadbalancer.default.yaml +65 -0
- package/src/loadbalancer/loadbalancer.js +324 -0
- package/src/loadbalancer/mcp-server.js +304 -0
- package/src/loadbalancer/metrics.js +146 -0
- package/src/loadbalancer/native/claude-integration.js +64 -0
- package/src/loadbalancer/native/copilot-integration.js +59 -0
- package/src/loadbalancer/pressure-score.js +102 -0
- package/src/loadbalancer/providers/base-provider.js +80 -0
- package/src/loadbalancer/providers/byan-api-provider.js +132 -0
- package/src/loadbalancer/providers/claude-provider.js +113 -0
- package/src/loadbalancer/providers/copilot-provider.js +104 -0
- package/src/loadbalancer/rate-limit-tracker.js +216 -0
- package/src/loadbalancer/session-bridge.js +179 -0
- package/src/loadbalancer/state/db.js +211 -0
- package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
- package/src/loadbalancer/tools/index.js +123 -0
- package/src/loadbalancer/velocity-estimator.js +147 -0
- package/update-byan-agent/bin/update-byan-agent.js +27 -2
- package/API-BYAN-V2.md +0 -741
- package/BMAD-QUICK-REFERENCE.md +0 -370
- package/CHANGELOG-v2.1.0.md +0 -371
- package/MIGRATION-v2.0-to-v2.1.md +0 -430
- package/README-BYAN-V2.md +0 -446
- package/TEST-GUIDE-v2.3.2.md +0 -161
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copilot Hooks — Rate Limit Detection for GitHub Copilot SDK
|
|
3
|
+
*
|
|
4
|
+
* Intercepts Copilot SDK session events and HTTP responses
|
|
5
|
+
* to detect 429 errors and X-RateLimit headers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attach rate limit detection to a Copilot SDK session.
|
|
10
|
+
* @param {object} session - Copilot SDK session object
|
|
11
|
+
* @param {import('../rate-limit-tracker').RateLimitTracker} tracker
|
|
12
|
+
*/
|
|
13
|
+
function attachCopilotHooks(session, tracker) {
|
|
14
|
+
if (!session || !session.on) return;
|
|
15
|
+
|
|
16
|
+
session.on('error', (err) => {
|
|
17
|
+
const is429 = err.status === 429 || err.statusCode === 429 ||
|
|
18
|
+
(err.message && err.message.includes('rate limit'));
|
|
19
|
+
|
|
20
|
+
if (is429) {
|
|
21
|
+
const headers = err.headers || err.response?.headers || {};
|
|
22
|
+
tracker.record429({
|
|
23
|
+
source: 'copilot-event',
|
|
24
|
+
headers: {
|
|
25
|
+
'x-ratelimit-remaining': headers['x-ratelimit-remaining'],
|
|
26
|
+
'x-ratelimit-reset': headers['x-ratelimit-reset'],
|
|
27
|
+
'retry-after': headers['retry-after'],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
session.on('response', (res) => {
|
|
34
|
+
const headers = res.headers || {};
|
|
35
|
+
if (headers['x-ratelimit-remaining'] || headers['retry-after']) {
|
|
36
|
+
tracker.recordHeaders(headers);
|
|
37
|
+
}
|
|
38
|
+
if (!res.rateLimited) {
|
|
39
|
+
tracker.recordSuccess();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap a Copilot SDK sendAndWait call with rate limit detection.
|
|
46
|
+
* @param {Function} sendFn - Original sendAndWait function
|
|
47
|
+
* @param {import('../rate-limit-tracker').RateLimitTracker} tracker
|
|
48
|
+
* @returns {Function} Wrapped function
|
|
49
|
+
*/
|
|
50
|
+
function wrapCopilotSend(sendFn, tracker) {
|
|
51
|
+
return async function wrappedSend(...args) {
|
|
52
|
+
try {
|
|
53
|
+
const result = await sendFn.apply(this, args);
|
|
54
|
+
tracker.recordSuccess();
|
|
55
|
+
return result;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const is429 = err.status === 429 || err.statusCode === 429 ||
|
|
58
|
+
(err.message && err.message.includes('rate limit'));
|
|
59
|
+
|
|
60
|
+
if (is429) {
|
|
61
|
+
const headers = err.headers || err.response?.headers || {};
|
|
62
|
+
tracker.record429({
|
|
63
|
+
source: 'copilot-send',
|
|
64
|
+
headers,
|
|
65
|
+
});
|
|
66
|
+
tracker.recordHeaders(headers);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { attachCopilotHooks, wrapCopilotSend };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYAN LoadBalancer — Module Index
|
|
3
|
+
*
|
|
4
|
+
* Public API for the loadbalancer module.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { LoadBalancer, loadConfig } = require('./src/loadbalancer');
|
|
8
|
+
* const config = loadConfig(projectRoot);
|
|
9
|
+
* const lb = new LoadBalancer({ config, providers: {...} });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { LoadBalancer } = require('./loadbalancer');
|
|
13
|
+
const { loadConfig, getEnabledProviders, getProviderOrder } = require('./config');
|
|
14
|
+
const { RateLimitTracker, STATES } = require('./rate-limit-tracker');
|
|
15
|
+
const { SessionBridge } = require('./session-bridge');
|
|
16
|
+
const { SharedStateStore } = require('./state/db');
|
|
17
|
+
const { BaseProvider } = require('./providers/base-provider');
|
|
18
|
+
const { CopilotProvider } = require('./providers/copilot-provider');
|
|
19
|
+
const { ClaudeProvider } = require('./providers/claude-provider');
|
|
20
|
+
const { CopilotIntegration } = require('./native/copilot-integration');
|
|
21
|
+
const { ClaudeIntegration } = require('./native/claude-integration');
|
|
22
|
+
const { ByanApiProvider } = require('./providers/byan-api-provider');
|
|
23
|
+
const { HealthProbe } = require('./health-probe');
|
|
24
|
+
const { GracefulDegradation, PRIORITY } = require('./graceful-degradation');
|
|
25
|
+
const { CapabilityMatrix, DEFAULT_CAPABILITIES } = require('./capability-matrix');
|
|
26
|
+
const { Metrics } = require('./metrics');
|
|
27
|
+
const { VelocityEstimator, TREND } = require('./velocity-estimator');
|
|
28
|
+
const { calculatePressure, formatPressureSummary, STATE_SCORES, DEFAULT_WEIGHTS } = require('./pressure-score');
|
|
29
|
+
|
|
30
|
+
// MCP server lazy-loaded — requires @modelcontextprotocol/sdk (optional dep)
|
|
31
|
+
let _mcp = null;
|
|
32
|
+
function getMcp() {
|
|
33
|
+
if (!_mcp) _mcp = require('./mcp-server');
|
|
34
|
+
return _mcp;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
// Core
|
|
39
|
+
LoadBalancer,
|
|
40
|
+
loadConfig,
|
|
41
|
+
getEnabledProviders,
|
|
42
|
+
getProviderOrder,
|
|
43
|
+
|
|
44
|
+
// Rate limits
|
|
45
|
+
RateLimitTracker,
|
|
46
|
+
STATES,
|
|
47
|
+
|
|
48
|
+
// Session management
|
|
49
|
+
SessionBridge,
|
|
50
|
+
SharedStateStore,
|
|
51
|
+
|
|
52
|
+
// Providers
|
|
53
|
+
BaseProvider,
|
|
54
|
+
CopilotProvider,
|
|
55
|
+
ClaudeProvider,
|
|
56
|
+
ByanApiProvider,
|
|
57
|
+
|
|
58
|
+
// Native integrations
|
|
59
|
+
CopilotIntegration,
|
|
60
|
+
ClaudeIntegration,
|
|
61
|
+
|
|
62
|
+
// Robustness (Sprint 3)
|
|
63
|
+
HealthProbe,
|
|
64
|
+
GracefulDegradation,
|
|
65
|
+
PRIORITY,
|
|
66
|
+
CapabilityMatrix,
|
|
67
|
+
DEFAULT_CAPABILITIES,
|
|
68
|
+
Metrics,
|
|
69
|
+
|
|
70
|
+
// Quota Monitoring (lb-quota-realtime)
|
|
71
|
+
VelocityEstimator,
|
|
72
|
+
TREND,
|
|
73
|
+
calculatePressure,
|
|
74
|
+
formatPressureSummary,
|
|
75
|
+
STATE_SCORES,
|
|
76
|
+
DEFAULT_WEIGHTS,
|
|
77
|
+
|
|
78
|
+
// MCP Server (lazy — requires @modelcontextprotocol/sdk)
|
|
79
|
+
get startServer() { return getMcp().startServer; },
|
|
80
|
+
get VERSION() { return getMcp().VERSION; },
|
|
81
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# BYAN LoadBalancer — Default Configuration
|
|
2
|
+
# Copilot CLI <-> Claude Code automatic failover
|
|
3
|
+
|
|
4
|
+
providers:
|
|
5
|
+
copilot:
|
|
6
|
+
enabled: true
|
|
7
|
+
sdk: "@github/copilot-sdk"
|
|
8
|
+
transport: "json-rpc"
|
|
9
|
+
auth_env: "COPILOT_GITHUB_TOKEN"
|
|
10
|
+
models:
|
|
11
|
+
worker: "gpt-4.1-mini"
|
|
12
|
+
agent: "gpt-4.1"
|
|
13
|
+
|
|
14
|
+
claude:
|
|
15
|
+
enabled: true
|
|
16
|
+
sdk: "@anthropic-ai/claude-agent-sdk"
|
|
17
|
+
transport: "native"
|
|
18
|
+
auth_env: "ANTHROPIC_API_KEY"
|
|
19
|
+
models:
|
|
20
|
+
worker: "claude-haiku-4.5"
|
|
21
|
+
agent: "claude-sonnet-4.5"
|
|
22
|
+
|
|
23
|
+
byan_api:
|
|
24
|
+
enabled: false
|
|
25
|
+
url: "http://localhost:3737"
|
|
26
|
+
auth_env: "BYAN_API_TOKEN"
|
|
27
|
+
|
|
28
|
+
primary: "claude"
|
|
29
|
+
fallback_order: ["copilot", "byan_api"]
|
|
30
|
+
|
|
31
|
+
rate_limits:
|
|
32
|
+
window_ms: 60000
|
|
33
|
+
max_429_in_window: 3
|
|
34
|
+
throttle_threshold: 2
|
|
35
|
+
block_threshold: 3
|
|
36
|
+
recovery_probe_interval_ms: 10000
|
|
37
|
+
half_open_max_requests: 2
|
|
38
|
+
|
|
39
|
+
sessions:
|
|
40
|
+
sticky_timeout_ms: 300000
|
|
41
|
+
context_summary_max_tokens: 2000
|
|
42
|
+
auto_switch_on_rate_limit: true
|
|
43
|
+
|
|
44
|
+
quota:
|
|
45
|
+
velocity_window_ms: 120000
|
|
46
|
+
warning_threshold_per_min: 10
|
|
47
|
+
max_requests_before_limit: 30
|
|
48
|
+
preemptive_threshold: 75
|
|
49
|
+
preemptive_enabled: true
|
|
50
|
+
|
|
51
|
+
health_probe:
|
|
52
|
+
enabled: true
|
|
53
|
+
interval_ms: 30000
|
|
54
|
+
timeout_ms: 5000
|
|
55
|
+
cheap_model: true
|
|
56
|
+
|
|
57
|
+
mcp_server:
|
|
58
|
+
transport: "http"
|
|
59
|
+
port: 3838
|
|
60
|
+
host: "127.0.0.1"
|
|
61
|
+
|
|
62
|
+
logging:
|
|
63
|
+
level: "info"
|
|
64
|
+
switchover_events: true
|
|
65
|
+
rate_limit_events: true
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadBalancer Core — Failover Routing Engine
|
|
3
|
+
*
|
|
4
|
+
* Routes requests to the healthiest provider. Handles:
|
|
5
|
+
* - Sticky sessions (prefer same provider for ongoing conversation)
|
|
6
|
+
* - Auto-failover on rate limit (THROTTLED/BLOCKED triggers switch)
|
|
7
|
+
* - Manual provider switch with context transfer
|
|
8
|
+
* - Integration with RateLimitTracker + SharedStateStore + SessionBridge
|
|
9
|
+
*
|
|
10
|
+
* Sits ABOVE existing BYAN routers:
|
|
11
|
+
* LoadBalancer (picks PLATFORM) → ExecutionRouter (picks STRATEGY) → Dispatcher (picks MODEL)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { EventEmitter } = require('events');
|
|
15
|
+
const { RateLimitTracker, STATES } = require('./rate-limit-tracker');
|
|
16
|
+
const { getProviderOrder } = require('./config');
|
|
17
|
+
|
|
18
|
+
class LoadBalancer extends EventEmitter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {object} opts.config - Loaded loadbalancer config
|
|
22
|
+
* @param {object} opts.providers - { name: ProviderAdapter } map
|
|
23
|
+
* @param {import('./state/db').SharedStateStore} [opts.store]
|
|
24
|
+
* @param {import('./session-bridge').SessionBridge} [opts.bridge]
|
|
25
|
+
*/
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
super();
|
|
28
|
+
this.config = opts.config;
|
|
29
|
+
this.providers = opts.providers || {};
|
|
30
|
+
this.store = opts.store || null;
|
|
31
|
+
this.bridge = opts.bridge || null;
|
|
32
|
+
|
|
33
|
+
this.trackers = {};
|
|
34
|
+
this.activeProvider = this.config.primary;
|
|
35
|
+
this.sessions = new Map();
|
|
36
|
+
|
|
37
|
+
this._initTrackers();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_initTrackers() {
|
|
41
|
+
const rlConfig = this.config.rate_limits || {};
|
|
42
|
+
|
|
43
|
+
for (const name of Object.keys(this.providers)) {
|
|
44
|
+
const tracker = new RateLimitTracker(name, rlConfig);
|
|
45
|
+
|
|
46
|
+
tracker.on('state_change', (event) => {
|
|
47
|
+
this.emit('rate_limit_change', event);
|
|
48
|
+
|
|
49
|
+
if (this.store) {
|
|
50
|
+
this.store.logRateLimit({
|
|
51
|
+
provider: event.provider,
|
|
52
|
+
eventType: '429',
|
|
53
|
+
stateBefore: event.from,
|
|
54
|
+
stateAfter: event.to,
|
|
55
|
+
meta: { reason: event.reason },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
this.config.sessions?.auto_switch_on_rate_limit &&
|
|
61
|
+
event.provider === this.activeProvider &&
|
|
62
|
+
(event.to === STATES.BLOCKED || event.to === STATES.THROTTLED)
|
|
63
|
+
) {
|
|
64
|
+
this._autoFailover(event);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.trackers[name] = tracker;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get overall loadbalancer status.
|
|
74
|
+
*/
|
|
75
|
+
getStatus() {
|
|
76
|
+
const providerStates = {};
|
|
77
|
+
for (const [name, tracker] of Object.entries(this.trackers)) {
|
|
78
|
+
providerStates[name] = tracker.getState();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
activeProvider: this.activeProvider,
|
|
83
|
+
primary: this.config.primary,
|
|
84
|
+
fallbackOrder: this.config.fallback_order,
|
|
85
|
+
providers: providerStates,
|
|
86
|
+
activeSessions: this.sessions.size,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get detailed rate limit info for all providers.
|
|
92
|
+
*/
|
|
93
|
+
getRateLimitDetails() {
|
|
94
|
+
const details = {};
|
|
95
|
+
for (const [name, tracker] of Object.entries(this.trackers)) {
|
|
96
|
+
details[name] = tracker.getState();
|
|
97
|
+
}
|
|
98
|
+
return details;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get switchover history from store.
|
|
103
|
+
*/
|
|
104
|
+
getSwitchoverHistory(limit = 20) {
|
|
105
|
+
if (!this.store) return { events: [], total: 0 };
|
|
106
|
+
const events = this.store.getSwitchoverHistory(limit);
|
|
107
|
+
return { events, total: events.length };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Route a request to the best available provider.
|
|
112
|
+
* @param {object} opts
|
|
113
|
+
* @param {string} opts.prompt
|
|
114
|
+
* @param {string} [opts.sessionId]
|
|
115
|
+
* @param {string} [opts.preferProvider] - 'auto', 'claude', 'copilot'
|
|
116
|
+
* @returns {Promise<object>} Provider response
|
|
117
|
+
*/
|
|
118
|
+
async send(opts) {
|
|
119
|
+
const provider = this._selectProvider(opts);
|
|
120
|
+
const tracker = this.trackers[provider.name];
|
|
121
|
+
|
|
122
|
+
if (!tracker?.canAcceptRequest()) {
|
|
123
|
+
const fallback = this._findHealthyFallback(provider.name);
|
|
124
|
+
if (!fallback) {
|
|
125
|
+
return {
|
|
126
|
+
provider: null,
|
|
127
|
+
content: null,
|
|
128
|
+
error: 'All providers rate-limited. Please wait.',
|
|
129
|
+
rateLimited: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return this._sendToProvider(fallback, opts, tracker);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this._sendToProvider(provider, opts, tracker);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _sendToProvider(provider, opts) {
|
|
139
|
+
const tracker = this.trackers[provider.name];
|
|
140
|
+
const start = Date.now();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await provider.send(opts);
|
|
144
|
+
const latencyMs = Date.now() - start;
|
|
145
|
+
|
|
146
|
+
if (result.rateLimited) {
|
|
147
|
+
tracker.record429(result.rateLimitHeaders || {});
|
|
148
|
+
|
|
149
|
+
if (result.rateLimitHeaders) {
|
|
150
|
+
tracker.recordHeaders(result.rateLimitHeaders);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fallback = this._findHealthyFallback(provider.name);
|
|
154
|
+
if (fallback) {
|
|
155
|
+
this.emit('failover', {
|
|
156
|
+
from: provider.name,
|
|
157
|
+
to: fallback.name,
|
|
158
|
+
reason: 'rate_limited',
|
|
159
|
+
});
|
|
160
|
+
return this._sendToProvider(fallback, opts);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
tracker.recordSuccess();
|
|
167
|
+
|
|
168
|
+
if (this.store) {
|
|
169
|
+
this.store.recordRequest(provider.name, latencyMs);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (opts.sessionId) {
|
|
173
|
+
this.sessions.set(opts.sessionId, {
|
|
174
|
+
provider: provider.name,
|
|
175
|
+
lastUsed: Date.now(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
const is429 = err.status === 429 || err.statusCode === 429;
|
|
182
|
+
if (is429) {
|
|
183
|
+
tracker.record429({ source: 'send_error' });
|
|
184
|
+
const fallback = this._findHealthyFallback(provider.name);
|
|
185
|
+
if (fallback) {
|
|
186
|
+
return this._sendToProvider(fallback, opts);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Force switch to a specific provider.
|
|
195
|
+
*/
|
|
196
|
+
async switchProvider(opts) {
|
|
197
|
+
const prev = this.activeProvider;
|
|
198
|
+
const target = opts.target;
|
|
199
|
+
|
|
200
|
+
let injectionPrompt = null;
|
|
201
|
+
let contextTransferred = false;
|
|
202
|
+
|
|
203
|
+
if (this.bridge && opts.sessionId) {
|
|
204
|
+
const sourceProvider = this.providers[prev];
|
|
205
|
+
if (sourceProvider) {
|
|
206
|
+
const transfer = await this.bridge.transfer(
|
|
207
|
+
sourceProvider, target, opts.sessionId, opts.reason || 'manual_switch'
|
|
208
|
+
);
|
|
209
|
+
injectionPrompt = transfer.injectionPrompt;
|
|
210
|
+
contextTransferred = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.activeProvider = target;
|
|
215
|
+
|
|
216
|
+
this.emit('switch', {
|
|
217
|
+
from: prev,
|
|
218
|
+
to: target,
|
|
219
|
+
reason: opts.reason || 'manual_switch',
|
|
220
|
+
contextTransferred,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
switched: true,
|
|
225
|
+
from: prev,
|
|
226
|
+
to: target,
|
|
227
|
+
reason: opts.reason,
|
|
228
|
+
contextTransferred,
|
|
229
|
+
injectionPrompt,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get session context in portable format.
|
|
235
|
+
*/
|
|
236
|
+
async getSessionContext(sessionId) {
|
|
237
|
+
if (!this.bridge || !sessionId) {
|
|
238
|
+
return {
|
|
239
|
+
sessionId: sessionId || 'none',
|
|
240
|
+
provider: this.activeProvider,
|
|
241
|
+
context: null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const provider = this.providers[this.activeProvider];
|
|
246
|
+
const context = await this.bridge.extract(provider, sessionId);
|
|
247
|
+
return { sessionId, provider: this.activeProvider, context };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_selectProvider(opts) {
|
|
251
|
+
if (opts.preferProvider && opts.preferProvider !== 'auto') {
|
|
252
|
+
const preferred = this.providers[opts.preferProvider];
|
|
253
|
+
if (preferred && this.trackers[opts.preferProvider]?.canAcceptRequest()) {
|
|
254
|
+
return preferred;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (opts.sessionId) {
|
|
259
|
+
const sticky = this.sessions.get(opts.sessionId);
|
|
260
|
+
if (sticky) {
|
|
261
|
+
const stickyTimeout = this.config.sessions?.sticky_timeout_ms || 300000;
|
|
262
|
+
if (Date.now() - sticky.lastUsed < stickyTimeout) {
|
|
263
|
+
const provider = this.providers[sticky.provider];
|
|
264
|
+
if (provider && this.trackers[sticky.provider]?.canAcceptRequest()) {
|
|
265
|
+
return provider;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const active = this.providers[this.activeProvider];
|
|
272
|
+
if (active && this.trackers[this.activeProvider]?.canAcceptRequest()) {
|
|
273
|
+
return active;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const fallback = this._findHealthyFallback(this.activeProvider);
|
|
277
|
+
return fallback || this.providers[this.activeProvider];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_findHealthyFallback(excludeProvider) {
|
|
281
|
+
const order = getProviderOrder(this.config);
|
|
282
|
+
|
|
283
|
+
for (const name of order) {
|
|
284
|
+
if (name === excludeProvider) continue;
|
|
285
|
+
const tracker = this.trackers[name];
|
|
286
|
+
if (tracker?.canAcceptRequest() && this.providers[name]) {
|
|
287
|
+
return this.providers[name];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_autoFailover(event) {
|
|
294
|
+
const fallback = this._findHealthyFallback(event.provider);
|
|
295
|
+
if (!fallback) return;
|
|
296
|
+
|
|
297
|
+
const prev = this.activeProvider;
|
|
298
|
+
this.activeProvider = fallback.name;
|
|
299
|
+
|
|
300
|
+
this.emit('auto_failover', {
|
|
301
|
+
from: prev,
|
|
302
|
+
to: fallback.name,
|
|
303
|
+
reason: `${event.provider} ${event.to}: ${event.reason}`,
|
|
304
|
+
trigger: 'rate_limit',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get tracker for a provider (for hooks integration).
|
|
310
|
+
*/
|
|
311
|
+
getTracker(providerName) {
|
|
312
|
+
return this.trackers[providerName];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
destroy() {
|
|
316
|
+
for (const tracker of Object.values(this.trackers)) {
|
|
317
|
+
tracker.destroy();
|
|
318
|
+
}
|
|
319
|
+
this.sessions.clear();
|
|
320
|
+
this.removeAllListeners();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = { LoadBalancer };
|