@bitkyc08/opencodex 2.64.0 → 2.65.0-preview.20260925
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 +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-CpuDF3ci.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
package/src/cli/dispatch.ts
CHANGED
|
@@ -555,24 +555,24 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
555
555
|
const cacheArgs = deps.args.slice(1);
|
|
556
556
|
const restartScope = readRestartScope(cacheArgs, console);
|
|
557
557
|
const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
|
|
558
|
-
const {
|
|
558
|
+
const { invalidateCodexModelsCacheWithPermitOutcome } = await import("../codex/catalog/sync");
|
|
559
559
|
const { getCodexHome } = await import("../codex/paths");
|
|
560
|
-
const { readCodexCatalogPathForHome } = await import("../codex/catalog/parsing");
|
|
561
|
-
const { existsSync } = await import("node:fs");
|
|
562
560
|
const owningCodexHome = getCodexHome();
|
|
563
561
|
const cacheGateSnapshot = deps.loadConfig();
|
|
564
562
|
const desiredDisabled = !shouldSyncCodexOnStart(cacheGateSnapshot);
|
|
565
563
|
const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
566
|
-
|
|
564
|
+
invalidateCodexModelsCacheWithPermitOutcome(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
|
|
567
565
|
const cacheJson = cacheArgs.includes("--json");
|
|
568
566
|
const jsonSafeLog = cacheJson
|
|
569
567
|
? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
|
|
570
568
|
: console;
|
|
571
569
|
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
572
|
-
if (invalidated.kind === "completed" && invalidated.value) {
|
|
570
|
+
if (invalidated.kind === "completed" && invalidated.value === "written") {
|
|
573
571
|
await handleRestartScopeAfterWrite(restartScope, jsonSafeLog);
|
|
574
|
-
} else if (
|
|
575
|
-
//
|
|
572
|
+
} else if (!cacheJson && invalidated.kind === "completed" && invalidated.value === "desired_disabled") {
|
|
573
|
+
// Only when the OFF gate itself stopped the write does OFF explain the outcome. An
|
|
574
|
+
// explicit sync-cache refreshes regardless of the toggle, so an unchanged cache, a
|
|
575
|
+
// missing catalog, or a contended writer is reported below on its own terms.
|
|
576
576
|
// Under --json this belongs on the envelope, not as a second stdout line.
|
|
577
577
|
console.log(localClientSkipMessage(
|
|
578
578
|
cacheGateSnapshot,
|
|
@@ -580,8 +580,8 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
580
580
|
"No catalog or cache write resulted.",
|
|
581
581
|
));
|
|
582
582
|
}
|
|
583
|
-
//
|
|
584
|
-
//
|
|
583
|
+
// An identical cache is a successful no-op, not a failed refresh. Only a real write
|
|
584
|
+
// should restart Codex; a missing catalog or contended writer is also a benign skip.
|
|
585
585
|
//
|
|
586
586
|
// Losing the catalog write lock to another process is a skip, not a failure:
|
|
587
587
|
// serialization working as designed is the expected outcome under concurrency, and a
|
|
@@ -596,30 +596,25 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
596
596
|
// means the user asked for it regardless of the toggle. Treating OFF as automatic success
|
|
597
597
|
// would report exit 0 and `skipped: true` for a refresh that actually failed.
|
|
598
598
|
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// normal state of a fully native home and the case
|
|
604
|
-
// `codex-composed-acceptance.test.ts` pins at exit 0. It is checked here rather than by
|
|
605
|
-
// widening that function's return type, because its boolean is consumed by a dozen
|
|
606
|
-
// management routes that have no use for the distinction.
|
|
607
|
-
const wrote = invalidated.kind === "completed" && Boolean(invalidated.value);
|
|
599
|
+
// The detailed outcome distinguishes an unchanged cache from a failed rewrite while
|
|
600
|
+
// the boolean wrapper remains available to callers that only care whether bytes changed.
|
|
601
|
+
const wrote = invalidated.kind === "completed" && invalidated.value === "written";
|
|
602
|
+
const unchanged = invalidated.kind === "completed" && invalidated.value === "unchanged";
|
|
608
603
|
const contended = invalidated.kind === "unavailable" && invalidated.reason === "busy";
|
|
609
|
-
const noCatalog =
|
|
610
|
-
const ok = wrote || contended || noCatalog;
|
|
604
|
+
const noCatalog = invalidated.kind === "completed" && invalidated.value === "missing_catalog";
|
|
605
|
+
const ok = wrote || unchanged || contended || noCatalog;
|
|
611
606
|
if (cacheJson) {
|
|
612
607
|
console.log(JSON.stringify({
|
|
613
608
|
schemaVersion: 1,
|
|
614
609
|
ok,
|
|
615
610
|
wrote,
|
|
616
|
-
skipped: contended || noCatalog,
|
|
611
|
+
skipped: unchanged || contended || noCatalog,
|
|
617
612
|
outcome: invalidated.kind,
|
|
618
613
|
// `outcome` alone cannot separate a contended lock from a hard serialization
|
|
619
614
|
// failure -- both are `unavailable`. Carry the reason so a caller can.
|
|
620
615
|
reason: invalidated.kind === "unavailable" ? invalidated.reason : undefined,
|
|
621
|
-
// Which of the
|
|
622
|
-
skippedReason: contended ? "contended" : noCatalog ? "no_catalog" : undefined,
|
|
616
|
+
// Which of the three benign skips this was, so `skipped: true` is never opaque.
|
|
617
|
+
skippedReason: unchanged ? "unchanged" : contended ? "contended" : noCatalog ? "no_catalog" : undefined,
|
|
623
618
|
desiredDisabled,
|
|
624
619
|
codexHome: owningCodexHome,
|
|
625
620
|
}, null, 2));
|
|
@@ -627,6 +622,8 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
627
622
|
console.log("Another process owns the catalog write; cache sync skipped.");
|
|
628
623
|
} else if (noCatalog) {
|
|
629
624
|
console.log("No Codex catalog to derive a cache from; nothing to sync.");
|
|
625
|
+
} else if (unchanged) {
|
|
626
|
+
console.log("Codex model cache is already current; nothing to sync.");
|
|
630
627
|
} else if (!ok) {
|
|
631
628
|
console.error(`Cache refresh did not complete (${invalidated.kind}). The Codex model cache was not rewritten.`);
|
|
632
629
|
}
|
|
@@ -733,6 +730,15 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
733
730
|
await refreshVersionCache(channel);
|
|
734
731
|
return 0;
|
|
735
732
|
},
|
|
733
|
+
"__update-badge": async deps => {
|
|
734
|
+
if (deps.args.length !== 1) {
|
|
735
|
+
console.error("Usage: ocx __update-badge");
|
|
736
|
+
return 64;
|
|
737
|
+
}
|
|
738
|
+
const { readUpdateBadge } = await import("../update/badge");
|
|
739
|
+
console.log(JSON.stringify(readUpdateBadge()));
|
|
740
|
+
return 0;
|
|
741
|
+
},
|
|
736
742
|
"__tray-start": async deps => {
|
|
737
743
|
return (await deps.handleTrayProxyStart()) ? 0 : 1;
|
|
738
744
|
},
|
package/src/cli/effort.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
mapReasoningEffort,
|
|
8
8
|
reasoningEffortMapFor,
|
|
9
9
|
} from "../reasoning-effort";
|
|
10
|
+
import { decodeRoutedModelId } from "../providers/slug-codec";
|
|
11
|
+
import { knownModelIdsForProvider } from "../router";
|
|
10
12
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
11
13
|
import { modelInList, type OcxConfig } from "../types";
|
|
12
14
|
import {
|
|
@@ -290,19 +292,26 @@ function inspectModelEffort(modelTarget: string, wantsJson: boolean): void {
|
|
|
290
292
|
);
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
295
|
+
// A Codex-facing slug encodes the inner "/" of a namespaced native id
|
|
296
|
+
// (`command-code/deepseek-deepseek-v4.1-flash` for `deepseek/deepseek-v4.1-flash`), so the
|
|
297
|
+
// literal id only resolves against the ladder through the decode the router already uses.
|
|
298
|
+
const known = knownModelIdsForProvider(providerName, provider, config);
|
|
299
|
+
const resolvedModelId = known.includes(modelId) ? modelId : decodeRoutedModelId(modelId, known);
|
|
300
|
+
|
|
301
|
+
const isReasoningDisabled = modelInList(provider.noReasoningModels, resolvedModelId);
|
|
302
|
+
const efforts = configuredReasoningEfforts(provider, resolvedModelId);
|
|
303
|
+
const wireMap = reasoningEffortMapFor(provider, resolvedModelId);
|
|
296
304
|
|
|
297
305
|
// Derive sample ladder directly from canonical CODEX_REASONING_LEVELS (#3528 review)
|
|
298
306
|
const mappedExamples: Record<string, string | undefined> = {};
|
|
299
307
|
for (const { effort } of CODEX_REASONING_LEVELS) {
|
|
300
|
-
mappedExamples[effort] = mapReasoningEffort(provider,
|
|
308
|
+
mappedExamples[effort] = mapReasoningEffort(provider, resolvedModelId, effort);
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
const result = {
|
|
304
312
|
provider: providerName,
|
|
305
|
-
model:
|
|
313
|
+
model: resolvedModelId,
|
|
314
|
+
...(resolvedModelId !== modelId ? { requestedModel: modelId } : {}),
|
|
306
315
|
reasoningDisabled: isReasoningDisabled,
|
|
307
316
|
supportedEfforts: efforts ?? null,
|
|
308
317
|
wireMap: wireMap ?? null,
|
|
@@ -310,7 +319,8 @@ function inspectModelEffort(modelTarget: string, wantsJson: boolean): void {
|
|
|
310
319
|
};
|
|
311
320
|
|
|
312
321
|
const lines = [
|
|
313
|
-
`Reasoning effort configuration for ${providerName}/${
|
|
322
|
+
`Reasoning effort configuration for ${providerName}/${resolvedModelId}:`,
|
|
323
|
+
...(resolvedModelId !== modelId ? [` Resolved from: ${modelId}`] : []),
|
|
314
324
|
` Reasoning disabled: ${isReasoningDisabled ? "yes (noReasoningModels)" : "no"}`,
|
|
315
325
|
` Supported ladder: ${efforts ? efforts.join(", ") : "(default / unconstrained)"}`,
|
|
316
326
|
` Wire mapping overrides: ${wireMap ? JSON.stringify(wireMap) : "(standard provider mapping)"}`,
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* each external-file mutation, and use that current config for sync inputs.
|
|
10
10
|
*/
|
|
11
11
|
import { loadConfig } from "../config";
|
|
12
|
+
import { removeDesktopPickerArtifacts } from "../claude/desktop-picker";
|
|
13
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
14
|
+
import { runtimeRequest } from "./runtime-api";
|
|
12
15
|
import { stripGrokConfig, type GrokInjectResult } from "../grok/inject";
|
|
13
16
|
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } from "../claude/desktop-3p";
|
|
14
17
|
import {
|
|
15
18
|
applyDesktopFirstParty,
|
|
16
19
|
inspectDesktopFirstParty,
|
|
20
|
+
observeClaudeDesktopMode,
|
|
17
21
|
removeDesktopFirstParty,
|
|
18
22
|
resolveClaudeDesktopMode,
|
|
19
23
|
} from "../claude/desktop-first-party";
|
|
@@ -44,7 +48,11 @@ export interface EnsureDesiredIntegrationsDeps {
|
|
|
44
48
|
removeDesktopFirstParty?: typeof removeDesktopFirstParty;
|
|
45
49
|
applyDesktopFirstParty?: typeof applyDesktopFirstParty;
|
|
46
50
|
inspectDesktopFirstParty?: typeof inspectDesktopFirstParty;
|
|
51
|
+
observeClaudeDesktopMode?: typeof observeClaudeDesktopMode;
|
|
47
52
|
inspectDesktop3pConfigLibrary?: typeof inspectDesktop3pConfigLibrary;
|
|
53
|
+
findLiveProxyImpl?: typeof findLiveProxy;
|
|
54
|
+
runtimeRequestImpl?: typeof runtimeRequest;
|
|
55
|
+
removeDesktopPickerArtifacts?: typeof removeDesktopPickerArtifacts;
|
|
48
56
|
log?: (message: string) => void;
|
|
49
57
|
error?: (message: string) => void;
|
|
50
58
|
}
|
|
@@ -130,13 +138,13 @@ export async function ensureGrokFenceMatchesDesired(
|
|
|
130
138
|
* When it is ON in first-party mode, refresh a stale env (the intercept port follows the
|
|
131
139
|
* public port, so a port change would otherwise leave Claude Code pointed at a dead proxy).
|
|
132
140
|
*/
|
|
133
|
-
export function ensureClaudeDesktopMatchesDesired(
|
|
141
|
+
export async function ensureClaudeDesktopMatchesDesired(
|
|
134
142
|
deps: EnsureDesiredIntegrationsDeps = productionDeps,
|
|
135
|
-
): void {
|
|
143
|
+
): Promise<void> {
|
|
136
144
|
const config = deps.loadConfig();
|
|
137
145
|
const { log, error } = io(deps);
|
|
138
146
|
if (claudeDesktopIntegrationEnabled(config)) {
|
|
139
|
-
if (resolveClaudeDesktopMode(config) !== "first-party") return;
|
|
147
|
+
if (resolveClaudeDesktopMode(config, (deps.observeClaudeDesktopMode ?? observeClaudeDesktopMode)(config)) !== "first-party") return;
|
|
140
148
|
const library = (deps.inspectDesktop3pConfigLibrary ?? inspectDesktop3pConfigLibrary)({
|
|
141
149
|
appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
|
|
142
150
|
});
|
|
@@ -153,6 +161,23 @@ export function ensureClaudeDesktopMatchesDesired(
|
|
|
153
161
|
else if (!applied.ok) error(`⚠️ Claude Desktop first-party env refresh skipped: ${applied.reason}.`);
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
164
|
+
try {
|
|
165
|
+
const live = await (deps.findLiveProxyImpl ?? findLiveProxy)();
|
|
166
|
+
if (live) {
|
|
167
|
+
const request = deps.runtimeRequestImpl ?? runtimeRequest;
|
|
168
|
+
await request(
|
|
169
|
+
"/api/claude-desktop/picker",
|
|
170
|
+
{ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ enabled: false, persist: false }) },
|
|
171
|
+
deps.findLiveProxyImpl ? { findLiveProxy: deps.findLiveProxyImpl } : {},
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
const removed = await (deps.removeDesktopPickerArtifacts ?? removeDesktopPickerArtifacts)({});
|
|
175
|
+
if (!removed.ok) error(`⚠️ Claude Desktop picker cleanup skipped${removed.residual?.length ? `: ${removed.residual.join(", ")}` : ""}.`);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
179
|
+
error(`⚠️ Claude Desktop picker cleanup skipped: ${detail}.`);
|
|
180
|
+
}
|
|
156
181
|
try {
|
|
157
182
|
const env = (deps.removeDesktopFirstParty ?? removeDesktopFirstParty)();
|
|
158
183
|
if (env.ok && env.changed) log(" ↩️ Claude Desktop first-party env removed.");
|
|
@@ -196,5 +221,5 @@ export async function reconcileEnsureDesiredIntegrations(
|
|
|
196
221
|
liveHost ? { hostname: liveHost } : {},
|
|
197
222
|
deps,
|
|
198
223
|
);
|
|
199
|
-
ensureClaudeDesktopMatchesDesired(deps);
|
|
224
|
+
await ensureClaudeDesktopMatchesDesired(deps);
|
|
200
225
|
}
|
package/src/cli/ready.ts
CHANGED
|
@@ -199,7 +199,7 @@ export async function runReady(args: ReadyArgs, io: ReadyIo = {}): Promise<numbe
|
|
|
199
199
|
if (!args.wait) {
|
|
200
200
|
// Default: exactly one identity-checked readiness probe. No wait deadline —
|
|
201
201
|
// bounded only by the probe's own default timeout (remainingMs=undefined so
|
|
202
|
-
// the production defaults keep their built-in
|
|
202
|
+
// the production defaults keep their built-in DEFAULT_PROBE_TIMEOUT_MS ceiling; no semantic
|
|
203
203
|
// regression vs. the single-probe behavior).
|
|
204
204
|
const live = await find(undefined);
|
|
205
205
|
if (!live) {
|
package/src/cli/registry.ts
CHANGED
|
@@ -451,6 +451,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
451
451
|
" ocx claude desktop [apply] Save and apply the four-family profile",
|
|
452
452
|
" ocx claude desktop show [--json] Show routes, families, and defaults",
|
|
453
453
|
" ocx claude desktop status [--json] Show applied state, drift, and health",
|
|
454
|
+
" ocx claude desktop bind <picker-id> <route> First-party: serve a Code tab picker model with a route",
|
|
455
|
+
" ocx claude desktop unbind <picker-id> Remove a first-party binding",
|
|
454
456
|
" ocx claude desktop move <route> <family> [--default]",
|
|
455
457
|
" ocx claude desktop default <family> <route|none>",
|
|
456
458
|
" ocx claude desktop export <path|-> Export versioned JSON (`-` = stdout)",
|
|
@@ -591,6 +593,12 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
591
593
|
usage: "ocx __refresh-version [preview|latest]",
|
|
592
594
|
summary: "Hidden detached helper: refresh the cached latest version.",
|
|
593
595
|
},
|
|
596
|
+
{
|
|
597
|
+
name: "__update-badge",
|
|
598
|
+
hidden: true,
|
|
599
|
+
usage: "ocx __update-badge",
|
|
600
|
+
summary: "Hidden internal: print cached package update badge JSON.",
|
|
601
|
+
},
|
|
594
602
|
{
|
|
595
603
|
name: "__tray-start",
|
|
596
604
|
hidden: true,
|
package/src/cli/restart-scope.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
|
|
8
8
|
import type { AfterCatalogWriteAppServerResult } from "../codex/app-server-processes";
|
|
9
|
-
import type { DesktopAppRestartResult } from "../codex/desktop-app-restart";
|
|
9
|
+
import type { DesktopAppRestartIo, DesktopAppRestartResult } from "../codex/desktop-app-restart";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Which restart a command was asked for.
|
|
@@ -102,10 +102,12 @@ export async function handleRestartScopeAfterWrite(
|
|
|
102
102
|
*/
|
|
103
103
|
export async function handleDesktopAppRestart(
|
|
104
104
|
log: Pick<Console, "log" | "error">,
|
|
105
|
+
io: DesktopAppRestartIo = {},
|
|
105
106
|
): Promise<DesktopAppRestartResult> {
|
|
106
107
|
const { restartCodexDesktopApp } = await import("../codex/desktop-app-restart");
|
|
107
108
|
const { startDesktopRestartHandoff } = await import("../codex/desktop-app/handoff");
|
|
108
109
|
const result = restartCodexDesktopApp({
|
|
110
|
+
...io,
|
|
109
111
|
// The CLI is the one caller whose exit is exactly the signal the helper waits for,
|
|
110
112
|
// so it is the one caller allowed to hand off. The management service is not (it
|
|
111
113
|
// runs in a proxy that never exits) and the helper itself is not (recursion).
|
|
@@ -123,6 +125,12 @@ export async function handleDesktopAppRestart(
|
|
|
123
125
|
+ "nothing was stopped.",
|
|
124
126
|
);
|
|
125
127
|
return result;
|
|
128
|
+
case "test_environment":
|
|
129
|
+
log.error(
|
|
130
|
+
"Skipped the Codex desktop app restart: this is an armed opencodex test process "
|
|
131
|
+
+ "(OCX_TEST_HOME_GUARD=1), so the real app was not touched.",
|
|
132
|
+
);
|
|
133
|
+
return result;
|
|
126
134
|
case "restart_in_flight":
|
|
127
135
|
log.error(
|
|
128
136
|
"Another Codex desktop-app restart is already running; this one did nothing. "
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -394,6 +394,23 @@ export function printData(value: unknown, wantsJson: boolean, lines?: string[]):
|
|
|
394
394
|
else for (const line of lines) console.log(line);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Operator text for the Codex-config apply report a management write returns.
|
|
399
|
+
*
|
|
400
|
+
* The report shape is shared by every route that re-runs the injection on the spot: the Desktop
|
|
401
|
+
* switches (`ocx system settings`) and the web-search sidecar's master switch. One vocabulary for
|
|
402
|
+
* both, so the same failure cannot read as two different things depending on which command the
|
|
403
|
+
* operator used -- and because the reason codes are internal, the human line never prints them.
|
|
404
|
+
*/
|
|
405
|
+
export function desktopSwitchApplyReason(reason: unknown): string {
|
|
406
|
+
if (reason === "not_requested") return "no desktop switch rewrite was requested";
|
|
407
|
+
if (reason === "proxy_not_running") return "the proxy is not running";
|
|
408
|
+
if (reason === "integration_disabled") return "Codex integration is disabled";
|
|
409
|
+
if (reason === "write_lock_busy") return "the Codex config write lock is busy";
|
|
410
|
+
if (reason === "injection_refused") return "Codex config injection was refused";
|
|
411
|
+
return "the rewrite could not be completed";
|
|
412
|
+
}
|
|
413
|
+
|
|
397
414
|
/**
|
|
398
415
|
* Render untrusted diagnostic text without letting it control the operator's terminal. Catalog
|
|
399
416
|
* values are hub-supplied and surface on more than one CLI path -- first-time `ocx connect` and the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CliUsageError,
|
|
3
|
+
desktopSwitchApplyReason,
|
|
3
4
|
printData,
|
|
4
5
|
rejectArgs,
|
|
5
6
|
runCliAction,
|
|
@@ -56,15 +57,6 @@ function desktopSwitchInertReason(reason: unknown): string {
|
|
|
56
57
|
return "the stored setting is not effective in the current runtime configuration";
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function desktopSwitchApplyReason(reason: unknown): string {
|
|
60
|
-
if (reason === "not_requested") return "no desktop switch rewrite was requested";
|
|
61
|
-
if (reason === "proxy_not_running") return "the proxy is not running";
|
|
62
|
-
if (reason === "integration_disabled") return "Codex integration is disabled";
|
|
63
|
-
if (reason === "write_lock_busy") return "the Codex config write lock is busy";
|
|
64
|
-
if (reason === "injection_refused") return "Codex config injection was refused";
|
|
65
|
-
return "the rewrite could not be completed";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
60
|
function settingsUpdateLines(
|
|
69
61
|
result: unknown,
|
|
70
62
|
changed: { desktopAuthless: boolean; clientCompaction: boolean },
|
|
@@ -202,13 +194,20 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
|
|
|
202
194
|
printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
|
|
203
195
|
} else if (sub === "codex-restart") {
|
|
204
196
|
// --yes required: this fully quits and relaunches the user's Codex desktop app as well as
|
|
205
|
-
// restarting app-servers
|
|
197
|
+
// restarting app-servers, which can discard unsaved drafts, selections, and approval prompts.
|
|
206
198
|
const args = [...rest];
|
|
207
199
|
const wantsJson = takeFlag(args, "--json");
|
|
208
200
|
const yes = takeFlag(args, "--yes");
|
|
209
|
-
if (!yes) throw new CliUsageError(
|
|
201
|
+
if (!yes) throw new CliUsageError(
|
|
202
|
+
"system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost; it also restarts the app-servers",
|
|
203
|
+
USAGE,
|
|
204
|
+
);
|
|
210
205
|
rejectArgs(args, USAGE);
|
|
211
|
-
printData(
|
|
206
|
+
printData(
|
|
207
|
+
await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps),
|
|
208
|
+
wantsJson,
|
|
209
|
+
["Codex desktop app and app-server restart requested. Unsaved composer drafts, model-picker selections, and pending approval prompts may be lost."],
|
|
210
|
+
);
|
|
212
211
|
} else if (sub === "update") await update(rest, deps);
|
|
213
212
|
else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
|
|
214
213
|
});
|
package/src/client/connect.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
replaceServiceApiTokenFile,
|
|
38
38
|
restoreTokenBackup,
|
|
39
39
|
serviceApiTokenBackupPath,
|
|
40
|
+
serviceApiTokenFingerprint,
|
|
40
41
|
writeTokenBackup,
|
|
41
42
|
writeServiceApiTokenFile,
|
|
42
43
|
} from "../lib/service-secrets";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
clearClientConnection,
|
|
66
67
|
commitClientConnection,
|
|
67
68
|
readClientConnectionState,
|
|
69
|
+
markClientConnectPending, clearClientConnectPending, pendingClientConnectMayOwnToken,
|
|
68
70
|
assertNoClientDisconnectPending, assertClientConnectionUnchanged, sameClientConnectionOwner,
|
|
69
71
|
} from "./state";
|
|
70
72
|
import { assertClientCatalogCompatible, type CatalogCompatibilityDeps } from "./catalog-compatibility";
|
|
@@ -492,6 +494,7 @@ function assertConnectingState(expectedTokenFingerprint?: string): void {
|
|
|
492
494
|
}
|
|
493
495
|
}
|
|
494
496
|
|
|
497
|
+
/** Enroll a client key, keeping its pending ownership visible until commit or rollback. */
|
|
495
498
|
export async function connectClient(
|
|
496
499
|
options: ConnectOptions,
|
|
497
500
|
deps: ClientConnectDeps = {},
|
|
@@ -501,6 +504,7 @@ export async function connectClient(
|
|
|
501
504
|
let issued: IssuedClientKey | null = null;
|
|
502
505
|
let cleanupCredential: { kind: "admin"; value: Uint8Array } | { kind: "gui-session"; value: ConnectGuiSession } | null = null;
|
|
503
506
|
let tokenFingerprint: string | null = null;
|
|
507
|
+
let pendingConnectFingerprint: string | null = null;
|
|
504
508
|
let priorCatalog: CatalogSnapshot | null = null;
|
|
505
509
|
let writtenCatalogFingerprint: string | null = null;
|
|
506
510
|
let injectionCommitted = false;
|
|
@@ -536,6 +540,9 @@ export async function connectClient(
|
|
|
536
540
|
|
|
537
541
|
const initialFiles = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
538
542
|
assertConnectingState();
|
|
543
|
+
const fingerprint = serviceApiTokenFingerprint(issued!.key);
|
|
544
|
+
markClientConnectPending(fingerprint);
|
|
545
|
+
pendingConnectFingerprint = fingerprint;
|
|
539
546
|
return { prior: catalogSnapshot(), persisted: writeServiceApiTokenFile(issued!.key) };
|
|
540
547
|
}), deps.lifecycleLockDeps);
|
|
541
548
|
priorCatalog = initialFiles.prior;
|
|
@@ -602,6 +609,7 @@ export async function connectClient(
|
|
|
602
609
|
};
|
|
603
610
|
withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
604
611
|
assertConnectingState(persisted.fingerprint);
|
|
612
|
+
clearClientConnectPending(persisted.fingerprint);
|
|
605
613
|
commitClientConnection(connection);
|
|
606
614
|
committed = true;
|
|
607
615
|
}), deps.lifecycleLockDeps);
|
|
@@ -618,9 +626,11 @@ export async function connectClient(
|
|
|
618
626
|
if (priorCatalog && writtenCatalogFingerprint && !restoreCatalogSnapshot(priorCatalog, writtenCatalogFingerprint)) {
|
|
619
627
|
rollbackFailures.push("catalog rollback did not match the written artifact");
|
|
620
628
|
}
|
|
621
|
-
if (
|
|
622
|
-
const removed = removeServiceApiTokenFileIfOwned(
|
|
629
|
+
if (pendingConnectFingerprint) {
|
|
630
|
+
const removed = removeServiceApiTokenFileIfOwned(pendingConnectFingerprint);
|
|
623
631
|
if (removed === "changed") rollbackFailures.push("service token changed during rollback");
|
|
632
|
+
// Final commit may fail after this attempt already cleared its marker under the same lock.
|
|
633
|
+
else if (pendingClientConnectMayOwnToken(pendingConnectFingerprint)) clearClientConnectPending(pendingConnectFingerprint);
|
|
624
634
|
}
|
|
625
635
|
}), deps.lifecycleLockDeps);
|
|
626
636
|
} catch { rollbackFailures.push("client cleanup ownership unavailable"); }
|
package/src/client/state.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import {
|
|
4
|
+
getConfigDir,
|
|
3
5
|
getConfigPath,
|
|
4
6
|
deleteConfigTopLevelKey,
|
|
5
7
|
getDefaultConfig,
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
saveConfig,
|
|
9
11
|
withConfigMutationLockSync,
|
|
10
12
|
} from "../config";
|
|
13
|
+
import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
|
|
11
14
|
import type { OcxClientConnectionConfig } from "../types";
|
|
12
15
|
import { inspectRemoteDesktopStore, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
|
|
13
16
|
import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "./lifecycle-lock";
|
|
@@ -23,6 +26,41 @@ export type ClientConnectionState =
|
|
|
23
26
|
| { kind: "invalid"; reason: string }
|
|
24
27
|
| { kind: "mismatched"; reason: string };
|
|
25
28
|
|
|
29
|
+
const pendingConnectPath = (): string => join(getConfigDir(), "client-connect-pending");
|
|
30
|
+
|
|
31
|
+
/** Validate pending ownership; an optional fingerprint restricts it to that exact key. */
|
|
32
|
+
export function pendingClientConnectMayOwnToken(fingerprint?: string): boolean {
|
|
33
|
+
const path = pendingConnectPath();
|
|
34
|
+
let stat;
|
|
35
|
+
try { stat = lstatSync(path); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== 65) {
|
|
41
|
+
throw new Error("pending client connection owner is unsafe");
|
|
42
|
+
}
|
|
43
|
+
const marker = readFileSync(path, "utf8");
|
|
44
|
+
if (!/^[a-f0-9]{64}\n$/.test(marker)) throw new Error("pending client connection owner is malformed");
|
|
45
|
+
return fingerprint === undefined || marker === `${fingerprint}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Publish only the token fingerprint, before the key file, under the client lifecycle lock. */
|
|
49
|
+
export function markClientConnectPending(fingerprint: string): void {
|
|
50
|
+
if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw new Error("invalid pending client fingerprint");
|
|
51
|
+
atomicWriteFileNoFollowUnclaimed(pendingConnectPath(), `${fingerprint}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Clear only the marker for this connect attempt while the client lifecycle lock is held. */
|
|
55
|
+
export function clearClientConnectPending(fingerprint: string): void {
|
|
56
|
+
const path = pendingConnectPath();
|
|
57
|
+
const stat = lstatSync(path);
|
|
58
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== 65 || readFileSync(path, "utf8") !== `${fingerprint}\n`) {
|
|
59
|
+
throw new Error("pending client connection owner changed");
|
|
60
|
+
}
|
|
61
|
+
unlinkSync(path);
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
export type ClientRotationRecoveryGate =
|
|
27
65
|
| { kind: "clean" }
|
|
28
66
|
| { kind: "orphan-cleaned" }
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* targeting it is the caller's explicit act.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
24
24
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
@@ -556,7 +556,17 @@ export function omoConfigPath(env: OpencodeLaunchEnv = process.env, home: string
|
|
|
556
556
|
* client-owned override to mirror, and this registry does not invent one.
|
|
557
557
|
*/
|
|
558
558
|
export function asideHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
559
|
-
|
|
559
|
+
const root = join(home, ".aside");
|
|
560
|
+
// A user who relocated Aside (for example to an external volume) leaves ~/.aside as a
|
|
561
|
+
// symlink, and Aside itself follows it (issue 5648). Canonicalize only that top-level
|
|
562
|
+
// alias, once, and only onto a directory: every boundary below the root (u/, account
|
|
563
|
+
// directories, models.json) keeps refusing links against the canonical path.
|
|
564
|
+
try {
|
|
565
|
+
if (lstatSync(root).isSymbolicLink() && statSync(root).isDirectory()) return realpathSync.native(root);
|
|
566
|
+
} catch {
|
|
567
|
+
// Missing or unreadable: the literal path lets the profile reader report it.
|
|
568
|
+
}
|
|
569
|
+
return root;
|
|
560
570
|
}
|
|
561
571
|
|
|
562
572
|
/**
|
|
@@ -812,10 +822,34 @@ export interface PiProviderBlock {
|
|
|
812
822
|
baseUrl: string;
|
|
813
823
|
api: string;
|
|
814
824
|
apiKey: string;
|
|
815
|
-
compat?:
|
|
825
|
+
compat?: PiProviderCompat;
|
|
816
826
|
models: PiModelEntry[];
|
|
817
827
|
}
|
|
818
828
|
|
|
829
|
+
/**
|
|
830
|
+
* The subset of Pi's per-provider `compat` block this export writes. Both keys are part of
|
|
831
|
+
* Pi's own model-config schema; an unknown key there would empty the whole config, so nothing
|
|
832
|
+
* outside this set is ever emitted.
|
|
833
|
+
*/
|
|
834
|
+
export interface PiProviderCompat {
|
|
835
|
+
sendSessionAffinityHeaders?: boolean;
|
|
836
|
+
supportsDeveloperRole?: boolean;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
interface PiExportOptions {
|
|
840
|
+
sendSessionAffinityHeaders?: boolean;
|
|
841
|
+
/**
|
|
842
|
+
* Tell Pi to send its system prompt as `system` rather than `developer` (#5664).
|
|
843
|
+
*
|
|
844
|
+
* Pi sends `developer` for reasoning models by default. On `/v1/chat/completions` the proxy
|
|
845
|
+
* forwards the caller's roles verbatim unless a destination has recorded
|
|
846
|
+
* `foldDeveloperRoleToSystem`, and many OpenAI-compatible upstreams reject `developer` with a
|
|
847
|
+
* 400. `system` is accepted by every destination behind this one provider block, so the export
|
|
848
|
+
* states it rather than leaving each user to hand-edit a block the next export rewrites.
|
|
849
|
+
*/
|
|
850
|
+
foldDeveloperRole?: boolean;
|
|
851
|
+
}
|
|
852
|
+
|
|
819
853
|
export interface PiGeneratedConfig {
|
|
820
854
|
providers: Record<string, PiProviderBlock>;
|
|
821
855
|
}
|
|
@@ -829,6 +863,8 @@ export interface HermesProviderBlock {
|
|
|
829
863
|
api: string;
|
|
830
864
|
api_key: string;
|
|
831
865
|
api_mode: "chat_completions";
|
|
866
|
+
/** Header name only; Hermes supplies a dynamic per-conversation value. */
|
|
867
|
+
session_affinity_header: "session-id";
|
|
832
868
|
/** We supply the list, so skip their live `/models` probe. */
|
|
833
869
|
discover_models: false;
|
|
834
870
|
models: Record<string, HermesModelEntry>;
|
|
@@ -935,7 +971,7 @@ export interface GajaeGeneratedConfig {
|
|
|
935
971
|
* model. The rest of this contract (omitting `cost`) is still ours rather than
|
|
936
972
|
* a claim about Pi's acceptance.
|
|
937
973
|
*/
|
|
938
|
-
function buildPiClientConfig(ctx: ExportContext,
|
|
974
|
+
function buildPiClientConfig(ctx: ExportContext, options: PiExportOptions = {}): PiGeneratedConfig {
|
|
939
975
|
const models: PiModelEntry[] = [];
|
|
940
976
|
for (const model of normalizeExportModels(ctx.models)) {
|
|
941
977
|
// Text is the one modality every routed model supports; anything richer must come
|
|
@@ -972,19 +1008,30 @@ function buildPiClientConfig(ctx: ExportContext, sendSessionAffinityHeaders = fa
|
|
|
972
1008
|
}
|
|
973
1009
|
models.push(entry);
|
|
974
1010
|
}
|
|
1011
|
+
const compat: PiProviderCompat = {
|
|
1012
|
+
...(options.sendSessionAffinityHeaders ? { sendSessionAffinityHeaders: true } : {}),
|
|
1013
|
+
...(options.foldDeveloperRole ? { supportsDeveloperRole: false } : {}),
|
|
1014
|
+
};
|
|
975
1015
|
return {
|
|
976
1016
|
providers: {
|
|
977
1017
|
[OPENCODE_PROVIDER_ID]: {
|
|
978
1018
|
baseUrl: ctx.baseUrl,
|
|
979
1019
|
api: PI_API_DIALECT,
|
|
980
1020
|
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
981
|
-
...(
|
|
1021
|
+
...(Object.keys(compat).length > 0 ? { compat } : {}),
|
|
982
1022
|
models,
|
|
983
1023
|
},
|
|
984
1024
|
},
|
|
985
1025
|
};
|
|
986
1026
|
}
|
|
987
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Pi's export options, shared by `ocx export --client pi` and the managed contribution so the two
|
|
1030
|
+
* never drift apart at the first refresh. omo uses the same options: senpi documents both keys in
|
|
1031
|
+
* its models.json `compat` block (docs/models.md, docs/custom-provider.md).
|
|
1032
|
+
*/
|
|
1033
|
+
const PI_EXPORT_OPTIONS: PiExportOptions = { sendSessionAffinityHeaders: true, foldDeveloperRole: true };
|
|
1034
|
+
|
|
988
1035
|
/** Do not let provider-controlled catalog text become an environment lookup. */
|
|
989
1036
|
function containsEnvInterpolation(value: string): boolean {
|
|
990
1037
|
return value.includes("${");
|
|
@@ -1006,6 +1053,7 @@ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
|
|
|
1006
1053
|
api: ctx.baseUrl,
|
|
1007
1054
|
api_key: HERMES_API_KEY_ENV_REF,
|
|
1008
1055
|
api_mode: "chat_completions",
|
|
1056
|
+
session_affinity_header: "session-id",
|
|
1009
1057
|
discover_models: false,
|
|
1010
1058
|
models,
|
|
1011
1059
|
...(headers ? { extra_headers: headers } : {}),
|
|
@@ -1162,7 +1210,7 @@ function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1162
1210
|
}
|
|
1163
1211
|
|
|
1164
1212
|
function buildPiContribution(ctx: ExportContext): ManagedContribution {
|
|
1165
|
-
const doc = buildPiClientConfig(ctx,
|
|
1213
|
+
const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
|
|
1166
1214
|
return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1167
1215
|
}
|
|
1168
1216
|
|
|
@@ -1254,7 +1302,7 @@ function buildAsideContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1254
1302
|
* the two would drift apart at the first refresh.
|
|
1255
1303
|
*/
|
|
1256
1304
|
function buildOmoContribution(ctx: ExportContext): ManagedContribution {
|
|
1257
|
-
const doc = buildPiClientConfig(ctx,
|
|
1305
|
+
const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
|
|
1258
1306
|
return singleFragment("omo", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1259
1307
|
}
|
|
1260
1308
|
|
|
@@ -1296,7 +1344,7 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1296
1344
|
destination: env => piConfigPath(env),
|
|
1297
1345
|
apiKeyEnv: "",
|
|
1298
1346
|
exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
1299
|
-
build: ctx => buildPiClientConfig(ctx,
|
|
1347
|
+
build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
|
|
1300
1348
|
format: "json",
|
|
1301
1349
|
summarize: summarizePi,
|
|
1302
1350
|
buildContribution: buildPiContribution,
|
|
@@ -1479,7 +1527,7 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1479
1527
|
destination: env => omoConfigPath(env),
|
|
1480
1528
|
apiKeyEnv: "",
|
|
1481
1529
|
exportHint: "omo reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
1482
|
-
build: ctx => buildPiClientConfig(ctx,
|
|
1530
|
+
build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
|
|
1483
1531
|
format: "json",
|
|
1484
1532
|
summarize: summarizePi,
|
|
1485
1533
|
buildContribution: buildOmoContribution,
|