@bitkyc08/opencodex 2.64.0-preview.20260923 → 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-BEq4OCOz.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-D5eN1TID.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
|
@@ -1,6 +1,33 @@
|
|
|
1
|
-
export type DetectedInstall = "bun" | "npm" | "pnpm" | "source";
|
|
1
|
+
export type DetectedInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
|
|
2
|
+
|
|
3
|
+
export interface MiseInstallOwner {
|
|
4
|
+
tool: string;
|
|
5
|
+
backend: string;
|
|
6
|
+
installPath: string;
|
|
7
|
+
toolRoot: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type InstallOwnership =
|
|
11
|
+
| { installer: Exclude<DetectedInstall, "mise"> }
|
|
12
|
+
| {
|
|
13
|
+
installer: "mise";
|
|
14
|
+
owner: MiseInstallOwner | null;
|
|
15
|
+
error?: "metadata_unreadable" | "metadata_inconsistent";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface InstallDetectionDeps {
|
|
19
|
+
exists?: (path: string) => boolean;
|
|
20
|
+
probe?: (path: string) => "present" | "absent" | "unreadable";
|
|
21
|
+
readFile?: (path: string) => string;
|
|
22
|
+
realpath?: (path: string) => string;
|
|
23
|
+
}
|
|
2
24
|
|
|
3
25
|
export declare function detectInstallFromPath(
|
|
4
26
|
packagePath: string,
|
|
5
|
-
deps?:
|
|
27
|
+
deps?: InstallDetectionDeps,
|
|
6
28
|
): DetectedInstall;
|
|
29
|
+
|
|
30
|
+
export declare function detectInstallOwnershipFromPath(
|
|
31
|
+
packagePath: string,
|
|
32
|
+
deps?: InstallDetectionDeps,
|
|
33
|
+
): InstallOwnership;
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const OPENCODEX_MISE_BACKEND = "npm:@bitkyc08/opencodex";
|
|
4
|
+
const OPENCODEX_MISE_BACKEND_DIR = "npm-bitkyc08-opencodex";
|
|
5
|
+
/** mise's core Node runtime. npm -g under it is an npm install that mise did not make. */
|
|
6
|
+
const MISE_NODE_RUNTIME = { tool: "node", backend: "core:node" };
|
|
7
|
+
const OPENCODEX_PACKAGE_SEGMENT = "/node_modules/@bitkyc08/opencodex";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* tool: string;
|
|
12
|
+
* backend: string;
|
|
13
|
+
* installPath: string;
|
|
14
|
+
* toolRoot: string;
|
|
15
|
+
* }} MiseInstallOwner
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* installer: "bun" | "npm" | "pnpm" | "source";
|
|
21
|
+
* } | {
|
|
22
|
+
* installer: "mise";
|
|
23
|
+
* owner: MiseInstallOwner | null;
|
|
24
|
+
* error?: "metadata_unreadable" | "metadata_inconsistent";
|
|
25
|
+
* }} InstallOwnership
|
|
26
|
+
*/
|
|
2
27
|
|
|
3
28
|
/**
|
|
4
29
|
* Infer the package manager from the path of the running package.
|
|
@@ -14,7 +39,30 @@ import { realpathSync } from "node:fs";
|
|
|
14
39
|
* virtual store.
|
|
15
40
|
*/
|
|
16
41
|
export function detectInstallFromPath(packagePath, deps = {}) {
|
|
17
|
-
|
|
42
|
+
return detectInstallOwnershipFromPath(packagePath, deps).installer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Infer the outer owner of the running package.
|
|
47
|
+
*
|
|
48
|
+
* mise's npm backend deliberately contains an ordinary npm/aube installation, so
|
|
49
|
+
* package-manager layout alone reports npm. The adjacent backend record is the
|
|
50
|
+
* stronger ownership signal: it identifies the mise alias and canonical backend,
|
|
51
|
+
* while containment proves that the running package belongs to that installation.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} packagePath
|
|
54
|
+
* @param {{
|
|
55
|
+
* exists?: (path: string) => boolean;
|
|
56
|
+
* probe?: (path: string) => "present" | "absent" | "unreadable";
|
|
57
|
+
* readFile?: (path: string) => string;
|
|
58
|
+
* realpath?: (path: string) => string;
|
|
59
|
+
* }} deps
|
|
60
|
+
* @returns {InstallOwnership}
|
|
61
|
+
*/
|
|
62
|
+
export function detectInstallOwnershipFromPath(packagePath, deps = {}) {
|
|
63
|
+
const exists = deps.exists ?? existsSync;
|
|
64
|
+
const probe = deps.probe ?? probeMetadata;
|
|
65
|
+
const readFile = deps.readFile ?? (path => readFileSync(path, "utf8"));
|
|
18
66
|
const candidates = [String(packagePath)];
|
|
19
67
|
try {
|
|
20
68
|
const resolved = (deps.realpath ?? realpathSync)(String(packagePath));
|
|
@@ -24,17 +72,175 @@ export function detectInstallFromPath(packagePath, deps = {}) {
|
|
|
24
72
|
// realpath. The lexical path still carries the evidence when it is available.
|
|
25
73
|
}
|
|
26
74
|
|
|
27
|
-
let
|
|
75
|
+
let detectedManager = "source";
|
|
76
|
+
/** @type {MiseInstallOwner[]} */
|
|
77
|
+
const miseOwners = [];
|
|
78
|
+
/** @type {"metadata_unreadable" | "metadata_inconsistent" | undefined} */
|
|
79
|
+
let miseError;
|
|
28
80
|
for (const candidate of candidates) {
|
|
81
|
+
const mise = detectMiseOwner(candidate, { probe, readFile });
|
|
82
|
+
if (mise.recognized) {
|
|
83
|
+
if (mise.owner) miseOwners.push(mise.owner);
|
|
84
|
+
else miseError = mise.error;
|
|
85
|
+
}
|
|
29
86
|
const detected = detectInstallCandidate(candidate, exists);
|
|
30
|
-
if (detected === "pnpm" || detected === "bun")
|
|
31
|
-
if (detected === "npm")
|
|
87
|
+
if (detected === "pnpm" || detected === "bun") detectedManager = detected;
|
|
88
|
+
else if (detected === "npm" && detectedManager === "source") detectedManager = "npm";
|
|
89
|
+
}
|
|
90
|
+
// Any recognized ownership error on either spelling takes precedence over every verified
|
|
91
|
+
// owner. Keeping a command from the other candidate could authorize mutation across a
|
|
92
|
+
// lexical/resolved-path mismatch, so fail closed without recovery guidance.
|
|
93
|
+
if (miseError) return { installer: "mise", owner: null, error: miseError };
|
|
94
|
+
const miseOwner = miseOwners.at(-1);
|
|
95
|
+
if (miseOwner) {
|
|
96
|
+
// The lexical and resolved spellings of one install differ when an ancestor such as the
|
|
97
|
+
// mise data directory is a symlink (macOS /var -> /private/var). Both still name the same
|
|
98
|
+
// physical tool directory and the same backend file, so compare canonical directories;
|
|
99
|
+
// any other disagreement stays inconsistent.
|
|
100
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
101
|
+
const consistent = miseOwners.every(owner =>
|
|
102
|
+
owner.tool === miseOwner.tool
|
|
103
|
+
&& owner.backend === miseOwner.backend
|
|
104
|
+
&& sameDirectory(owner.toolRoot, miseOwner.toolRoot, realpath)
|
|
105
|
+
);
|
|
106
|
+
return consistent
|
|
107
|
+
? { installer: "mise", owner: miseOwner }
|
|
108
|
+
: { installer: "mise", owner: null, error: "metadata_inconsistent" };
|
|
109
|
+
}
|
|
110
|
+
return { installer: detectedManager };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseBackendMetadata(content) {
|
|
114
|
+
const fields = new Map();
|
|
115
|
+
for (const line of String(content).split(/\r?\n/)) {
|
|
116
|
+
const match = /^\s*(short|full)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/.exec(line);
|
|
117
|
+
if (!match) continue;
|
|
118
|
+
if (fields.has(match[1])) return null;
|
|
119
|
+
try {
|
|
120
|
+
fields.set(
|
|
121
|
+
match[1],
|
|
122
|
+
match[2].startsWith('"') ? JSON.parse(match[2]) : match[2].slice(1, -1),
|
|
123
|
+
);
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
32
127
|
}
|
|
33
|
-
|
|
128
|
+
const tool = fields.get("short");
|
|
129
|
+
const backend = fields.get("full");
|
|
130
|
+
return typeof tool === "string" && typeof backend === "string"
|
|
131
|
+
? { tool, backend }
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function detectMiseOwner(packagePath, deps) {
|
|
136
|
+
const windowsPath = /^[A-Za-z]:[\\/]/.test(String(packagePath))
|
|
137
|
+
|| String(packagePath).startsWith("\\\\");
|
|
138
|
+
const normalized = (windowsPath ? String(packagePath).replaceAll("\\", "/") : String(packagePath))
|
|
139
|
+
.replace(/\/+$/, "");
|
|
140
|
+
const lower = normalized.toLowerCase();
|
|
141
|
+
let marker = -1;
|
|
142
|
+
let installPath;
|
|
143
|
+
let toolRoot;
|
|
144
|
+
let metadataPath;
|
|
145
|
+
while ((marker = lower.indexOf("/node_modules/", marker + 1)) >= 1) {
|
|
146
|
+
installPath = normalized.slice(0, marker);
|
|
147
|
+
const slash = installPath.lastIndexOf("/");
|
|
148
|
+
if (slash < 1) continue;
|
|
149
|
+
toolRoot = installPath.slice(0, slash);
|
|
150
|
+
metadataPath = `${toolRoot}/.mise.backend.toml`;
|
|
151
|
+
const metadataState = deps.probe(metadataPath);
|
|
152
|
+
if (metadataState === "present") break;
|
|
153
|
+
if (metadataState === "unreadable") {
|
|
154
|
+
return { recognized: true, owner: null, error: "metadata_unreadable" };
|
|
155
|
+
}
|
|
156
|
+
metadataPath = undefined;
|
|
157
|
+
}
|
|
158
|
+
if (!metadataPath || !installPath || !toolRoot) return { recognized: false };
|
|
159
|
+
|
|
160
|
+
let metadata;
|
|
161
|
+
try {
|
|
162
|
+
metadata = parseBackendMetadata(deps.readFile(metadataPath));
|
|
163
|
+
} catch {
|
|
164
|
+
return { recognized: true, owner: null, error: "metadata_unreadable" };
|
|
165
|
+
}
|
|
166
|
+
const toolDir = toolRoot.slice(toolRoot.lastIndexOf("/") + 1);
|
|
167
|
+
// On Windows, npm -g under a mise-managed Node writes the package straight into
|
|
168
|
+
// <mise>/installs/node/<version>/node_modules, so the adjacent record is Node's own
|
|
169
|
+
// (short = "node", full = "core:node"), not a statement about OpenCodex. Only that exact
|
|
170
|
+
// runtime record with the package directly in the runtime's global node_modules is an
|
|
171
|
+
// npm install; any other backend or layout stays fail-closed below.
|
|
172
|
+
if (
|
|
173
|
+
metadata
|
|
174
|
+
&& metadata.tool === MISE_NODE_RUNTIME.tool
|
|
175
|
+
&& metadata.backend === MISE_NODE_RUNTIME.backend
|
|
176
|
+
&& samePath(toolDir, MISE_NODE_RUNTIME.tool, windowsPath)
|
|
177
|
+
&& isRuntimeGlobalPackage(normalized, installPath, windowsPath)
|
|
178
|
+
) {
|
|
179
|
+
return { recognized: false };
|
|
180
|
+
}
|
|
181
|
+
const expectedToolDir = metadata?.tool === OPENCODEX_MISE_BACKEND
|
|
182
|
+
? OPENCODEX_MISE_BACKEND_DIR
|
|
183
|
+
: metadata?.tool;
|
|
184
|
+
if (
|
|
185
|
+
!metadata
|
|
186
|
+
|| metadata.backend !== OPENCODEX_MISE_BACKEND
|
|
187
|
+
|| (metadata.tool !== OPENCODEX_MISE_BACKEND
|
|
188
|
+
&& !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(metadata.tool))
|
|
189
|
+
|| !samePath(expectedToolDir, toolDir, windowsPath)
|
|
190
|
+
) {
|
|
191
|
+
return { recognized: true, owner: null, error: "metadata_inconsistent" };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
recognized: true,
|
|
195
|
+
owner: {
|
|
196
|
+
tool: metadata.tool,
|
|
197
|
+
backend: metadata.backend,
|
|
198
|
+
installPath,
|
|
199
|
+
toolRoot,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* True when the package sits directly in the runtime's global node_modules:
|
|
206
|
+
* <toolRoot>/<version>/node_modules/@bitkyc08/opencodex[/...].
|
|
207
|
+
*/
|
|
208
|
+
function isRuntimeGlobalPackage(packagePath, installPath, windowsPath) {
|
|
209
|
+
const rest = packagePath.slice(installPath.length);
|
|
210
|
+
const probe = windowsPath ? rest.toLowerCase() : rest;
|
|
211
|
+
return probe === OPENCODEX_PACKAGE_SEGMENT || probe.startsWith(`${OPENCODEX_PACKAGE_SEGMENT}/`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function probeMetadata(path) {
|
|
215
|
+
try {
|
|
216
|
+
statSync(path);
|
|
217
|
+
return "present";
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const code = error && typeof error === "object" && "code" in error
|
|
220
|
+
? error.code
|
|
221
|
+
: undefined;
|
|
222
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sameDirectory(left, right, realpath) {
|
|
227
|
+
if (samePath(left, right)) return true;
|
|
228
|
+
try {
|
|
229
|
+
return samePath(realpath(left), realpath(right));
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function samePath(left, right, windows = /^[A-Za-z]:\//.test(left) && /^[A-Za-z]:\//.test(right)) {
|
|
236
|
+
return windows ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
34
237
|
}
|
|
35
238
|
|
|
36
239
|
function detectInstallCandidate(packagePath, exists) {
|
|
37
|
-
const
|
|
240
|
+
const path = String(packagePath);
|
|
241
|
+
const normalized = /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\")
|
|
242
|
+
? path.replaceAll("\\", "/")
|
|
243
|
+
: path;
|
|
38
244
|
const segments = normalized.split("/").filter(Boolean);
|
|
39
245
|
// Windows paths are case-insensitive. Treating the structural marker this way also
|
|
40
246
|
// keeps a preserved-symlink path from being downgraded merely because its casing came
|
package/src/update/job.ts
CHANGED
|
@@ -34,19 +34,22 @@ import {
|
|
|
34
34
|
import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
|
|
35
35
|
import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
|
|
36
36
|
import {
|
|
37
|
-
type Channel,
|
|
38
|
-
type Installer,
|
|
37
|
+
type Channel, type Installer,
|
|
39
38
|
PKG,
|
|
40
39
|
checkUpdatePackageIntegrity,
|
|
41
40
|
currentVersion,
|
|
42
41
|
defaultUpdateTag,
|
|
43
42
|
detectInstall,
|
|
43
|
+
detectInstallOwnership,
|
|
44
44
|
latestVersion,
|
|
45
|
+
miseUpdateCommand,
|
|
45
46
|
updateCommand,
|
|
46
47
|
updateCommandStr,
|
|
47
48
|
resolveCurrentPnpmGlobalOwner,
|
|
48
49
|
resolvePnpmActiveLauncher,
|
|
49
50
|
} from "./index";
|
|
51
|
+
import type { UpdateCheckDeps } from "./check-types";
|
|
52
|
+
export type { UpdateCheckDeps } from "./check-types";
|
|
50
53
|
import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
|
|
51
54
|
import { isNewer } from "./notify";
|
|
52
55
|
import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
|
|
@@ -114,12 +117,6 @@ export class UpdateJobError extends Error {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
export interface UpdateCheckDeps {
|
|
118
|
-
currentVersion: () => string;
|
|
119
|
-
detectInstall: () => Installer;
|
|
120
|
-
latestVersion: (tag: Channel) => string | null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
120
|
interface UpdateWorkerProcess {
|
|
124
121
|
pid?: number;
|
|
125
122
|
unref(): void;
|
|
@@ -136,7 +133,9 @@ export interface StartUpdateJobDeps {
|
|
|
136
133
|
const defaultCheckDeps: UpdateCheckDeps = {
|
|
137
134
|
currentVersion,
|
|
138
135
|
detectInstall,
|
|
136
|
+
detectInstallOwnership,
|
|
139
137
|
latestVersion,
|
|
138
|
+
miseUpdateCommand,
|
|
140
139
|
};
|
|
141
140
|
|
|
142
141
|
function nodeBin(): string {
|
|
@@ -489,16 +488,22 @@ export function checkForUpdate(
|
|
|
489
488
|
deps: UpdateCheckDeps = defaultCheckDeps,
|
|
490
489
|
): UpdateCheckResult {
|
|
491
490
|
const current = deps.currentVersion();
|
|
492
|
-
const
|
|
491
|
+
const ownership = deps.detectInstallOwnership?.();
|
|
492
|
+
const installer = ownership?.installer ?? deps.detectInstall();
|
|
493
493
|
const channel = requestedChannel ?? normalizeUpdateChannel(null, current);
|
|
494
494
|
const latest = installer === "source" ? null : deps.latestVersion(channel);
|
|
495
495
|
const updateAvailable = !!latest && isNewer(latest, current, channel);
|
|
496
496
|
let reason: string | undefined;
|
|
497
|
-
let command = installer === "source"
|
|
497
|
+
let command = installer === "source"
|
|
498
|
+
? manualSourceCommand()
|
|
499
|
+
: installer === "mise"
|
|
500
|
+
? (ownership && deps.miseUpdateCommand?.(ownership)) ?? ""
|
|
501
|
+
: updateExecutionCommand(installer, channel).display;
|
|
498
502
|
|
|
499
503
|
if (installer === "source") {
|
|
500
504
|
reason = "source_checkout";
|
|
501
|
-
|
|
505
|
+
} else if (installer === "mise") {
|
|
506
|
+
reason = command ? "externally_managed" : "external_ownership_invalid";
|
|
502
507
|
} else if (!latest) {
|
|
503
508
|
reason = "latest_unavailable";
|
|
504
509
|
} else if (!updateAvailable) {
|
|
@@ -511,7 +516,7 @@ export function checkForUpdate(
|
|
|
511
516
|
channel,
|
|
512
517
|
installer,
|
|
513
518
|
updateAvailable,
|
|
514
|
-
canUpdate: installer !== "source" && updateAvailable,
|
|
519
|
+
canUpdate: installer !== "source" && installer !== "mise" && updateAvailable,
|
|
515
520
|
command,
|
|
516
521
|
releaseNotesUrl: RELEASE_NOTES_URL,
|
|
517
522
|
...(reason ? { reason } : {}),
|
package/src/update/notify.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
} from "./index";
|
|
18
18
|
|
|
19
19
|
const VERSION_FILENAME = "version.json";
|
|
20
|
-
const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
|
|
20
|
+
export const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
|
|
21
|
+
export const CACHE_MAX_AGE_MS = 40 * 60 * 60 * 1000;
|
|
21
22
|
const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
|
|
22
23
|
|
|
23
24
|
export interface VersionCache {
|
|
@@ -62,6 +63,17 @@ export function writeVersionCache(cache: VersionCache): void {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
export function writeFreshVersionCache(channel: Channel, latest: string, nowMs = Date.now()): void {
|
|
67
|
+
const previous = readVersionCache(channel);
|
|
68
|
+
writeVersionCache({
|
|
69
|
+
latest_version: latest,
|
|
70
|
+
last_checked_at: new Date(nowMs).toISOString(),
|
|
71
|
+
dismissed_version: previous?.latest_version === latest && previous.dismissed_version === latest
|
|
72
|
+
? latest : undefined,
|
|
73
|
+
tag: channel,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
function parseStable(v: string): [number, number, number] | null {
|
|
66
78
|
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
67
79
|
if (!m) return null;
|
|
@@ -139,7 +151,8 @@ export function interactiveGuardOk(): boolean {
|
|
|
139
151
|
* the one-time star prompt has already run (first-run yield, O1).
|
|
140
152
|
*/
|
|
141
153
|
export function shouldConsider(): { channel: Channel; current: string } | null {
|
|
142
|
-
|
|
154
|
+
const installer = detectInstall();
|
|
155
|
+
if (installer === "source" || installer === "mise") return null;
|
|
143
156
|
const current = currentVersion();
|
|
144
157
|
if (current === "?" || isSourceBuildVersion(current)) return null;
|
|
145
158
|
if (!interactiveGuardOk()) return null;
|
|
@@ -195,14 +208,7 @@ export function triggerBackgroundRefreshIfStale(channel: Channel, cache: Version
|
|
|
195
208
|
*/
|
|
196
209
|
export async function refreshVersionCache(channel: Channel): Promise<void> {
|
|
197
210
|
const latest = latestVersion(channel);
|
|
198
|
-
if (
|
|
199
|
-
const prev = readVersionCache(channel);
|
|
200
|
-
writeVersionCache({
|
|
201
|
-
latest_version: latest,
|
|
202
|
-
last_checked_at: new Date().toISOString(),
|
|
203
|
-
dismissed_version: prev?.dismissed_version,
|
|
204
|
-
tag: channel,
|
|
205
|
-
});
|
|
211
|
+
if (latest) writeFreshVersionCache(channel, latest);
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
/** Persist a dismissal so this exact version stops prompting. */
|
|
@@ -241,7 +247,6 @@ export async function maybeShowUpdatePrompt(): Promise<void> {
|
|
|
241
247
|
const { channel, current } = eligible;
|
|
242
248
|
|
|
243
249
|
const cache = readVersionCache(channel);
|
|
244
|
-
triggerBackgroundRefreshIfStale(channel, cache);
|
|
245
250
|
|
|
246
251
|
const latest = getUpgradeVersionForPopup(cache, current, channel);
|
|
247
252
|
if (!latest) return;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveCurrentPnpmGlobalOwner } from "./index";
|
|
2
|
+
|
|
3
|
+
onmessage = event => {
|
|
4
|
+
try {
|
|
5
|
+
const invoked = typeof event.data === "string" ? event.data : "";
|
|
6
|
+
if (!invoked) { postMessage(null); return; }
|
|
7
|
+
const result = resolveCurrentPnpmGlobalOwner(invoked);
|
|
8
|
+
postMessage(result.ok ? result.owner : null);
|
|
9
|
+
} catch {
|
|
10
|
+
postMessage(null);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { checkForUpdate, type UpdateCheckResult } from "./job";
|
|
2
|
+
import { currentVersion, defaultUpdateTag, detectInstall, detectInstallOwnership, miseUpdateCommand, type Channel, type Installer } from "./index";
|
|
3
|
+
import { isSourceBuildVersion, readVersionCache, REFRESH_INTERVAL_MS, writeFreshVersionCache } from "./notify";
|
|
4
|
+
import { latestVersionAsync } from "./async-check";
|
|
5
|
+
|
|
6
|
+
export const STALENESS_TICK_MS = 60 * 60 * 1000;
|
|
7
|
+
export const RETRY_BASE_MS = 60_000;
|
|
8
|
+
export const RETRY_CAP_MS = STALENESS_TICK_MS;
|
|
9
|
+
|
|
10
|
+
export interface RefreshDeps {
|
|
11
|
+
now: () => number;
|
|
12
|
+
lookup: (channel: Channel, installer: Installer) => Promise<string | null>;
|
|
13
|
+
current: () => string;
|
|
14
|
+
install: () => Installer;
|
|
15
|
+
ownership: typeof detectInstallOwnership;
|
|
16
|
+
guidance: typeof miseUpdateCommand;
|
|
17
|
+
read: typeof readVersionCache;
|
|
18
|
+
write: typeof writeFreshVersionCache;
|
|
19
|
+
setTimer: typeof setTimeout;
|
|
20
|
+
clearTimer: typeof clearTimeout;
|
|
21
|
+
disabled: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaults: RefreshDeps = {
|
|
25
|
+
now: Date.now,
|
|
26
|
+
lookup: latestVersionAsync,
|
|
27
|
+
current: currentVersion,
|
|
28
|
+
install: detectInstall,
|
|
29
|
+
ownership: detectInstallOwnership,
|
|
30
|
+
guidance: miseUpdateCommand,
|
|
31
|
+
read: readVersionCache,
|
|
32
|
+
write: writeFreshVersionCache,
|
|
33
|
+
setTimer: setTimeout,
|
|
34
|
+
clearTimer: clearTimeout,
|
|
35
|
+
disabled: () => process.env.OCX_DISABLE_UPDATE_CHECK === "1",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function createRefreshScheduler(deps: RefreshDeps = defaults) {
|
|
39
|
+
const inFlight = new Map<Channel, { task: Promise<string | null>; epoch: number; markExplicit: () => void }>();
|
|
40
|
+
// Flight order per channel: a flight started earlier never overwrites a cache entry that a
|
|
41
|
+
// later-started flight already wrote (an explicit caller can keep an old flight writable
|
|
42
|
+
// across stop/start while the new listener's flight finishes first).
|
|
43
|
+
const lastWrittenFlight = new Map<Channel, number>();
|
|
44
|
+
let flightSeq = 0;
|
|
45
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
+
let running = false;
|
|
47
|
+
let starts = 0;
|
|
48
|
+
let failures = 0;
|
|
49
|
+
let retryAt = 0;
|
|
50
|
+
let generation = 0;
|
|
51
|
+
|
|
52
|
+
const eligible = () => {
|
|
53
|
+
const version = deps.current();
|
|
54
|
+
const installer = deps.install();
|
|
55
|
+
return !deps.disabled() && version !== "?" && !isSourceBuildVersion(version)
|
|
56
|
+
&& installer !== "source" && installer !== "mise";
|
|
57
|
+
};
|
|
58
|
+
const channel = () => defaultUpdateTag(deps.current());
|
|
59
|
+
const stale = (tag: Channel) => {
|
|
60
|
+
const checked = Date.parse(deps.read(tag)?.last_checked_at ?? "");
|
|
61
|
+
return !Number.isFinite(checked) || checked > deps.now() || deps.now() - checked >= REFRESH_INTERVAL_MS;
|
|
62
|
+
};
|
|
63
|
+
const schedule = (delay: number) => {
|
|
64
|
+
if (!running) return;
|
|
65
|
+
if (timer) deps.clearTimer(timer);
|
|
66
|
+
timer = deps.setTimer(() => { timer = undefined; void tick(); }, delay);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
};
|
|
69
|
+
const lookup = (tag: Channel, automatic: boolean): Promise<string | null> => {
|
|
70
|
+
const existing = inFlight.get(tag);
|
|
71
|
+
// An automatic tick joins only a flight of its own generation. A flight left over from a
|
|
72
|
+
// stopped listener cannot write for the new one, so joining it would push the next check an
|
|
73
|
+
// hour out with nothing written. Explicit callers may join any flight: their interest writes.
|
|
74
|
+
if (existing && (!automatic || existing.epoch === generation)) {
|
|
75
|
+
if (!automatic) existing.markExplicit();
|
|
76
|
+
return existing.task;
|
|
77
|
+
}
|
|
78
|
+
const epoch = generation;
|
|
79
|
+
const seq = ++flightSeq;
|
|
80
|
+
let explicitInterest = !automatic;
|
|
81
|
+
const task = Promise.resolve().then(() => deps.lookup(tag, deps.install())).then(latest => {
|
|
82
|
+
if (latest && (explicitInterest || (automatic && running && epoch === generation))
|
|
83
|
+
&& seq > (lastWrittenFlight.get(tag) ?? 0)) {
|
|
84
|
+
lastWrittenFlight.set(tag, seq);
|
|
85
|
+
deps.write(tag, latest, deps.now());
|
|
86
|
+
}
|
|
87
|
+
if (automatic && running && epoch === generation) {
|
|
88
|
+
failures = latest ? 0 : failures + 1;
|
|
89
|
+
retryAt = latest ? 0 : deps.now() + Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 6));
|
|
90
|
+
}
|
|
91
|
+
return latest;
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
if (automatic && running && epoch === generation) {
|
|
94
|
+
failures += 1;
|
|
95
|
+
retryAt = deps.now() + Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 6));
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}).finally(() => { if (inFlight.get(tag)?.task === task) inFlight.delete(tag); });
|
|
99
|
+
inFlight.set(tag, { task, epoch, markExplicit: () => { explicitInterest = true; } });
|
|
100
|
+
return task;
|
|
101
|
+
};
|
|
102
|
+
const tick = async () => {
|
|
103
|
+
if (!running || !eligible()) return;
|
|
104
|
+
const tag = channel();
|
|
105
|
+
if (stale(tag) && deps.now() >= retryAt) await lookup(tag, true);
|
|
106
|
+
schedule(retryAt > deps.now() ? Math.min(STALENESS_TICK_MS, retryAt - deps.now()) : STALENESS_TICK_MS);
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
start() {
|
|
110
|
+
starts += 1;
|
|
111
|
+
if (starts !== 1 || !eligible()) return;
|
|
112
|
+
running = true;
|
|
113
|
+
schedule(0);
|
|
114
|
+
},
|
|
115
|
+
stop() {
|
|
116
|
+
if (starts === 0) return;
|
|
117
|
+
starts -= 1;
|
|
118
|
+
if (starts !== 0) return;
|
|
119
|
+
running = false;
|
|
120
|
+
generation += 1;
|
|
121
|
+
if (timer) deps.clearTimer(timer);
|
|
122
|
+
timer = undefined;
|
|
123
|
+
},
|
|
124
|
+
async check(tag: Channel): Promise<UpdateCheckResult> {
|
|
125
|
+
const installer = deps.install();
|
|
126
|
+
const latest = installer === "source" || installer === "mise" ? null : await lookup(tag, false);
|
|
127
|
+
return checkForUpdate(tag, {
|
|
128
|
+
currentVersion: deps.current,
|
|
129
|
+
detectInstall: deps.install,
|
|
130
|
+
detectInstallOwnership: deps.ownership,
|
|
131
|
+
miseUpdateCommand: deps.guidance,
|
|
132
|
+
latestVersion: () => latest,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const packageRefresh = createRefreshScheduler();
|
|
139
|
+
|
|
140
|
+
export function startPackageRefresh(): void { packageRefresh.start(); }
|
|
141
|
+
export function stopPackageRefresh(): void { packageRefresh.stop(); }
|
package/src/usage/cost.ts
CHANGED
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
findExpectedPriceOverlay,
|
|
26
26
|
findVerifiedPriceOverride,
|
|
27
27
|
findPriorityPricingRule,
|
|
28
|
+
cursorFastPriceMultiplier,
|
|
29
|
+
cursorFastPriceSource,
|
|
30
|
+
cursorFastPriceSupported,
|
|
28
31
|
findContextTier,
|
|
29
32
|
isLongContext,
|
|
30
33
|
type Cost4,
|
|
@@ -257,6 +260,7 @@ function resolveMatchedPriceExact(
|
|
|
257
260
|
// operator's explicit price is authoritative for the ~$ estimate.
|
|
258
261
|
const userOverlay = userOverlayMatch(provider, modelId, userOverlays);
|
|
259
262
|
if (userOverlay) return userOverlay;
|
|
263
|
+
if (!cursorFastPriceSupported(provider, modelId)) return null;
|
|
260
264
|
const verifiedOverride = overlays === EXPECTED_PRICE_OVERLAYS
|
|
261
265
|
? findVerifiedPriceOverride(provider, modelId)
|
|
262
266
|
: undefined;
|
|
@@ -264,11 +268,12 @@ function resolveMatchedPriceExact(
|
|
|
264
268
|
return {
|
|
265
269
|
provider,
|
|
266
270
|
modelId,
|
|
267
|
-
cost4: verifiedOverride.cost4,
|
|
271
|
+
cost4: multiplyCursorFastCost(verifiedOverride.cost4, provider, modelId),
|
|
268
272
|
source: "expected",
|
|
269
273
|
sourceRef: verifiedOverride.source,
|
|
270
274
|
verifiedAt: verifiedOverride.verifiedAt,
|
|
271
275
|
status: verifiedOverride.status,
|
|
276
|
+
...cursorFastPriceProvenance(provider, modelId),
|
|
272
277
|
};
|
|
273
278
|
}
|
|
274
279
|
const metadataProvider = resolveMetadataProvider(provider);
|
|
@@ -280,28 +285,50 @@ function resolveMatchedPriceExact(
|
|
|
280
285
|
provider,
|
|
281
286
|
modelId,
|
|
282
287
|
jawcodeProvider: metadataProvider,
|
|
283
|
-
cost4: bundled.cost,
|
|
288
|
+
cost4: multiplyCursorFastCost(bundled.cost, provider, modelId),
|
|
284
289
|
source: "jawcode",
|
|
285
290
|
status: "verified",
|
|
291
|
+
...cursorFastPriceProvenance(provider, modelId),
|
|
286
292
|
};
|
|
287
293
|
}
|
|
288
294
|
const overlay = findExpectedPriceOverlay(provider, modelId, overlays);
|
|
289
295
|
if (!overlay || !validCost4(overlay.cost4) || !hasNonZeroCost(overlay.cost4)) {
|
|
290
|
-
|
|
296
|
+
if (options.allowModelLevelFallback === false) return null;
|
|
297
|
+
const fallback = resolveModelLevelPrice(provider, modelId);
|
|
298
|
+
return fallback
|
|
299
|
+
? { ...fallback, cost4: multiplyCursorFastCost(fallback.cost4, provider, modelId), ...cursorFastPriceProvenance(provider, modelId) }
|
|
300
|
+
: null;
|
|
291
301
|
}
|
|
292
302
|
if (overlay.status === "unverified") return null;
|
|
293
303
|
return {
|
|
294
304
|
provider,
|
|
295
305
|
modelId,
|
|
296
306
|
...(metadataProvider ? { jawcodeProvider: metadataProvider } : {}),
|
|
297
|
-
cost4: overlay.cost4,
|
|
307
|
+
cost4: multiplyCursorFastCost(overlay.cost4, provider, modelId),
|
|
298
308
|
source: "expected",
|
|
299
309
|
sourceRef: overlay.source,
|
|
300
310
|
verifiedAt: overlay.verifiedAt,
|
|
301
311
|
status: overlay.status,
|
|
312
|
+
...cursorFastPriceProvenance(provider, modelId),
|
|
302
313
|
};
|
|
303
314
|
}
|
|
304
315
|
|
|
316
|
+
function multiplyCursorFastCost(cost4: Cost4, provider: string, modelId: string): Cost4 {
|
|
317
|
+
const multiplier = cursorFastPriceMultiplier(provider, modelId);
|
|
318
|
+
if (multiplier === 1) return cost4;
|
|
319
|
+
return {
|
|
320
|
+
input: cost4.input * multiplier,
|
|
321
|
+
output: cost4.output * multiplier,
|
|
322
|
+
cacheRead: cost4.cacheRead * multiplier,
|
|
323
|
+
cacheWrite: cost4.cacheWrite * multiplier,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cursorFastPriceProvenance(provider: string, modelId: string): Pick<MatchedPrice, "sourceRef" | "status"> | Record<never, never> {
|
|
328
|
+
const sourceRef = cursorFastPriceSource(provider, modelId);
|
|
329
|
+
return sourceRef ? { sourceRef, status: "verified" } : {};
|
|
330
|
+
}
|
|
331
|
+
|
|
305
332
|
/** User-configured overlay match; explicit zero rates are authoritative too. */
|
|
306
333
|
function userOverlayMatch(
|
|
307
334
|
provider: string,
|