@bitkyc08/opencodex 2.7.36 → 2.7.37

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 (66) hide show
  1. package/README.ja.md +8 -1
  2. package/README.ko.md +7 -1
  3. package/README.md +7 -1
  4. package/README.ru.md +7 -1
  5. package/README.zh-CN.md +7 -1
  6. package/gui/dist/assets/index-BhUTxmCy.js +52 -0
  7. package/gui/dist/assets/index-oOZcqVmj.css +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic.ts +22 -2
  11. package/src/adapters/cursor/live-transport.ts +7 -0
  12. package/src/adapters/cursor/message-mapper.ts +3 -0
  13. package/src/adapters/cursor/protobuf-request.ts +223 -27
  14. package/src/adapters/cursor/request-builder.ts +41 -15
  15. package/src/adapters/cursor/thread-continuity.ts +67 -0
  16. package/src/adapters/cursor/types.ts +3 -1
  17. package/src/adapters/cursor.ts +44 -9
  18. package/src/adapters/google.ts +115 -62
  19. package/src/adapters/kiro.ts +3 -17
  20. package/src/adapters/openai-chat.ts +16 -5
  21. package/src/adapters/openai-responses.ts +56 -1
  22. package/src/adapters/run-turn-queue.ts +11 -1
  23. package/src/bridge.ts +139 -69
  24. package/src/chat/outbound.ts +135 -73
  25. package/src/cli/codex-shim-autorestore.ts +45 -0
  26. package/src/cli/doctor.ts +197 -2
  27. package/src/cli/index.ts +17 -3
  28. package/src/cli/status.ts +80 -0
  29. package/src/cli/v2.ts +14 -2
  30. package/src/codex/auth-context.ts +18 -2
  31. package/src/codex/catalog/bundled.ts +83 -27
  32. package/src/codex/catalog/effort.ts +95 -3
  33. package/src/codex/catalog/parsing.ts +17 -0
  34. package/src/codex/catalog/provider-fetch.ts +31 -8
  35. package/src/codex/exec-invocation.ts +22 -0
  36. package/src/codex/model-cache.ts +44 -0
  37. package/src/codex/runtime.ts +529 -0
  38. package/src/codex/shim.ts +608 -10
  39. package/src/combos/resolve.ts +7 -2
  40. package/src/config.ts +32 -1
  41. package/src/lib/bun-stream-caps.ts +88 -0
  42. package/src/lib/crash-guard.ts +3 -1
  43. package/src/lib/sse-decoder.ts +25 -6
  44. package/src/responses/parser.ts +2 -1
  45. package/src/responses/state.ts +10 -2
  46. package/src/server/auth-cors.ts +4 -1
  47. package/src/server/index.ts +191 -1
  48. package/src/server/live.ts +491 -0
  49. package/src/server/management/config-routes.ts +79 -3
  50. package/src/server/management/provider-routes.ts +2 -0
  51. package/src/server/management/shared.ts +6 -6
  52. package/src/server/management/system-routes.ts +65 -0
  53. package/src/server/management-api.ts +3 -1
  54. package/src/server/memory-watchdog.ts +112 -0
  55. package/src/server/relay-eager.ts +199 -0
  56. package/src/server/relay.ts +131 -81
  57. package/src/server/responses/collaboration.ts +20 -3
  58. package/src/server/responses/core.ts +236 -21
  59. package/src/server/responses/encrypted-payload.ts +118 -41
  60. package/src/server/ws-bridge.ts +7 -0
  61. package/src/types.ts +25 -0
  62. package/src/usage/cost.ts +0 -0
  63. package/src/usage/expected-prices.ts +19 -0
  64. package/src/usage/summary.ts +11 -8
  65. package/gui/dist/assets/index-BpX-hoSd.css +0 -1
  66. package/gui/dist/assets/index-ZmFopEYw.js +0 -52
