@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.
@@ -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}}
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BVahEsvB.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-dCS-lwCM.css">
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.7",
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 (const msg of parsed.context.messages) {
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 accepts a string OR content blocks — render images natively
115
- // (e.g. Codex view_image output) instead of dropping them.
116
- const trContent = typeof msg.content === "string"
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
- if (usage) {
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?: { responseId?: string; stallTimeoutSec?: number; hideThinkingSummary?: boolean },
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
+ }