@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
|
@@ -14,11 +14,15 @@ import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalo
|
|
|
14
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
15
|
import {
|
|
16
16
|
canForwardForeignServiceTierForChatModel,
|
|
17
|
+
fastPolicyForModel,
|
|
17
18
|
supportsServiceTierForModel,
|
|
18
19
|
} from "../providers/service-tier";
|
|
19
20
|
import {
|
|
20
21
|
canonicalFastTierMarker,
|
|
21
22
|
createAdapterTierMetadata,
|
|
23
|
+
decideTier,
|
|
24
|
+
type AdapterTierMetadata,
|
|
25
|
+
type ResolvedFastPolicy,
|
|
22
26
|
} from "../providers/fastwire";
|
|
23
27
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
24
28
|
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
@@ -95,6 +99,8 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
95
99
|
rawBody: Record<string, unknown>,
|
|
96
100
|
modelId: string,
|
|
97
101
|
stream: boolean,
|
|
102
|
+
fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
|
|
103
|
+
fastMode?: boolean,
|
|
98
104
|
): AdapterRequest {
|
|
99
105
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
100
106
|
|
|
@@ -122,7 +128,16 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
122
128
|
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
123
129
|
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
// Run the same complete Fast policy as the translated Chat path, including explicit
|
|
132
|
+
// fastMode and foreign-tier handling. On inherited canonical Fast, the passthrough still
|
|
133
|
+
// retains the caller's exact spelling; forced Fast uses the policy-owned wire value.
|
|
134
|
+
const callerTier = typeof rawBody.service_tier === "string" ? rawBody.service_tier : undefined;
|
|
135
|
+
const tierDecision = decideTier(fastPolicy, fastMode, callerTier);
|
|
136
|
+
if (tierDecision.kind === "set") {
|
|
137
|
+
body.service_tier = fastMode === undefined && canonicalFastTierMarker(callerTier) !== undefined
|
|
138
|
+
? callerTier
|
|
139
|
+
: tierDecision.value;
|
|
140
|
+
} else if (tierDecision.kind === "forward-caller" && rawBody.service_tier !== undefined) {
|
|
126
141
|
body.service_tier = rawBody.service_tier;
|
|
127
142
|
}
|
|
128
143
|
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
@@ -307,8 +322,13 @@ function invalidToolCallsEvent(
|
|
|
307
322
|
rawToolCalls: unknown,
|
|
308
323
|
mode: "stream" | "response",
|
|
309
324
|
usage?: OcxUsage,
|
|
325
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
310
326
|
): Extract<AdapterEvent, { type: "error" }> {
|
|
311
|
-
|
|
327
|
+
// The streamed accumulator knows things a rescan cannot: which field on which pending call
|
|
328
|
+
// was actually rejected. Without the override, a stream carrying accepted padding on call 0
|
|
329
|
+
// and a real defect on call 1 blames call 0, because the stateless scan stops at the first
|
|
330
|
+
// structurally odd value it sees.
|
|
331
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
312
332
|
const detail = diagnostic
|
|
313
333
|
? ` (${diagnostic.reason}${diagnostic.callIndex !== undefined ? `; callIndex=${diagnostic.callIndex}` : ""}; valueType=${diagnostic.valueType})`
|
|
314
334
|
: "";
|
|
@@ -526,9 +546,13 @@ function diagnoseInvalidToolCalls(
|
|
|
526
546
|
return undefined;
|
|
527
547
|
}
|
|
528
548
|
|
|
529
|
-
function logInvalidToolCalls(
|
|
549
|
+
function logInvalidToolCalls(
|
|
550
|
+
mode: "stream" | "response",
|
|
551
|
+
rawToolCalls: unknown,
|
|
552
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
553
|
+
): void {
|
|
530
554
|
if (!isDebugEnabled()) return;
|
|
531
|
-
const diagnostic = diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
555
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
532
556
|
if (!diagnostic) return;
|
|
533
557
|
const fieldShape = fingerprintInvalidField(invalidToolCallField(rawToolCalls, diagnostic));
|
|
534
558
|
debugProviderDiagnostic("openai-chat", "invalid-tool-calls", {
|
|
@@ -1156,7 +1180,11 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1156
1180
|
if (!isXaiObjectSchema(parameters)) return undefined;
|
|
1157
1181
|
const resolved = resolveXaiSchemaRefs(parameters, parameters);
|
|
1158
1182
|
if (!isXaiObjectSchema(resolved)) return undefined;
|
|
1159
|
-
|
|
1183
|
+
|
|
1184
|
+
const normalizedRoot = { ...resolved };
|
|
1185
|
+
delete normalizedRoot.$schema;
|
|
1186
|
+
|
|
1187
|
+
const variants = expandXaiRootObjectSchemas(normalizedRoot);
|
|
1160
1188
|
if (!variants) return undefined;
|
|
1161
1189
|
if (variants.length === 1) {
|
|
1162
1190
|
return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
|
|
@@ -1166,7 +1194,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1166
1194
|
if (!additionalProperties.ok) return undefined;
|
|
1167
1195
|
if (!xaiPropertyMergeIsLossless(variants)) return undefined;
|
|
1168
1196
|
|
|
1169
|
-
const metadata = Object.fromEntries(Object.entries(
|
|
1197
|
+
const metadata = Object.fromEntries(Object.entries(normalizedRoot).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
|
|
1170
1198
|
delete metadata.properties;
|
|
1171
1199
|
delete metadata.required;
|
|
1172
1200
|
delete metadata.additionalProperties;
|
|
@@ -1180,6 +1208,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1180
1208
|
propertyValues.set(name, values);
|
|
1181
1209
|
}
|
|
1182
1210
|
}
|
|
1211
|
+
|
|
1183
1212
|
const properties = Object.fromEntries(
|
|
1184
1213
|
[...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
|
|
1185
1214
|
);
|
|
@@ -1196,7 +1225,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1196
1225
|
|
|
1197
1226
|
function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
1198
1227
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
1199
|
-
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice));
|
|
1228
|
+
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
1200
1229
|
if (tools.length === 0) return undefined;
|
|
1201
1230
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
1202
1231
|
const formatted = tools.flatMap(t => {
|
|
@@ -1284,6 +1313,26 @@ function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: stri
|
|
|
1284
1313
|
return fraction === undefined ? undefined : Math.max(1, Math.floor(maxBudget * fraction));
|
|
1285
1314
|
}
|
|
1286
1315
|
|
|
1316
|
+
function canSerializeOpenAIChatServiceTier(
|
|
1317
|
+
provider: OcxProviderConfig,
|
|
1318
|
+
modelId: string,
|
|
1319
|
+
serviceTier: unknown,
|
|
1320
|
+
tierDecision?: OcxParsedRequest["options"]["tierDecision"],
|
|
1321
|
+
): boolean {
|
|
1322
|
+
if (serviceTier === undefined) return false;
|
|
1323
|
+
if (tierDecision !== undefined) {
|
|
1324
|
+
return tierDecision.kind === "set" || tierDecision.kind === "forward-caller";
|
|
1325
|
+
}
|
|
1326
|
+
// No decision from the router means this call did not go through the tier state machine, so
|
|
1327
|
+
// ask that machine rather than re-deriving a looser answer beside it. The previous fallback
|
|
1328
|
+
// returned true whenever foreign forwarding was allowed at all, which let a caller tier
|
|
1329
|
+
// reach the wire in cases `decideTier` would have dropped — the two paths disagreeing is
|
|
1330
|
+
// precisely the bug, so there is now only one authority.
|
|
1331
|
+
const callerTier = typeof serviceTier === "string" ? serviceTier : undefined;
|
|
1332
|
+
const decision = decideTier(fastPolicyForModel(provider, modelId, undefined, "chat"), undefined, callerTier);
|
|
1333
|
+
return decision.kind === "set" || decision.kind === "forward-caller";
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1287
1336
|
export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
1288
1337
|
return {
|
|
1289
1338
|
name: "openai-chat",
|
|
@@ -1306,13 +1355,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1306
1355
|
// unclassified Chat routes remain behind the caller-forwarding opt-in.
|
|
1307
1356
|
const serviceTier = parsed.options.serviceTier;
|
|
1308
1357
|
const tierDecision = parsed.options.tierDecision;
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|| (tierDecision === undefined && (callerTierForwardAllowed || canonicalFastCapability));
|
|
1358
|
+
const canSerializeServiceTier = canSerializeOpenAIChatServiceTier(
|
|
1359
|
+
provider,
|
|
1360
|
+
parsed.modelId,
|
|
1361
|
+
serviceTier,
|
|
1362
|
+
tierDecision,
|
|
1363
|
+
);
|
|
1316
1364
|
if (canSerializeServiceTier && serviceTier !== undefined) {
|
|
1317
1365
|
body.service_tier = serviceTier;
|
|
1318
1366
|
}
|
|
@@ -1478,7 +1526,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1478
1526
|
};
|
|
1479
1527
|
},
|
|
1480
1528
|
|
|
1481
|
-
async *parseStream(
|
|
1529
|
+
async *parseStream(
|
|
1530
|
+
response: Response,
|
|
1531
|
+
budget: TranslatorBudget,
|
|
1532
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1533
|
+
): AsyncGenerator<AdapterEvent> {
|
|
1482
1534
|
if (!response.body) {
|
|
1483
1535
|
yield { type: "error", message: "No response body" };
|
|
1484
1536
|
return;
|
|
@@ -1489,7 +1541,20 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1489
1541
|
const budgetEncoder = new TextEncoder();
|
|
1490
1542
|
let buffer = "";
|
|
1491
1543
|
let bufferBytes = 0;
|
|
1492
|
-
interface PendingToolCall {
|
|
1544
|
+
interface PendingToolCall {
|
|
1545
|
+
key: string;
|
|
1546
|
+
id: string;
|
|
1547
|
+
name: string;
|
|
1548
|
+
args: string;
|
|
1549
|
+
argsBytes: number;
|
|
1550
|
+
/**
|
|
1551
|
+
* Whether this call has ever received `arguments` as an actual string, empty included.
|
|
1552
|
+
* An empty string still counts: it proves the upstream sent the field with the right
|
|
1553
|
+
* wire type, which is what a later malformed repeat of that field would be padding for.
|
|
1554
|
+
* A canonical NAME is not evidence about the ARGUMENTS field and must not stand in.
|
|
1555
|
+
*/
|
|
1556
|
+
sawArgumentsString: boolean;
|
|
1557
|
+
}
|
|
1493
1558
|
const pendingToolCalls: PendingToolCall[] = [];
|
|
1494
1559
|
let toolCallSeq = 0;
|
|
1495
1560
|
const closeToolCalls = (): PendingToolCall[] => {
|
|
@@ -1547,11 +1612,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1547
1612
|
try {
|
|
1548
1613
|
parsed = JSON.parse(payload);
|
|
1549
1614
|
} catch {
|
|
1615
|
+
tierMetadata?.markResponseUnparseable();
|
|
1550
1616
|
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
1551
1617
|
return "terminate";
|
|
1552
1618
|
}
|
|
1553
1619
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "continue";
|
|
1554
1620
|
const chunk = parsed as Record<string, unknown>;
|
|
1621
|
+
if (Object.hasOwn(chunk, "service_tier")) {
|
|
1622
|
+
tierMetadata?.observeResponseServiceTier(chunk.service_tier);
|
|
1623
|
+
}
|
|
1555
1624
|
|
|
1556
1625
|
if (chunk.error !== undefined && chunk.error !== null) {
|
|
1557
1626
|
const event = upstreamErrorEvent(chunk.error, pendingUsage);
|
|
@@ -1599,61 +1668,103 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1599
1668
|
logInvalidToolCalls("stream", rawToolCalls);
|
|
1600
1669
|
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1601
1670
|
}
|
|
1602
|
-
for (
|
|
1671
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
1672
|
+
const rawToolCall: unknown = rawToolCalls[callIndex];
|
|
1603
1673
|
if (!isRecord(rawToolCall)) {
|
|
1604
|
-
|
|
1605
|
-
|
|
1674
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1675
|
+
reason: "tool_call_not_object",
|
|
1676
|
+
callIndex,
|
|
1677
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
1678
|
+
};
|
|
1679
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1680
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1606
1681
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
logInvalidToolCalls("stream", rawToolCalls);
|
|
1620
|
-
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1621
|
-
}
|
|
1622
|
-
const rawName = rawFunction.name;
|
|
1623
|
-
const rawArguments = rawFunction.arguments;
|
|
1624
|
-
// Some OpenAI-compatible streamers repeat already-sent fields as null on
|
|
1625
|
-
// continuation deltas. Treat only null/undefined as absent; every other
|
|
1626
|
-
// non-string value still fails closed before entering the accumulator.
|
|
1627
|
-
if (isInvalidStreamStringField(rawName) || isInvalidStreamStringField(rawArguments)) {
|
|
1628
|
-
logInvalidToolCalls("stream", rawToolCalls);
|
|
1629
|
-
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
if (isInvalidStreamStringField(tc.id)) {
|
|
1633
|
-
logInvalidToolCalls("stream", rawToolCalls);
|
|
1634
|
-
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1682
|
+
// This is upstream JSON, so every field is validated before it is stored: a
|
|
1683
|
+
// malformed value must fail closed through the #1325 channel here rather than
|
|
1684
|
+
// escaping later as a TypeError from string handling at flush time.
|
|
1685
|
+
const rawFunction = rawToolCall.function;
|
|
1686
|
+
if (rawFunction !== undefined && rawFunction !== null && !isRecord(rawFunction)) {
|
|
1687
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1688
|
+
reason: "tool_call_function_not_object",
|
|
1689
|
+
callIndex,
|
|
1690
|
+
valueType: Array.isArray(rawFunction) ? "array" : typeof rawFunction,
|
|
1691
|
+
};
|
|
1692
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1693
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1635
1694
|
}
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1695
|
+
const fnRecord = isRecord(rawFunction) ? rawFunction : undefined;
|
|
1696
|
+
const rawName = fnRecord?.name;
|
|
1697
|
+
const rawArguments = fnRecord?.arguments;
|
|
1698
|
+
const rawId = rawToolCall.id;
|
|
1699
|
+
const idDelta = typeof rawId === "string" ? rawId : "";
|
|
1700
|
+
const rawIndex = rawToolCall.index;
|
|
1701
|
+
|
|
1702
|
+
// Resolve the pending call BEFORE judging the fields. Some OpenAI-compatible
|
|
1703
|
+
// streamers repeat an already-sent field as a non-string placeholder on a
|
|
1704
|
+
// continuation delta; judging first meant the whole stream died with a 502 even
|
|
1705
|
+
// though the value being repeated was already held in canonical form.
|
|
1706
|
+
const key = typeof rawIndex === "number"
|
|
1707
|
+
? `i:${rawIndex}`
|
|
1708
|
+
: idDelta
|
|
1709
|
+
? `id:${idDelta}`
|
|
1640
1710
|
: pendingToolCalls[pendingToolCalls.length - 1]?.key;
|
|
1641
1711
|
let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
|
|
1642
|
-
if (!call &&
|
|
1712
|
+
if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta);
|
|
1643
1713
|
if (!call) {
|
|
1644
|
-
call = {
|
|
1714
|
+
call = {
|
|
1715
|
+
key: key ?? `seq:${pendingToolCalls.length}`,
|
|
1716
|
+
id: "",
|
|
1717
|
+
name: "",
|
|
1718
|
+
args: "",
|
|
1719
|
+
argsBytes: 0,
|
|
1720
|
+
sawArgumentsString: false,
|
|
1721
|
+
};
|
|
1645
1722
|
pendingToolCalls.push(call);
|
|
1646
1723
|
budget.openCall(call.key);
|
|
1647
1724
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1725
|
+
|
|
1726
|
+
// Tolerance is per FIELD, keyed on that field's own provenance. A canonical name
|
|
1727
|
+
// says nothing about whether `arguments` was ever sent as a string, so it cannot
|
|
1728
|
+
// authorize a malformed arguments value — that would silently drop a real
|
|
1729
|
+
// argument payload the model intended to send.
|
|
1730
|
+
const rejection: InvalidToolCallDiagnostic | undefined =
|
|
1731
|
+
isInvalidStreamStringField(rawName) && call.name.trim() === ""
|
|
1732
|
+
? { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawName }
|
|
1733
|
+
: isInvalidStreamStringField(rawArguments) && !call.sawArgumentsString
|
|
1734
|
+
? { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawArguments }
|
|
1735
|
+
: isInvalidStreamStringField(rawId) && call.id === ""
|
|
1736
|
+
? { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawId }
|
|
1737
|
+
: undefined;
|
|
1738
|
+
if (rejection) {
|
|
1739
|
+
logInvalidToolCalls("stream", rawToolCalls, rejection);
|
|
1740
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, rejection));
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (idDelta && !call.id) call.id = idDelta;
|
|
1744
|
+
if (typeof rawName === "string" && rawName && !call.name) call.name = rawName;
|
|
1745
|
+
if (typeof rawArguments === "string") call.sawArgumentsString = true;
|
|
1746
|
+
// Tool-call deltas are BUFFERED until a terminal signal, so this adapter can
|
|
1747
|
+
// consume upstream frames for a long time while yielding nothing. The Responses
|
|
1748
|
+
// bridge reads adapter activity, not socket activity, so a model that streams a
|
|
1749
|
+
// large argument payload looks identical to a hung upstream and the stall
|
|
1750
|
+
// watchdog can abort a turn that was progressing normally.
|
|
1751
|
+
//
|
|
1752
|
+
// Found while investigating #2156, but it is NOT that bug: a stall abort emits
|
|
1753
|
+
// `response.incomplete` with `upstream_stall_timeout` from the bridge, whereas
|
|
1754
|
+
// that report shows the adapter's own end-of-stream error after `reader.read()`
|
|
1755
|
+
// returned EOF with tool calls still pending. Different path, different frame.
|
|
1756
|
+
//
|
|
1757
|
+
// A heartbeat is invisible downstream — the bridge consumes it to re-arm the
|
|
1758
|
+
// watchdog and emits nothing — which is the same remedy the Cursor, Anthropic,
|
|
1759
|
+
// Google, and Kiro adapters already use for their own silent phases.
|
|
1760
|
+
yield { type: "heartbeat" };
|
|
1761
|
+
if (typeof rawArguments === "string" && rawArguments) {
|
|
1651
1762
|
const previousBytes = call.argsBytes;
|
|
1652
|
-
const nextBytes = previousBytes + budgetEncoder.encode(
|
|
1763
|
+
const nextBytes = previousBytes + budgetEncoder.encode(rawArguments).byteLength;
|
|
1653
1764
|
const scope = { kind: "tool_args" as const, callId: call.key };
|
|
1654
1765
|
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
1655
1766
|
try {
|
|
1656
|
-
call.args +=
|
|
1767
|
+
call.args += rawArguments;
|
|
1657
1768
|
reservation.commitRetained();
|
|
1658
1769
|
budget.releaseRetained(previousBytes, scope);
|
|
1659
1770
|
call.argsBytes = nextBytes;
|
|
@@ -1752,8 +1863,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1752
1863
|
}
|
|
1753
1864
|
},
|
|
1754
1865
|
|
|
1755
|
-
async parseResponse(
|
|
1756
|
-
|
|
1866
|
+
async parseResponse(
|
|
1867
|
+
response: Response,
|
|
1868
|
+
budget: TranslatorBudget,
|
|
1869
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1870
|
+
): Promise<AdapterEvent[]> {
|
|
1871
|
+
let parsed: unknown;
|
|
1872
|
+
try {
|
|
1873
|
+
parsed = await response.json();
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
tierMetadata?.markResponseUnparseable();
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1879
|
+
tierMetadata?.markResponseUnparseable();
|
|
1880
|
+
throw new Error("upstream response was not a JSON object");
|
|
1881
|
+
}
|
|
1882
|
+
const json = parsed as Record<string, unknown>;
|
|
1883
|
+
if (Object.hasOwn(json, "service_tier")) {
|
|
1884
|
+
tierMetadata?.observeResponseServiceTier(json.service_tier);
|
|
1885
|
+
}
|
|
1757
1886
|
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
1758
1887
|
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
1759
1888
|
try {
|
|
@@ -11,6 +11,7 @@ import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
|
11
11
|
import { modelRecordValue } from "../reasoning-effort";
|
|
12
12
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
13
13
|
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
|
|
14
|
+
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
14
15
|
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
15
16
|
import {
|
|
16
17
|
createAdapterTierMetadata,
|
|
@@ -185,6 +186,28 @@ function stripUnsupportedReasoningParams(body: unknown): unknown {
|
|
|
185
186
|
return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* GPT-5.6 replaced the legacy 24-hour retention field with `prompt_cache_options.ttl`, and the
|
|
191
|
+
* ChatGPT backend 400s the whole request when the retired field is present (issue #2092).
|
|
192
|
+
*
|
|
193
|
+
* The retired field is NOT translated to the replacement: 5.6 carries a different TTL contract,
|
|
194
|
+
* and implicit caching still applies when the caller sent no replacement options. Inventing a
|
|
195
|
+
* value here would silently change a caching decision the caller never made.
|
|
196
|
+
*
|
|
197
|
+
* Deliberately narrow on both axes, because a wider strip is a behavior change rather than a fix:
|
|
198
|
+
* only the gpt-5.6 family (an older model may still honor the field), and only on the canonical
|
|
199
|
+
* ChatGPT backend, which is the deployment that rejects it. Matching is exact-or-dashed-prefix so
|
|
200
|
+
* a future `gpt-5.60` is not swept up by a bare `startsWith`.
|
|
201
|
+
*/
|
|
202
|
+
function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): unknown {
|
|
203
|
+
if (!isPlainObject(body)) return body;
|
|
204
|
+
if (typeof modelId !== "string") return body;
|
|
205
|
+
if (modelId !== "gpt-5.6" && !modelId.startsWith("gpt-5.6-")) return body;
|
|
206
|
+
if (!Object.hasOwn(body, "prompt_cache_retention")) return body;
|
|
207
|
+
const { prompt_cache_retention: _retention, ...rest } = body;
|
|
208
|
+
return rest;
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
/**
|
|
189
212
|
* A false model capability prevents Codex from emitting summary fields after the catalog refresh.
|
|
190
213
|
* Strip them here as well so an already-running client with a stale catalog cannot keep sending an
|
|
@@ -1468,6 +1491,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1468
1491
|
|
|
1469
1492
|
const forward = provider.authMode === "forward";
|
|
1470
1493
|
let convertedRoutedCustomToolNames: Set<string> | undefined;
|
|
1494
|
+
let convertedRoutedToolSearchNames: Set<string> | undefined;
|
|
1471
1495
|
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
|
|
1472
1496
|
let outBody = stripPreviousResponseId(
|
|
1473
1497
|
parsed._rawBody,
|
|
@@ -1491,6 +1515,11 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1491
1515
|
}
|
|
1492
1516
|
if (forward) {
|
|
1493
1517
|
outBody = stripUnsupportedForwardParams(outBody);
|
|
1518
|
+
// Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
|
|
1519
|
+
// third-party forward gateway may still accept it, so this must not be widened.
|
|
1520
|
+
if (isCanonicalOpenAiForwardProvider(provider)) {
|
|
1521
|
+
outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
|
|
1522
|
+
}
|
|
1494
1523
|
} else {
|
|
1495
1524
|
outBody = preferConfiguredHostedTools(
|
|
1496
1525
|
outBody,
|
|
@@ -1522,6 +1551,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1522
1551
|
outBody = rewritten.body;
|
|
1523
1552
|
convertedRoutedCustomToolNames = rewritten.names;
|
|
1524
1553
|
}
|
|
1554
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
1555
|
+
// Run after custom-tool lowering so the search compatibility layer can choose a
|
|
1556
|
+
// collision-free public function name against the final routed function catalog.
|
|
1557
|
+
const rewritten = rewriteRoutedToolSearchForUpstream(outBody);
|
|
1558
|
+
outBody = rewritten.body;
|
|
1559
|
+
convertedRoutedToolSearchNames = rewritten.names;
|
|
1560
|
+
}
|
|
1525
1561
|
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true })))))));
|
|
1526
1562
|
const finalBody = stripDisabledReasoningSummaries(
|
|
1527
1563
|
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
|
|
@@ -1549,6 +1585,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1549
1585
|
body,
|
|
1550
1586
|
releaseBodyObservation,
|
|
1551
1587
|
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
|
|
1588
|
+
...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
|
|
1552
1589
|
...(tierLog ? { tierLog } : {}),
|
|
1553
1590
|
};
|
|
1554
1591
|
},
|
|
@@ -135,7 +135,7 @@ export function buildNonOpenAIToolCatalogNudgeForTools(
|
|
|
135
135
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
136
136
|
toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
|
|
137
137
|
): string | undefined {
|
|
138
|
-
const visible = tools?.filter(toolChoiceToolPredicate(toolChoice));
|
|
138
|
+
const visible = tools?.filter(toolChoiceToolPredicate(toolChoice, tools));
|
|
139
139
|
const visibleNames = visible?.map(toWireName);
|
|
140
140
|
// Decide code mode from the tool OBJECTS, while the `freeform` flag still exists — reducing
|
|
141
141
|
// to wire names first throws away the only thing that distinguishes Codex's JavaScript
|
package/src/bridge.ts
CHANGED
|
@@ -167,7 +167,7 @@ export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
|
|
|
167
167
|
export function bridgeToResponsesSSE(
|
|
168
168
|
events: AsyncIterable<AdapterEvent>,
|
|
169
169
|
modelId: string,
|
|
170
|
-
toolNsMap?: Map<string, { namespace: string; name: string }>,
|
|
170
|
+
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>,
|
|
171
171
|
freeformToolNames?: Set<string>,
|
|
172
172
|
toolSearchToolNames?: Set<string>,
|
|
173
173
|
onCancel?: () => void,
|
|
@@ -1050,7 +1050,9 @@ export function bridgeToResponsesSSE(
|
|
|
1050
1050
|
}
|
|
1051
1051
|
const ns = mapped?.namespace;
|
|
1052
1052
|
const toolSearch = toolSearchToolNames?.has(realName) ?? false;
|
|
1053
|
-
const freeform = !toolSearch && (
|
|
1053
|
+
const freeform = !toolSearch && (mapped
|
|
1054
|
+
? mapped.freeform === true
|
|
1055
|
+
: (freeformToolNames?.has(realName) ?? false));
|
|
1054
1056
|
const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
|
|
1055
1057
|
const item = toolSearch
|
|
1056
1058
|
? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
|
|
@@ -1451,7 +1453,7 @@ function buildResponseJSONWithBudget(
|
|
|
1451
1453
|
modelId: string,
|
|
1452
1454
|
options?: {
|
|
1453
1455
|
hideThinkingSummary?: boolean;
|
|
1454
|
-
toolNsMap?: Map<string, { namespace: string; name: string }>;
|
|
1456
|
+
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
1455
1457
|
/** Request-visible tool names. When present, an upstream call outside this set fails closed. */
|
|
1456
1458
|
declaredToolNames?: ReadonlySet<string>;
|
|
1457
1459
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
@@ -1632,7 +1634,9 @@ function buildResponseJSONWithBudget(
|
|
|
1632
1634
|
const realName = mapped?.name ?? currentToolCallName;
|
|
1633
1635
|
const ns = mapped?.namespace;
|
|
1634
1636
|
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1635
|
-
const freeform = !toolSearch && (
|
|
1637
|
+
const freeform = !toolSearch && (mapped
|
|
1638
|
+
? mapped.freeform === true
|
|
1639
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
1636
1640
|
// #1611: same integral-float repair as the streaming path. Keyed by the wire name
|
|
1637
1641
|
// the request declared, which is the pre-namespace-mapping `currentToolCallName`.
|
|
1638
1642
|
const coercedArgs = coerceIntegerToolArguments(
|
|
@@ -1796,7 +1800,9 @@ function buildResponseJSONWithBudget(
|
|
|
1796
1800
|
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1797
1801
|
const realName = mapped?.name ?? currentToolCallName;
|
|
1798
1802
|
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1799
|
-
const freeform = !toolSearch && (
|
|
1803
|
+
const freeform = !toolSearch && (mapped
|
|
1804
|
+
? mapped.freeform === true
|
|
1805
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
1800
1806
|
if (!freeform && !toolSearch) {
|
|
1801
1807
|
flushToolCall("incomplete");
|
|
1802
1808
|
errorEvent = {
|
package/src/cli/doctor.ts
CHANGED
|
@@ -25,6 +25,11 @@ import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHome
|
|
|
25
25
|
import { scanCodexAgentRolesWithTomlModelFallback } from "../codex/subagent-model-fallback";
|
|
26
26
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
27
27
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
28
|
+
import {
|
|
29
|
+
inspectAbandonedResponseStateTemps,
|
|
30
|
+
reclaimAbandonedResponseStateTemps,
|
|
31
|
+
type ResponseStateTempRecoveryResult,
|
|
32
|
+
} from "../responses/state";
|
|
28
33
|
import {
|
|
29
34
|
CodexUserIdentityRefusal,
|
|
30
35
|
probeCodexCoordinatorNamespace,
|
|
@@ -678,6 +683,57 @@ export async function fetchServiceMemory(
|
|
|
678
683
|
|
|
679
684
|
const mb = (bytes: number): string => `${Math.round(bytes / (1024 * 1024))}MB`;
|
|
680
685
|
|
|
686
|
+
export const RECLAIM_RESPONSE_TEMPS_FLAG = "--reclaim-response-temps";
|
|
687
|
+
/** Matches the dry run's entry bound so report and reclaim agree on a large backlog. */
|
|
688
|
+
const RESPONSE_TEMP_RECLAIM_MAX_CLEANUPS = 4_096;
|
|
689
|
+
/** Names the subsystem: other components mint temps with the same shape and are not covered. */
|
|
690
|
+
const CLEAN_RESPONSE_TEMP_LINE = " ok No abandoned response-state temp files.";
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Render the abandoned-temp section (testable without console capture).
|
|
694
|
+
*
|
|
695
|
+
* Report is the DEFAULT and reclaim is opt-in: `doctor` is a diagnostic an operator runs
|
|
696
|
+
* to understand a machine, so deleting files as a side effect of asking a question is the
|
|
697
|
+
* wrong default even for cache files.
|
|
698
|
+
*
|
|
699
|
+
* Counts come from `eligible`/`eligibleBytes`, never `matched`: `matched` is incremented
|
|
700
|
+
* before the file-type, age, boot-floor, and liveness gates, so reporting it would tell an
|
|
701
|
+
* operator that live-pid temps and young temps are "abandoned".
|
|
702
|
+
*/
|
|
703
|
+
export function formatResponseTempLines(
|
|
704
|
+
result: ResponseStateTempRecoveryResult,
|
|
705
|
+
reclaimed: boolean,
|
|
706
|
+
): string[] {
|
|
707
|
+
if (reclaimed) {
|
|
708
|
+
if (result.removed === 0 && result.failed === 0) return [CLEAN_RESPONSE_TEMP_LINE];
|
|
709
|
+
const lines = [` ok Reclaimed ${result.removed} abandoned response-state temp file(s), ${mb(result.bytesRemoved)} freed.`];
|
|
710
|
+
if (result.failed > 0) {
|
|
711
|
+
// Never "retried automatically": this command exists for the operator whose proxy will
|
|
712
|
+
// NOT start, and in that state nothing retries anything.
|
|
713
|
+
lines.push(` !! ${result.failed} file(s) could not be removed (in use or locked). Retried on the next reclaim — automatically while the proxy runs, otherwise re-run this command.`);
|
|
714
|
+
}
|
|
715
|
+
// `truncated`, not `eligible > removed + failed`: outside a dry run every eligible entry
|
|
716
|
+
// is unlinked or failed on the same iteration it is counted, so those two are always
|
|
717
|
+
// equal and the comparison never fired. An operator with a backlog past the budget was
|
|
718
|
+
// told the reclaim had finished.
|
|
719
|
+
if (result.truncated) {
|
|
720
|
+
lines.push(" !! Cleanup budget reached; files remain. Run the command again to continue.");
|
|
721
|
+
}
|
|
722
|
+
return lines;
|
|
723
|
+
}
|
|
724
|
+
if (result.eligible === 0) return [CLEAN_RESPONSE_TEMP_LINE];
|
|
725
|
+
const lines = [
|
|
726
|
+
` !! ${result.eligible} abandoned response-state temp file(s), ${mb(result.eligibleBytes)} reclaimable.`,
|
|
727
|
+
" These are interrupted snapshot writes (continuation cache only) and are safe to remove.",
|
|
728
|
+
" Reclaim them with: ocx doctor --reclaim-response-temps",
|
|
729
|
+
];
|
|
730
|
+
// The dry run skips the cleanup budget but is still bounded by the entry cap, so a large
|
|
731
|
+
// enough backlog makes this a floor rather than a total. Say so instead of letting an
|
|
732
|
+
// operator size the problem from a truncated count.
|
|
733
|
+
if (result.truncated) lines.push(" Scan stopped at its entry budget; the real total is higher.");
|
|
734
|
+
return lines;
|
|
735
|
+
}
|
|
736
|
+
|
|
681
737
|
/** Render the doctor "Memory / runtime" section lines (testable without console capture). */
|
|
682
738
|
export function formatServiceMemoryLines(report: ServiceMemoryReport): string[] {
|
|
683
739
|
const lines: string[] = [];
|
|
@@ -805,6 +861,26 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
805
861
|
console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
|
|
806
862
|
}
|
|
807
863
|
|
|
864
|
+
// Runs without the proxy on purpose: the worst accumulation happens when the proxy will
|
|
865
|
+
// not start, which is exactly when the in-process periodic reclaim never ticks.
|
|
866
|
+
const reclaimTemps = args.includes(RECLAIM_RESPONSE_TEMPS_FLAG);
|
|
867
|
+
console.log("\nResponse-state temp files");
|
|
868
|
+
// A typo must not silently degrade into "nothing to reclaim" — the operator would read the
|
|
869
|
+
// report as an answer to a question they never actually asked.
|
|
870
|
+
for (const arg of args) {
|
|
871
|
+
if (arg !== RECLAIM_RESPONSE_TEMPS_FLAG && /^--reclaim/.test(arg)) {
|
|
872
|
+
console.log(` !! Unrecognized flag ${arg}; did you mean ${RECLAIM_RESPONSE_TEMPS_FLAG}? Reporting only.`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
for (const line of formatResponseTempLines(
|
|
876
|
+
// The reclaim budget matches the report budget: a report bounded by entries and a removal
|
|
877
|
+
// bounded by a smaller cleanup cap would tell an operator 816 and then silently free 512.
|
|
878
|
+
reclaimTemps
|
|
879
|
+
? reclaimAbandonedResponseStateTemps({ maxCleanups: RESPONSE_TEMP_RECLAIM_MAX_CLEANUPS })
|
|
880
|
+
: inspectAbandonedResponseStateTemps(),
|
|
881
|
+
reclaimTemps,
|
|
882
|
+
)) console.log(line);
|
|
883
|
+
|
|
808
884
|
const orcaHome = collectOrcaCodexHomeDiagnostic();
|
|
809
885
|
console.log("\nCodex app home targeting");
|
|
810
886
|
console.log(` ${orcaHome.mismatch ? "!! " : "ok "} Effective Codex home: ${orcaHome.effectiveCodexHome}`);
|
package/src/cli/help.ts
CHANGED
|
@@ -36,6 +36,8 @@ Usage:
|
|
|
36
36
|
Refresh Codex's model cache from the active catalog
|
|
37
37
|
ocx status Check proxy server status
|
|
38
38
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
39
|
+
ocx doctor --reclaim-response-temps
|
|
40
|
+
Reclaim abandoned response-state temp files (works without a running proxy)
|
|
39
41
|
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
|
|
40
42
|
ocx login <provider> OAuth or API-key provider login
|
|
41
43
|
ocx logout <provider> Remove a stored OAuth login
|