@bitkyc08/opencodex 2.39.0 → 2.40.0

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 (144) hide show
  1. package/README.md +49 -21
  2. package/gui/dist/assets/index-BHe2rl_C.js +112 -0
  3. package/gui/dist/assets/index-CJSb3HPe.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/cursor/catalog.ts +140 -7
  7. package/src/adapters/cursor/discovery.ts +79 -87
  8. package/src/adapters/cursor/effort-map.ts +11 -0
  9. package/src/adapters/cursor/request-builder.ts +31 -4
  10. package/src/adapters/cursor.ts +17 -0
  11. package/src/adapters/openai-chat.ts +95 -9
  12. package/src/adapters/openai-responses.ts +73 -9
  13. package/src/bridge.ts +58 -13
  14. package/src/claude/auth-mode.ts +6 -9
  15. package/src/claude/gateway-cache.ts +23 -3
  16. package/src/claude/model-info.ts +13 -2
  17. package/src/cli/access.ts +30 -0
  18. package/src/cli/account-extended.ts +19 -10
  19. package/src/cli/capabilities.ts +40 -1
  20. package/src/cli/claude-agent-startup-sync.ts +3 -0
  21. package/src/cli/claude.ts +134 -36
  22. package/src/cli/connect.ts +226 -0
  23. package/src/cli/dispatch.ts +126 -21
  24. package/src/cli/gui-pair-client.ts +170 -0
  25. package/src/cli/gui.ts +87 -0
  26. package/src/cli/help.ts +4 -1
  27. package/src/cli/index.ts +76 -22
  28. package/src/cli/models-runtime-subcommands.ts +34 -0
  29. package/src/cli/models-runtime.ts +4 -0
  30. package/src/cli/models.ts +2 -1
  31. package/src/cli/opencode.ts +2 -1
  32. package/src/cli/provider-runtime.ts +31 -1
  33. package/src/cli/registry.ts +32 -2
  34. package/src/cli/runtime-api.ts +10 -1
  35. package/src/cli/status.ts +26 -0
  36. package/src/cli/system-command.ts +9 -3
  37. package/src/client/connect.ts +650 -0
  38. package/src/client/hub-client.ts +457 -0
  39. package/src/client/hub-relay.ts +288 -0
  40. package/src/client/machine-api.ts +139 -0
  41. package/src/client/machine-auth.ts +54 -0
  42. package/src/client/machine-listener.ts +143 -0
  43. package/src/client/runtime.ts +93 -0
  44. package/src/client/state.ts +175 -0
  45. package/src/clients/config-export.ts +16 -5
  46. package/src/codex/auth-api.ts +169 -15
  47. package/src/codex/auth-context.ts +61 -6
  48. package/src/codex/catalog/aggregation.ts +17 -13
  49. package/src/codex/catalog/metadata.ts +28 -0
  50. package/src/codex/catalog/provider-fetch.ts +38 -5
  51. package/src/codex/catalog.ts +1 -1
  52. package/src/codex/desired-state.ts +13 -3
  53. package/src/codex/inject.ts +212 -31
  54. package/src/codex/journal.ts +54 -6
  55. package/src/codex/main-account.ts +105 -33
  56. package/src/codex/native-main-claim.ts +23 -1
  57. package/src/codex/native-residue.ts +7 -0
  58. package/src/codex/reset-credit-auto-redeem.ts +237 -0
  59. package/src/combos/request.ts +15 -3
  60. package/src/combos/types.ts +12 -0
  61. package/src/config/paths.ts +15 -1
  62. package/src/config/provider-validation.ts +40 -0
  63. package/src/config.ts +447 -3
  64. package/src/generated/compatibility-version.json +224 -108
  65. package/src/images/artifacts.ts +73 -37
  66. package/src/images/fulfill.ts +6 -1
  67. package/src/images/index.ts +1 -1
  68. package/src/images/plan.ts +16 -2
  69. package/src/images/synthetic-tool.ts +5 -0
  70. package/src/images/xai-client.ts +34 -1
  71. package/src/integrations/cursor-detect.ts +133 -0
  72. package/src/integrations/cursor-seen.ts +31 -0
  73. package/src/lib/errors.ts +18 -0
  74. package/src/lib/gui-pair-capability.ts +104 -0
  75. package/src/lib/lab-live-route-production.ts +2 -1
  76. package/src/lib/process-control.ts +4 -3
  77. package/src/lib/service-secrets.ts +161 -1
  78. package/src/lib/windows-system-proxy.ts +115 -0
  79. package/src/oauth/index.ts +3 -2
  80. package/src/oauth/pool-settings-capability.ts +55 -0
  81. package/src/providers/api-keys.ts +3 -1
  82. package/src/providers/codex-capacity.ts +34 -4
  83. package/src/providers/derive.ts +10 -0
  84. package/src/providers/fastwire.ts +10 -2
  85. package/src/providers/key-store.ts +197 -0
  86. package/src/providers/model-rename-migration.ts +3 -0
  87. package/src/providers/openai-sidecar.ts +3 -3
  88. package/src/providers/quota.ts +91 -42
  89. package/src/providers/registry.ts +33 -3
  90. package/src/reasoning-effort.ts +32 -0
  91. package/src/remote/protocol.ts +109 -0
  92. package/src/responses/citation-markers.ts +101 -0
  93. package/src/responses/parser.ts +43 -3
  94. package/src/responses/spill-store.ts +9 -4
  95. package/src/router.ts +72 -8
  96. package/src/server/auth-cors.ts +37 -1
  97. package/src/server/gui-session.ts +449 -0
  98. package/src/server/gui-static.ts +24 -7
  99. package/src/server/images.ts +237 -10
  100. package/src/server/index.ts +356 -30
  101. package/src/server/management/agent-settings-routes.ts +7 -1
  102. package/src/server/management/api-key-rotation.ts +74 -0
  103. package/src/server/management/combo-routes.ts +18 -6
  104. package/src/server/management/config-routes.ts +22 -3
  105. package/src/server/management/context.ts +5 -1
  106. package/src/server/management/cursor-integration-routes.ts +98 -0
  107. package/src/server/management/logs-usage-routes.ts +2 -1
  108. package/src/server/management/model-routes.ts +79 -1
  109. package/src/server/management/model-rows.ts +23 -1
  110. package/src/server/management/oauth-account-routes.ts +135 -2
  111. package/src/server/management/provider-routes.ts +143 -9
  112. package/src/server/management/route-registry.ts +10 -0
  113. package/src/server/management/session-routes.ts +13 -0
  114. package/src/server/management/system-routes.ts +13 -1
  115. package/src/server/management-api.ts +8 -3
  116. package/src/server/management-auth.ts +124 -82
  117. package/src/server/models-capabilities.ts +124 -0
  118. package/src/server/ports.ts +7 -0
  119. package/src/server/proxy-liveness.ts +7 -0
  120. package/src/server/request-log.ts +11 -5
  121. package/src/server/responses/agent-task-recovery.ts +1 -0
  122. package/src/server/responses/codex-auth-error.ts +3 -1
  123. package/src/server/responses/collaboration.ts +26 -1
  124. package/src/server/responses/combo-stream-preflight.ts +25 -6
  125. package/src/server/responses/compact.ts +32 -12
  126. package/src/server/responses/context-overflow.ts +49 -0
  127. package/src/server/responses/core.ts +196 -12
  128. package/src/server/responses/fetch-helpers.ts +2 -1
  129. package/src/server/responses/outbound-body-guard.ts +110 -0
  130. package/src/server/responses/ws-upstream.ts +87 -6
  131. package/src/server/responses-self-named-namespace-scrub.ts +181 -0
  132. package/src/server/system-env.ts +78 -25
  133. package/src/service.ts +218 -42
  134. package/src/types/config.ts +134 -0
  135. package/src/types/provider.ts +54 -1
  136. package/src/types/request.ts +2 -0
  137. package/src/types.ts +5 -0
  138. package/src/update/job.ts +33 -5
  139. package/src/usage/cost.ts +32 -1
  140. package/src/usage/expected-prices.ts +16 -0
  141. package/src/usage/log.ts +6 -2
  142. package/src/usage/summary.ts +48 -6
  143. package/gui/dist/assets/index-D-lchsPw.js +0 -112
  144. package/gui/dist/assets/index-uvENYLin.css +0 -1
