@bitkyc08/opencodex 2.82.0 → 2.83.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 (85) hide show
  1. package/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
  2. package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
  3. package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/catalog.ts +2 -2
  8. package/src/adapters/cursor/effort-map.ts +1 -1
  9. package/src/adapters/devin/live-models.ts +2 -2
  10. package/src/adapters/google-truncation.ts +22 -0
  11. package/src/adapters/google.ts +7 -3
  12. package/src/adapters/ollama-native.ts +18 -14
  13. package/src/adapters/openai-responses/passthrough.ts +2 -0
  14. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  15. package/src/adapters/openai-responses/reasoning.ts +11 -1
  16. package/src/bridge/errors.ts +7 -1
  17. package/src/claude/context-windows.ts +3 -3
  18. package/src/claude/desktop-picker.ts +1 -1
  19. package/src/cli/account-auth.ts +20 -12
  20. package/src/cli/resolve.ts +1 -1
  21. package/src/clients/config-export/model-metadata.ts +16 -0
  22. package/src/clients/config-export/zcode-store.ts +46 -11
  23. package/src/clients/config-export/zcode.ts +3 -5
  24. package/src/clients/config-export.ts +1 -1
  25. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  26. package/src/codex/auth-context.ts +8 -3
  27. package/src/codex/catalog/native-models.ts +1 -1
  28. package/src/codex/catalog/provider-models.ts +25 -0
  29. package/src/codex/desktop-app/darwin.ts +1 -1
  30. package/src/codex/desktop-app/handoff.ts +1 -1
  31. package/src/codex/desktop-app/linux.ts +1 -1
  32. package/src/codex/desktop-app/lock.ts +1 -1
  33. package/src/codex/desktop-app/types.ts +1 -1
  34. package/src/codex/desktop-app/windows.ts +1 -1
  35. package/src/codex/desktop-app-restart.ts +1 -1
  36. package/src/codex/main-account.ts +6 -7
  37. package/src/generated/compatibility-version.json +91 -75
  38. package/src/grok/inject.ts +245 -221
  39. package/src/grok/reset-coupon-ledger.ts +223 -57
  40. package/src/grok/reset-coupons.ts +13 -6
  41. package/src/integrations/cursor-detect.ts +1 -1
  42. package/src/lib/errors.ts +25 -0
  43. package/src/lib/redact.ts +2 -2
  44. package/src/oauth/index.ts +1 -1
  45. package/src/oauth/meta-muse-device.ts +1 -1
  46. package/src/oauth/types.ts +1 -1
  47. package/src/providers/anthropic-fast.ts +1 -1
  48. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  49. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  50. package/src/providers/devin-provider-merge-migration.ts +1 -1
  51. package/src/providers/github-copilot-auto.ts +169 -0
  52. package/src/providers/model-presets.ts +1 -1
  53. package/src/providers/muse-subscription-usage.ts +1 -1
  54. package/src/providers/registry/entries-core.ts +6 -6
  55. package/src/providers/registry/entries-extended.ts +4 -0
  56. package/src/providers/registry/model-seeds.ts +3 -3
  57. package/src/providers/xai-fast-model.ts +1 -1
  58. package/src/server/inbound-body-admission.ts +24 -7
  59. package/src/server/index/link-listener.ts +30 -17
  60. package/src/server/index/serve-options.ts +8 -2
  61. package/src/server/index.ts +1 -1
  62. package/src/server/lifecycle.ts +31 -4
  63. package/src/server/management/grok-coupon-routes.ts +277 -52
  64. package/src/server/management/link-routes.ts +42 -20
  65. package/src/server/management/route-registry.ts +2 -2
  66. package/src/server/relay-eager.ts +2 -0
  67. package/src/server/relay.ts +18 -7
  68. package/src/server/request-log-failure-attribution.ts +3 -0
  69. package/src/server/request-log-terminal-status.ts +186 -0
  70. package/src/server/request-log.ts +15 -118
  71. package/src/server/responses/adapter-continuation.ts +9 -1
  72. package/src/server/responses/adapter-dispatch.ts +65 -6
  73. package/src/server/responses/compact.ts +3 -2
  74. package/src/server/responses/core-opaque-recovery.ts +5 -5
  75. package/src/server/responses/core.ts +6 -6
  76. package/src/server/responses/empty-completion-guard.ts +18 -2
  77. package/src/server/responses/jev-model-invoke.ts +1 -1
  78. package/src/server/responses/passthrough-dispatch.ts +115 -60
  79. package/src/server/responses/passthrough-execution.ts +3 -3
  80. package/src/server/responses/request-transport.ts +125 -5
  81. package/src/server/responses/run-turn-execution.ts +13 -5
  82. package/src/server/responses/sidecar-execution.ts +5 -0
  83. package/src/server/responses/spend-storage-error.ts +33 -0
  84. package/src/types/request.ts +5 -0
  85. package/src/usage/expected-prices.ts +1 -1
