cc-x10ded 3.0.16 → 3.0.18
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/PLUGINS.md +173 -0
- package/examples/plugins/ollama/README.md +67 -0
- package/examples/plugins/ollama/index.ts +138 -0
- package/examples/plugins/ollama/plugin.json +7 -0
- package/package.json +1 -1
- package/packages/plugin-types/index.ts +60 -0
- package/packages/plugin-types/package.json +13 -0
- package/src/commands/doctor.ts +161 -31
- package/src/commands/models.ts +71 -0
- package/src/commands/run.ts +31 -28
- package/src/commands/setup.ts +16 -5
- package/src/core/circuit-breaker.ts +167 -0
- package/src/core/logger.ts +173 -0
- package/src/core/plugins.ts +138 -0
- package/src/core/registry.ts +172 -0
- package/src/core/shell.ts +47 -0
- package/src/core/telemetry.ts +253 -0
- package/src/index.ts +51 -17
- package/src/proxy/map.ts +22 -4
- package/src/proxy/providers.ts +15 -7
- package/src/proxy/server.ts +11 -1
- package/src/proxy/types.ts +1 -1
- package/src/proxy/utils.ts +10 -0
- package/src/types.ts +71 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import type { TelemetryEvent } from "../types";
|
|
5
|
+
import { createLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
const logger = createLogger();
|
|
8
|
+
|
|
9
|
+
export interface TelemetryData {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
sessionStart: number;
|
|
12
|
+
requests: TelemetryRequest[];
|
|
13
|
+
errors: TelemetryError[];
|
|
14
|
+
fallbacks: TelemetryFallback[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TelemetryRequest {
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
latencyMs: number;
|
|
21
|
+
success: boolean;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
errorCode?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TelemetryError {
|
|
27
|
+
provider: string;
|
|
28
|
+
error: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TelemetryFallback {
|
|
33
|
+
fromProvider: string;
|
|
34
|
+
toProvider: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Telemetry {
|
|
40
|
+
private data: TelemetryData;
|
|
41
|
+
private telemetryDir: string;
|
|
42
|
+
private telemetryFile: string;
|
|
43
|
+
private enabled: boolean = true;
|
|
44
|
+
private static instance: Telemetry | null = null;
|
|
45
|
+
|
|
46
|
+
private constructor() {
|
|
47
|
+
this.telemetryDir = join(homedir(), ".config", "claude-glm");
|
|
48
|
+
this.telemetryFile = join(this.telemetryDir, "telemetry.json");
|
|
49
|
+
this.data = this.loadOrCreate();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static getInstance(): Telemetry {
|
|
53
|
+
if (!Telemetry.instance) {
|
|
54
|
+
Telemetry.instance = new Telemetry();
|
|
55
|
+
}
|
|
56
|
+
return Telemetry.instance;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private loadOrCreate(): TelemetryData {
|
|
60
|
+
if (!existsSync(this.telemetryDir)) {
|
|
61
|
+
return this.createNewSession();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (existsSync(this.telemetryFile)) {
|
|
66
|
+
const content = readFileSync(this.telemetryFile, "utf-8");
|
|
67
|
+
const data = JSON.parse(content) as TelemetryData;
|
|
68
|
+
const sessionAge = Date.now() - data.sessionStart;
|
|
69
|
+
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
70
|
+
|
|
71
|
+
if (sessionAge > ONE_DAY) {
|
|
72
|
+
return this.createNewSession();
|
|
73
|
+
}
|
|
74
|
+
return data;
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.warn("Failed to load telemetry data", { error: (error as Error).message });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this.createNewSession();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private createNewSession(): TelemetryData {
|
|
84
|
+
return {
|
|
85
|
+
sessionId: this.generateSessionId(),
|
|
86
|
+
sessionStart: Date.now(),
|
|
87
|
+
requests: [],
|
|
88
|
+
errors: [],
|
|
89
|
+
fallbacks: []
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private generateSessionId(): string {
|
|
94
|
+
const array = new Uint8Array(16);
|
|
95
|
+
crypto.getRandomValues(array);
|
|
96
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, "0")).join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private save(): void {
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(this.telemetryFile, JSON.stringify(this.data, null, 2));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.warn("Failed to save telemetry data", { error: (error as Error).message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
disable(): void {
|
|
108
|
+
this.enabled = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
isEnabled(): boolean {
|
|
112
|
+
return this.enabled;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
trackRequest(
|
|
116
|
+
provider: string,
|
|
117
|
+
model: string,
|
|
118
|
+
latencyMs: number,
|
|
119
|
+
success: boolean,
|
|
120
|
+
errorCode?: string
|
|
121
|
+
): void {
|
|
122
|
+
if (!this.enabled) return;
|
|
123
|
+
|
|
124
|
+
const request: TelemetryRequest = {
|
|
125
|
+
provider,
|
|
126
|
+
model,
|
|
127
|
+
latencyMs,
|
|
128
|
+
success,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (errorCode) {
|
|
133
|
+
request.errorCode = errorCode;
|
|
134
|
+
this.trackError(provider, errorCode);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.data.requests.push(request);
|
|
138
|
+
this.save();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private trackError(provider: string, error: string): void {
|
|
142
|
+
const existing = this.data.errors.find(e => e.provider === provider && e.error === error);
|
|
143
|
+
if (existing) {
|
|
144
|
+
existing.count++;
|
|
145
|
+
} else {
|
|
146
|
+
this.data.errors.push({ provider, error, count: 1 });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
trackFallback(fromProvider: string, toProvider: string, reason: string): void {
|
|
151
|
+
if (!this.enabled) return;
|
|
152
|
+
|
|
153
|
+
this.data.fallbacks.push({
|
|
154
|
+
fromProvider,
|
|
155
|
+
toProvider,
|
|
156
|
+
reason,
|
|
157
|
+
timestamp: Date.now()
|
|
158
|
+
});
|
|
159
|
+
this.save();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
trackEvent(event: Omit<TelemetryEvent, "timestamp">): void {
|
|
163
|
+
if (!this.enabled) return;
|
|
164
|
+
|
|
165
|
+
const fullEvent: TelemetryEvent = {
|
|
166
|
+
...event,
|
|
167
|
+
timestamp: Date.now()
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
switch (event.type) {
|
|
171
|
+
case "request_complete":
|
|
172
|
+
if (event.provider && event.model && event.latencyMs !== undefined) {
|
|
173
|
+
this.trackRequest(
|
|
174
|
+
event.provider,
|
|
175
|
+
event.model,
|
|
176
|
+
event.latencyMs,
|
|
177
|
+
event.success ?? true,
|
|
178
|
+
event.errorCode
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "fallback":
|
|
183
|
+
if (event.fromProvider && event.toProvider) {
|
|
184
|
+
this.trackFallback(event.fromProvider, event.toProvider, event.reason || "unknown");
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getSessionId(): string {
|
|
191
|
+
return this.data.sessionId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getSessionStart(): number {
|
|
195
|
+
return this.data.sessionStart;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getRequestCount(): number {
|
|
199
|
+
return this.data.requests.length;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getRequests(): TelemetryRequest[] {
|
|
203
|
+
return [...this.data.requests];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getErrors(): TelemetryError[] {
|
|
207
|
+
return [...this.data.errors];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getFallbacks(): TelemetryFallback[] {
|
|
211
|
+
return [...this.data.fallbacks];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getProviderStats(): Record<string, { count: number; avgLatency: number; errors: number }> {
|
|
215
|
+
const stats: Record<string, { count: number; totalLatency: number; errors: number }> = {};
|
|
216
|
+
|
|
217
|
+
for (const request of this.data.requests) {
|
|
218
|
+
if (!stats[request.provider]) {
|
|
219
|
+
stats[request.provider] = { count: 0, totalLatency: 0, errors: 0 };
|
|
220
|
+
}
|
|
221
|
+
const providerStats = stats[request.provider];
|
|
222
|
+
if (providerStats) {
|
|
223
|
+
providerStats.count++;
|
|
224
|
+
providerStats.totalLatency += request.latencyMs;
|
|
225
|
+
if (!request.success) {
|
|
226
|
+
providerStats.errors++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result: Record<string, { count: number; avgLatency: number; errors: number }> = {};
|
|
232
|
+
for (const [provider, stat] of Object.entries(stats)) {
|
|
233
|
+
result[provider] = {
|
|
234
|
+
count: stat.count,
|
|
235
|
+
avgLatency: Math.round(stat.totalLatency / stat.count),
|
|
236
|
+
errors: stat.errors
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getSessionDuration(): number {
|
|
244
|
+
return Date.now() - this.data.sessionStart;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
clear(): void {
|
|
248
|
+
this.data = this.createNewSession();
|
|
249
|
+
this.save();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const telemetry = Telemetry.getInstance();
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,11 @@ import { runCommand } from "./commands/run";
|
|
|
4
4
|
import { setupCommand } from "./commands/setup";
|
|
5
5
|
import { configCommand } from "./commands/config";
|
|
6
6
|
import { doctorCommand } from "./commands/doctor";
|
|
7
|
+
import { modelsCommand } from "./commands/models";
|
|
7
8
|
import packageJson from "../package.json";
|
|
9
|
+
import { createLogger } from "./core/logger";
|
|
10
|
+
|
|
11
|
+
const logger = createLogger();
|
|
8
12
|
|
|
9
13
|
const cli = cac("ccx");
|
|
10
14
|
|
|
@@ -20,17 +24,35 @@ cli
|
|
|
20
24
|
.command("doctor", "Run self-diagnostics")
|
|
21
25
|
.action(doctorCommand);
|
|
22
26
|
|
|
27
|
+
cli
|
|
28
|
+
.command("models", "List all available models")
|
|
29
|
+
.action(async () => {
|
|
30
|
+
await modelsCommand();
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
cli
|
|
24
34
|
.command("update", "Update ccx to the latest version")
|
|
25
35
|
.option("--skip-aliases", "Skip alias installation")
|
|
26
|
-
.
|
|
36
|
+
.option("--skip-cleanup", "Skip removal of old binaries")
|
|
37
|
+
.action(async (options: { skipAliases?: boolean; skipCleanup?: boolean }) => {
|
|
27
38
|
const { spawn } = await import("bun");
|
|
28
39
|
const { ShellIntegrator } = await import("./core/shell");
|
|
29
40
|
const pc = await import("picocolors");
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
const shellInt = new ShellIntegrator();
|
|
43
|
+
const shell = shellInt.detectShell();
|
|
44
|
+
|
|
45
|
+
if (!options.skipCleanup) {
|
|
46
|
+
const removed = await shellInt.cleanupOldBinaries();
|
|
47
|
+
if (removed.length > 0) {
|
|
48
|
+
console.log(pc.default.yellow("🧹 Removed old ccx binaries:"));
|
|
49
|
+
for (const p of removed) {
|
|
50
|
+
console.log(pc.default.dim(` ${p}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
32
54
|
|
|
33
|
-
|
|
55
|
+
console.log(pc.default.blue("📦 Updating ccx..."));
|
|
34
56
|
const proc = spawn(["bun", "install", "-g", "cc-x10ded@latest"], {
|
|
35
57
|
stdio: ["inherit", "inherit", "inherit"]
|
|
36
58
|
});
|
|
@@ -43,33 +65,45 @@ cli
|
|
|
43
65
|
|
|
44
66
|
console.log(pc.default.green("✅ ccx updated!"));
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const shellInt = new ShellIntegrator();
|
|
49
|
-
const shell = shellInt.detectShell();
|
|
68
|
+
if (shell !== "unknown") {
|
|
69
|
+
await shellInt.ensureBunBinInPath(shell);
|
|
50
70
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
if (!shellInt.isBunBinFirst()) {
|
|
72
|
+
console.log(pc.default.yellow("⚠️ ~/.bun/bin should be first in PATH for ccx to work correctly."));
|
|
73
|
+
console.log(pc.default.dim(" Add this to the TOP of your shell config:"));
|
|
74
|
+
console.log(pc.default.cyan(' export PATH="$HOME/.bun/bin:$PATH"'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log(pc.default.dim(` Run: source ~/.${shell}rc (or restart your terminal)`));
|
|
60
|
-
}
|
|
78
|
+
if (!options.skipAliases && shell !== "unknown") {
|
|
79
|
+
const success = await shellInt.installAliases(shell);
|
|
80
|
+
if (success) {
|
|
81
|
+
console.log(pc.default.green("✅ Aliases updated!"));
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
console.log(pc.default.green("\n🎉 Update complete!"));
|
|
86
|
+
if (shell !== "unknown") {
|
|
87
|
+
console.log(pc.default.dim(` Run: source ~/.${shell}rc (or restart your terminal)`));
|
|
88
|
+
}
|
|
65
89
|
});
|
|
66
90
|
|
|
67
91
|
cli
|
|
68
92
|
.command("[...args]", "Run Claude Code with proxy (default)")
|
|
69
93
|
.option("-m, --model <model>", "Override the model (e.g., glm-4.5, openai:gpt-4o)")
|
|
70
94
|
.option("-p, --port <port>", "Port for the local proxy (default: 17870)")
|
|
95
|
+
.option("--json-log", "Output logs in JSON format")
|
|
71
96
|
.action((args, options) => {
|
|
72
|
-
|
|
97
|
+
if (options.jsonLog) {
|
|
98
|
+
logger.setJsonMode(true);
|
|
99
|
+
}
|
|
100
|
+
runCommand(args, { model: options.model, port: options.port });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
cli
|
|
104
|
+
.command("--list", "List all available models (alias for 'models')")
|
|
105
|
+
.action(async () => {
|
|
106
|
+
await modelsCommand();
|
|
73
107
|
});
|
|
74
108
|
|
|
75
109
|
cli.help();
|
package/src/proxy/map.ts
CHANGED
|
@@ -80,8 +80,26 @@ export function toOpenAIMessages(messages: AnthropicMessage[]) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function toGeminiContents(messages: AnthropicMessage[]) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
let systemInstruction: string | null = null;
|
|
84
|
+
const mappedMessages: Array<{ role: string; parts: Array<{ text: string }> }> = [];
|
|
85
|
+
|
|
86
|
+
for (const m of messages) {
|
|
87
|
+
if (m.role === "system") {
|
|
88
|
+
systemInstruction = toPlainText(m.content);
|
|
89
|
+
} else {
|
|
90
|
+
mappedMessages.push({
|
|
91
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
92
|
+
parts: [{ text: toPlainText(m.content) }]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (systemInstruction && mappedMessages.length > 0) {
|
|
98
|
+
const firstMessage = mappedMessages[0];
|
|
99
|
+
if (firstMessage) {
|
|
100
|
+
firstMessage.parts.unshift({ text: `[System Instruction: ${systemInstruction}] ` });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return mappedMessages;
|
|
87
105
|
}
|
package/src/proxy/providers.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createParser } from "eventsource-parser";
|
|
2
2
|
import type { AnthropicRequest } from "./types";
|
|
3
3
|
import { toOpenAIMessages, toGeminiContents } from "./map";
|
|
4
|
-
import { createStartMessage, createDelta, createStopMessage, ApiError } from "./utils";
|
|
4
|
+
import { createStartMessage, createDelta, createStopMessage, ApiError, parseErrorResponse } from "./utils";
|
|
5
|
+
|
|
6
|
+
const MAX_BUFFER_SIZE = 65536;
|
|
5
7
|
|
|
6
8
|
// OpenAI
|
|
7
9
|
export async function* streamOpenAI(
|
|
@@ -27,14 +29,14 @@ export async function* streamOpenAI(
|
|
|
27
29
|
body: JSON.stringify(reqBody)
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
if (!resp.ok) throw new ApiError(parseErrorResponse(await resp.text()), resp.status);
|
|
31
33
|
if (!resp.body) throw new ApiError("No response body", 500);
|
|
32
34
|
|
|
33
35
|
yield createStartMessage(model);
|
|
34
36
|
|
|
35
37
|
const reader = resp.body.getReader();
|
|
36
38
|
const decoder = new TextDecoder();
|
|
37
|
-
let buffer = "";
|
|
39
|
+
let buffer = "";
|
|
38
40
|
|
|
39
41
|
const parser = createParser(((event: any) => {
|
|
40
42
|
if (event.type !== "event") return;
|
|
@@ -51,7 +53,10 @@ export async function* streamOpenAI(
|
|
|
51
53
|
const { value, done } = await reader.read();
|
|
52
54
|
if (done) break;
|
|
53
55
|
parser.feed(decoder.decode(value));
|
|
54
|
-
if (buffer) {
|
|
56
|
+
if (buffer.length >= MAX_BUFFER_SIZE) {
|
|
57
|
+
yield buffer;
|
|
58
|
+
buffer = "";
|
|
59
|
+
} else if (buffer) {
|
|
55
60
|
yield buffer;
|
|
56
61
|
buffer = "";
|
|
57
62
|
}
|
|
@@ -82,7 +87,7 @@ export async function* streamGemini(
|
|
|
82
87
|
body: JSON.stringify(reqBody)
|
|
83
88
|
});
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
if (!resp.ok) throw new ApiError(parseErrorResponse(await resp.text()), resp.status);
|
|
86
91
|
if (!resp.body) throw new ApiError("No response body", 500);
|
|
87
92
|
|
|
88
93
|
yield createStartMessage(model);
|
|
@@ -106,7 +111,10 @@ export async function* streamGemini(
|
|
|
106
111
|
const { value, done } = await reader.read();
|
|
107
112
|
if (done) break;
|
|
108
113
|
parser.feed(decoder.decode(value));
|
|
109
|
-
if (buffer) {
|
|
114
|
+
if (buffer.length >= MAX_BUFFER_SIZE) {
|
|
115
|
+
yield buffer;
|
|
116
|
+
buffer = "";
|
|
117
|
+
} else if (buffer) {
|
|
110
118
|
yield buffer;
|
|
111
119
|
buffer = "";
|
|
112
120
|
}
|
|
@@ -130,7 +138,7 @@ export async function* streamPassThrough(
|
|
|
130
138
|
body: JSON.stringify(body)
|
|
131
139
|
});
|
|
132
140
|
|
|
133
|
-
|
|
141
|
+
if (!resp.ok) throw new ApiError(parseErrorResponse(await resp.text()), resp.status);
|
|
134
142
|
if (!resp.body) throw new ApiError("No response body", 500);
|
|
135
143
|
|
|
136
144
|
const reader = resp.body.getReader();
|
package/src/proxy/server.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Config } from "../core/config";
|
|
|
6
6
|
import type { AnthropicRequest } from "./types";
|
|
7
7
|
|
|
8
8
|
export function startProxyServer(config: Config, port: number = 17870) {
|
|
9
|
-
|
|
9
|
+
const server = serve({
|
|
10
10
|
port,
|
|
11
11
|
hostname: "127.0.0.1",
|
|
12
12
|
async fetch(req) {
|
|
@@ -85,4 +85,14 @@ export function startProxyServer(config: Config, port: number = 17870) {
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
const shutdown = () => {
|
|
90
|
+
server.stop();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
process.on("SIGINT", shutdown);
|
|
95
|
+
process.on("SIGTERM", shutdown);
|
|
96
|
+
|
|
97
|
+
return server;
|
|
88
98
|
}
|
package/src/proxy/types.ts
CHANGED
package/src/proxy/utils.ts
CHANGED
|
@@ -53,6 +53,16 @@ export class ApiError extends Error {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function parseErrorResponse(text: string): string {
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.parse(text);
|
|
59
|
+
if (json.error?.message) return json.error.message;
|
|
60
|
+
if (json.message) return json.message;
|
|
61
|
+
if (json.error) return typeof json.error === "string" ? json.error : JSON.stringify(json.error);
|
|
62
|
+
} catch {}
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
// Convert an async generator to a ReadableStream
|
|
57
67
|
export function toReadableStream<T>(gen: AsyncGenerator<T>): ReadableStream<T> {
|
|
58
68
|
return new ReadableStream({
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
export interface TelemetryEvent {
|
|
4
|
+
type: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
provider?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
latencyMs?: number;
|
|
9
|
+
success?: boolean;
|
|
10
|
+
errorCode?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
fromProvider?: string;
|
|
13
|
+
toProvider?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
models: ModelInfo[];
|
|
20
|
+
isNative: boolean;
|
|
21
|
+
requiresKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ModelInfo {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
maxOutputTokens?: number;
|
|
29
|
+
capabilities?: readonly ("text" | "vision" | "tools")[];
|
|
30
|
+
default?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CircuitState {
|
|
34
|
+
provider: string;
|
|
35
|
+
state: "closed" | "open" | "half-open";
|
|
36
|
+
failures: number;
|
|
37
|
+
lastFailure: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HealthStatus {
|
|
41
|
+
healthy: boolean;
|
|
42
|
+
latencyMs?: number;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PluginConfig {
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
extra?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ProviderClient {
|
|
53
|
+
readonly provider: string;
|
|
54
|
+
streamComplete(request: object): AsyncGenerator<object>;
|
|
55
|
+
getModelInfo(): ModelInfo | undefined;
|
|
56
|
+
healthCheck(): Promise<HealthStatus>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ProviderPlugin {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
models: ModelInfo[];
|
|
65
|
+
createClient(config: PluginConfig): ProviderClient;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SSEMessage {
|
|
69
|
+
type: string;
|
|
70
|
+
data: unknown;
|
|
71
|
+
}
|