@bitkyc08/opencodex 2.14.1 → 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/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
- 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 +15 -4
- 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.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 +48 -36
- 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 +12 -5
- 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/log.ts +430 -12
- package/gui/dist/assets/index-DuaUVm_d.js +0 -76
|
@@ -62,3 +62,13 @@ export function matchBaseUrlChoice(
|
|
|
62
62
|
}
|
|
63
63
|
return choices.some(c => c.id === "custom") ? "custom" : choices[0]!.id;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Moonshot/Kimi API endpoint presets (international default; China selectable). */
|
|
67
|
+
export const MOONSHOT_INTL_BASE_URL = "https://api.moonshot.ai/v1";
|
|
68
|
+
export const MOONSHOT_CN_BASE_URL = "https://api.moonshot.cn/v1";
|
|
69
|
+
|
|
70
|
+
export const MOONSHOT_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
71
|
+
{ id: "international", label: "International (.ai)", baseUrl: MOONSHOT_INTL_BASE_URL },
|
|
72
|
+
{ id: "china", label: "China (.cn)", baseUrl: MOONSHOT_CN_BASE_URL },
|
|
73
|
+
{ id: "custom", label: "Custom" },
|
|
74
|
+
];
|
|
@@ -13,6 +13,24 @@ const COMMAND_CODE_MODEL_EFFORTS = {
|
|
|
13
13
|
efforts: ["high", "max"],
|
|
14
14
|
profileUrl: "https://commandcode.ai/models/glm-5-2",
|
|
15
15
|
},
|
|
16
|
+
// Muse Spark: CLI currently prints "has no adjustable reasoning effort" and
|
|
17
|
+
// blocks --effort locally, but the upstream /alpha/generate endpoint accepts
|
|
18
|
+
// reasoning_effort low..max for meta/muse-spark-1.2-contributor (verified
|
|
19
|
+
// 2026-08-13: direct upstream POST with low/medium/high/xhigh/max all 200,
|
|
20
|
+
// ultra 400; reasoningTokens differentiated 114..253; proxy previously stripped
|
|
21
|
+
// the field so effort changes had no effect).
|
|
22
|
+
"meta/muse-spark-1.2": {
|
|
23
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
24
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2",
|
|
25
|
+
},
|
|
26
|
+
"meta/muse-spark-1.2-contributor": {
|
|
27
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
28
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2-contributor",
|
|
29
|
+
},
|
|
30
|
+
"meta/muse-spark-1.1": {
|
|
31
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
32
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.1",
|
|
33
|
+
},
|
|
16
34
|
} as const;
|
|
17
35
|
|
|
18
36
|
/**
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Registry model renames do not reach a saved provider config on their own.
|
|
2
|
+
//
|
|
3
|
+
// `reconcileOAuthProviders` refuses to touch a row whose `authMode` is not
|
|
4
|
+
// `oauth` (src/oauth/index.ts), and `enrichProviderFromRegistry` is fill-only by
|
|
5
|
+
// design: it backfills a MISSING field and never rewrites a present one, so a
|
|
6
|
+
// user's hand-edited model list survives an upgrade. Both postures are correct.
|
|
7
|
+
// Their gap is the case where the registry did not ADD a model but RENAMED one:
|
|
8
|
+
// the saved row keeps a retired id forever, the supported id never appears, and
|
|
9
|
+
// the capability metadata stays keyed to an id the vendor is taking offline
|
|
10
|
+
// (issue #1610 — `qwen3.8-max-preview` persisted through the `qwen3.8-max`
|
|
11
|
+
// rename in six separate fields, including a reasoning ladder that had since
|
|
12
|
+
// diverged from the registry's).
|
|
13
|
+
//
|
|
14
|
+
// This migration is deliberately NOT general reconciliation. It rewrites exactly
|
|
15
|
+
// one thing: an id this file declares retired, on a provider that still carries
|
|
16
|
+
// the registry's transport, and only when the registry currently seeds the
|
|
17
|
+
// replacement. Everything else in the row is left alone.
|
|
18
|
+
|
|
19
|
+
import { PROVIDER_REGISTRY } from "./registry";
|
|
20
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
21
|
+
|
|
22
|
+
export interface ModelRename {
|
|
23
|
+
/** Registry provider id whose saved rows may carry the retired model id. */
|
|
24
|
+
provider: string;
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
/** Why the vendor retired it, for the startup warning and future readers. */
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Renames already applied to `PROVIDER_REGISTRY`. An entry stays here after the
|
|
33
|
+
* registry moves on: it is what repairs configs saved before that move. Removing
|
|
34
|
+
* one strands every config that has not started since the rename shipped.
|
|
35
|
+
*/
|
|
36
|
+
export const MODEL_RENAMES: readonly ModelRename[] = [
|
|
37
|
+
{
|
|
38
|
+
provider: "alibaba-token-plan",
|
|
39
|
+
from: "qwen3.8-max-preview",
|
|
40
|
+
to: "qwen3.8-max",
|
|
41
|
+
reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
provider: "alibaba-token-plan-intl",
|
|
45
|
+
from: "qwen3.8-max-preview",
|
|
46
|
+
to: "qwen3.8-max",
|
|
47
|
+
reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Provider fields that key metadata by model id. */
|
|
52
|
+
const MODEL_KEYED_RECORDS = [
|
|
53
|
+
"modelContextWindows",
|
|
54
|
+
"modelMaxOutputTokens",
|
|
55
|
+
"modelInputModalities",
|
|
56
|
+
"modelReasoningEfforts",
|
|
57
|
+
"modelDefaultReasoningEfforts",
|
|
58
|
+
"modelReasoningEffortMap",
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
/** Provider fields that are flat lists of model ids. */
|
|
62
|
+
const MODEL_ID_LISTS = [
|
|
63
|
+
"models",
|
|
64
|
+
"noVisionModels",
|
|
65
|
+
"noReasoningModels",
|
|
66
|
+
"noTemperatureModels",
|
|
67
|
+
"noTopPModels",
|
|
68
|
+
"noPenaltyModels",
|
|
69
|
+
"autoToolChoiceOnlyModels",
|
|
70
|
+
"preserveReasoningContentModels",
|
|
71
|
+
"thinkingBudgetModels",
|
|
72
|
+
"directReasoningEffortModels",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
function renameInList(value: unknown, from: string, to: string): string[] | null {
|
|
76
|
+
if (!Array.isArray(value) || !value.includes(from)) return null;
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const next: string[] = [];
|
|
79
|
+
// Rename in place to preserve ordering, and collapse a duplicate if the target
|
|
80
|
+
// id was already present alongside the retired one.
|
|
81
|
+
for (const entry of value) {
|
|
82
|
+
if (typeof entry !== "string") continue;
|
|
83
|
+
const mapped = entry === from ? to : entry;
|
|
84
|
+
if (seen.has(mapped)) continue;
|
|
85
|
+
seen.add(mapped);
|
|
86
|
+
next.push(mapped);
|
|
87
|
+
}
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
93
|
+
const record = value as Record<string, unknown>;
|
|
94
|
+
if (!(from in record)) return null;
|
|
95
|
+
const next: Record<string, unknown> = {};
|
|
96
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
97
|
+
const mapped = key === from ? to : key;
|
|
98
|
+
if (mapped in next) continue;
|
|
99
|
+
// An explicit entry already saved under the new id is the newer intent.
|
|
100
|
+
next[mapped] = key === from && to in record ? record[to] : entry;
|
|
101
|
+
}
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `provider/model` rows in the top-level `disabledModels` list.
|
|
107
|
+
*
|
|
108
|
+
* The retired row is DROPPED rather than renamed: carrying its disabled state to
|
|
109
|
+
* the new id would hide the supported model behind a toggle the user set for a
|
|
110
|
+
* different model. An existing row for the new id is left untouched.
|
|
111
|
+
*/
|
|
112
|
+
function renameDisabledModels(config: OcxConfig, rename: ModelRename): boolean {
|
|
113
|
+
const list = config.disabledModels;
|
|
114
|
+
if (!Array.isArray(list)) return false;
|
|
115
|
+
const retired = `${rename.provider}/${rename.from}`;
|
|
116
|
+
if (!list.includes(retired)) return false;
|
|
117
|
+
config.disabledModels = list.filter(entry => entry !== retired);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Only migrate a row that still points at the registry's own endpoint. A user who
|
|
123
|
+
* repointed `baseUrl` at a different vendor owns their model ids.
|
|
124
|
+
*/
|
|
125
|
+
function providerStillMatchesRegistry(name: string, prov: OcxProviderConfig): boolean {
|
|
126
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
127
|
+
if (!entry) return false;
|
|
128
|
+
if (!prov.baseUrl || !entry.baseUrl) return true;
|
|
129
|
+
const choices = entry.baseUrlChoices?.map(choice => choice.baseUrl) ?? [];
|
|
130
|
+
const known = [entry.baseUrl, ...choices]
|
|
131
|
+
.filter((url): url is string => typeof url === "string")
|
|
132
|
+
.map(url => url.replace(/\/+$/, ""));
|
|
133
|
+
return known.includes(prov.baseUrl.replace(/\/+$/, ""));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Guard against a stale rename: only apply when the registry actually seeds `to`. */
|
|
137
|
+
function registrySeedsTarget(rename: ModelRename): boolean {
|
|
138
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === rename.provider);
|
|
139
|
+
return !!entry?.models?.includes(rename.to);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ModelRenameProjection {
|
|
143
|
+
config: OcxConfig;
|
|
144
|
+
changed: boolean;
|
|
145
|
+
warnings: string[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pure projection: apply every applicable rename and report what changed. The
|
|
150
|
+
* caller decides whether to persist.
|
|
151
|
+
*/
|
|
152
|
+
export function projectModelRenames(
|
|
153
|
+
config: OcxConfig,
|
|
154
|
+
renames: readonly ModelRename[] = MODEL_RENAMES,
|
|
155
|
+
): ModelRenameProjection {
|
|
156
|
+
const warnings: string[] = [];
|
|
157
|
+
let changed = false;
|
|
158
|
+
|
|
159
|
+
for (const rename of renames) {
|
|
160
|
+
const prov = config.providers?.[rename.provider];
|
|
161
|
+
if (!prov) continue;
|
|
162
|
+
if (!registrySeedsTarget(rename)) {
|
|
163
|
+
warnings.push(
|
|
164
|
+
`registry no longer seeds "${rename.to}" for "${rename.provider}"; skipping the `
|
|
165
|
+
+ `"${rename.from}" rename rather than writing an id the registry does not know.`,
|
|
166
|
+
);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!providerStillMatchesRegistry(rename.provider, prov)) continue;
|
|
170
|
+
|
|
171
|
+
// Provider config is a closed interface, so index through one unknown-cast
|
|
172
|
+
// view rather than casting at each assignment.
|
|
173
|
+
const row = prov as unknown as Record<string, unknown>;
|
|
174
|
+
let touched = false;
|
|
175
|
+
for (const field of MODEL_ID_LISTS) {
|
|
176
|
+
const next = renameInList(row[field], rename.from, rename.to);
|
|
177
|
+
if (!next) continue;
|
|
178
|
+
row[field] = next;
|
|
179
|
+
touched = true;
|
|
180
|
+
}
|
|
181
|
+
for (const field of MODEL_KEYED_RECORDS) {
|
|
182
|
+
const next = renameInRecord(row[field], rename.from, rename.to);
|
|
183
|
+
if (!next) continue;
|
|
184
|
+
row[field] = next;
|
|
185
|
+
touched = true;
|
|
186
|
+
}
|
|
187
|
+
if (prov.defaultModel === rename.from) {
|
|
188
|
+
prov.defaultModel = rename.to;
|
|
189
|
+
touched = true;
|
|
190
|
+
}
|
|
191
|
+
if (renameDisabledModels(config, rename)) touched = true;
|
|
192
|
+
|
|
193
|
+
if (touched) {
|
|
194
|
+
changed = true;
|
|
195
|
+
warnings.push(
|
|
196
|
+
`renamed "${rename.provider}/${rename.from}" to "${rename.to}" in the saved config: ${rename.reason}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { config, changed, warnings };
|
|
202
|
+
}
|
|
@@ -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,
|
|
@@ -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);
|