@@ -345,6 +345,86 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
345
345
  let pendingUsage: OcxUsage | undefined;
346
346
  let toolCallsStarted = 0;
347
347
  let lastFinishReason: string | undefined;
348
+ let sawAnyFrame = false;
349
+ let sawTerminalSignal = false;
350
+
351
+ const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "content" | "terminate"> {
352
+ const payload = line.slice(5).trim();
353
+ if (!payload) return "continue";
354
+ let emittedContentEvent = false;
355
+
356
+ let chunk: Record<string, unknown>;
357
+ try {
358
+ chunk = JSON.parse(payload);
359
+ } catch {
360
+ yield { type: "error", message: "malformed upstream SSE data frame" };
361
+ return "terminate";
362
+ }
363
+ sawAnyFrame = true;
364
+
365
+ // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
366
+ if (chunk.error) {
367
+ const err = chunk.error as { message?: string } | undefined;
368
+ // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
369
+ // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
370
+ if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
371
+ && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
372
+ clearAntigravityReplay(antigravityModel, antigravitySession);
373
+ }
374
+ yield { type: "error", message: err?.message ?? "upstream error" };
375
+ return "terminate";
376
+ }
377
+
378
+ // Antigravity (CCA) nests the standard Gemini payload under `response`.
379
+ let root = chunk;
380
+ if (provider.googleMode === "cloud-code-assist") {
381
+ const wrapped = chunk.response;
382
+ if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
383
+ yield { type: "error", message: "google-antigravity response missing response wrapper" };
384
+ return "terminate";
385
+ }
386
+ root = wrapped as Record<string, unknown>;
387
+ }
388
+ // usageMetadata is a top-level field independent of candidates; read it BEFORE the
389
+ // candidates guard so a usage-only final chunk is not dropped.
390
+ const usageMeta = root.usageMetadata as Record<string, number> | undefined;
391
+ if (usageMeta) {
392
+ // Accumulate usage; emit a single terminal `done` post-loop so usage is never
393
+ // dropped on EOF and the stream never yields two `done` events.
394
+ pendingUsage = usageFromGemini(usageMeta);
395
+ sawTerminalSignal = true;
396
+ }
397
+ const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
398
+ if (!candidates?.length) return "continue";
399
+
400
+ if (typeof candidates[0].finishReason === "string" && candidates[0].finishReason) {
401
+ lastFinishReason = candidates[0].finishReason;
402
+ sawTerminalSignal = true;
403
+ }
404
+
405
+ const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
406
+ // Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
407
+ if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
408
+ observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
409
+ }
410
+ if (parts) {
411
+ for (const part of parts) {
412
+ if (part.text) {
413
+ emittedContentEvent = true;
414
+ yield { type: "text_delta", text: part.text };
415
+ }
416
+ if (part.functionCall) {
417
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
418
+ toolCallsStarted++;
419
+ emittedContentEvent = true;
420
+ yield { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
421
+ yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
422
+ yield { type: "tool_call_end" };
423
+ }
424
+ }
425
+ }
426
+ return emittedContentEvent ? "content" : "continue";
427
+ };
348
428
 
