@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 (173) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { IncomingMeta, ProviderAdapter } from "./base";
3
- import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
3
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
4
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
5
  import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
6
+ import { collectResponsesToolGroups } from "../responses/tool-groups";
6
7
  import { decodeServerSentEvents } from "../lib/sse-decoder";
7
8
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
8
9
  import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
@@ -500,72 +501,252 @@ function stripUnsupportedForwardParams(body: unknown): unknown {
500
501
  return rest;
501
502
  }
502
503
 
504
+ const IMAGE_GEN_NAMESPACE = "image_gen";
505
+ const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
506
+ const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
507
+ const IMAGE_GEN_WIRE_PREFIX = `${IMAGE_GEN_NAMESPACE}__`;
508
+
509
+ /** Remove a supported client prefix before constructing the canonical image-gen wire alias. */
510
+ function imageGenLocalName(name: string): string {
511
+ if (name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) return name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
512
+ if (name.startsWith(IMAGE_GEN_WIRE_PREFIX)) return name.slice(IMAGE_GEN_WIRE_PREFIX.length);
513
+ return name;
514
+ }
515
+
516
+ /** Build the flat public-Responses name used only on the upstream wire. */
517
+ function imageGenWireName(name: string): string {
518
+ return namespacedToolName(IMAGE_GEN_NAMESPACE, imageGenLocalName(name));
519
+ }
520
+
521
+ /** Match client image-gen declarations across namespace, legacy dotted, and canonical wire forms. */
522
+ function isImageGenClientName(name: string): boolean {
523
+ return name === IMAGE_GEN_NAMESPACE
524
+ || name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
525
+ || name.startsWith(IMAGE_GEN_WIRE_PREFIX);
526
+ }
527
+
528
+ /** Identify declarations that should activate image-gen request normalization. */
529
+ function declaresImageGenClientTool(tool: unknown): boolean {
530
+ if (!isPlainObject(tool) || typeof tool.name !== "string") return false;
531
+ if (tool.type === "namespace") return tool.name === IMAGE_GEN_NAMESPACE;
532
+ return isImageGenClientName(tool.name);
533
+ }
534
+
503
535
  /**
504
- * Hosted tool types whose server-side function names collide with the client tools Codex
505
- * declares for the matching app skill. Codex sends BOTH (e.g. hosted `image_generation` plus a
506
- * declared `image_gen.imagegen` function/namespace tool for the imagegen skill). The ChatGPT
507
- * backend tolerates the pair, but the platform `/v1/responses` rejects it:
508
- * `Invalid Value: 'tools'. Function 'image_gen.imagegen' conflicts with a hosted tool in the
509
- * same request.` Keyed hosted-type conflicting client tool-name prefix; the hosted entry is
510
- * dropped (the declared tool wins Codex executes the skill client-side either way).
536
+ * Lower one complete Codex image-gen namespace to public Responses function tools.
537
+ *
538
+ * The public API reserves the `image_gen` namespace and restricts function names to a flat safe
539
+ * alphabet. `image_gen__<tool>` is therefore an upstream-only alias; client-facing responses are
540
+ * restored to explicit `{ namespace: "image_gen", name: "<tool>" }` calls by the server. Only a
541
+ * non-empty namespace containing named function tools is safe to lower. Malformed, empty, and
542
+ * future namespace shapes stay untouched instead of silently losing client capabilities.
511
543
  */
