@bitkyc08/opencodex 2.27.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-7jlKgmJd.js → index-D2sP-biU.js} +11 -11
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +59 -0
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +21 -4
- package/src/adapters/openai-chat.ts +151 -54
- package/src/adapters/openai-responses.ts +37 -0
- package/src/cli/index.ts +19 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +55 -2
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/sync.ts +57 -11
- package/src/codex/convergence.ts +61 -13
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/generated/compatibility-version.json +51 -39
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +74 -5
- package/src/providers/service-tier.ts +16 -8
- package/src/responses/parser.ts +3 -9
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +7 -0
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +41 -3
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +54 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +246 -39
- 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/usage/log.ts +4 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* how that affects eligibility.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
13
|
+
import { modelInList, type OcxConfig } from "../types";
|
|
14
14
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
15
15
|
import { serviceTierSupportForModel } from "../providers/service-tier";
|
|
16
16
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
nativeReasoningEfforts,
|
|
22
22
|
} from "../codex/catalog/metadata";
|
|
23
23
|
import { readCatalog, readCodexCatalogPath } from "../codex/catalog/parsing";
|
|
24
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
24
25
|
import { statSync } from "node:fs";
|
|
25
26
|
import type { RouteCapabilityEvidence } from "./trace";
|
|
26
27
|
|
|
@@ -159,9 +160,14 @@ export function candidateCapabilityEvidence(
|
|
|
159
160
|
const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId);
|
|
160
161
|
const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/");
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
// `modelRecordValue`, not a bare lookup: every runtime reader of these three maps
|
|
164
|
+
// resolves them that way, so a `gpt-oss` entry covers `gpt-oss:120b`. Reading raw
|
|
165
|
+
// made the evidence disagree with the resolver it claims to describe — and for the
|
|
166
|
+
// window it did not even degrade to unknown, it fell through to the provider-wide
|
|
167
|
+
// value, which is a definite wrong answer rather than an absent one.
|
|
168
|
+
const rawContextWindow = modelRecordValue(provider?.modelContextWindows, modelId)
|
|
163
169
|
?? provider?.contextWindow
|
|
164
|
-
?? registryEntry?.modelContextWindows
|
|
170
|
+
?? modelRecordValue(registryEntry?.modelContextWindows, modelId)
|
|
165
171
|
?? catalogRow?.contextWindow
|
|
166
172
|
?? (isNative ? nativeOpenAiContextWindow(modelId, nativeContextLimits(config)) : undefined);
|
|
167
173
|
// Native rows go through the accessor (raise-to-ceiling + opt-in). Routed rows keep
|
|
@@ -170,10 +176,21 @@ export function candidateCapabilityEvidence(
|
|
|
170
176
|
? (nativeOpenAiContextWindow(modelId, nativeContextLimits(config)) ?? rawContextWindow)
|
|
171
177
|
: rawContextWindow;
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
// `noVisionModels` is checked before the modality chain because that is the order
|
|
180
|
+
// `isModelTextOnly` uses: it matches the no-vision list and returns true before it
|
|
181
|
+
// ever reads `modelInputModalities` (`src/vision/index.ts:32`). So a `gpt-oss`
|
|
182
|
+
// no-vision entry beats an exact `gpt-oss:120b` entry that lists "image", and
|
|
183
|
+
// deriving `image` from the modality chain alone reported vision on a model the
|
|
184
|
+
// runtime refuses it for. That matters more here than on the CLI surface fixed in
|
|
185
|
+
// #2086: routing *acts* on this evidence, so it would select the candidate for image
|
|
186
|
+
// work that execution then rejects.
|
|
187
|
+
const noVision = modelInList(provider?.noVisionModels, modelId);
|
|
188
|
+
const modalities = noVision
|
|
189
|
+
? ["text"]
|
|
190
|
+
: (modelRecordValue(provider?.modelInputModalities, modelId)
|
|
191
|
+
?? modelRecordValue(registryEntry?.modelInputModalities, modelId)
|
|
192
|
+
?? catalogRow?.inputModalities
|
|
193
|
+
?? (isNative ? nativeInputModalities(modelId) : undefined));
|
|
177
194
|
const image = Array.isArray(modalities)
|
|
178
195
|
? modalities.includes("image")
|
|
179
196
|
: undefined;
|
|
@@ -196,8 +213,8 @@ export function candidateCapabilityEvidence(
|
|
|
196
213
|
|| provider?.parallelToolCalls === true
|
|
197
214
|
|| undefined;
|
|
198
215
|
|
|
199
|
-
const reasoningEfforts = provider?.modelReasoningEfforts
|
|
200
|
-
?? registryEntry?.modelReasoningEfforts
|
|
216
|
+
const reasoningEfforts = modelRecordValue(provider?.modelReasoningEfforts, modelId)
|
|
217
|
+
?? modelRecordValue(registryEntry?.modelReasoningEfforts, modelId)
|
|
201
218
|
?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
|
|
202
219
|
|
|
203
220
|
const tierSupport = provider
|
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -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
|
/**
|
|
@@ -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),
|
|
@@ -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,
|
|
@@ -899,8 +905,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
899
905
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
900
906
|
}
|
|
901
907
|
let goModels;
|
|
908
|
+
let modelEntitlements;
|
|
902
909
|
try {
|
|
903
|
-
goModels = await
|
|
910
|
+
[goModels, modelEntitlements] = await Promise.all([
|
|
911
|
+
fetchAllModels(config),
|
|
912
|
+
resolveCodexModelEntitlements(config),
|
|
913
|
+
]);
|
|
904
914
|
} catch (error) {
|
|
905
915
|
if (error instanceof CatalogGatherBusyError) {
|
|
906
916
|
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
@@ -911,24 +921,57 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
911
921
|
throw error;
|
|
912
922
|
}
|
|
913
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");
|
|
914
925
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
915
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
|
+
));
|
|
916
942
|
const nativeSlugs = includeNativeOpenAi
|
|
917
|
-
? nativeOpenAiSlugs()
|
|
943
|
+
? nativeOpenAiSlugs().filter(slug => (
|
|
944
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
945
|
+
))
|
|
918
946
|
: [];
|
|
919
947
|
const disabledNatives = disabledNativeSlugs(config);
|
|
920
948
|
const disabledModels = new Set(config.disabledModels ?? []);
|
|
921
949
|
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
922
|
-
const suppressedBareNativeSlugs =
|
|
950
|
+
const suppressedBareNativeSlugs = new Set([
|
|
951
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
952
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
953
|
+
]);
|
|
923
954
|
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
924
955
|
? visibleCodexAccountSelectors(config)
|
|
925
956
|
: [];
|
|
957
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
926
958
|
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
927
|
-
? 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
|
+
}))
|
|
928
968
|
: new Map<string, readonly string[]>();
|
|
929
969
|
const accountNativeSlugs = [...new Set(
|
|
930
970
|
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
931
971
|
)];
|
|
972
|
+
const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
|
|
973
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
974
|
+
));
|
|
932
975
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
933
976
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
934
977
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
@@ -945,7 +988,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
945
988
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
946
989
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
947
990
|
buildDesktop3pRegistry(
|
|
948
|
-
|
|
991
|
+
desktopNativeSlugs,
|
|
949
992
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
950
993
|
config.claudeCode?.desktopProfile,
|
|
951
994
|
);
|
|
@@ -962,7 +1005,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
962
1005
|
: idsParam === "desktop"
|
|
963
1006
|
? "desktop3p" as const
|
|
964
1007
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
965
|
-
const data = buildAnthropicModelInfos(
|
|
1008
|
+
const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
|
|
966
1009
|
return jsonResponse({ data }, 200, req, policy);
|
|
967
1010
|
}
|
|
968
1011
|
if (url.searchParams.has("client_version")) {
|
|
@@ -976,7 +1019,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
976
1019
|
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
977
1020
|
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
978
1021
|
const catalogNativeSlugs = accountSelectors.length > 0
|
|
979
|
-
? [...new Set([
|
|
1022
|
+
? [...new Set([
|
|
1023
|
+
...availableAccountNativeSlugs,
|
|
1024
|
+
...accountNativeSlugs,
|
|
1025
|
+
])]
|
|
980
1026
|
: nativeSlugs;
|
|
981
1027
|
const entries = buildCatalogEntries(
|
|
982
1028
|
loadCatalogTemplate(),
|
|
@@ -1043,7 +1089,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1043
1089
|
// for both bare and qualified rows. Without selectors, the live catalog continues to own
|
|
1044
1090
|
// bare availability.
|
|
1045
1091
|
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1046
|
-
?
|
|
1092
|
+
? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
|
|
1047
1093
|
: [];
|
|
1048
1094
|
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1049
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 };
|
|
@@ -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 } : {}),
|
|
@@ -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.
|