@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -7,11 +7,14 @@ import { sidecarEnter } from "../lib/sidecar-tracker";
7
7
  import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry";
8
8
  import { parseSidecarSSE } from "../web-search/parse";
9
9
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
10
+ import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
10
11
 
11
12
  export interface VisionSettings {
12
13
  model: string;
13
14
  reasoning: VisionReasoningEffort;
14
15
  timeoutMs: number;
16
+ /** Effective Desktop authless compatibility does not grant auxiliary model use. */
17
+ reserveCompatibility?: boolean;
15
18
  }
16
19
 
17
20
  /** A description, or an `error` string when it couldn't run (caller injects a graceful marker). */
@@ -58,6 +61,9 @@ export async function describeImage(
58
61
  abortSignal?: AbortSignal,
59
62
  recordOutcome?: SidecarOutcomeRecorder,
60
63
  ): Promise<DescribeOutcome> {
64
+ if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
65
+ return { text: "", error: "Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision helper model." };
66
+ }
61
67
  const invalid = validateImageUrl(imageUrl);
62
68
  if (invalid) return { text: "", error: invalid };
63
69
 
@@ -0,0 +1,108 @@
1
+ import type { OcxContentPart, OcxParsedRequest, OcxTextContent } from "../types";
2
+ import type { TranslatorBudget } from "../lib/translator-budget";
3
+
4
+ export const descriptionEncoder = new TextEncoder();
5
+
6
+ /** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
7
+ export function carriesImages(role: string): boolean {
8
+ return role === "user" || role === "developer" || role === "toolResult";
9
+ }
10
+
11
+
12
+ const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
13
+
14
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ /**
19
+ * Keep the native Responses passthrough body aligned with image replacements made in the parsed
20
+ * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
21
+ * `context.messages`; updating only the latter would send the original pixels to a text-only
22
+ * Responses upstream even after the vision sidecar produced a caption.
23
+ *
24
+ * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
25
+ * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
26
+ * untouched.
27
+ */
28
+ export function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
29
+ const rawBody = parsed._rawBody;
30
+ if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
31
+
32
+ let nextDescription = 0;
33
+ const rewriteImages = (value: unknown): unknown => {
34
+ if (Array.isArray(value)) {
35
+ let changed = false;
36
+ const rewritten = value.map(entry => {
37
+ const next = rewriteImages(entry);
38
+ if (next !== entry) changed = true;
39
+ return next;
40
+ });
41
+ return changed ? rewritten : value;
42
+ }
43
+ if (!isPlainRecord(value)) return value;
44
+ if (value.type === "input_image" && typeof value.image_url === "string") {
45
+ // Both message and tool-output parsers exclude empty URLs from caption jobs.
46
+ if (value.image_url.length === 0) {
47
+ const fileId = typeof value.file_id === "string" && value.file_id.length > 0 ? value.file_id : undefined;
48
+ return { type: "input_text", text: fileId ? `[image: ${fileId}]` : IMAGE_OMITTED_TEXT };
49
+ }
50
+ const description = descriptions[nextDescription++];
51
+ return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
52
+ }
53
+ return value;
54
+ };
55
+
56
+ let changed = false;
57
+ const input = rawBody.input.map(item => {
58
+ if (!isPlainRecord(item)) return item;
59
+ const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
60
+ const role = typeof item.role === "string" ? item.role : "";
61
+ const isMessageContent = (
62
+ (type === "message" && (role === "user" || role === "developer"))
63
+ || type === "agent_message"
64
+ );
65
+ const field = isMessageContent
66
+ ? "content"
67
+ : (type === "function_call_output" || type === "custom_tool_call_output")
68
+ ? "output"
69
+ : undefined;
70
+ if (!field) return item;
71
+ const rewritten = rewriteImages(item[field]);
72
+ if (rewritten === item[field]) return item;
73
+ changed = true;
74
+ return { ...item, [field]: rewritten };
75
+ });
76
+
77
+ if (changed) rawBody.input = input;
78
+ }
79
+
80
+ /**
81
+ * Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
82
+ * provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
83
+ * raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
84
+ * model (and the user, via its reply) knows the image was dropped rather than ignored.
85
+ */
86
+ export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
87
+ let stripped = false;
88
+ const descriptions: string[] = [];
89
+ for (const msg of parsed.context.messages) {
90
+ if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
91
+ const parts = msg.content as OcxContentPart[];
92
+ if (!parts.some(p => p.type === "image")) continue;
93
+ msg.content = parts.map(p => {
94
+ if (p.type !== "image") return p;
95
+ const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
96
+ descriptions.push((replacement as OcxTextContent).text);
97
+ const reservation = translatorBudget?.reserveTransient(
98
+ descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
99
+ { kind: "request_copies" },
100
+ );
101
+ reservation?.commitRetained();
102
+ return replacement;
103
+ });
104
+ stripped = true;
105
+ }
106
+ syncRawBodyImageDescriptions(parsed, descriptions);
107
+ return stripped;
108
+ }
@@ -1,26 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
3
- import type { VisionReasoningEffort } from "../reasoning-effort";
4
- import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
2
+ import type { OcxContentPart, OcxMessage, OcxParsedRequest, OcxTextContent } from "../types";
3
+ import { describeImage, type DescribeOutcome } from "./describe";
5
4
  import { describeImageAnthropic } from "./anthropic-describe";
