@bitkyc08/opencodex 2.1.7 → 2.1.9
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/gui/dist/assets/{index-BVahEsvB.js → index-DVvcVBD_.js} +1 -1
- package/gui/dist/assets/index-xJdeQjzZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/adapters/anthropic.ts +88 -20
- package/src/adapters/openai-responses.ts +12 -0
- package/src/bridge.ts +21 -1
- package/src/codex-account-label.ts +34 -0
- package/src/codex-account-lifecycle.ts +21 -0
- package/src/codex-account-runtime-state.ts +13 -0
- package/src/codex-account-store.ts +355 -0
- package/src/codex-account-usability.ts +10 -0
- package/src/codex-auth-api.ts +446 -0
- package/src/codex-auth-collision.ts +66 -0
- package/src/codex-auth-context.ts +136 -0
- package/src/codex-catalog.ts +14 -4
- package/src/codex-quota.ts +130 -0
- package/src/codex-routing.ts +382 -0
- package/src/codex-websocket-registry.ts +57 -0
- package/src/config.ts +86 -26
- package/src/debug.ts +5 -4
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/index.ts +35 -7
- package/src/oauth/store.ts +9 -5
- package/src/privacy.ts +11 -0
- package/src/router.ts +1 -1
- package/src/server.ts +360 -23
- package/src/types.ts +38 -0
- package/src/vision/describe.ts +7 -3
- package/src/vision/index.ts +7 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/index.ts +3 -1
- package/src/web-search/loop.ts +6 -5
- package/src/ws-bridge.ts +56 -10
- package/gui/dist/assets/index-dCS-lwCM.css +0 -1
|
@@ -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{color:var(--muted);border-radius:var(--radius-xs);padding:5px;transition:background .12s,color .12s}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.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}.select-sm{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);color:var(--text);font:inherit;cursor:pointer;padding:5px 8px;font-size:13px;transition:border-color .12s}.select-sm:focus{border-color:var(--accent);outline:none}.select-sm:disabled{opacity:.5;cursor:default}.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}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:14px 16px 6px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px;font-size:12px}.card-active{border-color:var(--accent-ring)}.card-right{color:var(--faint);align-items:center;gap:4px;margin-left:auto;font-size:11px;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;font-size:12px;font-weight:600}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:2px 16px 12px;display:grid}.quota-row{grid-template-columns:minmax(34px,max-content) 34px minmax(34px,44px) minmax(38px,42px) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{color:var(--muted);font-size:11px;font-weight:600}.quota-val{font-size:11px;font-family:var(--mono);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;overflow:hidden}.quota-reset-day,.quota-reset-time{color:var(--muted);white-space:nowrap;font-size:11px}.quota-reset-time{font-family:var(--mono)}.bar{background:var(--raised);border-radius:3px;min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:3px;height:100%;transition:width .3s}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{background:var(--raised);border:1px solid var(--border);cursor:pointer;border-radius:10px;flex-shrink:0;width:36px;height:20px;transition:background .15s;position:relative}.toggle.on{background:var(--accent);border-color:var(--accent)}.toggle-knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:left .15s;position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{left:17px}.notice-warn{border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;font-size:12px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{color:var(--muted);margin-bottom:14px;font-size:13px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.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
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DVvcVBD_.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-xJdeQjzZ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.9",
|
|
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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"start": "bun run src/cli.ts start",
|
|
23
23
|
"test": "bun test tests",
|
|
24
24
|
"typecheck": "bun x tsc --noEmit",
|
|
25
|
+
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
25
26
|
"generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
|
|
26
27
|
"build:gui": "cd gui && bun install && bun run build",
|
|
27
28
|
"prepublishOnly": "bun run typecheck && bun run build:gui",
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
OcxTextContent,
|
|
11
11
|
OcxThinkingContent,
|
|
12
12
|
OcxToolCall,
|
|
13
|
+
OcxToolResultMessage,
|
|
13
14
|
OcxUsage,
|
|
14
15
|
} from "../types";
|
|
15
16
|
import { namespacedToolName } from "../types";
|
|
@@ -62,6 +63,19 @@ function usageFromAnthropic(usage: Record<string, number> | undefined): OcxUsage
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function mergeAnthropicUsage(
|
|
67
|
+
base: Record<string, number> | undefined,
|
|
68
|
+
next: Record<string, number> | undefined,
|
|
69
|
+
): Record<string, number> | undefined {
|
|
70
|
+
if (!next) return base;
|
|
71
|
+
if (!base) return { ...next };
|
|
72
|
+
const merged = { ...base };
|
|
73
|
+
for (const [k, v] of Object.entries(next)) {
|
|
74
|
+
merged[k] = (merged[k] ?? 0) + v;
|
|
75
|
+
}
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
function buildToolNameTransforms(provider: OcxProviderConfig): { toWire: (name: string) => string; fromWire: (name: string) => string } {
|
|
66
80
|
if (provider.authMode === "oauth") {
|
|
67
81
|
return { toWire: applyClaudeToolPrefix, fromWire: stripClaudeToolPrefix };
|
|
@@ -75,6 +89,28 @@ function buildToolNameTransforms(provider: OcxProviderConfig): { toWire: (name:
|
|
|
75
89
|
return { toWire: (name) => name, fromWire: (name) => name };
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function toAnthropicToolResult(msg: OcxToolResultMessage): Record<string, unknown> {
|
|
93
|
+
// Anthropic tool_result accepts a string OR content blocks — render images natively
|
|
94
|
+
// (e.g. Codex view_image output) instead of dropping them.
|
|
95
|
+
const content = typeof msg.content === "string"
|
|
96
|
+
? msg.content
|
|
97
|
+
: (msg.content as OcxContentPart[]).map(toAnthropicContentPart);
|
|
98
|
+
return {
|
|
99
|
+
type: "tool_result",
|
|
100
|
+
tool_use_id: msg.toolCallId,
|
|
101
|
+
content,
|
|
102
|
+
...(msg.isError ? { is_error: true } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function orphanToolResultText(msg: OcxToolResultMessage): string {
|
|
107
|
+
const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
|
|
108
|
+
const content = typeof msg.content === "string"
|
|
109
|
+
? msg.content
|
|
110
|
+
: JSON.stringify(msg.content);
|
|
111
|
+
return `[tool_result without adjacent tool_use: ${label}]\n${content}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
function messagesToAnthropicFormat(
|
|
79
115
|
parsed: OcxParsedRequest,
|
|
80
116
|
toolNames: { toWire: (name: string) => string },
|
|
@@ -82,7 +118,8 @@ function messagesToAnthropicFormat(
|
|
|
82
118
|
const system = parsed.context.systemPrompt?.join("\n\n") || undefined;
|
|
83
119
|
const messages: unknown[] = [];
|
|
84
120
|
|
|
85
|
-
for (
|
|
121
|
+
for (let i = 0; i < parsed.context.messages.length; i++) {
|
|
122
|
+
const msg = parsed.context.messages[i];
|
|
86
123
|
switch (msg.role) {
|
|
87
124
|
case "user":
|
|
88
125
|
case "developer": {
|
|
@@ -95,6 +132,7 @@ function messagesToAnthropicFormat(
|
|
|
95
132
|
case "assistant": {
|
|
96
133
|
const aMsg = msg as OcxAssistantMessage;
|
|
97
134
|
const content: unknown[] = [];
|
|
135
|
+
const toolUseIds: string[] = [];
|
|
98
136
|
for (const part of aMsg.content) {
|
|
99
137
|
if (part.type === "text") {
|
|
100
138
|
content.push({ type: "text", text: (part as OcxTextContent).text });
|
|
@@ -104,26 +142,46 @@ function messagesToAnthropicFormat(
|
|
|
104
142
|
} else if (part.type === "toolCall") {
|
|
105
143
|
const tc = part as OcxToolCall;
|
|
106
144
|
const flatName = namespacedToolName(tc.namespace, tc.name);
|
|
145
|
+
toolUseIds.push(tc.id);
|
|
107
146
|
content.push({ type: "tool_use", id: tc.id, name: toolNames.toWire(flatName), input: tc.arguments });
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
messages.push({ role: "assistant", content });
|
|
150
|
+
if (toolUseIds.length > 0) {
|
|
151
|
+
const requiredIds = new Set(toolUseIds);
|
|
152
|
+
const resultBlocks: Record<string, unknown>[] = [];
|
|
153
|
+
const orphanBlocks: Record<string, unknown>[] = [];
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
let j = i + 1;
|
|
156
|
+
while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
|
|
157
|
+
const tr = parsed.context.messages[j] as OcxToolResultMessage;
|
|
158
|
+
if (requiredIds.has(tr.toolCallId) && !seen.has(tr.toolCallId)) {
|
|
159
|
+
resultBlocks.push(toAnthropicToolResult(tr));
|
|
160
|
+
seen.add(tr.toolCallId);
|
|
161
|
+
} else {
|
|
162
|
+
orphanBlocks.push({ type: "text", text: orphanToolResultText(tr) });
|
|
163
|
+
}
|
|
164
|
+
j++;
|
|
165
|
+
}
|
|
166
|
+
for (const id of toolUseIds) {
|
|
167
|
+
if (!seen.has(id)) {
|
|
168
|
+
resultBlocks.push({
|
|
169
|
+
type: "tool_result",
|
|
170
|
+
tool_use_id: id,
|
|
171
|
+
content: "[opencodex: missing tool_result for this tool_use in Codex history]",
|
|
172
|
+
is_error: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
messages.push({ role: "user", content: [...resultBlocks, ...orphanBlocks] });
|
|
177
|
+
i = j - 1;
|
|
178
|
+
}
|
|
111
179
|
break;
|
|
112
180
|
}
|
|
113
181
|
case "toolResult": {
|
|
114
|
-
// Anthropic tool_result
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
? msg.content
|
|
118
|
-
: (msg.content as OcxContentPart[]).map(toAnthropicContentPart);
|
|
119
|
-
messages.push({
|
|
120
|
-
role: "user",
|
|
121
|
-
content: [{
|
|
122
|
-
type: "tool_result",
|
|
123
|
-
tool_use_id: msg.toolCallId,
|
|
124
|
-
content: trContent,
|
|
125
|
-
}],
|
|
126
|
-
});
|
|
182
|
+
// A standalone Anthropic tool_result is invalid unless it immediately follows an
|
|
183
|
+
// assistant tool_use. Preserve the information as text instead of sending a 400-prone block.
|
|
184
|
+
messages.push({ role: "user", content: orphanToolResultText(msg as OcxToolResultMessage) });
|
|
127
185
|
break;
|
|
128
186
|
}
|
|
129
187
|
}
|
|
@@ -224,6 +282,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
224
282
|
let currentBlockType = "";
|
|
225
283
|
let currentToolCallId = "";
|
|
226
284
|
let currentToolCallName = "";
|
|
285
|
+
let pendingUsage: Record<string, number> | undefined;
|
|
286
|
+
let emittedDone = false;
|
|
287
|
+
|
|
288
|
+
const emitDone = function* (): Generator<AdapterEvent> {
|
|
289
|
+
if (emittedDone) return;
|
|
290
|
+
emittedDone = true;
|
|
291
|
+
yield { type: "done", usage: usageFromAnthropic(pendingUsage) };
|
|
292
|
+
};
|
|
227
293
|
|
|
228
294
|
try {
|
|
229
295
|
while (true) {
|
|
@@ -253,6 +319,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
253
319
|
}
|
|
254
320
|
|
|
255
321
|
switch (currentEventType || data.type) {
|
|
322
|
+
case "message_start": {
|
|
323
|
+
const message = data.message as { usage?: Record<string, number> } | undefined;
|
|
324
|
+
pendingUsage = mergeAnthropicUsage(pendingUsage, message?.usage);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
256
327
|
case "content_block_start": {
|
|
257
328
|
const block = data.content_block as { type: string; id?: string; name?: string } | undefined;
|
|
258
329
|
if (!block) break;
|
|
@@ -286,15 +357,11 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
286
357
|
}
|
|
287
358
|
case "message_delta": {
|
|
288
359
|
const usage = data.usage as Record<string, number> | undefined;
|
|
289
|
-
|
|
290
|
-
yield {
|
|
291
|
-
type: "done",
|
|
292
|
-
usage: usageFromAnthropic(usage),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
360
|
+
pendingUsage = mergeAnthropicUsage(pendingUsage, usage);
|
|
295
361
|
break;
|
|
296
362
|
}
|
|
297
363
|
case "message_stop": {
|
|
364
|
+
yield* emitDone();
|
|
298
365
|
break;
|
|
299
366
|
}
|
|
300
367
|
case "error": {
|
|
@@ -306,6 +373,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
306
373
|
currentEventType = "";
|
|
307
374
|
}
|
|
308
375
|
}
|
|
376
|
+
if (pendingUsage && !emittedDone) yield* emitDone();
|
|
309
377
|
} finally {
|
|
310
378
|
reader.releaseLock();
|
|
311
379
|
}
|
|
@@ -55,10 +55,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
55
55
|
// OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
|
|
56
56
|
url = `${provider.baseUrl}/responses`;
|
|
57
57
|
if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
|
|
58
|
+
const runtimeProvider = provider as {
|
|
59
|
+
_codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
|
|
60
|
+
_codexAccountRequired?: boolean;
|
|
61
|
+
};
|
|
62
|
+
if (runtimeProvider._codexAccountRequired && !runtimeProvider._codexAccountOverride) {
|
|
63
|
+
throw new Error("Codex pool account auth is required but unavailable");
|
|
64
|
+
}
|
|
58
65
|
for (const h of FORWARD_HEADERS) {
|
|
59
66
|
const v = incoming?.headers.get(h);
|
|
60
67
|
if (v) headers[h] = v; // …so forwarded auth always wins.
|
|
61
68
|
}
|
|
69
|
+
const override = runtimeProvider._codexAccountOverride;
|
|
70
|
+
if (override) {
|
|
71
|
+
headers["authorization"] = `Bearer ${override.accessToken}`;
|
|
72
|
+
headers["chatgpt-account-id"] = override.chatgptAccountId;
|
|
73
|
+
}
|
|
62
74
|
} else {
|
|
63
75
|
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
|
|
64
76
|
url = `${base}/v1/responses`;
|
package/src/bridge.ts
CHANGED
|
@@ -35,6 +35,8 @@ interface OutputItem {
|
|
|
35
35
|
[key: string]: unknown;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
|
|
39
|
+
|
|
38
40
|
export function bridgeToResponsesSSE(
|
|
39
41
|
events: AsyncIterable<AdapterEvent>,
|
|
40
42
|
modelId: string,
|
|
@@ -43,7 +45,12 @@ export function bridgeToResponsesSSE(
|
|
|
43
45
|
toolSearchToolNames?: Set<string>,
|
|
44
46
|
onCancel?: () => void,
|
|
45
47
|
heartbeatMs = 2_000,
|
|
46
|
-
options?: {
|
|
48
|
+
options?: {
|
|
49
|
+
responseId?: string;
|
|
50
|
+
stallTimeoutSec?: number;
|
|
51
|
+
hideThinkingSummary?: boolean;
|
|
52
|
+
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
53
|
+
},
|
|
47
54
|
): ReadableStream<Uint8Array> {
|
|
48
55
|
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
49
56
|
// function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
|
|
@@ -62,6 +69,13 @@ export function bridgeToResponsesSSE(
|
|
|
62
69
|
// never enqueue again and never throw a second time inside start() — the RC2 double-throw that
|
|
63
70
|
// otherwise surfaced as proxy-side stream noise on every client disconnect.
|
|
64
71
|
let closed = false;
|
|
72
|
+
let clientCancelled = false;
|
|
73
|
+
let terminalReported = false;
|
|
74
|
+
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
75
|
+
if (terminalReported || clientCancelled || closed) return;
|
|
76
|
+
terminalReported = true;
|
|
77
|
+
options?.onTerminal?.(status);
|
|
78
|
+
};
|
|
65
79
|
// RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
|
|
66
80
|
// eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
|
|
67
81
|
// (responses.rs `_ => Ok(None)`). We emit a real, parser-ignored `response.heartbeat` only during
|
|
@@ -120,6 +134,7 @@ export function bridgeToResponsesSSE(
|
|
|
120
134
|
incomplete_details: { reason: "upstream_stall_timeout" },
|
|
121
135
|
},
|
|
122
136
|
});
|
|
137
|
+
reportTerminal("incomplete");
|
|
123
138
|
terminated = true;
|
|
124
139
|
closed = true;
|
|
125
140
|
clearInterval(beat!);
|
|
@@ -341,6 +356,7 @@ export function bridgeToResponsesSSE(
|
|
|
341
356
|
emit("response.completed", {
|
|
342
357
|
response: { ...responseSnapshot("completed", finishedItems), usage: responsesUsage(event.usage) },
|
|
343
358
|
});
|
|
359
|
+
reportTerminal("completed");
|
|
344
360
|
terminated = true;
|
|
345
361
|
break;
|
|
346
362
|
}
|
|
@@ -356,6 +372,7 @@ export function bridgeToResponsesSSE(
|
|
|
356
372
|
last_error: responseError(502, "upstream_error", event.message),
|
|
357
373
|
},
|
|
358
374
|
});
|
|
375
|
+
reportTerminal("failed");
|
|
359
376
|
terminated = true;
|
|
360
377
|
break;
|
|
361
378
|
}
|
|
@@ -369,6 +386,7 @@ export function bridgeToResponsesSSE(
|
|
|
369
386
|
last_error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
|
|
370
387
|
},
|
|
371
388
|
});
|
|
389
|
+
reportTerminal("failed");
|
|
372
390
|
terminated = true;
|
|
373
391
|
}
|
|
374
392
|
|
|
@@ -388,6 +406,7 @@ export function bridgeToResponsesSSE(
|
|
|
388
406
|
incomplete_details: { reason: "adapter_eof" },
|
|
389
407
|
},
|
|
390
408
|
});
|
|
409
|
+
reportTerminal("incomplete");
|
|
391
410
|
}
|
|
392
411
|
|
|
393
412
|
emitDone();
|
|
@@ -400,6 +419,7 @@ export function bridgeToResponsesSSE(
|
|
|
400
419
|
cancel() {
|
|
401
420
|
// Client (Codex) disconnected. Stop emitting and let the caller abort the upstream fetch so a
|
|
402
421
|
// cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
|
|
422
|
+
clientCancelled = true;
|
|
403
423
|
closed = true;
|
|
404
424
|
if (beat) clearInterval(beat);
|
|
405
425
|
onCancel?.();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { CodexAccount } from "./types";
|
|
3
|
+
|
|
4
|
+
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
5
|
+
|
|
6
|
+
export function createCodexAccountLogLabel(existingLabels: Iterable<string | undefined | null> = []): string {
|
|
7
|
+
const used = new Set([...existingLabels].filter((value): value is string => !!value));
|
|
8
|
+
for (let i = 0; i < 16; i++) {
|
|
9
|
+
const label = `p${randomBytes(3).toString("hex")}`;
|
|
10
|
+
if (!used.has(label)) return label;
|
|
11
|
+
}
|
|
12
|
+
return `p${randomBytes(6).toString("hex").slice(0, 6)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fallbackCodexAccountLogLabel(accountId: string): string {
|
|
16
|
+
return `p${createHash("sha256").update(accountId).digest("hex").slice(0, 6)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function codexAccountLogLabel(account: CodexAccount): string {
|
|
20
|
+
return CODEX_ACCOUNT_LOG_LABEL_RE.test(account.logLabel ?? "")
|
|
21
|
+
? account.logLabel!
|
|
22
|
+
: fallbackCodexAccountLogLabel(account.id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function withCodexAccountLogLabel(
|
|
26
|
+
account: Omit<CodexAccount, "logLabel"> & Partial<Pick<CodexAccount, "logLabel">>,
|
|
27
|
+
existingAccounts: readonly CodexAccount[],
|
|
28
|
+
): CodexAccount {
|
|
29
|
+
if (account.logLabel && CODEX_ACCOUNT_LOG_LABEL_RE.test(account.logLabel)) return account as CodexAccount;
|
|
30
|
+
return {
|
|
31
|
+
...account,
|
|
32
|
+
logLabel: createCodexAccountLogLabel(existingAccounts.map(existing => existing.logLabel)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { removeCodexAccountCredential } from "./codex-account-store";
|
|
2
|
+
import { clearAccountNeedsReauth } from "./codex-account-runtime-state";
|
|
3
|
+
import { clearAccountQuota } from "./codex-quota";
|
|
4
|
+
import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./codex-routing";
|
|
5
|
+
import { invalidateCodexWebSocketsForAccount } from "./codex-websocket-registry";
|
|
6
|
+
import type { OcxConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
export function purgeCodexAccountRuntimeState(accountId: string): void {
|
|
9
|
+
clearAccountNeedsReauth(accountId);
|
|
10
|
+
clearAccountQuota(accountId);
|
|
11
|
+
clearThreadAccountMapForAccount(accountId);
|
|
12
|
+
clearCodexUpstreamHealthForAccount(accountId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string): void {
|
|
16
|
+
removeCodexAccountCredential(accountId);
|
|
17
|
+
runtimeConfig.codexAccounts = (runtimeConfig.codexAccounts ?? []).filter(account => account.id !== accountId);
|
|
18
|
+
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
19
|
+
purgeCodexAccountRuntimeState(accountId);
|
|
20
|
+
invalidateCodexWebSocketsForAccount(accountId);
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const reauthAccounts = new Set<string>();
|
|
2
|
+
|
|
3
|
+
export function markAccountNeedsReauth(id: string): void {
|
|
4
|
+
reauthAccounts.add(id);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isAccountNeedsReauth(id: string): boolean {
|
|
8
|
+
return reauthAccounts.has(id);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clearAccountNeedsReauth(id: string): void {
|
|
12
|
+
reauthAccounts.delete(id);
|
|
13
|
+
}
|