wraptc 1.0.2 → 1.0.4

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 (71) hide show
  1. package/bin/wraptc +4 -4
  2. package/package.json +2 -2
  3. package/src/cli/__tests__/cli.test.ts +337 -0
  4. package/src/cli/index.ts +149 -0
  5. package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
  6. package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
  7. package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
  8. package/src/core/__tests__/integration/integration.test.ts +241 -0
  9. package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
  10. package/src/core/__tests__/test-utils.ts +136 -0
  11. package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
  12. package/src/core/__tests__/unit/basic-test.test.ts +44 -0
  13. package/src/core/__tests__/unit/basic.test.ts +12 -0
  14. package/src/core/__tests__/unit/config.test.ts +244 -0
  15. package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
  16. package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
  17. package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
  18. package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
  19. package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
  20. package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
  21. package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
  22. package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
  23. package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
  24. package/src/core/__tests__/unit/router.test.ts +967 -0
  25. package/src/core/__tests__/unit/state.test.ts +1079 -0
  26. package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
  27. package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
  28. package/src/core/adapters/builtin/codex.ts +35 -0
  29. package/src/core/adapters/builtin/gemini.ts +34 -0
  30. package/src/core/adapters/builtin/index.ts +31 -0
  31. package/src/core/adapters/builtin/mock-coder.ts +148 -0
  32. package/src/core/adapters/builtin/qwen.ts +34 -0
  33. package/src/core/adapters/define.ts +48 -0
  34. package/src/core/adapters/index.ts +43 -0
  35. package/src/core/adapters/loader.ts +143 -0
  36. package/src/core/adapters/provider-bridge.ts +190 -0
  37. package/src/core/adapters/runner.ts +437 -0
  38. package/src/core/adapters/types.ts +172 -0
  39. package/src/core/config.ts +290 -0
  40. package/src/core/define-provider.ts +212 -0
  41. package/src/core/error-patterns.ts +147 -0
  42. package/src/core/index.ts +130 -0
  43. package/src/core/memory-monitor.ts +171 -0
  44. package/src/core/plugin/builtin.ts +87 -0
  45. package/src/core/plugin/index.ts +34 -0
  46. package/src/core/plugin/registry.ts +350 -0
  47. package/src/core/plugin/types.ts +209 -0
  48. package/src/core/provider-factory.ts +397 -0
  49. package/src/core/provider-loader.ts +171 -0
  50. package/src/core/providers/codex.ts +56 -0
  51. package/src/core/providers/configurable.ts +637 -0
  52. package/src/core/providers/custom.ts +261 -0
  53. package/src/core/providers/gemini.ts +41 -0
  54. package/src/core/providers/index.ts +383 -0
  55. package/src/core/providers/opencode.ts +168 -0
  56. package/src/core/providers/qwen-code.ts +41 -0
  57. package/src/core/router.ts +370 -0
  58. package/src/core/state.ts +258 -0
  59. package/src/core/types.ts +206 -0
  60. package/src/core/unified/capabilities.ts +184 -0
  61. package/src/core/unified/errors.ts +141 -0
  62. package/src/core/unified/index.ts +29 -0
  63. package/src/core/unified/output.ts +189 -0
  64. package/src/core/wrap-terminalcoder.ts +245 -0
  65. package/src/mcp/__tests__/server.test.ts +295 -0
  66. package/src/mcp/server.ts +284 -0
  67. package/src/test-fixtures/mock-coder.sh +194 -0
  68. package/dist/cli/index.js +0 -16501
  69. package/dist/core/index.js +0 -7531
  70. package/dist/mcp/server.js +0 -14568
  71. package/dist/wraptc-1.0.2.tgz +0 -0
