@bitkyc08/opencodex 2.45.0 → 2.46.0-preview.20260907

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.
Files changed (52) hide show
  1. package/gui/dist/assets/{index-J96sug5C.css → index-BFgUC17B.css} +1 -1
  2. package/gui/dist/assets/{index-CCfD72yq.js → index-NcAVXkST.js} +19 -19
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/raycast.svg +3 -0
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +8 -3
  7. package/src/adapters/openai-responses.ts +7 -0
  8. package/src/bridge.ts +27 -7
  9. package/src/claude/inbound.ts +23 -5
  10. package/src/claude/outbound.ts +87 -19
  11. package/src/cli/capabilities.ts +4 -1
  12. package/src/cli/dispatch.ts +1 -1
  13. package/src/cli/doctor.ts +2 -2
  14. package/src/cli/export-command.ts +22 -10
  15. package/src/cli/help.ts +1 -1
  16. package/src/cli/index.ts +34 -7
  17. package/src/cli/integrations.ts +34 -1
  18. package/src/cli/provider.ts +27 -15
  19. package/src/cli/registry.ts +2 -2
  20. package/src/cli/version-skew.ts +36 -3
  21. package/src/clients/aside-profiles.ts +8 -7
  22. package/src/clients/config-export/contracts.ts +2 -1
  23. package/src/clients/config-export/raycast.ts +106 -0
  24. package/src/clients/config-export.ts +36 -0
  25. package/src/clients/model-presentation.ts +61 -0
  26. package/src/codex/catalog/sync.ts +44 -2
  27. package/src/codex/convergence.ts +7 -0
  28. package/src/generated/compatibility-version.json +59 -43
  29. package/src/generated/model-metadata.ts +1 -0
  30. package/src/images/loop.ts +10 -2
  31. package/src/integrations/catalog-refresh.ts +1 -1
  32. package/src/integrations/merge.ts +158 -25
  33. package/src/integrations/raycast-detect.ts +111 -0
  34. package/src/integrations/registry.ts +18 -0
  35. package/src/integrations/state.ts +82 -13
  36. package/src/integrations/writer.ts +46 -31
  37. package/src/lib/bounded-body.ts +22 -7
  38. package/src/oauth/anthropic-routing.ts +59 -14
  39. package/src/oauth/health.ts +3 -0
  40. package/src/providers/quota.ts +145 -9
  41. package/src/providers/registry.ts +31 -0
  42. package/src/responses/parser.ts +16 -3
  43. package/src/responses/reasoning-envelope.ts +3 -2
  44. package/src/server/grok-responses-control-frame.ts +43 -0
  45. package/src/server/management/config-routes.ts +6 -2
  46. package/src/server/management/integration-routes.ts +29 -2
  47. package/src/server/management/model-routes.ts +9 -1
  48. package/src/server/request-decompress.ts +34 -8
  49. package/src/server/responses/agent-task-recovery-cache.ts +43 -14
  50. package/src/server/responses/agent-task-recovery.ts +28 -16
  51. package/src/server/responses/core.ts +39 -6
  52. package/src/web-search/loop.ts +10 -2
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-CCfD72yq.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-J96sug5C.css">
19
+ <script type="module" crossorigin src="/assets/index-NcAVXkST.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-BFgUC17B.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 228 228" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M57 147.207V171L-0.0518799 113.948L11.9031 102.084L57 147.207ZM80.7932 171H57L114.052 228.052L125.955 216.149L80.7932 171ZM216.11 125.89L228 114L114 0L102.11 11.8901L147.155 57H119.926L88.4822 25.5826L76.5792 37.4727L96.1583 57.0519H82.5307V145.521H171V131.907L190.579 151.486L202.469 139.583L171 108.048V80.8192L216.11 125.89ZM62.9515 51.0485L51.0614 62.9516L63.8203 75.7104L75.7104 63.8073L62.9515 51.0485ZM164.193 152.29L152.341 164.18L165.1 176.939L177.003 165.048L164.193 152.29ZM37.4208 76.5792L25.5307 88.4693L57 119.952V96.1454L37.4208 76.5792ZM131.842 171H108.048L139.531 202.469L151.421 190.579L131.842 171Z" fill="#FF6363"/>
3
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.45.0",
3
+ "version": "2.46.0-preview.20260907",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -1118,9 +1118,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1118
1118
  break;
1119
1119
  }