512
- const HOSTED_TOOL_NAME_CONFLICTS: ReadonlyArray<{ hostedType: string; namePrefix: string }> = [
513
- { hostedType: "image_generation", namePrefix: "image_gen" },
514
- ];
544
+ function flattenImageGenNamespace(tool: unknown): Record<string, unknown>[] | undefined {
545
+ if (
546
+ !isPlainObject(tool)
547
+ || tool.type !== "namespace"
548
+ || tool.name !== IMAGE_GEN_NAMESPACE
549
+ || !Array.isArray(tool.tools)
550
+ || tool.tools.length === 0
551
+ ) return undefined;
552
+
553
+ for (const innerTool of tool.tools) {
554
+ if (
555
+ !isPlainObject(innerTool)
556
+ || innerTool.type !== "function"
557
+ || typeof innerTool.name !== "string"
558
+ || innerTool.name.length === 0
559
+ ) return undefined;
560
+ }
561
+
562
+ return tool.tools.map(innerTool => {
563
+ const functionTool = innerTool as Record<string, unknown> & { name: string };
564
+ return {
565
+ ...functionTool,
566
+ name: imageGenWireName(functionTool.name),
567
+ };
568
+ });
569
+ }
570
+
571
+ /** Convert a legacy dotted function declaration while preserving all other function metadata. */
572
+ function normalizeFlatImageGenFunction(tool: unknown): unknown {
573
+ if (
574
+ !isPlainObject(tool)
575
+ || tool.type !== "function"
576
+ || typeof tool.name !== "string"
577
+ || !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
578
+ ) return tool;
579
+ return { ...tool, name: imageGenWireName(tool.name) };
580
+ }
581
+
582
+ /** Return the image-gen function name used for stable cross-container deduplication. */
583
+ function imageGenFunctionName(tool: unknown): string | undefined {
584
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
585
+ return undefined;
586
+ }
587
+ return isImageGenClientName(tool.name) ? tool.name : undefined;
588
+ }
589
+
590
+ /** True only when a declaration can yield a callable upstream-safe image-gen function alias. */
591
+ function declaresUsableImageGenAlias(tool: unknown): boolean {
592
+ if (flattenImageGenNamespace(tool)) return true;
593
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
594
+ return false;
595
+ }
596
+ if (tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
597
+ return tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length;
598
+ }
599
+ return tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
600
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length;
601
+ }
602
+
603
+ /** Collect client tool-choice names and the exact upstream aliases declared for them. */
604
+ function imageGenToolChoiceAliases(toolGroups: unknown[][]): Map<string, string> {
605
+ const aliases = new Map<string, string>();
606
+
607
+ for (const group of toolGroups) {
608
+ for (const tool of group) {
609
+ const flattened = flattenImageGenNamespace(tool);
610
+ if (flattened) {
611
+ for (const candidate of flattened) {
612
+ const wireName = candidate.name as string;
613
+ aliases.set(`${IMAGE_GEN_DOTTED_PREFIX}${imageGenLocalName(wireName)}`, wireName);
614
+ aliases.set(wireName, wireName);
615
+ }
616
+ continue;
617
+ }
618
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
619
+ continue;
620
+ }
621
+ if (
622
+ tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
623
+ && tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length
624
+ ) {
625
+ aliases.set(tool.name, imageGenWireName(tool.name));
626
+ } else if (
627
+ tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
628
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length
629
+ ) {
630
+ aliases.set(tool.name, tool.name);
631
+ }
632
+ }
633
+ }
634
+
635
+ return aliases;
636
+ }
637
+
638
+ /** Rewrite function selectors only when their corresponding declaration receives a wire alias. */
639
+ function normalizeImageGenToolChoice(
640
+ toolChoice: unknown,
641
+ aliases: ReadonlyMap<string, string>,
642
+ ): unknown {
643
+ if (!isPlainObject(toolChoice)) return toolChoice;
644
+
645
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
646
+ const alias = aliases.get(toolChoice.name);
647
+ return alias && alias !== toolChoice.name ? { ...toolChoice, name: alias } : toolChoice;
648
+ }
649
+
650
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
651
+ let changed = false;
652
+ const tools = toolChoice.tools.map(tool => {
653
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
654
+ return tool;
655
+ }
656
+ const alias = aliases.get(tool.name);
657
+ if (!alias || alias === tool.name) return tool;
658
+ changed = true;
659
+ return { ...tool, name: alias };
660
+ });
661
+ return changed ? { ...toolChoice, tools } : toolChoice;
662
+ }
663
+
664
+ /** Identify replayed image-gen calls that require upstream wire encoding. */
665
+ function declaresImageGenFunctionCall(item: unknown): boolean {
666
+ if (!isPlainObject(item) || item.type !== "function_call" || typeof item.name !== "string") {
667
+ return false;
668
+ }
669
+ return item.namespace === IMAGE_GEN_NAMESPACE || isImageGenClientName(item.name);
670
+ }
671
+
672
+ /** Encode native or legacy replay calls to the same flat name used by tool declarations. */
673
+ function normalizeImageGenFunctionCall(item: unknown): unknown {
674
+ if (!declaresImageGenFunctionCall(item) || !isPlainObject(item) || typeof item.name !== "string") {
675
+ return item;
676
+ }
677
+ if (item.namespace === IMAGE_GEN_NAMESPACE) {
678
+ const { namespace: _namespace, ...rest } = item;
679
+ return { ...rest, name: imageGenWireName(item.name) };
680
+ }
681
+ if (item.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
682
+ return { ...item, name: imageGenWireName(item.name) };
683
+ }
684
+ return item;
685
+ }
515
686
 
