@bitkyc08/opencodex 2.90.0 → 2.92.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/bin/ocx.mjs +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
package/src/cli/registry.ts
CHANGED
|
@@ -150,7 +150,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: "link",
|
|
153
|
-
usage: "ocx link <port|issue|status|revoke|reconnect>",
|
|
153
|
+
usage: "ocx link <port|issue|status|revoke|reconnect|cleanup>",
|
|
154
154
|
summary: "Allocate and manage a loopback remote home link.",
|
|
155
155
|
details: [
|
|
156
156
|
"Port: ocx link port [--json]",
|
|
@@ -158,6 +158,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
158
158
|
"Status: ocx link status [--json]",
|
|
159
159
|
"Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
|
|
160
160
|
"Reconnect: ocx link reconnect --link-id <id> [--json]",
|
|
161
|
+
"Cleanup: ocx link cleanup [--link-id <id>] [--json]",
|
|
161
162
|
"Issue, status, revoke, and reconnect use the running proxy's loopback management API and admin token.",
|
|
162
163
|
],
|
|
163
164
|
},
|
|
@@ -434,6 +435,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
434
435
|
"Works both online (via live proxy API) and offline (modifies persisted config safely with atomic writes).",
|
|
435
436
|
],
|
|
436
437
|
},
|
|
438
|
+
{
|
|
439
|
+
name: "fallback",
|
|
440
|
+
usage: "ocx fallback <status|off|on|clear|local> [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
|
|
441
|
+
summary: "Turn model and provider fallback off or on per scope, and record per-device local fallback.",
|
|
442
|
+
details: [
|
|
443
|
+
"A refused fallback answers fallback_refused with the status of the failure that triggered it, and names the command that allows it.",
|
|
444
|
+
"Precedence: a device set to off always refuses; otherwise route, then provider, then global; unset allows.",
|
|
445
|
+
"Rotation among the accounts or keys of one provider pool is not a fallback and is not governed here.",
|
|
446
|
+
"Live proxy only. On a Remote Link Child, status reads the Home; writes are refused. Run write commands on the Home.",
|
|
447
|
+
],
|
|
448
|
+
},
|
|
437
449
|
{
|
|
438
450
|
name: "agent",
|
|
439
451
|
usage: "ocx agent <status|injection|effort|subagents|fallback|sidecar> ...",
|
|
@@ -16,6 +16,10 @@ export function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: f
|
|
|
16
16
|
console.error("The proxy home or lifecycle ownership could not be verified. Run `ocx status`; if a service owns the proxy, use its installation's `ocx service restart`, otherwise use the owning app or supervisor. No changes were made.");
|
|
17
17
|
} else if (code === "restart_package_tree_unsettled") {
|
|
18
18
|
console.error(`The proxy's package files are unsettled. Wait for the install to finish, then run \`ocx restart\`. ${INCOMPLETE_INSTALL_RECOVERY}`);
|
|
19
|
+
} else if (code === "restart_supervised_service") {
|
|
20
|
+
console.error("Restart not performed; the running client runtime was kept. Its background service owns it: run `ocx service restart`.");
|
|
21
|
+
} else if (code === "restart_supervised_desktop") {
|
|
22
|
+
console.error("Restart not performed; the running client runtime was kept. The OpenCodex desktop app owns it: restart it from the app.");
|
|
19
23
|
} else {
|
|
20
24
|
console.error("Proxy restart request could not be confirmed; no fallback stop/start was attempted. Run `ocx status` to inspect ownership and runtime state.");
|
|
21
25
|
}
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -202,7 +202,7 @@ export async function runtimeRequest<T = unknown>(
|
|
|
202
202
|
if (!linkChildReadable(method, path)) {
|
|
203
203
|
throw new RuntimeApiError(linkChildRefusal(link, `${method} ${pathname}`), 503, null, "link_child_management_unavailable");
|
|
204
204
|
}
|
|
205
|
-
const { status, body } = await readLinkHomeJson(link, path, deps.linkHome);
|
|
205
|
+
const { status, body } = await readLinkHomeJson(link, path, deps.linkHome, init.signal ? { signal: init.signal } : {});
|
|
206
206
|
if (status < 200 || status >= 300) throw new RuntimeApiError(responseMessage(body, status), status, body);
|
|
207
207
|
return body as T;
|
|
208
208
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OCX-6 fields of `ocx status --json`: which Codex settings this machine applied, when, and
|
|
3
|
+
* when its client runtime started.
|
|
4
|
+
*
|
|
5
|
+
* - `appliedCodexSettingsDigest` (any device): the digest of the values this machine applies. On a
|
|
6
|
+
* Remote Link Child that is its apply record (the Home's values its last sync wrote); on a Home or
|
|
7
|
+
* standalone install, its own projection, which is also exactly what it serves its Children. Null
|
|
8
|
+
* on a hub client and on a Child that has no apply record yet.
|
|
9
|
+
* - `lastApplyAt` (Child): epoch ms of the last successful apply of a different Home settings
|
|
10
|
+
* document; null elsewhere.
|
|
11
|
+
* - `runtimeStartedAt` (Child): epoch ms the live client runtime reports as its start; null when no
|
|
12
|
+
* client runtime answers.
|
|
13
|
+
*/
|
|
14
|
+
import type { LiveProxy } from "../server/proxy-liveness";
|
|
15
|
+
import type { OcxConfig } from "../types";
|
|
16
|
+
|
|
17
|
+
export interface CliAppliedCodexSettingsStatus {
|
|
18
|
+
appliedCodexSettingsDigest: string | null;
|
|
19
|
+
lastApplyAt: number | null;
|
|
20
|
+
runtimeStartedAt: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AppliedCodexSettingsStatusDeps {
|
|
24
|
+
readStartedAt?: (live: LiveProxy) => Promise<number | null>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function collectAppliedCodexSettingsStatus(
|
|
28
|
+
config: OcxConfig,
|
|
29
|
+
live: LiveProxy | null,
|
|
30
|
+
deps: AppliedCodexSettingsStatusDeps = {},
|
|
31
|
+
): Promise<CliAppliedCodexSettingsStatus> {
|
|
32
|
+
const { codexSettingsDigest, projectHomeCodexSettings } = await import("../link/codex-settings");
|
|
33
|
+
if (config.runtimeRole !== "client") {
|
|
34
|
+
return {
|
|
35
|
+
appliedCodexSettingsDigest: codexSettingsDigest(projectHomeCodexSettings(config, "0.0.0").settings),
|
|
36
|
+
lastApplyAt: null,
|
|
37
|
+
runtimeStartedAt: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const linkId = config.client?.transport === "link" ? config.client.link?.linkId : undefined;
|
|
41
|
+
if (!linkId) return { appliedCodexSettingsDigest: null, lastApplyAt: null, runtimeStartedAt: null };
|
|
42
|
+
const { readHomeCodexSettingsRecord } = await import("../client/link-home");
|
|
43
|
+
const record = readHomeCodexSettingsRecord(linkId);
|
|
44
|
+
const appliedAt = record ? Date.parse(record.appliedAt) : Number.NaN;
|
|
45
|
+
let runtimeStartedAt: number | null = null;
|
|
46
|
+
if (live?.role === "client") {
|
|
47
|
+
const read = deps.readStartedAt ?? (async (target: LiveProxy) =>
|
|
48
|
+
(await import("./link-apply-restart")).readClientRuntimeStartedAt(target));
|
|
49
|
+
runtimeStartedAt = await read(live);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
appliedCodexSettingsDigest: record ? codexSettingsDigest(record.document.settings) : null,
|
|
53
|
+
lastApplyAt: Number.isFinite(appliedAt) ? appliedAt : null,
|
|
54
|
+
runtimeStartedAt,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx status --json` `fallback` (OCX-11). The live proxy's `GET /api/fallback` when it answers
|
|
3
|
+
* (on a Remote Link Child that read is the Home's, over the link); otherwise the policy from this
|
|
4
|
+
* machine's config with no events, marked `source: "local"`. Never fails the status report.
|
|
5
|
+
*/
|
|
6
|
+
import type { OcxConfig } from "../types";
|
|
7
|
+
import { buildFallbackState, type FallbackStateView } from "../routing/fallback-state";
|
|
8
|
+
import { runtimeRequest, type RuntimeApiDeps } from "./runtime-api";
|
|
9
|
+
|
|
10
|
+
export type StatusFallback = (FallbackStateView | ReturnType<typeof buildFallbackState>) & { source: "live" | "home" | "local" };
|
|
11
|
+
|
|
12
|
+
const STATUS_FALLBACK_TIMEOUT_MS = 2_000;
|
|
13
|
+
|
|
14
|
+
export async function collectStatusFallback(
|
|
15
|
+
config: Pick<OcxConfig, "fallback">,
|
|
16
|
+
options: { live: boolean; linkChild: boolean; deps?: RuntimeApiDeps },
|
|
17
|
+
): Promise<StatusFallback> {
|
|
18
|
+
if (options.live) {
|
|
19
|
+
try {
|
|
20
|
+
const state = await runtimeRequest<FallbackStateView>("/api/fallback", {
|
|
21
|
+
signal: AbortSignal.timeout(STATUS_FALLBACK_TIMEOUT_MS),
|
|
22
|
+
}, options.deps ?? {});
|
|
23
|
+
if (state && typeof state === "object" && state.schema === "ocx.fallback/1") {
|
|
24
|
+
return { ...state, source: options.linkChild ? "home" : "live" };
|
|
25
|
+
}
|
|
26
|
+
} catch { /* an older or unreachable proxy: report the local policy instead */ }
|
|
27
|
+
}
|
|
28
|
+
return { ...buildFallbackState(options.linkChild ? {} : config), source: "local" };
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { detectInstallOwnership, type InstallOwnership } from "../update";
|
|
2
|
+
|
|
3
|
+
/** How this ocx is installed, from the updater's own ownership detector. */
|
|
4
|
+
export type InstallMethod = "bun" | "npm" | "pnpm" | "mise" | "source" | "standalone" | "unknown";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ocx status --json` `install`. Additive within `schemaVersion: 1`.
|
|
8
|
+
*
|
|
9
|
+
* The verdict is the one `ocx update` acts on (`src/update/install-detection.mjs`), read
|
|
10
|
+
* from the running package's own location — PATH is never consulted, because a machine can
|
|
11
|
+
* carry several package managers at once. `evidence` names what decided it without adding
|
|
12
|
+
* a path: the package layout, mise's backend record (tool alias and backend only), the
|
|
13
|
+
* compiled standalone binary, or nothing when detection itself failed.
|
|
14
|
+
*/
|
|
15
|
+
export type InstallDetectionStatus = {
|
|
16
|
+
method: InstallMethod;
|
|
17
|
+
evidence: {
|
|
18
|
+
basis: "package-layout" | "mise-backend-record" | "standalone-binary" | "unavailable";
|
|
19
|
+
mise?: { tool: string; backend: string };
|
|
20
|
+
error?: "metadata_unreadable" | "metadata_inconsistent" | "detection_failed";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function installDetectionStatus(
|
|
25
|
+
detect: () => InstallOwnership = detectInstallOwnership,
|
|
26
|
+
): InstallDetectionStatus {
|
|
27
|
+
let ownership: InstallOwnership;
|
|
28
|
+
try {
|
|
29
|
+
ownership = detect();
|
|
30
|
+
} catch {
|
|
31
|
+
return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
|
|
32
|
+
}
|
|
33
|
+
switch (ownership.installer) {
|
|
34
|
+
case "standalone":
|
|
35
|
+
return { method: "standalone", evidence: { basis: "standalone-binary" } };
|
|
36
|
+
case "mise":
|
|
37
|
+
if (ownership.owner) {
|
|
38
|
+
return {
|
|
39
|
+
method: "mise",
|
|
40
|
+
evidence: { basis: "mise-backend-record", mise: { tool: ownership.owner.tool, backend: ownership.owner.backend } },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { method: "mise", evidence: { basis: "mise-backend-record", error: ownership.error ?? "metadata_unreadable" } };
|
|
44
|
+
case "bun":
|
|
45
|
+
case "npm":
|
|
46
|
+
case "pnpm":
|
|
47
|
+
case "source":
|
|
48
|
+
return { method: ownership.installer, evidence: { basis: "package-layout" } };
|
|
49
|
+
default:
|
|
50
|
+
return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type { ListenTarget } from "./status-probes";
|
|
|
36
36
|
import { checkProxyHealth, probeUncleanExitState, type ListenTarget } from "./status-probes";
|
|
37
37
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
38
38
|
import { fetchBoundLocalManagementRead } from "../server/local-management-read-client";
|
|
39
|
+
import { collectAppliedCodexSettingsStatus, type CliAppliedCodexSettingsStatus } from "./status-codex-settings";
|
|
40
|
+
import { installDetectionStatus, type InstallDetectionStatus } from "./status-install";
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* The state of the data-plane admission secret the SERVICE will use. State only -- never the value.
|
|
@@ -118,6 +120,8 @@ export type CliRemoteHubStatus = {
|
|
|
118
120
|
|
|
119
121
|
export type CliStatusJson = {
|
|
120
122
|
mainAccountHardLock?: CodexMainAccountPolicyHealth;
|
|
123
|
+
/** OCX-11 fallback state; see src/cli/status-fallback.ts. */
|
|
124
|
+
fallback?: import("./status-fallback").StatusFallback;
|
|
121
125
|
schemaVersion: 1;
|
|
122
126
|
/**
|
|
123
127
|
* This machine's topology role, named rather than inferred (#4236).
|
|
@@ -229,8 +233,10 @@ export type CliStatusJson = {
|
|
|
229
233
|
*/
|
|
230
234
|
versionSkew: VersionSkew;
|
|
231
235
|
cliCommand: CliPathDiagnostics;
|
|
236
|
+
/** Installer ownership from the updater's detector (OCX-10); additive, `schemaVersion` stays 1. */
|
|
237
|
+
install: InstallDetectionStatus;
|
|
232
238
|
startupSource: "live" | "local" | "local-supervision-override";
|
|
233
|
-
};
|
|
239
|
+
} & CliAppliedCodexSettingsStatus;
|
|
234
240
|
|
|
235
241
|
export type CliStatusView = {
|
|
236
242
|
json: CliStatusJson;
|
|
@@ -853,12 +859,22 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
853
859
|
const mainAccountHardLock = options.mainAccountPolicy && live
|
|
854
860
|
? (await (await import("../oauth/health")).fetchCodexHealthFromLiveProxy(undefined, async () => live)).mainAccountHardLock
|
|
855
861
|
: undefined;
|
|
862
|
+
// Same `--json`-only gate as the hard-lock read above: the human report stays one probe.
|
|
863
|
+
const fallback = options.mainAccountPolicy
|
|
864
|
+
? await (await import("./status-fallback")).collectStatusFallback(config, {
|
|
865
|
+
live: Boolean(live), linkChild: config.client?.transport === "link",
|
|
866
|
+
deps: { findLiveProxy: async () => live },
|
|
867
|
+
})
|
|
868
|
+
: undefined;
|
|
856
869
|
const pidFile = readPid();
|
|
857
870
|
// Preserve an authoritative null from orphan/legacy liveness — do not restore pidFile.
|
|
858
871
|
const pid = resolveStatusPid(live, pidFile);
|
|
859
872
|
// No extra request: findLiveProxy's identity probe already parsed and validated the
|
|
860
873
|
// healthz body, so the version came back with the liveness result.
|
|
861
874
|
const versionSkew = computeVersionSkew(packageVersion(), live?.version);
|
|
875
|
+
// OCX-6: what this machine applied and, on a Child, when its runtime started. Additive and
|
|
876
|
+
// always present (null when unknown), so `schemaVersion` stays 1.
|
|
877
|
+
const appliedCodexSettings = await collectAppliedCodexSettingsStatus(config, live);
|
|
862
878
|
const listen = live
|
|
863
879
|
? {
|
|
864
880
|
port: live.port,
|
|
@@ -1005,6 +1021,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
1005
1021
|
healthLabel: formatCliStatusHealthLabel(health.label, cliCommand, remoteHub.connected),
|
|
1006
1022
|
json: {
|
|
1007
1023
|
...(mainAccountHardLock ? { mainAccountHardLock } : {}),
|
|
1024
|
+
...(fallback ? { fallback } : {}),
|
|
1008
1025
|
schemaVersion: 1,
|
|
1009
1026
|
runtimeRole: config.runtimeRole ?? "standalone",
|
|
1010
1027
|
proxy: {
|
|
@@ -1067,7 +1084,9 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
1067
1084
|
// print it under the wrong heading (#2701).
|
|
1068
1085
|
versionSkew,
|
|
1069
1086
|
cliCommand,
|
|
1087
|
+
install: installDetectionStatus(),
|
|
1070
1088
|
startupSource,
|
|
1089
|
+
...appliedCodexSettings,
|
|
1071
1090
|
},
|
|
1072
1091
|
};
|
|
1073
1092
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readRuntimePort } from "../config/process-state";
|
|
2
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
2
3
|
import {
|
|
3
4
|
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
4
5
|
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
@@ -6,6 +7,8 @@ import {
|
|
|
6
7
|
verifyLocalAttestationProof,
|
|
7
8
|
} from "../lib/local-management-attestation";
|
|
8
9
|
import {
|
|
10
|
+
CLIENT_RESTART_REQUEST_MIN_REMAINING_MS,
|
|
11
|
+
RESTART_SUPERVISED_ERROR,
|
|
9
12
|
SYSTEM_RESTART_CAPABILITY_HEADER,
|
|
10
13
|
SYSTEM_RESTART_CAPABILITY_VERSION,
|
|
11
14
|
SYSTEM_RESTART_EXPECTED_PID_HEADER,
|
|
@@ -182,6 +185,13 @@ export async function requestBoundSystemRestart(
|
|
|
182
185
|
);
|
|
183
186
|
if (!capability) return rejected("restart_capability_unavailable");
|
|
184
187
|
|
|
188
|
+
// A connected client runtime keeps serving when its replacement fails, but its rollback takes
|
|
189
|
+
// up to CLIENT_RESTART_BUDGET_MS. Ask only while that still ends before this CLI stops observing,
|
|
190
|
+
// or the recovery start below could race the rollback for the port (OCX-6).
|
|
191
|
+
if (body.role === "client" && deadlineAt - now() < CLIENT_RESTART_REQUEST_MIN_REMAINING_MS) {
|
|
192
|
+
return rejected("restart_deadline_expired");
|
|
193
|
+
}
|
|
194
|
+
|
|
185
195
|
const requestBudget = remaining(deadlineAt, now, SYSTEM_RESTART_REQUEST_TIMEOUT_MS);
|
|
186
196
|
if (requestBudget <= 0) return rejected("restart_deadline_expired");
|
|
187
197
|
try {
|
|
@@ -194,10 +204,31 @@ export async function requestBoundSystemRestart(
|
|
|
194
204
|
},
|
|
195
205
|
signal: AbortSignal.timeout(requestBudget),
|
|
196
206
|
});
|
|
197
|
-
|
|
207
|
+
if (response.ok) return { accepted: true };
|
|
208
|
+
if (response.status === 409) {
|
|
209
|
+
const supervisor = await supervisedRefusal(response);
|
|
210
|
+
if (supervisor) return rejected(`${RESTART_SUPERVISED_ERROR}_${supervisor}`);
|
|
211
|
+
}
|
|
212
|
+
return rejected(`restart_request_http_${response.status}`);
|
|
198
213
|
} catch {
|
|
199
214
|
// The server may have accepted the restart before the response connection failed.
|
|
200
215
|
// The coordinator observes the original PID for replacement instead of replaying.
|
|
201
216
|
return uncertain("restart_request_outcome_unknown");
|
|
202
217
|
}
|
|
203
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A client runtime under the desktop app or a service answers 409 `restart_supervised` and keeps
|
|
222
|
+
* serving (OCX-6). Read that small body and return the validated supervisor, or null.
|
|
223
|
+
*/
|
|
224
|
+
async function supervisedRefusal(response: Response): Promise<"service" | "desktop" | null> {
|
|
225
|
+
try {
|
|
226
|
+
const read = await readBoundedResponseBytes(response, { maxBytes: 1024, signal: AbortSignal.timeout(SYSTEM_RESTART_REQUEST_TIMEOUT_MS) });
|
|
227
|
+
if (read.oversized) return null;
|
|
228
|
+
const body = JSON.parse(new TextDecoder().decode(read.bytes)) as { error?: { code?: unknown; supervisor?: unknown } } | null;
|
|
229
|
+
if (body?.error?.code !== RESTART_SUPERVISED_ERROR) return null;
|
|
230
|
+
return body.error.supervisor === "service" || body.error.supervisor === "desktop" ? body.error.supervisor : null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -20,10 +20,11 @@ function parseMarker(raw: string): UpdateRestartChildMarker {
|
|
|
20
20
|
|| typeof value.home.serviceRecord.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.home.serviceRecord.digest)) throw new Error("update_restart_child_marker_invalid");
|
|
21
21
|
for (const directory of [value.home.config, value.home.codex]) {
|
|
22
22
|
// Windows reports 64-bit NTFS file ids (MFT sequence in the high 16 bits) as doubles above
|
|
23
|
-
// 2^53
|
|
24
|
-
//
|
|
23
|
+
// 2^53, where neighbouring ids collapse. dev and ino may be large finite integers; fileId
|
|
24
|
+
// carries the exact "dev:ino" that assertUpdateRestartHome actually relies on.
|
|
25
25
|
if (!directory || typeof directory.path !== "string" || !directory.path
|
|
26
|
-
|| !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino)
|
|
26
|
+
|| !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino)
|
|
27
|
+
|| typeof directory.fileId !== "string" || !/^\d{1,20}:\d{1,20}$/.test(directory.fileId)) {
|
|
27
28
|
throw new Error("update_restart_child_marker_invalid");
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { realpathSync, statSync, type BigIntStats } from "node:fs";
|
|
2
2
|
import { readClientConnectionState } from "../client/state";
|
|
3
3
|
import { readConfigDiagnostics } from "../config/diagnostics";
|
|
4
4
|
import { getConfigDir } from "../config/paths";
|
|
@@ -6,23 +6,34 @@ import { currentCodexHome } from "../service/state";
|
|
|
6
6
|
import { captureUpdateRestartServiceRecord, type UpdateRestartServiceRecord, type UpdateRestartServiceRecordDeps } from "./update-restart-service-record";
|
|
7
7
|
import { probeUpdateRestartSupervision, type UpdateRestartSupervisionDeps } from "./update-restart-supervision";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* `fileId` is the exact identity ("dev:ino" from a bigint stat). `dev` and `ino` are doubles and
|
|
11
|
+
* cannot be: an NTFS file id carries the MFT sequence in its high bits, so above 2^53 a replacement
|
|
12
|
+
* directory with a nearby MFT index can round to the same `ino` as the one it replaced.
|
|
13
|
+
*/
|
|
14
|
+
export interface UpdateRestartDirectory { path: string; dev: number; ino: number; fileId: string }
|
|
15
|
+
|
|
9
16
|
export interface UpdateRestartHome {
|
|
10
|
-
config:
|
|
11
|
-
codex:
|
|
17
|
+
config: UpdateRestartDirectory;
|
|
18
|
+
codex: UpdateRestartDirectory;
|
|
12
19
|
revision: number;
|
|
13
20
|
serviceRecord: UpdateRestartServiceRecord;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
export type UpdateRestartDirectoryStat = Pick<BigIntStats, "dev" | "ino" | "isDirectory">;
|
|
24
|
+
|
|
25
|
+
function physicalDirectory(path: string, stat: (path: string) => UpdateRestartDirectoryStat): UpdateRestartDirectory {
|
|
17
26
|
const canonical = realpathSync.native(path);
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
20
|
-
return { path: canonical, dev:
|
|
27
|
+
const observed = stat(canonical);
|
|
28
|
+
if (!observed.isDirectory()) throw new Error("update_restart_home_unverified");
|
|
29
|
+
return { path: canonical, dev: Number(observed.dev), ino: Number(observed.ino), fileId: `${observed.dev}:${observed.ino}` };
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export interface UpdateRestartHomeDeps {
|
|
24
33
|
record?: UpdateRestartServiceRecordDeps;
|
|
25
34
|
supervision?: UpdateRestartSupervisionDeps;
|
|
35
|
+
/** Test seam for the directory identity source; production reads a bigint stat. */
|
|
36
|
+
statDirectory?: (path: string) => UpdateRestartDirectoryStat;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/** Capture eligibility from the same bytes that supply the frozen fingerprint. */
|
|
@@ -32,7 +43,8 @@ export function readUpdateRestartHome(deps: UpdateRestartHomeDeps = {}): UpdateR
|
|
|
32
43
|
if (owner.kind !== "none" || (state.kind === "state" && platform !== "darwin" && platform !== "linux")) {
|
|
33
44
|
throw new Error("update_restart_owner_unverified");
|
|
34
45
|
}
|
|
35
|
-
|
|
46
|
+
const stat = deps.statDirectory ?? (path => statSync(path, { bigint: true }));
|
|
47
|
+
return { config: physicalDirectory(getConfigDir(), stat), codex: physicalDirectory(currentCodexHome(), stat), revision: owner.revision, serviceRecord };
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
export function assertUpdateRestartHome(expected: UpdateRestartHome, deadlineAt = Date.now() + 2000, deps: UpdateRestartHomeDeps = {}): void {
|
package/src/cli/usage-compare.ts
CHANGED
|
@@ -8,7 +8,11 @@ import { readClientConnectionState, sameClientConnectionOwner } from "../client/
|
|
|
8
8
|
import { fetchHubUsage } from "../client/hub-client";
|
|
9
9
|
import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
10
10
|
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
11
|
+
import { readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
12
|
+
import { isLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
11
13
|
import { redactSecretString } from "../lib/redact";
|
|
14
|
+
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
15
|
+
import { fetchBoundLocalManagementRead, type LocalManagementReadDeps } from "../server/local-management-read-client";
|
|
12
16
|
import { rangeWindow, USAGE_SURFACES } from "../usage/summary";
|
|
13
17
|
import type { UsageTimeWindow } from "../usage/time-range";
|
|
14
18
|
import {
|
|
@@ -16,8 +20,7 @@ import {
|
|
|
16
20
|
printData,
|
|
17
21
|
rejectArgs,
|
|
18
22
|
RuntimeApiError,
|
|
19
|
-
|
|
20
|
-
runtimeBaseUrl,
|
|
23
|
+
PROXY_NOT_RUNNING_MESSAGE,
|
|
21
24
|
takeFlag,
|
|
22
25
|
takeOption,
|
|
23
26
|
takeOptionWithSyntax,
|
|
@@ -29,7 +32,10 @@ export const USAGE_COMPARE_USAGE = `Usage:
|
|
|
29
32
|
[--provider <name>] [--model <id>] [--api-key-id <id>] [--json]`;
|
|
30
33
|
|
|
31
34
|
export type CompareRange = "today" | "7d" | "30d";
|
|
32
|
-
export type UsageCompareDeps = RuntimeApiDeps &
|
|
35
|
+
export type UsageCompareDeps = RuntimeApiDeps & Pick<LocalManagementReadDeps, "directFetch"> & {
|
|
36
|
+
now?: () => number;
|
|
37
|
+
readRuntime?: typeof readRuntimePort;
|
|
38
|
+
};
|
|
33
39
|
|
|
34
40
|
const METRICS = ["requests", "totalTokens", "inputTokens", "outputTokens", "cachedInputTokens", "estimatedCostUsd"] as const;
|
|
35
41
|
type Metric = typeof METRICS[number];
|
|
@@ -189,10 +195,60 @@ function captureUsageOwner() {
|
|
|
189
195
|
const headers = runningProxyUpdateHeaders();
|
|
190
196
|
const fingerprint = connection.kind === "connected" ? connection.value.tokenFingerprint
|
|
191
197
|
: serviceApiTokenFingerprint(headers.get("X-OpenCodex-API-Key") ?? "");
|
|
192
|
-
return { connection, token,
|
|
198
|
+
return { connection, token, fingerprint };
|
|
193
199
|
}
|
|
194
200
|
type UsageOwner = ReturnType<typeof captureUsageOwner>;
|
|
195
201
|
|
|
202
|
+
function pinUsageRuntime(deps: UsageCompareDeps, owner: UsageOwner): (suffix: string) => Promise<ScopedUsage> {
|
|
203
|
+
const readRuntime = deps.readRuntime ?? readRuntimePort;
|
|
204
|
+
const initial = readRuntimePortForComparison();
|
|
205
|
+
const pinned = { ...initial };
|
|
206
|
+
let liveIdentity: string | undefined;
|
|
207
|
+
function readRuntimePortForComparison(): RuntimePortState {
|
|
208
|
+
const runtime = readRuntime();
|
|
209
|
+
if (!runtime || !isLocalAttestationSecret(runtime.attestationSecret)) {
|
|
210
|
+
throw new Error("Usage runtime unavailable: restart the proxy with an attested runtime record.");
|
|
211
|
+
}
|
|
212
|
+
return runtime;
|
|
213
|
+
}
|
|
214
|
+
function assertRuntime(): RuntimePortState {
|
|
215
|
+
const current = readRuntimePortForComparison();
|
|
216
|
+
if (current.pid !== pinned.pid || current.port !== pinned.port || current.hostname !== pinned.hostname
|
|
217
|
+
|| current.attestationSecret !== pinned.attestationSecret) {
|
|
218
|
+
throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
return async suffix => {
|
|
223
|
+
assertRuntime();
|
|
224
|
+
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
225
|
+
assertRuntime();
|
|
226
|
+
assertUsageOwner(owner);
|
|
227
|
+
if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
|
|
228
|
+
const identity = JSON.stringify([live.pid, live.port, live.hostname, live.source, live.role]);
|
|
229
|
+
if (live.source !== "runtime" || live.pid !== pinned.pid || live.port !== pinned.port
|
|
230
|
+
|| probeHostname(live.hostname) !== probeHostname(pinned.hostname) || live.role === "client"
|
|
231
|
+
|| (liveIdentity !== undefined && liveIdentity !== identity)) {
|
|
232
|
+
throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
|
|
233
|
+
}
|
|
234
|
+
liveIdentity = identity;
|
|
235
|
+
const read = await fetchBoundLocalManagementRead(live, `/api/usage?${suffix.replace(/^\?/, "")}`, {
|
|
236
|
+
fetchImpl: deps.fetchImpl, directFetch: deps.directFetch, readRuntime: assertRuntime,
|
|
237
|
+
timeoutMs: 60_000, requireResponseProof: true,
|
|
238
|
+
});
|
|
239
|
+
assertRuntime();
|
|
240
|
+
if (read.kind === "unavailable") {
|
|
241
|
+
throw new RuntimeApiError(`Local usage read unavailable (${read.reason}). No comparison was produced.`, 503, null);
|
|
242
|
+
}
|
|
243
|
+
if (!read.response.ok) {
|
|
244
|
+
throw new RuntimeApiError(`Usage request failed (HTTP ${read.response.status}). No comparison was produced.`, read.response.status, null);
|
|
245
|
+
}
|
|
246
|
+
const result = await read.response.json() as ScopedUsage;
|
|
247
|
+
assertRuntime();
|
|
248
|
+
return result;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
196
252
|
function assertUsageOwner(owner: UsageOwner): void {
|
|
197
253
|
const current = readClientConnectionState();
|
|
198
254
|
const expected = owner.connection;
|
|
@@ -216,6 +272,7 @@ async function readUsage(
|
|
|
216
272
|
apiKeyId: string | undefined,
|
|
217
273
|
deps: RuntimeApiDeps,
|
|
218
274
|
owner: UsageOwner,
|
|
275
|
+
localRead?: (suffix: string) => Promise<ScopedUsage>,
|
|
219
276
|
): Promise<ScopedUsage> {
|
|
220
277
|
assertUsageOwner(owner);
|
|
221
278
|
const { connection, token } = owner;
|
|
@@ -233,16 +290,15 @@ async function readUsage(
|
|
|
233
290
|
result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix.replace(/^\?/, "")), {
|
|
234
291
|
fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
|
|
235
292
|
});
|
|
236
|
-
} else if (apiKeyId === undefined) {
|
|
237
|
-
result = await runtimeRequest(`/api/usage${suffix}`, { headers: owner.headers }, deps);
|
|
238
293
|
} else {
|
|
239
294
|
try {
|
|
240
|
-
result = await
|
|
295
|
+
result = await localRead!(suffix);
|
|
241
296
|
} catch (error) {
|
|
242
|
-
if (
|
|
297
|
+
if (apiKeyId === undefined) throw error;
|
|
298
|
+
if (error instanceof RuntimeApiError) throw error;
|
|
243
299
|
throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
|
|
244
300
|
}
|
|
245
|
-
if (result.filter?.apiKeyId !== apiKeyId) {
|
|
301
|
+
if (apiKeyId !== undefined && result.filter?.apiKeyId !== apiKeyId) {
|
|
246
302
|
throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
|
|
247
303
|
}
|
|
248
304
|
}
|
|
@@ -276,9 +332,9 @@ export async function handleUsageCompareCommand(argv: string[], deps: UsageCompa
|
|
|
276
332
|
const windows = compareUsageWindows(range, now);
|
|
277
333
|
const filters = { range, surface, provider, model, apiKeyId };
|
|
278
334
|
const owner = captureUsageOwner();
|
|
279
|
-
const
|
|
280
|
-
const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId,
|
|
281
|
-
const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId,
|
|
335
|
+
const localRead = owner.connection.kind === "disconnected" ? pinUsageRuntime(deps, owner) : undefined;
|
|
336
|
+
const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, deps, owner, localRead);
|
|
337
|
+
const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, deps, owner, localRead);
|
|
282
338
|
const current = totalsOf(currentReport);
|
|
283
339
|
const previous = totalsOf(previousReport);
|
|
284
340
|
const deltaTotals: Totals = {};
|