6
5
  import { describeImageRouted } from "./routed-describe";
7
- import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility";
8
- import { normalizeVisionReasoningForModel } from "./reasoning";
9
6
  import type { CodexAuthContext } from "../codex/auth-context";
10
- import { resolveSidecarAuth } from "../sidecar/auth";
11
- import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
12
7
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
13
8
  import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
14
9
  import type { TranslatorBudget } from "../lib/translator-budget";
15
- import {
16
- DEFAULT_VISION_TIMEOUT_MS,
17
- MAX_VISION_TIMEOUT_MS,
18
- MIN_VISION_TIMEOUT_MS,
19
- } from "./timeout-bounds";
10
+ import type { VisionPlan } from "./plan";
11
+ import { carriesImages, descriptionEncoder, syncRawBodyImageDescriptions } from "./image-rewrite";
20
12
 
21
13
  export { describeImage } from "./describe";
22
-
23
- /** Backward-compatible request-time name for the shared vision-sidecar consumer predicate. */
24
14
  export { isModelVisionSidecarConsumer as isModelTextOnly } from "./eligibility";
25
15
  export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
26
16
  export {
@@ -33,19 +23,25 @@ export {
33
23
  visionEligibleModelOptions,
34
24
  } from "./eligibility";
35
25
  export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility";
26
+ export { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds";
36
27
  export {
37
- DEFAULT_VISION_TIMEOUT_MS,
38
- MAX_VISION_TIMEOUT_MS,
39
- MIN_VISION_TIMEOUT_MS,
40
- };
28
+ DEFAULT_MAX_DESCRIPTIONS_PER_TURN,
29
+ resolveMaxDescriptionsPerTurn,
30
+ isValidVisionTimeoutMs,
31
+ resolveVisionTimeoutMs,
32
+ findAnthropicVisionProvider,
33
+ resolveVisionBackend,
34
+ resolveOpenAiVisionModel,
35
+ resolveEffectiveVisionModel,
36
+ shouldResolveOpenAiVisionSidecar,
37
+ planVisionSidecar,
38
+ } from "./plan";
39
+ export type { AnthropicVisionProvider, VisionPlan } from "./plan";
40
+ export { stripImagesInPlace } from "./image-rewrite";
41
+
41
42
 
42
- const DEFAULT_VISION_MODEL = "gpt-5.4-mini";
43
- const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
44
- const DEFAULT_REASONING: VisionReasoningEffort = "low";
45
- export const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
46
43
  const DESCRIPTION_CACHE_MAX_ENTRIES = 256;
47
44
  export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024;
48
- const descriptionEncoder = new TextEncoder();
49
45
  /** Max images described in parallel — keeps first-token latency bounded without flooding the backend. */
50
46
  const VISION_CONCURRENCY = 3;
51
47
  /** Per-image description hard cap (chars) so multi-image turns can't blow the main model's context. */
@@ -160,25 +156,6 @@ export function evictOldestVisionDescriptionForBudget(): number {
160
156
  return descriptionCache.evictOldest?.() ?? 0;
161
157
  }
162
158
 
163
- /** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */
164
- export function resolveMaxDescriptionsPerTurn(value: unknown): number {
165
- if (value === 0) return 0;
166
- return typeof value === "number" && Number.isInteger(value) && value > 0
167
- ? value
168
- : DEFAULT_MAX_DESCRIPTIONS_PER_TURN;
169
- }
170
-
171
- export function isValidVisionTimeoutMs(value: unknown): value is number {
172
- return typeof value === "number"
173
- && Number.isInteger(value)
174
- && value >= MIN_VISION_TIMEOUT_MS
175
- && value <= MAX_VISION_TIMEOUT_MS;
176
- }
177
-
178
- /** Runtime config is permissive: malformed or out-of-range values fall back to the default. */
179
- export function resolveVisionTimeoutMs(value: unknown): number {
180
- return isValidVisionTimeoutMs(value) ? value : DEFAULT_VISION_TIMEOUT_MS;
181
- }
182
159
 
183
160
  /** Run `worker` over `items` with bounded concurrency, preserving input order in the result array. */
184
161
  async function runBounded<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>): Promise<R[]> {
@@ -198,176 +175,7 @@ function clamp(s: string, max: number): string {
198
175
  return s.length <= max ? s : `${s.slice(0, max)}\n…[description truncated]`;
199
176
  }
200
177
 
201
- export interface AnthropicVisionProvider {
202
- providerName: string;
203
- provider: OcxProviderConfig;
204
- }
205
-
206
- /**
207
- * First enabled Anthropic OAuth provider whose active stored account is not marked for reauth.
208
- * Delegates to the shared sidecar auth module (#2188) — same predicate as web-search.
209
- */
210
- export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined {
211
- const auth = resolveSidecarAuth(config);
212
- if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined;
213
- return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider };
214
- }
215
-
216
- export function resolveVisionBackend(
217
- explicit: "openai" | "anthropic" | "routed" | undefined,
218
- anthropicSidecar: AnthropicVisionProvider | undefined,
219
- ): "openai" | "anthropic" {
220
- if (explicit === "openai" || explicit === "anthropic") return explicit;
221
- // "routed" collapses to the legacy default order until its describe executor
222
- // lands (roadmap 170 → 180 revised): a persisted routed backend without a
223
- // dispatchable arm degrades exactly like unset rather than crashing. wp3
224
- // replaces this collapse with the real routed arm in planVisionSidecar.
225
- return anthropicSidecar ? "anthropic" : "openai";
226
- }
227
-
228
- /** Native model used by the OpenAI vision helper, including its bounded default. */
229
- export function resolveOpenAiVisionModel(config: Pick<OcxConfig, "visionSidecar">): string {
230
- const configured = config.visionSidecar?.model;
231
- // Namespaced routed ids never reach the forward executor (see
232
- // resolveEffectiveVisionModel).
233
- return configured && !configured.includes("/") ? configured : DEFAULT_VISION_MODEL;
234
- }
235
-
236
- /** Effective describer model for the backend `planVisionSidecar` selected. */
237
- export function resolveEffectiveVisionModel(
238
- config: Pick<OcxConfig, "visionSidecar">,
239
- backend: "openai" | "anthropic",
240
- ): string {
241
- const configured = config.visionSidecar?.model;
242
- // A namespaced "provider/model" id belongs to the routed backend only; the
243
- // forward/OAuth executors POST the model string verbatim, so it falls back
244
- // to the side's default here (PUT coherence rejects new writes of this
245
- // shape, but a legacy or hand-edited config must not break the executor).
246
- const usable = configured && !configured.includes("/") ? configured : undefined;
247
- return backend === "anthropic"
248
- ? usable || DEFAULT_ANTHROPIC_VISION_MODEL
249
- : usable || DEFAULT_VISION_MODEL;
250
- }
251
-
252
- /** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
253
- function carriesImages(role: string): boolean {
254
- return role === "user" || role === "developer" || role === "toolResult";
255
- }
256
-
257
- function messagesHaveImage(parsed: OcxParsedRequest): boolean {
258
- return parsed.context.messages.some(m =>
259
- carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image"));
260
- }
261
-
262
- export function shouldResolveOpenAiVisionSidecar(
263
- config: OcxConfig,
264
- provider: OcxProviderConfig,
265
- modelId: string,
266
- parsed: OcxParsedRequest,
267
- ): boolean {
268
- if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false;
269
- const cfg = config.visionSidecar ?? {};
270
- if (cfg.enabled === false) return false;
271
- return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
272
- }
273
-
274
- export interface VisionPlan {
275
- backend: "openai" | "anthropic" | "routed";
276
- forwardSidecar?: ResolvedOpenAiForwardSidecar;
277
- anthropicSidecar?: AnthropicVisionProvider;
278
- /** Namespaced "provider/model" describer for the routed backend (roadmap 180). */
279
- routedModel?: string;
280
- /** Loopback dispatch inputs for the routed backend. */
281
- routedConfig?: Pick<OcxConfig, "port" | "apiKeys">;
282
- settings: VisionSettings;
283
- maxDescriptionsPerTurn: number;
284
- }
285
-
286
- /**
287
- * Decide whether the vision sidecar should pre-describe images for this request, returning the plan
288
- * if so. Active when: the routed model is in `provider.noVisionModels`, the request actually carries
289
- * an image, the sidecar isn't disabled, and the selected backend has usable auth. Returns undefined
290
- * otherwise (the caller strips images before sending to a text-only model).
291
- */
292
- export function planVisionSidecar(
293
- config: OcxConfig,
294
- provider: OcxProviderConfig,
295
- modelId: string,
296
- parsed: OcxParsedRequest,
297
- openAiSidecar?: ResolvedOpenAiForwardSidecar,
298
- ): VisionPlan | undefined {
299
- if (!isModelTextOnly(provider, modelId)) return undefined;
300
- if (!messagesHaveImage(parsed)) return undefined;
301
- const cfg = config.visionSidecar ?? {};
302
- if (cfg.enabled === false) return undefined;
303
-
304
- // Routed arm (roadmap 180 revised): explicit backend + NAMESPACED explicit
305
- // model only — never inferred from credential availability. Plan-time
306
- // fence: the target must not be provably blind, and must not itself be a
307
- // model this planner would re-enter for (belt; the terminal marker on the
308
- // loopback request is the braces).
309
- if (cfg.backend === "routed") {
310
- const routedModel = cfg.model;
311
- const sep = routedModel ? routedModel.indexOf("/") : -1;
312
- if (routedModel && sep > 0) {
313
- const targetProvider = routedModel.slice(0, sep);
314
- const targetId = routedModel.slice(sep + 1);
315
- const targetProviderConfig = config.providers?.[targetProvider];
316
- const targetVisible = modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
317
- && !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId));
318
- if (targetVisible) {
319
- return {
320
- backend: "routed",
321
- routedModel,
322
- routedConfig: { port: config.port, ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}) },
323
- settings: {
324
- model: routedModel,
325
- reasoning: DEFAULT_REASONING,
326
- timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
327
- },
328
- maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn),
329
- };
330
- }
331
- }
332
- // Misconfigured routed backend (bare id, unknown provider, or provably
333
- // blind target): fall through to the legacy default order below rather
334
- // than dispatching a describe that cannot work.
335
- }
336
-
337
- const anthropicSidecar = findAnthropicVisionProvider(config);
338
- const backend = resolveVisionBackend(cfg.backend, anthropicSidecar);
339
- // A namespaced routed model must never reach the forward/OAuth executors
340
- // (they POST the string verbatim); the effective-model resolver falls back
341
- // to each side's default in that case.
342
- const model = resolveEffectiveVisionModel(config, backend);
343
- const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn);
344
178
 