@@ -0,0 +1,169 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { OcxParsedRequest, OcxProviderConfig } from "../types";
3
+ import { readBoundedResponseBody } from "../lib/bounded-body";
4
+ import { ProviderOutboundSendCancelledError, providerOutboundGet, providerOutboundPost } from "../lib/provider-outbound";
5
+ import { githubCopilotHttpError, resolveCopilotApiBaseUrl } from "../oauth/github-copilot";
6
+ import { resolveGithubCopilotTransport } from "./github-copilot-transport";
7
+ import { waitForProviderRequestSlot, releaseProviderRequestSlot } from "./request-pacing";
8
+
9
+ export interface CopilotModel {
10
+ id: string;
11
+ model_picker_enabled?: boolean;
12
+ supported_endpoints?: string[];
13
+ [key: string]: unknown;
14
+ }
15
+ const catalogs = new Map<string, { expires: number; models: CopilotModel[] }>();
16
+ const MAX_MODELS = 512;
17
+ const MAX_RESPONSE_BYTES = 1_048_576;
18
+ export class CopilotAutoHttpError extends Error {
19
+ /** Preserve only the upstream status and bounded retry delay; never retain response bodies. */
20
+ constructor(readonly status: number, readonly retryAfter?: string) {
21
+ super(githubCopilotHttpError("Auto negotiation", status).message);
22
+ }
23
+ }
24
+ /** Reject array/scalar payloads before interpreting negotiation objects. */
25
+ function record(value: unknown): Record<string, unknown> | undefined {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value)
27
+ ? value as Record<string, unknown> : undefined;
28
+ }
29
+ /** Accept a bounded, homogeneous list so malformed pools cannot grant routing eligibility. */
30
+ function strings(value: unknown): string[] {
31
+ return Array.isArray(value) && value.length <= MAX_MODELS
32
+ && value.every(/** Require every pool entry to be a bounded nonempty string before it can authorize model selection. */
33
+ v => typeof v === "string" && v.length > 0 && v.length <= 256)
34
+ ? value as string[] : [];
35
+ }
36
+ /** Isolate discovery caches by endpoint and credential without storing plaintext cache keys. */
37
+ function authority(provider: OcxProviderConfig): string {
38
+ return createHash("sha256").update(JSON.stringify([provider.baseUrl, provider.apiKey])).digest("hex");
39
+ }
40
+ /** Make one paced, origin-fenced negotiation send with bounded decoding and secret-safe errors. */
41
+ async function requestJson(provider: OcxProviderConfig, path: string, signal?: AbortSignal, body?: unknown,
42
+ sessionToken?: string, beforeSend?: () => boolean, concurrency = true): Promise<Record<string, unknown>> {
43
+ const boundedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(8_000)]) : AbortSignal.timeout(8_000);
44
+ boundedSignal.throwIfAborted();
45
+ const headers = new Headers(provider.headers);
46
+ headers.set("Authorization", `Bearer ${provider.apiKey}`);
47
+ headers.set("X-GitHub-Api-Version", "2026-08-01");
48
+ headers.set("Content-Type", "application/json");
49
+ headers.delete("Copilot-Session-Token");
50
+ if (sessionToken) headers.set("Copilot-Session-Token", sessionToken);
51
+ const url = `${provider.baseUrl}${path}`;
52
+ // Queued-dispatch rebuilding can already own a provider concurrency lease; that
53
+ // caller opts out of taking a second lease while retaining interval pacing.
54
+ const slot = await waitForProviderRequestSlot("github-copilot", provider, "auto", boundedSignal, { concurrency });
55
+ try {
56
+ const init = { headers, signal: boundedSignal };
57
+ const response = body === undefined
58
+ ? await providerOutboundGet("github-copilot", provider, url, init, { beforeSend })
59
+ : await providerOutboundPost("github-copilot", provider, url, { ...init, body: JSON.stringify(body) }, { beforeSend });
60
+ // Status-only failures: session endpoints can echo secret material in their bodies.
61
+ if (!response.ok) {
62
+ void response.body?.cancel().catch(/** Ignore cleanup failures after discarding a refusal body that may contain reflected credentials. */
63
+ () => undefined);
64
+ const retry = response.headers.get("retry-after");
65
+ throw new CopilotAutoHttpError(response.status, retry && /^\d{1,4}$/.test(retry)
66
+ ? String(Math.max(1, Math.min(3600, Number(retry)))) : undefined);
67
+ }
68
+ const bounded = await readBoundedResponseBody(response, { maxBytes: MAX_RESPONSE_BYTES,
69
+ signal: boundedSignal, totalTimeoutMs: 8_000, fatalUtf8: true });
70
+ if (bounded.truncated) throw new Error("GitHub Copilot Auto negotiation returned an incomplete response");
71
+ let value: unknown;
72
+ try { value = JSON.parse(bounded.text); } catch { /* never include raw bodies */ }
73
+ const payload = record(value);
74
+ if (!payload) throw new Error("GitHub Copilot Auto negotiation returned invalid JSON");
75
+ return payload;
76
+ } catch (error) {
77
+ if (error instanceof CopilotAutoHttpError || error instanceof ProviderOutboundSendCancelledError) throw error;
78
+ // Transport/decoder exception messages may contain reflected header values.
79
+ throw new Error(boundedSignal.aborted ? "GitHub Copilot Auto negotiation cancelled"
80
+ : "GitHub Copilot Auto negotiation transport failed");
81
+ } finally { releaseProviderRequestSlot(slot); }
82
+ }
83
+
84
+ /** Detect Auto-only permission only when every row explicitly denies named selection. */
85
+ export function copilotRequiresAuto(_provider: OcxProviderConfig, models: CopilotModel[]): boolean {
86
+ return models.length > 0 && models.every(/** Missing or malformed metadata cannot establish an account-wide picker denial. */
87
+ model => model.model_picker_enabled === false);
88
+ }
89
+ /** Retain legacy rows with unknown permissions; only an explicit denial removes a named picker row. */
90
+ export function copilotPickerModels(provider: OcxProviderConfig, models: CopilotModel[]): CopilotModel[] {
91
+ if (copilotRequiresAuto(provider, models)) return [{ id: "auto" }];
92
+ return models.filter(/** Preserve named selection unless the provider explicitly marks the row unavailable. */
93
+ model => model.model_picker_enabled !== false);
94
+ }
95
+ /** Read the credential-bound catalog; callers may disable caching or reuse an existing lease. */
96
+ export async function fetchCopilotAutoModels(provider: OcxProviderConfig, signal?: AbortSignal,
97
+ beforeSend?: () => boolean, cacheTtlMs = 60_000, concurrency = true): Promise<CopilotModel[]> {
98
+ const key = authority(provider);
99
+ const cached = catalogs.get(key);
100
+ if (cacheTtlMs > 0 && cached && cached.expires > Date.now()) return cached.models;
101
+ const payload = await requestJson(provider, "/models", signal, undefined, undefined, beforeSend, concurrency);
102
+ if (!Array.isArray(payload.data) || payload.data.length > MAX_MODELS)
103
+ throw new Error("GitHub Copilot model discovery returned an invalid catalog");
104
+ const models = payload.data.flatMap(/** Discard malformed catalog rows while retaining metadata for bounded model identifiers. */
105
+ value => {
106
+ const row = record(value);
107
+ if (!row || typeof row.id !== "string" || !row.id || row.id.length > 256) return [];
108
+ return [{ ...row, id: row.id } as CopilotModel];
109
+ });
110
+ if (cacheTtlMs > 0) {
111
+ if (!catalogs.has(key) && catalogs.size >= 32) catalogs.delete(catalogs.keys().next().value!);
112
+ catalogs.set(key, { expires: Date.now() + Math.min(cacheTtlMs, 60_000), models });
113
+ }
114
+ return models;
115
+ }
116
+ /** Reset discovery state between isolated account/permission regression cases. */
117
+ export function clearCopilotAutoModelsForTests(): void { catalogs.clear(); }
118
+
119
+ /** Resolve before adapter construction: the server can choose either wire on every turn. */
120
+ export async function resolveCopilotAuto(provider: OcxProviderConfig, requestedModel: string,
121
+ parsed: OcxParsedRequest, signal?: AbortSignal, beforeSend?: () => boolean, concurrency = true): Promise<{
122
+ provider: OcxProviderConfig; modelId: string; auto: boolean; expiresAt?: number;
123
+ }> {
124
+ const transport = resolveGithubCopilotTransport(provider, provider.authMode === "oauth"
125
+ ? resolveCopilotApiBaseUrl(provider.baseUrl) : undefined);
126
+ // Never inherit a session token from a previous account or persist it in config.
127
+ const cleanHeaders = new Headers(transport.headers);
128
+ cleanHeaders.delete("Copilot-Session-Token");
129
+ transport.headers = Object.fromEntries(cleanHeaders);
130
+ let models: CopilotModel[];
131
+ try { models = await fetchCopilotAutoModels(transport, signal, beforeSend, 60_000, concurrency); }
132
+ catch (error) {
133
+ if (signal?.aborted || error instanceof ProviderOutboundSendCancelledError) throw error;
134
+ // Permission discovery is additive for legacy paid configurations. A temporary
135
+ // discovery outage must not turn a previously usable named route into a refusal.
136
+ return { provider: transport, modelId: requestedModel, auto: false };
137
+ }
138
+ if (!copilotRequiresAuto(provider, models))
139
+ return { provider: transport, modelId: requestedModel, auto: false };
140
+ const session = await requestJson(transport, "/models/session", signal,
141
+ { auto_mode: { model_hints: ["auto"] } }, undefined, beforeSend, concurrency);
142
+ const pool = strings(session.available_models);
143
+ if (typeof session.session_token !== "string" || !session.session_token || session.session_token.length > 32_768 || !/^[\x21-\x7e]+$/.test(session.session_token) || !pool.length)
144
+ throw new Error("GitHub Copilot Auto session omitted its routing credentials");
145
+ if (session.expires_at !== undefined && (typeof session.expires_at !== "number" || !Number.isFinite(session.expires_at)))
146
+ throw new Error("GitHub Copilot Auto session returned an invalid expiry");
147
+ const expiresAt = typeof session.expires_at === "number" && Number.isFinite(session.expires_at)
148
+ ? session.expires_at * 1000 : Date.now() + 60_000;
149
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now() + 1000) throw new Error("GitHub Copilot Auto session is already expired");
150
+ const lastUser = [...parsed.context.messages].reverse().find(/** Find the newest user turn as the intent-routing source instead of replaying earlier conversation text. */
151
+ message => message.role === "user");
152
+ const prompt = lastUser?.role === "user"
153
+ ? typeof lastUser.content === "string" ? lastUser.content : lastUser.content.flatMap(/** Extract text parts for intent negotiation without forwarding image or other multimodal payloads. */
154
+ part => part.type === "text" ? [part.text] : []).join("\n")
155
+ : "";
156
+ const intent = await requestJson(transport, "/models/session/intent", signal,
157
+ { prompt: prompt.slice(0, 32_768), available_models: pool }, session.session_token, beforeSend, concurrency);
158
+ const modelId = strings(intent.candidate_models).find(/** Choose the first server-ranked candidate that belongs to this credential-bound session pool. */
159
+ candidate => pool.includes(candidate));
160
+ if (!modelId) throw new Error("GitHub Copilot Auto intent returned no eligible model");
161
+ const endpoints = strings(models.find(/** Read endpoint capabilities for the selected model before choosing its inference adapter. */
162
+ model => model.id === modelId)?.supported_endpoints);
163
+ const adapter = endpoints.includes("/responses") ? "openai-responses"
164
+ : endpoints.includes("/chat/completions") ? "openai-chat" : undefined;
165
+ if (!adapter) throw new Error("GitHub Copilot Auto selected a model with no supported inference endpoint");
166
+ return { auto: true, modelId, expiresAt: Math.min(expiresAt, Date.now() + 60_000), provider: { ...transport, adapter,
167
+ ...(adapter === "openai-responses" ? { responsesPath: "/responses" } : { chatCompletionsPath: "/chat/completions" }),
168
+ headers: { ...transport.headers, "X-GitHub-Api-Version": "2026-08-01", "Copilot-Session-Token": session.session_token } } };
169
+ }
@@ -12,7 +12,7 @@
12
12
  * The preset is a SEED, not a lock. Materialization evaluates these rules against the provider's