@@ -37,7 +37,15 @@ import { getValidAccessToken, getOAuthCredentialProjectId } from "../oauth/index
37
37
  import { safeAntigravityHttpErrorMessage } from "../adapters/google-errors";
38
38
  import { sanitizeUpstreamErrorText } from "../adapters/upstream-http-error";
39
39
  import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
40
- import { decodeValidatedImageBase64, MAX_ENCODED_BYTES_PER_IMAGE } from "../images/artifacts";
40
+ import {
41
+ decodeValidatedImageBase64,
42
+ fetchPublicHttpsImage,
43
+ MAX_ENCODED_BYTES_PER_IMAGE,
44
+ sniffImageExtension,
45
+ type PinnedDownloadFn,
46
+ } from "../images/artifacts";
47
+ import { findXaiProvider, resolveXaiImageAuthToken } from "../images/plan";
48
+ import { callXaiImages } from "../images/xai-client";
41
49
  import type { AdmissionLease } from "../lib/admission";
42
50
  import { codexAccountSelectionForTurn } from "./lifecycle";
43
51
 
@@ -49,7 +57,8 @@ const IMAGES_UPSTREAM_TIMEOUT_MS = 300_000;
49
57
  /**
50
58
  * Cap for the buffered upstream response body (100 MiB). Images responses are JSON documents
51
59
  * containing base64-encoded images — typically a few MB. This prevents an oversized or malicious
52
- * response from exhausting process memory.
60
+ * response from exhausting process memory. The xAI `/v1/images` relay also uses this as the
61
+ * combined decoded-byte and base64-encoded output budget across the whole batch.
53
62
  */
