@bitkyc08/opencodex 2.11.1 → 2.12.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 (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Which models may serve AS the vision sidecar (the describer), as opposed to the
3
+ * models the sidecar describes FOR.
4
+ *
5
+ * Three rules make this non-obvious, and all are load-bearing:
6
+ *
7
+ * 1. `provider.noVisionModels` marks models the proxy describes images for, and
8
+ * `applyProviderConfigHints` deliberately ADDS "image" to their advertised
9
+ * input modalities so the Codex app does not block the attachment client-side.
10
+ * A blind model therefore advertises image input. Membership in that list is a
11
+ * hard disqualifier here, checked BEFORE the modality list it rewrote.
12
+ * 2. The catalog enriches configured providers from the registry before it
13
+ * applies that rule. Eligibility must use the same effective provider policy,
14
+ * including a `noVisionModels` list learned after the config was persisted.
15
+ * 3. Catalog rows frequently omit `inputModalities` entirely (the live
16
+ * `/api/models` response carries none for either openai or anthropic rows).
17
+ * Unknown is not zero: when no source can speak, the model stays eligible
18
+ * rather than silently vanishing from the picker.
19
+ * Native OpenAI metadata is likewise authoritative only for a native row or
20
+ * the canonical OpenAI provider; a routed row with the same id owns its own
21
+ * explicit modalities.
22
+ */
23
+ import { modelInList, type OcxConfig, type OcxProviderConfig } from "../types";
24
+ import { getModelMetadataCaseInsensitive, resolveMetadataProvider } from "../generated/model-metadata";
25
+ import { nativeInputModalities } from "../codex/catalog/metadata";
26
+ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
27
+ import { enrichProviderFromRegistry } from "../providers/derive";
28
+
29
+ /** The two wire protocols `planVisionSidecar` can actually dispatch to. */
30
+ export type VisionSidecarBackend = "openai" | "anthropic";
31
+
32
+ /**
33
+ * Default entry per backend: cheap, image-capable, and present in every deployment. Offered
34
+ * whenever its side is enabled, and withheld only when that provider explicitly lists it as a
35
+ * model the sidecar describes FOR — never merely because a metadata table stayed silent.
36
+ */
37
+ export const BASELINE_VISION_MODELS: Record<VisionSidecarBackend, string> = {
38
+ openai: "gpt-5.6-luna",
39
+ anthropic: "claude-haiku-4-5",
40
+ };
41
+
42
+ export interface VisionCandidateModel {
43
+ provider: string;
44
+ id: string;
45
+ inputModalities?: string[];
46
+ native?: boolean;
47
+ }
48
+
49
+ export interface VisionModelOption {
50
+ value: string;
51
+ label: string;
52
+ backend: VisionSidecarBackend;
53
+ /** True when the row is a guaranteed baseline rather than a catalog discovery. */
54
+ baseline?: boolean;
55
+ }
56
+
57
+ type EnrichedProviderCache = Map<string, OcxProviderConfig>;
58
+
59
+ function advertisesImageInput(modalities: readonly string[] | undefined): boolean | undefined {
60
+ if (!modalities || modalities.length === 0) return undefined;
61
+ return modalities.includes("image");
62
+ }
63
+
64
+ /** Vendor-table modalities for a routed row, or undefined when the table has no opinion. */
65
+ function metadataImageInput(provider: string, modelId: string): boolean | undefined {
66
+ const resolved = resolveMetadataProvider(provider) ?? provider;
67
+ const meta = getModelMetadataCaseInsensitive(resolved, modelId);
68
+ return advertisesImageInput(meta?.input);
69
+ }
70
+
71
+ /**
72
+ * Mirrors the catalog's registry enrichment without mutating saved configuration. The picker
73
+ * checks many rows from one provider, so its caller shares this small call-local cache.
74
+ */
75
+ function enrichedProviderForVision(
76
+ config: Pick<OcxConfig, "providers">,
77
+ providerName: string,
78
+ cache: EnrichedProviderCache,
79
+ ): OcxProviderConfig | undefined {
80
+ const cached = cache.get(providerName);
81
+ if (cached) return cached;
82
+ const configured = config.providers?.[providerName];
83
+ if (!configured) return undefined;
84
+ const enriched = structuredClone(configured);
85
+ enrichProviderFromRegistry(providerName, enriched);
86
+ cache.set(providerName, enriched);
87
+ return enriched;
88
+ }
89
+
90
+ function isVisionSidecarConsumerWithCache(
91
+ config: Pick<OcxConfig, "providers">,
92
+ providerName: string,
93
+ modelId: string,
94
+ cache: EnrichedProviderCache,
95
+ ): boolean {
96
+ return modelInList(enrichedProviderForVision(config, providerName, cache)?.noVisionModels, modelId);
97
+ }
98
+
99
+ /**
100
+ * Is this model listed as one the sidecar describes FOR? Such a model cannot be
101
+ * the describer, and its advertised modalities are untrustworthy.
102
+ */
103
+ export function isVisionSidecarConsumer(config: Pick<OcxConfig, "providers">, providerName: string, modelId: string): boolean {
104
+ return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
105
+ }
106
+
107
+ /**
108
+ * Can this model accept an image on the wire? Sources are consulted in descending
109
+ * trustworthiness; the first that speaks wins. `undefined` means nothing knows,
110
+ * which callers treat as eligible.
111
+ */
112
+ export function modelAcceptsImageInput(
113
+ config: Pick<OcxConfig, "providers">,
114
+ candidate: VisionCandidateModel,
115
+ ): boolean | undefined {
116
+ return modelAcceptsImageInputWithCache(config, candidate, new Map());
117
+ }
118
+
119
+ function modelAcceptsImageInputWithCache(
120
+ config: Pick<OcxConfig, "providers">,
121
+ candidate: VisionCandidateModel,
122
+ cache: EnrichedProviderCache,
123
+ ): boolean | undefined {
124
+ if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false;
125
+ if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) {
126
+ return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;
127
+ }
128
+ const fromRow = advertisesImageInput(candidate.inputModalities);
129
+ if (fromRow !== undefined) return fromRow;
130
+ return metadataImageInput(candidate.provider, candidate.id);
131
+ }
132
+
133
+ /** Eligible = not a sidecar consumer, and not positively known to be text-only. */
134
+ export function isVisionEligibleModel(
135
+ config: Pick<OcxConfig, "providers">,
136
+ candidate: VisionCandidateModel,
137
+ ): boolean {
138
+ return isVisionEligibleModelWithCache(config, candidate, new Map());
139
+ }
140
+
141
+ function isVisionEligibleModelWithCache(
142
+ config: Pick<OcxConfig, "providers">,
143
+ candidate: VisionCandidateModel,
144
+ cache: EnrichedProviderCache,
145
+ ): boolean {
146
+ return modelAcceptsImageInputWithCache(config, candidate, cache) !== false;
147
+ }
148
+
149
+ /** Which executor can describe through this row, or undefined when neither can. */
150
+ export function visionBackendForCandidate(
151
+ config: Pick<OcxConfig, "providers">,
152
+ candidate: VisionCandidateModel,
153
+ anthropicProviderName?: string,
154
+ ): VisionSidecarBackend | undefined {
155
+ if (candidate.native || candidate.provider === "openai") return "openai";
156
+ // The runtime dispatches through exactly ONE Anthropic provider — the enabled OAuth row
157
+ // with a healthy active account that `findAnthropicVisionProvider` picks. Speaking the
158
+ // Messages wire is not enough: a key-auth row of the same adapter is unreachable, and an
159
+ // option that cannot be dispatched is worse than a missing one, because selecting it fails
160
+ // at describe time rather than at pick time.
161
+ //
162
+ // So the executor's name is REQUIRED for an Anthropic suggestion. When the caller has no
163
+ // executor to name, no catalog row qualifies; the side's baseline is added separately and
164
+ // keeps the picker populated. Narrowing here never widens the write gate, which is a
165
+ // different predicate (`modelAcceptsImageInput`) and still treats unknown as allowed.
166
+ if (anthropicProviderName === undefined) return undefined;
167
+ return candidate.provider === anthropicProviderName ? "anthropic" : undefined;
168
+ }
169
+
170
+ function baselineCandidate(
171
+ backend: VisionSidecarBackend,
172
+ anthropicProviderName: string | undefined,
173
+ ): VisionCandidateModel {
174
+ return {
175
+ provider: backend === "openai" ? "openai" : anthropicProviderName ?? "anthropic",
176
+ id: BASELINE_VISION_MODELS[backend],
177
+ // Both baselines are known image-capable. This makes their only exclusion path the
178
+ // provider's explicit consumer list, never a silent or stale metadata table.
179
+ inputModalities: ["text", "image"],
180
+ };
181
+ }
182
+
183
+ /**
184
+ * The picker's option list: every eligible row reachable by one of the two
185
+ * executors, plus each enabled side's baseline unless that baseline is explicitly
186
+ * excluded, de-duplicated and stably ordered (openai side first, baselines first
187
+ * within a side). Anthropic rows must belong to the OAuth provider that would
188
+ * actually execute them, so `anthropicProviderName` is what makes that side's
189
+ * catalog rows eligible at all.
190
+ *
191
+ * This is the SUGGESTION list (narrow): it emits only rows an executor can reach
192
+ * and some source has heard of. It is deliberately NOT the same set as the write
193
+ * gate. `modelAcceptsImageInput` answers "can we prove this model cannot see?"
194
+ * and is the only input to rejection. Absence from this list must never imply
195
+ * rejection — an unknown id stays eligible via the undefined → eligible fallback.
196
+ *
197
+ * De-duplication is by BARE model id, first eligible row wins. Two providers of
198
+ * the same adapter family can expose the same id; they resolve to the same
199
+ * backend, and only `value` reaches the client, so first-wins costs nothing.
200
+ */
201
+ export function visionEligibleModelOptions(
202
+ config: Pick<OcxConfig, "providers">,
203
+ candidates: readonly VisionCandidateModel[],
204
+ enabledBackends: readonly VisionSidecarBackend[],
205
+ anthropicProviderName?: string,
206
+ ): VisionModelOption[] {
207
+ const enabled = new Set(enabledBackends);
208
+ const byValue = new Map<string, VisionModelOption>();
209
+ const enrichedProviders: EnrichedProviderCache = new Map();
210
+
211
+ for (const backend of ["openai", "anthropic"] as const) {
212
+ if (!enabled.has(backend)) continue;
213
+ const candidate = baselineCandidate(backend, anthropicProviderName);
214
+ if (!isVisionEligibleModelWithCache(config, candidate, enrichedProviders)) continue;
215
+ const id = candidate.id;
216
+ byValue.set(id, { value: id, label: id, backend, baseline: true });
217
+ }
218
+ for (const candidate of candidates) {
219
+ const backend = visionBackendForCandidate(config, candidate, anthropicProviderName);
220
+ if (!backend || !enabled.has(backend)) continue;
221
+ if (!isVisionEligibleModelWithCache(config, candidate, enrichedProviders)) continue;
222
+ if (byValue.has(candidate.id)) continue;
223
+ byValue.set(candidate.id, { value: candidate.id, label: candidate.id, backend });
224
+ }
225
+
226
+ const order = (option: VisionModelOption) =>
227
+ (option.backend === "openai" ? 0 : 2) + (option.baseline ? 0 : 1);
228
+ return [...byValue.values()].sort((a, b) => order(a) - order(b) || a.value.localeCompare(b.value));
229
+ }
@@ -14,6 +14,15 @@ import type { TranslatorBudget } from "../lib/translator-budget";
14
14
 
