@bitkyc08/opencodex 2.11.0 → 2.11.1
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/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
|
-
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS } from "./antigravity-models";
|
|
3
|
+
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "./antigravity-models";
|
|
4
4
|
import type { ProviderBaseUrlChoice } from "./base-url-choices";
|
|
5
5
|
import {
|
|
6
6
|
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
|
|
@@ -1287,7 +1287,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1287
1287
|
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
|
|
1288
1288
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
1289
1289
|
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
|
|
1290
|
-
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels:
|
|
1290
|
+
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
1291
1291
|
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
|
|
1292
1292
|
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
1293
1293
|
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
@@ -1299,11 +1299,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1299
1299
|
adapter: "openai-chat",
|
|
1300
1300
|
authKind: "key",
|
|
1301
1301
|
dashboardUrl: "https://platform.deepseek.com/api_keys",
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1302
|
+
// Route DeepSeek's own catalog bundle so routed rebuilds restore the official
|
|
1303
|
+
// context window from the vendored model-metadata bundle instead of falling
|
|
1304
|
+
// back to the 128k strict-fields default (scripts/model-metadata.source.json,
|
|
1305
|
+
// verified 2026-08-08).
|
|
1306
|
+
jawcodeBundle: "deepseek",
|
|
1307
|
+
// deepseek-chat/deepseek-reasoner were deprecated upstream on 2026-07-24 15:59 UTC;
|
|
1308
|
+
// official identifiers are now deepseek-v4-flash / deepseek-v4-pro. They stay in
|
|
1309
|
+
// the list only as compatibility aliases so existing saved configs and requests
|
|
1310
|
+
// keep validating and routing (they previously mapped to v4-flash; devlog
|
|
1311
|
+
// _fin/260710_provider_hardening/002_research_cn.md). The current offerings are
|
|
1312
|
+
// the V4 ids — defaultModel and the model-specific wiring above use them.
|
|
1304
1313
|
models: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS],
|
|
1305
1314
|
defaultModel: "deepseek-v4-flash",
|
|
1306
|
-
|
|
1315
|
+
// Official DeepSeek Codex setup (codex-deepseek-setup.sh) advertises 1,048,576
|
|
1316
|
+
// for both V4 models; the older 1,000,000 figure was a rounded approximation.
|
|
1317
|
+
modelContextWindows: { "deepseek-v4-flash": 1_048_576, "deepseek-v4-pro": 1_048_576 },
|
|
1307
1318
|
// DeepSeek documents V4-Flash as a native Responses API model adapted for Codex. The
|
|
1308
1319
|
// API id is `deepseek-v4-flash`; `DeepSeek-V4-Flash-0731` is a release/version label.
|
|
1309
1320
|
modelWireDefaults: {
|
|
@@ -56,12 +56,19 @@ export function slugEquals(stored: string, provider: string, id: string): boolea
|
|
|
56
56
|
return stored === `${provider}/${id}` || stored === routedSlug(provider, id);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Stable key for the exact equivalence relation used by catalog/config slug matching. */
|
|
60
|
+
export function slugEquivalenceKey(slug: string): string {
|
|
61
|
+
const slash = slug.indexOf("/");
|
|
62
|
+
return slash <= 0
|
|
63
|
+
? JSON.stringify(["exact", slug])
|
|
64
|
+
: JSON.stringify([
|
|
65
|
+
"routed",
|
|
66
|
+
slug.slice(0, slash),
|
|
67
|
+
encodeRoutedModelId(slug.slice(slash + 1)),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/** Equivalence between two routed slugs regardless of raw/encoded mix. */
|
|
60
72
|
export function slugsEquivalent(a: string, b: string): boolean {
|
|
61
|
-
|
|
62
|
-
const pa = a.indexOf("/");
|
|
63
|
-
const pb = b.indexOf("/");
|
|
64
|
-
if (pa <= 0 || pb <= 0) return false;
|
|
65
|
-
if (a.slice(0, pa) !== b.slice(0, pb)) return false;
|
|
66
|
-
return encodeRoutedModelId(a.slice(pa + 1)) === encodeRoutedModelId(b.slice(pb + 1));
|
|
73
|
+
return a === b || slugEquivalenceKey(a) === slugEquivalenceKey(b);
|
|
67
74
|
}
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -19,6 +19,28 @@ export function isCodexReasoningEffort(effort: string): boolean {
|
|
|
19
19
|
return CODEX_REASONING_SET.has(effort);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Reasoning ladder accepted for the OpenAI vision sidecar. `ultra` is deliberately excluded:
|
|
24
|
+
* the vision describer is a single helper call, and `ultra` would be collapsed to `max` by the
|
|
25
|
+
* upstream client boundary anyway.
|
|
26
|
+
*/
|
|
27
|
+
export const VISION_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
28
|
+
export type VisionReasoningEffort = typeof VISION_REASONING_EFFORTS[number];
|
|
29
|
+
|
|
30
|
+
/** True when `effort` is one of the vision sidecar's supported Responses reasoning levels. */
|
|
31
|
+
export function isVisionReasoningEffort(effort: unknown): effort is VisionReasoningEffort {
|
|
32
|
+
return typeof effort === "string" && (VISION_REASONING_EFFORTS as readonly string[]).includes(effort);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a persisted/configured vision reasoning value. Invalid values (hand-edited config,
|
|
37
|
+
* stale files) degrade to `undefined` so the caller falls back to the documented default instead
|
|
38
|
+
* of forwarding an upstream-rejected effort.
|
|
39
|
+
*/
|
|
40
|
+
export function sanitizeVisionReasoning(effort: unknown): VisionReasoningEffort | undefined {
|
|
41
|
+
return isVisionReasoningEffort(effort) ? effort : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
/** Position of `effort` in the Codex ladder (low=0 .. ultra=5), or -1 when not a ladder member. */
|
|
23
45
|
export function codexEffortRank(effort: string): number {
|
|
24
46
|
return CODEX_REASONING_ORDER.indexOf(effort);
|
package/src/router.ts
CHANGED
|
@@ -22,8 +22,6 @@ import {
|
|
|
22
22
|
import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
|
|
23
23
|
import { getStaleCached } from "./codex/model-cache";
|
|
24
24
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
25
|
-
import { getEffectiveActiveCodexAccountId } from "./codex/routing";
|
|
26
|
-
import { getAccountSet } from "./oauth/store";
|
|
27
25
|
import {
|
|
28
26
|
buildRouteDecisionTrace,
|
|
29
27
|
type RouteDecisionKind,
|
|
@@ -513,24 +511,6 @@ function routeModelInternal(
|
|
|
513
511
|
quota: quotaEvidenceForCandidate({
|
|
514
512
|
provider: candidate.provider,
|
|
515
513
|
model: candidate.model,
|
|
516
|
-
...(candidate.provider === OPENAI_CODEX_PROVIDER_ID
|
|
517
|
-
&& providerCodexAccountMode(
|
|
518
|
-
OPENAI_CODEX_PROVIDER_ID,
|
|
519
|
-
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
520
|
-
) === "pool"
|
|
521
|
-
? (() => {
|
|
522
|
-
const codexAccountId = getEffectiveActiveCodexAccountId(config);
|
|
523
|
-
return {
|
|
524
|
-
codexAccountId,
|
|
525
|
-
codexAccountPlan: codexAccountId
|
|
526
|
-
? config.codexAccounts?.find(account => account.id === codexAccountId)?.plan
|
|
527
|
-
: undefined,
|
|
528
|
-
};
|
|
529
|
-
})()
|
|
530
|
-
: {}),
|
|
531
|
-
accountRef: candidate.provider === "anthropic"
|
|
532
|
-
? getAccountSet("anthropic")?.activeAccountId
|
|
533
|
-
: undefined,
|
|
534
514
|
}),
|
|
535
515
|
cost: costEvidenceForCandidate({
|
|
536
516
|
provider: candidate.provider,
|
|
@@ -76,6 +76,11 @@ export interface RequestHistoryPage {
|
|
|
76
76
|
export const REQUEST_HISTORY_MAX_PAGE_SIZE = 100;
|
|
77
77
|
export const REQUEST_HISTORY_DEFAULT_PAGE_SIZE = 50;
|
|
78
78
|
export const REQUEST_HISTORY_INSERT_BATCH = 500;
|
|
79
|
+
export const REQUEST_HISTORY_READ_CHUNK_BYTES = 64 * 1024;
|
|
80
|
+
// The SQLite index is a disposable projection. Complete JSONL records above
|
|
81
|
+
// this bound are omitted from the projection; the canonical usage.jsonl is
|
|
82
|
+
// never truncated or rewritten by the indexer.
|
|
83
|
+
export const REQUEST_HISTORY_MAX_RECORD_BYTES = 1024 * 1024;
|
|
79
84
|
|
|
80
85
|
let db: Database | null = null;
|
|
81
86
|
let dbPath = "";
|
|
@@ -190,24 +195,6 @@ INSERT OR IGNORE INTO requests (
|
|
|
190
195
|
?, ?, ?, ?, ?
|
|
191
196
|
)`;
|
|
192
197
|
|
|
193
|
-
function readCompleteTail(fd: number, fromOffset: number, size: number): { text: string; nextOffset: number } {
|
|
194
|
-
if (size <= fromOffset) return { text: "", nextOffset: fromOffset };
|
|
195
|
-
const length = size - fromOffset;
|
|
196
|
-
const buf = Buffer.allocUnsafe(length);
|
|
197
|
-
let offset = 0;
|
|
198
|
-
while (offset < length) {
|
|
199
|
-
const read = readSync(fd, buf, offset, length - offset, fromOffset + offset);
|
|
200
|
-
if (read === 0) throw new Error("usage log changed while indexing");
|
|
201
|
-
offset += read;
|
|
202
|
-
}
|
|
203
|
-
const newline = buf.lastIndexOf(0x0a);
|
|
204
|
-
if (newline < 0) return { text: "", nextOffset: fromOffset };
|
|
205
|
-
return {
|
|
206
|
-
text: buf.subarray(0, newline).toString("utf-8"),
|
|
207
|
-
nextOffset: fromOffset + newline + 1,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
198
|
function parsedEntryFromLine(line: string): PersistedUsageEntry | null {
|
|
212
199
|
if (!line.trim()) return null;
|
|
213
200
|
try {
|
|
@@ -227,12 +214,11 @@ function parsedEntryFromLine(line: string): PersistedUsageEntry | null {
|
|
|
227
214
|
return null;
|
|
228
215
|
}
|
|
229
216
|
|
|
230
|
-
function
|
|
231
|
-
if (!text) return 0;
|
|
232
|
-
const lines = text.split(/\r?\n/);
|
|
217
|
+
function ingestSourceTail(dbHandle: Database, path: string, fromOffset: number): number {
|
|
233
218
|
let inserted = 0;
|
|
234
219
|
let pending: Array<Array<string | number | null>> = [];
|
|
235
220
|
const insert = dbHandle.prepare(ROW_INSERT);
|
|
221
|
+
let fd: number | undefined;
|
|
236
222
|
try {
|
|
237
223
|
const commitBatch = () => {
|
|
238
224
|
dbHandle.transaction((rows: Array<Array<string | number | null>>) => {
|
|
@@ -243,29 +229,55 @@ function ingestText(dbHandle: Database, text: string): number {
|
|
|
243
229
|
})(pending);
|
|
244
230
|
pending = [];
|
|
245
231
|
};
|
|
246
|
-
|
|
247
|
-
const entry = parsedEntryFromLine(line);
|
|
248
|
-
if (!entry)
|
|
232
|
+
const ingestLine = (line: Buffer) => {
|
|
233
|
+
const entry = parsedEntryFromLine(line.toString("utf-8"));
|
|
234
|
+
if (!entry) return;
|
|
249
235
|
pending.push(extractRow(entry));
|
|
250
236
|
if (pending.length >= REQUEST_HISTORY_INSERT_BATCH) commitBatch();
|
|
251
|
-
}
|
|
252
|
-
if (pending.length > 0) commitBatch();
|
|
253
|
-
} finally {
|
|
254
|
-
// Windows file locks: an unterminated prepared statement keeps the DB
|
|
255
|
-
// file busy after close (verified on Bun 1.3.14). Finalize always.
|
|
256
|
-
insert.finalize();
|
|
257
|
-
}
|
|
258
|
-
return inserted;
|
|
259
|
-
}
|
|
237
|
+
};
|
|
260
238
|
|
|
261
|
-
function ingestSourceTail(dbHandle: Database, path: string, fromOffset: number): number {
|
|
262
|
-
let fd: number | undefined;
|
|
263
|
-
try {
|
|
264
239
|
fd = openSync(path, "r");
|
|
265
240
|
const stat = fstatSync(fd);
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
241
|
+
const size = Number(stat.size);
|
|
242
|
+
const readBuffer = Buffer.allocUnsafe(REQUEST_HISTORY_READ_CHUNK_BYTES);
|
|
243
|
+
let position = fromOffset;
|
|
244
|
+
let nextOffset = fromOffset;
|
|
245
|
+
let fragments: Buffer[] = [];
|
|
246
|
+
let fragmentBytes = 0;
|
|
247
|
+
let oversized = false;
|
|
248
|
+
while (position < size) {
|
|
249
|
+
const requested = Math.min(readBuffer.length, size - position);
|
|
250
|
+
const bytesRead = readSync(fd, readBuffer, 0, requested, position);
|
|
251
|
+
if (bytesRead === 0) throw new Error("usage log changed while indexing");
|
|
252
|
+
let lineStart = 0;
|
|
253
|
+
for (let index = 0; index < bytesRead; index++) {
|
|
254
|
+
if (readBuffer[index] !== 0x0a) continue;
|
|
255
|
+
const segment = readBuffer.subarray(lineStart, index);
|
|
256
|
+
if (!oversized && fragmentBytes + segment.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
|
|
257
|
+
const line = fragments.length === 0
|
|
258
|
+
? segment
|
|
259
|
+
: Buffer.concat([...fragments, segment], fragmentBytes + segment.length);
|
|
260
|
+
ingestLine(line);
|
|
261
|
+
}
|
|
262
|
+
fragments = [];
|
|
263
|
+
fragmentBytes = 0;
|
|
264
|
+
oversized = false;
|
|
265
|
+
lineStart = index + 1;
|
|
266
|
+
nextOffset = position + index + 1;
|
|
267
|
+
}
|
|
268
|
+
const remainder = readBuffer.subarray(lineStart, bytesRead);
|
|
269
|
+
if (!oversized && fragmentBytes + remainder.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
|
|
270
|
+
// Copy because readBuffer is reused on the next iteration.
|
|
271
|
+
fragments.push(Buffer.from(remainder));
|
|
272
|
+
fragmentBytes += remainder.length;
|
|
273
|
+
} else if (remainder.length > 0) {
|
|
274
|
+
fragments = [];
|
|
275
|
+
fragmentBytes = 0;
|
|
276
|
+
oversized = true;
|
|
277
|
+
}
|
|
278
|
+
position += bytesRead;
|
|
279
|
+
}
|
|
280
|
+
if (pending.length > 0) commitBatch();
|
|
269
281
|
const current = readIndexedMeta(dbHandle);
|
|
270
282
|
setMeta(dbHandle, HISTORY_META_KEYS.indexedOffset, nextOffset);
|
|
271
283
|
setMeta(dbHandle, HISTORY_META_KEYS.indexedRows, current.indexedRows + inserted);
|
|
@@ -275,6 +287,9 @@ function ingestSourceTail(dbHandle: Database, path: string, fromOffset: number):
|
|
|
275
287
|
return inserted;
|
|
276
288
|
} finally {
|
|
277
289
|
if (fd !== undefined) closeSync(fd);
|
|
290
|
+
// Windows file locks: an unterminated prepared statement keeps the DB
|
|
291
|
+
// file busy after close (verified on Bun 1.3.14). Finalize always.
|
|
292
|
+
insert.finalize();
|
|
278
293
|
}
|
|
279
294
|
}
|
|
280
295
|
|
package/src/routing/quota.ts
CHANGED
|
@@ -12,63 +12,100 @@
|
|
|
12
12
|
* carries an account reference (dry-run/evaluate), never invented.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
codexQuotaWindowForPlan,
|
|
17
|
+
getAccountQuota,
|
|
18
|
+
isCodexQuotaExhausted,
|
|
19
|
+
listAccountQuotas,
|
|
20
|
+
} from "../codex/quota";
|
|
16
21
|
import { getCachedProviderAccountQuota } from "../providers/quota";
|
|
17
22
|
import type { RouteQuotaEvidence } from "./trace";
|
|
18
23
|
|
|
19
24
|
export interface QuotaEvidenceInput {
|
|
20
25
|
provider: string;
|
|
21
26
|
model: string;
|
|
22
|
-
/** Opaque account reference for per-account quota sources. */
|
|
23
27
|
accountRef?: string;
|
|
24
|
-
/** Codex pool account id (provider "openai"). */
|
|
25
28
|
codexAccountId?: string;
|
|
26
|
-
/** The account's plan (provider "openai"); selects the governing quota window. */
|
|
27
29
|
codexAccountPlan?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
export interface CodexPoolQuotaAccount {
|
|
33
|
+
accountId: string;
|
|
34
|
+
plan?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuotaEvidence {
|
|
38
|
+
const quota = getAccountQuota(accountId);
|
|
39
|
+
if (!quota) return { known: false };
|
|
40
|
+
const monthly = codexQuotaWindowForPlan(plan) === "monthly";
|
|
41
|
+
const percents = [
|
|
42
|
+
...(monthly ? [] : [quota.weeklyPercent]),
|
|
43
|
+
quota.monthlyPercent,
|
|
44
|
+
].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
45
|
+
const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
|
|
46
|
+
// Credits-only snapshots prove neither usage nor exhaustion. Unknown must not
|
|
47
|
+
// become healthy capacity merely because a cache row exists.
|
|
48
|
+
if (maxPercent === undefined) return { known: false };
|
|
49
|
+
const resets = [
|
|
50
|
+
...(monthly ? [] : [quota.weeklyResetAt]),
|
|
51
|
+
quota.monthlyResetAt,
|
|
52
|
+
].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
53
|
+
.filter(value => value > Date.now());
|
|
54
|
+
return {
|
|
55
|
+
known: true,
|
|
56
|
+
headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)),
|
|
57
|
+
exhausted: isCodexQuotaExhausted(quota, plan),
|
|
58
|
+
...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
|
|
59
|
+
source: "codex-pool",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function codexPoolQuotaEvidence(accounts: readonly CodexPoolQuotaAccount[]): RouteQuotaEvidence {
|
|
64
|
+
if (accounts.length === 0) return { known: false };
|
|
65
|
+
const evidence = accounts.map(account => codexAccountQuotaEvidence(account.accountId, account.plan));
|
|
66
|
+
const known = evidence.filter(item => item.known);
|
|
67
|
+
if (known.length === 0) return { known: false };
|
|
68
|
+
const usable = known.filter(item => item.exhausted !== true);
|
|
69
|
+
if (usable.length > 0) {
|
|
70
|
+
const headrooms = usable.map(item => item.headroom)
|
|
71
|
+
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
72
|
+
return {
|
|
73
|
+
known: true,
|
|
74
|
+
exhausted: false,
|
|
75
|
+
...(headrooms.length > 0 ? { headroom: Math.max(...headrooms) } : {}),
|
|
76
|
+
source: "codex-pool",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (known.length < evidence.length) return { known: false };
|
|
80
|
+
const resets = known.map(item => item.resetAtMs)
|
|
81
|
+
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
82
|
+
return {
|
|
83
|
+
known: true,
|
|
84
|
+
exhausted: true,
|
|
85
|
+
headroom: 0,
|
|
86
|
+
...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
|
|
87
|
+
source: "codex-pool",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
34
91
|
export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuotaEvidence {
|
|
35
92
|
if (input.provider === "openai" && input.codexAccountId) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
quota.monthlyPercent,
|
|
46
|
-
].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
47
|
-
const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
|
|
48
|
-
const resets = [
|
|
49
|
-
...(monthly ? [] : [quota.weeklyResetAt]),
|
|
50
|
-
quota.monthlyResetAt,
|
|
51
|
-
].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
52
|
-
.filter(value => value > Date.now());
|
|
53
|
-
return {
|
|
54
|
-
known: true,
|
|
55
|
-
...(maxPercent !== undefined
|
|
56
|
-
? { headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)) }
|
|
57
|
-
: {}),
|
|
58
|
-
exhausted: isCodexQuotaExhausted(quota, input.codexAccountPlan),
|
|
59
|
-
...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
|
|
60
|
-
source: "codex-pool",
|
|
61
|
-
};
|
|
93
|
+
// listAccountQuotas() is the reconciled quota snapshot: config-generation
|
|
94
|
+
// reconciliation prunes removed accounts. Other eligibility dimensions
|
|
95
|
+
// (pause/reauth/cooldown/soft-avoid) remain health/pool-selector concerns.
|
|
96
|
+
if (input.codexAccountPlan !== undefined) {
|
|
97
|
+
const pool = [...listAccountQuotas()].map(([accountId]) => ({
|
|
98
|
+
accountId,
|
|
99
|
+
...(accountId === input.codexAccountId ? { plan: input.codexAccountPlan } : {}),
|
|
100
|
+
}));
|
|
101
|
+
if (pool.length > 1) return codexPoolQuotaEvidence(pool);
|
|
62
102
|
}
|
|
103
|
+
return codexAccountQuotaEvidence(input.codexAccountId, input.codexAccountPlan);
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
if (input.provider === "anthropic" && input.accountRef) {
|
|
66
107
|
const quota = getCachedProviderAccountQuota("anthropic", input.accountRef);
|
|
67
108
|
if (quota) {
|
|
68
|
-
// Anthropic per-family buckets (e.g. Opus / Sonnet) are stricter than the
|
|
69
|
-
// broad account windows: fold the candidate model's matching bucket into
|
|
70
|
-
// headroom and exhaustion so a model-specific overage is not hidden by
|
|
71
|
-
// a healthy aggregate window.
|
|
72
109
|
const family = anthropicFamilyWindow(input.model, quota.customWindows ?? []);
|
|
73
110
|
const percents = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, family?.percent]
|
|
74
111
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
@@ -78,25 +115,16 @@ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuota
|
|
|
78
115
|
.filter(value => value > Date.now());
|
|
79
116
|
return {
|
|
80
117
|
known: true,
|
|
81
|
-
...(maxPercent !== undefined
|
|
82
|
-
? { headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)) }
|
|
83
|
-
: {}),
|
|
118
|
+
...(maxPercent !== undefined ? { headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)) } : {}),
|
|
84
119
|
exhausted: maxPercent !== undefined && maxPercent >= 100,
|
|
85
120
|
...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
|
|
86
121
|
source: "provider-report",
|
|
87
122
|
};
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
|
-
|
|
91
125
|
return { known: false };
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
/**
|
|
95
|
-
* Match the candidate model to an Anthropic per-family quota window. Window
|
|
96
|
-
* labels from the provider probe are "Opus" / "Sonnet"; model ids carry the
|
|
97
|
-
* family as a segment (e.g. `claude-opus-...`, `claude-sonnet-...`). Returns
|
|
98
|
-
* undefined when no family window is cached or no label matches.
|
|
99
|
-
*/
|
|
100
128
|
function anthropicFamilyWindow(
|
|
101
129
|
model: string,
|
|
102
130
|
windows: Array<{ label: string; percent?: number; resetAt?: number }>,
|
|
@@ -104,18 +132,11 @@ function anthropicFamilyWindow(
|
|
|
104
132
|
const normalized = model.toLowerCase();
|
|
105
133
|
for (const window of windows) {
|
|
106
134
|
const family = window.label.trim().toLowerCase();
|
|
107
|
-
if (family && normalized.includes(family))
|
|
108
|
-
return window;
|
|
109
|
-
}
|
|
135
|
+
if (family && normalized.includes(family)) return window;
|
|
110
136
|
}
|
|
111
137
|
return undefined;
|
|
112
138
|
}
|
|
113
139
|
|
|
114
|
-
/**
|
|
115
|
-
* Deterministic quota score in [0,1]: larger available headroom scores
|
|
116
|
-
* higher; exhausted evidence scores 0. Unknown evidence returns null so the
|
|
117
|
-
* caller can apply the profile's unknownEvidence policy.
|
|
118
|
-
*/
|
|
119
140
|
export function quotaScore(evidence: RouteQuotaEvidence | undefined): number | null {
|
|
120
141
|
if (!evidence || !evidence.known) return null;
|
|
121
142
|
if (evidence.exhausted === true) return 0;
|
package/src/server/index.ts
CHANGED
|
@@ -204,6 +204,22 @@ function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
|
|
|
204
204
|
ws.data.liveTurnAdmissionLease = undefined;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Send one live-sideband frame to the upstream socket.
|
|
209
|
+
*
|
|
210
|
+
* Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
|
|
211
|
+
* `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
|
|
212
|
+
* `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
|
|
213
|
+
* arriving from `node:buffer` still round-trips byte-for-byte.
|
|
214
|
+
*/
|
|
215
|
+
function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
|
|
216
|
+
if (typeof frame === "string") {
|
|
217
|
+
upstream.send(frame);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
upstream.send(Uint8Array.from(frame));
|
|
221
|
+
}
|
|
222
|
+
|
|
207
223
|
function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
|
|
208
224
|
if (upstream && ws.data.liveUpstream !== upstream) return;
|
|
209
225
|
if (ws.data.liveCloseFallback !== undefined) {
|
|
@@ -236,7 +252,10 @@ function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: Web
|
|
|
236
252
|
// close event. That is still an observed CLOSED transport and is safe to
|
|
237
253
|
// finalize. CONNECTING/CLOSING peers keep the lease so profile switching
|
|
238
254
|
// fails at its own bounded drain deadline instead of racing live traffic.
|
|
239
|
-
|
|
255
|
+
// The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
|
|
256
|
+
// system, but the socket can still transition to CLOSED (3) before this
|
|
257
|
+
// fallback fires; the cast keeps the runtime-identical check.
|
|
258
|
+
if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
|
|
240
259
|
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
241
260
|
}
|
|
242
261
|
|
|
@@ -246,7 +265,9 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
|
|
|
246
265
|
ws.data.livePending = undefined;
|
|
247
266
|
ws.data.cancel = undefined;
|
|
248
267
|
const upstream = ws.data.liveUpstream;
|
|
249
|
-
|
|
268
|
+
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
269
|
+
// constant CLOSED is 3; the numeric literal is the runtime-identical check.
|
|
270
|
+
if (!upstream || upstream.readyState === 3) {
|
|
250
271
|
finalizeLiveSideband(ws, upstream);
|
|
251
272
|
} else {
|
|
252
273
|
// The sideband holds a native-main admission lease. Do not release it just
|
|
@@ -299,7 +320,7 @@ function attachLiveSidebandUpstream(
|
|
|
299
320
|
ws.data.livePending = undefined;
|
|
300
321
|
for (const frame of pending) {
|
|
301
322
|
try {
|
|
302
|
-
upstream
|
|
323
|
+
sendUpstreamFrame(upstream, frame);
|
|
303
324
|
} catch {
|
|
304
325
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
305
326
|
return;
|
|
@@ -794,12 +815,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
794
815
|
}
|
|
795
816
|
throw error;
|
|
796
817
|
}
|
|
797
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
818
|
+
const { applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
798
819
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
799
820
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
800
821
|
const nativeSlugs = includeNativeOpenAi ? nativeOpenAiSlugs() : [];
|
|
801
822
|
const disabledNatives = disabledNativeSlugs(config);
|
|
802
823
|
const disabledModels = new Set(config.disabledModels ?? []);
|
|
824
|
+
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
825
|
+
const suppressedBareNativeSlugs = desktopAllowlistSuppressedNativeSlugs(config);
|
|
803
826
|
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
804
827
|
? visibleCodexAccountSelectors(config)
|
|
805
828
|
: [];
|
|
@@ -852,7 +875,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
852
875
|
const catalogNativeSlugs = accountSelectors.length > 0
|
|
853
876
|
? NATIVE_OPENAI_MODELS
|
|
854
877
|
: nativeSlugs;
|
|
855
|
-
const entries = buildCatalogEntries(loadCatalogTemplate(), catalogNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), accountSelectors);
|
|
878
|
+
const entries = buildCatalogEntries(loadCatalogTemplate(), catalogNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), accountSelectors, suppressedBareNativeSlugs);
|
|
856
879
|
return jsonResponse({
|
|
857
880
|
models: applyNativeVisibility(
|
|
858
881
|
entries,
|
|
@@ -904,7 +927,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
904
927
|
? NATIVE_OPENAI_MODELS.filter(slug => !disabledNatives.has(slug))
|
|
905
928
|
: [];
|
|
906
929
|
const visibleNatives = includeNativeOpenAi
|
|
907
|
-
? accountSelectors.length > 0
|
|
930
|
+
? accountSelectors.length > 0
|
|
931
|
+
? selectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
|
|
932
|
+
: visibleNativeSlugs(config)
|
|
908
933
|
: [];
|
|
909
934
|
const visibleAccountNatives = accountSelectors.flatMap(selector =>
|
|
910
935
|
selectorNativeSlugs.flatMap(metadataId => {
|
|
@@ -1312,7 +1337,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1312
1337
|
return;
|
|
1313
1338
|
}
|
|
1314
1339
|
try {
|
|
1315
|
-
upstream
|
|
1340
|
+
sendUpstreamFrame(upstream, raw);
|
|
1316
1341
|
} catch {
|
|
1317
1342
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
1318
1343
|
}
|