Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
- **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.
- **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.
- **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths.
- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts.
- **Task Orchestrator**: Plan and execute complex tasks with dependency tracking.

---
Expand All @@ -80,7 +80,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
| **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
| **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely |
| **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect and path switching |
| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect, path switching, and a shared preset pool. Applying a preset only updates the editor; save manually to write the file. |
| **MCP Integration** | ✅ | Expose local tools and resources via MCP stdio |
| **Auto Update** | ✅ | Quick update CLI via `codexmate update` |

Expand Down
1 change: 1 addition & 0 deletions README.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag
| **Bridge OpenAI** | ✅ | Chuyển đổi Codex Responses API sang định dạng OpenAI chuẩn |
| **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 |
| **Mẫu Prompt** | ✅ | Plugin prompt tái sử dụng được với hỗ trợ biến |
| **Trình sửa tệp prompt** | ✅ | Chỉnh sửa CLAUDE.md / AGENTS.md và dùng kho preset chung. Áp dụng preset chỉ cập nhật editor; cần lưu thủ công để ghi vào tệp. |
| **Tích hợp MCP** | ✅ | Expose tool và resource cục bộ qua MCP stdio |
| **Tự động cập nhật** | ✅ | Cập nhật nhanh qua `codexmate update` |

Expand Down
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
- **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。
- **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。
- **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测。
- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测,并提供通用预设池复用 prompt。
- **任务编排器**:支持带有依赖跟踪的复杂任务规划与执行。

---
Expand All @@ -79,7 +79,7 @@
| **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 |
| **Provider 健康检查** | ✅ | 探测本地 Codex/Claude provider 路由,标记失败配置,并安全批量移除已选坏配置 |
| **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测与路径切换 |
| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测、路径切换与通用预设池。应用预设只更新编辑器,需手动保存才会写入文件。 |
| **MCP 集成** | ✅ | 通过 MCP stdio 暴露本地工具与资源 |
| **自动更新** | ✅ | 通过 `codexmate update` 快速更新 CLI |

Expand Down
122 changes: 7 additions & 115 deletions cli/openai-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,54 +425,14 @@ function createOpenaiBridgeHttpHandler(options = {}) {
const acceptHeader = req && req.headers ? (req.headers.accept || req.headers.Accept || '') : '';
const wantsSse = /text\/event-stream/i.test(String(acceptHeader || ''));

if (streamRequested && wantsSse) {
const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
const skipResponsesProbe = isResponsesKnownUnsupported(upstream.baseUrl);
const streamedResponses = skipResponsesProbe
? { ok: false, status: 404, bodyText: '' }
: await retryTransientRequest(() => streamResponsesSse(upstreamResponsesUrl, {
method: 'POST',
body: normalizeResponsesPayloadForUpstream(responsesRequest, true),
headers: codexHeaders,
timeoutMs: streamTimeoutMs,
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent,
res
}), { maxRetries: upstream.maxRetries });

if (streamedResponses.ok) {
clearResponsesUnsupported(upstream.baseUrl);
return;
}

const canFallbackToChat = streamedResponses.status
? shouldFallbackFromUpstreamResponses(streamedResponses.status, streamedResponses.bodyText)
: false;
if (!canFallbackToChat) {
if (res.writableEnded || res.destroyed) return;
const status = streamedResponses.status && streamedResponses.status >= 400 ? streamedResponses.status : 502;
if (!res.headersSent) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(streamedResponses.bodyText || JSON.stringify({ error: streamedResponses.error || 'Upstream request failed' }));
} else {
writeSse(res, 'response.failed', { type: 'response.failed', error: streamedResponses.error || streamedResponses.bodyText || 'Upstream request failed' });
writeSse(res, 'done', '[DONE]');
res.end();
}
return;
}

if (!skipResponsesProbe && isResponsesEndpointUnsupported(streamedResponses.status, streamedResponses.bodyText)) {
markResponsesUnsupported(upstream.baseUrl);
}
const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}

