create-byan-agent 2.9.4 → 2.9.6
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/bin/create-byan-agent-v2.js +15 -1
- package/install/lib/cleanup/detector.js +154 -0
- package/install/lib/cleanup/executor.js +72 -0
- package/install/lib/staging-consent.js +149 -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/stage-to-byan.js +119 -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 +27 -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/.github/extensions/byan-staging/extension.mjs +169 -0
- package/install/templates/.github/extensions/byan-staging/package.json +8 -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/src/staging/staging.js +394 -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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseProvider — Abstract Provider Interface
|
|
3
|
+
*
|
|
4
|
+
* All providers (Copilot, Claude, BYAN API) extend this class.
|
|
5
|
+
* Defines the contract for the LoadBalancer to interact with any provider.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class BaseProvider {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} name - Provider identifier (e.g., 'copilot', 'claude')
|
|
11
|
+
* @param {object} providerConfig - Provider section from loadbalancer config
|
|
12
|
+
*/
|
|
13
|
+
constructor(name, providerConfig) {
|
|
14
|
+
if (new.target === BaseProvider) {
|
|
15
|
+
throw new Error('BaseProvider is abstract — use CopilotProvider or ClaudeProvider');
|
|
16
|
+
}
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.config = providerConfig;
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the provider (SDK auth, session setup, etc.)
|
|
24
|
+
*/
|
|
25
|
+
async initialize() {
|
|
26
|
+
throw new Error(`${this.name}: initialize() not implemented`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a prompt and get a response.
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
* @param {string} opts.prompt - The user prompt
|
|
33
|
+
* @param {string} [opts.model] - Model override
|
|
34
|
+
* @param {string} [opts.sessionId] - Session ID for continuity
|
|
35
|
+
* @returns {Promise<ProviderResponse>}
|
|
36
|
+
*/
|
|
37
|
+
async send(opts) {
|
|
38
|
+
throw new Error(`${this.name}: send() not implemented`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if provider is available (auth valid, SDK loaded).
|
|
43
|
+
* @returns {Promise<boolean>}
|
|
44
|
+
*/
|
|
45
|
+
async isAvailable() {
|
|
46
|
+
throw new Error(`${this.name}: isAvailable() not implemented`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get provider capabilities.
|
|
51
|
+
* @returns {object} - { streaming, tools, multiTurn, maxContextTokens }
|
|
52
|
+
*/
|
|
53
|
+
getCapabilities() {
|
|
54
|
+
return {
|
|
55
|
+
streaming: false,
|
|
56
|
+
tools: false,
|
|
57
|
+
multiTurn: false,
|
|
58
|
+
maxContextTokens: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clean up resources.
|
|
64
|
+
*/
|
|
65
|
+
async destroy() {
|
|
66
|
+
this.initialized = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {object} ProviderResponse
|
|
72
|
+
* @property {string} provider - Provider name
|
|
73
|
+
* @property {string} content - Response text
|
|
74
|
+
* @property {string} model - Model used
|
|
75
|
+
* @property {object} [rateLimitHeaders] - Raw rate limit headers if available
|
|
76
|
+
* @property {number} latencyMs - Round-trip time in ms
|
|
77
|
+
* @property {boolean} rateLimited - Whether a 429 was encountered
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
module.exports = { BaseProvider };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ByanApiProvider — Third Provider via BYAN API
|
|
3
|
+
*
|
|
4
|
+
* When BYAN_API_TOKEN is set, registers as a third provider.
|
|
5
|
+
* Uses existing BYAN API (port 3737) with BSP tree + context resolver.
|
|
6
|
+
* Lightweight fallback when both Copilot and Claude are rate-limited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { BaseProvider } = require('./base-provider');
|
|
10
|
+
|
|
11
|
+
class ByanApiProvider extends BaseProvider {
|
|
12
|
+
constructor(providerConfig) {
|
|
13
|
+
super('byan_api', providerConfig);
|
|
14
|
+
this.baseUrl = providerConfig.url || 'http://localhost:3737';
|
|
15
|
+
this.token = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize() {
|
|
19
|
+
this.token = process.env[this.config.auth_env || 'BYAN_API_TOKEN'];
|
|
20
|
+
this.initialized = !!this.token;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async isAvailable() {
|
|
24
|
+
if (!this.token) return false;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const http = require('http');
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const url = new URL('/api/health', this.baseUrl);
|
|
30
|
+
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
31
|
+
resolve(res.statusCode === 200);
|
|
32
|
+
});
|
|
33
|
+
req.on('error', () => resolve(false));
|
|
34
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async send(opts) {
|
|
42
|
+
if (!this.initialized) throw new Error('ByanApiProvider not initialized');
|
|
43
|
+
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const http = require('http');
|
|
46
|
+
|
|
47
|
+
const body = JSON.stringify({
|
|
48
|
+
prompt: opts.prompt,
|
|
49
|
+
session_id: opts.sessionId,
|
|
50
|
+
model: opts.model,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const url = new URL('/api/agent/execute', this.baseUrl);
|
|
55
|
+
const req = http.request(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'Authorization': `Bearer ${this.token}`,
|
|
60
|
+
'Content-Length': Buffer.byteLength(body),
|
|
61
|
+
},
|
|
62
|
+
timeout: 30000,
|
|
63
|
+
}, (res) => {
|
|
64
|
+
let data = '';
|
|
65
|
+
res.on('data', chunk => { data += chunk; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
const latencyMs = Date.now() - start;
|
|
68
|
+
|
|
69
|
+
if (res.statusCode === 429) {
|
|
70
|
+
const retryAfter = res.headers['retry-after'];
|
|
71
|
+
resolve({
|
|
72
|
+
provider: this.name,
|
|
73
|
+
content: null,
|
|
74
|
+
model: 'byan-api',
|
|
75
|
+
rateLimitHeaders: { 'retry-after': retryAfter },
|
|
76
|
+
latencyMs,
|
|
77
|
+
rateLimited: true,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (res.statusCode >= 400) {
|
|
83
|
+
reject(new Error(`BYAN API error ${res.statusCode}: ${data}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(data);
|
|
89
|
+
resolve({
|
|
90
|
+
provider: this.name,
|
|
91
|
+
content: parsed.response || parsed.content || data,
|
|
92
|
+
model: parsed.model || 'byan-api',
|
|
93
|
+
rateLimitHeaders: null,
|
|
94
|
+
latencyMs,
|
|
95
|
+
rateLimited: false,
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
resolve({
|
|
99
|
+
provider: this.name,
|
|
100
|
+
content: data,
|
|
101
|
+
model: 'byan-api',
|
|
102
|
+
rateLimitHeaders: null,
|
|
103
|
+
latencyMs,
|
|
104
|
+
rateLimited: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.on('error', reject);
|
|
111
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('BYAN API timeout')); });
|
|
112
|
+
req.write(body);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getCapabilities() {
|
|
118
|
+
return {
|
|
119
|
+
streaming: false,
|
|
120
|
+
tools: false,
|
|
121
|
+
multiTurn: true,
|
|
122
|
+
maxContextTokens: 32000,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async destroy() {
|
|
127
|
+
this.token = null;
|
|
128
|
+
await super.destroy();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { ByanApiProvider };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeProvider — Wraps @anthropic-ai/claude-agent-sdk
|
|
3
|
+
*
|
|
4
|
+
* Uses Claude Agent SDK to query and resume sessions.
|
|
5
|
+
* Catches RateLimitError for circuit breaker integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { BaseProvider } = require('./base-provider');
|
|
9
|
+
|
|
10
|
+
class ClaudeProvider extends BaseProvider {
|
|
11
|
+
constructor(providerConfig) {
|
|
12
|
+
super('claude', providerConfig);
|
|
13
|
+
this.sdk = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async initialize() {
|
|
17
|
+
try {
|
|
18
|
+
this.sdk = require('@anthropic-ai/claude-code');
|
|
19
|
+
this.initialized = true;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
22
|
+
this.initialized = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async isAvailable() {
|
|
30
|
+
if (!this.initialized || !this.sdk) return false;
|
|
31
|
+
try {
|
|
32
|
+
const token = process.env[this.config.auth_env || 'ANTHROPIC_API_KEY'];
|
|
33
|
+
return !!token;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async send(opts) {
|
|
40
|
+
if (!this.initialized) throw new Error('ClaudeProvider not initialized');
|
|
41
|
+
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const model = opts.model || this.config.models?.agent || 'claude-sonnet-4-20250514';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const messages = [];
|
|
47
|
+
const result = this.sdk.query({
|
|
48
|
+
prompt: opts.prompt,
|
|
49
|
+
options: {
|
|
50
|
+
model,
|
|
51
|
+
maxTurns: 1,
|
|
52
|
+
...(opts.sessionId ? { continue: opts.sessionId } : {}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for await (const event of result) {
|
|
57
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
58
|
+
for (const block of event.message.content) {
|
|
59
|
+
if (block.type === 'text') {
|
|
60
|
+
messages.push(block.text);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
provider: this.name,
|
|
68
|
+
content: messages.join('\n'),
|
|
69
|
+
model,
|
|
70
|
+
rateLimitHeaders: null,
|
|
71
|
+
latencyMs: Date.now() - start,
|
|
72
|
+
rateLimited: false,
|
|
73
|
+
};
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const isRateLimit =
|
|
76
|
+
err.name === 'RateLimitError' ||
|
|
77
|
+
err.status === 429 ||
|
|
78
|
+
(err.message && err.message.includes('rate_limit'));
|
|
79
|
+
|
|
80
|
+
if (isRateLimit) {
|
|
81
|
+
const retryAfter = err.headers?.['retry-after'] || err.retryAfter;
|
|
82
|
+
return {
|
|
83
|
+
provider: this.name,
|
|
84
|
+
content: null,
|
|
85
|
+
model,
|
|
86
|
+
rateLimitHeaders: {
|
|
87
|
+
'retry-after': retryAfter ? String(retryAfter) : undefined,
|
|
88
|
+
},
|
|
89
|
+
latencyMs: Date.now() - start,
|
|
90
|
+
rateLimited: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getCapabilities() {
|
|
99
|
+
return {
|
|
100
|
+
streaming: true,
|
|
101
|
+
tools: true,
|
|
102
|
+
multiTurn: true,
|
|
103
|
+
maxContextTokens: 200000,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async destroy() {
|
|
108
|
+
this.sdk = null;
|
|
109
|
+
await super.destroy();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { ClaudeProvider };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CopilotProvider — Wraps @github/copilot-sdk
|
|
3
|
+
*
|
|
4
|
+
* Uses the Copilot SDK to create sessions and send prompts.
|
|
5
|
+
* Extracts rate limit signals from HTTP headers and error events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { BaseProvider } = require('./base-provider');
|
|
9
|
+
|
|
10
|
+
class CopilotProvider extends BaseProvider {
|
|
11
|
+
constructor(providerConfig) {
|
|
12
|
+
super('copilot', providerConfig);
|
|
13
|
+
this.sdk = null;
|
|
14
|
+
this.session = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize() {
|
|
18
|
+
try {
|
|
19
|
+
const { createClient } = require('@github/copilot-sdk');
|
|
20
|
+
this.sdk = createClient();
|
|
21
|
+
this.initialized = true;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async isAvailable() {
|
|
32
|
+
if (!this.initialized || !this.sdk) return false;
|
|
33
|
+
try {
|
|
34
|
+
const token = process.env[this.config.auth_env || 'COPILOT_GITHUB_TOKEN'];
|
|
35
|
+
return !!token;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async send(opts) {
|
|
42
|
+
if (!this.initialized) throw new Error('CopilotProvider not initialized');
|
|
43
|
+
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const model = opts.model || this.config.models?.agent || 'gpt-4.1';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const { createSession } = require('@github/copilot-sdk');
|
|
49
|
+
const session = createSession({
|
|
50
|
+
model,
|
|
51
|
+
onPermissionRequest: () => ({ allow: true }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const response = await session.sendAndWait({ prompt: opts.prompt });
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
provider: this.name,
|
|
58
|
+
content: response.content || response.data?.content || '',
|
|
59
|
+
model,
|
|
60
|
+
rateLimitHeaders: null,
|
|
61
|
+
latencyMs: Date.now() - start,
|
|
62
|
+
rateLimited: false,
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const is429 = err.status === 429 || err.statusCode === 429 ||
|
|
66
|
+
(err.message && err.message.includes('rate limit'));
|
|
67
|
+
|
|
68
|
+
if (is429) {
|
|
69
|
+
const headers = err.headers || err.response?.headers || {};
|
|
70
|
+
return {
|
|
71
|
+
provider: this.name,
|
|
72
|
+
content: null,
|
|
73
|
+
model,
|
|
74
|
+
rateLimitHeaders: {
|
|
75
|
+
'x-ratelimit-remaining': headers['x-ratelimit-remaining'],
|
|
76
|
+
'x-ratelimit-reset': headers['x-ratelimit-reset'],
|
|
77
|
+
'retry-after': headers['retry-after'],
|
|
78
|
+
},
|
|
79
|
+
latencyMs: Date.now() - start,
|
|
80
|
+
rateLimited: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getCapabilities() {
|
|
89
|
+
return {
|
|
90
|
+
streaming: true,
|
|
91
|
+
tools: true,
|
|
92
|
+
multiTurn: true,
|
|
93
|
+
maxContextTokens: 128000,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async destroy() {
|
|
98
|
+
this.sdk = null;
|
|
99
|
+
this.session = null;
|
|
100
|
+
await super.destroy();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { CopilotProvider };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RateLimitTracker — CircuitBreaker State Machine
|
|
3
|
+
*
|
|
4
|
+
* Per-provider rate limit tracking with circuit breaker pattern.
|
|
5
|
+
*
|
|
6
|
+
* States:
|
|
7
|
+
* HEALTHY → Normal operation, all requests pass through
|
|
8
|
+
* THROTTLED → Approaching limit, requests allowed but monitored
|
|
9
|
+
* BLOCKED → Circuit open, no requests pass. Waiting for recovery.
|
|
10
|
+
* RECOVERING → Half-open, limited probe requests to test recovery
|
|
11
|
+
*
|
|
12
|
+
* Transitions:
|
|
13
|
+
* HEALTHY → THROTTLED (429 count >= throttle_threshold in window)
|
|
14
|
+
* THROTTLED → BLOCKED (429 count >= block_threshold in window)
|
|
15
|
+
* BLOCKED → RECOVERING (recovery_probe_interval elapsed)
|
|
16
|
+
* RECOVERING → HEALTHY (probe succeeded, no 429 in half_open_max_requests)
|
|
17
|
+
* RECOVERING → BLOCKED (probe failed, got another 429)
|
|
18
|
+
* THROTTLED → HEALTHY (window elapsed with no new 429s)
|
|
19
|
+
* any → HEALTHY (manual reset)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { EventEmitter } = require('events');
|
|
23
|
+
|
|
24
|
+
const STATES = {
|
|
25
|
+
HEALTHY: 'HEALTHY',
|
|
26
|
+
THROTTLED: 'THROTTLED',
|
|
27
|
+
BLOCKED: 'BLOCKED',
|
|
28
|
+
RECOVERING: 'RECOVERING',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class RateLimitTracker extends EventEmitter {
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} providerName
|
|
34
|
+
* @param {object} opts - from config.rate_limits
|
|
35
|
+
*/
|
|
36
|
+
constructor(providerName, opts = {}) {
|
|
37
|
+
super();
|
|
38
|
+
this.provider = providerName;
|
|
39
|
+
this.state = STATES.HEALTHY;
|
|
40
|
+
|
|
41
|
+
this.windowMs = opts.window_ms || 60000;
|
|
42
|
+
this.throttleThreshold = opts.throttle_threshold || 2;
|
|
43
|
+
this.blockThreshold = opts.block_threshold || 3;
|
|
44
|
+
this.recoveryProbeMs = opts.recovery_probe_interval_ms || 10000;
|
|
45
|
+
this.halfOpenMax = opts.half_open_max_requests || 2;
|
|
46
|
+
|
|
47
|
+
this.events429 = [];
|
|
48
|
+
this.lastStateChange = Date.now();
|
|
49
|
+
this.recoveryTimer = null;
|
|
50
|
+
this.halfOpenRequests = 0;
|
|
51
|
+
this.halfOpenSuccesses = 0;
|
|
52
|
+
this.totalRequests = 0;
|
|
53
|
+
this.total429s = 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getState() {
|
|
57
|
+
return {
|
|
58
|
+
provider: this.provider,
|
|
59
|
+
state: this.state,
|
|
60
|
+
count429InWindow: this._countInWindow(),
|
|
61
|
+
windowMs: this.windowMs,
|
|
62
|
+
lastStateChange: this.lastStateChange,
|
|
63
|
+
totalRequests: this.totalRequests,
|
|
64
|
+
total429s: this.total429s,
|
|
65
|
+
canAcceptRequest: this.canAcceptRequest(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
canAcceptRequest() {
|
|
70
|
+
switch (this.state) {
|
|
71
|
+
case STATES.HEALTHY:
|
|
72
|
+
case STATES.THROTTLED:
|
|
73
|
+
return true;
|
|
74
|
+
case STATES.RECOVERING:
|
|
75
|
+
return this.halfOpenRequests < this.halfOpenMax;
|
|
76
|
+
case STATES.BLOCKED:
|
|
77
|
+
return false;
|
|
78
|
+
default:
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record a successful request (no rate limit error).
|
|
85
|
+
*/
|
|
86
|
+
recordSuccess() {
|
|
87
|
+
this.totalRequests++;
|
|
88
|
+
|
|
89
|
+
if (this.state === STATES.RECOVERING) {
|
|
90
|
+
this.halfOpenSuccesses++;
|
|
91
|
+
if (this.halfOpenSuccesses >= this.halfOpenMax) {
|
|
92
|
+
this._transition(STATES.HEALTHY, 'Recovery probes all succeeded');
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.state === STATES.THROTTLED) {
|
|
98
|
+
this._pruneWindow();
|
|
99
|
+
if (this._countInWindow() === 0) {
|
|
100
|
+
this._transition(STATES.HEALTHY, 'Window cleared, no recent 429s');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Record a 429 rate limit error.
|
|
107
|
+
* @param {object} [meta] - Optional metadata (headers, retry-after, etc.)
|
|
108
|
+
*/
|
|
109
|
+
record429(meta = {}) {
|
|
110
|
+
this.totalRequests++;
|
|
111
|
+
this.total429s++;
|
|
112
|
+
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
this.events429.push({ timestamp: now, meta });
|
|
115
|
+
this._pruneWindow();
|
|
116
|
+
|
|
117
|
+
const count = this._countInWindow();
|
|
118
|
+
|
|
119
|
+
if (this.state === STATES.RECOVERING) {
|
|
120
|
+
this._transition(STATES.BLOCKED, `429 during recovery (count: ${count})`);
|
|
121
|
+
this._scheduleRecovery();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (count >= this.blockThreshold) {
|
|
126
|
+
this._transition(STATES.BLOCKED, `429 count ${count} >= block threshold ${this.blockThreshold}`);
|
|
127
|
+
this._scheduleRecovery();
|
|
128
|
+
} else if (count >= this.throttleThreshold && this.state === STATES.HEALTHY) {
|
|
129
|
+
this._transition(STATES.THROTTLED, `429 count ${count} >= throttle threshold ${this.throttleThreshold}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record a response with rate limit headers (preemptive detection).
|
|
135
|
+
* @param {object} headers - { 'x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after' }
|
|
136
|
+
*/
|
|
137
|
+
recordHeaders(headers = {}) {
|
|
138
|
+
const remaining = parseInt(headers['x-ratelimit-remaining'], 10);
|
|
139
|
+
const retryAfter = parseInt(headers['retry-after'], 10);
|
|
140
|
+
|
|
141
|
+
if (retryAfter > 0) {
|
|
142
|
+
this.record429({ retryAfter, source: 'header' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!isNaN(remaining) && remaining <= 2 && this.state === STATES.HEALTHY) {
|
|
147
|
+
this._transition(STATES.THROTTLED, `Preemptive: x-ratelimit-remaining=${remaining}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Force reset to HEALTHY state.
|
|
153
|
+
*/
|
|
154
|
+
reset() {
|
|
155
|
+
this._clearRecoveryTimer();
|
|
156
|
+
this.events429 = [];
|
|
157
|
+
this.halfOpenRequests = 0;
|
|
158
|
+
this.halfOpenSuccesses = 0;
|
|
159
|
+
this._transition(STATES.HEALTHY, 'Manual reset');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
destroy() {
|
|
163
|
+
this._clearRecoveryTimer();
|
|
164
|
+
this.removeAllListeners();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_transition(newState, reason) {
|
|
168
|
+
const prev = this.state;
|
|
169
|
+
if (prev === newState) return;
|
|
170
|
+
|
|
171
|
+
this.state = newState;
|
|
172
|
+
this.lastStateChange = Date.now();
|
|
173
|
+
|
|
174
|
+
if (newState === STATES.RECOVERING) {
|
|
175
|
+
this.halfOpenRequests = 0;
|
|
176
|
+
this.halfOpenSuccesses = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.emit('state_change', {
|
|
180
|
+
provider: this.provider,
|
|
181
|
+
from: prev,
|
|
182
|
+
to: newState,
|
|
183
|
+
reason,
|
|
184
|
+
timestamp: this.lastStateChange,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_scheduleRecovery() {
|
|
189
|
+
this._clearRecoveryTimer();
|
|
190
|
+
this.recoveryTimer = setTimeout(() => {
|
|
191
|
+
if (this.state === STATES.BLOCKED) {
|
|
192
|
+
this._transition(STATES.RECOVERING, `Recovery probe after ${this.recoveryProbeMs}ms`);
|
|
193
|
+
}
|
|
194
|
+
}, this.recoveryProbeMs);
|
|
195
|
+
if (this.recoveryTimer.unref) this.recoveryTimer.unref();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_clearRecoveryTimer() {
|
|
199
|
+
if (this.recoveryTimer) {
|
|
200
|
+
clearTimeout(this.recoveryTimer);
|
|
201
|
+
this.recoveryTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_pruneWindow() {
|
|
206
|
+
const cutoff = Date.now() - this.windowMs;
|
|
207
|
+
this.events429 = this.events429.filter(e => e.timestamp > cutoff);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_countInWindow() {
|
|
211
|
+
this._pruneWindow();
|
|
212
|
+
return this.events429.length;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { RateLimitTracker, STATES };
|