@bitkyc08/opencodex 2.26.0 → 2.28.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/{index-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +60 -1
- package/src/adapters/base.ts +16 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +22 -5
- package/src/adapters/openai-chat.ts +189 -60
- package/src/adapters/openai-responses.ts +37 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +19 -6
- package/src/cli/models.ts +13 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +108 -3
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +63 -15
- package/src/codex/convergence.ts +61 -13
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +110 -70
- package/src/images/loop.ts +11 -4
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +120 -6
- package/src/providers/service-tier.ts +50 -15
- package/src/responses/parser.ts +59 -11
- package/src/responses/state.ts +162 -5
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +17 -3
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +44 -6
- package/src/routing/profile.ts +1 -1
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +59 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/management/shared.ts +3 -1
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +259 -43
- package/src/server/responses/input-admission.ts +7 -2
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/usage/log.ts +4 -0
- package/src/web-search/loop.ts +11 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { modelRecordValue } from "../../reasoning-effort";
|
|
1
2
|
import { modelInList } from "../../types";
|
|
2
3
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
3
4
|
import { PROVIDER_REGISTRY } from "../../providers/registry";
|
|
4
|
-
import { fastPolicyForModel,
|
|
5
|
+
import { fastPolicyForModel, serviceTierSupportFromPolicy } from "../../providers/service-tier";
|
|
5
6
|
import { resolveProviderAuthTransport } from "../../providers/fastwire";
|
|
6
7
|
import { localFingerprint } from "../../lab/digest";
|
|
7
8
|
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
@@ -51,8 +52,45 @@ function includesModel(list: string[] | undefined, modelId: string): boolean {
|
|
|
51
52
|
return modelInList(list, modelId);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Per-model override lookup for the nine family-aware report rows.
|
|
57
|
+
*
|
|
58
|
+
* Delegates to modelRecordValue so the report reads these maps the way the
|
|
59
|
+
* runtime does -- own properties only, then the pre-colon family, then a
|
|
60
|
+
* case-folded key. A bare index disagreed on all three: it missed the
|
|
61
|
+
* `gpt-oss` entry ollama-cloud's `gpt-oss:120b` actually resolves, missed a
|
|
62
|
+
* differently-cased key, and walked the prototype chain, so a routed model id
|
|
63
|
+
* of `constructor`/`toString` yielded an Object.prototype function. That last
|
|
64
|
+
* one made buildBehaviorFingerprintV1 throw ("unsupported value type
|
|
65
|
+
* function"); the caller catches it (`src/routing/compatibility/subject.ts:125`)
|
|
66
|
+
* and returns no route, so the subject is silently dropped -- and the linker
|
|
67
|
+
* contract says implementations do not throw.
|
|
68
|
+
*
|
|
69
|
+
* Not every override map belongs here. `modelPreferHostedTools` and
|
|
70
|
+
* `modelOpenRouterRouting` are exact-own at runtime and go through
|
|
71
|
+
* `exactOwnValue` below; widening those to the family would be this same bug
|
|
72
|
+
* with the sign flipped.
|
|
73
|
+
*/
|
|
54
74
|
function modelValue<T>(map: Record<string, T> | undefined, modelId: string): T | undefined {
|
|
55
|
-
return map
|
|
75
|
+
return modelRecordValue(map, modelId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Exact, own-property lookup for the two maps the runtime resolves that way.
|
|
80
|
+
*
|
|
81
|
+
* `modelPreferHostedTools` and `modelOpenRouterRouting` are deliberately exact: the
|
|
82
|
+
* adapter reads the first through `hasOwnProperty`
|
|
83
|
+
* (`src/adapters/openai-responses.ts:1001`) and the second through `Object.hasOwn`
|
|
84
|
+
* (`src/providers/openrouter-routing.ts:89`), and the type documents the first as
|
|
85
|
+
* "Exact-model hosted tools" (`src/types.ts:1584`). Sending them through
|
|
86
|
+
* `modelRecordValue` would make the report say a `gpt-oss` entry applies to
|
|
87
|
+
* `gpt-oss:120b` when the adapter will never apply it -- the same divergence this
|
|
88
|
+
* file exists to remove, pointed the other way. A bare index is not the answer
|
|
89
|
+
* either: it walks the prototype chain, which is the bug `modelValue` just fixed.
|
|
90
|
+
* Neither existing primitive is right for these two, so this is the third one.
|
|
91
|
+
*/
|
|
92
|
+
function exactOwnValue<T>(map: Record<string, T> | undefined, modelId: string): T | undefined {
|
|
93
|
+
return map !== undefined && Object.hasOwn(map, modelId) ? map[modelId] : undefined;
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
const CREDENTIAL_HEADER = /(authorization|api[-_]?key|token|secret|credential|cookie)/i;
|
|
@@ -69,7 +107,7 @@ function nonCredentialHeaderDigest(
|
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
function effectiveOpenRouterRouting(effective: OcxProviderConfig, modelId: string) {
|
|
72
|
-
return effective.modelOpenRouterRouting
|
|
110
|
+
return exactOwnValue(effective.modelOpenRouterRouting, modelId) ?? effective.openRouterRouting;
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
/**
|
|
@@ -103,7 +141,7 @@ export function resolveProductionBehaviorValues(
|
|
|
103
141
|
const project = typeof effective.project === "string" && effective.project ? effective.project : null;
|
|
104
142
|
const location = typeof effective.location === "string" && effective.location ? effective.location : null;
|
|
105
143
|
const nativeLocalExec = effective.nativeLocalExec === "on" || effective.unsafeAllowNativeLocalExec === true;
|
|
106
|
-
const fastPolicy = fastPolicyForModel(effective, modelId, providerName);
|
|
144
|
+
const fastPolicy = fastPolicyForModel(effective, modelId, providerName, "responses", provider);
|
|
107
145
|
|
|
108
146
|
const values: LabBehaviorValues = {
|
|
109
147
|
"wire.adapter": behaviorRow("provider_config", adapter),
|
|
@@ -122,7 +160,7 @@ export function resolveProductionBehaviorValues(
|
|
|
122
160
|
"responses.stateful": behaviorRow("provider_config", effective.statelessResponses !== true),
|
|
123
161
|
"responses.serviceTier": behaviorRow(
|
|
124
162
|
"provider_config",
|
|
125
|
-
|
|
163
|
+
serviceTierSupportFromPolicy(fastPolicy) ?? null,
|
|
126
164
|
),
|
|
127
165
|
"responses.fastWireKind": behaviorRow(
|
|
128
166
|
"provider_config",
|
|
@@ -184,7 +222,7 @@ export function resolveProductionBehaviorValues(
|
|
|
184
222
|
effective.parallelToolCalls ?? (upstreamProtocol === "openai-chat"),
|
|
185
223
|
),
|
|
186
224
|
"tools.hostedPreference": behaviorRow("provider_config", {
|
|
187
|
-
tools:
|
|
225
|
+
tools: exactOwnValue(effective.modelPreferHostedTools, modelId) ?? [],
|
|
188
226
|
}),
|
|
189
227
|
"tools.builtinNameEscaping": behaviorRow("provider_config", effective.escapeBuiltinToolNames === true),
|
|
190
228
|
"cache.forwarding": behaviorRow("provider_config", effective.promptCacheKey === true),
|
package/src/routing/profile.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "../types";
|
|
14
14
|
import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
|
|
15
15
|
import { listComboIds, resolveComboId } from "../combos";
|
|
16
|
-
import { hasOwnProvider } from "../config";
|
|
16
|
+
import { hasOwnProvider } from "../config/provider-name";
|
|
17
17
|
import { MAX_COMPATIBILITY_REQUIRED_SUITES } from "./compatibility/types";
|
|
18
18
|
import { POLICY_NAMESPACE } from "./profile-namespace";
|
|
19
19
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
rateLimitRetryPolicyFor,
|
|
28
28
|
rotateProviderTransportOn429,
|
|
29
29
|
} from "../providers/key-failover";
|
|
30
|
+
import { fastPolicyForModel } from "../providers/service-tier";
|
|
30
31
|
import type { RouteResult } from "../router";
|
|
31
32
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
32
33
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
|
|
@@ -154,8 +155,16 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
154
155
|
translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
|
|
155
156
|
retainedRequestBytes = bytes;
|
|
156
157
|
};
|
|
158
|
+
const buildActiveRequest = () => buildOpenAIChatPassthroughRequest(
|
|
159
|
+
activeProvider,
|
|
160
|
+
options.chatBody,
|
|
161
|
+
route.modelId,
|
|
162
|
+
requestedStream,
|
|
163
|
+
fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
|
|
164
|
+
config.fastMode,
|
|
165
|
+
);
|
|
157
166
|
try {
|
|
158
|
-
activeRequest =
|
|
167
|
+
activeRequest = buildActiveRequest();
|
|
159
168
|
retainRequest(activeRequest);
|
|
160
169
|
} catch (error) {
|
|
161
170
|
releaseRetainedRequest();
|
|
@@ -222,7 +231,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
222
231
|
activeProvider = rotated;
|
|
223
232
|
activeAdapter = createOpenAIChatAdapter(activeProvider);
|
|
224
233
|
releaseRetainedRequest();
|
|
225
|
-
activeRequest =
|
|
234
|
+
activeRequest = buildActiveRequest();
|
|
226
235
|
retainRequest(activeRequest);
|
|
227
236
|
response = await send(activeRequest, "key-429");
|
|
228
237
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ import {
|
|
|
58
58
|
cooldownErrorMessage,
|
|
59
59
|
} from "../codex/auth-context";
|
|
60
60
|
import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
|
|
61
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex/account-namespaces";
|
|
62
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
63
|
+
import {
|
|
64
|
+
availableAccountGatedNativeModels,
|
|
65
|
+
resolveCodexModelEntitlements,
|
|
66
|
+
} from "../codex/model-entitlements";
|
|
61
67
|
export {
|
|
62
68
|
clearThreadAccountMap,
|
|
63
69
|
formatCodexProviderForLog,
|
|
@@ -705,6 +711,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
705
711
|
? startNativeMainStartupLifecycle(deps.nativeMainStartup)
|
|
706
712
|
: blockNativeMainStartupForUnownedServiceHome(
|
|
707
713
|
nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
|
|
714
|
+
// #2108: an `unknown` verdict means the probe could not answer, not that this host
|
|
715
|
+
// is unownable. Hand the fence a way to re-ask so a host that becomes answerable
|
|
716
|
+
// after boot reopens on its own instead of needing `ocx restart`. A `foreign`
|
|
717
|
+
// verdict ignores this by design — that one is a fact, not a question.
|
|
718
|
+
{ reprobe: () => inspectStartupOwnership(deps).ownership },
|
|
708
719
|
)
|
|
709
720
|
: {
|
|
710
721
|
homeId: null,
|
|
@@ -894,8 +905,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
894
905
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
895
906
|
}
|
|
896
907
|
let goModels;
|
|
908
|
+
let modelEntitlements;
|
|
897
909
|
try {
|
|
898
|
-
goModels = await
|
|
910
|
+
[goModels, modelEntitlements] = await Promise.all([
|
|
911
|
+
fetchAllModels(config),
|
|
912
|
+
resolveCodexModelEntitlements(config),
|
|
913
|
+
]);
|
|
899
914
|
} catch (error) {
|
|
900
915
|
if (error instanceof CatalogGatherBusyError) {
|
|
901
916
|
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
@@ -906,24 +921,57 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
906
921
|
throw error;
|
|
907
922
|
}
|
|
908
923
|
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
924
|
+
const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
|
|
909
925
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
910
926
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
927
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
928
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
929
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
930
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
931
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
932
|
+
modelEntitlements,
|
|
933
|
+
bareEligibleAccountIds,
|
|
934
|
+
);
|
|
935
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
936
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
937
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
938
|
+
));
|
|
939
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
940
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
941
|
+
));
|
|
911
942
|
const nativeSlugs = includeNativeOpenAi
|
|
912
|
-
? nativeOpenAiSlugs()
|
|
943
|
+
? nativeOpenAiSlugs().filter(slug => (
|
|
944
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
945
|
+
))
|
|
913
946
|
: [];
|
|
914
947
|
const disabledNatives = disabledNativeSlugs(config);
|
|
915
948
|
const disabledModels = new Set(config.disabledModels ?? []);
|
|
916
949
|
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
917
|
-
const suppressedBareNativeSlugs =
|
|
950
|
+
const suppressedBareNativeSlugs = new Set([
|
|
951
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
952
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
953
|
+
]);
|
|
918
954
|
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
919
955
|
? visibleCodexAccountSelectors(config)
|
|
920
956
|
: [];
|
|
957
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
921
958
|
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
922
|
-
? accountBoundNativeOpenAiSlugsBySelector(config)
|
|
959
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
|
|
960
|
+
const target = accountTargets.get(selector);
|
|
961
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
962
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
963
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
964
|
+
return [selector, slugs.filter(slug => (
|
|
965
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
966
|
+
))] as const;
|
|
967
|
+
}))
|
|
923
968
|
: new Map<string, readonly string[]>();
|
|
924
969
|
const accountNativeSlugs = [...new Set(
|
|
925
970
|
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
926
971
|
)];
|
|
972
|
+
const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
|
|
973
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
974
|
+
));
|
|
927
975
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
928
976
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
929
977
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
@@ -940,7 +988,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
940
988
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
941
989
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
942
990
|
buildDesktop3pRegistry(
|
|
943
|
-
|
|
991
|
+
desktopNativeSlugs,
|
|
944
992
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
945
993
|
config.claudeCode?.desktopProfile,
|
|
946
994
|
);
|
|
@@ -957,7 +1005,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
957
1005
|
: idsParam === "desktop"
|
|
958
1006
|
? "desktop3p" as const
|
|
959
1007
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
960
|
-
const data = buildAnthropicModelInfos(
|
|
1008
|
+
const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
|
|
961
1009
|
return jsonResponse({ data }, 200, req, policy);
|
|
962
1010
|
}
|
|
963
1011
|
if (url.searchParams.has("client_version")) {
|
|
@@ -971,7 +1019,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
971
1019
|
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
972
1020
|
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
973
1021
|
const catalogNativeSlugs = accountSelectors.length > 0
|
|
974
|
-
? [...new Set([
|
|
1022
|
+
? [...new Set([
|
|
1023
|
+
...availableAccountNativeSlugs,
|
|
1024
|
+
...accountNativeSlugs,
|
|
1025
|
+
])]
|
|
975
1026
|
: nativeSlugs;
|
|
976
1027
|
const entries = buildCatalogEntries(
|
|
977
1028
|
loadCatalogTemplate(),
|
|
@@ -1038,7 +1089,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1038
1089
|
// for both bare and qualified rows. Without selectors, the live catalog continues to own
|
|
1039
1090
|
// bare availability.
|
|
1040
1091
|
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1041
|
-
?
|
|
1092
|
+
? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
|
|
1042
1093
|
: [];
|
|
1043
1094
|
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1044
1095
|
? selectorNativeSlugs
|
|
@@ -613,15 +613,29 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
613
613
|
stored === catalogModelSlug(m) || slugEquals(stored, m.provider, m.id)
|
|
614
614
|
))
|
|
615
615
|
.map(catalogModelSlug))];
|
|
616
|
-
const
|
|
616
|
+
const chosen = config.subagentModels ?? [];
|
|
617
|
+
const selectable = [
|
|
617
618
|
...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
|
|
618
619
|
...visibleRouted,
|
|
619
620
|
];
|
|
621
|
+
// A saved roster slot must stay representable even after its model is disabled
|
|
622
|
+
// elsewhere (Models page, provider allowlist, a provider row going away). The
|
|
623
|
+
// dashboard treats `available` as the set of rows it can render, so a chosen id
|
|
624
|
+
// missing from it disappears from the roster UI and the next Save — which PUTs
|
|
625
|
+
// exactly what the UI holds — silently truncates the persisted list. Losing a
|
|
626
|
+
// deliberate 5-model roster to an unrelated visibility toggle is data loss, not a
|
|
627
|
+
// filter. Same reasoning as `fetchGrokCandidateModels`, which deliberately lists a
|
|
628
|
+
// model the user already excluded so its switch remains reachable.
|
|
629
|
+
const selectableSet = new Set(selectable);
|
|
630
|
+
const available = [
|
|
631
|
+
...selectable,
|
|
632
|
+
...[...new Set(chosen)].filter(model => !selectableSet.has(model)),
|
|
633
|
+
];
|
|
620
634
|
// #857: let CLI/GUI show when a running Codex app-server keeps an older
|
|
621
635
|
// in-memory catalog than the one on disk.
|
|
622
636
|
const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
|
|
623
637
|
const catalogState = collectCodexAppServerCatalogState();
|
|
624
|
-
return jsonResponse({ chosen
|
|
638
|
+
return jsonResponse({ chosen, available, catalogState });
|
|
625
639
|
}
|
|
626
640
|
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
627
641
|
let body: { models?: unknown };
|
|
@@ -86,7 +86,8 @@ export type CostEstimateReason =
|
|
|
86
86
|
| "usage_estimated"
|
|
87
87
|
| "cache_detail_missing"
|
|
88
88
|
| "expected_price_overlay"
|
|
89
|
-
| "provider_cost_overlay"
|
|
89
|
+
| "provider_cost_overlay"
|
|
90
|
+
| "priority_lower_bound";
|
|
90
91
|
|
|
91
92
|
export type CostResult =
|
|
92
93
|
| { kind: "value"; estimate: NonNullable<ReturnType<typeof estimateRequestCost>>; estimateReasons: CostEstimateReason[] }
|
|
@@ -143,6 +144,7 @@ export function costResult(entry: MetricSource): CostResult {
|
|
|
143
144
|
? "expected_price_overlay" as const : undefined,
|
|
144
145
|
estimate.price?.source === "user" || estimate.attempts?.some(a => a.price.source === "user")
|
|
145
146
|
? "provider_cost_overlay" as const : undefined,
|
|
147
|
+
estimate.priorityLowerBound ? "priority_lower_bound" as const : undefined,
|
|
146
148
|
].filter((reason): reason is CostEstimateReason => reason !== undefined);
|
|
147
149
|
return { kind: "value", estimate, estimateReasons };
|
|
148
150
|
}
|
|
@@ -65,6 +65,8 @@ export interface RequestLogContext {
|
|
|
65
65
|
/** Stable non-PII Codex Pool account identity for durable usage attribution. */
|
|
66
66
|
accountLogLabel?: string;
|
|
67
67
|
requestedModel?: string;
|
|
68
|
+
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
69
|
+
shadowCallRewrittenFrom?: string;
|
|
68
70
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
69
71
|
comboId?: string;
|
|
70
72
|
requestedEffort?: string;
|
|
@@ -142,6 +144,8 @@ export interface RequestLogEntry {
|
|
|
142
144
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
143
145
|
conversationId?: string;
|
|
144
146
|
requestedModel?: string;
|
|
147
|
+
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
148
|
+
shadowCallRewrittenFrom?: string;
|
|
145
149
|
requestedEffort?: string;
|
|
146
150
|
effectiveEffort?: string;
|
|
147
151
|
reasoningWireField?: string;
|
|
@@ -255,6 +259,9 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
255
259
|
? { accountLogLabel: entry.accountLogLabel }
|
|
256
260
|
: {}),
|
|
257
261
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
262
|
+
...(entry.shadowCallRewrittenFrom
|
|
263
|
+
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
264
|
+
: {}),
|
|
258
265
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
259
266
|
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
260
267
|
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
@@ -327,6 +334,20 @@ export function hydrateRequestLogsFromDisk(
|
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
export function addRequestLog(entry: RequestLogEntry) {
|
|
337
|
+
// Sanitize ONCE, at the ingress, and use that one value for both destinations.
|
|
338
|
+
//
|
|
339
|
+
// `addFinalRequestLog` is not the only way in: `addRequestLog` is exported and callable
|
|
340
|
+
// directly, and it retained the caller's entry verbatim in the in-memory ring while only the
|
|
341
|
+
// field-by-field disk projection below saw a sanitized value. That split let `/api/logs`
|
|
342
|
+
// serve a raw upstream-supplied marker — a newline in it can forge a record boundary in a
|
|
343
|
+
// line-oriented viewer — while `usage.jsonl` looked clean, which is the worst shape for a
|
|
344
|
+
// sanitization bug because the safe surface is the one you check.
|
|
345
|
+
const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
|
|
346
|
+
const retained: RequestLogEntry = shadowCallRewrittenFrom === entry.shadowCallRewrittenFrom
|
|
347
|
+
? entry
|
|
348
|
+
: { ...entry, ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}) };
|
|
349
|
+
if (!shadowCallRewrittenFrom && retained !== entry) delete retained.shadowCallRewrittenFrom;
|
|
350
|
+
entry = retained;
|
|
330
351
|
retainRequestLogEntry(entry);
|
|
331
352
|
try {
|
|
332
353
|
// Failure diagnostics survive the 200-entry ring buffer by riding the persisted
|
|
@@ -358,6 +379,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
358
379
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
359
380
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
360
381
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
382
|
+
...(entry.shadowCallRewrittenFrom
|
|
383
|
+
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
384
|
+
: {}),
|
|
361
385
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
362
386
|
...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
|
|
363
387
|
...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
|
|
@@ -905,6 +929,12 @@ export function addFinalRequestLog(
|
|
|
905
929
|
const loggedUsage = aggregate?.usage ?? existing.usage;
|
|
906
930
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
907
931
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
932
|
+
// Sanitize at the logging layer, not only at the one call site that populates this today.
|
|
933
|
+
// The value originates in an upstream-supplied model id, so an unsanitized newline would
|
|
934
|
+
// let a single field forge a record boundary in any line-oriented log viewer. Doing it here
|
|
935
|
+
// means a future caller cannot reintroduce the hole by forgetting to sanitize first, and
|
|
936
|
+
// the in-memory /api/logs row matches what usage.jsonl already stores.
|
|
937
|
+
const shadowCallRewrittenFrom = sanitizeLogMetadataString(logCtx.shadowCallRewrittenFrom);
|
|
908
938
|
addLog({
|
|
909
939
|
requestId,
|
|
910
940
|
timestamp: start,
|
|
@@ -919,6 +949,7 @@ export function addFinalRequestLog(
|
|
|
919
949
|
: {}),
|
|
920
950
|
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
921
951
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
952
|
+
...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
|
|
922
953
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
923
954
|
...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
|
|
924
955
|
...(logCtx.reasoningWireField ? { reasoningWireField: logCtx.reasoningWireField } : {}),
|
|
@@ -101,22 +101,23 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: TranslatorBudget): {
|
|
104
|
-
toolNsMap: Map<string, { namespace: string; name: string }>;
|
|
104
|
+
toolNsMap: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
105
105
|
declaredToolNames: Set<string>;
|
|
106
106
|
/** Declared parameter schema per request-visible tool name (#1611 integer repair). */
|
|
107
107
|
toolParameterSchemas: Map<string, Record<string, unknown>>;
|
|
108
108
|
freeformToolNames: Set<string>;
|
|
109
109
|
toolSearchToolNames: Set<string>;
|
|
110
110
|
} {
|
|
111
|
-
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
111
|
+
const toolNsMap = new Map<string, { namespace: string; name: string; freeform?: true }>();
|
|
112
112
|
const declaredToolNames = new Set<string>();
|
|
113
113
|
const toolParameterSchemas = new Map<string, Record<string, unknown>>();
|
|
114
114
|
const freeformToolNames = new Set<string>();
|
|
115
115
|
const toolSearchToolNames = new Set<string>();
|
|
116
|
-
const
|
|
117
|
-
|
|
116
|
+
const requestedTools = parsed.context.tools ?? [];
|
|
117
|
+
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice, requestedTools);
|
|
118
|
+
const authorizedTools = requestedTools.filter(toolAllowed);
|
|
119
|
+
for (const t of authorizedTools) {
|
|
118
120
|
// Upstream output is untrusted: only restore calls for tools the caller authorized.
|
|
119
|
-
if (!toolAllowed(t)) continue;
|
|
120
121
|
const wireName = namespacedToolName(t.namespace, t.name);
|
|
121
122
|
budget?.chargeRetained(new TextEncoder().encode(wireName).byteLength, { kind: "retained_collectors" });
|
|
122
123
|
declaredToolNames.add(wireName);
|
|
@@ -125,7 +126,7 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
125
126
|
if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(wireName, t.parameters);
|
|
126
127
|
if (t.namespace) {
|
|
127
128
|
budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([wireName, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
|
|
128
|
-
toolNsMap.set(wireName, { namespace: t.namespace, name: t.name });
|
|
129
|
+
toolNsMap.set(wireName, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
|
|
129
130
|
}
|
|
130
131
|
if (t.freeform) {
|
|
131
132
|
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
@@ -136,6 +137,29 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
136
137
|
toolSearchToolNames.add(t.name);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
140
|
+
// Some routed providers echo a bare tool_choice selector instead of the flattened catalog
|
|
141
|
+
// name. Accept only selectors the client actually sent and only when the full request catalog
|
|
142
|
+
// contains one tool with that logical name.
|
|
143
|
+
const choice = parsed.options.toolChoice;
|
|
144
|
+
const bareChoiceNames = new Set(
|
|
145
|
+
choice && typeof choice === "object"
|
|
146
|
+
? ("allowedTools" in choice ? choice.allowedTools : [choice.name])
|
|
147
|
+
: [],
|
|
148
|
+
);
|
|
149
|
+
const bareNameCounts = new Map<string, number>();
|
|
150
|
+
for (const t of requestedTools) {
|
|
151
|
+
bareNameCounts.set(t.name, (bareNameCounts.get(t.name) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
for (const t of authorizedTools) {
|
|
154
|
+
if (!t.namespace || !bareChoiceNames.has(t.name) || bareNameCounts.get(t.name) !== 1 || declaredToolNames.has(t.name)) continue;
|
|
155
|
+
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
156
|
+
declaredToolNames.add(t.name);
|
|
157
|
+
budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([t.name, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
|
|
158
|
+
toolNsMap.set(t.name, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
|
|
159
|
+
if (t.parameters && typeof t.parameters === "object") {
|
|
160
|
+
toolParameterSchemas.set(t.name, t.parameters);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
139
163
|
return { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames };
|
|
140
164
|
}
|
|
141
165
|
|
|
@@ -197,7 +221,8 @@ export interface MultiAgentGuidanceDeps {
|
|
|
197
221
|
configuredModels: readonly string[],
|
|
198
222
|
surface: SpawnAgentSurface,
|
|
199
223
|
) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
|
|
200
|
-
collectCatalogState?: () => { state: "fresh" | "stale" | "not_running" | "unknown" }
|
|
224
|
+
collectCatalogState?: () => { state: "fresh" | "stale" | "not_running" | "unknown" }
|
|
225
|
+
| Promise<{ state: "fresh" | "stale" | "not_running" | "unknown" }>;
|
|
201
226
|
}
|
|
202
227
|
|
|
203
228
|
async function defaultCollectCatalogState(): Promise<{ state: "fresh" | "stale" | "not_running" | "unknown" }> {
|
|
@@ -207,8 +232,8 @@ async function defaultCollectCatalogState(): Promise<{ state: "fresh" | "stale"
|
|
|
207
232
|
if (override === "fresh" || override === "stale" || override === "not_running" || override === "unknown") {
|
|
208
233
|
return { state: override };
|
|
209
234
|
}
|
|
210
|
-
const {
|
|
211
|
-
return
|
|
235
|
+
const { collectCodexAppServerCatalogStateForRequest } = await import("../../codex/app-server-processes");
|
|
236
|
+
return collectCodexAppServerCatalogStateForRequest();
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
|
|
@@ -126,6 +126,7 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
|
|
|
126
126
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
127
127
|
|
|
128
128
|
import {
|
|
129
|
+
codexAccountGatedCanonicalWireModel,
|
|
129
130
|
decodeRequestErrorResponse,
|
|
130
131
|
handleResponses,
|
|
131
132
|
preAuthUpstreamHostCircuitKey,
|
|
@@ -304,6 +305,12 @@ export async function handleResponsesCompact(
|
|
|
304
305
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
305
306
|
}
|
|
306
307
|
const selectedModelId = route.modelId;
|
|
308
|
+
// Derive from the RESOLVED route model, not the caller's raw string. An account-qualified
|
|
309
|
+
// selector like `side/gpt-daybreak-blue-latest` does not match the gated map — `slugsEquivalent`
|
|
310
|
+
// reads the account namespace as a routed provider prefix — so keying on `raw.model` sent
|
|
311
|
+
// exactly the selector form back down the native compact endpoint this guard exists to avoid.
|
|
312
|
+
// `route.modelId` is the same value `applyCodexAccountGatedWireNormalization` uses in core.ts.
|
|
313
|
+
const accountGatedCompactWireModel = codexAccountGatedCanonicalWireModel(selectedModelId);
|
|
307
314
|
logCtx.requestedModel = raw.model;
|
|
308
315
|
logCtx.model = selectedModelId;
|
|
309
316
|
logCtx.routeDecision = route.routeDecision;
|
|
@@ -322,7 +329,10 @@ export async function handleResponsesCompact(
|
|
|
322
329
|
|
|
323
330
|
// #1686: a bearer-presented admission secret is one of ours, so the stored main credential
|
|
324
331
|
// is substituted below instead of the caller bearer being forwarded.
|
|
325
|
-
|
|
332
|
+
// #2132: and only when the route is a native Codex one, which is the only route that can
|
|
333
|
+
// consume that credential. See the longer note in core.ts resolveResponsesCodexAuth.
|
|
334
|
+
const substituteMainCredential = admission?.source === "bearer"
|
|
335
|
+
&& route.codexAccountMode !== undefined;
|
|
326
336
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
327
337
|
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
328
338
|
catch (err) {
|
|
@@ -334,7 +344,7 @@ export async function handleResponsesCompact(
|
|
|
334
344
|
// Native /responses/compact exists on the canonical ChatGPT backend and on the
|
|
335
345
|
// official OpenAI API. Any other Responses-shaped gateway must take the routed
|
|
336
346
|
// summarizer path below, or compaction fails against an endpoint it never had (#422).
|
|
337
|
-
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)) {
|
|
347
|
+
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel) {
|
|
338
348
|
if (req.signal.aborted) {
|
|
339
349
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
340
350
|
}
|
|
@@ -368,6 +378,7 @@ export async function handleResponsesCompact(
|
|
|
368
378
|
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
369
379
|
accountId: route.codexAccountId,
|
|
370
380
|
modelId: selectedModelId,
|
|
381
|
+
substituteMainCredentialForDirect: substituteMainCredential,
|
|
371
382
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
372
383
|
});
|
|
373
384
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
@@ -665,7 +676,10 @@ export async function handleResponsesCompact(
|
|
|
665
676
|
const inputItems = Array.isArray(raw.input) ? (raw.input as unknown[]) : [];
|
|
666
677
|
const internalBody = {
|
|
667
678
|
...raw,
|
|
668
|
-
|
|
679
|
+
// Canonical ChatGPT Responses rejects non-streaming turns. Daybreak cannot use the
|
|
680
|
+
// native compact endpoint either, so run its synthetic compaction as SSE and collapse
|
|
681
|
+
// the completed event back into the v1 compact JSON contract below.
|
|
682
|
+
stream: accountGatedCompactWireModel ? true : false,
|
|
669
683
|
input: [...inputItems, { type: "compaction_trigger" }],
|
|
670
684
|
};
|
|
671
685
|
const internalHeaders = new Headers({ "content-type": "application/json" });
|
|
@@ -681,10 +695,36 @@ export async function handleResponsesCompact(
|
|
|
681
695
|
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, ...(admission ? { admission } : {}) });
|
|
682
696
|
if (!response.ok) return response;
|
|
683
697
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
699
|
+
if (!response.body) {
|
|
700
|
+
return formatErrorResponse(502, "server_error", "compaction turn returned an empty event stream");
|
|
701
|
+
}
|
|
702
|
+
const terminal = { status: "incomplete" as "completed" | "failed" | "incomplete" };
|
|
703
|
+
let completed: { id?: unknown; output?: unknown; status?: unknown } | undefined;
|
|
704
|
+
await new Promise<void>(resolve => {
|
|
705
|
+
consumeForInspection(
|
|
706
|
+
response.body!,
|
|
707
|
+
status => { terminal.status = status; },
|
|
708
|
+
req.signal,
|
|
709
|
+
resolve,
|
|
710
|
+
undefined,
|
|
711
|
+
undefined,
|
|
712
|
+
value => { completed = value; },
|
|
713
|
+
);
|
|
714
|
+
});
|
|
715
|
+
if (req.signal.aborted) {
|
|
716
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
717
|
+
}
|
|
718
|
+
if (terminal.status !== "completed" || !completed) {
|
|
719
|
+
return formatErrorResponse(502, "upstream_error", `compaction turn did not complete (status: ${terminal.status})`);
|
|
720
|
+
}
|
|
721
|
+
json = completed as { output?: unknown[]; status?: unknown; error?: unknown };
|
|
722
|
+
} else {
|
|
723
|
+
try {
|
|
724
|
+
json = await response.json() as { output?: unknown[]; status?: unknown; error?: unknown };
|
|
725
|
+
} catch {
|
|
726
|
+
return formatErrorResponse(502, "server_error", "compaction turn returned a non-JSON response");
|
|
727
|
+
}
|
|
688
728
|
}
|
|
689
729
|
// The internal turn answers 200 even when it failed or was truncated, so the body
|
|
690
730
|
// has to be inspected. Reporting a failure beats installing "(no summary
|
|
@@ -713,6 +753,13 @@ export async function handleResponsesCompact(
|
|
|
713
753
|
`compaction turn produced ${compactionItems.length} compaction items, expected exactly 1`,
|
|
714
754
|
);
|
|
715
755
|
}
|
|
756
|
+
// The canonical Responses stream returns a real OpenAI-encrypted compaction item. OCX cannot
|
|
757
|
+
// and should not decrypt it; /responses/compact callers can consume that item directly.
|
|
758
|
+
if (accountGatedCompactWireModel) {
|
|
759
|
+
return new Response(JSON.stringify({ output: compactionItems }), {
|
|
760
|
+
headers: { "Content-Type": "application/json" },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
716
763
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
717
764
|
const decoded = typeof encrypted === "string" ? decodeCompactionSummary(encrypted) : null;
|
|
718
765
|
// An empty `ocx1:` envelope decodes to "" rather than null, so length is what matters.
|