13
13
  * current catalog and stores CONCRETE ids in `selectedModels`, so the visibility hot path
14
14
  * (`filterCatalogVisibleModels`) never learns about patterns and an older binary still sees a
15
- * plain allowlist. Design: devlog/_plan/260824_model_ux_aliases_and_defaults/030_default_preset.md
15
+ * plain allowlist. Design: devlog/_fin/260824_model_ux_aliases_and_defaults/030_default_preset.md
16
16
  */
17
17
 
18
18
  import type { OcxProviderConfig } from "../types";
@@ -99,7 +99,7 @@ export function museUsageWindowsToQuota(usage: unknown): ProviderQuota | null {
99
99
  *
100
100
  * Kept as a thin adapter over the shared mapper: the frame nests the same two windows
101
101
  * under `subscription`, the muse-code/key response nests them under `subs_usage`
102
- * (devlog/_plan/260912_muse_device_oauth/001 C). Two parsers would drift, and the
102
+ * (devlog/_fin/260912_muse_device_oauth/001 C). Two parsers would drift, and the
103
103
  * five-hour-window discrimination above is the part that must not.
104
104
  */
105
105
  export function parseMuseSubscriptionUsage(payload: unknown): ProviderQuota | null {
@@ -94,7 +94,7 @@ import {
94
94
  * Only the models Anthropic documents for the lane are classified; Opus 4.6 silently runs
95
95
  * standard and Opus 4.7, Sonnet, Haiku and Fable reject `speed`, so they and future ids stay
96
96
  * unclassified. Source: https://platform.claude.com/docs/en/build-with-claude/fast-mode
97
- * (2026-09-23) and the live probe in devlog/_plan/260923_anthropic_fast_speed.
97
+ * (2026-09-23) and the live probe in devlog/_fin/260923_anthropic_fast_speed.
98
98
  */
99
99
  const ANTHROPIC_FAST_WIRE = Object.freeze({
100
100
  kind: "anthropic-speed" as const,
@@ -209,7 +209,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
209
209
  },
210
210
  {
211
211
  // The canonical Cognition account provider, after absorbing `devin-cli`
212
- // (devlog/_plan/260913_devin_provider_merge). The two ids were the same
212
+ // (devlog/_fin/260913_devin_provider_merge). The two ids were the same
213
213
  // `devin` adapter, the same server.codeium.com api-server, and the same
214
214
  // `devin-session-token$<JWT>` credential — only the account source
215
215
  // differed: this entry did an Auth0 browser sign-in while `devin-cli`
@@ -273,7 +273,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
273
273
  // the field but answers service_tier "default" — a live downgrade, not a fast tier.
274
274
  // Unlisted and future-discovered ids stay unclassified.
275
275
  // grok-4.7 applied and confirmed priority on OAuth Responses in the 2026-09-23
276
- // live probe: devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
276
+ // live probe: devlog/_fin/260923_grok47_parity/010_probe-evidence.md.
277
277
  modelSupportsServiceTier: {
278
278
  "grok-4.7": true,
279
279
  "grok-4.6": true,
@@ -320,7 +320,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
320
320
  models: XAI_MODELS,
321
321
  // grok-4.7-build-fast arrives only through OAuth discovery: xAI's Grok 4.7 Fast, "the same model
322
322
  // served on faster infrastructure" (docs.x.ai/developers/grok-4-7). Probed 2026-09-30
323
- // (devlog/_plan/260930_grok47_build_unify/010_probe-evidence.md): identical effort ladder, image
323
+ // (devlog/_fin/260930_grok47_build_unify/010_probe-evidence.md): identical effort ladder, image
324
324
  // input, 500k limit and advertised defaults, 1.5-1.7x faster, ~2x the ticks per output token. It is
325
325
  // not a second model row: shouldExposeProviderModel hides it, and grok-4.7's Fast selection on OAuth
326
326
  // serializes it (src/providers/xai-fast-model.ts). It keeps grok-4.7's facts in the lists below so an
@@ -450,7 +450,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
450
450
  // documents high as the 4.6 default but no multi-agent default, so do not invent one.
451
451
  modelReasoningEfforts: {
452
452
  // 2026-09-23 live probe accepted low..xhigh and rejected max on both wires;
453
- // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
453
+ // devlog/_fin/260923_grok47_parity/010_probe-evidence.md.
454
454
  "grok-4.7": ["low", "medium", "high", "xhigh"],
455
455
  "grok-4.7-build-fast": ["low", "medium", "high", "xhigh"],
456
456
  "grok-4.6": ["low", "medium", "high", "xhigh"],
@@ -460,7 +460,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
460
460
  modelDefaultReasoningEfforts: { "grok-4.7": "high", "grok-4.7-build-fast": "high", "grok-4.6": "high" },
461
461
  modelContextWindows: {
462
462
  // 500k confirmed by context_length_exceeded:
463
- // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
463
+ // devlog/_fin/260923_grok47_parity/010_probe-evidence.md.
464
464
  "grok-4.7": 500_000,
465
465
  "grok-4.7-build-fast": 500_000,
466
466
  "grok-4.6": 500_000,
@@ -1176,6 +1176,10 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1176
1176
  "openai/o3-pro": ["text"],
1177
1177
  },
1178
1178
  noVisionModels: ["openai/o3-pro"],
1179
+ // #6877: on the Responses wire OpenGateway answers 400 once history replays native
1180
+ // custom_tool_call/custom_tool_call_output items (Codex code-mode exec), and accepts the
1181
+ // same request after the custom tool is lowered to a function. Same contract as xAI.
1182
+ supportsResponsesCustomTools: false,
1179
1183
  // Chat is the broad wire (77/80 active rows). o3-pro is Responses-only, on all
1180
1184
  // inbounds. Only the two live-verified Sionic models bypass translation for Codex.
1181
1185
  // Other OpenAI/Anthropic/Gemini rows stay on Chat until their Responses tool replay
@@ -129,7 +129,7 @@ export const ZAI_GLM_5X_SIDECAR_VISION_MODELS = ZAI_GLM_5X_MODELS.filter(id => i
129
129
  *
130
130
  * Authoritative source: `GET https://api.z.ai/api/v1/models` returns `input_modalities:
131
131
  * ["text"]` for glm-5.3 and `["text", "image"]` for glm-5.3-flash (captured in
132
- * devlog/_plan/260912_zcode_protocol_and_catalog/evidence/zai-responses-models.json).
132
+ * devlog/_fin/260912_zcode_protocol_and_catalog/evidence/zai-responses-models.json).
133
133
  * docs.z.ai/devpack/latest-model says the same in prose: "GLM-5.3 is a text-only model...
134
134
  * GLM-5.3-FLASH is a multimodal model". Upstream also lists video and file for Flash;
135
135
  * neither the internal vocabulary nor the export vocabulary can express them, so `image`
@@ -158,7 +158,7 @@ export const ZAI_GLM_5X_REASONING_EFFORTS: Record<string, string[]> = {
158
158
  // 260930 MiniMax-M3.1-Flash-Preview: Token Plan / MiniMax Code only, 1M context, thinking
159
159
  // always on (effort none or thinking disabled answers 400 code 2013), omitted effort = max.
160
160
  // It returns thinking as reasoning_content and ignores reasoning_split. The live /v1/models
161
- // roster does not list it yet. Evidence: devlog/_plan/260930_minimax_m31_flash_preview/.
161
+ // roster does not list it yet. Evidence: devlog/_fin/260930_minimax_m31_flash_preview/.
162
162
  export const MINIMAX_M31_FLASH_PREVIEW = "MiniMax-M3.1-Flash-Preview";
163
163
  export const MINIMAX_MODELS = [
164
164
  MINIMAX_M31_FLASH_PREVIEW,
@@ -416,7 +416,7 @@ export const COMMAND_CODE_IMAGE_MODELS = [
416
416
  // Live 2026-09-23 3x3 random-color grid (180x180) via ocx 2.62.0:
417
417
  // 4.7 read 9/9 in user messages and tool results; 4.6 read 9/9 and 8/9.
418
418
  // Neither route requested a vision sidecar. Evidence:
419
- // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
419
+ // devlog/_fin/260923_grok47_parity/010_probe-evidence.md.
420
420
  "xai/grok-4.6",
421
421
  "xai/grok-4.7",
422
422
  // Native Z.AI VLM (docs.z.ai/guides/vlm/glm-5.3-flash). This exact id is already
@@ -7,7 +7,7 @@ import type { FastWire, OcxParsedRequest, OcxProviderConfig, TierDecision } from
7
7
  * on two lanes (same effort ladder, image input, 500k limit and advertised defaults). Measured live on
8
8
  * 2026-09-30: build-fast is 1.5-1.7x faster end to end, while `service_tier: "priority"` on grok-4.7 bought
9
9
  * no measurable speed and cost ~5.9x the ticks per output token (build-fast costs ~2x).
10
- * Evidence: devlog/_plan/260930_grok47_build_unify/010_probe-evidence.md.
10
+ * Evidence: devlog/_fin/260930_grok47_build_unify/010_probe-evidence.md.
11
11
  *
12
12
  * So on the OAuth lane only, a Fast grok-4.7 request is SERIALIZED as the build-fast id and sends no tier.
13
13
  * The logical id stays grok-4.7 everywhere else (parsed.modelId, route, policy, usage attempt), so effort,
@@ -71,20 +71,37 @@ function capacityResponse(pathname: string, error: InboundBodyCapacityError): Re
71
71
  }, { status: 503, headers: { "Retry-After": "1" } });
72
72
  }
73
73
 
74
- function retainUntilResponseSettles(response: Response, release: () => void): Response {
74
+ function retainUntilResponseSettles(response: Response, signal: AbortSignal, release: () => void): Response {
75
75
  if (!response.body) {
76
76
  release();
77
77
  return response;
78
78
  }
79
79
  const reader = response.body.getReader();
80
80
  let cancelling = false;
81
+ let cancellation: Promise<void> | undefined;
81
82
  let finalized = false;
83
+ let bodyController!: ReadableStreamDefaultController<Uint8Array>;
84
+ const onAbort = (): void => {
85
+ if (finalized) return;
86
+ bodyController.error(signal.reason);
87
+ void cancel(signal.reason).catch(() => undefined);
88
+ };
82
89
  const finalize = (): void => {
83
90
  if (finalized) return;
84
91
  finalized = true;
92
+ signal.removeEventListener("abort", onAbort);
85
93
  try { reader.releaseLock(); } finally { release(); }
86
94
  };
95
+ const cancel = (reason: unknown): Promise<void> => {
96
+ if (cancellation) return cancellation;
97
+ if (finalized) return Promise.resolve();
98
+ cancelling = true;
99
+ // Share one settlement even if explicit cancellation races request abort.
100
+ cancellation = Promise.resolve().then(() => reader.cancel(reason)).finally(finalize);
101
+ return cancellation;
102
+ };
87
103
  const body = new ReadableStream<Uint8Array>({
104
+ start(controller) { bodyController = controller; },
88
105
  async pull(controller) {
89
106
  try {
90
107
  const result = await reader.read();
@@ -103,18 +120,18 @@ function retainUntilResponseSettles(response: Response, release: () => void): Re
103
120
  controller.error(error);
104
121
  }
105
122
  },
106
- async cancel(reason) {
107
- cancelling = true;
108
- try { await reader.cancel(reason); } finally { finalize(); }
109
- },
123
+ cancel,
110
124
  }, { highWaterMark: 0 });
111
125
  // This is the outermost HTTP wrapper, after protocol conversion, relay
112
126
  // preflight and request logging; no internal relay marker is consumed later.
113
- return new Response(body, {
127
+ const wrapped = new Response(body, {
114
128
  status: response.status,
115
129
  statusText: response.statusText,
116
130
  headers: response.headers,
117
131
  });
132
+ signal.addEventListener("abort", onAbort, { once: true });
133
+ if (signal.aborted) onAbort();
134
+ return wrapped;
118
135
  }
119
136
 
120
137
  /**
@@ -155,7 +172,7 @@ export async function withRaisedInboundBodyAdmission(
155
172
  reservedInboundBodyBytes -= maxBytes;
156
173
  };
157
174
  try {
158
- return retainUntilResponseSettles(await work(), release);
175
+ return retainUntilResponseSettles(await work(), req.signal, release);
159
176
  } catch (error) {
160
177
  release();
161
178
  throw error;
@@ -8,9 +8,18 @@ import {
8
8
  } from "../../link/store";
9
9
  import { linkStorePath } from "../../link/paths";
10
10
  import { linkRouteAllowed } from "../../link/routes";
11
+ import { withConfigMutationLockSync } from "../../config/mutation-lock";
11
12
 
12
13
  export const LINK_INGRESS_HOSTNAME = "opencodex-link.invalid";
13
14
 
15
+ /** Internal signal: the store's port differs from the port this bind landed on. */
16
+ class LinkListenerPortMismatchError extends Error {
17
+ constructor(storedPort: number, boundPort: number) {
18
+ super(`stored port ${storedPort} differs from bound port ${boundPort}`);
19
+ this.name = "LinkListenerPortMismatchError";
20
+ }
21
+ }
22
+
14
23
  export interface LinkListenerStartContext<T> {
15
24
  dispatch: (req: Request, server: Server<T>) => Promise<Response>;
16
25
  maxRequestBodySize: number;
@@ -118,29 +127,33 @@ export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): Lin
118
127
  closeWithoutAwait(bound);
119
128
  return;
120
129
  }
121
- if (store.listenerPort === null) {
122
- const port = bound.port;
123
- if (!port || port === 0) {
124
- closeWithoutAwait(bound);
125
- reportFailure("bind", new Error("Bun did not report a concrete listener port"), "bind");
126
- return;
127
- }
128
- try {
130
+ const port = bound.port;
131
+ if (!port || port === 0) {
132
+ closeWithoutAwait(bound);
133
+ reportFailure("bind", new Error("Bun did not report a concrete listener port"), "bind");
134
+ return;
135
+ }
136
+ try {
137
+ // Revalidate the admission and persist a new port in one mutation section.
138
+ // Even a known-port bind can race the deletion of its last recorded link.
139
+ const needed = withConfigMutationLockSync(() => {
129
140
  const current = readStore(storePath);
141
+ if (current.links.length === 0) return false;
130
142
  if (current.listenerPort !== null && current.listenerPort !== port) {
131
- // Another writer fixed a different port while this bind ran. Tunnels target the stored
132
- // port, so serving on this one would strand them: give it up and let the next
133
- // ensureStarted() bind the stored port.
134
- closeWithoutAwait(bound);
135
- reportFailure("listenerPort persistence", new Error(`stored port ${current.listenerPort} differs from bound port ${port}`), "persist");
136
- return;
143
+ throw new LinkListenerPortMismatchError(current.listenerPort, port);
137
144
  }
138
- writeStore(storePath, { ...current, listenerPort: port });
139
- } catch (error) {
145
+ if (current.listenerPort === null) writeStore(storePath, { ...current, listenerPort: port });
146
+ return true;
147
+ });
148
+ if (!needed) {
140
149
  closeWithoutAwait(bound);
141
- reportFailure("listenerPort persistence", error, "persist");
150
+ setStatus("off", null, null);
142
151
  return;
143
152
  }
153
+ } catch (error) {
154
+ closeWithoutAwait(bound);
155
+ reportFailure("listenerPort persistence", error, "persist");
156
+ return;
144
157
  }
145
158
  listener = bound as Server<T>;
146
159
  relaySessions = sessions;
@@ -1594,8 +1594,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1594
1594
  // `policy`, not `config`: this route is now served on the unauthenticated loopback
1595
1595
  // listener too (#4236), and only the receiving listener's view produces CORS headers
1596
1596
  // that match the admission decision made above.
1597
- return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1598
- await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
1597
+ // Promote a caller `x-session-id` before admission, as Responses and Messages do (#6520):
1598
+ // the ChatGPT bridge forwards only canonical session headers, so the raw marker is lost.
1599
+ const sessionReq = withCallerSessionIdentity(req, admission);
1600
+ return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
1601
+ await handleChatCompletions(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
1599
1602
  req,
1600
1603
  policy,
1601
1604
  ), { requestId, start, logCtx });
@@ -1716,7 +1719,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1716
1719
  if (audioController) registerTurn(audioController, turnAdmissionLease);
1717
1720
  const acquisition = audioController
1718
1721
  ? clearableDeadline(120_000, AbortSignal.any([req.signal, audioController.signal])) : undefined;
1722
+ const detachClientAbort = turnAdmissionLease.bindAbortSignal(req.signal);
1719
1723
  const releaseAcquisition = () => {
1724
+ detachClientAbort();
1720
1725
  acquisition?.clear();
1721
1726
  if (audioController) unregisterTurn(audioController);
1722
1727
  else turnAdmissionLease.release();
@@ -1835,6 +1840,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1835
1840
  return withCors(formatErrorResponse(502, "upstream_error", "Audio WebSocket upgrade failed"), req, policy);
1836
1841
  }
1837
1842
  if (upgraded) {
1843
+ detachClientAbort();
1838
1844
  acquisition?.clear();
1839
1845
  finalizeLiveRequest(101);
1840
1846
  return undefined as unknown as Response;
@@ -502,7 +502,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
502
502
  work: (lease: ActiveTurnLease) => Promise<Response>,
503
503
  refusalLog?: WorkflowRefusalLog,
504
504
  ): Promise<Response> {
505
- const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
505
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers), req.signal);
506
506
  if (!lease) return serverBusyResponse(req, "active turns", policy);
507
507
  // Root, lane, and the refusal that follows from them, all live in ./workflow-refusal.
508
508
  let workflow: ReturnType<typeof admitHttpWorkflowTurn>;
@@ -36,6 +36,7 @@ export const SESSION_LANE_ID_BYTES = 32;
36
36
  const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
37
37
  export interface ActiveTurnLease extends AdmissionLease {
38
38
  attach(lease: AdmissionLease): void;
39
+ bindAbortSignal(signal: AbortSignal): () => void;
39
40
  bindAbortController(ac: AbortController): void;
40
41
  beginCodexAccountSelection(): CodexAccountSelectionAdmission;
41
42
  isTransferred(): boolean;
@@ -169,7 +170,7 @@ export function resetLifecycleDrainStateForTests(): void {
169
170
  serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
170
171
  releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
171
172
  }
172
- export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
173
+ export function tryAdmitTurn(sessionLaneId?: string, clientSignal?: AbortSignal): ActiveTurnLease | null {
173
174
  if (isDraining()) return null;
174
175
  const opaqueSessionLaneId = sessionLaneId
175
176
  ? createHash("sha256").update(sessionLaneId).digest("hex").slice(0, SESSION_LANE_ID_BYTES)
@@ -192,6 +193,8 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
192
193
  }
193
194
  const controllers = new Set<AbortController>();
194
195
  const attachedLeases = new Set<AdmissionLease>();
196
+ const abortListeners = new Map<AbortSignal, () => void>();
197
+ let cancellationReason: unknown;
195
198
  let active = true;
196
199
  let transferred = false;
197
200
  let nativeMainClaimed = false;
@@ -200,22 +203,40 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
200
203
  if (!active) attachedLease.release();
201
204
  else attachedLeases.add(attachedLease);
202
205
  },
206
+ bindAbortSignal(signal) {
207
+ if (!active || abortListeners.has(signal)) return () => {};
208
+ const onAbort = () => {
209
+ cancellationReason = signal.reason;
210
+ const boundControllers = [...controllers];
211
+ lease.release();
212
+ for (const controller of boundControllers) controller.abort(cancellationReason);
213
+ };
214
+ abortListeners.set(signal, onAbort);
215
+ signal.addEventListener("abort", onAbort, { once: true });
216
+ if (signal.aborted) onAbort();
217
+ return () => {
218
+ if (abortListeners.get(signal) !== onAbort) return;
219
+ signal.removeEventListener("abort", onAbort);
220
+ abortListeners.delete(signal);
221
+ };
222
+ },
203
223
  bindAbortController(ac) {
204
224
  knownTurnControllers.add(ac);
205
225
  if (!active) {
206
- ac.abort(new Error("turn already settled"));
226
+ ac.abort(cancellationReason ?? new Error("turn already settled"));
207
227
  return;
208
228
  }
209
229
  transferred = true;
210
230
  controllers.add(ac);
211
231
  activeTurns.set(ac, lease);
232
+ lease.bindAbortSignal(ac.signal);
212
233
  },
213
234
  beginCodexAccountSelection() {
214
235
  const mainProfileDraining = nativeMainDrainOwners.size > 0;
215
- let selectionActive = !mainProfileDraining;
236
+ let selectionActive = active && !mainProfileDraining;
216
237
  let released = false;
217
238
  if (selectionActive) nativeMainSelections += 1;
218
- return {
239
+ const selection: CodexAccountSelectionAdmission = {
219
240
  mainProfileDraining,
220
241
  claimMainProfile() {
221
242
  if (released || mainProfileDraining || !active) return false;
@@ -228,18 +249,23 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
228
249
  release() {
229
250
  if (released) return;
230
251
  released = true;
252
+ attachedLeases.delete(selection);
231
253
  if (selectionActive) {
232
254
  selectionActive = false;
233
255
  nativeMainSelections = Math.max(0, nativeMainSelections - 1);
234
256
  }
235
257
  },
236
258
  };
259
+ lease.attach(selection);
260
+ return selection;
237
261
  },
238
262
  isTransferred() { return transferred; },
239
263
  release() {
240
264
  if (!active) return;
241
265
  active = false;
242
266
  admittedTurns.delete(lease);
267
+ for (const [signal, listener] of abortListeners) signal.removeEventListener("abort", listener);
268
+ abortListeners.clear();
243
269
  for (const controller of controllers) {
244
270
  if (activeTurns.get(controller) === lease) activeTurns.delete(controller);
245
271
  }
@@ -258,6 +284,7 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
258
284
  },
259
285
  };
260
286
  admittedTurns.add(lease);
287
+ if (clientSignal) lease.bindAbortSignal(clientSignal);
261
288
  return lease;
262
289
  }
263
290
  export function codexAccountSelectionForTurn(