349
429
  try {
350
430
  while (true) {
@@ -355,70 +435,30 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
355
435
  const lines = buffer.split("\n");
356
436
  buffer = lines.pop() ?? "";
357
437
 
438
+ let sawLiveness = false;
439
+ let sawContentEvent = false;
358
440
  for (const line of lines) {
359
- if (!line.startsWith("data: ")) continue;
360
- const payload = line.slice(6).trim();
361
- if (!payload) continue;
362
-
363
- let chunk: Record<string, unknown>;
364
- try { chunk = JSON.parse(payload); } catch { debugDroppedFrame("google", payload); continue; }
365
-
366
- // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
367
- if (chunk.error) {
368
- const err = chunk.error as { message?: string } | undefined;
369
- // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
370
- // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
371
- if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
372
- && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
373
- clearAntigravityReplay(antigravityModel, antigravitySession);
374
- }
375
- yield { type: "error", message: err?.message ?? "upstream error" };
376
- return;
377
- }
378
-
379
- // Antigravity (CCA) nests the standard Gemini payload under `response`.
380
- let root = chunk;
381
- if (provider.googleMode === "cloud-code-assist") {
382
- const wrapped = chunk.response;
383
- if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
384
- yield { type: "error", message: "google-antigravity response missing response wrapper" };
385
- return;
386
- }
387
- root = wrapped as Record<string, unknown>;
388
- }
389
- // usageMetadata is a top-level field independent of candidates; read it BEFORE the
390
- // candidates guard so a usage-only final chunk is not dropped.
391
- const usageMeta = root.usageMetadata as Record<string, number> | undefined;
392
- if (usageMeta) {
393
- // Accumulate usage; emit a single terminal `done` post-loop so usage is never
394
- // dropped on EOF and the stream never yields two `done` events.
395
- pendingUsage = usageFromGemini(usageMeta);
396
- }
397
- const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
398
- if (!candidates?.length) continue;
399
-
400
- lastFinishReason = candidates[0].finishReason ?? lastFinishReason;
401
-
402
- const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
403
- // Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
404
- if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
405
- observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
406
- }
407
- if (parts) {
408
- for (const part of parts) {
409
- if (part.text) {
410
- yield { type: "text_delta", text: part.text };
411
- }
412
- if (part.functionCall) {
413
- const id = `call_${crypto.randomUUID().slice(0, 8)}`;
414
- toolCallsStarted++;
415
- yield { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
416
- yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
417
- yield { type: "tool_call_end" };
418
- }
419
- }
441
+ if (line.startsWith("data:")) {
442
+ const result = yield* handleDataLine(line);
443
+ if (result === "terminate") return;
444
+ if (result === "content") sawContentEvent = true;
445
+ continue;
420
446
  }
447
+ sawLiveness = true;
448
+ if (line.startsWith(":") || !line.trim()) continue;
449
+ debugDroppedFrame("google", line);
421
450
  }
451
+ if (sawLiveness && !sawContentEvent) yield { type: "heartbeat" };
452
+ }
453
+ buffer += decoder.decode();
454
+ if (buffer.trim().length > 0) {
455
+ const residual = buffer.trim();
456
+ if (residual.startsWith(":")) {
457
+ yield { type: "heartbeat" };
458
+ } else if (!residual.startsWith("data:")) {
459
+ yield { type: "error", message: "upstream stream ended with an incomplete SSE frame — possible truncation" };
460
+ return;
461
+ } else if ((yield* handleDataLine(residual)) === "terminate") return;
422
462
  }
423
463
  // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
424
464
  // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
@@ -427,7 +467,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
427
467
  yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
428
468
  return;
429
469
  }
430
- yield { type: "done", usage: pendingUsage };
470
+ if (!sawAnyFrame || !sawTerminalSignal) {
471
+ yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
472
+ return;
473
+ }
474
+ const stopReason = lastFinishReason === "MAX_TOKENS"
475
+ ? "max_tokens"
476
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
477
+ ? "content_filter"
478
+ : undefined;
479
+ yield {
480
+ type: "done",
481
+ usage: pendingUsage,
482
+ ...(stopReason ? { stopReason } : {}),
483
+ };
431
484
  } finally {
432
485
  reader.releaseLock();
433
486
  }
@@ -917,23 +917,9 @@ async function* parseKiroAttempt(
917
917
  terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
918
918
  };
919
919
  }