15
15
  export { describeImage } from "./describe";
16
16
  export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
17
+ export {
18
+ BASELINE_VISION_MODELS,
19
+ isVisionEligibleModel,
20
+ isVisionSidecarConsumer,
21
+ modelAcceptsImageInput,
22
+ visionBackendForCandidate,
23
+ visionEligibleModelOptions,
24
+ } from "./eligibility";
25
+ export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility";
17
26
 
18
27
  const DEFAULT_VISION_MODEL = "gpt-5.4-mini";
19
28
  const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
@@ -192,6 +201,16 @@ export function resolveOpenAiVisionModel(config: Pick<OcxConfig, "visionSidecar"
192
201
  return config.visionSidecar?.model || DEFAULT_VISION_MODEL;
193
202
  }
194
203
 
204
+ /** Effective describer model for the backend `planVisionSidecar` selected. */
205
+ export function resolveEffectiveVisionModel(
206
+ config: Pick<OcxConfig, "visionSidecar">,
207
+ backend: "openai" | "anthropic",
208
+ ): string {
209
+ return backend === "anthropic"
210
+ ? config.visionSidecar?.model || DEFAULT_ANTHROPIC_VISION_MODEL
211
+ : resolveOpenAiVisionModel(config);
212
+ }
213
+
195
214
  /** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */
196
215
  function carriesImages(role: string): boolean {
197
216
  return role === "user" || role === "developer" || role === "toolResult";
@@ -241,11 +260,11 @@ export function planVisionSidecar(
241
260
  if (cfg.enabled === false) return undefined;
242
261
  const anthropicSidecar = findAnthropicVisionProvider(config);
243
262
  const backend = resolveVisionBackend(cfg.backend, anthropicSidecar);
263
+ const model = resolveEffectiveVisionModel(config, backend);
244
264
  const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn);
245
265
 
246
266
  if (backend === "anthropic") {
247
267
  if (!anthropicSidecar) return undefined;
248
- const model = cfg.model || DEFAULT_ANTHROPIC_VISION_MODEL;
249
268
  return {
250
269
  backend,
251
270
  anthropicSidecar,
@@ -259,7 +278,6 @@ export function planVisionSidecar(
259
278
  }
260
279
 
261
280
  if (!openAiSidecar) return undefined;
262
- const model = resolveOpenAiVisionModel(config);
263
281
  return {
264
282
  backend,
265
283
  forwardSidecar: openAiSidecar,
@@ -62,8 +62,8 @@ export async function runWebSearch(
62
62
  tool_choice: "auto",
63
63
  reasoning: { effort: settings.reasoning },
64
64
  // NOTE: the ChatGPT (codex) backend rejects `max_output_tokens` ("Unsupported parameter") and
65
- // requires `store: false` — keep this body minimal. Answer length is capped downstream
66
- // (format-result clamps the injected tool_result), so no upstream cap is needed.
65
+ // requires `store: false` — keep this body minimal. The shared SSE parser bounds raw response
66
+ // bytes before format-result applies its smaller display clamp.
67
67
  store: false,
68
68
  stream: true,
69
69
  };
@@ -1,12 +1,13 @@
1
1
  import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
2
- import { modelInList } from "../types";
2
+ import { modelInList, toolChoiceToolPredicate } from "../types";
3
3
  import type { SidecarSettings } from "./executor";
4
4
  import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
5
5
  import { getAccountSet } from "../oauth/store";
6
6
  import { DEFAULT_STALL_TIMEOUT_SEC } from "../stall-timeout";
7
+ import { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
7
8
 
8
9
  export { runWithWebSearch } from "./loop";
9
- export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
10
+ export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME };
10
11
  export { runAnthropicWebSearch, parseAnthropicSidecarSSE } from "./anthropic-executor";
11
12
 
12
13
  const DEFAULT_SIDECAR_MODEL = "gpt-5.6-luna";
@@ -146,6 +147,7 @@ export function planWebSearch(
146
147
  openAiSidecar?: ResolvedOpenAiForwardSidecar,
147
148
  ): SidecarPlan | undefined {
148
149
  if (!parsed._webSearch || isPassthrough) return undefined;
150
+ if (!toolChoiceToolPredicate(parsed.options.toolChoice)(buildWebSearchTool())) return undefined;
149
151
  const cfg = config.webSearchSidecar ?? {};
150
152
  if (cfg.enabled === false) return undefined;
151
153
  const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -1,6 +1,6 @@
1
1
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
2
2
  import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
3
- import { namespacedToolName } from "../types";
3
+ import { namespacedToolName, toolChoiceToolPredicate } from "../types";
4
4
  import type { AttemptRecoveryKind } from "../usage/log";
5
5
  import { bridgeToResponsesSSE } from "../bridge";
6
6
  import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
@@ -693,7 +693,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
693
693
  const toolNsMap = new Map<string, { namespace: string; name: string }>();
694
694
  const freeform = new Set<string>();
695
695
  const toolSearch = new Set<string>();
696
+ const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
696
697
  for (const t of parsed.context.tools ?? []) {
698
+ if (!toolAllowed(t)) continue;
697
699
  if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
698
700
  if (t.freeform) freeform.add(t.name);
699
701
  if (t.toolSearch) toolSearch.add(t.name);
@@ -29,6 +29,10 @@ interface OutputItem {
29
29
  content?: OutputTextBlock[];
30
30
  }
31
31
 
32
+ // ChatGPT's Codex backend does not accept `max_output_tokens` on sidecar requests. Bound the raw
33
+ // streamed response here, before decoded text and authoritative/delta copies can accumulate.
34
+ export const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024;
35
+
32
36
  /** Push a `url_citation` annotation as a source, de-duplicated by URL. */
33
37
  function collectAnnotation(ann: AnnotationLike | undefined, sources: WebSearchSource[], seen: Set<string>): void {
34
38
  if (!ann || ann.type !== "url_citation" || typeof ann.url !== "string" || seen.has(ann.url)) return;
@@ -49,7 +53,52 @@ function collectAnnotation(ann: AnnotationLike | undefined, sources: WebSearchSo
49
53
  */
50
54
  const URL_RE = /https?:\/\/[^\s<>()\[\]]+/;
51
55
  // A "Sources:" / "Source:" header, allowing markdown prefixes (#, *, -, >) and bold/italic wrappers.
52
- const SOURCES_HEADER_RE = /^\s*(?:#{1,6}\s*)?[-*>\s]*\**\s*sources?\s*\**\s*:?\s*\**\s*$/i;
56
+ const SOURCES_WORD_RE = /^sources?/i;
57
+
58
+ /** Match the legacy Sources-header grammar without overlapping regex quantifiers. */
59
+ function isSourcesHeader(line: string): boolean {
60
+ let cursor = 0;
61
+ /** Match one code unit using JavaScript's existing `\s` semantics. */
62
+ const isWhitespace = (char: string | undefined): boolean => char !== undefined && /\s/u.test(char);
63
+ /** Advance over the current contiguous whitespace run. */
64
+ const skipWhitespace = (): void => {
65
+ while (isWhitespace(line[cursor])) cursor += 1;
66
+ };
67
+ /** Advance over the current contiguous Markdown-star run. */
68
+ const skipStars = (): void => {
69
+ while (line[cursor] === "*") cursor += 1;
70
+ };
71
+
72
+ skipWhitespace();
73
+ if (line[cursor] === "#") {
74
+ const start = cursor;
75
+ while (line[cursor] === "#") cursor += 1;
76
+ if (cursor - start > 6) return false;
77
+ skipWhitespace();
78
+ }
79
+
80
+ while (isWhitespace(line[cursor]) || line[cursor] === "-" || line[cursor] === "*" || line[cursor] === ">") {
81
+ cursor += 1;
82
+ }
83
+
84
+ const word = SOURCES_WORD_RE.exec(line.slice(cursor, cursor + 7));
85
+ if (!word) return false;
86
+ cursor += word[0].length;
87
+
88
+ // Preserve `\s*\**\s*:?\s*\**\s*$`: at most two star runs, with the optional
89
+ // colon between them. Either run may be empty, so `Sources:*` is one trailing run; three
90
+ // separated runs and a colon after the second run remain invalid.
91
+ skipWhitespace();
92
+ skipStars();
93
+ skipWhitespace();
94
+ if (line[cursor] === ":") {
95
+ cursor += 1;
96
+ skipWhitespace();
97
+ }
98
+ skipStars();
99
+ skipWhitespace();
100
+ return cursor === line.length;
101
+ }
53
102
 
54
103
  /** Trim wrapping/trailing noise from a captured URL: angle brackets, then trailing punctuation. */
55
104
  function cleanUrl(url: string): string {
@@ -69,7 +118,7 @@ function extractTrailingSources(text: string): { text: string; sources: WebSearc
69
118
  // Find the LAST line that is a "Sources:" header (markdown prefixes allowed).
70
119
  let headerIdx = -1;
71
120
  for (let i = lines.length - 1; i >= 0; i--) {
72
- if (SOURCES_HEADER_RE.test(lines[i])) { headerIdx = i; break; }
121
+ if (isSourcesHeader(lines[i])) { headerIdx = i; break; }
73
122
  }
74
123
  if (headerIdx === -1) return { text, sources: [] };
75
124
  const sources: WebSearchSource[] = [];
@@ -130,6 +179,15 @@ function fromOutputArray(output: OutputItem[], seen: Set<string>): WebSearchResu
130
179
  return { text, sources };
131
180
  }
132
181
 
182
+ function cancelReaderWithoutWaiting(
183
+ reader: ReadableStreamDefaultReader<Uint8Array>,
184
+ reason: string,
185
+ ): void {
186
+ try {
187
+ void reader.cancel(reason).catch(() => undefined);
188
+ } catch { /* best-effort body teardown */ }
189
+ }
190
+
133
191
  /**
134
192
  * Parse the sidecar's streamed Responses SSE into a final answer + sources. Tolerant of the full set of
135
193
  * Responses streaming events: prefers the authoritative `response.completed` output[], then the
@@ -143,6 +201,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
143
201
  const reader = response.body.getReader();
144
202
  const decoder = new TextDecoder();
145
203
  let buffer = "";
204
+ let responseBytes = 0;
146
205
  const seen = new Set<string>();
147
206
  // Holder object — fields are mutated inside the closure, so they can't live as narrowed locals.
148
207
  const acc: {
@@ -197,13 +256,23 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
197
256
  while (true) {
198
257
  const { done, value } = await reader.read();
199
258
  if (done) break;
200
- buffer += decoder.decode(value, { stream: true });
259
+ const remaining = MAX_SIDECAR_RESPONSE_BYTES - responseBytes;
260
+ const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
261
+ responseBytes += accepted.byteLength;
262
+ buffer += decoder.decode(accepted, { stream: true });
201
263
  const lines = buffer.split("\n");
202
264
  buffer = lines.pop() ?? "";
203
265
  for (const line of lines) {
204
266
  const data = sseFieldValue(line, "data");
205
267
  if (data !== null) handle(data.trim());
206
268
  }
269
+ if (responseBytes >= MAX_SIDECAR_RESPONSE_BYTES) {
270
+ // Preserve complete events accepted up to the cap, but discard any unterminated line and
271
+ // TextDecoder carry. Do not let a rejecting/hung cancel turn bounded partial output into
272
+ // an error or keep this parser waiting on upstream teardown.
273
+ cancelReaderWithoutWaiting(reader, "sidecar response byte limit reached");
274
+ break;
275
+ }
207
276
  }
208
277
  } finally {
209
278
  reader.releaseLock();