@bitkyc08/opencodex 2.13.0 → 2.14.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/index-Co12XTT-.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +5 -1
- package/src/adapters/cursor/request-builder.ts +3 -3
- package/src/adapters/google.ts +25 -5
- package/src/adapters/openai-chat.ts +182 -6
- package/src/adapters/openai-responses.ts +17 -7
- package/src/codex/catalog/bundled.ts +16 -0
- package/src/codex/catalog/metadata.ts +180 -5
- package/src/codex/catalog/parsing.ts +7 -6
- package/src/codex/catalog/sync.ts +73 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +20 -0
- package/src/codex/prompt-journal.ts +50 -13
- package/src/codex/prompt-layers.ts +1 -1
- package/src/config.ts +57 -0
- package/src/generated/compatibility-version.json +64 -48
- package/src/generated/model-metadata.ts +3 -3
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/oauth/login-cli.ts +52 -26
- package/src/providers/derive.ts +2 -0
- package/src/providers/openai-sidecar.ts +9 -2
- package/src/providers/quota.ts +57 -0
- package/src/providers/registry.ts +53 -25
- package/src/responses/state.ts +22 -0
- package/src/router.ts +1 -0
- package/src/server/claude-messages.ts +57 -11
- package/src/server/direct-local-http.ts +7 -3
- package/src/server/images.ts +6 -0
- package/src/server/index.ts +51 -11
- package/src/server/live.ts +117 -13
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/config-routes.ts +20 -3
- package/src/server/management/logs-usage-routes.ts +28 -0
- package/src/server/management/model-routes.ts +11 -3
- package/src/server/management/model-rows.ts +18 -3
- package/src/server/management/provider-routes.ts +107 -3
- package/src/server/management-auth.ts +65 -1
- package/src/server/proxy-liveness.ts +1 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +460 -0
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core.ts +142 -6
- package/src/server/responses/encrypted-payload.ts +4 -1
- package/src/server/search.ts +4 -0
- package/src/types.ts +27 -0
- package/src/usage/expected-prices.ts +11 -0
- package/src/vision/describe.ts +4 -0
- package/src/web-search/anthropic-executor.ts +5 -1
- package/src/web-search/executor.ts +9 -1
- package/src/web-search/index.ts +5 -0
- package/src/web-search/loop.ts +42 -5
- package/gui/dist/assets/index-BHldBl6_.js +0 -76
|
@@ -237,6 +237,12 @@ export interface ProviderRegistryEntry {
|
|
|
237
237
|
parallelToolCalls?: boolean;
|
|
238
238
|
/** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */
|
|
239
239
|
promptCacheKey?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Opt-in: forward `service_tier` on the `/chat/completions` wire. Same hazard as
|
|
242
|
+
* `promptCacheKey` — an OpenAI-specific extension that strict gateways reject. Distinct from
|
|
243
|
+
* `supportsServiceTier`, which governs the Responses wire.
|
|
244
|
+
*/
|
|
245
|
+
chatServiceTier?: boolean;
|
|
240
246
|
autoToolChoiceOnlyModels?: string[];
|
|
241
247
|
preserveReasoningContentModels?: string[];
|
|
242
248
|
requiresReasoningPlaceholderModels?: string[];
|
|
@@ -427,38 +433,35 @@ const OPENCODE_ZEN_TEXT_ONLY_MODELS = [
|
|
|
427
433
|
"deepseek-v4-flash-free",
|
|
428
434
|
];
|
|
429
435
|
/*
|
|
430
|
-
* DeepSeek's Codex ladder is low/high/max
|
|
431
|
-
*
|
|
432
|
-
*
|
|
436
|
+
* DeepSeek's Codex ladder is low/high/max. With the V4 Pro GA release
|
|
437
|
+
* (DeepSeek-V4-Pro-0813) the official thinking-mode table is IDENTICAL for both
|
|
438
|
+
* V4 models (api-docs.deepseek.com/guides/thinking_mode, verified 2026-08-13):
|
|
433
439
|
*
|
|
434
440
|
* requested | v4-flash | v4-pro
|
|
435
|
-
* low | low |
|
|
441
|
+
* low | low | low
|
|
442
|
+
* medium | high | high
|
|
436
443
|
* high | high | high
|
|
437
|
-
* xhigh | high |
|
|
444
|
+
* xhigh | high | high
|
|
438
445
|
* max | max | max
|
|
439
446
|
*
|
|
440
|
-
*
|
|
447
|
+
* Before GA, Pro silently upgraded low->high and mapped xhigh->max (#1057-era
|
|
448
|
+
* table); the page's footnote about an early-August Pro mapping update landed
|
|
449
|
+
* with this GA, so Pro now advertises the same three real tiers as Flash.
|
|
450
|
+
*
|
|
451
|
+
* Two standing notes (#1057):
|
|
441
452
|
*
|
|
442
453
|
* - `xhigh` is a COMPATIBILITY ALIAS, not a native tier. It stays in the wire maps
|
|
443
454
|
* so existing requests and saved configs keep working, but it is not advertised.
|
|
444
|
-
* -
|
|
445
|
-
*
|
|
446
|
-
* there would put a tier in the picker that costs `high`, which is the same
|
|
447
|
-
* defect this fixes wearing a different value.
|
|
448
|
-
*
|
|
449
|
-
* The vendor page footnotes that Pro's mapping updates in early August 2026; as of
|
|
450
|
-
* the re-verification above it had not changed. When it does, Pro gains `low` here.
|
|
451
|
-
*
|
|
452
|
-
* `medium` has no row in the vendor table — mapping it to `high` is OUR
|
|
453
|
-
* compatibility choice for clients that only speak the OpenAI ladder.
|
|
455
|
+
* - `medium` has no row in the vendor table — mapping it to `high` is OUR
|
|
456
|
+
* compatibility choice for clients that only speak the OpenAI ladder.
|
|
454
457
|
*/
|
|
455
458
|
const DEEPSEEK_FLASH_THINKING_EFFORTS = ["low", "high", "max"];
|
|
456
|
-
const DEEPSEEK_PRO_THINKING_EFFORTS = ["high", "max"];
|
|
459
|
+
const DEEPSEEK_PRO_THINKING_EFFORTS = ["low", "high", "max"];
|
|
457
460
|
const DEEPSEEK_PRO_REASONING_MAP: Record<string, string> = {
|
|
458
|
-
low: "
|
|
461
|
+
low: "low",
|
|
459
462
|
medium: "high",
|
|
460
463
|
high: "high",
|
|
461
|
-
xhigh: "
|
|
464
|
+
xhigh: "high",
|
|
462
465
|
max: "max",
|
|
463
466
|
};
|
|
464
467
|
const DEEPSEEK_FLASH_REASONING_MAP: Record<string, string> = {
|
|
@@ -947,7 +950,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
947
950
|
// devlog/model_update/260709_model_refresh/001_xai_lineup.md.
|
|
948
951
|
// grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
|
|
949
952
|
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
950
|
-
|
|
953
|
+
// 260813: grok-4.6 added per the new docs.x.ai/developers/grok-4-6 page; specs mirrored
|
|
954
|
+
// from grok-4.5 until the official capability/pricing tables settle.
|
|
955
|
+
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
951
956
|
defaultModel: "grok-4.5",
|
|
952
957
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
953
958
|
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
@@ -956,6 +961,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
956
961
|
// the app blocks attachments client-side. grok-build-0.1 / grok-composer-2.5-fast stay out
|
|
957
962
|
// (they are already listed in noVisionModels below).
|
|
958
963
|
modelInputModalities: {
|
|
964
|
+
"grok-4.6": ["text", "image"],
|
|
959
965
|
"grok-4.5": ["text", "image"],
|
|
960
966
|
"grok-4.3": ["text", "image"],
|
|
961
967
|
"grok-4.20-0309-reasoning": ["text", "image"],
|
|
@@ -966,10 +972,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
966
972
|
// reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
|
|
967
973
|
// (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
|
|
968
974
|
// Models that never emit reasoning simply have no thinking parts to replay (no-op).
|
|
969
|
-
preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
|
|
975
|
+
preserveReasoningContentModels: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
|
|
970
976
|
// grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
|
|
971
|
-
modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
|
|
977
|
+
modelReasoningEfforts: { "grok-4.6": ["low", "medium", "high"], "grok-4.5": ["low", "medium", "high"] },
|
|
972
978
|
modelContextWindows: {
|
|
979
|
+
"grok-4.6": 500_000,
|
|
973
980
|
"grok-4.5": 500_000,
|
|
974
981
|
"grok-4.3": 1_000_000,
|
|
975
982
|
"grok-4.20-0309-reasoning": 1_000_000,
|
|
@@ -1414,8 +1421,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1414
1421
|
// Official DeepSeek Codex setup (codex-deepseek-setup.sh) advertises 1,048,576
|
|
1415
1422
|
// for both V4 models; the older 1,000,000 figure was a rounded approximation.
|
|
1416
1423
|
modelContextWindows: { "deepseek-v4-flash": 1_048_576, "deepseek-v4-pro": 1_048_576 },
|
|
1417
|
-
// DeepSeek documents V4
|
|
1418
|
-
// API
|
|
1424
|
+
// DeepSeek documents both V4 models as native Responses API models adapted for Codex
|
|
1425
|
+
// (model table marks Responses API ✓ for flash and pro; the /responses reference lists
|
|
1426
|
+
// both ids as accepted `model` values — verified 2026-08-13 with the V4 Pro GA,
|
|
1427
|
+
// version label DeepSeek-V4-Pro-0813).
|
|
1419
1428
|
modelWireDefaults: {
|
|
1420
1429
|
// Codex speaks Responses natively and DeepSeek ships a Codex-compatible
|
|
1421
1430
|
// apply_patch tool on that wire, so a Responses inbound goes straight out with
|
|
@@ -1424,6 +1433,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1424
1433
|
// translating them into Responses would add a hop onto our newest upstream path
|
|
1425
1434
|
// for no gain.
|
|
1426
1435
|
"deepseek-v4-flash": { wire: "openai-responses", inbound: ["responses"] },
|
|
1436
|
+
"deepseek-v4-pro": { wire: "openai-responses", inbound: ["responses"] },
|
|
1427
1437
|
},
|
|
1428
1438
|
// The #875-era bounded-JSON force (`modelResponsesUpstreamStreaming`) is retired
|
|
1429
1439
|
// for this entry: the official guide documents a `response.completed` /
|
|
@@ -1438,7 +1448,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1438
1448
|
// devlog/_plan/260807_deepseek_responses_streaming/000_plan.md.
|
|
1439
1449
|
// Current official streams normally carry a real terminal; retain a narrow grace
|
|
1440
1450
|
// repair for the historical shape that closes after a complete graph without one.
|
|
1441
|
-
modelResponsesTerminalRepair: { "deepseek-v4-flash": { graceMs: 5_000 } },
|
|
1451
|
+
modelResponsesTerminalRepair: { "deepseek-v4-flash": { graceMs: 5_000 }, "deepseek-v4-pro": { graceMs: 5_000 } },
|
|
1442
1452
|
// DeepSeek's Responses route emits bare UUID item ids, which leave Codex
|
|
1443
1453
|
// clients stuck on an uncommitted turn (#938). Client-facing only — raw
|
|
1444
1454
|
// continuation snapshots keep the upstream ids.
|
|
@@ -2313,6 +2323,24 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2313
2323
|
noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
|
|
2314
2324
|
},
|
|
2315
2325
|
{ id: "xiaomi", label: "Xiaomi MiMo", baseUrl: "https://api.xiaomimimo.com/anthropic", adapter: "anthropic", authKind: "key", dashboardUrl: "https://xiaomimimo.com", defaultModel: "mimo-v2.5-pro" },
|
|
2326
|
+
// Xiaomi's public OpenAI-compatible endpoint is a distinct transport from both the Anthropic
|
|
2327
|
+
// preset above and the paid token-plan host below. Keep a separate fixed-destination contract
|
|
2328
|
+
// so existing custom providers are never retargeted while the official route receives the
|
|
2329
|
+
// strict reasoning ladder its validator enforces (#1483).
|
|
2330
|
+
{
|
|
2331
|
+
id: "xiaomi-mimo",
|
|
2332
|
+
label: "Xiaomi MiMo (OpenAI Chat)",
|
|
2333
|
+
baseUrl: "https://api.xiaomimimo.com/v1",
|
|
2334
|
+
adapter: "openai-chat",
|
|
2335
|
+
authKind: "key",
|
|
2336
|
+
dashboardUrl: "https://platform.xiaomimimo.com/console/balance",
|
|
2337
|
+
defaultModel: "mimo-v2.5",
|
|
2338
|
+
models: ["mimo-v2.5"],
|
|
2339
|
+
reasoningEfforts: ["low", "medium", "high"],
|
|
2340
|
+
reasoningEffortMap: { xhigh: "high", max: "high", ultra: "high" },
|
|
2341
|
+
preserveCustomDestination: true,
|
|
2342
|
+
note: "Official Xiaomi MiMo OpenAI-compatible Chat endpoint. The upstream validator accepts reasoning_effort none/low/medium/high; higher Codex tiers are clamped to high.",
|
|
2343
|
+
},
|
|
2316
2344
|
{ id: "kilo", label: "Kilo", baseUrl: "https://api.kilo.ai/api/gateway", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://kilo.ai" },
|
|
2317
2345
|
{
|
|
2318
2346
|
id: "mimo-free",
|
package/src/responses/state.ts
CHANGED
|
@@ -947,6 +947,27 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
947
947
|
* Cache completed output and max_output_tokens partial output for previous_response_id replay.
|
|
948
948
|
* Content-filtered incomplete and failed output are not authoritative replay history.
|
|
949
949
|
*/
|
|
950
|
+
/**
|
|
951
|
+
* Request bodies that must never enter the continuation cache.
|
|
952
|
+
*
|
|
953
|
+
* The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
|
|
954
|
+
* Encrypted-agent-task recovery decrypts task text into the request body and promises
|
|
955
|
+
* in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
|
|
956
|
+
* no TTL and break the promise.
|
|
957
|
+
*
|
|
958
|
+
* A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
|
|
959
|
+
* native passthrough, so any marker written into the body itself would be sent upstream.
|
|
960
|
+
* Marking is enforced once here rather than at each call site, because every recording path
|
|
961
|
+
* (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
|
|
962
|
+
* a new call site cannot reintroduce the leak by forgetting a guard.
|
|
963
|
+
*/
|
|
964
|
+
const nonPersistableBodies = new WeakSet<object>();
|
|
965
|
+
|
|
966
|
+
/** Bar this exact request body from the continuation cache, and therefore from disk. */
|
|
967
|
+
export function markBodyNonPersistable(body: unknown): void {
|
|
968
|
+
if (body && typeof body === "object") nonPersistableBodies.add(body as object);
|
|
969
|
+
}
|
|
970
|
+
|
|
950
971
|
export function rememberResponseState(
|
|
951
972
|
requestBody: unknown,
|
|
952
973
|
response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
|
|
@@ -955,6 +976,7 @@ export function rememberResponseState(
|
|
|
955
976
|
): void {
|
|
956
977
|
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
957
978
|
const request = requestBody as Record<string, unknown>;
|
|
979
|
+
if (nonPersistableBodies.has(request)) return;
|
|
958
980
|
// `force` bypasses only the store:false skip: Codex sends `store:false` on every non-Azure
|
|
959
981
|
// HTTP request (and WS inherits it), yet its WS turns still chain with previous_response_id.
|
|
960
982
|
// The passthrough branch records with force so those chains can be expanded locally; the
|
package/src/router.ts
CHANGED
|
@@ -350,6 +350,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
350
350
|
// opt-in, while an explicit user `false` keeps overriding registry `true`.
|
|
351
351
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
352
352
|
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
|
|
353
|
+
...(provider.chatServiceTier === undefined && registryEntry.chatServiceTier !== undefined ? { chatServiceTier: registryEntry.chatServiceTier } : {}),
|
|
353
354
|
...(provider.reasoningWireFormat === undefined && registryEntry.reasoningWireFormat !== undefined
|
|
354
355
|
? { reasoningWireFormat: registryEntry.reasoningWireFormat }
|
|
355
356
|
: {}),
|
|
@@ -35,6 +35,12 @@ import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput,
|
|
|
35
35
|
import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
|
|
36
36
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
37
37
|
import { handleResponses } from "./responses";
|
|
38
|
+
import {
|
|
39
|
+
isApiAuthRequired,
|
|
40
|
+
isDataPlaneAdmissionSecret,
|
|
41
|
+
isProxyAdmissionSecret,
|
|
42
|
+
type RequestPolicyView,
|
|
43
|
+
} from "./auth-cors";
|
|
38
44
|
import type { AdmissionLease } from "../lib/admission";
|
|
39
45
|
import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
|
|
40
46
|
import { CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE } from "../codex/auth-context";
|
|
@@ -96,20 +102,54 @@ const PASSTHROUGH_STRIP_HEADERS = new Set([
|
|
|
96
102
|
"accept-encoding", "x-opencodex-api-key", "origin",
|
|
97
103
|
]);
|
|
98
104
|
|
|
99
|
-
function
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
function singleCredentialToken(name: "authorization" | "x-api-key", value: string | null): string | null {
|
|
106
|
+
const raw = value?.trim() ?? "";
|
|
107
|
+
// Fetch Headers comma-joins duplicate fields. Neither Anthropic credential format permits a
|
|
108
|
+
// comma, so treating a joined value as one token could hide an admission secret behind a real
|
|
109
|
+
// provider credential. Ambiguous credential headers fail closed.
|
|
110
|
+
if (!raw || raw.includes(",")) return null;
|
|
111
|
+
if (name === "authorization") {
|
|
112
|
+
const match = /^Bearer\s+(.+)$/i.exec(raw);
|
|
113
|
+
return match?.[1]?.trim() || null;
|
|
114
|
+
}
|
|
115
|
+
return raw;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasAnthropicNativeCredential(req: Request, config: OcxConfig): boolean {
|
|
119
|
+
const bearer = singleCredentialToken("authorization", req.headers.get("authorization"));
|
|
120
|
+
const apiKey = singleCredentialToken("x-api-key", req.headers.get("x-api-key"));
|
|
121
|
+
return (!!bearer && bearer.startsWith("sk-ant-") && !isProxyAdmissionSecret(bearer, config))
|
|
122
|
+
|| (!!apiKey && apiKey.startsWith("sk-ant-") && !isProxyAdmissionSecret(apiKey, config));
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
function wantsNativePassthrough(
|
|
125
|
+
function wantsNativePassthrough(
|
|
126
|
+
req: Request,
|
|
127
|
+
config: OcxConfig,
|
|
128
|
+
requestPolicy: RequestPolicyView,
|
|
129
|
+
model: unknown,
|
|
130
|
+
): model is string {
|
|
106
131
|
if (config.claudeCode?.nativePassthrough === false) return false;
|
|
107
132
|
if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
|
|
108
|
-
|
|
133
|
+
// Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
|
|
134
|
+
// therefore requires the dedicated admission header even though the routed Messages surface
|
|
135
|
+
// keeps accepting all three legacy admission forms.
|
|
136
|
+
if (isApiAuthRequired(requestPolicy)) {
|
|
137
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim() ?? "";
|
|
138
|
+
if (!isDataPlaneAdmissionSecret(dedicated, config)) return false;
|
|
139
|
+
}
|
|
140
|
+
if (!hasAnthropicNativeCredential(req, config)) return false;
|
|
109
141
|
// An alias or modelMap hit means the user asked for a ROUTED model: translate instead.
|
|
110
142
|
return resolveInboundModel(model, config.claudeCode) === model;
|
|
111
143
|
}
|
|
112
144
|
|
|
145
|
+
function shouldForwardNativeHeader(name: string, value: string, config: OcxConfig): boolean {
|
|
146
|
+
const lowerName = name.toLowerCase();
|
|
147
|
+
if (PASSTHROUGH_STRIP_HEADERS.has(lowerName)) return false;
|
|
148
|
+
if (lowerName !== "authorization" && lowerName !== "x-api-key") return true;
|
|
149
|
+
const token = singleCredentialToken(lowerName, value);
|
|
150
|
+
return !!token && !isProxyAdmissionSecret(token, config);
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
/** Format a 32-hex cache key as a uuid-shaped session id (version/variant nibbles forced). */
|
|
114
154
|
function uuidFromHex(hex32: string): string {
|
|
115
155
|
const h = (hex32 + "0".repeat(32)).slice(0, 32);
|
|
@@ -330,7 +370,7 @@ async function anthropicNativePassthrough(
|
|
|
330
370
|
}
|
|
331
371
|
const headers = new Headers();
|
|
332
372
|
req.headers.forEach((value, name) => {
|
|
333
|
-
if (
|
|
373
|
+
if (shouldForwardNativeHeader(name, value, config)) headers.set(name, value);
|
|
334
374
|
});
|
|
335
375
|
headers.set("content-type", "application/json");
|
|
336
376
|
|
|
@@ -528,11 +568,12 @@ export async function handleClaudeMessages(
|
|
|
528
568
|
config: OcxConfig,
|
|
529
569
|
logCtx: RequestLogContext,
|
|
530
570
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
571
|
+
requestPolicy: RequestPolicyView = config,
|
|
531
572
|
): Promise<Response> {
|
|
532
573
|
const translatorBudget = createTranslatorBudget();
|
|
533
574
|
try {
|
|
534
575
|
return finalizeTranslatorBudgetResponse(
|
|
535
|
-
await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds),
|
|
576
|
+
await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy),
|
|
536
577
|
translatorBudget,
|
|
537
578
|
);
|
|
538
579
|
} catch (error) {
|
|
@@ -547,6 +588,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
547
588
|
logCtx: RequestLogContext,
|
|
548
589
|
translatorBudget: TranslatorBudget,
|
|
549
590
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
591
|
+
requestPolicy: RequestPolicyView = config,
|
|
550
592
|
): Promise<Response> {
|
|
551
593
|
logCtx.surface = "claude";
|
|
552
594
|
const disabled = claudeInboundDisabled(config);
|
|
@@ -601,7 +643,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
601
643
|
);
|
|
602
644
|
if (claudeConversationId) logCtx.conversationId = claudeConversationId;
|
|
603
645
|
}
|
|
604
|
-
if (isRec(anthropicBody) && wantsNativePassthrough(req, config, anthropicBody.model)) {
|
|
646
|
+
if (isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
|
|
605
647
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
606
648
|
}
|
|
607
649
|
if (isRec(anthropicBody) && effortOverride) {
|
|
@@ -940,7 +982,11 @@ export function estimateClaudeRequestTokens(
|
|
|
940
982
|
return Math.max(1, estimateTokens(parts.join("\n"), modelId) + attachmentTokens);
|
|
941
983
|
}
|
|
942
984
|
|
|
943
|
-
export async function handleClaudeCountTokens(
|
|
985
|
+
export async function handleClaudeCountTokens(
|
|
986
|
+
req: Request,
|
|
987
|
+
config: OcxConfig,
|
|
988
|
+
requestPolicy: RequestPolicyView = config,
|
|
989
|
+
): Promise<Response> {
|
|
944
990
|
const disabled = claudeInboundDisabled(config);
|
|
945
991
|
if (disabled) return disabled;
|
|
946
992
|
|
|
@@ -973,7 +1019,7 @@ export async function handleClaudeCountTokens(req: Request, config: OcxConfig):
|
|
|
973
1019
|
raw.model = model;
|
|
974
1020
|
}
|
|
975
1021
|
captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
|
|
976
|
-
if (wantsNativePassthrough(req, config, model)) {
|
|
1022
|
+
if (wantsNativePassthrough(req, config, requestPolicy, model)) {
|
|
977
1023
|
return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
|
|
978
1024
|
}
|
|
979
1025
|
const inputTokens = estimateClaudeRequestTokens(raw, model);
|
|
@@ -219,7 +219,7 @@ function parseResponse(bytes: Buffer): Response {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* Fetch one local HTTP GET over a direct TCP connection.
|
|
222
|
+
* Fetch one bodyless local HTTP GET or POST over a direct TCP connection.
|
|
223
223
|
*
|
|
224
224
|
* Bun's global fetch and Bun 1.3's node:http compatibility layer can honor
|
|
225
225
|
* HTTP(S)_PROXY. Local identity and capability probes must not expose headers
|
|
@@ -239,18 +239,22 @@ export async function directLocalHttpFetch(
|
|
|
239
239
|
|
|
240
240
|
if (url.protocol !== "http:") throw new Error("direct local request must use HTTP");
|
|
241
241
|
if (url.username || url.password) throw new Error("direct local request URL must not contain credentials");
|
|
242
|
-
if (method !== "GET"
|
|
242
|
+
if ((method !== "GET" && method !== "POST") || body !== null) {
|
|
243
|
+
throw new Error("direct local request must be a bodyless GET or POST");
|
|
244
|
+
}
|
|
243
245
|
if (signal?.aborted) throw abortReason(signal);
|
|
244
246
|
|
|
245
247
|
const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
246
248
|
headers.delete("proxy-authorization");
|
|
247
249
|
headers.delete("proxy-connection");
|
|
250
|
+
if (method === "POST") headers.set("content-length", "0");
|
|
251
|
+
else headers.delete("content-length");
|
|
248
252
|
headers.set("host", url.host);
|
|
249
253
|
headers.set("connection", "close");
|
|
250
254
|
const headerLines: string[] = [];
|
|
251
255
|
headers.forEach((value, key) => { headerLines.push(`${key}: ${value}`); });
|
|
252
256
|
const requestBytes = Buffer.from(
|
|
253
|
-
|
|
257
|
+
`${method} ${url.pathname}${url.search} HTTP/1.1\r\n${headerLines.join("\r\n")}\r\n\r\n`,
|
|
254
258
|
"latin1",
|
|
255
259
|
);
|
|
256
260
|
const parsedHostname = url.hostname.startsWith("[") && url.hostname.endsWith("]")
|
package/src/server/images.ts
CHANGED
|
@@ -499,6 +499,12 @@ export async function handleImages(
|
|
|
499
499
|
headers,
|
|
500
500
|
body: JSON.stringify(body),
|
|
501
501
|
signal: linkedSignal.signal,
|
|
502
|
+
// Do not follow a cross-origin 3xx while carrying Codex credentials. Bun strips
|
|
503
|
+
// `Authorization` across origins but forwards nonstandard headers, so
|
|
504
|
+
// `chatgpt-account-id`, `session_id`, and `x-codex-turn-metadata` would reach the
|
|
505
|
+
// redirect target. Verified with a two-server probe. The Responses path and native
|
|
506
|
+
// compact already set this; the credential-bearing sidecars did not.
|
|
507
|
+
redirect: "manual",
|
|
502
508
|
});
|
|
503
509
|
const observed = await readImageResponseBytes(upstreamResponse, {
|
|
504
510
|
maxBytes: IMAGES_RESPONSE_MAX_BYTES,
|
package/src/server/index.ts
CHANGED
|
@@ -195,6 +195,7 @@ import {
|
|
|
195
195
|
createLocalAttestationSecret,
|
|
196
196
|
} from "../lib/local-management-attestation";
|
|
197
197
|
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
198
|
+
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
198
199
|
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
199
200
|
|
|
200
201
|
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
@@ -478,9 +479,19 @@ export function consumeStartupCacheInvalidationWrite(): boolean {
|
|
|
478
479
|
return wrote;
|
|
479
480
|
}
|
|
480
481
|
|
|
482
|
+
export function warnAgentTaskRecoveryStartup(config: {
|
|
483
|
+
agentTaskRecovery?: { enabled?: boolean };
|
|
484
|
+
}): void {
|
|
485
|
+
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
486
|
+
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
487
|
+
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
|
|
488
|
+
console.warn(" Recovered model output is retained only in a bounded in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
489
|
+
}
|
|
490
|
+
|
|
481
491
|
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
482
492
|
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
483
493
|
const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
|
|
494
|
+
warnAgentTaskRecoveryStartup(config);
|
|
484
495
|
setLiveStateStoreConfig(config);
|
|
485
496
|
applyProxyEnv(config);
|
|
486
497
|
assertServerAuthConfig(config);
|
|
@@ -617,6 +628,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
617
628
|
}
|
|
618
629
|
if (path === "/v1/responses/compact") return req.method === "POST";
|
|
619
630
|
if (path === "/v1/models") return req.method === "GET";
|
|
631
|
+
// Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
|
|
632
|
+
// transport) — a directly-spawned `codex app-server` needs these for desktop
|
|
633
|
+
// voice the same way it needs /v1/responses. WebSocket upgrades only; plain
|
|
634
|
+
// HTTP on these paths stays rejected.
|
|
635
|
+
if (path === "/v1/realtime" || path === "/v1/live") {
|
|
636
|
+
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
637
|
+
}
|
|
620
638
|
return false;
|
|
621
639
|
}
|
|
622
640
|
|
|
@@ -804,6 +822,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
804
822
|
pid: process.pid,
|
|
805
823
|
port: healthPort,
|
|
806
824
|
restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
|
|
825
|
+
providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
|
|
807
826
|
}, 200, req, policy);
|
|
808
827
|
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
|
|
809
828
|
if (challenge) {
|
|
@@ -887,10 +906,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
887
906
|
}
|
|
888
907
|
throw error;
|
|
889
908
|
}
|
|
890
|
-
const { applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
909
|
+
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
891
910
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
892
911
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
893
|
-
const nativeSlugs = includeNativeOpenAi
|
|
912
|
+
const nativeSlugs = includeNativeOpenAi
|
|
913
|
+
? nativeOpenAiSlugs()
|
|
914
|
+
: [];
|
|
894
915
|
const disabledNatives = disabledNativeSlugs(config);
|
|
895
916
|
const disabledModels = new Set(config.disabledModels ?? []);
|
|
896
917
|
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
@@ -898,6 +919,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
898
919
|
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
899
920
|
? visibleCodexAccountSelectors(config)
|
|
900
921
|
: [];
|
|
922
|
+
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
923
|
+
? accountBoundNativeOpenAiSlugsBySelector(config)
|
|
924
|
+
: new Map<string, readonly string[]>();
|
|
925
|
+
const accountNativeSlugs = [...new Set(
|
|
926
|
+
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
927
|
+
)];
|
|
901
928
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
902
929
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
903
930
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
@@ -945,7 +972,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
945
972
|
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
946
973
|
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
947
974
|
const catalogNativeSlugs = accountSelectors.length > 0
|
|
948
|
-
? NATIVE_OPENAI_MODELS
|
|
975
|
+
? [...new Set([...NATIVE_OPENAI_MODELS, ...accountNativeSlugs])]
|
|
949
976
|
: nativeSlugs;
|
|
950
977
|
const entries = buildCatalogEntries(
|
|
951
978
|
loadCatalogTemplate(),
|
|
@@ -959,12 +986,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
959
986
|
suppressedBareNativeSlugs,
|
|
960
987
|
new Set(),
|
|
961
988
|
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
989
|
+
accountNativeSlugs,
|
|
990
|
+
accountNativeSlugsBySelector,
|
|
962
991
|
);
|
|
963
992
|
return jsonResponse({
|
|
964
993
|
models: applyNativeVisibility(
|
|
965
994
|
entries,
|
|
966
995
|
disabledModels,
|
|
967
996
|
accountSelectors.length > 0,
|
|
997
|
+
new Set(accountNativeSlugs),
|
|
968
998
|
),
|
|
969
999
|
}, 200, req, policy);
|
|
970
1000
|
}
|
|
@@ -1010,13 +1040,16 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1010
1040
|
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1011
1041
|
? NATIVE_OPENAI_MODELS.filter(slug => !disabledNatives.has(slug))
|
|
1012
1042
|
: [];
|
|
1043
|
+
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1044
|
+
? selectorNativeSlugs
|
|
1045
|
+
: [];
|
|
1013
1046
|
const visibleNatives = includeNativeOpenAi
|
|
1014
1047
|
? accountSelectors.length > 0
|
|
1015
|
-
?
|
|
1048
|
+
? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
|
|
1016
1049
|
: visibleNativeSlugs(config)
|
|
1017
1050
|
: [];
|
|
1018
1051
|
const visibleAccountNatives = accountSelectors.flatMap(selector =>
|
|
1019
|
-
|
|
1052
|
+
(accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
|
|
1020
1053
|
const id = `${selector}/${metadataId}`;
|
|
1021
1054
|
return disabledModels.has(id) ? [] : [{ id, metadataId }];
|
|
1022
1055
|
})
|
|
@@ -1210,7 +1243,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1210
1243
|
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1211
1244
|
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
1212
1245
|
}
|
|
1213
|
-
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1246
|
+
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1247
|
+
await handleClaudeCountTokens(req, config, policy),
|
|
1248
|
+
req,
|
|
1249
|
+
policy,
|
|
1250
|
+
));
|
|
1214
1251
|
}
|
|
1215
1252
|
|
|
1216
1253
|
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
@@ -1237,9 +1274,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1237
1274
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1238
1275
|
// translated Anthropic stream here.
|
|
1239
1276
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1240
|
-
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1277
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
|
|
1241
1278
|
req,
|
|
1242
|
-
|
|
1279
|
+
policy,
|
|
1243
1280
|
));
|
|
1244
1281
|
}
|
|
1245
1282
|
|
|
@@ -1306,10 +1343,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1306
1343
|
});
|
|
1307
1344
|
}
|
|
1308
1345
|
|
|
1309
|
-
// Voice / Realtime
|
|
1310
|
-
// /v1/realtime?call_id= (or /v1/realtime/calls/{callId}).
|
|
1346
|
+
// Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
|
|
1347
|
+
// Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
|
|
1348
|
+
// sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
|
|
1349
|
+
// path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
|
|
1350
|
+
// Transparent bidirectional relay.
|
|
1311
1351
|
const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
1312
|
-
? parseLiveSidebandTarget(url.pathname, url.searchParams)
|
|
1352
|
+
? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
|
|
1313
1353
|
: null;
|
|
1314
1354
|
if (liveSidebandTarget) {
|
|
1315
1355
|
if (isDraining()) {
|