@bitkyc08/opencodex 2.28.0 → 2.29.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 +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
type WriteRefused,
|
|
28
28
|
} from "../../integrations/writer";
|
|
29
29
|
import { IntegrationWriterLockBusyError, type IntegrationWriterLockSeams } from "../../integrations/writer-lock";
|
|
30
|
+
import {
|
|
31
|
+
INTEGRATION_MUTATION_TERMINAL_MS,
|
|
32
|
+
IntegrationMutationBusyError,
|
|
33
|
+
runIntegrationMutationFlight,
|
|
34
|
+
setIntegrationMutationFlightTestHook,
|
|
35
|
+
} from "../../integrations/mutation-flight";
|
|
30
36
|
import { jsonResponse } from "../auth-cors";
|
|
31
37
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
32
38
|
import type { ManagementContext } from "./context";
|
|
@@ -34,8 +40,7 @@ import { loadExportModels } from "./model-rows";
|
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
const INTEGRATION_ROUTE_PREFIX = "/api/client-integrations/";
|
|
37
|
-
|
|
38
|
-
export const INTEGRATION_MUTATION_TERMINAL_MS = 10 * 60_000;
|
|
43
|
+
export { INTEGRATION_MUTATION_TERMINAL_MS };
|
|
39
44
|
|
|
40
45
|
type IntegrationStateRecord = Awaited<ReturnType<typeof readIntegrationState>>;
|
|
41
46
|
type ApplyResult = Awaited<ReturnType<typeof applyIntegrationCoordinated>>;
|
|
@@ -81,19 +86,6 @@ export interface IntegrationRestoreBody {
|
|
|
81
86
|
confirmDrift?: boolean;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
interface IntegrationMutationFlight {
|
|
85
|
-
key: string;
|
|
86
|
-
startedAt: number;
|
|
87
|
-
promise: Promise<unknown>;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
class IntegrationMutationBusyError extends Error {
|
|
91
|
-
constructor(readonly clientId: IntegrationClientId) {
|
|
92
|
-
super("integration_mutation_busy");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const integrationMutationFlights = new Map<IntegrationClientId, IntegrationMutationFlight>();
|
|
97
89
|
let integrationMutationTestHooks: {
|
|
98
90
|
io?: IntegrationIO;
|
|
99
91
|
lockSeams?: IntegrationWriterLockSeams;
|
|
@@ -145,45 +137,6 @@ function decodeClientPath(pathname: string): string | null {
|
|
|
145
137
|
}
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
function runIntegrationMutationFlight<T>(
|
|
149
|
-
clientId: IntegrationClientId,
|
|
150
|
-
key: string,
|
|
151
|
-
now: () => number,
|
|
152
|
-
operation: () => Promise<T>,
|
|
153
|
-
): Promise<T> {
|
|
154
|
-
const startedAt = now();
|
|
155
|
-
const current = integrationMutationFlights.get(clientId);
|
|
156
|
-
if (current) {
|
|
157
|
-
const age = startedAt - current.startedAt;
|
|
158
|
-
if (current.key === key && age < INTEGRATION_MUTATION_JOIN_MS) {
|
|
159
|
-
return current.promise as Promise<T>;
|
|
160
|
-
}
|
|
161
|
-
if (age <= INTEGRATION_MUTATION_TERMINAL_MS) {
|
|
162
|
-
return Promise.reject(new IntegrationMutationBusyError(clientId));
|
|
163
|
-
}
|
|
164
|
-
if (integrationMutationFlights.get(clientId) === current) {
|
|
165
|
-
integrationMutationFlights.delete(clientId);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const flight: IntegrationMutationFlight = {
|
|
170
|
-
key,
|
|
171
|
-
startedAt,
|
|
172
|
-
promise: Promise.resolve(),
|
|
173
|
-
};
|
|
174
|
-
const run = async (): Promise<unknown> => operation();
|
|
175
|
-
flight.promise = (integrationMutationTestHooks?.run
|
|
176
|
-
? integrationMutationTestHooks.run(run)
|
|
177
|
-
: run()
|
|
178
|
-
).finally(() => {
|
|
179
|
-
if (integrationMutationFlights.get(clientId) === flight) {
|
|
180
|
-
integrationMutationFlights.delete(clientId);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
integrationMutationFlights.set(clientId, flight);
|
|
184
|
-
return flight.promise as Promise<T>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
140
|
export function setIntegrationMutationFlightTestHooks(
|
|
188
141
|
hooks: {
|
|
189
142
|
io?: IntegrationIO;
|
|
@@ -194,7 +147,7 @@ export function setIntegrationMutationFlightTestHooks(
|
|
|
194
147
|
} | null,
|
|
195
148
|
): void {
|
|
196
149
|
integrationMutationTestHooks = hooks;
|
|
197
|
-
|
|
150
|
+
setIntegrationMutationFlightTestHook(hooks?.run ?? null);
|
|
198
151
|
// Path overrides are part of the same isolation contract: clearing flights
|
|
199
152
|
// while leaving a temp home bound would let the next suite write real files.
|
|
200
153
|
if (hooks === null) integrationPathTestHooks = null;
|
|
@@ -129,6 +129,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
|
|
|
129
129
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
|
|
130
130
|
import { applySystemEnvToggle } from "../system-env";
|
|
131
131
|
import {
|
|
132
|
+
ClientPathError,
|
|
132
133
|
EXPORT_CLIENTS,
|
|
133
134
|
EXPORT_CLIENT_IDS,
|
|
134
135
|
OPENCODE_PROVIDER_ID,
|
|
@@ -204,6 +205,27 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
204
205
|
);
|
|
205
206
|
}
|
|
206
207
|
const spec = EXPORT_CLIENTS[requested];
|
|
208
|
+
// Resolved before the catalog load on purpose. A refused override is a
|
|
209
|
+
// property of the request, not of the catalog: validating it afterwards
|
|
210
|
+
// let a busy or failing catalog answer 503 first, so a user with a
|
|
211
|
+
// relative override never saw the message that says how to fix it — and
|
|
212
|
+
// the route did the enumeration work anyway for input it was going to
|
|
213
|
+
// reject.
|
|
214
|
+
let destination: string;
|
|
215
|
+
try {
|
|
216
|
+
destination = spec.destination(process.env);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// A client's own environment override can name a path the resolver
|
|
219
|
+
// refuses — a relative value, which this process and the client would
|
|
220
|
+
// resolve against different working directories. That is a
|
|
221
|
+
// user-correctable configuration error, not a server fault, so it leaves
|
|
222
|
+
// this boundary as a bounded 400 instead of escaping handleManagementAPI
|
|
223
|
+
// as a generic 500 and stripping the message that says how to fix it.
|
|
224
|
+
// `integrations/state.ts` and `integrations/writer.ts` already catch the
|
|
225
|
+
// same error on their paths; this route was the one that did not.
|
|
226
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
227
|
+
return jsonResponse({ error: error.message }, 400, req, config);
|
|
228
|
+
}
|
|
207
229
|
let models: ExportModel[];
|
|
208
230
|
try {
|
|
209
231
|
// The ONE loader every export surface uses. It carries the visibility
|
|
@@ -231,7 +253,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
231
253
|
return jsonResponse({
|
|
232
254
|
client: spec.id,
|
|
233
255
|
filename: spec.filename,
|
|
234
|
-
destination
|
|
256
|
+
destination,
|
|
235
257
|
apiKeyEnv: spec.apiKeyEnv,
|
|
236
258
|
exportHint: spec.exportHint,
|
|
237
259
|
// The client's own format and the exact bytes for it. The GUI previously
|
|
@@ -82,6 +82,10 @@ import {
|
|
|
82
82
|
LOCAL_PROVIDER_RELOAD_PATH,
|
|
83
83
|
} from "../../lib/local-provider-reload-contract";
|
|
84
84
|
import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
|
|
85
|
+
import {
|
|
86
|
+
XAI_RESPONSES_OPT_IN_MODELS,
|
|
87
|
+
xaiResponsesOptInState,
|
|
88
|
+
} from "../../providers/xai-responses-opt-in";
|
|
85
89
|
|
|
86
90
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
87
91
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
@@ -182,6 +186,20 @@ function applyProviderPatchFields(
|
|
|
182
186
|
next.liveModels = rawBody.liveModels;
|
|
183
187
|
touched = true;
|
|
184
188
|
}
|
|
189
|
+
if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
|
|
190
|
+
if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
|
|
191
|
+
if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
|
|
192
|
+
return { error: "xaiResponsesOptIn must be a boolean" };
|
|
193
|
+
}
|
|
194
|
+
const modelAdapters = { ...(next.modelAdapters ?? {}) };
|
|
195
|
+
for (const model of XAI_RESPONSES_OPT_IN_MODELS) {
|
|
196
|
+
if (rawBody.xaiResponsesOptIn) modelAdapters[model] = "openai-responses";
|
|
197
|
+
else delete modelAdapters[model];
|
|
198
|
+
}
|
|
199
|
+
if (Object.keys(modelAdapters).length > 0) next.modelAdapters = modelAdapters;
|
|
200
|
+
else delete next.modelAdapters;
|
|
201
|
+
touched = true;
|
|
202
|
+
}
|
|
185
203
|
if (Object.hasOwn(rawBody, "requestPacing")) {
|
|
186
204
|
const value = rawBody.requestPacing;
|
|
187
205
|
if (value === null) {
|
|
@@ -394,6 +412,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
394
412
|
apiKeyTransport: p.apiKeyTransport,
|
|
395
413
|
disabled: p.disabled === true,
|
|
396
414
|
codexAccountMode: providerCodexAccountMode(name, p),
|
|
415
|
+
...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
|
|
397
416
|
discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
|
|
398
417
|
})));
|
|
399
418
|
}
|
|
@@ -694,6 +713,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
694
713
|
name,
|
|
695
714
|
disabled: config.providers[name]!.disabled === true,
|
|
696
715
|
hasApiKey: !!config.providers[name]!.apiKey,
|
|
716
|
+
...(name === "xai"
|
|
717
|
+
? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
|
|
718
|
+
: {}),
|
|
697
719
|
catalogRefresh,
|
|
698
720
|
});
|
|
699
721
|
}
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
type VisionSidecarBackend,
|
|
18
18
|
} from "../../vision/eligibility";
|
|
19
19
|
import { listOpenAiForwardSidecarCandidates } from "../../providers/openai-sidecar";
|
|
20
|
-
import {
|
|
20
|
+
import { pickerVisibleSidecarCandidates } from "../../sidecar/candidates";
|
|
21
|
+
import { resolveSidecarAuth } from "../../sidecar/auth";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Backends whose executor could actually run: openai forward, anthropic OAuth.
|
|
@@ -41,17 +42,23 @@ export function enabledVisionBackends(
|
|
|
41
42
|
return backends.length > 0 ? backends : ["openai", "anthropic"];
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Visible catalog rows in the shape the eligibility predicate consumes.
|
|
47
|
+
* Sourced from the unified picker set (#2188): picker-visible rows ∪ auth
|
|
48
|
+
* slots, UNFILTERED. Rule 2 (− provably text-only) belongs to the OPTIONS
|
|
49
|
+
* path only (visionEligibleModelOptions already applies it). The PUT gate
|
|
50
|
+
* consumes this list as EVIDENCE: a picker row proving an id text-only is
|
|
51
|
+
* exactly what visionDescriberIsProvablyBlind needs to reject that id, so
|
|
52
|
+
* pre-filtering here would deaden the gate (review F1: reject → allow flip).
|
|
53
|
+
*/
|
|
45
54
|
export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
54
|
-
...(row.native ? { native: true } : {}),
|
|
55
|
+
const auth = resolveSidecarAuth(config);
|
|
56
|
+
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
57
|
+
return all.map(candidate => ({
|
|
58
|
+
provider: candidate.provider,
|
|
59
|
+
id: candidate.id,
|
|
60
|
+
...(candidate.inputModalities ? { inputModalities: candidate.inputModalities } : {}),
|
|
61
|
+
...(candidate.native ? { native: true } : {}),
|
|
55
62
|
}));
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that decides which models the management API offers as the
|
|
3
|
+
* web-search sidecar, and which it refuses to persist (#2188).
|
|
4
|
+
*
|
|
5
|
+
* Two routes write `webSearchSidecar.model` — `PUT /api/sidecar-settings`
|
|
6
|
+
* and the `webSearchSidecar` override in `PUT /api/claude-code`. They share
|
|
7
|
+
* this module for the same reason the vision routes share
|
|
8
|
+
* vision-sidecar-options.ts: a gate on one route and a stale copy on the
|
|
9
|
+
* other is the same as no gate at all. `ocx config set` raw JSON writes
|
|
10
|
+
* bypass management gates by design (operator escape hatch, same as vision).
|
|
11
|
+
*
|
|
12
|
+
* Unlike vision's provably-blind gate (reject only on positive proof), the
|
|
13
|
+
* web-search gate is MEMBERSHIP: the executor set is closed and known, so an
|
|
14
|
+
* id outside (candidates ∪ auth slots) can never run and is refused.
|
|
15
|
+
*/
|
|
16
|
+
import type { OcxConfig } from "../../types";
|
|
17
|
+
import { AUTH_SLOT_MODELS, resolveSidecarAuth } from "../../sidecar/auth";
|
|
18
|
+
import { pickerVisibleSidecarCandidates, type SidecarCandidate } from "../../sidecar/candidates";
|
|
19
|
+
import { WEB_SEARCH_BACKENDS, webSearchSidecarCandidates } from "../../web-search/backends";
|
|
20
|
+
import type { WebSearchBackendId } from "../../web-search/index";
|
|
21
|
+
|
|
22
|
+
// The full configured union. Rows only ever materialize for backends whose
|
|
23
|
+
// executor admits the candidate, so inert arms (gemini/exa without config)
|
|
24
|
+
// simply never produce rows.
|
|
25
|
+
export type WebSearchBackend = WebSearchBackendId;
|
|
26
|
+
|
|
27
|
+
export interface WebSearchCandidateRow extends SidecarCandidate {
|
|
28
|
+
/** Executor backend that admitted this exact candidate row. */
|
|
29
|
+
backend: WebSearchBackend;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WebSearchModelOption {
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
/** Exact runnable pair represented by this picker row. */
|
|
36
|
+
backend: WebSearchBackend;
|
|
37
|
+
model: string;
|
|
38
|
+
/** True when the row is an auth-slot entitlement rather than a picker row. */
|
|
39
|
+
authSlot?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The candidate rows the web-search executors can actually run right now. */
|
|
43
|
+
export async function webSearchCandidateRows(config: OcxConfig): Promise<WebSearchCandidateRow[]> {
|
|
44
|
+
const auth = resolveSidecarAuth(config);
|
|
45
|
+
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
46
|
+
return webSearchSidecarCandidates(config, auth, all).flatMap(candidate => {
|
|
47
|
+
const descriptor = WEB_SEARCH_BACKENDS.find(entry =>
|
|
48
|
+
entry.isActive(auth, config) && entry.eligibleModel(candidate, auth));
|
|
49
|
+
return descriptor ? [{ ...candidate, backend: descriptor.backend }] : [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The GUI/CLI option list. The persisted model is display-grandfathered so an
|
|
55
|
+
* operator can SEE a now-illegal setting in the picker (parity with the vision
|
|
56
|
+
* GET); new writes of such an id are still rejected by the gate below.
|
|
57
|
+
*/
|
|
58
|
+
export function webSearchModelOptionsFrom(
|
|
59
|
+
config: Pick<OcxConfig, "webSearchSidecar">,
|
|
60
|
+
candidates: readonly WebSearchCandidateRow[],
|
|
61
|
+
): WebSearchModelOption[] {
|
|
62
|
+
const byValue = new Map<string, WebSearchModelOption>();
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
if (byValue.has(candidate.id)) continue;
|
|
65
|
+
byValue.set(candidate.id, {
|
|
66
|
+
value: candidate.id,
|
|
67
|
+
label: candidate.id,
|
|
68
|
+
backend: candidate.backend,
|
|
69
|
+
model: candidate.id,
|
|
70
|
+
...(candidate.authSlot ? { authSlot: true } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const persisted = config.webSearchSidecar?.model;
|
|
74
|
+
if (persisted && !byValue.has(persisted)) {
|
|
75
|
+
byValue.set(persisted, {
|
|
76
|
+
value: persisted,
|
|
77
|
+
label: persisted,
|
|
78
|
+
backend: config.webSearchSidecar?.backend ?? "openai",
|
|
79
|
+
model: persisted,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Membership gate: reject when the id is neither a runnable candidate nor an
|
|
87
|
+
* auth-slot model. Auth slots pass even when the matching login is currently
|
|
88
|
+
* absent — the slot is a legal setting whose executor simply is not live yet,
|
|
89
|
+
* and refusing it would make settings order-dependent on login state.
|
|
90
|
+
*/
|
|
91
|
+
export function webSearchModelIsRejected(
|
|
92
|
+
backend: WebSearchBackend,
|
|
93
|
+
requested: string,
|
|
94
|
+
candidates: readonly WebSearchCandidateRow[],
|
|
95
|
+
): boolean {
|
|
96
|
+
if (requested === AUTH_SLOT_MODELS.codex) return backend !== "openai";
|
|
97
|
+
if (requested === AUTH_SLOT_MODELS.anthropic) return backend !== "anthropic";
|
|
98
|
+
return !candidates.some(candidate => candidate.backend === backend && candidate.id === requested);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Uniform 400 payload naming the filter, mirroring visionDescriberRejection's shape. */
|
|
102
|
+
export function webSearchModelRejection(
|
|
103
|
+
field: string,
|
|
104
|
+
backend: WebSearchBackend,
|
|
105
|
+
requested: string,
|
|
106
|
+
candidates: readonly WebSearchCandidateRow[],
|
|
107
|
+
): { error: string; allowedModels: string[] } {
|
|
108
|
+
const allowed = new Set(candidates
|
|
109
|
+
.filter(candidate => candidate.backend === backend)
|
|
110
|
+
.map(candidate => candidate.id));
|
|
111
|
+
// Preserve the response contract: allowedModels lists both always-configurable
|
|
112
|
+
// auth-slot ids even though the pair gate below binds each to its own backend.
|
|
113
|
+
allowed.add(AUTH_SLOT_MODELS.codex);
|
|
114
|
+
allowed.add(AUTH_SLOT_MODELS.anthropic);
|
|
115
|
+
return {
|
|
116
|
+
error: `${field}: backend/model pair "${backend}/${requested}" is not a web-search sidecar candidate — ` +
|
|
117
|
+
"the model must be picker-visible and runnable by that executor-backed backend (or its auth-slot model)",
|
|
118
|
+
allowedModels: [...allowed].sort(),
|
|
119
|
+
};
|
|
120
|
+
}
|