@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.
- package/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
- package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/bridge/errors.ts +7 -1
- package/src/claude/context-windows.ts +3 -3
- package/src/claude/desktop-picker.ts +1 -1
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/resolve.ts +1 -1
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/generated/compatibility-version.json +91 -75
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +65 -6
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-dispatch.ts +115 -60
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +5 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/types/request.ts +5 -0
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
if (current.listenerPort === null) writeStore(storePath, { ...current, listenerPort: port });
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
if (!needed) {
|
|
140
149
|
closeWithoutAwait(bound);
|
|
141
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
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;
|
package/src/server/index.ts
CHANGED
|
@@ -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>;
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|