516
687
  /**
517
- * Drop hosted tools whose names collide with declared function/namespace tools (see
518
- * HOSTED_TOOL_NAME_CONFLICTS). Only applies on the API-key platform path: the ChatGPT backend
519
- * ("forward" mode) accepts the pair, and stripping there would disable native imagegen. No-op
520
- * (returns the original reference) when nothing matches.
688
+ * Normalize Codex's private image-gen tool declaration for API-key Responses providers.
521
689
  *
522
- * Codex Desktop Responses Lite requests do not always use top-level `body.tools`; they may place
523
- * the same declarations in `body.input[]` entries of type `additional_tools`. The platform validates
524
- * these containers as one tool namespace, so conflict detection must span all of them. The function
525
- * uses copy-on-write semantics and preserves the original request reference when nothing is removed.
690
+ * A complete `image_gen` namespace is flattened to safe `image_gen__<tool>` aliases even when it is
691
+ * the only image tool in the request. Replayed client calls are encoded to the same alias, including
692
+ * legacy dotted calls from older compatibility attempts. When a usable alias replaces a client
693
+ * image-gen declaration, the duplicate hosted `image_generation` entry is removed. Duplicate aliases
694
+ * are resolved in stable container order: top-level tools first, then Responses Lite
695
+ * `additional_tools` entries.
696
+ *
697
+ * This function is called only on the API-key path. ChatGPT forward mode understands the private
698
+ * namespace and must keep it. Copy-on-write preserves the original request reference when no
699
+ * namespace is flattened, hosted tool removed, or duplicate function discarded.
526
700
  */
