@agimon-ai/model-proxy-mcp 0.2.3 → 0.2.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/dist/cli.cjs +3 -0
- package/dist/cli.mjs +2 -651
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -3
- package/dist/stdio-BKQRhaEs.cjs +1127 -0
- package/dist/stdio-xpvsxU_k.mjs +596 -0
- package/package.json +6 -4
- package/dist/cli.d.mts +0 -1
- package/dist/index.d.mts +0 -468
- package/dist/stdio-C23n_O3v.mjs +0 -4041
package/dist/stdio-C23n_O3v.mjs
DELETED
|
@@ -1,4041 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import fs$1 from "node:fs/promises";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import { parse, stringify } from "yaml";
|
|
8
|
-
import { ZodError, z } from "zod";
|
|
9
|
-
import { serve } from "@hono/node-server";
|
|
10
|
-
import { Hono } from "hono";
|
|
11
|
-
import { ulid } from "ulidx";
|
|
12
|
-
import Database from "better-sqlite3";
|
|
13
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
-
|
|
17
|
-
//#region src/constants/defaults.ts
|
|
18
|
-
const DEFAULT_SETTINGS_DIR = path.join(os.homedir(), ".model-proxy");
|
|
19
|
-
const DEFAULT_MODEL_PROVIDER_PATH = path.join(DEFAULT_SETTINGS_DIR, "model-provider.yaml");
|
|
20
|
-
const DEFAULT_MODEL_LIST_PATH = path.join(DEFAULT_SETTINGS_DIR, "model-list.yaml");
|
|
21
|
-
const DEFAULT_SCOPE_SETTINGS_DIR = path.join(DEFAULT_SETTINGS_DIR, "scopes");
|
|
22
|
-
const DEFAULT_HISTORY_DB_PATH = path.join(DEFAULT_SETTINGS_DIR, "history.sqlite");
|
|
23
|
-
const DEFAULT_AUTH_FILE_PATH = path.join(os.homedir(), ".codex", "auth.json");
|
|
24
|
-
const DEFAULT_HTTP_PORT = 43191;
|
|
25
|
-
const DEFAULT_SERVICE_NAME = "model-proxy-mcp-http";
|
|
26
|
-
const DEFAULT_HISTORY_RETENTION_LIMIT = 1e3;
|
|
27
|
-
const DEFAULT_PROVIDER_SETTINGS = {
|
|
28
|
-
"chatgpt-codex": {
|
|
29
|
-
type: "chatgpt-codex",
|
|
30
|
-
endpoint: "https://chatgpt.com/backend-api/codex/responses",
|
|
31
|
-
authTokenEnvVar: null,
|
|
32
|
-
apiTimeoutMs: null
|
|
33
|
-
},
|
|
34
|
-
"zai-anthropic-compat": {
|
|
35
|
-
type: "anthropic-compatible",
|
|
36
|
-
endpoint: "https://api.z.ai/api/anthropic/v1/messages",
|
|
37
|
-
authTokenEnvVar: "ZAI_ANTHROPIC_AUTH_TOKEN",
|
|
38
|
-
apiTimeoutMs: 3e6
|
|
39
|
-
},
|
|
40
|
-
"google-gemini-direct": {
|
|
41
|
-
type: "gemini-direct",
|
|
42
|
-
endpoint: "https://generativelanguage.googleapis.com",
|
|
43
|
-
authTokenEnvVar: "GEMINI_API_KEY",
|
|
44
|
-
apiTimeoutMs: 3e5,
|
|
45
|
-
authMode: "auto",
|
|
46
|
-
apiKeyEnvVar: "GEMINI_API_KEY",
|
|
47
|
-
project: null,
|
|
48
|
-
location: "global",
|
|
49
|
-
apiVersion: "v1beta"
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
const DEFAULT_MODEL_LIST = [
|
|
53
|
-
{
|
|
54
|
-
id: "chatgpt-codex-gpt-5.3-codex",
|
|
55
|
-
label: "ChatGPT Codex GPT-5.3 Codex",
|
|
56
|
-
provider: "chatgpt-codex",
|
|
57
|
-
model: "gpt-5.3-codex",
|
|
58
|
-
reasoningEffort: "medium",
|
|
59
|
-
enabled: true
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: "chatgpt-codex-gpt-5.3-codex-spark",
|
|
63
|
-
label: "ChatGPT Codex GPT-5.3 Codex Spark",
|
|
64
|
-
provider: "chatgpt-codex",
|
|
65
|
-
model: "gpt-5.3-codex-spark",
|
|
66
|
-
reasoningEffort: "low",
|
|
67
|
-
enabled: true
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "chatgpt-codex-gpt-5.4",
|
|
71
|
-
label: "ChatGPT Codex GPT-5.4",
|
|
72
|
-
provider: "chatgpt-codex",
|
|
73
|
-
model: "gpt-5.4",
|
|
74
|
-
reasoningEffort: "medium",
|
|
75
|
-
enabled: true
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: "chatgpt-codex-gpt-5.2",
|
|
79
|
-
label: "ChatGPT Codex GPT-5.2",
|
|
80
|
-
provider: "chatgpt-codex",
|
|
81
|
-
model: "gpt-5.2",
|
|
82
|
-
reasoningEffort: "medium",
|
|
83
|
-
enabled: true
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
id: "zai-anthropic-compat-glm-4.7",
|
|
87
|
-
label: "Z.ai GLM-4.7",
|
|
88
|
-
provider: "zai-anthropic-compat",
|
|
89
|
-
model: "GLM-4.7",
|
|
90
|
-
reasoningEffort: "high",
|
|
91
|
-
enabled: true
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "zai-anthropic-compat-glm-5",
|
|
95
|
-
label: "Z.ai GLM-5",
|
|
96
|
-
provider: "zai-anthropic-compat",
|
|
97
|
-
model: "glm-5",
|
|
98
|
-
reasoningEffort: "high",
|
|
99
|
-
enabled: true
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "zai-anthropic-compat-glm-4.5-air",
|
|
103
|
-
label: "Z.ai GLM-4.5-Air",
|
|
104
|
-
provider: "zai-anthropic-compat",
|
|
105
|
-
model: "GLM-4.5-Air",
|
|
106
|
-
reasoningEffort: "medium",
|
|
107
|
-
enabled: true
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "google-gemini-direct-gemini-2.5-flash",
|
|
111
|
-
label: "Google Gemini 2.5 Flash",
|
|
112
|
-
provider: "google-gemini-direct",
|
|
113
|
-
model: "gemini-2.5-flash",
|
|
114
|
-
reasoningEffort: "medium",
|
|
115
|
-
enabled: true
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: "google-gemini-direct-gemini-3-flash-preview",
|
|
119
|
-
label: "Google Gemini 3 Flash Preview",
|
|
120
|
-
provider: "google-gemini-direct",
|
|
121
|
-
model: "gemini-3-flash-preview",
|
|
122
|
-
reasoningEffort: "medium",
|
|
123
|
-
enabled: true
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
id: "google-gemini-direct-gemini-3.1-pro-preview",
|
|
127
|
-
label: "Google Gemini 3.1 Pro Preview",
|
|
128
|
-
provider: "google-gemini-direct",
|
|
129
|
-
model: "gemini-3.1-pro-preview",
|
|
130
|
-
reasoningEffort: "high",
|
|
131
|
-
enabled: true
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
const DEFAULT_SCOPE_SETTINGS = { models: {
|
|
135
|
-
default: {
|
|
136
|
-
main: {
|
|
137
|
-
provider: "chatgpt-codex",
|
|
138
|
-
model: "gpt-5.4",
|
|
139
|
-
reasoningEffort: "medium"
|
|
140
|
-
},
|
|
141
|
-
fallbacks: [{
|
|
142
|
-
provider: "chatgpt-codex",
|
|
143
|
-
model: "gpt-5.4",
|
|
144
|
-
reasoningEffort: "low"
|
|
145
|
-
}]
|
|
146
|
-
},
|
|
147
|
-
sonnet: {
|
|
148
|
-
main: {
|
|
149
|
-
provider: "chatgpt-codex",
|
|
150
|
-
model: "gpt-5.3-codex",
|
|
151
|
-
reasoningEffort: "medium"
|
|
152
|
-
},
|
|
153
|
-
fallbacks: [{
|
|
154
|
-
provider: "chatgpt-codex",
|
|
155
|
-
model: "gpt-5.4",
|
|
156
|
-
reasoningEffort: "medium"
|
|
157
|
-
}]
|
|
158
|
-
},
|
|
159
|
-
opus: {
|
|
160
|
-
main: {
|
|
161
|
-
provider: "chatgpt-codex",
|
|
162
|
-
model: "gpt-5.4",
|
|
163
|
-
reasoningEffort: "medium"
|
|
164
|
-
},
|
|
165
|
-
fallbacks: [{
|
|
166
|
-
provider: "chatgpt-codex",
|
|
167
|
-
model: "gpt-5.4",
|
|
168
|
-
reasoningEffort: "low"
|
|
169
|
-
}]
|
|
170
|
-
},
|
|
171
|
-
haiku: {
|
|
172
|
-
main: {
|
|
173
|
-
provider: "chatgpt-codex",
|
|
174
|
-
model: "gpt-5.3-codex-spark",
|
|
175
|
-
reasoningEffort: "low"
|
|
176
|
-
},
|
|
177
|
-
fallbacks: [{
|
|
178
|
-
provider: "chatgpt-codex",
|
|
179
|
-
model: "gpt-5.4",
|
|
180
|
-
reasoningEffort: "medium"
|
|
181
|
-
}]
|
|
182
|
-
},
|
|
183
|
-
subagent: {
|
|
184
|
-
main: {
|
|
185
|
-
provider: "chatgpt-codex",
|
|
186
|
-
model: "gpt-5.3-codex-spark",
|
|
187
|
-
reasoningEffort: "low"
|
|
188
|
-
},
|
|
189
|
-
fallbacks: [{
|
|
190
|
-
provider: "chatgpt-codex",
|
|
191
|
-
model: "gpt-5.4",
|
|
192
|
-
reasoningEffort: "medium"
|
|
193
|
-
}]
|
|
194
|
-
}
|
|
195
|
-
} };
|
|
196
|
-
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region src/services/logger.ts
|
|
199
|
-
const consoleLogger = {
|
|
200
|
-
info: (msg, data) => console.error(msg, data ?? ""),
|
|
201
|
-
error: (msg, error) => console.error(msg, error ?? ""),
|
|
202
|
-
debug: () => void 0,
|
|
203
|
-
warn: (msg, data) => console.error(msg, data ?? "")
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region src/services/ProfileStore.ts
|
|
208
|
-
const DEFAULT_SCOPE$2 = "default";
|
|
209
|
-
const DEFAULT_ZAI_PROVIDER_ID = "zai-anthropic-compat";
|
|
210
|
-
const LEGACY_ZAI_AUTH_ENV = "ANTHROPIC_AUTH_TOKEN";
|
|
211
|
-
const DEFAULT_ZAI_AUTH_ENV = "ZAI_ANTHROPIC_AUTH_TOKEN";
|
|
212
|
-
const SETTINGS_EXTENSION = ".yaml";
|
|
213
|
-
const DEFAULT_REASONING_EFFORT = "medium";
|
|
214
|
-
const MODEL_SLOTS$1 = [
|
|
215
|
-
"default",
|
|
216
|
-
"sonnet",
|
|
217
|
-
"opus",
|
|
218
|
-
"haiku",
|
|
219
|
-
"subagent"
|
|
220
|
-
];
|
|
221
|
-
const LOG_PREFIX = "[model-proxy-mcp]";
|
|
222
|
-
const FILE_NOT_FOUND_ERROR_CODE = "ENOENT";
|
|
223
|
-
const YAML_INDENT = 2;
|
|
224
|
-
const FILE_ENCODING = "utf8";
|
|
225
|
-
const PROVIDER_TYPES = [
|
|
226
|
-
"chatgpt-codex",
|
|
227
|
-
"anthropic-compatible",
|
|
228
|
-
"gemini-direct"
|
|
229
|
-
];
|
|
230
|
-
const PROFILE_STORE_ERROR_CODES = {
|
|
231
|
-
profileNotFound: "PROFILE_NOT_FOUND",
|
|
232
|
-
settingsReadFailed: "SETTINGS_READ_FAILED",
|
|
233
|
-
settingsWriteFailed: "SETTINGS_WRITE_FAILED"
|
|
234
|
-
};
|
|
235
|
-
const reasoningEffortSchema$1 = z.enum([
|
|
236
|
-
"minimal",
|
|
237
|
-
"low",
|
|
238
|
-
"medium",
|
|
239
|
-
"high"
|
|
240
|
-
]);
|
|
241
|
-
const providerRuntimeConfigSchema = z.object({
|
|
242
|
-
type: z.enum(PROVIDER_TYPES),
|
|
243
|
-
endpoint: z.url(),
|
|
244
|
-
authTokenEnvVar: z.string().min(1).nullable().optional(),
|
|
245
|
-
apiTimeoutMs: z.number().int().positive().nullable().optional(),
|
|
246
|
-
authMode: z.enum([
|
|
247
|
-
"auto",
|
|
248
|
-
"api-key",
|
|
249
|
-
"oauth"
|
|
250
|
-
]).nullable().optional(),
|
|
251
|
-
apiKeyEnvVar: z.string().min(1).nullable().optional(),
|
|
252
|
-
project: z.string().min(1).nullable().optional(),
|
|
253
|
-
location: z.string().min(1).nullable().optional(),
|
|
254
|
-
apiVersion: z.string().min(1).nullable().optional()
|
|
255
|
-
});
|
|
256
|
-
const providerRegistrySchema = z.object({ providers: z.record(z.string().min(1), providerRuntimeConfigSchema) });
|
|
257
|
-
const modelConfigSchema = z.object({
|
|
258
|
-
id: z.string().min(1),
|
|
259
|
-
label: z.string().min(1),
|
|
260
|
-
provider: z.string().min(1),
|
|
261
|
-
model: z.string().min(1),
|
|
262
|
-
reasoningEffort: reasoningEffortSchema$1.default(DEFAULT_REASONING_EFFORT),
|
|
263
|
-
enabled: z.boolean().default(true)
|
|
264
|
-
});
|
|
265
|
-
const selectionSchema$1 = z.object({
|
|
266
|
-
provider: z.string().min(1),
|
|
267
|
-
model: z.string().min(1),
|
|
268
|
-
reasoningEffort: reasoningEffortSchema$1.default(DEFAULT_REASONING_EFFORT),
|
|
269
|
-
thinkingDisabled: z.boolean().optional()
|
|
270
|
-
});
|
|
271
|
-
const slotConfigSchema$1 = z.object({
|
|
272
|
-
main: selectionSchema$1,
|
|
273
|
-
fallbacks: z.array(selectionSchema$1).default([])
|
|
274
|
-
});
|
|
275
|
-
const scopeSettingsSchema = z.object({ models: z.object({
|
|
276
|
-
default: slotConfigSchema$1.optional(),
|
|
277
|
-
sonnet: slotConfigSchema$1.optional(),
|
|
278
|
-
opus: slotConfigSchema$1.optional(),
|
|
279
|
-
haiku: slotConfigSchema$1.optional(),
|
|
280
|
-
subagent: slotConfigSchema$1.optional()
|
|
281
|
-
}).default({}) });
|
|
282
|
-
const modelListSchema = z.array(modelConfigSchema);
|
|
283
|
-
const adminConfigUpdateSchema$1 = z.object({ models: z.object({
|
|
284
|
-
default: slotConfigSchema$1.nullable().optional(),
|
|
285
|
-
sonnet: slotConfigSchema$1.nullable().optional(),
|
|
286
|
-
opus: slotConfigSchema$1.nullable().optional(),
|
|
287
|
-
haiku: slotConfigSchema$1.nullable().optional(),
|
|
288
|
-
subagent: slotConfigSchema$1.nullable().optional()
|
|
289
|
-
}).optional() });
|
|
290
|
-
var ProfileStoreError = class extends Error {
|
|
291
|
-
constructor(message, code, options) {
|
|
292
|
-
super(message, options);
|
|
293
|
-
this.code = code;
|
|
294
|
-
this.name = "ProfileStoreError";
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
var ProfileStore = class {
|
|
298
|
-
constructor(providerConfigPath = process.env.MODEL_PROXY_MCP_PROVIDER_PATH || DEFAULT_MODEL_PROVIDER_PATH, modelListPath = process.env.MODEL_PROXY_MCP_MODEL_LIST_PATH || path.join(path.dirname(process.env.MODEL_PROXY_MCP_PROVIDER_PATH || DEFAULT_MODEL_PROVIDER_PATH), path.basename(DEFAULT_MODEL_LIST_PATH)), scopeSettingsDir = process.env.MODEL_PROXY_MCP_SCOPE_DIR || path.join(path.dirname(process.env.MODEL_PROXY_MCP_PROVIDER_PATH || DEFAULT_MODEL_PROVIDER_PATH), path.basename(DEFAULT_SCOPE_SETTINGS_DIR)), logger = consoleLogger) {
|
|
299
|
-
this.providerConfigPath = providerConfigPath;
|
|
300
|
-
this.modelListPath = modelListPath;
|
|
301
|
-
this.scopeSettingsDir = scopeSettingsDir;
|
|
302
|
-
this.logger = logger;
|
|
303
|
-
}
|
|
304
|
-
getConfigPath(scope = DEFAULT_SCOPE$2) {
|
|
305
|
-
return this.getScopeConfigPath(scope);
|
|
306
|
-
}
|
|
307
|
-
async ensureConfig(scope = DEFAULT_SCOPE$2, seedConfigPath) {
|
|
308
|
-
return this.getConfig(scope, seedConfigPath);
|
|
309
|
-
}
|
|
310
|
-
async listScopes() {
|
|
311
|
-
try {
|
|
312
|
-
const entries = await fs$1.readdir(this.scopeSettingsDir, { withFileTypes: true });
|
|
313
|
-
const scopes = new Set([DEFAULT_SCOPE$2]);
|
|
314
|
-
for (const entry of entries) {
|
|
315
|
-
if (!entry.isFile() || !entry.name.endsWith(SETTINGS_EXTENSION)) continue;
|
|
316
|
-
const scopeName = entry.name.slice(0, -5);
|
|
317
|
-
if (scopeName) scopes.add(scopeName);
|
|
318
|
-
}
|
|
319
|
-
return Array.from(scopes).sort();
|
|
320
|
-
} catch (error) {
|
|
321
|
-
if (this.isFileNotFoundError(error)) return [DEFAULT_SCOPE$2];
|
|
322
|
-
this.logger.error(`${LOG_PREFIX} Failed to list scopes`, {
|
|
323
|
-
scopeSettingsDir: this.scopeSettingsDir,
|
|
324
|
-
cause: error
|
|
325
|
-
});
|
|
326
|
-
throw new ProfileStoreError(`Failed to list scopes in ${this.scopeSettingsDir}`, PROFILE_STORE_ERROR_CODES.settingsReadFailed, { cause: error });
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
async getConfig(scope = DEFAULT_SCOPE$2, seedConfigPath) {
|
|
330
|
-
return this.toProxyConfig(await this.getSettings(scope, seedConfigPath));
|
|
331
|
-
}
|
|
332
|
-
async getAdminConfig(scope = DEFAULT_SCOPE$2) {
|
|
333
|
-
const settings = await this.getSettings(scope);
|
|
334
|
-
const profiles = this.toProfiles(settings);
|
|
335
|
-
const slots = Object.fromEntries(MODEL_SLOTS$1.map((slot) => [slot, this.resolveSlotConfig(settings, profiles, slot)]));
|
|
336
|
-
return {
|
|
337
|
-
scope,
|
|
338
|
-
providerConfigPath: this.providerConfigPath,
|
|
339
|
-
modelListPath: this.modelListPath,
|
|
340
|
-
scopeConfigPath: this.getScopeConfigPath(scope),
|
|
341
|
-
providers: settings.providers,
|
|
342
|
-
models: profiles,
|
|
343
|
-
scopeModels: settings.scope.models,
|
|
344
|
-
slots
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
async listProfiles(scope = DEFAULT_SCOPE$2) {
|
|
348
|
-
return (await this.getAdminConfig(scope)).models;
|
|
349
|
-
}
|
|
350
|
-
async getActiveProfile(scope = DEFAULT_SCOPE$2, slot = DEFAULT_SCOPE$2) {
|
|
351
|
-
const config = await this.getAdminConfig(scope);
|
|
352
|
-
const resolvedSlot = config.slots[slot];
|
|
353
|
-
return config.models.find((profile) => profile.id === resolvedSlot.profileId && profile.enabled) ?? null;
|
|
354
|
-
}
|
|
355
|
-
async getResolvedSlotConfig(scope = DEFAULT_SCOPE$2, slot = DEFAULT_SCOPE$2) {
|
|
356
|
-
return (await this.getAdminConfig(scope)).slots[slot];
|
|
357
|
-
}
|
|
358
|
-
async setActiveProfile(profileId, scope = DEFAULT_SCOPE$2, slot = DEFAULT_SCOPE$2) {
|
|
359
|
-
const settings = await this.getSettings(scope);
|
|
360
|
-
const profile = this.toProfiles(settings).find((item) => item.id === profileId && item.enabled);
|
|
361
|
-
if (!profile) throw new ProfileStoreError(`Profile not found or disabled: ${profileId}`, PROFILE_STORE_ERROR_CODES.profileNotFound, { cause: {
|
|
362
|
-
profileId,
|
|
363
|
-
scope,
|
|
364
|
-
slot
|
|
365
|
-
} });
|
|
366
|
-
return this.updateConfig({ models: { [slot]: {
|
|
367
|
-
main: {
|
|
368
|
-
provider: profile.provider,
|
|
369
|
-
model: profile.model,
|
|
370
|
-
reasoningEffort: profile.reasoningEffort,
|
|
371
|
-
thinkingDisabled: settings.scope.models[slot]?.main.thinkingDisabled ?? false
|
|
372
|
-
},
|
|
373
|
-
fallbacks: settings.scope.models[slot]?.fallbacks ?? []
|
|
374
|
-
} } }, scope);
|
|
375
|
-
}
|
|
376
|
-
async upsertProfile(profile, scope = DEFAULT_SCOPE$2) {
|
|
377
|
-
const nextProfile = modelConfigSchema.parse({
|
|
378
|
-
id: profile.id,
|
|
379
|
-
label: profile.label,
|
|
380
|
-
provider: profile.provider,
|
|
381
|
-
model: profile.model,
|
|
382
|
-
reasoningEffort: profile.reasoningEffort,
|
|
383
|
-
enabled: profile.enabled
|
|
384
|
-
});
|
|
385
|
-
const settings = await this.getSettings(scope);
|
|
386
|
-
const models = settings.models.filter((item) => item.id !== nextProfile.id);
|
|
387
|
-
models.push(nextProfile);
|
|
388
|
-
const nextSettings = this.normalizeSettings({
|
|
389
|
-
providers: settings.providers,
|
|
390
|
-
models,
|
|
391
|
-
scope: settings.scope
|
|
392
|
-
});
|
|
393
|
-
await this.saveSettings(nextSettings, scope);
|
|
394
|
-
return this.toProxyConfig(nextSettings);
|
|
395
|
-
}
|
|
396
|
-
async updateConfig(update, scope = DEFAULT_SCOPE$2) {
|
|
397
|
-
const parsedUpdate = adminConfigUpdateSchema$1.parse(update);
|
|
398
|
-
const settings = await this.getSettings(scope);
|
|
399
|
-
const nextScopeModels = { ...settings.scope.models };
|
|
400
|
-
for (const slot of MODEL_SLOTS$1) {
|
|
401
|
-
const selection = parsedUpdate.models?.[slot];
|
|
402
|
-
if (selection === void 0) continue;
|
|
403
|
-
if (selection === null) {
|
|
404
|
-
delete nextScopeModels[slot];
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
nextScopeModels[slot] = selection;
|
|
408
|
-
}
|
|
409
|
-
const nextSettings = this.normalizeSettings({
|
|
410
|
-
providers: settings.providers,
|
|
411
|
-
models: settings.models,
|
|
412
|
-
scope: { models: nextScopeModels }
|
|
413
|
-
});
|
|
414
|
-
await this.saveSettings(nextSettings, scope);
|
|
415
|
-
return this.toProxyConfig(nextSettings);
|
|
416
|
-
}
|
|
417
|
-
getScopeConfigPath(scope) {
|
|
418
|
-
return path.join(this.scopeSettingsDir, `${this.sanitizeScope(scope)}${SETTINGS_EXTENSION}`);
|
|
419
|
-
}
|
|
420
|
-
sanitizeScope(scope) {
|
|
421
|
-
return scope.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "") || DEFAULT_SCOPE$2;
|
|
422
|
-
}
|
|
423
|
-
async getSettings(scope, seedConfigPath) {
|
|
424
|
-
const settings = await this.readSettings(scope, seedConfigPath);
|
|
425
|
-
const normalized = this.normalizeSettings(settings);
|
|
426
|
-
if (JSON.stringify(normalized) !== JSON.stringify(settings)) await this.saveSettings(normalized, scope);
|
|
427
|
-
return normalized;
|
|
428
|
-
}
|
|
429
|
-
async readSettings(scope, seedConfigPath) {
|
|
430
|
-
const scopeConfigPath = this.getScopeConfigPath(scope);
|
|
431
|
-
const [providerResult, modelListResult, scopeResult] = await Promise.allSettled([
|
|
432
|
-
fs$1.readFile(this.providerConfigPath, FILE_ENCODING),
|
|
433
|
-
fs$1.readFile(this.modelListPath, FILE_ENCODING),
|
|
434
|
-
fs$1.readFile(scopeConfigPath, FILE_ENCODING)
|
|
435
|
-
]);
|
|
436
|
-
try {
|
|
437
|
-
const providerRaw = providerResult.status === "fulfilled" ? providerResult.value : null;
|
|
438
|
-
const modelListRaw = modelListResult.status === "fulfilled" ? modelListResult.value : null;
|
|
439
|
-
const scopeRaw = scopeResult.status === "fulfilled" ? scopeResult.value : null;
|
|
440
|
-
const providerMissing = providerResult.status === "rejected" && this.isFileNotFoundError(providerResult.reason);
|
|
441
|
-
const modelListMissing = modelListResult.status === "rejected" && this.isFileNotFoundError(modelListResult.reason);
|
|
442
|
-
const scopeMissing = scopeResult.status === "rejected" && this.isFileNotFoundError(scopeResult.reason);
|
|
443
|
-
if (!providerRaw && providerResult.status === "rejected" && !providerMissing) throw providerResult.reason;
|
|
444
|
-
if (!modelListRaw && modelListResult.status === "rejected" && !modelListMissing) throw modelListResult.reason;
|
|
445
|
-
if (!scopeRaw && scopeResult.status === "rejected" && !scopeMissing) throw scopeResult.reason;
|
|
446
|
-
const scopeSettings = scopeRaw ? scopeSettingsSchema.parse(parse(scopeRaw)) : await this.loadScopeSeedSettings(scope, seedConfigPath);
|
|
447
|
-
const settings = this.normalizeSettings({
|
|
448
|
-
providers: providerRaw ? providerRegistrySchema.parse(parse(providerRaw)).providers : structuredClone(DEFAULT_PROVIDER_SETTINGS),
|
|
449
|
-
models: modelListRaw ? modelListSchema.parse(parse(modelListRaw)) : structuredClone(DEFAULT_MODEL_LIST),
|
|
450
|
-
scope: scopeSettings
|
|
451
|
-
});
|
|
452
|
-
if (providerMissing || modelListMissing || scopeMissing) await this.saveSettings(settings, scope);
|
|
453
|
-
return settings;
|
|
454
|
-
} catch (error) {
|
|
455
|
-
this.logger.error(`${LOG_PREFIX} Failed to read settings`, {
|
|
456
|
-
scope,
|
|
457
|
-
providerConfigPath: this.providerConfigPath,
|
|
458
|
-
modelListPath: this.modelListPath,
|
|
459
|
-
scopeConfigPath,
|
|
460
|
-
code: PROFILE_STORE_ERROR_CODES.settingsReadFailed,
|
|
461
|
-
cause: error
|
|
462
|
-
});
|
|
463
|
-
throw new ProfileStoreError(`Failed to read settings from ${this.providerConfigPath}, ${this.modelListPath}, and ${scopeConfigPath}`, PROFILE_STORE_ERROR_CODES.settingsReadFailed, { cause: error });
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
isFileNotFoundError(error) {
|
|
467
|
-
return error?.code === FILE_NOT_FOUND_ERROR_CODE;
|
|
468
|
-
}
|
|
469
|
-
async loadScopeSeedSettings(scope, seedConfigPath) {
|
|
470
|
-
const candidatePaths = [seedConfigPath, this.getDefaultScopeSeedPath(scope)].filter((candidate) => Boolean(candidate));
|
|
471
|
-
for (const candidatePath of candidatePaths) try {
|
|
472
|
-
const raw = await fs$1.readFile(candidatePath, FILE_ENCODING);
|
|
473
|
-
return scopeSettingsSchema.parse(parse(raw));
|
|
474
|
-
} catch (error) {
|
|
475
|
-
if (this.isFileNotFoundError(error)) continue;
|
|
476
|
-
this.logger.error(`${LOG_PREFIX} Failed to read scope seed settings`, {
|
|
477
|
-
scope,
|
|
478
|
-
seedConfigPath: candidatePath,
|
|
479
|
-
cause: error
|
|
480
|
-
});
|
|
481
|
-
throw error;
|
|
482
|
-
}
|
|
483
|
-
return structuredClone(DEFAULT_SCOPE_SETTINGS);
|
|
484
|
-
}
|
|
485
|
-
getDefaultScopeSeedPath(scope) {
|
|
486
|
-
const defaultScopePath = this.getScopeConfigPath(DEFAULT_SCOPE$2);
|
|
487
|
-
return scope === DEFAULT_SCOPE$2 ? null : defaultScopePath;
|
|
488
|
-
}
|
|
489
|
-
normalizeSettings(settings) {
|
|
490
|
-
const providers = this.normalizeProviders(Object.keys(settings.providers).length > 0 ? settings.providers : structuredClone(DEFAULT_PROVIDER_SETTINGS));
|
|
491
|
-
const models = settings.models.map((model) => modelConfigSchema.parse(model));
|
|
492
|
-
const defaultSelection = this.normalizeSlotSelection(settings.scope.models.default?.main ?? null, models, providers, this.getDefaultSelection(models, providers));
|
|
493
|
-
const normalizedScopeModels = { default: defaultSelection ? {
|
|
494
|
-
main: defaultSelection,
|
|
495
|
-
fallbacks: this.normalizeFallbacks(settings.scope.models.default?.fallbacks ?? [], defaultSelection, models, providers)
|
|
496
|
-
} : void 0 };
|
|
497
|
-
for (const slot of MODEL_SLOTS$1) {
|
|
498
|
-
if (slot === DEFAULT_SCOPE$2) continue;
|
|
499
|
-
const fallback = defaultSelection ?? this.getDefaultSelection(models, providers);
|
|
500
|
-
const selection = this.normalizeSlotSelection(settings.scope.models[slot]?.main ?? null, models, providers, fallback);
|
|
501
|
-
normalizedScopeModels[slot] = selection ? {
|
|
502
|
-
main: selection,
|
|
503
|
-
fallbacks: this.normalizeFallbacks(settings.scope.models[slot]?.fallbacks ?? [], selection, models, providers)
|
|
504
|
-
} : void 0;
|
|
505
|
-
}
|
|
506
|
-
return {
|
|
507
|
-
providers,
|
|
508
|
-
models,
|
|
509
|
-
scope: { models: normalizedScopeModels }
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
normalizeProviders(providers) {
|
|
513
|
-
const normalizedProviders = { ...providers };
|
|
514
|
-
const zaiProvider = normalizedProviders[DEFAULT_ZAI_PROVIDER_ID];
|
|
515
|
-
if (zaiProvider?.type === "anthropic-compatible" && zaiProvider.authTokenEnvVar === LEGACY_ZAI_AUTH_ENV) normalizedProviders[DEFAULT_ZAI_PROVIDER_ID] = {
|
|
516
|
-
...zaiProvider,
|
|
517
|
-
authTokenEnvVar: DEFAULT_ZAI_AUTH_ENV
|
|
518
|
-
};
|
|
519
|
-
return normalizedProviders;
|
|
520
|
-
}
|
|
521
|
-
getDefaultSelection(models, providers) {
|
|
522
|
-
const fallbackModel = models.find((model) => model.enabled && providers[model.provider]);
|
|
523
|
-
if (!fallbackModel) return null;
|
|
524
|
-
return {
|
|
525
|
-
provider: fallbackModel.provider,
|
|
526
|
-
model: fallbackModel.model,
|
|
527
|
-
reasoningEffort: fallbackModel.reasoningEffort,
|
|
528
|
-
thinkingDisabled: false
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
normalizeSlotSelection(selection, models, providers, fallback) {
|
|
532
|
-
if (!selection) return fallback ? { ...fallback } : null;
|
|
533
|
-
const provider = providers[selection.provider] ? selection.provider : fallback?.provider ?? null;
|
|
534
|
-
if (!provider) return null;
|
|
535
|
-
if (provider !== selection.provider) return fallback && providers[fallback.provider] ? { ...fallback } : null;
|
|
536
|
-
const catalogEntry = models.find((model) => model.enabled && model.provider === provider && model.model === selection.model);
|
|
537
|
-
return {
|
|
538
|
-
provider,
|
|
539
|
-
model: catalogEntry?.model ?? selection.model,
|
|
540
|
-
reasoningEffort: selection.reasoningEffort ?? catalogEntry?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
541
|
-
thinkingDisabled: selection.thinkingDisabled ?? false
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
normalizeFallbacks(fallbacks, main, models, providers) {
|
|
545
|
-
const normalized = [];
|
|
546
|
-
const seen = new Set([`${main.provider}:${main.model}:${main.reasoningEffort}:${main.thinkingDisabled ? "off" : "on"}`]);
|
|
547
|
-
for (const fallback of fallbacks) {
|
|
548
|
-
const nextFallback = this.normalizeSlotSelection(fallback, models, providers, null);
|
|
549
|
-
if (!nextFallback) continue;
|
|
550
|
-
const key = `${nextFallback.provider}:${nextFallback.model}:${nextFallback.reasoningEffort}:${nextFallback.thinkingDisabled ? "off" : "on"}`;
|
|
551
|
-
if (seen.has(key)) continue;
|
|
552
|
-
seen.add(key);
|
|
553
|
-
normalized.push(nextFallback);
|
|
554
|
-
}
|
|
555
|
-
return normalized;
|
|
556
|
-
}
|
|
557
|
-
toProxyConfig(settings) {
|
|
558
|
-
const profiles = this.toProfiles(settings);
|
|
559
|
-
const slotProfileIds = Object.fromEntries(MODEL_SLOTS$1.map((slot) => {
|
|
560
|
-
return [slot, this.resolveSlotConfig(settings, profiles, slot).profileId];
|
|
561
|
-
}));
|
|
562
|
-
return {
|
|
563
|
-
activeProfileId: slotProfileIds.default ?? null,
|
|
564
|
-
slotProfileIds,
|
|
565
|
-
providers: settings.providers,
|
|
566
|
-
profiles,
|
|
567
|
-
scope: settings.scope
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
resolveSlotConfig(settings, profiles, slot) {
|
|
571
|
-
const slotConfig = settings.scope.models[slot] ?? settings.scope.models.default;
|
|
572
|
-
const selection = slotConfig?.main ?? null;
|
|
573
|
-
const profile = selection ? profiles.find((item) => item.provider === selection.provider && item.model === selection.model && item.enabled) : void 0;
|
|
574
|
-
const provider = selection?.provider ? settings.providers[selection.provider] : void 0;
|
|
575
|
-
return {
|
|
576
|
-
slot,
|
|
577
|
-
profileId: profile?.id ?? null,
|
|
578
|
-
label: profile?.label ?? null,
|
|
579
|
-
provider: selection?.provider ?? null,
|
|
580
|
-
providerType: provider?.type ?? null,
|
|
581
|
-
endpoint: provider?.endpoint ?? null,
|
|
582
|
-
model: selection?.model ?? null,
|
|
583
|
-
reasoningEffort: selection?.reasoningEffort ?? profile?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
584
|
-
thinkingDisabled: selection?.thinkingDisabled ?? false,
|
|
585
|
-
fallbacks: slotConfig?.fallbacks ?? []
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
toProfiles(settings) {
|
|
589
|
-
return settings.models.map((model) => ({
|
|
590
|
-
id: model.id,
|
|
591
|
-
label: model.label,
|
|
592
|
-
provider: model.provider,
|
|
593
|
-
providerType: settings.providers[model.provider]?.type ?? null,
|
|
594
|
-
model: model.model,
|
|
595
|
-
endpoint: settings.providers[model.provider]?.endpoint ?? null,
|
|
596
|
-
reasoningEffort: model.reasoningEffort,
|
|
597
|
-
enabled: model.enabled
|
|
598
|
-
}));
|
|
599
|
-
}
|
|
600
|
-
async saveSettings(settings, scope = DEFAULT_SCOPE$2) {
|
|
601
|
-
const scopeConfigPath = this.getScopeConfigPath(scope);
|
|
602
|
-
try {
|
|
603
|
-
const directories = new Set([
|
|
604
|
-
path.dirname(this.providerConfigPath),
|
|
605
|
-
path.dirname(this.modelListPath),
|
|
606
|
-
path.dirname(scopeConfigPath)
|
|
607
|
-
]);
|
|
608
|
-
await Promise.all(Array.from(directories, (directory) => fs$1.mkdir(directory, { recursive: true })));
|
|
609
|
-
await Promise.all([
|
|
610
|
-
fs$1.writeFile(this.providerConfigPath, stringify(providerRegistrySchema.parse({ providers: settings.providers }), { indent: YAML_INDENT }), FILE_ENCODING),
|
|
611
|
-
fs$1.writeFile(this.modelListPath, stringify(modelListSchema.parse(settings.models), { indent: YAML_INDENT }), FILE_ENCODING),
|
|
612
|
-
fs$1.writeFile(scopeConfigPath, stringify(scopeSettingsSchema.parse(settings.scope), { indent: YAML_INDENT }), FILE_ENCODING)
|
|
613
|
-
]);
|
|
614
|
-
} catch (error) {
|
|
615
|
-
this.logger.error(`${LOG_PREFIX} Failed to write settings`, {
|
|
616
|
-
scope,
|
|
617
|
-
providerConfigPath: this.providerConfigPath,
|
|
618
|
-
modelListPath: this.modelListPath,
|
|
619
|
-
scopeConfigPath,
|
|
620
|
-
code: PROFILE_STORE_ERROR_CODES.settingsWriteFailed,
|
|
621
|
-
cause: error
|
|
622
|
-
});
|
|
623
|
-
throw new ProfileStoreError(`Failed to write settings to ${this.providerConfigPath}, ${this.modelListPath}, and ${scopeConfigPath}`, PROFILE_STORE_ERROR_CODES.settingsWriteFailed, { cause: error });
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
//#endregion
|
|
629
|
-
//#region src/adapters/codex/ClaudeToOpenAITransformer.ts
|
|
630
|
-
/**
|
|
631
|
-
* Claude to OpenAI Request Transformer
|
|
632
|
-
*
|
|
633
|
-
* Converts Anthropic Claude Messages API requests to OpenAI Responses API format.
|
|
634
|
-
* Handles message structure, system prompts, streaming, and other parameters.
|
|
635
|
-
*/
|
|
636
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
637
|
-
const __dirname = path.dirname(__filename);
|
|
638
|
-
var ClaudeToOpenAITransformer = class {
|
|
639
|
-
config;
|
|
640
|
-
codexAuth;
|
|
641
|
-
codexInstructions;
|
|
642
|
-
constructor(config, codexAuth) {
|
|
643
|
-
this.config = config;
|
|
644
|
-
this.codexAuth = codexAuth;
|
|
645
|
-
this.codexInstructions = this.loadCodexInstructions();
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Load the complete Codex CLI system prompt from codex.md
|
|
649
|
-
*/
|
|
650
|
-
loadCodexInstructions() {
|
|
651
|
-
const codexMdPath = path.join(__dirname, "codex.md");
|
|
652
|
-
try {
|
|
653
|
-
return fs.readFileSync(codexMdPath, "utf-8");
|
|
654
|
-
} catch (error) {
|
|
655
|
-
console.warn(`Warning: Could not load codex.md from ${codexMdPath}`);
|
|
656
|
-
return "";
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
async transform(_url, requestBody) {
|
|
660
|
-
try {
|
|
661
|
-
const claudeRequest = JSON.parse(requestBody);
|
|
662
|
-
this.config.logger?.debug("[ClaudeToOpenAI] ===== ORIGINAL CLAUDE REQUEST =====");
|
|
663
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Original body", { body: JSON.stringify(claudeRequest, null, 2) });
|
|
664
|
-
const conversationId = randomUUID();
|
|
665
|
-
const sessionId = conversationId;
|
|
666
|
-
const sessionReasoningEffort = this.config.sessionReasoningEffort;
|
|
667
|
-
const isHaikuModel = claudeRequest.model && claudeRequest.model.toLowerCase().includes("haiku");
|
|
668
|
-
const reasoningEffort = sessionReasoningEffort || (isHaikuModel ? "minimal" : "medium");
|
|
669
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Model detection and reasoning effort", {
|
|
670
|
-
originalModel: claudeRequest.model,
|
|
671
|
-
isHaikuModel,
|
|
672
|
-
sessionReasoningEffort: sessionReasoningEffort || "none",
|
|
673
|
-
finalReasoningEffort: reasoningEffort,
|
|
674
|
-
source: sessionReasoningEffort ? "session override" : "model-based"
|
|
675
|
-
});
|
|
676
|
-
const responsesRequest = {
|
|
677
|
-
model: this.config.toModel || "gpt-5",
|
|
678
|
-
stream: true,
|
|
679
|
-
store: false,
|
|
680
|
-
tool_choice: "auto",
|
|
681
|
-
parallel_tool_calls: false,
|
|
682
|
-
prompt_cache_key: conversationId
|
|
683
|
-
};
|
|
684
|
-
if (!this.config.thinkingDisabled) {
|
|
685
|
-
responsesRequest.reasoning = {
|
|
686
|
-
effort: reasoningEffort,
|
|
687
|
-
summary: "auto"
|
|
688
|
-
};
|
|
689
|
-
responsesRequest.include = ["reasoning.encrypted_content"];
|
|
690
|
-
}
|
|
691
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Thinking mode", { thinkingDisabled: this.config.thinkingDisabled ?? false });
|
|
692
|
-
responsesRequest.instructions = this.adaptInstructionsForChatGPT(this.codexInstructions);
|
|
693
|
-
const input = [];
|
|
694
|
-
let claudeSystemPrompt = "";
|
|
695
|
-
if (claudeRequest.system) {
|
|
696
|
-
const systemMessages = this.extractSystemMessages(claudeRequest.system);
|
|
697
|
-
if (systemMessages && Array.isArray(systemMessages) && systemMessages.length > 0) claudeSystemPrompt = systemMessages.map((msg) => msg.content).join("\n\n");
|
|
698
|
-
}
|
|
699
|
-
claudeSystemPrompt = this.removeClaudeCodeInstructions(claudeSystemPrompt);
|
|
700
|
-
if (claudeSystemPrompt) input.push({
|
|
701
|
-
type: "message",
|
|
702
|
-
role: "user",
|
|
703
|
-
content: [{
|
|
704
|
-
type: "input_text",
|
|
705
|
-
text: claudeSystemPrompt
|
|
706
|
-
}]
|
|
707
|
-
});
|
|
708
|
-
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) for (const msg of claudeRequest.messages) {
|
|
709
|
-
const converted = this.convertMessageToInput(msg);
|
|
710
|
-
if (Array.isArray(converted)) input.push(...converted);
|
|
711
|
-
else if (converted) input.push(converted);
|
|
712
|
-
}
|
|
713
|
-
responsesRequest.input = input;
|
|
714
|
-
if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
|
|
715
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Original Claude tools", { tools: JSON.stringify(claudeRequest.tools, null, 2) });
|
|
716
|
-
const convertedTools = this.convertTools(claudeRequest.tools);
|
|
717
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Converted tools", { tools: JSON.stringify(convertedTools, null, 2) });
|
|
718
|
-
if (convertedTools.length > 0) {
|
|
719
|
-
responsesRequest.tools = convertedTools;
|
|
720
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Added tools to responsesRequest", { toolCount: convertedTools.length });
|
|
721
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Verify responsesRequest.tools exists", {
|
|
722
|
-
hasTools: !!responsesRequest.tools,
|
|
723
|
-
toolsLength: responsesRequest.tools?.length,
|
|
724
|
-
keys: Object.keys(responsesRequest)
|
|
725
|
-
});
|
|
726
|
-
} else this.config.logger?.warn("[ClaudeToOpenAI] No valid tools after conversion, omitting tools field");
|
|
727
|
-
} else this.config.logger?.debug("[ClaudeToOpenAI] No tools in Claude request", {
|
|
728
|
-
hasTools: !!claudeRequest.tools,
|
|
729
|
-
isArray: Array.isArray(claudeRequest.tools)
|
|
730
|
-
});
|
|
731
|
-
const targetUrl = this.config.toEndpoint || "https://chatgpt.com/backend-api/codex/responses";
|
|
732
|
-
const headers = {
|
|
733
|
-
version: "0.46.0",
|
|
734
|
-
"openai-beta": "responses=experimental",
|
|
735
|
-
conversation_id: conversationId,
|
|
736
|
-
session_id: sessionId,
|
|
737
|
-
accept: "text/event-stream",
|
|
738
|
-
"content-type": "application/json",
|
|
739
|
-
"user-agent": "codex_cli_rs/0.46.0 (Mac OS 15.6.0; arm64) iTerm.app/3.6.2",
|
|
740
|
-
originator: "codex_cli_rs"
|
|
741
|
-
};
|
|
742
|
-
if (this.codexAuth) {
|
|
743
|
-
const accessToken = await this.codexAuth.getAccessToken();
|
|
744
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Raw access token", { token: accessToken?.substring(0, 30) + "..." });
|
|
745
|
-
if (accessToken) if (accessToken.startsWith("Bearer ")) {
|
|
746
|
-
headers.authorization = accessToken;
|
|
747
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Token already has Bearer prefix, using as-is");
|
|
748
|
-
} else {
|
|
749
|
-
headers.authorization = `Bearer ${accessToken}`;
|
|
750
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Added Bearer prefix to token");
|
|
751
|
-
}
|
|
752
|
-
const accountId = await this.codexAuth.getAccountId();
|
|
753
|
-
if (accountId) headers["chatgpt-account-id"] = accountId;
|
|
754
|
-
} else if (this.config.toApiKey) headers.authorization = `Bearer ${this.config.toApiKey}`;
|
|
755
|
-
this.config.logger?.debug("[ClaudeToOpenAI] ===== REQUEST DETAILS =====");
|
|
756
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Target URL", { targetUrl });
|
|
757
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Headers", { headers: {
|
|
758
|
-
...headers,
|
|
759
|
-
authorization: headers.authorization ? `${headers.authorization.substring(0, 30)}...` : void 0
|
|
760
|
-
} });
|
|
761
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Body", { body: JSON.stringify(responsesRequest, null, 2) });
|
|
762
|
-
this.config.logger?.debug("[ClaudeToOpenAI] ===============================");
|
|
763
|
-
this.config.logger?.debug("[ClaudeToOpenAI] ===== FINAL TRANSFORMED REQUEST =====");
|
|
764
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Pre-final check - responsesRequest.tools", {
|
|
765
|
-
hasTools: !!responsesRequest.tools,
|
|
766
|
-
toolsLength: responsesRequest.tools?.length,
|
|
767
|
-
keys: Object.keys(responsesRequest)
|
|
768
|
-
});
|
|
769
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Final body", { body: JSON.stringify(responsesRequest, null, 2) });
|
|
770
|
-
this.config.logger?.debug("[ClaudeToOpenAI] ===== END FINAL TRANSFORMED REQUEST =====");
|
|
771
|
-
return {
|
|
772
|
-
url: targetUrl,
|
|
773
|
-
body: JSON.stringify(responsesRequest),
|
|
774
|
-
headers
|
|
775
|
-
};
|
|
776
|
-
} catch (error) {
|
|
777
|
-
throw new Error(`Failed to transform Claude request to OpenAI: ${error}`);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Adapt Codex instructions for ChatGPT by replacing Claude-specific model information
|
|
782
|
-
*/
|
|
783
|
-
adaptInstructionsForChatGPT(instructions) {
|
|
784
|
-
let adapted = instructions;
|
|
785
|
-
adapted = adapted.replace(/You are powered by the model named Sonnet 4\.5\. The exact model ID is claude-sonnet-4-5-\d+\./g, "You are powered by ChatGPT (GPT-5 reasoning model).");
|
|
786
|
-
adapted = adapted.replace(/Assistant knowledge cutoff is January 2025/g, "Assistant knowledge cutoff is October 2023");
|
|
787
|
-
adapted = adapted.replace(/\bClaude\b/g, "ChatGPT");
|
|
788
|
-
adapted = adapted.replace(/\bAnthropic\b/g, "OpenAI");
|
|
789
|
-
return adapted;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Remove Claude Code-specific instructions from system messages
|
|
793
|
-
* These should not be sent to ChatGPT as user messages
|
|
794
|
-
*/
|
|
795
|
-
removeClaudeCodeInstructions(text) {
|
|
796
|
-
const claudeCodePatterns = [/You are Claude Code, Anthropic's official CLI for Claude\.[\s\S]*?claude_code_docs_map\.md/, /You are Claude Code[\s\S]*?using Claude Code\n/];
|
|
797
|
-
let filtered = text;
|
|
798
|
-
for (const pattern of claudeCodePatterns) filtered = filtered.replace(pattern, "");
|
|
799
|
-
filtered = filtered.replace(/\n{3,}/g, "\n\n").trim();
|
|
800
|
-
return filtered;
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Extract and convert system messages from Claude format
|
|
804
|
-
*/
|
|
805
|
-
extractSystemMessages(system) {
|
|
806
|
-
const messages = [];
|
|
807
|
-
if (typeof system === "string") messages.push({
|
|
808
|
-
role: "system",
|
|
809
|
-
content: system
|
|
810
|
-
});
|
|
811
|
-
else if (Array.isArray(system)) {
|
|
812
|
-
for (const item of system) if (typeof item === "string") messages.push({
|
|
813
|
-
role: "system",
|
|
814
|
-
content: item
|
|
815
|
-
});
|
|
816
|
-
else if (item.type === "text" && item.text) messages.push({
|
|
817
|
-
role: "system",
|
|
818
|
-
content: item.text
|
|
819
|
-
});
|
|
820
|
-
} else if (system.type === "text" && system.text) messages.push({
|
|
821
|
-
role: "system",
|
|
822
|
-
content: system.text
|
|
823
|
-
});
|
|
824
|
-
return messages;
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Convert a single Claude message to Responses API input format
|
|
828
|
-
* Returns a single message or array of messages (for tool results)
|
|
829
|
-
*/
|
|
830
|
-
convertMessageToInput(msg) {
|
|
831
|
-
if (!msg.role || !msg.content) return null;
|
|
832
|
-
const textType = msg.role === "assistant" ? "output_text" : "input_text";
|
|
833
|
-
if (Array.isArray(msg.content)) {
|
|
834
|
-
const toolResults = [];
|
|
835
|
-
const toolCalls = [];
|
|
836
|
-
const textContent = [];
|
|
837
|
-
for (const block of msg.content) if (block.type === "tool_result") {
|
|
838
|
-
const resultText = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
839
|
-
toolResults.push({
|
|
840
|
-
type: "function_call_output",
|
|
841
|
-
call_id: block.tool_use_id,
|
|
842
|
-
output: resultText
|
|
843
|
-
});
|
|
844
|
-
} else if (block.type === "text") textContent.push({
|
|
845
|
-
type: textType,
|
|
846
|
-
text: block.text || ""
|
|
847
|
-
});
|
|
848
|
-
else if (block.type === "image") if (block.source && block.source.type === "base64" && block.source.data) {
|
|
849
|
-
const mediaType = block.source.media_type || "image/jpeg";
|
|
850
|
-
const imageUrl = `data:${mediaType};base64,${block.source.data}`;
|
|
851
|
-
textContent.push({
|
|
852
|
-
type: "input_image",
|
|
853
|
-
image_url: imageUrl
|
|
854
|
-
});
|
|
855
|
-
this.config.logger?.debug("[ClaudeToOpenAI] Converted image block", {
|
|
856
|
-
mediaType,
|
|
857
|
-
dataLength: block.source.data.length
|
|
858
|
-
});
|
|
859
|
-
} else this.config.logger?.warn("[ClaudeToOpenAI] Unsupported image format", { source: block.source });
|
|
860
|
-
else if (block.type === "tool_use") toolCalls.push({
|
|
861
|
-
type: "function_call",
|
|
862
|
-
call_id: block.id,
|
|
863
|
-
name: block.name,
|
|
864
|
-
arguments: JSON.stringify(block.input || {})
|
|
865
|
-
});
|
|
866
|
-
const result = [];
|
|
867
|
-
if (textContent.length > 0) result.push({
|
|
868
|
-
type: "message",
|
|
869
|
-
role: msg.role,
|
|
870
|
-
content: textContent
|
|
871
|
-
});
|
|
872
|
-
if (toolCalls.length > 0) result.push(...toolCalls);
|
|
873
|
-
if (toolResults.length > 0) result.push(...toolResults);
|
|
874
|
-
if (result.length > 1) return result;
|
|
875
|
-
else if (result.length === 1) return result[0];
|
|
876
|
-
return null;
|
|
877
|
-
} else if (typeof msg.content === "string") return {
|
|
878
|
-
type: "message",
|
|
879
|
-
role: msg.role,
|
|
880
|
-
content: [{
|
|
881
|
-
type: textType,
|
|
882
|
-
text: msg.content
|
|
883
|
-
}]
|
|
884
|
-
};
|
|
885
|
-
return null;
|
|
886
|
-
}
|
|
887
|
-
/**
|
|
888
|
-
* Convert Claude tools to ChatGPT Responses API format
|
|
889
|
-
*
|
|
890
|
-
* The Responses API uses a flat structure with type at the top level:
|
|
891
|
-
* { type: "function", name: "...", description: "...", parameters: {...} }
|
|
892
|
-
*/
|
|
893
|
-
convertTools(tools) {
|
|
894
|
-
if (!tools || !Array.isArray(tools)) return [];
|
|
895
|
-
return tools.filter((tool) => {
|
|
896
|
-
if (!tool || typeof tool !== "object") return false;
|
|
897
|
-
if (!tool.name) return false;
|
|
898
|
-
return true;
|
|
899
|
-
}).map((tool) => {
|
|
900
|
-
return {
|
|
901
|
-
type: "function",
|
|
902
|
-
name: tool.name,
|
|
903
|
-
description: tool.description || "",
|
|
904
|
-
parameters: tool.input_schema || tool.parameters || {}
|
|
905
|
-
};
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
//#endregion
|
|
911
|
-
//#region src/adapters/codex/CodexAuth.ts
|
|
912
|
-
var CodexAuth = class CodexAuth {
|
|
913
|
-
static TOKEN_REFRESH_URL = "https://auth.openai.com/oauth/token";
|
|
914
|
-
static CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
915
|
-
constructor(logger = consoleLogger, authFilePath = path.join(process.env.HOME || "", ".codex", "auth.json")) {
|
|
916
|
-
this.logger = logger;
|
|
917
|
-
this.authFilePath = authFilePath;
|
|
918
|
-
}
|
|
919
|
-
getAuthFilePath() {
|
|
920
|
-
return this.authFilePath;
|
|
921
|
-
}
|
|
922
|
-
async getAuthStatus() {
|
|
923
|
-
const authData = this.readAuthFile();
|
|
924
|
-
return {
|
|
925
|
-
configured: Boolean(authData?.tokens?.refresh_token),
|
|
926
|
-
accountId: authData?.tokens?.account_id ?? null,
|
|
927
|
-
authFilePath: this.authFilePath,
|
|
928
|
-
lastRefresh: authData?.last_refresh ?? null
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
async getAccountId() {
|
|
932
|
-
return this.readAuthFile()?.tokens?.account_id ?? null;
|
|
933
|
-
}
|
|
934
|
-
async getAccessToken() {
|
|
935
|
-
const authData = this.readAuthFile();
|
|
936
|
-
if (!authData?.tokens) {
|
|
937
|
-
this.logger.warn("[CodexAuth] No tokens found in auth file");
|
|
938
|
-
return null;
|
|
939
|
-
}
|
|
940
|
-
let accessToken = authData.tokens.access_token;
|
|
941
|
-
if (accessToken.startsWith("Bearer ")) accessToken = accessToken.slice(7);
|
|
942
|
-
if (!this.isTokenExpired(accessToken)) return accessToken;
|
|
943
|
-
const refreshed = await this.refreshAccessToken(authData.tokens.refresh_token);
|
|
944
|
-
if (!refreshed) return null;
|
|
945
|
-
this.saveTokens(refreshed);
|
|
946
|
-
return refreshed.access_token;
|
|
947
|
-
}
|
|
948
|
-
readAuthFile() {
|
|
949
|
-
try {
|
|
950
|
-
if (!fs.existsSync(this.authFilePath)) return null;
|
|
951
|
-
return JSON.parse(fs.readFileSync(this.authFilePath, "utf8"));
|
|
952
|
-
} catch (error) {
|
|
953
|
-
this.logger.error("[CodexAuth] Failed to read auth file", error);
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
isTokenExpired(token) {
|
|
958
|
-
const payload = this.decodeJWT(token);
|
|
959
|
-
if (!payload?.exp) return true;
|
|
960
|
-
return payload.exp * 1e3 - Date.now() < 300 * 1e3;
|
|
961
|
-
}
|
|
962
|
-
decodeJWT(token) {
|
|
963
|
-
try {
|
|
964
|
-
const [, payload] = token.split(".");
|
|
965
|
-
if (!payload) return null;
|
|
966
|
-
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
967
|
-
} catch (error) {
|
|
968
|
-
this.logger.error("[CodexAuth] Failed to decode JWT", error);
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
async refreshAccessToken(refreshToken) {
|
|
973
|
-
try {
|
|
974
|
-
const response = await fetch(CodexAuth.TOKEN_REFRESH_URL, {
|
|
975
|
-
method: "POST",
|
|
976
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
977
|
-
body: new URLSearchParams({
|
|
978
|
-
grant_type: "refresh_token",
|
|
979
|
-
refresh_token: refreshToken,
|
|
980
|
-
client_id: CodexAuth.CLIENT_ID
|
|
981
|
-
})
|
|
982
|
-
});
|
|
983
|
-
if (!response.ok) {
|
|
984
|
-
this.logger.error("[CodexAuth] Token refresh failed", await response.text());
|
|
985
|
-
return null;
|
|
986
|
-
}
|
|
987
|
-
const data = await response.json();
|
|
988
|
-
return {
|
|
989
|
-
id_token: data.id_token ?? "",
|
|
990
|
-
access_token: (data.access_token ?? "").replace(/^Bearer\s+/i, ""),
|
|
991
|
-
refresh_token: data.refresh_token || refreshToken,
|
|
992
|
-
account_id: data.account_id ?? ""
|
|
993
|
-
};
|
|
994
|
-
} catch (error) {
|
|
995
|
-
this.logger.error("[CodexAuth] Failed to refresh token", error);
|
|
996
|
-
return null;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
saveTokens(tokens) {
|
|
1000
|
-
try {
|
|
1001
|
-
const authData = this.readAuthFile();
|
|
1002
|
-
if (!authData) return;
|
|
1003
|
-
authData.tokens = tokens;
|
|
1004
|
-
authData.last_refresh = (/* @__PURE__ */ new Date()).toISOString();
|
|
1005
|
-
fs.writeFileSync(this.authFilePath, JSON.stringify(authData, null, 2), "utf8");
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
this.logger.error("[CodexAuth] Failed to save tokens", error);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
|
-
//#endregion
|
|
1013
|
-
//#region src/adapters/codex/OpenAIToClaudeTransformer.ts
|
|
1014
|
-
/**
|
|
1015
|
-
* OpenAI to Claude Response Transformer
|
|
1016
|
-
*
|
|
1017
|
-
* Converts OpenAI Chat Completions API streaming responses (SSE format)
|
|
1018
|
-
* to Anthropic Claude Messages API streaming format.
|
|
1019
|
-
*
|
|
1020
|
-
* OpenAI SSE events: data: {"choices":[{"delta":{"content":"text"}}]}
|
|
1021
|
-
* Claude SSE events: event: content_block_delta\ndata: {"delta":{"type":"text","text":"text"}}
|
|
1022
|
-
*/
|
|
1023
|
-
var OpenAIToClaudeTransformer = class {
|
|
1024
|
-
logger;
|
|
1025
|
-
thinkingDisabled;
|
|
1026
|
-
constructor(logger, thinkingDisabled = false) {
|
|
1027
|
-
this.logger = logger;
|
|
1028
|
-
this.thinkingDisabled = thinkingDisabled;
|
|
1029
|
-
}
|
|
1030
|
-
transform(responseBody) {
|
|
1031
|
-
try {
|
|
1032
|
-
if (!responseBody || responseBody.trim() === "") return this.createEmptyClaudeResponse();
|
|
1033
|
-
if (!responseBody.includes("data:")) return this.convertNonStreamingResponse(responseBody);
|
|
1034
|
-
return this.convertStreamingResponse(responseBody);
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
this.logger?.error("[OpenAIToClaude] ERROR in transform", error);
|
|
1037
|
-
return this.createEmptyClaudeResponse();
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
/**
|
|
1041
|
-
* Create a valid empty Claude streaming response to prevent parsing errors
|
|
1042
|
-
*/
|
|
1043
|
-
createEmptyClaudeResponse() {
|
|
1044
|
-
const messageId = `msg_${ulid()}`;
|
|
1045
|
-
const events = [];
|
|
1046
|
-
events.push("event: message_start");
|
|
1047
|
-
events.push(`data: ${JSON.stringify({
|
|
1048
|
-
type: "message_start",
|
|
1049
|
-
message: {
|
|
1050
|
-
id: messageId,
|
|
1051
|
-
type: "message",
|
|
1052
|
-
role: "assistant",
|
|
1053
|
-
content: [],
|
|
1054
|
-
model: "gpt-5",
|
|
1055
|
-
stop_reason: null,
|
|
1056
|
-
stop_sequence: null,
|
|
1057
|
-
usage: {
|
|
1058
|
-
input_tokens: 0,
|
|
1059
|
-
output_tokens: 0
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
})}`);
|
|
1063
|
-
events.push("");
|
|
1064
|
-
events.push("event: message_stop");
|
|
1065
|
-
events.push("data: {\"type\":\"message_stop\"}");
|
|
1066
|
-
events.push("");
|
|
1067
|
-
return events.join("\n");
|
|
1068
|
-
}
|
|
1069
|
-
convertStreamingResponse(sseText) {
|
|
1070
|
-
const parsed = this.parseOpenAIStream(sseText);
|
|
1071
|
-
const claudeStream = this.createClaudeStreamFromParsed(parsed);
|
|
1072
|
-
const fallbackMessage = parsed.errorMessage || "Empty streaming response from provider";
|
|
1073
|
-
return this.ensureValidClaudeStream(claudeStream, fallbackMessage);
|
|
1074
|
-
}
|
|
1075
|
-
parseOpenAIStream(sseText) {
|
|
1076
|
-
const parsed = {
|
|
1077
|
-
textSegments: [],
|
|
1078
|
-
thinkingSegments: [],
|
|
1079
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
1080
|
-
model: "gpt-5",
|
|
1081
|
-
inputTokens: 0,
|
|
1082
|
-
outputTokens: 0,
|
|
1083
|
-
cachedTokens: 0,
|
|
1084
|
-
reasoningTokens: 0,
|
|
1085
|
-
reasoningEffort: void 0,
|
|
1086
|
-
stopReason: void 0,
|
|
1087
|
-
errorMessage: void 0
|
|
1088
|
-
};
|
|
1089
|
-
const lines = sseText.split("\n");
|
|
1090
|
-
let currentEvent = "";
|
|
1091
|
-
const isResponsesApi = /event:\s*response\./i.test(sseText) || /"type"\s*:\s*"response\./i.test(sseText) || /"response"\s*:\s*\{/i.test(sseText);
|
|
1092
|
-
for (const rawLine of lines) {
|
|
1093
|
-
const line = rawLine.trim();
|
|
1094
|
-
if (!line) continue;
|
|
1095
|
-
if (line.startsWith("event:")) {
|
|
1096
|
-
currentEvent = line.slice(6).trim();
|
|
1097
|
-
continue;
|
|
1098
|
-
}
|
|
1099
|
-
if (!line.startsWith("data:")) continue;
|
|
1100
|
-
const dataContent = line.slice(5).trim();
|
|
1101
|
-
if (!dataContent || dataContent === "[DONE]") continue;
|
|
1102
|
-
let data;
|
|
1103
|
-
try {
|
|
1104
|
-
data = JSON.parse(dataContent);
|
|
1105
|
-
} catch {
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
if (data?.error) {
|
|
1109
|
-
parsed.errorMessage = data.error?.message || data.error?.error || (typeof data.error === "string" ? data.error : "Unexpected API error");
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
if (isResponsesApi || currentEvent.startsWith("response.")) this.handleResponsesEvent(currentEvent, data, parsed);
|
|
1113
|
-
else this.handleChatCompletionChunk(data, parsed);
|
|
1114
|
-
}
|
|
1115
|
-
return parsed;
|
|
1116
|
-
}
|
|
1117
|
-
handleResponsesEvent(eventName, data, parsed) {
|
|
1118
|
-
const eventType = typeof data?.type === "string" ? data.type : eventName;
|
|
1119
|
-
if (data?.model && typeof data.model === "string") parsed.model = data.model;
|
|
1120
|
-
if (data?.usage) {
|
|
1121
|
-
parsed.inputTokens = data.usage.input_tokens ?? data.usage.prompt_tokens ?? parsed.inputTokens;
|
|
1122
|
-
parsed.outputTokens = data.usage.output_tokens ?? data.usage.completion_tokens ?? parsed.outputTokens;
|
|
1123
|
-
}
|
|
1124
|
-
switch (eventType) {
|
|
1125
|
-
case "response.created":
|
|
1126
|
-
if (data?.response?.model) parsed.model = data.response.model;
|
|
1127
|
-
break;
|
|
1128
|
-
case "response.reasoning.delta":
|
|
1129
|
-
case "response.reasoning_summary_text.delta":
|
|
1130
|
-
case "response.function_call_arguments.delta":
|
|
1131
|
-
case "response.function_call_arguments.done":
|
|
1132
|
-
case "response.in_progress":
|
|
1133
|
-
case "response.output_item.added":
|
|
1134
|
-
case "response.output_item.done":
|
|
1135
|
-
case "response.content_part.added":
|
|
1136
|
-
case "response.content_part.done":
|
|
1137
|
-
case "response.reasoning_summary_part.added":
|
|
1138
|
-
case "response.reasoning_summary_part.done":
|
|
1139
|
-
case "response.reasoning_summary_text.done": break;
|
|
1140
|
-
case "response.output_text.delta":
|
|
1141
|
-
case "response.delta":
|
|
1142
|
-
if (data?.delta?.tool_calls) {}
|
|
1143
|
-
break;
|
|
1144
|
-
case "response.output_text.done": break;
|
|
1145
|
-
case "response.completed":
|
|
1146
|
-
if (data?.response?.model) parsed.model = data.response.model;
|
|
1147
|
-
if (data?.response?.usage) {
|
|
1148
|
-
parsed.inputTokens = data.response.usage.input_tokens ?? data.response.usage.prompt_tokens ?? parsed.inputTokens;
|
|
1149
|
-
parsed.outputTokens = data.response.usage.output_tokens ?? data.response.usage.completion_tokens ?? parsed.outputTokens;
|
|
1150
|
-
if (data.response.usage.input_tokens_details?.cached_tokens) parsed.cachedTokens = data.response.usage.input_tokens_details.cached_tokens;
|
|
1151
|
-
if (data.response.usage.output_tokens_details?.reasoning_tokens) parsed.reasoningTokens = data.response.usage.output_tokens_details.reasoning_tokens;
|
|
1152
|
-
}
|
|
1153
|
-
if (data?.response?.reasoning?.effort) parsed.reasoningEffort = data.response.reasoning.effort;
|
|
1154
|
-
if (Array.isArray(data?.response?.tool_calls)) this.collectToolCalls(data.response.tool_calls, parsed.toolCalls);
|
|
1155
|
-
if (Array.isArray(data?.response?.output)) {
|
|
1156
|
-
for (const output of data.response.output) if (output?.type === "reasoning") {
|
|
1157
|
-
if (Array.isArray(output?.summary)) {
|
|
1158
|
-
for (const summaryItem of output.summary) if (summaryItem?.type === "summary_text" && summaryItem?.text) parsed.thinkingSegments.push(summaryItem.text);
|
|
1159
|
-
}
|
|
1160
|
-
} else if (output?.type === "message") {
|
|
1161
|
-
if (Array.isArray(output?.content)) {
|
|
1162
|
-
for (const contentItem of output.content) if ((contentItem?.type === "output_text" || contentItem?.type === "text") && contentItem?.text) parsed.textSegments.push(contentItem.text);
|
|
1163
|
-
}
|
|
1164
|
-
} else if (output?.type === "function_call") {
|
|
1165
|
-
const toolCallData = {
|
|
1166
|
-
index: parsed.toolCalls.size,
|
|
1167
|
-
id: output.id || `tool_${ulid()}`,
|
|
1168
|
-
function: {
|
|
1169
|
-
name: output.name,
|
|
1170
|
-
arguments: output.arguments
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
this.collectToolCalls([toolCallData], parsed.toolCalls);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
if (data?.response?.output_text) this.collectTextFromNode(data.response.output_text, parsed.textSegments);
|
|
1177
|
-
parsed.stopReason = this.mapResponseStatusToStopReason(data?.response?.status);
|
|
1178
|
-
break;
|
|
1179
|
-
case "response.error":
|
|
1180
|
-
parsed.errorMessage = data?.error?.message || data?.message || "Unexpected API error";
|
|
1181
|
-
break;
|
|
1182
|
-
default:
|
|
1183
|
-
if (data?.delta) {
|
|
1184
|
-
this.collectTextFromNode(data.delta, parsed.textSegments);
|
|
1185
|
-
if (data.delta.tool_calls) this.collectToolCalls(data.delta.tool_calls, parsed.toolCalls);
|
|
1186
|
-
}
|
|
1187
|
-
break;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
handleChatCompletionChunk(chunk, parsed) {
|
|
1191
|
-
if (!chunk) return;
|
|
1192
|
-
if (chunk.model && typeof chunk.model === "string") parsed.model = chunk.model;
|
|
1193
|
-
if (chunk.usage) {
|
|
1194
|
-
parsed.inputTokens = chunk.usage.prompt_tokens ?? parsed.inputTokens;
|
|
1195
|
-
parsed.outputTokens = chunk.usage.completion_tokens ?? parsed.outputTokens;
|
|
1196
|
-
}
|
|
1197
|
-
if (!Array.isArray(chunk.choices)) return;
|
|
1198
|
-
for (const choice of chunk.choices) {
|
|
1199
|
-
if (choice?.delta) {
|
|
1200
|
-
this.collectTextFromNode(choice.delta, parsed.textSegments);
|
|
1201
|
-
if (choice.delta.tool_calls) this.collectToolCalls(choice.delta.tool_calls, parsed.toolCalls);
|
|
1202
|
-
}
|
|
1203
|
-
if (choice?.message?.content) this.collectTextFromNode(choice.message.content, parsed.textSegments);
|
|
1204
|
-
if (choice?.finish_reason) parsed.stopReason = this.mapFinishReason(choice.finish_reason);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
collectTextFromNode(node, collector) {
|
|
1208
|
-
if (node == null) return;
|
|
1209
|
-
if (typeof node === "string") {
|
|
1210
|
-
if (node.length > 0) collector.push(node);
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
if (Array.isArray(node)) {
|
|
1214
|
-
for (const item of node) this.collectTextFromNode(item, collector);
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
if (typeof node !== "object") return;
|
|
1218
|
-
if (typeof node.text === "string") collector.push(node.text);
|
|
1219
|
-
if (typeof node.output_text === "string") collector.push(node.output_text);
|
|
1220
|
-
if (typeof node.value === "string") collector.push(node.value);
|
|
1221
|
-
if (typeof node.delta === "string") collector.push(node.delta);
|
|
1222
|
-
else if (node.delta) this.collectTextFromNode(node.delta, collector);
|
|
1223
|
-
if (node.token && typeof node.token.text === "string") collector.push(node.token.text);
|
|
1224
|
-
for (const key of [
|
|
1225
|
-
"content",
|
|
1226
|
-
"output",
|
|
1227
|
-
"output_text",
|
|
1228
|
-
"message",
|
|
1229
|
-
"choices",
|
|
1230
|
-
"segments"
|
|
1231
|
-
]) if (node[key] !== void 0) if (key === "choices" && Array.isArray(node[key])) for (const choice of node[key]) {
|
|
1232
|
-
if (choice?.message?.content) this.collectTextFromNode(choice.message.content, collector);
|
|
1233
|
-
if (choice?.delta) this.collectTextFromNode(choice.delta, collector);
|
|
1234
|
-
}
|
|
1235
|
-
else this.collectTextFromNode(node[key], collector);
|
|
1236
|
-
}
|
|
1237
|
-
collectToolCalls(rawToolCalls, toolCallMap) {
|
|
1238
|
-
if (!rawToolCalls) return;
|
|
1239
|
-
const callsArray = Array.isArray(rawToolCalls) ? rawToolCalls : [rawToolCalls];
|
|
1240
|
-
for (const call of callsArray) {
|
|
1241
|
-
if (!call) continue;
|
|
1242
|
-
const index = typeof call.index === "number" ? call.index : toolCallMap.size;
|
|
1243
|
-
const existing = toolCallMap.get(index) || {
|
|
1244
|
-
id: "",
|
|
1245
|
-
name: "",
|
|
1246
|
-
argumentChunks: []
|
|
1247
|
-
};
|
|
1248
|
-
if (typeof call.id === "string" && !existing.id) existing.id = call.id;
|
|
1249
|
-
const functionName = call.function?.name;
|
|
1250
|
-
if (typeof functionName === "string" && functionName.length > 0) existing.name = functionName;
|
|
1251
|
-
const functionArgs = call.function?.arguments;
|
|
1252
|
-
if (typeof functionArgs === "string" && functionArgs.length > 0) existing.argumentChunks.push(functionArgs);
|
|
1253
|
-
toolCallMap.set(index, existing);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
createClaudeStreamFromParsed(parsed) {
|
|
1257
|
-
if (parsed.errorMessage) return this.createClaudeErrorStream(parsed.errorMessage);
|
|
1258
|
-
const thinkingSegments = this.thinkingDisabled ? [] : this.mergeAndChunkSegments(parsed.thinkingSegments);
|
|
1259
|
-
const textSegments = this.mergeAndChunkSegments(parsed.textSegments);
|
|
1260
|
-
const toolCalls = Array.from(parsed.toolCalls.entries()).sort((a, b) => a[0] - b[0]).map(([index, value]) => ({
|
|
1261
|
-
index,
|
|
1262
|
-
id: value.id,
|
|
1263
|
-
name: value.name,
|
|
1264
|
-
arguments: value.argumentChunks.join("")
|
|
1265
|
-
})).filter((call) => call.name);
|
|
1266
|
-
if (thinkingSegments.length === 0 && textSegments.length === 0 && toolCalls.length === 0) return this.createClaudeErrorStream("Empty streaming response from provider");
|
|
1267
|
-
const messageId = `msg_${ulid()}`;
|
|
1268
|
-
const model = parsed.model || "gpt-5";
|
|
1269
|
-
const events = [];
|
|
1270
|
-
events.push("event: message_start");
|
|
1271
|
-
events.push(`data: ${JSON.stringify({
|
|
1272
|
-
type: "message_start",
|
|
1273
|
-
message: {
|
|
1274
|
-
id: messageId,
|
|
1275
|
-
type: "message",
|
|
1276
|
-
role: "assistant",
|
|
1277
|
-
content: [],
|
|
1278
|
-
model,
|
|
1279
|
-
stop_reason: null,
|
|
1280
|
-
stop_sequence: null,
|
|
1281
|
-
usage: {
|
|
1282
|
-
input_tokens: parsed.inputTokens ?? 0,
|
|
1283
|
-
output_tokens: 0
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
})}`);
|
|
1287
|
-
events.push("");
|
|
1288
|
-
let blockIndex = 0;
|
|
1289
|
-
if (thinkingSegments.length > 0) {
|
|
1290
|
-
events.push("event: content_block_start");
|
|
1291
|
-
events.push(`data: ${JSON.stringify({
|
|
1292
|
-
type: "content_block_start",
|
|
1293
|
-
index: blockIndex,
|
|
1294
|
-
content_block: {
|
|
1295
|
-
type: "thinking",
|
|
1296
|
-
thinking: ""
|
|
1297
|
-
}
|
|
1298
|
-
})}`);
|
|
1299
|
-
events.push("");
|
|
1300
|
-
for (const chunk of thinkingSegments) {
|
|
1301
|
-
if (!chunk) continue;
|
|
1302
|
-
events.push("event: content_block_delta");
|
|
1303
|
-
events.push(`data: ${JSON.stringify({
|
|
1304
|
-
type: "content_block_delta",
|
|
1305
|
-
index: blockIndex,
|
|
1306
|
-
delta: {
|
|
1307
|
-
type: "thinking_delta",
|
|
1308
|
-
thinking: chunk
|
|
1309
|
-
}
|
|
1310
|
-
})}`);
|
|
1311
|
-
events.push("");
|
|
1312
|
-
}
|
|
1313
|
-
events.push("event: content_block_stop");
|
|
1314
|
-
events.push(`data: ${JSON.stringify({
|
|
1315
|
-
type: "content_block_stop",
|
|
1316
|
-
index: blockIndex
|
|
1317
|
-
})}`);
|
|
1318
|
-
events.push("");
|
|
1319
|
-
blockIndex += 1;
|
|
1320
|
-
}
|
|
1321
|
-
if (textSegments.length > 0) {
|
|
1322
|
-
events.push("event: content_block_start");
|
|
1323
|
-
events.push(`data: ${JSON.stringify({
|
|
1324
|
-
type: "content_block_start",
|
|
1325
|
-
index: blockIndex,
|
|
1326
|
-
content_block: {
|
|
1327
|
-
type: "text",
|
|
1328
|
-
text: ""
|
|
1329
|
-
}
|
|
1330
|
-
})}`);
|
|
1331
|
-
events.push("");
|
|
1332
|
-
for (const chunk of textSegments) {
|
|
1333
|
-
if (!chunk) continue;
|
|
1334
|
-
events.push("event: content_block_delta");
|
|
1335
|
-
events.push(`data: ${JSON.stringify({
|
|
1336
|
-
type: "content_block_delta",
|
|
1337
|
-
index: blockIndex,
|
|
1338
|
-
delta: {
|
|
1339
|
-
type: "text_delta",
|
|
1340
|
-
text: chunk
|
|
1341
|
-
}
|
|
1342
|
-
})}`);
|
|
1343
|
-
events.push("");
|
|
1344
|
-
}
|
|
1345
|
-
events.push("event: content_block_stop");
|
|
1346
|
-
events.push(`data: ${JSON.stringify({
|
|
1347
|
-
type: "content_block_stop",
|
|
1348
|
-
index: blockIndex
|
|
1349
|
-
})}`);
|
|
1350
|
-
events.push("");
|
|
1351
|
-
blockIndex += 1;
|
|
1352
|
-
}
|
|
1353
|
-
for (const toolCall of toolCalls) {
|
|
1354
|
-
const toolIndex = blockIndex + toolCall.index;
|
|
1355
|
-
const toolId = toolCall.id || `tool_${ulid()}`;
|
|
1356
|
-
events.push("event: content_block_start");
|
|
1357
|
-
events.push(`data: ${JSON.stringify({
|
|
1358
|
-
type: "content_block_start",
|
|
1359
|
-
index: toolIndex,
|
|
1360
|
-
content_block: {
|
|
1361
|
-
type: "tool_use",
|
|
1362
|
-
id: toolId,
|
|
1363
|
-
name: toolCall.name,
|
|
1364
|
-
input: {}
|
|
1365
|
-
}
|
|
1366
|
-
})}`);
|
|
1367
|
-
events.push("");
|
|
1368
|
-
if (toolCall.arguments) {
|
|
1369
|
-
events.push("event: content_block_delta");
|
|
1370
|
-
events.push(`data: ${JSON.stringify({
|
|
1371
|
-
type: "content_block_delta",
|
|
1372
|
-
index: toolIndex,
|
|
1373
|
-
delta: {
|
|
1374
|
-
type: "input_json_delta",
|
|
1375
|
-
partial_json: toolCall.arguments
|
|
1376
|
-
}
|
|
1377
|
-
})}`);
|
|
1378
|
-
events.push("");
|
|
1379
|
-
}
|
|
1380
|
-
events.push("event: content_block_stop");
|
|
1381
|
-
events.push(`data: ${JSON.stringify({
|
|
1382
|
-
type: "content_block_stop",
|
|
1383
|
-
index: toolIndex
|
|
1384
|
-
})}`);
|
|
1385
|
-
events.push("");
|
|
1386
|
-
}
|
|
1387
|
-
const stopReason = parsed.stopReason || "end_turn";
|
|
1388
|
-
const usageDetails = { output_tokens: parsed.outputTokens ?? 0 };
|
|
1389
|
-
if (parsed.cachedTokens || parsed.reasoningTokens || parsed.reasoningEffort) {
|
|
1390
|
-
usageDetails.metadata = {};
|
|
1391
|
-
if (parsed.cachedTokens) usageDetails.metadata.cached_tokens = parsed.cachedTokens;
|
|
1392
|
-
if (parsed.reasoningTokens) usageDetails.metadata.reasoning_tokens = parsed.reasoningTokens;
|
|
1393
|
-
if (parsed.reasoningEffort) usageDetails.metadata.reasoning_effort = parsed.reasoningEffort;
|
|
1394
|
-
}
|
|
1395
|
-
events.push("event: message_delta");
|
|
1396
|
-
events.push(`data: ${JSON.stringify({
|
|
1397
|
-
type: "message_delta",
|
|
1398
|
-
delta: {
|
|
1399
|
-
stop_reason: stopReason,
|
|
1400
|
-
stop_sequence: null
|
|
1401
|
-
},
|
|
1402
|
-
usage: usageDetails
|
|
1403
|
-
})}`);
|
|
1404
|
-
events.push("");
|
|
1405
|
-
events.push("event: message_stop");
|
|
1406
|
-
events.push("data: {\"type\":\"message_stop\"}");
|
|
1407
|
-
events.push("");
|
|
1408
|
-
return events.join("\n");
|
|
1409
|
-
}
|
|
1410
|
-
mergeAndChunkSegments(segments, chunkSize = 2e3) {
|
|
1411
|
-
if (!segments || segments.length === 0) return [];
|
|
1412
|
-
const combined = segments.join("");
|
|
1413
|
-
if (!combined) return [];
|
|
1414
|
-
const result = [];
|
|
1415
|
-
for (let i = 0; i < combined.length; i += chunkSize) result.push(combined.slice(i, i + chunkSize));
|
|
1416
|
-
return result;
|
|
1417
|
-
}
|
|
1418
|
-
mapResponseStatusToStopReason(status) {
|
|
1419
|
-
if (!status) return "end_turn";
|
|
1420
|
-
return {
|
|
1421
|
-
completed: "end_turn",
|
|
1422
|
-
completed_with_error: "error",
|
|
1423
|
-
completed_with_streaming_error: "error",
|
|
1424
|
-
cancelled: "error",
|
|
1425
|
-
errored: "error"
|
|
1426
|
-
}[status] || "end_turn";
|
|
1427
|
-
}
|
|
1428
|
-
/**
|
|
1429
|
-
* Convert non-streaming OpenAI response to Claude format
|
|
1430
|
-
*/
|
|
1431
|
-
convertNonStreamingResponse(responseBody) {
|
|
1432
|
-
try {
|
|
1433
|
-
const openAIResponse = JSON.parse(responseBody);
|
|
1434
|
-
if (openAIResponse?.error) {
|
|
1435
|
-
const errorMessage = openAIResponse.error?.message || openAIResponse.error?.error || (typeof openAIResponse.error === "string" ? openAIResponse.error : "Unexpected API error");
|
|
1436
|
-
return this.createClaudeErrorResponse(errorMessage);
|
|
1437
|
-
}
|
|
1438
|
-
const choice = openAIResponse.choices?.[0];
|
|
1439
|
-
if (!choice) return responseBody;
|
|
1440
|
-
const claudeResponse = {
|
|
1441
|
-
id: `msg_${ulid()}`,
|
|
1442
|
-
type: "message",
|
|
1443
|
-
role: "assistant",
|
|
1444
|
-
content: [{
|
|
1445
|
-
type: "text",
|
|
1446
|
-
text: choice.message?.content || ""
|
|
1447
|
-
}],
|
|
1448
|
-
model: openAIResponse.model || "gpt-4-turbo",
|
|
1449
|
-
stop_reason: this.mapFinishReason(choice.finish_reason),
|
|
1450
|
-
stop_sequence: null,
|
|
1451
|
-
usage: {
|
|
1452
|
-
input_tokens: openAIResponse.usage?.prompt_tokens || 0,
|
|
1453
|
-
output_tokens: openAIResponse.usage?.completion_tokens || 0
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
return JSON.stringify(claudeResponse);
|
|
1457
|
-
} catch (error) {
|
|
1458
|
-
this.logger?.error("Failed to transform OpenAI response to Claude format", error);
|
|
1459
|
-
const fallbackMessage = typeof responseBody === "string" && responseBody.trim() ? responseBody.trim() : "Unexpected API error";
|
|
1460
|
-
return this.createClaudeErrorResponse(fallbackMessage);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
/**
|
|
1464
|
-
* Map OpenAI finish_reason to Claude stop_reason
|
|
1465
|
-
*/
|
|
1466
|
-
mapFinishReason(finishReason) {
|
|
1467
|
-
if (!finishReason) return null;
|
|
1468
|
-
return {
|
|
1469
|
-
stop: "end_turn",
|
|
1470
|
-
length: "max_tokens",
|
|
1471
|
-
function_call: "tool_use",
|
|
1472
|
-
tool_calls: "tool_use",
|
|
1473
|
-
content_filter: "stop_sequence"
|
|
1474
|
-
}[finishReason] || "end_turn";
|
|
1475
|
-
}
|
|
1476
|
-
createClaudeErrorResponse(message) {
|
|
1477
|
-
const messageId = `msg_${ulid()}`;
|
|
1478
|
-
const text = message || "Unexpected API error";
|
|
1479
|
-
return JSON.stringify({
|
|
1480
|
-
id: messageId,
|
|
1481
|
-
type: "message",
|
|
1482
|
-
role: "assistant",
|
|
1483
|
-
content: [{
|
|
1484
|
-
type: "text",
|
|
1485
|
-
text
|
|
1486
|
-
}],
|
|
1487
|
-
model: "gpt-5",
|
|
1488
|
-
stop_reason: "error",
|
|
1489
|
-
stop_sequence: null,
|
|
1490
|
-
usage: {
|
|
1491
|
-
input_tokens: 0,
|
|
1492
|
-
output_tokens: 0
|
|
1493
|
-
}
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
createClaudeErrorStream(message) {
|
|
1497
|
-
const messageId = `msg_${ulid()}`;
|
|
1498
|
-
const text = message || "Unexpected API error";
|
|
1499
|
-
const events = [];
|
|
1500
|
-
events.push("event: message_start");
|
|
1501
|
-
events.push(`data: ${JSON.stringify({
|
|
1502
|
-
type: "message_start",
|
|
1503
|
-
message: {
|
|
1504
|
-
id: messageId,
|
|
1505
|
-
type: "message",
|
|
1506
|
-
role: "assistant",
|
|
1507
|
-
content: [],
|
|
1508
|
-
model: "gpt-5",
|
|
1509
|
-
stop_reason: null,
|
|
1510
|
-
stop_sequence: null,
|
|
1511
|
-
usage: {
|
|
1512
|
-
input_tokens: 0,
|
|
1513
|
-
output_tokens: 0
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
})}`);
|
|
1517
|
-
events.push("");
|
|
1518
|
-
events.push("event: content_block_start");
|
|
1519
|
-
events.push(`data: ${JSON.stringify({
|
|
1520
|
-
type: "content_block_start",
|
|
1521
|
-
index: 0,
|
|
1522
|
-
content_block: {
|
|
1523
|
-
type: "text",
|
|
1524
|
-
text: ""
|
|
1525
|
-
}
|
|
1526
|
-
})}`);
|
|
1527
|
-
events.push("");
|
|
1528
|
-
events.push("event: content_block_delta");
|
|
1529
|
-
events.push(`data: ${JSON.stringify({
|
|
1530
|
-
type: "content_block_delta",
|
|
1531
|
-
index: 0,
|
|
1532
|
-
delta: {
|
|
1533
|
-
type: "text_delta",
|
|
1534
|
-
text
|
|
1535
|
-
}
|
|
1536
|
-
})}`);
|
|
1537
|
-
events.push("");
|
|
1538
|
-
events.push("event: content_block_stop");
|
|
1539
|
-
events.push(`data: ${JSON.stringify({
|
|
1540
|
-
type: "content_block_stop",
|
|
1541
|
-
index: 0
|
|
1542
|
-
})}`);
|
|
1543
|
-
events.push("");
|
|
1544
|
-
events.push("event: message_delta");
|
|
1545
|
-
events.push(`data: ${JSON.stringify({
|
|
1546
|
-
type: "message_delta",
|
|
1547
|
-
delta: {
|
|
1548
|
-
stop_reason: "error",
|
|
1549
|
-
stop_sequence: null
|
|
1550
|
-
},
|
|
1551
|
-
usage: { output_tokens: 0 }
|
|
1552
|
-
})}`);
|
|
1553
|
-
events.push("");
|
|
1554
|
-
events.push("event: message_stop");
|
|
1555
|
-
events.push("data: {\"type\":\"message_stop\"}");
|
|
1556
|
-
events.push("");
|
|
1557
|
-
return events.join("\n");
|
|
1558
|
-
}
|
|
1559
|
-
ensureValidClaudeStream(stream, errorMessage) {
|
|
1560
|
-
if (!stream || !stream.includes("event: message_start")) return this.createClaudeErrorStream(errorMessage);
|
|
1561
|
-
return stream;
|
|
1562
|
-
}
|
|
1563
|
-
};
|
|
1564
|
-
|
|
1565
|
-
//#endregion
|
|
1566
|
-
//#region src/adapters/gemini/types.ts
|
|
1567
|
-
const GEMINI_API_KEY_ENV_FALLBACK = "GEMINI_API_KEY";
|
|
1568
|
-
const GEMINI_CLI_HOME_ENV = "GEMINI_CLI_HOME";
|
|
1569
|
-
const GEMINI_DEFAULT_API_VERSION = "v1beta";
|
|
1570
|
-
const GEMINI_DEFAULT_AUTH_MODE = "auto";
|
|
1571
|
-
const GEMINI_PERSONAL_OAUTH_AUTH_TYPE = "oauth-personal";
|
|
1572
|
-
const GEMINI_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
1573
|
-
const GEMINI_CODE_ASSIST_API_VERSION = "v1internal";
|
|
1574
|
-
const GEMINI_SETTINGS_FILE = "settings.json";
|
|
1575
|
-
const GEMINI_OAUTH_FILE = "oauth_creds.json";
|
|
1576
|
-
const GEMINI_STREAM_QUERY_PARAM = "alt=sse";
|
|
1577
|
-
const GEMINI_USER_ROLE = "user";
|
|
1578
|
-
const GEMINI_MODEL_ROLE = "model";
|
|
1579
|
-
const GEMINI_MODEL_PREFIX = "models/";
|
|
1580
|
-
|
|
1581
|
-
//#endregion
|
|
1582
|
-
//#region src/adapters/gemini/ClaudeToGeminiTransformer.ts
|
|
1583
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
1584
|
-
const MINIMAL_REASONING_MAX_OUTPUT_TOKENS = 2048;
|
|
1585
|
-
const TOOL_MODE_AUTO = "AUTO";
|
|
1586
|
-
const TOOL_MODE_NONE = "NONE";
|
|
1587
|
-
const GEMINI_SUPPORTED_SCHEMA_KEYS = new Set([
|
|
1588
|
-
"type",
|
|
1589
|
-
"format",
|
|
1590
|
-
"description",
|
|
1591
|
-
"nullable",
|
|
1592
|
-
"enum",
|
|
1593
|
-
"items",
|
|
1594
|
-
"maxItems",
|
|
1595
|
-
"minItems",
|
|
1596
|
-
"properties",
|
|
1597
|
-
"required",
|
|
1598
|
-
"propertyOrdering",
|
|
1599
|
-
"maxProperties",
|
|
1600
|
-
"minProperties",
|
|
1601
|
-
"minimum",
|
|
1602
|
-
"maximum",
|
|
1603
|
-
"minLength",
|
|
1604
|
-
"maxLength",
|
|
1605
|
-
"pattern",
|
|
1606
|
-
"example",
|
|
1607
|
-
"anyOf",
|
|
1608
|
-
"title"
|
|
1609
|
-
]);
|
|
1610
|
-
var ClaudeToGeminiTransformer = class {
|
|
1611
|
-
transform(request, resolvedModel, reasoningEffort, thinkingDisabled = false) {
|
|
1612
|
-
const contents = this.toGeminiContents(request.messages);
|
|
1613
|
-
const systemInstruction = this.toSystemInstruction(request.system);
|
|
1614
|
-
const tools = this.toGeminiTools(request.tools);
|
|
1615
|
-
return {
|
|
1616
|
-
modelPath: this.toGeminiModelPath(resolvedModel),
|
|
1617
|
-
body: {
|
|
1618
|
-
contents,
|
|
1619
|
-
system_instruction: systemInstruction,
|
|
1620
|
-
tools,
|
|
1621
|
-
tool_config: { function_calling_config: { mode: tools.length > 0 ? TOOL_MODE_AUTO : TOOL_MODE_NONE } },
|
|
1622
|
-
generationConfig: { maxOutputTokens: this.resolveMaxOutputTokens(request.max_tokens, reasoningEffort, thinkingDisabled) }
|
|
1623
|
-
}
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
toGeminiModelPath(model) {
|
|
1627
|
-
return model.startsWith(GEMINI_MODEL_PREFIX) ? model : `${GEMINI_MODEL_PREFIX}${model}`;
|
|
1628
|
-
}
|
|
1629
|
-
toSystemInstruction(system) {
|
|
1630
|
-
const texts = this.extractTextParts(system);
|
|
1631
|
-
if (texts.length === 0) return;
|
|
1632
|
-
return { parts: texts.map((text) => ({ text })) };
|
|
1633
|
-
}
|
|
1634
|
-
toGeminiContents(messages) {
|
|
1635
|
-
const contents = [];
|
|
1636
|
-
for (const message of messages) {
|
|
1637
|
-
const parts = this.toGeminiParts(message.content);
|
|
1638
|
-
if (parts.length === 0) continue;
|
|
1639
|
-
contents.push({
|
|
1640
|
-
role: message.role === "assistant" ? GEMINI_MODEL_ROLE : GEMINI_USER_ROLE,
|
|
1641
|
-
parts
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
return contents;
|
|
1645
|
-
}
|
|
1646
|
-
toGeminiParts(content) {
|
|
1647
|
-
if (typeof content === "string") return content.trim() ? [{ text: content }] : [];
|
|
1648
|
-
if (!Array.isArray(content)) return [];
|
|
1649
|
-
const parts = [];
|
|
1650
|
-
for (const block of content) {
|
|
1651
|
-
if (block.type === "text" && block.text) parts.push({ text: block.text });
|
|
1652
|
-
if (block.type === "tool_use") parts.push({ functionCall: {
|
|
1653
|
-
name: block.name,
|
|
1654
|
-
args: block.input
|
|
1655
|
-
} });
|
|
1656
|
-
if (block.type === "tool_result") parts.push({ functionResponse: {
|
|
1657
|
-
name: block.tool_use_id,
|
|
1658
|
-
response: { content: typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? {}) }
|
|
1659
|
-
} });
|
|
1660
|
-
}
|
|
1661
|
-
return parts;
|
|
1662
|
-
}
|
|
1663
|
-
toGeminiTools(tools) {
|
|
1664
|
-
if (!tools || tools.length === 0) return [];
|
|
1665
|
-
return [{ function_declarations: tools.map((tool) => ({
|
|
1666
|
-
name: tool.name,
|
|
1667
|
-
description: tool.description,
|
|
1668
|
-
parameters: this.toGeminiParameters(tool.input_schema)
|
|
1669
|
-
})) }];
|
|
1670
|
-
}
|
|
1671
|
-
toGeminiParameters(schema) {
|
|
1672
|
-
if (!schema) return;
|
|
1673
|
-
return this.sanitizeGeminiSchema(schema);
|
|
1674
|
-
}
|
|
1675
|
-
sanitizeGeminiSchema(value) {
|
|
1676
|
-
if (Array.isArray(value)) return value.map((item) => this.sanitizeGeminiSchema(item));
|
|
1677
|
-
if (!value || typeof value !== "object") return value;
|
|
1678
|
-
const sanitizedEntries = Object.entries(value).flatMap(([key, nestedValue]) => {
|
|
1679
|
-
if (!GEMINI_SUPPORTED_SCHEMA_KEYS.has(key)) return [];
|
|
1680
|
-
if (key === "properties" && nestedValue && typeof nestedValue === "object" && !Array.isArray(nestedValue)) {
|
|
1681
|
-
const propertyEntries = Object.entries(nestedValue).map(([propertyName, propertySchema]) => [propertyName, this.sanitizeGeminiSchema(propertySchema)]);
|
|
1682
|
-
return [[key, Object.fromEntries(propertyEntries)]];
|
|
1683
|
-
}
|
|
1684
|
-
return [[key, this.sanitizeGeminiSchema(nestedValue)]];
|
|
1685
|
-
});
|
|
1686
|
-
return Object.fromEntries(sanitizedEntries);
|
|
1687
|
-
}
|
|
1688
|
-
extractTextParts(value) {
|
|
1689
|
-
if (typeof value === "string") return value.trim() ? [value] : [];
|
|
1690
|
-
if (!Array.isArray(value)) return [];
|
|
1691
|
-
return value.map((item) => {
|
|
1692
|
-
if (typeof item === "string") return item;
|
|
1693
|
-
if (item && typeof item === "object" && "text" in item && typeof item.text === "string") return item.text;
|
|
1694
|
-
return null;
|
|
1695
|
-
}).filter((item) => Boolean(item?.trim()));
|
|
1696
|
-
}
|
|
1697
|
-
resolveMaxOutputTokens(maxTokens, reasoningEffort, thinkingDisabled) {
|
|
1698
|
-
if (typeof maxTokens === "number" && maxTokens > 0) return maxTokens;
|
|
1699
|
-
if (thinkingDisabled) return MINIMAL_REASONING_MAX_OUTPUT_TOKENS;
|
|
1700
|
-
return reasoningEffort === "minimal" ? MINIMAL_REASONING_MAX_OUTPUT_TOKENS : DEFAULT_MAX_OUTPUT_TOKENS;
|
|
1701
|
-
}
|
|
1702
|
-
};
|
|
1703
|
-
|
|
1704
|
-
//#endregion
|
|
1705
|
-
//#region src/adapters/gemini/GeminiAuth.ts
|
|
1706
|
-
const GEMINI_CONFIG_DIRECTORY = ".gemini";
|
|
1707
|
-
const AUTHORIZATION_HEADER$1 = "Authorization";
|
|
1708
|
-
const API_KEY_HEADER = "x-goog-api-key";
|
|
1709
|
-
const BEARER_PREFIX = "Bearer ";
|
|
1710
|
-
const GEMINI_AUTH_CONFIG_ERROR = "GEMINI_AUTH_CONFIG_ERROR";
|
|
1711
|
-
const OAUTH_REFRESH_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
1712
|
-
const OAUTH_REFRESH_GRANT_TYPE = "refresh_token";
|
|
1713
|
-
const OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
1714
|
-
const OAUTH_CLIENT_SECRET = "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";
|
|
1715
|
-
const OAUTH_EXPIRY_SKEW_MS = 6e4;
|
|
1716
|
-
var GeminiAuthError = class extends Error {
|
|
1717
|
-
constructor(message, code) {
|
|
1718
|
-
super(message);
|
|
1719
|
-
this.code = code;
|
|
1720
|
-
this.name = "GeminiAuthError";
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
var GeminiAuth = class {
|
|
1724
|
-
constructor(logger = consoleLogger, env = process.env) {
|
|
1725
|
-
this.logger = logger;
|
|
1726
|
-
this.env = env;
|
|
1727
|
-
}
|
|
1728
|
-
async resolveHeaders(provider) {
|
|
1729
|
-
const authMode = provider.authMode ?? GEMINI_DEFAULT_AUTH_MODE;
|
|
1730
|
-
if (authMode === "api-key") return this.resolveApiKeyHeaders(provider);
|
|
1731
|
-
if (authMode === "oauth") return this.resolveOAuthHeaders();
|
|
1732
|
-
return await this.tryResolveApiKeyHeaders(provider) ?? this.resolveOAuthHeaders();
|
|
1733
|
-
}
|
|
1734
|
-
getGeminiDirectory() {
|
|
1735
|
-
const homeDirectory = this.env[GEMINI_CLI_HOME_ENV] || os.homedir();
|
|
1736
|
-
return path.join(homeDirectory, GEMINI_CONFIG_DIRECTORY);
|
|
1737
|
-
}
|
|
1738
|
-
getSettingsPath() {
|
|
1739
|
-
return path.join(this.getGeminiDirectory(), GEMINI_SETTINGS_FILE);
|
|
1740
|
-
}
|
|
1741
|
-
getOAuthPath() {
|
|
1742
|
-
return path.join(this.getGeminiDirectory(), GEMINI_OAUTH_FILE);
|
|
1743
|
-
}
|
|
1744
|
-
async tryResolveApiKeyHeaders(provider) {
|
|
1745
|
-
try {
|
|
1746
|
-
return await this.resolveApiKeyHeaders(provider);
|
|
1747
|
-
} catch {
|
|
1748
|
-
return null;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
async resolveApiKeyHeaders(provider) {
|
|
1752
|
-
const apiKeyEnvVar = provider.apiKeyEnvVar ?? provider.authTokenEnvVar ?? GEMINI_API_KEY_ENV_FALLBACK;
|
|
1753
|
-
const apiKey = this.env[apiKeyEnvVar]?.trim();
|
|
1754
|
-
if (!apiKey) throw new GeminiAuthError(`Missing Gemini API key. Set ${apiKeyEnvVar}.`, GEMINI_AUTH_CONFIG_ERROR);
|
|
1755
|
-
return {
|
|
1756
|
-
headers: { [API_KEY_HEADER]: apiKey },
|
|
1757
|
-
authMode: "api-key",
|
|
1758
|
-
authSource: "env"
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
async resolveOAuthHeaders() {
|
|
1762
|
-
const settings = await this.readSettings();
|
|
1763
|
-
const credentials = await this.readOAuthCredentials();
|
|
1764
|
-
const resolvedCredentials = await this.refreshOAuthCredentialsIfNeeded(credentials);
|
|
1765
|
-
const selectedType = settings?.security?.auth?.selectedType ?? null;
|
|
1766
|
-
const resolvedAccessToken = resolvedCredentials.access_token?.trim();
|
|
1767
|
-
if (!resolvedAccessToken) throw new GeminiAuthError(`Missing Gemini OAuth credentials. Expected ${this.getOAuthPath()} or ${GEMINI_API_KEY_ENV_FALLBACK}.`, GEMINI_AUTH_CONFIG_ERROR);
|
|
1768
|
-
return {
|
|
1769
|
-
headers: { [AUTHORIZATION_HEADER$1]: `${BEARER_PREFIX}${resolvedAccessToken}` },
|
|
1770
|
-
authMode: "oauth",
|
|
1771
|
-
authSource: "gemini-home",
|
|
1772
|
-
authType: selectedType
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
async readOAuthCredentials() {
|
|
1776
|
-
try {
|
|
1777
|
-
const raw = await fs$1.readFile(this.getOAuthPath(), "utf8");
|
|
1778
|
-
return JSON.parse(raw);
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
this.logger.warn("[GeminiAuth] Failed to read oauth credentials", {
|
|
1781
|
-
oauthPath: this.getOAuthPath(),
|
|
1782
|
-
cause: error
|
|
1783
|
-
});
|
|
1784
|
-
throw new GeminiAuthError(`Unable to read Gemini OAuth credentials from ${this.getOAuthPath()}.`, GEMINI_AUTH_CONFIG_ERROR);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
async readSettings() {
|
|
1788
|
-
try {
|
|
1789
|
-
const raw = await fs$1.readFile(this.getSettingsPath(), "utf8");
|
|
1790
|
-
return JSON.parse(raw);
|
|
1791
|
-
} catch (error) {
|
|
1792
|
-
if (error?.code === "ENOENT") return null;
|
|
1793
|
-
this.logger.warn("[GeminiAuth] Failed to read settings", {
|
|
1794
|
-
settingsPath: this.getSettingsPath(),
|
|
1795
|
-
cause: error
|
|
1796
|
-
});
|
|
1797
|
-
return null;
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
async refreshOAuthCredentialsIfNeeded(credentials) {
|
|
1801
|
-
if (!this.shouldRefreshCredentials(credentials)) return credentials;
|
|
1802
|
-
const refreshToken = credentials.refresh_token?.trim();
|
|
1803
|
-
if (!refreshToken) return credentials;
|
|
1804
|
-
const body = new URLSearchParams({
|
|
1805
|
-
client_id: OAUTH_CLIENT_ID,
|
|
1806
|
-
client_secret: OAUTH_CLIENT_SECRET,
|
|
1807
|
-
grant_type: OAUTH_REFRESH_GRANT_TYPE,
|
|
1808
|
-
refresh_token: refreshToken
|
|
1809
|
-
});
|
|
1810
|
-
const response = await fetch(OAUTH_REFRESH_ENDPOINT, {
|
|
1811
|
-
method: "POST",
|
|
1812
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1813
|
-
body
|
|
1814
|
-
});
|
|
1815
|
-
if (!response.ok) {
|
|
1816
|
-
this.logger.warn("[GeminiAuth] Failed to refresh oauth credentials", {
|
|
1817
|
-
status: response.status,
|
|
1818
|
-
oauthPath: this.getOAuthPath()
|
|
1819
|
-
});
|
|
1820
|
-
return credentials;
|
|
1821
|
-
}
|
|
1822
|
-
const payload = await response.json();
|
|
1823
|
-
const nextCredentials = {
|
|
1824
|
-
...credentials,
|
|
1825
|
-
access_token: payload.access_token ?? credentials.access_token,
|
|
1826
|
-
token_type: payload.token_type ?? credentials.token_type,
|
|
1827
|
-
scope: payload.scope ?? credentials.scope,
|
|
1828
|
-
expiry_date: typeof payload.expires_in === "number" ? Date.now() + payload.expires_in * 1e3 : credentials.expiry_date
|
|
1829
|
-
};
|
|
1830
|
-
await fs$1.writeFile(this.getOAuthPath(), `${JSON.stringify(nextCredentials, null, 2)}\n`, "utf8");
|
|
1831
|
-
return nextCredentials;
|
|
1832
|
-
}
|
|
1833
|
-
shouldRefreshCredentials(credentials) {
|
|
1834
|
-
const accessToken = credentials.access_token?.trim();
|
|
1835
|
-
const expiryDate = credentials.expiry_date;
|
|
1836
|
-
if (!accessToken) return true;
|
|
1837
|
-
if (typeof expiryDate !== "number") return false;
|
|
1838
|
-
return expiryDate <= Date.now() + OAUTH_EXPIRY_SKEW_MS;
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1841
|
-
|
|
1842
|
-
//#endregion
|
|
1843
|
-
//#region src/adapters/gemini/GeminiToClaudeTransformer.ts
|
|
1844
|
-
const SSE_EVENT_PREFIX = "event: ";
|
|
1845
|
-
const SSE_DATA_PREFIX = "data: ";
|
|
1846
|
-
const MESSAGE_START_EVENT = "message_start";
|
|
1847
|
-
const CONTENT_BLOCK_START_EVENT = "content_block_start";
|
|
1848
|
-
const CONTENT_BLOCK_DELTA_EVENT = "content_block_delta";
|
|
1849
|
-
const CONTENT_BLOCK_STOP_EVENT = "content_block_stop";
|
|
1850
|
-
const MESSAGE_DELTA_EVENT = "message_delta";
|
|
1851
|
-
const MESSAGE_STOP_EVENT = "message_stop";
|
|
1852
|
-
const CONTENT_BLOCK_TYPE = "text";
|
|
1853
|
-
const TEXT_DELTA_TYPE = "text_delta";
|
|
1854
|
-
const STOP_REASON_END_TURN = "end_turn";
|
|
1855
|
-
const STOP_REASON_TOOL_USE = "tool_use";
|
|
1856
|
-
const DONE_SENTINEL = "[DONE]";
|
|
1857
|
-
var GeminiToClaudeTransformer = class {
|
|
1858
|
-
transformBuffered(responseBody, model) {
|
|
1859
|
-
const parsed = JSON.parse(responseBody);
|
|
1860
|
-
return this.toClaudeStream([parsed], model);
|
|
1861
|
-
}
|
|
1862
|
-
transformStreaming(responseBody, model) {
|
|
1863
|
-
const payloads = [];
|
|
1864
|
-
for (const chunk of responseBody.split("\n\n")) {
|
|
1865
|
-
const dataLine = chunk.split("\n").find((line) => line.startsWith(SSE_DATA_PREFIX))?.slice(6).trim();
|
|
1866
|
-
if (!dataLine || dataLine === DONE_SENTINEL) continue;
|
|
1867
|
-
payloads.push(JSON.parse(dataLine));
|
|
1868
|
-
}
|
|
1869
|
-
return this.toClaudeStream(payloads, model);
|
|
1870
|
-
}
|
|
1871
|
-
toClaudeStream(payloads, model) {
|
|
1872
|
-
const text = payloads.flatMap((payload) => payload.candidates ?? []).flatMap((candidate) => candidate.content?.parts ?? []).map((part) => "text" in part && typeof part.text === "string" ? part.text : "").join("");
|
|
1873
|
-
const finishReason = payloads.map((payload) => payload.candidates?.[0]?.finishReason).find((value) => typeof value === "string");
|
|
1874
|
-
const usage = payloads.find((payload) => payload.usageMetadata)?.usageMetadata;
|
|
1875
|
-
const messageId = `msg_${ulid()}`;
|
|
1876
|
-
const events = [];
|
|
1877
|
-
events.push(`${SSE_EVENT_PREFIX}${MESSAGE_START_EVENT}`);
|
|
1878
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({
|
|
1879
|
-
type: MESSAGE_START_EVENT,
|
|
1880
|
-
message: {
|
|
1881
|
-
id: messageId,
|
|
1882
|
-
type: "message",
|
|
1883
|
-
role: "assistant",
|
|
1884
|
-
content: [],
|
|
1885
|
-
model,
|
|
1886
|
-
stop_reason: null,
|
|
1887
|
-
stop_sequence: null,
|
|
1888
|
-
usage: {
|
|
1889
|
-
input_tokens: usage?.promptTokenCount ?? 0,
|
|
1890
|
-
output_tokens: 0
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
})}`);
|
|
1894
|
-
events.push("");
|
|
1895
|
-
events.push(`${SSE_EVENT_PREFIX}${CONTENT_BLOCK_START_EVENT}`);
|
|
1896
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({
|
|
1897
|
-
type: CONTENT_BLOCK_START_EVENT,
|
|
1898
|
-
index: 0,
|
|
1899
|
-
content_block: {
|
|
1900
|
-
type: CONTENT_BLOCK_TYPE,
|
|
1901
|
-
text: ""
|
|
1902
|
-
}
|
|
1903
|
-
})}`);
|
|
1904
|
-
events.push("");
|
|
1905
|
-
if (text) {
|
|
1906
|
-
events.push(`${SSE_EVENT_PREFIX}${CONTENT_BLOCK_DELTA_EVENT}`);
|
|
1907
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({
|
|
1908
|
-
type: CONTENT_BLOCK_DELTA_EVENT,
|
|
1909
|
-
index: 0,
|
|
1910
|
-
delta: {
|
|
1911
|
-
type: TEXT_DELTA_TYPE,
|
|
1912
|
-
text
|
|
1913
|
-
}
|
|
1914
|
-
})}`);
|
|
1915
|
-
events.push("");
|
|
1916
|
-
}
|
|
1917
|
-
events.push(`${SSE_EVENT_PREFIX}${CONTENT_BLOCK_STOP_EVENT}`);
|
|
1918
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({
|
|
1919
|
-
type: CONTENT_BLOCK_STOP_EVENT,
|
|
1920
|
-
index: 0
|
|
1921
|
-
})}`);
|
|
1922
|
-
events.push("");
|
|
1923
|
-
events.push(`${SSE_EVENT_PREFIX}${MESSAGE_DELTA_EVENT}`);
|
|
1924
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({
|
|
1925
|
-
type: MESSAGE_DELTA_EVENT,
|
|
1926
|
-
delta: {
|
|
1927
|
-
stop_reason: this.mapStopReason(finishReason),
|
|
1928
|
-
stop_sequence: null
|
|
1929
|
-
},
|
|
1930
|
-
usage: { output_tokens: usage?.candidatesTokenCount ?? 0 }
|
|
1931
|
-
})}`);
|
|
1932
|
-
events.push("");
|
|
1933
|
-
events.push(`${SSE_EVENT_PREFIX}${MESSAGE_STOP_EVENT}`);
|
|
1934
|
-
events.push(`${SSE_DATA_PREFIX}${JSON.stringify({ type: MESSAGE_STOP_EVENT })}`);
|
|
1935
|
-
events.push("");
|
|
1936
|
-
return events.join("\n");
|
|
1937
|
-
}
|
|
1938
|
-
mapStopReason(finishReason) {
|
|
1939
|
-
if (!finishReason) return STOP_REASON_END_TURN;
|
|
1940
|
-
return finishReason === "STOP" ? STOP_REASON_END_TURN : STOP_REASON_TOOL_USE;
|
|
1941
|
-
}
|
|
1942
|
-
};
|
|
1943
|
-
|
|
1944
|
-
//#endregion
|
|
1945
|
-
//#region src/services/ConversationHistoryService.ts
|
|
1946
|
-
const DEFAULT_HISTORY_PAGE_SIZE$1 = 50;
|
|
1947
|
-
const MIN_HISTORY_PAGE_SIZE = 1;
|
|
1948
|
-
const MAX_HISTORY_PAGE_SIZE = 200;
|
|
1949
|
-
const HISTORY_TABLE = "conversation_history";
|
|
1950
|
-
const HISTORY_SCOPE_SEQUENCE_INDEX = "conversation_history_scope_sequence_idx";
|
|
1951
|
-
const HISTORY_SCOPE_REQUEST_INDEX = "conversation_history_scope_request_idx";
|
|
1952
|
-
const HISTORY_ERROR_CODES = {
|
|
1953
|
-
appendFailed: "HISTORY_APPEND_FAILED",
|
|
1954
|
-
listFailed: "HISTORY_LIST_FAILED",
|
|
1955
|
-
clearFailed: "HISTORY_CLEAR_FAILED",
|
|
1956
|
-
statsFailed: "HISTORY_STATS_FAILED",
|
|
1957
|
-
initFailed: "HISTORY_INIT_FAILED"
|
|
1958
|
-
};
|
|
1959
|
-
const historyCursorSchema = z.string().transform((value) => Number(value)).refine((value) => Number.isInteger(value) && value > 0, "Cursor must be a positive integer");
|
|
1960
|
-
/**
|
|
1961
|
-
* Error raised for conversation history persistence failures.
|
|
1962
|
-
*/
|
|
1963
|
-
var ConversationHistoryServiceError = class extends Error {
|
|
1964
|
-
constructor(message, code, options) {
|
|
1965
|
-
super(message, options);
|
|
1966
|
-
this.code = code;
|
|
1967
|
-
this.name = "ConversationHistoryServiceError";
|
|
1968
|
-
}
|
|
1969
|
-
};
|
|
1970
|
-
/**
|
|
1971
|
-
* Persists scoped conversation history using SQLite and enforces bounded retention.
|
|
1972
|
-
*
|
|
1973
|
-
* Environment variables:
|
|
1974
|
-
* - `MODEL_PROXY_MCP_DB_PATH`
|
|
1975
|
-
*/
|
|
1976
|
-
var ConversationHistoryService = class {
|
|
1977
|
-
sqlite = null;
|
|
1978
|
-
/**
|
|
1979
|
-
* Creates a SQLite-backed history store.
|
|
1980
|
-
*
|
|
1981
|
-
* `dbPath` defaults to `MODEL_PROXY_MCP_DB_PATH` or `~/.model-proxy/history.sqlite`.
|
|
1982
|
-
* `retentionLimit` is enforced per scope after each insert batch.
|
|
1983
|
-
* `logger` receives structured operational failures.
|
|
1984
|
-
*/
|
|
1985
|
-
constructor(dbPath = process.env.MODEL_PROXY_MCP_DB_PATH || DEFAULT_HISTORY_DB_PATH, retentionLimit = DEFAULT_HISTORY_RETENTION_LIMIT, logger = consoleLogger) {
|
|
1986
|
-
this.dbPath = dbPath;
|
|
1987
|
-
this.retentionLimit = retentionLimit;
|
|
1988
|
-
this.logger = logger;
|
|
1989
|
-
}
|
|
1990
|
-
/**
|
|
1991
|
-
* Eagerly initializes the SQLite database and schema.
|
|
1992
|
-
*/
|
|
1993
|
-
async ensureInitialized() {
|
|
1994
|
-
await this.getDb();
|
|
1995
|
-
}
|
|
1996
|
-
/**
|
|
1997
|
-
* Inserts normalized history entries and prunes overflow per scope.
|
|
1998
|
-
*/
|
|
1999
|
-
async appendEntries(entries) {
|
|
2000
|
-
if (entries.length === 0) return;
|
|
2001
|
-
try {
|
|
2002
|
-
const db = await this.getDb();
|
|
2003
|
-
const insert = db.prepare(`
|
|
2004
|
-
INSERT INTO conversation_history (
|
|
2005
|
-
id, scope, request_id, direction, role, message_type, model, slot, payload_json, created_at
|
|
2006
|
-
) VALUES (
|
|
2007
|
-
@id, @scope, @request_id, @direction, @role, @message_type, @model, @slot, @payload_json, @created_at
|
|
2008
|
-
)
|
|
2009
|
-
`);
|
|
2010
|
-
const transaction = db.transaction((scopeEntries) => {
|
|
2011
|
-
for (const entry of scopeEntries) insert.run({
|
|
2012
|
-
id: entry.id ?? ulid(),
|
|
2013
|
-
scope: entry.scope,
|
|
2014
|
-
request_id: entry.requestId,
|
|
2015
|
-
direction: entry.direction,
|
|
2016
|
-
role: entry.role,
|
|
2017
|
-
message_type: entry.messageType,
|
|
2018
|
-
model: entry.model,
|
|
2019
|
-
slot: entry.slot,
|
|
2020
|
-
payload_json: entry.payloadJson,
|
|
2021
|
-
created_at: entry.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2022
|
-
});
|
|
2023
|
-
});
|
|
2024
|
-
const entriesByScope = /* @__PURE__ */ new Map();
|
|
2025
|
-
for (const entry of entries) {
|
|
2026
|
-
const bucket = entriesByScope.get(entry.scope) ?? [];
|
|
2027
|
-
bucket.push(entry);
|
|
2028
|
-
entriesByScope.set(entry.scope, bucket);
|
|
2029
|
-
}
|
|
2030
|
-
for (const scopeEntries of entriesByScope.values()) transaction(scopeEntries);
|
|
2031
|
-
for (const scope of entriesByScope.keys()) await this.pruneScope(scope);
|
|
2032
|
-
} catch (error) {
|
|
2033
|
-
this.logger.error("[model-proxy-mcp] Failed to append history", {
|
|
2034
|
-
code: HISTORY_ERROR_CODES.appendFailed,
|
|
2035
|
-
dbPath: this.dbPath,
|
|
2036
|
-
cause: error
|
|
2037
|
-
});
|
|
2038
|
-
throw new ConversationHistoryServiceError("Failed to append conversation history", HISTORY_ERROR_CODES.appendFailed, { cause: error });
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
/**
|
|
2042
|
-
* Lists conversation history for a scope ordered from newest to oldest.
|
|
2043
|
-
*/
|
|
2044
|
-
async listHistory(scope, limit = DEFAULT_HISTORY_PAGE_SIZE$1, cursor) {
|
|
2045
|
-
try {
|
|
2046
|
-
const db = await this.getDb();
|
|
2047
|
-
const parsedCursor = cursor ? historyCursorSchema.parse(cursor) : void 0;
|
|
2048
|
-
const boundedLimit = Math.max(MIN_HISTORY_PAGE_SIZE, Math.min(limit, MAX_HISTORY_PAGE_SIZE));
|
|
2049
|
-
const rows = parsedCursor ? db.prepare(`
|
|
2050
|
-
SELECT sequence, id, scope, request_id, direction, role, message_type, model, slot, payload_json, created_at
|
|
2051
|
-
FROM ${HISTORY_TABLE}
|
|
2052
|
-
WHERE scope = ? AND sequence < ?
|
|
2053
|
-
ORDER BY sequence DESC
|
|
2054
|
-
LIMIT ?
|
|
2055
|
-
`).all(scope, parsedCursor, boundedLimit + 1) : db.prepare(`
|
|
2056
|
-
SELECT sequence, id, scope, request_id, direction, role, message_type, model, slot, payload_json, created_at
|
|
2057
|
-
FROM ${HISTORY_TABLE}
|
|
2058
|
-
WHERE scope = ?
|
|
2059
|
-
ORDER BY sequence DESC
|
|
2060
|
-
LIMIT ?
|
|
2061
|
-
`).all(scope, boundedLimit + 1);
|
|
2062
|
-
const total = Number(db.prepare(`SELECT COUNT(*) as count FROM ${HISTORY_TABLE} WHERE scope = ?`).get(scope).count);
|
|
2063
|
-
const nextRow = rows.length > boundedLimit ? rows.pop() : void 0;
|
|
2064
|
-
return {
|
|
2065
|
-
items: rows.map((row) => this.toEntry(row)),
|
|
2066
|
-
nextCursor: nextRow ? String(nextRow.sequence) : null,
|
|
2067
|
-
total
|
|
2068
|
-
};
|
|
2069
|
-
} catch (error) {
|
|
2070
|
-
this.logger.error("[model-proxy-mcp] Failed to list history", {
|
|
2071
|
-
code: HISTORY_ERROR_CODES.listFailed,
|
|
2072
|
-
scope,
|
|
2073
|
-
cause: error
|
|
2074
|
-
});
|
|
2075
|
-
throw new ConversationHistoryServiceError("Failed to list conversation history", HISTORY_ERROR_CODES.listFailed, { cause: error });
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
/**
|
|
2079
|
-
* Clears all history rows for a single scope and returns the deleted count.
|
|
2080
|
-
*/
|
|
2081
|
-
async clearScope(scope) {
|
|
2082
|
-
try {
|
|
2083
|
-
return (await this.getDb()).prepare(`DELETE FROM ${HISTORY_TABLE} WHERE scope = ?`).run(scope).changes;
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
this.logger.error("[model-proxy-mcp] Failed to clear history", {
|
|
2086
|
-
code: HISTORY_ERROR_CODES.clearFailed,
|
|
2087
|
-
scope,
|
|
2088
|
-
cause: error
|
|
2089
|
-
});
|
|
2090
|
-
throw new ConversationHistoryServiceError("Failed to clear conversation history", HISTORY_ERROR_CODES.clearFailed, { cause: error });
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
/**
|
|
2094
|
-
* Returns retention stats for a scope.
|
|
2095
|
-
*/
|
|
2096
|
-
async getStats(scope) {
|
|
2097
|
-
try {
|
|
2098
|
-
const row = (await this.getDb()).prepare(`
|
|
2099
|
-
SELECT COUNT(*) as count, MIN(created_at) as oldest, MAX(created_at) as newest
|
|
2100
|
-
FROM ${HISTORY_TABLE}
|
|
2101
|
-
WHERE scope = ?
|
|
2102
|
-
`).get(scope);
|
|
2103
|
-
return {
|
|
2104
|
-
scope,
|
|
2105
|
-
retentionLimit: this.retentionLimit,
|
|
2106
|
-
totalMessages: Number(row.count),
|
|
2107
|
-
oldestCreatedAt: row.oldest,
|
|
2108
|
-
newestCreatedAt: row.newest
|
|
2109
|
-
};
|
|
2110
|
-
} catch (error) {
|
|
2111
|
-
this.logger.error("[model-proxy-mcp] Failed to read history stats", {
|
|
2112
|
-
code: HISTORY_ERROR_CODES.statsFailed,
|
|
2113
|
-
scope,
|
|
2114
|
-
cause: error
|
|
2115
|
-
});
|
|
2116
|
-
throw new ConversationHistoryServiceError("Failed to read history stats", HISTORY_ERROR_CODES.statsFailed, { cause: error });
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
/**
|
|
2120
|
-
* Lists known scopes discovered from stored conversation history.
|
|
2121
|
-
*/
|
|
2122
|
-
async listScopes() {
|
|
2123
|
-
try {
|
|
2124
|
-
return (await this.getDb()).prepare(`SELECT DISTINCT scope FROM ${HISTORY_TABLE} ORDER BY scope ASC`).all().map((row) => row.scope);
|
|
2125
|
-
} catch (error) {
|
|
2126
|
-
this.logger.error("[model-proxy-mcp] Failed to list history scopes", {
|
|
2127
|
-
code: HISTORY_ERROR_CODES.listFailed,
|
|
2128
|
-
cause: error
|
|
2129
|
-
});
|
|
2130
|
-
return [];
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
/**
|
|
2134
|
-
* Removes the oldest rows for a scope when it exceeds retention.
|
|
2135
|
-
*/
|
|
2136
|
-
async pruneScope(scope) {
|
|
2137
|
-
const db = await this.getDb();
|
|
2138
|
-
const row = db.prepare(`SELECT COUNT(*) as count FROM ${HISTORY_TABLE} WHERE scope = ?`).get(scope);
|
|
2139
|
-
const overflow = Number(row.count) - this.retentionLimit;
|
|
2140
|
-
if (overflow <= 0) return;
|
|
2141
|
-
db.prepare(`
|
|
2142
|
-
DELETE FROM ${HISTORY_TABLE}
|
|
2143
|
-
WHERE sequence IN (
|
|
2144
|
-
SELECT sequence
|
|
2145
|
-
FROM ${HISTORY_TABLE}
|
|
2146
|
-
WHERE scope = ?
|
|
2147
|
-
ORDER BY sequence ASC
|
|
2148
|
-
LIMIT ?
|
|
2149
|
-
)
|
|
2150
|
-
`).run(scope, overflow);
|
|
2151
|
-
}
|
|
2152
|
-
/**
|
|
2153
|
-
* Lazily initializes the SQLite connection and schema.
|
|
2154
|
-
*/
|
|
2155
|
-
async getDb() {
|
|
2156
|
-
if (this.sqlite) return this.sqlite;
|
|
2157
|
-
try {
|
|
2158
|
-
await fs$1.mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
2159
|
-
this.sqlite = new Database(this.dbPath);
|
|
2160
|
-
this.sqlite.pragma("journal_mode = WAL");
|
|
2161
|
-
this.sqlite.exec(`
|
|
2162
|
-
CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (
|
|
2163
|
-
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2164
|
-
id TEXT NOT NULL UNIQUE,
|
|
2165
|
-
scope TEXT NOT NULL,
|
|
2166
|
-
request_id TEXT NOT NULL,
|
|
2167
|
-
direction TEXT NOT NULL,
|
|
2168
|
-
role TEXT,
|
|
2169
|
-
message_type TEXT NOT NULL,
|
|
2170
|
-
model TEXT,
|
|
2171
|
-
slot TEXT NOT NULL,
|
|
2172
|
-
payload_json TEXT NOT NULL,
|
|
2173
|
-
created_at TEXT NOT NULL
|
|
2174
|
-
)
|
|
2175
|
-
`);
|
|
2176
|
-
this.sqlite.exec(`CREATE INDEX IF NOT EXISTS ${HISTORY_SCOPE_SEQUENCE_INDEX} ON ${HISTORY_TABLE} (scope, sequence DESC)`);
|
|
2177
|
-
this.sqlite.exec(`CREATE INDEX IF NOT EXISTS ${HISTORY_SCOPE_REQUEST_INDEX} ON ${HISTORY_TABLE} (scope, request_id)`);
|
|
2178
|
-
return this.sqlite;
|
|
2179
|
-
} catch (error) {
|
|
2180
|
-
this.logger.error("[model-proxy-mcp] Failed to initialize history database", {
|
|
2181
|
-
code: HISTORY_ERROR_CODES.initFailed,
|
|
2182
|
-
dbPath: this.dbPath,
|
|
2183
|
-
cause: error
|
|
2184
|
-
});
|
|
2185
|
-
throw new ConversationHistoryServiceError("Failed to initialize history database", HISTORY_ERROR_CODES.initFailed, { cause: error });
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
/**
|
|
2189
|
-
* Maps a SQLite row into the public history DTO.
|
|
2190
|
-
*/
|
|
2191
|
-
toEntry(row) {
|
|
2192
|
-
return {
|
|
2193
|
-
id: row.id,
|
|
2194
|
-
scope: row.scope,
|
|
2195
|
-
requestId: row.request_id,
|
|
2196
|
-
direction: row.direction,
|
|
2197
|
-
role: row.role,
|
|
2198
|
-
messageType: row.message_type,
|
|
2199
|
-
model: row.model,
|
|
2200
|
-
slot: row.slot,
|
|
2201
|
-
payloadJson: row.payload_json,
|
|
2202
|
-
createdAt: row.created_at
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
};
|
|
2206
|
-
|
|
2207
|
-
//#endregion
|
|
2208
|
-
//#region src/services/GatewayService.ts
|
|
2209
|
-
const MODEL_SLOT_BY_NAME = {
|
|
2210
|
-
"ccproxy-default": "default",
|
|
2211
|
-
"ccproxy-sonnet": "sonnet",
|
|
2212
|
-
"ccproxy-opus": "opus",
|
|
2213
|
-
"ccproxy-haiku": "haiku",
|
|
2214
|
-
"ccproxy-subagent": "subagent"
|
|
2215
|
-
};
|
|
2216
|
-
const DEFAULT_SCOPE$1 = "default";
|
|
2217
|
-
const DEFAULT_SLOT$1 = "default";
|
|
2218
|
-
const DEFAULT_HISTORY_PAGE_SIZE = 50;
|
|
2219
|
-
const MODEL_SLOTS = [
|
|
2220
|
-
"default",
|
|
2221
|
-
"sonnet",
|
|
2222
|
-
"opus",
|
|
2223
|
-
"haiku",
|
|
2224
|
-
"subagent"
|
|
2225
|
-
];
|
|
2226
|
-
const CLAUDE_MESSAGES_ENDPOINT = "https://api.anthropic.com/v1/messages";
|
|
2227
|
-
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
2228
|
-
const ANTHROPIC_COMPATIBLE_PROVIDER = "anthropic-compatible";
|
|
2229
|
-
const CHATGPT_CODEX_PROVIDER = "chatgpt-codex";
|
|
2230
|
-
const GEMINI_DIRECT_PROVIDER = "gemini-direct";
|
|
2231
|
-
const DEFAULT_UPSTREAM_AUTH_ENV = "ANTHROPIC_AUTH_TOKEN";
|
|
2232
|
-
const FALLBACK_UPSTREAM_AUTH_ENV = "MODEL_PROXY_MCP_UPSTREAM_AUTH_TOKEN";
|
|
2233
|
-
const FALLBACK_UPSTREAM_TIMEOUT_ENV = "MODEL_PROXY_MCP_UPSTREAM_TIMEOUT_MS";
|
|
2234
|
-
const CONFIGURED_UPSTREAM_AUTH_ENV_NAME = "MODEL_PROXY_MCP_UPSTREAM_AUTH_ENV";
|
|
2235
|
-
const DEFAULT_API_TIMEOUT_ENV = "API_TIMEOUT_MS";
|
|
2236
|
-
const ERROR_RESPONSE_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
2237
|
-
const JSON_CONTENT_TYPE = "application/json";
|
|
2238
|
-
const SSE_CONTENT_TYPE = "text/event-stream; charset=utf-8";
|
|
2239
|
-
const RESPONSE_CONTENT_TYPE_HEADER = "content-type";
|
|
2240
|
-
const RESPONSE_CACHE_CONTROL_HEADER = "cache-control";
|
|
2241
|
-
const RESPONSE_CONNECTION_HEADER = "connection";
|
|
2242
|
-
const AUTHORIZATION_HEADER = "authorization";
|
|
2243
|
-
const X_API_KEY_HEADER = "x-api-key";
|
|
2244
|
-
const ACCEPT_HEADER = "accept";
|
|
2245
|
-
const ANTHROPIC_VERSION_HEADER = "anthropic-version";
|
|
2246
|
-
const ANTHROPIC_BETA_HEADER = "anthropic-beta";
|
|
2247
|
-
const HEADER_NO_CACHE = "no-cache";
|
|
2248
|
-
const HEADER_KEEP_ALIVE = "keep-alive";
|
|
2249
|
-
const API_ERROR_TYPE = "api_error";
|
|
2250
|
-
const RAW_REQUEST_MESSAGE_TYPE = "raw-request";
|
|
2251
|
-
const RESPONSE_STREAM_MESSAGE_TYPE = "response-stream";
|
|
2252
|
-
const ERROR_MESSAGE_TYPE = "error";
|
|
2253
|
-
const REQUEST_DIRECTION = "request";
|
|
2254
|
-
const RESPONSE_DIRECTION = "response";
|
|
2255
|
-
const ERROR_DIRECTION = "error";
|
|
2256
|
-
const SYSTEM_ROLE = "system";
|
|
2257
|
-
const ASSISTANT_ROLE = "assistant";
|
|
2258
|
-
const MESSAGE_MESSAGE_TYPE = "message";
|
|
2259
|
-
const RESPONSE_ITEM_MESSAGE_TYPE = "response-item";
|
|
2260
|
-
const MODEL_NOT_FOUND = "MODEL_NOT_FOUND";
|
|
2261
|
-
const REQUEST_FORWARD_FAILED = "REQUEST_FORWARD_FAILED";
|
|
2262
|
-
const REQUEST_VALIDATION_FAILED = "REQUEST_VALIDATION_FAILED";
|
|
2263
|
-
const REQUEST_PAYLOAD_PREVIEW_LIMIT = 240;
|
|
2264
|
-
const HTTP_STATUS_OK = 200;
|
|
2265
|
-
const GEMINI_GENERATE_CONTENT_METHOD = "generateContent";
|
|
2266
|
-
const GEMINI_STREAM_GENERATE_CONTENT_METHOD = "streamGenerateContent";
|
|
2267
|
-
const CODE_ASSIST_LOAD_METHOD = "loadCodeAssist";
|
|
2268
|
-
const CODE_ASSIST_IDE_TYPE = "IDE_UNSPECIFIED";
|
|
2269
|
-
const CODE_ASSIST_PLATFORM = "PLATFORM_UNSPECIFIED";
|
|
2270
|
-
const CODE_ASSIST_PLUGIN_TYPE = "GEMINI";
|
|
2271
|
-
const CLAUDE_REQUEST_SCHEMA = z.object({
|
|
2272
|
-
model: z.string().optional(),
|
|
2273
|
-
system: z.unknown().optional(),
|
|
2274
|
-
messages: z.array(z.unknown()).default([]),
|
|
2275
|
-
tools: z.array(z.unknown()).optional(),
|
|
2276
|
-
max_tokens: z.number().int().positive().optional(),
|
|
2277
|
-
stream: z.boolean().optional()
|
|
2278
|
-
});
|
|
2279
|
-
/**
|
|
2280
|
-
* Base error type for gateway operations with HTTP status and domain code metadata.
|
|
2281
|
-
*/
|
|
2282
|
-
var GatewayServiceError = class extends Error {
|
|
2283
|
-
constructor(message, code, status, options) {
|
|
2284
|
-
super(message, options);
|
|
2285
|
-
this.code = code;
|
|
2286
|
-
this.status = status;
|
|
2287
|
-
this.name = new.target.name;
|
|
2288
|
-
}
|
|
2289
|
-
};
|
|
2290
|
-
/**
|
|
2291
|
-
* Raised when an inbound Claude request payload is invalid.
|
|
2292
|
-
*/
|
|
2293
|
-
var GatewayValidationError = class extends GatewayServiceError {};
|
|
2294
|
-
/**
|
|
2295
|
-
* Raised when the active proxy configuration cannot satisfy a request.
|
|
2296
|
-
*/
|
|
2297
|
-
var GatewayConfigError = class extends GatewayServiceError {};
|
|
2298
|
-
/**
|
|
2299
|
-
* Raised when upstream authentication is missing or invalid.
|
|
2300
|
-
*/
|
|
2301
|
-
var GatewayAuthError = class extends GatewayServiceError {};
|
|
2302
|
-
/**
|
|
2303
|
-
* Raised when an upstream request fails in a way that might be recoverable by trying fallbacks.
|
|
2304
|
-
*/
|
|
2305
|
-
var GatewayUpstreamError = class extends GatewayServiceError {
|
|
2306
|
-
constructor(message, status, upstreamBody, attemptLabel, options) {
|
|
2307
|
-
super(message, REQUEST_FORWARD_FAILED, status, options);
|
|
2308
|
-
this.upstreamBody = upstreamBody;
|
|
2309
|
-
this.attemptLabel = attemptLabel;
|
|
2310
|
-
}
|
|
2311
|
-
};
|
|
2312
|
-
const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500;
|
|
2313
|
-
const RECOVERABLE_UPSTREAM_STATUSES = new Set([
|
|
2314
|
-
408,
|
|
2315
|
-
429,
|
|
2316
|
-
500,
|
|
2317
|
-
502,
|
|
2318
|
-
503,
|
|
2319
|
-
504
|
|
2320
|
-
]);
|
|
2321
|
-
/**
|
|
2322
|
-
* Coordinates settings-backed model selection, request forwarding, and scoped history persistence.
|
|
2323
|
-
*/
|
|
2324
|
-
var GatewayService = class {
|
|
2325
|
-
constructor(profileStore = new ProfileStore(), codexAuth = new CodexAuth(consoleLogger, DEFAULT_AUTH_FILE_PATH), logger = consoleLogger, fetchImpl = fetch, historyService = new ConversationHistoryService()) {
|
|
2326
|
-
this.profileStore = profileStore;
|
|
2327
|
-
this.codexAuth = codexAuth;
|
|
2328
|
-
this.logger = logger;
|
|
2329
|
-
this.fetchImpl = fetchImpl;
|
|
2330
|
-
this.historyService = historyService;
|
|
2331
|
-
}
|
|
2332
|
-
/**
|
|
2333
|
-
* Lists all configured profiles for the given scope.
|
|
2334
|
-
*/
|
|
2335
|
-
async listProfiles(scope = DEFAULT_SCOPE$1) {
|
|
2336
|
-
return this.profileStore.listProfiles(scope);
|
|
2337
|
-
}
|
|
2338
|
-
/**
|
|
2339
|
-
* Eagerly seeds the scoped settings file so startup always has a backing config.
|
|
2340
|
-
*/
|
|
2341
|
-
async ensureConfig(scope = DEFAULT_SCOPE$1) {
|
|
2342
|
-
await this.historyService.ensureInitialized();
|
|
2343
|
-
return this.profileStore.ensureConfig(scope);
|
|
2344
|
-
}
|
|
2345
|
-
/**
|
|
2346
|
-
* Returns the resolved admin config used by the live HTTP server and UI.
|
|
2347
|
-
*/
|
|
2348
|
-
async getAdminConfig(scope = DEFAULT_SCOPE$1) {
|
|
2349
|
-
return this.profileStore.getAdminConfig(scope);
|
|
2350
|
-
}
|
|
2351
|
-
/**
|
|
2352
|
-
* Updates the resolved admin config.
|
|
2353
|
-
*/
|
|
2354
|
-
async updateAdminConfig(update, scope = DEFAULT_SCOPE$1) {
|
|
2355
|
-
await this.profileStore.updateConfig(update, scope);
|
|
2356
|
-
return this.profileStore.getAdminConfig(scope);
|
|
2357
|
-
}
|
|
2358
|
-
/**
|
|
2359
|
-
* Returns the currently selected profile for a scope/slot pair.
|
|
2360
|
-
*/
|
|
2361
|
-
async getActiveProfile(scope = DEFAULT_SCOPE$1, slot = DEFAULT_SLOT$1) {
|
|
2362
|
-
return this.profileStore.getActiveProfile(scope, slot);
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* Creates or updates a profile in the scoped settings file.
|
|
2366
|
-
*/
|
|
2367
|
-
async upsertProfile(profile, scope = DEFAULT_SCOPE$1) {
|
|
2368
|
-
return this.profileStore.upsertProfile(profile, scope);
|
|
2369
|
-
}
|
|
2370
|
-
/**
|
|
2371
|
-
* Selects the active profile by profile id for the requested slot.
|
|
2372
|
-
*/
|
|
2373
|
-
async setActiveProfile(profileId, scope = DEFAULT_SCOPE$1, slot = DEFAULT_SLOT$1) {
|
|
2374
|
-
return this.profileStore.setActiveProfile(profileId, scope, slot);
|
|
2375
|
-
}
|
|
2376
|
-
/**
|
|
2377
|
-
* Returns the active profile currently used for routing requests.
|
|
2378
|
-
*/
|
|
2379
|
-
async getCurrentModel(scope = DEFAULT_SCOPE$1, slot = DEFAULT_SLOT$1) {
|
|
2380
|
-
return this.profileStore.getActiveProfile(scope, slot);
|
|
2381
|
-
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Switches the selected model by model name.
|
|
2384
|
-
*/
|
|
2385
|
-
async switchModel(model, scope = DEFAULT_SCOPE$1, slot = DEFAULT_SLOT$1) {
|
|
2386
|
-
const profiles = await this.profileStore.listProfiles(scope);
|
|
2387
|
-
const current = await this.profileStore.getResolvedSlotConfig(scope, slot);
|
|
2388
|
-
const profile = profiles.find((item) => item.model === model && item.provider === current.provider && item.enabled) ?? profiles.find((item) => item.model === model && item.enabled);
|
|
2389
|
-
if (!profile) throw new GatewayConfigError(`Model not found or disabled: ${model}`, MODEL_NOT_FOUND, 400, { cause: {
|
|
2390
|
-
model,
|
|
2391
|
-
scope,
|
|
2392
|
-
slot
|
|
2393
|
-
} });
|
|
2394
|
-
return this.profileStore.updateConfig({ models: { [slot]: {
|
|
2395
|
-
main: {
|
|
2396
|
-
provider: profile.provider,
|
|
2397
|
-
model: profile.model,
|
|
2398
|
-
reasoningEffort: profile.reasoningEffort
|
|
2399
|
-
},
|
|
2400
|
-
fallbacks: (await this.profileStore.getAdminConfig(scope)).scopeModels[slot]?.fallbacks ?? []
|
|
2401
|
-
} } }, scope);
|
|
2402
|
-
}
|
|
2403
|
-
/**
|
|
2404
|
-
* Lists persisted history for a scope.
|
|
2405
|
-
*/
|
|
2406
|
-
async listHistory(scope = DEFAULT_SCOPE$1, limit = DEFAULT_HISTORY_PAGE_SIZE, cursor) {
|
|
2407
|
-
return this.historyService.listHistory(scope, limit, cursor);
|
|
2408
|
-
}
|
|
2409
|
-
/**
|
|
2410
|
-
* Clears persisted history for a scope.
|
|
2411
|
-
*/
|
|
2412
|
-
async clearHistory(scope = DEFAULT_SCOPE$1) {
|
|
2413
|
-
return { deleted: await this.historyService.clearScope(scope) };
|
|
2414
|
-
}
|
|
2415
|
-
/**
|
|
2416
|
-
* Lists known scopes from config files and stored history.
|
|
2417
|
-
*/
|
|
2418
|
-
async listScopes() {
|
|
2419
|
-
const [configScopes, historyScopes] = await Promise.all([this.profileStore.listScopes(), this.historyService.listScopes()]);
|
|
2420
|
-
return Array.from(new Set([
|
|
2421
|
-
...configScopes,
|
|
2422
|
-
...historyScopes,
|
|
2423
|
-
DEFAULT_SCOPE$1
|
|
2424
|
-
])).sort();
|
|
2425
|
-
}
|
|
2426
|
-
/**
|
|
2427
|
-
* Returns persisted history stats for a scope.
|
|
2428
|
-
*/
|
|
2429
|
-
async getHistoryStats(scope = DEFAULT_SCOPE$1) {
|
|
2430
|
-
return this.historyService.getStats(scope);
|
|
2431
|
-
}
|
|
2432
|
-
/**
|
|
2433
|
-
* Returns the current gateway status, auth state, and active slot routing.
|
|
2434
|
-
*/
|
|
2435
|
-
async getStatus(scope = DEFAULT_SCOPE$1, port, pid) {
|
|
2436
|
-
const config = await this.profileStore.getConfig(scope);
|
|
2437
|
-
const activeSlot = await this.profileStore.getResolvedSlotConfig(scope, DEFAULT_SLOT$1);
|
|
2438
|
-
const authStatus = await this.codexAuth.getAuthStatus();
|
|
2439
|
-
const slotModels = Object.fromEntries(await Promise.all(MODEL_SLOTS.map(async (slot) => {
|
|
2440
|
-
const resolved = await this.profileStore.getResolvedSlotConfig(scope, slot);
|
|
2441
|
-
return [slot, {
|
|
2442
|
-
profileId: resolved.profileId,
|
|
2443
|
-
provider: resolved.provider,
|
|
2444
|
-
model: resolved.model,
|
|
2445
|
-
reasoningEffort: resolved.reasoningEffort,
|
|
2446
|
-
thinkingDisabled: resolved.thinkingDisabled ?? false
|
|
2447
|
-
}];
|
|
2448
|
-
})));
|
|
2449
|
-
return {
|
|
2450
|
-
running: true,
|
|
2451
|
-
port,
|
|
2452
|
-
pid,
|
|
2453
|
-
scope,
|
|
2454
|
-
activeProfileId: config.activeProfileId,
|
|
2455
|
-
activeModel: activeSlot.model ?? void 0,
|
|
2456
|
-
activeReasoningEffort: activeSlot.reasoningEffort,
|
|
2457
|
-
slotModels,
|
|
2458
|
-
auth: authStatus,
|
|
2459
|
-
profiles: config.profiles
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
/**
|
|
2463
|
-
* Returns the OpenAI-compatible model listing served by the proxy.
|
|
2464
|
-
*/
|
|
2465
|
-
async getModels(scope = DEFAULT_SCOPE$1) {
|
|
2466
|
-
return {
|
|
2467
|
-
object: "list",
|
|
2468
|
-
data: (await this.profileStore.listProfiles(scope)).map((profile) => ({
|
|
2469
|
-
id: profile.model,
|
|
2470
|
-
object: "model",
|
|
2471
|
-
created: 0,
|
|
2472
|
-
owned_by: `${profile.provider}:${profile.id}`,
|
|
2473
|
-
metadata: {
|
|
2474
|
-
profileId: profile.id,
|
|
2475
|
-
reasoningEffort: profile.reasoningEffort
|
|
2476
|
-
}
|
|
2477
|
-
}))
|
|
2478
|
-
};
|
|
2479
|
-
}
|
|
2480
|
-
/**
|
|
2481
|
-
* Forwards a Claude-compatible request to the configured upstream Codex endpoint.
|
|
2482
|
-
*/
|
|
2483
|
-
async forwardClaudeRequest(requestBody, scope = DEFAULT_SCOPE$1, requestHeaders = new Headers()) {
|
|
2484
|
-
const requestId = ulid();
|
|
2485
|
-
let slot = DEFAULT_SLOT$1;
|
|
2486
|
-
let resolved = null;
|
|
2487
|
-
try {
|
|
2488
|
-
const parsed = this.parseClaudeRequest(requestBody);
|
|
2489
|
-
const adminConfig = await this.profileStore.getAdminConfig(scope);
|
|
2490
|
-
slot = this.resolveSlot(parsed.model, adminConfig);
|
|
2491
|
-
resolved = this.resolveRequestedModel(parsed.model, adminConfig, slot);
|
|
2492
|
-
if (!resolved.model) {
|
|
2493
|
-
const message$1 = `No active model configured for scope '${scope}' and slot '${slot}'`;
|
|
2494
|
-
await this.recordError(scope, slot, resolved, requestId, message$1, requestBody);
|
|
2495
|
-
return this.createErrorResponse(400, message$1);
|
|
2496
|
-
}
|
|
2497
|
-
const attemptTargets = this.buildAttemptTargets(adminConfig, resolved, slot);
|
|
2498
|
-
if (attemptTargets.length === 0) {
|
|
2499
|
-
const message$1 = `No provider configured for scope '${scope}' and slot '${slot}'`;
|
|
2500
|
-
await this.recordError(scope, slot, resolved, requestId, message$1, requestBody);
|
|
2501
|
-
return this.createErrorResponse(400, message$1);
|
|
2502
|
-
}
|
|
2503
|
-
await this.recordRequest(scope, slot, attemptTargets[0].resolved, requestId, parsed, requestBody);
|
|
2504
|
-
let lastRecoverableFailure = null;
|
|
2505
|
-
for (const target of attemptTargets) {
|
|
2506
|
-
const result = await this.forwardSingleAttempt(target, parsed, requestBody, scope, slot, requestHeaders);
|
|
2507
|
-
if (result.ok) {
|
|
2508
|
-
await this.recordResponse(scope, slot, target.resolved, requestId, result.success.upstreamText, result.success.claudeBody);
|
|
2509
|
-
return result.success.response;
|
|
2510
|
-
}
|
|
2511
|
-
const { error, payloadForHistory } = result.failure;
|
|
2512
|
-
if (!this.isRecoverableUpstreamError(error)) {
|
|
2513
|
-
this.logger.error("[model-proxy-mcp] Non-recoverable model attempt failed", {
|
|
2514
|
-
scope,
|
|
2515
|
-
slot,
|
|
2516
|
-
model: target.resolved.model,
|
|
2517
|
-
code: error.code,
|
|
2518
|
-
status: error.status,
|
|
2519
|
-
message: error.message
|
|
2520
|
-
});
|
|
2521
|
-
await this.recordError(scope, slot, target.resolved, requestId, error.message, payloadForHistory);
|
|
2522
|
-
return this.createErrorResponse(error.status, error.message);
|
|
2523
|
-
}
|
|
2524
|
-
lastRecoverableFailure = result.failure;
|
|
2525
|
-
this.logger.warn("[model-proxy-mcp] Recoverable model attempt failed; trying fallback", {
|
|
2526
|
-
scope,
|
|
2527
|
-
slot,
|
|
2528
|
-
model: target.resolved.model,
|
|
2529
|
-
code: error.code,
|
|
2530
|
-
status: error.status,
|
|
2531
|
-
message: error.message
|
|
2532
|
-
});
|
|
2533
|
-
await this.recordError(scope, slot, target.resolved, requestId, `Attempt failed for ${target.label}: ${error.message}`, payloadForHistory);
|
|
2534
|
-
}
|
|
2535
|
-
if (lastRecoverableFailure) {
|
|
2536
|
-
await this.recordError(scope, slot, resolved, requestId, lastRecoverableFailure.error.message, lastRecoverableFailure.payloadForHistory);
|
|
2537
|
-
return this.createErrorResponse(lastRecoverableFailure.error.status, lastRecoverableFailure.error.message);
|
|
2538
|
-
}
|
|
2539
|
-
const message = "No model attempt could be executed";
|
|
2540
|
-
await this.recordError(scope, slot, resolved, requestId, message, requestBody);
|
|
2541
|
-
return this.createErrorResponse(HTTP_STATUS_INTERNAL_SERVER_ERROR, message);
|
|
2542
|
-
} catch (error) {
|
|
2543
|
-
const gatewayError = error instanceof GatewayServiceError ? error : new GatewayServiceError("Failed to process Claude proxy request", REQUEST_FORWARD_FAILED, 500, { cause: error });
|
|
2544
|
-
this.logger.error("[model-proxy-mcp] Request forwarding failed", {
|
|
2545
|
-
scope,
|
|
2546
|
-
slot,
|
|
2547
|
-
code: gatewayError.code,
|
|
2548
|
-
message: gatewayError.message,
|
|
2549
|
-
cause: gatewayError.cause
|
|
2550
|
-
});
|
|
2551
|
-
await this.recordError(scope, slot, resolved, requestId, gatewayError.message, requestBody);
|
|
2552
|
-
return this.createErrorResponse(gatewayError.status, gatewayError.message);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
buildAttemptTargets(adminConfig, primary, slot) {
|
|
2556
|
-
const targets = [];
|
|
2557
|
-
const dedupe = /* @__PURE__ */ new Set();
|
|
2558
|
-
const candidates = [primary, ...primary.fallbacks.map((fallback) => ({
|
|
2559
|
-
...primary,
|
|
2560
|
-
...fallback,
|
|
2561
|
-
slot
|
|
2562
|
-
}))];
|
|
2563
|
-
for (const candidate of candidates) {
|
|
2564
|
-
const providerId = candidate.provider;
|
|
2565
|
-
const modelId = candidate.model;
|
|
2566
|
-
if (!providerId || !modelId) continue;
|
|
2567
|
-
const provider = adminConfig.providers[providerId];
|
|
2568
|
-
if (!provider) continue;
|
|
2569
|
-
const key = `${providerId}:${modelId}:${candidate.reasoningEffort}:${candidate.thinkingDisabled ? "off" : "on"}`;
|
|
2570
|
-
if (dedupe.has(key)) continue;
|
|
2571
|
-
dedupe.add(key);
|
|
2572
|
-
targets.push({
|
|
2573
|
-
resolved: {
|
|
2574
|
-
...candidate,
|
|
2575
|
-
slot,
|
|
2576
|
-
provider: providerId,
|
|
2577
|
-
model: modelId,
|
|
2578
|
-
providerType: provider.type,
|
|
2579
|
-
endpoint: provider.endpoint,
|
|
2580
|
-
reasoningEffort: candidate.reasoningEffort,
|
|
2581
|
-
thinkingDisabled: candidate.thinkingDisabled ?? false,
|
|
2582
|
-
fallbacks: []
|
|
2583
|
-
},
|
|
2584
|
-
provider,
|
|
2585
|
-
label: `${providerId}/${modelId}`
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
return targets;
|
|
2589
|
-
}
|
|
2590
|
-
async forwardSingleAttempt(target, parsedRequest, requestBody, scope, slot, requestHeaders) {
|
|
2591
|
-
try {
|
|
2592
|
-
if (target.resolved.providerType === ANTHROPIC_COMPATIBLE_PROVIDER) return {
|
|
2593
|
-
ok: true,
|
|
2594
|
-
success: await this.forwardAnthropicCompatibleRequest(requestBody, scope, slot, target.resolved, target.provider, requestHeaders)
|
|
2595
|
-
};
|
|
2596
|
-
if (target.resolved.providerType === GEMINI_DIRECT_PROVIDER) return {
|
|
2597
|
-
ok: true,
|
|
2598
|
-
success: await this.forwardGeminiDirectRequest(parsedRequest, scope, slot, target.resolved, target.provider, requestHeaders)
|
|
2599
|
-
};
|
|
2600
|
-
return {
|
|
2601
|
-
ok: true,
|
|
2602
|
-
success: await this.forwardCodexRequest(requestBody, scope, slot, target.resolved)
|
|
2603
|
-
};
|
|
2604
|
-
} catch (error) {
|
|
2605
|
-
return {
|
|
2606
|
-
ok: false,
|
|
2607
|
-
failure: {
|
|
2608
|
-
error: error instanceof GatewayServiceError ? error : new GatewayServiceError("Failed to process Claude proxy request", REQUEST_FORWARD_FAILED, 500, { cause: error }),
|
|
2609
|
-
payloadForHistory: error instanceof GatewayUpstreamError ? error.upstreamBody : requestBody
|
|
2610
|
-
}
|
|
2611
|
-
};
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
isRecoverableUpstreamError(error) {
|
|
2615
|
-
return error instanceof GatewayUpstreamError && RECOVERABLE_UPSTREAM_STATUSES.has(error.status);
|
|
2616
|
-
}
|
|
2617
|
-
async forwardCodexRequest(requestBody, scope, slot, resolved) {
|
|
2618
|
-
const resolvedModel = resolved.model;
|
|
2619
|
-
if (!resolvedModel) throw new GatewayConfigError(`No active model configured for slot '${slot}'`, MODEL_NOT_FOUND, 400);
|
|
2620
|
-
if (!await this.codexAuth.getAccessToken()) throw new GatewayAuthError("Missing Codex auth. Sign in with the official Codex CLI first.", "AUTH_MISSING", 401);
|
|
2621
|
-
const transformer = new ClaudeToOpenAITransformer({
|
|
2622
|
-
toProvider: CHATGPT_CODEX_PROVIDER,
|
|
2623
|
-
toModel: resolvedModel,
|
|
2624
|
-
toEndpoint: resolved.endpoint ?? CLAUDE_MESSAGES_ENDPOINT,
|
|
2625
|
-
sessionReasoningEffort: resolved.reasoningEffort,
|
|
2626
|
-
thinkingDisabled: resolved.thinkingDisabled ?? false,
|
|
2627
|
-
logger: this.logger
|
|
2628
|
-
}, this.codexAuth);
|
|
2629
|
-
const responseTransformer = new OpenAIToClaudeTransformer(this.logger, resolved.thinkingDisabled ?? false);
|
|
2630
|
-
const transformed = await transformer.transform(CLAUDE_MESSAGES_ENDPOINT, requestBody);
|
|
2631
|
-
const upstreamResponse = await this.fetchImpl(transformed.url, {
|
|
2632
|
-
method: "POST",
|
|
2633
|
-
headers: transformed.headers,
|
|
2634
|
-
body: transformed.body
|
|
2635
|
-
});
|
|
2636
|
-
const upstreamText = await upstreamResponse.text();
|
|
2637
|
-
if (!upstreamResponse.ok) {
|
|
2638
|
-
this.logger.error("[model-proxy-mcp] Upstream request failed", {
|
|
2639
|
-
scope,
|
|
2640
|
-
slot,
|
|
2641
|
-
profileId: resolved.profileId,
|
|
2642
|
-
model: resolved.model,
|
|
2643
|
-
status: upstreamResponse.status,
|
|
2644
|
-
body: upstreamText
|
|
2645
|
-
});
|
|
2646
|
-
throw new GatewayUpstreamError(upstreamText || "Codex upstream request failed", upstreamResponse.status, upstreamText, `${resolved.provider}/${resolved.model}`);
|
|
2647
|
-
}
|
|
2648
|
-
const body = responseTransformer.transform(upstreamText);
|
|
2649
|
-
return {
|
|
2650
|
-
response: {
|
|
2651
|
-
status: HTTP_STATUS_OK,
|
|
2652
|
-
body,
|
|
2653
|
-
headers: this.createSuccessHeaders(scope, slot)
|
|
2654
|
-
},
|
|
2655
|
-
upstreamText,
|
|
2656
|
-
claudeBody: body
|
|
2657
|
-
};
|
|
2658
|
-
}
|
|
2659
|
-
/**
|
|
2660
|
-
* Forwards a Claude-compatible request to the Gemini GenerateContent REST API and translates the result back to Claude SSE.
|
|
2661
|
-
*/
|
|
2662
|
-
async forwardGeminiDirectRequest(parsedRequest, scope, slot, resolved, provider, requestHeaders) {
|
|
2663
|
-
try {
|
|
2664
|
-
const resolvedModel = resolved.model;
|
|
2665
|
-
if (!resolvedModel) throw new GatewayConfigError(`No active Gemini model configured for scope '${scope}' and slot '${slot}'`, MODEL_NOT_FOUND, 400);
|
|
2666
|
-
const authHeaders = await new GeminiAuth(this.logger).resolveHeaders(provider);
|
|
2667
|
-
const requestTransformer = new ClaudeToGeminiTransformer();
|
|
2668
|
-
const responseTransformer = new GeminiToClaudeTransformer();
|
|
2669
|
-
const geminiRequest = {
|
|
2670
|
-
model: parsedRequest.model,
|
|
2671
|
-
system: parsedRequest.system,
|
|
2672
|
-
messages: parsedRequest.messages,
|
|
2673
|
-
tools: parsedRequest.tools,
|
|
2674
|
-
max_tokens: parsedRequest.max_tokens
|
|
2675
|
-
};
|
|
2676
|
-
const transformed = requestTransformer.transform(geminiRequest, resolvedModel, resolved.reasoningEffort, resolved.thinkingDisabled ?? false);
|
|
2677
|
-
if (authHeaders.authType === GEMINI_PERSONAL_OAUTH_AUTH_TYPE) return await this.forwardGeminiCodeAssistRequest(transformed.body, parsedRequest, scope, slot, resolved, authHeaders, responseTransformer);
|
|
2678
|
-
const method = parsedRequest.stream === false ? GEMINI_GENERATE_CONTENT_METHOD : GEMINI_STREAM_GENERATE_CONTENT_METHOD;
|
|
2679
|
-
const url = this.createGeminiRequestUrl(provider, transformed.modelPath, method, parsedRequest.stream !== false);
|
|
2680
|
-
const headers = new Headers(authHeaders.headers);
|
|
2681
|
-
headers.set(RESPONSE_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE);
|
|
2682
|
-
headers.set(ACCEPT_HEADER, requestHeaders.get(ACCEPT_HEADER) ?? SSE_CONTENT_TYPE);
|
|
2683
|
-
const timeoutMs = provider.apiTimeoutMs ?? this.resolveUpstreamTimeoutMs();
|
|
2684
|
-
const upstreamResponse = await this.fetchImpl(url, {
|
|
2685
|
-
method: "POST",
|
|
2686
|
-
headers,
|
|
2687
|
-
body: JSON.stringify(transformed.body),
|
|
2688
|
-
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0
|
|
2689
|
-
});
|
|
2690
|
-
const upstreamText = await upstreamResponse.text();
|
|
2691
|
-
if (!upstreamResponse.ok) {
|
|
2692
|
-
this.logger.error("[model-proxy-mcp] Gemini upstream request failed", {
|
|
2693
|
-
scope,
|
|
2694
|
-
slot,
|
|
2695
|
-
profileId: resolved.profileId,
|
|
2696
|
-
model: resolved.model,
|
|
2697
|
-
status: upstreamResponse.status,
|
|
2698
|
-
body: upstreamText,
|
|
2699
|
-
authMode: authHeaders.authMode,
|
|
2700
|
-
authSource: authHeaders.authSource
|
|
2701
|
-
});
|
|
2702
|
-
throw new GatewayUpstreamError(upstreamText || "Gemini upstream request failed", upstreamResponse.status, upstreamText, `${resolved.provider}/${resolved.model}`);
|
|
2703
|
-
}
|
|
2704
|
-
const body = parsedRequest.stream === false ? responseTransformer.transformBuffered(upstreamText, resolvedModel) : responseTransformer.transformStreaming(upstreamText, resolvedModel);
|
|
2705
|
-
return {
|
|
2706
|
-
response: {
|
|
2707
|
-
status: HTTP_STATUS_OK,
|
|
2708
|
-
body,
|
|
2709
|
-
headers: this.createSuccessHeaders(scope, slot)
|
|
2710
|
-
},
|
|
2711
|
-
upstreamText,
|
|
2712
|
-
claudeBody: body
|
|
2713
|
-
};
|
|
2714
|
-
} catch (error) {
|
|
2715
|
-
if (error instanceof GeminiAuthError) throw new GatewayAuthError(error.message, error.code, 401, { cause: error });
|
|
2716
|
-
throw error;
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
async forwardGeminiCodeAssistRequest(transformedBody, parsedRequest, scope, slot, resolved, authHeaders, responseTransformer) {
|
|
2720
|
-
const projectId = await this.resolveCodeAssistProjectId(authHeaders);
|
|
2721
|
-
const url = this.createCodeAssistUrl(parsedRequest.stream === false ? GEMINI_GENERATE_CONTENT_METHOD : GEMINI_STREAM_GENERATE_CONTENT_METHOD, parsedRequest.stream !== false);
|
|
2722
|
-
const headers = new Headers(authHeaders.headers);
|
|
2723
|
-
headers.set(RESPONSE_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE);
|
|
2724
|
-
headers.set(ACCEPT_HEADER, SSE_CONTENT_TYPE);
|
|
2725
|
-
const requestBody = {
|
|
2726
|
-
model: resolved.model,
|
|
2727
|
-
project: projectId,
|
|
2728
|
-
user_prompt_id: ulid(),
|
|
2729
|
-
request: this.toCodeAssistGenerateContentRequest(transformedBody)
|
|
2730
|
-
};
|
|
2731
|
-
const upstreamResponse = await this.fetchImpl(url, {
|
|
2732
|
-
method: "POST",
|
|
2733
|
-
headers,
|
|
2734
|
-
body: JSON.stringify(requestBody)
|
|
2735
|
-
});
|
|
2736
|
-
const upstreamText = await upstreamResponse.text();
|
|
2737
|
-
if (!upstreamResponse.ok) {
|
|
2738
|
-
this.logger.error("[model-proxy-mcp] Gemini Code Assist request failed", {
|
|
2739
|
-
scope,
|
|
2740
|
-
slot,
|
|
2741
|
-
profileId: resolved.profileId,
|
|
2742
|
-
model: resolved.model,
|
|
2743
|
-
status: upstreamResponse.status,
|
|
2744
|
-
body: upstreamText,
|
|
2745
|
-
authMode: authHeaders.authMode,
|
|
2746
|
-
authSource: authHeaders.authSource,
|
|
2747
|
-
authType: authHeaders.authType
|
|
2748
|
-
});
|
|
2749
|
-
throw new GatewayUpstreamError(upstreamText || "Gemini Code Assist request failed", upstreamResponse.status, upstreamText, `${resolved.provider}/${resolved.model}`);
|
|
2750
|
-
}
|
|
2751
|
-
const normalizedResponse = parsedRequest.stream === false ? this.normalizeCodeAssistBufferedResponse(upstreamText) : this.normalizeCodeAssistStreamingResponse(upstreamText);
|
|
2752
|
-
const body = parsedRequest.stream === false ? responseTransformer.transformBuffered(normalizedResponse, resolved.model ?? "gemini") : responseTransformer.transformStreaming(normalizedResponse, resolved.model ?? "gemini");
|
|
2753
|
-
return {
|
|
2754
|
-
response: {
|
|
2755
|
-
status: HTTP_STATUS_OK,
|
|
2756
|
-
body,
|
|
2757
|
-
headers: this.createSuccessHeaders(scope, slot)
|
|
2758
|
-
},
|
|
2759
|
-
upstreamText,
|
|
2760
|
-
claudeBody: body
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
/**
|
|
2764
|
-
* Builds response headers shared by successful SSE responses.
|
|
2765
|
-
*/
|
|
2766
|
-
createSuccessHeaders(scope, slot) {
|
|
2767
|
-
return {
|
|
2768
|
-
[RESPONSE_CONTENT_TYPE_HEADER]: SSE_CONTENT_TYPE,
|
|
2769
|
-
[RESPONSE_CACHE_CONTROL_HEADER]: HEADER_NO_CACHE,
|
|
2770
|
-
[RESPONSE_CONNECTION_HEADER]: HEADER_KEEP_ALIVE,
|
|
2771
|
-
"x-model-proxy-service": DEFAULT_SERVICE_NAME,
|
|
2772
|
-
"x-model-proxy-scope": scope,
|
|
2773
|
-
"x-model-proxy-slot": slot
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
|
-
/**
|
|
2777
|
-
* Builds the target Gemini REST endpoint for buffered or streaming requests.
|
|
2778
|
-
*/
|
|
2779
|
-
createGeminiRequestUrl(provider, modelPath, method, streaming) {
|
|
2780
|
-
const baseUrl = provider.endpoint.replace(/\/$/, "");
|
|
2781
|
-
const apiVersion = provider.apiVersion ?? GEMINI_DEFAULT_API_VERSION;
|
|
2782
|
-
const url = new URL(`${baseUrl}/${apiVersion}/${modelPath}:${method}`);
|
|
2783
|
-
if (streaming) {
|
|
2784
|
-
const [key, value] = GEMINI_STREAM_QUERY_PARAM.split("=");
|
|
2785
|
-
if (key && value) url.searchParams.set(key, value);
|
|
2786
|
-
}
|
|
2787
|
-
return url.toString();
|
|
2788
|
-
}
|
|
2789
|
-
createCodeAssistUrl(method, streaming = false) {
|
|
2790
|
-
const url = new URL(`${GEMINI_CODE_ASSIST_ENDPOINT}/${GEMINI_CODE_ASSIST_API_VERSION}:${method}`);
|
|
2791
|
-
if (streaming) {
|
|
2792
|
-
const [key, value] = GEMINI_STREAM_QUERY_PARAM.split("=");
|
|
2793
|
-
if (key && value) url.searchParams.set(key, value);
|
|
2794
|
-
}
|
|
2795
|
-
return url.toString();
|
|
2796
|
-
}
|
|
2797
|
-
async resolveCodeAssistProjectId(authHeaders) {
|
|
2798
|
-
const headers = new Headers(authHeaders.headers);
|
|
2799
|
-
headers.set(RESPONSE_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE);
|
|
2800
|
-
const configuredProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID || void 0;
|
|
2801
|
-
const response = await this.fetchImpl(this.createCodeAssistUrl(CODE_ASSIST_LOAD_METHOD), {
|
|
2802
|
-
method: "POST",
|
|
2803
|
-
headers,
|
|
2804
|
-
body: JSON.stringify({
|
|
2805
|
-
cloudaicompanionProject: configuredProject,
|
|
2806
|
-
metadata: {
|
|
2807
|
-
ideType: CODE_ASSIST_IDE_TYPE,
|
|
2808
|
-
platform: CODE_ASSIST_PLATFORM,
|
|
2809
|
-
pluginType: CODE_ASSIST_PLUGIN_TYPE,
|
|
2810
|
-
duetProject: configuredProject
|
|
2811
|
-
}
|
|
2812
|
-
})
|
|
2813
|
-
});
|
|
2814
|
-
const responseText = await response.text();
|
|
2815
|
-
if (!response.ok) throw new GatewayUpstreamError(responseText || "Failed to load Gemini Code Assist project context", response.status, responseText, "google-gemini-direct/loadCodeAssist");
|
|
2816
|
-
const projectId = JSON.parse(responseText).cloudaicompanionProject || configuredProject;
|
|
2817
|
-
if (!projectId) throw new GatewayAuthError("Gemini Code Assist did not return a usable project ID.", "GEMINI_PROJECT_MISSING", 401);
|
|
2818
|
-
return projectId;
|
|
2819
|
-
}
|
|
2820
|
-
toCodeAssistGenerateContentRequest(body) {
|
|
2821
|
-
return {
|
|
2822
|
-
contents: body.contents,
|
|
2823
|
-
systemInstruction: body.system_instruction,
|
|
2824
|
-
tools: body.tools,
|
|
2825
|
-
toolConfig: body.tool_config,
|
|
2826
|
-
generationConfig: body.generationConfig,
|
|
2827
|
-
session_id: ulid()
|
|
2828
|
-
};
|
|
2829
|
-
}
|
|
2830
|
-
normalizeCodeAssistBufferedResponse(responseBody) {
|
|
2831
|
-
const parsed = JSON.parse(responseBody);
|
|
2832
|
-
return JSON.stringify(this.extractCodeAssistResponse(parsed));
|
|
2833
|
-
}
|
|
2834
|
-
normalizeCodeAssistStreamingResponse(responseBody) {
|
|
2835
|
-
const chunks = [];
|
|
2836
|
-
for (const event of responseBody.split("\n\n")) {
|
|
2837
|
-
const dataLine = event.split("\n").find((line) => line.startsWith("data:"))?.slice(5).trim();
|
|
2838
|
-
if (!dataLine || dataLine === "[DONE]") continue;
|
|
2839
|
-
const parsed = JSON.parse(dataLine);
|
|
2840
|
-
chunks.push(`data: ${JSON.stringify(this.extractCodeAssistResponse(parsed))}`);
|
|
2841
|
-
chunks.push("");
|
|
2842
|
-
}
|
|
2843
|
-
return chunks.join("\n");
|
|
2844
|
-
}
|
|
2845
|
-
extractCodeAssistResponse(payload) {
|
|
2846
|
-
return {
|
|
2847
|
-
candidates: payload.response?.candidates ?? [],
|
|
2848
|
-
usageMetadata: payload.response?.usageMetadata,
|
|
2849
|
-
modelVersion: payload.response?.modelVersion
|
|
2850
|
-
};
|
|
2851
|
-
}
|
|
2852
|
-
/**
|
|
2853
|
-
* Resolves the logical slot from a Claude model name or configured model mapping.
|
|
2854
|
-
*/
|
|
2855
|
-
resolveSlot(model, adminConfig) {
|
|
2856
|
-
if (!model) return DEFAULT_SLOT$1;
|
|
2857
|
-
const mappedSlot = MODEL_SLOT_BY_NAME[model];
|
|
2858
|
-
if (mappedSlot) return mappedSlot;
|
|
2859
|
-
for (const slot of MODEL_SLOTS) if (adminConfig.slots[slot].model === model) return slot;
|
|
2860
|
-
return DEFAULT_SLOT$1;
|
|
2861
|
-
}
|
|
2862
|
-
/**
|
|
2863
|
-
* Reconciles a request-specified concrete model with the resolved slot configuration.
|
|
2864
|
-
*/
|
|
2865
|
-
resolveRequestedModel(model, adminConfig, slot) {
|
|
2866
|
-
const resolved = adminConfig.slots[slot];
|
|
2867
|
-
if (!model || MODEL_SLOT_BY_NAME[model]) return resolved;
|
|
2868
|
-
const profile = adminConfig.models.find((item) => item.model === model && item.provider === resolved.provider && item.enabled) ?? adminConfig.models.find((item) => item.model === model && item.enabled);
|
|
2869
|
-
if (!profile && resolved.model !== model) throw new GatewayConfigError(`Model not found or disabled: ${model}`, MODEL_NOT_FOUND, 400, { cause: {
|
|
2870
|
-
model,
|
|
2871
|
-
slot,
|
|
2872
|
-
scope: adminConfig.scope
|
|
2873
|
-
} });
|
|
2874
|
-
return {
|
|
2875
|
-
...resolved,
|
|
2876
|
-
profileId: profile?.id ?? null,
|
|
2877
|
-
label: profile?.label ?? null,
|
|
2878
|
-
provider: profile?.provider ?? resolved.provider,
|
|
2879
|
-
providerType: profile?.providerType ?? resolved.providerType,
|
|
2880
|
-
endpoint: profile?.endpoint ?? resolved.endpoint,
|
|
2881
|
-
model: profile?.model ?? model,
|
|
2882
|
-
reasoningEffort: profile?.reasoningEffort ?? resolved.reasoningEffort,
|
|
2883
|
-
thinkingDisabled: resolved.thinkingDisabled ?? false
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
/**
|
|
2887
|
-
* Forwards a Claude-compatible request to an Anthropic-compatible upstream such as Z.ai.
|
|
2888
|
-
*/
|
|
2889
|
-
async forwardAnthropicCompatibleRequest(requestBody, scope, slot, resolved, provider, requestHeaders) {
|
|
2890
|
-
const token = this.getAnthropicCompatibleAuthToken(provider.authTokenEnvVar);
|
|
2891
|
-
if (!token) throw new GatewayAuthError(`Missing upstream auth token for ${ANTHROPIC_COMPATIBLE_PROVIDER}. Set ${provider.authTokenEnvVar || DEFAULT_UPSTREAM_AUTH_ENV}.`, "UPSTREAM_AUTH_MISSING", 401);
|
|
2892
|
-
const payload = this.sanitizeClaudePayloadForAnthropic({
|
|
2893
|
-
...CLAUDE_REQUEST_SCHEMA.parse(JSON.parse(requestBody)),
|
|
2894
|
-
model: resolved.model
|
|
2895
|
-
});
|
|
2896
|
-
const headers = new Headers();
|
|
2897
|
-
headers.set(ACCEPT_HEADER, requestHeaders.get(ACCEPT_HEADER) ?? SSE_CONTENT_TYPE);
|
|
2898
|
-
headers.set(RESPONSE_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE);
|
|
2899
|
-
headers.set(ANTHROPIC_VERSION_HEADER, requestHeaders.get(ANTHROPIC_VERSION_HEADER) ?? DEFAULT_ANTHROPIC_VERSION);
|
|
2900
|
-
headers.set(X_API_KEY_HEADER, token);
|
|
2901
|
-
headers.set(AUTHORIZATION_HEADER, `Bearer ${token}`);
|
|
2902
|
-
const anthropicBeta = requestHeaders.get(ANTHROPIC_BETA_HEADER);
|
|
2903
|
-
if (anthropicBeta) headers.set(ANTHROPIC_BETA_HEADER, anthropicBeta);
|
|
2904
|
-
const timeoutMs = provider.apiTimeoutMs ?? this.resolveUpstreamTimeoutMs();
|
|
2905
|
-
const upstreamResponse = await this.fetchImpl(provider.endpoint, {
|
|
2906
|
-
method: "POST",
|
|
2907
|
-
headers,
|
|
2908
|
-
body: JSON.stringify(payload),
|
|
2909
|
-
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0
|
|
2910
|
-
});
|
|
2911
|
-
const upstreamText = await upstreamResponse.text();
|
|
2912
|
-
if (!upstreamResponse.ok) {
|
|
2913
|
-
this.logger.error("[model-proxy-mcp] Anthropic-compatible upstream request failed", {
|
|
2914
|
-
scope,
|
|
2915
|
-
slot,
|
|
2916
|
-
profileId: resolved.profileId,
|
|
2917
|
-
model: resolved.model,
|
|
2918
|
-
status: upstreamResponse.status,
|
|
2919
|
-
body: upstreamText
|
|
2920
|
-
});
|
|
2921
|
-
throw new GatewayUpstreamError(upstreamText || "Anthropic-compatible upstream request failed", upstreamResponse.status, upstreamText, `${resolved.provider}/${resolved.model}`);
|
|
2922
|
-
}
|
|
2923
|
-
return {
|
|
2924
|
-
response: {
|
|
2925
|
-
status: upstreamResponse.status,
|
|
2926
|
-
body: upstreamText,
|
|
2927
|
-
headers: {
|
|
2928
|
-
[RESPONSE_CONTENT_TYPE_HEADER]: upstreamResponse.headers.get(RESPONSE_CONTENT_TYPE_HEADER) ?? SSE_CONTENT_TYPE,
|
|
2929
|
-
[RESPONSE_CACHE_CONTROL_HEADER]: upstreamResponse.headers.get(RESPONSE_CACHE_CONTROL_HEADER) ?? HEADER_NO_CACHE,
|
|
2930
|
-
[RESPONSE_CONNECTION_HEADER]: upstreamResponse.headers.get(RESPONSE_CONNECTION_HEADER) ?? HEADER_KEEP_ALIVE,
|
|
2931
|
-
"x-model-proxy-service": DEFAULT_SERVICE_NAME,
|
|
2932
|
-
"x-model-proxy-scope": scope,
|
|
2933
|
-
"x-model-proxy-slot": slot
|
|
2934
|
-
}
|
|
2935
|
-
},
|
|
2936
|
-
upstreamText,
|
|
2937
|
-
claudeBody: upstreamText
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
/**
|
|
2941
|
-
* Resolves the environment-provided auth token for Anthropic-compatible upstreams.
|
|
2942
|
-
*/
|
|
2943
|
-
getAnthropicCompatibleAuthToken(configuredEnvVar) {
|
|
2944
|
-
const envVarName = configuredEnvVar || process.env[CONFIGURED_UPSTREAM_AUTH_ENV_NAME] || DEFAULT_UPSTREAM_AUTH_ENV;
|
|
2945
|
-
return process.env[envVarName] || process.env[FALLBACK_UPSTREAM_AUTH_ENV] || null;
|
|
2946
|
-
}
|
|
2947
|
-
/**
|
|
2948
|
-
* Resolves the request timeout for upstream Anthropic-compatible calls.
|
|
2949
|
-
*/
|
|
2950
|
-
resolveUpstreamTimeoutMs() {
|
|
2951
|
-
const rawTimeout = process.env[DEFAULT_API_TIMEOUT_ENV] || process.env[FALLBACK_UPSTREAM_TIMEOUT_ENV];
|
|
2952
|
-
if (!rawTimeout) return null;
|
|
2953
|
-
const timeoutMs = Number.parseInt(rawTimeout, 10);
|
|
2954
|
-
return Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : null;
|
|
2955
|
-
}
|
|
2956
|
-
createErrorResponse(status, message) {
|
|
2957
|
-
return {
|
|
2958
|
-
status,
|
|
2959
|
-
body: JSON.stringify({
|
|
2960
|
-
type: "error",
|
|
2961
|
-
error: {
|
|
2962
|
-
type: API_ERROR_TYPE,
|
|
2963
|
-
message
|
|
2964
|
-
}
|
|
2965
|
-
}),
|
|
2966
|
-
headers: { "content-type": ERROR_RESPONSE_CONTENT_TYPE }
|
|
2967
|
-
};
|
|
2968
|
-
}
|
|
2969
|
-
/**
|
|
2970
|
-
* Parses and validates an inbound Claude request.
|
|
2971
|
-
*/
|
|
2972
|
-
parseClaudeRequest(requestBody) {
|
|
2973
|
-
try {
|
|
2974
|
-
return CLAUDE_REQUEST_SCHEMA.parse(JSON.parse(requestBody));
|
|
2975
|
-
} catch (error) {
|
|
2976
|
-
if (error instanceof SyntaxError || error instanceof ZodError) throw new GatewayValidationError("Invalid Claude request payload", REQUEST_VALIDATION_FAILED, 400, { cause: error });
|
|
2977
|
-
throw error;
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
/**
|
|
2981
|
-
* Persists normalized inbound request messages for a proxied call.
|
|
2982
|
-
*/
|
|
2983
|
-
async recordRequest(scope, slot, resolved, requestId, parsedRequest, rawBody) {
|
|
2984
|
-
const entries = [];
|
|
2985
|
-
const systemEntries = this.normalizeSystemPayloads(parsedRequest.system);
|
|
2986
|
-
for (const payload of systemEntries) entries.push({
|
|
2987
|
-
scope,
|
|
2988
|
-
requestId,
|
|
2989
|
-
direction: REQUEST_DIRECTION,
|
|
2990
|
-
role: SYSTEM_ROLE,
|
|
2991
|
-
messageType: SYSTEM_ROLE,
|
|
2992
|
-
model: resolved.model,
|
|
2993
|
-
slot,
|
|
2994
|
-
payloadJson: JSON.stringify(payload)
|
|
2995
|
-
});
|
|
2996
|
-
const messages = Array.isArray(parsedRequest.messages) ? parsedRequest.messages : [];
|
|
2997
|
-
for (const message of messages) {
|
|
2998
|
-
const role = this.extractRole(message);
|
|
2999
|
-
entries.push({
|
|
3000
|
-
scope,
|
|
3001
|
-
requestId,
|
|
3002
|
-
direction: REQUEST_DIRECTION,
|
|
3003
|
-
role,
|
|
3004
|
-
messageType: MESSAGE_MESSAGE_TYPE,
|
|
3005
|
-
model: resolved.model,
|
|
3006
|
-
slot,
|
|
3007
|
-
payloadJson: JSON.stringify(message)
|
|
3008
|
-
});
|
|
3009
|
-
}
|
|
3010
|
-
if (entries.length === 0) entries.push({
|
|
3011
|
-
scope,
|
|
3012
|
-
requestId,
|
|
3013
|
-
direction: REQUEST_DIRECTION,
|
|
3014
|
-
role: null,
|
|
3015
|
-
messageType: RAW_REQUEST_MESSAGE_TYPE,
|
|
3016
|
-
model: resolved.model,
|
|
3017
|
-
slot,
|
|
3018
|
-
payloadJson: rawBody
|
|
3019
|
-
});
|
|
3020
|
-
await this.historyService.appendEntries(entries);
|
|
3021
|
-
}
|
|
3022
|
-
/**
|
|
3023
|
-
* Persists normalized upstream response items or a stream fallback.
|
|
3024
|
-
*/
|
|
3025
|
-
async recordResponse(scope, slot, resolved, requestId, upstreamText, claudeBody) {
|
|
3026
|
-
const parsedEntries = this.normalizeResponsePayloads(scope, requestId, slot, resolved.model, upstreamText);
|
|
3027
|
-
if (parsedEntries.length > 0) {
|
|
3028
|
-
await this.historyService.appendEntries(parsedEntries);
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
await this.historyService.appendEntries([{
|
|
3032
|
-
scope,
|
|
3033
|
-
requestId,
|
|
3034
|
-
direction: RESPONSE_DIRECTION,
|
|
3035
|
-
role: ASSISTANT_ROLE,
|
|
3036
|
-
messageType: RESPONSE_STREAM_MESSAGE_TYPE,
|
|
3037
|
-
model: resolved.model,
|
|
3038
|
-
slot,
|
|
3039
|
-
payloadJson: JSON.stringify({
|
|
3040
|
-
upstreamText,
|
|
3041
|
-
claudeBody
|
|
3042
|
-
})
|
|
3043
|
-
}]);
|
|
3044
|
-
}
|
|
3045
|
-
/**
|
|
3046
|
-
* Persists a redacted error record for the request lifecycle.
|
|
3047
|
-
*/
|
|
3048
|
-
async recordError(scope, slot, resolved, requestId, message, payload) {
|
|
3049
|
-
await this.historyService.appendEntries([{
|
|
3050
|
-
scope,
|
|
3051
|
-
requestId,
|
|
3052
|
-
direction: ERROR_DIRECTION,
|
|
3053
|
-
role: null,
|
|
3054
|
-
messageType: ERROR_MESSAGE_TYPE,
|
|
3055
|
-
model: resolved?.model ?? null,
|
|
3056
|
-
slot,
|
|
3057
|
-
payloadJson: JSON.stringify({
|
|
3058
|
-
message,
|
|
3059
|
-
payloadPreview: this.summarizePayload(payload)
|
|
3060
|
-
})
|
|
3061
|
-
}]);
|
|
3062
|
-
}
|
|
3063
|
-
/**
|
|
3064
|
-
* Normalizes Claude system payloads into storable entries.
|
|
3065
|
-
*/
|
|
3066
|
-
normalizeSystemPayloads(system) {
|
|
3067
|
-
if (!system) return [];
|
|
3068
|
-
if (typeof system === "string") return [{
|
|
3069
|
-
type: "text",
|
|
3070
|
-
text: system
|
|
3071
|
-
}];
|
|
3072
|
-
if (Array.isArray(system)) return system;
|
|
3073
|
-
return [system];
|
|
3074
|
-
}
|
|
3075
|
-
/**
|
|
3076
|
-
* Removes proxy-private metadata before sending Claude payloads to strict Anthropic-compatible upstreams.
|
|
3077
|
-
*/
|
|
3078
|
-
sanitizeClaudePayloadForAnthropic(payload) {
|
|
3079
|
-
return this.stripPrivateChatGptMetadata(payload);
|
|
3080
|
-
}
|
|
3081
|
-
stripPrivateChatGptMetadata(value) {
|
|
3082
|
-
if (Array.isArray(value)) return value.map((item) => this.stripPrivateChatGptMetadata(item));
|
|
3083
|
-
if (!value || typeof value !== "object") return value;
|
|
3084
|
-
return Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("_chatgpt_")).map(([key, nestedValue]) => [key, this.stripPrivateChatGptMetadata(nestedValue)]));
|
|
3085
|
-
}
|
|
3086
|
-
/**
|
|
3087
|
-
* Extracts a message role string when present.
|
|
3088
|
-
*/
|
|
3089
|
-
extractRole(message) {
|
|
3090
|
-
if (!message || typeof message !== "object") return null;
|
|
3091
|
-
const candidate = message.role;
|
|
3092
|
-
return typeof candidate === "string" ? candidate : null;
|
|
3093
|
-
}
|
|
3094
|
-
/**
|
|
3095
|
-
* Extracts response output items from upstream SSE payloads for storage.
|
|
3096
|
-
*/
|
|
3097
|
-
normalizeResponsePayloads(scope, requestId, slot, model, upstreamText) {
|
|
3098
|
-
const entries = [];
|
|
3099
|
-
for (const event of upstreamText.split("\n\n")) {
|
|
3100
|
-
const dataLine = event.split("\n").find((line) => line.startsWith("data:"))?.slice(5).trim();
|
|
3101
|
-
if (!dataLine || dataLine === "[DONE]") continue;
|
|
3102
|
-
let payload;
|
|
3103
|
-
try {
|
|
3104
|
-
payload = JSON.parse(dataLine);
|
|
3105
|
-
} catch {
|
|
3106
|
-
continue;
|
|
3107
|
-
}
|
|
3108
|
-
const response = this.extractCompletedResponse(payload);
|
|
3109
|
-
if (!response?.output || !Array.isArray(response.output)) continue;
|
|
3110
|
-
for (const output of response.output) entries.push({
|
|
3111
|
-
scope,
|
|
3112
|
-
requestId,
|
|
3113
|
-
direction: RESPONSE_DIRECTION,
|
|
3114
|
-
role: ASSISTANT_ROLE,
|
|
3115
|
-
messageType: typeof output.type === "string" ? output.type : RESPONSE_ITEM_MESSAGE_TYPE,
|
|
3116
|
-
model: typeof response.model === "string" ? response.model : model,
|
|
3117
|
-
slot,
|
|
3118
|
-
payloadJson: JSON.stringify(output)
|
|
3119
|
-
});
|
|
3120
|
-
}
|
|
3121
|
-
return entries;
|
|
3122
|
-
}
|
|
3123
|
-
/**
|
|
3124
|
-
* Extracts the completed response envelope from an upstream SSE event payload.
|
|
3125
|
-
*/
|
|
3126
|
-
extractCompletedResponse(payload) {
|
|
3127
|
-
if (!payload || typeof payload !== "object") return null;
|
|
3128
|
-
const candidate = payload;
|
|
3129
|
-
if (candidate.type === "response.completed" && candidate.response && typeof candidate.response === "object") return candidate.response;
|
|
3130
|
-
if (candidate.response && typeof candidate.response === "object") return candidate.response;
|
|
3131
|
-
return {
|
|
3132
|
-
model: typeof candidate.model === "string" ? candidate.model : void 0,
|
|
3133
|
-
output: Array.isArray(candidate.output) ? candidate.output : void 0
|
|
3134
|
-
};
|
|
3135
|
-
}
|
|
3136
|
-
/**
|
|
3137
|
-
* Redacts large request bodies before they are stored in error history.
|
|
3138
|
-
*/
|
|
3139
|
-
summarizePayload(payload) {
|
|
3140
|
-
const compact = payload.replace(/\s+/g, " ").trim();
|
|
3141
|
-
return compact.length <= REQUEST_PAYLOAD_PREVIEW_LIMIT ? compact : `${compact.slice(0, REQUEST_PAYLOAD_PREVIEW_LIMIT)}...`;
|
|
3142
|
-
}
|
|
3143
|
-
};
|
|
3144
|
-
|
|
3145
|
-
//#endregion
|
|
3146
|
-
//#region src/server/adminPage.ts
|
|
3147
|
-
const ADMIN_PAGE_HTML = `<!DOCTYPE html>
|
|
3148
|
-
<html lang="en">
|
|
3149
|
-
<head>
|
|
3150
|
-
<meta charset="utf-8" />
|
|
3151
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3152
|
-
<title>Model Proxy Admin</title>
|
|
3153
|
-
<style>
|
|
3154
|
-
:root {
|
|
3155
|
-
color-scheme: light;
|
|
3156
|
-
--bg: #f3efe6;
|
|
3157
|
-
--panel: rgba(255, 252, 246, 0.92);
|
|
3158
|
-
--panel-border: #d7cdb7;
|
|
3159
|
-
--text: #1f1b16;
|
|
3160
|
-
--muted: #6b6255;
|
|
3161
|
-
--accent: #b24a2f;
|
|
3162
|
-
--accent-2: #1b6b73;
|
|
3163
|
-
--danger: #8f2d21;
|
|
3164
|
-
}
|
|
3165
|
-
* { box-sizing: border-box; }
|
|
3166
|
-
body {
|
|
3167
|
-
margin: 0;
|
|
3168
|
-
font-family: "Iowan Old Style", "Palatino Linotype", serif;
|
|
3169
|
-
color: var(--text);
|
|
3170
|
-
background:
|
|
3171
|
-
radial-gradient(circle at top left, rgba(178, 74, 47, 0.14), transparent 28%),
|
|
3172
|
-
radial-gradient(circle at top right, rgba(27, 107, 115, 0.12), transparent 24%),
|
|
3173
|
-
linear-gradient(180deg, #f8f4eb 0%, var(--bg) 100%);
|
|
3174
|
-
}
|
|
3175
|
-
main {
|
|
3176
|
-
max-width: 1280px;
|
|
3177
|
-
margin: 0 auto;
|
|
3178
|
-
padding: 32px 20px 48px;
|
|
3179
|
-
}
|
|
3180
|
-
h1, h2 {
|
|
3181
|
-
margin: 0 0 12px;
|
|
3182
|
-
font-weight: 700;
|
|
3183
|
-
}
|
|
3184
|
-
p { color: var(--muted); margin-top: 0; }
|
|
3185
|
-
.hero {
|
|
3186
|
-
display: grid;
|
|
3187
|
-
gap: 16px;
|
|
3188
|
-
margin-bottom: 24px;
|
|
3189
|
-
}
|
|
3190
|
-
.panel {
|
|
3191
|
-
background: var(--panel);
|
|
3192
|
-
border: 1px solid var(--panel-border);
|
|
3193
|
-
border-radius: 18px;
|
|
3194
|
-
padding: 18px;
|
|
3195
|
-
box-shadow: 0 14px 40px rgba(80, 58, 24, 0.08);
|
|
3196
|
-
}
|
|
3197
|
-
.row {
|
|
3198
|
-
display: grid;
|
|
3199
|
-
gap: 12px;
|
|
3200
|
-
}
|
|
3201
|
-
.row.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
3202
|
-
.row.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
3203
|
-
.toolbar {
|
|
3204
|
-
display: flex;
|
|
3205
|
-
flex-wrap: wrap;
|
|
3206
|
-
gap: 10px;
|
|
3207
|
-
align-items: end;
|
|
3208
|
-
}
|
|
3209
|
-
label {
|
|
3210
|
-
display: grid;
|
|
3211
|
-
gap: 6px;
|
|
3212
|
-
font-size: 13px;
|
|
3213
|
-
color: var(--muted);
|
|
3214
|
-
}
|
|
3215
|
-
input, select, button, textarea {
|
|
3216
|
-
font: inherit;
|
|
3217
|
-
border-radius: 10px;
|
|
3218
|
-
border: 1px solid #c9bea7;
|
|
3219
|
-
padding: 10px 12px;
|
|
3220
|
-
background: #fffdf8;
|
|
3221
|
-
color: var(--text);
|
|
3222
|
-
}
|
|
3223
|
-
button {
|
|
3224
|
-
background: var(--text);
|
|
3225
|
-
color: #fff7ea;
|
|
3226
|
-
border-color: var(--text);
|
|
3227
|
-
cursor: pointer;
|
|
3228
|
-
}
|
|
3229
|
-
button.secondary {
|
|
3230
|
-
background: #fff7ea;
|
|
3231
|
-
color: var(--text);
|
|
3232
|
-
}
|
|
3233
|
-
button.danger {
|
|
3234
|
-
background: var(--danger);
|
|
3235
|
-
border-color: var(--danger);
|
|
3236
|
-
}
|
|
3237
|
-
table {
|
|
3238
|
-
width: 100%;
|
|
3239
|
-
border-collapse: collapse;
|
|
3240
|
-
}
|
|
3241
|
-
th, td {
|
|
3242
|
-
padding: 10px 8px;
|
|
3243
|
-
text-align: left;
|
|
3244
|
-
border-bottom: 1px solid rgba(107, 98, 85, 0.16);
|
|
3245
|
-
vertical-align: top;
|
|
3246
|
-
}
|
|
3247
|
-
th {
|
|
3248
|
-
font-size: 12px;
|
|
3249
|
-
text-transform: uppercase;
|
|
3250
|
-
letter-spacing: 0.08em;
|
|
3251
|
-
color: var(--muted);
|
|
3252
|
-
}
|
|
3253
|
-
code, pre {
|
|
3254
|
-
font-family: "SFMono-Regular", "Menlo", monospace;
|
|
3255
|
-
font-size: 12px;
|
|
3256
|
-
}
|
|
3257
|
-
pre {
|
|
3258
|
-
margin: 0;
|
|
3259
|
-
white-space: pre-wrap;
|
|
3260
|
-
word-break: break-word;
|
|
3261
|
-
max-height: 180px;
|
|
3262
|
-
overflow: auto;
|
|
3263
|
-
}
|
|
3264
|
-
.status {
|
|
3265
|
-
min-height: 20px;
|
|
3266
|
-
color: var(--accent-2);
|
|
3267
|
-
}
|
|
3268
|
-
.muted { color: var(--muted); }
|
|
3269
|
-
.slot-grid {
|
|
3270
|
-
display: grid;
|
|
3271
|
-
gap: 12px;
|
|
3272
|
-
}
|
|
3273
|
-
.slot-card {
|
|
3274
|
-
padding: 14px;
|
|
3275
|
-
border-radius: 14px;
|
|
3276
|
-
border: 1px solid rgba(107, 98, 85, 0.16);
|
|
3277
|
-
background: rgba(255, 255, 255, 0.7);
|
|
3278
|
-
}
|
|
3279
|
-
.slot-card h3 {
|
|
3280
|
-
margin: 0 0 10px;
|
|
3281
|
-
font-size: 18px;
|
|
3282
|
-
}
|
|
3283
|
-
.pill {
|
|
3284
|
-
display: inline-flex;
|
|
3285
|
-
padding: 4px 8px;
|
|
3286
|
-
border-radius: 999px;
|
|
3287
|
-
background: rgba(27, 107, 115, 0.1);
|
|
3288
|
-
color: var(--accent-2);
|
|
3289
|
-
font-size: 12px;
|
|
3290
|
-
}
|
|
3291
|
-
.paths {
|
|
3292
|
-
display: grid;
|
|
3293
|
-
gap: 8px;
|
|
3294
|
-
}
|
|
3295
|
-
@media (max-width: 900px) {
|
|
3296
|
-
.row.cols-2, .row.cols-3 { grid-template-columns: 1fr; }
|
|
3297
|
-
}
|
|
3298
|
-
</style>
|
|
3299
|
-
</head>
|
|
3300
|
-
<body>
|
|
3301
|
-
<main>
|
|
3302
|
-
<section class="hero">
|
|
3303
|
-
<div>
|
|
3304
|
-
<h1>Model Proxy Admin</h1>
|
|
3305
|
-
<p>Scoped slot routing, provider-aware model selection, and conversation history.</p>
|
|
3306
|
-
</div>
|
|
3307
|
-
<div class="panel toolbar">
|
|
3308
|
-
<label>
|
|
3309
|
-
Scope
|
|
3310
|
-
<select id="scope-input"></select>
|
|
3311
|
-
</label>
|
|
3312
|
-
<button id="load-button" class="secondary">Load scope</button>
|
|
3313
|
-
<span id="status" class="status"></span>
|
|
3314
|
-
</div>
|
|
3315
|
-
</section>
|
|
3316
|
-
|
|
3317
|
-
<section class="panel">
|
|
3318
|
-
<h2>Runtime Config</h2>
|
|
3319
|
-
<div class="paths muted">
|
|
3320
|
-
<div>Providers: <code id="provider-config-path"></code></div>
|
|
3321
|
-
<div>Model catalog: <code id="model-list-path"></code></div>
|
|
3322
|
-
<div>Scope config: <code id="scope-config-path"></code></div>
|
|
3323
|
-
</div>
|
|
3324
|
-
<div id="slot-grid" class="slot-grid" style="margin-top:16px;"></div>
|
|
3325
|
-
<div class="toolbar" style="margin-top:16px;">
|
|
3326
|
-
<button id="save-config">Save config</button>
|
|
3327
|
-
</div>
|
|
3328
|
-
</section>
|
|
3329
|
-
|
|
3330
|
-
<section class="panel" style="margin-top:20px;">
|
|
3331
|
-
<h2>Model Catalog</h2>
|
|
3332
|
-
<div id="catalog" class="row"></div>
|
|
3333
|
-
</section>
|
|
3334
|
-
|
|
3335
|
-
<section class="panel" style="margin-top:20px;">
|
|
3336
|
-
<div class="toolbar" style="justify-content:space-between;">
|
|
3337
|
-
<div>
|
|
3338
|
-
<h2 style="margin-bottom:4px;">Conversation History</h2>
|
|
3339
|
-
<p id="history-stats"></p>
|
|
3340
|
-
</div>
|
|
3341
|
-
<div class="toolbar">
|
|
3342
|
-
<label>
|
|
3343
|
-
Limit
|
|
3344
|
-
<select id="history-limit">
|
|
3345
|
-
<option>25</option>
|
|
3346
|
-
<option selected>50</option>
|
|
3347
|
-
<option>100</option>
|
|
3348
|
-
</select>
|
|
3349
|
-
</label>
|
|
3350
|
-
<button id="refresh-history" class="secondary">Refresh history</button>
|
|
3351
|
-
<button id="clear-history" class="danger">Clear history</button>
|
|
3352
|
-
</div>
|
|
3353
|
-
</div>
|
|
3354
|
-
<div style="overflow:auto;">
|
|
3355
|
-
<table>
|
|
3356
|
-
<thead>
|
|
3357
|
-
<tr>
|
|
3358
|
-
<th>Created</th>
|
|
3359
|
-
<th>Direction</th>
|
|
3360
|
-
<th>Role</th>
|
|
3361
|
-
<th>Slot</th>
|
|
3362
|
-
<th>Model</th>
|
|
3363
|
-
<th>Payload</th>
|
|
3364
|
-
</tr>
|
|
3365
|
-
</thead>
|
|
3366
|
-
<tbody id="history-body"></tbody>
|
|
3367
|
-
</table>
|
|
3368
|
-
</div>
|
|
3369
|
-
<div class="toolbar" style="margin-top:16px;">
|
|
3370
|
-
<button id="more-history" class="secondary">Load more</button>
|
|
3371
|
-
</div>
|
|
3372
|
-
</section>
|
|
3373
|
-
</main>
|
|
3374
|
-
|
|
3375
|
-
<script>
|
|
3376
|
-
const reasoningOptions = ['minimal', 'low', 'medium', 'high'];
|
|
3377
|
-
const slots = ['default', 'sonnet', 'opus', 'haiku', 'subagent'];
|
|
3378
|
-
let currentConfig = null;
|
|
3379
|
-
let nextCursor = null;
|
|
3380
|
-
|
|
3381
|
-
const scopeInput = document.getElementById('scope-input');
|
|
3382
|
-
const statusEl = document.getElementById('status');
|
|
3383
|
-
const slotGridEl = document.getElementById('slot-grid');
|
|
3384
|
-
const catalogEl = document.getElementById('catalog');
|
|
3385
|
-
const providerConfigPathEl = document.getElementById('provider-config-path');
|
|
3386
|
-
const modelListPathEl = document.getElementById('model-list-path');
|
|
3387
|
-
const scopeConfigPathEl = document.getElementById('scope-config-path');
|
|
3388
|
-
const historyBodyEl = document.getElementById('history-body');
|
|
3389
|
-
const historyStatsEl = document.getElementById('history-stats');
|
|
3390
|
-
const historyLimitEl = document.getElementById('history-limit');
|
|
3391
|
-
|
|
3392
|
-
function setStatus(message, isError = false) {
|
|
3393
|
-
statusEl.textContent = message;
|
|
3394
|
-
statusEl.style.color = isError ? 'var(--danger)' : 'var(--accent-2)';
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
function renderScopeOptions(scopes) {
|
|
3398
|
-
const currentScope = scopeInput.value || 'default';
|
|
3399
|
-
const scopeList = Array.from(new Set(['default'].concat(scopes || []))).sort();
|
|
3400
|
-
scopeInput.innerHTML = scopeList
|
|
3401
|
-
.map((scope) => '<option value="' + scope + '">' + scope + '</option>')
|
|
3402
|
-
.join('');
|
|
3403
|
-
scopeInput.value = scopeList.includes(currentScope) ? currentScope : 'default';
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
function providerOptions(providers, selected) {
|
|
3407
|
-
return Object.entries(providers)
|
|
3408
|
-
.sort((left, right) => left[0].localeCompare(right[0]))
|
|
3409
|
-
.map(([id, config]) => {
|
|
3410
|
-
const isSelected = id === selected ? ' selected' : '';
|
|
3411
|
-
return '<option value="' + id + '"' + isSelected + '>' + id + ' (' + config.type + ')</option>';
|
|
3412
|
-
})
|
|
3413
|
-
.join('');
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
function modelOptions(models, provider, selected) {
|
|
3417
|
-
return models
|
|
3418
|
-
.filter((model) => model.enabled && model.provider === provider)
|
|
3419
|
-
.map((model) => {
|
|
3420
|
-
const isSelected = model.model === selected ? ' selected' : '';
|
|
3421
|
-
return '<option value="' + model.model + '"' + isSelected + '>' + model.label + ' (' + model.model + ')</option>';
|
|
3422
|
-
})
|
|
3423
|
-
.join('');
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
function reasoningSelect(value, slot) {
|
|
3427
|
-
return (
|
|
3428
|
-
'<select data-slot-reasoning="' +
|
|
3429
|
-
slot +
|
|
3430
|
-
'">' +
|
|
3431
|
-
reasoningOptions
|
|
3432
|
-
.map((option) => {
|
|
3433
|
-
const selected = option === value ? ' selected' : '';
|
|
3434
|
-
return '<option value="' + option + '"' + selected + '>' + option + '</option>';
|
|
3435
|
-
})
|
|
3436
|
-
.join('') +
|
|
3437
|
-
'</select>'
|
|
3438
|
-
);
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
function refreshSlotModelOptions(slot) {
|
|
3442
|
-
if (!currentConfig) {
|
|
3443
|
-
return;
|
|
3444
|
-
}
|
|
3445
|
-
|
|
3446
|
-
const providerSelect = document.querySelector('[data-slot-provider="' + slot + '"]');
|
|
3447
|
-
const modelSelect = document.querySelector('[data-slot-model="' + slot + '"]');
|
|
3448
|
-
if (!providerSelect || !modelSelect) {
|
|
3449
|
-
return;
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
const resolved = currentConfig.slots[slot];
|
|
3453
|
-
const provider = providerSelect.value;
|
|
3454
|
-
modelSelect.innerHTML = modelOptions(currentConfig.models, provider, resolved.model);
|
|
3455
|
-
|
|
3456
|
-
if (!modelSelect.value) {
|
|
3457
|
-
const fallbackModel = currentConfig.models.find((model) => model.enabled && model.provider === provider);
|
|
3458
|
-
if (fallbackModel) {
|
|
3459
|
-
modelSelect.value = fallbackModel.model;
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
const endpointEl = document.querySelector('[data-slot-endpoint="' + slot + '"]');
|
|
3464
|
-
if (endpointEl) {
|
|
3465
|
-
endpointEl.textContent = currentConfig.providers[provider]?.endpoint || 'No provider config';
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
function renderConfig(config) {
|
|
3470
|
-
currentConfig = config;
|
|
3471
|
-
providerConfigPathEl.textContent = config.providerConfigPath;
|
|
3472
|
-
modelListPathEl.textContent = config.modelListPath;
|
|
3473
|
-
scopeConfigPathEl.textContent = config.scopeConfigPath;
|
|
3474
|
-
|
|
3475
|
-
slotGridEl.innerHTML = slots.map((slot) => {
|
|
3476
|
-
const resolved = config.slots[slot];
|
|
3477
|
-
return (
|
|
3478
|
-
'<div class="slot-card">' +
|
|
3479
|
-
'<div class="toolbar" style="justify-content:space-between;">' +
|
|
3480
|
-
'<h3>' + slot + '</h3>' +
|
|
3481
|
-
'<span class="pill">' +
|
|
3482
|
-
(resolved.providerType ?? 'unconfigured') +
|
|
3483
|
-
(resolved.fallbacks.length > 0 ? ' · +' + resolved.fallbacks.length + ' fallback' + (resolved.fallbacks.length > 1 ? 's' : '') : '') +
|
|
3484
|
-
'</span>' +
|
|
3485
|
-
'</div>' +
|
|
3486
|
-
'<div class="row cols-3">' +
|
|
3487
|
-
'<label>Provider<select data-slot-provider="' + slot + '">' +
|
|
3488
|
-
providerOptions(config.providers, resolved.provider || '') +
|
|
3489
|
-
'</select></label>' +
|
|
3490
|
-
'<label>Model<select data-slot-model="' + slot + '"></select></label>' +
|
|
3491
|
-
'<label>Thinking' + reasoningSelect(resolved.reasoningEffort, slot) + '</label>' +
|
|
3492
|
-
'</div>' +
|
|
3493
|
-
'<div class="muted" style="margin-top:10px;">Endpoint: <code data-slot-endpoint="' + slot + '"></code></div>' +
|
|
3494
|
-
'</div>'
|
|
3495
|
-
);
|
|
3496
|
-
}).join('');
|
|
3497
|
-
|
|
3498
|
-
for (const slot of slots) {
|
|
3499
|
-
refreshSlotModelOptions(slot);
|
|
3500
|
-
const providerSelect = document.querySelector('[data-slot-provider="' + slot + '"]');
|
|
3501
|
-
if (providerSelect) {
|
|
3502
|
-
providerSelect.addEventListener('change', () => refreshSlotModelOptions(slot));
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
catalogEl.innerHTML = config.models
|
|
3507
|
-
.map(
|
|
3508
|
-
(model) =>
|
|
3509
|
-
'<div class="panel" style="padding:12px;">' +
|
|
3510
|
-
'<strong>' + model.label + '</strong>' +
|
|
3511
|
-
'<div class="muted"><code>' + model.provider + '</code> · <code>' + model.model + '</code> · default thinking: ' + model.reasoningEffort + '</div>' +
|
|
3512
|
-
'</div>',
|
|
3513
|
-
)
|
|
3514
|
-
.join('');
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
function payloadPreview(payloadJson) {
|
|
3518
|
-
try {
|
|
3519
|
-
return JSON.stringify(JSON.parse(payloadJson), null, 2);
|
|
3520
|
-
} catch {
|
|
3521
|
-
return payloadJson;
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
async function loadConfig() {
|
|
3526
|
-
const scope = encodeURIComponent(scopeInput.value || 'default');
|
|
3527
|
-
const response = await fetch('/admin/config?scope=' + scope);
|
|
3528
|
-
if (!response.ok) {
|
|
3529
|
-
throw new Error(await response.text());
|
|
3530
|
-
}
|
|
3531
|
-
renderConfig(await response.json());
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
async function loadScopes() {
|
|
3535
|
-
const response = await fetch('/admin/scopes');
|
|
3536
|
-
if (!response.ok) {
|
|
3537
|
-
throw new Error(await response.text());
|
|
3538
|
-
}
|
|
3539
|
-
const payload = await response.json();
|
|
3540
|
-
renderScopeOptions(payload.scopes || []);
|
|
3541
|
-
}
|
|
3542
|
-
|
|
3543
|
-
async function saveConfig() {
|
|
3544
|
-
const models = {};
|
|
3545
|
-
for (const slot of slots) {
|
|
3546
|
-
const currentSlot = currentConfig.scopeModels[slot];
|
|
3547
|
-
models[slot] = {
|
|
3548
|
-
main: {
|
|
3549
|
-
provider: document.querySelector('[data-slot-provider="' + slot + '"]')?.value,
|
|
3550
|
-
model: document.querySelector('[data-slot-model="' + slot + '"]')?.value,
|
|
3551
|
-
reasoningEffort: document.querySelector('[data-slot-reasoning="' + slot + '"]')?.value,
|
|
3552
|
-
thinkingDisabled:
|
|
3553
|
-
currentSlot?.main?.thinkingDisabled ?? currentConfig.slots[slot]?.thinkingDisabled ?? false,
|
|
3554
|
-
},
|
|
3555
|
-
fallbacks: currentSlot?.fallbacks ?? [],
|
|
3556
|
-
};
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
const response = await fetch('/admin/config?scope=' + encodeURIComponent(scopeInput.value || 'default'), {
|
|
3560
|
-
method: 'PUT',
|
|
3561
|
-
headers: { 'content-type': 'application/json' },
|
|
3562
|
-
body: JSON.stringify({ models }),
|
|
3563
|
-
});
|
|
3564
|
-
if (!response.ok) {
|
|
3565
|
-
throw new Error(await response.text());
|
|
3566
|
-
}
|
|
3567
|
-
renderConfig(await response.json());
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
async function loadHistory(append = false) {
|
|
3571
|
-
const scope = encodeURIComponent(scopeInput.value || 'default');
|
|
3572
|
-
const cursorQuery = append && nextCursor ? '&cursor=' + encodeURIComponent(nextCursor) : '';
|
|
3573
|
-
const limit = encodeURIComponent(historyLimitEl.value);
|
|
3574
|
-
const response = await fetch('/admin/history?scope=' + scope + '&limit=' + limit + cursorQuery);
|
|
3575
|
-
if (!response.ok) {
|
|
3576
|
-
throw new Error(await response.text());
|
|
3577
|
-
}
|
|
3578
|
-
const payload = await response.json();
|
|
3579
|
-
nextCursor = payload.nextCursor;
|
|
3580
|
-
const rows = payload.items
|
|
3581
|
-
.map(
|
|
3582
|
-
(item) =>
|
|
3583
|
-
'<tr>' +
|
|
3584
|
-
'<td>' + item.createdAt + '</td>' +
|
|
3585
|
-
'<td>' + item.direction + '</td>' +
|
|
3586
|
-
'<td>' + (item.role ?? '') + '</td>' +
|
|
3587
|
-
'<td>' + item.slot + '</td>' +
|
|
3588
|
-
'<td>' + (item.model ?? '') + '</td>' +
|
|
3589
|
-
'<td><pre>' + payloadPreview(item.payloadJson) + '</pre></td>' +
|
|
3590
|
-
'</tr>',
|
|
3591
|
-
)
|
|
3592
|
-
.join('');
|
|
3593
|
-
historyBodyEl.innerHTML = append ? historyBodyEl.innerHTML + rows : rows;
|
|
3594
|
-
document.getElementById('more-history').disabled = !nextCursor;
|
|
3595
|
-
|
|
3596
|
-
const statsResponse = await fetch('/admin/history/stats?scope=' + scope);
|
|
3597
|
-
const stats = await statsResponse.json();
|
|
3598
|
-
historyStatsEl.textContent = stats.totalMessages + ' stored messages. Retention limit ' + stats.retentionLimit + '.';
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
async function clearHistory() {
|
|
3602
|
-
const scope = encodeURIComponent(scopeInput.value || 'default');
|
|
3603
|
-
const response = await fetch('/admin/history?scope=' + scope, { method: 'DELETE' });
|
|
3604
|
-
if (!response.ok) {
|
|
3605
|
-
throw new Error(await response.text());
|
|
3606
|
-
}
|
|
3607
|
-
nextCursor = null;
|
|
3608
|
-
historyBodyEl.innerHTML = '';
|
|
3609
|
-
await loadHistory();
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
document.getElementById('load-button').addEventListener('click', async () => {
|
|
3613
|
-
try {
|
|
3614
|
-
setStatus('Loading...');
|
|
3615
|
-
nextCursor = null;
|
|
3616
|
-
await loadConfig();
|
|
3617
|
-
await loadHistory();
|
|
3618
|
-
setStatus('Scope loaded.');
|
|
3619
|
-
} catch (error) {
|
|
3620
|
-
setStatus(error.message, true);
|
|
3621
|
-
}
|
|
3622
|
-
});
|
|
3623
|
-
|
|
3624
|
-
document.getElementById('save-config').addEventListener('click', async () => {
|
|
3625
|
-
try {
|
|
3626
|
-
setStatus('Saving config...');
|
|
3627
|
-
await saveConfig();
|
|
3628
|
-
setStatus('Config saved.');
|
|
3629
|
-
} catch (error) {
|
|
3630
|
-
setStatus(error.message, true);
|
|
3631
|
-
}
|
|
3632
|
-
});
|
|
3633
|
-
|
|
3634
|
-
document.getElementById('refresh-history').addEventListener('click', async () => {
|
|
3635
|
-
try {
|
|
3636
|
-
setStatus('Refreshing history...');
|
|
3637
|
-
nextCursor = null;
|
|
3638
|
-
await loadHistory();
|
|
3639
|
-
setStatus('History refreshed.');
|
|
3640
|
-
} catch (error) {
|
|
3641
|
-
setStatus(error.message, true);
|
|
3642
|
-
}
|
|
3643
|
-
});
|
|
3644
|
-
|
|
3645
|
-
document.getElementById('clear-history').addEventListener('click', async () => {
|
|
3646
|
-
try {
|
|
3647
|
-
setStatus('Clearing history...');
|
|
3648
|
-
await clearHistory();
|
|
3649
|
-
setStatus('History cleared.');
|
|
3650
|
-
} catch (error) {
|
|
3651
|
-
setStatus(error.message, true);
|
|
3652
|
-
}
|
|
3653
|
-
});
|
|
3654
|
-
|
|
3655
|
-
document.getElementById('more-history').addEventListener('click', async () => {
|
|
3656
|
-
try {
|
|
3657
|
-
setStatus('Loading more history...');
|
|
3658
|
-
await loadHistory(true);
|
|
3659
|
-
setStatus('More history loaded.');
|
|
3660
|
-
} catch (error) {
|
|
3661
|
-
setStatus(error.message, true);
|
|
3662
|
-
}
|
|
3663
|
-
});
|
|
3664
|
-
|
|
3665
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
3666
|
-
try {
|
|
3667
|
-
await loadScopes();
|
|
3668
|
-
await loadConfig();
|
|
3669
|
-
await loadHistory();
|
|
3670
|
-
setStatus('Ready.');
|
|
3671
|
-
} catch (error) {
|
|
3672
|
-
setStatus(error.message, true);
|
|
3673
|
-
}
|
|
3674
|
-
});
|
|
3675
|
-
<\/script>
|
|
3676
|
-
</body>
|
|
3677
|
-
</html>
|
|
3678
|
-
`;
|
|
3679
|
-
|
|
3680
|
-
//#endregion
|
|
3681
|
-
//#region src/server/http.ts
|
|
3682
|
-
const reasoningEffortSchema = z.enum([
|
|
3683
|
-
"minimal",
|
|
3684
|
-
"low",
|
|
3685
|
-
"medium",
|
|
3686
|
-
"high"
|
|
3687
|
-
]);
|
|
3688
|
-
const modelSlotSchema$1 = z.enum([
|
|
3689
|
-
"default",
|
|
3690
|
-
"sonnet",
|
|
3691
|
-
"opus",
|
|
3692
|
-
"haiku",
|
|
3693
|
-
"subagent"
|
|
3694
|
-
]);
|
|
3695
|
-
const selectionSchema = z.object({
|
|
3696
|
-
provider: z.string().min(1),
|
|
3697
|
-
model: z.string().min(1),
|
|
3698
|
-
reasoningEffort: reasoningEffortSchema,
|
|
3699
|
-
thinkingDisabled: z.boolean().optional()
|
|
3700
|
-
});
|
|
3701
|
-
const slotConfigSchema = z.object({
|
|
3702
|
-
main: selectionSchema,
|
|
3703
|
-
fallbacks: z.array(selectionSchema).default([])
|
|
3704
|
-
});
|
|
3705
|
-
const adminConfigUpdateSchema = z.object({ models: z.object({
|
|
3706
|
-
default: slotConfigSchema.nullable().optional(),
|
|
3707
|
-
sonnet: slotConfigSchema.nullable().optional(),
|
|
3708
|
-
opus: slotConfigSchema.nullable().optional(),
|
|
3709
|
-
haiku: slotConfigSchema.nullable().optional(),
|
|
3710
|
-
subagent: slotConfigSchema.nullable().optional()
|
|
3711
|
-
}).optional() });
|
|
3712
|
-
const profileSchema = z.object({
|
|
3713
|
-
id: z.string().min(1),
|
|
3714
|
-
label: z.string().min(1),
|
|
3715
|
-
provider: z.string().min(1),
|
|
3716
|
-
model: z.string().min(1),
|
|
3717
|
-
endpoint: z.url().nullable(),
|
|
3718
|
-
reasoningEffort: reasoningEffortSchema.default("medium"),
|
|
3719
|
-
enabled: z.boolean().default(true),
|
|
3720
|
-
providerType: z.enum([
|
|
3721
|
-
"chatgpt-codex",
|
|
3722
|
-
"anthropic-compatible",
|
|
3723
|
-
"gemini-direct"
|
|
3724
|
-
]).nullable().optional()
|
|
3725
|
-
});
|
|
3726
|
-
function createHttpServer(gatewayService = new GatewayService()) {
|
|
3727
|
-
const app = new Hono();
|
|
3728
|
-
const getScope = (scope) => scope || "default";
|
|
3729
|
-
app.get("/health", (c) => c.json({
|
|
3730
|
-
status: "healthy",
|
|
3731
|
-
service: DEFAULT_SERVICE_NAME
|
|
3732
|
-
}));
|
|
3733
|
-
app.get("/status", async (c) => {
|
|
3734
|
-
const status = await gatewayService.getStatus();
|
|
3735
|
-
return c.json(status);
|
|
3736
|
-
});
|
|
3737
|
-
app.get("/v1/models", async (c) => c.json(await gatewayService.getModels()));
|
|
3738
|
-
app.get("/scopes/:scope/status", async (c) => c.json(await gatewayService.getStatus(getScope(c.req.param("scope")))));
|
|
3739
|
-
app.get("/scopes/:scope/v1/models", async (c) => c.json(await gatewayService.getModels(getScope(c.req.param("scope")))));
|
|
3740
|
-
app.post("/v1/messages", async (c) => {
|
|
3741
|
-
const body = await c.req.text();
|
|
3742
|
-
const response = await gatewayService.forwardClaudeRequest(body, "default", c.req.raw.headers);
|
|
3743
|
-
return new Response(response.body, {
|
|
3744
|
-
status: response.status,
|
|
3745
|
-
headers: response.headers
|
|
3746
|
-
});
|
|
3747
|
-
});
|
|
3748
|
-
app.post("/scopes/:scope/v1/messages", async (c) => {
|
|
3749
|
-
const body = await c.req.text();
|
|
3750
|
-
const response = await gatewayService.forwardClaudeRequest(body, getScope(c.req.param("scope")), c.req.raw.headers);
|
|
3751
|
-
return new Response(response.body, {
|
|
3752
|
-
status: response.status,
|
|
3753
|
-
headers: response.headers
|
|
3754
|
-
});
|
|
3755
|
-
});
|
|
3756
|
-
app.get("/admin", () => new Response(ADMIN_PAGE_HTML, { headers: { "content-type": "text/html; charset=utf-8" } }));
|
|
3757
|
-
app.get("/admin/scopes", async (c) => c.json({ scopes: await gatewayService.listScopes() }));
|
|
3758
|
-
app.get("/admin/config", async (c) => {
|
|
3759
|
-
const scope = getScope(c.req.query("scope"));
|
|
3760
|
-
return c.json(await gatewayService.getAdminConfig(scope));
|
|
3761
|
-
});
|
|
3762
|
-
app.get("/admin/profiles", async (c) => {
|
|
3763
|
-
const scope = getScope(c.req.query("scope"));
|
|
3764
|
-
return c.json({ profiles: await gatewayService.listProfiles(scope) });
|
|
3765
|
-
});
|
|
3766
|
-
app.get("/admin/current-model", async (c) => {
|
|
3767
|
-
const scope = getScope(c.req.query("scope"));
|
|
3768
|
-
const slot = c.req.query("slot") || "default";
|
|
3769
|
-
return c.json({ profile: await gatewayService.getCurrentModel(scope, slot) });
|
|
3770
|
-
});
|
|
3771
|
-
app.get("/admin/history", async (c) => {
|
|
3772
|
-
const scope = getScope(c.req.query("scope"));
|
|
3773
|
-
const limit = z.coerce.number().int().positive().max(200).default(50).parse(c.req.query("limit") ?? 50);
|
|
3774
|
-
const cursor = c.req.query("cursor") || void 0;
|
|
3775
|
-
return c.json(await gatewayService.listHistory(scope, limit, cursor));
|
|
3776
|
-
});
|
|
3777
|
-
app.get("/admin/history/stats", async (c) => {
|
|
3778
|
-
const scope = getScope(c.req.query("scope"));
|
|
3779
|
-
return c.json(await gatewayService.getHistoryStats(scope));
|
|
3780
|
-
});
|
|
3781
|
-
app.put("/admin/profiles/:id", async (c) => {
|
|
3782
|
-
const payload = profileSchema.parse(await c.req.json());
|
|
3783
|
-
const scope = getScope(c.req.query("scope"));
|
|
3784
|
-
const result = await gatewayService.upsertProfile({
|
|
3785
|
-
...payload,
|
|
3786
|
-
id: c.req.param("id"),
|
|
3787
|
-
providerType: payload.providerType ?? null
|
|
3788
|
-
}, scope);
|
|
3789
|
-
return c.json(result);
|
|
3790
|
-
});
|
|
3791
|
-
app.put("/admin/active-profile", async (c) => {
|
|
3792
|
-
const scope = getScope(c.req.query("scope"));
|
|
3793
|
-
const payload = z.object({
|
|
3794
|
-
profileId: z.string().min(1),
|
|
3795
|
-
slot: modelSlotSchema$1.default("default")
|
|
3796
|
-
}).parse(await c.req.json());
|
|
3797
|
-
const result = await gatewayService.setActiveProfile(payload.profileId, scope, payload.slot);
|
|
3798
|
-
return c.json(result);
|
|
3799
|
-
});
|
|
3800
|
-
app.put("/admin/current-model", async (c) => {
|
|
3801
|
-
const scope = getScope(c.req.query("scope"));
|
|
3802
|
-
const payload = z.object({
|
|
3803
|
-
model: z.string().min(1),
|
|
3804
|
-
slot: modelSlotSchema$1.default("default")
|
|
3805
|
-
}).parse(await c.req.json());
|
|
3806
|
-
const result = await gatewayService.switchModel(payload.model, scope, payload.slot);
|
|
3807
|
-
return c.json(result);
|
|
3808
|
-
});
|
|
3809
|
-
app.put("/admin/config", async (c) => {
|
|
3810
|
-
const scope = getScope(c.req.query("scope"));
|
|
3811
|
-
const payload = adminConfigUpdateSchema.parse(await c.req.json());
|
|
3812
|
-
return c.json(await gatewayService.updateAdminConfig(payload, scope));
|
|
3813
|
-
});
|
|
3814
|
-
app.delete("/admin/history", async (c) => {
|
|
3815
|
-
const scope = getScope(c.req.query("scope"));
|
|
3816
|
-
return c.json(await gatewayService.clearHistory(scope));
|
|
3817
|
-
});
|
|
3818
|
-
return app;
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
//#endregion
|
|
3822
|
-
//#region src/server/index.ts
|
|
3823
|
-
const DEFAULT_SCOPE = "default";
|
|
3824
|
-
const DEFAULT_SLOT = "default";
|
|
3825
|
-
const MODEL_PROXY_SCOPE_ENV = "MODEL_PROXY_MCP_SCOPE";
|
|
3826
|
-
const MODEL_PROXY_SLOT_ENV = "MODEL_PROXY_MCP_SLOT";
|
|
3827
|
-
const modelSlotSchema = z.enum([
|
|
3828
|
-
"default",
|
|
3829
|
-
"sonnet",
|
|
3830
|
-
"opus",
|
|
3831
|
-
"haiku",
|
|
3832
|
-
"subagent"
|
|
3833
|
-
]);
|
|
3834
|
-
function createServer(gatewayService = new GatewayService(), environment = process.env) {
|
|
3835
|
-
const resolveScope = (args) => {
|
|
3836
|
-
const scope = args?.scope;
|
|
3837
|
-
return typeof scope === "string" && scope.trim() ? scope : environment[MODEL_PROXY_SCOPE_ENV] || DEFAULT_SCOPE;
|
|
3838
|
-
};
|
|
3839
|
-
const resolveSlot = (args) => {
|
|
3840
|
-
const slot = typeof args?.slot === "string" ? args.slot : environment[MODEL_PROXY_SLOT_ENV] || DEFAULT_SLOT;
|
|
3841
|
-
return modelSlotSchema.parse(slot);
|
|
3842
|
-
};
|
|
3843
|
-
const server = new Server({
|
|
3844
|
-
name: "model-proxy-mcp",
|
|
3845
|
-
version: "0.1.0"
|
|
3846
|
-
}, { capabilities: { tools: {} } });
|
|
3847
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
|
|
3848
|
-
{
|
|
3849
|
-
name: "get_proxy_status",
|
|
3850
|
-
description: "Get the HTTP proxy status, auth status, and active profile.",
|
|
3851
|
-
inputSchema: {
|
|
3852
|
-
type: "object",
|
|
3853
|
-
properties: { scope: { type: "string" } },
|
|
3854
|
-
additionalProperties: false
|
|
3855
|
-
}
|
|
3856
|
-
},
|
|
3857
|
-
{
|
|
3858
|
-
name: "list_profiles",
|
|
3859
|
-
description: "List all configured model proxy profiles.",
|
|
3860
|
-
inputSchema: {
|
|
3861
|
-
type: "object",
|
|
3862
|
-
properties: { scope: { type: "string" } },
|
|
3863
|
-
additionalProperties: false
|
|
3864
|
-
}
|
|
3865
|
-
},
|
|
3866
|
-
{
|
|
3867
|
-
name: "get_active_profile",
|
|
3868
|
-
description: "Get the active model proxy profile.",
|
|
3869
|
-
inputSchema: {
|
|
3870
|
-
type: "object",
|
|
3871
|
-
properties: {
|
|
3872
|
-
scope: { type: "string" },
|
|
3873
|
-
slot: {
|
|
3874
|
-
type: "string",
|
|
3875
|
-
enum: modelSlotSchema.options
|
|
3876
|
-
}
|
|
3877
|
-
},
|
|
3878
|
-
additionalProperties: false
|
|
3879
|
-
}
|
|
3880
|
-
},
|
|
3881
|
-
{
|
|
3882
|
-
name: "get_current_model",
|
|
3883
|
-
description: "Get the currently active model and profile settings.",
|
|
3884
|
-
inputSchema: {
|
|
3885
|
-
type: "object",
|
|
3886
|
-
properties: {
|
|
3887
|
-
scope: { type: "string" },
|
|
3888
|
-
slot: {
|
|
3889
|
-
type: "string",
|
|
3890
|
-
enum: modelSlotSchema.options
|
|
3891
|
-
}
|
|
3892
|
-
},
|
|
3893
|
-
additionalProperties: false
|
|
3894
|
-
}
|
|
3895
|
-
},
|
|
3896
|
-
{
|
|
3897
|
-
name: "set_model_target",
|
|
3898
|
-
description: "Set the scoped slot main target directly by provider, model, and reasoning effort.",
|
|
3899
|
-
inputSchema: {
|
|
3900
|
-
type: "object",
|
|
3901
|
-
properties: {
|
|
3902
|
-
provider: { type: "string" },
|
|
3903
|
-
model: { type: "string" },
|
|
3904
|
-
reasoningEffort: {
|
|
3905
|
-
type: "string",
|
|
3906
|
-
enum: [
|
|
3907
|
-
"minimal",
|
|
3908
|
-
"low",
|
|
3909
|
-
"medium",
|
|
3910
|
-
"high"
|
|
3911
|
-
]
|
|
3912
|
-
},
|
|
3913
|
-
scope: { type: "string" },
|
|
3914
|
-
slot: {
|
|
3915
|
-
type: "string",
|
|
3916
|
-
enum: modelSlotSchema.options
|
|
3917
|
-
},
|
|
3918
|
-
thinkingDisabled: { type: "boolean" }
|
|
3919
|
-
},
|
|
3920
|
-
required: [
|
|
3921
|
-
"provider",
|
|
3922
|
-
"model",
|
|
3923
|
-
"reasoningEffort"
|
|
3924
|
-
],
|
|
3925
|
-
additionalProperties: false
|
|
3926
|
-
}
|
|
3927
|
-
},
|
|
3928
|
-
{
|
|
3929
|
-
name: "upsert_profile",
|
|
3930
|
-
description: "Create or update a Codex model profile.",
|
|
3931
|
-
inputSchema: {
|
|
3932
|
-
type: "object",
|
|
3933
|
-
properties: {
|
|
3934
|
-
id: { type: "string" },
|
|
3935
|
-
label: { type: "string" },
|
|
3936
|
-
provider: { type: "string" },
|
|
3937
|
-
model: { type: "string" },
|
|
3938
|
-
endpoint: { type: "string" },
|
|
3939
|
-
reasoningEffort: {
|
|
3940
|
-
type: "string",
|
|
3941
|
-
enum: [
|
|
3942
|
-
"minimal",
|
|
3943
|
-
"low",
|
|
3944
|
-
"medium",
|
|
3945
|
-
"high"
|
|
3946
|
-
]
|
|
3947
|
-
},
|
|
3948
|
-
enabled: { type: "boolean" },
|
|
3949
|
-
scope: { type: "string" }
|
|
3950
|
-
},
|
|
3951
|
-
required: [
|
|
3952
|
-
"id",
|
|
3953
|
-
"label",
|
|
3954
|
-
"model",
|
|
3955
|
-
"endpoint",
|
|
3956
|
-
"reasoningEffort",
|
|
3957
|
-
"enabled"
|
|
3958
|
-
],
|
|
3959
|
-
additionalProperties: false
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
] }));
|
|
3963
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3964
|
-
const { name, arguments: args } = request.params;
|
|
3965
|
-
try {
|
|
3966
|
-
switch (name) {
|
|
3967
|
-
case "get_proxy_status": return textResult(JSON.stringify(await gatewayService.getStatus(resolveScope(args)), null, 2));
|
|
3968
|
-
case "list_profiles": return textResult(JSON.stringify(await gatewayService.listProfiles(resolveScope(args)), null, 2));
|
|
3969
|
-
case "get_active_profile": return textResult(JSON.stringify(await gatewayService.getActiveProfile(resolveScope(args), resolveSlot(args)), null, 2));
|
|
3970
|
-
case "get_current_model": return textResult(JSON.stringify(await gatewayService.getCurrentModel(resolveScope(args), resolveSlot(args)), null, 2));
|
|
3971
|
-
case "set_model_target": {
|
|
3972
|
-
const scope = resolveScope(args);
|
|
3973
|
-
const slot = resolveSlot(args);
|
|
3974
|
-
const existingConfig = await gatewayService.getAdminConfig(scope);
|
|
3975
|
-
return textResult(JSON.stringify(await gatewayService.updateAdminConfig({ models: { [slot]: {
|
|
3976
|
-
main: {
|
|
3977
|
-
provider: String(args?.provider),
|
|
3978
|
-
model: String(args?.model),
|
|
3979
|
-
reasoningEffort: z.enum([
|
|
3980
|
-
"minimal",
|
|
3981
|
-
"low",
|
|
3982
|
-
"medium",
|
|
3983
|
-
"high"
|
|
3984
|
-
]).parse(String(args?.reasoningEffort)),
|
|
3985
|
-
thinkingDisabled: args?.thinkingDisabled === void 0 ? false : Boolean(args.thinkingDisabled)
|
|
3986
|
-
},
|
|
3987
|
-
fallbacks: existingConfig.scopeModels[slot]?.fallbacks ?? []
|
|
3988
|
-
} } }, scope), null, 2));
|
|
3989
|
-
}
|
|
3990
|
-
case "upsert_profile": return textResult(JSON.stringify(await gatewayService.upsertProfile({
|
|
3991
|
-
id: String(args?.id),
|
|
3992
|
-
label: String(args?.label),
|
|
3993
|
-
provider: String(args?.provider || "chatgpt-codex"),
|
|
3994
|
-
providerType: null,
|
|
3995
|
-
model: String(args?.model),
|
|
3996
|
-
endpoint: String(args?.endpoint),
|
|
3997
|
-
reasoningEffort: String(args?.reasoningEffort),
|
|
3998
|
-
enabled: Boolean(args?.enabled)
|
|
3999
|
-
}, resolveScope(args)), null, 2));
|
|
4000
|
-
default: throw new Error(`Unknown tool: ${name}`);
|
|
4001
|
-
}
|
|
4002
|
-
} catch (error) {
|
|
4003
|
-
return {
|
|
4004
|
-
content: [{
|
|
4005
|
-
type: "text",
|
|
4006
|
-
text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
4007
|
-
}],
|
|
4008
|
-
isError: true
|
|
4009
|
-
};
|
|
4010
|
-
}
|
|
4011
|
-
});
|
|
4012
|
-
return server;
|
|
4013
|
-
}
|
|
4014
|
-
function textResult(text) {
|
|
4015
|
-
return { content: [{
|
|
4016
|
-
type: "text",
|
|
4017
|
-
text
|
|
4018
|
-
}] };
|
|
4019
|
-
}
|
|
4020
|
-
|
|
4021
|
-
//#endregion
|
|
4022
|
-
//#region src/transports/stdio.ts
|
|
4023
|
-
var StdioTransportHandler = class {
|
|
4024
|
-
transport = null;
|
|
4025
|
-
constructor(server) {
|
|
4026
|
-
this.server = server;
|
|
4027
|
-
}
|
|
4028
|
-
async start() {
|
|
4029
|
-
this.transport = new StdioServerTransport();
|
|
4030
|
-
await this.server.connect(this.transport);
|
|
4031
|
-
}
|
|
4032
|
-
async stop() {
|
|
4033
|
-
if (this.transport) {
|
|
4034
|
-
await this.transport.close();
|
|
4035
|
-
this.transport = null;
|
|
4036
|
-
}
|
|
4037
|
-
}
|
|
4038
|
-
};
|
|
4039
|
-
|
|
4040
|
-
//#endregion
|
|
4041
|
-
export { ConversationHistoryService as a, DEFAULT_HTTP_PORT as c, GatewayService as i, DEFAULT_SERVICE_NAME as l, createServer as n, ProfileStore as o, createHttpServer as r, consoleLogger as s, StdioTransportHandler as t };
|