@bitkyc08/opencodex 2.46.0-preview.20260907 → 2.47.0-preview.20260908

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 (65) hide show
  1. package/README.md +22 -0
  2. package/SPONSORS.md +105 -0
  3. package/gui/dist/assets/index-B8ZZ0ivI.css +1 -0
  4. package/gui/dist/assets/{index-NcAVXkST.js → index-CNTWhC5F.js} +21 -21
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +2 -1
  7. package/src/adapters/cursor/protobuf-request.ts +34 -21
  8. package/src/adapters/cursor/tool-guidance.ts +2 -2
  9. package/src/adapters/cursor/tool-result-normalize.ts +22 -2
  10. package/src/adapters/exec-tool-result-normalize.ts +87 -0
  11. package/src/adapters/kiro.ts +29 -18
  12. package/src/adapters/responses-code-mode.ts +14 -4
  13. package/src/adapters/tool-catalog-nudge.ts +2 -2
  14. package/src/bridge.ts +40 -24
  15. package/src/claude/inbound.ts +21 -6
  16. package/src/claude/outbound.ts +43 -26
  17. package/src/cli/capabilities.ts +27 -0
  18. package/src/cli/integrations.ts +1 -1
  19. package/src/cli/models-runtime-subcommands.ts +2 -0
  20. package/src/cli/models-runtime.ts +108 -1
  21. package/src/cli/observe.ts +18 -3
  22. package/src/cli/usage-report.ts +6 -1
  23. package/src/clients/config-export.ts +5 -3
  24. package/src/codex/auth-api.ts +36 -0
  25. package/src/codex/catalog/provider-fetch.ts +2 -1
  26. package/src/codex/quota-auto-refresh-state.ts +8 -0
  27. package/src/codex/quota-auto-refresh.ts +157 -27
  28. package/src/codex/warmup.ts +5 -0
  29. package/src/config/provider-validation.ts +70 -1
  30. package/src/config.ts +73 -1
  31. package/src/generated/compatibility-version.json +67 -55
  32. package/src/lib/json-byte-size.ts +61 -0
  33. package/src/lib/provider-outbound.ts +11 -10
  34. package/src/oauth/index.ts +42 -8
  35. package/src/oauth/orcarouter.ts +200 -0
  36. package/src/providers/key-store.ts +22 -0
  37. package/src/providers/registry.ts +78 -29
  38. package/src/responses/citation-markers.ts +56 -24
  39. package/src/responses/reasoning-envelope.ts +53 -19
  40. package/src/server/auth-cors.ts +8 -0
  41. package/src/server/chat-completions.ts +7 -0
  42. package/src/server/chat-native.ts +65 -3
  43. package/src/server/claude-messages.ts +30 -10
  44. package/src/server/effort-policy.ts +183 -1
  45. package/src/server/management/agent-settings-routes.ts +50 -9
  46. package/src/server/management/config-routes.ts +2 -0
  47. package/src/server/management/logs-usage-routes.ts +11 -3
  48. package/src/server/management/model-routes.ts +65 -1
  49. package/src/server/management/model-rows.ts +5 -0
  50. package/src/server/management/provider-routes.ts +99 -6
  51. package/src/server/management/route-registry.ts +2 -0
  52. package/src/server/management/usage-aggregate-cache.ts +14 -7
  53. package/src/server/responses/compact.ts +3 -2
  54. package/src/server/responses/core.ts +16 -2
  55. package/src/server/startup-health-cache.ts +31 -9
  56. package/src/types/config.ts +5 -0
  57. package/src/types/provider.ts +4 -0
  58. package/src/usage/cost.ts +27 -31
  59. package/src/usage/summary.ts +52 -9
  60. package/src/usage/time-range.ts +48 -0
  61. package/src/usage/user-cost-overlays.ts +35 -3
  62. package/src/vision/anthropic-describe.ts +46 -2
  63. package/src/web-search/anthropic-executor.ts +49 -3
  64. package/src/web-search/parse.ts +1 -1
  65. package/gui/dist/assets/index-BFgUC17B.css +0 -1
package/src/bridge.ts CHANGED
@@ -493,7 +493,7 @@ export function bridgeToResponsesSSE(
493
493
  const previousBytes = pendingSignatureBytes
494
494
  + pendingRedacted.reduce((sum, value) => sum + bytesOf(value), 0)
495
495
  + (hiddenText ? hiddenThinkingBytes : 0);
496
- const encoded = encodeReasoningEnvelope(envelope);
496
+ const encoded = encodeReasoningEnvelope(envelope, budget);
497
497
  const reservation = budget?.reserveTransient(bytesOf(encoded), { kind: "reasoning" });
498
498
  pendingSignature = undefined;
499
499
  pendingSignatureBytes = 0;
@@ -533,7 +533,7 @@ export function bridgeToResponsesSSE(
533
533
  if (!hiddenRawReasoningText) return;
534
534
  rawReasoningForNextToolCall = hiddenRawReasoningText;
535
535
  const previousBytes = hiddenRawReasoningBytes;
536
- const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
536
+ const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText }, budget);
537
537
  const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