@@ -0,0 +1,168 @@
1
+ import { createErrorClassifier } from "../error-patterns";
2
+ /**
3
+ * OpenCodeProvider - Provider for OpenCode agent delegation
4
+ *
5
+ * Uses 'opencode agent run' command with JSON output.
6
+ * Provider ID: 'opencode' (also aliased as 'delegate' for compatibility)
7
+ *
8
+ * Key features:
9
+ * - Runs via `opencode agent run <prompt> -f json -q`
10
+ * - Supports file context via prompt injection
11
+ * - JSON output parsing with summary extraction
12
+ * - Files changed detection from output
13
+ */
14
+ import type {
15
+ CodingRequest,
16
+ ProviderConfig,
17
+ ProviderErrorContext,
18
+ ProviderErrorKind,
19
+ ProviderInvokeOptions,
20
+ TokenUsage,
21
+ } from "../types";
22
+ import { ProcessProvider } from "./index";
23
+
24
+ // OpenCode-specific error patterns (extend defaults)
25
+ const OPENCODE_SPECIFIC_PATTERNS: Partial<Record<ProviderErrorKind, string[]>> = {
26
+ RATE_LIMIT: ["api limit exceeded"],
27
+ };
28
+
29
+ // Create classifier with OpenCode-specific patterns merged with defaults
30
+ const classifyOpenCodeError = createErrorClassifier(OPENCODE_SPECIFIC_PATTERNS);
31
+
32
+ export class OpenCodeProvider extends ProcessProvider {
33
+ constructor(config: ProviderConfig) {
34
+ super("opencode", "OpenCode Agent", {
35
+ ...config,
36
+ binary: config.binary || "opencode",
37
+ // OpenCode uses -f json for JSON output
38
+ jsonMode: "flag",
39
+ jsonFlag: "-f",
40
+ streamingMode: "none", // OpenCode doesn't support streaming
41
+ capabilities: config.capabilities || ["generate", "edit", "explain", "test", "refactor"],
42
+ });
43
+ }
44
+
45
+ /**
46
+ * OpenCode uses positional prompt with args, not stdin
47
+ */
48
+ protected getStdinInput(): string | undefined {
49
+ return undefined;
50
+ }
51
+
52
+ /**
53
+ * Build args: opencode agent run <prompt> -f json -q
54
+ */
55
+ protected buildArgs(req: CodingRequest, _opts: ProviderInvokeOptions): string[] {
56
+ const args: string[] = ["agent", "run"];
57
+
58
+ // Build prompt with file context if provided
59
+ let prompt = req.prompt;
60
+ if (req.files && req.files.length > 0) {
61
+ const fileList = req.files.map((f) => (typeof f === "string" ? f : f.path)).join("\n");
62
+ prompt = `${prompt}\n\nFiles to consider:\n${fileList}`;
63
+ }
64
+
65
+ args.push(prompt);
66
+
67
+ // Add JSON output flag and quiet mode
68
+ args.push("-f", "json", "-q");
69
+
70
+ // Add any additional args from config
71
+ args.push(...this.config.args);
72
+
73
+ return args;
74
+ }
75
+
76
+ /**
77
+ * Parse OpenCode JSON output to extract text and usage
78
+ */
79
+ protected parseOutput(stdout: string): { text: string; usage?: TokenUsage } {
80
+ try {
81
+ const output = JSON.parse(stdout);
82
+
83
+ // Extract text from various possible fields
84
+ const text =
85
+ output.summary ||
86
+ output.response ||
87
+ output.message ||
88
+ output.output ||
89
+ output.text ||
90
+ this.extractSummaryFromText(stdout);
91
+
92
+ return {
93
+ text,
94
+ usage: output.usage,
95
+ };
96
+ } catch {
97
+ // Not JSON, extract summary from plain text
98
+ return {
99
+ text: this.extractSummaryFromText(stdout),
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Extract a summary from plain text output
106
+ */
107
+ private extractSummaryFromText(stdout: string): string {
108
+ const lines = stdout.split("\n").filter((l) => l.trim());
109
+
110
+ // Look for summary-like patterns
111
+ for (const line of lines) {
112
+ if (line.match(/completed|fixed|added|updated|created|implemented/i)) {
113
+ return line.substring(0, 500);
114
+ }
115
+ }
116
+
117
+ // Fallback: first non-empty line or truncated output
118
+ return lines[0]?.substring(0, 500) || stdout.substring(0, 500);
119
+ }
120
+
121
+ /**
122
+ * Classify errors with OpenCode-specific patterns merged with defaults
123
+ */
124
+ classifyError(error: ProviderErrorContext): ProviderErrorKind {
125
+ return classifyOpenCodeError(error.stderr, error.stdout, error.exitCode);
126
+ }
127
+
128
+ /**
129
+ * Extract files changed from OpenCode output (for result enrichment)
130
+ */
131
+ extractFilesChanged(stdout: string): string[] {
132
+ const files: string[] = [];
133
+
134
+ const filePatterns = [
135
+ /(?:modified|changed|updated|created|edited):\s*([^\s]+)/gi,
136
+ /File:\s*([^\s]+)/gi,
137
+ /→\s*([^\s]+\.[a-z]{2,4})/gi,
138
+ /```[\w]*\s*([^\s]+)/gi,
139
+ /\[.*?\]\(([^)]+\.[a-z]{2,4})\)/gi,
140
+ ];
141
+
142
+ for (const pattern of filePatterns) {
143
+ const matches = stdout.matchAll(pattern);
144
+ for (const match of matches) {
145
+ if (match[1]) {
146
+ files.push(match[1]);
147
+ }
148
+ }
149
+ }
150
+
151
+ return [...new Set(files)];
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Create an OpenCode provider with default or custom config
157
+ */
158
+ export function createOpenCodeProvider(config?: Partial<ProviderConfig>): OpenCodeProvider {
159
+ return new OpenCodeProvider({
160
+ binary: "opencode",
161
+ args: [],
162
+ jsonMode: "flag",
163
+ jsonFlag: "-f",
164
+ streamingMode: "none",
165
+ capabilities: ["generate", "edit", "explain", "test", "refactor"],
166
+ ...config,
167
+ });
168
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * QwenCodeProvider - Simplified provider using ProcessProvider defaults
3
+ *
4
+ * Qwen Code CLI uses:
5
+ * - Positional prompt: qwen [options] <prompt>
6
+ * - JSON output flag: -o json
7
+ */
8
+ import type { CodingRequest, ProviderConfig, ProviderInvokeOptions } from "../types";
9
+ import { ProcessProvider } from "./index";
10
+
11
+ export class QwenCodeProvider extends ProcessProvider {
12
+ constructor(config: ProviderConfig) {
13
+ super("qwen-code", "Qwen Code CLI", {
14
+ ...config,
15
+ binary: config.binary || "qwen",
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Qwen uses positional prompt (not stdin), so override getStdinInput
21
+ */
22
+ protected getStdinInput(): string | undefined {
23
+ return undefined; // Qwen uses positional prompt
24
+ }
25
+
26
+ /**
27
+ * Build args: qwen -o json [...args] <prompt>
28
+ */
29
+ protected buildArgs(req: CodingRequest, _opts: ProviderInvokeOptions): string[] {
30
+ const args: string[] = [];
31
+
32
+ if (this.config.jsonMode === "flag") {
33
+ args.push("-o", "json");
34
+ }
35
+
36
+ args.push(...this.config.args);
37
+ args.push(req.prompt);
38
+
39
+ return args;
40
+ }
41
+ }
@@ -0,0 +1,370 @@
1
+ import type { Provider } from "./providers/index";
2
+ import type { StateManager } from "./state";
3
+ import type {
4
+ CodingRequest,
5
+ CodingResponse,
6
+ ProviderErrorContext,
7
+ ProviderErrorKind,
8
+ } from "./types";
9
+ import type { Config } from "./types";
10
+
11
+ export interface RouterOptions {
12
+ config: Config;
13
+ stateManager: StateManager;
14
+ }
15
+
16
+ /**
17
+ * Extended Map interface that supports async provider loading
18
+ */
19
+ interface AsyncProviderMap extends Map<string, Provider> {
20
+ getAsync?(key: string): Promise<Provider | null>;
21
+ }
22
+
23
+ export class Router {
24
+ private providers: AsyncProviderMap;
25
+ private config: Config;
26
+ private stateManager: StateManager;
27
+
28
+ constructor(providers: AsyncProviderMap, options: RouterOptions) {
29
+ this.providers = providers;
30
+ this.config = options.config;
31
+ this.stateManager = options.stateManager;
32
+ }
33
+
34
+ /**
35
+ * Get a provider, supporting both sync and async loading
36
+ */
37
+ private async getProvider(id: string): Promise<Provider | null> {
38
+ // Try async loading first (for lazy-loaded providers)
39
+ if (this.providers.getAsync) {
40
+ return this.providers.getAsync(id);
41
+ }
42
+ // Fall back to sync get
43
+ return this.providers.get(id) || null;
44
+ }
45
+
46
+ async route(
47
+ req: CodingRequest,
48
+ opts: { signal?: AbortSignal; cwd?: string } = {},
49
+ ): Promise<CodingResponse> {
50
+ const startTime = Date.now();
51
+
52
+ // Build candidate list
53
+ const candidates = this.getCandidateProviders(req);
54
+ if (candidates.length === 0) {
55
+ throw new Error(
56
+ "No providers configured. Add providers to your config or check your routing.defaultOrder setting.",
57
+ );
58
+ }
59
+
60
+ // Try each provider in order
61
+ const errors: Array<{ provider: string; error: Error; kind: ProviderErrorKind }> = [];
62
+
63
+ for (const providerId of candidates) {
64
+ const provider = await this.getProvider(providerId);
65
+ if (!provider) {
66
+ errors.push({
67
+ provider: providerId,
68
+ error: new Error(`Provider ${providerId} not found or unavailable`),
69
+ kind: "NOT_FOUND",
70
+ });
71
+ continue;
72
+ }
73
+
74
+ // Check if provider is marked as out of credits
75
+ const state = await this.stateManager.getProviderState(providerId);
76
+ if (state.outOfCreditsUntil && new Date(state.outOfCreditsUntil) > new Date()) {
77
+ errors.push({
78
+ provider: providerId,
79
+ error: new Error(
80
+ `Provider ${providerId} is out of credits until ${state.outOfCreditsUntil}`,
81
+ ),
82
+ kind: "OUT_OF_CREDITS",
83
+ });
84
+ continue;
85
+ }
86
+
87
+ // Check daily limit
88
+ const creditConfig = this.config.credits.providers[providerId];
89
+ if (
90
+ creditConfig &&
91
+ "dailyRequestLimit" in creditConfig &&
92
+ state.requestsToday >= creditConfig.dailyRequestLimit
93
+ ) {
94
+ // Mark as out of credits for rest of day
95
+ const tomorrow = new Date();
96
+ tomorrow.setDate(tomorrow.getDate() + 1);
97
+ tomorrow.setHours(0, 0, 0, 0);
98
+ await this.stateManager.markOutOfCredits(providerId, tomorrow);
99
+
100
+ errors.push({
101
+ provider: providerId,
102
+ error: new Error(`Daily request limit exceeded for ${providerId}`),
103
+ kind: "OUT_OF_CREDITS",
104
+ });
105
+ continue;
106
+ }
107
+
108
+ try {
109
+ // Execute request
110
+ const result = await provider.runOnce(req, opts);
111
+
112
+ // Update state on success, including token usage
113
+ const tokensSaved = result.usage?.totalTokens ?? result.usage?.outputTokens ?? 0;
114
+ await this.stateManager.recordSuccess(providerId, tokensSaved);
115
+
116
+ return {
117
+ provider: providerId,
118
+ text: result.text,
119
+ usage: result.usage,
120
+ meta: {
121
+ elapsedMs: Date.now() - startTime,
122
+ },
123
+ };
124
+ } catch (error) {
125
+ const err = error as Error;
126
+ const context: ProviderErrorContext = {
127
+ exitCode: "exitCode" in err ? (err as any).exitCode : undefined,
128
+ stderr: err.message,
129
+ stdout: "stdout" in err ? (err as any).stdout : undefined,
130
+ };
131
+
132
+ const errorKind = provider.classifyError(context);
133
+
134
+ // Update state based on error
135
+ await this.stateManager.recordError(providerId, errorKind, err.message);
136
+
137
+ // For OUT_OF_CREDITS or RATE_LIMIT, mark provider as unavailable for cooldown
138
+ if (errorKind === "OUT_OF_CREDITS" || errorKind === "RATE_LIMIT") {
139
+ const cooldownTime = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
140
+ await this.stateManager.markOutOfCredits(providerId, cooldownTime);
141
+ }
142
+
143
+ // Errors that should fail immediately (user needs to fix request/config)
144
+ if (errorKind === "BAD_REQUEST") {
145
+ throw new Error(
146
+ `Bad request to ${providerId}: ${err.message}. Check your prompt and options.`,
147
+ );
148
+ }
149
+ if (errorKind === "UNAUTHORIZED") {
150
+ throw new Error(
151
+ `Unauthorized for ${providerId}: ${err.message}. Check your API key or authentication.`,
152
+ );
153
+ }
154
+ if (errorKind === "FORBIDDEN") {
155
+ throw new Error(
156
+ `Access denied for ${providerId}: ${err.message}. Check your permissions or subscription.`,
157
+ );
158
+ }
159
+ if (errorKind === "CONTEXT_LENGTH") {
160
+ throw new Error(
161
+ `Context too long for ${providerId}: ${err.message}. Try a shorter prompt or fewer files.`,
162
+ );
163
+ }
164
+ if (errorKind === "CONTENT_FILTER") {
165
+ throw new Error(
166
+ `Content filtered by ${providerId}: ${err.message}. Rephrase your prompt to avoid policy violations.`,
167
+ );
168
+ }
169
+
170
+ // For other errors, log and continue to next provider
171
+ errors.push({ provider: providerId, error: err, kind: errorKind });
172
+ }
173
+ }
174
+
175
+ // All providers failed
176
+ const errorSummary = errors
177
+ .map((e) => `${e.provider} (${e.kind}): ${e.error.message}`)
178
+ .join("; ");
179
+ throw new Error(
180
+ `All ${errors.length} providers failed. Tried: ${candidates.join(", ")}. Details: ${errorSummary}`,
181
+ );
182
+ }
183
+
184
+ async *routeStream(
185
+ req: CodingRequest,
186
+ opts: { signal?: AbortSignal; cwd?: string } = {},
187
+ ): AsyncGenerator<CodingResponse> {
188
+ const startTime = Date.now();
189
+
190
+ // Build candidate list
191
+ const candidates = this.getCandidateProviders(req);
192
+ if (candidates.length === 0) {
193
+ throw new Error(
194
+ "No providers configured. Add providers to your config or check your routing.defaultOrder setting.",
195
+ );
196
+ }
197
+
198
+ // Try each provider in order
199
+ const errors: Array<{ provider: string; error: Error; kind: ProviderErrorKind }> = [];
200
+
201
+ for (const providerId of candidates) {
202
+ const provider = await this.getProvider(providerId);
203
+ if (!provider) {
204
+ errors.push({
205
+ provider: providerId,
206
+ error: new Error(`Provider ${providerId} not found or unavailable`),
207
+ kind: "NOT_FOUND",
208
+ });
209
+ continue;
210
+ }
211
+
212
+ // Check if provider is marked as out of credits
213
+ const state = await this.stateManager.getProviderState(providerId);
214
+ if (state.outOfCreditsUntil && new Date(state.outOfCreditsUntil) > new Date()) {
215
+ errors.push({
216
+ provider: providerId,
217
+ error: new Error(
218
+ `Provider ${providerId} is out of credits until ${state.outOfCreditsUntil}`,
219
+ ),
220
+ kind: "OUT_OF_CREDITS",
221
+ });
222
+ continue;
223
+ }
224
+
225
+ // Check daily limit
226
+ const creditConfig = this.config.credits.providers[providerId];
227
+ if (
228
+ creditConfig &&
229
+ "dailyRequestLimit" in creditConfig &&
230
+ state.requestsToday >= creditConfig.dailyRequestLimit
231
+ ) {
232
+ const tomorrow = new Date();
233
+ tomorrow.setDate(tomorrow.getDate() + 1);
234
+ tomorrow.setHours(0, 0, 0, 0);
235
+ await this.stateManager.markOutOfCredits(providerId, tomorrow);
236
+
237
+ errors.push({
238
+ provider: providerId,
239
+ error: new Error(`Daily request limit exceeded for ${providerId}`),
240
+ kind: "OUT_OF_CREDITS",
241
+ });
242
+ continue;
243
+ }
244
+
245
+ try {
246
+ const fullText = "";
247
+
248
+ // Execute streaming request
249
+ for await (const event of provider.runStream(req, opts)) {
250
+ if (event.type === "complete") {
251
+ // Record success with token usage
252
+ const tokensSaved = event.usage?.totalTokens ?? event.usage?.outputTokens ?? 0;
253
+ await this.stateManager.recordSuccess(providerId, tokensSaved);
254
+ yield {
255
+ provider: providerId,
256
+ text: event.text,
257
+ usage: event.usage,
258
+ meta: {
259
+ elapsedMs: Date.now() - startTime,
260
+ },
261
+ };
262
+ return; // Success, exit generator
263
+ }
264
+ // Pass through events (could be extended to yield events too)
265
+ }
266
+ } catch (error) {
267
+ const err = error as Error;
268
+ const context: ProviderErrorContext = {
269
+ exitCode: "exitCode" in err ? (err as any).exitCode : undefined,
270
+ stderr: err.message,
271
+ stdout: "stdout" in err ? (err as any).stdout : undefined,
272
+ };
273
+
274
+ const errorKind = provider.classifyError(context);
275
+ await this.stateManager.recordError(providerId, errorKind, err.message);
276
+
277
+ if (errorKind === "OUT_OF_CREDITS" || errorKind === "RATE_LIMIT") {
278
+ const cooldownTime = new Date(Date.now() + 60 * 60 * 1000);
279
+ await this.stateManager.markOutOfCredits(providerId, cooldownTime);
280
+ }
281
+
282
+ // Errors that should fail immediately (user needs to fix request/config)
283
+ if (errorKind === "BAD_REQUEST") {
284
+ throw new Error(
285
+ `Bad request to ${providerId}: ${err.message}. Check your prompt and options.`,
286
+ );
287
+ }
288
+ if (errorKind === "UNAUTHORIZED") {
289
+ throw new Error(
290
+ `Unauthorized for ${providerId}: ${err.message}. Check your API key or authentication.`,
291
+ );
292
+ }
293
+ if (errorKind === "FORBIDDEN") {
294
+ throw new Error(
295
+ `Access denied for ${providerId}: ${err.message}. Check your permissions or subscription.`,
296
+ );
297
+ }
298
+ if (errorKind === "CONTEXT_LENGTH") {
299
+ throw new Error(
300
+ `Context too long for ${providerId}: ${err.message}. Try a shorter prompt or fewer files.`,
301
+ );
302
+ }
303
+ if (errorKind === "CONTENT_FILTER") {
304
+ throw new Error(
305
+ `Content filtered by ${providerId}: ${err.message}. Rephrase your prompt to avoid policy violations.`,
306
+ );
307
+ }
308
+
309
+ errors.push({ provider: providerId, error: err, kind: errorKind });
310
+ }
311
+ }
312
+
313
+ // All providers failed
314
+ const errorSummary = errors
315
+ .map((e) => `${e.provider} (${e.kind}): ${e.error.message}`)
316
+ .join("; ");
317
+ throw new Error(
318
+ `All ${errors.length} providers failed. Tried: ${candidates.join(", ")}. Details: ${errorSummary}`,
319
+ );
320
+ }
321
+
322
+ private getCandidateProviders(req: CodingRequest): string[] {
323
+ // If provider is explicitly specified, use only that provider
324
+ if (req.provider) {
325
+ return [req.provider];
326
+ }
327
+
328
+ // Check for per-mode override
329
+ const mode = req.mode || "generate";
330
+ if (this.config.routing.perModeOverride?.[mode]) {
331
+ return this.config.routing.perModeOverride[mode];
332
+ }
333
+
334
+ // Use default order
335
+ return this.config.routing.defaultOrder;
336
+ }
337
+
338
+ async getProviderInfo(): Promise<
339
+ Array<{
340
+ id: string;
341
+ displayName: string;
342
+ outOfCreditsUntil?: Date;
343
+ requestsToday: number;
344
+ available: boolean;
345
+ }>
346
+ > {
347
+ const info: Array<{
348
+ id: string;
349
+ displayName: string;
350
+ outOfCreditsUntil?: Date;
351
+ requestsToday: number;
352
+ available: boolean;
353
+ }> = [];
354
+
355
+ for (const providerId of this.providers.keys()) {
356
+ const provider = await this.getProvider(providerId);
357
+ const state = await this.stateManager.getProviderState(providerId);
358
+
359
+ info.push({
360
+ id: providerId,
361
+ displayName: provider?.displayName || providerId,
362
+ outOfCreditsUntil: state.outOfCreditsUntil ? new Date(state.outOfCreditsUntil) : undefined,
363
+ requestsToday: state.requestsToday,
364
+ available: !!provider,
365
+ });
366
+ }
367
+
368
+ return info;
369
+ }
370
+ }