527
- function stripConflictingHostedTools(body: unknown): unknown {
701
+ function normalizeImageGenClientTools(body: unknown): unknown {
528
702
  if (!isPlainObject(body)) return body;
529
703
 
530
- // Collect every tool container in the request. Traditional HTTP/SSE requests use top-level
531
- // `tools`, while Responses Lite may carry the same declarations in `additional_tools` entries.
532
- const toolGroups: unknown[][] = [];
533
- if (Array.isArray(body.tools)) toolGroups.push(body.tools);
534
- if (Array.isArray(body.input)) {
535
- for (const item of body.input) {
536
- if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
537
- toolGroups.push(item.tools);
704
+ const toolGroups = collectResponsesToolGroups(body);
705
+ const hasImageGenClientTool = toolGroups.some(group => group.some(declaresImageGenClientTool))
706
+ || (Array.isArray(body.input) && body.input.some(declaresImageGenFunctionCall));
707
+ if (!hasImageGenClientTool) return body;
708
+ const hasUsableImageGenAlias = toolGroups.some(group => group.some(declaresUsableImageGenAlias));
709
+ const toolChoiceAliases = imageGenToolChoiceAliases(toolGroups);
710
+
711
+ const seenFunctionNames = new Set<string>();
712
+ const normalizeGroup = (tools: unknown[]): unknown[] => {
713
+ const normalized: unknown[] = [];
714
+ let groupChanged = false;
715
+
716
+ for (const tool of tools) {
717
+ if (
718
+ hasUsableImageGenAlias
719
+ && isPlainObject(tool)
720
+ && tool.type === HOSTED_IMAGE_GENERATION_TOOL
721
+ ) {
722
+ groupChanged = true;
723
+ continue;
724
+ }
725
+
726
+ const flattened = flattenImageGenNamespace(tool);
727
+ const candidates = flattened ?? [tool];
728
+ if (flattened) groupChanged = true;
729
+
730
+ for (const candidate of candidates) {
731
+ const normalizedCandidate = normalizeFlatImageGenFunction(candidate);
732
+ if (normalizedCandidate !== candidate) groupChanged = true;
733
+ const functionName = imageGenFunctionName(normalizedCandidate);
734
+ if (functionName && seenFunctionNames.has(functionName)) {
735
+ groupChanged = true;
736
+ continue;
737
+ }
738
+ if (functionName) seenFunctionNames.add(functionName);
739
+ normalized.push(normalizedCandidate);
538
740
  }
539
741
  }
540
- }
541
- if (toolGroups.length === 0) return body;
542
-
543
- // Detect conflicts across containers because the client namespace and the hosted tool may be
544
- // declared in different groups while still sharing one platform-validated namespace.
545
- const conflicting = HOSTED_TOOL_NAME_CONFLICTS.filter(c => toolGroups.some(group =>
546
- group.some(t => {
547
- if (!isPlainObject(t) || typeof t.name !== "string") return false;
548
- if (t.type === "namespace") return t.name === c.namePrefix;
549
- return t.name === c.namePrefix || t.name.startsWith(`${c.namePrefix}.`);
550
- }),
551
- ));
552
- if (conflicting.length === 0) return body;
553
-
554
- // Allocate a replacement only when a hosted tool is removed. Preserve malformed entries,
555
- // unrelated tools, and future tool types so this compatibility rule remains narrowly scoped.
556
- const stripGroup = (tools: unknown[]): unknown[] => {
557
- const filtered = tools.filter(t => {
558
- const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
559
- if (!type) return true;
560
- return !conflicting.some(c => c.hostedType === type);
561
- });
562
- return filtered.length === tools.length ? tools : filtered;
742
+
743
+ return groupChanged ? normalized : tools;
563
744
  };
564
745
 
565
746
  let changed = false;
566
747
  let tools = body.tools;
567
748
  if (Array.isArray(body.tools)) {
568
- tools = stripGroup(body.tools);
749
+ tools = normalizeGroup(body.tools);
569
750
  changed ||= tools !== body.tools;
570
751
  }
571
752
 
@@ -573,13 +754,15 @@ function stripConflictingHostedTools(body: unknown): unknown {
573
754
  if (Array.isArray(body.input)) {
574
755
  let nestedChanged = false;
575
756
  const mappedInput = body.input.map(item => {
576
- if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
577
- return item;
757
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
758
+ const nestedTools = normalizeGroup(item.tools);
759
+ if (nestedTools === item.tools) return item;
760
+ nestedChanged = true;
761
+ return { ...item, tools: nestedTools };
578
762
  }
579
- const nestedTools = stripGroup(item.tools);
580
- if (nestedTools === item.tools) return item;
581
- nestedChanged = true;
582
- return { ...item, tools: nestedTools };
763
+ const normalizedCall = normalizeImageGenFunctionCall(item);
764
+ if (normalizedCall !== item) nestedChanged = true;
765
+ return normalizedCall;
583
766
  });
584
767
  if (nestedChanged) {
585
768
  input = mappedInput;
@@ -587,11 +770,15 @@ function stripConflictingHostedTools(body: unknown): unknown {
587
770
  }
588
771
  }
589
772
 
773
+ const toolChoice = normalizeImageGenToolChoice(body.tool_choice, toolChoiceAliases);
774
+ changed ||= toolChoice !== body.tool_choice;
775
+
590
776
  if (!changed) return body;
591
777
  return {
592
778
  ...body,
593
779
  ...(Array.isArray(body.tools) ? { tools } : {}),
594
780
  ...(Array.isArray(body.input) ? { input } : {}),
781
+ ...(Object.prototype.hasOwnProperty.call(body, "tool_choice") ? { tool_choice: toolChoice } : {}),
595
782
  };
596
783
  }
597
784
 
@@ -740,7 +927,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
740
927
  outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
741
928
  outBody = stripUnsupportedForwardParams(outBody);
742
929
  }
743
- else outBody = stripConflictingHostedTools(outBody);
930
+ else outBody = normalizeImageGenClientTools(outBody);
744
931
  if (forward || parsed._previousResponseInputExpanded === true) {
745
932
  outBody = repairOversizedReplayCallIds(outBody);
746
933
  }
@@ -27,6 +27,7 @@ export interface ClaudeAgentDef {
27
27
  name: string;
28
28
  model: string;
29
29
  description: string;
30
+ effort?: NonNullable<OcxConfig["claudeCode"]>["subagentEffort"];
30
31
  blockedSkills: readonly string[];
31
32
  }
32
33
 
@@ -99,6 +100,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
99
100
  name: `${OWNED_PREFIX}${unique}`,
100
101
  model,
101
102
  description,
103
+ effort: config.claudeCode?.subagentEffort,
102
104
  blockedSkills: blockedSkillsFor(model),
103
105
  });
104
106
  };
