@bitkyc08/opencodex 2.75.0 → 2.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -0,0 +1,598 @@
1
+ import { constants, createHash, generateKeyPairSync, privateDecrypt, randomUUID } from "node:crypto";
2
+ import { redactSecretString } from "../lib/redact";
3
+
4
+ export const ZED_WEB_BASE_URL = "https://zed.dev";
5
+ export const ZED_CLOUD_BASE_URL = "https://cloud.zed.dev";
6
+ export const ZED_HEADERS = Object.freeze({
7
+ expiredToken: "x-zed-expired-token",
8
+ outdatedToken: "x-zed-outdated-token",
9
+ clientSupportsStatus: "x-zed-client-supports-status-messages",
10
+ clientSupportsStreamEnded: "x-zed-client-supports-stream-ended-request-completion-status",
11
+ clientSupportsXai: "x-zed-client-supports-x-ai",
12
+ systemId: "x-zed-system-id",
13
+ });
14
+
15
+ const PRIVATE_KEY_PREFIX = "zed-rsa-pkcs1:";
16
+ const LLM_TOKEN_TTL_MS = 50 * 60 * 1000;
17
+ const MODEL_CACHE_TTL_MS = 60 * 60 * 1000;
18
+ const MODEL_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
19
+ const SMALL_RESPONSE_MAX_BYTES = 64 * 1024;
20
+ const MODEL_MAX_ROWS = 2_000;
21
+
22
+ export interface ZedCredentials {
23
+ userId: string;
24
+ accessToken: string;
25
+ systemId?: string;
26
+ }
27
+
28
+ export interface ZedNativeAuthData {
29
+ authUrl: string;
30
+ privateKeyVerifier: string;
31
+ nativeAppPort: number;
32
+ systemId: string;
33
+ publicKey: string;
34
+ }
35
+
36
+ export interface ZedCallbackPayload {
37
+ userId: string;
38
+ encryptedAccessToken: string;
39
+ }
40
+
41
+ export interface ZedModel {
42
+ id: string;
43
+ name: string;
44
+ provider?: string;
45
+ contextLength?: number;
46
+ contextLengthInMaxMode?: number;
47
+ maxOutputTokens?: number;
48
+ supportsTools: boolean;
49
+ supportsImages: boolean;
50
+ supportsThinking: boolean;
51
+ supportsDisablingThinking: boolean;
52
+ supportsFastMode: boolean;
53
+ supportedEffortLevels: string[];
54
+ isDisabled: boolean;
55
+ }
56
+
57
+ export interface ZedModelCatalog {
58
+ expiresAt: number;
59
+ models: ZedModel[];
60
+ rawById: Map<string, Record<string, unknown>>;
61
+ defaultModel?: string;
62
+ defaultFastModel?: string;
63
+ recommendedModels: string[];
64
+ }
65
+
66
+ export type ZedFetch = typeof globalThis.fetch;
67
+
68
+ interface ZedRequestOptions {
69
+ fetchFn?: ZedFetch;
70
+ signal?: AbortSignal;
71
+ forceRefresh?: boolean;
72
+ }
73
+
74
+ interface CachedLlmToken {
75
+ token: string;
76
+ expiresAt: number;
77
+ organizationId: string;
78
+ }
79
+
80
+ const llmTokenCache = new Map<string, CachedLlmToken>();
81
+ const organizationCache = new Map<string, string>();
82
+ const modelCache = new Map<string, ZedModelCatalog>();
83
+ const modelInflight = new Map<string, Promise<ZedModelCatalog>>();
84
+
85
+ function base64Url(value: Uint8Array | string): string {
86
+ return Buffer.from(value).toString("base64url");
87
+ }
88
+
89
+ function decodeBase64Url(value: string): Buffer {
90
+ return Buffer.from(value, "base64url");
91
+ }
92
+
93
+ function normalizeBaseUrl(value: string | undefined, fallback: string): string {
94
+ return String(value || fallback).replace(/\/+$/, "");
95
+ }
96
+
97
+ function stringValue(value: unknown): string | undefined {
98
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
99
+ }
100
+
101
+ function normalizeModelId(value: unknown): string {
102
+ if (typeof value === "string") return value.trim();
103
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0].trim();
104
+ if (value && typeof value === "object" && typeof (value as { id?: unknown }).id === "string") {
105
+ return String((value as { id: string }).id).trim();
106
+ }
107
+ return "";
108
+ }
109
+
110
+ function zedCacheKey(credentials: ZedCredentials): string {
111
+ // In-memory caches are keyed by an irreversible digest so no map key holds the raw token.
112
+ return createHash("sha256").update(JSON.stringify([credentials.userId, credentials.accessToken])).digest("hex");
113
+ }
114
+
115
+ function zedUrl(path: string, baseUrl = ZED_CLOUD_BASE_URL): string {
116
+ return `${normalizeBaseUrl(baseUrl, ZED_CLOUD_BASE_URL)}${path}`;
117
+ }
118
+
119
+ /** Build Zed's native-app URL and retain the private key only in an opaque verifier string. */
120
+ export function createZedNativeAuthData(nativeAppPort: number, systemId = randomUUID()): ZedNativeAuthData {
121
+ if (!Number.isInteger(nativeAppPort) || nativeAppPort <= 0 || nativeAppPort > 65_535) {
122
+ throw new Error("Zed native-app callback port is invalid");
123
+ }
124
+ const { publicKey, privateKey } = generateKeyPairSync("rsa", {
125
+ modulusLength: 2_048,
126
+ publicKeyEncoding: { type: "pkcs1", format: "der" },
127
+ privateKeyEncoding: { type: "pkcs1", format: "pem" },
128
+ });
129
+ const url = new URL(`${ZED_WEB_BASE_URL}/native_app_signin`);
130
+ url.searchParams.set("native_app_port", String(nativeAppPort));
131
+ url.searchParams.set("native_app_public_key", base64Url(publicKey));
132
+ url.searchParams.set("system_id", systemId);
133
+ return {
134
+ authUrl: url.toString(),
135
+ privateKeyVerifier: `${PRIVATE_KEY_PREFIX}${base64Url(privateKey)}`,
136
+ nativeAppPort,
137
+ systemId,
138
+ publicKey: base64Url(publicKey),
139
+ };
140
+ }
141
+
142
+ function decodePrivateKeyVerifier(verifier: string): string {
143
+ if (!verifier.startsWith(PRIVATE_KEY_PREFIX)) {
144
+ throw new Error("Missing Zed private-key verifier; restart the login flow");
145
+ }
146
+ return decodeBase64Url(verifier.slice(PRIVATE_KEY_PREFIX.length)).toString("utf8");
147
+ }
148
+
149
+ /** Parse a native-app callback URL, query string, or JSON payload without logging its token. */
150
+ export function parseZedCallbackPayload(input: string): ZedCallbackPayload {
151
+ const raw = input.trim();
152
+ if (!raw) throw new Error("Missing Zed callback URL");
153
+ let data: Record<string, unknown> = {};
154
+ try {
155
+ const parsed: unknown = JSON.parse(raw);
156
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
157
+ data = parsed as Record<string, unknown>;
158
+ } catch {
159
+ let url: URL;
160
+ try {
161
+ url = new URL(raw);
162
+ } catch {
163
+ const query = raw.includes("?") ? raw.slice(raw.indexOf("?") + 1) : raw.replace(/^\?/, "");
164
+ url = new URL(`http://127.0.0.1/?${query}`);
165
+ }
166
+ url.searchParams.forEach((value, key) => { data[key] = value; });
167
+ }
168
+ const userId = stringValue(data.user_id) ?? stringValue(data.userId);
169
+ const encryptedAccessToken = stringValue(data.access_token)
170
+ ?? stringValue(data.accessToken)
171
+ ?? stringValue(data.token);
172
+ if (!userId || !encryptedAccessToken) {
173
+ throw new Error("Zed callback must include user_id and access_token");
174
+ }
175
+ return { userId, encryptedAccessToken };
176
+ }
177
+
178
+ /** Decrypt the callback token with OAEP-SHA256, falling back to PKCS#1 v1.5 for older Zed builds. */
179
+ export function decryptZedAccessToken(encryptedAccessToken: string, privateKeyVerifier: string): string {
180
+ const encrypted = decodeBase64Url(encryptedAccessToken);
181
+ const privateKey = decodePrivateKeyVerifier(privateKeyVerifier);
182
+ let oaepError: unknown;
183
+ try {
184
+ const token = privateDecrypt(
185
+ { key: privateKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
186
+ encrypted,
187
+ ).toString("utf8");
188
+ if (!token || token.includes("�")) throw new Error("decrypted token is invalid UTF-8");
189
+ return token;
190
+ } catch (error) {
191
+ oaepError = error;
192
+ }
193
+ try {
194
+ const token = privateDecrypt(
195
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
196
+ encrypted,
197
+ ).toString("utf8");
198
+ // PKCS#1 v1.5 unpadding is not integrity-checked. Replacement characters are a
199
+ // concrete signal that a wrong key or malformed ciphertext produced garbage.
200
+ if (!token || token.includes("�")) throw new Error("decrypted token is invalid UTF-8");
201
+ return token;
202
+ } catch {
203
+ const reason = oaepError instanceof Error ? oaepError.message : "unsupported ciphertext";
204
+ throw new Error(`Failed to decrypt Zed access token: ${reason}`);
205
+ }
206
+ }
207
+
208
+ export function buildZedUserAuthHeader(credentials: ZedCredentials): string {
209
+ if (!credentials.userId || !credentials.accessToken) throw new Error("Zed credential is incomplete");
210
+ return `${credentials.userId} ${credentials.accessToken}`;
211
+ }
212
+
213
+ function systemHeaders(credentials: ZedCredentials): Record<string, string> {
214
+ return credentials.systemId ? { [ZED_HEADERS.systemId]: credentials.systemId } : {};
215
+ }
216
+
217
+ /** Remove the account token and user id from upstream text before it can reach an error or log. */
218
+ export function scrubZedCredentials(text: string, credentials: ZedCredentials): string {
219
+ let scrubbed = text;
220
+ for (const secret of [credentials.accessToken, credentials.userId]) {
221
+ const value = secret?.trim();
222
+ if (value) scrubbed = scrubbed.split(value).join("[redacted]");
223
+ }
224
+ return redactSecretString(scrubbed);
225
+ }
226
+
227
+ function rejectionMessage(error: unknown): string {
228
+ return error instanceof Error ? error.message : String(error);
229
+ }
230
+
231
+ function containsZedCredential(text: string, credentials: ZedCredentials): boolean {
232
+ return [credentials.accessToken, credentials.userId].some((secret) => {
233
+ const value = secret?.trim();
234
+ return Boolean(value) && text.includes(value!);
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Rethrow a rejection from a credential-bearing Zed call without the account token or user id.
240
+ *
241
+ * The decision is made on content alone: a rejection that carries neither value is rethrown by
242
+ * identity, so a clean abort stays the same object. One that carries either is replaced, keeping
243
+ * its `name` (a DOMException stays a DOMException, so AbortError/TimeoutError still read as
244
+ * cancellation) and any numeric `status`. The original is not kept as `cause`, because its
245
+ * message is exactly what must not travel.
246
+ */
247
+ export function scrubZedRejection(error: unknown, credentials: ZedCredentials): never {
248
+ const message = rejectionMessage(error);
249
+ const name = error instanceof Error ? error.name : "Error";
250
+ if (!containsZedCredential(message, credentials) && !containsZedCredential(name, credentials)) throw error;
251
+ const scrubbed = scrubZedCredentials(message, credentials);
252
+ const safeName = containsZedCredential(name, credentials) ? "Error" : name;
253
+ const replacement: Error = error instanceof DOMException
254
+ ? new DOMException(scrubbed, safeName)
255
+ : Object.assign(new Error(scrubbed), { name: safeName });
256
+ const status = error && typeof error === "object" ? (error as { status?: unknown }).status : undefined;
257
+ if (typeof status === "number") Object.assign(replacement, { status });
258
+ throw replacement;
259
+ }
260
+
261
+ async function responseJson(response: Response, label: string, credentials: ZedCredentials): Promise<unknown> {
262
+ const length = Number(response.headers.get("content-length"));
263
+ if (Number.isFinite(length) && length > SMALL_RESPONSE_MAX_BYTES) {
264
+ await response.body?.cancel().catch(() => undefined);
265
+ throw new Error(`${label}: response exceeded the size limit`);
266
+ }
267
+ const reader = response.body?.getReader();
268
+ if (!reader) {
269
+ if (!response.ok) throw new Error(`${label}: HTTP ${response.status}`);
270
+ return null;
271
+ }
272
+ const chunks: Uint8Array[] = [];
273
+ let total = 0;
274
+ try {
275
+ while (true) {
276
+ const { done, value } = await reader.read();
277
+ if (done) break;
278
+ if (!value) continue;
279
+ total += value.byteLength;
280
+ if (total > SMALL_RESPONSE_MAX_BYTES) {
281
+ await reader.cancel().catch(() => undefined);
282
+ throw new Error(`${label}: response exceeded the size limit`);
283
+ }
284
+ chunks.push(value);
285
+ }
286
+ } finally {
287
+ reader.releaseLock();
288
+ }
289
+ const bytes = new Uint8Array(total);
290
+ let offset = 0;
291
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
292
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
293
+ let data: unknown = null;
294
+ if (text) {
295
+ try { data = JSON.parse(text) as unknown; } catch { data = { raw: text }; }
296
+ }
297
+ if (response.ok) return data;
298
+ const record = data && typeof data === "object" && !Array.isArray(data)
299
+ ? data as Record<string, unknown>
300
+ : undefined;
301
+ const nested = record?.error && typeof record.error === "object" && !Array.isArray(record.error)
302
+ ? record.error as Record<string, unknown>
303
+ : undefined;
304
+ const message = stringValue(record?.message) ?? stringValue(nested?.message) ?? `HTTP ${response.status}`;
305
+ const error = new Error(`${label}: ${scrubZedCredentials(message, credentials)}`);
306
+ Object.assign(error, { status: response.status });
307
+ throw error;
308
+ }
309
+
310
+ export async function fetchZedAuthenticatedUser(
311
+ credentials: ZedCredentials,
312
+ options: ZedRequestOptions = {},
313
+ ): Promise<Record<string, unknown>> {
314
+ const fetchFn = options.fetchFn ?? globalThis.fetch;
315
+ let value: unknown;
316
+ try {
317
+ const response = await fetchFn(zedUrl("/client/users/me"), {
318
+ method: "GET",
319
+ headers: {
320
+ Accept: "application/json",
321
+ Authorization: buildZedUserAuthHeader(credentials),
322
+ ...systemHeaders(credentials),
323
+ },
324
+ signal: options.signal,
325
+ });
326
+ value = await responseJson(response, "Zed account lookup", credentials);
327
+ } catch (error) {
328
+ scrubZedRejection(error, credentials);
329
+ }
330
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Zed account lookup returned an invalid response");
331
+ return value as Record<string, unknown>;
332
+ }
333
+
334
+ function normalizeOrganizationId(value: unknown): string | undefined {
335
+ if (typeof value === "string" && value.trim()) return value.trim();
336
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].trim()) return value[0].trim();
337
+ if (value && typeof value === "object" && typeof (value as { id?: unknown }).id === "string") {
338
+ return String((value as { id: string }).id).trim() || undefined;
339
+ }
340
+ return undefined;
341
+ }
342
+
343
+ export function resolveZedOrganizationId(user: Record<string, unknown>): string | undefined {
344
+ const explicit = normalizeOrganizationId(user.default_organization_id)
345
+ ?? normalizeOrganizationId(user.defaultOrganizationId)
346
+ ?? normalizeOrganizationId(user.organization_id)
347
+ ?? normalizeOrganizationId(user.organizationId)
348
+ ?? normalizeOrganizationId(user.default_organization);
349
+ if (explicit) return explicit;
350
+ const organizations = Array.isArray(user.organizations) ? user.organizations : [];
351
+ const personal = organizations.find(item => item && typeof item === "object" && (item as { is_personal?: unknown }).is_personal === true);
352
+ const selected = personal ?? organizations[0];
353
+ return selected && typeof selected === "object"
354
+ ? normalizeOrganizationId((selected as { id?: unknown }).id)
355
+ : undefined;
356
+ }
357
+
358
+ async function resolveOrganization(
359
+ credentials: ZedCredentials,
360
+ options: ZedRequestOptions,
361
+ ): Promise<string> {
362
+ const key = zedCacheKey(credentials);
363
+ const cached = organizationCache.get(key);
364
+ if (cached) return cached;
365
+ const user = await fetchZedAuthenticatedUser(credentials, options);
366
+ const organizationId = resolveZedOrganizationId(user);
367
+ if (!organizationId) throw new Error("Zed account has no organization");
368
+ organizationCache.set(key, organizationId);
369
+ return organizationId;
370
+ }
371
+
372
+ export async function fetchZedLlmToken(
373
+ credentials: ZedCredentials,
374
+ options: ZedRequestOptions = {},
375
+ ): Promise<string> {
376
+ const key = zedCacheKey(credentials);
377
+ const cached = llmTokenCache.get(key);
378
+ if (!options.forceRefresh && cached && cached.expiresAt > Date.now()) return cached.token;
379
+ const organizationId = await resolveOrganization(credentials, options);
380
+ const fetchFn = options.fetchFn ?? globalThis.fetch;
381
+ let data: unknown;
382
+ try {
383
+ const response = await fetchFn(zedUrl("/client/llm_tokens"), {
384
+ method: "POST",
385
+ headers: {
386
+ Accept: "application/json",
387
+ "Content-Type": "application/json",
388
+ Authorization: buildZedUserAuthHeader(credentials),
389
+ ...systemHeaders(credentials),
390
+ },
391
+ body: JSON.stringify({ organization_id: organizationId }),
392
+ signal: options.signal,
393
+ });
394
+ data = await responseJson(response, "Zed LLM token exchange", credentials);
395
+ } catch (error) {
396
+ scrubZedRejection(error, credentials);
397
+ }
398
+ const record = data && typeof data === "object" && !Array.isArray(data)
399
+ ? data as Record<string, unknown>
400
+ : undefined;
401
+ const token = stringValue(record?.token)
402
+ ?? (record?.token && typeof record.token === "object" ? stringValue((record.token as { value?: unknown }).value) : undefined)
403
+ ?? stringValue(record?.access_token)
404
+ ?? stringValue(record?.llm_token)
405
+ ?? stringValue(record?.llmToken);
406
+ if (!token) throw new Error("Zed LLM token exchange returned no token");
407
+ llmTokenCache.set(key, { token, organizationId, expiresAt: Date.now() + LLM_TOKEN_TTL_MS });
408
+ return token;
409
+ }
410
+
411
+ export function shouldRefreshZedLlmToken(response: Response): boolean {
412
+ return response.status === 401
413
+ || response.headers.has(ZED_HEADERS.expiredToken)
414
+ || response.headers.has(ZED_HEADERS.outdatedToken);
415
+ }
416
+
417
+ export async function zedLlmFetch(
418
+ credentials: ZedCredentials,
419
+ path: string,
420
+ options: ZedRequestOptions & { fetchInit?: RequestInit; baseUrl?: string } = {},
421
+ ): Promise<Response> {
422
+ const fetchFn = options.fetchFn ?? globalThis.fetch;
423
+ const request = async (forceRefresh: boolean): Promise<Response> => {
424
+ const token = await fetchZedLlmToken(credentials, { ...options, forceRefresh });
425
+ const headers = new Headers(options.fetchInit?.headers);
426
+ headers.set("Authorization", `Bearer ${token}`);
427
+ try {
428
+ return await fetchFn(zedUrl(path, options.baseUrl), {
429
+ ...options.fetchInit,
430
+ headers,
431
+ signal: options.signal,
432
+ });
433
+ } catch (error) {
434
+ scrubZedRejection(error, credentials);
435
+ }
436
+ };
437
+ let response = await request(false);
438
+ if (shouldRefreshZedLlmToken(response)) {
439
+ await response.body?.cancel().catch(() => undefined);
440
+ response = await request(true);
441
+ }
442
+ return response;
443
+ }
444
+
445
+ function mapZedModel(raw: unknown): ZedModel | undefined {
446
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
447
+ const model = raw as Record<string, unknown>;
448
+ const id = normalizeModelId(model.id);
449
+ if (!id) return undefined;
450
+ const numberField = (...keys: string[]): number | undefined => {
451
+ for (const key of keys) {
452
+ const value = model[key];
453
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
454
+ }
455
+ return undefined;
456
+ };
457
+ const listField = (...keys: string[]): string[] => {
458
+ for (const key of keys) {
459
+ if (!Array.isArray(model[key])) continue;
460
+ return model[key].filter(value => typeof value === "string" && value.trim()).map(value => String(value));
461
+ }
462
+ return [];
463
+ };
464
+ const contextLength = numberField("max_token_count", "maxTokenCount");
465
+ const contextLengthInMaxMode = numberField("max_token_count_in_max_mode", "maxTokenCountInMaxMode");
466
+ const maxOutputTokens = numberField("max_output_tokens", "maxOutputTokens");
467
+ return {
468
+ id,
469
+ name: stringValue(model.display_name) ?? stringValue(model.displayName) ?? id,
470
+ ...(stringValue(model.provider) ? { provider: stringValue(model.provider) } : {}),
471
+ ...(contextLength ? { contextLength } : {}),
472
+ ...(contextLengthInMaxMode ? { contextLengthInMaxMode } : {}),
473
+ ...(maxOutputTokens ? { maxOutputTokens } : {}),
474
+ supportsTools: model.supports_tools === true || model.supportsTools === true,
475
+ supportsImages: model.supports_images === true || model.supportsImages === true,
476
+ supportsThinking: model.supports_thinking === true || model.supportsThinking === true,
477
+ supportsDisablingThinking: model.supports_disabling_thinking === true || model.supportsDisablingThinking === true,
478
+ supportsFastMode: model.supports_fast_mode === true || model.supportsFastMode === true,
479
+ supportedEffortLevels: listField("supported_effort_levels", "supportedEffortLevels"),
480
+ isDisabled: model.is_disabled === true || model.isDisabled === true,
481
+ };
482
+ }
483
+
484
+ async function readBoundedJson(response: Response, maxBytes: number): Promise<unknown> {
485
+ const length = Number(response.headers.get("content-length"));
486
+ if (Number.isFinite(length) && length > maxBytes) {
487
+ if (response.body) await response.body.cancel().catch(() => undefined);
488
+ throw new Error("Zed model catalog exceeded the response size limit");
489
+ }
490
+ const reader = response.body?.getReader();
491
+ if (!reader) throw new Error("Zed model catalog returned no body");
492
+ const chunks: Uint8Array[] = [];
493
+ let total = 0;
494
+ try {
495
+ while (true) {
496
+ const { done, value } = await reader.read();
497
+ if (done) break;
498
+ if (!value) continue;
499
+ total += value.byteLength;
500
+ if (total > maxBytes) {
501
+ await reader.cancel().catch(() => undefined);
502
+ throw new Error("Zed model catalog exceeded the response size limit");
503
+ }
504
+ chunks.push(value);
505
+ }
506
+ } finally {
507
+ reader.releaseLock();
508
+ }
509
+ const bytes = new Uint8Array(total);
510
+ let offset = 0;
511
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
512
+ try {
513
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
514
+ } catch {
515
+ throw new Error("Zed model catalog returned invalid JSON");
516
+ }
517
+ }
518
+
519
+ /** Resolve the account-scoped live model roster; the list is display metadata, not a model allowlist. */
520
+ export async function resolveZedModels(
521
+ credentials: ZedCredentials,
522
+ options: ZedRequestOptions = {},
523
+ ): Promise<ZedModelCatalog> {
524
+ const key = zedCacheKey(credentials);
525
+ const cached = modelCache.get(key);
526
+ if (!options.forceRefresh && cached && cached.expiresAt > Date.now()) return cached;
527
+ const existing = modelInflight.get(key);
528
+ if (existing && !options.forceRefresh) return existing;
529
+ const promise = (async (): Promise<ZedModelCatalog> => {
530
+ const response = await zedLlmFetch(credentials, "/models", {
531
+ ...options,
532
+ fetchInit: {
533
+ method: "GET",
534
+ headers: {
535
+ Accept: "application/json",
536
+ [ZED_HEADERS.clientSupportsXai]: "true",
537
+ },
538
+ },
539
+ });
540
+ if (!response.ok) throw new Error(`Zed model catalog failed: HTTP ${response.status}`);
541
+ const data = await readBoundedJson(response, MODEL_RESPONSE_MAX_BYTES);
542
+ const record = data && typeof data === "object" && !Array.isArray(data)
543
+ ? data as Record<string, unknown>
544
+ : undefined;
545
+ const rawModels = Array.isArray(data)
546
+ ? data
547
+ : Array.isArray(record?.models)
548
+ ? record.models
549
+ : Array.isArray(record?.data)
550
+ ? record.data
551
+ : Array.isArray(record?.available_models) ? record.available_models : undefined;
552
+ if (!rawModels || rawModels.length > MODEL_MAX_ROWS) throw new Error("Zed model catalog returned too many models");
553
+ const models = rawModels.map(mapZedModel).filter((model): model is ZedModel => !!model && !model.isDisabled);
554
+ const rawById = new Map<string, Record<string, unknown>>();
555
+ for (const raw of rawModels) {
556
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
557
+ const id = normalizeModelId((raw as Record<string, unknown>).id);
558
+ if (id) rawById.set(id, raw as Record<string, unknown>);
559
+ }
560
+ const catalog: ZedModelCatalog = {
561
+ expiresAt: Date.now() + MODEL_CACHE_TTL_MS,
562
+ models,
563
+ rawById,
564
+ ...(normalizeModelId(record?.default_model ?? record?.defaultModel) ? { defaultModel: normalizeModelId(record?.default_model ?? record?.defaultModel) } : {}),
565
+ ...(normalizeModelId(record?.default_fast_model ?? record?.defaultFastModel) ? { defaultFastModel: normalizeModelId(record?.default_fast_model ?? record?.defaultFastModel) } : {}),
566
+ recommendedModels: (Array.isArray(record?.recommended_models) ? record.recommended_models : Array.isArray(record?.recommendedModels) ? record.recommendedModels : [])
567
+ .map(normalizeModelId).filter(Boolean),
568
+ };
569
+ modelCache.set(key, catalog);
570
+ return catalog;
571
+ })();
572
+ modelInflight.set(key, promise);
573
+ try {
574
+ return await promise;
575
+ } finally {
576
+ if (modelInflight.get(key) === promise) modelInflight.delete(key);
577
+ }
578
+ }
579
+
580
+ export function normalizeZedProvider(value: unknown, model: string): "anthropic" | "open_ai" | "google" | "x_ai" {
581
+ const raw = String(value ?? "").toLowerCase();
582
+ if (raw === "anthropic") return "anthropic";
583
+ if (raw === "openai" || raw === "open_ai") return "open_ai";
584
+ if (raw === "google" || raw === "gemini") return "google";
585
+ if (raw === "xai" || raw === "x_ai" || raw === "x-ai") return "x_ai";
586
+ const lowerModel = model.toLowerCase();
587
+ if (lowerModel.includes("claude")) return "anthropic";
588
+ if (lowerModel.includes("gemini")) return "google";
589
+ if (lowerModel.includes("grok") || lowerModel.includes("xai")) return "x_ai";
590
+ return "open_ai";
591
+ }
592
+
593
+ export function clearZedCaches(): void {
594
+ llmTokenCache.clear();
595
+ organizationCache.clear();
596
+ modelCache.clear();
597
+ modelInflight.clear();
598
+ }
@@ -1,30 +1,34 @@
1
1
  import type { StoredResponseState } from "../state";
2
2
 
3
3
  /**
4
- * Pick the snapshot entries that fit the byte budgets, in `states` order.
4
+ * Pick the snapshot entries that fit the byte budgets, in `states` order, and
5
+ * return them already serialized.
5
6
  *
6
7
  * Bounded stubs and tombstones are selected before residents: they are the
7
8
  * only durable references a spill file has, and demotion is oldest-first, so a
8
9
  * single newest-first pass would let resident payloads consume the whole
9
10
  * budget ahead of them. Residents then fill what remains, newest-first so the
10
11
  * most recent chains survive both legacy snapshot caps.
12
+ *
13
+ * Each entry has to be stringified once anyway to measure it, so the kept
14
+ * strings are returned instead of the entries: serializing the whole selection
15
+ * again would walk up to the full snapshot budget a second time per write.
11
16
  */
12
17
  export function selectSnapshotEntries(
13
18
  states: ReadonlyMap<string, StoredResponseState>,
14
19
  totalMaxBytes: number,
15
20
  residentEntryMaxBytes: number,
16
- ): Array<[string, unknown]> {
21
+ ): string[] {
17
22
  const ordered = [...states].reverse();
18
- const persisted = new Map<string, [string, unknown]>();
23
+ const persisted = new Map<string, string>();
19
24
  let total = 0;
20
25
  // UTF-8 bytes, not UTF-16 code units: multibyte items otherwise slip past
21
26
  // both snapshot caps at up to 2x the intended size.
22
- const sizeOf = (entry: [string, unknown]): number => Buffer.byteLength(JSON.stringify(entry), "utf8");
23
27
  for (const [id, state] of ordered) {
24
28
  if (state.kind === "resident") continue;
25
29
  const { sizeBytes: _sizeBytes, ...smallState } = state;
26
- const entry: [string, unknown] = [id, smallState];
27
- const size = sizeOf(entry);
30
+ const entry = JSON.stringify([id, smallState]);
31
+ const size = Buffer.byteLength(entry, "utf8");
28
32
  if (total + size > totalMaxBytes) continue;
29
33
  total += size;
30
34
  persisted.set(id, entry);
@@ -32,18 +36,27 @@ export function selectSnapshotEntries(
32
36
  for (const [id, state] of ordered) {
33
37
  if (state.kind !== "resident") continue;
34
38
  const { sizeBytes: _sizeBytes, kind: _kind, ...resident } = state;
35
- const entry: [string, unknown] = [id, resident];
36
- const size = sizeOf(entry);
39
+ const entry = JSON.stringify([id, resident]);
40
+ const size = Buffer.byteLength(entry, "utf8");
37
41
  if (size > residentEntryMaxBytes) continue;
38
42
  if (total + size > totalMaxBytes) break;
39
43
  total += size;
40
44
  persisted.set(id, entry);
41
45
  }
42
46
  // Emit in map order so reload and count eviction keep the same relative order as `states`.
43
- const entries: Array<[string, unknown]> = [];
47
+ const entries: string[] = [];
44
48
  for (const [id] of states) {
45
49
  const kept = persisted.get(id);
46
50
  if (kept) entries.push(kept);
47
51
  }
48
52
  return entries;
49
53
  }
54
+
55
+ /**
56
+ * The snapshot file body for serialized entries. Byte-identical to
57
+ * `JSON.stringify({ version: 2, states: entries.map(e => JSON.parse(e)) })`: compact
58
+ * JSON.stringify renders an array element exactly as it renders that value alone.
59
+ */
60
+ export function snapshotPayload(serializedEntries: readonly string[]): string {
61
+ return '{"version":2,"states":[' + serializedEntries.join(",") + "]}";
62
+ }
@@ -19,7 +19,7 @@ import {
19
19
  writeResponseSpillDurably,
20
20
  } from "./spill-store";
21
21
  import { collectReferencedSpillFileNames, snapshotReferencedSpillFileNames } from "./state/spill-inspect";
22
- import { selectSnapshotEntries } from "./state/snapshot-select";
22
+ import { selectSnapshotEntries, snapshotPayload } from "./state/snapshot-select";
23
23
  import { clientCarriedPrefixLength, providerIssuedIdentity } from "./state/replay-fingerprint";
24
24
  export type { ResponseStateTempRecoveryResult, ResponseStateTempRecoveryOptions } from "./state/temp-recovery";
25
25
  export type { ResponseSpillDirInspection } from "./spill-store";
@@ -705,8 +705,7 @@ async function writeBoundedSnapshot(path: string, attemptLimit: number): Promise
705
705
  try {
706
706
  for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
707
707
  const revision = stateRevision;
708
- const entries = selectSnapshotEntries(states, snapshotTotalBytes(), SNAPSHOT_ENTRY_MAX_BYTES);
709
- const payload = JSON.stringify({ version: 2, states: entries });
708
+ const payload = snapshotPayload(selectSnapshotEntries(states, snapshotTotalBytes(), SNAPSHOT_ENTRY_MAX_BYTES));
710
709
  const payloadBytes = Buffer.byteLength(payload, "utf8");
711
710
  const payloadDigest = Bun.hash(payload).toString(36);
712
711
  // A mutation does not always change what gets persisted: entries past the