const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}
if (streamRequested && wantsSse) {
const upstreamUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
const chatBody = { ...converted.chat, stream: true };
const streamed = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(upstreamUrl, {
Expand Down Expand Up @@ -503,74 +463,6 @@ function createOpenaiBridgeHttpHandler(options = {}) {
return;
}

// Maxx-style behavior: prefer upstream /responses if supported.
// Fallback to /chat/completions conversion when upstream does not implement /responses (404/405).
// 已知不支持的上游:直接跳过探测,节省一次 round-trip。
const skipResponsesProbe = isResponsesKnownUnsupported(upstream.baseUrl);
const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
const upstreamResponsesResult = skipResponsesProbe
? { ok: true, status: 404, bodyText: '' }
: await retryTransientRequest(() => proxyRequestJson(upstreamResponsesUrl, {
method: 'POST',
body: toUpstreamNonStreamingResponsesPayload(responsesRequest),
headers: codexHeaders,
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent
}), { maxRetries: upstream.maxRetries });

if (upstreamResponsesResult.ok && upstreamResponsesResult.status >= 200 && upstreamResponsesResult.status < 300) {
clearResponsesUnsupported(upstream.baseUrl);
const upstreamJson = parseJsonOrError(upstreamResponsesResult.bodyText);
if (upstreamJson.error) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${upstreamJson.error}` }));
return;
}
const upstreamPayload = upstreamJson.value;
if (streamRequested && wantsSse) {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
if (typeof res.flushHeaders === 'function') res.flushHeaders();
sendResponsesSse(res, upstreamPayload);
res.end();
return;
}

res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(ensureResponseMetadata(upstreamPayload)));
return;
}

if (upstreamResponsesResult.ok && upstreamResponsesResult.status >= 400) {
if (!shouldFallbackFromUpstreamResponses(upstreamResponsesResult.status, upstreamResponsesResult.bodyText)) {
res.writeHead(upstreamResponsesResult.status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(upstreamResponsesResult.bodyText || JSON.stringify({ error: 'Upstream error' }));
return;
}
if (!skipResponsesProbe && isResponsesEndpointUnsupported(upstreamResponsesResult.status, upstreamResponsesResult.bodyText)) {
markResponsesUnsupported(upstream.baseUrl);
}
// fallthrough to chat/completions conversion
}

if (!upstreamResponsesResult.ok) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResponsesResult.error}` }));
return;
}

const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}

const upstreamUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
method: 'POST',
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(webUiRedirectBlock, /url\.pathname\s*=\s*'\/'/);
assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/);
assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/);
assert.match(html, /id="side-tab-prompts"/);
assert.doesNotMatch(html, /<div class="side-section-title">\{\{ t\('side\.prompts'\) \}\}<\/div>/);
assert.doesNotMatch(html, /<div class="side-section" role="navigation" :aria-label="t\('side\.prompts'\)">/);
assert.doesNotMatch(html, /id="side-tab-prompts-agents"/);
assert.doesNotMatch(html, /id="side-tab-prompts-project"/);
assert.doesNotMatch(html, /id="side-tab-prompts-presets"/);
assert.match(html, /@click="onMainTabClick\('prompts', \$event\)"/);
assert.match(html, /t\('side\.prompts'\)/);
assert.match(html, /t\('side\.prompts\.meta'\)/);
assert.match(html, /v-if="promptsSubTab === 'presets'" class="prompts-editor prompt-presets-panel"/);
assert.match(html, /class="prompts-editor-toolbar prompt-presets-toolbar"/);
assert.match(html, /class="prompts-editor-actions prompt-presets-save-row"/);
assert.match(html, /@click="applyPromptPresetToEditor\(preset\)"/);
assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'codex'\)"/);
assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'claude-project'\)"/);
assert.doesNotMatch(html, /@click="saveEditorPromptAsPreset"/);
assert.match(html, /class="form-group prompt-presets-body"/);
assert.match(html, /class="editor-frame prompt-presets-frame"/);
assert.match(html, /id="side-tab-orchestration"/);
assert.match(html, /id="tab-orchestration"/);
assert.match(html, /data-main-tab="orchestration"/);
Expand Down
10 changes: 9 additions & 1 deletion tests/unit/i18n-locales.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,16 @@ test('Japanese orchestration template copy stays localized', () => {
}
});

test('provider cache and local Web preference settings are localized in every locale', () => {
test('provider cache, prompt sidebar, and local Web preference settings are localized in every locale', () => {
const keys = [
'side.prompts.meta',
'side.prompts.presets',
'side.prompts.presets.meta',
'prompts.presets.addCurrent',
'prompts.presets.defaultName.project',
'prompts.presets.confirm.addCurrentTitle',
'prompts.presets.confirm.addCurrentMessage',
'prompts.presets.toast.pasted',
'announcement.providerCache.open',
'announcement.project.eyebrow',
'announcement.project.title',
Expand Down
Loading
Loading