@bitkyc08/opencodex 2.48.0-preview.20260908 → 2.49.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/README.md +8 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-BtyONQrZ.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +24 -1
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/auth-api.ts +6 -3
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +75 -7
- package/src/codex/catalog/provider-fetch.ts +73 -9
- package/src/codex/inject.ts +127 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota.ts +18 -8
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +126 -74
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/registry.ts +139 -0
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/chat-completions.ts +26 -9
- package/src/server/claude-messages.ts +12 -2
- package/src/server/images.ts +1 -0
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-routes.ts +24 -0
- package/src/server/management-api.ts +14 -0
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/core.ts +141 -19
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +12 -0
- package/src/service.ts +36 -2
- package/src/storage/cleanup.ts +77 -45
- package/src/types/config.ts +7 -1
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B6EFoatP.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
|
@@ -210,11 +210,14 @@ import {
|
|
|
210
210
|
import {
|
|
211
211
|
ForwardAdmissionCredentialError,
|
|
212
212
|
hasForwardableCodexBearer,
|
|
213
|
+
isProxyAdmissionSecret,
|
|
213
214
|
validateForwardAdmissionCredential,
|
|
214
215
|
} from "../auth-cors";
|
|
215
216
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
216
217
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
217
|
-
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
218
|
+
import { captureExplicitOpenAiCallerAuth, listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ExplicitOpenAiCallerAuth, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
219
|
+
import { inspectChatGptDomainClaim } from "../../oauth/chatgpt";
|
|
220
|
+
import { captureCallerDirectAuth, providerConsumesCallerAuthorization, type CallerDirectAuth } from "../../providers/caller-authorization";
|
|
218
221
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
219
222
|
import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../../codex/loopback-target";
|
|
220
223
|
import { providerContextCap } from "../../providers/context-cap";
|
|
@@ -1121,7 +1124,8 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
1121
1124
|
}
|
|
1122
1125
|
|
|
1123
1126
|
interface CodexPoolAccountRetryArgs {
|
|
1124
|
-
|
|
1127
|
+
/** Sanitized caller input, before any selected Pool credential was materialized. */
|
|
1128
|
+
callerAuthHeaders: Headers;
|
|
1125
1129
|
config: OcxConfig;
|
|
1126
1130
|
route: { providerName: string; modelId: string; provider: OcxProviderConfig };
|
|
1127
1131
|
parsed: OcxParsedRequest;
|
|
@@ -1287,7 +1291,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1287
1291
|
args: CodexPoolAccountRetryArgs,
|
|
1288
1292
|
): Promise<CodexPoolAccountRetryResult> {
|
|
1289
1293
|
const {
|
|
1290
|
-
|
|
1294
|
+
callerAuthHeaders, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
|
|
1291
1295
|
outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
|
|
1292
1296
|
} = args;
|
|
1293
1297
|
const inboundWire = options.inboundWire ?? "responses";
|
|
@@ -1321,7 +1325,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1321
1325
|
}
|
|
1322
1326
|
try {
|
|
1323
1327
|
retryAuthCtx ??= await resolveCodexAuthContext(
|
|
1324
|
-
|
|
1328
|
+
callerAuthHeaders,
|
|
1325
1329
|
config,
|
|
1326
1330
|
"pool",
|
|
1327
1331
|
{
|
|
@@ -1329,7 +1333,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1329
1333
|
admission: options.admission,
|
|
1330
1334
|
codexAuthPolicy: options.codexAuthPolicy,
|
|
1331
1335
|
modelId: route.modelId,
|
|
1332
|
-
requestScopedMainCredential: hasForwardableCodexBearer(
|
|
1336
|
+
requestScopedMainCredential: hasForwardableCodexBearer(callerAuthHeaders, config),
|
|
1333
1337
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1334
1338
|
resolveCodexModelEntitlements: entitlementResolver,
|
|
1335
1339
|
},
|
|
@@ -1399,7 +1403,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1399
1403
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
1400
1404
|
// ordinary requests must block the first account before the alternate send.
|
|
1401
1405
|
if (!deferFirstOutcome) recordFirstOutcome();
|
|
1402
|
-
const retryHeaders = headersForCodexAuthContext(
|
|
1406
|
+
const retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
|
|
1403
1407
|
const retryProvider = applyCodexAuthContextToProvider(
|
|
1404
1408
|
stripCodexRuntimeProviderFields(route.provider),
|
|
1405
1409
|
retryAuthCtx,
|
|
@@ -1703,9 +1707,17 @@ export interface HandleResponsesOptions {
|
|
|
1703
1707
|
/**
|
|
1704
1708
|
* Claude replay may add native-main auth so OpenAI sidecars remain available.
|
|
1705
1709
|
* Strip only that internal credential when the final route is a noncanonical
|
|
1706
|
-
* forward destination; final routing can differ from Claude's preflight route.
|
|
1710
|
+
* forward/caller-auth destination; final routing can differ from Claude's preflight route.
|
|
1707
1711
|
*/
|
|
1708
1712
|
stripClaudeMainAuthForNoncanonicalForward?: boolean;
|
|
1713
|
+
/** In-memory credential proven by Claude's native-main turn claim; never persist or log. */
|
|
1714
|
+
trustedClaudeMainAuth?: { authorization: string; chatgptAccountId?: string };
|
|
1715
|
+
/** Sidecar-only auth captured before route changes; null means no usable original pair. */
|
|
1716
|
+
openAiSidecarAuth?: ExplicitOpenAiCallerAuth | null;
|
|
1717
|
+
/** Original caller-owned native pair; separate from any claimed sidecar enrichment. */
|
|
1718
|
+
nativeCallerAuth?: ExplicitOpenAiCallerAuth | null;
|
|
1719
|
+
/** Caller Direct credential under Direct\'s own predicate; restored only for the canonical OpenAI final route. */
|
|
1720
|
+
callerDirectAuth?: CallerDirectAuth | null;
|
|
1709
1721
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
1710
1722
|
comboAttempt?: boolean;
|
|
1711
1723
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
@@ -1925,6 +1937,9 @@ export function createChildPassthroughCallbackGate(options: HandleResponsesOptio
|
|
|
1925
1937
|
|
|
1926
1938
|
export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
|
|
1927
1939
|
const childHeaders = new Headers(parentHeaders);
|
|
1940
|
+
// A provisional caller credential is not authoritative for a Combo child.
|
|
1941
|
+
childHeaders.delete("authorization");
|
|
1942
|
+
childHeaders.delete("chatgpt-account-id");
|
|
1928
1943
|
// Combo children re-serialize already-decoded JSON. Keeping transport metadata from
|
|
1929
1944
|
// the parent would make the child decoder treat plain JSON as compressed bytes.
|
|
1930
1945
|
childHeaders.delete("content-length");
|
|
@@ -2009,7 +2024,7 @@ function canPassThroughEncryptedV2AgentTask(
|
|
|
2009
2024
|
}
|
|
2010
2025
|
|
|
2011
2026
|
type ResponsesAuthResolution =
|
|
2012
|
-
| { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
|
|
2027
|
+
| { ok: true; authCtx: CodexAuthContext; headers: Headers; callerAuthHeaders: Headers; substituteMainCredential: boolean }
|
|
2013
2028
|
| { ok: false; response: Response };
|
|
2014
2029
|
|
|
2015
2030
|
/**
|
|
@@ -2021,8 +2036,75 @@ async function resolveResponsesCodexAuth(
|
|
|
2021
2036
|
config: OcxConfig,
|
|
2022
2037
|
route: RouteResult,
|
|
2023
2038
|
options: HandleResponsesOptions,
|
|
2039
|
+
credentialDomainWasRewritten = false,
|
|
2024
2040
|
): Promise<ResponsesAuthResolution> {
|
|
2025
2041
|
try {
|
|
2042
|
+
const routeMayChangeCredentialDomain = options.comboAttempt === true
|
|
2043
|
+
|| route.routeKind === "policy"
|
|
2044
|
+
|| credentialDomainWasRewritten;
|
|
2045
|
+
const trustedClaudeMainForFinalRoute = options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
2046
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
2047
|
+
? options.trustedClaudeMainAuth : undefined;
|
|
2048
|
+
let authInputHeaders = req.headers;
|
|
2049
|
+
// Route-changing recursion retains typed admission, never an unscoped raw
|
|
2050
|
+
// caller credential. Bearer admission is substituted or stripped below.
|
|
2051
|
+
if (routeMayChangeCredentialDomain && options.admission?.source !== "bearer"
|
|
2052
|
+
&& !trustedClaudeMainForFinalRoute) {
|
|
2053
|
+
authInputHeaders = new Headers(req.headers);
|
|
2054
|
+
authInputHeaders.delete("authorization");
|
|
2055
|
+
authInputHeaders.delete("chatgpt-account-id");
|
|
2056
|
+
}
|
|
2057
|
+
if (trustedClaudeMainForFinalRoute) {
|
|
2058
|
+
authInputHeaders = new Headers(authInputHeaders);
|
|
2059
|
+
authInputHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
|
|
2060
|
+
if (trustedClaudeMainForFinalRoute.chatgptAccountId) {
|
|
2061
|
+
authInputHeaders.set("chatgpt-account-id", trustedClaudeMainForFinalRoute.chatgptAccountId);
|
|
2062
|
+
} else {
|
|
2063
|
+
authInputHeaders.delete("chatgpt-account-id");
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
// A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
|
|
2067
|
+
// caller's Authorization as its own upstream token. Keep that contract only for a clean
|
|
2068
|
+
// single bearer with NO ChatGPT-domain marker. A bearer marked for the ChatGPT domain —
|
|
2069
|
+
// whether its marker is valid or malformed/conflicting — a combined/malformed value, or
|
|
2070
|
+
// the captured explicit OpenAI pair is never a Cursor token; a foreign JWT carrying only
|
|
2071
|
+
// a generic organizations claim is not ChatGPT-marked and keeps the legacy contract.
|
|
2072
|
+
// chatgpt-account-id has no meaning outside the ChatGPT domain.
|
|
2073
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider)
|
|
2074
|
+
&& providerConsumesCallerAuthorization(route.provider)) {
|
|
2075
|
+
const rawAuth = authInputHeaders.get("authorization")?.trim();
|
|
2076
|
+
const singleBearer = /^Bearer[\t ]+([^\s,]+)$/i.exec(rawAuth ?? "")?.[1];
|
|
2077
|
+
const domainClaim = singleBearer ? inspectChatGptDomainClaim(singleBearer) : { kind: "absent" as const };
|
|
2078
|
+
const dropBearer = options.nativeCallerAuth != null || domainClaim.kind !== "absent"
|
|
2079
|
+
|| (rawAuth !== undefined && singleBearer === undefined);
|
|
2080
|
+
if (dropBearer || authInputHeaders.has("chatgpt-account-id")) {
|
|
2081
|
+
const scoped = new Headers(authInputHeaders);
|
|
2082
|
+
if (dropBearer) scoped.delete("authorization");
|
|
2083
|
+
scoped.delete("chatgpt-account-id");
|
|
2084
|
+
authInputHeaders = scoped;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
// The caller's own Direct credential may cross an internal route change only to the
|
|
2088
|
+
// canonical OpenAI transport, under a predicate deliberately STRICTER than plain
|
|
2089
|
+
// unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
|
|
2090
|
+
// marker is valid, with any explicit account header matching that marker. Unchanged
|
|
2091
|
+
// routes keep their legacy rules; sidecar enrichment grants no primary authority.
|
|
2092
|
+
if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
2093
|
+
const directHeaders = new Headers({
|
|
2094
|
+
authorization: options.callerDirectAuth.authorization,
|
|
2095
|
+
...(options.callerDirectAuth.chatgptAccountId
|
|
2096
|
+
? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
|
|
2097
|
+
});
|
|
2098
|
+
if (captureCallerDirectAuth(directHeaders, config)) {
|
|
2099
|
+
authInputHeaders = new Headers(authInputHeaders);
|
|
2100
|
+
authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
|
|
2101
|
+
if (options.callerDirectAuth.chatgptAccountId) {
|
|
2102
|
+
authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
|
|
2103
|
+
} else {
|
|
2104
|
+
authInputHeaders.delete("chatgpt-account-id");
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2026
2108
|
// #1686: a caller that proved admission with a BEARER presented one of our own secrets.
|
|
2027
2109
|
// Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
|
|
2028
2110
|
// Admitting it is only safe because the stored main credential is substituted below, so
|
|
@@ -2046,15 +2128,16 @@ async function resolveResponsesCodexAuth(
|
|
|
2046
2128
|
// no-ChatGPT-login install keeps working.
|
|
2047
2129
|
const substituteMainCredential = options.admission?.source === "bearer"
|
|
2048
2130
|
&& (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
|
|
2131
|
+
const stripAuthorization = options.admission?.source === "bearer" && !substituteMainCredential;
|
|
2049
2132
|
const requestScopedMainCredential = route.codexAccountMode !== undefined
|
|
2050
2133
|
&& !substituteMainCredential
|
|
2051
|
-
&& hasForwardableCodexBearer(
|
|
2134
|
+
&& hasForwardableCodexBearer(authInputHeaders, config);
|
|
2052
2135
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
2053
|
-
validateForwardAdmissionCredential(
|
|
2136
|
+
validateForwardAdmissionCredential(authInputHeaders, config);
|
|
2054
2137
|
}
|
|
2055
2138
|
let authCtx: CodexAuthContext;
|
|
2056
2139
|
if (route.codexAccountMode) {
|
|
2057
|
-
authCtx = await resolveCodexAuthContext(
|
|
2140
|
+
authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
|
|
2058
2141
|
admission: options.admission,
|
|
2059
2142
|
codexAuthPolicy: options.codexAuthPolicy,
|
|
2060
2143
|
accountId: route.codexAccountId,
|
|
@@ -2090,7 +2173,7 @@ async function resolveResponsesCodexAuth(
|
|
|
2090
2173
|
// (custom-named canonical-forward providers must retain the same protection).
|
|
2091
2174
|
const mainPolicyConfig = isCanonicalOpenAiForwardProvider(route.provider)
|
|
2092
2175
|
? options.codexAuthPolicy ?? config : undefined;
|
|
2093
|
-
const headers = await materializeCodexUpstreamAuthAsync(
|
|
2176
|
+
const headers = await materializeCodexUpstreamAuthAsync(authInputHeaders, authCtx, {
|
|
2094
2177
|
admission: options.admission,
|
|
2095
2178
|
config: mainPolicyConfig,
|
|
2096
2179
|
modelId: route.modelId,
|
|
@@ -2109,10 +2192,27 @@ async function resolveResponsesCodexAuth(
|
|
|
2109
2192
|
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
2110
2193
|
};
|
|
2111
2194
|
}
|
|
2195
|
+
if (stripAuthorization) {
|
|
2196
|
+
headers.delete("authorization");
|
|
2197
|
+
headers.delete("chatgpt-account-id");
|
|
2198
|
+
}
|
|
2199
|
+
if (providerConsumesCallerAuthorization(route.provider) && options.admission?.source !== undefined
|
|
2200
|
+
&& options.admission.source !== "loopback") {
|
|
2201
|
+
validateForwardAdmissionCredential(headers, config);
|
|
2202
|
+
} else {
|
|
2203
|
+
// Even adapters that ignore caller auth must not retain a proxy secret for
|
|
2204
|
+
// a later internal hop or a future transport change.
|
|
2205
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
2206
|
+
if (bearer && isProxyAdmissionSecret(bearer, config)) {
|
|
2207
|
+
headers.delete("authorization");
|
|
2208
|
+
headers.delete("chatgpt-account-id");
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2112
2211
|
return {
|
|
2113
2212
|
ok: true,
|
|
2114
2213
|
authCtx,
|
|
2115
2214
|
headers,
|
|
2215
|
+
callerAuthHeaders: new Headers(authInputHeaders),
|
|
2116
2216
|
substituteMainCredential,
|
|
2117
2217
|
};
|
|
2118
2218
|
} catch (err) {
|
|
@@ -3084,6 +3184,12 @@ export async function handleResponses(
|
|
|
3084
3184
|
try {
|
|
3085
3185
|
const response = await handleResponsesInner(req, config, logCtx, {
|
|
3086
3186
|
...options,
|
|
3187
|
+
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
3188
|
+
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
|
|
3189
|
+
nativeCallerAuth: options.nativeCallerAuth === undefined
|
|
3190
|
+
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.nativeCallerAuth,
|
|
3191
|
+
callerDirectAuth: options.callerDirectAuth === undefined
|
|
3192
|
+
? captureCallerDirectAuth(req.headers, config) : options.callerDirectAuth,
|
|
3087
3193
|
// Capture before combo replay rebuilds the Request headers; children carry options.
|
|
3088
3194
|
visionDescribeTerminal: options.visionDescribeTerminal === true
|
|
3089
3195
|
|| req.headers.get("x-opencodex-vision-describe") === "1",
|
|
@@ -3332,6 +3438,7 @@ async function handleResponsesInner(
|
|
|
3332
3438
|
logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
|
|
3333
3439
|
|
|
3334
3440
|
let route: RouteResult;
|
|
3441
|
+
let credentialDomainWasRewritten = false;
|
|
3335
3442
|
try {
|
|
3336
3443
|
// A `compaction_trigger` turn may name a bare native model the operator has
|
|
3337
3444
|
// no canonical OpenAI route for (#2901). Only the initial compaction route
|
|
@@ -3353,6 +3460,7 @@ async function handleResponsesInner(
|
|
|
3353
3460
|
} catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
|
|
3354
3461
|
const targetRoute = resolveRoute(_sci.model);
|
|
3355
3462
|
if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
|
|
3463
|
+
credentialDomainWasRewritten = true;
|
|
3356
3464
|
const _sciOriginal = parsed.modelId;
|
|
3357
3465
|
parsed.modelId = _sci.model;
|
|
3358
3466
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
@@ -3484,6 +3592,7 @@ async function handleResponsesInner(
|
|
|
3484
3592
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
3485
3593
|
try {
|
|
3486
3594
|
route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
|
|
3595
|
+
credentialDomainWasRewritten = true;
|
|
3487
3596
|
logCtx.routeDecision = route.routeDecision;
|
|
3488
3597
|
} catch (err) {
|
|
3489
3598
|
if (err instanceof NoAvailableComboTargetsError) {
|
|
@@ -3620,6 +3729,7 @@ async function handleResponsesInner(
|
|
|
3620
3729
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
3621
3730
|
try {
|
|
3622
3731
|
route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
|
|
3732
|
+
credentialDomainWasRewritten = true;
|
|
3623
3733
|
logCtx.routeDecision = route.routeDecision;
|
|
3624
3734
|
} catch (err) {
|
|
3625
3735
|
if (err instanceof NoAvailableComboTargetsError) {
|
|
@@ -3742,11 +3852,13 @@ async function handleResponsesInner(
|
|
|
3742
3852
|
}
|
|
3743
3853
|
|
|
3744
3854
|
let substituteMainCredential = false;
|
|
3855
|
+
let callerAuthHeaders: Headers;
|
|
3745
3856
|
{
|
|
3746
|
-
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
|
|
3857
|
+
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options, credentialDomainWasRewritten);
|
|
3747
3858
|
if (!finalAuth.ok) return finalAuth.response;
|
|
3748
3859
|
authCtx = finalAuth.authCtx;
|
|
3749
3860
|
selectedForwardHeaders = finalAuth.headers;
|
|
3861
|
+
callerAuthHeaders = finalAuth.callerAuthHeaders;
|
|
3750
3862
|
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
3751
3863
|
}
|
|
3752
3864
|
|
|
@@ -4036,7 +4148,8 @@ async function handleResponsesInner(
|
|
|
4036
4148
|
const ownsBearer = snapshot !== undefined
|
|
4037
4149
|
&& sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
|
|
4038
4150
|
&& !sentHeaders?.has("x-api-key");
|
|
4039
|
-
|
|
4151
|
+
// Reselection can choose a provider override instead of the supplied executor.
|
|
4152
|
+
const response = await fetchImpl(destination, { ...dispatchInit, redirect: "manual" });
|
|
4040
4153
|
// Observe each physical response before retries replace it. The binding belongs to
|
|
4041
4154
|
// this dispatch, so a manual switch cannot file A's headers against B. Header
|
|
4042
4155
|
// overrides and credential replacement make ownership unprovable: skip those writes.
|
|
@@ -4223,9 +4336,9 @@ async function handleResponsesInner(
|
|
|
4223
4336
|
);
|
|
4224
4337
|
let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
4225
4338
|
const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
4226
|
-
&& adapterProvider
|
|
4227
|
-
&& adapterProvider.authMode === "forward"
|
|
4228
|
-
|
|
4339
|
+
&& !isCanonicalOpenAiForwardProvider(adapterProvider)
|
|
4340
|
+
&& ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
|
|
4341
|
+
|| providerConsumesCallerAuthorization(adapterProvider));
|
|
4229
4342
|
if (stripClaudeMainAuth) {
|
|
4230
4343
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
4231
4344
|
authCtx = { kind: "main", accountId: null };
|
|
@@ -4313,9 +4426,18 @@ async function handleResponsesInner(
|
|
|
4313
4426
|
const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
|
|
4314
4427
|
if (needsOpenAiVision || needsOpenAiSearch) {
|
|
4315
4428
|
try {
|
|
4429
|
+
// Preserve explicit OpenAI helper auth across route changes without returning it to
|
|
4430
|
+
// primary-provider headers or alternate-main retry. The resolver revalidates scope.
|
|
4431
|
+
const sidecarHeaders = new Headers(req.headers);
|
|
4432
|
+
sidecarHeaders.delete("authorization");
|
|
4433
|
+
sidecarHeaders.delete("chatgpt-account-id");
|
|
4434
|
+
if (options.openAiSidecarAuth) {
|
|
4435
|
+
sidecarHeaders.set("authorization", options.openAiSidecarAuth.authorization);
|
|
4436
|
+
sidecarHeaders.set("chatgpt-account-id", options.openAiSidecarAuth.chatgptAccountId);
|
|
4437
|
+
}
|
|
4316
4438
|
openAiSidecar = await resolveFirstUsableOpenAiSidecar(
|
|
4317
4439
|
listOpenAiForwardSidecarCandidates(config),
|
|
4318
|
-
|
|
4440
|
+
sidecarHeaders,
|
|
4319
4441
|
config,
|
|
4320
4442
|
{
|
|
4321
4443
|
admission: options.admission,
|
|
@@ -5400,7 +5522,7 @@ async function handleResponsesInner(
|
|
|
5400
5522
|
// justify because this flag already produced the identical result.
|
|
5401
5523
|
const storedReplaySpent = codex401ReplayKind === "stored";
|
|
5402
5524
|
const retry = await retryCodexPoolOnAlternateAccount({
|
|
5403
|
-
|
|
5525
|
+
callerAuthHeaders,
|
|
5404
5526
|
config,
|
|
5405
5527
|
route,
|
|
5406
5528
|
parsed,
|
|
@@ -74,13 +74,20 @@ export function providerFetch(
|
|
|
74
74
|
const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
|
|
75
75
|
base.preconnect?.(...args);
|
|
76
76
|
};
|
|
77
|
+
// Rebuilt dispatches must use the same physical-send boundary as ordinary HTTP sends.
|
|
78
|
+
// Return the original 3xx so the response owner retains its retry/health/relay contract.
|
|
79
|
+
const dispatch = Object.assign(
|
|
80
|
+
(input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
|
|
81
|
+
base(input, { ...init, redirect: "manual" }),
|
|
82
|
+
{ preconnect },
|
|
83
|
+
) as typeof globalThis.fetch;
|
|
77
84
|
const httpFetch = Object.assign(
|
|
78
85
|
async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
79
86
|
options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
|
|
80
87
|
const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
|
|
81
88
|
return options.dispatchOverride
|
|
82
|
-
? options.dispatchOverride(input, dispatchInit,
|
|
83
|
-
:
|
|
89
|
+
? options.dispatchOverride(input, dispatchInit, dispatch)
|
|
90
|
+
: dispatch(input, dispatchInit);
|
|
84
91
|
},
|
|
85
92
|
{ preconnect },
|
|
86
93
|
) as typeof globalThis.fetch;
|
|
@@ -163,6 +170,10 @@ export function storedPoolReplayDispatchNotifier(
|
|
|
163
170
|
}) as ProviderFetch;
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Fetch through the header deadline with redirects always manual.
|
|
175
|
+
* @param _manualRedirect Ignored; retained for call compatibility. Even false uses manual.
|
|
176
|
+
*/
|
|
166
177
|
export async function fetchWithHeaderTimeout(
|
|
167
178
|
url: string,
|
|
168
179
|
init: Omit<RequestInit, "signal">,
|
|
@@ -170,7 +181,8 @@ export async function fetchWithHeaderTimeout(
|
|
|
170
181
|
timeoutMs: number,
|
|
171
182
|
preferIdentityEncoding = false,
|
|
172
183
|
executor: typeof globalThis.fetch = globalThis.fetch,
|
|
173
|
-
|
|
184
|
+
// Retained for existing callers; credential-bearing transport no longer opts out.
|
|
185
|
+
_manualRedirect = false,
|
|
174
186
|
): Promise<Response> {
|
|
175
187
|
const pacing = executor as ProviderFetch;
|
|
176
188
|
await pacing.waitForPacing?.(abortSignal);
|
|
@@ -189,10 +201,9 @@ export async function fetchWithHeaderTimeout(
|
|
|
189
201
|
return await fetchExecutor(url, {
|
|
190
202
|
...init,
|
|
191
203
|
headers,
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
...(manualRedirect ? { redirect: "manual" as const } : {}),
|
|
204
|
+
// Never replay provider credentials or request bodies to a redirect destination.
|
|
205
|
+
// Preserve the 3xx for the owner's existing response/health policy (#914, #1471).
|
|
206
|
+
redirect: "manual",
|
|
196
207
|
signal: AbortSignal.any([abortSignal, timeout.signal]),
|
|
197
208
|
timeout: 0,
|
|
198
209
|
});
|
|
@@ -6,6 +6,8 @@ import type { OcxConfig } from "../../types";
|
|
|
6
6
|
import type { RouteCandidateTrace, RouteDecisionTraceV1 } from "../../routing/trace";
|
|
7
7
|
import { handleResponses as handleResponsesCore } from "./core";
|
|
8
8
|
import { requestPacingOverloadResponse } from "./pacing-overload";
|
|
9
|
+
import { captureExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
10
|
+
import { captureCallerDirectAuth } from "../../providers/caller-authorization";
|
|
9
11
|
|
|
10
12
|
type CoreHandler = typeof handleResponsesCore;
|
|
11
13
|
type CoreOptions = Parameters<CoreHandler>[3];
|
|
@@ -47,6 +49,10 @@ function requestWithCandidate(
|
|
|
47
49
|
candidate: Pick<RouteCandidateTrace, "provider" | "model">,
|
|
48
50
|
): Request {
|
|
49
51
|
const headers = new Headers(req.headers);
|
|
52
|
+
// The next candidate owns a different physical credential domain. Typed
|
|
53
|
+
// admission and any claimed Claude snapshot stay in caller-owned CoreOptions.
|
|
54
|
+
headers.delete("authorization");
|
|
55
|
+
headers.delete("chatgpt-account-id");
|
|
50
56
|
headers.delete("content-encoding");
|
|
51
57
|
headers.delete("content-length");
|
|
52
58
|
headers.set("content-type", "application/json");
|
|
@@ -119,6 +125,12 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
119
125
|
let storedPool401ReplayDispatched = false;
|
|
120
126
|
const coreOptions: CoreOptions = {
|
|
121
127
|
...options,
|
|
128
|
+
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
129
|
+
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
|
|
130
|
+
nativeCallerAuth: options.nativeCallerAuth === undefined
|
|
131
|
+
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.nativeCallerAuth,
|
|
132
|
+
callerDirectAuth: options.callerDirectAuth === undefined
|
|
133
|
+
? captureCallerDirectAuth(req.headers, config) : options.callerDirectAuth,
|
|
122
134
|
...(options.onRequestBodyRead ? {
|
|
123
135
|
onRequestBodyRead: () => {
|
|
124
136
|
if (requestBodyReadNotified) return;
|
package/src/service.ts
CHANGED
|
@@ -16,6 +16,13 @@ import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
|
|
|
16
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
17
17
|
import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
|
|
18
18
|
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Written only by the launchd plist and the systemd unit. `OCX_SERVICE=1` cannot stand in
|
|
22
|
+
* for it: `ocx claude` and `ocx opencode` set that on the proxies they spawn to borrow its
|
|
23
|
+
* routing-preservation meaning, so a proxy carrying it is not necessarily the managed job.
|
|
24
|
+
*/
|
|
25
|
+
export const SERVICE_MANAGED_ENV = "OCX_SERVICE_MANAGED";
|
|
19
26
|
import type { BunRuntimeSource, DurableBunRuntime } from "./lib/bun-runtime";
|
|
20
27
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
21
28
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
@@ -508,6 +515,11 @@ export function buildPlist(
|
|
|
508
515
|
const opencodexHome = process.env.OPENCODEX_HOME?.trim();
|
|
509
516
|
const envLines = [
|
|
510
517
|
` <key>OCX_SERVICE</key><string>1</string>`,
|
|
518
|
+
// OCX_SERVICE alone cannot identify the managed job: `ocx claude` and `ocx opencode`
|
|
519
|
+
// also set it on the proxies they spawn, to borrow its routing-preservation meaning
|
|
520
|
+
// (src/cli/index.ts preserveRouting). Only the wrapper writes this second marker, so
|
|
521
|
+
// the dashboard-stop refusal below can tell a real launchd job from an ordinary child.
|
|
522
|
+
` <key>${SERVICE_MANAGED_ENV}</key><string>1</string>`,
|
|
511
523
|
...(launcher ? [] : [
|
|
512
524
|
` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
|
|
513
525
|
` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
|
|
@@ -3328,6 +3340,7 @@ export function buildUnit(
|
|
|
3328
3340
|
const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
|
|
3329
3341
|
const envLines = [
|
|
3330
3342
|
systemdEnvironmentAssignment("OCX_SERVICE", "1"),
|
|
3343
|
+
systemdEnvironmentAssignment(SERVICE_MANAGED_ENV, "1"),
|
|
3331
3344
|
...(launcher ? [] : [
|
|
3332
3345
|
systemdEnvironmentAssignment(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
|
|
3333
3346
|
systemdEnvironmentAssignment(BUN_RUNTIME_PATH_ENV, bun),
|
|
@@ -3860,10 +3873,31 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
3860
3873
|
export function installedServiceRespawnRisk(
|
|
3861
3874
|
probe: () => WindowsSchedulerTaskProbe = probeWindowsSchedulerTask,
|
|
3862
3875
|
platform: NodeJS.Platform = process.platform,
|
|
3863
|
-
|
|
3876
|
+
io: { env?: NodeJS.ProcessEnv; exists?: (path: string) => boolean } = {},
|
|
3877
|
+
): "none" | "respawnable" | "unknown" | "self-unload" {
|
|
3864
3878
|
// launchd, systemd and WinSW are down when they report stopped; only the Task Scheduler
|
|
3865
3879
|
// wrapper survives its task ending (#764).
|
|
3866
|
-
|
|
3880
|
+
//
|
|
3881
|
+
// "Down when they report stopped" answers the RESPAWN question but not the SELF-UNLOAD
|
|
3882
|
+
// one (#4023). When the proxy is itself the managed job, `launchctl unload` /
|
|
3883
|
+
// `systemctl stop` terminate this very process, so the manager stop can kill the request
|
|
3884
|
+
// handler before the shared teardown restores the native Codex config keys — leaving
|
|
3885
|
+
// `openai_base_url`, `experimental_realtime_ws_base_url` and `model_catalog_json`
|
|
3886
|
+
// pointed at a proxy that is gone. Reordering teardown ahead of the manager stop is not
|
|
3887
|
+
// available here: the #3008 contract requires the manager to be proven stopped first.
|
|
3888
|
+
// So refuse, exactly as Windows does, and send the operator to `ocx stop`, which stops
|
|
3889
|
+
// the proxy from the outside and owns the teardown through its receipt.
|
|
3890
|
+
if (platform !== "win32") {
|
|
3891
|
+
const env = io.env ?? process.env;
|
|
3892
|
+
// Discriminate on the wrapper-only marker, not on OCX_SERVICE: `ocx claude` and
|
|
3893
|
+
// `ocx opencode` set OCX_SERVICE=1 on the proxies they spawn (for preserveRouting),
|
|
3894
|
+
// and refusing their dashboard stop would break a proxy that no manager supervises.
|
|
3895
|
+
if (env[SERVICE_MANAGED_ENV] !== "1") return "none";
|
|
3896
|
+
const exists = io.exists ?? existsSync;
|
|
3897
|
+
if (platform === "darwin") return exists(plistPath()) ? "self-unload" : "none";
|
|
3898
|
+
if (platform === "linux") return exists(unitPath()) ? "self-unload" : "none";
|
|
3899
|
+
return "none";
|
|
3900
|
+
}
|
|
3867
3901
|
try {
|
|
3868
3902
|
// `probeWindowsSchedulerTask` returns "unknown" as an ordinary value when its queries
|
|
3869
3903
|
// fail — it does not throw — so testing for "present" let an unanswerable probe
|