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.
- package/bin/wraptc +4 -4
- package/package.json +2 -2
- package/src/cli/__tests__/cli.test.ts +337 -0
- package/src/cli/index.ts +149 -0
- package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
- package/src/core/__tests__/integration/integration.test.ts +241 -0
- package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
- package/src/core/__tests__/test-utils.ts +136 -0
- package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
- package/src/core/__tests__/unit/basic-test.test.ts +44 -0
- package/src/core/__tests__/unit/basic.test.ts +12 -0
- package/src/core/__tests__/unit/config.test.ts +244 -0
- package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
- package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
- package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
- package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
- package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
- package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
- package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
- package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
- package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
- package/src/core/__tests__/unit/router.test.ts +967 -0
- package/src/core/__tests__/unit/state.test.ts +1079 -0
- package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
- package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
- package/src/core/adapters/builtin/codex.ts +35 -0
- package/src/core/adapters/builtin/gemini.ts +34 -0
- package/src/core/adapters/builtin/index.ts +31 -0
- package/src/core/adapters/builtin/mock-coder.ts +148 -0
- package/src/core/adapters/builtin/qwen.ts +34 -0
- package/src/core/adapters/define.ts +48 -0
- package/src/core/adapters/index.ts +43 -0
- package/src/core/adapters/loader.ts +143 -0
- package/src/core/adapters/provider-bridge.ts +190 -0
- package/src/core/adapters/runner.ts +437 -0
- package/src/core/adapters/types.ts +172 -0
- package/src/core/config.ts +290 -0
- package/src/core/define-provider.ts +212 -0
- package/src/core/error-patterns.ts +147 -0
- package/src/core/index.ts +130 -0
- package/src/core/memory-monitor.ts +171 -0
- package/src/core/plugin/builtin.ts +87 -0
- package/src/core/plugin/index.ts +34 -0
- package/src/core/plugin/registry.ts +350 -0
- package/src/core/plugin/types.ts +209 -0
- package/src/core/provider-factory.ts +397 -0
- package/src/core/provider-loader.ts +171 -0
- package/src/core/providers/codex.ts +56 -0
- package/src/core/providers/configurable.ts +637 -0
- package/src/core/providers/custom.ts +261 -0
- package/src/core/providers/gemini.ts +41 -0
- package/src/core/providers/index.ts +383 -0
- package/src/core/providers/opencode.ts +168 -0
- package/src/core/providers/qwen-code.ts +41 -0
- package/src/core/router.ts +370 -0
- package/src/core/state.ts +258 -0
- package/src/core/types.ts +206 -0
- package/src/core/unified/capabilities.ts +184 -0
- package/src/core/unified/errors.ts +141 -0
- package/src/core/unified/index.ts +29 -0
- package/src/core/unified/output.ts +189 -0
- package/src/core/wrap-terminalcoder.ts +245 -0
- package/src/mcp/__tests__/server.test.ts +295 -0
- package/src/mcp/server.ts +284 -0
- package/src/test-fixtures/mock-coder.sh +194 -0
- package/dist/cli/index.js +0 -16501
- package/dist/core/index.js +0 -7531
- package/dist/mcp/server.js +0 -14568
- package/dist/wraptc-1.0.2.tgz +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Error Types
|
|
3
|
+
*
|
|
4
|
+
* Provides structured error handling with categorization and suggestions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ProviderErrorContext, ProviderErrorKind } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Enhanced provider error interface
|
|
11
|
+
*/
|
|
12
|
+
export interface IProviderError extends Error {
|
|
13
|
+
readonly kind: ProviderErrorKind;
|
|
14
|
+
readonly provider: string;
|
|
15
|
+
readonly context: ProviderErrorContext;
|
|
16
|
+
readonly isRetryable: boolean;
|
|
17
|
+
readonly isUserError: boolean;
|
|
18
|
+
readonly suggestedAction?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enhanced error class with helpful properties
|
|
23
|
+
*/
|
|
24
|
+
export class ProviderError extends Error implements IProviderError {
|
|
25
|
+
readonly kind: ProviderErrorKind;
|
|
26
|
+
readonly provider: string;
|
|
27
|
+
readonly context: ProviderErrorContext;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
kind: ProviderErrorKind,
|
|
32
|
+
provider: string,
|
|
33
|
+
context: ProviderErrorContext = {},
|
|
34
|
+
) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ProviderError";
|
|
37
|
+
this.kind = kind;
|
|
38
|
+
this.provider = provider;
|
|
39
|
+
this.context = context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether this error is safe to retry
|
|
44
|
+
*/
|
|
45
|
+
get isRetryable(): boolean {
|
|
46
|
+
return ["TRANSIENT", "RATE_LIMIT", "TIMEOUT"].includes(this.kind);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether this error is likely due to user input
|
|
51
|
+
*/
|
|
52
|
+
get isUserError(): boolean {
|
|
53
|
+
return ["BAD_REQUEST", "UNAUTHORIZED", "CONTEXT_LENGTH", "CONTENT_FILTER"].includes(this.kind);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Suggested action to resolve the error
|
|
58
|
+
*/
|
|
59
|
+
get suggestedAction(): string | undefined {
|
|
60
|
+
const actions: Partial<Record<ProviderErrorKind, string>> = {
|
|
61
|
+
UNAUTHORIZED: "Check your API key configuration",
|
|
62
|
+
RATE_LIMIT: "Wait and retry, or switch to a different provider",
|
|
63
|
+
OUT_OF_CREDITS: "Check your quota or upgrade your plan",
|
|
64
|
+
CONTEXT_LENGTH: "Reduce the input size or use a different model",
|
|
65
|
+
CONTENT_FILTER: "Modify your prompt to comply with content policies",
|
|
66
|
+
TIMEOUT: "Try again with a shorter prompt or increase timeout",
|
|
67
|
+
BAD_REQUEST: "Check the request format and parameters",
|
|
68
|
+
NOT_FOUND: "Verify the model or endpoint exists",
|
|
69
|
+
FORBIDDEN: "Check your permissions for this operation",
|
|
70
|
+
INTERNAL: "The provider is experiencing issues, try again later",
|
|
71
|
+
TRANSIENT: "This is a temporary error, retry the request",
|
|
72
|
+
};
|
|
73
|
+
return actions[this.kind];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create from an adapter error
|
|
78
|
+
*/
|
|
79
|
+
static fromAdapterError(
|
|
80
|
+
err: Error & { kind?: ProviderErrorKind; adapterId?: string; context?: ProviderErrorContext },
|
|
81
|
+
): ProviderError {
|
|
82
|
+
return new ProviderError(
|
|
83
|
+
err.message,
|
|
84
|
+
err.kind ?? "UNKNOWN",
|
|
85
|
+
err.adapterId ?? "unknown",
|
|
86
|
+
err.context ?? {},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create from a generic error
|
|
92
|
+
*/
|
|
93
|
+
static fromError(
|
|
94
|
+
err: Error,
|
|
95
|
+
provider: string,
|
|
96
|
+
kind: ProviderErrorKind = "UNKNOWN",
|
|
97
|
+
): ProviderError {
|
|
98
|
+
return new ProviderError(err.message, kind, provider, {});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Error classification helper
|
|
104
|
+
*/
|
|
105
|
+
export function classifyErrorMessage(message: string): ProviderErrorKind {
|
|
106
|
+
const lower = message.toLowerCase();
|
|
107
|
+
|
|
108
|
+
// Check patterns in priority order
|
|
109
|
+
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many")) {
|
|
110
|
+
return "RATE_LIMIT";
|
|
111
|
+
}
|
|
112
|
+
if (lower.includes("quota") || lower.includes("credits") || lower.includes("exceeded")) {
|
|
113
|
+
return "OUT_OF_CREDITS";
|
|
114
|
+
}
|
|
115
|
+
if (lower.includes("unauthorized") || lower.includes("401") || lower.includes("api key")) {
|
|
116
|
+
return "UNAUTHORIZED";
|
|
117
|
+
}
|
|
118
|
+
if (lower.includes("forbidden") || lower.includes("403")) {
|
|
119
|
+
return "FORBIDDEN";
|
|
120
|
+
}
|
|
121
|
+
if (lower.includes("not found") || lower.includes("404")) {
|
|
122
|
+
return "NOT_FOUND";
|
|
123
|
+
}
|
|
124
|
+
if (lower.includes("timeout") || lower.includes("timed out")) {
|
|
125
|
+
return "TIMEOUT";
|
|
126
|
+
}
|
|
127
|
+
if (lower.includes("context length") || lower.includes("too long")) {
|
|
128
|
+
return "CONTEXT_LENGTH";
|
|
129
|
+
}
|
|
130
|
+
if (lower.includes("content filter") || lower.includes("blocked")) {
|
|
131
|
+
return "CONTENT_FILTER";
|
|
132
|
+
}
|
|
133
|
+
if (lower.includes("500") || lower.includes("internal error")) {
|
|
134
|
+
return "INTERNAL";
|
|
135
|
+
}
|
|
136
|
+
if (lower.includes("bad request") || lower.includes("400")) {
|
|
137
|
+
return "BAD_REQUEST";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return "TRANSIENT";
|
|
141
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Interface Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Capabilities
|
|
6
|
+
export {
|
|
7
|
+
Capability,
|
|
8
|
+
type CapabilityType,
|
|
9
|
+
type ICapabilities,
|
|
10
|
+
ProviderCapabilities,
|
|
11
|
+
} from "./capabilities";
|
|
12
|
+
|
|
13
|
+
// Output
|
|
14
|
+
export {
|
|
15
|
+
type IOutput,
|
|
16
|
+
type IOutputMeta,
|
|
17
|
+
type UnifiedStreamChunk,
|
|
18
|
+
type IProviderError as IOutputError,
|
|
19
|
+
type NormalizerConfig,
|
|
20
|
+
DEFAULT_NORMALIZER_CONFIG,
|
|
21
|
+
OutputNormalizer,
|
|
22
|
+
} from "./output";
|
|
23
|
+
|
|
24
|
+
// Errors
|
|
25
|
+
export {
|
|
26
|
+
type IProviderError,
|
|
27
|
+
ProviderError,
|
|
28
|
+
classifyErrorMessage,
|
|
29
|
+
} from "./errors";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Output Types
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent output format across all providers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TokenUsage } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalized output format - all providers produce this
|
|
11
|
+
*/
|
|
12
|
+
export interface IOutput {
|
|
13
|
+
/** The text response */
|
|
14
|
+
readonly text: string;
|
|
15
|
+
|
|
16
|
+
/** Token usage (if available) */
|
|
17
|
+
readonly usage?: TokenUsage;
|
|
18
|
+
|
|
19
|
+
/** Response metadata */
|
|
20
|
+
readonly meta: IOutputMeta;
|
|
21
|
+
|
|
22
|
+
/** Original raw output (for debugging) */
|
|
23
|
+
readonly raw?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Output metadata
|
|
28
|
+
*/
|
|
29
|
+
export interface IOutputMeta {
|
|
30
|
+
readonly provider: string;
|
|
31
|
+
readonly model?: string;
|
|
32
|
+
readonly elapsedMs: number;
|
|
33
|
+
readonly finishReason?: "stop" | "length" | "content_filter" | "error";
|
|
34
|
+
readonly streamingMode?: "none" | "text" | "jsonl";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Streaming output chunk types (unified)
|
|
39
|
+
*/
|
|
40
|
+
export type UnifiedStreamChunk =
|
|
41
|
+
| { type: "start"; provider: string; model?: string; requestId: string }
|
|
42
|
+
| { type: "text"; content: string }
|
|
43
|
+
| { type: "json"; data: unknown }
|
|
44
|
+
| { type: "usage"; usage: TokenUsage }
|
|
45
|
+
| { type: "complete"; output: IOutput }
|
|
46
|
+
| { type: "error"; error: IProviderError };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Provider error interface
|
|
50
|
+
*/
|
|
51
|
+
export interface IProviderError {
|
|
52
|
+
readonly message: string;
|
|
53
|
+
readonly kind: string;
|
|
54
|
+
readonly provider: string;
|
|
55
|
+
readonly isRetryable: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Output normalizer configuration
|
|
60
|
+
*/
|
|
61
|
+
export interface NormalizerConfig {
|
|
62
|
+
/** Fields to check for text in JSON output */
|
|
63
|
+
jsonTextFields: string[];
|
|
64
|
+
/** Field containing usage info */
|
|
65
|
+
jsonUsageField?: string;
|
|
66
|
+
/** Fields to check for text in JSONL streaming */
|
|
67
|
+
jsonlTextFields: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default normalizer configuration
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_NORMALIZER_CONFIG: NormalizerConfig = {
|
|
74
|
+
jsonTextFields: ["text", "response", "output", "content", "result", "message"],
|
|
75
|
+
jsonUsageField: "usage",
|
|
76
|
+
jsonlTextFields: ["text", "delta", "content"],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Output normalizer - converts raw output to unified format
|
|
81
|
+
*/
|
|
82
|
+
export class OutputNormalizer {
|
|
83
|
+
private config: NormalizerConfig;
|
|
84
|
+
|
|
85
|
+
constructor(config: NormalizerConfig = DEFAULT_NORMALIZER_CONFIG) {
|
|
86
|
+
this.config = config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Normalize raw text output
|
|
91
|
+
*/
|
|
92
|
+
normalizeText(stdout: string, meta: Partial<IOutputMeta>): IOutput {
|
|
93
|
+
return {
|
|
94
|
+
text: stdout.trim(),
|
|
95
|
+
meta: this.buildMeta(meta),
|
|
96
|
+
raw: stdout,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Normalize JSON output
|
|
102
|
+
*/
|
|
103
|
+
normalizeJson(stdout: string, meta: Partial<IOutputMeta>): IOutput {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(stdout);
|
|
106
|
+
const text = this.extractTextField(parsed);
|
|
107
|
+
const usage = this.extractUsage(parsed);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
text,
|
|
111
|
+
usage,
|
|
112
|
+
meta: this.buildMeta(meta),
|
|
113
|
+
raw: parsed,
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
// Fall back to text normalization if JSON parsing fails
|
|
117
|
+
return this.normalizeText(stdout, meta);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize JSONL streaming output
|
|
123
|
+
*/
|
|
124
|
+
*normalizeJsonl(lines: string[]): Generator<{ text?: string; usage?: TokenUsage }> {
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (!line.trim()) continue;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(line);
|
|
130
|
+
const text = this.extractTextField(parsed);
|
|
131
|
+
const usage = this.extractUsage(parsed);
|
|
132
|
+
|
|
133
|
+
if (text) yield { text };
|
|
134
|
+
if (usage) yield { usage };
|
|
135
|
+
} catch {
|
|
136
|
+
// Non-JSON line, emit as text
|
|
137
|
+
yield { text: line };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private extractTextField(obj: unknown): string {
|
|
143
|
+
if (typeof obj === "string") return obj;
|
|
144
|
+
if (typeof obj !== "object" || obj === null) return String(obj);
|
|
145
|
+
|
|
146
|
+
for (const field of this.config.jsonTextFields) {
|
|
147
|
+
const value = this.getNestedField(obj, field);
|
|
148
|
+
if (typeof value === "string") return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return JSON.stringify(obj);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private extractUsage(obj: unknown): TokenUsage | undefined {
|
|
155
|
+
if (typeof obj !== "object" || obj === null) return undefined;
|
|
156
|
+
|
|
157
|
+
const usageField = this.config.jsonUsageField;
|
|
158
|
+
if (usageField) {
|
|
159
|
+
const usage = this.getNestedField(obj, usageField);
|
|
160
|
+
if (usage && typeof usage === "object") {
|
|
161
|
+
return usage as TokenUsage;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private getNestedField(obj: unknown, path: string): unknown {
|
|
169
|
+
const parts = path.split(".");
|
|
170
|
+
let current = obj;
|
|
171
|
+
|
|
172
|
+
for (const part of parts) {
|
|
173
|
+
if (current === null || current === undefined) return undefined;
|
|
174
|
+
current = (current as Record<string, unknown>)[part];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return current;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private buildMeta(partial: Partial<IOutputMeta>): IOutputMeta {
|
|
181
|
+
return {
|
|
182
|
+
provider: partial.provider || "unknown",
|
|
183
|
+
model: partial.model,
|
|
184
|
+
elapsedMs: partial.elapsedMs || 0,
|
|
185
|
+
finishReason: partial.finishReason,
|
|
186
|
+
streamingMode: partial.streamingMode,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { ConfigLoader } from "./config";
|
|
2
|
+
import { ProviderFactory, RequestDeduplicator } from "./provider-factory";
|
|
3
|
+
import { CodexProvider } from "./providers/codex";
|
|
4
|
+
import { CustomProvider } from "./providers/custom";
|
|
5
|
+
import { GeminiProvider } from "./providers/gemini";
|
|
6
|
+
import type { Provider } from "./providers/index";
|
|
7
|
+
import { OpenCodeProvider } from "./providers/opencode";
|
|
8
|
+
import { QwenCodeProvider } from "./providers/qwen-code";
|
|
9
|
+
import { Router } from "./router";
|
|
10
|
+
import { StateManager } from "./state";
|
|
11
|
+
import type { CodingEvent, CodingRequest, CodingResponse, Config } from "./types";
|
|
12
|
+
|
|
13
|
+
export interface WrapTerminalCoderConfig {
|
|
14
|
+
configPath?: string;
|
|
15
|
+
/** Preload providers at startup (default: false for lazy loading) */
|
|
16
|
+
preloadProviders?: boolean;
|
|
17
|
+
/** Enable request deduplication (default: true) */
|
|
18
|
+
deduplicateRequests?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class WrapTerminalCoder {
|
|
22
|
+
private configLoader: ConfigLoader;
|
|
23
|
+
private config: Config;
|
|
24
|
+
private stateManager: StateManager;
|
|
25
|
+
private router: Router;
|
|
26
|
+
private providerFactory: ProviderFactory;
|
|
27
|
+
private deduplicator: RequestDeduplicator | null;
|
|
28
|
+
|
|
29
|
+
private constructor(
|
|
30
|
+
configLoader: ConfigLoader,
|
|
31
|
+
config: Config,
|
|
32
|
+
stateManager: StateManager,
|
|
33
|
+
router: Router,
|
|
34
|
+
providerFactory: ProviderFactory,
|
|
35
|
+
deduplicator: RequestDeduplicator | null,
|
|
36
|
+
) {
|
|
37
|
+
this.configLoader = configLoader;
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.stateManager = stateManager;
|
|
40
|
+
this.router = router;
|
|
41
|
+
this.providerFactory = providerFactory;
|
|
42
|
+
this.deduplicator = deduplicator;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async create(config: WrapTerminalCoderConfig = {}): Promise<WrapTerminalCoder> {
|
|
46
|
+
// Load configuration
|
|
47
|
+
const configLoader = new ConfigLoader({
|
|
48
|
+
projectConfigPath: config.configPath,
|
|
49
|
+
});
|
|
50
|
+
const loadedConfig = await configLoader.loadConfig();
|
|
51
|
+
|
|
52
|
+
// Initialize state manager
|
|
53
|
+
const stateManager = new StateManager();
|
|
54
|
+
await stateManager.initialize();
|
|
55
|
+
|
|
56
|
+
// Create provider factory with lazy loading
|
|
57
|
+
const providerFactory = new ProviderFactory(loadedConfig);
|
|
58
|
+
|
|
59
|
+
// Register known provider constructors
|
|
60
|
+
providerFactory.registerProvider("gemini", (id, cfg) => new GeminiProvider(cfg));
|
|
61
|
+
providerFactory.registerProvider("qwen-code", (id, cfg) => new QwenCodeProvider(cfg));
|
|
62
|
+
providerFactory.registerProvider("codex", (id, cfg) => new CodexProvider(cfg));
|
|
63
|
+
providerFactory.registerProvider("opencode", (id, cfg) => new OpenCodeProvider(cfg));
|
|
64
|
+
providerFactory.registerCustomProvider((id, cfg) => new CustomProvider(id, cfg));
|
|
65
|
+
|
|
66
|
+
// Create request deduplicator
|
|
67
|
+
const deduplicator = config.deduplicateRequests !== false ? new RequestDeduplicator() : null;
|
|
68
|
+
|
|
69
|
+
// Optionally preload providers (legacy behavior)
|
|
70
|
+
let providers: Map<string, Provider>;
|
|
71
|
+
if (config.preloadProviders) {
|
|
72
|
+
const providerIds = Object.keys(loadedConfig.providers);
|
|
73
|
+
await providerFactory.preloadProviders(providerIds);
|
|
74
|
+
providers = providerFactory.getCachedProviders();
|
|
75
|
+
} else {
|
|
76
|
+
// Create a lazy-loading proxy map for the router
|
|
77
|
+
providers = new LazyProviderMap(providerFactory, Object.keys(loadedConfig.providers));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create router
|
|
81
|
+
const router = new Router(providers, {
|
|
82
|
+
config: loadedConfig,
|
|
83
|
+
stateManager,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return new WrapTerminalCoder(
|
|
87
|
+
configLoader,
|
|
88
|
+
loadedConfig,
|
|
89
|
+
stateManager,
|
|
90
|
+
router,
|
|
91
|
+
providerFactory,
|
|
92
|
+
deduplicator,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async route(request: CodingRequest): Promise<CodingResponse> {
|
|
97
|
+
return await this.router.route(request);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async *routeStream(request: CodingRequest): AsyncGenerator<CodingResponse> {
|
|
101
|
+
yield* this.router.routeStream(request);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async getProviderInfo(): Promise<any[]> {
|
|
105
|
+
const providerInfo = [];
|
|
106
|
+
const providerIds = Object.keys(this.config.providers);
|
|
107
|
+
|
|
108
|
+
for (const id of providerIds) {
|
|
109
|
+
const state = await this.stateManager.getProviderState(id);
|
|
110
|
+
const isAvailable = await this.providerFactory.isProviderAvailable(id);
|
|
111
|
+
providerInfo.push({
|
|
112
|
+
id,
|
|
113
|
+
displayName: this.getProviderDisplayName(id),
|
|
114
|
+
requestsToday: state.requestsToday,
|
|
115
|
+
outOfCreditsUntil: state.outOfCreditsUntil,
|
|
116
|
+
available: isAvailable,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return providerInfo;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get available provider IDs (those with valid binaries)
|
|
125
|
+
*/
|
|
126
|
+
async getAvailableProviders(): Promise<string[]> {
|
|
127
|
+
return this.providerFactory.getAvailableProviderIds();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getRouter(): Router {
|
|
131
|
+
return this.router;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getStateManager(): StateManager {
|
|
135
|
+
return this.stateManager;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getConfig(): Config {
|
|
139
|
+
return this.config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getProviderFactory(): ProviderFactory {
|
|
143
|
+
return this.providerFactory;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private getProviderDisplayName(id: string): string {
|
|
147
|
+
const displayNames: Record<string, string> = {
|
|
148
|
+
gemini: "Gemini CLI",
|
|
149
|
+
"qwen-code": "Qwen Code CLI",
|
|
150
|
+
codex: "Codex CLI",
|
|
151
|
+
opencode: "OpenCode Agent",
|
|
152
|
+
};
|
|
153
|
+
return displayNames[id] || id;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* LazyProviderMap - A Map-like object that lazily loads providers
|
|
159
|
+
*
|
|
160
|
+
* This allows the Router to work with providers without loading them all upfront.
|
|
161
|
+
* Providers are loaded on first access via get().
|
|
162
|
+
*/
|
|
163
|
+
class LazyProviderMap implements Map<string, Provider> {
|
|
164
|
+
private factory: ProviderFactory;
|
|
165
|
+
private knownIds: Set<string>;
|
|
166
|
+
private cache: Map<string, Provider> = new Map();
|
|
167
|
+
|
|
168
|
+
constructor(factory: ProviderFactory, providerIds: string[]) {
|
|
169
|
+
this.factory = factory;
|
|
170
|
+
this.knownIds = new Set(providerIds);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get(key: string): Provider | undefined {
|
|
174
|
+
// Return from cache if available
|
|
175
|
+
if (this.cache.has(key)) {
|
|
176
|
+
return this.cache.get(key);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For lazy loading, we need to handle async loading synchronously
|
|
180
|
+
// The Router will need to be updated to handle async provider loading
|
|
181
|
+
// For now, return undefined and let the Router handle missing providers
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
has(key: string): boolean {
|
|
186
|
+
return this.knownIds.has(key) || this.cache.has(key);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
set(key: string, value: Provider): this {
|
|
190
|
+
this.cache.set(key, value);
|
|
191
|
+
this.knownIds.add(key);
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
delete(key: string): boolean {
|
|
196
|
+
this.knownIds.delete(key);
|
|
197
|
+
return this.cache.delete(key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clear(): void {
|
|
201
|
+
this.knownIds.clear();
|
|
202
|
+
this.cache.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get size(): number {
|
|
206
|
+
return this.knownIds.size;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
keys(): IterableIterator<string> {
|
|
210
|
+
return this.knownIds.values();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
values(): IterableIterator<Provider> {
|
|
214
|
+
return this.cache.values();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
entries(): IterableIterator<[string, Provider]> {
|
|
218
|
+
return this.cache.entries();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
forEach(callbackfn: (value: Provider, key: string, map: Map<string, Provider>) => void): void {
|
|
222
|
+
this.cache.forEach(callbackfn);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[Symbol.iterator](): IterableIterator<[string, Provider]> {
|
|
226
|
+
return this.cache[Symbol.iterator]();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
[Symbol.toStringTag] = "LazyProviderMap";
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Async method to get a provider (loads if not cached)
|
|
233
|
+
*/
|
|
234
|
+
async getAsync(key: string): Promise<Provider | null> {
|
|
235
|
+
if (this.cache.has(key)) {
|
|
236
|
+
return this.cache.get(key)!;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const provider = await this.factory.getProvider(key);
|
|
240
|
+
if (provider) {
|
|
241
|
+
this.cache.set(key, provider);
|
|
242
|
+
}
|
|
243
|
+
return provider;
|
|
244
|
+
}
|
|
245
|
+
}
|