whale-code 6.4.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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Capability Matrix — static feature map for intelligent request routing.
|
|
3
|
+
*
|
|
4
|
+
* Each provider declares which features it supports. This enables:
|
|
5
|
+
* - Pre-flight checks before sending a request to a provider
|
|
6
|
+
* - Capability-aware failover (don't fail over to a provider that can't handle the request)
|
|
7
|
+
* - Intelligent routing (pick the best provider for a given request shape)
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CAPABILITY MAP — static, no runtime computation
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const PROVIDER_CAPABILITIES = {
|
|
13
|
+
anthropic: {
|
|
14
|
+
vision: true,
|
|
15
|
+
pdf: true,
|
|
16
|
+
computerUse: true,
|
|
17
|
+
codeExecution: false,
|
|
18
|
+
streaming: true,
|
|
19
|
+
batchApi: true,
|
|
20
|
+
citations: true,
|
|
21
|
+
thinking: true,
|
|
22
|
+
toolUse: true,
|
|
23
|
+
caching: true,
|
|
24
|
+
contextManagement: true,
|
|
25
|
+
maxContextTokens: 200_000,
|
|
26
|
+
maxOutputTokens: 128_000,
|
|
27
|
+
},
|
|
28
|
+
openai: {
|
|
29
|
+
vision: true,
|
|
30
|
+
pdf: false,
|
|
31
|
+
computerUse: false,
|
|
32
|
+
codeExecution: true,
|
|
33
|
+
streaming: true,
|
|
34
|
+
batchApi: true,
|
|
35
|
+
citations: false,
|
|
36
|
+
thinking: true,
|
|
37
|
+
toolUse: true,
|
|
38
|
+
caching: true,
|
|
39
|
+
contextManagement: false,
|
|
40
|
+
maxContextTokens: 128_000,
|
|
41
|
+
maxOutputTokens: 128_000,
|
|
42
|
+
},
|
|
43
|
+
gemini: {
|
|
44
|
+
vision: true,
|
|
45
|
+
pdf: true,
|
|
46
|
+
computerUse: false,
|
|
47
|
+
codeExecution: true,
|
|
48
|
+
streaming: true,
|
|
49
|
+
batchApi: false,
|
|
50
|
+
citations: false,
|
|
51
|
+
thinking: true,
|
|
52
|
+
toolUse: true,
|
|
53
|
+
caching: true,
|
|
54
|
+
contextManagement: false,
|
|
55
|
+
maxContextTokens: 1_000_000,
|
|
56
|
+
maxOutputTokens: 65_536,
|
|
57
|
+
},
|
|
58
|
+
bedrock: {
|
|
59
|
+
vision: true,
|
|
60
|
+
pdf: true,
|
|
61
|
+
computerUse: true,
|
|
62
|
+
codeExecution: false,
|
|
63
|
+
streaming: true,
|
|
64
|
+
batchApi: false,
|
|
65
|
+
citations: true,
|
|
66
|
+
thinking: true,
|
|
67
|
+
toolUse: true,
|
|
68
|
+
caching: false,
|
|
69
|
+
contextManagement: false,
|
|
70
|
+
maxContextTokens: 200_000,
|
|
71
|
+
maxOutputTokens: 64_000,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// QUERY FUNCTIONS
|
|
76
|
+
// ============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Get the full capability set for a provider.
|
|
79
|
+
* Returns a defensive copy so callers can't mutate the static map.
|
|
80
|
+
*/
|
|
81
|
+
export function getCapabilities(provider) {
|
|
82
|
+
const caps = PROVIDER_CAPABILITIES[provider];
|
|
83
|
+
if (!caps) {
|
|
84
|
+
throw new Error(`Unknown provider: "${provider}". Known: ${Object.keys(PROVIDER_CAPABILITIES).join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
return { ...caps };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if a provider supports a specific boolean feature.
|
|
90
|
+
*/
|
|
91
|
+
export function supportsFeature(provider, feature) {
|
|
92
|
+
const caps = PROVIDER_CAPABILITIES[provider];
|
|
93
|
+
if (!caps)
|
|
94
|
+
return false;
|
|
95
|
+
return caps[feature] === true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find all providers that support a given boolean feature.
|
|
99
|
+
* Returns provider names sorted with anthropic first (default preference).
|
|
100
|
+
*/
|
|
101
|
+
export function findProvidersWithCapability(feature) {
|
|
102
|
+
const preferred = ["anthropic", "openai", "gemini", "bedrock"];
|
|
103
|
+
return preferred.filter((p) => {
|
|
104
|
+
const caps = PROVIDER_CAPABILITIES[p];
|
|
105
|
+
return caps && caps[feature] === true;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Determine the best provider for a request based on its capability requirements.
|
|
110
|
+
*
|
|
111
|
+
* Strategy:
|
|
112
|
+
* 1. Filter to providers that satisfy ALL required capabilities
|
|
113
|
+
* 2. Prefer Anthropic as default
|
|
114
|
+
* 3. Among remaining candidates, prefer the one with the largest context window
|
|
115
|
+
*
|
|
116
|
+
* Returns "anthropic" as fallback if no provider matches all requirements
|
|
117
|
+
* (better to attempt than to hard-fail — the provider will return a clear error).
|
|
118
|
+
*/
|
|
119
|
+
export function getBestProviderForRequest(request, excludeProviders) {
|
|
120
|
+
const allProviders = ["anthropic", "openai", "gemini", "bedrock"];
|
|
121
|
+
const candidates = allProviders.filter((provider) => {
|
|
122
|
+
if (excludeProviders?.has(provider))
|
|
123
|
+
return false;
|
|
124
|
+
const caps = PROVIDER_CAPABILITIES[provider];
|
|
125
|
+
if (!caps)
|
|
126
|
+
return false;
|
|
127
|
+
// Check boolean requirements
|
|
128
|
+
if (request.hasImages && !caps.vision)
|
|
129
|
+
return false;
|
|
130
|
+
if (request.hasPdf && !caps.pdf)
|
|
131
|
+
return false;
|
|
132
|
+
if (request.needsBatch && !caps.batchApi)
|
|
133
|
+
return false;
|
|
134
|
+
if (request.needsCitations && !caps.citations)
|
|
135
|
+
return false;
|
|
136
|
+
if (request.needsComputerUse && !caps.computerUse)
|
|
137
|
+
return false;
|
|
138
|
+
if (request.needsCodeExecution && !caps.codeExecution)
|
|
139
|
+
return false;
|
|
140
|
+
if (request.needsThinking && !caps.thinking)
|
|
141
|
+
return false;
|
|
142
|
+
if (request.needsContextManagement && !caps.contextManagement)
|
|
143
|
+
return false;
|
|
144
|
+
// Check numeric requirements
|
|
145
|
+
if (request.minContextTokens && caps.maxContextTokens < request.minContextTokens)
|
|
146
|
+
return false;
|
|
147
|
+
if (request.minOutputTokens && caps.maxOutputTokens < request.minOutputTokens)
|
|
148
|
+
return false;
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
if (candidates.length === 0) {
|
|
152
|
+
// No provider matches all requirements — fall back to anthropic
|
|
153
|
+
return "anthropic";
|
|
154
|
+
}
|
|
155
|
+
// Prefer anthropic if it's among candidates
|
|
156
|
+
if (candidates.includes("anthropic"))
|
|
157
|
+
return "anthropic";
|
|
158
|
+
// Otherwise return the first candidate (ordered by preference: openai, gemini, bedrock)
|
|
159
|
+
return candidates[0];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if a provider can handle a request with the given requirements.
|
|
163
|
+
* Returns true if all requirements are met.
|
|
164
|
+
*/
|
|
165
|
+
export function canProviderHandleRequest(provider, requirements) {
|
|
166
|
+
const caps = PROVIDER_CAPABILITIES[provider];
|
|
167
|
+
if (!caps)
|
|
168
|
+
return false;
|
|
169
|
+
if (requirements.hasImages && !caps.vision)
|
|
170
|
+
return false;
|
|
171
|
+
if (requirements.hasPdf && !caps.pdf)
|
|
172
|
+
return false;
|
|
173
|
+
if (requirements.needsBatch && !caps.batchApi)
|
|
174
|
+
return false;
|
|
175
|
+
if (requirements.needsCitations && !caps.citations)
|
|
176
|
+
return false;
|
|
177
|
+
if (requirements.needsComputerUse && !caps.computerUse)
|
|
178
|
+
return false;
|
|
179
|
+
if (requirements.needsCodeExecution && !caps.codeExecution)
|
|
180
|
+
return false;
|
|
181
|
+
if (requirements.needsThinking && !caps.thinking)
|
|
182
|
+
return false;
|
|
183
|
+
if (requirements.needsContextManagement && !caps.contextManagement)
|
|
184
|
+
return false;
|
|
185
|
+
if (requirements.minContextTokens && caps.maxContextTokens < requirements.minContextTokens)
|
|
186
|
+
return false;
|
|
187
|
+
if (requirements.minOutputTokens && caps.maxOutputTokens < requirements.minOutputTokens)
|
|
188
|
+
return false;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Failover Chain — Phase 4.1
|
|
3
|
+
*
|
|
4
|
+
* Manages provider health and automatic failover routing.
|
|
5
|
+
* If a provider accumulates 3 consecutive failures within 5 minutes,
|
|
6
|
+
* it is marked degraded and requests are routed to the next healthy
|
|
7
|
+
* provider in the chain. Degraded providers are re-checked every 60s.
|
|
8
|
+
*/
|
|
9
|
+
import { type RequestCapabilityRequirements } from "./provider-capabilities.js";
|
|
10
|
+
export interface ProviderHealth {
|
|
11
|
+
name: string;
|
|
12
|
+
status: "healthy" | "degraded";
|
|
13
|
+
consecutiveFailures: number;
|
|
14
|
+
lastFailure: number;
|
|
15
|
+
degradedAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface FailoverEvent {
|
|
18
|
+
timestamp: number;
|
|
19
|
+
originalProvider: string;
|
|
20
|
+
fallbackProvider: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class ProviderFailover {
|
|
24
|
+
private providers;
|
|
25
|
+
private chain;
|
|
26
|
+
private recoveryTimer;
|
|
27
|
+
private auditLog;
|
|
28
|
+
constructor(chain: string[]);
|
|
29
|
+
/**
|
|
30
|
+
* Returns the provider (and optionally a fallback model) to use.
|
|
31
|
+
* If the requested provider is healthy, returns it as-is.
|
|
32
|
+
* If degraded, walks the chain to find the next healthy provider
|
|
33
|
+
* and returns a sensible fallback model for that provider.
|
|
34
|
+
*
|
|
35
|
+
* When requiredCapabilities is provided, failover candidates that lack
|
|
36
|
+
* the required capabilities are skipped — prevents routing a PDF request
|
|
37
|
+
* to a provider that doesn't support PDFs, for example.
|
|
38
|
+
*/
|
|
39
|
+
getActiveProvider(requestedModel: string, requiredCapabilities?: RequestCapabilityRequirements): {
|
|
40
|
+
provider: string;
|
|
41
|
+
model: string;
|
|
42
|
+
failedOver: boolean;
|
|
43
|
+
originalProvider: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Record a successful API call — resets consecutive failures and
|
|
47
|
+
* marks the provider healthy if it was degraded.
|
|
48
|
+
*/
|
|
49
|
+
recordSuccess(provider: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Record a failed API call — increments consecutive failures.
|
|
52
|
+
* If threshold is reached within the failure window, marks degraded.
|
|
53
|
+
*/
|
|
54
|
+
recordFailure(provider: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Start the recovery loop — checks degraded providers every 60s.
|
|
57
|
+
* Resets them to healthy so the next request can attempt them.
|
|
58
|
+
* If the attempt fails, recordFailure will re-degrade immediately.
|
|
59
|
+
*/
|
|
60
|
+
startRecoveryLoop(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Stop the recovery loop (for clean shutdown / testing).
|
|
63
|
+
*/
|
|
64
|
+
stopRecoveryLoop(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Get current health status of all providers.
|
|
67
|
+
*/
|
|
68
|
+
getStatus(): ProviderHealth[];
|
|
69
|
+
/**
|
|
70
|
+
* Get the failover audit log.
|
|
71
|
+
*/
|
|
72
|
+
getAuditLog(): FailoverEvent[];
|
|
73
|
+
}
|
|
74
|
+
export declare const providerFailover: ProviderFailover;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Failover Chain — Phase 4.1
|
|
3
|
+
*
|
|
4
|
+
* Manages provider health and automatic failover routing.
|
|
5
|
+
* If a provider accumulates 3 consecutive failures within 5 minutes,
|
|
6
|
+
* it is marked degraded and requests are routed to the next healthy
|
|
7
|
+
* provider in the chain. Degraded providers are re-checked every 60s.
|
|
8
|
+
*/
|
|
9
|
+
import { getProvider } from "../../shared/constants.js";
|
|
10
|
+
import { canProviderHandleRequest } from "./provider-capabilities.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// CONSTANTS
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const DEGRADATION_THRESHOLD = 3; // consecutive failures to mark degraded
|
|
15
|
+
const FAILURE_WINDOW_MS = 5 * 60 * 1000; // 5 minutes — failures outside this window don't count
|
|
16
|
+
const RECOVERY_CHECK_MS = 60 * 1000; // 60 seconds — how often to auto-recover degraded providers
|
|
17
|
+
// Default fallback model mapping — when a provider is degraded, use the
|
|
18
|
+
// equivalent-tier model from the fallback provider.
|
|
19
|
+
const FALLBACK_MODELS = {
|
|
20
|
+
// If anthropic is down, which openai/gemini model to use
|
|
21
|
+
anthropic: {
|
|
22
|
+
openai: "gpt-5",
|
|
23
|
+
gemini: "gemini-2.5-pro",
|
|
24
|
+
},
|
|
25
|
+
// If openai is down, which anthropic/gemini model to use
|
|
26
|
+
openai: {
|
|
27
|
+
anthropic: "claude-sonnet-4-6",
|
|
28
|
+
gemini: "gemini-2.5-pro",
|
|
29
|
+
},
|
|
30
|
+
// If gemini is down, which anthropic/openai model to use
|
|
31
|
+
gemini: {
|
|
32
|
+
anthropic: "claude-sonnet-4-6",
|
|
33
|
+
openai: "gpt-5",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// PROVIDER FAILOVER CLASS
|
|
38
|
+
// ============================================================================
|
|
39
|
+
export class ProviderFailover {
|
|
40
|
+
providers;
|
|
41
|
+
chain;
|
|
42
|
+
recoveryTimer = null;
|
|
43
|
+
auditLog = [];
|
|
44
|
+
constructor(chain) {
|
|
45
|
+
this.chain = chain;
|
|
46
|
+
this.providers = new Map();
|
|
47
|
+
for (const name of chain) {
|
|
48
|
+
this.providers.set(name, {
|
|
49
|
+
name,
|
|
50
|
+
status: "healthy",
|
|
51
|
+
consecutiveFailures: 0,
|
|
52
|
+
lastFailure: 0,
|
|
53
|
+
degradedAt: 0,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns the provider (and optionally a fallback model) to use.
|
|
59
|
+
* If the requested provider is healthy, returns it as-is.
|
|
60
|
+
* If degraded, walks the chain to find the next healthy provider
|
|
61
|
+
* and returns a sensible fallback model for that provider.
|
|
62
|
+
*
|
|
63
|
+
* When requiredCapabilities is provided, failover candidates that lack
|
|
64
|
+
* the required capabilities are skipped — prevents routing a PDF request
|
|
65
|
+
* to a provider that doesn't support PDFs, for example.
|
|
66
|
+
*/
|
|
67
|
+
getActiveProvider(requestedModel, requiredCapabilities) {
|
|
68
|
+
const originalProvider = getProvider(requestedModel);
|
|
69
|
+
const health = this.providers.get(originalProvider);
|
|
70
|
+
// If provider is healthy (or unknown to chain), use as-is
|
|
71
|
+
if (!health || health.status === "healthy") {
|
|
72
|
+
return {
|
|
73
|
+
provider: originalProvider,
|
|
74
|
+
model: requestedModel,
|
|
75
|
+
failedOver: false,
|
|
76
|
+
originalProvider,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Provider is degraded — find next healthy in chain
|
|
80
|
+
const chainIndex = this.chain.indexOf(originalProvider);
|
|
81
|
+
for (let offset = 1; offset < this.chain.length; offset++) {
|
|
82
|
+
const candidateIndex = (chainIndex + offset) % this.chain.length;
|
|
83
|
+
const candidateName = this.chain[candidateIndex];
|
|
84
|
+
const candidateHealth = this.providers.get(candidateName);
|
|
85
|
+
if (candidateHealth && candidateHealth.status === "healthy") {
|
|
86
|
+
// Skip candidates that can't handle the request's required capabilities
|
|
87
|
+
if (requiredCapabilities &&
|
|
88
|
+
!canProviderHandleRequest(candidateName, requiredCapabilities)) {
|
|
89
|
+
console.log(`[failover] Skipping ${candidateName} — lacks required capabilities`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Map to a fallback model for the candidate provider
|
|
93
|
+
const fallbackModel = FALLBACK_MODELS[originalProvider]?.[candidateName] || requestedModel;
|
|
94
|
+
const event = {
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
originalProvider,
|
|
97
|
+
fallbackProvider: candidateName,
|
|
98
|
+
reason: `${originalProvider} degraded (${health.consecutiveFailures} consecutive failures)`,
|
|
99
|
+
};
|
|
100
|
+
this.auditLog.push(event);
|
|
101
|
+
// Keep audit log bounded
|
|
102
|
+
if (this.auditLog.length > 1000) {
|
|
103
|
+
this.auditLog = this.auditLog.slice(-500);
|
|
104
|
+
}
|
|
105
|
+
console.log(`[failover] ${originalProvider} degraded, routing to ${candidateName} (model: ${fallbackModel})`);
|
|
106
|
+
return {
|
|
107
|
+
provider: candidateName,
|
|
108
|
+
model: fallbackModel,
|
|
109
|
+
failedOver: true,
|
|
110
|
+
originalProvider,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// All providers degraded (or none have required capabilities) — use the original anyway
|
|
115
|
+
console.log(`[failover] All providers degraded, attempting ${originalProvider} anyway`);
|
|
116
|
+
return {
|
|
117
|
+
provider: originalProvider,
|
|
118
|
+
model: requestedModel,
|
|
119
|
+
failedOver: false,
|
|
120
|
+
originalProvider,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Record a successful API call — resets consecutive failures and
|
|
125
|
+
* marks the provider healthy if it was degraded.
|
|
126
|
+
*/
|
|
127
|
+
recordSuccess(provider) {
|
|
128
|
+
const health = this.providers.get(provider);
|
|
129
|
+
if (!health)
|
|
130
|
+
return;
|
|
131
|
+
const wasDegraded = health.status === "degraded";
|
|
132
|
+
health.consecutiveFailures = 0;
|
|
133
|
+
health.status = "healthy";
|
|
134
|
+
if (wasDegraded) {
|
|
135
|
+
console.log(`[failover] ${provider} recovered — marked healthy`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Record a failed API call — increments consecutive failures.
|
|
140
|
+
* If threshold is reached within the failure window, marks degraded.
|
|
141
|
+
*/
|
|
142
|
+
recordFailure(provider) {
|
|
143
|
+
const health = this.providers.get(provider);
|
|
144
|
+
if (!health)
|
|
145
|
+
return;
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
// If last failure was outside the window, reset the counter
|
|
148
|
+
if (health.lastFailure > 0 && now - health.lastFailure > FAILURE_WINDOW_MS) {
|
|
149
|
+
health.consecutiveFailures = 0;
|
|
150
|
+
}
|
|
151
|
+
health.consecutiveFailures++;
|
|
152
|
+
health.lastFailure = now;
|
|
153
|
+
if (health.consecutiveFailures >= DEGRADATION_THRESHOLD &&
|
|
154
|
+
health.status === "healthy") {
|
|
155
|
+
health.status = "degraded";
|
|
156
|
+
health.degradedAt = now;
|
|
157
|
+
console.log(`[failover] ${provider} marked DEGRADED after ${health.consecutiveFailures} consecutive failures`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Start the recovery loop — checks degraded providers every 60s.
|
|
162
|
+
* Resets them to healthy so the next request can attempt them.
|
|
163
|
+
* If the attempt fails, recordFailure will re-degrade immediately.
|
|
164
|
+
*/
|
|
165
|
+
startRecoveryLoop() {
|
|
166
|
+
if (this.recoveryTimer)
|
|
167
|
+
return; // already running
|
|
168
|
+
this.recoveryTimer = setInterval(() => {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
for (const [name, health] of this.providers) {
|
|
171
|
+
if (health.status === "degraded" &&
|
|
172
|
+
now - health.degradedAt >= RECOVERY_CHECK_MS) {
|
|
173
|
+
console.log(`[failover] Recovery check: resetting ${name} to healthy for re-probe`);
|
|
174
|
+
health.status = "healthy";
|
|
175
|
+
health.consecutiveFailures = 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, RECOVERY_CHECK_MS);
|
|
179
|
+
// Don't let the recovery timer prevent process exit
|
|
180
|
+
if (this.recoveryTimer && typeof this.recoveryTimer === "object" && "unref" in this.recoveryTimer) {
|
|
181
|
+
this.recoveryTimer.unref();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Stop the recovery loop (for clean shutdown / testing).
|
|
186
|
+
*/
|
|
187
|
+
stopRecoveryLoop() {
|
|
188
|
+
if (this.recoveryTimer) {
|
|
189
|
+
clearInterval(this.recoveryTimer);
|
|
190
|
+
this.recoveryTimer = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get current health status of all providers.
|
|
195
|
+
*/
|
|
196
|
+
getStatus() {
|
|
197
|
+
return Array.from(this.providers.values()).map((h) => ({ ...h }));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the failover audit log.
|
|
201
|
+
*/
|
|
202
|
+
getAuditLog() {
|
|
203
|
+
return [...this.auditLog];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// SINGLETON — default chain: anthropic → openai → gemini
|
|
208
|
+
// ============================================================================
|
|
209
|
+
export const providerFailover = new ProviderFailover(["anthropic", "openai", "gemini"]);
|
|
210
|
+
providerFailover.startRecoveryLoop();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-bucket rate limiter — Phase 7.2
|
|
3
|
+
*
|
|
4
|
+
* In-memory token buckets supporting per-user, per-IP, per-tool,
|
|
5
|
+
* and cost-based rate limiting. No external dependencies (Redis etc.)
|
|
6
|
+
* required — designed for single-server deployment.
|
|
7
|
+
*
|
|
8
|
+
* Supplements (does not replace) the existing Supabase RPC rate check.
|
|
9
|
+
*/
|
|
10
|
+
export interface BucketConfig {
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
refillRate: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RateLimitResult {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
remaining: number;
|
|
17
|
+
retryAfterMs: number;
|
|
18
|
+
bucket: string;
|
|
19
|
+
}
|
|
20
|
+
export type RateLimitTier = "unauthenticated" | "authenticated" | "serviceRole";
|
|
21
|
+
export declare class RateLimiter {
|
|
22
|
+
private buckets;
|
|
23
|
+
private cleanupInterval;
|
|
24
|
+
private readonly TIERS;
|
|
25
|
+
private readonly COST_BUDGET;
|
|
26
|
+
private readonly TOOL_LIMITS;
|
|
27
|
+
private readonly STALE_MS;
|
|
28
|
+
constructor();
|
|
29
|
+
checkRequest(key: string, tier: RateLimitTier): RateLimitResult;
|
|
30
|
+
checkCostBudget(userId: string, costCents: number): RateLimitResult;
|
|
31
|
+
checkToolLimit(userId: string, toolName: string): RateLimitResult;
|
|
32
|
+
getStats(): {
|
|
33
|
+
totalBuckets: number;
|
|
34
|
+
};
|
|
35
|
+
shutdown(): void;
|
|
36
|
+
private getBucket;
|
|
37
|
+
private cleanup;
|
|
38
|
+
}
|
|
39
|
+
export declare const rateLimiter: RateLimiter;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-bucket rate limiter — Phase 7.2
|
|
3
|
+
*
|
|
4
|
+
* In-memory token buckets supporting per-user, per-IP, per-tool,
|
|
5
|
+
* and cost-based rate limiting. No external dependencies (Redis etc.)
|
|
6
|
+
* required — designed for single-server deployment.
|
|
7
|
+
*
|
|
8
|
+
* Supplements (does not replace) the existing Supabase RPC rate check.
|
|
9
|
+
*/
|
|
10
|
+
import { createLogger } from "./logger.js";
|
|
11
|
+
const log = createLogger("rate-limiter");
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// TOKEN BUCKET
|
|
14
|
+
// ============================================================================
|
|
15
|
+
class TokenBucket {
|
|
16
|
+
config;
|
|
17
|
+
tokens;
|
|
18
|
+
lastRefill;
|
|
19
|
+
lastAccess;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.tokens = config.maxTokens;
|
|
23
|
+
this.lastRefill = Date.now();
|
|
24
|
+
this.lastAccess = Date.now();
|
|
25
|
+
}
|
|
26
|
+
tryConsume(cost = 1) {
|
|
27
|
+
this.refill();
|
|
28
|
+
this.lastAccess = Date.now();
|
|
29
|
+
if (this.tokens >= cost) {
|
|
30
|
+
this.tokens -= cost;
|
|
31
|
+
return { allowed: true, remaining: Math.floor(this.tokens), retryAfterMs: 0 };
|
|
32
|
+
}
|
|
33
|
+
const refillNeeded = cost - this.tokens;
|
|
34
|
+
const retryAfterMs = Math.ceil((refillNeeded / this.config.refillRate) * 1000);
|
|
35
|
+
return { allowed: false, remaining: 0, retryAfterMs };
|
|
36
|
+
}
|
|
37
|
+
refill() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
40
|
+
this.tokens = Math.min(this.config.maxTokens, this.tokens + elapsed * this.config.refillRate);
|
|
41
|
+
this.lastRefill = now;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// RATE LIMITER
|
|
46
|
+
// ============================================================================
|
|
47
|
+
export class RateLimiter {
|
|
48
|
+
buckets = new Map();
|
|
49
|
+
cleanupInterval;
|
|
50
|
+
// ---- Tier configs ----
|
|
51
|
+
TIERS = {
|
|
52
|
+
unauthenticated: { maxTokens: 5, refillRate: 5 / 60 }, // 5 req/min
|
|
53
|
+
authenticated: { maxTokens: 100, refillRate: 100 / 60 }, // 100 req/min
|
|
54
|
+
serviceRole: { maxTokens: 1000, refillRate: 1000 / 60 }, // 1000 req/min
|
|
55
|
+
};
|
|
56
|
+
// ---- Cost budget: $10/hour per user (tracked in cents) ----
|
|
57
|
+
COST_BUDGET = {
|
|
58
|
+
maxTokens: 1000, // 1000 cents = $10
|
|
59
|
+
refillRate: 1000 / 3600, // cents per second (~0.278 c/s)
|
|
60
|
+
};
|
|
61
|
+
// ---- Per-tool limits ----
|
|
62
|
+
TOOL_LIMITS = {
|
|
63
|
+
browser: { maxTokens: 10, refillRate: 10 / 60 }, // 10/min
|
|
64
|
+
kali_exec: { maxTokens: 20, refillRate: 20 / 60 }, // 20/min
|
|
65
|
+
kali: { maxTokens: 20, refillRate: 20 / 60 }, // alias
|
|
66
|
+
local_agent: { maxTokens: 30, refillRate: 30 / 60 }, // 30/min
|
|
67
|
+
};
|
|
68
|
+
// Stale bucket threshold (10 minutes)
|
|
69
|
+
STALE_MS = 600_000;
|
|
70
|
+
constructor() {
|
|
71
|
+
// Cleanup stale buckets every 10 minutes
|
|
72
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), this.STALE_MS);
|
|
73
|
+
this.cleanupInterval.unref();
|
|
74
|
+
}
|
|
75
|
+
// ------------------------------------------------------------------
|
|
76
|
+
// Public: check per-IP or per-user request rate
|
|
77
|
+
// ------------------------------------------------------------------
|
|
78
|
+
checkRequest(key, tier) {
|
|
79
|
+
const config = this.TIERS[tier];
|
|
80
|
+
const bucketKey = `req:${tier}:${key}`;
|
|
81
|
+
const bucket = this.getBucket(bucketKey, config);
|
|
82
|
+
const result = bucket.tryConsume(1);
|
|
83
|
+
return { ...result, bucket: bucketKey };
|
|
84
|
+
}
|
|
85
|
+
// ------------------------------------------------------------------
|
|
86
|
+
// Public: check cost budget for a user (cost in cents)
|
|
87
|
+
// ------------------------------------------------------------------
|
|
88
|
+
checkCostBudget(userId, costCents) {
|
|
89
|
+
const bucketKey = `cost:${userId}`;
|
|
90
|
+
const bucket = this.getBucket(bucketKey, this.COST_BUDGET);
|
|
91
|
+
const result = bucket.tryConsume(costCents);
|
|
92
|
+
return { ...result, bucket: bucketKey };
|
|
93
|
+
}
|
|
94
|
+
// ------------------------------------------------------------------
|
|
95
|
+
// Public: check per-tool rate limit
|
|
96
|
+
// ------------------------------------------------------------------
|
|
97
|
+
checkToolLimit(userId, toolName) {
|
|
98
|
+
const config = this.TOOL_LIMITS[toolName];
|
|
99
|
+
if (!config) {
|
|
100
|
+
// No specific limit for this tool — allow
|
|
101
|
+
return { allowed: true, remaining: -1, retryAfterMs: 0, bucket: "none" };
|
|
102
|
+
}
|
|
103
|
+
const bucketKey = `tool:${toolName}:${userId}`;
|
|
104
|
+
const bucket = this.getBucket(bucketKey, config);
|
|
105
|
+
const result = bucket.tryConsume(1);
|
|
106
|
+
return { ...result, bucket: bucketKey };
|
|
107
|
+
}
|
|
108
|
+
// ------------------------------------------------------------------
|
|
109
|
+
// Public: get stats for monitoring
|
|
110
|
+
// ------------------------------------------------------------------
|
|
111
|
+
getStats() {
|
|
112
|
+
return { totalBuckets: this.buckets.size };
|
|
113
|
+
}
|
|
114
|
+
// ------------------------------------------------------------------
|
|
115
|
+
// Public: teardown
|
|
116
|
+
// ------------------------------------------------------------------
|
|
117
|
+
shutdown() {
|
|
118
|
+
clearInterval(this.cleanupInterval);
|
|
119
|
+
this.buckets.clear();
|
|
120
|
+
}
|
|
121
|
+
// ------------------------------------------------------------------
|
|
122
|
+
// Private helpers
|
|
123
|
+
// ------------------------------------------------------------------
|
|
124
|
+
getBucket(key, config) {
|
|
125
|
+
let bucket = this.buckets.get(key);
|
|
126
|
+
if (!bucket) {
|
|
127
|
+
bucket = new TokenBucket(config);
|
|
128
|
+
this.buckets.set(key, bucket);
|
|
129
|
+
}
|
|
130
|
+
return bucket;
|
|
131
|
+
}
|
|
132
|
+
cleanup() {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
let removed = 0;
|
|
135
|
+
for (const [key, bucket] of this.buckets) {
|
|
136
|
+
if (now - bucket.lastAccess > this.STALE_MS) {
|
|
137
|
+
this.buckets.delete(key);
|
|
138
|
+
removed++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (removed > 0) {
|
|
142
|
+
log.debug({ removed, remaining: this.buckets.size }, "rate-limiter cleanup");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Singleton instance
|
|
147
|
+
export const rateLimiter = new RateLimiter();
|