@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GPT-5.6 retired the legacy 24-hour retention field, and the ChatGPT backend 400s the whole
|
|
5
|
+
* request when that field is present (issue #2092).
|
|
6
|
+
*
|
|
7
|
+
* The retired field is NOT translated to the replacement: 5.6 carries a different TTL contract,
|
|
8
|
+
* and implicit caching still applies when the caller sent no replacement options. Inventing a
|
|
9
|
+
* value here would silently change a caching decision the caller never made.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately narrow on both axes, because a wider strip is a behavior change rather than a fix:
|
|
12
|
+
* only the gpt-5.6 family (an older model may still honor the field), and only on the canonical
|
|
13
|
+
* ChatGPT backend, which is the deployment that rejects it. Matching is exact-or-dashed-prefix so
|
|
14
|
+
* a future `gpt-5.60` is not swept up by a bare `startsWith`.
|
|
15
|
+
*/
|
|
16
|
+
export function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): unknown {
|
|
17
|
+
if (!isPlainObject(body)) return body;
|
|
18
|
+
if (typeof modelId !== "string") return body;
|
|
19
|
+
if (modelId !== "gpt-5.6" && !modelId.startsWith("gpt-5.6-")) return body;
|
|
20
|
+
if (!Object.hasOwn(body, "prompt_cache_retention")) return body;
|
|
21
|
+
const { prompt_cache_retention: _retention, ...rest } = body;
|
|
22
|
+
return rest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Public Responses clients can send `prompt_cache_options`, but the canonical ChatGPT Codex
|
|
27
|
+
* backend rejects the top-level field before inference (issue #2765). Custom forward gateways and
|
|
28
|
+
* API-key Responses providers own different wire contracts, so the caller applies this only after
|
|
29
|
+
* the canonical destination predicate succeeds.
|
|
30
|
+
*/
|
|
31
|
+
export function stripCanonicalForwardPromptCacheOptions(body: unknown): unknown {
|
|
32
|
+
if (!isPlainObject(body) || !Object.hasOwn(body, "prompt_cache_options")) return body;
|
|
33
|
+
const { prompt_cache_options: _options, ...rest } = body;
|
|
34
|
+
return rest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const POSIT_CACHE_MARKER_MAX_DEPTH = 64;
|
|
38
|
+
const POSIT_CACHE_MARKER_MAX_NODES = 100_000;
|
|
39
|
+
|
|
40
|
+
type PromptCacheMarkerRewrite = {
|
|
41
|
+
value: unknown;
|
|
42
|
+
changed: boolean;
|
|
43
|
+
complete: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove Posit/Anthropic-style prompt-cache markers without trusting request nesting. The walk
|
|
48
|
+
* aborts atomically when its depth or node budget is exceeded, so a hostile extension object can
|
|
49
|
+
* neither overflow the stack nor receive a partially rewritten subtree.
|
|
50
|
+
*/
|
|
51
|
+
export function stripPromptCacheBreakpoints(
|
|
52
|
+
value: unknown,
|
|
53
|
+
state: { nodes: number },
|
|
54
|
+
depth = 0,
|
|
55
|
+
): PromptCacheMarkerRewrite {
|
|
56
|
+
state.nodes += 1;
|
|
57
|
+
if (depth > POSIT_CACHE_MARKER_MAX_DEPTH || state.nodes > POSIT_CACHE_MARKER_MAX_NODES) {
|
|
58
|
+
return { value, changed: false, complete: false };
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
let changed = false;
|
|
62
|
+
const next: unknown[] = [];
|
|
63
|
+
for (const entry of value) {
|
|
64
|
+
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
|
|
65
|
+
if (!rewritten.complete) return { value, changed: false, complete: false };
|
|
66
|
+
changed ||= rewritten.changed;
|
|
67
|
+
next.push(rewritten.value);
|
|
68
|
+
}
|
|
69
|
+
return { value: changed ? next : value, changed, complete: true };
|
|
70
|
+
}
|
|
71
|
+
if (!isPlainObject(value)) return { value, changed: false, complete: true };
|
|
72
|
+
|
|
73
|
+
let changed = Object.hasOwn(value, "prompt_cache_breakpoint");
|
|
74
|
+
const next: Record<string, unknown> = {};
|
|
75
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
76
|
+
if (key === "prompt_cache_breakpoint") continue;
|
|
77
|
+
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
|
|
78
|
+
if (!rewritten.complete) return { value, changed: false, complete: false };
|
|
79
|
+
changed ||= rewritten.changed;
|
|
80
|
+
next[key] = rewritten.value;
|
|
81
|
+
}
|
|
82
|
+
return { value: changed ? next : value, changed, complete: true };
|
|
83
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
|
|
2
|
+
import { catalogModelSupportsReasoningSummaries } from "../../codex/catalog";
|
|
3
|
+
import { OCX_REASONING_PREFIX } from "../../responses/reasoning-envelope";
|
|
4
|
+
import { configuredReasoningEfforts, mapReasoningEffort, modelRecordValue } from "../../reasoning-effort";
|
|
5
|
+
import { isPlainObject } from "./internal";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
|
|
9
|
+
* native `encrypted_content` guarantees only that blob value: `status` is always removed;
|
|
10
|
+
* proxy-owned `ocxr1:` envelopes are always removed; and native blobs are removed when the caller
|
|
11
|
+
* requests stripping after a route-identity change or opaque-blob recovery. On routed/non-OpenAI
|
|
12
|
+
* destinations, a present non-array `content` field is omitted. Otherwise non-empty array content
|
|
13
|
+
* is blanked unless raw reasoning preservation is enabled; removing an `ocxr1:` envelope selects
|
|
14
|
+
* the same blanking path when non-array omission is not active.
|
|
15
|
+
*
|
|
16
|
+
* A reasoning item that arrives with no `summary` key at all also gets an empty one. The field is
|
|
17
|
+
* required on a reasoning input item by the Responses API — a missing one is refused with
|
|
18
|
+
* `Missing required parameter: 'input[N].summary'` before inference — while
|
|
19
|
+
* responsesRequestSchema marks it optional, so such an item passes every local gate and fails only
|
|
20
|
+
* on the wire. This is not gated on the destination, because it reshapes nothing a canonical
|
|
21
|
+
* backend issued: every reasoning item Codex and this proxy emit already carries `summary`, so an
|
|
22
|
+
* item missing the key came from a translated ingress (`/v1/chat/completions`, `/v1/messages`) or
|
|
23
|
+
* a third-party client, and injecting the empty array is the whole shape it was missing.
|
|
24
|
+
*/
|
|
25
|
+
export function sanitizeReasoningInputContent(
|
|
26
|
+
body: unknown,
|
|
27
|
+
opts?: {
|
|
28
|
+
preserveRawReasoningContent?: boolean;
|
|
29
|
+
dropNullContentChannel?: boolean;
|
|
30
|
+
stripEncryptedContent?: boolean;
|
|
31
|
+
},
|
|
32
|
+
): unknown {
|
|
33
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
34
|
+
const raw = body as Record<string, unknown>;
|
|
35
|
+
if (!Array.isArray(raw.input)) return body;
|
|
36
|
+
|
|
37
|
+
let changed = false;
|
|
38
|
+
const input = raw.input.map(item => {
|
|
39
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
40
|
+
const rec = item as Record<string, unknown>;
|
|
41
|
+
if (rec.type !== "reasoning") return item;
|
|
42
|
+
const hasRawContent = Array.isArray(rec.content) && rec.content.length > 0;
|
|
43
|
+
// ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
|
|
44
|
+
// backend cannot decrypt them and would reject the request. Strip regardless of content shape.
|
|
45
|
+
const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
|
|
46
|
+
const hasOutputStatus = Object.prototype.hasOwnProperty.call(rec, "status");
|
|
47
|
+
const hasEncryptedContent = Object.prototype.hasOwnProperty.call(rec, "encrypted_content");
|
|
48
|
+
const missingSummary = !Object.prototype.hasOwnProperty.call(rec, "summary");
|
|
49
|
+
const stripEncryptedContent = hasOcxEnvelope
|
|
50
|
+
|| (opts?.stripEncryptedContent === true && hasEncryptedContent);
|
|
51
|
+
// Codex serializes an absent reasoning content channel as `"content": null`. The field is
|
|
52
|
+
// optional and null carries nothing, but a strict gateway rejects the item on its declared type
|
|
53
|
+
// — xAI answers `Could not decode the compaction blob`, naming the sibling `encrypted_content`
|
|
54
|
+
// rather than the field it actually refused, which is why this reads as a blob failure. Drop the
|
|
55
|
+
// key so the item matches the shape the upstream issued.
|
|
56
|
+
//
|
|
57
|
+
// Gated to routed destinations. An OpenAI-operated backend rejects a blob-bearing item when its
|
|
58
|
+
// null `content` channel is deleted (`The encrypted content ... could not be verified`); that
|
|
59
|
+
// live result establishes this channel constraint, not whole-item shape preservation. The gate
|
|
60
|
+
// is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its own
|
|
61
|
+
// blob without the null channel. This is independent of the output-only status removal below.
|
|
62
|
+
const dropNullContentChannel = opts?.dropNullContentChannel === true
|
|
63
|
+
&& "content" in rec && !Array.isArray(rec.content);
|
|
64
|
+
// `status` is output-only. Measured OpenAI reasoning items never contain it, and Grok accepts
|
|
65
|
+
// its own encrypted_content with status removed. Keeping a foreign status beside a retained
|
|
66
|
+
// blob makes OpenAI reject the field before blob validation, starving the provenance recovery
|
|
67
|
+
// of the opaque-blob error it needs. Content blanking remains the separate pre-existing rule.
|
|
68
|
+
const stripOutputStatus = hasOutputStatus;
|
|
69
|
+
const blankContent = !dropNullContentChannel
|
|
70
|
+
&& !opts?.preserveRawReasoningContent
|
|
71
|
+
&& (hasRawContent || hasOcxEnvelope);
|
|
72
|
+
if (!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel && !missingSummary) {
|
|
73
|
+
return item;
|
|
74
|
+
}
|
|
75
|
+
changed = true;
|
|
76
|
+
const next: Record<string, unknown> = { ...rec };
|
|
77
|
+
if (missingSummary) next.summary = [];
|
|
78
|
+
if (dropNullContentChannel) delete next.content;
|
|
79
|
+
if (stripOutputStatus) delete next.status;
|
|
80
|
+
if (stripEncryptedContent) delete next.encrypted_content;
|
|
81
|
+
// Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
|
|
82
|
+
// native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
|
|
83
|
+
// `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
|
|
84
|
+
// DeepSeek's Responses API instead ACCEPTS plaintext reasoning replay (its compatibility
|
|
85
|
+
// guide merges reasoning items into the adjacent assistant message), so providers flagged
|
|
86
|
+
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
87
|
+
// continuations after tool calls (issue #875 family).
|
|
88
|
+
if (blankContent) next.content = [];
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return changed ? { ...raw, input } : body;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function stripUnsupportedReasoningSummaryDelivery(body: unknown, modelId: string): unknown {
|
|
96
|
+
if (catalogModelSupportsReasoningSummaries(modelId) !== false) return body;
|
|
97
|
+
if (!isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
|
|
98
|
+
if (!("reasoning_summary_delivery" in body.stream_options)) return body;
|
|
99
|
+
|
|
100
|
+
const streamOptions = { ...body.stream_options };
|
|
101
|
+
delete streamOptions.reasoning_summary_delivery;
|
|
102
|
+
const next = { ...body };
|
|
103
|
+
if (Object.keys(streamOptions).length > 0) next.stream_options = streamOptions;
|
|
104
|
+
else delete next.stream_options;
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A false model capability prevents Codex from emitting summary fields after the catalog refresh.
|
|
110
|
+
* Strip them here as well so an already-running client with a stale catalog cannot keep sending an
|
|
111
|
+
* upstream-rejected `reasoning_summary_delivery` value (issue #323).
|
|
112
|
+
*/
|
|
113
|
+
export function stripDisabledReasoningSummaries(
|
|
114
|
+
body: unknown,
|
|
115
|
+
provider: OcxProviderConfig,
|
|
116
|
+
modelId: string,
|
|
117
|
+
): unknown {
|
|
118
|
+
if (modelRecordValue(provider.modelSupportsReasoningSummaries, modelId) !== false || !isPlainObject(body)) {
|
|
119
|
+
return body;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let changed = false;
|
|
123
|
+
let streamOptions = body.stream_options;
|
|
124
|
+
if (isPlainObject(streamOptions) && Object.hasOwn(streamOptions, "reasoning_summary_delivery")) {
|
|
125
|
+
const { reasoning_summary_delivery: _delivery, ...rest } = streamOptions;
|
|
126
|
+
streamOptions = rest;
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let reasoning = body.reasoning;
|
|
131
|
+
if (isPlainObject(reasoning)) {
|
|
132
|
+
const { summary: _summary, generate_summary: _generateSummary, ...rest } = reasoning;
|
|
133
|
+
if (_summary !== undefined || _generateSummary !== undefined) {
|
|
134
|
+
reasoning = rest;
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!changed) return body;
|
|
140
|
+
return {
|
|
141
|
+
...body,
|
|
142
|
+
...(isPlainObject(streamOptions) && Object.keys(streamOptions).length > 0
|
|
143
|
+
? { stream_options: streamOptions }
|
|
144
|
+
: { stream_options: undefined }),
|
|
145
|
+
...(isPlainObject(reasoning) && Object.keys(reasoning).length > 0
|
|
146
|
+
? { reasoning }
|
|
147
|
+
: { reasoning: undefined }),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hide a no-op Responses verbosity control from the wire as well as the catalog. This runs at
|
|
153
|
+
* final serialization so a stale catalog or direct caller cannot bypass the capability. Other
|
|
154
|
+
* `text` settings (notably structured-output `format`) remain untouched.
|
|
155
|
+
*/
|
|
156
|
+
export function stripDisabledVerbosity(
|
|
157
|
+
body: unknown,
|
|
158
|
+
provider: OcxProviderConfig,
|
|
159
|
+
modelId: string,
|
|
160
|
+
): unknown {
|
|
161
|
+
if (modelRecordValue(provider.modelSupportsVerbosity, modelId) !== false || !isPlainObject(body)) {
|
|
162
|
+
return body;
|
|
163
|
+
}
|
|
164
|
+
if (!isPlainObject(body.text) || !Object.hasOwn(body.text, "verbosity")) return body;
|
|
165
|
+
const { verbosity: _verbosity, ...rest } = body.text;
|
|
166
|
+
return {
|
|
167
|
+
...body,
|
|
168
|
+
...(Object.keys(rest).length > 0 ? { text: rest } : { text: undefined }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
|
|
174
|
+
* did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
|
|
175
|
+
*/
|
|
176
|
+
export function normalizeConfiguredReasoningSummaryDelivery(
|
|
177
|
+
body: unknown,
|
|
178
|
+
provider: OcxProviderConfig,
|
|
179
|
+
modelId: string,
|
|
180
|
+
): unknown {
|
|
181
|
+
const delivery = modelRecordValue(provider.modelReasoningSummaryDelivery, modelId);
|
|
182
|
+
if (delivery === undefined || !isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
|
|
183
|
+
if (!Object.hasOwn(body.stream_options, "reasoning_summary_delivery")) return body;
|
|
184
|
+
if (body.stream_options.reasoning_summary_delivery === delivery) return body;
|
|
185
|
+
return {
|
|
186
|
+
...body,
|
|
187
|
+
stream_options: {
|
|
188
|
+
...body.stream_options,
|
|
189
|
+
reasoning_summary_delivery: delivery,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Apply the routed provider's real effort ladder to an existing Responses reasoning field.
|
|
196
|
+
* Native forward requests keep the server-owned native clamp; unknown third-party ladders stay
|
|
197
|
+
* byte-equivalent instead of acquiring a policy from this adapter.
|
|
198
|
+
*/
|
|
199
|
+
export function mapRoutedResponsesReasoningEffort(
|
|
200
|
+
body: unknown,
|
|
201
|
+
provider: OcxProviderConfig,
|
|
202
|
+
modelId: string,
|
|
203
|
+
): unknown {
|
|
204
|
+
if (provider.authMode === "forward") return body;
|
|
205
|
+
if (configuredReasoningEfforts(provider, modelId) === undefined) return body;
|
|
206
|
+
if (!isPlainObject(body) || !isPlainObject(body.reasoning)) return body;
|
|
207
|
+
const declaredEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
208
|
+
// An explicitly empty ladder means no effort control, not no reasoning output.
|
|
209
|
+
// Omit only effort so the upstream default applies; unknown/non-rankable ladders stay untouched.
|
|
210
|
+
if (declaredEfforts?.length === 0 && Object.hasOwn(body.reasoning, "effort")) {
|
|
211
|
+
const { effort: _effort, ...reasoning } = body.reasoning;
|
|
212
|
+
return { ...body, reasoning: Object.keys(reasoning).length > 0 ? reasoning : undefined };
|
|
213
|
+
}
|
|
214
|
+
const requested = body.reasoning.effort;
|
|
215
|
+
if (typeof requested !== "string") return body;
|
|
216
|
+
|
|
217
|
+
const mapped = mapReasoningEffort(provider, modelId, requested);
|
|
218
|
+
if (!mapped || mapped === requested) return body;
|
|
219
|
+
return { ...body, reasoning: { ...body.reasoning, effort: mapped } };
|
|
220
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
|
|
2
|
+
import { isPlainObject } from "./internal";
|
|
3
|
+
import { activateDeferredTool } from "./tool-schema";
|
|
4
|
+
import { stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
5
|
+
|
|
6
|
+
export function stripInvalidItemIds(body: unknown): unknown {
|
|
7
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
8
|
+
|
|
9
|
+
const validPrefixes: Record<string, string> = {
|
|
10
|
+
message: "msg_",
|
|
11
|
+
agent_message: "amsg_",
|
|
12
|
+
reasoning: "rs_",
|
|
13
|
+
function_call: "fc_",
|
|
14
|
+
custom_tool_call: "ctc_",
|
|
15
|
+
tool_search_call: "tsc_",
|
|
16
|
+
web_search_call: "ws_",
|
|
17
|
+
};
|
|
18
|
+
let changed = false;
|
|
19
|
+
const input = body.input.map(item => {
|
|
20
|
+
if (!isPlainObject(item) || typeof item.type !== "string") return item;
|
|
21
|
+
const validPrefix = validPrefixes[item.type];
|
|
22
|
+
if (!validPrefix) return item;
|
|
23
|
+
if (typeof item.id === "string" && item.id.startsWith(validPrefix)) return item;
|
|
24
|
+
if (!("id" in item)) return item;
|
|
25
|
+
changed = true;
|
|
26
|
+
const next = { ...item };
|
|
27
|
+
delete next.id;
|
|
28
|
+
return next;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return changed ? { ...body, input } : body;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Codex-private tool fields that only the ChatGPT backend understands.
|
|
36
|
+
*
|
|
37
|
+
* A third-party Responses gateway validates its schema and rejects the whole request before
|
|
38
|
+
* inference — xAI answers `Argument not supported: external_web_access` — so these are removed at
|
|
39
|
+
* the noncanonical boundary while the tool and every public option stay.
|
|
40
|
+
*
|
|
41
|
+
* Keep this a table. Each private bit Codex attaches has so far arrived as its own bespoke strip
|
|
42
|
+
* with its own traversal, and the traversals disagreed about which containers they covered; a new
|
|
43
|
+
* one should be a row here instead. `toolTypes` omitted means the field is private on any tool.
|
|
44
|
+
*/
|
|
45
|
+
const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: ReadonlySet<string>; capabilityGated?: boolean }[] = [
|
|
46
|
+
// ChatGPT's browsing policy bit. The public hosted tool is enabled by its presence alone.
|
|
47
|
+
// OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
|
|
48
|
+
// it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
|
|
49
|
+
// === false), matching stripOpenAiOnlyWebSearchFields; see
|
|
50
|
+
// tests/responses/responses-routed-web-search-fields.test.ts.
|
|
51
|
+
{ field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
|
|
52
|
+
// Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
|
|
53
|
+
// already loaded, so a still-deferred declaration — including one promoted out of a namespace
|
|
54
|
+
// group — otherwise reaches the wire carrying it.
|
|
55
|
+
{ field: "defer_loading" },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function stripCanonicalOnlyToolFields(body: unknown, includeCapabilityGated: boolean): unknown {
|
|
59
|
+
if (!isPlainObject(body)) return body;
|
|
60
|
+
|
|
61
|
+
const rewriteTools = (tools: unknown[]): unknown[] => {
|
|
62
|
+
let changed = false;
|
|
63
|
+
const rewritten = tools.map(tool => {
|
|
64
|
+
if (!isPlainObject(tool)) return tool;
|
|
65
|
+
let next = tool;
|
|
66
|
+
for (const { field, toolTypes, capabilityGated } of CANONICAL_ONLY_TOOL_FIELDS) {
|
|
67
|
+
if (capabilityGated && !includeCapabilityGated) continue;
|
|
68
|
+
if (!Object.hasOwn(next, field)) continue;
|
|
69
|
+
if (toolTypes && (typeof next.type !== "string" || !toolTypes.has(next.type))) continue;
|
|
70
|
+
const { [field]: _private, ...rest } = next;
|
|
71
|
+
next = rest;
|
|
72
|
+
}
|
|
73
|
+
if (next === tool) return tool;
|
|
74
|
+
changed = true;
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
return changed ? rewritten : tools;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let rewrittenBody = body;
|
|
81
|
+
if (Array.isArray(body.tools)) {
|
|
82
|
+
const tools = rewriteTools(body.tools);
|
|
83
|
+
if (tools !== body.tools) rewrittenBody = { ...rewrittenBody, tools };
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(body.input)) return rewrittenBody;
|
|
86
|
+
|
|
87
|
+
let input: unknown[] | undefined;
|
|
88
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
89
|
+
const item = body.input[index];
|
|
90
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) continue;
|
|
91
|
+
const tools = rewriteTools(item.tools);
|
|
92
|
+
if (tools === item.tools) continue;
|
|
93
|
+
input ??= [...body.input];
|
|
94
|
+
input[index] = { ...item, tools };
|
|
95
|
+
}
|
|
96
|
+
return input ? { ...rewrittenBody, input } : rewrittenBody;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Codex keeps this ChatGPT-internal item metadata when its configured provider name is `openai`.
|
|
101
|
+
* Loopback OpenCodex injection intentionally retains that provider identity for history continuity,
|
|
102
|
+
* even when the proxy ultimately routes the request to a public Responses destination. Those
|
|
103
|
+
* destinations reject the private field as an unknown `input[*]` parameter, so remove it at the
|
|
104
|
+
* noncanonical boundary without mutating the caller-owned raw body.
|
|
105
|
+
*/
|
|
106
|
+
export function stripInternalChatMessageMetadataPassthrough(body: unknown): unknown {
|
|
107
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
108
|
+
|
|
109
|
+
let changed = false;
|
|
110
|
+
const input = body.input.map(item => {
|
|
111
|
+
if (!isPlainObject(item) || !Object.hasOwn(item, "internal_chat_message_metadata_passthrough")) {
|
|
112
|
+
return item;
|
|
113
|
+
}
|
|
114
|
+
changed = true;
|
|
115
|
+
const next = { ...item };
|
|
116
|
+
delete next.internal_chat_message_metadata_passthrough;
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return changed ? { ...body, input } : body;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
125
|
+
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
126
|
+
* exist, producing a 404. Strip all item IDs in this case — `call_id` pairing is unaffected.
|
|
127
|
+
* Matches codex-rs behavior (core/src/client.rs:918-925).
|
|
128
|
+
*/
|
|
129
|
+
export function stripItemIdsWhenUnstored(body: unknown): unknown {
|
|
130
|
+
if (!isPlainObject(body) || body.store !== false) return body;
|
|
131
|
+
if (!Array.isArray(body.input)) return body;
|
|
132
|
+
|
|
133
|
+
let changed = false;
|
|
134
|
+
const input = body.input.map(item => {
|
|
135
|
+
if (!isPlainObject(item) || !("id" in item)) return item;
|
|
136
|
+
changed = true;
|
|
137
|
+
const next = { ...item };
|
|
138
|
+
delete next.id;
|
|
139
|
+
return next;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return changed ? { ...body, input } : body;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Normalize replayed compaction items for the destination backend.
|
|
147
|
+
*
|
|
148
|
+
* A compaction item carries an `encrypted_content` blob the client replays verbatim on every later
|
|
149
|
+
* turn, and only the backend that minted it can decode it. Proxy-minted `ocx1:` envelopes are
|
|
150
|
+
* transparent base64 rather than encryption, so no upstream can read them and they always become
|
|
151
|
+
* plain user messages. Native blobs have multiple possible minters, so a destination's ability to
|
|
152
|
+
* decode its own blobs does not make a blob from a previous serving identity portable. On a known
|
|
153
|
+
* identity mismatch the blob degrades to the same note the bridged parser uses, even when the
|
|
154
|
+
* destination normally accepts native blobs. Without a known mismatch, the destination capability
|
|
155
|
+
* keeps the existing behavior.
|
|
156
|
+
*
|
|
157
|
+
* A bare `context_compaction` marker carries no blob and is forwarded untouched.
|
|
158
|
+
*/
|
|
159
|
+
export function scrubOcxCompactionItems(
|
|
160
|
+
body: unknown,
|
|
161
|
+
destinationDecodesNativeBlob: boolean,
|
|
162
|
+
threadServingIdentityChanged: boolean,
|
|
163
|
+
): unknown {
|
|
164
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
165
|
+
|
|
166
|
+
let changed = false;
|
|
167
|
+
const input = body.input.map(item => {
|
|
168
|
+
if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
|
|
169
|
+
const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
|
|
170
|
+
if (encrypted === undefined) return item;
|
|
171
|
+
if (
|
|
172
|
+
decodeCompactionSummary(encrypted) === null
|
|
173
|
+
&& destinationDecodesNativeBlob
|
|
174
|
+
&& !threadServingIdentityChanged
|
|
175
|
+
) return item;
|
|
176
|
+
changed = true;
|
|
177
|
+
return {
|
|
178
|
+
type: "message",
|
|
179
|
+
role: "user",
|
|
180
|
+
content: [{ type: "input_text", text: compactionItemToText(encrypted) }],
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return changed ? { ...body, input } : body;
|
|
185
|
+
}
|