@bitkyc08/opencodex 2.89.0 → 2.91.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-DMps4OLk.css +1 -0
- package/gui/dist/assets/App-nZ34cR5G.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +28 -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 +7 -2
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +19 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- 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/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +84 -9
- package/src/cli/provider.ts +44 -2
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- 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 +58 -12
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -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/codex/subagent-model-fallback.ts +24 -1
- package/src/combos/failover.ts +8 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -2
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +360 -192
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- 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/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- 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 +240 -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 +8 -10
- 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/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- 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/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- 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/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- package/src/server/chat-native.ts +8 -7
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +27 -4
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/live.ts +5 -0
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +12 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.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/agent-task-recovery.ts +1 -1
- 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 +4 -2
- package/src/server/responses/core-auth.ts +31 -1
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +75 -1
- 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/encrypted-payload.ts +3 -3
- 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 +85 -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/policy-fallback.ts +44 -1
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +19 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/orchestration.ts +17 -0
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -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/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -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);
|
|
@@ -214,6 +214,9 @@ export interface ReplacementStartIo extends ReplacementReadinessIo {
|
|
|
214
214
|
logPath?: string | null;
|
|
215
215
|
parentPid?: number;
|
|
216
216
|
retryDelayMs?: number;
|
|
217
|
+
/** Exit waits for a terminated not-ready replacement; tests shorten them. */
|
|
218
|
+
termWaitMs?: number;
|
|
219
|
+
killWaitMs?: number;
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
export interface ReplacementStartRequest {
|
|
@@ -227,10 +230,19 @@ export interface ReplacementStartRequest {
|
|
|
227
230
|
waitForHealth: boolean;
|
|
228
231
|
/** The replacement's environment, already prepared by the caller; the parent marker is added here. */
|
|
229
232
|
env: NodeJS.ProcessEnv;
|
|
233
|
+
/**
|
|
234
|
+
* What a replacement still unready at the deadline means. `handoff` (the default, every
|
|
235
|
+
* standalone restart) hands off to it anyway, because the parent is about to exit and release
|
|
236
|
+
* what it may be waiting for. `terminate` is for a parent that keeps serving when the
|
|
237
|
+
* replacement fails (the client runtime): the replacement is stopped, its exit is observed, and
|
|
238
|
+
* the call rejects with `not_ready`, so nothing that may still bind the port survives.
|
|
239
|
+
*/
|
|
240
|
+
notReady?: "handoff" | "terminate";
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
type AttemptOutcome =
|
|
233
|
-
| { kind: "spawned" | "ready"
|
|
244
|
+
| { kind: "spawned" | "ready"; pid: number | undefined }
|
|
245
|
+
| { kind: "not-ready"; pid: number | undefined; terminate: () => Promise<boolean> }
|
|
234
246
|
| { kind: "early-exit"; pid: number | undefined; code: number | null; signal: NodeJS.Signals | null };
|
|
235
247
|
|
|
236
248
|
function pinnedPort(port: number | undefined): number | undefined {
|
|
@@ -314,7 +326,11 @@ function runReplacementAttempt(
|
|
|
314
326
|
void waitForReplacementReady(child.pid, parentPid, expectedPort, { ...io, cancelled: () => settled }).then(
|
|
315
327
|
// Never kill a live ordinary start at its valid reclaim boundary. Parent exit is the
|
|
316
328
|
// final resource release the child may still be waiting for.
|
|
317
|
-
ready => {
|
|
329
|
+
ready => {
|
|
330
|
+
settle(ready
|
|
331
|
+
? { kind: "ready", pid: child.pid }
|
|
332
|
+
: { kind: "not-ready", pid: child.pid, terminate: () => terminateReplacement(child, io) });
|
|
333
|
+
},
|
|
318
334
|
err => { settle({ error: err }); },
|
|
319
335
|
);
|
|
320
336
|
};
|
|
@@ -369,6 +385,11 @@ export async function spawnReplacementStart(
|
|
|
369
385
|
continue;
|
|
370
386
|
}
|
|
371
387
|
if (outcome.kind === "not-ready") {
|
|
388
|
+
if (request.notReady === "terminate") {
|
|
389
|
+
const exited = await outcome.terminate();
|
|
390
|
+
log?.note(`replacement pid ${pid} did not answer within the readiness window; ${exited ? "stopped it" : "it did not exit after SIGKILL"}`);
|
|
391
|
+
throw handoffError(exited ? "not_ready" : "not_ready_unstopped");
|
|
392
|
+
}
|
|
372
393
|
console.warn("Restart replacement is still starting after the readiness window; handing off to it anyway");
|
|
373
394
|
log?.note(`replacement pid ${pid} is still starting after the readiness window; handing off anyway`);
|
|
374
395
|
} else {
|
|
@@ -380,3 +401,30 @@ export async function spawnReplacementStart(
|
|
|
380
401
|
log?.close();
|
|
381
402
|
}
|
|
382
403
|
}
|
|
404
|
+
|
|
405
|
+
/** Exit-observation budgets for {@link terminateReplacement}. */
|
|
406
|
+
export const REPLACEMENT_TERM_WAIT_MS = 3_000;
|
|
407
|
+
export const REPLACEMENT_KILL_WAIT_MS = 2_000;
|
|
408
|
+
|
|
409
|
+
function waitForChildExit(child: ChildProcess, ms: number, sleep: (ms: number) => Promise<void>): Promise<boolean> {
|
|
410
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
|
|
411
|
+
return new Promise<boolean>(resolve => {
|
|
412
|
+
let done = false;
|
|
413
|
+
const finish = (exited: boolean) => { if (!done) { done = true; child.off("exit", onExit); resolve(exited); } };
|
|
414
|
+
const onExit = () => finish(true);
|
|
415
|
+
child.once("exit", onExit);
|
|
416
|
+
void sleep(ms).then(() => finish(child.exitCode !== null || child.signalCode !== null));
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Stop a replacement that never answered: SIGTERM, wait for its exit, then SIGKILL and wait again.
|
|
422
|
+
* True only once the exit was observed; a kill call alone is not proof the port is free.
|
|
423
|
+
*/
|
|
424
|
+
async function terminateReplacement(child: ChildProcess, io: ReplacementStartIo): Promise<boolean> {
|
|
425
|
+
const sleep = io.sleep ?? Bun.sleep;
|
|
426
|
+
try { child.kill("SIGTERM"); } catch { /* exited between the check and the signal */ }
|
|
427
|
+
if (await waitForChildExit(child, io.termWaitMs ?? REPLACEMENT_TERM_WAIT_MS, sleep)) return true;
|
|
428
|
+
try { child.kill("SIGKILL"); } catch { /* exited between the check and the signal */ }
|
|
429
|
+
return waitForChildExit(child, io.killWaitMs ?? REPLACEMENT_KILL_WAIT_MS, sleep);
|
|
430
|
+
}
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
|
|
|
12
12
|
import { safeResponseHeaders } from "./safe-response-headers";
|
|
13
13
|
import type { AudioSocketTarget } from "./audio-dictation";
|
|
14
14
|
import type { CodexWebSocketActivity } from "../codex/websocket-activity";
|
|
15
|
+
import { AGENT_ASSERTION_HEADER } from "../lib/authorization-scheme";
|
|
15
16
|
|
|
16
17
|
export { safeResponseHeaders } from "./safe-response-headers";
|
|
17
18
|
|
|
@@ -131,6 +132,10 @@ export function selectForwardHeaders(
|
|
|
131
132
|
if (codexOverride) {
|
|
132
133
|
selected.set("authorization", `Bearer ${codexOverride.accessToken}`);
|
|
133
134
|
selected.set("chatgpt-account-id", codexOverride.chatgptAccountId);
|
|
135
|
+
} else {
|
|
136
|
+
// Kept for the WS turn pipeline, which drops it again everywhere except first-party Direct.
|
|
137
|
+
const assertion = headers.get(AGENT_ASSERTION_HEADER);
|
|
138
|
+
if (assertion) selected.set(AGENT_ASSERTION_HEADER, assertion);
|
|
134
139
|
}
|
|
135
140
|
return selected;
|
|
136
141
|
}
|
package/src/service/cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
|
|
|
11
11
|
import { withWindowsServiceMutationLock } from "../lib/windows-service-mutation-lock";
|
|
12
12
|
import { maybeShowStarPrompt } from "../cli/star-prompt";
|
|
13
13
|
import { serviceStatusReport } from "./diagnostics";
|
|
14
|
+
import { collectServiceStatusJson } from "./status-json";
|
|
14
15
|
import { assertServiceAuthEnvironment } from "./auth-preflight";
|
|
15
16
|
import { assertServiceEnvironmentMatchesInstall } from "./guards";
|
|
16
17
|
import { resolveServiceListenPort } from "./definition";
|
|
@@ -47,10 +48,14 @@ export function normalizeServiceSubcommand(sub?: string): string {
|
|
|
47
48
|
return sub ?? "install";
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
const SERVICE_JSON_STATUS_ONLY = "--json applies to `ocx service status` only.";
|
|
52
|
+
|
|
50
53
|
export interface ParsedServiceArgs {
|
|
51
54
|
sub: string;
|
|
52
55
|
backend: ServiceBackend | null;
|
|
53
56
|
invalid: string[];
|
|
57
|
+
/** `--json`: accepted for `status` only; planServiceCommand rejects it elsewhere. */
|
|
58
|
+
json: boolean;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export type ServiceInstallationState = "installed" | "absent" | "unknown";
|
|
@@ -140,6 +145,9 @@ export function planServiceCommand(
|
|
|
140
145
|
if (parsed.invalid.length > 0) {
|
|
141
146
|
return { ok: false, message: `Unknown service option: ${parsed.invalid.join(" ")}` };
|
|
142
147
|
}
|
|
148
|
+
if (parsed.json && parsed.sub !== "status") {
|
|
149
|
+
return { ok: false, message: SERVICE_JSON_STATUS_ONLY };
|
|
150
|
+
}
|
|
143
151
|
if (parsed.backend && parsed.sub !== "install") {
|
|
144
152
|
return { ok: false, message: "--native/--scheduler apply to `ocx service install` only; other subcommands use the installed backend." };
|
|
145
153
|
}
|
|
@@ -174,9 +182,11 @@ export function planServiceCommand(
|
|
|
174
182
|
export function parseServiceArgs(args: string[]): ParsedServiceArgs {
|
|
175
183
|
let sub: string | undefined;
|
|
176
184
|
let backend: ServiceBackend | null = null;
|
|
185
|
+
let json = false;
|
|
177
186
|
const invalid: string[] = [];
|
|
178
187
|
for (const arg of args) {
|
|
179
|
-
if (arg === "--
|
|
188
|
+
if (arg === "--json") json = true;
|
|
189
|
+
else if (arg === "--native") {
|
|
180
190
|
if (backend === "scheduler") { invalid.push("--native (conflicts with --scheduler)"); continue; }
|
|
181
191
|
backend = "native";
|
|
182
192
|
}
|
|
@@ -188,7 +198,7 @@ export function parseServiceArgs(args: string[]): ParsedServiceArgs {
|
|
|
188
198
|
else if (sub === undefined) sub = arg;
|
|
189
199
|
else invalid.push(arg);
|
|
190
200
|
}
|
|
191
|
-
return { sub: normalizeServiceSubcommand(sub), backend, invalid };
|
|
201
|
+
return { sub: normalizeServiceSubcommand(sub), backend, invalid, json };
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
/** Remove the service credential only when no client connection can own it. */
|
|
@@ -243,6 +253,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
243
253
|
process.exitCode = 1;
|
|
244
254
|
return;
|
|
245
255
|
}
|
|
256
|
+
// Answered before platformOps() resolves: the JSON status reaches no install, start, stop,
|
|
257
|
+
// repair or migration path, and an unsupported platform still gets a document, not an exit.
|
|
258
|
+
if (command === "status" && parsed.json) {
|
|
259
|
+
console.log(JSON.stringify(await collectServiceStatusJson(), null, 2));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
246
262
|
if (command === "repair" || command === "restart") {
|
|
247
263
|
const verb: ServiceRepairVerb = command === "restart" ? "restart" : "repair";
|
|
248
264
|
assertServiceEnvironmentMatchesInstall();
|
|
@@ -488,6 +504,13 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
488
504
|
};
|
|
489
505
|
|
|
490
506
|
const preliminary = parseServiceArgs(filteredArgs);
|
|
507
|
+
// `claim` parses its own --json. Any other verb is refused here, before the Desktop check
|
|
508
|
+
// and the Windows mutation lock, so a misplaced --json never reaches either.
|
|
509
|
+
if (preliminary.json && preliminary.sub !== "status" && preliminary.sub !== "claim") {
|
|
510
|
+
console.error(SERVICE_JSON_STATUS_ONLY);
|
|
511
|
+
process.exitCode = 1;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
491
514
|
if (preliminary.invalid.length === 0) {
|
|
492
515
|
const refusal = desktopServiceCommandRefusal(preliminary.sub, undefined, supervisionLatch);
|
|
493
516
|
if (refusal) {
|
|
@@ -282,16 +282,30 @@ export function diagnoseService(): ServiceDiagnostic {
|
|
|
282
282
|
const enabled = installed && (() => { try { return sh(`systemctl --user is-enabled ${TASK}`) === "enabled"; } catch { return false; } })();
|
|
283
283
|
const running = installed && (() => { try { return sh(`systemctl --user is-active ${TASK}`) === "active"; } catch { return false; } })();
|
|
284
284
|
const stale = installed && bakedServicePathsDiagnostic() !== null;
|
|
285
|
-
|
|
286
|
-
const summary = !installed ? `not installed (${diagnostics})`
|
|
287
|
-
: stale ? `installed, but stale (systemd user; ${diagnostics})`
|
|
288
|
-
: viable ? `installed, enabled and running (systemd user; ${diagnostics})`
|
|
289
|
-
: `installed, but ${!enabled ? "disabled" : "not running"} (systemd user; ${diagnostics})`;
|
|
290
|
-
return { supported: true, installed, enabled, running, viable, startable: installed && !stale, stale, conflict: false, backend: "systemd", summary };
|
|
285
|
+
return deriveSystemdServiceDiagnostic({ installed, enabled, running, stale, diagnostics });
|
|
291
286
|
}
|
|
292
287
|
return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: `unsupported on ${process.platform}` };
|
|
293
288
|
}
|
|
294
289
|
|
|
290
|
+
export interface SystemdServiceDiagnosticInputs {
|
|
291
|
+
installed: boolean;
|
|
292
|
+
enabled: boolean;
|
|
293
|
+
running: boolean;
|
|
294
|
+
stale: boolean;
|
|
295
|
+
diagnostics: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The systemd user-unit verdict, pure so its states are testable without a user bus. */
|
|
299
|
+
export function deriveSystemdServiceDiagnostic(inputs: SystemdServiceDiagnosticInputs): ServiceDiagnostic {
|
|
300
|
+
const { installed, enabled, running, stale, diagnostics } = inputs;
|
|
301
|
+
const viable = installed && enabled && running && !stale;
|
|
302
|
+
const summary = !installed ? `not installed (${diagnostics})`
|
|
303
|
+
: stale ? `installed, but stale (systemd user; ${diagnostics})`
|
|
304
|
+
: viable ? `installed, enabled and running (systemd user; ${diagnostics})`
|
|
305
|
+
: `installed, but ${!enabled ? "disabled" : "not running"} (systemd user; ${diagnostics})`;
|
|
306
|
+
return { supported: true, installed, enabled, running, viable, startable: installed && !stale, stale, conflict: false, backend: "systemd", summary };
|
|
307
|
+
}
|
|
308
|
+
|
|
295
309
|
export function serviceStatusSummary(): string {
|
|
296
310
|
return diagnoseService().summary;
|
|
297
311
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { diagnoseService, type ServiceDiagnostic } from "./diagnostics";
|
|
2
|
+
import { desktopLoginStatusSummary, diagnoseDesktopStartup } from "./desktop-startup";
|
|
3
|
+
import { confirmServiceServing } from "./health";
|
|
4
|
+
import { ownershipMutationLeaseStatusLine } from "./ownership-mutation-lease.mjs";
|
|
5
|
+
import { LABEL, TASK, serviceLogPath, serviceStatePaths } from "./state";
|
|
6
|
+
import { WINSW_SERVICE_ID } from "../lib/winsw";
|
|
7
|
+
import { runPassiveRead } from "../lib/passive-read";
|
|
8
|
+
|
|
9
|
+
/** The service manager holding the registration; `none` when nothing is registered. */
|
|
10
|
+
export type ServiceStatusBackend = "launchd" | "systemd" | "schtasks" | "winsw" | "none";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `ocx service status --json`. Additive within `schemaVersion: 1`.
|
|
14
|
+
*
|
|
15
|
+
* `running` is the service manager's own verdict — the value `ocx status --json` already
|
|
16
|
+
* reports as `startup.serviceRunning` — and `serving` says separately whether a proxy
|
|
17
|
+
* answers on the installed port, because a loaded job that never bound is the failure the
|
|
18
|
+
* text report exists to show.
|
|
19
|
+
*/
|
|
20
|
+
export interface ServiceStatusJson {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
platform: NodeJS.Platform;
|
|
23
|
+
supported: boolean;
|
|
24
|
+
backend: ServiceStatusBackend;
|
|
25
|
+
label: string | null;
|
|
26
|
+
installed: boolean;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
running: boolean;
|
|
29
|
+
viable: boolean;
|
|
30
|
+
stale: boolean;
|
|
31
|
+
conflict: boolean;
|
|
32
|
+
serving: { probed: boolean; ok: boolean; port: number | null };
|
|
33
|
+
summary: string;
|
|
34
|
+
logPath: string;
|
|
35
|
+
ownershipLease: string | null;
|
|
36
|
+
desktopLogin: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ServiceStatusJsonDeps {
|
|
40
|
+
platform?: NodeJS.Platform;
|
|
41
|
+
diagnose?: () => ServiceDiagnostic;
|
|
42
|
+
serving?: () => Promise<{ ok: boolean; port: number }>;
|
|
43
|
+
logPath?: () => string;
|
|
44
|
+
leaseLine?: () => string | null;
|
|
45
|
+
desktopLogin?: () => string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function serviceStatusBackend(diag: ServiceDiagnostic): ServiceStatusBackend {
|
|
49
|
+
if (!diag.supported || !diag.installed) return "none";
|
|
50
|
+
switch (diag.backend) {
|
|
51
|
+
case "launchd": return "launchd";
|
|
52
|
+
case "systemd": return "systemd";
|
|
53
|
+
case "scheduler": return "schtasks";
|
|
54
|
+
case "native": return "winsw";
|
|
55
|
+
default: return "none";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const BACKEND_LABEL: Record<ServiceStatusBackend, string | null> = {
|
|
60
|
+
launchd: LABEL,
|
|
61
|
+
systemd: `${TASK}.service`,
|
|
62
|
+
schtasks: TASK,
|
|
63
|
+
winsw: WINSW_SERVICE_ID,
|
|
64
|
+
none: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Passive by construction: it performs the reads `ocx service status` already performs
|
|
69
|
+
* (manager diagnosis, the loopback serving probe only when a registration exists, the
|
|
70
|
+
* ownership-lease and Desktop login lines) and nothing else. The caller answers before
|
|
71
|
+
* `platformOps()` is resolved, so no install, start, stop or repair path is reachable.
|
|
72
|
+
* The whole collection runs in a passive-read scope: the serving probe resolves the port
|
|
73
|
+
* through loadConfig(), which outside that scope hardens config modes and backs up a
|
|
74
|
+
* malformed config — writes an inventory read must not make.
|
|
75
|
+
*/
|
|
76
|
+
export function collectServiceStatusJson(deps: ServiceStatusJsonDeps = {}): Promise<ServiceStatusJson> {
|
|
77
|
+
return runPassiveRead(() => collectPassively(deps));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function collectPassively(deps: ServiceStatusJsonDeps): Promise<ServiceStatusJson> {
|
|
81
|
+
const diag = (deps.diagnose ?? diagnoseService)();
|
|
82
|
+
const backend = serviceStatusBackend(diag);
|
|
83
|
+
let serving: ServiceStatusJson["serving"] = { probed: false, ok: false, port: null };
|
|
84
|
+
if (diag.installed) {
|
|
85
|
+
// A probe that throws (config load, port resolution) must not cost the caller the whole
|
|
86
|
+
// document; it is reported as probed, not serving, with no port.
|
|
87
|
+
try {
|
|
88
|
+
const result = await (deps.serving ?? (() => confirmServiceServing({ timeoutMs: 1_500 })))();
|
|
89
|
+
serving = { probed: true, ok: result.ok, port: result.port };
|
|
90
|
+
} catch {
|
|
91
|
+
serving = { probed: true, ok: false, port: null };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const desktopLogin = deps.desktopLogin ?? (() => {
|
|
95
|
+
const desktop = diagnoseDesktopStartup();
|
|
96
|
+
return desktop ? desktopLoginStatusSummary(desktop) : null;
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
platform: deps.platform ?? process.platform,
|
|
101
|
+
supported: diag.supported,
|
|
102
|
+
backend,
|
|
103
|
+
label: BACKEND_LABEL[backend],
|
|
104
|
+
installed: diag.installed,
|
|
105
|
+
enabled: diag.enabled,
|
|
106
|
+
running: diag.running,
|
|
107
|
+
viable: diag.viable,
|
|
108
|
+
stale: diag.stale,
|
|
109
|
+
conflict: diag.conflict,
|
|
110
|
+
serving,
|
|
111
|
+
summary: diag.summary,
|
|
112
|
+
logPath: (deps.logPath ?? serviceLogPath)(),
|
|
113
|
+
ownershipLease: (deps.leaseLine ?? (() => ownershipMutationLeaseStatusLine(serviceStatePaths())))() ?? null,
|
|
114
|
+
desktopLogin: desktopLogin(),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { OcxProviderConfig } from "./provider";
|
|
|
2
2
|
import type { CodexAccount } from "./accounts";
|
|
3
3
|
import type { AnthropicAccountPoolConfig, OcxAccountPoolRotationStrategy } from "./anthropic-account-pool";
|
|
4
4
|
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
5
|
+
import type { OcxFallbackConfig } from "./fallback";
|
|
5
6
|
export type { AnthropicAccountPoolConfig, AnthropicModelRoute, OcxAccountPoolRotationStrategy, OcxAccountPoolQuotaWindow } from "./anthropic-account-pool";
|
|
7
|
+
export type { OcxFallbackConfig, OcxFallbackDeviceConfig, OcxFallbackKind, OcxFallbackSwitches } from "./fallback";
|
|
6
8
|
|
|
7
9
|
/** Public inference API exposure. Responses and Chat Completions are always served. */
|
|
8
10
|
export interface OcxApiSurfacesConfig {
|
|
@@ -1361,6 +1363,11 @@ export interface OcxConfig {
|
|
|
1361
1363
|
tokenGuardian?: OcxTokenGuardianConfig;
|
|
1362
1364
|
/** Additional exact origins allowed for CORS (e.g. HTTPS or chrome-extension://<id>). Loopback origins are always allowed. */
|
|
1363
1365
|
corsAllowOrigins?: string[];
|
|
1366
|
+
/**
|
|
1367
|
+
* OCX-11 fallback off: refuse model or provider fallback per scope, and record per-device local
|
|
1368
|
+
* fallback permission. Absent keeps every chain as it was. See src/routing/fallback-policy.ts.
|
|
1369
|
+
*/
|
|
1370
|
+
fallback?: OcxFallbackConfig;
|
|
1364
1371
|
}
|
|
1365
1372
|
|
|
1366
1373
|
export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window" | "jev";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCX-11 fallback policy (`fallback` in config.json). Every leaf is optional and absence keeps
|
|
3
|
+
* today's behavior: model and provider fallback allowed, local fallback not granted.
|
|
4
|
+
*
|
|
5
|
+
* Contract and precedence: `src/routing/fallback-policy.ts` and `structure/fallback-policy.md`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A fallback kind this policy governs. Intra-pool account or key rotation is not one of them. */
|
|
9
|
+
export type OcxFallbackKind = "model" | "provider";
|
|
10
|
+
|
|
11
|
+
export interface OcxFallbackSwitches {
|
|
12
|
+
/** false refuses a hop to another model of the same provider. */
|
|
13
|
+
model?: boolean;
|
|
14
|
+
/** false refuses a hop to another provider. */
|
|
15
|
+
provider?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OcxFallbackDeviceConfig extends OcxFallbackSwitches {
|
|
19
|
+
/**
|
|
20
|
+
* Permission only: true tells GOAT it may move this device's work to the device's own
|
|
21
|
+
* providers while the Home is unreachable. OpenCodex itself never serves a Child or Hub client
|
|
22
|
+
* from local providers; absent or false means no permission.
|
|
23
|
+
*/
|
|
24
|
+
localFallback?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OcxFallbackConfig extends OcxFallbackSwitches {
|
|
28
|
+
/**
|
|
29
|
+
* Keyed by the provider a hop leaves: the resolved provider of the target that just failed or
|
|
30
|
+
* is known unavailable. In combo [a, b, c] with providers.b.provider false, a to b is allowed and
|
|
31
|
+
* b to c is refused.
|
|
32
|
+
*/
|
|
33
|
+
providers?: Record<string, OcxFallbackSwitches>;
|
|
34
|
+
/** Keyed by an exact public selector: combo/<id>, policy/<id>, provider/model or an alias. */
|
|
35
|
+
routes?: Record<string, OcxFallbackSwitches>;
|
|
36
|
+
/** Keyed by a Remote Link id (lnk_…) or a Hub-client data key id. Read on the Home. */
|
|
37
|
+
devices?: Record<string, OcxFallbackDeviceConfig>;
|
|
38
|
+
}
|
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
|
) {
|