538
538
  hiddenRawReasoningText = "";
539
539
  hiddenRawReasoningBytes = 0;
@@ -556,7 +556,7 @@ export function bridgeToResponsesSSE(
556
556
  const flushKiroRedactedReasoning = () => {
557
557
  if (!pendingKiroRedacted) return;
558
558
  const previousBytes = pendingKiroRedactedBytes;
559
- const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted });
559
+ const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted }, budget);
560
560
  const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
561
561
  pendingKiroRedacted = undefined;
562
562
  pendingKiroRedactedBytes = 0;
@@ -902,6 +902,16 @@ export function bridgeToResponsesSSE(
902
902
  gated = true;
903
903
  stepping = false;
904
904
  };
905
+ const attemptTerminationCleanup = (action: () => void): boolean => {
906
+ try {
907
+ action();
908
+ return !terminated && !closed;
909
+ } catch (error) {
910
+ if (!isTranslatorBudgetExceededError(error)) throw error;
911
+ terminateForTranslatorOverflow(error);
912
+ return false;
913
+ }
914
+ };
905
915
  const step = async () => {
906
916
  if (stepping || closed) return;
907
917
  stepping = true;
@@ -1415,10 +1425,12 @@ export function bridgeToResponsesSSE(
1415
1425
  return;
1416
1426
  }
1417
1427
  if (!terminated) {
1418
- flushHiddenRawReasoning();
1419
- if (currentToolCall) failCurrentToolCall();
1420
- if (currentWebSearch) closeCurrentWebSearch("failed", []);
1421
- releasePendingWebSources();
1428
+ if (!attemptTerminationCleanup(() => {
1429
+ flushHiddenRawReasoning();
1430
+ if (currentToolCall) failCurrentToolCall();
1431
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1432
+ releasePendingWebSources();
1433
+ })) return;
1422
1434
  const failure = responseError(
1423
1435
  500,
1424
1436
  "proxy_error",
@@ -1448,13 +1460,15 @@ export function bridgeToResponsesSSE(
1448
1460
  if (!terminated) {
1449
1461
  // The adapter generator ended without an explicit done/error event. Mark as incomplete
1450
1462
  // rather than completed so Codex can distinguish a clean finish from a truncated stream.
1451
- if (currentMsg) closeCurrentMessage();
1452
- if (currentReasoning) closeCurrentReasoning();
1453
- if (currentRawReasoning) closeCurrentRawReasoning();
1454
- flushHiddenRawReasoning();
1455
- if (currentToolCall) failCurrentToolCall();
1456
- if (currentWebSearch) closeCurrentWebSearch("failed", []);
1457
- releasePendingWebSources();
1463
+ if (!attemptTerminationCleanup(() => {
1464
+ if (currentMsg) closeCurrentMessage();
1465
+ if (currentReasoning) closeCurrentReasoning();
1466
+ if (currentRawReasoning) closeCurrentRawReasoning();
1467
+ flushHiddenRawReasoning();
1468
+ if (currentToolCall) failCurrentToolCall();
1469
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1470
+ releasePendingWebSources();
1471
+ })) return;
1458
1472
  options?.onUsage?.(undefined);
1459
1473
  await awaitThoughtSignatureDurability();
1460
1474
  emit("response.incomplete", {
@@ -1493,13 +1507,15 @@ export function bridgeToResponsesSSE(
1493
1507
  upstreamActivity = false;
1494
1508
  stallTicks = 0;
1495
1509
  } else if (++stallTicks >= maxStallTicks) {
1496
- if (currentMsg) closeCurrentMessage();
1497
- if (currentReasoning) closeCurrentReasoning();
1498
- if (currentRawReasoning) closeCurrentRawReasoning();
1499
- flushHiddenRawReasoning();
1500
- if (currentToolCall) failCurrentToolCall();
1501
- if (currentWebSearch) closeCurrentWebSearch("failed", []);
1502
- releasePendingWebSources();
1510
+ if (!attemptTerminationCleanup(() => {
1511
+ if (currentMsg) closeCurrentMessage();
1512
+ if (currentReasoning) closeCurrentReasoning();
1513
+ if (currentRawReasoning) closeCurrentRawReasoning();
1514
+ flushHiddenRawReasoning();
1515
+ if (currentToolCall) failCurrentToolCall();
1516
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1517
+ releasePendingWebSources();
1518
+ })) return;
1503
1519
  // #1926 gap 2 residual: this beat callback is synchronous, so the durability
1504
1520
  // barrier is not awaited on the stall-timeout kill path. The in-memory store is
1505
1521
  // already updated; only a crash between here and the queued write loses it,
@@ -1728,7 +1744,7 @@ function buildResponseJSONWithBudget(
1728
1744
  if (batchRedacted.length > 0) envelope.red = batchRedacted;
1729
1745
  const hidden = options?.hideThinkingSummary === true;
1730
1746
  if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
1731
- const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope) : undefined;
1747
+ const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope, budget) : undefined;
1732
1748
  const sourceBytes = currentSummaryReasoningBytes + batchSignatureBytes + batchRedactedBytes;
1733
1749
  batchSignature = undefined;
1734
1750
  batchSignatureBytes = 0;
@@ -1756,7 +1772,7 @@ function buildResponseJSONWithBudget(
1756
1772
  // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
1757
1773
  pushOutput({
1758
1774
  type: "reasoning", id: `rs_${uuid()}`, summary: [],
1759
- encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
1775
+ encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }, budget),
1760
1776
  }, currentRawReasoningBytes, "reasoning");
1761
1777
  currentRawReasoning = "";
1762
1778
  currentRawReasoningBytes = 0;
@@ -2044,7 +2060,7 @@ function buildResponseJSONWithBudget(
2044
2060
  // pushOutput reserves the item itself and releases the retained raw blob it replaces.
2045
2061
  pushOutput({
2046
2062
  type: "reasoning", id: `rs_${uuid()}`, summary: [],
2047
- encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }),
2063
+ encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }, budget),
2048
2064
  }, batchKiroRedactedBytes, "reasoning");
2049
2065
  batchKiroRedacted = undefined;
2050
2066
  batchKiroRedactedBytes = 0;
@@ -18,6 +18,7 @@ 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
20
  import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
21
+ import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
21
22
 
22
23
 
23
24
 
@@ -210,7 +211,7 @@ function userMessageToItems(content: unknown, input: Rec[], elide: SkillElisionC
210
211
  pushUserMessage(input, pending);
211
212
  }
212
213
 
213
- function assistantMessageToItems(content: unknown, input: Rec[]): void {
214
+ function assistantMessageToItems(content: unknown, input: Rec[], budget: TranslatorBudget): void {
214
215
  if (typeof content === "string") {
215
216
  if (content.length > 0) input.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: content }] });
216
217
  return;
@@ -240,11 +241,12 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
240
241
  const thinking = typeof raw.thinking === "string" ? raw.thinking : "";
241
242
  const signature = typeof raw.signature === "string" ? raw.signature : "";
242
243
  if (signature.startsWith(OCX_REASONING_PREFIX)) {
243
- const owned = decodeReasoningEnvelope(signature);
244
+ const owned = decodeReasoningEnvelope(signature, budget);
244
245
  if (!owned) throw new AnthropicRequestError("malformed ocxr1 reasoning signature");
245
246
  if (Object.hasOwn(owned, "sig")) throw new AnthropicRequestError("OpenCodex reasoning continuity cannot be replayed as an Anthropic signature");
246
247
  }
247
- const encrypted = signature.length === 0 ? undefined : signature.startsWith(OCX_REASONING_PREFIX) ? signature : encodeReasoningEnvelope({ sig: signature });
248
+ const encrypted = signature.length === 0 ? undefined : signature.startsWith(OCX_REASONING_PREFIX) ? signature : encodeReasoningEnvelope({ sig: signature }, budget);
249
+ if (encrypted) budget.chargeRetained(2 * encrypted.length, { kind: "reasoning" });
248
250
  if (thinking.length === 0 && !encrypted) break;
249
251
  input.push({ type: "reasoning", id: `rs_${crypto.randomUUID().replace(/-/g, "")}`, summary: thinking.length > 0 ? [{ type: "summary_text", text: thinking }] : [], ...(encrypted ? { encrypted_content: encrypted } : {}) });
250
252
  break;
@@ -252,7 +254,11 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
252
254
  case "redacted_thinking": {
253
255
  flush();
254
256
  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] }) });
