@bitkyc08/opencodex 2.80.0 → 2.81.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 +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +1 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +3 -2
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +320 -184
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +4 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +16 -11
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +27 -5
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { OcxProviderConfig } from "../../types";
|
|
3
|
+
import type { ProviderEditorProviderDTO } from "../auth-cors";
|
|
4
|
+
import { anthropicInstanceRowShapeMatches } from "../../providers/anthropic-instance-id";
|
|
5
|
+
import { assertAnthropicInstanceLoginConfig } from "../../oauth/store-anthropic-instance";
|
|
6
|
+
|
|
7
|
+
/** Preset publication may create B, but may never take over an existing custom row. */
|
|
8
|
+
export function anthropicInstancePublicationError(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): string | undefined {
|
|
9
|
+
if (name === "anthropic2" && anthropicInstanceRowShapeMatches(name, next)) {
|
|
10
|
+
try { assertAnthropicInstanceLoginConfig({ providers: existing ? { [name]: existing } : {} }, name); }
|
|
11
|
+
catch { return "cannot add Pool 2 over an existing custom provider or unreadable configuration; resolve it first"; }
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Full form edits omit provenance; preserve only an already owned compatible row. */
|
|
17
|
+
export function preserveAnthropicInstanceMarker(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): void {
|
|
18
|
+
if (name !== "anthropic2" || Object.hasOwn(next, "anthropicOAuthInstance")) return;
|
|
19
|
+
if (anthropicInstanceRowShapeMatches(name, existing) && next.adapter === "anthropic" && next.authMode === "oauth") {
|
|
20
|
+
next.anthropicOAuthInstance = "anthropic2";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mergeProviderEditorRow(
|
|
25
|
+
persisted: OcxProviderConfig | undefined,
|
|
26
|
+
baseline: ProviderEditorProviderDTO | undefined,
|
|
27
|
+
next: ProviderEditorProviderDTO,
|
|
28
|
+
): OcxProviderConfig {
|
|
29
|
+
const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
|
|
30
|
+
const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
|
|
31
|
+
for (const field of fields) {
|
|
32
|
+
const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
|
|
33
|
+
const nextHasField = Object.hasOwn(next, field);
|
|
34
|
+
if (
|
|
35
|
+
baselineHasField === nextHasField
|
|
36
|
+
&& (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
|
|
37
|
+
) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (nextHasField) merged[field] = structuredClone(next[field]);
|
|
41
|
+
else delete merged[field];
|
|
42
|
+
}
|
|
43
|
+
return merged as unknown as OcxProviderConfig;
|
|
44
|
+
}
|
|
@@ -40,6 +40,7 @@ import { commitProviderPatch } from "./provider-patch-transaction";
|
|
|
40
40
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
41
41
|
import { canonicalAutoReviewModelKey, mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
|
|
42
42
|
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
43
|
+
import { captureModelsOAuthTarget, mayResolveModelsOAuth, modelsOAuthTargetMatches } from "../../oauth/model-discovery-auth";
|
|
43
44
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
44
45
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
45
46
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
@@ -115,7 +116,6 @@ import {
|
|
|
115
116
|
publicProviderBaseUrl,
|
|
116
117
|
safeConfigDTO,
|
|
117
118
|
type ProviderEditorConfigDTO,
|
|
118
|
-
type ProviderEditorProviderDTO,
|
|
119
119
|
} from "../auth-cors";
|
|
120
120
|
import { providerCatalogCapabilityConfigError } from "./provider-capability-config";
|
|
121
121
|
import { providerEmptyToolOutputConfigError } from "../../config/provider-validation";
|
|
@@ -138,6 +138,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
138
138
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
139
139
|
import type { ManagementContext } from "./context";
|
|
140
140
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
141
|
+
import { anthropicInstancePublicationError, mergeProviderEditorRow, preserveAnthropicInstanceMarker } from "./provider-instance-ownership";
|
|
141
142
|
import { providerTlsProfileDiagnostic } from "../../lib/provider-tls-profile";
|
|
142
143
|
|
|
143
144
|
type ProviderPatchApplication =
|
|
@@ -213,28 +214,6 @@ type ProviderEditorCandidateResult =
|
|
|
213
214
|
|
|
214
215
|
type ProviderEditorMutationValue = ProviderEditorCandidateResult;
|
|
215
216
|
|
|
216
|
-
function mergeProviderEditorRow(
|
|
217
|
-
persisted: OcxProviderConfig | undefined,
|
|
218
|
-
baseline: ProviderEditorProviderDTO | undefined,
|
|
219
|
-
next: ProviderEditorProviderDTO,
|
|
220
|
-
): OcxProviderConfig {
|
|
221
|
-
const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
|
|
222
|
-
const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
|
|
223
|
-
for (const field of fields) {
|
|
224
|
-
const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
|
|
225
|
-
const nextHasField = Object.hasOwn(next, field);
|
|
226
|
-
if (
|
|
227
|
-
baselineHasField === nextHasField
|
|
228
|
-
&& (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
|
|
229
|
-
) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (nextHasField) merged[field] = structuredClone(next[field]);
|
|
233
|
-
else delete merged[field];
|
|
234
|
-
}
|
|
235
|
-
return merged as unknown as OcxProviderConfig;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
217
|
/** Build and validate a complete candidate without mutating the caller's snapshot. */
|
|
239
218
|
function providerEditorCandidate(
|
|
240
219
|
persisted: OcxConfig,
|
|
@@ -273,6 +252,8 @@ function providerEditorCandidate(
|
|
|
273
252
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(candidate.codexAccountNamespaces, name);
|
|
274
253
|
if (namespaceCollision) return { ok: false, status: 409, error: namespaceCollision, code: "provider_namespace_conflict" };
|
|
275
254
|
const merged = mergeProviderEditorRow(persisted.providers[name], baseline.providers[name], publicProvider);
|
|
255
|
+
const markerCollision = anthropicInstancePublicationError(name, merged, persisted.providers[name]);
|
|
256
|
+
if (markerCollision) return { ok: false, status: 409, error: markerCollision, code: "anthropic_instance_collision" };
|
|
276
257
|
const transportCandidate = providerTransportValidationCandidate(merged as unknown as Record<string, unknown>);
|
|
277
258
|
// The editor merges onto the persisted row, so stored operator overlays (selectedModels,
|
|
278
259
|
// disabled, …) ride along in the candidate. They are owned by their own write boundaries;
|
|
@@ -1172,6 +1153,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1172
1153
|
const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
|
|
1173
1154
|
if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
|
|
1174
1155
|
const transportCandidate = providerTransportValidationCandidate(body.provider);
|
|
1156
|
+
preserveAnthropicInstanceMarker(name, transportCandidate as unknown as OcxProviderConfig, existing);
|
|
1157
|
+
const markerCollision = anthropicInstancePublicationError(name, transportCandidate as unknown as OcxProviderConfig, existing);
|
|
1158
|
+
if (markerCollision) return jsonResponse({ error: markerCollision }, 409);
|
|
1175
1159
|
const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
|
|
1176
1160
|
if (pinError) return jsonResponse({ error: pinError }, 400);
|
|
1177
1161
|
const providerError = providerManagementConfigError(name, transportCandidate)
|
|
@@ -1328,6 +1312,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1328
1312
|
// DNS validation above awaits. Re-read the live row so a dedicated alias write that
|
|
1329
1313
|
// completed during that wait remains authoritative instead of being overwritten by the
|
|
1330
1314
|
// older ownership snapshot used to admit this POST.
|
|
1315
|
+
const lateMarkerCollision = anthropicInstancePublicationError(name, prov, config.providers[name]);
|
|
1316
|
+
if (lateMarkerCollision) return jsonResponse({ error: lateMarkerCollision }, 409);
|
|
1317
|
+
preserveAnthropicInstanceMarker(name, prov, config.providers[name]);
|
|
1331
1318
|
restorePersistedAliasOverlays(prov, config.providers[name]);
|
|
1332
1319
|
const capabilities = Object.hasOwn(body.provider, "modelCapabilities")
|
|
1333
1320
|
? mergeModelCapabilities(undefined, prov.modelCapabilities)
|
|
@@ -1371,6 +1358,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1371
1358
|
const rollback = pinsOwned ? captureConfigTopLevelRollback(config, ["defaultProvider", "modelDiscovery", "disabledModels"]) : undefined;
|
|
1372
1359
|
let validationError: string | undefined;
|
|
1373
1360
|
withConfigMutationLockSync(() => {
|
|
1361
|
+
validationError = anthropicInstancePublicationError(name, candidate, config.providers[name]);
|
|
1362
|
+
if (validationError) return;
|
|
1374
1363
|
const liveTiers = config.providers[name]?.modelContextTiers;
|
|
1375
1364
|
if (submittedModelContextTiers && liveTiers) candidate.modelContextTiers = Object.assign(Object.create(null), liveTiers, candidate.modelContextTiers ?? {});
|
|
1376
1365
|
else if (!submittedModelContextTiers && liveTiers) candidate.modelContextTiers = { ...liveTiers };
|
|
@@ -1621,7 +1610,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1621
1610
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
|
|
1622
1611
|
return jsonResponse({ error: "unknown provider" }, 404);
|
|
1623
1612
|
}
|
|
1624
|
-
const prov = config.providers[name]
|
|
1613
|
+
const prov = { ...config.providers[name]! };
|
|
1614
|
+
const authorizedTarget = captureModelsOAuthTarget(name, prov);
|
|
1625
1615
|
if (prov.disabled) {
|
|
1626
1616
|
return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
|
|
1627
1617
|
}
|
|
@@ -1646,10 +1636,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1646
1636
|
// credential resolution/network access for providers such as Antigravity.
|
|
1647
1637
|
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
1648
1638
|
}
|
|
1649
|
-
const { buildModelsRequest,
|
|
1639
|
+
const { buildModelsRequest, getModelsOAuthAccessSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
1650
1640
|
const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
1651
|
-
const snapshot = prov.authMode === "oauth"
|
|
1652
|
-
? await
|
|
1641
|
+
const snapshot = prov.authMode === "oauth" && mayResolveModelsOAuth(name, config.providers[name])
|
|
1642
|
+
? await getModelsOAuthAccessSnapshot(name, prov).catch(() => undefined)
|
|
1653
1643
|
: undefined;
|
|
1654
1644
|
const apiKey = prov.authMode === "oauth" ? snapshot?.accessToken : await resolveModelsAuthToken(name, prov);
|
|
1655
1645
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
@@ -1735,7 +1725,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1735
1725
|
// Same canonical-URL TUN transparency as catalog discovery: the registry's
|
|
1736
1726
|
// own fixed discovery URL survives purely-benchmark (Clash/Surge/Mihomo
|
|
1737
1727
|
// fake-IP) DNS without proxy env.
|
|
1738
|
-
const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl
|
|
1728
|
+
const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl,
|
|
1729
|
+
beforeSend: () => prov.authMode !== "oauth" || (mayResolveModelsOAuth(name, config.providers[name])
|
|
1730
|
+
&& modelsOAuthTargetMatches(name, config.providers[name], authorizedTarget)
|
|
1731
|
+
&& (name !== "anthropic2" || new URL(modelsUrl).href === authorizedTarget)) };
|
|
1739
1732
|
const res = method === "POST"
|
|
1740
1733
|
? await providerOutboundPost(name, prov, modelsUrl, {
|
|
1741
1734
|
headers,
|
|
@@ -231,6 +231,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
231
231
|
{ method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
|
|
232
232
|
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false },
|
|
233
233
|
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false },
|
|
234
|
+
{ method: "POST", path: "/api/client-integrations/commandcode/restore", module: "server/management/integration-routes", mutates: true },
|
|
235
|
+
{ method: "POST", path: "/api/client-integrations/commandcode/restore/preview", module: "server/management/integration-routes", mutates: false },
|
|
234
236
|
// server/management/lab-automation-routes
|
|
235
237
|
{ method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
236
238
|
{ method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -324,7 +326,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
324
326
|
{ method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
325
327
|
{ method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
|
|
326
328
|
{ method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
327
|
-
{ method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "
|
|
329
|
+
{ method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Stored-key reads require a current pairing or trusted Tailscale-identity session; automatic loopback sessions and admin tokens are refused." } },
|
|
328
330
|
{ method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
329
331
|
{ method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
|
|
330
332
|
{ method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -63,7 +63,7 @@ function parseEvidence(raw: unknown): { evidence: PolicyRequestEvidence; ok: boo
|
|
|
63
63
|
return { evidence, ok: true };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null {
|
|
66
|
+
function parseCandidateEvidence(raw: unknown, config: OcxConfig): PolicyCandidateEvidence[] | null {
|
|
67
67
|
if (!Array.isArray(raw)) return null;
|
|
68
68
|
const out: PolicyCandidateEvidence[] = [];
|
|
69
69
|
for (const item of raw) {
|
|
@@ -87,10 +87,10 @@ function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null
|
|
|
87
87
|
// caller does not supply an explicit quota object, so a dry-run following
|
|
88
88
|
// the documented shape reports the same cached account quota as routing.
|
|
89
89
|
...(item.quota === undefined && typeof item.codexAccountId === "string"
|
|
90
|
-
? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }) }
|
|
90
|
+
? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }, config) }
|
|
91
91
|
: {}),
|
|
92
92
|
...(item.quota === undefined && typeof item.accountRef === "string" && typeof item.codexAccountId !== "string"
|
|
93
|
-
? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }) }
|
|
93
|
+
? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }, config) }
|
|
94
94
|
: {}),
|
|
95
95
|
});
|
|
96
96
|
}
|
|
@@ -392,7 +392,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
392
392
|
if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
|
|
393
393
|
const candidateEvidence = body.candidates === undefined
|
|
394
394
|
? assembleCandidateEvidence(config, resolvedProfile, now)
|
|
395
|
-
: parseCandidateEvidence(body.candidates);
|
|
395
|
+
: parseCandidateEvidence(body.candidates, config);
|
|
396
396
|
if (candidateEvidence === null) {
|
|
397
397
|
return jsonResponse({ error: { code: "invalid_candidates", message: "candidates must be an array of evidence objects" } }, 400, req, config);
|
|
398
398
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard memory-card drain-and-restart (#563).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
|
|
4
|
+
* Waits up to 60s for active turns unless a manual API caller explicitly opts
|
|
5
|
+
* into a shorter grace, with the existing 60s cleanup watchdog, then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
|
|
6
6
|
* recycle to reclaim RSS, not a teardown.
|
|
7
7
|
*
|
|
8
8
|
* Respawn policy (matches real supervisor configs in src/service.ts):
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* Checked before the service rule: that app, not a service manager, is the parent.
|
|
38
38
|
*/
|
|
39
39
|
import {
|
|
40
|
+
abortAndReleaseAllTurns,
|
|
40
41
|
acquireTemporaryDrain,
|
|
41
42
|
beginShutdownDrain,
|
|
42
43
|
drainAndShutdown,
|
|
@@ -64,8 +65,11 @@ import { spawnReplacementStart } from "../restart-replacement";
|
|
|
64
65
|
export { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../../lib/system-restart-contract";
|
|
65
66
|
export { waitForReplacementReady, type ReplacementReadinessIo } from "../restart-replacement";
|
|
66
67
|
export const DEADLINE_LISTENER_STOP_TIMEOUT_MS = 5_000;
|
|
68
|
+
const RESTART_RESPONSE_FLUSH_DELAY_MS = 200;
|
|
67
69
|
|
|
68
70
|
export interface SystemRestartIo {
|
|
71
|
+
/** Cancel and release admitted turns without stopping the listener or process. */
|
|
72
|
+
abortActiveTurns?: () => void;
|
|
69
73
|
acquireTemporaryDrain?: () => { release(): void } | null;
|
|
70
74
|
drainAndShutdown?: typeof drainAndShutdown;
|
|
71
75
|
/** True when a background service can actually respawn this process after exit(1). */
|
|
@@ -92,6 +96,11 @@ export interface SystemRestartIo {
|
|
|
92
96
|
now?: () => number;
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
export interface SystemRestartOptions {
|
|
100
|
+
/** Explicit API opt-in; omission preserves the existing 60s drain. */
|
|
101
|
+
drainGraceMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
export interface SystemRestartAdmission {
|
|
96
105
|
/** Only the caller that created this pending restart receives its veto. */
|
|
97
106
|
onAccepted?: (veto: () => void) => void;
|
|
@@ -100,8 +109,9 @@ export interface SystemRestartAdmission {
|
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
let restartIo: SystemRestartIo = {};
|
|
103
|
-
/** Prevents double-scheduling
|
|
112
|
+
/** Prevents double-scheduling during the 200ms response-flush window. */
|
|
104
113
|
let restartAccepted = false;
|
|
114
|
+
let acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
|
|
105
115
|
|
|
106
116
|
type RestartDrainOutcome = "completed" | "failed" | "rejected" | "deadline";
|
|
107
117
|
type BoundedSettlementOutcome = "completed" | "rejected" | "deadline";
|
|
@@ -173,6 +183,7 @@ export function noteExplicitShutdownRequested(): void {
|
|
|
173
183
|
export function setSystemRestartIoForTests(io: SystemRestartIo = {}): void {
|
|
174
184
|
restartIo = io;
|
|
175
185
|
restartAccepted = false;
|
|
186
|
+
acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
|
|
176
187
|
explicitShutdownRequested = false;
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -324,10 +335,10 @@ async function completeDeadlineRestartHandoff(
|
|
|
324
335
|
|
|
325
336
|
/**
|
|
326
337
|
* Accept a drain-and-restart request. Returns immediately; the drain +
|
|
327
|
-
* respawn
|
|
338
|
+
* respawn waits 200ms for the HTTP response to flush; shorter grace cuts turns separately.
|
|
328
339
|
* Idempotent while already draining: returns the accepted shape again.
|
|
329
340
|
*/
|
|
330
|
-
export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}): {
|
|
341
|
+
export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}, options: SystemRestartOptions = {}): {
|
|
331
342
|
accepted: true;
|
|
332
343
|
alreadyDraining: boolean;
|
|
333
344
|
activeTurnCount: number;
|
|
@@ -337,16 +348,22 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
337
348
|
?? io.isDraining
|
|
338
349
|
?? isShutdownDraining;
|
|
339
350
|
const alreadyDraining = restartAccepted || shutdownActive();
|
|
351
|
+
const drainGraceMs = alreadyDraining
|
|
352
|
+
? (restartAccepted ? acceptedDrainGraceMs : MEMORY_DRAIN_RESTART_MS)
|
|
353
|
+
: (options.drainGraceMs ?? MEMORY_DRAIN_RESTART_MS);
|
|
340
354
|
const activeTurnCount = (io.getActiveTurnCount ?? getActiveTurnCount)();
|
|
341
355
|
const schedule = io.schedule ?? ((fn, ms) => { setTimeout(() => { void fn(); }, ms); });
|
|
342
356
|
|
|
343
357
|
if (!alreadyDraining) {
|
|
344
358
|
restartAccepted = true;
|
|
359
|
+
acceptedDrainGraceMs = drainGraceMs;
|
|
345
360
|
const automatic = Boolean(admission.onAccepted);
|
|
346
361
|
const temporaryDrain = automatic
|
|
347
362
|
? (io.acquireTemporaryDrain ?? (() => acquireTemporaryDrain("automatic-restart")))()
|
|
348
363
|
: null;
|
|
364
|
+
let cancelEarlyCut: (() => void) | undefined;
|
|
349
365
|
const releasePending = () => {
|
|
366
|
+
cancelEarlyCut?.();
|
|
350
367
|
temporaryDrain?.release();
|
|
351
368
|
restartAccepted = false;
|
|
352
369
|
};
|
|
@@ -363,13 +380,27 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
363
380
|
releasePending();
|
|
364
381
|
});
|
|
365
382
|
const now = io.now ?? Date.now;
|
|
366
|
-
const
|
|
383
|
+
const acceptedAtMs = now();
|
|
384
|
+
const restartDeadlineMs = acceptedAtMs + MEMORY_DRAIN_RESTART_MS;
|
|
385
|
+
const drainDeadlineMs = acceptedAtMs + drainGraceMs;
|
|
386
|
+
const scheduleDeadline = io.scheduleDeadline ?? ((fn, ms) => {
|
|
387
|
+
const timer = setTimeout(fn, ms);
|
|
388
|
+
return () => clearTimeout(timer);
|
|
389
|
+
});
|
|
367
390
|
// Reject new data-plane traffic immediately (503), before the 200ms response-flush delay.
|
|
368
391
|
if (!automatic) {
|
|
369
392
|
if (io.beginShutdownDrain) io.beginShutdownDrain();
|
|
370
393
|
else if (io.setDraining) io.setDraining(true);
|
|
371
394
|
else beginShutdownDrain();
|
|
372
395
|
}
|
|
396
|
+
// A shorter grace cuts admitted turns at acceptance + grace, but listener stop
|
|
397
|
+
// and process exit must still wait for the full response-flush window.
|
|
398
|
+
if (!vetoed && drainGraceMs < RESTART_RESPONSE_FLUSH_DELAY_MS) {
|
|
399
|
+
cancelEarlyCut = scheduleDeadline(() => {
|
|
400
|
+
if (vetoed) return;
|
|
401
|
+
(io.abortActiveTurns ?? (() => abortAndReleaseAllTurns(new Error("server shutdown"))))();
|
|
402
|
+
}, drainGraceMs);
|
|
403
|
+
}
|
|
373
404
|
schedule(async () => {
|
|
374
405
|
if (vetoed) return;
|
|
375
406
|
pending = false;
|
|
@@ -390,12 +421,11 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
390
421
|
// closes the listener and makes both the server ref and runtime metadata stale.
|
|
391
422
|
const restartPort = (io.listenPort ?? resolveListenPort)();
|
|
392
423
|
const drain = io.drainAndShutdown ?? drainAndShutdown;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
});
|
|
424
|
+
// Bound active/scoped-drain waiting independently of cleanup and readiness.
|
|
425
|
+
// A cut turn may already have executed upstream; never replay it automatically.
|
|
426
|
+
const drainPromise = Promise.resolve().then(
|
|
427
|
+
() => drain(undefined, Math.max(0, drainDeadlineMs - now())),
|
|
428
|
+
);
|
|
399
429
|
const drainOutcome = await waitForRestartDrain(drainPromise, restartDeadlineMs, now, scheduleDeadline);
|
|
400
430
|
if (!canHandoff()) return;
|
|
401
431
|
const exitProcess = io.exitProcess ?? ((code: number) => { process.exit(code); });
|
|
@@ -451,13 +481,13 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
451
481
|
}
|
|
452
482
|
(io.markRecycling ?? markRecyclingForExit)();
|
|
453
483
|
exitProcess(drainOutcome === "failed" || drainOutcome === "rejected" ? 1 : 0);
|
|
454
|
-
},
|
|
484
|
+
}, RESTART_RESPONSE_FLUSH_DELAY_MS);
|
|
455
485
|
}
|
|
456
486
|
|
|
457
487
|
return {
|
|
458
488
|
accepted: true,
|
|
459
489
|
alreadyDraining,
|
|
460
490
|
activeTurnCount,
|
|
461
|
-
drainTimeoutMs:
|
|
491
|
+
drainTimeoutMs: drainGraceMs,
|
|
462
492
|
};
|
|
463
493
|
}
|
|
@@ -30,6 +30,7 @@ import { inspectResponseSpillStorage, responseStateMetrics, type ResponseSpillDi
|
|
|
30
30
|
import { appOwnedBytesSnapshot } from "../../lib/app-owned-memory";
|
|
31
31
|
import { readWindowsReplaceRetryCounters } from "../../lib/windows-atomic-replace";
|
|
32
32
|
import {
|
|
33
|
+
MEMORY_DRAIN_RESTART_MS,
|
|
33
34
|
SYSTEM_RESTART_EXPECTED_PID_HEADER,
|
|
34
35
|
parseExpectedSystemRestartPid,
|
|
35
36
|
} from "../../lib/system-restart-contract";
|
|
@@ -46,6 +47,7 @@ import type {
|
|
|
46
47
|
} from "../../codex/app-server-restart-service";
|
|
47
48
|
import type { ManagementContext } from "./context";
|
|
48
49
|
import { acceptSystemRestart } from "./system-restart";
|
|
50
|
+
import { readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
49
51
|
|
|
50
52
|
const ENDPOINT_SAMPLE_LIMIT = 60;
|
|
51
53
|
// The spill report reads the snapshot and walks the spill directory synchronously,
|
|
@@ -179,8 +181,28 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
179
181
|
}, 409, req, config);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
let body: unknown;
|
|
185
|
+
try { body = await readOptionalManagementJsonBody(req); }
|
|
186
|
+
catch (error) {
|
|
187
|
+
rethrowManagementBodyTooLarge(error);
|
|
188
|
+
return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
|
|
189
|
+
}
|
|
190
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
191
|
+
return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
|
|
192
|
+
}
|
|
193
|
+
const drainGraceMs = (body as Record<string, unknown>).drainGraceMs;
|
|
194
|
+
if (drainGraceMs !== undefined && (
|
|
195
|
+
typeof drainGraceMs !== "number" || !Number.isSafeInteger(drainGraceMs)
|
|
196
|
+
|| drainGraceMs < 1 || drainGraceMs > MEMORY_DRAIN_RESTART_MS
|
|
197
|
+
)) {
|
|
198
|
+
return jsonResponse({ success: false, error: "drainGraceMs must be an integer from 1 to 60000." }, 400, req, config);
|
|
199
|
+
}
|
|
200
|
+
// The process capability signs the restart target, not this new body option.
|
|
201
|
+
if (drainGraceMs !== undefined && ctx.principal === "system-restart-capability") {
|
|
202
|
+
return jsonResponse({ success: false, error: "Short grace requires management authentication." }, 403, req, config);
|
|
203
|
+
}
|
|
204
|
+
// Omission retains the informed 60s drain; only an explicit API request shortens it.
|
|
205
|
+
const result = acceptSystemRestart(undefined, {}, { drainGraceMs });
|
|
184
206
|
return jsonResponse({
|
|
185
207
|
success: true,
|
|
186
208
|
message: result.alreadyDraining
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the same as no gate at all.
|
|
9
9
|
*/
|
|
10
10
|
import type { OcxConfig } from "../../types";
|
|
11
|
-
import {
|
|
11
|
+
import { type AnthropicVisionProvider } from "../../vision";
|
|
12
12
|
import { VISION_BACKENDS } from "../../vision/backends";
|
|
13
13
|
import {
|
|
14
14
|
modelAcceptsImageInput,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type VisionSidecarBackend,
|
|
19
19
|
} from "../../vision/eligibility";
|
|
20
20
|
import { pickerVisibleSidecarCandidates } from "../../sidecar/candidates";
|
|
21
|
-
import {
|
|
21
|
+
import { sidecarOptionsAuth } from "./web-search-sidecar-options";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Backends whose executor could actually run (#2188 roadmap 170): openai
|
|
@@ -38,7 +38,7 @@ export function enabledVisionBackends(
|
|
|
38
38
|
config: OcxConfig,
|
|
39
39
|
anthropicSidecar: AnthropicVisionProvider | undefined,
|
|
40
40
|
): VisionSidecarBackend[] {
|
|
41
|
-
const auth =
|
|
41
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
42
42
|
// Preserve the caller's resolution for the anthropic side: the descriptor
|
|
43
43
|
// reads the shared auth module, but a caller that already resolved "no
|
|
44
44
|
// executor" must not see anthropic options it cannot dispatch. The filter
|
|
@@ -52,7 +52,7 @@ export function enabledVisionBackends(
|
|
|
52
52
|
// the UNIVERSAL sides: when neither resolves, both are offered so the picker
|
|
53
53
|
// stays populated (permissive-unknown rule; test 6 pins it).
|
|
54
54
|
if (!active.includes("openai") && !active.includes("anthropic")) {
|
|
55
|
-
return ["openai", "anthropic", ...active];
|
|
55
|
+
return ["openai", ...(config.visionSidecar?.anthropicInstance ? [] : ["anthropic" as const]), ...active];
|
|
56
56
|
}
|
|
57
57
|
return active;
|
|
58
58
|
}
|
|
@@ -67,7 +67,7 @@ export function enabledVisionBackends(
|
|
|
67
67
|
* pre-filtering here would deaden the gate (review F1: reject → allow flip).
|
|
68
68
|
*/
|
|
69
69
|
export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> {
|
|
70
|
-
const auth =
|
|
70
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
71
71
|
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
72
72
|
return all.map(candidate => ({
|
|
73
73
|
provider: candidate.provider,
|
|
@@ -158,10 +158,13 @@ export function visionDescriberRejection(
|
|
|
158
158
|
config: OcxConfig,
|
|
159
159
|
candidates: readonly VisionCandidateModel[],
|
|
160
160
|
): { error: string; allowed: string[] } {
|
|
161
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
162
|
+
const anthropic = auth.anthropicProviderName && auth.anthropicProvider
|
|
163
|
+
? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
|
|
161
164
|
return {
|
|
162
165
|
error: `${field} "${requested}" cannot describe images: it has no image input support, or it is a model the vision sidecar describes FOR.`,
|
|
163
166
|
// The rejection path is not hot, so it resolves the executor itself rather than
|
|
164
167
|
// making every 400 caller thread one through.
|
|
165
|
-
allowed: visionModelOptionsFrom(config, candidates,
|
|
168
|
+
allowed: visionModelOptionsFrom(config, candidates, anthropic).map(option => option.value),
|
|
166
169
|
};
|
|
167
170
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* id outside (candidates ∪ auth slots) can never run and is refused.
|
|
15
15
|
*/
|
|
16
16
|
import type { OcxConfig } from "../../types";
|
|
17
|
-
import { AUTH_SLOT_MODELS, resolveSidecarAuth } from "../../sidecar/auth";
|
|
17
|
+
import { AUTH_SLOT_MODELS, resolveSidecarAuth, resolveAnthropicSidecarAuth, resolveAnthropicHelperInstance, AnthropicHelperUnavailableError, type SidecarAuthState } from "../../sidecar/auth";
|
|
18
|
+
import { ANTHROPIC_INSTANCE_IDS, isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
19
|
+
import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
18
20
|
import { pickerVisibleSidecarCandidates, type SidecarCandidate } from "../../sidecar/candidates";
|
|
19
21
|
import { WEB_SEARCH_BACKENDS, webSearchSidecarCandidates } from "../../web-search/backends";
|
|
20
22
|
import type { WebSearchBackendId } from "../../web-search/index";
|
|
@@ -24,6 +26,53 @@ import type { WebSearchBackendId } from "../../web-search/index";
|
|
|
24
26
|
// simply never produce rows.
|
|
25
27
|
export type WebSearchBackend = WebSearchBackendId;
|
|
26
28
|
|
|
29
|
+
export function sidecarAnthropicPoolOptions(config: OcxConfig,
|
|
30
|
+
settings: { backend?: string; anthropicInstance?: AnthropicInstanceId }, parentProviderName?: string): {
|
|
31
|
+
backend?: string; parent?: AnthropicInstanceId; selected?: AnthropicInstanceId;
|
|
32
|
+
resolved?: AnthropicInstanceId; mixed: boolean; available: AnthropicInstanceId[];
|
|
33
|
+
code?: "anthropic_helper_unavailable";
|
|
34
|
+
} {
|
|
35
|
+
const parent = configuredAnthropicInstance(config, parentProviderName);
|
|
36
|
+
const available = ANTHROPIC_INSTANCE_IDS.filter(instance => resolveAnthropicSidecarAuth(config, instance) !== undefined);
|
|
37
|
+
const base = { ...(settings.backend ? { backend: settings.backend } : {}), ...(parent ? { parent } : {}), ...(settings.anthropicInstance ? { selected: settings.anthropicInstance } : {}),
|
|
38
|
+
mixed: !!parent && !!settings.anthropicInstance && parent !== settings.anthropicInstance, available };
|
|
39
|
+
if (settings.backend !== "anthropic") return base;
|
|
40
|
+
try {
|
|
41
|
+
const resolved = resolveAnthropicHelperInstance(config, { backendFamily: "anthropic",
|
|
42
|
+
anthropicInstance: settings.anthropicInstance, parentProviderName });
|
|
43
|
+
const legacy = resolved === undefined ? resolveSidecarAuth(config).anthropicProviderName : resolved;
|
|
44
|
+
return { ...base, ...(isAnthropicInstanceId(legacy) ? { resolved: legacy } : {}) };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
|
|
47
|
+
return { ...base, code: error.code };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An unavailable explicit pool removes its executor; it never resumes legacy discovery. */
|
|
52
|
+
export function sidecarOptionsAuth(config: OcxConfig, instance?: AnthropicInstanceId): SidecarAuthState {
|
|
53
|
+
try { return resolveSidecarAuth(config, instance); }
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
|
|
56
|
+
// Preserve other backend defaults while removing all Anthropic executor authority.
|
|
57
|
+
const otherBackends = { ...config, providers: Object.fromEntries(Object.entries(config.providers)
|
|
58
|
+
.filter(([, provider]) => provider.adapter !== "anthropic")) };
|
|
59
|
+
return { isCodexAuth: resolveSidecarAuth(otherBackends).isCodexAuth, isAnthropicAuth: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Apply only the helper identity fields to a typed, isolated validation snapshot. */
|
|
64
|
+
export function sidecarSettingsAfterPatch<T extends { backend?: string; model?: string; anthropicInstance?: AnthropicInstanceId }>(
|
|
65
|
+
stored: T | undefined, patch: Record<string, unknown>,
|
|
66
|
+
): T {
|
|
67
|
+
const next = { ...stored } as T;
|
|
68
|
+
for (const key of ["backend", "model", "anthropicInstance"] as const) {
|
|
69
|
+
const value = patch[key];
|
|
70
|
+
if (value === null || (key === "model" && value === "")) delete next[key];
|
|
71
|
+
else if (value !== undefined) Object.assign(next, { [key]: value });
|
|
72
|
+
}
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
export interface WebSearchCandidateRow extends SidecarCandidate {
|
|
28
77
|
/** Executor backend that admitted this exact candidate row. */
|
|
29
78
|
backend: WebSearchBackend;
|
|
@@ -41,7 +90,7 @@ export interface WebSearchModelOption {
|
|
|
41
90
|
|
|
42
91
|
/** The candidate rows the web-search executors can actually run right now. */
|
|
43
92
|
export async function webSearchCandidateRows(config: OcxConfig): Promise<WebSearchCandidateRow[]> {
|
|
44
|
-
const auth =
|
|
93
|
+
const auth = sidecarOptionsAuth(config, config.webSearchSidecar?.anthropicInstance);
|
|
45
94
|
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
46
95
|
return webSearchSidecarCandidates(config, auth, all).flatMap(candidate => {
|
|
47
96
|
const descriptor = WEB_SEARCH_BACKENDS.find(entry =>
|
|
@@ -279,6 +279,8 @@ export interface ManagementSessionControl {
|
|
|
279
279
|
isCurrent(req: Request, config: OcxConfig): boolean;
|
|
280
280
|
/** Prove that the current browser session came from the operator-mediated pairing flow. */
|
|
281
281
|
isPaired(req: Request, config: OcxConfig): boolean;
|
|
282
|
+
/** Revalidate independent session authority before disclosing stored data keys. */
|
|
283
|
+
canRevealDataKeys?(req: Request, config: OcxConfig): boolean;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
export function createManagementSessionControl(state: ManagementAuthState): ManagementSessionControl {
|
|
@@ -307,6 +309,10 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
|
|
|
307
309
|
isPaired(req: Request, config: OcxConfig): boolean {
|
|
308
310
|
return currentSession(req, config)?.issuance === "pairing";
|
|
309
311
|
},
|
|
312
|
+
canRevealDataKeys(req: Request, config: OcxConfig): boolean {
|
|
313
|
+
const issuance = currentSession(req, config)?.issuance;
|
|
314
|
+
return issuance === "pairing" || issuance === "tailscale-identity";
|
|
315
|
+
},
|
|
310
316
|
revokeCurrent(req: Request): boolean {
|
|
311
317
|
if (!state.available) return false;
|
|
312
318
|
const credential = requestManagementCredential(req);
|
|
@@ -13,6 +13,7 @@ import { credentialDomainFor } from "../protocols/opaque-state";
|
|
|
13
13
|
import { resolveProtocolSettings } from "../protocols/settings";
|
|
14
14
|
import type { RouteResult } from "../router";
|
|
15
15
|
import type { OcxConfig } from "../types";
|
|
16
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
16
17
|
import { requiresVisionPreprocessing } from "../vision";
|
|
17
18
|
import { resolvePinnedEffort } from "./effort-policy";
|
|
18
19
|
|
|
@@ -94,7 +95,7 @@ function bridgeOnlyPolicyApplies(
|
|
|
94
95
|
/**
|
|
95
96
|
* The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
|
|
96
97
|
* is native only with `managedMessagesNativeOAuth` on (itself effective only with
|
|
97
|
-
* `managedMessagesNative`), only for
|
|
98
|
+
* `managedMessagesNative`), only for a configured Anthropic OAuth instance, and only
|
|
98
99
|
* to `api.anthropic.com`. Pool selection and recovery are resolved at native dispatch. `forward`
|
|
99
100
|
* belongs to the caller.
|
|
100
101
|
*/
|
|
@@ -107,7 +108,7 @@ function credentialDeclineReason(
|
|
|
107
108
|
if (provider.authMode === undefined || provider.authMode === "key") return undefined;
|
|
108
109
|
if (provider.authMode !== "oauth") return "auth-mode-not-native";
|
|
109
110
|
if (!resolveProtocolSettings(config, route.providerName).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
|
|
110
|
-
if (route.providerName
|
|
111
|
+
if (!configuredAnthropicInstance(config, route.providerName)) return "auth-mode-not-native";
|
|
111
112
|
if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
|
|
112
113
|
return undefined;
|
|
113
114
|
}
|