@bitkyc08/opencodex 2.14.0 → 2.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/gui/dist/assets/index-DUCH59lJ.css +1 -0
- package/gui/dist/assets/index-DUyQeU1j.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +46 -6
- package/src/adapters/cursor/request-builder.ts +54 -10
- package/src/adapters/cursor/tool-definitions.ts +24 -0
- package/src/adapters/kiro.ts +10 -1
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +7 -4
- package/src/adapters/openai-responses-url.ts +14 -0
- package/src/adapters/openai-responses.ts +111 -2
- 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/auth-api.ts +2 -74
- 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 +27 -8
- package/src/codex/catalog/provider-fetch.ts +47 -5
- package/src/codex/catalog/sync.ts +31 -8
- package/src/codex/catalog.ts +1 -1
- package/src/codex/features.ts +14 -3
- package/src/codex/model-cache.ts +7 -1
- package/src/codex/native-main-claim.ts +13 -2
- package/src/config.ts +79 -4
- package/src/generated/compatibility-version.json +74 -46
- package/src/lab/ledger/store.ts +0 -18
- package/src/lab/subject/installation-salt.ts +13 -2
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/tool-argument-integers.ts +158 -0
- package/src/oauth/nous.ts +58 -9
- 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 +202 -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 +17 -10
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state.ts +50 -2
- package/src/router.ts +12 -1
- package/src/server/index.ts +3 -2
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/config-routes.ts +51 -16
- 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 -17
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/ws-upstream.ts +199 -0
- package/src/server/startup-health-cache.ts +12 -0
- package/src/usage/log.ts +430 -12
- package/src/vision/index.ts +25 -4
- package/src/vision/timeout-bounds.ts +9 -0
- package/gui/dist/assets/index-BNVYzdn0.css +0 -1
- package/gui/dist/assets/index-Co12XTT-.js +0 -76
|
@@ -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,
|
|
@@ -1300,10 +1304,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1300
1304
|
modelContextWindows: CLINE_PASS_MODEL_CONTEXT_WINDOWS,
|
|
1301
1305
|
modelInputModalities: CLINE_PASS_MODEL_INPUT_MODALITIES,
|
|
1302
1306
|
noVisionModels: CLINE_PASS_TEXT_ONLY_MODELS,
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
reasoningEfforts: ["low"],
|
|
1307
|
+
// Live-probed 2026-08-13 across every static ClinePass model: the gateway accepts and
|
|
1308
|
+
// validates low/medium/high/xhigh/max, and rejects an invalid sentinel. Preserve the
|
|
1309
|
+
// caller's requested tier and let ClinePass own any backend-specific normalization.
|
|
1310
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
1307
1311
|
reasoningWireFormat: "gateway-object",
|
|
1308
1312
|
preserveCustomDestination: true,
|
|
1309
1313
|
note: "ClinePass subscription API. Uses a Cline API key and the full cline-pass/<model> upstream slug; quota is shared across the account's rolling 5-hour, weekly, and monthly limits.",
|
|
@@ -1789,7 +1793,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1789
1793
|
liveModels: true,
|
|
1790
1794
|
preserveCustomDestination: true,
|
|
1791
1795
|
// /v1/models is documented as callable authenticated or unauthenticated, so a 2xx catalog
|
|
1792
|
-
// response cannot prove the supplied Bearer key is valid.
|
|
1796
|
+
// response cannot prove that the supplied Bearer key is valid.
|
|
1793
1797
|
apiKeyValidation: "unknown",
|
|
1794
1798
|
// Featherless documents tool calling, but not a provider-wide parallel tool-call contract.
|
|
1795
1799
|
parallelToolCalls: false,
|
|
@@ -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/router.ts
CHANGED
|
@@ -267,6 +267,14 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
267
267
|
const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
|
|
268
268
|
const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
|
|
269
269
|
const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
|
|
270
|
+
// Key-login used to persist this exact low-only ClinePass capability seed. Once the gateway's
|
|
271
|
+
// wider input ladder was live-verified, leaving that generated row untouched would keep old
|
|
272
|
+
// installs clamped forever. This branch is reached only after canonical transport matching, so
|
|
273
|
+
// same-named custom destinations and every other explicit ladder still retain user precedence.
|
|
274
|
+
const repairLegacyClinePassReasoningEfforts = providerName === "cline-pass"
|
|
275
|
+
&& provider.reasoningWireFormat === "gateway-object"
|
|
276
|
+
&& provider.reasoningEfforts?.length === 1
|
|
277
|
+
&& provider.reasoningEfforts[0] === "low";
|
|
270
278
|
const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
|
|
271
279
|
? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
|
|
272
280
|
: mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
|
|
@@ -342,7 +350,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
342
350
|
...(provider.project === undefined && registryEntry.project !== undefined ? { project: registryEntry.project } : {}),
|
|
343
351
|
...(provider.location === undefined && registryEntry.location !== undefined ? { location: registryEntry.location } : {}),
|
|
344
352
|
...(provider.contextWindow === undefined && registryEntry.contextWindow !== undefined ? { contextWindow: registryEntry.contextWindow } : {}),
|
|
345
|
-
...(provider.reasoningEfforts === undefined
|
|
353
|
+
...((provider.reasoningEfforts === undefined || repairLegacyClinePassReasoningEfforts)
|
|
354
|
+
&& registryEntry.reasoningEfforts !== undefined
|
|
355
|
+
? { reasoningEfforts: [...registryEntry.reasoningEfforts] }
|
|
356
|
+
: {}),
|
|
346
357
|
...(provider.escapeBuiltinToolNames === undefined && registryEntry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: registryEntry.escapeBuiltinToolNames } : {}),
|
|
347
358
|
...(provider.keyOptional === undefined && registryEntry.keyOptional !== undefined ? { keyOptional: registryEntry.keyOptional } : {}),
|
|
348
359
|
...(provider.modelSuffixBracketStrip === undefined && registryEntry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: registryEntry.modelSuffixBracketStrip } : {}),
|
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";
|
|
@@ -485,12 +486,12 @@ export function warnAgentTaskRecoveryStartup(config: {
|
|
|
485
486
|
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
486
487
|
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
487
488
|
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
|
|
488
|
-
console.warn(" Recovered
|
|
489
|
+
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
489
490
|
}
|
|
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;
|
|
@@ -54,7 +54,16 @@ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
|
54
54
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
55
55
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
|
|
56
56
|
import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
findAnthropicVisionProvider,
|
|
59
|
+
isValidVisionTimeoutMs,
|
|
60
|
+
MAX_VISION_TIMEOUT_MS,
|
|
61
|
+
MIN_VISION_TIMEOUT_MS,
|
|
62
|
+
resolveEffectiveVisionModel,
|
|
63
|
+
resolveMaxDescriptionsPerTurn,
|
|
64
|
+
resolveVisionBackend,
|
|
65
|
+
resolveVisionTimeoutMs,
|
|
66
|
+
} from "../../vision";
|
|
58
67
|
import {
|
|
59
68
|
visionCandidateRows,
|
|
60
69
|
visionDescriberIsProvablyBlind,
|
|
@@ -108,6 +117,21 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
|
108
117
|
return { model, reasoning, models };
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
function publicVisionSidecarSettings(
|
|
121
|
+
config: OcxConfig,
|
|
122
|
+
vision: Awaited<ReturnType<typeof sidecarVisionResponseSettings>>,
|
|
123
|
+
) {
|
|
124
|
+
const vs = config.visionSidecar ?? {};
|
|
125
|
+
return {
|
|
126
|
+
enabled: vs.enabled !== false,
|
|
127
|
+
model: vision.model,
|
|
128
|
+
backend: vs.backend,
|
|
129
|
+
reasoning: vision.reasoning,
|
|
130
|
+
maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
|
|
131
|
+
timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
111
135
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
112
136
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
113
137
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
@@ -407,7 +431,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
407
431
|
|
|
408
432
|
if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
|
|
409
433
|
const ws = config.webSearchSidecar ?? {};
|
|
410
|
-
const vs = config.visionSidecar ?? {};
|
|
411
434
|
const vision = await sidecarVisionResponseSettings(config);
|
|
412
435
|
return jsonResponse({
|
|
413
436
|
webSearch: {
|
|
@@ -415,12 +438,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
415
438
|
backend: ws.backend,
|
|
416
439
|
streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
|
|
417
440
|
},
|
|
418
|
-
vision:
|
|
419
|
-
model: vision.model,
|
|
420
|
-
backend: vs.backend,
|
|
421
|
-
reasoning: vision.reasoning,
|
|
422
|
-
maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
|
|
423
|
-
},
|
|
441
|
+
vision: publicVisionSidecarSettings(config, vision),
|
|
424
442
|
visionModels: vision.models,
|
|
425
443
|
});
|
|
426
444
|
}
|
|
@@ -435,7 +453,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
435
453
|
if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
|
|
436
454
|
const body = raw as {
|
|
437
455
|
webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown };
|
|
438
|
-
vision?: {
|
|
456
|
+
vision?: {
|
|
457
|
+
model?: unknown;
|
|
458
|
+
backend?: unknown;
|
|
459
|
+
reasoning?: unknown;
|
|
460
|
+
maxDescriptionsPerTurn?: unknown;
|
|
461
|
+
enabled?: unknown;
|
|
462
|
+
timeoutMs?: unknown;
|
|
463
|
+
};
|
|
439
464
|
};
|
|
440
465
|
if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
|
|
441
466
|
&& body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
|
|
@@ -455,6 +480,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
455
480
|
|| body.vision.maxDescriptionsPerTurn <= 0)) {
|
|
456
481
|
return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400);
|
|
457
482
|
}
|
|
483
|
+
if (body.vision && body.vision.enabled !== undefined && typeof body.vision.enabled !== "boolean") {
|
|
484
|
+
return jsonResponse({ error: "vision.enabled must be a boolean" }, 400);
|
|
485
|
+
}
|
|
486
|
+
if (body.vision && body.vision.timeoutMs !== undefined && !isValidVisionTimeoutMs(body.vision.timeoutMs)) {
|
|
487
|
+
return jsonResponse({
|
|
488
|
+
error: `vision.timeoutMs must be an integer from ${MIN_VISION_TIMEOUT_MS} to ${MAX_VISION_TIMEOUT_MS}`,
|
|
489
|
+
}, 400);
|
|
490
|
+
}
|
|
458
491
|
if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) {
|
|
459
492
|
return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400);
|
|
460
493
|
}
|
|
@@ -517,6 +550,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
517
550
|
if (typeof body.vision.maxDescriptionsPerTurn === "number") {
|
|
518
551
|
config.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
|
|
519
552
|
}
|
|
553
|
+
if (typeof body.vision.enabled === "boolean") {
|
|
554
|
+
// `true` is the default — drop the key so disable/re-enable does not rewrite the file.
|
|
555
|
+
if (body.vision.enabled) delete config.visionSidecar.enabled;
|
|
556
|
+
else config.visionSidecar.enabled = false;
|
|
557
|
+
}
|
|
558
|
+
if (typeof body.vision.timeoutMs === "number") {
|
|
559
|
+
config.visionSidecar.timeoutMs = body.vision.timeoutMs;
|
|
560
|
+
}
|
|
520
561
|
if (visionReasoningTouched) {
|
|
521
562
|
if (normalizedVisionReasoning === undefined) delete config.visionSidecar.reasoning;
|
|
522
563
|
else config.visionSidecar.reasoning = normalizedVisionReasoning;
|
|
@@ -524,7 +565,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
524
565
|
}
|
|
525
566
|
saveConfigPreservingClaudeCode(config);
|
|
526
567
|
const ws = config.webSearchSidecar ?? {};
|
|
527
|
-
const vs = config.visionSidecar ?? {};
|
|
528
568
|
const vision = await sidecarVisionResponseSettings(config);
|
|
529
569
|
return jsonResponse({
|
|
530
570
|
ok: true,
|
|
@@ -533,12 +573,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
533
573
|
backend: ws.backend,
|
|
534
574
|
streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
|
|
535
575
|
},
|
|
536
|
-
vision:
|
|
537
|
-
model: vision.model,
|
|
538
|
-
backend: vs.backend,
|
|
539
|
-
reasoning: vision.reasoning,
|
|
540
|
-
maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
|
|
541
|
-
},
|
|
576
|
+
vision: publicVisionSidecarSettings(config, vision),
|
|
542
577
|
visionModels: vision.models,
|
|
543
578
|
});
|
|
544
579
|
}
|