@bitkyc08/opencodex 2.76.0 → 2.77.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/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
type IntegrationClientId,
|
|
25
25
|
} from "../../integrations/registry";
|
|
26
26
|
import { detectRaycast, type RaycastInstall } from "../../integrations/raycast-detect";
|
|
27
|
-
import { readIntegrationState } from "../../integrations/state";
|
|
27
|
+
import { readIntegrationState, readOwnedDroidReasoningDefaults } from "../../integrations/state";
|
|
28
|
+
import { droidReasoningModels, validateDroidReasoningDefaults } from "../../clients/config-export";
|
|
28
29
|
import { createIntegrationStateStore, type IntegrationStateStore } from "../../integrations/store";
|
|
29
30
|
import {
|
|
30
31
|
applyIntegrationCoordinated,
|
|
@@ -74,6 +75,10 @@ export type IntegrationStateEnvelope = {
|
|
|
74
75
|
* the shared `IntegrationStatus`, which describes the file, not the app.
|
|
75
76
|
*/
|
|
76
77
|
raycast?: RaycastInstall;
|
|
78
|
+
droidReasoning?: {
|
|
79
|
+
models: Array<{ model: string; label: string; efforts: string[] }>;
|
|
80
|
+
defaults: Record<string, string>;
|
|
81
|
+
};
|
|
77
82
|
} & IntegrationStateRecord;
|
|
78
83
|
|
|
79
84
|
export interface IntegrationStateListEnvelope {
|
|
@@ -241,12 +246,14 @@ async function buildIntegrationWriteInput(
|
|
|
241
246
|
clientId: IntegrationClientId,
|
|
242
247
|
ctx: ManagementContext,
|
|
243
248
|
store: IntegrationStateStore,
|
|
249
|
+
droidReasoningDefaults?: Record<string, string>,
|
|
244
250
|
): Promise<IntegrationWriteInput> {
|
|
245
251
|
return {
|
|
246
252
|
clientId,
|
|
247
253
|
models: await loadExportModels(ctx.config),
|
|
248
254
|
config: ctx.config,
|
|
249
255
|
port: Number(ctx.url.port) || ctx.config.port,
|
|
256
|
+
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
|
|
250
257
|
store,
|
|
251
258
|
io: integrationMutationTestHooks?.io,
|
|
252
259
|
...pathOverrides(),
|
|
@@ -265,6 +272,7 @@ async function buildIntegrationPreviewInput(
|
|
|
265
272
|
clientId: IntegrationClientId,
|
|
266
273
|
ctx: ManagementContext,
|
|
267
274
|
store: IntegrationStateStore,
|
|
275
|
+
droidReasoningDefaults?: Record<string, string>,
|
|
268
276
|
): Promise<{ input: IntegrationWriteInput; identity: string } | null> {
|
|
269
277
|
const snapshot = previewExportSnapshot(ctx.config);
|
|
270
278
|
// No cached roster means no honest snapshot to plan against. Gathering one here would make a
|
|
@@ -277,6 +285,7 @@ async function buildIntegrationPreviewInput(
|
|
|
277
285
|
models: snapshot.models,
|
|
278
286
|
config: ctx.config,
|
|
279
287
|
port: Number(ctx.url.port) || ctx.config.port,
|
|
288
|
+
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
|
|
280
289
|
store,
|
|
281
290
|
io: integrationMutationTestHooks?.io,
|
|
282
291
|
...pathOverrides(),
|
|
@@ -309,6 +318,31 @@ function invalidClientResponse(ctx: ManagementContext): Response {
|
|
|
309
318
|
}, 400, ctx.req, ctx.config);
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
322
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
323
|
+
&& Object.values(value).every(item => typeof item === "string");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function invalidDroidDefaultsResponse(ctx: ManagementContext, message: string): Response {
|
|
327
|
+
return jsonResponse({ error: message, code: "invalid_droid_reasoning_defaults" }, 400, ctx.req, ctx.config);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function readDroidReasoningDefaults(
|
|
331
|
+
ctx: ManagementContext,
|
|
332
|
+
body: Record<string, unknown>,
|
|
333
|
+
clientId: string,
|
|
334
|
+
operation: IntegrationPlanOperation,
|
|
335
|
+
): Record<string, string> | Response | undefined {
|
|
336
|
+
if (!Object.hasOwn(body, "droidReasoningDefaults")) return undefined;
|
|
337
|
+
if (clientId !== "droid" || (operation !== "apply" && operation !== "overwrite")) {
|
|
338
|
+
return invalidDroidDefaultsResponse(ctx, "droidReasoningDefaults is allowed only for Droid apply or overwrite");
|
|
339
|
+
}
|
|
340
|
+
const defaults = body.droidReasoningDefaults;
|
|
341
|
+
return isStringRecord(defaults)
|
|
342
|
+
? defaults
|
|
343
|
+
: invalidDroidDefaultsResponse(ctx, "droidReasoningDefaults must map model names to effort strings");
|
|
344
|
+
}
|
|
345
|
+
|
|
312
346
|
/**
|
|
313
347
|
* No cached model roster, so there is nothing honest to plan against.
|
|
314
348
|
*
|
|
@@ -364,6 +398,7 @@ function stalePlanGuard(
|
|
|
364
398
|
request: PreviewRequest,
|
|
365
399
|
fingerprint: string,
|
|
366
400
|
capturedIdentity: string | null,
|
|
401
|
+
droidReasoningDefaults?: Record<string, string>,
|
|
367
402
|
): {
|
|
368
403
|
revalidate: NonNullable<CoordinatedIntegrationOptions["revalidate"]>;
|
|
369
404
|
response: () => Response | null;
|
|
@@ -379,7 +414,7 @@ function stalePlanGuard(
|
|
|
379
414
|
* without ever swapping the roster this mutation is about to use.
|
|
380
415
|
*/
|
|
381
416
|
if (exportSnapshotIdentity(ctx.config) !== capturedIdentity) {
|
|
382
|
-
const refreshed = await buildIntegrationPreviewInput(clientId, ctx, store);
|
|
417
|
+
const refreshed = await buildIntegrationPreviewInput(clientId, ctx, store, droidReasoningDefaults);
|
|
383
418
|
stale = refreshed === null ? "unavailable" : previewIntegration(refreshed.input, request);
|
|
384
419
|
return { ok: false, reason: "conflict", state: "conflict", clientId, message: "the model roster changed while confirming" };
|
|
385
420
|
}
|
|
@@ -723,9 +758,17 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
723
758
|
code: "invalid_preview_operation",
|
|
724
759
|
}, 400, req, ctx.config);
|
|
725
760
|
}
|
|
761
|
+
const droidDefaults = readDroidReasoningDefaults(ctx, parsed, previewClient, operation);
|
|
762
|
+
if (droidDefaults instanceof Response) return droidDefaults;
|
|
726
763
|
try {
|
|
727
|
-
const captured = await buildIntegrationPreviewInput(
|
|
764
|
+
const captured = await buildIntegrationPreviewInput(
|
|
765
|
+
previewClient as IntegrationClientId, ctx, integrationStore(), droidDefaults,
|
|
766
|
+
);
|
|
728
767
|
if (!captured) return previewUnavailableResponse(ctx);
|
|
768
|
+
if (droidDefaults !== undefined) {
|
|
769
|
+
const invalid = validateDroidReasoningDefaults(captured.input.models, droidDefaults);
|
|
770
|
+
if (invalid) return invalidDroidDefaultsResponse(ctx, invalid);
|
|
771
|
+
}
|
|
729
772
|
return jsonResponse(previewIntegration(captured.input, { operation }), 200, req, ctx.config);
|
|
730
773
|
} catch (error) {
|
|
731
774
|
return internalErrorResponse(error, ctx);
|
|
@@ -907,7 +950,15 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
907
950
|
// process spawn, and no other client's read should pay for it.
|
|
908
951
|
const envelope: IntegrationStateEnvelope = requestedClient === "raycast"
|
|
909
952
|
? { ...state, raycast: (raycastDetectTestHook ?? detectRaycast)() }
|
|
910
|
-
:
|
|
953
|
+
: requestedClient === "droid"
|
|
954
|
+
? {
|
|
955
|
+
...state,
|
|
956
|
+
droidReasoning: {
|
|
957
|
+
models: droidReasoningModels(input.models),
|
|
958
|
+
defaults: readOwnedDroidReasoningDefaults(input),
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
: state;
|
|
911
962
|
return jsonResponse(envelope, 200, req, ctx.config);
|
|
912
963
|
} catch (error) {
|
|
913
964
|
return internalErrorResponse(error, ctx);
|
|
@@ -944,6 +995,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
944
995
|
const requestedOperation: IntegrationPlanOperation = parsed.enabled
|
|
945
996
|
? (parsed.overwriteConflict === true ? "overwrite" : "apply")
|
|
946
997
|
: "disable";
|
|
998
|
+
const droidDefaults = readDroidReasoningDefaults(ctx, parsed, requestedClient, requestedOperation);
|
|
999
|
+
if (droidDefaults instanceof Response) return droidDefaults;
|
|
947
1000
|
const binding = planBindingOf(parsed);
|
|
948
1001
|
if (binding === "half") return halfBoundResponse(ctx);
|
|
949
1002
|
if (binding === "unknown-operation" || (binding !== "none" && binding.operation !== requestedOperation)) {
|
|
@@ -964,11 +1017,15 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
964
1017
|
*/
|
|
965
1018
|
const boundToggle = binding === "none"
|
|
966
1019
|
? null
|
|
967
|
-
: await buildIntegrationPreviewInput(requestedClient, ctx, integrationStore());
|
|
1020
|
+
: await buildIntegrationPreviewInput(requestedClient, ctx, integrationStore(), droidDefaults);
|
|
968
1021
|
if (binding !== "none" && boundToggle === null) return previewUnavailableResponse(ctx);
|
|
969
1022
|
const input = boundToggle
|
|
970
1023
|
? boundToggle.input
|
|
971
|
-
: await buildIntegrationWriteInput(requestedClient, ctx, integrationStore());
|
|
1024
|
+
: await buildIntegrationWriteInput(requestedClient, ctx, integrationStore(), droidDefaults);
|
|
1025
|
+
if (droidDefaults !== undefined) {
|
|
1026
|
+
const invalid = validateDroidReasoningDefaults(input.models, droidDefaults);
|
|
1027
|
+
if (invalid) return invalidDroidDefaultsResponse(ctx, invalid);
|
|
1028
|
+
}
|
|
972
1029
|
const guard = binding === "none" ? null : stalePlanGuard(
|
|
973
1030
|
requestedClient,
|
|
974
1031
|
ctx,
|
|
@@ -976,6 +1033,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
976
1033
|
{ operation: binding.operation },
|
|
977
1034
|
binding.fingerprint,
|
|
978
1035
|
boundToggle === null ? null : boundToggle.identity,
|
|
1036
|
+
droidDefaults,
|
|
979
1037
|
);
|
|
980
1038
|
const result = await runIntegrationMutationFlight(
|
|
981
1039
|
requestedClient,
|
|
@@ -86,7 +86,7 @@ function port(value: unknown): value is number {
|
|
|
86
86
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** A
|
|
89
|
+
/** A GUI session redeemed from an operator-created, one-use pairing grant. */
|
|
90
90
|
function pairedSession(ctx: ManagementContext): boolean {
|
|
91
91
|
return ctx.principal === "gui-session"
|
|
92
92
|
&& ctx.sessionControl?.isPaired(ctx.req, ctx.config) === true;
|
|
@@ -96,9 +96,8 @@ function pairedSession(ctx: ManagementContext): boolean {
|
|
|
96
96
|
* A paired session, or on a standalone runtime the current loopback-issued session that reached
|
|
97
97
|
* the public listener bound to a loopback hostname. The loopback bootstrap mints that session
|
|
98
98
|
* without a credential, so this is casual-path protection like POST /api/github/star, not a
|
|
99
|
-
* secret-backed boundary like the admin token. Hubs keep the paired-only rule.
|
|
100
|
-
*
|
|
101
|
-
* dashboard warns first that the restart briefly interrupts running Codex turns.
|
|
99
|
+
* secret-backed boundary like the admin token. Hubs keep the paired-only rule. Durable operations
|
|
100
|
+
* that require operator approval, such as joining this machine as a Child, use pairedSession.
|
|
102
101
|
*/
|
|
103
102
|
function dashboardSession(ctx: ManagementContext): boolean {
|
|
104
103
|
if (pairedSession(ctx)) return true;
|
|
@@ -113,9 +112,10 @@ function adminLoopback(ctx: ManagementContext): boolean {
|
|
|
113
112
|
return ctx.principal === "admin-token" && ctx.trustedLoopbackIngress;
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
function auth(ctx: ManagementContext, kind: "dashboard" | "admin" | "either"): Response | null {
|
|
115
|
+
function auth(ctx: ManagementContext, kind: "dashboard" | "paired" | "admin" | "either"): Response | null {
|
|
117
116
|
if (ctx.guiSessionIssuance === "tailscale-identity") return fail("tailscale_session_refused", "Tailscale identity sessions cannot use link routes.", 403);
|
|
118
117
|
const allowed = kind === "dashboard" ? dashboardSession(ctx)
|
|
118
|
+
: kind === "paired" ? pairedSession(ctx)
|
|
119
119
|
: kind === "admin" ? adminLoopback(ctx)
|
|
120
120
|
: dashboardSession(ctx) || adminLoopback(ctx);
|
|
121
121
|
return allowed ? null : fail("forbidden", "The required link authorization was not present.", 403);
|
|
@@ -131,9 +131,14 @@ function joinPortMatches(ctx: ManagementContext): boolean {
|
|
|
131
131
|
return live !== undefined && live === ctx.config.port;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
type LinkJoinDenied = "pairing_required" | "standalone_required" | "join_port_mismatch";
|
|
135
|
+
|
|
136
|
+
/** Read-only explanation of the join gates, in the same order as `POST /api/link/join`. */
|
|
137
|
+
function joinDenied(ctx: ManagementContext): LinkJoinDenied | null {
|
|
138
|
+
if (!pairedSession(ctx)) return "pairing_required";
|
|
139
|
+
if ((ctx.config.runtimeRole ?? "standalone") !== "standalone") return "standalone_required";
|
|
140
|
+
if (!joinPortMatches(ctx)) return "join_port_mismatch";
|
|
141
|
+
return null;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
function runnerFor(ctx: ManagementContext): SshRunner {
|
|
@@ -549,9 +554,9 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
549
554
|
if (!isLinkPath(path)) return null;
|
|
550
555
|
if (ctx.guiSessionIssuance === "tailscale-identity") return fail("tailscale_session_refused", "Tailscale identity sessions cannot use link routes.", 403);
|
|
551
556
|
if (url.pathname === "/api/link/join" && req.method === "POST") {
|
|
552
|
-
//
|
|
553
|
-
// is
|
|
554
|
-
const denied = auth(ctx, "
|
|
557
|
+
// Joining durably redirects local client traffic, so a credentiallessly bootstrapped loopback
|
|
558
|
+
// session is insufficient. Every refusal below runs before link state and before any SSH.
|
|
559
|
+
const denied = auth(ctx, "paired");
|
|
555
560
|
if (denied) return denied;
|
|
556
561
|
if ((ctx.config.runtimeRole ?? "standalone") !== "standalone") return fail("standalone_required", "Client initiated links require standalone runtime mode.", 409);
|
|
557
562
|
if (!joinPortMatches(ctx)) return fail("join_port_mismatch", "OpenCodex is not running on its configured port, so it cannot restart as a Child.", 409);
|
|
@@ -578,9 +583,11 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
578
583
|
const failure = state.compensationFailures?.get(store.links[0].id);
|
|
579
584
|
if (failure) Object.assign(dto.child, { state: "failed" as const, since: failure.since, reason: failure.reason });
|
|
580
585
|
}
|
|
581
|
-
// A dashboard session also learns
|
|
586
|
+
// A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
|
|
582
587
|
// the exact K16 document that `ocx link status` validates key by key.
|
|
583
|
-
const
|
|
588
|
+
const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
|
|
589
|
+
const body: LinkStatusDto & { joinAvailable?: boolean; joinDenied?: LinkJoinDenied | null } = ctx.principal === "gui-session"
|
|
590
|
+
? { ...dto, joinAvailable: denial === null, joinDenied: denial } : dto;
|
|
584
591
|
return Response.json(body, { headers: { "cache-control": "no-store" } });
|
|
585
592
|
}
|
|
586
593
|
if (url.pathname === "/api/link/candidates" && req.method === "GET") {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effective export metadata for custom `/api/models` rows.
|
|
3
|
+
*
|
|
4
|
+
* A custom row's own fields are the operator's stored OVERRIDE. `/api/models` returns them
|
|
5
|
+
* verbatim because the edit dialog must show what the user set — including an explicit empty
|
|
6
|
+
* "no reasoning" ladder — not what the provider happens to advertise today. But a client
|
|
7
|
+
* export has to serialize what the proxy will actually route: the canonical resolved catalog
|
|
8
|
+
* metadata for the same `provider/modelId`. `listManagementModelRows` resolves that
|
|
9
|
+
* projection here and attaches it additively, so the editor's raw fields and the exporter's
|
|
10
|
+
* effective fields can never overwrite each other.
|
|
11
|
+
*
|
|
12
|
+
* Sources are the ones the routed-row projection already uses — the gathered catalog row for
|
|
13
|
+
* the same slug (the gather materializes custom rows with provider/native inheritance), the
|
|
14
|
+
* registry-enriched provider config, and the ladder resolution in `model-rows.ts`. No
|
|
15
|
+
* independent provider fact tables, and no per-model fetch: everything comes from the roster
|
|
16
|
+
* and config already in hand.
|
|
17
|
+
*/
|
|
18
|
+
import { enrichProviderFromRegistry } from "../../providers/derive";
|
|
19
|
+
import { modelRecordValue } from "../../reasoning-effort";
|
|
20
|
+
import { configuredReasoningSummarySupport } from "../../codex/catalog/model-hints";
|
|
21
|
+
import type { CatalogModel } from "../../codex/catalog";
|
|
22
|
+
import type { OcxConfig } from "../../types";
|
|
23
|
+
import {
|
|
24
|
+
knownReasoningSupport,
|
|
25
|
+
knownToolsSupport,
|
|
26
|
+
type EffectiveModelExportMetadata,
|
|
27
|
+
} from "../../clients/config-export/contracts";
|
|
28
|
+
|
|
29
|
+
/** The stored-override fields a custom row carries, exactly as `config.customModels` writes them. */
|
|
30
|
+
export interface CustomRowOverrideSource {
|
|
31
|
+
provider: string;
|
|
32
|
+
id: string;
|
|
33
|
+
contextWindow?: number;
|
|
34
|
+
inputModalities?: string[];
|
|
35
|
+
/** Explicit ladder; `[]` is a declaration ("no rungs"), not an absence. */
|
|
36
|
+
reasoningEfforts?: string[];
|
|
37
|
+
defaultReasoningEffort?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the effective export metadata for one custom row.
|
|
42
|
+
*
|
|
43
|
+
* Explicit custom overrides win; resolved gathered facts precede provider-config fallbacks.
|
|
44
|
+
* `effectiveReasoningEfforts` is
|
|
45
|
+
* supplied by the caller because its resolution (declared ladder first, then the enriched
|
|
46
|
+
* provider maps, then the pinned native table, then the catalog hints) lives with the other
|
|
47
|
+
* ladder readers in `model-rows.ts`; here it is an input, resolved once per row.
|
|
48
|
+
*/
|
|
49
|
+
export function customRowExportMetadata(
|
|
50
|
+
config: OcxConfig,
|
|
51
|
+
source: CustomRowOverrideSource,
|
|
52
|
+
catalogRow: CatalogModel | undefined,
|
|
53
|
+
effectiveReasoningEfforts: string[] | undefined,
|
|
54
|
+
): EffectiveModelExportMetadata {
|
|
55
|
+
const provider = config.providers[source.provider];
|
|
56
|
+
// The same registry hydration the other per-model readers at this boundary perform, so a
|
|
57
|
+
// custom row whose provider declares facts only in the registry still inherits them. The
|
|
58
|
+
// per-model records are cloned first because the enrichment fills and folds them in place.
|
|
59
|
+
const enriched = provider === undefined ? undefined : {
|
|
60
|
+
...provider,
|
|
61
|
+
...(provider.modelContextWindows ? { modelContextWindows: { ...provider.modelContextWindows } } : {}),
|
|
62
|
+
...(provider.modelInputModalities ? { modelInputModalities: { ...provider.modelInputModalities } } : {}),
|
|
63
|
+
...(provider.modelCapabilities ? { modelCapabilities: { ...provider.modelCapabilities } } : {}),
|
|
64
|
+
...(provider.modelReasoningEfforts ? { modelReasoningEfforts: { ...provider.modelReasoningEfforts } } : {}),
|
|
65
|
+
...(provider.modelDefaultReasoningEfforts
|
|
66
|
+
? { modelDefaultReasoningEfforts: { ...provider.modelDefaultReasoningEfforts } }
|
|
67
|
+
: {}),
|
|
68
|
+
};
|
|
69
|
+
if (enriched) enrichProviderFromRegistry(source.provider, enriched);
|
|
70
|
+
|
|
71
|
+
// Gathered resolved context first after the explicit override: the gather already ran the
|
|
72
|
+
// canonical precedence (operator maps, registry, native clamps) and may have applied a
|
|
73
|
+
// tighter cap than a config map read here would suggest. Re-reading the config maps ahead
|
|
74
|
+
// of it would duplicate that policy and could widen past an observed tighter window.
|
|
75
|
+
const contextWindow = source.contextWindow
|
|
76
|
+
?? catalogRow?.contextWindow
|
|
77
|
+
?? modelRecordValue(enriched?.modelContextWindows, source.id)
|
|
78
|
+
?? enriched?.contextWindow;
|
|
79
|
+
// Same declaration chain `declaredModelInputModalities` reads, as the fallback when neither
|
|
80
|
+
// the override nor the gathered row says anything.
|
|
81
|
+
const declaredModalities = enriched === undefined ? undefined : (
|
|
82
|
+
enriched.modelCapabilities?.[source.id]?.inputModalities
|
|
83
|
+
?? modelRecordValue(enriched.modelInputModalities, source.id)
|
|
84
|
+
);
|
|
85
|
+
const inputModalities = source.inputModalities ?? catalogRow?.inputModalities ?? declaredModalities;
|
|
86
|
+
// The gathered row carries what the materializer already resolved; for a rosterless row the
|
|
87
|
+
// same reader the materializer uses answers from the provider config, so both paths agree.
|
|
88
|
+
const supportsReasoningSummaries = catalogRow?.supportsReasoningSummaries
|
|
89
|
+
?? configuredReasoningSummarySupport(provider, source.id);
|
|
90
|
+
const defaultReasoningEffort = declaredDefaultWithin(
|
|
91
|
+
effectiveReasoningEfforts,
|
|
92
|
+
defaultEffort(source, catalogRow, provider),
|
|
93
|
+
);
|
|
94
|
+
return {
|
|
95
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
96
|
+
...(catalogRow?.maxInputTokens !== undefined ? { maxInputTokens: catalogRow.maxInputTokens } : {}),
|
|
97
|
+
...(catalogRow?.maxOutputTokens !== undefined ? { maxTokens: catalogRow.maxOutputTokens } : {}),
|
|
98
|
+
...(inputModalities !== undefined ? { inputModalities: [...inputModalities] } : {}),
|
|
99
|
+
...(effectiveReasoningEfforts !== undefined ? { reasoningEfforts: [...effectiveReasoningEfforts] } : {}),
|
|
100
|
+
...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}),
|
|
101
|
+
...(knownToolsSupport({
|
|
102
|
+
capabilities: catalogRow?.capabilities,
|
|
103
|
+
parallelToolCalls: catalogRow?.parallelToolCalls,
|
|
104
|
+
}) === true ? { supportsTools: true } : {}),
|
|
105
|
+
...(knownReasoningSupport({ reasoningEfforts: effectiveReasoningEfforts, supportsReasoningSummaries }) === true
|
|
106
|
+
? { supportsReasoning: true }
|
|
107
|
+
: {}),
|
|
108
|
+
...(supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The declared default, suppressed unless it is a member of the effective ladder. An empty
|
|
114
|
+
* ladder has no rung to default to, and a default the ladder does not contain describes an
|
|
115
|
+
* effort the proxy would never send — the same rule the settings route applies when it
|
|
116
|
+
* rejects a default "not in the declared reasoningEfforts ladder", applied here so an export
|
|
117
|
+
* never ships a default the row cannot act on.
|
|
118
|
+
*/
|
|
119
|
+
function declaredDefaultWithin(
|
|
120
|
+
effectiveReasoningEfforts: string[] | undefined,
|
|
121
|
+
declared: string | undefined,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (declared === undefined) return undefined;
|
|
124
|
+
if (effectiveReasoningEfforts === undefined) return declared;
|
|
125
|
+
return effectiveReasoningEfforts.includes(declared) ? declared : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The declared default, and nothing else. The ladder-preference fallback
|
|
130
|
+
* (`effectiveModelDefaultReasoningEffort`'s medium → low → first) fabricates a default no
|
|
131
|
+
* source declared; it is right for the picker's own preference order but wrong for an export
|
|
132
|
+
* that claims to carry provider truth, so this projection stops at:
|
|
133
|
+
*
|
|
134
|
+
* 1. the explicit custom default (an override always wins),
|
|
135
|
+
* 2. the gathered catalog row's declared default (the gather only lets it ride when it is a
|
|
136
|
+
* member of the effective ladder),
|
|
137
|
+
* 3. the operator's per-model record — the same raw map the routed-row reader consults.
|
|
138
|
+
*
|
|
139
|
+
* `declaredDefaultWithin` then drops the answer when it is not a member of the effective
|
|
140
|
+
* ladder, empty ladder included.
|
|
141
|
+
*/
|
|
142
|
+
function defaultEffort(
|
|
143
|
+
source: CustomRowOverrideSource,
|
|
144
|
+
catalogRow: CatalogModel | undefined,
|
|
145
|
+
provider: { modelDefaultReasoningEfforts?: Record<string, string> } | undefined,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
if (source.defaultReasoningEffort) return source.defaultReasoningEffort;
|
|
148
|
+
if (catalogRow?.defaultReasoningEffort) return catalogRow.defaultReasoningEffort;
|
|
149
|
+
return modelRecordValue(provider?.modelDefaultReasoningEfforts, source.id) ?? undefined;
|
|
150
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Bodies are unchanged from their previous home; only `export` was added.
|
|
10
10
|
*/
|
|
11
11
|
import type { CatalogModel } from "../../codex/catalog";
|
|
12
|
+
import { antigravityFamilyDisabled } from "../../providers/antigravity-effort-families";
|
|
12
13
|
import { observeModelCacheRevision } from "../../codex/model-cache";
|
|
13
14
|
import {
|
|
14
15
|
captureExportConfigAdmission,
|
|
@@ -30,7 +31,13 @@ import {
|
|
|
30
31
|
catalogModelEfforts,
|
|
31
32
|
} from "../../codex/catalog";
|
|
32
33
|
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../../codex/catalog/native-models";
|
|
33
|
-
import
|
|
34
|
+
import {
|
|
35
|
+
knownReasoningSupport,
|
|
36
|
+
knownToolsSupport,
|
|
37
|
+
type EffectiveModelExportMetadata,
|
|
38
|
+
type ExportModel,
|
|
39
|
+
} from "../../clients/config-export/contracts";
|
|
40
|
+
import { customRowExportMetadata } from "./model-row-export-metadata";
|
|
34
41
|
import { providerContextCap } from "../../providers/context-cap";
|
|
35
42
|
import { isVisionReasoningEffort, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
36
43
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
@@ -78,6 +85,28 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
78
85
|
* config — so this compares values against what the model would inherit anyway.
|
|
79
86
|
*/
|
|
80
87
|
reasoningOverridden?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Tool support a canonical source positively asserts (native identity, a catalog row's
|
|
90
|
+
* `"tools"` capability, or the parallel-call opt-in — the same evidence
|
|
91
|
+
* `src/routing/capability.ts` counts). A row without positive evidence stays absent:
|
|
92
|
+
* unknown is not false.
|
|
93
|
+
*/
|
|
94
|
+
supportsTools?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* True when a canonical source positively asserts reasoning (a non-empty effective ladder
|
|
97
|
+
* or delivered reasoning summaries). Absent means unknown, never "cannot reason": an empty
|
|
98
|
+
* ladder only says the effort is not adjustable.
|
|
99
|
+
*/
|
|
100
|
+
supportsReasoning?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Effective export projection for a row, resolved from canonical sources only — for a
|
|
103
|
+
* custom row the full effective view (the raw override fields above stay exactly what the
|
|
104
|
+
* editor wrote), for routed and native rows the authoritative declared default where the
|
|
105
|
+
* row's own `defaultReasoningEffort` is the picker's preference order instead. Additive on
|
|
106
|
+
* `/api/models`, and its PRESENCE is the authority signal: an absent field inside it means
|
|
107
|
+
* "no declared value", never "fall back to the row".
|
|
108
|
+
*/
|
|
109
|
+
exportMetadata?: EffectiveModelExportMetadata;
|
|
81
110
|
};
|
|
82
111
|
|
|
83
112
|
/** Resolve the exact text and source shown for one routed discovered model. */
|
|
@@ -322,6 +351,13 @@ export async function listManagementModelRows(
|
|
|
322
351
|
...(row.autoCompactTokenLimit !== undefined
|
|
323
352
|
? { autoCompactTokenLimit: row.autoCompactTokenLimit }
|
|
324
353
|
: {}),
|
|
354
|
+
// The pinned default above IS the declared default for a native row — no heuristic
|
|
355
|
+
// touches it — so the authoritative projection carries it verbatim and exists even
|
|
356
|
+
// when empty, which is what tells an export consumer "absence means no default"
|
|
357
|
+
// rather than "fall back to the row".
|
|
358
|
+
exportMetadata: {
|
|
359
|
+
...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}),
|
|
360
|
+
},
|
|
325
361
|
};
|
|
326
362
|
});
|
|
327
363
|
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
@@ -374,6 +410,34 @@ export async function listManagementModelRows(
|
|
|
374
410
|
// the guard and the value) re-read the same file thousands of times on a large roster.
|
|
375
411
|
const routedSlugs = publicModels.filter(m => m.provider !== "combo").map(m => routedSlug(m.provider, m.id));
|
|
376
412
|
const catalogLadders = catalogModelEfforts([...new Set(routedSlugs)]);
|
|
413
|
+
// The gathered roster, keyed once by Codex-facing slug. A custom row looks itself up here —
|
|
414
|
+
// the gather materializes custom rows with the provider/native inheritance the catalog
|
|
415
|
+
// publishes, so this is the canonical resolved metadata the export must preserve, and no
|
|
416
|
+
// per-model fetch ever happens to obtain it.
|
|
417
|
+
const rosterByNamespaced = new Map(publicModels.map(model => [catalogModelSlug(model), model] as const));
|
|
418
|
+
const customRowsWithExportMetadata = visibleCustomModels.map(model => {
|
|
419
|
+
const catalogRow = rosterByNamespaced.get(model.namespaced);
|
|
420
|
+
// The gathered materialization's ladder is authoritative when it has one: it already
|
|
421
|
+
// folded the explicit override in and then applied the canonical bounds (the native
|
|
422
|
+
// clamp for a capability-backed id, learned refusals), so re-declaring the raw override
|
|
423
|
+
// here would undo a bound. Two raw values still win: the explicit empty ladder — "no
|
|
424
|
+
// rungs" is an operator statement no normalization may fill in, even against a stale
|
|
425
|
+
// gathered row — and any override with no gathered ladder behind it.
|
|
426
|
+
const declaredEfforts = Array.isArray(model.reasoningEfforts) && model.reasoningEfforts.length === 0
|
|
427
|
+
? model.reasoningEfforts
|
|
428
|
+
: catalogRow?.reasoningEfforts !== undefined
|
|
429
|
+
? catalogRow.reasoningEfforts
|
|
430
|
+
: model.reasoningEfforts;
|
|
431
|
+
return {
|
|
432
|
+
...model,
|
|
433
|
+
exportMetadata: customRowExportMetadata(
|
|
434
|
+
config,
|
|
435
|
+
model,
|
|
436
|
+
catalogRow,
|
|
437
|
+
effectiveModelReasoningEfforts(config, model.provider, model.id, declaredEfforts, catalogLadders),
|
|
438
|
+
),
|
|
439
|
+
};
|
|
440
|
+
});
|
|
377
441
|
const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
|
|
378
442
|
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
379
443
|
const namespaced = catalogModelSlug(m);
|
|
@@ -395,7 +459,7 @@ export async function listManagementModelRows(
|
|
|
395
459
|
...m,
|
|
396
460
|
...displayName,
|
|
397
461
|
namespaced,
|
|
398
|
-
disabled: [...disabled].some(stored => (
|
|
462
|
+
disabled: antigravityFamilyDisabled(config, m, models) || [...disabled].some(stored => (
|
|
399
463
|
(!nativeAlias && stored === namespaced) || slugEquals(stored, m.provider, m.id)
|
|
400
464
|
)),
|
|
401
465
|
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
@@ -407,13 +471,25 @@ export async function listManagementModelRows(
|
|
|
407
471
|
...(routed ? { reasoningOverridden: reasoningOverrideFor(config, m.provider, m.id, catalogLadders) } : {}),
|
|
408
472
|
...(inputModalitiesDeclared !== undefined ? { inputModalitiesDeclared } : {}),
|
|
409
473
|
...(contextWindowDeclared !== undefined ? { contextWindowDeclared } : {}),
|
|
474
|
+
// The authoritative export default is the GATHERED row's own declared default — never
|
|
475
|
+
// the picker value above, whose medium → low → first fallback invents a default no
|
|
476
|
+
// source declared. The object exists even when empty: its presence is what tells
|
|
477
|
+
// `toExportModel` and the CLI that an absent default means "no default", not "fall
|
|
478
|
+
// back to the row". A default outside the effective ladder is dropped for the same
|
|
479
|
+
// reason the settings route rejects one: the proxy would never send it.
|
|
480
|
+
exportMetadata: {
|
|
481
|
+
...(m.defaultReasoningEffort !== undefined
|
|
482
|
+
&& (reasoningEfforts === undefined || reasoningEfforts.includes(m.defaultReasoningEffort))
|
|
483
|
+
? { defaultReasoningEffort: m.defaultReasoningEffort }
|
|
484
|
+
: {}),
|
|
485
|
+
},
|
|
410
486
|
};
|
|
411
487
|
}).filter((row): row is ManagementModelRow => row !== null);
|
|
412
488
|
// Manual OpenAI rows retain their routed selector but replace the bare dashboard row.
|
|
413
489
|
// Account-qualified rows remain distinct, explicitly selected routes.
|
|
414
490
|
const visibleNative = native.filter(model => model.id.includes("/")
|
|
415
491
|
|| !customNamespaced.has(routedSlug(model.provider, model.id)));
|
|
416
|
-
const rows = [...visibleNative, ...dedupedRouted, ...
|
|
492
|
+
const rows = [...visibleNative, ...dedupedRouted, ...customRowsWithExportMetadata];
|
|
417
493
|
// Include disabled rows and configured aliases before the export visibility filter:
|
|
418
494
|
// a hidden real `x--fast` must never become a synthetic selector for another model.
|
|
419
495
|
const knownIds = config.fastRows === false ? new Set<string>() : knownEffortRowIds(config);
|
|
@@ -422,6 +498,23 @@ export async function listManagementModelRows(
|
|
|
422
498
|
const pending = initialModelSelectionPending(config.providers[row.provider]);
|
|
423
499
|
const modelCosts = Object.hasOwn(config.providers, row.provider)
|
|
424
500
|
? config.providers[row.provider]?.modelCosts : undefined;
|
|
501
|
+
// Derived once here so the wire row carries the same booleans the export projection
|
|
502
|
+
// serializes; the CLI reads /api/models and cannot re-derive them without duplicating
|
|
503
|
+
// this policy. Custom rows read their effective view from `exportMetadata`; routed and
|
|
504
|
+
// native rows read their own evidence.
|
|
505
|
+
const supportsTools = row.exportMetadata?.supportsTools
|
|
506
|
+
?? row.supportsTools
|
|
507
|
+
?? knownToolsSupport({
|
|
508
|
+
capabilities: row.capabilities,
|
|
509
|
+
parallelToolCalls: row.parallelToolCalls,
|
|
510
|
+
native: row.native,
|
|
511
|
+
});
|
|
512
|
+
const supportsReasoning = row.exportMetadata?.supportsReasoning
|
|
513
|
+
?? row.supportsReasoning
|
|
514
|
+
?? knownReasoningSupport({
|
|
515
|
+
reasoningEfforts: row.reasoningEfforts,
|
|
516
|
+
supportsReasoningSummaries: row.supportsReasoningSummaries,
|
|
517
|
+
});
|
|
425
518
|
return {
|
|
426
519
|
...row,
|
|
427
520
|
...(!row.native && modelCosts !== undefined && Object.hasOwn(modelCosts, row.id)
|
|
@@ -429,12 +522,52 @@ export async function listManagementModelRows(
|
|
|
429
522
|
...(pending ? { disabled: true, initialSelectionPending: true } : {}),
|
|
430
523
|
fastRowAvailable: !row.disabled && !pending
|
|
431
524
|
&& !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
|
|
525
|
+
// An explicit false survives; derivation itself is true-or-nothing.
|
|
526
|
+
...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
|
|
527
|
+
...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
|
|
432
528
|
};
|
|
433
529
|
});
|
|
434
530
|
}
|
|
435
531
|
|
|
436
|
-
/**
|
|
532
|
+
/**
|
|
533
|
+
* `/api/models` row → the narrower input the client-config serializers accept.
|
|
534
|
+
*
|
|
535
|
+
* A row's `exportMetadata`, when present, is the authority: custom rows project through it
|
|
536
|
+
* because their own fields are stored overrides (an absent field means "not overridden"),
|
|
537
|
+
* and routed/native rows carry the declared default in it because the row-level
|
|
538
|
+
* `defaultReasoningEffort` is the picker's preference order, whose medium → low → first
|
|
539
|
+
* fallback fabricates a default no source declared.
|
|
540
|
+
*/
|
|
437
541
|
export function toExportModel(row: ManagementModelRow): ExportModel {
|
|
542
|
+
const effective = row.exportMetadata;
|
|
543
|
+
const contextWindow = effective?.contextWindow ?? row.contextWindow;
|
|
544
|
+
const maxTokens = effective?.maxTokens ?? row.maxOutputTokens;
|
|
545
|
+
const maxInputTokens = effective?.maxInputTokens ?? row.maxInputTokens;
|
|
546
|
+
const inputModalities = effective?.inputModalities ?? row.inputModalities;
|
|
547
|
+
const reasoningEfforts = effective?.reasoningEfforts ?? row.reasoningEfforts;
|
|
548
|
+
// The projection is the authority on the default. When it exists, an absent default means
|
|
549
|
+
// NO declared default — falling back to the row would reintroduce the picker's synthetic
|
|
550
|
+
// medium/low value, which is exactly what this projection exists to keep out of exports.
|
|
551
|
+
// Rows without a projection (native-era shapes, hand-built rows) keep the row's own value.
|
|
552
|
+
const defaultReasoningEffort = effective !== undefined
|
|
553
|
+
? effective.defaultReasoningEffort
|
|
554
|
+
: row.defaultReasoningEffort;
|
|
555
|
+
const supportsReasoningSummaries = effective?.supportsReasoningSummaries ?? row.supportsReasoningSummaries;
|
|
556
|
+
// Projection first, then the row's own explicit boolean (an external producer may state
|
|
557
|
+
// false, which is evidence), and only then derivation from positive evidence.
|
|
558
|
+
const supportsTools = effective?.supportsTools
|
|
559
|
+
?? row.supportsTools
|
|
560
|
+
?? knownToolsSupport({
|
|
561
|
+
capabilities: row.capabilities,
|
|
562
|
+
parallelToolCalls: row.parallelToolCalls,
|
|
563
|
+
native: row.native,
|
|
564
|
+
});
|
|
565
|
+
const supportsReasoning = effective?.supportsReasoning
|
|
566
|
+
?? row.supportsReasoning
|
|
567
|
+
?? knownReasoningSupport({
|
|
568
|
+
reasoningEfforts: row.reasoningEfforts,
|
|
569
|
+
supportsReasoningSummaries: row.supportsReasoningSummaries,
|
|
570
|
+
});
|
|
438
571
|
return {
|
|
439
572
|
namespaced: row.namespaced,
|
|
440
573
|
provider: row.provider,
|
|
@@ -442,11 +575,17 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
|
|
|
442
575
|
fastRowAvailable: row.fastRowAvailable === true,
|
|
443
576
|
...(row.native ? { native: true } : {}),
|
|
444
577
|
...(row.displayName && row.displayNameSource !== "fallback" ? { displayName: row.displayName } : {}),
|
|
445
|
-
...(
|
|
446
|
-
...(
|
|
447
|
-
...(
|
|
448
|
-
...(
|
|
449
|
-
|
|
578
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
579
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
580
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
581
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
582
|
+
// An empty array is a declaration ("no rungs"), and `[]` is truthy: it survives here.
|
|
583
|
+
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
584
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
585
|
+
// An explicit false is evidence and survives; only derivation is true-or-nothing.
|
|
586
|
+
...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
|
|
587
|
+
...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
|
|
588
|
+
...(supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries } : {}),
|
|
450
589
|
};
|
|
451
590
|
}
|
|
452
591
|
|