codexmate 0.0.55 → 0.0.56
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/README.md +2 -0
- package/README.vi.md +2 -0
- package/README.zh.md +2 -0
- package/cli/local-bridge.js +221 -22
- package/cli.js +117 -5
- package/package.json +1 -1
- package/web-ui/app.js +2 -0
- package/web-ui/modules/app.computed.session.mjs +210 -0
- package/web-ui/modules/app.methods.claude-config.mjs +128 -12
- package/web-ui/modules/app.methods.codex-config.mjs +294 -65
- package/web-ui/modules/app.methods.navigation.mjs +4 -1
- package/web-ui/modules/app.methods.providers.mjs +15 -1
- package/web-ui/modules/app.methods.runtime.mjs +7 -2
- package/web-ui/modules/app.methods.session-actions.mjs +24 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +34 -1
- package/web-ui/modules/i18n/locales/en.mjs +41 -2
- package/web-ui/modules/i18n/locales/ja.mjs +41 -2
- package/web-ui/modules/i18n/locales/vi.mjs +41 -8
- package/web-ui/modules/i18n/locales/zh-tw.mjs +41 -2
- package/web-ui/modules/i18n/locales/zh.mjs +41 -2
- package/web-ui/modules/provider-default-names.mjs +25 -0
- package/web-ui/partials/index/modal-health-check.html +69 -5
- package/web-ui/partials/index/panel-config-codex.html +2 -2
- package/web-ui/partials/index/panel-sessions.html +97 -17
- package/web-ui/res/web-ui-render.precompiled.js +345 -93
- package/web-ui/session-helpers.mjs +4 -1
- package/web-ui/styles/responsive.css +98 -0
- package/web-ui/styles/sessions-preview.css +212 -2
- package/web-ui/styles/sessions-toolbar-trash.css +61 -18
- package/web-ui/styles/skills-list.css +122 -0
- package/web-ui/styles/titles-cards.css +52 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
58
58
|
- **OpenAI-Compatible Bridge**: Use Codex with any OpenAI-compatible UI by normalizing the Responses API; the built-in Codex conversion also fills and normalizes Codex fingerprint headers such as `User-Agent`, `Version`, `OpenAI-Beta`, and `Originator` so upstream providers see an official Codex CLI-shaped request.
|
|
59
59
|
- **Claude Provider Bridge**: Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama through the built-in local Claude-compatible proxy.
|
|
60
60
|
- **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings.
|
|
61
|
+
- **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries.
|
|
61
62
|
- **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
|
|
62
63
|
- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths.
|
|
63
64
|
- **Task Orchestrator**: Plan and execute complex tasks with dependency tracking.
|
|
@@ -77,6 +78,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
77
78
|
| **OpenAI Bridge** | ✅ | Convert Codex Responses API to standard OpenAI format and attach/normalize Codex fingerprints in the built-in conversion |
|
|
78
79
|
| **Claude Provider Bridge** | ✅ | Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama via the built-in Claude-compatible proxy |
|
|
79
80
|
| **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
|
|
81
|
+
| **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely |
|
|
80
82
|
| **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
|
|
81
83
|
| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect and path switching |
|
|
82
84
|
| **MCP Integration** | ✅ | Expose local tools and resources via MCP stdio |
|
package/README.vi.md
CHANGED
|
@@ -48,6 +48,7 @@ Bạn có bao giờ cảm thấy rối khi phải quản lý nhiều AI agent c
|
|
|
48
48
|
Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Agent Bridge cục bộ**:
|
|
49
49
|
- **Trình duyệt phiên thống nhất**: Tìm kiếm và xuất phiên làm việc từ tất cả tool trong một nơi duy nhất.
|
|
50
50
|
- **Bridge tương thích OpenAI**: Dùng Codex với bất kỳ UI nào hỗ trợ OpenAI bằng cách chuẩn hóa Responses API.
|
|
51
|
+
- **Dọn dẹp provider lỗi**: Kiểm tra route provider cục bộ của Codex/Claude, hiển thị cấu hình lỗi trong một modal và xóa hàng loạt các provider hỏng đã chọn mà không chạm vào mục khỏe mạnh hoặc được bảo vệ.
|
|
51
52
|
- **Chợ skill cục bộ**: Chia sẻ và nhập skill giữa các app agent khác nhau.
|
|
52
53
|
- **Điều phối tác vụ**: Lập kế hoạch và thực thi tác vụ phức tạp với theo dõi phụ thuộc.
|
|
53
54
|
|
|
@@ -64,6 +65,7 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag
|
|
|
64
65
|
| **Chợ skill cục bộ** | ✅ | Import/export skill giữa các app agent |
|
|
65
66
|
| **Hàng đợi tác vụ** | ✅ | Thực thi tác vụ theo DAG và xem log |
|
|
66
67
|
| **Bridge OpenAI** | ✅ | Chuyển đổi Codex Responses API sang định dạng OpenAI chuẩn |
|
|
68
|
+
| **Kiểm tra sức khỏe Provider** | ✅ | Kiểm tra route provider Codex/Claude, đánh dấu cấu hình lỗi và xóa hàng loạt các provider hỏng đã chọn một cách an toàn |
|
|
67
69
|
| **Mẫu Prompt** | ✅ | Plugin prompt tái sử dụng được với hỗ trợ biến |
|
|
68
70
|
| **Tích hợp MCP** | ✅ | Expose tool và resource cục bộ qua MCP stdio |
|
|
69
71
|
| **Tự động cập nhật** | ✅ | Cập nhật nhanh qua `codexmate update` |
|
package/README.zh.md
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
- **OpenAI 兼容桥接**:通过归一化 Responses API,让 Codex 能够与任何支持 OpenAI 格式的 UI 配合使用;内建 Codex 转换会补齐并规范化 `User-Agent`、`Version`、`OpenAI-Beta`、`Originator` 等 Codex 指纹,对上游伪装为官方 Codex CLI 请求。
|
|
58
58
|
- **Claude Provider 桥接**:通过内建本地 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama。
|
|
59
59
|
- **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。
|
|
60
|
+
- **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。
|
|
60
61
|
- **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
|
|
61
62
|
- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测。
|
|
62
63
|
- **任务编排器**:支持带有依赖跟踪的复杂任务规划与执行。
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
| **OpenAI 桥接** | ✅ | 将 Codex Responses API 转换为标准 OpenAI 格式,并在内建转换中附加/规范化 Codex 指纹 |
|
|
77
78
|
| **Claude Provider 桥接** | ✅ | 通过内建 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama |
|
|
78
79
|
| **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 |
|
|
80
|
+
| **Provider 健康检查** | ✅ | 探测本地 Codex/Claude provider 路由,标记失败配置,并安全批量移除已选坏配置 |
|
|
79
81
|
| **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
|
|
80
82
|
| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测与路径切换 |
|
|
81
83
|
| **MCP 集成** | ✅ | 通过 MCP stdio 暴露本地工具与资源 |
|
package/cli/local-bridge.js
CHANGED
|
@@ -20,6 +20,14 @@ const {
|
|
|
20
20
|
isLoopbackAddress
|
|
21
21
|
} = require('./openai-bridge');
|
|
22
22
|
const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
|
|
23
|
+
const {
|
|
24
|
+
buildBuiltinClaudeChatCompletionsRequest,
|
|
25
|
+
buildBuiltinClaudeOllamaChatRequest,
|
|
26
|
+
buildAnthropicMessageFromChatCompletion,
|
|
27
|
+
buildAnthropicMessageFromOllamaChat,
|
|
28
|
+
buildAnthropicModelsPayload,
|
|
29
|
+
buildAnthropicStreamEvents
|
|
30
|
+
} = require('./claude-proxy');
|
|
23
31
|
|
|
24
32
|
const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
25
33
|
const BUILTIN_LOCAL_PROVIDER_NAME = 'local';
|
|
@@ -28,6 +36,52 @@ const CLAUDE_LOCAL_EXCLUDED_KEY = 'claudeLocalExcluded';
|
|
|
28
36
|
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
29
37
|
const CIRCUIT_BREAKER_COOLDOWN_MS = 5 * 60 * 1000;
|
|
30
38
|
|
|
39
|
+
function normalizeClaudeLocalTargetApi(value) {
|
|
40
|
+
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
41
|
+
if (raw === 'chat_completions' || raw === 'chat-completions' || raw === 'chat/completions') return 'chat_completions';
|
|
42
|
+
if (raw === 'ollama') return 'ollama';
|
|
43
|
+
return 'responses';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeClaudeLocalSuffix(pathname) {
|
|
47
|
+
const suffix = String(pathname || '').replace(/^\/bridge\/claude-local\/?/, '').replace(/^\/+/, '');
|
|
48
|
+
if (!suffix || suffix === 'v1') return '';
|
|
49
|
+
return suffix.replace(/^v1\/?/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function joinClaudeLocalUpstreamUrl(baseUrl, pathSuffix) {
|
|
53
|
+
const suffix = typeof pathSuffix === 'string' ? pathSuffix.replace(/^\/+/, '') : '';
|
|
54
|
+
if (suffix === 'api/tags' || suffix === 'api/chat') {
|
|
55
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
56
|
+
return normalized ? `${normalized}/${suffix}` : '';
|
|
57
|
+
}
|
|
58
|
+
return joinApiUrl(baseUrl, suffix);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildClaudeLocalAuthHeaders(entry, requestToken, targetApi, expectedToken = '') {
|
|
62
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
63
|
+
const trimmedRequestToken = typeof requestToken === 'string' ? requestToken.trim() : '';
|
|
64
|
+
const bridgeToken = typeof expectedToken === 'string' ? expectedToken.trim() : '';
|
|
65
|
+
const upstreamRequestToken = trimmedRequestToken && (!bridgeToken || trimmedRequestToken !== bridgeToken)
|
|
66
|
+
? trimmedRequestToken
|
|
67
|
+
: '';
|
|
68
|
+
const token = entry && typeof entry.apiKey === 'string' && entry.apiKey.trim()
|
|
69
|
+
? entry.apiKey.trim()
|
|
70
|
+
: upstreamRequestToken;
|
|
71
|
+
if (!token) {
|
|
72
|
+
if (targetApi === 'responses') headers['anthropic-version'] = '2023-06-01';
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
const bareToken = token.replace(/^Bearer\s+/i, '');
|
|
76
|
+
if (targetApi === 'responses') {
|
|
77
|
+
headers['x-api-key'] = bareToken;
|
|
78
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
79
|
+
} else {
|
|
80
|
+
headers.Authorization = /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`;
|
|
81
|
+
}
|
|
82
|
+
return headers;
|
|
83
|
+
}
|
|
84
|
+
|
|
31
85
|
function buildUpstreamPool(readConfigFn, openaiBridgeFile, excludedProviders) {
|
|
32
86
|
let config;
|
|
33
87
|
try { config = readConfigFn(); } catch (e) { return { error: '读取配置失败' }; }
|
|
@@ -74,7 +128,13 @@ function buildClaudeUpstreamPool(claudeProvidersFile, excludedProviders) {
|
|
|
74
128
|
if (excludedSet.has(name.toLowerCase())) continue;
|
|
75
129
|
const baseUrl = typeof p.baseUrl === 'string' ? p.baseUrl.trim() : '';
|
|
76
130
|
if (!baseUrl || !isValidHttpUrl(normalizeBaseUrl(baseUrl))) continue;
|
|
77
|
-
pool.push({
|
|
131
|
+
pool.push({
|
|
132
|
+
name,
|
|
133
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
134
|
+
apiKey: typeof p.apiKey === 'string' ? p.apiKey : '',
|
|
135
|
+
model: typeof p.model === 'string' ? p.model.trim() : '',
|
|
136
|
+
targetApi: normalizeClaudeLocalTargetApi(p.targetApi)
|
|
137
|
+
});
|
|
78
138
|
}
|
|
79
139
|
if (pool.length === 0) return { error: '请先添加可用的 Claude 上游提供商' };
|
|
80
140
|
return { pool };
|
|
@@ -263,7 +323,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
263
323
|
const pool = poolResult.pool;
|
|
264
324
|
const { entry } = pickUpstream(pool);
|
|
265
325
|
|
|
266
|
-
const suffix = (parsedUrl.pathname || '')
|
|
326
|
+
const suffix = normalizeClaudeLocalSuffix(parsedUrl.pathname || '');
|
|
267
327
|
if (!suffix) {
|
|
268
328
|
if ((req.method || 'GET').toUpperCase() !== 'GET') {
|
|
269
329
|
res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -271,11 +331,151 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
271
331
|
return;
|
|
272
332
|
}
|
|
273
333
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
274
|
-
res.end(JSON.stringify({
|
|
334
|
+
res.end(JSON.stringify({
|
|
335
|
+
object: 'codexmate.claude_local_bridge',
|
|
336
|
+
provider: entry.name,
|
|
337
|
+
model: entry.model || '',
|
|
338
|
+
targetApi: entry.targetApi,
|
|
339
|
+
status: 'ok',
|
|
340
|
+
pool: pool.map(p => p.name),
|
|
341
|
+
targetApis: Object.fromEntries(pool.map(p => [p.name, p.targetApi]))
|
|
342
|
+
}));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const targetApi = normalizeClaudeLocalTargetApi(entry.targetApi);
|
|
347
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
348
|
+
const isMessagesRequest = suffix === 'messages';
|
|
349
|
+
const isModelsRequest = suffix === 'models';
|
|
350
|
+
|
|
351
|
+
if ((targetApi === 'chat_completions' || targetApi === 'ollama') && !isMessagesRequest && !isModelsRequest) {
|
|
352
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
353
|
+
res.end(JSON.stringify({ error: 'Claude local bridge transform providers only support /v1/messages and /v1/models' }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (targetApi === 'chat_completions' || targetApi === 'ollama') {
|
|
358
|
+
if (isModelsRequest) {
|
|
359
|
+
if (method !== 'GET') {
|
|
360
|
+
res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: 'GET' });
|
|
361
|
+
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const modelsUrl = joinClaudeLocalUpstreamUrl(entry.baseUrl.replace(/\/+$/, ''), targetApi === 'ollama' ? 'api/tags' : 'models');
|
|
365
|
+
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(modelsUrl, {
|
|
366
|
+
method: 'GET',
|
|
367
|
+
body: null,
|
|
368
|
+
headers: buildClaudeLocalAuthHeaders(entry, token, targetApi, expectedToken),
|
|
369
|
+
maxBytes: maxUpstreamBytes,
|
|
370
|
+
httpAgent,
|
|
371
|
+
httpsAgent
|
|
372
|
+
}));
|
|
373
|
+
if (!upstreamResult.ok) {
|
|
374
|
+
recordFailure(entry.name);
|
|
375
|
+
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
376
|
+
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResult.error}` }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const parsedModels = parseJsonOrError(upstreamResult.bodyText || '{}');
|
|
380
|
+
if (upstreamResult.status >= 400) {
|
|
381
|
+
recordFailure(entry.name);
|
|
382
|
+
res.writeHead(upstreamResult.status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
383
|
+
res.end(upstreamResult.bodyText || JSON.stringify({ error: 'Upstream error' }));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (parsedModels.error) {
|
|
387
|
+
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
388
|
+
res.end(JSON.stringify({ error: `Upstream parse failed: ${parsedModels.error}` }));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
recordSuccess(entry.name);
|
|
392
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
393
|
+
res.end(JSON.stringify(buildAnthropicModelsPayload(parsedModels.value)));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (method !== 'POST') {
|
|
398
|
+
res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: 'POST' });
|
|
399
|
+
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const bodyResult = await readRequestBody(req, maxBodySize);
|
|
403
|
+
if (bodyResult.error) {
|
|
404
|
+
res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
405
|
+
res.end(JSON.stringify({ error: bodyResult.error }));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const parsed = parseJsonOrError(bodyResult.body);
|
|
409
|
+
if (parsed.error) {
|
|
410
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
411
|
+
res.end(JSON.stringify({ error: parsed.error }));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const requestPayload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
|
|
415
|
+
if (entry.model) requestPayload.model = entry.model;
|
|
416
|
+
let upstreamBody;
|
|
417
|
+
try {
|
|
418
|
+
upstreamBody = targetApi === 'ollama'
|
|
419
|
+
? buildBuiltinClaudeOllamaChatRequest(requestPayload)
|
|
420
|
+
: buildBuiltinClaudeChatCompletionsRequest(requestPayload);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
423
|
+
res.end(JSON.stringify({ error: e && e.message ? e.message : 'Invalid request payload' }));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const upstreamUrl = joinClaudeLocalUpstreamUrl(entry.baseUrl.replace(/\/+$/, ''), targetApi === 'ollama' ? 'api/chat' : 'chat/completions');
|
|
427
|
+
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
|
|
428
|
+
method: 'POST',
|
|
429
|
+
body: upstreamBody,
|
|
430
|
+
headers: buildClaudeLocalAuthHeaders(entry, token, targetApi, expectedToken),
|
|
431
|
+
maxBytes: maxUpstreamBytes,
|
|
432
|
+
httpAgent,
|
|
433
|
+
httpsAgent
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
if (!upstreamResult.ok) {
|
|
437
|
+
recordFailure(entry.name);
|
|
438
|
+
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
439
|
+
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResult.error}` }));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (upstreamResult.status >= 400) {
|
|
443
|
+
recordFailure(entry.name);
|
|
444
|
+
res.writeHead(upstreamResult.status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
445
|
+
res.end(upstreamResult.bodyText || JSON.stringify({ error: 'Upstream error' }));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const parsedUpstream = parseJsonOrError(upstreamResult.bodyText || '{}');
|
|
449
|
+
if (parsedUpstream.error) {
|
|
450
|
+
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
451
|
+
res.end(JSON.stringify({ error: `Upstream parse failed: ${parsedUpstream.error}` }));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
recordSuccess(entry.name);
|
|
455
|
+
const anthropicMessage = targetApi === 'ollama'
|
|
456
|
+
? buildAnthropicMessageFromOllamaChat(parsedUpstream.value || {}, requestPayload)
|
|
457
|
+
: buildAnthropicMessageFromChatCompletion(parsedUpstream.value || {}, requestPayload);
|
|
458
|
+
if (requestPayload.stream === true) {
|
|
459
|
+
const events = buildAnthropicStreamEvents(anthropicMessage);
|
|
460
|
+
res.writeHead(200, {
|
|
461
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
462
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
463
|
+
Connection: 'keep-alive',
|
|
464
|
+
'X-Accel-Buffering': 'no'
|
|
465
|
+
});
|
|
466
|
+
for (const event of events) {
|
|
467
|
+
if (event && event.event) res.write(`event: ${event.event}\n`);
|
|
468
|
+
res.write(`data: ${JSON.stringify(event && event.data ? event.data : {})}\n\n`);
|
|
469
|
+
}
|
|
470
|
+
res.end();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
474
|
+
res.end(JSON.stringify(anthropicMessage));
|
|
275
475
|
return;
|
|
276
476
|
}
|
|
277
477
|
|
|
278
|
-
//
|
|
478
|
+
// Native Anthropic-compatible provider: keep /v1/messages passthrough.
|
|
279
479
|
const bodyResult = await readRequestBody(req, maxBodySize);
|
|
280
480
|
if (bodyResult.error) {
|
|
281
481
|
res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -283,26 +483,26 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
283
483
|
return;
|
|
284
484
|
}
|
|
285
485
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
486
|
+
const hasRequestBody = typeof bodyResult.body === 'string' && bodyResult.body.trim().length > 0;
|
|
487
|
+
let parsedBody = null;
|
|
488
|
+
if (hasRequestBody) {
|
|
489
|
+
const parsed = parseJsonOrError(bodyResult.body);
|
|
490
|
+
if (parsed.error) {
|
|
491
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
492
|
+
res.end(JSON.stringify({ error: parsed.error }));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
parsedBody = parsed.value;
|
|
496
|
+
}
|
|
289
497
|
if (entry.model && parsedBody && typeof parsedBody === 'object') {
|
|
290
498
|
parsedBody.model = entry.model;
|
|
291
499
|
}
|
|
292
|
-
const wantsStream = !!(parsedBody && parsedBody.stream);
|
|
293
|
-
const bodyToForward = JSON.stringify(parsedBody);
|
|
500
|
+
const wantsStream = !!(parsedBody && typeof parsedBody === 'object' && parsedBody.stream);
|
|
501
|
+
const bodyToForward = hasRequestBody ? JSON.stringify(parsedBody) : null;
|
|
294
502
|
const upstreamUrl = joinApiUrl(entry.baseUrl.replace(/\/+$/, ''), suffix);
|
|
295
|
-
const headers =
|
|
296
|
-
if (entry.apiKey) {
|
|
297
|
-
headers['x-api-key'] = entry.apiKey.startsWith('Bearer ') ? entry.apiKey.slice(7) : entry.apiKey;
|
|
298
|
-
}
|
|
299
|
-
if (token && !entry.apiKey) {
|
|
300
|
-
headers['x-api-key'] = token.startsWith('Bearer ') ? token.slice(7) : token;
|
|
301
|
-
}
|
|
302
|
-
headers['anthropic-version'] = '2023-06-01';
|
|
503
|
+
const headers = buildClaudeLocalAuthHeaders(entry, token, targetApi, expectedToken);
|
|
303
504
|
|
|
304
505
|
if (wantsStream) {
|
|
305
|
-
// Streaming proxy: pipe upstream SSE directly to client
|
|
306
506
|
const upstreamResult = await streamClaudeUpstream(upstreamUrl, {
|
|
307
507
|
method: req.method || 'POST',
|
|
308
508
|
body: bodyToForward,
|
|
@@ -324,10 +524,9 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
324
524
|
return;
|
|
325
525
|
}
|
|
326
526
|
|
|
327
|
-
// Non-streaming proxy
|
|
328
527
|
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
|
|
329
528
|
method: req.method || 'POST',
|
|
330
|
-
body:
|
|
529
|
+
body: parsedBody || null,
|
|
331
530
|
headers,
|
|
332
531
|
maxBytes: maxUpstreamBytes,
|
|
333
532
|
httpAgent,
|
|
@@ -432,7 +631,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
432
631
|
const upstreamResponsesUrl = joinApiUrl(upstreamBase, 'responses');
|
|
433
632
|
const upstreamResponsesResult = await retryTransientRequest(() => proxyRequestJson(upstreamResponsesUrl, {
|
|
434
633
|
method: 'POST',
|
|
435
|
-
body:
|
|
634
|
+
body: responsesRequest,
|
|
436
635
|
headers: { ...(authHeader ? { Authorization: authHeader } : {}) },
|
|
437
636
|
maxBytes: maxUpstreamBytes,
|
|
438
637
|
httpAgent,
|
|
@@ -484,7 +683,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
484
683
|
const chatUrl = joinApiUrl(upstreamBase, 'chat/completions');
|
|
485
684
|
const chatResult = await retryTransientRequest(() => proxyRequestJson(chatUrl, {
|
|
486
685
|
method: 'POST',
|
|
487
|
-
body:
|
|
686
|
+
body: converted.chat,
|
|
488
687
|
headers: { ...(authHeader ? { Authorization: authHeader } : {}), 'Content-Type': 'application/json' },
|
|
489
688
|
maxBytes: maxUpstreamBytes,
|
|
490
689
|
httpAgent,
|
package/cli.js
CHANGED
|
@@ -231,6 +231,15 @@ const PROVIDER_CACHE_FILE_GROUPS = Object.freeze({
|
|
|
231
231
|
'opencode-provider-current-models.json'
|
|
232
232
|
]
|
|
233
233
|
});
|
|
234
|
+
const PROVIDER_CACHE_PROVIDER_FILES = Object.freeze([
|
|
235
|
+
'codex-providers.json',
|
|
236
|
+
'claude-providers.json',
|
|
237
|
+
'opencode-providers.json'
|
|
238
|
+
]);
|
|
239
|
+
const PROVIDER_CACHE_CURRENT_MODEL_FILES = Object.freeze([
|
|
240
|
+
'codex-provider-current-models.json',
|
|
241
|
+
'opencode-provider-current-models.json'
|
|
242
|
+
]);
|
|
234
243
|
const PROVIDER_CACHE_MAX_FILE_BYTES = 256 * 1024;
|
|
235
244
|
const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
|
|
236
245
|
const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
|
|
@@ -1083,7 +1092,8 @@ function getApiToolConfigWriteTarget(action) {
|
|
|
1083
1092
|
'restore-claude-dir',
|
|
1084
1093
|
'claude-local-bridge-toggle',
|
|
1085
1094
|
'claude-local-bridge-set-excluded',
|
|
1086
|
-
'claude-local-bridge-sync-providers'
|
|
1095
|
+
'claude-local-bridge-sync-providers',
|
|
1096
|
+
'delete-provider-cache-record'
|
|
1087
1097
|
]);
|
|
1088
1098
|
const opencodeWriteActions = new Set([
|
|
1089
1099
|
'apply-opencode-config',
|
|
@@ -2877,6 +2887,99 @@ function normalizeProviderCacheProviderMap(rawProviders) {
|
|
|
2877
2887
|
return providers;
|
|
2878
2888
|
}
|
|
2879
2889
|
|
|
2890
|
+
function removeProviderFromProviderCacheContainer(rawProviders, providerName) {
|
|
2891
|
+
const targetName = typeof providerName === 'string' ? providerName.trim() : '';
|
|
2892
|
+
if (!targetName) return { value: rawProviders, changed: false };
|
|
2893
|
+
|
|
2894
|
+
const matchesProviderName = (name) => String(name || '').trim() === targetName;
|
|
2895
|
+
if (Array.isArray(rawProviders)) {
|
|
2896
|
+
const filtered = rawProviders.filter((item) => {
|
|
2897
|
+
if (!isPlainObject(item)) return true;
|
|
2898
|
+
const itemName = pickProviderCacheString(item, ['name', 'id', 'provider']);
|
|
2899
|
+
return !matchesProviderName(itemName);
|
|
2900
|
+
});
|
|
2901
|
+
return { value: filtered, changed: filtered.length !== rawProviders.length };
|
|
2902
|
+
}
|
|
2903
|
+
if (!isPlainObject(rawProviders)) return { value: rawProviders, changed: false };
|
|
2904
|
+
|
|
2905
|
+
const next = { ...rawProviders };
|
|
2906
|
+
let changed = false;
|
|
2907
|
+
for (const [name, entry] of Object.entries(rawProviders)) {
|
|
2908
|
+
const entryName = isPlainObject(entry)
|
|
2909
|
+
? pickProviderCacheString(entry, ['name', 'id', 'provider'])
|
|
2910
|
+
: '';
|
|
2911
|
+
if (matchesProviderName(name) || matchesProviderName(entryName)) {
|
|
2912
|
+
delete next[name];
|
|
2913
|
+
changed = true;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return { value: next, changed };
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function resolveProviderCacheDeleteGroups(groups) {
|
|
2920
|
+
const requested = Array.isArray(groups) ? groups : (groups ? [groups] : []);
|
|
2921
|
+
const normalized = requested
|
|
2922
|
+
.map((item) => String(item || '').trim().toLowerCase())
|
|
2923
|
+
.filter((item) => Object.prototype.hasOwnProperty.call(PROVIDER_CACHE_FILE_GROUPS, item));
|
|
2924
|
+
return normalized.length ? Array.from(new Set(normalized)) : Object.keys(PROVIDER_CACHE_FILE_GROUPS);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function removeProviderFromProviderCacheRecords(providerName, options = {}) {
|
|
2928
|
+
const targetName = typeof providerName === 'string' ? providerName.trim() : '';
|
|
2929
|
+
const summary = { removed: false, providerFiles: [], currentModelFiles: [] };
|
|
2930
|
+
if (!targetName) return summary;
|
|
2931
|
+
|
|
2932
|
+
const groups = resolveProviderCacheDeleteGroups(options.groups || options.group);
|
|
2933
|
+
const providerFiles = groups
|
|
2934
|
+
.flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
|
|
2935
|
+
.filter((fileName) => PROVIDER_CACHE_PROVIDER_FILES.includes(fileName));
|
|
2936
|
+
const currentModelFiles = groups
|
|
2937
|
+
.flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
|
|
2938
|
+
.filter((fileName) => PROVIDER_CACHE_CURRENT_MODEL_FILES.includes(fileName));
|
|
2939
|
+
|
|
2940
|
+
for (const fileName of Array.from(new Set(providerFiles))) {
|
|
2941
|
+
const existing = readProviderCacheJsonObject(fileName);
|
|
2942
|
+
if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, 'providers')) continue;
|
|
2943
|
+
const removed = removeProviderFromProviderCacheContainer(existing.providers, targetName);
|
|
2944
|
+
if (!removed.changed) continue;
|
|
2945
|
+
writeProviderCacheJsonObject(fileName, {
|
|
2946
|
+
...existing,
|
|
2947
|
+
generatedAt: new Date().toISOString(),
|
|
2948
|
+
providers: removed.value
|
|
2949
|
+
});
|
|
2950
|
+
summary.removed = true;
|
|
2951
|
+
summary.providerFiles.push(fileName);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
for (const fileName of Array.from(new Set(currentModelFiles))) {
|
|
2955
|
+
const existing = readProviderCacheJsonObject(fileName);
|
|
2956
|
+
if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, targetName)) continue;
|
|
2957
|
+
const next = { ...existing };
|
|
2958
|
+
delete next[targetName];
|
|
2959
|
+
writeProviderCacheJsonObject(fileName, next);
|
|
2960
|
+
summary.removed = true;
|
|
2961
|
+
summary.currentModelFiles.push(fileName);
|
|
2962
|
+
}
|
|
2963
|
+
return summary;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
function deleteProviderCacheRecord(params = {}) {
|
|
2967
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
2968
|
+
if (!name) return { error: '名称不能为空' };
|
|
2969
|
+
const group = typeof params.group === 'string' ? params.group.trim().toLowerCase() : '';
|
|
2970
|
+
const groups = resolveProviderCacheDeleteGroups(group || params.groups);
|
|
2971
|
+
const summary = removeProviderFromProviderCacheRecords(name, { groups });
|
|
2972
|
+
return {
|
|
2973
|
+
success: true,
|
|
2974
|
+
name,
|
|
2975
|
+
groups,
|
|
2976
|
+
removed: summary.removed,
|
|
2977
|
+
providerFiles: summary.providerFiles,
|
|
2978
|
+
currentModelFiles: summary.currentModelFiles,
|
|
2979
|
+
records: readProviderCacheRecords()
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2880
2983
|
function readClaudeProviderCacheProvider(name) {
|
|
2881
2984
|
const targetName = typeof name === 'string' ? name.trim() : '';
|
|
2882
2985
|
if (!targetName) return null;
|
|
@@ -2953,10 +3056,11 @@ function buildProviderCacheSyncProviders() {
|
|
|
2953
3056
|
function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
|
|
2954
3057
|
const existing = readProviderCacheJsonObject(fileName);
|
|
2955
3058
|
const existingProviders = normalizeProviderCacheProviderMap(existing.providers);
|
|
2956
|
-
const providers = {
|
|
3059
|
+
const providers = {};
|
|
2957
3060
|
for (const provider of nextProviders) {
|
|
2958
3061
|
const previous = isPlainObject(providers[provider.name]) ? providers[provider.name] : {};
|
|
2959
|
-
|
|
3062
|
+
const cachedPrevious = isPlainObject(existingProviders[provider.name]) ? existingProviders[provider.name] : previous;
|
|
3063
|
+
providers[provider.name] = { ...cachedPrevious, ...buildEntry(provider) };
|
|
2960
3064
|
}
|
|
2961
3065
|
const next = {
|
|
2962
3066
|
...existing,
|
|
@@ -2970,9 +3074,13 @@ function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
|
|
|
2970
3074
|
|
|
2971
3075
|
function mergeProviderCacheCurrentModelsFile(fileName, nextProviders) {
|
|
2972
3076
|
const existing = readProviderCacheJsonObject(fileName);
|
|
2973
|
-
const next = {
|
|
3077
|
+
const next = {};
|
|
2974
3078
|
for (const provider of nextProviders) {
|
|
2975
|
-
if (provider.model)
|
|
3079
|
+
if (provider.model) {
|
|
3080
|
+
next[provider.name] = provider.model;
|
|
3081
|
+
} else if (typeof existing[provider.name] === 'string' && existing[provider.name].trim()) {
|
|
3082
|
+
next[provider.name] = existing[provider.name];
|
|
3083
|
+
}
|
|
2976
3084
|
}
|
|
2977
3085
|
const displayPath = writeProviderCacheJsonObject(fileName, next);
|
|
2978
3086
|
return { path: displayPath, modelCount: Object.keys(next).length };
|
|
@@ -3213,6 +3321,7 @@ function performProviderDeletion(name, options = {}) {
|
|
|
3213
3321
|
|
|
3214
3322
|
writeCurrentModels(currentModels);
|
|
3215
3323
|
writeConfig(updatedContent.trimEnd() + lineEnding);
|
|
3324
|
+
removeProviderFromProviderCacheRecords(name);
|
|
3216
3325
|
|
|
3217
3326
|
return result;
|
|
3218
3327
|
}
|
|
@@ -12387,6 +12496,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
12387
12496
|
case 'sync-provider-cache-records':
|
|
12388
12497
|
result = syncProviderCacheRecords();
|
|
12389
12498
|
break;
|
|
12499
|
+
case 'delete-provider-cache-record':
|
|
12500
|
+
result = deleteProviderCacheRecord(params || {});
|
|
12501
|
+
break;
|
|
12390
12502
|
case 'delete-provider':
|
|
12391
12503
|
result = deleteProviderFromConfig(params || {});
|
|
12392
12504
|
break;
|
package/package.json
CHANGED
package/web-ui/app.js
CHANGED
|
@@ -272,6 +272,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
272
272
|
healthCheckBatchTotal: 0,
|
|
273
273
|
healthCheckBatchDone: 0,
|
|
274
274
|
healthCheckBatchFailed: 0,
|
|
275
|
+
healthCheckFailedProviderSelections: {},
|
|
276
|
+
healthCheckFailedProviderDeleting: false,
|
|
275
277
|
installPackageManager: 'npm',
|
|
276
278
|
installCommandAction: 'install',
|
|
277
279
|
installRegistryPreset: 'npmmirror',
|