@bitkyc08/opencodex 2.59.0 → 2.60.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.
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -35,6 +35,54 @@ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-dest
|
|
|
35
35
|
*/
|
|
36
36
|
export type VisionSidecarBackend = "openai" | "anthropic" | "routed";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Input modalities an explicit custom row declares for one routed model.
|
|
40
|
+
*
|
|
41
|
+
* A custom row is the operator's own definition of that model, so its declaration outranks the
|
|
42
|
+
* provider-level hints the predicates below read. Without this the two halves of one config
|
|
43
|
+
* disagreed in production: the catalog overlay in `src/codex/catalog/routed-gather.ts` copies
|
|
44
|
+
* `customModels[].inputModalities` onto the row and the dashboard showed "text, image", while
|
|
45
|
+
* the request path consulted only `providers[].noVisionModels` / `modelInputModalities` and
|
|
46
|
+
* stripped the image before dispatch. A model the operator had declared image-capable therefore
|
|
47
|
+
* received an omission marker instead of its attachment.
|
|
48
|
+
*
|
|
49
|
+
* Precedence, highest first: `modelCapabilities` (the documented per-model capability
|
|
50
|
+
* declaration), then this custom-row declaration, then `noVisionModels`, then
|
|
51
|
+
* `modelInputModalities`, then registry/vendor metadata. `modelCapabilities` stays on top
|
|
52
|
+
* because it is the dedicated capability axis and the CLI `--text-only` flag writes it; when the
|
|
53
|
+
* two explicit forms contradict each other the more specific axis wins.
|
|
54
|
+
*
|
|
55
|
+
* Matching is exact on the routed identity — provider name and native model id, the pair the
|
|
56
|
+
* custom-model API keys rows by. A row that declares no modalities returns `undefined` rather
|
|
57
|
+
* than `["text"]`, so it stays silent instead of turning into a text-only claim.
|
|
58
|
+
*/
|
|
59
|
+
export function customRowInputModalities(
|
|
60
|
+
config: Pick<OcxConfig, "customModels">,
|
|
61
|
+
providerName: string,
|
|
62
|
+
modelId: string,
|
|
63
|
+
): string[] | undefined {
|
|
64
|
+
for (const row of config.customModels ?? []) {
|
|
65
|
+
if (row.provider !== providerName || row.modelId !== modelId) continue;
|
|
66
|
+
if (Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
|
|
67
|
+
return [...row.inputModalities];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The custom row's verdict for one model, in the shape the predicates need:
|
|
75
|
+
* `true`/`false` when the row declares modalities, `undefined` when it declares none.
|
|
76
|
+
*/
|
|
77
|
+
function customRowAcceptsImageInput(
|
|
78
|
+
config: Pick<OcxConfig, "customModels">,
|
|
79
|
+
providerName: string,
|
|
80
|
+
modelId: string,
|
|
81
|
+
): boolean | undefined {
|
|
82
|
+
const declared = customRowInputModalities(config, providerName, modelId);
|
|
83
|
+
return declared === undefined ? undefined : declared.includes("image");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** The two sides every deployment has; also the empty-auth fallback set. */
|
|
39
87
|
export type UniversalVisionBackend = "openai" | "anthropic";
|
|
40
88
|
|
|
@@ -60,6 +108,14 @@ export interface VisionCandidateModel {
|
|
|
60
108
|
native?: boolean;
|
|
61
109
|
}
|
|
62
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The config slice the capability predicates need. `customModels` is optional so provider-only
|
|
113
|
+
* callers and unit tests keep compiling; a caller that omits it is simply silent about custom
|
|
114
|
+
* rows rather than wrong about them.
|
|
115
|
+
*/
|
|
116
|
+
export type VisionCapabilityConfig =
|
|
117
|
+
Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] };
|
|
118
|
+
|
|
63
119
|
export interface VisionModelOption {
|
|
64
120
|
value: string;
|
|
65
121
|
label: string;
|
|
@@ -142,20 +198,31 @@ function enrichedProviderForVision(
|
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
function isVisionSidecarConsumerWithCache(
|
|
145
|
-
config:
|
|
201
|
+
config: VisionCapabilityConfig,
|
|
146
202
|
providerName: string,
|
|
147
203
|
modelId: string,
|
|
148
204
|
cache: EnrichedProviderCache,
|
|
149
205
|
): boolean {
|
|
150
206
|
const provider = enrichedProviderForVision(config, providerName, cache);
|
|
151
|
-
|
|
207
|
+
if (provider === undefined) return false;
|
|
208
|
+
// Documented precedence, highest first: the dedicated per-model capability axis, then the
|
|
209
|
+
// operator's own custom row, then the provider-level hints. Reading the custom row ahead of
|
|
210
|
+
// `modelCapabilities` would let it override the more specific axis.
|
|
211
|
+
const capabilityDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
|
|
212
|
+
? provider.modelCapabilities?.[modelId]?.inputModalities : undefined;
|
|
213
|
+
if (capabilityDeclared !== undefined) {
|
|
214
|
+
return capabilityDeclared.includes("text") && !capabilityDeclared.includes("image");
|
|
215
|
+
}
|
|
216
|
+
const customDeclared = customRowInputModalities(config, providerName, modelId);
|
|
217
|
+
if (customDeclared !== undefined) return customDeclared.includes("text") && !customDeclared.includes("image");
|
|
218
|
+
return isModelVisionSidecarConsumer(provider, modelId);
|
|
152
219
|
}
|
|
153
220
|
|
|
154
221
|
/**
|
|
155
222
|
* Is this model listed as one the sidecar describes FOR? Such a model cannot be
|
|
156
223
|
* the describer, and its advertised modalities are untrustworthy.
|
|
157
224
|
*/
|
|
158
|
-
export function isVisionSidecarConsumer(config:
|
|
225
|
+
export function isVisionSidecarConsumer(config: VisionCapabilityConfig, providerName: string, modelId: string): boolean {
|
|
159
226
|
return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
|
|
160
227
|
}
|
|
161
228
|
|
|
@@ -165,23 +232,30 @@ export function isVisionSidecarConsumer(config: Pick<OcxConfig, "providers">, pr
|
|
|
165
232
|
* which callers treat as eligible.
|
|
166
233
|
*/
|
|
167
234
|
export function modelAcceptsImageInput(
|
|
168
|
-
config:
|
|
235
|
+
config: VisionCapabilityConfig,
|
|
169
236
|
candidate: VisionCandidateModel,
|
|
170
237
|
): boolean | undefined {
|
|
171
238
|
return modelAcceptsImageInputWithCache(config, candidate, new Map());
|
|
172
239
|
}
|
|
173
240
|
|
|
174
241
|
function modelAcceptsImageInputWithCache(
|
|
175
|
-
config:
|
|
242
|
+
config: VisionCapabilityConfig,
|
|
176
243
|
candidate: VisionCandidateModel,
|
|
177
244
|
cache: EnrichedProviderCache,
|
|
178
245
|
): boolean | undefined {
|
|
179
246
|
if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) {
|
|
180
247
|
const nativeProvider = enrichedProviderForVision(config, candidate.provider, cache);
|
|
181
|
-
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
182
248
|
const declared = Object.hasOwn(nativeProvider?.modelCapabilities ?? {}, candidate.id)
|
|
183
249
|
? nativeProvider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
184
250
|
if (declared !== undefined) return declared.includes("image");
|
|
251
|
+
// The catalog already lets an explicit custom row replace the native modality list for the
|
|
252
|
+
// same slug; consult it here too or the row would advertise what the request path ignores.
|
|
253
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
254
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
255
|
+
// The sidecar hints come after both explicit declarations, matching the documented order.
|
|
256
|
+
// Ahead of them a `noVisionModels` membership would short-circuit to text-only before the
|
|
257
|
+
// operator's own row was read.
|
|
258
|
+
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
185
259
|
return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;
|
|
186
260
|
}
|
|
187
261
|
if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false;
|
|
@@ -189,6 +263,11 @@ function modelAcceptsImageInputWithCache(
|
|
|
189
263
|
const declared = Object.hasOwn(provider?.modelCapabilities ?? {}, candidate.id)
|
|
190
264
|
? provider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
191
265
|
if (declared !== undefined) return declared.includes("image");
|
|
266
|
+
// Ahead of the provider's own hints: this row is the operator's definition of this exact model,
|
|
267
|
+
// and the catalog overlay reads the same field. Behind modelCapabilities, which is the dedicated
|
|
268
|
+
// capability axis the CLI `--text-only` writes.
|
|
269
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
270
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
192
271
|
const configuredModalities = provider ? modelRecordValue(provider.modelInputModalities, candidate.id) : undefined;
|
|
193
272
|
const fromConfiguredModalities = advertisesImageInput(configuredModalities);
|
|
194
273
|
if (fromConfiguredModalities !== undefined) return fromConfiguredModalities;
|
|
@@ -206,14 +285,14 @@ function modelAcceptsImageInputWithCache(
|
|
|
206
285
|
|
|
207
286
|
/** Eligible = not a sidecar consumer, and not positively known to be text-only. */
|
|
208
287
|
export function isVisionEligibleModel(
|
|
209
|
-
config:
|
|
288
|
+
config: VisionCapabilityConfig,
|
|
210
289
|
candidate: VisionCandidateModel,
|
|
211
290
|
): boolean {
|
|
212
291
|
return isVisionEligibleModelWithCache(config, candidate, new Map());
|
|
213
292
|
}
|
|
214
293
|
|
|
215
294
|
function isVisionEligibleModelWithCache(
|
|
216
|
-
config:
|
|
295
|
+
config: VisionCapabilityConfig,
|
|
217
296
|
candidate: VisionCandidateModel,
|
|
218
297
|
cache: EnrichedProviderCache,
|
|
219
298
|
): boolean {
|
|
@@ -274,7 +353,7 @@ function baselineCandidate(
|
|
|
274
353
|
* backend, and only `value` reaches the client, so first-wins costs nothing.
|
|
275
354
|
*/
|
|
276
355
|
export function visionEligibleModelOptions(
|
|
277
|
-
config:
|
|
356
|
+
config: VisionCapabilityConfig,
|
|
278
357
|
candidates: readonly VisionCandidateModel[],
|
|
279
358
|
enabledBackends: readonly VisionSidecarBackend[],
|
|
280
359
|
anthropicProviderName?: string,
|
package/src/vision/plan.ts
CHANGED
|
@@ -5,7 +5,11 @@ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
|
5
5
|
import type { CodexAuthPolicyConfig } from "../codex/auth-context";
|
|
6
6
|
import { isCodexReserveRequestEligible } from "../codex/loopback-target";
|
|
7
7
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
customRowInputModalities,
|
|
10
|
+
isModelVisionSidecarConsumer as isModelTextOnly,
|
|
11
|
+
modelAcceptsImageInput,
|
|
12
|
+
} from "./eligibility";
|
|
9
13
|
import { normalizeVisionReasoningForModel } from "./reasoning";
|
|
10
14
|
import { resolveSidecarAuth } from "../sidecar/auth";
|
|
11
15
|
import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds";
|
|
@@ -95,23 +99,40 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
|
|
|
95
99
|
/**
|
|
96
100
|
* Direct-image admission for a routed target. Returns true when capability evidence proves the
|
|
97
101
|
* target cannot accept image input, so the caller must describe or strip the image first.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
102
|
+
* Evidence is consulted highest-first: `modelCapabilities` (the dedicated per-model capability
|
|
103
|
+
* axis), an explicit custom row for the same routed identity, `noVisionModels`, an explicit
|
|
104
|
+
* per-model modality list without `image`, and finally proven-negative registry/vendor metadata.
|
|
105
|
+
* Any of them can require the vision preprocessor. A genuinely unknown custom model is NOT
|
|
106
|
+
* guessed blind: it keeps the established pass-through behaviour. The provider-only fallback
|
|
107
|
+
* keeps legacy unit callers stable; production dispatch always supplies providerName so the
|
|
108
|
+
* complete capability chain is consulted.
|
|
103
109
|
*/
|
|
104
110
|
export function requiresVisionPreprocessing(
|
|
105
|
-
config: Pick<OcxConfig, "providers"
|
|
111
|
+
config: Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] },
|
|
106
112
|
provider: Pick<OcxProviderConfig, "noVisionModels" | "modelInputModalities" | "modelCapabilities">,
|
|
107
113
|
modelId: string,
|
|
108
114
|
providerName?: string,
|
|
109
115
|
): boolean {
|
|
110
|
-
if (isModelTextOnly(provider, modelId)) return true;
|
|
111
116
|
const runtimeDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
|
|
112
117
|
? provider.modelCapabilities?.[modelId]?.inputModalities
|
|
113
118
|
: undefined;
|
|
119
|
+
// `modelCapabilities` is the dedicated per-model capability axis, so it outranks everything
|
|
120
|
+
// below — including an explicit custom row that disagrees. The CLI `--text-only` flag writes it.
|
|
114
121
|
if (runtimeDeclared !== undefined) return !runtimeDeclared.includes("image");
|
|
122
|
+
// An explicit custom row outranks the provider-level hints below, mirroring the catalog overlay
|
|
123
|
+
// in `src/codex/catalog/routed-gather.ts` that already copies this same declaration onto the
|
|
124
|
+
// advertised row. Without it the dashboard advertised "text, image" from the operator's own row
|
|
125
|
+
// while this predicate stripped the image before dispatch.
|
|
126
|
+
const customDeclared = providerName === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: customRowInputModalities(config, providerName, modelId);
|
|
129
|
+
// Same rule as the `modelCapabilities` branch above: the declaration answers "can this model
|
|
130
|
+
// take an image", not "is it a text model". A row that lists only `audio` or `video` excludes
|
|
131
|
+
// image input just as `["text"]` does, and `modelAcceptsImageInput` already answers "no image"
|
|
132
|
+
// for it; the narrower `includes("text")` test here made the two predicates disagree and sent
|
|
133
|
+
// the attachment to a model that cannot read it.
|
|
134
|
+
if (customDeclared !== undefined) return !customDeclared.includes("image");
|
|
135
|
+
if (isModelTextOnly(provider, modelId)) return true;
|
|
115
136
|
const runtimeModalities = modelRecordValue(provider.modelInputModalities, modelId);
|
|
116
137
|
if (Array.isArray(runtimeModalities) && runtimeModalities.length > 0) {
|
|
117
138
|
return !runtimeModalities.includes("image");
|
|
@@ -129,9 +150,12 @@ function usableRoutedVisionModel(config: OcxConfig): string | undefined {
|
|
|
129
150
|
if (!routedModel || sep <= 0) return undefined;
|
|
130
151
|
const targetProvider = routedModel.slice(0, sep);
|
|
131
152
|
const targetId = routedModel.slice(sep + 1);
|
|
132
|
-
|
|
153
|
+
// `modelAcceptsImageInput` is the one predicate here: it resolves the whole capability chain,
|
|
154
|
+
// custom row included. The provider-only `isModelTextOnly` that used to be ANDed in could only
|
|
155
|
+
// subtract — it never saw a custom row, so a describer the operator had declared image-capable
|
|
156
|
+
// was refused for a provider hint that same row overrides.
|
|
133
157
|
return modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
|
|
134
|
-
|
|
158
|
+
? routedModel : undefined;
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
export function shouldResolveOpenAiVisionSidecar(
|
|
@@ -3,7 +3,14 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
|
3
3
|
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
4
4
|
import { redactSecretString } from "../lib/redact";
|
|
5
5
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
applyUpstreamRecoveryInit,
|
|
8
|
+
fetchWithResetRetry,
|
|
9
|
+
releaseResponseBodyBestEffort,
|
|
10
|
+
retryBackoffDelayMs,
|
|
11
|
+
sleepWithAbort,
|
|
12
|
+
RETRY_AFTER_CEILING_MS,
|
|
13
|
+
} from "../lib/upstream-retry";
|
|
7
14
|
import { withUpstreamHttpVersion } from "../lib/upstream-http-version";
|
|
8
15
|
import { parseSidecarSSE, type WebSearchResult } from "./parse";
|
|
9
16
|
import type { CodexUpstreamOutcome } from "../codex/routing";
|
|
@@ -35,6 +42,22 @@ export const IMAGE_INSTRUCTION =
|
|
|
35
42
|
|
|
36
43
|
/** A search result, or an `error` string when the search couldn't run (surfaced as a tool result). */
|
|
37
44
|
export type SidecarOutcome = WebSearchResult & { error?: string };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bounded same-search 429 replays for the sidecar POST.
|
|
48
|
+
*
|
|
49
|
+
* The forward backend throttles burst sidecar traffic, and without a replay the 429 becomes a
|
|
50
|
+
* failed tool result that poisons the query for the whole turn (see failedQueries in loop.ts).
|
|
51
|
+
* 1 initial send + 2 replays; Retry-After is honored as a lower bound and capped by
|
|
52
|
+
* RETRY_AFTER_CEILING_MS (an instruction past the ceiling ends with the 429 instead of
|
|
53
|
+
* parking the search). Each wait releases the unread 429 body first so sockets do not
|
|
54
|
+
* accumulate under a rate-limit storm. Abort or timeout ends the wait through the existing
|
|
55
|
+
* catch, exactly like an abort during the SSE parse.
|
|
56
|
+
*/
|
|
57
|
+
const SIDECAR_429_MAX_ATTEMPTS = 3;
|
|
58
|
+
const SIDECAR_429_BASE_DELAY_MS = 1_000;
|
|
59
|
+
const SIDECAR_429_MAX_DELAY_MS = 10_000;
|
|
60
|
+
|
|
38
61
|
export type SidecarOutcomeRecorder = (outcome: CodexUpstreamOutcome) => void;
|
|
39
62
|
|
|
40
63
|
/**
|
|
@@ -79,7 +102,7 @@ export async function runWebSearch(
|
|
|
79
102
|
const sidecarExit = sidecarEnter("web-search");
|
|
80
103
|
const t0 = Date.now();
|
|
81
104
|
try {
|
|
82
|
-
const
|
|
105
|
+
const sendOnce = () => fetchWithResetRetry(
|
|
83
106
|
// Recovery nests INSIDE the version helper: applyUpstreamRecoveryInit then always receives a
|
|
84
107
|
// defined init, and withUpstreamHttpVersion spreads the result, so `protocol` and the
|
|
85
108
|
// recovery fields (`connection: close` + Bun's transport-level `keepalive: false`) survive
|
|
@@ -96,6 +119,22 @@ export async function runWebSearch(
|
|
|
96
119
|
}, recovery), forwardProvider)),
|
|
97
120
|
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
|
|
98
121
|
);
|
|
122
|
+
let res = await sendOnce();
|
|
123
|
+
for (let attempt = 0; res.status === 429 && attempt + 1 < SIDECAR_429_MAX_ATTEMPTS; attempt++) {
|
|
124
|
+
const delay = retryBackoffDelayMs(attempt, {
|
|
125
|
+
baseDelayMs: SIDECAR_429_BASE_DELAY_MS,
|
|
126
|
+
maxDelayMs: SIDECAR_429_MAX_DELAY_MS,
|
|
127
|
+
headers: res.headers,
|
|
128
|
+
retryAfterIsLowerBound: true,
|
|
129
|
+
});
|
|
130
|
+
// A deadline, not a clamp: an instruction past the ceiling ends the search with the
|
|
131
|
+
// 429 instead of parking it at a provider that already said it would refuse.
|
|
132
|
+
if (delay > RETRY_AFTER_CEILING_MS) break;
|
|
133
|
+
console.warn(`[web-search] sidecar HTTP 429 — retrying (${attempt + 2}/${SIDECAR_429_MAX_ATTEMPTS}) after ${delay}ms`);
|
|
134
|
+
await releaseResponseBodyBestEffort(res.body, linkedSignal.signal);
|
|
135
|
+
await sleepWithAbort(delay, linkedSignal.signal);
|
|
136
|
+
res = await sendOnce();
|
|
137
|
+
}
|
|
99
138
|
// Attach the body guard before ANY branch reads it. The success path guarded itself below,
|
|
100
139
|
// but the failure branch's `res.text()` runs first, so a cancel landing between fetch
|
|
101
140
|
// resolution and reader attach orphaned the internal rejection (found investigating #1419).
|
package/src/web-search/loop.ts
CHANGED
|
@@ -333,10 +333,15 @@ export interface WebSearchLoopDeps {
|
|
|
333
333
|
* 429 states the window's reset epoch even when it omits Retry-After -- and a rotation that
|
|
334
334
|
* cannot see it cools the drained account for the short default instead of until the window
|
|
335
335
|
* actually reopens. Optional so existing callers keep compiling.
|
|
336
|
+
*
|
|
337
|
+
* `retryParsed` is the exact iteration-local request the retry will be built from. The loop
|
|
338
|
+
* sends a shallow copy of the outer parsed request, so a rotation that rebinds only the outer
|
|
339
|
+
* object never reaches the wire. Optional so existing callers keep compiling.
|
|
336
340
|
*/
|
|
337
341
|
on429?: (
|
|
338
342
|
retryAfterHeader: string | null,
|
|
339
343
|
responseHeaders?: Headers,
|
|
344
|
+
retryParsed?: OcxParsedRequest,
|
|
340
345
|
) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
|
|
341
346
|
/** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
|
|
342
347
|
retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
|
|
@@ -558,7 +563,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
558
563
|
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
559
564
|
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
560
565
|
while (prepared.response.status === 429 && deps.on429) {
|
|
561
|
-
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers);
|
|
566
|
+
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers, iterParsed);
|
|
562
567
|
if (!rotated) break;
|
|
563
568
|
// Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
|
|
564
569
|
// it, but proceed immediately to the rotated fetch under the SAME deadline signal.
|