1120
1120
  case "content_block_start": {
1121
- const block = data.content_block as { type: string; id?: string; name?: string; data?: string } | undefined;
1121
+ const block = data.content_block as { type: string; id?: string; name?: string; data?: string; thinking?: string } | undefined;
1122
1122
  if (!block) break;
1123
1123
  currentBlockType = block.type;
1124
+ if (block.type === "thinking") {
1125
+ // Preserve even a display:omitted block boundary. The bridge can then
1126
+ // distinguish consecutive empty signed blocks from signature updates.
1127
+ yield { type: "thinking_delta", thinking: typeof block.thinking === "string" ? block.thinking : "" };
1128
+ }
1124
1129
  if (block.type === "tool_use") {
1125
1130
  currentToolCallId = usableToolUseId(block.id);
1126
1131
  currentToolCallName = toolNames.fromWire(block.name ?? "");
@@ -1151,8 +1156,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1151
1156
  // later text blocks independent.
1152
1157
  yield { type: "thinking_delta", thinking: delta.reasoning };
1153
1158
  } else if (delta.type === "signature_delta" && typeof delta.signature === "string" && (currentBlockType === "thinking" || currentBlockType === "reasoning")) {
1154
- // Arrives once, just before the thinking block's content_block_stop; block-scoped
1155
- // so a stray signature on a non-thinking block can never be captured.
1159
+ // Anthropic SDKs replace the signature with this value. Forward updates
1160
+ // within the block; the bridge closes on the next semantic boundary.
1156
1161
  yield { type: "thinking_signature", signature: delta.signature };
1157
1162
  } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && currentBlockType === "tool_use") {
1158
1163
  // Forwarded immediately: the bridge maps each delta to a client-visible
@@ -630,6 +630,13 @@ function mapRoutedResponsesReasoningEffort(
630
630
  if (provider.authMode === "forward") return body;
631
631
  if (configuredReasoningEfforts(provider, modelId) === undefined) return body;
632
632
  if (!isPlainObject(body) || !isPlainObject(body.reasoning)) return body;
633
+ const declaredEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
634
+ // An explicitly empty ladder means no effort control, not no reasoning output.
635
+ // Omit only effort so the upstream default applies; unknown/non-rankable ladders stay untouched.
636
+ if (declaredEfforts?.length === 0 && Object.hasOwn(body.reasoning, "effort")) {
637
+ const { effort: _effort, ...reasoning } = body.reasoning;
638
+ return { ...body, reasoning: Object.keys(reasoning).length > 0 ? reasoning : undefined };
639
+ }
633
640
  const requested = body.reasoning.effort;
634
641
  if (typeof requested !== "string") return body;
635
642
 
package/src/bridge.ts CHANGED
@@ -945,6 +945,13 @@ export function bridgeToResponsesSSE(
945
945
  }
946
946
  if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
947
947
  }
948
+ // Anthropic signature_delta supplies the latest signature, not an append-only
949
+ // fragment (anthropic-sdk-typescript MessageStream). Keep consecutive updates
950
+ // together; the next semantic event belongs to the following block.
951
+ if (pendingSignature !== undefined && event.type !== "thinking_signature" && event.type !== "heartbeat") {
952
+ if (currentReasoning) closeCurrentReasoning();
953
+ else flushHiddenReasoningEnvelope();
954
+ }
948
955
  switch (event.type) {
949
956
  case "assistant_boundary": {
950
957
  // A guarded continuation starts a fresh assistant output item while keeping the
@@ -1054,15 +1061,21 @@ export function bridgeToResponsesSSE(
1054
1061
  case "thinking_signature": {
1055
1062
  pendingSignatureBytes = replaceRetainedString(pendingSignatureBytes, event.signature, "reasoning");
1056
1063
  pendingSignature = event.signature;
1057
- // Signature arrives at the end of the thinking block. With a visible reasoning item
1058
- // open, closeCurrentReasoning attaches the envelope; hidden/suppressed blocks flush
1059
- // an envelope-only reasoning item now.
1060
- if (!currentReasoning) flushHiddenReasoningEnvelope();
1064
+ // Delay closing until the next semantic event so a signature update cannot
1065
+ // create another block or become attached to the following thinking text.
1061
1066
  break;
1062
1067
  }
1063
1068
  case "redacted_thinking": {
1069
+ if (currentMsg) closeCurrentMessage("commentary");
1070
+ if (currentReasoning) closeCurrentReasoning();
1071
+ if (currentRawReasoning) closeCurrentRawReasoning();
1072
+ flushHiddenRawReasoning();
1073
+ if (currentToolCall) closeCurrentToolCall();
1064
1074
  budget?.chargeRetained(bytesOf(event.data), { kind: "reasoning" });
1065
1075
  pendingRedacted.push(event.data);
1076
+ // A redacted block is complete at content_block_start. Emit it here,
1077
+ // not with a later thinking block or after a tool call at turn end.
1078
+ flushHiddenReasoningEnvelope();
1066
1079
  break;
1067
1080
  }
1068
1081
  case "kiro_redacted_reasoning": {
@@ -1816,6 +1829,9 @@ function buildResponseJSONWithBudget(
1816
1829
  if (budget) releaseTranslatedEvent(e, budget);
1817
1830
  continue;
1818
1831
  }
1832
+ if (batchSignature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") {
1833
+ flushSummaryReasoning();
1834
+ }
1819
1835
  switch (e.type) {
1820
1836
  case "assistant_boundary":
1821
1837
  flushText("commentary");
@@ -1860,19 +1876,23 @@ function buildResponseJSONWithBudget(
1860
1876
  }
1861
1877
  break;
1862
1878
  case "thinking_signature":
1863
- // End of the current thinking block flush it WITH the signature envelope so the
1864
- // block/signature pairing survives multi-block turns.
1879
+ // Like streaming, retain the latest signature update until the next semantic
1880
+ // event. Flushing every update would manufacture signature-only siblings.
1865
1881
  batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
1866
1882
  batchSignature = e.signature;
1867
- flushSummaryReasoning();
1868
1883
  break;
1869
1884
  case "redacted_thinking":
1885
+ flushText("commentary");
1886
+ flushSummaryReasoning();
1887
+ flushRawReasoning();
1888
+ flushToolCall();
1870
1889
  {
1871
1890
  const dataBytes = bytesOf(e.data);
1872
1891
  budget?.chargeRetained(dataBytes, { kind: "reasoning" });
1873
1892
  batchRedactedBytes += dataBytes;
1874
1893
  }
1875
1894
  batchRedacted.push(e.data);
1895
+ flushSummaryReasoning();
1876
1896
  break;
1877
1897
  case "kiro_redacted_reasoning":
1878
1898
  // Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
@@ -4,8 +4,8 @@
4
4
  * Design (devlog/260711_claude_inbound/010, 003_evidence.md):
5
5
  * - translate-and-replay: the produced body MUST pass the real responsesRequestSchema
6
6
  * parse so routing/OAuth/pool/failover are inherited unchanged.
7
- * - thinking/redacted_thinking blocks on replay are DROPPED (v1 policy) routed
8
- * providers carry reasoning in Responses items/ocxr1 envelopes instead.
7
+ * - thinking/redacted_thinking replay is preserved in Responses reasoning items;
8
+ * signatures and redacted payloads travel in bounded ocxr1 envelopes.
9
9
  * - thinking.budget_tokens is NEVER forwarded raw; it maps to an effort tier.
10
10
  * - top_k is accepted and silently dropped (no Responses equivalent, CCR parity).
11
11
  */
@@ -17,6 +17,7 @@ export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, e
17
17
  import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
18
18
  import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
19
19
  import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
20
+ import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
20
21
 
21
22
 
22
23
 
@@ -234,9 +235,26 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
234
235
  input.push({ type: "function_call", call_id: raw.id, name: raw.name, arguments: JSON.stringify(raw.input ?? {}) });
235
236
  break;
236
237
  }
237
- case "thinking":
238
- case "redacted_thinking":
239
- break; // v1 policy: dropped on replay (003 evidence safe for routed providers)
238
+ case "thinking": {
239
+ flush();
240
+ const thinking = typeof raw.thinking === "string" ? raw.thinking : "";
241
+ const signature = typeof raw.signature === "string" ? raw.signature : "";
242
+ if (signature.startsWith(OCX_REASONING_PREFIX)) {
243
+ const owned = decodeReasoningEnvelope(signature);
244
+ if (!owned) throw new AnthropicRequestError("malformed ocxr1 reasoning signature");
245
+ if (Object.hasOwn(owned, "sig")) throw new AnthropicRequestError("OpenCodex reasoning continuity cannot be replayed as an Anthropic signature");
246
+ }
247
+ const encrypted = signature.length === 0 ? undefined : signature.startsWith(OCX_REASONING_PREFIX) ? signature : encodeReasoningEnvelope({ sig: signature });
248
+ if (thinking.length === 0 && !encrypted) break;
249
+ input.push({ type: "reasoning", id: `rs_${crypto.randomUUID().replace(/-/g, "")}`, summary: thinking.length > 0 ? [{ type: "summary_text", text: thinking }] : [], ...(encrypted ? { encrypted_content: encrypted } : {}) });
250
+ break;
251
+ }
252
+ case "redacted_thinking": {
253
+ flush();
254
+ const data = typeof raw.data === "string" ? raw.data : "";
255
+ if (data.length > 0) input.push({ type: "reasoning", id: `rs_${crypto.randomUUID().replace(/-/g, "")}`, summary: [], encrypted_content: encodeReasoningEnvelope({ red: [data] }) });
256
+ break;
257
+ }
240
258
  default:
241
259
  break;
242
260
  }
@@ -5,8 +5,8 @@
5
5
  * - Transport-only `ping` events may appear at any point, including before
6
6
  * message_start. Semantic framing stays message_start ->
7
7
  * (content_block_start -> deltas -> content_block_stop)* -> message_delta -> message_stop.
8
- * - thinking blocks get thinking_delta(s) then ONE synthetic signature_delta just
9
- * before content_block_stop (CCR precedent: Claude Code does not verify signatures).
8
+ * - thinking blocks get thinking_delta(s), then one signature_delta containing the
9
+ * genuine replay signature or a bounded ocxr1 fallback envelope.
10
10
  * - message_delta.usage is cumulative; message_start embeds a full message snapshot.
11
11
  * - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
12
12
  */
@@ -20,6 +20,7 @@ import {
20
20
  type TranslatorBudget,
21
21
  } from "../lib/translator-budget";
22
22
  import { sseFieldOffset, sseFieldValue } from "../lib/sse-decoder";
23
+ import { decodeReasoningEnvelope, encodeReasoningEnvelope } from "../responses/reasoning-envelope";
23
24
 
24
25
  type Rec = Record<string, unknown>;
25
26
 
@@ -214,6 +215,9 @@ interface OpenBlock {
214
215
  callId?: string;
215
216
  /** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
216
217
  reasoningPartKey?: string;
218
+ thinkingBuf?: string;
219
+ thinkingBufBytes?: number;
220
+ reasoningSig?: string;
217
221
  }
218
222
 
219
223
  /** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
@@ -230,6 +234,9 @@ export function responsesSseToAnthropicSse(
230
234
  let bufferBytes = 0;
231
235
  let started = false;
232
236
  let terminated = false;
237
+ // Starting termination can still throw while closing a block or emitting its
238
+ // terminal frame. Only a delivered terminal forbids the bounded overflow error.
239
+ let terminalDelivered = false;
233
240
  let cancelled = false;
234
241
  let blockIndex = 0;
235
242
  let open: OpenBlock | null = null;
@@ -253,6 +260,11 @@ export function responsesSseToAnthropicSse(
253
260
  const bytes = queuedLiveFrameBytes.shift();
254
261
  if (bytes !== undefined) translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
255
262
  };
263
+ const releaseThinkingBuffer = (block: OpenBlock | null | undefined) => {
264
+ if (block?.kind !== "thinking") return;
265
+ translatorBudget.releaseRetained(block.thinkingBufBytes ?? 0, { kind: "reasoning" });
266
+ block.thinkingBufBytes = 0;
267
+ };
256
268
 
257
269
  return new ReadableStream<Uint8Array>({
258
270
  start(controller) {
@@ -296,13 +308,14 @@ export function responsesSseToAnthropicSse(
296
308
  open.webSearchArgsEmitted = true;
297
309
  }
298
310
  if (open.kind === "thinking") {
299
- // Synthetic signature: Claude Code accepts it (003 E6); inbound drops replays anyway.
311
+ const signature = open.reasoningSig ?? encodeReasoningEnvelope({ txt: open.thinkingBuf ?? "" });
300
312
  emit("content_block_delta", {
301
313
  type: "content_block_delta", index: open.index,
302
- delta: { type: "signature_delta", signature: `ocx${Date.now()}` },
314
+ delta: { type: "signature_delta", signature },
303
315
  });
304
316
  }
305
317
  emit("content_block_stop", { type: "content_block_stop", index: open.index });
318
+ releaseThinkingBuffer(open);
306
319
  if (open.callId) translatorBudget.closeCall(open.callId);
307
320
  open = null;
308
321
  };
@@ -315,7 +328,7 @@ export function responsesSseToAnthropicSse(
315
328
  ? { type: "text", text: "" }
316
329
  : { type: "thinking", thinking: "", signature: "" };
317
330
  emit("content_block_start", { type: "content_block_start", index, content_block: contentBlock });
318
- open = { kind, index };
331
+ open = { kind, index, thinkingBuf: "", thinkingBufBytes: 0 };
319
332
  };
320
333
  const finish = (stopReason: string, usage: unknown) => {
321
334
  if (terminated) return;
@@ -328,6 +341,7 @@ export function responsesSseToAnthropicSse(
328
341
  usage: anthropicUsage(usage, webSearchRequests),
329
342
  });
330
343
  emit("message_stop", { type: "message_stop" });
344
+ terminalDelivered = true;
331
345
  };
332
346
  // upstreamDerived: transient upstream statuses become overloaded_error so the
333
347
  // Anthropic-SDK client retries with backoff; proxy-internal exceptions stay
@@ -336,11 +350,15 @@ export function responsesSseToAnthropicSse(
336
350
  // resets reach the reader catch (no failed-tail relay) and stay api_error —
337
351
  // same as today, deliberate residual.
338
352
  const fail = (status: number, message: string, upstreamDerived = false, code?: string) => {
339
- if (terminated) return;
353
+ // finish/fail sets terminated before closeOpenBlock. A closure-time
354
+ // allocation failure must still emit one error, without retrying closure.
355
+ if (terminated && (code !== "translation_buffer_limit" || terminalDelivered)) return;
340
356
  terminated = true;
341
357
  if (code === "translation_buffer_limit") {
358
+ releaseThinkingBuffer(open);
342
359
  if (open?.callId) translatorBudget.closeCall(open.callId);
343
360
  open = null;
361
+ terminalDelivered = true;
344
362
  // No normal close frames are valid after overflow. Emit exactly one bounded
345
363
  // typed terminal without consulting the exhausted budget.
346
364
  controller.enqueue(encoder.encode(sseFrame("error", anthropicErrorBody(
@@ -357,10 +375,12 @@ export function responsesSseToAnthropicSse(
357
375
  // Do not manufacture message_start before the terminal error. Earlier transport-only
358
376
  // pings remain valid and do not turn the failure into a partial message.
359
377
  emit("error", anthropicErrorBody(status, message, type, code));
378
+ terminalDelivered = true;
360
379
  return;
361
380
  }
362
381
  closeOpenBlock();
363
382
  emit("error", anthropicErrorBody(status, message, type, code));
383
+ terminalDelivered = true;
364
384
  };
365
385
 
366
386
  const handleFrame = (eventName: string, data: Rec) => {
@@ -374,8 +394,10 @@ export function responsesSseToAnthropicSse(
374
394
  case "response.output_text.delta": {
375
395
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
376
396
  ensureBlock("text");
397
+ const active = open;
398
+ if (!active || active.kind !== "text") break;
377
399
  emit("content_block_delta", {
378
- type: "content_block_delta", index: open!.index,
400
+ type: "content_block_delta", index: active.index,
379
401
  delta: { type: "text_delta", text: data.delta },
380
402
  });
381
403
  break;
@@ -384,6 +406,8 @@ export function responsesSseToAnthropicSse(
384
406
  case "response.reasoning_text.delta": {
385
407
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
386
408
  ensureBlock("thinking");
409
+ const active = open;
410
+ if (!active || active.kind !== "thinking") break;
387
411
  // The JSON path joins reasoning summary/content parts with "\n\n"
388
412
  // (responsesJsonToAnthropicMessage); mirror that at part and item boundaries
389
413
  // so multi-part summaries do not glue into one run-on paragraph. Frames
@@ -395,15 +419,32 @@ export function responsesSseToAnthropicSse(
395
419
  // components while retaining item and part equality, rather than dropping item_id and
396
420
  // accidentally joining distinct malformed reasoning items.
397
421
  const partKey = `${boundedReasoningIdentity(data.item_id)}:${slot}`;
398
- if (open!.reasoningPartKey !== undefined && open!.reasoningPartKey !== partKey) {
422
+ const needsPartSeparator = active.reasoningPartKey !== undefined
423
+ && active.reasoningPartKey !== partKey;
424
+ const appended = `${needsPartSeparator ? "\n\n" : ""}${data.delta}`;
425
+ const previous = active.thinkingBuf ?? "";
426
+ const previousBytes = active.thinkingBufBytes ?? 0;
427
+ const nextBytes = appendedUtf8Bytes(previous, previousBytes, appended);
428
+ const scope = { kind: "reasoning" } as const;
429
+ const reservation = translatorBudget.reserveTransient(nextBytes, scope);
430
+ try {
431
+ active.thinkingBuf = previous + appended;
432
+ active.thinkingBufBytes = nextBytes;
433
+ reservation.commitRetained();
434
+ translatorBudget.releaseRetained(previousBytes, scope);
435
+ } catch (error) {
436
+ reservation.release();
437
+ throw error;
438
+ }
439
+ if (needsPartSeparator) {
399
440
  emit("content_block_delta", {
400
- type: "content_block_delta", index: open!.index,
441
+ type: "content_block_delta", index: active.index,
401
442
  delta: { type: "thinking_delta", thinking: "\n\n" },
402
443
  });
403
444
  }
404
- open!.reasoningPartKey = partKey;
445
+ active.reasoningPartKey = partKey;
405
446
  emit("content_block_delta", {
406
- type: "content_block_delta", index: open!.index,
447
+ type: "content_block_delta", index: active.index,
407
448
  delta: { type: "thinking_delta", thinking: data.delta },
408
449
  });
409
450
  break;
@@ -499,10 +540,9 @@ export function responsesSseToAnthropicSse(
499
540
  if (pair.completed) webSearchRequests++;
500
541
  break;
501
542
  }
502
- if (!open) break;
503
543
  // Close the matching open block (message/reasoning items close implicitly on
504
544
  // the next block; function_call items must close here so tool input parses).
505
- if (open.kind === "tool_use" && item.type === "function_call") {
545
+ if (open && open.kind === "tool_use" && item.type === "function_call") {
506
546
  if (open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
507
547
  const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0
508
548
  ? item.arguments
@@ -518,8 +558,26 @@ export function responsesSseToAnthropicSse(
518
558
  }
519
559
  closeOpenBlock();
520
560
  }
521
- else if (open.kind === "text" && item.type === "message") closeOpenBlock();
522
- else if (open.kind === "thinking" && item.type === "reasoning") closeOpenBlock();
561
+ else if (open && open.kind === "text" && item.type === "message") closeOpenBlock();
562
+ else if (item.type === "reasoning") {
563
+ const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : "";
564
+ const env = encrypted ? decodeReasoningEnvelope(encrypted) : null;
565
+ const red = env?.red ?? [];
566
+ if (env?.sig && open?.kind !== "thinking") ensureBlock("thinking");
567
+ if (open?.kind === "thinking") {
568
+ if (env?.sig) open.reasoningSig = env.sig;
569
+ closeOpenBlock();
570
+ }
571
+ if (red.length > 0) {
572
+ ensureStarted();
573
+ closeOpenBlock();
574
+ }
575
+ for (const data of red) {
576
+ const idx = blockIndex++;
577
+ emit("content_block_start", { type: "content_block_start", index: idx, content_block: { type: "redacted_thinking", data } });
578
+ emit("content_block_stop", { type: "content_block_stop", index: idx });
579
+ }
580
+ }
523
581
  break;
524
582
  }
525
583
  case "response.completed": {
@@ -704,6 +762,7 @@ export function responsesSseToAnthropicSse(
704
762
  fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
705
763
  } else fail(500, err instanceof Error ? err.message : String(err));
706
764
  } finally {
765
+ releaseThinkingBuffer(open);
707
766
  translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
708
767
  if (pingTimer !== undefined) clearInterval(pingTimer);
709
768
  reader.releaseLock();
@@ -717,6 +776,7 @@ export function responsesSseToAnthropicSse(
717
776
  cancel(reason) {
718
777
  cancelled = true;
719
778
  while (queuedLiveFrameBytes.length > 0) releaseDeliveredFrame();
779
+ releaseThinkingBuffer(open);
720
780
  if (open?.callId) translatorBudget.closeCall(open.callId);
721
781
  if (pingTimer !== undefined) clearInterval(pingTimer);
722
782
  return reader?.cancel(reason);
@@ -756,8 +816,15 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string): R
756
816
  if (isRec(s) && typeof s.text === "string" && s.text.length > 0) parts.push(s.text);
757
817
  }
758
818
  }
759
- if (parts.length > 0) {
760
- content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: `ocx${Date.now()}` });
819
+ const encrypted = typeof raw.encrypted_content === "string" ? raw.encrypted_content : "";
820
+ const env = encrypted ? decodeReasoningEnvelope(encrypted) : null;
821
+ // Legacy combined envelopes place redacted blocks before the signed block,
822
+ // matching the Anthropic adapter. New bridge output uses separate items.
823
+ for (const data of env?.red ?? []) content.push({ type: "redacted_thinking", data });
824
+ // env.txt may be locally hidden text. Do not expose it here or manufacture
825
+ // a new signed continuity carrier; hidden-summary replay remains limited.
826
+ if (parts.length > 0 || env?.sig) {
827
+ content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: env?.sig ?? encodeReasoningEnvelope({ txt: parts.join("\n\n") }) });
761
828
  }
762
829
  break;
763
830
  }
@@ -923,9 +990,10 @@ export async function collectAnthropicMessage(
923
990
  } finally {
924
991
  reader.releaseLock();
925
992
  }
926
- closeBlock();
927
-
993
+ // Error is authoritative. In particular, do not allocate another copy of an
994
+ // unfinished thinking block after the translator reported closure overflow.
928
995
  if (error) return error;
996
+ closeBlock();
929
997
  return {
930
998
  id: `msg_${uuid()}`,
931
999
  type: "message",
@@ -148,7 +148,10 @@ export const CAPABILITIES: readonly Capability[] = [
148
148
  summary: "Configured providers with connectivity and selected models.",
149
149
  // Local config + PROVIDER_REGISTRY. Does not call GET /api/providers.
150
150
  routes: [],
151
- flags: [{ name: "--json", value: "boolean", summary: "Emit the provider list as JSON." }],
151
+ flags: [
152
+ { name: "--json", value: "boolean", summary: "Emit the provider list as JSON." },
153
+ { name: "--jsonl", value: "boolean", summary: "Emit one configured provider per JSON line." },
154
+ ],
152
155
  mutates: false,
153
156
  json: "envelope",
154
157
  details: ["Reads local config; drives no management API route."],
@@ -403,7 +403,7 @@ const commandRunners: Record<string, CommandRunner> = {
403
403
  },
404
404
  config,
405
405
  port: live.port,
406
- }, ["mcode", "pi"]));
406
+ }, ["mcode", "pi", "raycast"]));
407
407
  } catch (error) {
408
408
  console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
409
409
  }
package/src/cli/doctor.ts CHANGED
@@ -1157,11 +1157,11 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1157
1157
  // No extra probe -- findLiveProxy already carried the version back.
1158
1158
  {
1159
1159
  const { packageVersion } = await import("./help");
1160
- const { computeVersionSkew } = await import("./version-skew");
1160
+ const { computeVersionSkew, isConfirmedVersionMatch } = await import("./version-skew");
1161
1161
  const skew = computeVersionSkew(packageVersion(), live?.version);
1162
1162
  if (skew.skewed && skew.warning) {
1163
1163
  console.log(`!! ${skew.warning}`);
1164
- } else if (skew.proxyVersion !== null) {
1164
+ } else if (isConfirmedVersionMatch(skew)) {
1165
1165
  console.log(`ok ocx ${skew.cliVersion} matches the running proxy`);
1166
1166
  }
1167
1167
  }
@@ -172,17 +172,29 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
172
172
 
173
173
  const spec = EXPORT_CLIENTS[client];
174
174
  const root = await runtimeBaseUrl(deps);
175
- const rows = await runtimeRequest<ExportProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
176
- if (!Array.isArray(rows)) {
177
- throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
175
+ let built: { document: unknown; text: string };
176
+ if (client === "raycast") {
177
+ // The dial address alone cannot distinguish a wildcard authenticated bind
178
+ // from loopback. Let the live server resolve its admission/listener policy;
179
+ // saved config can differ from the process serving this request.
180
+ const exported = await runtimeRequest<{
181
+ client: string; format: string; config: unknown; text: string;
182
+ }>("/api/client-config?client=raycast", {}, { ...deps, baseUrl: root });
183
+ if (!exported || exported.client !== "raycast" || exported.format !== "yaml"
184
+ || typeof exported.text !== "string" || exported.config === undefined) {
185
+ throw new RuntimeApiError("Management API returned an unexpected Raycast export payload.", 502, null);
186
+ }
187
+ built = { document: exported.config, text: exported.text };
188
+ } else {
189
+ const rows = await runtimeRequest<ExportProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
190
+ if (!Array.isArray(rows)) {
191
+ throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
192
+ }
193
+ // Discovery can persist selection; preserve the existing exporters' flow.
194
+ const config = (deps.configImpl ?? loadConfig)();
195
+ const models = exportModelsFromProxyRows(rows, config);
196
+ built = buildClientConfigText(client, { baseUrl: proxyV1BaseUrl(root), models, config });
178
197
  }
179
- // Discovery can persist pending -> ready selection. Read from the caller's
180
- // config source after the response, rather than filtering with a stale snapshot.
181
- const config = (deps.configImpl ?? loadConfig)();
182
- const models = exportModelsFromProxyRows(rows, config);
183
- // The text is the client's OWN format — YAML, TOML and JSON5 clients would
184
- // otherwise receive a JSON rendering their parser reads differently.
185
- const built = buildClientConfigText(client, { baseUrl: proxyV1BaseUrl(root), models, config });
186
198
  const clientConfig = built.document;
187
199
  const text = built.text;
188
200
 
package/src/cli/help.ts CHANGED
@@ -77,7 +77,7 @@ Usage:
77
77
  ocx memory [--json] Alias of ocx observe memory
78
78
  ocx api-key <sub> Alias of ocx access key
79
79
  ocx access <sub> External API keys and endpoint information
80
- ocx export --client <id> Print a client config wired to the running proxy (12 clients)
80
+ ocx export --client <id> Print a client config wired to the running proxy (13 clients)
81
81
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
82
82
  ocx grok <sub> Grok Build model selection and apply
83
83
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
package/src/cli/index.ts CHANGED
@@ -92,6 +92,15 @@ import {
92
92
  grokSyncFailureMessage,
93
93
  reconcileEnsureDesiredIntegrations,
94
94
  } from "./ensure-desired-integrations";
95
+ import { refreshOwnedCatalogIntegrations } from "../integrations/catalog-refresh";
96
+ import { loadExportModels } from "../server/management/model-rows";
97
+
98
+ import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
99
+ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
100
+ import { selfLaunchArgv } from "../lib/self-launch-argv";
101
+ import { initializeNodeLauncherContext } from "./launcher-context";
102
+ import { createLocalAttestationSecret } from "../lib/local-management-attestation";
103
+ import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
95
104
 
96
105
  /**
97
106
  * A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
@@ -105,13 +114,25 @@ function reportShellHookFailure(result: { state: "installed" | "absent" | "faile
105
114
  console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
106
115
  }
107
116
 
108
-
109
- import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
110
- import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
111
- import { selfLaunchArgv } from "../lib/self-launch-argv";
112
- import { initializeNodeLauncherContext } from "./launcher-context";
113
- import { createLocalAttestationSecret } from "../lib/local-management-attestation";
114
- import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
117
+ async function refreshOwnedRaycastCatalog(
118
+ config: ReturnType<typeof loadConfig>,
119
+ port: number,
120
+ ): Promise<void> {
121
+ try {
122
+ const outcomes = await refreshOwnedCatalogIntegrations({
123
+ models: () => loadExportModels(config),
124
+ config,
125
+ port,
126
+ }, ["raycast"]);
127
+ for (const outcome of outcomes) {
128
+ if (!outcome.ok) {
129
+ console.error(`⚠️ Raycast integration was not refreshed: ${outcome.reason}`);
130
+ }
131
+ }
132
+ } catch (error) {
133
+ console.error(`⚠️ Raycast integration was not refreshed: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ }
115
136
 
116
137
  initializeNodeLauncherContext();
117
138
 
@@ -493,6 +514,7 @@ async function handleStart(options: { block?: boolean } = {}) {
493
514
  },
494
515
  );
495
516
  if (!startupSync.ran) console.log(" Codex integration OFF; startup left Codex native.");
517
+ await refreshOwnedRaycastCatalog(config, port);
496
518
  // #1046: one warning per startup, after BOTH writes. The server's cache
497
519
  // invalidation happens first and the catalog sync second, so the mtime is only
498
520
  // final here — and neither write site warns on its own, or a boot that hits
@@ -558,6 +580,9 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
558
580
  return null;
559
581
  });
560
582
  if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
583
+ // Do not refresh Raycast from saved config here: live bind/admission and
584
+ // secondary-listener settings may differ. Explicit sync or server startup
585
+ // owns catalog refresh; ensure must not overwrite a working destination.
561
586
  // Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
562
587
  const systemEnv = await injectSystemEnv(live.port, config).catch(() => ({ injected: false }));
563
588
  reportShellHookFailure(reconcileShellHook(systemEnv.injected));
@@ -602,6 +627,8 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
602
627
  return null;
603
628
  });
604
629
  if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
630
+ // The child performs Raycast refresh with its actual startup config. The
631
+ // parent's pre-spawn snapshot is not authoritative for a client-file write.
605
632
  // The child opens /healthz before its best-effort roster reconcile. Await the same idempotent
606
633
  // operation in the parent so `ocx ensure` cannot report success while stale ocx-*.md files are
607
634
  // still observable. Always use the live port, including fallback-port starts.