@@ -124,6 +126,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
124
126
  name: `${OWNED_PREFIX}self`,
125
127
  model: marked,
126
128
  description: `Self-clone: delegate to your default main model (${marked}), synced from the /model picker at launch. ${NO_MODEL_ARG}`,
129
+ effort: config.claudeCode?.subagentEffort,
127
130
  blockedSkills: blockedSkillsFor(marked),
128
131
  });
129
132
  }
@@ -149,6 +152,7 @@ function renderAgentDef(def: ClaudeAgentDef): string {
149
152
  `name: ${JSON.stringify(def.name)}`,
150
153
  `description: ${JSON.stringify(def.description)}`,
151
154
  `model: ${JSON.stringify(def.model)}`,
155
+ ...(def.effort ? [`effort: ${JSON.stringify(def.effort)}`] : []),
152
156
  "---",
153
157
  "",
154
158
  `<!-- ${GENERATED_MARKER} -->`,
@@ -157,6 +161,7 @@ function renderAgentDef(def: ClaudeAgentDef): string {
157
161
  // BODY rides the subagent's system prompt verbatim. The proxy detects this
158
162
  // directive and overrides the request model before routing/passthrough.
159
163
  `<!-- ocx-route: ${def.model} -->`,
164
+ ...(def.effort ? [`<!-- ocx-effort: ${def.effort} -->`] : []),
160
165
  "",
161
166
  `You are a delegated worker running on \`${def.model}\` through the local opencodex proxy.`,
162
167
  `IDENTITY: your ACTUAL underlying model is \`${def.model}\` — the opencodex proxy routes this`,
@@ -7,9 +7,19 @@
7
7
  * deterministic, reversible, and STABLE across releases (picker selections
8
8
  * persist to Claude Code's settings.json `model` field).
9
9
  *
10
+ * Versioned prefixes:
11
+ * - `claude-ocx-` (v1) — legacy / plain model ids with no `/` or `~`. Decode
12
+ * is literal (no escape expansion), so a persisted model id that literally
13
+ * contained the two-char sequences `~s` / `~t` keeps resolving.
14
+ * - `claude-ocx2-` (v2) — used whenever the model id needs escape encoding
15
+ * (`/` → `~s`, `~` → `~t`). Decode expands those escapes. New slash/tilde
16
+ * models always mint v2 so they cannot collide with v1 literals.
17
+ *
10
18
  * Reversibility rules:
11
19
  * - providers containing `--` or `/` are not aliased (split boundary safety);
12
- * - model ids containing `/` are not aliased (would be ambiguous on resolve);
20
+ * - model ids MAY contain `/` or `~` minted under the v2 prefix with escapes
21
+ * (e.g. openrouter `anthropic/claude-opus-4-8` →
22
+ * `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`);
13
23
  * - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
14
24
  * - native OpenAI slugs use the pseudo-provider `native` and resolve back to
15
25
  * the bare slug; a real provider named "native" is therefore never aliased.
@@ -17,20 +27,82 @@
17
27
 
18
28
  import { desktop3pAlias } from "./desktop-3p";
19
29
 
20
- export const CLAUDE_ALIAS_PREFIX = "claude-ocx-";
30
+ /** Legacy / plain readable prefix (literal model portion on decode). */
31
+ export const CLAUDE_ALIAS_PREFIX_V1 = "claude-ocx-";
32
+ /** Escape-encoded readable prefix (`~s`/`~t` expanded on decode). */
33
+ export const CLAUDE_ALIAS_PREFIX_V2 = "claude-ocx2-";
34
+ /**
35
+ * Current write prefix for plain (unescaped) model ids.
36
+ * Escape-needing models mint {@link CLAUDE_ALIAS_PREFIX_V2} instead.
37
+ */
38
+ export const CLAUDE_ALIAS_PREFIX = CLAUDE_ALIAS_PREFIX_V1;
39
+
40
+ /** Encoded `/` inside the model portion of a v2 Claude Code alias. */
41
+ const CLAUDE_ALIAS_SLASH_ENC = "~s";
42
+ /** Encoded literal `~` inside the model portion of a v2 Claude Code alias. */
43
+ const CLAUDE_ALIAS_TILDE_ENC = "~t";
21
44
  const NATIVE_PSEUDO_PROVIDER = "native";
22
45
 
46
+ function modelNeedsEscapeEncoding(modelId: string): boolean {
47
+ return modelId.includes("/") || modelId.includes("~");
48
+ }
49
+
50
+ function encodeModelId(modelId: string): string {
51
+ // Escape literal tildes first so slash encoding cannot create ambiguity.
52
+ return modelId
53
+ .replaceAll("~", CLAUDE_ALIAS_TILDE_ENC)
54
+ .replaceAll("/", CLAUDE_ALIAS_SLASH_ENC);
55
+ }
56
+
57
+ function decodeEscapedModelId(encoded: string): string {
58
+ let out = "";
59
+ for (let i = 0; i < encoded.length; i++) {
60
+ if (encoded[i] === "~" && i + 1 < encoded.length) {
61
+ const next = encoded[i + 1];
62
+ if (next === "s") {
63
+ out += "/";
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (next === "t") {
68
+ out += "~";
69
+ i += 1;
70
+ continue;
71
+ }
72
+ }
73
+ out += encoded[i];
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function splitAlias(id: string, prefix: string): { provider: string; model: string } | null {
79
+ const rest = id.slice(prefix.length);
80
+ const sep = rest.indexOf("--");
81
+ if (sep <= 0) return null;
82
+ const provider = rest.slice(0, sep);
83
+ const model = rest.slice(sep + 2);
84
+ if (!provider || !model) return null;
85
+ return { provider, model };
86
+ }
87
+
23
88
  /** Alias for a routed "<provider>/<model>" pair; null when not representable. */
24
89
  export function aliasForRoute(provider: string, modelId: string): string | null {
25
90
  if (!provider || provider.includes("--") || provider.includes("/") || provider === NATIVE_PSEUDO_PROVIDER) return null;
26
- if (!modelId || modelId.includes("/")) return null;
27
- return `${CLAUDE_ALIAS_PREFIX}${provider}--${modelId}`;
91
+ if (!modelId) return null;
92
+ if (modelNeedsEscapeEncoding(modelId)) {
93
+ return `${CLAUDE_ALIAS_PREFIX_V2}${provider}--${encodeModelId(modelId)}`;
94
+ }
95
+ return `${CLAUDE_ALIAS_PREFIX_V1}${provider}--${modelId}`;
28
96
  }
29
97
 
30
98
  /** Alias for a native OpenAI slug (bare model id, no provider namespace). */
31
99
  export function aliasForNative(slug: string): string | null {
100
+ // Reject "/" — native ids are bare slugs. Literal `~` is fine via v2 + ~t.
32
101
  if (!slug || slug.includes("/") || slug.includes("--")) return null;
33
- return `${CLAUDE_ALIAS_PREFIX}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
102
+ if (modelNeedsEscapeEncoding(slug)) {
103
+ return `${CLAUDE_ALIAS_PREFIX_V2}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
104
+ }
105
+ return `${CLAUDE_ALIAS_PREFIX_V1}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
34
106
  }
35
107
 
36
108
  /**
@@ -38,20 +110,28 @@ export function aliasForNative(slug: string): string | null {
38
110
  * routed -> "<provider>/<model>", native -> bare slug. Null when not an alias.
39
111
  */
40
112
  export function resolveAlias(id: string): string | null {
41
- if (!id.startsWith(CLAUDE_ALIAS_PREFIX)) return null;
42
- const rest = id.slice(CLAUDE_ALIAS_PREFIX.length);
43
- const sep = rest.indexOf("--");
44
- if (sep <= 0) return null;
45
- const provider = rest.slice(0, sep);
46
- const model = rest.slice(sep + 2);
47
- if (!model) return null;
48
- return provider === NATIVE_PSEUDO_PROVIDER ? model : `${provider}/${model}`;
113
+ // Check v2 before v1 for clarity (prefixes are disjoint: ocx2 vs ocx-).
114
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_V2)) {
115
+ const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V2);
116
+ if (!parts) return null;
117
+ const model = decodeEscapedModelId(parts.model);
118
+ if (!model) return null;
119
+ return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
120
+ }
121
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1)) {
122
+ const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V1);
123
+ if (!parts) return null;
124
+ // Literal decode — preserves pre-escape aliases whose model id contained
125
+ // the two-char sequences ~s / ~t.
126
+ return parts.provider === NATIVE_PSEUDO_PROVIDER ? parts.model : `${parts.provider}/${parts.model}`;
127
+ }
128
+ return null;
49
129
  }
50
130
 
51
131
  /**
52
132
  * Claude Code (CLI) surface alias — devlog 050 + audit 051 #2.
53
133
  *
54
- * The readable `claude-ocx-*` form when representable; otherwise the desktop-3p
134
+ * The readable `claude-ocx*` form when representable; otherwise the desktop-3p
55
135
  * hash so the model still appears in discovery (collisions follow the same
56
136
  * first-wins policy as the desktop registry — audit 051 #1). Real Anthropic
57
137
  * models pass through unchanged (they must keep hitting the sk-ant passthrough).
@@ -147,23 +147,40 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
147
147
  * FIRST directive wins; the scan is bounded to the system field.
148
148
  */
149
149
  const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
150
+ const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
150
151
 
151
- export function extractOcxRouteDirective(body: unknown): string | null {
152
+ function systemText(body: unknown): string | null {
152
153
  if (!isRec(body)) return null;
153
154
  const system = body.system;
154
- let text: string | undefined;
155
- if (typeof system === "string") text = system;
156
- else if (Array.isArray(system)) {
157
- text = system
158
- .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
159
- .map(b => b.text as string)
160
- .join("\n");
161
- }
155
+ if (typeof system === "string") return system || null;
156
+ if (!Array.isArray(system)) return null;
157
+ const text = system
158
+ .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
159
+ .map(b => b.text as string)
160
+ .join("\n");
161
+ return text || null;
162
+ }
163
+
164
+ export function extractOcxRouteDirective(body: unknown): string | null {
165
+ const text = systemText(body);
162
166
  if (!text) return null;
163
167
  const match = OCX_ROUTE_RE.exec(text);
164
168
  return match ? match[1]! : null;
165
169
  }
166
170
 
171
+ /**
172
+ * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
173
+ * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
174
+ * exact generated-agent setting through the same trusted system-body channel as
175
+ * ocx-route so the inbound translator can restore `output_config.effort`.
176
+ */
177
+ export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
178
+ const text = systemText(body);
179
+ if (!text) return null;
180
+ const match = OCX_EFFORT_RE.exec(text);
181
+ return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
182
+ }
183
+
167
184
  /** Injected-skill payloads below this size are never stubbed (not worth it). */
168
185
  const SKILL_ELISION_MIN_CHARS = 10_000;
169
186
  const SKILL_TEXT_MARKER = "Base directory for this skill: ";
@@ -453,12 +453,15 @@ export function responsesSseToAnthropicSse(
453
453
  if (line.startsWith("event: ")) eventName = line.slice(7).trim();
454
454
  else if (line.startsWith("data: ")) dataLine += line.slice(6);
455
455
  }
456
- if (!eventName || !dataLine) continue;
456
+ if (!dataLine) continue;
457
457
  let data: unknown;
458
458
  try { data = JSON.parse(dataLine); } catch { continue; }
459
459
  if (!isRec(data)) continue;
460
- if (terminated) continue;
461
- handleFrame(eventName, data);
460
+ // Responses-compatible gateways may omit the optional SSE event field
461
+ // while retaining the event name in the JSON payload's required type.
462
+ const resolvedEventName = eventName || (typeof data.type === "string" ? data.type : "");
463
+ if (!resolvedEventName || terminated) continue;
464
+ handleFrame(resolvedEventName, data);
462
465
  }
463
466
  }
464
467
  // EOF without a terminal frame is a TRUNCATION, not success (devlog 100:
@@ -137,11 +137,11 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
137
137
  for (let attempt = 0; attempt < 100; attempt++) {
138
138
  await Bun.sleep(2_000);
139
139
  const state = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, {}, deps);
140
+ if (state.error) throw new CliUsageError(String(state.error));
140
141
  if (state.loggedIn === true) {
141
142
  printData(state, wantsJson, [`Logged in to ${provider}.`]);
142
143
  return;
143
144
  }
144
- if (state.error) throw new CliUsageError(String(state.error));
145
145
  }
146
146
  throw new CliUsageError("login timed out");
147
147
  }