@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The served-by response header, the Home instance id it carries, and whether this build sends it.
|
|
3
|
+
*
|
|
4
|
+
* Every `GET /v1/models` response the server answers itself names the serving Home in
|
|
5
|
+
* `x-opencodex-served-by`, and `GET /healthz` publishes the same value as `instanceId`, so a peer
|
|
6
|
+
* (GOAT's parity check P4) can prove which Home answered without reading logs. A Remote Link Child
|
|
7
|
+
* never answers `/v1/models` itself: its relay copies the Home's response headers, so the value a
|
|
8
|
+
* Child returns is the Home's. A Hub-mode client's Codex calls the Hub's data listener directly, so
|
|
9
|
+
* the Hub stamps its own response.
|
|
10
|
+
*
|
|
11
|
+
* The instance id is the install's `homeId` from `src/quota-pool/identity.ts`: a random
|
|
12
|
+
* `home_<32 hex>` minted once per install, persisted owner-only, and stable across restarts. It is
|
|
13
|
+
* never a hostname and is not derived from one. Until the identity exists (it is created in the
|
|
14
|
+
* background after the listener starts) or when its file is unreadable, the header and the
|
|
15
|
+
* `/healthz` field are omitted; an empty or malformed value is never sent.
|
|
16
|
+
*
|
|
17
|
+
* `SERVED_BY_HEADER_EMITTED` tells `/readyz` to advertise `served-by-header`
|
|
18
|
+
* (`src/remote/features.ts`); `tests/server/readyz-features.test.ts` fails if the advertisement
|
|
19
|
+
* and live responses disagree.
|
|
20
|
+
*/
|
|
21
|
+
import { readQuotaPoolIdentity } from "../quota-pool/identity";
|
|
22
|
+
|
|
23
|
+
export const SERVED_BY_HEADER = "x-opencodex-served-by";
|
|
24
|
+
|
|
25
|
+
/** Whether responses from this build carry {@link SERVED_BY_HEADER}. */
|
|
26
|
+
export const SERVED_BY_HEADER_EMITTED: boolean = true;
|
|
27
|
+
|
|
28
|
+
const INSTANCE_ID = /^home_[0-9a-f]{32}$/;
|
|
29
|
+
|
|
30
|
+
/** This Home's opaque instance id, or undefined while the install identity is unavailable. */
|
|
31
|
+
export function homeInstanceId(): string | undefined {
|
|
32
|
+
let homeId: unknown;
|
|
33
|
+
try { homeId = readQuotaPoolIdentity()?.homeId; } catch { return undefined; }
|
|
34
|
+
return typeof homeId === "string" && INSTANCE_ID.test(homeId) ? homeId : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Stamp {@link SERVED_BY_HEADER} with this Home's instance id; leaves the response alone without one. */
|
|
38
|
+
export function withServedBy(response: Response, instanceId = homeInstanceId()): Response {
|
|
39
|
+
if (!instanceId) return response;
|
|
40
|
+
try {
|
|
41
|
+
response.headers.set(SERVED_BY_HEADER, instanceId);
|
|
42
|
+
return response;
|
|
43
|
+
} catch {
|
|
44
|
+
const headers = new Headers(response.headers);
|
|
45
|
+
headers.set(SERVED_BY_HEADER, instanceId);
|
|
46
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The server's last step for every answered request: `GET /v1/models` names the serving Home,
|
|
52
|
+
* whichever branch produced the response. `pathname` is the dispatch path the router matched.
|
|
53
|
+
*/
|
|
54
|
+
export function stampServedBy(method: string, pathname: string, response: Response): Response {
|
|
55
|
+
if (!response || method !== "GET" || pathname !== "/v1/models") return response;
|
|
56
|
+
return withServedBy(response);
|
|
57
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { OcxAudioContent } from "../types";
|
|
2
|
+
|
|
3
|
+
export type ChatAudioFormat = OcxAudioContent["format"];
|
|
4
|
+
|
|
5
|
+
const MIME_FORMATS: Readonly<Record<string, ChatAudioFormat>> = {
|
|
6
|
+
"audio/mp3": "mp3",
|
|
7
|
+
"audio/mpeg": "mp3",
|
|
8
|
+
"audio/wav": "wav",
|
|
9
|
+
"audio/wave": "wav",
|
|
10
|
+
"audio/x-wav": "wav",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function normalizedFormat(value: unknown): ChatAudioFormat | undefined {
|
|
14
|
+
if (typeof value !== "string") return undefined;
|
|
15
|
+
const format = value.trim().toLowerCase();
|
|
16
|
+
return format === "mp3" || format === "wav" ? format : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validBase64(value: string): boolean {
|
|
20
|
+
return value.length > 0
|
|
21
|
+
&& value.length % 4 === 0
|
|
22
|
+
&& /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse the inline audio shape Codex puts in a Responses content block. Chat Completions
|
|
27
|
+
* needs bare base64 plus `wav`/`mp3`, while Codex supplies an RFC 2397 data URL. A raw
|
|
28
|
+
* base64 value with an explicit supported format is accepted as well; remote URLs are not.
|
|
29
|
+
*/
|
|
30
|
+
export function audioContentFromRaw(audioUrl: unknown, declaredFormat?: unknown): OcxAudioContent | undefined {
|
|
31
|
+
if (typeof audioUrl !== "string" || audioUrl.length === 0) return undefined;
|
|
32
|
+
const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/.exec(audioUrl);
|
|
33
|
+
let data: string;
|
|
34
|
+
let format: ChatAudioFormat | undefined;
|
|
35
|
+
if (dataUrl) {
|
|
36
|
+
const declared = normalizedFormat(declaredFormat);
|
|
37
|
+
const mime = dataUrl[1].trim().toLowerCase();
|
|
38
|
+
format = MIME_FORMATS[mime] ?? (mime === "application/octet-stream" ? declared : undefined);
|
|
39
|
+
data = dataUrl[2].replace(/\s+/g, "");
|
|
40
|
+
} else {
|
|
41
|
+
format = normalizedFormat(declaredFormat);
|
|
42
|
+
data = audioUrl.replace(/\s+/g, "");
|
|
43
|
+
}
|
|
44
|
+
if (!format || !validBase64(data)) return undefined;
|
|
45
|
+
return { type: "audio", data, format, text: `[audio: ${format}]` };
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isInlineDocumentDataUrl } from "./inline-document";
|
|
2
|
+
import { audioContentFromRaw } from "./audio";
|
|
2
3
|
|
|
3
4
|
/** Input kinds for which the normalized request has no lossless content carrier. */
|
|
4
5
|
export type UntranslatedInputMedia = "audio" | "file";
|
|
@@ -10,23 +11,35 @@ function isRecord(value: unknown): value is RecordValue {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Whether the position being scanned has a converter that builds a
|
|
14
|
+
* Whether the position being scanned has a converter that builds a lossless media part.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* User content carries documents and inline audio; developer content carries documents only.
|
|
17
|
+
* Tool output carries inline audio through the Chat adapter's follow-up user message. System and
|
|
18
|
+
* assistant content have no lossless translated media carrier, so exempting an attachment there
|
|
19
|
+
* would turn today's explicit refusal into the silent drop this scanner exists to prevent.
|
|
18
20
|
*/
|
|
19
|
-
type CarrierPosition = "user-content" | "flattened";
|
|
21
|
+
type CarrierPosition = "user-content" | "developer-content" | "tool-output" | "flattened";
|
|
20
22
|
|
|
21
|
-
function mediaKind(value: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
|
|
23
|
+
function mediaKind(value: unknown, position: CarrierPosition, allowAudio: boolean): UntranslatedInputMedia | undefined {
|
|
22
24
|
if (!isRecord(value)) return undefined;
|
|
23
|
-
if (value.type === "input_audio" || value.type === "audio")
|
|
25
|
+
if (value.type === "input_audio" || value.type === "audio") {
|
|
26
|
+
// A Chat content part has a nested input_audio object, while Codex's Responses
|
|
27
|
+
// block uses audio_url. Only the latter is carried by the translated Chat adapter; the
|
|
28
|
+
// Chat-shaped `audio` spelling has no parser path and must never pass the guard.
|
|
29
|
+
return value.type === "input_audio"
|
|
30
|
+
&& allowAudio
|
|
31
|
+
&& (position === "user-content" || position === "tool-output")
|
|
32
|
+
&& audioContentFromRaw(value.audio_url, value.format)
|
|
33
|
+
? undefined
|
|
34
|
+
: "audio";
|
|
35
|
+
}
|
|
24
36
|
if (value.type === "input_file" || value.type === "file" || value.type === "document") {
|
|
25
37
|
// A document that carries its own bytes has a lossless carrier in user content, so refusing
|
|
26
38
|
// it there would reject the very request #5212 exists to preserve. A reference with no
|
|
27
39
|
// payload has none anywhere: translated adapters cannot dereference a file_id or a remote
|
|
28
40
|
// source.
|
|
29
|
-
return position === "user-content"
|
|
41
|
+
return (position === "user-content" || position === "developer-content")
|
|
42
|
+
&& carriesInlineDocumentBytes(value) ? undefined : "file";
|
|
30
43
|
}
|
|
31
44
|
// A file-id-only image is not pixels: translated adapters cannot dereference it.
|
|
32
45
|
if (value.type === "input_image" && typeof value.file_id === "string" && value.file_id.length > 0
|
|
@@ -46,10 +59,10 @@ function carriesInlineDocumentBytes(value: RecordValue): boolean {
|
|
|
46
59
|
&& value.source.data.length > 0;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
function contentMedia(content: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
|
|
62
|
+
function contentMedia(content: unknown, position: CarrierPosition, allowAudio: boolean): UntranslatedInputMedia | undefined {
|
|
50
63
|
if (!Array.isArray(content)) return undefined;
|
|
51
64
|
for (const part of content) {
|
|
52
|
-
const kind = mediaKind(part, position);
|
|
65
|
+
const kind = mediaKind(part, position, allowAudio);
|
|
53
66
|
if (kind) return kind;
|
|
54
67
|
}
|
|
55
68
|
return undefined;
|
|
@@ -60,20 +73,29 @@ function contentMedia(content: unknown, position: CarrierPosition): Untranslated
|
|
|
60
73
|
* arguments, schema properties, or arbitrary nested objects. No payload is copied,
|
|
61
74
|
* decoded, fetched or included in the returned value.
|
|
62
75
|
*/
|
|
63
|
-
export function untranslatedResponsesInputMedia(
|
|
76
|
+
export function untranslatedResponsesInputMedia(
|
|
77
|
+
body: unknown,
|
|
78
|
+
options: { allowAudio?: boolean } = {},
|
|
79
|
+
): UntranslatedInputMedia | undefined {
|
|
64
80
|
if (!isRecord(body) || !Array.isArray(body.input)) return undefined;
|
|
65
81
|
for (const item of body.input) {
|
|
66
82
|
if (!isRecord(item)) continue;
|
|
67
|
-
const direct = mediaKind(item, "flattened");
|
|
83
|
+
const direct = mediaKind(item, "flattened", options.allowAudio === true);
|
|
68
84
|
if (direct) return direct;
|
|
69
85
|
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
70
|
-
const kind = contentMedia(item.output, "
|
|
86
|
+
const kind = contentMedia(item.output, "tool-output", options.allowAudio === true);
|
|
71
87
|
if (kind) return kind;
|
|
72
88
|
} else if (item.type === "message" || item.type === undefined) {
|
|
73
89
|
// `inputContentParts` runs for user and developer messages only; a system message is
|
|
74
90
|
// flattened to text by the parser.
|
|
75
91
|
const role = item.role;
|
|
76
|
-
const kind = contentMedia(
|
|
92
|
+
const kind = contentMedia(
|
|
93
|
+
item.content,
|
|
94
|
+
role === "user" ? "user-content"
|
|
95
|
+
: role === "developer" ? "developer-content"
|
|
96
|
+
: "flattened",
|
|
97
|
+
options.allowAudio === true,
|
|
98
|
+
);
|
|
77
99
|
if (kind) return kind;
|
|
78
100
|
}
|
|
79
101
|
}
|
|
@@ -87,7 +109,7 @@ export function untranslatedChatInputMedia(body: unknown): UntranslatedInputMedi
|
|
|
87
109
|
if (!isRecord(message)) continue;
|
|
88
110
|
// Only the `user` branch of the Chat projection builds content blocks. `system`,
|
|
89
111
|
// `developer`, `assistant` and `tool` all reduce their content to a string.
|
|
90
|
-
const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened");
|
|
112
|
+
const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened", false);
|
|
91
113
|
if (kind) return kind;
|
|
92
114
|
}
|
|
93
115
|
return undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxContentPart, OcxTextContent } from "../types";
|
|
2
|
+
import { audioContentFromRaw } from "./audio";
|
|
2
3
|
import { inlineDocumentFromDataUrl } from "./inline-document";
|
|
3
4
|
|
|
4
5
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
|
@@ -65,23 +66,15 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
65
66
|
const processing = nonEmptyString((block as { processing?: string }).processing);
|
|
66
67
|
if (videoUrl) parts.push({ type: "video", videoUrl, ...(processing ? { processing } : {}) });
|
|
67
68
|
} else if (block.type === "input_audio") {
|
|
68
|
-
// Upstream Codex sends input_audio with an audio_url (codex-rs
|
|
69
|
-
// protocol/src/models.rs). The IR has no audio carrier and no adapter consumes
|
|
70
|
-
// one, so this part used to vanish with no trace at all.
|
|
71
|
-
//
|
|
72
|
-
// This records PRESENCE only and is NOT audio support: never the payload, which
|
|
73
|
-
// is large base64 and would explode the token count, and never the URL, which
|
|
74
|
-
// can carry a signed token. This parser must stay non-throwing — the native
|
|
75
|
-
// Responses passthrough also runs through parseRequest before the adapter
|
|
76
|
-
// forwards _rawBody, so refusing here would regress legitimate raw passthrough.
|
|
77
|
-
//
|
|
78
|
-
// No adapter refuses audio at its wire today: by this point the part is already a
|
|
79
|
-
// text marker, so downstream adapters see text and continue. A typed unsupported-
|
|
80
|
-
// modality carrier that survives to final adapter dispatch is a separate, recorded
|
|
81
|
-
// residual — do not describe this branch as a refusal.
|
|
82
69
|
const b = block as { audio_url?: string; format?: string };
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
// Keep bytes only when they have a Chat-compatible inline form. Unsupported or
|
|
71
|
+
// remote references retain a marker for text-only wires; the final adapter guard
|
|
72
|
+
// refuses them rather than allowing a silent drop.
|
|
73
|
+
const audio = audioContentFromRaw(b.audio_url, b.format);
|
|
74
|
+
if (audio) {
|
|
75
|
+
parts.push(audio);
|
|
76
|
+
} else if (nonEmptyString(b.audio_url)) {
|
|
77
|
+
const format = safeAudioFormat(b.format);
|
|
85
78
|
parts.push({ type: "text", text: format ? `[audio: ${format}]` : "[audio]" });
|
|
86
79
|
}
|
|
87
80
|
} else if (block.type === "input_file") {
|
|
@@ -104,7 +97,7 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
104
97
|
// fabricating a "[file: ...]" marker for an attachment that was never sent.
|
|
105
98
|
}
|
|
106
99
|
}
|
|
107
|
-
// Collapse to a plain string only for a single TEXT part;
|
|
100
|
+
// Collapse to a plain string only for a single TEXT part; media must stay structured.
|
|
108
101
|
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
109
102
|
return parts;
|
|
110
103
|
}
|
|
@@ -129,15 +122,15 @@ export function outputTextOf(blocks: unknown): OcxTextContent[] {
|
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
/**
|
|
132
|
-
* Tool-call output content. Preserves images (e.g. Codex `view_image` returns
|
|
133
|
-
*
|
|
134
|
-
* Never inlines an image_url as text (that would explode the token count).
|
|
125
|
+
* Tool-call output content. Preserves images (e.g. Codex `view_image` returns)
|
|
126
|
+
* and inline audio: returns content parts when either is present, else a plain joined string.
|
|
127
|
+
* Never inlines an image_url or audio payload as text (that would explode the token count).
|
|
135
128
|
*/
|
|
136
129
|
export function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] {
|
|
137
130
|
if (typeof output === "string") return output;
|
|
138
131
|
if (!Array.isArray(output)) return "";
|
|
139
132
|
const parts: OcxContentPart[] = [];
|
|
140
|
-
let
|
|
133
|
+
let hasMedia = false;
|
|
141
134
|
for (const raw of output) {
|
|
142
135
|
if (!isObj(raw)) continue;
|
|
143
136
|
if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") {
|
|
@@ -149,15 +142,17 @@ export function outputToToolResultContent(output: string | unknown[] | undefined
|
|
|
149
142
|
const fileId = nonEmptyString(raw.file_id);
|
|
150
143
|
if (imageUrl) {
|
|
151
144
|
parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) });
|
|
152
|
-
|
|
145
|
+
hasMedia = true;
|
|
153
146
|
} else if (fileId) {
|
|
154
147
|
parts.push({ type: "text", text: `[image: ${fileId}]` });
|
|
155
148
|
}
|
|
156
149
|
} else if (raw.type === "input_audio") {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
const audio = audioContentFromRaw(raw.audio_url, raw.format);
|
|
151
|
+
if (audio) {
|
|
152
|
+
parts.push(audio);
|
|
153
|
+
hasMedia = true;
|
|
154
|
+
} else if (nonEmptyString(raw.audio_url)) {
|
|
155
|
+
const format = safeAudioFormat(raw.format);
|
|
161
156
|
parts.push({ type: "text", text: format ? `[audio: ${format}]` : "[audio]" });
|
|
162
157
|
}
|
|
163
158
|
} else if (raw.type === "encrypted_content") {
|
|
@@ -165,7 +160,7 @@ export function outputToToolResultContent(output: string | unknown[] | undefined
|
|
|
165
160
|
parts.push({ type: "text", text: "[encrypted content omitted]" });
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
|
-
if (!
|
|
163
|
+
if (!hasMedia) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
|
|
169
164
|
return parts;
|
|
170
165
|
}
|
|
171
166
|
|
package/src/responses/parser.ts
CHANGED
|
@@ -329,6 +329,13 @@ export function parseRequest(
|
|
|
329
329
|
continue;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
if (effectiveType === "configuration_update") {
|
|
333
|
+
// Codex 0.164 positional reasoning-effort control. It is not a turn, so a translated route
|
|
334
|
+
// drops it without touching pending reasoning; the native passthrough keeps it verbatim in
|
|
335
|
+
// _rawBody. Mapping its effort onto the routed request is deliberately not done (#6895).
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
332
339
|
if (isCompactionItemType(effectiveType)) {
|
|
333
340
|
// A stored summary from a previous compaction. Decode our ocx1/ocx2 envelopes into plain text so
|
|
334
341
|
// the routed model keeps the compacted context; real OpenAI-encrypted blobs degrade to a note.
|
|
@@ -121,6 +121,18 @@ export function isProviderIssuedThinkingSignature(
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Whether a value has the spelling of a signature Anthropic issued. An allow-list on top of
|
|
126
|
+
* `isProviderIssuedThinkingSignature`: Anthropic's signature is base64, so item ids and the
|
|
127
|
+
* parser's serialized placeholder never pass. Shared by the adapter, which forwards only such a
|
|
128
|
+
* signature, and the Chat `reasoning_details` carrier, which accepts and emits only such a one.
|
|
129
|
+
*/
|
|
130
|
+
export function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
|
|
131
|
+
if (typeof signature !== "string" || signature.length < 16) return false;
|
|
132
|
+
if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(signature)) return false;
|
|
133
|
+
return /^[A-Za-z0-9+/_=-]+$/.test(signature);
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
/** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
|
|
125
137
|
export function decodeReasoningEnvelope(encryptedContent: string, budget?: TranslatorBudget): ReasoningEnvelope | null {
|
|
126
138
|
if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
|
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
* Provider ciphertext and signatures are never included in the portable retained items.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
7
8
|
import { expandRetainedCompactionInput } from "./retained-compaction";
|
|
8
|
-
import { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, opendirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { getConfigDir } from "../config/paths";
|
|
12
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
13
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
10
14
|
import { estimateTokens } from "../lib/token-estimate";
|
|
11
15
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
12
16
|
|
|
13
17
|
export const DEFAULT_REASONING_RETENTION_PERCENT = 20;
|
|
14
18
|
export const DEFAULT_MAX_REASONING_TOKENS = 100_000;
|
|
19
|
+
/** Compiled-in aggregate ceiling for the on-disk reasoning archive directory. */
|
|
20
|
+
export const DEFAULT_ARCHIVE_MAX_BYTES = 512 * 1024 * 1024;
|
|
21
|
+
export const DEFAULT_ARCHIVE_MAX_FILES = 256;
|
|
15
22
|
|
|
16
23
|
export interface ReasoningRetentionOptions {
|
|
17
24
|
/** Directory that receives archived reasoning files. */
|
|
@@ -24,6 +31,10 @@ export interface ReasoningRetentionOptions {
|
|
|
24
31
|
maxTokens?: number;
|
|
25
32
|
/** Clock injection for tests. */
|
|
26
33
|
now?: number;
|
|
34
|
+
/** Aggregate archive byte ceiling. Defaults to 512 MiB; smaller values are a test seam. */
|
|
35
|
+
archiveMaxBytes?: number;
|
|
36
|
+
/** Aggregate archive file-count ceiling. Defaults to 256; smaller values are a test seam. */
|
|
37
|
+
archiveMaxFiles?: number;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
export interface ReasoningArchive {
|
|
@@ -100,6 +111,183 @@ function retentionTokenCap(value: number | undefined): number {
|
|
|
100
111
|
: DEFAULT_MAX_REASONING_TOKENS;
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/** Aggregate-capacity refusal: the archive directory is at or over its compiled-in ceiling. */
|
|
115
|
+
export class ReasoningArchiveQuotaError extends Error {
|
|
116
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
117
|
+
super(message, options);
|
|
118
|
+
this.name = "ReasoningArchiveQuotaError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The sibling archive lock is held by another cooperating writer. */
|
|
123
|
+
export class ReasoningArchiveBusyError extends Error {
|
|
124
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
125
|
+
super(message, options);
|
|
126
|
+
this.name = "ReasoningArchiveBusyError";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Unsafe or uninspectable archive state; the attempt is refused without touching anything. */
|
|
131
|
+
export class ReasoningArchiveError extends Error {
|
|
132
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
133
|
+
super(message, options);
|
|
134
|
+
this.name = "ReasoningArchiveError";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ARCHIVE_SEPARATOR = "\n\n---\n\n";
|
|
139
|
+
const ARCHIVE_SEPARATOR_BYTES = Buffer.byteLength(ARCHIVE_SEPARATOR, "utf8");
|
|
140
|
+
const ARCHIVE_LOCK_SIDECARS = ["-journal", "-wal", "-shm"] as const;
|
|
141
|
+
|
|
142
|
+
function archiveCap(value: number | undefined, fallback: number): number {
|
|
143
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** lstat that treats a vanished path as absent and refuses anything it cannot classify. */
|
|
147
|
+
function archiveLstat(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
148
|
+
try {
|
|
149
|
+
return lstatSync(path);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
152
|
+
throw new ReasoningArchiveError(`reasoning archive path is not inspectable: ${path}`, { cause: error });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertRegularArchiveDirectory(archiveDir: string): void {
|
|
157
|
+
const entry = archiveLstat(archiveDir);
|
|
158
|
+
if (!entry?.isDirectory() || entry.isSymbolicLink()) {
|
|
159
|
+
throw new ReasoningArchiveError(`reasoning archive directory is not a regular directory: ${archiveDir}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertRegularArchiveLockPath(lockPath: string): void {
|
|
164
|
+
const entry = archiveLstat(lockPath);
|
|
165
|
+
if (entry !== undefined && (!entry.isFile() || entry.isSymbolicLink())) {
|
|
166
|
+
throw new ReasoningArchiveError(`reasoning archive lock path is not a regular file: ${lockPath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Exact UTF-8 byte total of the would-be archive payload, computed before any joined string exists. */
|
|
171
|
+
function archivePayloadBytes(texts: string[]): number {
|
|
172
|
+
let bytes = texts.length > 0 ? (texts.length - 1) * ARCHIVE_SEPARATOR_BYTES : 0;
|
|
173
|
+
for (const text of texts) {
|
|
174
|
+
const next = bytes + Buffer.byteLength(text, "utf8");
|
|
175
|
+
if (!Number.isSafeInteger(next)) {
|
|
176
|
+
throw new ReasoningArchiveError("reasoning archive payload byte size is not representable");
|
|
177
|
+
}
|
|
178
|
+
bytes = next;
|
|
179
|
+
}
|
|
180
|
+
return bytes;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ArchiveUsage {
|
|
184
|
+
files: number;
|
|
185
|
+
bytes: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Non-recursive accounting of every committed, remnant and foreign file. Anything that is not a
|
|
190
|
+
* plain regular file refuses closed; an entry vanishing mid-scan is already gone and skipped.
|
|
191
|
+
*/
|
|
192
|
+
function scanArchiveUsage(archiveDir: string, maxBytes: number, maxFiles: number): ArchiveUsage {
|
|
193
|
+
let directory: ReturnType<typeof opendirSync> | undefined;
|
|
194
|
+
let files = 0;
|
|
195
|
+
let bytes = 0;
|
|
196
|
+
let inspected = 0;
|
|
197
|
+
let failure: ReasoningArchiveError | undefined;
|
|
198
|
+
try {
|
|
199
|
+
// One entry per read: a legacy over-capacity directory must not first become an array
|
|
200
|
+
// or an unbounded read-ahead buffer on this synchronous request path.
|
|
201
|
+
directory = opendirSync(archiveDir, { bufferSize: 1 });
|
|
202
|
+
let entry;
|
|
203
|
+
while ((entry = directory.readSync()) !== null) {
|
|
204
|
+
inspected += 1;
|
|
205
|
+
if (!entry.isFile()) {
|
|
206
|
+
throw new ReasoningArchiveError(`reasoning archive entry is not a regular file: ${entry.name}`);
|
|
207
|
+
}
|
|
208
|
+
const stat = archiveLstat(join(archiveDir, entry.name));
|
|
209
|
+
if (stat === undefined) {
|
|
210
|
+
if (inspected >= maxFiles) throw new ReasoningArchiveError("reasoning archive scan changed beyond its inspection limit");
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
214
|
+
throw new ReasoningArchiveError(`reasoning archive entry is not a regular file: ${entry.name}`);
|
|
215
|
+
}
|
|
216
|
+
files += 1;
|
|
217
|
+
bytes += Number(stat.size);
|
|
218
|
+
if (files >= maxFiles || bytes > maxBytes) break; // refusal is already certain
|
|
219
|
+
if (inspected >= maxFiles) throw new ReasoningArchiveError("reasoning archive scan changed beyond its inspection limit");
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
failure = error instanceof ReasoningArchiveError ? error
|
|
223
|
+
: new ReasoningArchiveError("reasoning archive directory cannot be scanned", { cause: error });
|
|
224
|
+
} finally {
|
|
225
|
+
try { directory?.closeSync(); } catch (error) {
|
|
226
|
+
failure ??= new ReasoningArchiveError("reasoning archive directory could not be closed", { cause: error });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (failure !== undefined) throw failure;
|
|
230
|
+
return { files, bytes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isSqliteBusy(error: unknown): boolean {
|
|
234
|
+
const code = error && typeof error === "object" && "code" in error
|
|
235
|
+
? String((error as { code?: unknown }).code)
|
|
236
|
+
: "";
|
|
237
|
+
const message = error instanceof Error ? error.message : "";
|
|
238
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || /database (?:is|table is) locked/i.test(message);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Serialize usage scan, exclusive create, write and close for one physical archive directory
|
|
243
|
+
* across cooperating processes. The sibling SQLite write transaction is the lock: the OS
|
|
244
|
+
* releases it on process exit, so no stale-owner reclamation is needed. busy_timeout=0 keeps
|
|
245
|
+
* the synchronous request path fail-fast instead of freezing the event loop.
|
|
246
|
+
*/
|
|
247
|
+
function withArchiveQuotaLockSync<T>(archiveDir: string, fn: () => T): T {
|
|
248
|
+
const normalized = archiveDir.replace(/[/\\]+$/, "");
|
|
249
|
+
const base = basename(normalized);
|
|
250
|
+
if (!base || base === "." || base === "..") {
|
|
251
|
+
throw new ReasoningArchiveError(`reasoning archive directory is not a usable path: ${archiveDir}`);
|
|
252
|
+
}
|
|
253
|
+
const parent = dirname(normalized);
|
|
254
|
+
assertNotRealHomeUnderTest(parent);
|
|
255
|
+
const lockPath = `${normalized}.lock`;
|
|
256
|
+
// The lock lives beside the archive inside the config home; foreign parents need no manifest entry.
|
|
257
|
+
if (parent === getConfigDir()) {
|
|
258
|
+
recordOwnedConfigPath(parent, lockPath);
|
|
259
|
+
for (const suffix of ARCHIVE_LOCK_SIDECARS) recordOwnedConfigPath(parent, `${lockPath}${suffix}`);
|
|
260
|
+
}
|
|
261
|
+
assertRegularArchiveLockPath(lockPath);
|
|
262
|
+
let database: Database;
|
|
263
|
+
try {
|
|
264
|
+
database = new Database(lockPath, { create: true });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new ReasoningArchiveError(`reasoning archive lock could not be opened: ${lockPath}`, { cause: error });
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
try { chmodSync(lockPath, 0o600); } catch { /* platform may ignore chmod */ }
|
|
270
|
+
assertRegularArchiveLockPath(lockPath);
|
|
271
|
+
database.exec("PRAGMA locking_mode = NORMAL; PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
|
272
|
+
} catch (error) {
|
|
273
|
+
try { database.close(); } catch { /* acquisition already failed */ }
|
|
274
|
+
if (error instanceof ReasoningArchiveError) throw error;
|
|
275
|
+
if (isSqliteBusy(error)) {
|
|
276
|
+
throw new ReasoningArchiveBusyError(
|
|
277
|
+
`reasoning archive lock is held by another writer: ${lockPath}`,
|
|
278
|
+
{ cause: error },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
throw new ReasoningArchiveError(`reasoning archive lock could not be acquired: ${lockPath}`, { cause: error });
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return fn();
|
|
285
|
+
} finally {
|
|
286
|
+
try { database.exec("ROLLBACK"); } catch { /* close below still releases the OS lock */ }
|
|
287
|
+
try { database.close(); } catch { /* the OS lock is released with the handle */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
103
291
|
/** Separate reasoning locally before dispatch; unknown windows enforce the absolute cap. */
|
|
104
292
|
export function applyReasoningRetention(
|
|
105
293
|
input: unknown,
|
|
@@ -130,17 +318,36 @@ export function applyReasoningRetention(
|
|
|
130
318
|
content: [{ type: "input_text", text }] })),
|
|
131
319
|
};
|
|
132
320
|
|
|
133
|
-
|
|
321
|
+
const archiveBytes = archiveCap(options.archiveMaxBytes, DEFAULT_ARCHIVE_MAX_BYTES);
|
|
322
|
+
const archiveFiles = archiveCap(options.archiveMaxFiles, DEFAULT_ARCHIVE_MAX_FILES);
|
|
323
|
+
const incomingBytes = archivePayloadBytes(texts);
|
|
134
324
|
const stamp = new Date(options.now ?? Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
135
325
|
const path = join(options.archiveDir, "reasoning-" + stamp + "-" + Math.random().toString(36).slice(2, 8) + ".md");
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
326
|
+
try {
|
|
327
|
+
mkdirSync(options.archiveDir, { recursive: true, mode: 0o700 });
|
|
328
|
+
assertRegularArchiveDirectory(options.archiveDir);
|
|
329
|
+
withArchiveQuotaLockSync(options.archiveDir, () => {
|
|
330
|
+
assertRegularArchiveDirectory(options.archiveDir);
|
|
331
|
+
const usage = scanArchiveUsage(options.archiveDir, archiveBytes, archiveFiles);
|
|
332
|
+
if (usage.files >= archiveFiles || incomingBytes > archiveBytes - usage.bytes) {
|
|
333
|
+
throw new ReasoningArchiveQuotaError(
|
|
334
|
+
`reasoning archive is at capacity: ${usage.files} file(s), ${usage.bytes} byte(s) under ${options.archiveDir}; delete older archives to archive more`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
// Only a successful exclusive open grants ownership; an occupied path is never removed.
|
|
338
|
+
const fd = openSync(path, "wx", 0o600);
|
|
339
|
+
let failure: unknown;
|
|
340
|
+
try { writeFileSync(fd, texts.join(ARCHIVE_SEPARATOR), "utf-8"); } catch (error) { failure = error; }
|
|
341
|
+
try { closeSync(fd); } catch (error) { failure ??= error; }
|
|
342
|
+
if (failure !== undefined) {
|
|
343
|
+
try { rmSync(path); } catch { /* preserve the original I/O error; cleanup is best-effort */ }
|
|
344
|
+
throw failure;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof ReasoningArchiveQuotaError || error instanceof ReasoningArchiveBusyError
|
|
349
|
+
|| error instanceof ReasoningArchiveError) throw error;
|
|
350
|
+
throw new ReasoningArchiveError("reasoning archive could not be written", { cause: error });
|
|
144
351
|
}
|
|
145
352
|
|
|
146
353
|
const limitDescription = windowKnown
|
package/src/responses/schema.ts
CHANGED
|
@@ -27,10 +27,9 @@ const inputFileBlockSchema = z.object({
|
|
|
27
27
|
file_data: z.string().optional(),
|
|
28
28
|
});
|
|
29
29
|
// codex-rs protocol/src/models.rs sends audio as input_audio with an audio_url, in
|
|
30
|
-
// both user content and tool output.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// signal reaching final adapter dispatch remains a recorded residual.
|
|
30
|
+
// both user content and tool output. The translated IR preserves supported inline
|
|
31
|
+
// WAV/MP3 bytes and the Chat adapter sends them as input_audio; other wires and
|
|
32
|
+
// unsupported formats are refused at the final translated-adapter boundary.
|
|
34
33
|
const inputAudioBlockSchema = z.object({
|
|
35
34
|
type: z.literal("input_audio"),
|
|
36
35
|
audio_url: z.string().min(1),
|
|
@@ -91,9 +91,18 @@ export function rewriteReplayedWebSearchCells(input: readonly unknown[]): Rewrit
|
|
|
91
91
|
if (!input.some(item => isObj(item) && item.type === "web_search_call")) return undefined;
|
|
92
92
|
// A synthetic call id must not collide with any client call id, nor with another cell's.
|
|
93
93
|
const taken = new Set(input.flatMap(item => isObj(item) && typeof item.call_id === "string" ? [item.call_id] : []));
|
|
94
|
+
// First suffix each base has not settled yet; taken stays the collision check on every candidate,
|
|
95
|
+
// so seeded ids and ids handed out under another base still block the smallest free suffix.
|
|
96
|
+
const nextSuffix = new Map<string, number>();
|
|
94
97
|
const allocate = (base: string): string => {
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
if (!taken.has(base)) {
|
|
99
|
+
taken.add(base);
|
|
100
|
+
return base;
|
|
101
|
+
}
|
|
102
|
+
let n = nextSuffix.get(base) ?? 1;
|
|
103
|
+
let id = `${base}_${n}`;
|
|
104
|
+
while (taken.has(id)) id = `${base}_${++n}`;
|
|
105
|
+
nextSuffix.set(base, n + 1);
|
|
97
106
|
taken.add(id);
|
|
98
107
|
return id;
|
|
99
108
|
};
|