@bitkyc08/opencodex 1.9.5 → 2.0.1
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.ko.md +95 -71
- package/README.md +94 -46
- package/README.zh-CN.md +101 -70
- package/gui/dist/assets/index-B9nYLpkt.js +9 -0
- package/gui/dist/assets/index-cEIM1XWY.css +1 -0
- package/gui/dist/index.html +13 -3
- package/package.json +1 -3
- package/src/adapters/openai-chat.ts +34 -20
- package/src/bridge.ts +13 -5
- package/src/cli.ts +48 -15
- package/src/codex-catalog.ts +155 -36
- package/src/codex-refresh.ts +49 -0
- package/src/codex-shim.ts +145 -0
- package/src/config.ts +2 -1
- package/src/oauth/index.ts +28 -12
- package/src/oauth/key-providers.ts +27 -0
- package/src/providers/derive.ts +35 -0
- package/src/providers/registry.ts +133 -9
- package/src/reasoning-effort.ts +102 -0
- package/src/responses/parser.ts +1 -1
- package/src/server.ts +33 -17
- package/src/service.ts +26 -2
- package/src/star-prompt.ts +5 -4
- package/src/types.ts +22 -0
- package/src/ws-bridge.ts +5 -2
- package/gui/dist/assets/index-C1wlp1SM.css +0 -1
- package/gui/dist/assets/index-CDhJ0DI7.js +0 -9
- package/scripts/postinstall.mjs +0 -57
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#f6f7f9)var(--lightningcss-dark,#0c0d11);--rail:var(--lightningcss-light,#fff)var(--lightningcss-dark,#101218);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#15171d);--raised:var(--lightningcss-light,#f1f2f5)var(--lightningcss-dark,#1c1f27);--raised-hover:var(--lightningcss-light,#e8eaee)var(--lightningcss-dark,#242833);--border:var(--lightningcss-light,#e2e4e9)var(--lightningcss-dark,#2a2e39);--border-soft:var(--lightningcss-light,#ededf1)var(--lightningcss-dark,#20242d);--hover:var(--lightningcss-light,#11131c09)var(--lightningcss-dark,#ffffff06);--text:var(--lightningcss-light,#16181d)var(--lightningcss-dark,#edeef2);--muted:var(--lightningcss-light,#5b6270)var(--lightningcss-dark,#a3a9b5);--faint:var(--lightningcss-light,#868d9b)var(--lightningcss-dark,#6b7280);--accent:var(--lightningcss-light,#4f46e5)var(--lightningcss-dark,#6366f1);--accent-hover:var(--lightningcss-light,#4338ca)var(--lightningcss-dark,#818cf8);--accent-ink:#fff;--accent-soft:var(--lightningcss-light,#4f46e51a)var(--lightningcss-dark,#6366f129);--accent-ring:var(--lightningcss-light,#4f46e566)var(--lightningcss-dark,#6366f180);--green:var(--lightningcss-light,#047857)var(--lightningcss-dark,#34d399);--green-soft:var(--lightningcss-light,#0596691a)var(--lightningcss-dark,#34d39921);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#b45309)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--radius:8px;--radius-sm:5px;--radius-xs:4px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, "Helvetica Neue", sans-serif;--mono:ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}a{color:var(--accent-hover);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:-.01em;margin:0;font-weight:650}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border:2px solid var(--bg);border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;align-self:start;gap:4px;height:100dvh;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{letter-spacing:-.02em;font-size:15px;font-weight:700;line-height:26px}.brand .ver{font-family:var(--mono);color:var(--muted);background:var(--raised);border:1px solid var(--border);border-radius:99px;align-self:center;padding:2px 6px;font-size:10px;line-height:1}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;font-weight:500;transition:background .12s,color .12s;display:flex}.nav-item:hover{background:var(--raised);color:var(--text)}.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--accent)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;display:flex}.sidebar-link:hover{background:var(--raised);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;border-radius:var(--radius-sm);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;display:flex}.theme-toggle:hover{background:var(--raised);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:19px}.page-sub{color:var(--muted);max-width:70ch;margin:4px 0 22px;font-size:13.5px}.page-sub b{color:var(--text);font-weight:600}.btn{border-radius:var(--radius-sm);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:550;transition:background .12s,border-color .12s,opacity .12s;display:inline-flex}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--raised);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised-hover)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{border-radius:var(--radius-xs);padding:4px 9px;font-size:12px}.btn-icon{padding:5px}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{background:linear-gradient(180deg, var(--accent-soft), transparent 120%), var(--surface);border-color:#7c5cff47}.stat-row{grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px;display:grid}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--accent-ring)}.stat .label{color:var(--muted);text-transform:uppercase;letter-spacing:.05em;align-items:center;gap:6px;margin-bottom:9px;font-size:11px;font-weight:600;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:-.02em;font-size:24px;font-weight:700;line-height:1.1}.stat .value.mono{font-family:var(--mono);font-size:19px}.model-group-head{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--faint);font-weight:500}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--accent-ring);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:-.01em;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:.01em;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent-hover)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);padding:9px 12px;font-size:11.5px;font-weight:600}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.tbl .num{text-align:right;font-family:var(--mono)}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;padding:8px 11px;font-size:13px;transition:border-color .12s}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--accent);outline:none}textarea.input{resize:vertical;font-family:var(--mono);line-height:1.55}.field-label{color:var(--muted);margin-bottom:5px;font-size:12px;font-weight:500;display:block}select.input{appearance:none}.switch{cursor:pointer;background:var(--lightningcss-light,#c5c9d2)var(--lightningcss-dark,#3a3f4b);border:none;border-radius:99px;flex-shrink:0;width:34px;height:19px;padding:0;transition:background .15s;position:relative}.switch.on{background:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:#fff;border-radius:50%;width:15px;height:15px;transition:left .15s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #1018284d}.switch.on .knob{left:17px}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.stack{flex-direction:column;display:flex}.chip{font-family:var(--mono);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px;font-size:12px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);margin-bottom:6px;font-weight:600}.notice{border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;font-size:13px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.notice-ok{background:var(--green-soft);color:var(--green)}.notice-err{background:var(--red-soft);color:var(--red)}.h-section{color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;font-size:13px;font-weight:600;display:flex}.h-section .count{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:12px}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}.modal-overlay{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:var(--lightningcss-light,#11131c73)var(--lightningcss-dark,#0009);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:100%;max-width:520px;box-shadow:var(--shadow);max-height:84vh;padding:20px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:16px}.setup-guide{border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px;font-size:13px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:500}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;transition:background .12s,border-color .12s;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-size:14px;font-weight:600}.list-row .sub{color:var(--muted);margin-top:2px;font-size:12px}.prov-card{justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.link-btn{color:var(--accent-hover);font:inherit;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}@media (width<=760px){.app{grid-template-columns:1fr}.sidebar{z-index:20;border-right:none;border-bottom:1px solid var(--border);background:var(--rail);flex-flow:wrap;align-items:center;gap:0;min-width:0;height:auto;padding:0 10px;position:sticky;top:0}.brand{flex:auto;order:1;width:auto;padding:10px 4px}.sidebar-foot{flex-direction:row;flex:none;order:2;gap:4px;margin:0;padding:0}.sidebar-foot .sidebar-link{display:none}.theme-toggle{justify-content:center;min-width:44px;min-height:44px;padding:8px}.theme-toggle .mode{display:none}.sidebar nav{overscroll-behavior-x:contain;border-top:1px solid var(--border-soft);scrollbar-width:none;flex-direction:row;flex:100%;order:3;gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:flex;overflow-x:auto}.sidebar nav::-webkit-scrollbar{display:none}.nav-item{white-space:nowrap;width:auto;min-height:44px;padding:9px 14px;font-size:14px}.main-inner{padding:22px 18px 48px}.stat-row{grid-template-columns:repeat(2,minmax(0,1fr))}.tbl{min-width:460px}}
|
package/gui/dist/index.html
CHANGED
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<meta name="color-scheme" content="dark" />
|
|
7
|
+
<meta name="color-scheme" content="light dark" />
|
|
8
8
|
<title>opencodex · proxy dashboard</title>
|
|
9
|
-
<script
|
|
10
|
-
|
|
9
|
+
<script>
|
|
10
|
+
// FOWT guard: apply an explicit light/dark choice before first paint. "system" leaves the
|
|
11
|
+
// attribute unset so color-scheme:light-dark follows the OS. Mirrors App.tsx.
|
|
12
|
+
(function () {
|
|
13
|
+
try {
|
|
14
|
+
var t = localStorage.getItem("ocx-theme");
|
|
15
|
+
if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t);
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-B9nYLpkt.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-cEIM1XWY.css">
|
|
11
21
|
</head>
|
|
12
22
|
<body>
|
|
13
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"src",
|
|
13
|
-
"scripts/postinstall.mjs",
|
|
14
13
|
"gui/dist",
|
|
15
14
|
"README.md",
|
|
16
15
|
"LICENSE"
|
|
@@ -25,7 +24,6 @@
|
|
|
25
24
|
"typecheck": "bun x tsc --noEmit",
|
|
26
25
|
"generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
|
|
27
26
|
"build:gui": "cd gui && bun install && bun run build",
|
|
28
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
29
27
|
"prepublishOnly": "bun run typecheck && bun run build:gui",
|
|
30
28
|
"release": "bun scripts/release.ts",
|
|
31
29
|
"release:watch": "bun scripts/release.ts watch"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "./base";
|
|
2
2
|
import { debugDroppedFrame } from "../debug";
|
|
3
|
-
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxToolCall, OcxUsage } from "../types";
|
|
4
|
-
import { namespacedToolName } from "../types";
|
|
3
|
+
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
4
|
+
import { modelInList, namespacedToolName } from "../types";
|
|
5
|
+
import { mapReasoningEffort } from "../reasoning-effort";
|
|
5
6
|
import { contentPartsToText } from "./image";
|
|
6
7
|
|
|
7
|
-
function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
8
|
+
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
8
9
|
const out: unknown[] = [];
|
|
9
10
|
const { context, options } = parsed;
|
|
10
11
|
let pendingToolCallIds = new Set<string>();
|
|
@@ -46,11 +47,16 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
|
46
47
|
case "assistant": {
|
|
47
48
|
const aMsg = msg as OcxAssistantMessage;
|
|
48
49
|
const textParts = aMsg.content.filter(p => p.type === "text") as OcxTextContent[];
|
|
50
|
+
const thinkingParts = aMsg.content.filter(p => p.type === "thinking") as OcxThinkingContent[];
|
|
49
51
|
const toolCalls = aMsg.content.filter(p => p.type === "toolCall") as OcxToolCall[];
|
|
50
52
|
const chatMsg: Record<string, unknown> = { role: "assistant" };
|
|
51
53
|
if (textParts.length > 0) {
|
|
52
54
|
chatMsg.content = textParts.map(p => p.text).join("");
|
|
53
55
|
}
|
|
56
|
+
const reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
57
|
+
if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
58
|
+
chatMsg.reasoning_content = reasoningContent;
|
|
59
|
+
}
|
|
54
60
|
if (toolCalls.length > 0) {
|
|
55
61
|
chatMsg.tool_calls = toolCalls.map(tc => ({
|
|
56
62
|
id: tc.id,
|
|
@@ -59,9 +65,12 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
|
59
65
|
}));
|
|
60
66
|
if (!chatMsg.content) chatMsg.content = null;
|
|
61
67
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
|
|
69
|
+
chatMsg.content = "";
|
|
70
|
+
}
|
|
71
|
+
// Skip empty assistant messages: chat APIs like DeepSeek reject an assistant message
|
|
72
|
+
// with neither content, tool calls, nor a provider-supported reasoning_content field.
|
|
73
|
+
if (chatMsg.content === undefined && chatMsg.tool_calls === undefined && chatMsg.reasoning_content === undefined) break;
|
|
65
74
|
out.push(chatMsg);
|
|
66
75
|
pendingToolCallIds = new Set(toolCalls.map(tc => tc.id).filter(Boolean));
|
|
67
76
|
break;
|
|
@@ -141,7 +150,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
141
150
|
name: "openai-chat",
|
|
142
151
|
|
|
143
152
|
buildRequest(parsed: OcxParsedRequest) {
|
|
144
|
-
const messages = messagesToChatFormat(parsed);
|
|
153
|
+
const messages = messagesToChatFormat(parsed, provider);
|
|
145
154
|
const tools = toolsToChatFormat(parsed);
|
|
146
155
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice);
|
|
147
156
|
|
|
@@ -151,22 +160,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
151
160
|
stream: parsed.stream,
|
|
152
161
|
};
|
|
153
162
|
if (tools) body.tools = tools;
|
|
154
|
-
if (toolChoice !== undefined)
|
|
163
|
+
if (toolChoice !== undefined) {
|
|
164
|
+
body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
|
|
165
|
+
? (toolChoice === "none" ? "none" : "auto")
|
|
166
|
+
: toolChoice;
|
|
167
|
+
}
|
|
155
168
|
if (parsed.options.maxOutputTokens !== undefined) body.max_tokens = parsed.options.maxOutputTokens;
|
|
156
|
-
if (parsed.options.temperature !== undefined
|
|
157
|
-
|
|
169
|
+
if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
|
|
170
|
+
body.temperature = parsed.options.temperature;
|
|
171
|
+
}
|
|
172
|
+
if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
|
|
173
|
+
body.top_p = parsed.options.topP;
|
|
174
|
+
}
|
|
158
175
|
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (parsed.options.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
body.reasoning_effort = r === "minimal" ? "low" : r === "max" ? "xhigh" : r;
|
|
176
|
+
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
177
|
+
if (reasoningEffort !== undefined) body.reasoning_effort = reasoningEffort;
|
|
178
|
+
if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
179
|
+
body.presence_penalty = parsed.options.presencePenalty;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
182
|
+
body.frequency_penalty = parsed.options.frequencyPenalty;
|
|
167
183
|
}
|
|
168
|
-
if (parsed.options.presencePenalty !== undefined) body.presence_penalty = parsed.options.presencePenalty;
|
|
169
|
-
if (parsed.options.frequencyPenalty !== undefined) body.frequency_penalty = parsed.options.frequencyPenalty;
|
|
170
184
|
|
|
171
185
|
if (parsed.stream) {
|
|
172
186
|
body.stream_options = { include_usage: true };
|
package/src/bridge.ts
CHANGED
|
@@ -103,9 +103,18 @@ export function bridgeToResponsesSSE(
|
|
|
103
103
|
// Re-arm Codex's idle timer during silence with a parser-ignored heartbeat (RC3). Skips a tick
|
|
104
104
|
// whenever a real event was emitted since the last tick, so it only fires on a genuine stall.
|
|
105
105
|
const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
|
|
106
|
+
let stallTicks = 0;
|
|
107
|
+
const maxStallTicks = 150; // 5 min at default 2 s interval
|
|
106
108
|
beat = setInterval(() => {
|
|
107
109
|
if (closed) return;
|
|
108
|
-
if (activity) { activity = false; return; }
|
|
110
|
+
if (activity) { activity = false; stallTicks = 0; return; }
|
|
111
|
+
if (++stallTicks >= maxStallTicks) {
|
|
112
|
+
closed = true;
|
|
113
|
+
clearInterval(beat!);
|
|
114
|
+
beat = undefined;
|
|
115
|
+
onCancel?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
try { controller.enqueue(heartbeatFrame); } catch { closed = true; }
|
|
110
119
|
}, heartbeatMs);
|
|
111
120
|
|
|
@@ -351,15 +360,14 @@ export function bridgeToResponsesSSE(
|
|
|
351
360
|
if (beat) clearInterval(beat);
|
|
352
361
|
|
|
353
362
|
if (!terminated) {
|
|
354
|
-
// The adapter generator ended without
|
|
355
|
-
//
|
|
356
|
-
// open items and synthesize a clean completion so the stream is never terminal-less.
|
|
363
|
+
// The adapter generator ended without an explicit done/error event. Mark as incomplete
|
|
364
|
+
// rather than completed so Codex can distinguish a clean finish from a truncated stream.
|
|
357
365
|
if (currentMsg) closeCurrentMessage();
|
|
358
366
|
if (currentReasoning) closeCurrentReasoning();
|
|
359
367
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
360
368
|
if (currentToolCall) closeCurrentToolCall();
|
|
361
369
|
emit("response.completed", {
|
|
362
|
-
response: { ...responseSnapshot("
|
|
370
|
+
response: { ...responseSnapshot("incomplete", finishedItems), usage: responsesUsage(undefined) },
|
|
363
371
|
});
|
|
364
372
|
}
|
|
365
373
|
|
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
3
|
import { restoreNativeCodex } from "./codex-inject";
|
|
5
4
|
import { loadConfig, readPid, removePid, writePid } from "./config";
|
|
6
|
-
import { serviceCommand } from "./service";
|
|
5
|
+
import { serviceCommand, stopServiceIfInstalled } from "./service";
|
|
7
6
|
import { startServer } from "./server";
|
|
8
7
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
9
8
|
|
|
@@ -19,6 +18,7 @@ Usage:
|
|
|
19
18
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
20
19
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
21
20
|
ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
|
|
21
|
+
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
|
|
22
22
|
ocx sync Fetch models from providers and inject into Codex config
|
|
23
23
|
ocx status Check proxy server status
|
|
24
24
|
ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
|
|
@@ -38,11 +38,10 @@ async function syncModelsToCodex(port?: number) {
|
|
|
38
38
|
const p = port ?? config.port ?? 10100;
|
|
39
39
|
let catalogPath: string | null | undefined;
|
|
40
40
|
try {
|
|
41
|
-
const {
|
|
42
|
-
const cat = await
|
|
43
|
-
catalogPath =
|
|
41
|
+
const { refreshCodexModelCatalog } = await import("./codex-refresh");
|
|
42
|
+
const cat = await refreshCodexModelCatalog(config);
|
|
43
|
+
catalogPath = cat.catalogExists ? cat.path : null;
|
|
44
44
|
if (cat.added > 0) {
|
|
45
|
-
invalidateCodexModelsCache();
|
|
46
45
|
console.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
47
46
|
} else if (catalogPath === null) {
|
|
48
47
|
console.error("catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.");
|
|
@@ -56,7 +55,7 @@ async function syncModelsToCodex(port?: number) {
|
|
|
56
55
|
return result;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
function handleStart() {
|
|
58
|
+
async function handleStart(options: { block?: boolean } = {}) {
|
|
60
59
|
const existingPid = readPid();
|
|
61
60
|
if (existingPid) {
|
|
62
61
|
console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
|
|
@@ -76,9 +75,6 @@ function handleStart() {
|
|
|
76
75
|
const server = startServer(port);
|
|
77
76
|
writePid(process.pid);
|
|
78
77
|
|
|
79
|
-
void maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
|
|
80
|
-
syncModelsToCodex(port).catch(() => {});
|
|
81
|
-
|
|
82
78
|
const shutdown = () => {
|
|
83
79
|
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
84
80
|
server.stop(true);
|
|
@@ -91,6 +87,13 @@ function handleStart() {
|
|
|
91
87
|
|
|
92
88
|
process.on("SIGINT", shutdown);
|
|
93
89
|
process.on("SIGTERM", shutdown);
|
|
90
|
+
|
|
91
|
+
await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
|
|
92
|
+
await syncModelsToCodex(port).catch(() => {});
|
|
93
|
+
if (options.block ?? true) {
|
|
94
|
+
setInterval(() => {}, 60_000);
|
|
95
|
+
await new Promise<void>(() => {});
|
|
96
|
+
}
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
function killProxy(pid: number): void {
|
|
@@ -129,6 +132,9 @@ function waitForExit(pid: number, timeoutMs: number): boolean {
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
function handleStop() {
|
|
135
|
+
const stoppedService = stopServiceIfInstalled();
|
|
136
|
+
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
137
|
+
|
|
132
138
|
const pid = readPid();
|
|
133
139
|
let stopFailed = false;
|
|
134
140
|
if (pid) {
|
|
@@ -140,10 +146,9 @@ function handleStop() {
|
|
|
140
146
|
stopFailed = true;
|
|
141
147
|
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
142
148
|
}
|
|
143
|
-
} else {
|
|
149
|
+
} else if (!stoppedService) {
|
|
144
150
|
console.log("No running proxy found.");
|
|
145
151
|
}
|
|
146
|
-
// Recover native Codex so plain `codex` keeps working while the proxy is down.
|
|
147
152
|
const r = restoreNativeCodex();
|
|
148
153
|
console.log(`↩️ ${r.message}`);
|
|
149
154
|
if (stopFailed) process.exit(1);
|
|
@@ -165,7 +170,7 @@ switch (command) {
|
|
|
165
170
|
break;
|
|
166
171
|
}
|
|
167
172
|
case "start":
|
|
168
|
-
handleStart();
|
|
173
|
+
await handleStart();
|
|
169
174
|
break;
|
|
170
175
|
case "stop":
|
|
171
176
|
handleStop();
|
|
@@ -202,7 +207,12 @@ switch (command) {
|
|
|
202
207
|
const guiUrl = `http://localhost:${config.port}`;
|
|
203
208
|
if (!cfg.readPid()) {
|
|
204
209
|
console.log("Proxy not running. Starting...");
|
|
205
|
-
|
|
210
|
+
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
211
|
+
detached: true,
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
env: process.env,
|
|
214
|
+
});
|
|
215
|
+
child.unref();
|
|
206
216
|
await new Promise(r => setTimeout(r, 1000));
|
|
207
217
|
}
|
|
208
218
|
console.log(`Opening ${guiUrl}`);
|
|
@@ -213,6 +223,29 @@ switch (command) {
|
|
|
213
223
|
case "service":
|
|
214
224
|
serviceCommand(args[1]);
|
|
215
225
|
break;
|
|
226
|
+
case "codex-shim": {
|
|
227
|
+
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("./codex-shim");
|
|
228
|
+
switch (args[1]) {
|
|
229
|
+
case "install": {
|
|
230
|
+
const r = installCodexShim();
|
|
231
|
+
console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "status":
|
|
235
|
+
console.log(codexShimStatus());
|
|
236
|
+
break;
|
|
237
|
+
case "uninstall":
|
|
238
|
+
case "remove": {
|
|
239
|
+
const r = uninstallCodexShim();
|
|
240
|
+
console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
console.error("Usage: ocx codex-shim <install|status|uninstall>");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
216
249
|
case "update": {
|
|
217
250
|
const { runUpdate } = await import("./update");
|
|
218
251
|
runUpdate();
|