345
- if (backend === "anthropic") {
346
- if (!anthropicSidecar) return undefined;
347
- return {
348
- backend,
349
- anthropicSidecar,
350
- settings: {
351
- model,
352
- reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
353
- timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
354
- },
355
- maxDescriptionsPerTurn,
356
- };
357
- }
358
-
359
- if (!openAiSidecar) return undefined;
360
- return {
361
- backend,
362
- forwardSidecar: openAiSidecar,
363
- settings: {
364
- model,
365
- reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING,
366
- timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs),
367
- },
368
- maxDescriptionsPerTurn,
369
- };
370
- }
371
179
 
372
180
  interface ImageJob {
373
181
  imageUrl: string;
@@ -385,71 +193,6 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten
385
193
  };
386
194
  }
387
195
 
388
- const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
389
-
390
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
391
- return typeof value === "object" && value !== null && !Array.isArray(value);
392
- }
393
-
394
- /**
395
- * Keep the native Responses passthrough body aligned with image replacements made in the parsed
396
- * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
397
- * `context.messages`; updating only the latter would send the original pixels to a text-only
398
- * Responses upstream even after the vision sidecar produced a caption.
399
- *
400
- * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
401
- * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
402
- * untouched.
403
- */
404
- function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
405
- const rawBody = parsed._rawBody;
406
- if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
407
-
408
- let nextDescription = 0;
409
- const rewriteImages = (value: unknown, nonEmptyImageUrlsOnly: boolean): unknown => {
410
- if (Array.isArray(value)) {
411
- let changed = false;
412
- const rewritten = value.map(entry => {
413
- const next = rewriteImages(entry, nonEmptyImageUrlsOnly);
414
- if (next !== entry) changed = true;
415
- return next;
416
- });
417
- return changed ? rewritten : value;
418
- }
419
- if (!isPlainRecord(value)) return value;
420
- if (value.type === "input_image" && typeof value.image_url === "string") {
421
- if (nonEmptyImageUrlsOnly && value.image_url.length === 0) {
422
- return { type: "input_text", text: IMAGE_OMITTED_TEXT };
423
- }
424
- const description = descriptions[nextDescription++];
425
- return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
426
- }
427
- return value;
428
- };
429
-
430
- let changed = false;
431
- const input = rawBody.input.map(item => {
432
- if (!isPlainRecord(item)) return item;
433
- const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
434
- const role = typeof item.role === "string" ? item.role : "";
435
- const isMessageContent = (
436
- (type === "message" && (role === "user" || role === "developer"))
437
- || type === "agent_message"
438
- );
439
- const field = isMessageContent
440
- ? "content"
441
- : (type === "function_call_output" || type === "custom_tool_call_output")
442
- ? "output"
443
- : undefined;
444
- if (!field) return item;
445
- const rewritten = rewriteImages(item[field], isMessageContent);
446
- if (rewritten === item[field]) return item;
447
- changed = true;
448
- return { ...item, [field]: rewritten };
449
- });
450
-
451
- if (changed) rawBody.input = input;
452
- }
453
196
 
454
197
  function sha256(value: string | Uint8Array): string {
455
198
  return createHash("sha256").update(value).digest("hex");
@@ -635,33 +378,3 @@ export async function describeImagesInPlace(
635
378
  }
636
379
  syncRawBodyImageDescriptions(parsed, descriptions);
637
380
  }
638
-
639
- /**
640
- * Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward
641
- * provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding
642
- * raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
643
- * model (and the user, via its reply) knows the image was dropped rather than ignored.
644
- */
645
- export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
646
- let stripped = false;
647
- const descriptions: string[] = [];
648
- for (const msg of parsed.context.messages) {
649
- if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
650
- const parts = msg.content as OcxContentPart[];
651
- if (!parts.some(p => p.type === "image")) continue;
652
- msg.content = parts.map(p => {
653
- if (p.type !== "image") return p;
654
- const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
655
- descriptions.push((replacement as OcxTextContent).text);
656
- const reservation = translatorBudget?.reserveTransient(
657
- descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
658
- { kind: "request_copies" },
659
- );
660
- reservation?.commitRetained();
661
- return replacement;
662
- });
663
- stripped = true;
664
- }
665
- syncRawBodyImageDescriptions(parsed, descriptions);
666
- return stripped;
667
- }