@bitkyc08/opencodex 2.7.27 → 2.7.28
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.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-DyBPh28A.css +1 -0
- package/gui/dist/assets/index-TZysP4q4.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google-antigravity-wire.ts +15 -7
- package/src/bridge.ts +19 -1
- package/src/cli/provider.ts +11 -9
- package/src/codex/auth-api.ts +68 -5
- package/src/codex/auth-collision.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +40 -13
- package/src/oauth/index.ts +16 -2
- package/src/providers/base-url-choices.ts +36 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/registry.ts +21 -2
- package/src/responses/parser.ts +4 -2
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index.ts +3 -0
- package/src/server/management-api.ts +144 -13
- package/src/server/relay.ts +41 -0
- package/src/server/request-log.ts +26 -0
- package/src/server/responses.ts +22 -1
- package/src/stall-timeout.ts +20 -0
- package/src/storage/scanner.ts +223 -0
- package/src/types.ts +4 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +112 -0
- package/src/usage/log.ts +12 -0
- package/src/usage/summary.ts +35 -0
- package/src/web-search/index.ts +2 -3
- package/src/web-search/loop.ts +3 -0
- package/gui/dist/assets/index-BnrJO9Wz.css +0 -1
- package/gui/dist/assets/index-Vcr0pzdO.js +0 -40
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { resolveCodexHomeDir } from "../codex/home";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read-only CODEX_HOME storage scanner — Phase 1 of the Storage page epic
|
|
8
|
+
* (devlog/_plan/500_storage-page-session-cleanup). Pure measurement: sizes via
|
|
9
|
+
* fs.stat walks, DB row counts via short-timeout readonly opens that degrade to
|
|
10
|
+
* null on lock/corruption. Performs zero writes under CODEX_HOME.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type StorageBucketKey =
|
|
14
|
+
| "sessions"
|
|
15
|
+
| "archived_sessions"
|
|
16
|
+
| "logs_db"
|
|
17
|
+
| "state_db"
|
|
18
|
+
| "attachments"
|
|
19
|
+
| "deletion_manifests"
|
|
20
|
+
| "other";
|
|
21
|
+
|
|
22
|
+
export interface StorageLargestEntry {
|
|
23
|
+
/** Path relative to CODEX_HOME, forward-slash separated on every platform. */
|
|
24
|
+
path: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StorageBucket {
|
|
29
|
+
key: StorageBucketKey;
|
|
30
|
+
label: string;
|
|
31
|
+
bytes: number;
|
|
32
|
+
fileCount: number;
|
|
33
|
+
/** Epoch ms of the oldest/newest file mtime; absent for empty buckets. */
|
|
34
|
+
oldest?: number;
|
|
35
|
+
newest?: number;
|
|
36
|
+
largest?: StorageLargestEntry[];
|
|
37
|
+
/** sqlite buckets only: row count from the newest versioned DB, null when locked/unreadable. */
|
|
38
|
+
rows?: number | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StorageReport {
|
|
42
|
+
codexHome: string;
|
|
43
|
+
generatedAt: number;
|
|
44
|
+
total: { bytes: number; fileCount: number };
|
|
45
|
+
buckets: StorageBucket[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const LARGEST_CAP = 5;
|
|
49
|
+
|
|
50
|
+
const BUCKET_LABELS: Record<StorageBucketKey, string> = {
|
|
51
|
+
sessions: "Active sessions",
|
|
52
|
+
archived_sessions: "Archived sessions",
|
|
53
|
+
logs_db: "Logs database",
|
|
54
|
+
state_db: "State database",
|
|
55
|
+
attachments: "Attachments",
|
|
56
|
+
deletion_manifests: "Deletion manifests",
|
|
57
|
+
other: "Other",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Dirs under CODEX_HOME that map to a dedicated bucket; anything else is "other". */
|
|
61
|
+
const DIR_BUCKETS: Record<string, StorageBucketKey> = {
|
|
62
|
+
sessions: "sessions",
|
|
63
|
+
archived_sessions: "archived_sessions",
|
|
64
|
+
attachments: "attachments",
|
|
65
|
+
deletion_manifests: "deletion_manifests",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// state_5.sqlite / logs_2.sqlite carry a version suffix and live WAL/SHM siblings.
|
|
69
|
+
const STATE_DB_FILE = /^state_(\d+)\.sqlite(-wal|-shm)?$/;
|
|
70
|
+
const LOGS_DB_FILE = /^logs_(\d+)\.sqlite(-wal|-shm)?$/;
|
|
71
|
+
|
|
72
|
+
interface FileEntry {
|
|
73
|
+
relPath: string;
|
|
74
|
+
bytes: number;
|
|
75
|
+
mtimeMs: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Recursive fs.stat walk. Unreadable entries (races, broken symlinks) are skipped, never fatal. */
|
|
79
|
+
function walkFiles(dir: string, relPrefix: string, out: FileEntry[]): void {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const full = join(dir, entry.name);
|
|
88
|
+
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
89
|
+
try {
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
walkFiles(full, relPath, out);
|
|
92
|
+
} else if (entry.isFile()) {
|
|
93
|
+
const stat = statSync(full);
|
|
94
|
+
out.push({ relPath, bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
/* entry vanished mid-scan — diagnostics tolerate racy trees */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildBucket(key: StorageBucketKey, files: FileEntry[]): StorageBucket {
|
|
103
|
+
const bucket: StorageBucket = {
|
|
104
|
+
key,
|
|
105
|
+
label: BUCKET_LABELS[key],
|
|
106
|
+
bytes: 0,
|
|
107
|
+
fileCount: files.length,
|
|
108
|
+
};
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
bucket.bytes += file.bytes;
|
|
111
|
+
if (bucket.oldest === undefined || file.mtimeMs < bucket.oldest) bucket.oldest = file.mtimeMs;
|
|
112
|
+
if (bucket.newest === undefined || file.mtimeMs > bucket.newest) bucket.newest = file.mtimeMs;
|
|
113
|
+
}
|
|
114
|
+
if (files.length > 0) {
|
|
115
|
+
bucket.largest = [...files]
|
|
116
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
117
|
+
.slice(0, LARGEST_CAP)
|
|
118
|
+
.map(f => ({ path: f.relPath, bytes: f.bytes }));
|
|
119
|
+
}
|
|
120
|
+
return bucket;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Row count via a lock-safe readonly open (the same secondary-reader contract as
|
|
125
|
+
* codex/history-provider.ts): short busy timeout, and any lock/corruption/schema
|
|
126
|
+
* error degrades to null — "unknown", never a crash and never a write.
|
|
127
|
+
*/
|
|
128
|
+
function countRowsReadonly(dbPath: string, table: string): number | null {
|
|
129
|
+
try {
|
|
130
|
+
const db = new Database(dbPath, { readonly: true });
|
|
131
|
+
try {
|
|
132
|
+
db.exec("PRAGMA busy_timeout = 100");
|
|
133
|
+
const row = db.query<{ n: number }, []>(`SELECT count(*) AS n FROM "${table}"`).get();
|
|
134
|
+
return row?.n ?? null;
|
|
135
|
+
} finally {
|
|
136
|
+
db.close();
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Newest versioned DB main file (e.g. state_5.sqlite over state_4.sqlite), or null. */
|
|
144
|
+
function newestVersionedDb(names: string[], pattern: RegExp): string | null {
|
|
145
|
+
let best: string | null = null;
|
|
146
|
+
let bestVersion = -1;
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
const match = name.match(pattern);
|
|
149
|
+
if (!match || match[2]) continue; // -wal/-shm siblings never win
|
|
150
|
+
const version = Number(match[1]);
|
|
151
|
+
if (version > bestVersion) {
|
|
152
|
+
bestVersion = version;
|
|
153
|
+
best = name;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return best;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageReport {
|
|
160
|
+
const files: Record<StorageBucketKey, FileEntry[]> = {
|
|
161
|
+
sessions: [],
|
|
162
|
+
archived_sessions: [],
|
|
163
|
+
logs_db: [],
|
|
164
|
+
state_db: [],
|
|
165
|
+
attachments: [],
|
|
166
|
+
deletion_manifests: [],
|
|
167
|
+
other: [],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let rootNames: string[] = [];
|
|
171
|
+
try {
|
|
172
|
+
rootNames = readdirSync(codexHome);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// A missing home is a normal fresh-machine state — report zeros. Anything else
|
|
175
|
+
// (e.g. ENOTDIR: CODEX_HOME points at a file) is a broken setup the caller
|
|
176
|
+
// must surface as a scan failure, not silently render as an empty home.
|
|
177
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
178
|
+
if (code !== "ENOENT") throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const name of rootNames) {
|
|
182
|
+
const full = join(codexHome, name);
|
|
183
|
+
let stat;
|
|
184
|
+
try {
|
|
185
|
+
stat = statSync(full);
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (stat.isDirectory()) {
|
|
190
|
+
walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
|
|
191
|
+
} else if (stat.isFile()) {
|
|
192
|
+
const key: StorageBucketKey = STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
|
|
193
|
+
files[key].push({ relPath: name, bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const buckets = (Object.keys(files) as StorageBucketKey[]).map(key => buildBucket(key, files[key]));
|
|
198
|
+
|
|
199
|
+
const stateDbName = newestVersionedDb(rootNames, STATE_DB_FILE);
|
|
200
|
+
const stateBucket = buckets.find(b => b.key === "state_db");
|
|
201
|
+
if (stateBucket && stateBucket.fileCount > 0) {
|
|
202
|
+
stateBucket.rows = stateDbName ? countRowsReadonly(join(codexHome, stateDbName), "threads") : null;
|
|
203
|
+
}
|
|
204
|
+
const logsDbName = newestVersionedDb(rootNames, LOGS_DB_FILE);
|
|
205
|
+
const logsBucket = buckets.find(b => b.key === "logs_db");
|
|
206
|
+
if (logsBucket && logsBucket.fileCount > 0) {
|
|
207
|
+
logsBucket.rows = logsDbName ? countRowsReadonly(join(codexHome, logsDbName), "logs") : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let totalBytes = 0;
|
|
211
|
+
let totalFiles = 0;
|
|
212
|
+
for (const bucket of buckets) {
|
|
213
|
+
totalBytes += bucket.bytes;
|
|
214
|
+
totalFiles += bucket.fileCount;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
codexHome,
|
|
219
|
+
generatedAt: Date.now(),
|
|
220
|
+
total: { bytes: totalBytes, fileCount: totalFiles },
|
|
221
|
+
buckets,
|
|
222
|
+
};
|
|
223
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -430,7 +430,10 @@ export interface OcxConfig {
|
|
|
430
430
|
* those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
|
|
431
431
|
*/
|
|
432
432
|
proxy?: string;
|
|
433
|
-
/**
|
|
433
|
+
/**
|
|
434
|
+
* Upstream stall timeout (seconds). After this many seconds of no upstream data, emits
|
|
435
|
+
* response.incomplete. Default 300. Min 1.
|
|
436
|
+
*/
|
|
434
437
|
stallTimeoutSec?: number;
|
|
435
438
|
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default 200000. */
|
|
436
439
|
connectTimeoutMs?: number;
|
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected-price overlay for models whose jawcode cost rows are missing or all-zero
|
|
3
|
+
* (subscription/OAuth surfaces). Sourced from official pricing pages only
|
|
4
|
+
* (devlog/_plan/260720_toks_speed_price_columns/003 — Luna research, main-verified).
|
|
5
|
+
*
|
|
6
|
+
* Status semantics:
|
|
7
|
+
* - "verified": official page opened directly; the 4-tuple is the published API price.
|
|
8
|
+
* - "verified-derived": mapped from a verified base-model price (for example an
|
|
9
|
+
* effort-suffix variant); propagates `estimated=true` downstream.
|
|
10
|
+
* - "unverified": research lead only. NEVER registered here and never returned by
|
|
11
|
+
* the resolver — unverified prices live in the 003 §5 backlog until promoted.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface Cost4 {
|
|
15
|
+
input: number;
|
|
16
|
+
output: number;
|
|
17
|
+
cacheRead: number;
|
|
18
|
+
cacheWrite: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ExpectedPriceStatus = "verified" | "verified-derived" | "unverified";
|
|
22
|
+
|
|
23
|
+
export interface ExpectedPriceOverlay {
|
|
24
|
+
provider: string;
|
|
25
|
+
modelId: string;
|
|
26
|
+
cost4: Cost4;
|
|
27
|
+
source: string;
|
|
28
|
+
verifiedAt: string;
|
|
29
|
+
status: ExpectedPriceStatus;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const GEMINI_31_PRO: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 };
|
|
33
|
+
const GEMINI_35_FLASH: Cost4 = { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 };
|
|
34
|
+
const GEMINI_3_FLASH: Cost4 = { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 };
|
|
35
|
+
const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 };
|
|
36
|
+
const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
|
|
37
|
+
const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
|
|
38
|
+
const KIMI_K27_CODE_HIGHSPEED: Cost4 = { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 1.9 };
|
|
39
|
+
const KIMI_K26: Cost4 = { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 };
|
|
40
|
+
const KIMI_K25: Cost4 = { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 };
|
|
41
|
+
|
|
42
|
+
const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-06-18); cacheWrite=0: storage is billed per-hour, not per-token";
|
|
43
|
+
const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
|
|
44
|
+
const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
|
|
45
|
+
// Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
|
|
46
|
+
// cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
|
|
47
|
+
const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cacheWrite derived = input, Kimi auto-cache has no write billing)";
|
|
48
|
+
|
|
49
|
+
export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
50
|
+
// MiniMax M2.1 highspeed — published PAYG price (verified).
|
|
51
|
+
{ provider: "minimax", modelId: "MiniMax-M2.1-highspeed", cost4: MINIMAX_M21_HIGHSPEED, source: MINIMAX_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
52
|
+
{ provider: "minimax-cn", modelId: "MiniMax-M2.1-highspeed", cost4: MINIMAX_M21_HIGHSPEED, source: MINIMAX_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
53
|
+
// DeepSeek current-generation IDs (verified; cache-hit price mapped to cacheRead).
|
|
54
|
+
{ provider: "deepseek", modelId: "deepseek-chat", cost4: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0 }, source: DEEPSEEK_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
55
|
+
{ provider: "deepseek", modelId: "deepseek-reasoner", cost4: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0 }, source: DEEPSEEK_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
56
|
+
// Google Antigravity effort-suffix variants — derived from the verified base-model
|
|
57
|
+
// price (Google does not publish per-suffix prices; Agent inference bills at the
|
|
58
|
+
// base model's standard rate per the official Billing FAQ).
|
|
59
|
+
{ provider: "google-antigravity", modelId: "gemini-3.1-pro-low", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
60
|
+
{ provider: "google-antigravity", modelId: "gemini-3.1-pro-high", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
61
|
+
{ provider: "google-antigravity", modelId: "gemini-3.5-flash-extra-low", cost4: GEMINI_35_FLASH, source: `derived: gemini-3.5-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
62
|
+
{ provider: "google-antigravity", modelId: "gemini-3.5-flash-low", cost4: GEMINI_35_FLASH, source: `derived: gemini-3.5-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
63
|
+
{ provider: "google-antigravity", modelId: "gemini-3.5-flash-mid", cost4: GEMINI_35_FLASH, source: `derived: gemini-3.5-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
64
|
+
{ provider: "google-antigravity", modelId: "gemini-3.5-flash-high", cost4: GEMINI_35_FLASH, source: `derived: gemini-3.5-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
65
|
+
{ provider: "google-antigravity", modelId: "gemini-3-flash-agent", cost4: GEMINI_3_FLASH, source: `derived: gemini-3-flash + Agent billing principle ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
66
|
+
// Google Vertex/Gemini API current model (verified — published table).
|
|
67
|
+
{ provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
68
|
+
// Antigravity-bundled third-party models — derived from the underlying vendor's
|
|
69
|
+
// official API price (Antigravity itself bills via subscription quota).
|
|
70
|
+
{ provider: "google-antigravity", modelId: "claude-sonnet-4-6", cost4: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, source: "derived: anthropic official https://platform.claude.com/docs/en/about-claude/pricing (5m cache-write; 1h is $6)", verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
71
|
+
{ provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, source: "derived: anthropic official https://platform.claude.com/docs/en/about-claude/pricing (5m cache-write; 1h is $10)", verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
72
|
+
{ provider: "google-antigravity", modelId: "gpt-oss-120b-medium", cost4: { input: 0.03, output: 0.15, cacheRead: 0, cacheWrite: 0 }, source: "derived: gpt-oss-120b open-weights — OpenRouter advertised lowest https://openrouter.ai/openai/gpt-oss-120b/providers", verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
73
|
+
// Kimi / Moonshot — official price tables are now published (2026-07-20 re-check;
|
|
74
|
+
// previously empty). kimi = Kimi Code OAuth surface, moonshot = CN key surface,
|
|
75
|
+
// kimi-code = API key surface (expected list price, not actual billing).
|
|
76
|
+
{ provider: "kimi", modelId: "k3", cost4: KIMI_K3, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
77
|
+
{ provider: "kimi", modelId: "k3[1m]", cost4: KIMI_K3, source: `derived: k3 (official docs: k3[1m] is the 1M-context compat notation for k3) ${KIMI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
78
|
+
{ provider: "kimi", modelId: "kimi-k2.7-code", cost4: KIMI_K27_CODE, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
79
|
+
{ provider: "kimi", modelId: "kimi-k2.7-code-highspeed", cost4: KIMI_K27_CODE_HIGHSPEED, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
80
|
+
{ provider: "kimi", modelId: "kimi-k2.6", cost4: KIMI_K26, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
81
|
+
{ provider: "kimi", modelId: "kimi-k2.5", cost4: KIMI_K25, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
82
|
+
{ provider: "kimi", modelId: "kimi-for-coding", cost4: KIMI_K27_CODE, source: `derived: kimi-k2.7-code (Kimi Code maps to K2.7 Code per official model docs) ${KIMI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
83
|
+
{ provider: "moonshot", modelId: "kimi-k3", cost4: KIMI_K3, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
84
|
+
{ provider: "moonshot", modelId: "kimi-k2.7-code", cost4: KIMI_K27_CODE, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
85
|
+
{ provider: "moonshot", modelId: "kimi-k2.7-code-highspeed", cost4: KIMI_K27_CODE_HIGHSPEED, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
86
|
+
{ provider: "moonshot", modelId: "kimi-k2.6", cost4: KIMI_K26, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
87
|
+
{ provider: "moonshot", modelId: "kimi-k2.5", cost4: KIMI_K25, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
88
|
+
{ provider: "kimi-code", modelId: "k3", cost4: KIMI_K3, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
89
|
+
{ provider: "kimi-code", modelId: "k3[1m]", cost4: KIMI_K3, source: `derived: k3 ${KIMI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
90
|
+
{ provider: "kimi-code", modelId: "kimi-k2.7-code", cost4: KIMI_K27_CODE, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
91
|
+
{ provider: "kimi-code", modelId: "kimi-k2.7-code-highspeed", cost4: KIMI_K27_CODE_HIGHSPEED, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
92
|
+
{ provider: "kimi-code", modelId: "kimi-k2.6", cost4: KIMI_K26, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
93
|
+
{ provider: "kimi-code", modelId: "kimi-k2.5", cost4: KIMI_K25, source: KIMI_PRICING, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
94
|
+
{ provider: "kimi-code", modelId: "kimi-for-coding", cost4: KIMI_K27_CODE, source: `derived: kimi-k2.7-code ${KIMI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
95
|
+
// Cursor Auto router — Cursor's published fixed token price (verified).
|
|
96
|
+
{ provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Exact-key overlay lookup. Returns verified first, then verified-derived.
|
|
101
|
+
* NEVER returns "unverified" rows — fail-closed is enforced in code, not just docs.
|
|
102
|
+
* No fuzzy / case-fold / wire-model fallback.
|
|
103
|
+
*/
|
|
104
|
+
export function findExpectedPriceOverlay(
|
|
105
|
+
provider: string,
|
|
106
|
+
modelId: string,
|
|
107
|
+
overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
|
|
108
|
+
): ExpectedPriceOverlay | undefined {
|
|
109
|
+
const exact = overlays.filter(row => row.provider === provider && row.modelId === modelId);
|
|
110
|
+
return exact.find(row => row.status === "verified")
|
|
111
|
+
?? exact.find(row => row.status === "verified-derived");
|
|
112
|
+
}
|
package/src/usage/log.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface PersistedUsageAttempt {
|
|
|
20
20
|
adapter: string;
|
|
21
21
|
status: number;
|
|
22
22
|
durationMs: number;
|
|
23
|
+
/** TTFT relative to THIS attempt's start (WP4); unset for non-streaming/tool-only. */
|
|
24
|
+
firstOutputMs?: number;
|
|
23
25
|
sendCount: number;
|
|
24
26
|
recoveryKinds: AttemptRecoveryKind[];
|
|
25
27
|
usageStatus: UsageStatus;
|
|
@@ -39,6 +41,8 @@ export interface PersistedUsageEntry {
|
|
|
39
41
|
requestedModel?: string;
|
|
40
42
|
status: number;
|
|
41
43
|
durationMs: number;
|
|
44
|
+
/** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
|
|
45
|
+
firstOutputMs?: number;
|
|
42
46
|
usageStatus: UsageStatus;
|
|
43
47
|
usage?: OcxUsage;
|
|
44
48
|
totalTokens?: number;
|
|
@@ -151,6 +155,8 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
151
155
|
}
|
|
152
156
|
if ("inputTokenEstimate" in attempt
|
|
153
157
|
&& !isNonNegativeFiniteNumber(attempt.inputTokenEstimate)) return null;
|
|
158
|
+
if ("firstOutputMs" in attempt
|
|
159
|
+
&& !isNonNegativeFiniteNumber(attempt.firstOutputMs)) return null;
|
|
154
160
|
if ("totalTokens" in attempt
|
|
155
161
|
&& !isNonNegativeFiniteNumber(attempt.totalTokens)) return null;
|
|
156
162
|
const usage = "usage" in attempt ? normalizeAttemptUsage(attempt.usage) : undefined;
|
|
@@ -168,6 +174,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
168
174
|
adapter: attempt.adapter,
|
|
169
175
|
status: attempt.status,
|
|
170
176
|
durationMs: attempt.durationMs,
|
|
177
|
+
...(isNonNegativeFiniteNumber(attempt.firstOutputMs)
|
|
178
|
+
? { firstOutputMs: attempt.firstOutputMs }
|
|
179
|
+
: {}),
|
|
171
180
|
sendCount: attempt.sendCount as number,
|
|
172
181
|
recoveryKinds,
|
|
173
182
|
usageStatus: attempt.usageStatus as UsageStatus,
|
|
@@ -200,6 +209,9 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
200
209
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
201
210
|
status: entry.status,
|
|
202
211
|
durationMs: entry.durationMs,
|
|
212
|
+
...(isNonNegativeFiniteNumber(entry.firstOutputMs)
|
|
213
|
+
? { firstOutputMs: entry.firstOutputMs }
|
|
214
|
+
: {}),
|
|
203
215
|
usageStatus: entry.usageStatus,
|
|
204
216
|
...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
|
|
205
217
|
...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
|
package/src/usage/summary.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { baseProviderLabel } from "../providers/label";
|
|
2
2
|
import { usageDisplayTotalTokens } from "./totals";
|
|
3
3
|
import type { PersistedUsageEntry, UsageStatus } from "./log";
|
|
4
|
+
import { estimateComboCost, estimateRequestCost } from "./cost";
|
|
4
5
|
|
|
5
6
|
export type UsageRange = "7d" | "30d" | "all";
|
|
6
7
|
export type UsageSurface = "all" | "codex" | "claude";
|
|
@@ -21,6 +22,15 @@ export interface UsageSummaryTotals {
|
|
|
21
22
|
reasoningOutputTokens: number;
|
|
22
23
|
totalTokens: number;
|
|
23
24
|
coverageRatio: number;
|
|
25
|
+
/** Display-time estimated cost in USD for the filtered window (WP6, devlog 004).
|
|
26
|
+
* Sums per-request estimateRequestCost / per-attempt combo costs; requests whose
|
|
27
|
+
* price is unmatched are excluded from the sum and counted separately. */
|
|
28
|
+
estimatedCostUsd: number;
|
|
29
|
+
pricedRequests: number;
|
|
30
|
+
/** Requests with usage but no matched price anywhere (excluded from the sum). */
|
|
31
|
+
unpricedRequests: number;
|
|
32
|
+
/** Requests whose usage itself is missing/unsupported, so no cost can be computed. */
|
|
33
|
+
unmeteredRequests: number;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
export interface UsageDay {
|
|
@@ -127,6 +137,10 @@ function blankTotals(): UsageSummaryTotals {
|
|
|
127
137
|
reasoningOutputTokens: 0,
|
|
128
138
|
totalTokens: 0,
|
|
129
139
|
coverageRatio: 0,
|
|
140
|
+
estimatedCostUsd: 0,
|
|
141
|
+
pricedRequests: 0,
|
|
142
|
+
unpricedRequests: 0,
|
|
143
|
+
unmeteredRequests: 0,
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
|
|
@@ -215,6 +229,26 @@ function finalizeCoverage(totals: UsageSummaryTotals): void {
|
|
|
215
229
|
totals.coverageRatio = totals.requests === 0 ? 0 : totals.measuredRequests / totals.requests;
|
|
216
230
|
}
|
|
217
231
|
|
|
232
|
+
function addEstimatedCost(
|
|
233
|
+
totals: UsageSummaryTotals,
|
|
234
|
+
entry: Pick<PersistedUsageEntry, "provider" | "model" | "usageStatus" | "usage" | "attempts">,
|
|
235
|
+
): void {
|
|
236
|
+
if (entry.usageStatus === "unreported" || entry.usageStatus === "unsupported"
|
|
237
|
+
|| (!entry.usage && !entry.attempts?.length)) {
|
|
238
|
+
totals.unmeteredRequests += 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const estimate = entry.attempts?.length
|
|
242
|
+
? estimateComboCost(entry.attempts)
|
|
243
|
+
: estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus });
|
|
244
|
+
if (!estimate) {
|
|
245
|
+
totals.unpricedRequests += 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
totals.pricedRequests += 1;
|
|
249
|
+
totals.estimatedCostUsd += estimate.cost.total;
|
|
250
|
+
}
|
|
251
|
+
|
|
218
252
|
function buildDayGrid(range: UsageRange, since: number | null, now: number, entries: PersistedUsageEntry[]): UsageDay[] {
|
|
219
253
|
const window = rangeWindow(range, now);
|
|
220
254
|
const days = range === "all" ? dayCountForAllRange(entries, now) : window.days;
|
|
@@ -385,6 +419,7 @@ export function summarizeUsage(
|
|
|
385
419
|
bumpStatus(totals, entry.usageStatus);
|
|
386
420
|
totals.attemptCount += entry.attempts?.length ?? 1;
|
|
387
421
|
addTokens(totals, entry);
|
|
422
|
+
addEstimatedCost(totals, entry);
|
|
388
423
|
}
|
|
389
424
|
finalizeCoverage(totals);
|
|
390
425
|
return {
|
package/src/web-search/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { modelInList } from "../types";
|
|
|
3
3
|
import type { SidecarSettings } from "./executor";
|
|
4
4
|
import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
5
5
|
import { getAccountSet } from "../oauth/store";
|
|
6
|
+
import { DEFAULT_STALL_TIMEOUT_SEC } from "../stall-timeout";
|
|
6
7
|
|
|
7
8
|
export { runWithWebSearch } from "./loop";
|
|
8
9
|
export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
@@ -18,8 +19,6 @@ const DEFAULT_MAX_SEARCHES = 3;
|
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = 200_000;
|
|
19
20
|
const DEFAULT_ROUTED_MODEL_STALL_TIMEOUT_MS = 200_000;
|
|
20
21
|
const MAX_ROUTED_MODEL_STALL_TIMEOUT_MS = 2_147_483_647;
|
|
21
|
-
// Mirrors the bridge's stall default (bridge.ts `options?.stallTimeoutSec ?? 90`).
|
|
22
|
-
const DEFAULT_STALL_TIMEOUT_SEC = 90;
|
|
23
22
|
const STALL_MARGIN_SEC = 30;
|
|
24
23
|
|
|
25
24
|
/**
|
|
@@ -47,7 +46,7 @@ function finiteCeil(value: number | undefined, fallback: number): number {
|
|
|
47
46
|
* are individually bounded by the configured bridge stall, response-header connect timeout,
|
|
48
47
|
* routed-model response-body inactivity timeout, or sidecar timeout. The stall deadline must cover
|
|
49
48
|
* the largest unit plus a margin;
|
|
50
|
-
* otherwise a legitimately slow search trips the bridge's
|
|
49
|
+
* otherwise a legitimately slow search trips the bridge's default upstream_stall_timeout and
|
|
51
50
|
* kills the whole turn. Stays finite so a genuine hang is still cut off.
|
|
52
51
|
*/
|
|
53
52
|
export function webSearchStallTimeoutSec(
|
package/src/web-search/loop.ts
CHANGED
|
@@ -186,6 +186,8 @@ export interface WebSearchLoopDeps {
|
|
|
186
186
|
* sidecar search, so a legitimately slow-but-progressing unit never trips the bridge watchdog.
|
|
187
187
|
*/
|
|
188
188
|
stallTimeoutSec?: number;
|
|
189
|
+
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
190
|
+
onFirstOutput?: () => void;
|
|
189
191
|
/**
|
|
190
192
|
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
191
193
|
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
@@ -550,6 +552,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
550
552
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
551
553
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
552
554
|
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|
|
555
|
+
...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
|
|
553
556
|
},
|
|
554
557
|
);
|
|
555
558
|
return new Response(sse, { headers: SSE_HEADERS });
|