@ebowwa/coder 0.7.64 → 0.7.66
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/dist/index.js +36233 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +377 -176
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry - Central registry for all LLM providers
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - PROVIDERS: All provider configurations
|
|
6
|
+
* - getProvider(): Get provider by name
|
|
7
|
+
* - getProviderForModel(): Detect provider from model name
|
|
8
|
+
* - resolveProvider(): Resolve provider with API key
|
|
9
|
+
* - RollingKeysManager integration for multi-key rotation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ProviderName,
|
|
14
|
+
ProviderConfig,
|
|
15
|
+
ProviderHealth,
|
|
16
|
+
ResolvedProvider,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
import { DEFAULT_ROUTING_CONFIG, BACKOFF_CONFIG } from "./types.js";
|
|
19
|
+
import { RollingKeyManager } from "@ebowwa/rolling-keys";
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Provider Configurations
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* All available provider configurations
|
|
27
|
+
*/
|
|
28
|
+
export const PROVIDERS: Record<ProviderName, ProviderConfig> = {
|
|
29
|
+
// ============================================================
|
|
30
|
+
// Z.AI / Zhipu (GLM Models)
|
|
31
|
+
// ============================================================
|
|
32
|
+
zhipu: {
|
|
33
|
+
name: "zhipu",
|
|
34
|
+
displayName: "Z.AI (GLM)",
|
|
35
|
+
endpoint: process.env.ZHIPU_BASE_URL || "https://api.z.ai/api/coding/paas/v4",
|
|
36
|
+
authHeader: "Authorization",
|
|
37
|
+
apiKeyEnv: ["Z_AI_API_KEY", "ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"],
|
|
38
|
+
format: "openai",
|
|
39
|
+
defaultModel: "GLM-4.7",
|
|
40
|
+
models: [
|
|
41
|
+
// GLM-5 (quota: 3x peak, 2x off-peak)
|
|
42
|
+
"GLM-5", "glm-5",
|
|
43
|
+
// GLM-4.x series (shared quota)
|
|
44
|
+
"GLM-4.7", "glm-4.7",
|
|
45
|
+
"GLM-4.6", "glm-4.6",
|
|
46
|
+
"GLM-4.5V", "glm-4.5v", // Vision variant
|
|
47
|
+
"GLM-4.5", "glm-4.5",
|
|
48
|
+
"GLM-4.5-Air", "glm-4.5-air",
|
|
49
|
+
],
|
|
50
|
+
supportsStreaming: true,
|
|
51
|
+
supportsToolCalling: true,
|
|
52
|
+
supportsVision: true,
|
|
53
|
+
supportsThinking: true,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// MiniMax (M2.5 Model)
|
|
58
|
+
// ============================================================
|
|
59
|
+
minimax: {
|
|
60
|
+
name: "minimax",
|
|
61
|
+
displayName: "MiniMax",
|
|
62
|
+
endpoint: process.env.MINIMAX_BASE_URL || "https://api.minimax.io/anthropic",
|
|
63
|
+
authHeader: "ANTHROPIC_AUTH_TOKEN", // Anthropic-compatible
|
|
64
|
+
apiKeyEnv: ["MINIMAX_API_KEY"],
|
|
65
|
+
format: "anthropic",
|
|
66
|
+
defaultModel: "MiniMax-M2.5",
|
|
67
|
+
models: ["MiniMax-M2.5", "minimax-m2.5"],
|
|
68
|
+
supportsStreaming: true,
|
|
69
|
+
supportsToolCalling: true,
|
|
70
|
+
supportsVision: true,
|
|
71
|
+
supportsThinking: false,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// OpenAI (Future)
|
|
76
|
+
// ============================================================
|
|
77
|
+
openai: {
|
|
78
|
+
name: "openai",
|
|
79
|
+
displayName: "OpenAI",
|
|
80
|
+
endpoint: process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
|
|
81
|
+
authHeader: "Authorization",
|
|
82
|
+
apiKeyEnv: ["OPENAI_API_KEY"],
|
|
83
|
+
format: "openai",
|
|
84
|
+
defaultModel: "gpt-4-turbo",
|
|
85
|
+
models: ["gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"],
|
|
86
|
+
supportsStreaming: true,
|
|
87
|
+
supportsToolCalling: true,
|
|
88
|
+
supportsVision: true,
|
|
89
|
+
supportsThinking: false,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ============================================================
|
|
93
|
+
// Anthropic (Stub - Not Implemented)
|
|
94
|
+
// ============================================================
|
|
95
|
+
anthropic: {
|
|
96
|
+
name: "anthropic",
|
|
97
|
+
displayName: "Anthropic (Not Implemented)",
|
|
98
|
+
endpoint: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
99
|
+
authHeader: "x-api-key",
|
|
100
|
+
apiKeyEnv: ["ANTHROPIC_API_KEY"],
|
|
101
|
+
format: "anthropic",
|
|
102
|
+
defaultModel: "claude-sonnet-4-6",
|
|
103
|
+
models: ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"],
|
|
104
|
+
supportsStreaming: true,
|
|
105
|
+
supportsToolCalling: true,
|
|
106
|
+
supportsVision: true,
|
|
107
|
+
supportsThinking: true,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ============================================================
|
|
112
|
+
// Provider Health Tracking
|
|
113
|
+
// ============================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Health status for each provider
|
|
117
|
+
*/
|
|
118
|
+
const providerHealth: Record<ProviderName, ProviderHealth> = {
|
|
119
|
+
zhipu: { name: "zhipu", healthy: true, failureCount: 0, avgLatencyMs: 0, totalRequests: 0 },
|
|
120
|
+
minimax: { name: "minimax", healthy: true, failureCount: 0, avgLatencyMs: 0, totalRequests: 0 },
|
|
121
|
+
openai: { name: "openai", healthy: true, failureCount: 0, avgLatencyMs: 0, totalRequests: 0 },
|
|
122
|
+
anthropic: { name: "anthropic", healthy: false, failureCount: 999, avgLatencyMs: 0, totalRequests: 0 },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// Rolling Keys Support (using @ebowwa/rolling-keys)
|
|
127
|
+
// ============================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Rolling key managers for each provider
|
|
131
|
+
*/
|
|
132
|
+
const keyManagers: Partial<Record<ProviderName, RollingKeyManager>> = {};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get or create rolling key manager for a provider
|
|
136
|
+
*/
|
|
137
|
+
function getKeyManager(provider: ProviderName): RollingKeyManager | null {
|
|
138
|
+
if (keyManagers[provider]) {
|
|
139
|
+
return keyManagers[provider]!;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const config = PROVIDERS[provider];
|
|
143
|
+
if (!config || config.apiKeyEnv.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Create manager with first env var as primary, optional plural for array
|
|
149
|
+
const manager = new RollingKeyManager({
|
|
150
|
+
keysEnvVar: config.apiKeyEnv[0] + "S", // e.g., Z_AI_API_KEYS
|
|
151
|
+
singleKeyEnvVar: config.apiKeyEnv[0], // e.g., Z_AI_API_KEY
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Only cache if it has keys
|
|
155
|
+
if (manager.getKeyCount() > 0) {
|
|
156
|
+
keyManagers[provider] = manager;
|
|
157
|
+
return manager;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// No keys configured for this provider
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get next API key using round-robin rotation with health tracking
|
|
169
|
+
*/
|
|
170
|
+
function getNextKey(provider: ProviderName): string | null {
|
|
171
|
+
const manager = getKeyManager(provider);
|
|
172
|
+
if (!manager) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = manager.getNextKey();
|
|
177
|
+
return result?.key ?? null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Record key success (for health tracking)
|
|
182
|
+
* Note: RollingKeyManager tracks success internally via key rotation
|
|
183
|
+
*/
|
|
184
|
+
function recordKeySuccess(_provider: ProviderName): void {
|
|
185
|
+
// Success is tracked implicitly through continued key usage
|
|
186
|
+
// RollingKeyManager handles health recovery automatically
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Record key failure (triggers backoff)
|
|
191
|
+
*/
|
|
192
|
+
function recordKeyFailure(provider: ProviderName, error?: unknown): void {
|
|
193
|
+
const manager = getKeyManager(provider);
|
|
194
|
+
if (manager && error) {
|
|
195
|
+
manager.handleError(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================
|
|
200
|
+
// Provider Resolution
|
|
201
|
+
// ============================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Model name to provider mapping
|
|
205
|
+
*/
|
|
206
|
+
const MODEL_TO_PROVIDER: Record<string, ProviderName> = {
|
|
207
|
+
// Zhipu / Z.AI models (coding plan - shared quota)
|
|
208
|
+
"glm-5": "zhipu", // 3x peak, 2x off-peak
|
|
209
|
+
"glm-4.7": "zhipu",
|
|
210
|
+
"glm-4.6": "zhipu",
|
|
211
|
+
"glm-4.5v": "zhipu", // Vision variant
|
|
212
|
+
"glm-4.5-air": "zhipu",
|
|
213
|
+
"glm-4.5": "zhipu",
|
|
214
|
+
"glm-4-plus": "zhipu",
|
|
215
|
+
|
|
216
|
+
// MiniMax models
|
|
217
|
+
"minimax-m2.5": "minimax",
|
|
218
|
+
"minimax-m2": "minimax",
|
|
219
|
+
"abab6.5-chat": "minimax",
|
|
220
|
+
|
|
221
|
+
// OpenAI models
|
|
222
|
+
"gpt-4": "openai",
|
|
223
|
+
"gpt-4-turbo": "openai",
|
|
224
|
+
"gpt-3.5-turbo": "openai",
|
|
225
|
+
"gpt-4o": "openai",
|
|
226
|
+
|
|
227
|
+
// Anthropic models (stub)
|
|
228
|
+
"claude-opus-4-6": "anthropic",
|
|
229
|
+
"claude-sonnet-4-6": "anthropic",
|
|
230
|
+
"claude-haiku-4-5": "anthropic",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get provider configuration by name
|
|
235
|
+
*/
|
|
236
|
+
export function getProvider(name: ProviderName): ProviderConfig | undefined {
|
|
237
|
+
return PROVIDERS[name];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Detect provider from model name
|
|
242
|
+
*/
|
|
243
|
+
export function getProviderForModel(model: string): ProviderName {
|
|
244
|
+
// Normalize model name
|
|
245
|
+
const normalizedModel = model.toLowerCase();
|
|
246
|
+
|
|
247
|
+
// Direct lookup
|
|
248
|
+
if (MODEL_TO_PROVIDER[normalizedModel]) {
|
|
249
|
+
return MODEL_TO_PROVIDER[normalizedModel];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Partial match
|
|
253
|
+
for (const [modelPrefix, provider] of Object.entries(MODEL_TO_PROVIDER)) {
|
|
254
|
+
if (normalizedModel.includes(modelPrefix) || modelPrefix.includes(normalizedModel)) {
|
|
255
|
+
return provider;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check provider model lists
|
|
260
|
+
for (const [providerName, config] of Object.entries(PROVIDERS)) {
|
|
261
|
+
if (config.models.some((m) => m.toLowerCase() === normalizedModel)) {
|
|
262
|
+
return providerName as ProviderName;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Default to zhipu
|
|
267
|
+
return "zhipu";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Resolve provider with API key and endpoint
|
|
272
|
+
*/
|
|
273
|
+
export function resolveProvider(
|
|
274
|
+
model: string,
|
|
275
|
+
preferredProvider?: ProviderName
|
|
276
|
+
): ResolvedProvider | null {
|
|
277
|
+
// Determine provider
|
|
278
|
+
const providerName = preferredProvider || getProviderForModel(model);
|
|
279
|
+
const config = PROVIDERS[providerName];
|
|
280
|
+
|
|
281
|
+
if (!config) {
|
|
282
|
+
console.error(`Unknown provider: ${providerName}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Get API key
|
|
287
|
+
const apiKey = getNextKey(providerName);
|
|
288
|
+
if (!apiKey) {
|
|
289
|
+
console.error(`No API key found for provider: ${providerName}`);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Build endpoint URL
|
|
294
|
+
let endpoint = config.endpoint;
|
|
295
|
+
if (config.format === "openai") {
|
|
296
|
+
endpoint = `${endpoint}/chat/completions`;
|
|
297
|
+
} else {
|
|
298
|
+
endpoint = `${endpoint}/v1/messages`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Resolve model name
|
|
302
|
+
const resolvedModel = config.models.includes(model)
|
|
303
|
+
? model
|
|
304
|
+
: config.defaultModel;
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
config,
|
|
308
|
+
apiKey,
|
|
309
|
+
endpoint,
|
|
310
|
+
model: resolvedModel,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if provider is healthy
|
|
316
|
+
*/
|
|
317
|
+
export function isProviderHealthy(provider: ProviderName): boolean {
|
|
318
|
+
const health = providerHealth[provider];
|
|
319
|
+
if (!health) return false;
|
|
320
|
+
|
|
321
|
+
// Check if in backoff period
|
|
322
|
+
if (health.backoffUntil && health.backoffUntil > Date.now()) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return health.healthy;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Record provider success
|
|
331
|
+
*/
|
|
332
|
+
export function recordProviderSuccess(provider: ProviderName, latencyMs: number): void {
|
|
333
|
+
const health = providerHealth[provider];
|
|
334
|
+
if (!health) return;
|
|
335
|
+
|
|
336
|
+
health.healthy = true;
|
|
337
|
+
health.lastSuccess = Date.now();
|
|
338
|
+
health.failureCount = 0;
|
|
339
|
+
health.backoffUntil = undefined;
|
|
340
|
+
health.totalRequests++;
|
|
341
|
+
|
|
342
|
+
// Update average latency
|
|
343
|
+
health.avgLatencyMs = Math.round(
|
|
344
|
+
(health.avgLatencyMs * (health.totalRequests - 1) + latencyMs) / health.totalRequests
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Record provider failure
|
|
350
|
+
*/
|
|
351
|
+
export function recordProviderFailure(provider: ProviderName): void {
|
|
352
|
+
const health = providerHealth[provider];
|
|
353
|
+
if (!health) return;
|
|
354
|
+
|
|
355
|
+
health.failureCount++;
|
|
356
|
+
health.lastFailure = Date.now();
|
|
357
|
+
health.totalRequests++;
|
|
358
|
+
|
|
359
|
+
// Apply backoff after threshold
|
|
360
|
+
if (health.failureCount >= BACKOFF_CONFIG.failureThreshold) {
|
|
361
|
+
health.healthy = false;
|
|
362
|
+
const backoffMs = Math.min(
|
|
363
|
+
BACKOFF_CONFIG.baseMs * Math.pow(2, health.failureCount - BACKOFF_CONFIG.failureThreshold),
|
|
364
|
+
BACKOFF_CONFIG.maxMs
|
|
365
|
+
);
|
|
366
|
+
health.backoffUntil = Date.now() + backoffMs;
|
|
367
|
+
console.warn(
|
|
368
|
+
`[Provider] ${provider} unhealthy after ${health.failureCount} failures, ` +
|
|
369
|
+
`backoff for ${Math.round(backoffMs / 1000)}s`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get health status for all providers
|
|
376
|
+
*/
|
|
377
|
+
export function getProviderHealth(): Record<ProviderName, ProviderHealth> {
|
|
378
|
+
return { ...providerHealth };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get list of healthy providers
|
|
383
|
+
*/
|
|
384
|
+
export function getHealthyProviders(): ProviderName[] {
|
|
385
|
+
return (Object.keys(PROVIDERS) as ProviderName[]).filter(isProviderHealthy);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get next healthy provider from fallback chain
|
|
390
|
+
*/
|
|
391
|
+
export function getNextHealthyProvider(
|
|
392
|
+
excludeProvider?: ProviderName
|
|
393
|
+
): ProviderName | null {
|
|
394
|
+
const chain = DEFAULT_ROUTING_CONFIG.fallbackChain;
|
|
395
|
+
|
|
396
|
+
for (const provider of chain) {
|
|
397
|
+
if (excludeProvider && provider === excludeProvider) continue;
|
|
398
|
+
if (isProviderHealthy(provider)) {
|
|
399
|
+
return provider;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============================================================
|
|
407
|
+
// Initialization
|
|
408
|
+
// ============================================================
|
|
409
|
+
|
|
410
|
+
// Key pools are loaded lazily via getKeyManager()
|
|
411
|
+
|
|
412
|
+
// Re-export types
|
|
413
|
+
export type {
|
|
414
|
+
ProviderName,
|
|
415
|
+
ProviderConfig,
|
|
416
|
+
ProviderHealth,
|
|
417
|
+
ResolvedProvider,
|
|
418
|
+
ProviderRoutingConfig,
|
|
419
|
+
} from "./types.js";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Types - Generic LLM Provider Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple providers:
|
|
5
|
+
* - zhipu (Z.AI / GLM models)
|
|
6
|
+
* - minimax (MiniMax M2.5)
|
|
7
|
+
* - openai (future)
|
|
8
|
+
* - anthropic (stub/commented only)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Provider Types
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Supported provider names
|
|
17
|
+
*/
|
|
18
|
+
export type ProviderName = "zhipu" | "minimax" | "openai" | "anthropic";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* API format type - determines request/response format
|
|
22
|
+
*/
|
|
23
|
+
export type APIFormat = "anthropic" | "openai";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider configuration
|
|
27
|
+
*/
|
|
28
|
+
export interface ProviderConfig {
|
|
29
|
+
/** Provider identifier */
|
|
30
|
+
name: ProviderName;
|
|
31
|
+
/** Human-readable display name */
|
|
32
|
+
displayName: string;
|
|
33
|
+
/** API endpoint base URL */
|
|
34
|
+
endpoint: string;
|
|
35
|
+
/** Authentication header name */
|
|
36
|
+
authHeader: string;
|
|
37
|
+
/** Environment variable names for API keys (checked in order) */
|
|
38
|
+
apiKeyEnv: string[];
|
|
39
|
+
/** API format (anthropic or openai compatible) */
|
|
40
|
+
format: APIFormat;
|
|
41
|
+
/** Default model for this provider */
|
|
42
|
+
defaultModel: string;
|
|
43
|
+
/** List of supported model IDs */
|
|
44
|
+
models: string[];
|
|
45
|
+
/** Whether provider supports streaming */
|
|
46
|
+
supportsStreaming: boolean;
|
|
47
|
+
/** Whether provider supports tool calling */
|
|
48
|
+
supportsToolCalling: boolean;
|
|
49
|
+
/** Whether provider supports vision/images */
|
|
50
|
+
supportsVision: boolean;
|
|
51
|
+
/** Whether provider supports extended thinking */
|
|
52
|
+
supportsThinking: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Provider health status
|
|
57
|
+
*/
|
|
58
|
+
export interface ProviderHealth {
|
|
59
|
+
/** Provider name */
|
|
60
|
+
name: ProviderName;
|
|
61
|
+
/** Whether provider is healthy */
|
|
62
|
+
healthy: boolean;
|
|
63
|
+
/** Last successful request timestamp */
|
|
64
|
+
lastSuccess?: number;
|
|
65
|
+
/** Last failure timestamp */
|
|
66
|
+
lastFailure?: number;
|
|
67
|
+
/** Consecutive failure count */
|
|
68
|
+
failureCount: number;
|
|
69
|
+
/** Backoff until timestamp (if in cooldown) */
|
|
70
|
+
backoffUntil?: number;
|
|
71
|
+
/** Average latency in ms */
|
|
72
|
+
avgLatencyMs: number;
|
|
73
|
+
/** Total requests made */
|
|
74
|
+
totalRequests: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Provider routing configuration
|
|
79
|
+
*/
|
|
80
|
+
export interface ProviderRoutingConfig {
|
|
81
|
+
/** Enable fallback between providers */
|
|
82
|
+
fallbackEnabled: boolean;
|
|
83
|
+
/** Fallback chain (order of providers to try) */
|
|
84
|
+
fallbackChain: ProviderName[];
|
|
85
|
+
/** Latency threshold in ms before trying next provider */
|
|
86
|
+
latencyThresholdMs: number;
|
|
87
|
+
/** Enable health tracking */
|
|
88
|
+
healthTracking: boolean;
|
|
89
|
+
/** Enable load balancing across healthy providers */
|
|
90
|
+
loadBalancing: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolved provider info for a request
|
|
95
|
+
*/
|
|
96
|
+
export interface ResolvedProvider {
|
|
97
|
+
/** Provider configuration */
|
|
98
|
+
config: ProviderConfig;
|
|
99
|
+
/** API key to use */
|
|
100
|
+
apiKey: string;
|
|
101
|
+
/** Full endpoint URL */
|
|
102
|
+
endpoint: string;
|
|
103
|
+
/** Model to use (may be mapped from requested model) */
|
|
104
|
+
model: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Default Configuration
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Default provider routing configuration
|
|
113
|
+
*/
|
|
114
|
+
export const DEFAULT_ROUTING_CONFIG: ProviderRoutingConfig = {
|
|
115
|
+
fallbackEnabled: true,
|
|
116
|
+
fallbackChain: ["zhipu", "minimax"],
|
|
117
|
+
latencyThresholdMs: 30000, // 30 seconds
|
|
118
|
+
healthTracking: true,
|
|
119
|
+
loadBalancing: false, // Disabled by default - use primary provider
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Backoff configuration for health tracking
|
|
124
|
+
*/
|
|
125
|
+
export const BACKOFF_CONFIG = {
|
|
126
|
+
/** Base backoff in ms */
|
|
127
|
+
baseMs: 60000, // 1 minute
|
|
128
|
+
/** Maximum backoff in ms */
|
|
129
|
+
maxMs: 3600000, // 1 hour
|
|
130
|
+
/** Number of failures before backoff */
|
|
131
|
+
failureThreshold: 3,
|
|
132
|
+
} as const;
|
|
@@ -69,6 +69,16 @@ function isRetryableError(
|
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Check for transient API errors (empty/malformed responses)
|
|
73
|
+
if (error.message.includes("No message received") ||
|
|
74
|
+
error.message.includes("empty response") ||
|
|
75
|
+
error.message.includes("invalid response") ||
|
|
76
|
+
error.message.includes("unexpected EOF") ||
|
|
77
|
+
error.message.includes("connection reset") ||
|
|
78
|
+
error.message.includes("aborted")) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
// Check for specific status codes in error message
|
|
73
83
|
for (const code of retryableStatusCodes) {
|
|
74
84
|
if (error.message.includes(String(code))) {
|