920
- // A clean Smithy EOF after ordinary assistant text is a complete answer even when the
921
- // private completion tool was advertised. Replaying that answer through a second Kiro
922
- // request adds latency and can hang an otherwise-finished Codex turn if the retry stalls.
923
- // Reasoning-only output still needs the bounded fallback because it has no user-facing text.
924
- if (mode === "required" && sawText) {
925
- return {
926
- assistantText,
927
- sawReasoning,
928
- terminal: {
929
- type: "done",
930
- usage: finalUsage,
931
- endTurn: true,
932
- ...(finalProviderState ? { providerState: finalProviderState } : {}),
933
- },
934
- };
935
- }
936
- if (mode === "required" && sawReasoning) {
920
+ // Kiro text has no trustworthy final/progress marker. When completion is required, ordinary
921
+ // text and reasoning remain unfinished until the one bounded fallback validates the turn.
922
+ if (mode === "required" && (sawText || sawReasoning)) {
937
923
  return { assistantText, sawReasoning, needsFallback: true, usage: finalUsage, providerState: finalProviderState };
938
924
  }
939
925
  if (!sawText && !sawReasoning) {
@@ -604,7 +604,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
604
604
  // being reported as a clean completion (silent truncation). A graceful close is either an
605
605
  // explicit `[DONE]` sentinel OR a chunk carrying a non-null `finish_reason` (some
606
606
  // OpenAI-compatible providers omit `[DONE]` but do send finish_reason).
607
- let sawFinish = false;
607
+ let finishReason: string | undefined;
608
608
 
609
609
  // Single per-line handler shared by the streaming loop and the EOF residual-frame flush, so
610
610
  // a final frame is parsed identically wherever it lands (no duplicated, drift-prone parsing).
@@ -615,7 +615,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
615
615
  const payload = line.slice(6).trim();
616
616
  if (payload === "[DONE]") {
617
617
  yield* flushToolCalls();
618
- yield { type: "done", usage: pendingUsage };
618
+ const stopReason = finishReason === "length"
619
+ ? "max_tokens"
620
+ : finishReason === "content_filter"
621
+ ? "content_filter"
622
+ : undefined;
623
+ yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
619
624
  return "terminate";
620
625
  }
621
626
 
@@ -647,9 +652,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
647
652
  const choices = chunk.choices as { delta?: Record<string, unknown>; finish_reason?: string }[] | undefined;
648
653
  if (!choices || choices.length === 0) return "continue";
649
654
  // Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
650
- // no delta) is a graceful close and must mark sawFinish even though we skip it below.
655
+ // no delta) is a graceful close and must record finishReason even though we skip it below.
651
656
  if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
652
- sawFinish = true;
657
+ finishReason = choices[0].finish_reason;
653
658
  }
654
659
  const delta = choices[0].delta;
655
660
  if (delta) {
@@ -720,12 +725,18 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
720
725
  // a non-null finish_reason (sawFinish), or a trailing usage chunk (providers emit usage only
721
726
  // at end-of-generation). If NONE of those were seen, the stream was cut mid-flight — fail
722
727
  // closed so the bridge emits a classified response.failed rather than a silent truncation.
728
+ const sawFinish = finishReason !== undefined;
723
729
  if (!sawFinish && pendingUsage === undefined) {
724
730
  yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
725
731
  return;
726
732
  }
727
733
  // Graceful close that omitted [DONE] but delivered finish_reason and/or final usage.
728
- yield { type: "done", usage: pendingUsage };
734
+ const stopReason = finishReason === "length"
735
+ ? "max_tokens"
736
+ : finishReason === "content_filter"
737
+ ? "content_filter"
738
+ : undefined;
739
+ yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
729
740
  } finally {
730
741
  reader.releaseLock();
731
742
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import type { IncomingMeta, ProviderAdapter } from "./base";
2
3
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
3
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
@@ -328,6 +329,55 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
328
329
  return !!v && typeof v === "object" && !Array.isArray(v);
329
330
  }
330
331
 
332
+ const MAX_RESPONSES_CALL_ID_LENGTH = 64;
333
+ const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
334
+ const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
335
+
336
+ /**
337
+ * The ChatGPT Responses backend rejects input `call_id` values longer than 64 characters. Codex
338
+ * sidechat/fork replay can namespace call ids from routed providers past that limit. Forward mode
339
+ * already sends explicit replay input without `previous_response_id`, so it is safe to replace each
340
+ * oversized id and every matching call/output occurrence with one deterministic request-local alias.
341
+ * Raw API-key continuations are intentionally excluded because an output-only continuation may
342
+ * reference a call stored upstream under the original id. Proxy-expanded API-key replays are
343
+ * explicit and stateless here, so they are safe to repair too.
344
+ */
345
+ function repairOversizedReplayCallIds(body: unknown): unknown {
346
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
347
+
348
+ const occupied = new Set<string>();
349
+ for (const item of body.input) {
350
+ if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
351
+ if (item.call_id.length <= MAX_RESPONSES_CALL_ID_LENGTH) occupied.add(item.call_id);
352
+ }
353
+
354
+ const aliases = new Map<string, string>();
355
+ let changed = false;
356
+ const input = body.input.map(item => {
357
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return item;
358
+ const original = item.call_id;
359
+ if (original.length <= MAX_RESPONSES_CALL_ID_LENGTH) return item;
360
+
361
+ let alias = aliases.get(original);
362
+ if (!alias) {
363
+ let salt = 0;
364
+ do {
365
+ const hashInput = salt === 0 ? original : `${original}\0${salt}`;
366
+ const digest = createHash("sha256").update(hashInput).digest("hex");
367
+ alias = `${REPAIRED_CALL_ID_PREFIX}${digest.slice(0, REPAIRED_CALL_ID_DIGEST_LENGTH)}`;
368
+ salt += 1;
369
+ } while (occupied.has(alias));
370
+ aliases.set(original, alias);
371
+ occupied.add(alias);
372
+ }
373
+
374
+ changed = true;
375
+ return { ...item, call_id: alias };
376
+ });
377
+
378
+ return changed ? { ...body, input } : body;
379
+ }
380
+
331
381
  /** Flatten a Responses tool-output `output` value (string or content-part array) to plain text. */
332
382
  function toolOutputText(output: unknown): string {
333
383
  if (typeof output === "string") return output;
@@ -515,8 +565,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
515
565
  parsed._rawBody,
516
566
  forward || parsed._previousResponseInputExpanded === true,
517
567
  );
518
- if (forward) outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
568
+ if (forward) {
569
+ outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
570
+ }
519
571
  else outBody = stripConflictingHostedTools(outBody);
572
+ if (forward || parsed._previousResponseInputExpanded === true) {
573
+ outBody = repairOversizedReplayCallIds(outBody);
574
+ }
520
575
  outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
521
576
  return {
522
577
  url,
@@ -49,9 +49,13 @@ export async function preflightAdapterEvents(
49
49
  }
50
50
  }
51
51
 
52
- export function createAdapterEventQueue(): AdapterEventQueue {
52
+ export function createAdapterEventQueue(opts?: {
53
+ maxBacklog?: number;
54
+ onBacklogExceeded?: () => void;
55
+ }): AdapterEventQueue {
53
56
  const queued: AdapterEvent[] = [];
54
57
  const readers: QueueReader[] = [];
58
+ const maxBacklog = opts?.maxBacklog ?? 1_024;
55
59
  let closed = false;
56
60
 
57
61
  const push = (event: AdapterEvent): void => {
@@ -61,6 +65,12 @@ export function createAdapterEventQueue(): AdapterEventQueue {
61
65
  reader({ done: false, value: event });
62
66
  return;
63
67
  }
68
+ if (queued.length >= maxBacklog) {
69
+ opts?.onBacklogExceeded?.();
70
+ queued.push({ type: "error", message: "consumer backlog exceeded — turn aborted" });
71
+ close();
72
+ return;
73
+ }
64
74
  queued.push(event);
65
75
  };
66
76