@bitkyc08/opencodex 2.63.0 → 2.64.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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-CpuDF3ci.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/index-SggB6t3z.js +86 -0
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
|
|
2
|
+
import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
5
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -231,18 +232,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
231
232
|
}).kind;
|
|
232
233
|
if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return;
|
|
233
234
|
const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
|
|
235
|
+
const writtenProfile = current.claudeCode.desktopProfile;
|
|
236
|
+
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
234
237
|
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
235
238
|
current.port ?? 10100,
|
|
236
239
|
[...desktopVisibleNativeSlugs(current)],
|
|
237
240
|
routed,
|
|
238
241
|
current.apiKeys?.[0]?.key,
|
|
239
242
|
"static",
|
|
240
|
-
|
|
243
|
+
writtenProfile,
|
|
241
244
|
nativeContextLimits(current),
|
|
242
245
|
);
|
|
243
246
|
if (result.written && result.fingerprint) {
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
const marked = commitDesktopAppliedMarker(markerBaseline, result.fingerprint);
|
|
248
|
+
if (marked.status === "unavailable" || marked.value === false) {
|
|
249
|
+
console.warn("[claude-desktop] provider-change applied marker skipped");
|
|
250
|
+
}
|
|
246
251
|
}
|
|
247
252
|
} catch { /* best-effort */ }
|
|
248
253
|
}
|
|
@@ -985,9 +990,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
985
990
|
let body: { profile?: unknown };
|
|
986
991
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
987
992
|
try {
|
|
988
|
-
const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile");
|
|
993
|
+
const { parseDesktopProfile, preserveDesktopAppliedState, reconcileDesktopProfile, sameProfileContent } = await import("../../claude/desktop-profile");
|
|
989
994
|
const parsed = parseDesktopProfile(body.profile);
|
|
990
|
-
const
|
|
995
|
+
const initial = loadConfig();
|
|
996
|
+
const current = await buildClaudeDesktopState(initial);
|
|
991
997
|
const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route));
|
|
992
998
|
for (const route of Object.keys(parsed.assignments)) {
|
|
993
999
|
if (!current.profile.assignments[route] && !availableRoutes.has(route)) {
|
|
@@ -1008,9 +1014,39 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1008
1014
|
throw new Error(`현재 사용할 수 없는 모델은 기본값으로 지정할 수 없습니다: ${nextDefault}`);
|
|
1009
1015
|
}
|
|
1010
1016
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1017
|
+
// Applied markers are server-owned bookkeeping. Discard client copies, then
|
|
1018
|
+
// restore the trusted markers only if the desired profile stayed identical.
|
|
1019
|
+
const editable = { version: 1 as const, assignments: parsed.assignments, defaults: parsed.defaults };
|
|
1020
|
+
const state = await buildClaudeDesktopState(initial, editable);
|
|
1021
|
+
const rebuilt = reconcileDesktopProfile(state.profile, state.models);
|
|
1022
|
+
const expectedProfile = initial.claudeCode?.desktopProfile;
|
|
1023
|
+
const outcome = mutatePersistedConfig<OcxConfig["claudeCode"] | null>(persisted => {
|
|
1024
|
+
const latest = persisted.claudeCode?.desktopProfile;
|
|
1025
|
+
if ((latest == null) !== (expectedProfile == null)
|
|
1026
|
+
|| (latest && expectedProfile && !sameProfileContent(latest, expectedProfile))) {
|
|
1027
|
+
return { changed: false, value: null };
|
|
1028
|
+
}
|
|
1029
|
+
persisted.claudeCode = {
|
|
1030
|
+
...(persisted.claudeCode ?? {}),
|
|
1031
|
+
desktopProfile: preserveDesktopAppliedState(latest ?? current.profile, rebuilt),
|
|
1032
|
+
};
|
|
1033
|
+
return { changed: true, value: structuredClone(persisted.claudeCode) };
|
|
1034
|
+
});
|
|
1035
|
+
if (outcome.status === "unavailable" && outcome.reason !== "conflict") {
|
|
1036
|
+
return jsonResponse({ error: "Claude Desktop profile could not be saved (config " + outcome.reason + ")" }, 500);
|
|
1037
|
+
}
|
|
1038
|
+
if (outcome.status === "unavailable" || outcome.value === null) {
|
|
1039
|
+
return jsonResponse({ error: "Claude Desktop profile changed during save" }, 409);
|
|
1040
|
+
}
|
|
1041
|
+
adoptPersistedClaudeCode(config, outcome.value);
|
|
1042
|
+
// An unarmed live snapshot may prefer its old marker during reconciliation.
|
|
1043
|
+
// Pin the field this route just committed while leaving other live leaves intact.
|
|
1044
|
+
if (outcome.value?.desktopProfile) {
|
|
1045
|
+
config.claudeCode = {
|
|
1046
|
+
...(config.claudeCode ?? {}),
|
|
1047
|
+
desktopProfile: structuredClone(outcome.value.desktopProfile),
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1014
1050
|
const saved = await buildClaudeDesktopState(config);
|
|
1015
1051
|
const runtimePort = Number(url.port) || config.port;
|
|
1016
1052
|
return jsonResponse({ ok: true, ...saved, port: runtimePort });
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Management API for Anthropic OAuth usage-limit reset grants.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/anthropic/reset-grants?accountId=... read one account's grants
|
|
5
|
+
* POST /api/anthropic/reset-grants/consume spend one grant
|
|
6
|
+
*
|
|
7
|
+
* Spending uses the user's own subscription benefit, so the POST requires the
|
|
8
|
+
* dashboard session principal (AGENTS.md, "User-consent actions"); the admin token
|
|
9
|
+
* alone is refused. Responses carry fixed codes and fixed messages only: no
|
|
10
|
+
* upstream body, exception text, token, organization id, or email. Nothing here
|
|
11
|
+
* logs. Lazy-loaded by `handleAnthropicResetGrantRoutesOnDemand` in
|
|
12
|
+
* management-api.ts so the core request path never imports it.
|
|
13
|
+
*/
|
|
14
|
+
import { jsonResponse } from "../auth-cors";
|
|
15
|
+
import type { ManagementContext } from "./context";
|
|
16
|
+
import { getValidAccessSnapshotForAccount } from "../../oauth";
|
|
17
|
+
import { captureOAuthAccountSelection, listAccounts } from "../../oauth/store";
|
|
18
|
+
import {
|
|
19
|
+
ANTHROPIC_RESET_GRANT_ID_RE,
|
|
20
|
+
AnthropicResetGrantError,
|
|
21
|
+
AnthropicResetGrantUnknownOutcome,
|
|
22
|
+
anthropicResetGrantBlocker,
|
|
23
|
+
claimAnthropicResetGrant,
|
|
24
|
+
fetchAnthropicOrganizationUuid,
|
|
25
|
+
fetchAnthropicResetGrantStatus,
|
|
26
|
+
} from "../../providers/anthropic-reset-grants";
|
|
27
|
+
import {
|
|
28
|
+
AnthropicResetLedgerError,
|
|
29
|
+
anthropicOrgDigest,
|
|
30
|
+
anthropicResetOperationExists,
|
|
31
|
+
beginAnthropicResetOperation,
|
|
32
|
+
pendingAnthropicResetOperation,
|
|
33
|
+
releaseAnthropicResetLease,
|
|
34
|
+
settleAnthropicResetOperation,
|
|
35
|
+
} from "../../providers/anthropic-reset-grant-ledger";
|
|
36
|
+
|
|
37
|
+
const PROVIDER = "anthropic";
|
|
38
|
+
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
39
|
+
|
|
40
|
+
const MESSAGES = {
|
|
41
|
+
method_not_allowed: "Method not allowed",
|
|
42
|
+
session_required: "Spending a reset requires a dashboard session.",
|
|
43
|
+
invalid_json: "Request body must be JSON.",
|
|
44
|
+
invalid_grant_id: "grantId is missing or malformed.",
|
|
45
|
+
invalid_operation_id: "operationId must be a UUIDv4.",
|
|
46
|
+
no_account: "No matching Anthropic OAuth account.",
|
|
47
|
+
auth_failed: "Sign in to this Anthropic account again.",
|
|
48
|
+
upstream_unavailable: "Anthropic did not return the reset-grant status.",
|
|
49
|
+
grant_not_usable: "This reset grant cannot be used right now.",
|
|
50
|
+
operation_identity_mismatch: "This operation belongs to another account or grant.",
|
|
51
|
+
in_flight: "This reset is still being processed.",
|
|
52
|
+
unresolved_prior_operation: "An earlier reset attempt for this grant is not confirmed yet.",
|
|
53
|
+
unknown_outcome_expired: "This attempt can no longer be retried; re-read the account.",
|
|
54
|
+
unknown_outcome: "The reset did not answer; its outcome is unknown.",
|
|
55
|
+
journal_write_failed: "The reset answered but could not be recorded; its outcome is unknown here.",
|
|
56
|
+
ledger_busy: "The reset journal is busy. Nothing was used.",
|
|
57
|
+
ledger_unavailable: "The reset journal is unavailable. Nothing was used.",
|
|
58
|
+
ledger_capacity: "The reset journal is full. Nothing was used.",
|
|
59
|
+
} as const;
|
|
60
|
+
type ErrorCode = keyof typeof MESSAGES;
|
|
61
|
+
|
|
62
|
+
export interface AnthropicResetGrantRouteDeps {
|
|
63
|
+
fetchFn?: typeof globalThis.fetch;
|
|
64
|
+
journalPath?: string;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
listAccountIds: () => string[];
|
|
67
|
+
activeAccountId: () => string | null;
|
|
68
|
+
accessTokenFor: (accountId: string) => Promise<string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const defaultDeps: AnthropicResetGrantRouteDeps = {
|
|
72
|
+
listAccountIds: () => listAccounts(PROVIDER).map(account => account.id),
|
|
73
|
+
activeAccountId: () => captureOAuthAccountSelection(PROVIDER)?.accountId ?? null,
|
|
74
|
+
accessTokenFor: async accountId =>
|
|
75
|
+
(await getValidAccessSnapshotForAccount(PROVIDER, accountId, { requireUsableAccount: true })).accessToken,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function fail(ctx: ManagementContext, status: number, code: ErrorCode, extra: Record<string, unknown> = {}): Response {
|
|
79
|
+
return jsonResponse({ error: { code, message: MESSAGES[code], ...extra } }, status, ctx.req, ctx.config);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ledgerFailure(ctx: ManagementContext, error: unknown): Response {
|
|
83
|
+
if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy");
|
|
84
|
+
return fail(ctx, 503, "ledger_unavailable");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readFailure(ctx: ManagementContext, error: unknown): Response {
|
|
88
|
+
if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed");
|
|
89
|
+
return fail(ctx, 502, "upstream_unavailable");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveAccountId(deps: AnthropicResetGrantRouteDeps, requested: unknown): string | null {
|
|
93
|
+
const ids = deps.listAccountIds();
|
|
94
|
+
if (typeof requested === "string" && requested.trim() !== "") {
|
|
95
|
+
return ids.includes(requested.trim()) ? requested.trim() : null;
|
|
96
|
+
}
|
|
97
|
+
const active = deps.activeAccountId();
|
|
98
|
+
return active && ids.includes(active) ? active : ids[0] ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function tokenFor(deps: AnthropicResetGrantRouteDeps, accountId: string): Promise<string | null> {
|
|
102
|
+
try {
|
|
103
|
+
return await deps.accessTokenFor(accountId);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
|
|
110
|
+
const accountId = resolveAccountId(deps, ctx.url.searchParams.get("accountId") ?? undefined);
|
|
111
|
+
if (!accountId) return fail(ctx, 400, "no_account");
|
|
112
|
+
const accessToken = await tokenFor(deps, accountId);
|
|
113
|
+
if (!accessToken) return fail(ctx, 401, "auth_failed");
|
|
114
|
+
let status;
|
|
115
|
+
try {
|
|
116
|
+
status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return readFailure(ctx, error);
|
|
119
|
+
}
|
|
120
|
+
let pendingOperation = null;
|
|
121
|
+
let journalAvailable = true;
|
|
122
|
+
try {
|
|
123
|
+
pendingOperation = pendingAnthropicResetOperation(accountId, { journalPath: deps.journalPath, now: deps.now?.() });
|
|
124
|
+
} catch {
|
|
125
|
+
journalAvailable = false;
|
|
126
|
+
}
|
|
127
|
+
return jsonResponse({ accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
|
|
131
|
+
if (ctx.principal !== "gui-session") return fail(ctx, 403, "session_required");
|
|
132
|
+
let body: Record<string, unknown>;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = await ctx.req.json() as unknown;
|
|
135
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return fail(ctx, 400, "invalid_json");
|
|
136
|
+
body = parsed as Record<string, unknown>;
|
|
137
|
+
} catch {
|
|
138
|
+
return fail(ctx, 400, "invalid_json");
|
|
139
|
+
}
|
|
140
|
+
const { grantId, operationId } = body;
|
|
141
|
+
if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return fail(ctx, 400, "invalid_grant_id");
|
|
142
|
+
if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return fail(ctx, 400, "invalid_operation_id");
|
|
143
|
+
if (typeof body.accountId !== "string") return fail(ctx, 400, "no_account");
|
|
144
|
+
const accountId = resolveAccountId(deps, body.accountId);
|
|
145
|
+
if (!accountId) return fail(ctx, 400, "no_account");
|
|
146
|
+
|
|
147
|
+
const accessToken = await tokenFor(deps, accountId);
|
|
148
|
+
if (!accessToken) return fail(ctx, 401, "auth_failed");
|
|
149
|
+
let organizationUuid: string;
|
|
150
|
+
try {
|
|
151
|
+
organizationUuid = await fetchAnthropicOrganizationUuid({ accessToken, fetchFn: deps.fetchFn });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return readFailure(ctx, error);
|
|
154
|
+
}
|
|
155
|
+
const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
|
|
156
|
+
|
|
157
|
+
// A new operation must pass the spend gate on a fresh read. A same-id retry of
|
|
158
|
+
// an open record skips it: that attempt was already gated, and upstream
|
|
159
|
+
// answers a repeated request id for itself.
|
|
160
|
+
let known: boolean;
|
|
161
|
+
try {
|
|
162
|
+
known = anthropicResetOperationExists(operationId, ledger);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return ledgerFailure(ctx, error);
|
|
165
|
+
}
|
|
166
|
+
if (!known) {
|
|
167
|
+
let status;
|
|
168
|
+
try {
|
|
169
|
+
status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return readFailure(ctx, error);
|
|
172
|
+
}
|
|
173
|
+
const blocker = anthropicResetGrantBlocker(status, grantId);
|
|
174
|
+
if (blocker) return fail(ctx, 409, "grant_not_usable", { reason: blocker });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let begin;
|
|
178
|
+
try {
|
|
179
|
+
begin = beginAnthropicResetOperation(
|
|
180
|
+
{ operationId, accountId, grantId, orgDigest: anthropicOrgDigest(organizationUuid) },
|
|
181
|
+
{ journalPath: deps.journalPath, now: deps.now?.() },
|
|
182
|
+
);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return ledgerFailure(ctx, error);
|
|
185
|
+
}
|
|
186
|
+
switch (begin.kind) {
|
|
187
|
+
case "replay":
|
|
188
|
+
return jsonResponse(
|
|
189
|
+
{ code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
|
|
190
|
+
200, ctx.req, ctx.config,
|
|
191
|
+
);
|
|
192
|
+
case "identity-mismatch": return fail(ctx, 409, "operation_identity_mismatch");
|
|
193
|
+
case "in-flight": return fail(ctx, 409, "in_flight");
|
|
194
|
+
case "expired": return fail(ctx, 409, "unknown_outcome_expired");
|
|
195
|
+
case "unresolved-prior": return fail(ctx, 409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
|
|
196
|
+
case "capacity": return fail(ctx, 503, "ledger_capacity");
|
|
197
|
+
case "execute": break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let answer;
|
|
201
|
+
try {
|
|
202
|
+
answer = await claimAnthropicResetGrant({
|
|
203
|
+
accessToken, organizationUuid, grantId, requestId: operationId, fetchFn: deps.fetchFn,
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
try {
|
|
207
|
+
releaseAnthropicResetLease(operationId, { journalPath: deps.journalPath, now: deps.now?.() });
|
|
208
|
+
} catch { /* the lease expires on its own */ }
|
|
209
|
+
return fail(ctx, 502, "unknown_outcome", { operationId, grantId });
|
|
210
|
+
}
|
|
211
|
+
let settled;
|
|
212
|
+
try {
|
|
213
|
+
settled = settleAnthropicResetOperation(
|
|
214
|
+
{ operationId, code: answer.code, resetsLeft: answer.resetsLeft },
|
|
215
|
+
{ journalPath: deps.journalPath, now: deps.now?.() },
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
// Fail closed: the caller must not read this as a durable result.
|
|
219
|
+
return fail(ctx, 500, "journal_write_failed", { operationId, grantId });
|
|
220
|
+
}
|
|
221
|
+
// Report what the journal holds. An overlapping same-id attempt may have
|
|
222
|
+
// settled first; its answer is the canonical one.
|
|
223
|
+
const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
|
|
224
|
+
return jsonResponse(
|
|
225
|
+
{
|
|
226
|
+
code: settled.code,
|
|
227
|
+
replayed,
|
|
228
|
+
resetsLeft: settled.resetsLeft,
|
|
229
|
+
cleared: replayed ? [] : answer.cleared,
|
|
230
|
+
accountId,
|
|
231
|
+
grantId,
|
|
232
|
+
operationId,
|
|
233
|
+
},
|
|
234
|
+
200, ctx.req, ctx.config,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function handleAnthropicResetGrantRoutes(
|
|
239
|
+
ctx: ManagementContext,
|
|
240
|
+
deps: AnthropicResetGrantRouteDeps = defaultDeps,
|
|
241
|
+
): Promise<Response | null> {
|
|
242
|
+
const { pathname } = ctx.url;
|
|
243
|
+
if (pathname === "/api/anthropic/reset-grants") {
|
|
244
|
+
if (ctx.req.method !== "GET") return fail(ctx, 405, "method_not_allowed");
|
|
245
|
+
return handleRead(ctx, deps);
|
|
246
|
+
}
|
|
247
|
+
if (pathname === "/api/anthropic/reset-grants/consume") {
|
|
248
|
+
if (ctx.req.method !== "POST") return fail(ctx, 405, "method_not_allowed");
|
|
249
|
+
return handleConsume(ctx, deps);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
@@ -403,7 +403,11 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
|
|
|
403
403
|
}
|
|
404
404
|
if (typeof body.title !== "string") return fail(ctx, "invalid_body", 400, "title must be a string");
|
|
405
405
|
if (typeof body.body !== "string") return fail(ctx, "invalid_body", 400, "body must be a string");
|
|
406
|
-
|
|
406
|
+
const normalized = normalizeBody(body.body);
|
|
407
|
+
if (utf8Bytes(normalized) > MAX_BODY_BYTES) {
|
|
408
|
+
return fail(ctx, "body_too_large", 400, `base variant exceeds ${MAX_BODY_BYTES} bytes`);
|
|
409
|
+
}
|
|
410
|
+
return settle(ctx, writeBaseVariant({ id, title: body.title, body: normalized }, revision, paths(ctx)));
|
|
407
411
|
}
|
|
408
412
|
|
|
409
413
|
if (url.pathname === "/api/codex-prompt/adopt" && req.method === "POST") {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
providerHeadersConfigError,
|
|
23
23
|
saveConfigPreservingClaudeCode,
|
|
24
24
|
} from "../../config";
|
|
25
|
+
import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
|
|
25
26
|
import {
|
|
26
27
|
clearLoginState,
|
|
27
28
|
getLoginStatus,
|
|
@@ -219,18 +220,27 @@ export async function syncEnabledClientIntegrations(
|
|
|
219
220
|
if (claudeDesktopIntegrationEnabled(latest)) {
|
|
220
221
|
const routed = filterCatalogVisibleModels(models, latest)
|
|
221
222
|
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
|
|
223
|
+
const writtenProfile = latest.claudeCode?.desktopProfile;
|
|
224
|
+
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
222
225
|
const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
223
226
|
port,
|
|
224
227
|
[...desktopVisibleNativeSlugs(latest)],
|
|
225
228
|
routed,
|
|
226
229
|
latest.apiKeys?.[0]?.key,
|
|
227
230
|
"static",
|
|
228
|
-
|
|
231
|
+
writtenProfile,
|
|
229
232
|
nativeContextLimits(latest),
|
|
230
233
|
);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
if (!r.written || !r.fingerprint) {
|
|
235
|
+
out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
|
|
236
|
+
} else {
|
|
237
|
+
const marked = commitDesktopAppliedMarker(markerBaseline, r.fingerprint);
|
|
238
|
+
out.push(marked.status === "unavailable"
|
|
239
|
+
? { client: "claude-desktop", ok: false, reason: "Claude Desktop applied marker was not saved (" + marked.reason + ")" }
|
|
240
|
+
: marked.value === false
|
|
241
|
+
? { client: "claude-desktop", ok: false, reason: "Claude Desktop desired profile changed during sync; applied marker skipped" }
|
|
242
|
+
: { client: "claude-desktop", ok: true, changed: true });
|
|
243
|
+
}
|
|
234
244
|
}
|
|
235
245
|
} catch (error) {
|
|
236
246
|
out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
@@ -147,8 +147,16 @@ function validateKeyName(
|
|
|
147
147
|
return { value };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function metaMuseConsentRequired(provider: string, principal: ManagementContext["principal"]): Response | null {
|
|
151
|
+
if (provider !== "meta-muse" || principal === "gui-session") return null;
|
|
152
|
+
return jsonResponse({
|
|
153
|
+
error: "Meta Muse login requires acknowledgement in the OpenCodex dashboard.",
|
|
154
|
+
code: "oauth_consent_required",
|
|
155
|
+
}, 403);
|
|
156
|
+
}
|
|
157
|
+
|
|
150
158
|
export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
151
|
-
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
|
|
159
|
+
const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
|
|
152
160
|
|
|
153
161
|
if (url.pathname === "/api/accounts/events" && req.method === "GET") {
|
|
154
162
|
const { accountSelectionStream } = await import("./account-selection-stream");
|
|
@@ -172,6 +180,12 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
172
180
|
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
|
|
173
181
|
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
174
182
|
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
183
|
+
// Muse may import a local Keychain credential or start a device grant; add-account
|
|
184
|
+
// and reauth skip the import. All management login paths require the dashboard
|
|
185
|
+
// principal before credential acquisition. A raw token proves administration,
|
|
186
|
+
// not acknowledgement; caller-supplied headers are not consent evidence.
|
|
187
|
+
const consentRequired = metaMuseConsentRequired(provider, principal);
|
|
188
|
+
if (consentRequired) return consentRequired;
|
|
175
189
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
176
190
|
if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
|
|
177
191
|
const accountId = body.accountId?.trim();
|
|
@@ -242,6 +256,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
242
256
|
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; input?: string; code?: string };
|
|
243
257
|
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
244
258
|
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
259
|
+
const consentRequired = metaMuseConsentRequired(provider, principal);
|
|
260
|
+
if (consentRequired) return consentRequired;
|
|
245
261
|
const input = typeof body.input === "string" ? body.input : typeof body.code === "string" ? body.code : "";
|
|
246
262
|
// Authorization responses are measured in hundreds of bytes; never accept the
|
|
247
263
|
// generic management-body allowance here.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { nonBlankStringArrayConfigError, normalizeNonBlankStringArray } from "../../config/provider-validation";
|
|
2
|
+
import type { OcxProviderConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a provider save keeps (#5563).
|
|
6
|
+
*
|
|
7
|
+
* `POST /api/providers` with an existing name replaces the stored row with a candidate built from
|
|
8
|
+
* the request. The dashboard form cannot send the fields below, so a save that omits them used
|
|
9
|
+
* to drop them from a custom provider, or reset them to the registry seed through
|
|
10
|
+
* `enrichProviderFromCatalog` for a registry provider. The live-config reconcile cannot restore
|
|
11
|
+
* them afterwards: the disk row equals its baseline, so the three-way merge keeps the value the
|
|
12
|
+
* save wrote.
|
|
13
|
+
*
|
|
14
|
+
* These five settings record how one upstream behaves. An overwrite that keeps the destination
|
|
15
|
+
* carries each one the request omits, including an explicit `[]` or `false`. An overwrite that
|
|
16
|
+
* moves the provider to another destination carries none of them: they describe the previous
|
|
17
|
+
* upstream, and registry enrichment already fills what is known about the new one. The same
|
|
18
|
+
* destination rule gates the stored `apiKeyPool`, whose keys were issued for the previous
|
|
19
|
+
* destination. A value the request sends always wins, and the rest of the old row is never merged
|
|
20
|
+
* into the candidate.
|
|
21
|
+
*/
|
|
22
|
+
export const PROVIDER_COMPAT_CARRY_FIELDS = [
|
|
23
|
+
"preserveReasoningContentModels",
|
|
24
|
+
"requiresReasoningPlaceholderModels",
|
|
25
|
+
"foldDeveloperRoleToSystem",
|
|
26
|
+
"reasoningWireFormat",
|
|
27
|
+
"omitReasoningEffortWithToolsModels",
|
|
28
|
+
] as const satisfies readonly (keyof OcxProviderConfig)[];
|
|
29
|
+
|
|
30
|
+
export type ProviderCompatCarryField = typeof PROVIDER_COMPAT_CARRY_FIELDS[number];
|
|
31
|
+
|
|
32
|
+
/** The only `reasoningWireFormat` value the adapters understand. */
|
|
33
|
+
export const PROVIDER_REASONING_WIRE_FORMATS: readonly NonNullable<OcxProviderConfig["reasoningWireFormat"]>[] = [
|
|
34
|
+
"gateway-object",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Request ownership, sampled before `enrichProviderFromCatalog`. After enrichment "the client
|
|
39
|
+
* omitted this" and "the registry supplied it" look the same.
|
|
40
|
+
*/
|
|
41
|
+
export interface ProviderOverwriteSample {
|
|
42
|
+
readonly submitted: ReadonlySet<ProviderCompatCarryField>;
|
|
43
|
+
readonly namesAuthMode: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sampleProviderOverwrite(provider: object): ProviderOverwriteSample {
|
|
47
|
+
return {
|
|
48
|
+
submitted: new Set(PROVIDER_COMPAT_CARRY_FIELDS.filter(field => Object.hasOwn(provider, field))),
|
|
49
|
+
namesAuthMode: Object.hasOwn(provider, "authMode"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizedDestinationUrl(value: unknown): string {
|
|
54
|
+
if (typeof value !== "string") return "";
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(trimmed);
|
|
58
|
+
return `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}${url.search}`;
|
|
59
|
+
} catch {
|
|
60
|
+
return trimmed.replace(/\/+$/, "");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether an overwrite keeps the provider on the same upstream: same adapter, same base URL
|
|
66
|
+
* (scheme and host compared case-insensitively, trailing slashes ignored) and, when the request
|
|
67
|
+
* names one, the same auth mode. The dashboard form sends `authMode` only for key and forward
|
|
68
|
+
* auth, so an omitted value is not evidence of a move; a stored row without `authMode` is a
|
|
69
|
+
* key-auth row.
|
|
70
|
+
*/
|
|
71
|
+
export function providerOverwriteKeepsDestination(
|
|
72
|
+
candidate: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode">,
|
|
73
|
+
stored: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode"> | undefined,
|
|
74
|
+
sample: ProviderOverwriteSample,
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!stored) return false;
|
|
77
|
+
if ((candidate.adapter ?? "").trim() !== (stored.adapter ?? "").trim()) return false;
|
|
78
|
+
if (normalizedDestinationUrl(candidate.baseUrl) !== normalizedDestinationUrl(stored.baseUrl)) return false;
|
|
79
|
+
if (sample.namesAuthMode && (candidate.authMode ?? "key") !== (stored.authMode ?? "key")) return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Carry the stored compatibility settings the request omitted. `live` must be the row read after
|
|
85
|
+
* the route's DNS await, so a PATCH that saved one of them during the wait is kept.
|
|
86
|
+
*/
|
|
87
|
+
export function carryProviderCompatFields(
|
|
88
|
+
candidate: OcxProviderConfig,
|
|
89
|
+
live: OcxProviderConfig | undefined,
|
|
90
|
+
sample: ProviderOverwriteSample,
|
|
91
|
+
): void {
|
|
92
|
+
if (!live || !providerOverwriteKeepsDestination(candidate, live, sample)) return;
|
|
93
|
+
const target = candidate as unknown as Record<string, unknown>;
|
|
94
|
+
for (const field of PROVIDER_COMPAT_CARRY_FIELDS) {
|
|
95
|
+
if (sample.submitted.has(field)) continue;
|
|
96
|
+
const stored = live[field];
|
|
97
|
+
if (stored === undefined) continue;
|
|
98
|
+
target[field] = Array.isArray(stored) ? [...stored] : stored;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const REASONING_LIST_FIELDS = ["preserveReasoningContentModels", "requiresReasoningPlaceholderModels"] as const;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Type checks for a POST that does send one of the settings. `omitReasoningEffortWithToolsModels`
|
|
106
|
+
* is already checked by `providerManagementConfigError`.
|
|
107
|
+
*/
|
|
108
|
+
export function providerCompatFieldConfigError(provider: Record<string, unknown>): string | null {
|
|
109
|
+
for (const field of REASONING_LIST_FIELDS) {
|
|
110
|
+
const error = nonBlankStringArrayConfigError(provider[field], field);
|
|
111
|
+
if (error) return error;
|
|
112
|
+
}
|
|
113
|
+
const fold = provider.foldDeveloperRoleToSystem;
|
|
114
|
+
if (fold !== undefined && typeof fold !== "boolean") return "foldDeveloperRoleToSystem must be a boolean";
|
|
115
|
+
const wire = provider.reasoningWireFormat;
|
|
116
|
+
if (wire !== undefined && !PROVIDER_REASONING_WIRE_FORMATS.includes(wire as never)) {
|
|
117
|
+
return `reasoningWireFormat must be one of: ${PROVIDER_REASONING_WIRE_FORMATS.join(", ")}`;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* PATCH branches for the settings the field mask did not know. `null` clears a field. For the two
|
|
124
|
+
* reasoning lists an empty array is kept rather than deleted: it is the explicit opt-out that stops
|
|
125
|
+
* the registry seed from filling the field back in (see the note under OAUTH_RECONCILE_FIELDS in
|
|
126
|
+
* src/oauth/index.ts).
|
|
127
|
+
*/
|
|
128
|
+
export function applyProviderCompatPatchFields(
|
|
129
|
+
rawBody: Record<string, unknown>,
|
|
130
|
+
next: OcxProviderConfig,
|
|
131
|
+
): { touched: boolean } | { error: string } {
|
|
132
|
+
let touched = false;
|
|
133
|
+
for (const field of REASONING_LIST_FIELDS) {
|
|
134
|
+
if (!Object.hasOwn(rawBody, field)) continue;
|
|
135
|
+
const value = rawBody[field];
|
|
136
|
+
if (value === null) {
|
|
137
|
+
delete next[field];
|
|
138
|
+
} else {
|
|
139
|
+
const error = nonBlankStringArrayConfigError(value, field);
|
|
140
|
+
if (error) return { error };
|
|
141
|
+
next[field] = normalizeNonBlankStringArray(value as string[]);
|
|
142
|
+
}
|
|
143
|
+
touched = true;
|
|
144
|
+
}
|
|
145
|
+
if (Object.hasOwn(rawBody, "foldDeveloperRoleToSystem")) {
|
|
146
|
+
const value = rawBody.foldDeveloperRoleToSystem;
|
|
147
|
+
if (value === null) delete next.foldDeveloperRoleToSystem;
|
|
148
|
+
else if (typeof value === "boolean") next.foldDeveloperRoleToSystem = value;
|
|
149
|
+
else return { error: "foldDeveloperRoleToSystem must be a boolean" };
|
|
150
|
+
touched = true;
|
|
151
|
+
}
|
|
152
|
+
if (Object.hasOwn(rawBody, "reasoningWireFormat")) {
|
|
153
|
+
const value = rawBody.reasoningWireFormat;
|
|
154
|
+
if (value === null) {
|
|
155
|
+
delete next.reasoningWireFormat;
|
|
156
|
+
} else {
|
|
157
|
+
const format = PROVIDER_REASONING_WIRE_FORMATS.find(candidate => candidate === value);
|
|
158
|
+
if (!format) return { error: `reasoningWireFormat must be one of: ${PROVIDER_REASONING_WIRE_FORMATS.join(", ")}` };
|
|
159
|
+
next.reasoningWireFormat = format;
|
|
160
|
+
}
|
|
161
|
+
touched = true;
|
|
162
|
+
}
|
|
163
|
+
return { touched };
|
|
164
|
+
}
|