@bitkyc08/opencodex 2.14.1 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-B5T5ADgY.js +76 -0
- package/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +15 -4
- package/src/adapters/cursor/effort-map.ts +4 -5
- package/src/adapters/cursor/request-builder.ts +55 -11
- package/src/adapters/cursor/tool-definitions.ts +24 -0
- package/src/adapters/kiro.ts +10 -1
- package/src/adapters/openai-chat.ts +5 -3
- package/src/adapters/openai-responses.ts +109 -0
- package/src/adapters/tool-catalog-nudge.ts +26 -4
- package/src/bridge.ts +50 -3
- package/src/cli/init.ts +4 -17
- package/src/codex/catalog/effort.ts +2 -1
- package/src/codex/catalog/metadata.ts +62 -12
- package/src/codex/catalog/native-models.ts +27 -0
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +47 -5
- package/src/codex/catalog/sync.ts +21 -7
- package/src/codex/catalog.ts +1 -1
- package/src/config.ts +79 -4
- package/src/generated/compatibility-version.json +54 -42
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/tool-argument-integers.ts +158 -0
- package/src/oauth/index.ts +3 -0
- package/src/oauth/nous.ts +58 -9
- package/src/providers/antigravity-models.ts +93 -28
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/command-code-efforts.ts +18 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +13 -6
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state.ts +50 -2
- package/src/server/index.ts +2 -1
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/logs-usage-routes.ts +48 -10
- package/src/server/management/provider-routes.ts +2 -1
- package/src/server/management/usage-summary-cache.ts +7 -1
- package/src/server/responses/collaboration.ts +12 -2
- package/src/server/responses/core.ts +33 -16
- package/src/server/startup-health-cache.ts +12 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/log.ts +430 -12
- package/gui/dist/assets/index-DuaUVm_d.js +0 -76
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { saveConfig } from "../config";
|
|
2
|
+
import { projectModelRenames } from "./model-rename-migration";
|
|
3
|
+
import type { OcxConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface ModelRenameStartupDeps {
|
|
6
|
+
project: typeof projectModelRenames;
|
|
7
|
+
save: (config: OcxConfig) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Apply registry model renames to the saved config at startup (issue #1610).
|
|
12
|
+
*
|
|
13
|
+
* No backup is taken, unlike the OpenAI tier and Alibaba region migrations: those
|
|
14
|
+
* rewrite credentials and provider identity, where a bad projection is not
|
|
15
|
+
* recoverable from the config alone. This one only rewrites model ids that the
|
|
16
|
+
* registry itself no longer seeds, and the pre-migration value is a string this
|
|
17
|
+
* file still names, so the change is reversible by hand.
|
|
18
|
+
*/
|
|
19
|
+
export function runModelRenameStartupMigration(
|
|
20
|
+
config: OcxConfig,
|
|
21
|
+
deps: ModelRenameStartupDeps = { project: projectModelRenames, save: saveConfig },
|
|
22
|
+
): OcxConfig {
|
|
23
|
+
const projection = deps.project(config);
|
|
24
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
25
|
+
if (!projection.changed) return projection.config;
|
|
26
|
+
deps.save(projection.config);
|
|
27
|
+
return projection.config;
|
|
28
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
backupConfigBeforeOpenAiTierMigration,
|
|
3
|
+
OpenAiTierBackupCollisionError,
|
|
4
|
+
OpenAiTierRollbackPreserveError,
|
|
5
|
+
preserveOpenAiTierRollbackSnapshot,
|
|
6
|
+
saveConfig,
|
|
7
|
+
} from "../config";
|
|
2
8
|
import type { OcxConfig } from "../types";
|
|
3
9
|
import { projectOpenAiTierMigration } from "./openai-tiers";
|
|
4
10
|
|
|
@@ -6,6 +12,23 @@ export interface OpenAiTierStartupDeps {
|
|
|
6
12
|
project: typeof projectOpenAiTierMigration;
|
|
7
13
|
backup: () => void;
|
|
8
14
|
save: (config: OcxConfig) => void;
|
|
15
|
+
preserveRollback?: (error: OpenAiTierBackupCollisionError) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function defaultPreserveRollback(error: OpenAiTierBackupCollisionError): void {
|
|
19
|
+
if (!error.configPath) throw error;
|
|
20
|
+
try {
|
|
21
|
+
const preserved = preserveOpenAiTierRollbackSnapshot(error.configPath);
|
|
22
|
+
console.warn(`[openai-provider-migration] Preserved rollback snapshot at ${preserved}`);
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
if (
|
|
25
|
+
cause instanceof OpenAiTierRollbackPreserveError
|
|
26
|
+
&& (cause.code === "missing" || cause.code === "not-rollback")
|
|
27
|
+
) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
throw cause;
|
|
31
|
+
}
|
|
9
32
|
}
|
|
10
33
|
|
|
11
34
|
const DEFAULT_DEPS: OpenAiTierStartupDeps = {
|
|
@@ -20,7 +43,13 @@ export function runOpenAiTierStartupMigration(
|
|
|
20
43
|
): OcxConfig {
|
|
21
44
|
const projection = deps.project(config);
|
|
22
45
|
if (!projection.changed) return projection.config;
|
|
23
|
-
|
|
46
|
+
try {
|
|
47
|
+
deps.backup();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!(error instanceof OpenAiTierBackupCollisionError)) throw error;
|
|
50
|
+
(deps.preserveRollback ?? defaultPreserveRollback)(error);
|
|
51
|
+
deps.backup();
|
|
52
|
+
}
|
|
24
53
|
deps.save(projection.config);
|
|
25
54
|
for (const warning of projection.warnings) console.warn(`[openai-provider-migration] ${warning}`);
|
|
26
55
|
return projection.config;
|
package/src/providers/quota.ts
CHANGED
|
@@ -783,9 +783,16 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
|
|
|
783
783
|
if (available === undefined || available < 0) return null;
|
|
784
784
|
// Moonshot exposes no per-window quota ceiling, only a balance — report it
|
|
785
785
|
// as a balance-only window (percent 0) rather than a fabricated utilization.
|
|
786
|
+
// Currency is host-scoped: China platform (api.moonshot.cn) bills in CNY;
|
|
787
|
+
// the international platform (api.moonshot.ai) bills in USD. Do not force
|
|
788
|
+
// either side into the other unit — the number is correct, only the unit
|
|
789
|
+
// must match the host.
|
|
790
|
+
const isChinaHost = host.startsWith("https://api.moonshot.cn");
|
|
791
|
+
const money = (n: number) => isChinaHost ? `¥${n.toFixed(2)}` : `$${n.toFixed(2)}`;
|
|
792
|
+
const unit = isChinaHost ? "CNY" : "USD";
|
|
786
793
|
const label = voucher !== undefined && cash !== undefined
|
|
787
|
-
? `Balance (
|
|
788
|
-
: `Balance (
|
|
794
|
+
? `Balance (${money(available)} ${unit} available, ${money(voucher)} voucher)`
|
|
795
|
+
: `Balance (${money(available)} ${unit} available)`;
|
|
789
796
|
return report(provider, "moonshot:balance", {
|
|
790
797
|
customWindows: [{ label, percent: 0 }],
|
|
791
798
|
updatedAt: Date.now(),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
|
|
7
7
|
ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL,
|
|
8
8
|
ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL,
|
|
9
|
+
MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL,
|
|
9
10
|
} from "./base-url-choices";
|
|
10
11
|
import {
|
|
11
12
|
CURSOR_STATIC_MODELS,
|
|
@@ -950,8 +951,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
950
951
|
// devlog/model_update/260709_model_refresh/001_xai_lineup.md.
|
|
951
952
|
// grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
|
|
952
953
|
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
953
|
-
// 260813: grok-4.6 added per
|
|
954
|
-
//
|
|
954
|
+
// 260813: grok-4.6 added per docs.x.ai/developers/grok-4-6. Context/vision still match
|
|
955
|
+
// grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
|
|
955
956
|
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
956
957
|
defaultModel: "grok-4.5",
|
|
957
958
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
@@ -973,8 +974,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
973
974
|
// (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
|
|
974
975
|
// Models that never emit reasoning simply have no thinking parts to replay (no-op).
|
|
975
976
|
preserveReasoningContentModels: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
|
|
976
|
-
// grok-4.5 reasoning is always-on with low/medium/high
|
|
977
|
-
|
|
977
|
+
// grok-4.5 reasoning is always-on with low/medium/high (no off tier, no xhigh).
|
|
978
|
+
// grok-4.6 adds xhigh per docs.x.ai/developers/model-capabilities/text/reasoning;
|
|
979
|
+
// xAI documents high as the upstream default.
|
|
980
|
+
modelReasoningEfforts: { "grok-4.6": ["low", "medium", "high", "xhigh"], "grok-4.5": ["low", "medium", "high"] },
|
|
981
|
+
modelDefaultReasoningEfforts: { "grok-4.6": "high" },
|
|
978
982
|
modelContextWindows: {
|
|
979
983
|
"grok-4.6": 500_000,
|
|
980
984
|
"grok-4.5": 500_000,
|
|
@@ -1393,7 +1397,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1393
1397
|
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
|
|
1394
1398
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
1395
1399
|
{ 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"] },
|
|
1396
|
-
{ 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.
|
|
1400
|
+
{ 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.7-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"] },
|
|
1397
1401
|
{ 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" },
|
|
1398
1402
|
{ 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" },
|
|
1399
1403
|
{ 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" },
|
|
@@ -1870,7 +1874,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1870
1874
|
note: "Model data frozen pending Tier-2 entitlement proof",
|
|
1871
1875
|
},
|
|
1872
1876
|
{
|
|
1873
|
-
id: "moonshot", label: "Moonshot (Kimi API)", baseUrl:
|
|
1877
|
+
id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: MOONSHOT_INTL_BASE_URL, adapter: "openai-chat", authKind: "key",
|
|
1878
|
+
allowBaseUrlOverride: true,
|
|
1879
|
+
baseUrlChoices: MOONSHOT_BASE_URL_CHOICES,
|
|
1874
1880
|
dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
|
|
1875
1881
|
models: KIMI_API_MODELS,
|
|
1876
1882
|
modelContextWindows: KIMI_API_MODEL_CONTEXT_WINDOWS,
|
|
@@ -1882,6 +1888,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1882
1888
|
noPenaltyModels: KIMI_API_MODELS,
|
|
1883
1889
|
autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
|
|
1884
1890
|
preserveReasoningContentModels: KIMI_API_MODELS,
|
|
1891
|
+
note: "International default (api.moonshot.ai). China accounts: choose China (.cn) or Custom for api.moonshot.cn.",
|
|
1885
1892
|
},
|
|
1886
1893
|
{ id: "huggingface", label: "Hugging Face", baseUrl: "https://router.huggingface.co/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://huggingface.co/settings/tokens" },
|
|
1887
1894
|
// 260715 NIM hardening (issue #126, devlog/_plan/260715_issue126_nim_kimi):
|
|
@@ -34,6 +34,7 @@ export interface ResponseSpillPayload {
|
|
|
34
34
|
version: 1;
|
|
35
35
|
responseId: string;
|
|
36
36
|
createdAt: number;
|
|
37
|
+
clientThreadId?: string;
|
|
37
38
|
items: unknown[];
|
|
38
39
|
providers?: OcxProviderContinuationState;
|
|
39
40
|
}
|
|
@@ -260,9 +261,11 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
|
|
|
260
261
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
261
262
|
const payload = value as Record<string, unknown>;
|
|
262
263
|
const keys = Object.keys(payload);
|
|
263
|
-
if (keys.some(key => !["version", "responseId", "createdAt", "items", "providers"].includes(key))) return false;
|
|
264
|
+
if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providers"].includes(key))) return false;
|
|
264
265
|
if (payload.version !== 1 || payload.responseId !== responseId) return false;
|
|
265
266
|
if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false;
|
|
267
|
+
if (payload.clientThreadId !== undefined
|
|
268
|
+
&& (typeof payload.clientThreadId !== "string" || payload.clientThreadId.trim().length === 0)) return false;
|
|
266
269
|
if (!Array.isArray(payload.items)) return false;
|
|
267
270
|
if (payload.providers !== undefined) {
|
|
268
271
|
if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false;
|
|
@@ -284,6 +287,7 @@ export function writeResponseSpillDurably(
|
|
|
284
287
|
version: 1,
|
|
285
288
|
responseId,
|
|
286
289
|
createdAt: state.createdAt,
|
|
290
|
+
...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}),
|
|
287
291
|
items: state.items,
|
|
288
292
|
...(state.providers ? { providers: state.providers } : {}),
|
|
289
293
|
};
|
package/src/responses/state.ts
CHANGED
|
@@ -37,6 +37,7 @@ const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4;
|
|
|
37
37
|
interface ResidentResponseState {
|
|
38
38
|
kind: "resident";
|
|
39
39
|
createdAt: number;
|
|
40
|
+
clientThreadId?: string;
|
|
40
41
|
items: unknown[];
|
|
41
42
|
providers?: OcxProviderContinuationState;
|
|
42
43
|
sizeBytes: number;
|
|
@@ -45,6 +46,7 @@ interface ResidentResponseState {
|
|
|
45
46
|
interface SpilledResponseState {
|
|
46
47
|
kind: "spill";
|
|
47
48
|
createdAt: number;
|
|
49
|
+
clientThreadId?: string;
|
|
48
50
|
providers?: OcxProviderContinuationState;
|
|
49
51
|
spill: ResponseSpillRef;
|
|
50
52
|
sizeBytes: number;
|
|
@@ -65,6 +67,7 @@ export type PreviousResponseReplayFailure = {
|
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
const states = new Map<string, StoredResponseState>();
|
|
70
|
+
const replayScopeMismatches = new WeakSet<object>();
|
|
68
71
|
let storedResponseBytes = 0;
|
|
69
72
|
let residentResponseBytes = 0;
|
|
70
73
|
let oldestResidentId: string | undefined;
|
|
@@ -80,6 +83,7 @@ const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
|
80
83
|
* snapshot files refused before parse.
|
|
81
84
|
*/
|
|
82
85
|
const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
|
|
86
|
+
let replayScopeMismatchDrops = 0;
|
|
83
87
|
|
|
84
88
|
/** Test-only: admission-boundary counters (proves the new paths fire). */
|
|
85
89
|
export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
|
|
@@ -124,6 +128,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
|
|
|
124
128
|
const sizeBytes = serializedBytes({
|
|
125
129
|
responseId: id,
|
|
126
130
|
createdAt: entry.createdAt,
|
|
131
|
+
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
127
132
|
items: entry.items,
|
|
128
133
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
129
134
|
});
|
|
@@ -224,6 +229,7 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
|
|
|
224
229
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
225
230
|
kind: "spill",
|
|
226
231
|
createdAt: expected.createdAt,
|
|
232
|
+
...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
|
|
227
233
|
...(expected.providers ? { providers: expected.providers } : {}),
|
|
228
234
|
spill: ref,
|
|
229
235
|
};
|
|
@@ -244,12 +250,14 @@ function replaceSpillEntryAtomically(
|
|
|
244
250
|
try {
|
|
245
251
|
const ref = writeResponseSpillDurably(id, {
|
|
246
252
|
createdAt: candidate.createdAt,
|
|
253
|
+
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
247
254
|
items: candidate.items,
|
|
248
255
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
249
256
|
});
|
|
250
257
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
251
258
|
kind: "spill",
|
|
252
259
|
createdAt: candidate.createdAt,
|
|
260
|
+
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
253
261
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
254
262
|
spill: ref,
|
|
255
263
|
};
|
|
@@ -322,6 +330,7 @@ function admitOversizedCandidate(
|
|
|
322
330
|
try {
|
|
323
331
|
const ref = writeResponseSpillDurably(id, {
|
|
324
332
|
createdAt: candidate.createdAt,
|
|
333
|
+
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
325
334
|
items: candidate.items,
|
|
326
335
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
327
336
|
});
|
|
@@ -337,6 +346,7 @@ function admitOversizedCandidate(
|
|
|
337
346
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
338
347
|
kind: "spill",
|
|
339
348
|
createdAt: candidate.createdAt,
|
|
349
|
+
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
340
350
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
341
351
|
spill: ref,
|
|
342
352
|
};
|
|
@@ -385,6 +395,7 @@ function snapshotPath(): string {
|
|
|
385
395
|
|
|
386
396
|
interface LegacySnapshotState {
|
|
387
397
|
createdAt?: unknown;
|
|
398
|
+
clientThreadId?: unknown;
|
|
388
399
|
items?: unknown;
|
|
389
400
|
providers?: OcxProviderContinuationState;
|
|
390
401
|
conversationId?: unknown;
|
|
@@ -405,11 +416,15 @@ function loadSnapshotEntry(id: string, value: unknown): void {
|
|
|
405
416
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
406
417
|
const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown };
|
|
407
418
|
if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return;
|
|
419
|
+
const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0
|
|
420
|
+
? rec.clientThreadId.trim()
|
|
421
|
+
: undefined;
|
|
408
422
|
if (rec.kind === "spill") {
|
|
409
423
|
if (!isSpillRef(rec.spill)) return;
|
|
410
424
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
411
425
|
kind: "spill",
|
|
412
426
|
createdAt: rec.createdAt,
|
|
427
|
+
...(clientThreadId ? { clientThreadId } : {}),
|
|
413
428
|
...(rec.providers ? { providers: rec.providers } : {}),
|
|
414
429
|
spill: rec.spill,
|
|
415
430
|
};
|
|
@@ -434,6 +449,7 @@ function loadSnapshotEntry(id: string, value: unknown): void {
|
|
|
434
449
|
: undefined);
|
|
435
450
|
const resident = measureResidentEntry(id, {
|
|
436
451
|
createdAt: rec.createdAt,
|
|
452
|
+
...(clientThreadId ? { clientThreadId } : {}),
|
|
437
453
|
items: rec.items,
|
|
438
454
|
...(providers ? { providers } : {}),
|
|
439
455
|
});
|
|
@@ -747,6 +763,7 @@ function pruneResponses(at = now()): void {
|
|
|
747
763
|
try {
|
|
748
764
|
const ref = writeResponseSpillDurably(oldestId, {
|
|
749
765
|
createdAt: entry.createdAt,
|
|
766
|
+
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
750
767
|
items: entry.items,
|
|
751
768
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
752
769
|
});
|
|
@@ -788,6 +805,7 @@ export function evictOldestResponseContinuationForBudget(): number {
|
|
|
788
805
|
try {
|
|
789
806
|
const ref = writeResponseSpillDurably(id, {
|
|
790
807
|
createdAt: entry.createdAt,
|
|
808
|
+
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
791
809
|
items: entry.items,
|
|
792
810
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
793
811
|
});
|
|
@@ -828,6 +846,7 @@ function materializeEntry(
|
|
|
828
846
|
}
|
|
829
847
|
const state = measureResidentEntry(id, {
|
|
830
848
|
createdAt: result.payload.createdAt,
|
|
849
|
+
...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
|
|
831
850
|
items: result.payload.items,
|
|
832
851
|
...(result.payload.providers ? { providers: result.payload.providers } : {}),
|
|
833
852
|
});
|
|
@@ -840,7 +859,16 @@ function materializeEntry(
|
|
|
840
859
|
return { ok: true, state };
|
|
841
860
|
}
|
|
842
861
|
|
|
843
|
-
|
|
862
|
+
function normalizedClientThreadId(value: unknown): string | undefined {
|
|
863
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
|
|
867
|
+
const { previous_response_id: _previousResponseId, ...freshRequest } = request;
|
|
868
|
+
return freshRequest;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
|
|
844
872
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
845
873
|
const request = body as Record<string, unknown>;
|
|
846
874
|
const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
|
|
@@ -854,6 +882,16 @@ export function expandPreviousResponseInput(body: unknown): unknown {
|
|
|
854
882
|
replayFailures.set(request, materialized.failure);
|
|
855
883
|
return body;
|
|
856
884
|
}
|
|
885
|
+
const requestThreadId = normalizedClientThreadId(clientThreadId);
|
|
886
|
+
const storedThreadId = normalizedClientThreadId(materialized.state.clientThreadId);
|
|
887
|
+
// A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
|
|
888
|
+
// Unscoped callers retain backward-compatible replay only with other unscoped entries.
|
|
889
|
+
if (requestThreadId !== storedThreadId) {
|
|
890
|
+
const freshRequest = withoutPreviousResponseId(request);
|
|
891
|
+
replayScopeMismatches.add(freshRequest);
|
|
892
|
+
replayScopeMismatchDrops += 1;
|
|
893
|
+
return freshRequest;
|
|
894
|
+
}
|
|
857
895
|
const expanded = {
|
|
858
896
|
...request,
|
|
859
897
|
input: [...materialized.state.items, ...inputItems(request.input)],
|
|
@@ -873,6 +911,11 @@ export function previousResponseReplayPrefixLength(body: unknown): number {
|
|
|
873
911
|
return replayedInputPrefixLengths.get(body) ?? 0;
|
|
874
912
|
}
|
|
875
913
|
|
|
914
|
+
/** True when a stale or foreign previous_response_id was removed from this exact request body. */
|
|
915
|
+
export function previousResponseScopeMismatch(body: unknown): boolean {
|
|
916
|
+
return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
|
|
917
|
+
}
|
|
918
|
+
|
|
876
919
|
export function previousResponseConversationId(responseId: string | undefined): string | undefined {
|
|
877
920
|
return previousResponseProviderState(responseId)?.cursor?.conversationId;
|
|
878
921
|
}
|
|
@@ -898,6 +941,7 @@ export interface ResponseStateMetrics {
|
|
|
898
941
|
spillWrites: number;
|
|
899
942
|
spillWriteFailures: number;
|
|
900
943
|
spillReadFailures: number;
|
|
944
|
+
replayScopeMismatchDrops: number;
|
|
901
945
|
}
|
|
902
946
|
|
|
903
947
|
/**
|
|
@@ -940,6 +984,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
940
984
|
spillWrites: spillCounters.writes,
|
|
941
985
|
spillWriteFailures: spillCounters.writeFailures,
|
|
942
986
|
spillReadFailures: spillCounters.readFailures,
|
|
987
|
+
replayScopeMismatchDrops,
|
|
943
988
|
};
|
|
944
989
|
}
|
|
945
990
|
|
|
@@ -972,7 +1017,7 @@ export function rememberResponseState(
|
|
|
972
1017
|
requestBody: unknown,
|
|
973
1018
|
response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
|
|
974
1019
|
providerState?: OcxProviderContinuationState | string,
|
|
975
|
-
opts?: { force?: boolean },
|
|
1020
|
+
opts?: { force?: boolean; clientThreadId?: string },
|
|
976
1021
|
): void {
|
|
977
1022
|
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
978
1023
|
const request = requestBody as Record<string, unknown>;
|
|
@@ -998,8 +1043,10 @@ export function rememberResponseState(
|
|
|
998
1043
|
return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
|
|
999
1044
|
});
|
|
1000
1045
|
}
|
|
1046
|
+
const clientThreadId = normalizedClientThreadId(opts?.clientThreadId);
|
|
1001
1047
|
setResidentEntry(response.id, {
|
|
1002
1048
|
createdAt: now(),
|
|
1049
|
+
...(clientThreadId ? { clientThreadId } : {}),
|
|
1003
1050
|
items: [...inputItems(request.input), ...response.output],
|
|
1004
1051
|
// Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
|
|
1005
1052
|
// Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
|
|
@@ -1045,6 +1092,7 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1045
1092
|
spillCounters.writes = 0;
|
|
1046
1093
|
spillCounters.writeFailures = 0;
|
|
1047
1094
|
spillCounters.readFailures = 0;
|
|
1095
|
+
replayScopeMismatchDrops = 0;
|
|
1048
1096
|
persistAttemptHookForTests = null;
|
|
1049
1097
|
loaded = false;
|
|
1050
1098
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { loadLabAutomationPolicy } from "../lab/automation/persistence";
|
|
|
53
53
|
import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
|
|
54
54
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
55
55
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
56
|
+
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
56
57
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
57
58
|
import { providerContextCap } from "../providers/context-cap";
|
|
58
59
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
@@ -490,7 +491,7 @@ export function warnAgentTaskRecoveryStartup(config: {
|
|
|
490
491
|
|
|
491
492
|
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
492
493
|
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
493
|
-
const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
|
|
494
|
+
const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
|
|
494
495
|
warnAgentTaskRecoveryStartup(config);
|
|
495
496
|
setLiveStateStoreConfig(config);
|
|
496
497
|
applyProxyEnv(config);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
currentUsageLogRevision,
|
|
3
3
|
readUsageSnapshotForManagement,
|
|
4
|
-
|
|
4
|
+
usageLogIdentityKey,
|
|
5
5
|
type PersistedUsageEntry,
|
|
6
6
|
} from "../../usage/log";
|
|
7
7
|
|
|
@@ -110,7 +110,7 @@ export function rollupApiKeyUsage(
|
|
|
110
110
|
* create/rename/delete. The compact rollup is a handful of counters per key, so
|
|
111
111
|
* caching it costs nothing; a new row changes the revision and invalidates it.
|
|
112
112
|
*/
|
|
113
|
-
let rollupCache: { revisionKey: string; expiresAt: number; snapshot: ApiKeyUsageSnapshot } | null = null;
|
|
113
|
+
let rollupCache: { revisionKey: string; expiresAt: number; lastSeenSize?: number; snapshot: ApiKeyUsageSnapshot } | null = null;
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* The rollup is a function of the log AND of the clock: a request ages out of
|
|
@@ -136,6 +136,29 @@ export function clearApiKeyUsageCacheForTests(): void {
|
|
|
136
136
|
* `attributionSince`. Key management working matters more than usage numbers
|
|
137
137
|
* being present, and the GUI already treats an absent field as "no data".
|
|
138
138
|
*/
|
|
139
|
+
export function cacheApiKeyUsageFromSnapshot(
|
|
140
|
+
entries: PersistedUsageEntry[],
|
|
141
|
+
configuredIds: string[],
|
|
142
|
+
identityKey: string,
|
|
143
|
+
lastSeenSize: number,
|
|
144
|
+
truncated: boolean,
|
|
145
|
+
maxReadBytes: number | undefined,
|
|
146
|
+
now: number = Date.now(),
|
|
147
|
+
): ApiKeyUsageSnapshot {
|
|
148
|
+
const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
|
|
149
|
+
const rolled = {
|
|
150
|
+
...rollupApiKeyUsage(entries, configuredIds, now),
|
|
151
|
+
...(truncated ? { historyTruncated: true as const } : {}),
|
|
152
|
+
};
|
|
153
|
+
rollupCache = {
|
|
154
|
+
revisionKey: `${identityKey}|${idsKey}`,
|
|
155
|
+
expiresAt: now + ROLLUP_CACHE_TTL_MS,
|
|
156
|
+
lastSeenSize,
|
|
157
|
+
snapshot: rolled,
|
|
158
|
+
};
|
|
159
|
+
return rolled;
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
|
|
140
163
|
// JSON rather than a joined string: ids are only validated as non-empty
|
|
141
164
|
// strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
|
|
@@ -143,8 +166,10 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
|
|
|
143
166
|
const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
|
|
144
167
|
const now = Date.now();
|
|
145
168
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
169
|
+
const observed = currentUsageLogRevision();
|
|
170
|
+
const observedKey = `${usageLogIdentityKey(observed)}|${idsKey}`;
|
|
171
|
+
const observedSize = observed?.size ?? 0;
|
|
172
|
+
if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt && observedSize >= (rollupCache.lastSeenSize ?? 0)) {
|
|
148
173
|
return rollupCache.snapshot;
|
|
149
174
|
}
|
|
150
175
|
|
|
@@ -154,8 +179,9 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
|
|
|
154
179
|
...(snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated ? { historyTruncated: true as const } : {}),
|
|
155
180
|
};
|
|
156
181
|
rollupCache = {
|
|
157
|
-
revisionKey: `${
|
|
182
|
+
revisionKey: `${usageLogIdentityKey(snapshot.revision)}|${idsKey}`,
|
|
158
183
|
expiresAt: now + ROLLUP_CACHE_TTL_MS,
|
|
184
|
+
lastSeenSize: snapshot.revision?.size ?? 0,
|
|
159
185
|
snapshot: rolled,
|
|
160
186
|
};
|
|
161
187
|
return rolled;
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import {
|
|
51
51
|
currentUsageLogRevision,
|
|
52
52
|
readUsageSnapshotForManagement,
|
|
53
|
+
usageLogIdentityKey,
|
|
53
54
|
usageLogRevisionKey,
|
|
54
55
|
type PersistedUsageEntry,
|
|
55
56
|
} from "../../usage/log";
|
|
@@ -84,6 +85,7 @@ import {
|
|
|
84
85
|
getUsageSummaryCacheEntry,
|
|
85
86
|
setUsageSummaryCacheEntry,
|
|
86
87
|
} from "./usage-summary-cache";
|
|
88
|
+
import { cacheApiKeyUsageFromSnapshot } from "./api-key-usage";
|
|
87
89
|
|
|
88
90
|
const USAGE_DAY_MS = 86_400_000;
|
|
89
91
|
function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
|
|
@@ -215,12 +217,16 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
215
217
|
try {
|
|
216
218
|
const cacheKey = `${range}:${surface}`;
|
|
217
219
|
const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
|
|
218
|
-
const
|
|
220
|
+
const observed = currentUsageLogRevision();
|
|
221
|
+
const identityKey = `${usageLogIdentityKey(observed)}\0${effectiveReadLimit}`;
|
|
222
|
+
const observedSize = observed?.size ?? 0;
|
|
219
223
|
const cached = getUsageSummaryCacheEntry(cacheKey);
|
|
220
224
|
if (cached
|
|
221
|
-
&& cached.
|
|
225
|
+
&& cached.identityKey === identityKey
|
|
226
|
+
&& cached.maxReadBytes === effectiveReadLimit
|
|
222
227
|
&& cached.overlayVersion === userCostOverlayVersion()
|
|
223
|
-
&& now < cached.
|
|
228
|
+
&& now < cached.freshUntil
|
|
229
|
+
&& observedSize >= cached.lastSeenSize) {
|
|
224
230
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
225
231
|
}
|
|
226
232
|
if (cached) discardUsageSummaryCacheEntry(cacheKey);
|
|
@@ -249,13 +255,45 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
249
255
|
// mixed-price entry under either version.
|
|
250
256
|
return jsonResponse(summary);
|
|
251
257
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
const freshUntil = now + 60_000;
|
|
259
|
+
const snapshotIdentity = `${usageLogIdentityKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
260
|
+
const revisionKey = `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
261
|
+
const lastSeenSize = snapshot.revision?.size ?? 0;
|
|
262
|
+
const ranges: UsageRange[] = ["7d", "30d", "all"];
|
|
263
|
+
const surfaces: UsageSurface[] = ["all", "codex", "claude", "grok"];
|
|
264
|
+
for (const nextRange of ranges) {
|
|
265
|
+
for (const nextSurface of surfaces) {
|
|
266
|
+
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
267
|
+
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface),
|
|
268
|
+
historyTruncated: summary.historyTruncated,
|
|
269
|
+
truncatedPrefixBytes: summary.truncatedPrefixBytes,
|
|
270
|
+
entriesTruncated: summary.entriesTruncated,
|
|
271
|
+
entriesDropped: summary.entriesDropped,
|
|
272
|
+
snapshotWindowStart: summary.snapshotWindowStart,
|
|
273
|
+
snapshotWindowEnd: summary.snapshotWindowEnd,
|
|
274
|
+
};
|
|
275
|
+
setUsageSummaryCacheEntry(`${nextRange}:${nextSurface}`, {
|
|
276
|
+
revisionKey,
|
|
277
|
+
identityKey: snapshotIdentity,
|
|
278
|
+
maxReadBytes: effectiveReadLimit,
|
|
279
|
+
overlayVersion,
|
|
280
|
+
expiresAt: usageSummaryExpiresAt(snapshot.entries, nextRange, nextSurface, now),
|
|
281
|
+
freshUntil,
|
|
282
|
+
lastSeenSize,
|
|
283
|
+
revisionReadAt,
|
|
284
|
+
summary: nextSummary,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
cacheApiKeyUsageFromSnapshot(
|
|
289
|
+
snapshot.entries,
|
|
290
|
+
(config.apiKeys ?? []).map(key => key.id),
|
|
291
|
+
usageLogIdentityKey(snapshot.revision),
|
|
292
|
+
snapshot.revision?.size ?? 0,
|
|
293
|
+
snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
294
|
+
effectiveReadLimit,
|
|
295
|
+
now,
|
|
296
|
+
);
|
|
259
297
|
return jsonResponse(summary);
|
|
260
298
|
} catch {
|
|
261
299
|
return jsonResponse({
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
submitManualLoginCode,
|
|
28
28
|
upsertOAuthProvider,
|
|
29
29
|
} from "../../oauth";
|
|
30
|
-
import {
|
|
30
|
+
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
31
31
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
32
32
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
33
33
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
@@ -765,6 +765,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
765
765
|
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
766
766
|
setProviderContextCap(config, name, false);
|
|
767
767
|
save(config);
|
|
768
|
+
await replaceProviderAccountSet(name, null);
|
|
768
769
|
reconcileLiveStateStores();
|
|
769
770
|
const { clearModelCache: clearCache } = await import("../../codex/model-cache");
|
|
770
771
|
clearCache(name);
|
|
@@ -8,11 +8,17 @@ export type CachedUsageSummary = UsageSummary & {
|
|
|
8
8
|
entriesDropped: number;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
interface UsageSummaryCacheEntry {
|
|
11
|
+
export interface UsageSummaryCacheEntry {
|
|
12
12
|
revisionKey: string;
|
|
13
|
+
/** path/dev/ino/birthtime only; appends keep this stable. */
|
|
14
|
+
identityKey: string;
|
|
15
|
+
maxReadBytes: number;
|
|
13
16
|
/** userCostOverlayVersion() when the summary was computed; overlay edits invalidate the entry. */
|
|
14
17
|
overlayVersion: number;
|
|
15
18
|
expiresAt: number;
|
|
19
|
+
/** Generation freshness: ignore size/mtime until this instant. */
|
|
20
|
+
freshUntil: number;
|
|
21
|
+
lastSeenSize: number;
|
|
16
22
|
summary: CachedUsageSummary;
|
|
17
23
|
revisionReadAt: number;
|
|
18
24
|
sizeBytes: number;
|