257
+ if (data.length > 0) {
258
+ const encrypted = encodeReasoningEnvelope({ red: [data] }, budget);
259
+ budget.chargeRetained(2 * encrypted.length, { kind: "reasoning" });
260
+ input.push({ type: "reasoning", id: `rs_${crypto.randomUUID().replace(/-/g, "")}`, summary: [], encrypted_content: encrypted });
261
+ }
256
262
  break;
257
263
  }
258
264
  default:
@@ -294,7 +300,16 @@ export function anthropicToResponsesBody(raw: unknown, cc?: OcxClaudeCodeConfig)
294
300
  * OUT-OF-BODY tuple (audit 133 R3#1 — an in-body marker would leak upstream through
295
301
  * the native Responses forward and 400).
296
302
  */
297
- export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCodeConfig): ClaudeInboundTranslation {
303
+ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCodeConfig, budget?: TranslatorBudget): ClaudeInboundTranslation {
304
+ const activeBudget = budget ?? createTranslatorBudget();
305
+ try {
306
+ return translateAnthropicRequest(raw, cc, activeBudget);
307
+ } finally {
308
+ if (!budget) activeBudget.dispose();
309
+ }
310
+ }
311
+
312
+ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undefined, budget: TranslatorBudget): ClaudeInboundTranslation {
298
313
  if (!isRec(raw)) throw new AnthropicRequestError("request body must be a JSON object");
299
314
  if (typeof raw.model !== "string" || raw.model.length === 0) {
300
315
  throw new AnthropicRequestError("model is required");
@@ -315,7 +330,7 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode
315
330
  for (const msg of raw.messages) {
316
331
  if (!isRec(msg)) throw new AnthropicRequestError("each message must be an object");
317
332
  if (msg.role === "user") userMessageToItems(msg.content, input, elide);
318
- else if (msg.role === "assistant") assistantMessageToItems(msg.content, input);
333
+ else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
319
334
  else if (msg.role === "system") {
320
335
  const text = systemMessageText(msg.content);
321
336
  if (text.length > 0) systemParts.push(text);
@@ -215,6 +215,8 @@ interface OpenBlock {
215
215
  callId?: string;
216
216
  /** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
217
217
  reasoningPartKey?: string;
218
+ /** Fixed-size item identity; missing IDs only match other missing IDs. */
219
+ reasoningItemKey?: string;
218
220
  thinkingBuf?: string;
219
221
  thinkingBufBytes?: number;
220
222
  reasoningSig?: string;
@@ -308,7 +310,21 @@ export function responsesSseToAnthropicSse(
308
310
  open.webSearchArgsEmitted = true;
309
311
  }
310
312
  if (open.kind === "thinking") {
311
- const signature = open.reasoningSig ?? encodeReasoningEnvelope({ txt: open.thinkingBuf ?? "" });
313
+ // Delay the index and all thinking frames until closure so a matching
314
+ // done envelope can put its redacted blocks first. The existing buffer
315
+ // remains charged through signature emission, including queued frames.
316
+ open.index = blockIndex++;
317
+ emit("content_block_start", {
318
+ type: "content_block_start", index: open.index,
319
+ content_block: { type: "thinking", thinking: "", signature: "" },
320
+ });
321
+ if (open.thinkingBuf) {
322
+ emit("content_block_delta", {
323
+ type: "content_block_delta", index: open.index,
324
+ delta: { type: "thinking_delta", thinking: open.thinkingBuf },
325
+ });
326
+ }
327
+ const signature = open.reasoningSig ?? encodeReasoningEnvelope({ txt: open.thinkingBuf ?? "" }, translatorBudget);
312
328
  emit("content_block_delta", {
313
329
  type: "content_block_delta", index: open.index,
314
330
  delta: { type: "signature_delta", signature },
@@ -323,12 +339,13 @@ export function responsesSseToAnthropicSse(
323
339
  ensureStarted();
324
340
  if (open && open.kind === kind) return;
325
341
  closeOpenBlock();
342
+ if (kind === "thinking") {
343
+ open = { kind, index: -1, thinkingBuf: "", thinkingBufBytes: 0 };
344
+ return;
345
+ }
326
346
  const index = blockIndex++;
327
- const contentBlock: Rec = kind === "text"
328
- ? { type: "text", text: "" }
329
- : { type: "thinking", thinking: "", signature: "" };
330
- emit("content_block_start", { type: "content_block_start", index, content_block: contentBlock });
331
- open = { kind, index, thinkingBuf: "", thinkingBufBytes: 0 };
347
+ emit("content_block_start", { type: "content_block_start", index, content_block: { type: "text", text: "" } });
348
+ open = { kind, index };
332
349
  };
333
350
  const finish = (stopReason: string, usage: unknown) => {
334
351
  if (terminated) return;
@@ -405,11 +422,13 @@ export function responsesSseToAnthropicSse(
405
422
  case "response.reasoning_summary_text.delta":
406
423
  case "response.reasoning_text.delta": {
407
424
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
425
+ const itemKey = boundedReasoningIdentity(data.item_id);
426
+ if (open?.kind === "thinking" && open.reasoningItemKey !== itemKey) closeOpenBlock();
408
427
  ensureBlock("thinking");
409
428
  const active = open;
410
429
  if (!active || active.kind !== "thinking") break;
411
430
  // The JSON path joins reasoning summary/content parts with "\n\n"
412
- // (responsesJsonToAnthropicMessage); mirror that at part and item boundaries
431
+ // (responsesJsonToAnthropicMessage); mirror that at part boundaries
413
432
  // so multi-part summaries do not glue into one run-on paragraph. Frames
414
433
  // without part indices produce a constant key and never get a separator.
415
434
  const slot = eventName === "response.reasoning_summary_text.delta"
@@ -418,7 +437,7 @@ export function responsesSseToAnthropicSse(
418
437
  // Upstream string metadata can be arbitrarily large. Hash strings into fixed-size
419
438
  // components while retaining item and part equality, rather than dropping item_id and
420
439
  // accidentally joining distinct malformed reasoning items.
421
- const partKey = `${boundedReasoningIdentity(data.item_id)}:${slot}`;
440
+ const partKey = `${itemKey}:${slot}`;
422
441
  const needsPartSeparator = active.reasoningPartKey !== undefined
423
442
  && active.reasoningPartKey !== partKey;
424
443
  const appended = `${needsPartSeparator ? "\n\n" : ""}${data.delta}`;
@@ -436,17 +455,8 @@ export function responsesSseToAnthropicSse(
436
455
  reservation.release();
437
456
  throw error;
438
457
  }
439
- if (needsPartSeparator) {
440
- emit("content_block_delta", {
441
- type: "content_block_delta", index: active.index,
442
- delta: { type: "thinking_delta", thinking: "\n\n" },
443
- });
444
- }
458
+ active.reasoningItemKey = itemKey;
445
459
  active.reasoningPartKey = partKey;
446
- emit("content_block_delta", {
447
- type: "content_block_delta", index: active.index,
448
- delta: { type: "thinking_delta", thinking: data.delta },
449
- });
450
460
  break;
451
461
  }
452
462
  case "response.output_item.added": {
@@ -561,22 +571,29 @@ export function responsesSseToAnthropicSse(
561
571
  else if (open && open.kind === "text" && item.type === "message") closeOpenBlock();
562
572
  else if (item.type === "reasoning") {
563
573
  const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : "";
564
- const env = encrypted ? decodeReasoningEnvelope(encrypted) : null;
574
+ const env = encrypted ? decodeReasoningEnvelope(encrypted, translatorBudget) : null;
565
575
  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;
576
+ const itemKey = boundedReasoningIdentity(item.id);
577
+ // A late/unrelated done cannot reorder or sign another item's text.
578
+ if (open?.kind === "thinking" && open.reasoningItemKey !== itemKey) {
569
579
  closeOpenBlock();
570
580
  }
571
581
  if (red.length > 0) {
572
582
  ensureStarted();
573
- closeOpenBlock();
583
+ if (open?.kind !== "thinking") closeOpenBlock();
574
584
  }
575
585
  for (const data of red) {
576
586
  const idx = blockIndex++;
577
587
  emit("content_block_start", { type: "content_block_start", index: idx, content_block: { type: "redacted_thinking", data } });
578
588
  emit("content_block_stop", { type: "content_block_stop", index: idx });
579
589
  }
590
+ if (env?.sig && open?.kind !== "thinking") {
591
+ ensureBlock("thinking");
592
+ }
593
+ if (open?.kind === "thinking") {
594
+ if (env?.sig) open.reasoningSig = env.sig;
595
+ closeOpenBlock();
596
+ }
580
597
  }
581
598
  break;
582
599
  }
@@ -785,7 +802,7 @@ export function responsesSseToAnthropicSse(
785
802
  }
786
803
 
787
804
  /** Non-streaming: /v1/responses JSON -> Anthropic message JSON. */
788
- export function responsesJsonToAnthropicMessage(json: unknown, model: string): Rec {
805
+ export function responsesJsonToAnthropicMessage(json: unknown, model: string, translatorBudget?: TranslatorBudget): Rec {
789
806
  const body = isRec(json) ? json : {};
790
807
  const output = Array.isArray(body.output) ? body.output : [];
791
808
  const content: Rec[] = [];
@@ -817,14 +834,14 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string): R
817
834
  }
818
835
  }
819
836
  const encrypted = typeof raw.encrypted_content === "string" ? raw.encrypted_content : "";
820
- const env = encrypted ? decodeReasoningEnvelope(encrypted) : null;
837
+ const env = encrypted ? decodeReasoningEnvelope(encrypted, translatorBudget) : null;
821
838
  // Legacy combined envelopes place redacted blocks before the signed block,
822
839
  // matching the Anthropic adapter. New bridge output uses separate items.
823
840
  for (const data of env?.red ?? []) content.push({ type: "redacted_thinking", data });
824
841
  // env.txt may be locally hidden text. Do not expose it here or manufacture
825
842
  // a new signed continuity carrier; hidden-summary replay remains limited.
826
843
  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") }) });
844
+ content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: env?.sig ?? encodeReasoningEnvelope({ txt: parts.join("\n\n") }, translatorBudget) });
828
845
  }
829
846
  break;
830
847
  }
@@ -95,6 +95,31 @@ export const HEAD_CAPABILITIES: readonly HeadCapability[] = [
95
95
  * A capability must not name a route the command does not actually fetch.
96
96
  */
97
97
  export const CAPABILITIES: readonly Capability[] = [
98
+ {
99
+ command: ["models", "price"],
100
+ summary: "Read the saved manual price for an exact provider/model selector.",
101
+ routes: [{ method: "GET", path: "/api/providers/{provider}/model-costs" }],
102
+ flags: [{ name: "--json", value: "boolean", summary: "Emit provider, modelId, and cost (null for automatic pricing)." }],
103
+ mutates: false,
104
+ json: "envelope",
105
+ details: ["The provider must be configured; everything after the first slash is the exact upstream model ID."],
106
+ },
107
+ {
108
+ command: ["models", "set-price"],
109
+ summary: "Save four manual USD-per-1M-token rates, or restore automatic pricing for one model.",
110
+ routes: [{ method: "PUT", path: "/api/providers/{provider}/model-costs" }],
111
+ flags: [
112
+ { name: "--input", value: "number", summary: "Input rate; required unless --auto is used." },
113
+ { name: "--output", value: "number", summary: "Output rate; required unless --auto is used." },
114
+ { name: "--cache-read", value: "number", summary: "Cache read rate; defaults to 0." },
115
+ { name: "--cache-write", value: "number", summary: "Cache write rate; defaults to 0." },
116
+ { name: "--auto", value: "boolean", summary: "Remove this model's override; cannot be combined with rates." },
117
+ { name: "--json", value: "boolean", summary: "Emit the saved price or reset result as JSON." },
118
+ ],
119
+ mutates: true,
120
+ json: "payload",
121
+ details: ["Uses the exact upstream model ID after the first slash. Omitted cache rates default to zero; sibling model prices are preserved."],
122
+ },
98
123
  {
99
124
  command: ["status"],
100
125
  summary: "Proxy status, injection state, and version skew between this CLI and the running proxy.",
@@ -200,6 +225,8 @@ export const CAPABILITIES: readonly Capability[] = [
200
225
  routes: [{ method: "GET", path: "/api/usage" }],
201
226
  flags: [
202
227
  { name: "--range", value: "string", summary: "today | 1d | 7d | 30d | all" },
228
+ { name: "--since", value: "string", summary: "Inclusive start: epoch milliseconds or full ISO datetime with timezone; requires --until and overrides --range." },
229
+ { name: "--until", value: "string", summary: "Inclusive end: epoch milliseconds or full ISO datetime with timezone; requires --since." },
203
230
  { name: "--provider", value: "string", summary: "Restrict to one provider." },
204
231
  { name: "--model", value: "string", summary: "Restrict to one model id." },
205
232
  { name: "--json", value: "boolean", summary: "Emit the usage report as JSON." },
@@ -189,7 +189,7 @@ function singleClientStatusLines(result: unknown): string[] {
189
189
  const rest = Object.fromEntries(Object.entries(result as Record<string, unknown>).filter(([key]) => key !== "raycast"));
190
190
  const lines = [...summaryLines(rest), `plan: ${raycast.plan}`];
191
191
  if (!raycast.aiDirPresent) {
192
- lines.push('Open Raycast → Settings → AI → "Reveal Providers Config" once so the ai folder exists.');
192
+ lines.push('On macOS or Windows, open Raycast → Settings → AI → "Reveal Providers Config" once so the ai folder exists.');
193
193
  }
194
194
  return lines;
195
195
  }
@@ -15,6 +15,8 @@
15
15
  */
16
16
  export const MODELS_RUNTIME_SUBCOMMANDS = [
17
17
  "live",
18
+ "price",
19
+ "set-price",
18
20
  "edit",
19
21
  "enable",
20
22
  "disable",
@@ -13,9 +13,18 @@ import {
13
13
  type RuntimeApiDeps,
14
14
  } from "./runtime-api";
15
15
  import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
16
+ import { isValidProviderName } from "../config/provider-name";
17
+ import { isValidModelDiscoveryModelId } from "../providers/model-discovery-limits";
18
+ import { redactSecretString } from "../lib/redact";
19
+ import type { ProviderCostOverlay } from "../types";
20
+ import { MAX_COST4_RATE } from "../usage/expected-prices";
21
+ import { isValidCost4Rate } from "../usage/user-cost-overlays";
16
22
 
17
23
  const USAGE = `Usage:
18
24
  ocx models live [--provider <name>] [--json]
25
+ ocx models price <provider/model> [--json]
26
+ ocx models set-price <provider/model> --input N --output N [--cache-read N] [--cache-write N] [--json]
27
+ ocx models set-price <provider/model> --auto [--json]
19
28
  ocx models edit <custom-id> [--model-id <id>] [--display-name <name|->]
20
29
  [--context-window <tokens|0>] [--modalities <text,image,audio|->]
21
30
  [--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra|->]
@@ -28,7 +37,10 @@ const USAGE = `Usage:
28
37
  ocx models new-policy [on|off] [--provider <name>] [--json]
29
38
  ocx models new-arrivals [--json]
30
39
  ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]
31
- ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]`;
40
+ ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]
41
+
42
+ Prices are USD per 1M tokens. Omitted cache rates default to 0.
43
+ Price selectors use the exact upstream model ID after the first slash.`;
32
44
 
33
45
  type ModelRow = {
34
46
  provider?: string;
@@ -55,6 +67,99 @@ async function live(argv: string[], deps: RuntimeApiDeps): Promise<void> {
55
67
  }));
56
68
  }
57
69
 
70
+ function priceRecord(value: unknown): value is Record<string, unknown> {
71
+ return value !== null && typeof value === "object" && !Array.isArray(value);
72
+ }
73
+
74
+ const PRICE_RATE_KEYS = ["input", "output", "cacheRead", "cacheWrite"] as const;
75
+
76
+ function validPriceCost(value: unknown): value is ProviderCostOverlay {
77
+ return priceRecord(value) && Object.keys(value).length === PRICE_RATE_KEYS.length
78
+ && PRICE_RATE_KEYS.every(key => Object.hasOwn(value, key) && isValidCost4Rate(value[key]));
79
+ }
80
+
81
+ async function price(write: boolean, argv: string[], deps: RuntimeApiDeps): Promise<void> {
82
+ try {
83
+ await priceRequest(write, argv, deps);
84
+ } catch (error) {
85
+ // Duplicated, inline and stray options also reach parser diagnostics.
86
+ // Keep HTTP-specific RuntimeApiError exits while masking usage errors.
87
+ if (error instanceof CliUsageError) {
88
+ throw new CliUsageError(redactSecretString(error.message), error.usage);
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ async function priceRequest(write: boolean, argv: string[], deps: RuntimeApiDeps): Promise<void> {
95
+ const args = [...argv];
96
+ const selector = args.shift() ?? "";
97
+ const slash = selector.indexOf("/");
98
+ const provider = selector.slice(0, slash);
99
+ const modelId = selector.slice(slash + 1);
100
+ if (slash < 1 || !isValidProviderName(provider) || !isValidModelDiscoveryModelId(modelId)) {
101
+ throw new CliUsageError("model selector must be provider/model with an exact upstream model id", USAGE);
102
+ }
103
+ if (redactSecretString(modelId) !== modelId) {
104
+ throw new CliUsageError("modelId cannot be displayed safely", USAGE);
105
+ }
106
+ const wantsJson = takeFlag(args, "--json");
107
+ const path = `/api/providers/${encodeURIComponent(provider)}/model-costs`;
108
+ if (!write) {
109
+ rejectArgs(args, USAGE);
110
+ const result = await runtimeRequest<unknown>(path, {}, deps);
111
+ if (!priceRecord(result) || result.provider !== provider || !priceRecord(result.modelCosts)
112
+ || !Object.values(result.modelCosts).every(validPriceCost)) {
113
+ throw new Error("Invalid model price response");
114
+ }
115
+ let cost: ProviderCostOverlay | null = null;
116
+ if (Object.hasOwn(result.modelCosts, modelId)) {
117
+ const stored = result.modelCosts[modelId];
118
+ if (!validPriceCost(stored)) throw new Error("Invalid model price response");
119
+ cost = { ...stored };
120
+ }
121
+ printData({ provider, modelId, cost }, wantsJson, [
122
+ cost === null ? `${selector}: automatic pricing` : `${selector}: ${JSON.stringify(cost)} USD per 1M tokens`,
123
+ ]);
124
+ return;
125
+ }
126
+ const auto = takeFlag(args, "--auto");
127
+ const input = takeOption(args, "--input");
128
+ const output = takeOption(args, "--output");
129
+ const cacheRead = takeOption(args, "--cache-read");
130
+ const cacheWrite = takeOption(args, "--cache-write");
131
+ rejectArgs(args, USAGE);
132
+ if (auto && [input, output, cacheRead, cacheWrite].some(value => value !== undefined)) {
133
+ throw new CliUsageError("--auto cannot be combined with price rates", USAGE);
134
+ }
135
+ if (!auto && (input === undefined || output === undefined)) {
136
+ throw new CliUsageError("--input and --output are required unless --auto is used", USAGE);
137
+ }
138
+ const rate = (raw: string, flag: string): number => {
139
+ const value = Number(raw);
140
+ if (!raw.trim() || !isValidCost4Rate(value)) {
141
+ throw new CliUsageError(`${flag} must be a finite number between 0 and ${MAX_COST4_RATE}`, USAGE);
142
+ }
143
+ return value;
144
+ };
145
+ const cost: ProviderCostOverlay | null = auto ? null : {
146
+ input: rate(input!, "--input"),
147
+ output: rate(output!, "--output"),
148
+ cacheRead: rate(cacheRead ?? "0", "--cache-read"),
149
+ cacheWrite: rate(cacheWrite ?? "0", "--cache-write"),
150
+ };
151
+ const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify({ modelId, cost }) }, deps);
152
+ const receivedCost = priceRecord(result) ? result.cost : undefined;
153
+ if (!priceRecord(result) || result.ok !== true || result.provider !== provider || result.modelId !== modelId
154
+ || (cost === null ? receivedCost !== null : !validPriceCost(receivedCost)
155
+ || !PRICE_RATE_KEYS.every(key => receivedCost[key] === cost[key]))) {
156
+ throw new Error("Invalid model price persistence receipt");
157
+ }
158
+ // Project the acknowledged fields only; unrelated response fields are not CLI output.
159
+ printData({ ok: true, provider, modelId, cost }, wantsJson,
160
+ [auto ? `${selector}: automatic pricing restored.` : `${selector}: manual pricing saved.`]);
161
+ }
162
+
58
163
  async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
59
164
  const args = [...argv];
60
165
  const id = args.shift()?.trim();
@@ -328,6 +433,8 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
328
433
  if (!isModelsRuntimeSubcommand(sub)) return null;
329
434
  let action: (() => Promise<void>) | undefined;
330
435
  if (sub === "live") action = () => live(argv, deps);
436
+ else if (sub === "price") action = () => price(false, argv, deps);
437
+ else if (sub === "set-price") action = () => price(true, argv, deps);
331
438
  else if (sub === "edit") action = () => edit(argv, deps);
332
439
  else if (sub === "enable") action = () => visibility(true, argv, deps);
333
440
  else if (sub === "disable") action = () => visibility(false, argv, deps);
@@ -11,7 +11,9 @@ import {
11
11
  type RuntimeApiDeps,
12
12
  } from "./runtime-api";
13
13
  import { formatUsageReport } from "./usage-report";
14
- import { USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
14
+ import { USAGE_RANGES, USAGE_SURFACES, type UsageSummary } from "../usage/summary";
15
+ import { parseUsageTimeWindow, type UsageTimeWindow } from "../usage/time-range";
16
+ import { redactSecretString } from "../lib/redact";
15
17
 
16
18
  const USAGE = `Usage:
17
19
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
@@ -20,6 +22,7 @@ const USAGE = `Usage:
20
22
  ocx logs rebuild-index
21
23
  ocx logs index-status
22
24
  ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
25
+ [--since <epoch-ms|ISO-datetime>] [--until <epoch-ms|ISO-datetime>]
23
26
  [--provider <name>] [--model <id>] [--json]
24
27
  ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
25
28
  ocx observe memory [--json]
@@ -146,6 +149,14 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
146
149
  const surface = takeOption(args, "--surface") ?? "all";
147
150
  const provider = takeOption(args, "--provider");
148
151
  const model = takeOption(args, "--model");
152
+ const since = takeOption(args, "--since");
153
+ const until = takeOption(args, "--until");
154
+ let window: UsageTimeWindow | undefined;
155
+ try {
156
+ window = parseUsageTimeWindow(since, until);
157
+ } catch (error) {
158
+ throw new CliUsageError(error instanceof Error ? error.message : "invalid usage time window", USAGE);
159
+ }
149
160
  // `1d` is accepted here as well as server-side so the CLI does not reject an
150
161
  // alias the API would have understood.
151
162
  const ranges = [...USAGE_RANGES, "1d"];
@@ -153,8 +164,12 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
153
164
  if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
154
165
  throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
155
166
  }
156
- rejectArgs(args, USAGE);
157
- const result = await runtimeRequest(`/api/usage${query({ range, surface, provider, model })}`, {}, deps);
167
+ rejectArgs(args.map(redactSecretString), USAGE);
168
+ const result = await runtimeRequest<UsageSummary>(`/api/usage${query({ range, surface, provider, model, since: window?.since, until: window?.until })}`, {}, deps);
169
+ // Older daemons ignore custom bounds and return successful preset reports.
170
+ if (window && (result?.customWindow !== true || result.since !== window.since || result.until !== window.until)) {
171
+ throw new Error("The server did not confirm the requested custom usage window. Upgrade and restart the proxy, then retry.");
172
+ }
158
173
  // Built only when it will be printed: JavaScript evaluates arguments before
159
174
  // the call, so passing formatUsageReport(...) inline would run the human
160
175
  // renderer during --json and let its assumptions affect a path that is meant
@@ -24,6 +24,8 @@ interface UsageReportInput {
24
24
  range?: string;
25
25
  surface?: string;
26
26
  since?: number | null;
27
+ until?: number;
28
+ customWindow?: boolean;
27
29
  summary?: {
28
30
  requests?: number;
29
31
  totalTokens?: number;
@@ -90,7 +92,10 @@ function table(header: string[], rows: string[][]): string[] {
90
92
  }
91
93
 
92
94
  function describeScope(data: UsageReportInput): string {
93
- const parts = [`Usage — ${data.range ?? "?"}`];
95
+ const interval = data.customWindow && typeof data.since === "number" && typeof data.until === "number"
96
+ ? `custom ${new Date(data.since).toISOString()} to ${new Date(data.until).toISOString()} (inclusive)`
97
+ : data.range ?? "?";
98
+ const parts = [`Usage — ${interval}`];
94
99
  if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
95
100
  if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
96
101
  if (data.filter?.model) parts.push(`model=${data.filter.model}`);