54
63
  export const IMAGES_RESPONSE_MAX_BYTES = 100 * 1024 * 1024;
55
64
 
@@ -118,6 +127,57 @@ const CCA_BLOCKING_FINISH_REASONS: ReadonlySet<string> = new Set([
118
127
  "RECITATION",
119
128
  ]);
120
129
 
130
+ function decodedBytesFromBase64(encoded: string): number {
131
+ const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
132
+ return Math.max(0, Math.floor((encoded.length * 3) / 4) - padding);
133
+ }
134
+
135
+ /** Largest decoded payload whose base64 form still fits in `remainingEncoded`. */
136
+ function remainingRelayDecodedBytes(spentDecoded: number, spentEncoded: number): number {
137
+ const remainingDecoded = IMAGES_RESPONSE_MAX_BYTES - spentDecoded;
138
+ const remainingEncoded = IMAGES_RESPONSE_MAX_BYTES - spentEncoded;
139
+ if (remainingDecoded <= 0 || remainingEncoded < 4) return 0;
140
+ return Math.max(0, Math.min(remainingDecoded, 3 * Math.floor(remainingEncoded / 4)));
141
+ }
142
+
143
+ function wouldExceedRelayBudget(
144
+ spentDecoded: number,
145
+ spentEncoded: number,
146
+ decodedBytes: number,
147
+ encodedBytes: number,
148
+ ): boolean {
149
+ return spentDecoded + decodedBytes > IMAGES_RESPONSE_MAX_BYTES
150
+ || spentEncoded + encodedBytes > IMAGES_RESPONSE_MAX_BYTES;
151
+ }
152
+
153
+ function xaiImageOutputTooLarge(): Response {
154
+ return formatErrorResponse(
155
+ 502,
156
+ "upstream_error",
157
+ `xAI image generation output too large (exceeded ${IMAGES_RESPONSE_MAX_BYTES} bytes)`,
158
+ );
159
+ }
160
+
161
+ function xaiImageDownloadFailed(): Response {
162
+ return formatErrorResponse(502, "upstream_error", "xAI image download failed");
163
+ }
164
+
165
+ function xaiImageAuthMissing(): Response {
166
+ return formatErrorResponse(
167
+ 400,
168
+ "invalid_request_error",
169
+ "xAI Imagine relay is enabled but no usable Grok CLI OAuth token or xAI API key was found. "
170
+ + "Run `ocx login xai` or set an xAI API key. The request was not forwarded to ChatGPT.",
171
+ );
172
+ }
173
+
174
+ /** Test seam: inject a pinned HTTPS GET so relay tests never open a real socket. */
175
+ let xaiResultPinnedDownload: PinnedDownloadFn | undefined;
176
+
177
+ export function setXaiResultPinnedDownloadForTests(fn: PinnedDownloadFn | undefined): void {
178
+ xaiResultPinnedDownload = fn;
179
+ }
180
+
121
181
  /**
122
182
  * Race a promise against an abort signal. If the signal aborts first, reject
123
183
  * immediately — our code stops awaiting the underlying operation even though
@@ -372,6 +432,166 @@ async function tryCcaImageGeneration(
372
432
  }
373
433
  }
374
434
 
435
+ /**
436
+ * Codex's client-side image_gen POSTs here. When the Grok Imagine bridge is
437
+ * opted in, send that request to api.x.ai instead of ChatGPT.
438
+ */
439
+ async function tryXaiImageRelay(
440
+ body: unknown,
441
+ config: OcxConfig,
442
+ logCtx: RequestLogContext,
443
+ signal: AbortSignal | undefined,
444
+ endpoint: ImagesEndpoint,
445
+ ): Promise<Response | undefined> {
446
+ if (config.images?.bridgeEnabled !== true) return undefined;
447
+ const found = findXaiProvider(config);
448
+ if (!found) return undefined;
449
+ const obj = body && typeof body === "object" && !Array.isArray(body)
450
+ ? body as Record<string, unknown>
451
+ : {};
452
+ const prompt = typeof obj.prompt === "string" ? obj.prompt
453
+ : typeof obj.input === "string" ? obj.input
454
+ : "";
455
+ if (!prompt.trim()) {
456
+ return formatErrorResponse(400, "invalid_request_error", "image generation requires a prompt");
457
+ }
458
+ const n = typeof obj.n === "number" && Number.isFinite(obj.n) ? Math.max(1, Math.min(4, Math.floor(obj.n))) : 1;
459
+ const size = typeof obj.size === "string" ? obj.size : undefined;
460
+ const quality = typeof obj.quality === "string" ? obj.quality : undefined;
461
+ const aspectRatio = typeof obj.aspect_ratio === "string" ? obj.aspect_ratio : undefined;
462
+ let imageUrl: string | undefined;
463
+ if (endpoint === "edits") {
464
+ const images = obj.images;
465
+ const first = Array.isArray(images) ? images[0] : undefined;
466
+ if (typeof obj.image === "string") imageUrl = obj.image;
467
+ else if (typeof obj.image_url === "string") imageUrl = obj.image_url;
468
+ else if (first && typeof first === "object" && first !== null) {
469
+ const rec = first as Record<string, unknown>;
470
+ if (typeof rec.image_url === "string") imageUrl = rec.image_url;
471
+ else if (typeof rec.url === "string") imageUrl = rec.url;
472
+ }
473
+ if (!imageUrl?.trim()) {
474
+ return formatErrorResponse(400, "invalid_request_error", "image edits require an image URL");
475
+ }
476
+ imageUrl = imageUrl.trim();
477
+ }
478
+ const timeoutMs = config.images?.timeoutMs ?? IMAGES_UPSTREAM_TIMEOUT_MS;
479
+ const linkedSignal = signalWithTimeout(timeoutMs, signal);
480
+ try {
481
+ let token: string | undefined;
482
+ try {
483
+ token = await abortableRace(resolveXaiImageAuthToken(found.provider), linkedSignal.signal);
484
+ } catch {
485
+ if (signal?.aborted) {
486
+ return formatErrorResponse(499, "client_closed_request", `image ${endpoint} request canceled by client`);
487
+ }
488
+ if (linkedSignal.signal.aborted) {
489
+ return formatErrorResponse(504, "upstream_error", `xAI image ${endpoint} timed out during authentication`);
490
+ }
491
+ return xaiImageAuthMissing();
492
+ }
493
+ if (!token) return xaiImageAuthMissing();
494
+ logCtx.provider = "xai";
495
+ logCtx.model = config.images?.bridgeModel ?? "grok-imagine-image-quality";
496
+ const result = await callXaiImages(
497
+ {
498
+ prompt,
499
+ model: logCtx.model,
500
+ n,
501
+ size,
502
+ quality,
503
+ aspectRatio,
504
+ imageUrl,
505
+ },
506
+ { baseUrl: "https://api.x.ai/v1", token },
507
+ linkedSignal.signal,
508
+ timeoutMs,
509
+ );
510
+ const data: Array<{ b64_json: string }> = [];
511
+ let spentDecoded = 0;
512
+ let spentEncoded = 0;
513
+ for (const img of result.images) {
514
+ if (typeof img.b64_json === "string" && img.b64_json) {
515
+ const encodedBytes = img.b64_json.length;
516
+ if (encodedBytes > MAX_ENCODED_BYTES_PER_IMAGE) {
517
+ return formatErrorResponse(502, "upstream_error", "xAI image payload exceeds per-image size cap");
518
+ }
519
+ try {
520
+ decodeValidatedImageBase64(img.b64_json);
521
+ } catch {
522
+ return formatErrorResponse(502, "upstream_error", "xAI image payload failed base64/magic validation");
523
+ }
524
+ const decodedBytes = decodedBytesFromBase64(img.b64_json);
525
+ if (wouldExceedRelayBudget(spentDecoded, spentEncoded, decodedBytes, encodedBytes)) {
526
+ return xaiImageOutputTooLarge();
527
+ }
528
+ data.push({ b64_json: img.b64_json });
529
+ spentDecoded += decodedBytes;
530
+ spentEncoded += encodedBytes;
531
+ continue;
532
+ }
533
+ if (typeof img.url !== "string" || !img.url) continue;
534
+ const remaining = remainingRelayDecodedBytes(spentDecoded, spentEncoded);
535
+ if (remaining <= 0) return xaiImageOutputTooLarge();
536
+ let fetched: Response;
537
+ try {
538
+ fetched = await fetchPublicHttpsImage(img.url, {
539
+ signal: linkedSignal.signal,
540
+ pinnedDownload: xaiResultPinnedDownload,
541
+ maxBytes: remaining,
542
+ });
543
+ } catch (err) {
544
+ if (signal?.aborted || linkedSignal.signal.aborted) throw err;
545
+ return xaiImageDownloadFailed();
546
+ }
547
+ const observed = await readImageResponseBytes(fetched, {
548
+ maxBytes: remaining,
549
+ signal: linkedSignal.signal,
550
+ });
551
+ if (observed.oversized) return xaiImageOutputTooLarge();
552
+ if (observed.bytes.byteLength === 0) continue;
553
+ if (!sniffImageExtension(observed.bytes)) return xaiImageDownloadFailed();
554
+ const decodedBytes = observed.bytes.byteLength;
555
+ const b64 = Buffer.from(observed.bytes).toString("base64");
556
+ if (wouldExceedRelayBudget(spentDecoded, spentEncoded, decodedBytes, b64.length)) {
557
+ return xaiImageOutputTooLarge();
558
+ }
559
+ data.push({ b64_json: b64 });
560
+ spentDecoded += decodedBytes;
561
+ spentEncoded += b64.length;
562
+ }
563
+ if (data.length === 0) {
564
+ return formatErrorResponse(502, "upstream_error", "xAI image generation returned no usable images");
565
+ }
566
+ return new Response(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }), {
567
+ status: 200,
568
+ headers: { "content-type": "application/json" },
569
+ });
570
+ } catch (err) {
571
+ if (signal?.aborted) {
572
+ return formatErrorResponse(499, "client_closed_request", `image ${endpoint} request canceled by client`);
573
+ }
574
+ if (linkedSignal.signal.aborted || (err instanceof Error && err.name === "TimeoutError")) {
575
+ return formatErrorResponse(504, "upstream_error", `xAI image ${endpoint} timed out`);
576
+ }
577
+ const status = typeof err === "object" && err && "status" in err && typeof (err as { status: unknown }).status === "number"
578
+ ? (err as { status: number }).status
579
+ : 502;
580
+ const message = err instanceof Error ? err.message : String(err);
581
+ const safeMessage = sanitizeUpstreamErrorText(message).replace(
582
+ /https?:\/\/[^\s"'<>]+/gi,
583
+ "[upstream-url]",
584
+ );
585
+ return formatErrorResponse(
586
+ status >= 400 && status < 600 ? status : 502,
587
+ "upstream_error",
588
+ `xAI image ${endpoint} failed: ${safeMessage}`,
589
+ );
590
+ } finally {
591
+ linkedSignal.cleanup();
592
+ }
593
+ }
594
+
375
595
  export async function handleImages(
376
596
  req: Request,
377
597
  config: OcxConfig,
@@ -379,7 +599,22 @@ export async function handleImages(
379
599
  logCtx: RequestLogContext,
380
600
  turnAdmissionLease?: AdmissionLease,
381
601
  ): Promise<Response> {
602
+ let body: unknown;
603
+ try {
604
+ body = await readJsonRequestBody(req);
605
+ } catch (err) {
606
+ return decodeRequestErrorResponse(err, "images");
607
+ }
608
+ const model = (body as { model?: unknown } | null)?.model;
609
+ if (typeof model === "string" && model) logCtx.model = model;
610
+
382
611
  const candidates = selectImagesProvider(config);
612
+ // Explicit images.provider owns the route, including its validation errors.
613
+ // Do not divert that selection to the xAI Imagine relay.
614
+ if (config.images?.provider === undefined) {
615
+ const xaiRelay = await tryXaiImageRelay(body, config, logCtx, req.signal, endpoint);
616
+ if (xaiRelay) return xaiRelay;
617
+ }
383
618
  if (candidates.error) {
384
619
  return formatErrorResponse(400, "invalid_request_error", candidates.error);
385
620
  }
@@ -398,14 +633,6 @@ export async function handleImages(
398
633
  }
399
634
  }
400
635
  }
401
- let body: unknown;
402
- try {
403
- body = await readJsonRequestBody(req);
404
- } catch (err) {
405
- return decodeRequestErrorResponse(err, "images");
406
- }
407
- const model = (body as { model?: unknown } | null)?.model;
408
- if (typeof model === "string" && model) logCtx.model = model;
409
636
 
410
637
  const canUseOpenAiForward = !skipOpenAiForwardForAdmissionBearer && candidates.forwardCandidates.length > 0;
411
638