@coseung2/opencodex 2.8.0-cs.16 → 2.8.0-cs.17
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-BZGMtkmp.js → index-Ch-99jy3.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +12 -0
- package/src/adapters/kiro-calibration.ts +83 -0
- package/src/adapters/kiro-constants.ts +11 -2
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +12 -3
- package/src/adapters/kiro.ts +300 -78
- package/src/adapters/openai-chat.ts +1 -42
- package/src/adapters/openai-responses.ts +93 -14
- package/src/adapters/xai-schema-analysis.ts +78 -0
- package/src/adapters/xai-tool-schema.ts +274 -0
- package/src/adapters/xai-web-search.ts +138 -0
- package/src/bridge.ts +61 -6
- package/src/codex/catalog/effort.ts +4 -2
- package/src/codex/catalog/metadata.ts +38 -9
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +9 -3
- package/src/codex/catalog/sync.ts +8 -5
- package/src/codex/data/upstream-models.json +169 -0
- package/src/grok/inject.ts +1 -1
- package/src/lib/token-estimate.ts +42 -38
- package/src/lib/translator-budget.ts +34 -0
- package/src/oauth/index.ts +10 -4
- package/src/oauth/kiro.ts +71 -6
- package/src/oauth/store.ts +3 -1
- package/src/oauth/types.ts +4 -0
- package/src/providers/derive.ts +7 -5
- package/src/providers/opencode-go-transport.ts +18 -0
- package/src/providers/registry.ts +34 -10
- package/src/providers/xai-transport.ts +10 -0
- package/src/responses/compaction.ts +8 -1
- package/src/responses/namespace-aliases.ts +56 -0
- package/src/responses/parser.ts +12 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/snapshot-policy.ts +108 -0
- package/src/responses/state.ts +23 -10
- package/src/responses/turn-termination.ts +108 -0
- package/src/responses/xai-custom-tool-compat.ts +237 -0
- package/src/server/grok-responses-snapshot-repair.ts +338 -0
- package/src/server/index.ts +2 -1
- package/src/server/relay-eager.ts +1 -0
- package/src/server/responses/core.ts +173 -13
- package/src/server/responses-image-gen-repair.ts +2 -2
- package/src/server/sse-payload-rewrite.ts +20 -3
- package/src/types.ts +10 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +7 -0
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
package/src/oauth/kiro.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type KiroImportDiagnostic,
|
|
28
28
|
} from "./kiro-credentials";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
|
+
import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants";
|
|
30
31
|
import { getAccountSet, saveAccountCredential } from "./store";
|
|
31
32
|
|
|
32
33
|
const DEFAULT_REGION = "us-east-1";
|
|
@@ -172,13 +173,37 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
/** Kiro profile ARN structure: arn:<partition>:codewhisperer:<region>:<account>:profile/<id> */
|
|
177
|
+
const KIRO_PROFILE_ARN_PATTERN = /^arn:[a-z0-9-]+:codewhisperer:[a-z0-9-]+:\d{12}:profile\/[A-Za-z0-9-]+$/;
|
|
178
|
+
const KIRO_PROFILE_ARN_MAX_LENGTH = 256;
|
|
179
|
+
|
|
180
|
+
function parseKiroProfileArn(value: unknown): string | undefined {
|
|
181
|
+
if (typeof value !== "string") return undefined;
|
|
182
|
+
const trimmed = value.trim();
|
|
183
|
+
if (trimmed.length === 0 || trimmed.length > KIRO_PROFILE_ARN_MAX_LENGTH) return undefined;
|
|
184
|
+
return KIRO_PROFILE_ARN_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefined {
|
|
188
|
+
// Only narrowly-named documented-ish shapes; never invent an ARN (#993).
|
|
189
|
+
return parseKiroProfileArn(parsed.profileArn)
|
|
190
|
+
?? parseKiroProfileArn(parsed.profile_arn)
|
|
191
|
+
?? (parsed.profile && typeof parsed.profile === "object" && !Array.isArray(parsed.profile)
|
|
192
|
+
? parseKiroProfileArn((parsed.profile as Record<string, unknown>).arn)
|
|
193
|
+
: undefined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> {
|
|
176
197
|
try {
|
|
177
198
|
const result = await runner(["whoami", "--format", "json"], signal);
|
|
178
199
|
if (result.exitCode !== 0) return {};
|
|
179
|
-
const parsed = JSON.parse(result.stdout) as
|
|
200
|
+
const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
180
201
|
const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
|
|
181
|
-
|
|
202
|
+
const profileArn = profileArnFromWhoami(parsed);
|
|
203
|
+
return {
|
|
204
|
+
...(email && email.length <= 320 ? { email } : {}),
|
|
205
|
+
...(profileArn ? { profileArn } : {}),
|
|
206
|
+
};
|
|
182
207
|
} catch {
|
|
183
208
|
return {};
|
|
184
209
|
}
|
|
@@ -191,6 +216,7 @@ function metadataFromImported(imported: ImportedKiroCredential): KiroOAuthMetada
|
|
|
191
216
|
...(imported.apiRegion ? { apiRegion: imported.apiRegion } : {}),
|
|
192
217
|
...(imported.clientId ? { clientId: imported.clientId } : {}),
|
|
193
218
|
...(imported.clientSecret ? { clientSecret: imported.clientSecret } : {}),
|
|
219
|
+
...(imported.authType ? { authType: imported.authType } : {}),
|
|
194
220
|
};
|
|
195
221
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
196
222
|
}
|
|
@@ -251,14 +277,34 @@ async function oauthCredentialFromImported(
|
|
|
251
277
|
runner: KiroCliRunner,
|
|
252
278
|
signal?: AbortSignal,
|
|
253
279
|
): Promise<OAuthCredentials> {
|
|
254
|
-
|
|
255
|
-
|
|
280
|
+
let identity: { email?: string; profileArn?: string } = {};
|
|
281
|
+
if (imported.source === "sqlite") {
|
|
282
|
+
identity = await readKiroCliIdentity(runner, signal);
|
|
283
|
+
// Session-switch race (#993 review): another process may have switched the
|
|
284
|
+
// active Kiro CLI session between the SQLite read and whoami. Accept
|
|
285
|
+
// whoami's identity only when the session token STILL matches the import —
|
|
286
|
+
// refresh token, or access token when refresh is absent.
|
|
287
|
+
if (identity.profileArn !== undefined || identity.email !== undefined) {
|
|
288
|
+
const current = readKiroCliSqliteCredential();
|
|
289
|
+
const importedKey = imported.refresh || imported.access;
|
|
290
|
+
const currentKey = current ? current.refresh || current.access : "";
|
|
291
|
+
if (!current || currentKey !== importedKey) identity = {};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Builder ID imports often lack a profileArn in SQLite; whoami against the
|
|
295
|
+
// SAME active CLI session can supply it (#993). Imported stays authoritative.
|
|
296
|
+
const resolvedProfileArn = imported.profileArn ?? identity.profileArn;
|
|
297
|
+
const metadata: KiroOAuthMetadata | undefined = (() => {
|
|
298
|
+
const base = metadataFromImported(imported) ?? {};
|
|
299
|
+
if (resolvedProfileArn && !base.profileArn) base.profileArn = resolvedProfileArn;
|
|
300
|
+
return Object.keys(base).length > 0 ? base : undefined;
|
|
301
|
+
})();
|
|
256
302
|
return {
|
|
257
303
|
access: imported.access,
|
|
258
304
|
refresh: imported.refresh,
|
|
259
305
|
expires: imported.expires,
|
|
260
306
|
source: imported.source === "json" ? "credential-file" : "local-cli",
|
|
261
|
-
...(
|
|
307
|
+
...(resolvedProfileArn ? { accountId: resolvedProfileArn } : {}),
|
|
262
308
|
...(identity.email ? { email: identity.email } : {}),
|
|
263
309
|
...(metadata ? { kiro: metadata } : {}),
|
|
264
310
|
};
|
|
@@ -429,6 +475,25 @@ export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profile
|
|
|
429
475
|
return readImportedKiroCredential()?.profileArn;
|
|
430
476
|
}
|
|
431
477
|
|
|
478
|
+
/** Request profile and envelope choice are one decision; a fallback is never account identity. */
|
|
479
|
+
export function resolveKiroRequestProfile(
|
|
480
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
481
|
+
): { profileArn: string | undefined; builderIdFallback: boolean } {
|
|
482
|
+
const own = resolveKiroProfileArn(account);
|
|
483
|
+
if (own) return { profileArn: own, builderIdFallback: false };
|
|
484
|
+
// An explicitly selected account must never borrow a different active local CLI account.
|
|
485
|
+
const authType = account === undefined ? readImportedKiroCredential()?.authType : account.authType;
|
|
486
|
+
return authType === "aws_sso_oidc"
|
|
487
|
+
? { profileArn: KIRO_BUILDER_ID_SERVICE_PROFILE_ARN, builderIdFallback: true }
|
|
488
|
+
: { profileArn: undefined, builderIdFallback: false };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function resolveKiroRequestProfileArn(
|
|
492
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
493
|
+
): string | undefined {
|
|
494
|
+
return resolveKiroRequestProfile(account).profileArn;
|
|
495
|
+
}
|
|
496
|
+
|
|
432
497
|
async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
|
|
433
498
|
let oauthError: string | undefined;
|
|
434
499
|
try {
|
package/src/oauth/store.ts
CHANGED
|
@@ -247,13 +247,15 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
|
|
|
247
247
|
const apiRegion = clean(kiro.apiRegion, 64);
|
|
248
248
|
const clientId = clean(kiro.clientId, 4096);
|
|
249
249
|
const clientSecret = clean(kiro.clientSecret, 4096);
|
|
250
|
-
|
|
250
|
+
const authType = kiro.authType === "aws_sso_oidc" || kiro.authType === "kiro_desktop" ? kiro.authType : undefined;
|
|
251
|
+
if (profileArn || ssoRegion || apiRegion || clientId || clientSecret || authType) {
|
|
251
252
|
normalized.kiro = {
|
|
252
253
|
...(profileArn ? { profileArn } : {}),
|
|
253
254
|
...(ssoRegion ? { ssoRegion } : {}),
|
|
254
255
|
...(apiRegion ? { apiRegion } : {}),
|
|
255
256
|
...(clientId ? { clientId } : {}),
|
|
256
257
|
...(clientSecret ? { clientSecret } : {}),
|
|
258
|
+
...(authType ? { authType } : {}),
|
|
257
259
|
};
|
|
258
260
|
}
|
|
259
261
|
}
|
package/src/oauth/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
|
|
2
2
|
export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
|
|
3
3
|
|
|
4
|
+
export type KiroCredentialAuthType = "kiro_desktop" | "aws_sso_oidc";
|
|
5
|
+
|
|
4
6
|
/** Account-scoped Kiro data required for refresh and request routing. */
|
|
5
7
|
export interface KiroOAuthMetadata {
|
|
6
8
|
profileArn?: string;
|
|
@@ -8,6 +10,8 @@ export interface KiroOAuthMetadata {
|
|
|
8
10
|
apiRegion?: string;
|
|
9
11
|
clientId?: string;
|
|
10
12
|
clientSecret?: string;
|
|
13
|
+
/** Non-secret request-routing signal; never substitute for an account profile. */
|
|
14
|
+
authType?: KiroCredentialAuthType;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export type OAuthCredentials = {
|
package/src/providers/derive.ts
CHANGED
|
@@ -239,15 +239,17 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
239
239
|
if (!prov.models && seed.models) prov.models = [...seed.models];
|
|
240
240
|
if (prov.liveModels === undefined && seed.liveModels !== undefined) prov.liveModels = seed.liveModels;
|
|
241
241
|
if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
|
|
242
|
-
if (
|
|
242
|
+
if (seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows, ...prov.modelContextWindows };
|
|
243
243
|
if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
|
|
244
244
|
if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
|
|
245
|
-
if (
|
|
245
|
+
if (seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens, ...prov.modelMaxOutputTokens };
|
|
246
246
|
if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
// One customized model must not hide the registry's knowledge of every other
|
|
248
|
+
// model. Fill per key on new maps; explicit empty ladders remain authoritative.
|
|
249
|
+
if (seed.modelReasoningEfforts) prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
|
|
250
|
+
if (seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts, ...prov.modelDefaultReasoningEfforts };
|
|
249
251
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
250
|
-
if (
|
|
252
|
+
if (seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = { ...cloneNestedRecord(seed.modelReasoningEffortMap), ...cloneNestedRecord(prov.modelReasoningEffortMap ?? {}) };
|
|
251
253
|
if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
|
|
252
254
|
if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
|
|
253
255
|
if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
|
|
@@ -4,6 +4,24 @@ import { registryEntryForProviderDestination } from "./registry";
|
|
|
4
4
|
|
|
5
5
|
export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
|
|
6
6
|
|
|
7
|
+
const MUSE_RESPONSE_MODELS = new Set(["muse-spark-1.2-contributor", "muse-spark-1.3-contributor"]);
|
|
8
|
+
const MUSE_RESPONSE_URLS = new Set([
|
|
9
|
+
"https://opencode.ai/zen/v1/responses",
|
|
10
|
+
"https://opencode.ai/zen/go/v1/responses",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/** Compatibility belongs to the exact model AND effective destination, not a provider label. */
|
|
14
|
+
export function isOpenCodeMuseResponses(modelId: unknown, responseUrl: string): boolean {
|
|
15
|
+
if (typeof modelId !== "string" || !MUSE_RESPONSE_MODELS.has(modelId.trim().toLowerCase())) return false;
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(responseUrl);
|
|
18
|
+
if (url.username || url.password || url.search || url.hash) return false;
|
|
19
|
+
return MUSE_RESPONSE_URLS.has(`${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
function hasHeaderCaseInsensitive(
|
|
8
26
|
headers: Record<string, string> | undefined,
|
|
9
27
|
name: string,
|
|
@@ -29,7 +29,12 @@ export type InboundWire = "responses" | "chat" | "anthropic";
|
|
|
29
29
|
* A per-model wire default: a bare string applies to every inbound, while the object
|
|
30
30
|
* form applies only to the listed inbound protocols.
|
|
31
31
|
*/
|
|
32
|
-
export type ModelWireDefault = string | {
|
|
32
|
+
export type ModelWireDefault = string | {
|
|
33
|
+
wire: string;
|
|
34
|
+
inbound: readonly InboundWire[];
|
|
35
|
+
/** Optional auth-mode gate for providers whose subscription and API-key products use different wires. */
|
|
36
|
+
authModes?: readonly ProviderAuthKind[];
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
export type ProviderModelDiscoveryScalar = string | number | boolean;
|
|
35
40
|
|
|
@@ -700,6 +705,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
700
705
|
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
701
706
|
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"],
|
|
702
707
|
defaultModel: "grok-4.5",
|
|
708
|
+
// Grok's subscription gateway exposes 4.6/4.5 natively on Responses. Scope the default to
|
|
709
|
+
// OAuth Codex traffic: API-key and Chat/Anthropic callers keep their existing wire, while an
|
|
710
|
+
// explicit modelAdapters override still wins over this registry-only default.
|
|
711
|
+
modelWireDefaults: {
|
|
712
|
+
"grok-4.6": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
|
|
713
|
+
"grok-4.5": { wire: "openai-responses", inbound: ["responses"], authModes: ["oauth"] },
|
|
714
|
+
},
|
|
703
715
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
704
716
|
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
705
717
|
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
|
|
@@ -824,16 +836,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
824
836
|
featured: true,
|
|
825
837
|
dashboardUrl: "https://platform.openai.com/api-keys",
|
|
826
838
|
defaultModel: "gpt-5.5",
|
|
827
|
-
models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS],
|
|
839
|
+
models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, "gpt-6-astra"],
|
|
828
840
|
liveModels: true,
|
|
829
|
-
|
|
830
|
-
|
|
841
|
+
// API limits differ from the Codex-login Astra pin. Keep the public API ladder
|
|
842
|
+
// separate: low..max is documented, while the native catalog also carries ultra.
|
|
843
|
+
modelContextWindows: { ...OPENAI_API_GPT56_CONTEXT_WINDOWS, "gpt-6-astra": 1_050_000 },
|
|
844
|
+
modelMaxInputTokens: { ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, "gpt-6-astra": 922_000 },
|
|
845
|
+
modelMaxOutputTokens: { "gpt-6-astra": 128_000 },
|
|
831
846
|
modelInputModalities: Object.fromEntries(
|
|
832
|
-
["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, ["text", "image"]]),
|
|
833
|
-
),
|
|
834
|
-
modelReasoningEfforts: Object.fromEntries(
|
|
835
|
-
[...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
|
|
847
|
+
["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, "gpt-6-astra"].map(id => [id, ["text", "image"]]),
|
|
836
848
|
),
|
|
849
|
+
modelReasoningEfforts: {
|
|
850
|
+
...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS])),
|
|
851
|
+
"gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
|
|
852
|
+
},
|
|
837
853
|
virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
|
|
838
854
|
},
|
|
839
855
|
{
|
|
@@ -871,11 +887,15 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
871
887
|
},
|
|
872
888
|
modelContextWindows: {
|
|
873
889
|
"kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
|
|
890
|
+
"muse-spark-1.3-contributor": 1_048_576,
|
|
891
|
+
"muse-spark-1.2-contributor": 1_048_576,
|
|
874
892
|
[OPENCODE_OX_ALPHA_FREE_MODEL]: OX_ALPHA_CONTEXT_WINDOW,
|
|
875
893
|
[DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
|
|
876
894
|
},
|
|
877
895
|
modelInputModalities: {
|
|
878
896
|
"kimi-k3": ["text", "image"],
|
|
897
|
+
"muse-spark-1.3-contributor": ["text", "image"],
|
|
898
|
+
"muse-spark-1.2-contributor": ["text", "image"],
|
|
879
899
|
[OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"],
|
|
880
900
|
[DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
|
|
881
901
|
},
|
|
@@ -1799,8 +1819,12 @@ export function providerModelWireDefault(
|
|
|
1799
1819
|
if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
|
|
1800
1820
|
const declared = entry.modelWireDefaults[modelId.trim().toLowerCase()];
|
|
1801
1821
|
if (declared === undefined) return undefined;
|
|
1802
|
-
// A bare string applies to every inbound; the object form
|
|
1803
|
-
if (typeof declared !== "string"
|
|
1822
|
+
// A bare string applies to every inbound/auth mode; the object form may narrow either.
|
|
1823
|
+
if (typeof declared !== "string") {
|
|
1824
|
+
if (!declared.inbound.includes(inbound)) return undefined;
|
|
1825
|
+
const authMode = provider.authMode ?? entry.authKind;
|
|
1826
|
+
if (declared.authModes && !declared.authModes.includes(authMode)) return undefined;
|
|
1827
|
+
}
|
|
1804
1828
|
const wire = typeof declared === "string" ? declared : declared.wire;
|
|
1805
1829
|
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
|
|
1806
1830
|
}
|
|
@@ -22,6 +22,16 @@ export const XAI_GROK_COMPATIBILITY = {
|
|
|
22
22
|
export const XAI_GROK_CLIENT_VERSION = XAI_GROK_COMPATIBILITY.version;
|
|
23
23
|
export const XAI_CONV_ID_HEADER = XAI_GROK_COMPATIBILITY.headers.conversationId;
|
|
24
24
|
|
|
25
|
+
/** Both xAI Responses hosts share the same request dialect. */
|
|
26
|
+
export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
27
|
+
try {
|
|
28
|
+
const hostname = new URL(provider.baseUrl).hostname.toLowerCase();
|
|
29
|
+
return hostname === "api.x.ai" || hostname === "cli-chat-proxy.grok.com";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export type OcxProviderTransport = OcxProviderConfig & {
|
|
26
36
|
/** Request executor used only at runtime; never persisted. */
|
|
27
37
|
fetch?: typeof globalThis.fetch;
|
|
@@ -105,7 +105,14 @@ export function buildCompactV1Output(userMessages: string[], summary: string): R
|
|
|
105
105
|
remaining -= msg.length;
|
|
106
106
|
} else {
|
|
107
107
|
// Budget partially covers this older message: keep its tail (most recent context) and stop.
|
|
108
|
-
|
|
108
|
+
let tailStart = msg.length - remaining;
|
|
109
|
+
// Never start the retained tail on a lone LOW surrogate: the pair's
|
|
110
|
+
// other half would be lost and encoding substitutes U+FFFD.
|
|
111
|
+
if (tailStart > 0 && tailStart < msg.length) {
|
|
112
|
+
const first = msg.charCodeAt(tailStart);
|
|
113
|
+
if (first >= 0xdc00 && first <= 0xdfff) tailStart += 1;
|
|
114
|
+
}
|
|
115
|
+
selected.push(msg.slice(tailStart));
|
|
109
116
|
break;
|
|
110
117
|
}
|
|
111
118
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { namespacedToolName, type OcxTool } from "../types";
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
|
+
|
|
4
|
+
export interface NamespacedToolIdentity { namespace: string; name: string }
|
|
5
|
+
type ToolIdentity = Pick<OcxTool, "namespace" | "name">;
|
|
6
|
+
|
|
7
|
+
function sameIdentity(left: ToolIdentity, right: ToolIdentity): boolean {
|
|
8
|
+
return (left.namespace ?? "") === (right.namespace ?? "") && left.name === right.name;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve all ownership before publishing aliases. Dots may occur in both halves,
|
|
13
|
+
* and a flat declaration can own a spelling which looks namespaced. Neither the
|
|
14
|
+
* declaration order nor an ambiguous dotted name may choose a different tool.
|
|
15
|
+
*/
|
|
16
|
+
export function declaredNamespaceAliases(
|
|
17
|
+
tools: readonly ToolIdentity[],
|
|
18
|
+
budget?: TranslatorBudget,
|
|
19
|
+
): Map<string, NamespacedToolIdentity> {
|
|
20
|
+
const owners = new Map<string, ToolIdentity | null>();
|
|
21
|
+
const candidates = new Set<string>();
|
|
22
|
+
let temporaryBytes = 0;
|
|
23
|
+
const claim = (spelling: string, tool: ToolIdentity, candidate: boolean) => {
|
|
24
|
+
if (!owners.has(spelling)) {
|
|
25
|
+
const bytes = Buffer.byteLength(spelling) + Buffer.byteLength(tool.name)
|
|
26
|
+
+ Buffer.byteLength(tool.namespace ?? "") + 64;
|
|
27
|
+
budget?.chargeRetained(bytes, { kind: "request_copies" });
|
|
28
|
+
temporaryBytes += bytes;
|
|
29
|
+
owners.set(spelling, tool);
|
|
30
|
+
} else {
|
|
31
|
+
const owner = owners.get(spelling);
|
|
32
|
+
if (owner && !sameIdentity(owner, tool)) owners.set(spelling, null);
|
|
33
|
+
}
|
|
34
|
+
if (candidate) candidates.add(spelling);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
for (const tool of tools) claim(namespacedToolName(tool.namespace, tool.name), tool, !!tool.namespace);
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
if (!tool.namespace || tool.namespace.includes("__") || tool.name.includes("__")) continue;
|
|
40
|
+
claim(`${tool.namespace}.${tool.name}`, tool, true);
|
|
41
|
+
}
|
|
42
|
+
const aliases = new Map<string, NamespacedToolIdentity>();
|
|
43
|
+
for (const spelling of candidates) {
|
|
44
|
+
const owner = owners.get(spelling);
|
|
45
|
+
if (!owner?.namespace) continue;
|
|
46
|
+
budget?.chargeRetained(
|
|
47
|
+
Buffer.byteLength(spelling) + Buffer.byteLength(owner.namespace) + Buffer.byteLength(owner.name),
|
|
48
|
+
{ kind: "request_copies" },
|
|
49
|
+
);
|
|
50
|
+
aliases.set(spelling, { namespace: owner.namespace, name: owner.name });
|
|
51
|
+
}
|
|
52
|
+
return aliases;
|
|
53
|
+
} finally {
|
|
54
|
+
budget?.releaseRetained(temporaryBytes, { kind: "request_copies" });
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -425,6 +425,18 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
425
425
|
: null;
|
|
426
426
|
const thinkingText = envelope?.txt || text;
|
|
427
427
|
|
|
428
|
+
// Kiro reasoning round-trip: a krc-only item carries nothing renderable — it is provider
|
|
429
|
+
// state for the assistant turn that ALREADY closed, because Kiro emits its
|
|
430
|
+
// reasoningContentEvent at the END of a turn (after content AND tool calls, verified
|
|
431
|
+
// against kiro-cli 2.14.1/2.16.0). Folding it into the FOLLOWING turn like ordinary
|
|
432
|
+
// reasoning would attach turn N's blob to turn N+1, so attach it backwards instead. With
|
|
433
|
+
// no assistant turn to own it the blob is dropped rather than mis-paired.
|
|
434
|
+
if (envelope?.krc && thinkingText.length === 0) {
|
|
435
|
+
const previous = messages[messages.length - 1];
|
|
436
|
+
if (previous?.role === "assistant") previous.kiroRedactedReasoning = envelope.krc;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
// Native/non-ocxr1 encrypted-only reasoning is opaque here. Do not create a detached
|
|
429
441
|
// assistant turn or invent replayable plaintext/signatures from the encrypted payload.
|
|
430
442
|
if (thinkingText.length > 0) {
|
|
@@ -24,6 +24,12 @@ export interface ReasoningEnvelope {
|
|
|
24
24
|
* so replay needs it even though the visible summary was suppressed.
|
|
25
25
|
*/
|
|
26
26
|
txt?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Kiro `reasoningContentEvent.redactedContent`: a KMS-encrypted reasoning blob that is opaque to
|
|
29
|
+
* the proxy. Kiro's own CLI replays it on the matching `assistantResponseMessage` to preserve
|
|
30
|
+
* model reasoning across turns, so it round-trips here the same way a signature does.
|
|
31
|
+
*/
|
|
32
|
+
krc?: string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function encodeReasoningEnvelope(envelope: ReasoningEnvelope): string {
|
|
@@ -45,7 +51,9 @@ export function decodeReasoningEnvelope(encryptedContent: string): ReasoningEnve
|
|
|
45
51
|
}
|
|
46
52
|
const txt = (parsed as { txt?: unknown }).txt;
|
|
47
53
|
if (typeof txt === "string" && txt.length > 0) envelope.txt = txt;
|
|
48
|
-
|
|
54
|
+
const krc = (parsed as { krc?: unknown }).krc;
|
|
55
|
+
if (typeof krc === "string" && krc.length > 0) envelope.krc = krc;
|
|
56
|
+
return envelope.sig || envelope.red || envelope.txt || envelope.krc ? envelope : null;
|
|
49
57
|
} catch {
|
|
50
58
|
return null;
|
|
51
59
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { lstatSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
forgetHardenedSecretPath,
|
|
5
|
+
hardenSecretPathAsync,
|
|
6
|
+
windowsSecretAclApplies,
|
|
7
|
+
} from "../lib/windows-secret-acl";
|
|
8
|
+
|
|
9
|
+
const BASE_DEBOUNCE_MS = 2_000;
|
|
10
|
+
const SCALE_FROM_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_DEBOUNCE_MS = 30_000;
|
|
12
|
+
|
|
13
|
+
/** Large snapshots trade a bounded hard-kill recovery window for fewer atomic replacements. */
|
|
14
|
+
export function responseSnapshotDebounceMs(bytes: number): number {
|
|
15
|
+
if (!Number.isFinite(bytes) || bytes <= SCALE_FROM_BYTES) return BASE_DEBOUNCE_MS;
|
|
16
|
+
return Math.min(MAX_DEBOUNCE_MS, Math.round(BASE_DEBOUNCE_MS * bytes / SCALE_FROM_BYTES));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResponseSnapshotWriteMetrics {
|
|
20
|
+
writes: number;
|
|
21
|
+
unchangedSkips: number;
|
|
22
|
+
bytesWritten: number;
|
|
23
|
+
lastSnapshotBytes: number;
|
|
24
|
+
debounceMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type SnapshotWrite = (path: string, payload: string) => Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Memoize only a digest, size and resolved target, never another copy of the snapshot.
|
|
31
|
+
* The state store owns the single-flight writer gate; this class does not queue writes.
|
|
32
|
+
*/
|
|
33
|
+
export class ResponseSnapshotWriter {
|
|
34
|
+
private digest: string | null = null;
|
|
35
|
+
private target: string | null = null;
|
|
36
|
+
private bytes = 0;
|
|
37
|
+
private writes = 0;
|
|
38
|
+
private unchangedSkips = 0;
|
|
39
|
+
private bytesWritten = 0;
|
|
40
|
+
|
|
41
|
+
constructor(private readonly write: SnapshotWrite) {}
|
|
42
|
+
|
|
43
|
+
metrics(): ResponseSnapshotWriteMetrics {
|
|
44
|
+
return {
|
|
45
|
+
writes: this.writes,
|
|
46
|
+
unchangedSkips: this.unchangedSkips,
|
|
47
|
+
bytesWritten: this.bytesWritten,
|
|
48
|
+
lastSnapshotBytes: this.bytes,
|
|
49
|
+
debounceMs: responseSnapshotDebounceMs(this.bytes),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reset(): void {
|
|
54
|
+
this.digest = null;
|
|
55
|
+
this.target = null;
|
|
56
|
+
this.bytes = 0;
|
|
57
|
+
this.writes = 0;
|
|
58
|
+
this.unchangedSkips = 0;
|
|
59
|
+
this.bytesWritten = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async diskMatches(path: string, payload: string, bytes: number): Promise<boolean> {
|
|
63
|
+
try {
|
|
64
|
+
const before = lstatSync(path);
|
|
65
|
+
// Atomic replacement would replace a symlink or break a hardlink. An optimization must
|
|
66
|
+
// not silently preserve either, even if it currently resolves to identical bytes.
|
|
67
|
+
if (!before.isFile() || before.nlink !== 1 || before.size !== bytes) return false;
|
|
68
|
+
if (realpathSync(path) !== this.target) return false;
|
|
69
|
+
if (!windowsSecretAclApplies()) {
|
|
70
|
+
if ((before.mode & 0o777) !== 0o600) return false;
|
|
71
|
+
if ((statSync(dirname(path)).mode & 0o777) !== 0o700) return false;
|
|
72
|
+
} else {
|
|
73
|
+
// An ordinary atomic write hardens a new temp. A skipped write must still use the
|
|
74
|
+
// required publication policy, not a potentially stale pathname-only success memo.
|
|
75
|
+
forgetHardenedSecretPath(path);
|
|
76
|
+
try { await hardenSecretPathAsync(path, { required: true }); }
|
|
77
|
+
finally { forgetHardenedSecretPath(path); }
|
|
78
|
+
}
|
|
79
|
+
// The digest records our last write, not the current disk contents. A same-size edit,
|
|
80
|
+
// a second process or a deleted snapshot must never turn into a false cache hit.
|
|
81
|
+
if (await Bun.file(path).text() !== payload) return false;
|
|
82
|
+
const after = lstatSync(path);
|
|
83
|
+
return after.isFile() && after.nlink === 1 && after.dev === before.dev
|
|
84
|
+
&& after.ino === before.ino && after.size === before.size
|
|
85
|
+
&& after.mtimeMs === before.mtimeMs && realpathSync(path) === this.target;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async persist(path: string, payload: string): Promise<void> {
|
|
92
|
+
const bytes = Buffer.byteLength(payload, "utf8");
|
|
93
|
+
const digest = Bun.hash(payload).toString(36);
|
|
94
|
+
if (this.digest === digest && this.bytes === bytes && await this.diskMatches(path, payload, bytes)) {
|
|
95
|
+
this.unchangedSkips += 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.write(path, payload);
|
|
99
|
+
// Publish the memo only after a successful atomic write. A failed write keeps the previous
|
|
100
|
+
// fingerprint, which is harmless because every prospective skip rechecks the disk.
|
|
101
|
+
this.digest = digest;
|
|
102
|
+
this.bytes = bytes;
|
|
103
|
+
this.writes += 1;
|
|
104
|
+
this.bytesWritten += bytes;
|
|
105
|
+
try { this.target = realpathSync(path); }
|
|
106
|
+
catch { this.target = null; }
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/responses/state.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
4
4
|
import { atomicWriteFileAsync, getConfigDir } from "../config";
|
|
5
5
|
import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
|
|
6
6
|
import type { OcxProviderContinuationState } from "../types";
|
|
7
|
+
import { ResponseSnapshotWriter } from "./snapshot-policy";
|
|
7
8
|
import {
|
|
8
9
|
deleteResponseSpill,
|
|
9
10
|
noteStubSwapForTest,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
|
|
18
19
|
const MAX_STORED_RESPONSES = 1_000;
|
|
19
20
|
const RESPONSE_TTL_MS = 60 * 60 * 1_000;
|
|
20
|
-
const SNAPSHOT_DEBOUNCE_MS = 2_000;
|
|
21
21
|
/** In-memory high-water byte cap across all entries. Forced store:false retention (kiro/cursor
|
|
22
22
|
* continuation chains) stores the full expanded input each turn — ~quadratic bytes per chain —
|
|
23
23
|
* so a count cap alone cannot bound memory. Oldest-first eviction applies past this mark. */
|
|
@@ -69,6 +69,17 @@ let oldestResidentId: string | undefined;
|
|
|
69
69
|
let oldestResidentAt: number | null = null;
|
|
70
70
|
let byteCapOverride: number | null = null;
|
|
71
71
|
let stateRevision = 0;
|
|
72
|
+
const snapshotWriter = new ResponseSnapshotWriter(async (path, payload) => {
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
74
|
+
try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
75
|
+
await atomicWriteFileAsync(path, payload);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Scalar-only observation; does not load, prune or retain snapshot contents. */
|
|
79
|
+
export function responseSnapshotMetricsForTests() {
|
|
80
|
+
return snapshotWriter.metrics();
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
73
84
|
const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
|
|
74
85
|
|
|
@@ -573,14 +584,14 @@ function ensureLoaded(): void {
|
|
|
573
584
|
|
|
574
585
|
type SnapshotWriteOutcome = "stable" | "unstable" | "failed";
|
|
575
586
|
|
|
576
|
-
async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome> {
|
|
587
|
+
async function writeBoundedSnapshot(path: string, attemptLimit: number): Promise<SnapshotWriteOutcome> {
|
|
577
588
|
// Serialize writers so concurrent flush + debounce cannot race on temps / ACL (#612).
|
|
578
589
|
const previous = persistGate;
|
|
579
590
|
let release!: () => void;
|
|
580
591
|
persistGate = new Promise<void>(resolve => { release = resolve; });
|
|
581
592
|
await previous;
|
|
582
593
|
try {
|
|
583
|
-
for (let attempt = 0; attempt <
|
|
594
|
+
for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
|
|
584
595
|
const revision = stateRevision;
|
|
585
596
|
const entries: Array<[string, unknown]> = [];
|
|
586
597
|
let total = 0;
|
|
@@ -602,9 +613,7 @@ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome>
|
|
|
602
613
|
entries.push(persistEntry);
|
|
603
614
|
}
|
|
604
615
|
entries.reverse();
|
|
605
|
-
|
|
606
|
-
try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
607
|
-
await atomicWriteFileAsync(path, JSON.stringify({ version: 2, states: entries }));
|
|
616
|
+
await snapshotWriter.persist(path, JSON.stringify({ version: 2, states: entries }));
|
|
608
617
|
persistAttemptHookForTests?.();
|
|
609
618
|
if (revision === stateRevision) return "stable";
|
|
610
619
|
}
|
|
@@ -627,7 +636,7 @@ function schedulePersistAt(path: string, replace = false): void {
|
|
|
627
636
|
if (persistTimer && !replace) return;
|
|
628
637
|
if (persistTimer) clearTimeout(persistTimer);
|
|
629
638
|
pendingPersistPath = path;
|
|
630
|
-
persistTimer = setTimeout(() => { void persistNow(path); },
|
|
639
|
+
persistTimer = setTimeout(() => { void persistNow(path); }, snapshotWriter.metrics().debounceMs);
|
|
631
640
|
(persistTimer as { unref?: () => void }).unref?.();
|
|
632
641
|
}
|
|
633
642
|
|
|
@@ -637,12 +646,15 @@ async function persistNow(path: string, awaitFollowUp = false): Promise<void> {
|
|
|
637
646
|
persistTimer = null;
|
|
638
647
|
}
|
|
639
648
|
pendingPersistPath = null;
|
|
640
|
-
|
|
649
|
+
// Background traffic gets one write per debounce, not four immediate full-file rewrites.
|
|
650
|
+
// Explicit shutdown flushes retain the existing bounded stabilization contract.
|
|
651
|
+
const attemptLimit = awaitFollowUp ? MAX_SNAPSHOT_REWRITE_ATTEMPTS : 1;
|
|
652
|
+
let outcome = await writeBoundedSnapshot(path, attemptLimit);
|
|
641
653
|
if (outcome === "unstable" && awaitFollowUp) {
|
|
642
654
|
if (persistTimer) clearTimeout(persistTimer);
|
|
643
655
|
persistTimer = null;
|
|
644
656
|
pendingPersistPath = null;
|
|
645
|
-
outcome = await writeBoundedSnapshot(path);
|
|
657
|
+
outcome = await writeBoundedSnapshot(path, attemptLimit);
|
|
646
658
|
}
|
|
647
659
|
if (outcome === "stable") drainPendingSpillUnlinks();
|
|
648
660
|
else if (outcome === "unstable" && !awaitFollowUp) schedulePersistAt(path, true);
|
|
@@ -957,7 +969,7 @@ export function rememberResponseState(
|
|
|
957
969
|
schedulePersist();
|
|
958
970
|
}
|
|
959
971
|
|
|
960
|
-
/** Test-only persistence churn hook; invoked after each
|
|
972
|
+
/** Test-only persistence churn hook; invoked after each write/unchanged-validation attempt. */
|
|
961
973
|
export function setResponseStatePersistAttemptHookForTests(hook: (() => void) | null): void {
|
|
962
974
|
persistAttemptHookForTests = hook;
|
|
963
975
|
}
|
|
@@ -986,6 +998,7 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
986
998
|
oldestResidentId = undefined;
|
|
987
999
|
oldestResidentAt = null;
|
|
988
1000
|
stateRevision = 0;
|
|
1001
|
+
snapshotWriter.reset();
|
|
989
1002
|
pendingSpillUnlinks.length = 0;
|
|
990
1003
|
spillCounters.writes = 0;
|
|
991
1004
|
spillCounters.writeFailures = 0;
|