@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.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- 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
|
+
}
|
package/src/vision/index.ts
CHANGED
|
@@ -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.
|
|
66
|
-
//
|
|
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
|
};
|
package/src/web-search/index.ts
CHANGED
|
@@ -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 }
|
|
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;
|
package/src/web-search/loop.ts
CHANGED
|
@@ -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);
|
package/src/web-search/parse.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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();
|