@bitkyc08/opencodex 2.89.0 → 2.90.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-DgxkE6Md.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-TmaHa54u.js} +5 -5
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google.ts +28 -9
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-store.ts +27 -8
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +10 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +48 -6
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +8 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/generated/compatibility-version.json +168 -108
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/providers/claude-cli-usage.ts +20 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +10 -2
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota/anthropic-account-quota.ts +18 -6
- package/src/providers/quota/antigravity.ts +6 -2
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/vendor-probes-oauth.ts +12 -3
- package/src/providers/quota-routing-cache.ts +1 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry/entries-extended.ts +6 -1
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +11 -2
- package/src/server/chat-native.ts +8 -7
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/messages-native.ts +11 -0
- package/src/server/request-log.ts +16 -7
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +2 -2
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-stream-preflight.ts +7 -2
- package/src/server/responses/compact.ts +2 -0
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +3 -0
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +8 -0
- package/src/server/responses/fetch-helpers.ts +30 -10
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +2 -0
- package/src/server/responses/passthrough-dispatch.ts +13 -13
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/service/orchestration.ts +17 -0
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +16 -1
- package/src/types.ts +1 -0
- package/src/web-search/loop.ts +2 -2
- package/gui/dist/assets/App-BnRSujOq.js +0 -58
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { recoverAutoPausedAccount } from "./
|
|
1
|
+
import { recoverAutoPausedAccount, rotatePassthroughKeyOn429 } from "./passthrough-key-rotation";
|
|
2
2
|
import { adoptCodexWsStage, capturePassthroughAffinity } from "./passthrough-observations";
|
|
3
3
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
4
4
|
import { requestPacingOverloadResponse } from "./pacing-overload";
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import { restoreRoutedCustomCalls, RoutedCustomToolCompatError } from "../../responses/custom-tool-compat";
|
|
54
54
|
import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
|
|
55
55
|
import { MuseToolChoiceCompatibilityError } from "../../adapters/openai-responses/muse-tool-choice";
|
|
56
|
+
import { OpenGatewayInputCompatibilityError } from "../../adapters/openai-responses/opengateway-input";
|
|
56
57
|
import { formatErrorResponse } from "../../bridge";
|
|
57
58
|
import { redactSecretString } from "../../lib/redact";
|
|
58
59
|
import {
|
|
@@ -132,7 +133,7 @@ import {
|
|
|
132
133
|
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
133
134
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
134
135
|
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
135
|
-
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
136
|
+
import { codexFailureWindowOutcomeMeta, recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
136
137
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
137
138
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
138
139
|
import {
|
|
@@ -172,6 +173,7 @@ import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION }
|
|
|
172
173
|
import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
|
|
173
174
|
import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
|
|
174
175
|
import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
|
|
176
|
+
import { guardPassthroughCompletion, parseOutboundRequestBody } from "./passthrough-completion";
|
|
175
177
|
|
|
176
178
|
/** Transfer an uncommitted Copilot exchange to the existing translated pipeline without new admission. */
|
|
177
179
|
export interface PassthroughAdapterHandoff { kind: "adapter-handoff" }
|
|
@@ -386,6 +388,7 @@ export async function preparePassthroughExchange(
|
|
|
386
388
|
|| error instanceof XaiToolSchemaCompatibilityError
|
|
387
389
|
|| error instanceof RoutedCustomToolCompatError
|
|
388
390
|
|| error instanceof MuseToolChoiceCompatibilityError
|
|
391
|
+
|| error instanceof OpenGatewayInputCompatibilityError
|
|
389
392
|
) {
|
|
390
393
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
391
394
|
}
|
|
@@ -434,16 +437,6 @@ export async function preparePassthroughExchange(
|
|
|
434
437
|
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
435
438
|
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
436
439
|
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
437
|
-
const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
|
|
438
|
-
try {
|
|
439
|
-
const body = JSON.parse(bodyText) as unknown;
|
|
440
|
-
return body && typeof body === "object" && !Array.isArray(body)
|
|
441
|
-
? body as Record<string, unknown>
|
|
442
|
-
: undefined;
|
|
443
|
-
} catch {
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
440
|
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
448
441
|
const declaredWireToolNames = new Set<string>();
|
|
449
442
|
const declaredBareWireToolNames = new Set<string>();
|
|
@@ -990,6 +983,7 @@ export async function preparePassthroughExchange(
|
|
|
990
983
|
}
|
|
991
984
|
if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
|
|
992
985
|
recordCodexUpstreamOutcome(config, admissionState.authCtx.accountId, outcome, {
|
|
986
|
+
...codexFailureWindowOutcomeMeta(admissionState.authCtx),
|
|
993
987
|
threadId: admissionState.authCtx.affinityKey,
|
|
994
988
|
fixedAccount: admissionState.authCtx.fixedAccount,
|
|
995
989
|
modelId: route.modelId,
|
|
@@ -1157,6 +1151,7 @@ export async function preparePassthroughExchange(
|
|
|
1157
1151
|
`${route.providerName}|${route.modelId}|${recovery}`,
|
|
1158
1152
|
{ allowFinalRecoveryReserve: transientSendPolicy() === null },
|
|
1159
1153
|
);
|
|
1154
|
+
if (recovery === "empty-completion") allowance.attempts = Math.min(1, allowance.attempts);
|
|
1160
1155
|
try {
|
|
1161
1156
|
return await fetchWithTransientRetry(
|
|
1162
1157
|
innerRecovery => {
|
|
@@ -1188,7 +1183,8 @@ export async function preparePassthroughExchange(
|
|
|
1188
1183
|
.then(adoptObservedResponse);
|
|
1189
1184
|
},
|
|
1190
1185
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
|
|
1191
|
-
onSendsConsumed: transientSendReporter(allowance.permit),
|
|
1186
|
+
onSendsConsumed: transientSendReporter(allowance.permit),
|
|
1187
|
+
claimAmbiguousResend: recovery === "empty-completion" ? undefined : claimPreHeaderResend },
|
|
1192
1188
|
);
|
|
1193
1189
|
} catch (err) {
|
|
1194
1190
|
return { failed: transportFailureResponse(err) };
|
|
@@ -1606,6 +1602,7 @@ export async function preparePassthroughExchange(
|
|
|
1606
1602
|
return transportFailureResponse(err);
|
|
1607
1603
|
}
|
|
1608
1604
|
}
|
|
1605
|
+
const keyRotation = await rotatePassthroughKeyOn429({ config, route, parsed, response: upstreamResponse, adapterName: transportState.adapter.name, budgetSpent: () => sendBudgetExhausted(transientSendAttempts()), refetch: () => rebuildAndRefetch("key-429") }); if (keyRotation) { if ("failed" in keyRotation) return keyRotation.failed; upstreamResponse = keyRotation; continue passthroughRecovery; }
|
|
1609
1606
|
const captureAffinityResponse = (
|
|
1610
1607
|
response: Response,
|
|
1611
1608
|
captureAuthCtx: CodexAuthContext = admissionState.authCtx,
|
|
@@ -1942,6 +1939,9 @@ export async function preparePassthroughExchange(
|
|
|
1942
1939
|
break;
|
|
1943
1940
|
}
|
|
1944
1941
|
|
|
1942
|
+
upstreamResponse = await guardPassthroughCompletion(
|
|
1943
|
+
upstreamResponse, request, config, upstream.signal, logCtx, rebuildAndRefetch,
|
|
1944
|
+
);
|
|
1945
1945
|
// Where this relay dials upstream. Local infrastructure (loopback / private / `.local` / `.lan`)
|
|
1946
1946
|
// is operator-trusted and its silent phases are normal, so an unset stall budget resolves to
|
|
1947
1947
|
// disabled for it. Provider rotation above keeps the same endpoint origin, so the routed
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
2
|
+
import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
|
|
3
|
+
import type { RouteResult } from "../../router";
|
|
4
|
+
import type { OcxConfig, OcxParsedRequest } from "../../types";
|
|
5
|
+
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
6
|
+
|
|
7
|
+
// Passthrough's credential moves share one import site so passthrough-dispatch.ts stays under its line cap.
|
|
8
|
+
export { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Key-pool failover for a native passthrough 429 before any output. This wire returns before the
|
|
12
|
+
* adapter loop's key-429 arm, so without it a pooled key's 429 reached the client while the same
|
|
13
|
+
* refusal rotates on the adapter and chat-native wires. Distributed (round-robin) selection already
|
|
14
|
+
* cools the key on the send and deliberately answers without a replay. A synthesized replay refusal
|
|
15
|
+
* is not a provider rate limit and must not cool a key. A spent send budget is checked before any
|
|
16
|
+
* key is cooled, so it keeps the real 429 body. Returns undefined when nothing rotated.
|
|
17
|
+
*/
|
|
18
|
+
export async function rotatePassthroughKeyOn429<T>(options: {
|
|
19
|
+
config: OcxConfig;
|
|
20
|
+
route: Pick<RouteResult, "provider" | "providerName">;
|
|
21
|
+
parsed: OcxParsedRequest;
|
|
22
|
+
response: Response;
|
|
23
|
+
adapterName: string;
|
|
24
|
+
budgetSpent: () => boolean;
|
|
25
|
+
refetch: () => Promise<T>;
|
|
26
|
+
}): Promise<T | undefined> {
|
|
27
|
+
const { config, route, parsed, response, adapterName, refetch } = options;
|
|
28
|
+
if (response.status !== 429 || isNonReplayableResponse(response) || !hasKeyPoolFailover(route.provider)
|
|
29
|
+
|| route.provider._apiKeyAttempt?.distributed || options.budgetSpent()) return;
|
|
30
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
31
|
+
retryAfter: response.headers.get("retry-after"),
|
|
32
|
+
attemptedKey: route.provider.apiKey,
|
|
33
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
34
|
+
});
|
|
35
|
+
if (!rotated) return;
|
|
36
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
37
|
+
route.provider = rotated;
|
|
38
|
+
bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: rotated, adapterName });
|
|
39
|
+
return refetch();
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import { ReasoningArchiveBusyError, ReasoningArchiveError,
|
|
3
|
+
ReasoningArchiveQuotaError } from "../../responses/reasoning-retention";
|
|
4
|
+
|
|
5
|
+
/** Local storage refusal before any send. Never expose filesystem messages or causes. */
|
|
6
|
+
export function reasoningArchiveErrorResponse(error: unknown): Response | undefined {
|
|
7
|
+
let code: string;
|
|
8
|
+
let message: string;
|
|
9
|
+
if (error instanceof ReasoningArchiveQuotaError) {
|
|
10
|
+
code = "reasoning_archive_quota_exceeded";
|
|
11
|
+
message = "Local reasoning archive is full. Review and manually remove older reasoning archives, then retry the unchanged request.";
|
|
12
|
+
} else if (error instanceof ReasoningArchiveBusyError) {
|
|
13
|
+
code = "reasoning_archive_busy";
|
|
14
|
+
message = "Another local reasoning archive writer is active. Wait for it to finish, then retry the unchanged request.";
|
|
15
|
+
} else if (error instanceof ReasoningArchiveError) {
|
|
16
|
+
code = "reasoning_archive_unavailable";
|
|
17
|
+
message = "Local reasoning archive is unavailable. Check archive directory permissions and local storage health before retrying the unchanged request.";
|
|
18
|
+
} else return undefined;
|
|
19
|
+
// Match the local spend-ledger storage refusal's 503 convention without guessing a delay.
|
|
20
|
+
return formatErrorResponse(503, "server_error", message, { code, classified: true });
|
|
21
|
+
}
|
|
@@ -100,6 +100,7 @@ import type {
|
|
|
100
100
|
import {
|
|
101
101
|
codexRouteCredentialDomainHeaders,
|
|
102
102
|
codexRouteCredentialOwnership,
|
|
103
|
+
mainHardLockFallbackCallerCheck,
|
|
103
104
|
resolveResponsesCodexAuth,
|
|
104
105
|
withClaudeNativeSession,
|
|
105
106
|
} from "./core-auth";
|
|
@@ -364,7 +365,16 @@ export async function prepareResponsesRequest(
|
|
|
364
365
|
// must run before comboIdFromRawBody reads `model`.
|
|
365
366
|
if (!concreteSelection && !options.compactionRoutingOverride && !isCompactionTrigger && !options.internalDecisionCall
|
|
366
367
|
&& !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
|
|
367
|
-
|
|
368
|
+
try {
|
|
369
|
+
options.mainHardLockFallbackOverride = await applyMainHardLockFallbackOverride(
|
|
370
|
+
body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options),
|
|
371
|
+
);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
|
|
374
|
+
const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now() });
|
|
375
|
+
if (mapped) return mapped;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
368
378
|
}
|
|
369
379
|
const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
|
|
370
380
|
// A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
|
|
@@ -71,7 +71,8 @@ import {
|
|
|
71
71
|
resolveCurrentProviderApiKeyTransport,
|
|
72
72
|
} from "../../providers/api-key-selection";
|
|
73
73
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
74
|
-
import { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
74
|
+
import { codexWsExecutorBinding, physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
75
|
+
import { deepSeekImageFileBody, needsDeepSeekImageFiles } from "./deepseek-image-files";
|
|
75
76
|
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
76
77
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
77
78
|
import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
|
|
@@ -590,10 +591,62 @@ export async function prepareResponsesTransport(
|
|
|
590
591
|
};
|
|
591
592
|
}
|
|
592
593
|
const build = resolved.buildRequest.bind(resolved);
|
|
594
|
+
const fileProvider = route.provider;
|
|
595
|
+
const fileProviderName = route.providerName;
|
|
596
|
+
// Persistence consent is configuration: capture its generation with the provider snapshot.
|
|
597
|
+
const fileConsentGeneration = captureConfigGeneration();
|
|
593
598
|
resolved.buildRequest = async (requestParsed, incoming) => {
|
|
594
599
|
const request = await build(requestParsed, incoming);
|
|
595
600
|
// Capture at adapter creation, never from mutable serving state after an await.
|
|
596
601
|
if (binding) requestBindings.set(request, binding);
|
|
602
|
+
const allowedImageFileModels = fileProvider.modelCapabilities
|
|
603
|
+
? Object.entries(fileProvider.modelCapabilities)
|
|
604
|
+
.filter(([, cap]) => cap?.deepseekImageFiles === true)
|
|
605
|
+
.map(([model]) => model)
|
|
606
|
+
: [];
|
|
607
|
+
if (allowedImageFileModels.includes(request.outboundModelId ?? requestParsed.modelId) && needsDeepSeekImageFiles(request)) {
|
|
608
|
+
const consentModel = request.outboundModelId ?? requestParsed.modelId;
|
|
609
|
+
try {
|
|
610
|
+
// Ordinary uploads use provider pacing. A queued rebuild retains its enclosing
|
|
611
|
+
// fetch lease; reacquiring that same concurrency slot would deadlock at a limit of one.
|
|
612
|
+
const uploadFetch = providerFetch(fileProvider, options.codexWsRuntimeIdentity, {
|
|
613
|
+
httpOnly: true, providerName: fileProviderName, modelId: requestParsed.modelId,
|
|
614
|
+
beforeDispatch: () => {
|
|
615
|
+
if (!selectionIsCurrent(binding)) throw new Error("Credential selection changed before image upload");
|
|
616
|
+
// A queued upload must not outlive a revoked opt-in or a configuration reload.
|
|
617
|
+
if (captureConfigGeneration() !== fileConsentGeneration
|
|
618
|
+
|| fileProvider.modelCapabilities?.[consentModel]?.deepseekImageFiles !== true) {
|
|
619
|
+
throw new Error("DeepSeek image file consent changed before image upload");
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
const uploadSignal = incoming.abortSignal ?? options.abortSignal ?? req.signal;
|
|
624
|
+
const pacedUpload: typeof fetch = incoming.dispatchPacingHeld
|
|
625
|
+
? (async (input, init) => {
|
|
626
|
+
// The inference lease owns concurrency, but each physical upload still
|
|
627
|
+
// consumes an interval slot, including after a credential rebuild.
|
|
628
|
+
const slot = await waitForProviderRequestSlot(fileProviderName, fileProvider, requestParsed.modelId,
|
|
629
|
+
init?.signal ?? uploadSignal, { concurrency: false });
|
|
630
|
+
try { return await uploadFetch.unpacedFetch!(input, init); }
|
|
631
|
+
finally { releaseProviderRequestSlot(slot); }
|
|
632
|
+
}) as typeof fetch
|
|
633
|
+
: uploadFetch;
|
|
634
|
+
const body = await deepSeekImageFileBody(request, pacedUpload,
|
|
635
|
+
uploadSignal, {
|
|
636
|
+
allowedModels: allowedImageFileModels, budget: incoming.translatorBudget,
|
|
637
|
+
});
|
|
638
|
+
if (body !== undefined) {
|
|
639
|
+
request.releaseBodyObservation?.();
|
|
640
|
+
request.body = body;
|
|
641
|
+
const headers = new Headers(request.headers);
|
|
642
|
+
headers.delete("content-length");
|
|
643
|
+
request.headers = Object.fromEntries(headers);
|
|
644
|
+
request.releaseBodyObservation = incoming.translatorBudget.observeExternallyCapped(
|
|
645
|
+
"passthrough_serialization", Buffer.byteLength(body, "utf8"),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
} catch (error) { request.releaseBodyObservation?.(); throw error; }
|
|
649
|
+
}
|
|
597
650
|
return request;
|
|
598
651
|
};
|
|
599
652
|
if (resolved.runTurn) {
|
|
@@ -758,7 +811,8 @@ export async function prepareResponsesTransport(
|
|
|
758
811
|
pacingChanged = false;
|
|
759
812
|
}
|
|
760
813
|
if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
|
|
761
|
-
const
|
|
814
|
+
const ws = codexWsExecutorBinding(execute);
|
|
815
|
+
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? ws?.http ?? execute;
|
|
762
816
|
const binding = requestBindings.get(wireRequest);
|
|
763
817
|
const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
|
|
764
818
|
? binding.snapshot : undefined;
|
|
@@ -798,7 +852,10 @@ export async function prepareResponsesTransport(
|
|
|
798
852
|
passiveQuotaWriterGeneration = writerGeneration;
|
|
799
853
|
}
|
|
800
854
|
commitKeyAttemptSend();
|
|
801
|
-
|
|
855
|
+
// The WS executor dials its own socket: HTTP connection policy and body coding do not apply.
|
|
856
|
+
// WS eligibility (egress, pacing, URL) was checked for the provider it was built for; a
|
|
857
|
+
// provider reselected above was not, so it takes the HTTP fallback under its own route.
|
|
858
|
+
response = ws?.provider === route.provider ? await execute(destination, dispatchInit) : await sendWithConnectionPolicy(
|
|
802
859
|
distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
|
|
803
860
|
destination,
|
|
804
861
|
{ ...dispatchInit, redirect: "manual",
|
|
@@ -843,6 +900,7 @@ export async function prepareResponsesTransport(
|
|
|
843
900
|
adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
|
|
844
901
|
const rebuilt = await nextAdapter.buildRequest(requestParsed, {
|
|
845
902
|
headers: requestState.selectedForwardHeaders, providerName: route.providerName, translatorBudget,
|
|
903
|
+
dispatchPacingHeld: true,
|
|
846
904
|
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
847
905
|
});
|
|
848
906
|
const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
|
|
@@ -238,6 +238,11 @@ export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
|
|
|
238
238
|
supervisionLatch?: SupervisionLatch;
|
|
239
239
|
diagnose?: () => ServiceDiagnostic;
|
|
240
240
|
managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
|
|
241
|
+
/**
|
|
242
|
+
* Task Scheduler only: ends surviving launcher/wrapper processes of THIS
|
|
243
|
+
* installation. Scoped to canonical script/launcher tokens, never a bare PID.
|
|
244
|
+
*/
|
|
245
|
+
killServiceWrappers?: () => void;
|
|
241
246
|
stopTrackedProxy?: () => Promise<unknown>;
|
|
242
247
|
platform?: NodeJS.Platform;
|
|
243
248
|
}
|
|
@@ -279,6 +284,18 @@ export async function prepareServiceInstall(
|
|
|
279
284
|
if (!manager) throw new Error(`Background service manager is unavailable for ${backend}.`);
|
|
280
285
|
if (manager.status() !== null) manager.stop();
|
|
281
286
|
}
|
|
287
|
+
if (platform === "win32") {
|
|
288
|
+
// `schtasks /end` ends the task instance but leaves the cmd `:loop` wrapper
|
|
289
|
+
// alive to respawn the proxy mid-install — the same survivor
|
|
290
|
+
// `ocx stop` kills via killWindowsServiceWrapperProcesses. Run it
|
|
291
|
+
// after every manager has been asked to stop and before the tracked-proxy
|
|
292
|
+
// kill so a child respawned in that gap is still caught below. It also runs
|
|
293
|
+
// when no manager stop happened: a registration deleted mid-uninstall can
|
|
294
|
+
// leave the detached wrapper looping. A manager stop that refused propagates
|
|
295
|
+
// above before this runs, so the respawner of a still-running service is
|
|
296
|
+
// never killed.
|
|
297
|
+
(deps.killServiceWrappers ?? killWindowsServiceWrapperProcesses)();
|
|
298
|
+
}
|
|
282
299
|
await (deps.stopTrackedProxy ?? stopTrackedProxyIfRunning)();
|
|
283
300
|
}
|
|
284
301
|
|
package/src/types/provider.ts
CHANGED
|
@@ -288,6 +288,8 @@ export interface ModelCapabilities {
|
|
|
288
288
|
/** Requested tier only; does not imply an upstream window or activate an unverified wire. */
|
|
289
289
|
contextTier?: "default" | "long_context";
|
|
290
290
|
video?: { processing?: "static" | "agentic" };
|
|
291
|
+
/** Opt in exact first-party DeepSeek models to one-hour image file persistence. */
|
|
292
|
+
deepseekImageFiles?: boolean;
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
export interface OcxProviderConfig {
|
package/src/types/request.ts
CHANGED
|
@@ -322,8 +322,23 @@ export interface OcxDocumentContent {
|
|
|
322
322
|
filename?: string;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Inline audio carried by Codex's Responses input. Chat Completions accepts this as an
|
|
327
|
+
* `input_audio` content part only for WAV and MP3 payloads, so the normalized form keeps the
|
|
328
|
+
* base64 bytes and a wire-safe format label rather than the original data URL.
|
|
329
|
+
*/
|
|
330
|
+
export interface OcxAudioContent {
|
|
331
|
+
type: "audio";
|
|
332
|
+
/** Base64 payload with no `data:` prefix. */
|
|
333
|
+
data: string;
|
|
334
|
+
/** Chat Completions input_audio supports WAV and MP3. */
|
|
335
|
+
format: "mp3" | "wav";
|
|
336
|
+
/** Text marker for wires that cannot carry the bytes. */
|
|
337
|
+
text: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
325
340
|
/** A user/developer message content part: text, native media, or an attached document. */
|
|
326
|
-
export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent;
|
|
341
|
+
export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent | OcxAudioContent;
|
|
327
342
|
|
|
328
343
|
export interface OcxThinkingContent {
|
|
329
344
|
type: "thinking";
|
package/src/types.ts
CHANGED
package/src/web-search/loop.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { WebSearchBackendId } from "./index";
|
|
|
24
24
|
import { pacingHeaderDeadline } from "./header-deadline";
|
|
25
25
|
import { redactSecretString } from "../lib/redact";
|
|
26
26
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
27
|
-
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
27
|
+
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, isSameTargetRetryHold, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
28
28
|
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
29
29
|
import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
30
30
|
import {
|
|
@@ -670,7 +670,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
670
670
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
|
|
671
671
|
// rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
|
|
672
672
|
while (
|
|
673
|
-
prepared.response.status === 429
|
|
673
|
+
prepared.response.status === 429 && !isSameTargetRetryHold(prepared.response)
|
|
674
674
|
&& rateLimitRetryPolicy !== null
|
|
675
675
|
&& rateLimitRetries < rateLimitRetryPolicy.attempts
|
|
676
676
|
) {
|