@ebowwa/coder 0.7.64 → 0.7.65

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.
Files changed (101) hide show
  1. package/dist/index.js +36168 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +283 -173
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. 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))) {