@yansigit/opencodex 2.31.2 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
package/src/images/plan.ts
CHANGED
|
@@ -48,11 +48,12 @@ export async function planImageBridge(
|
|
|
48
48
|
if (config.images?.bridgeEnabled !== true) return undefined;
|
|
49
49
|
if (!parsed._imageGeneration) return undefined;
|
|
50
50
|
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
|
|
51
|
-
const toolNames = new Set(
|
|
52
|
-
|
|
53
|
-
.filter(name => toolAllowed({ name })),
|
|
54
|
-
);
|
|
51
|
+
const toolNames = new Set([...parsed._imageGeneration.toolNames].filter(name => toolAllowed({ name })));
|
|
52
|
+
if (toolAllowed({ name: IMAGE_GEN_TOOL_NAME })) toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
55
53
|
if (toolNames.size === 0) return undefined;
|
|
54
|
+
// Responses advertises and rewrites authorized aliases to this synthetic name, so the loop
|
|
55
|
+
// must always intercept it once any image-generation name has armed the bridge.
|
|
56
|
+
toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
56
57
|
// Don't intercept for OpenAI native passthrough
|
|
57
58
|
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
58
59
|
if (host === "api.openai.com") return undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-scoped exceptions to strict managed-fragment ownership.
|
|
3
|
+
*
|
|
4
|
+
* Most clients must preserve every byte represented by their managed
|
|
5
|
+
* contribution. A client that persists runtime-derived fields back into an
|
|
6
|
+
* OpenCodex-owned fragment needs a narrower contract: name the exact paths it
|
|
7
|
+
* may rewrite, then fingerprint everything else. Keeping that policy here
|
|
8
|
+
* prevents a client quirk from weakening the shared classifier.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
OPENCODE_PROVIDER_ID,
|
|
12
|
+
type ManagedContribution,
|
|
13
|
+
type ManagedFragment,
|
|
14
|
+
} from "../clients/config-export";
|
|
15
|
+
import { canonicalContribution, fingerprint } from "./ownership";
|
|
16
|
+
|
|
17
|
+
type JsonObject = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
function isObject(value: unknown): value is JsonObject {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pathStartsWith(path: readonly string[], prefix: readonly string[]): boolean {
|
|
24
|
+
return prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Delete one nested object member and prune only the empty ancestors created by that deletion. */
|
|
28
|
+
function deletePath(root: unknown, path: readonly string[]): void {
|
|
29
|
+
if (!isObject(root) || path.length === 0) return;
|
|
30
|
+
const parents: Array<{ parent: JsonObject; key: string }> = [];
|
|
31
|
+
let cursor: JsonObject = root;
|
|
32
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
33
|
+
const key = path[index]!;
|
|
34
|
+
const next = cursor[key];
|
|
35
|
+
if (!isObject(next)) return;
|
|
36
|
+
parents.push({ parent: cursor, key });
|
|
37
|
+
cursor = next;
|
|
38
|
+
}
|
|
39
|
+
delete cursor[path[path.length - 1]!];
|
|
40
|
+
for (let index = parents.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (Object.keys(cursor).length > 0) break;
|
|
42
|
+
const { parent, key } = parents[index]!;
|
|
43
|
+
delete parent[key];
|
|
44
|
+
cursor = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cloneFragment(fragment: ManagedFragment): ManagedFragment {
|
|
49
|
+
return {
|
|
50
|
+
path: [...fragment.path],
|
|
51
|
+
value: structuredClone(fragment.value),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Paths a client is documented to derive after OpenCodex writes its block.
|
|
57
|
+
*
|
|
58
|
+
* ZCode 3.8.1 persists reasoning and output defaults for every generated
|
|
59
|
+
* model. It may also fill a context default when OpenCodex intentionally
|
|
60
|
+
* omitted one; an authoritative context emitted by OpenCodex remains
|
|
61
|
+
* protected and is never listed here.
|
|
62
|
+
*/
|
|
63
|
+
export function refreshablePathsOf(
|
|
64
|
+
contribution: ManagedContribution,
|
|
65
|
+
): readonly (readonly string[])[] {
|
|
66
|
+
if (contribution.clientId !== "zcode") return [];
|
|
67
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
68
|
+
candidate.path.length === 2
|
|
69
|
+
&& candidate.path[0] === "provider"
|
|
70
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
71
|
+
));
|
|
72
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return [];
|
|
73
|
+
|
|
74
|
+
const paths: string[][] = [];
|
|
75
|
+
for (const modelId of Object.keys(fragment.value.models).sort()) {
|
|
76
|
+
const entry = fragment.value.models[modelId];
|
|
77
|
+
if (!isObject(entry)) continue;
|
|
78
|
+
const base = [...fragment.path, "models", modelId];
|
|
79
|
+
paths.push([...base, "reasoning"]);
|
|
80
|
+
paths.push([...base, "limit", "output"]);
|
|
81
|
+
const limit = entry.limit;
|
|
82
|
+
if (!isObject(limit) || typeof limit.context !== "number") {
|
|
83
|
+
paths.push([...base, "limit", "context"]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return paths;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validRefreshablePaths(
|
|
90
|
+
contribution: ManagedContribution,
|
|
91
|
+
value: unknown,
|
|
92
|
+
): value is readonly (readonly string[])[] {
|
|
93
|
+
if (contribution.clientId !== "zcode" || !Array.isArray(value) || value.length === 0) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const fragment = contribution.fragments.find(candidate => (
|
|
97
|
+
candidate.path.length === 2
|
|
98
|
+
&& candidate.path[0] === "provider"
|
|
99
|
+
&& candidate.path[1] === OPENCODE_PROVIDER_ID
|
|
100
|
+
));
|
|
101
|
+
if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return false;
|
|
102
|
+
|
|
103
|
+
const modelIds = new Set(Object.keys(fragment.value.models));
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
return value.every(path => {
|
|
106
|
+
if (!Array.isArray(path) || !path.every(part => typeof part === "string")) return false;
|
|
107
|
+
const modelId = path[3];
|
|
108
|
+
const isReasoning = path.length === 5 && path[4] === "reasoning";
|
|
109
|
+
const isLimitDefault = path.length === 6
|
|
110
|
+
&& path[4] === "limit"
|
|
111
|
+
&& (path[5] === "output" || path[5] === "context");
|
|
112
|
+
const key = path.join("\u0000");
|
|
113
|
+
if (
|
|
114
|
+
path[0] !== "provider"
|
|
115
|
+
|| path[1] !== OPENCODE_PROVIDER_ID
|
|
116
|
+
|| path[2] !== "models"
|
|
117
|
+
|| typeof modelId !== "string"
|
|
118
|
+
|| !modelIds.has(modelId)
|
|
119
|
+
|| (!isReasoning && !isLimitDefault)
|
|
120
|
+
|| seen.has(key)
|
|
121
|
+
) return false;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Fingerprint a contribution after removing only its explicitly refreshable paths. */
|
|
128
|
+
export function protectedContributionFingerprint(
|
|
129
|
+
contribution: ManagedContribution,
|
|
130
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
131
|
+
): string {
|
|
132
|
+
const fragments = contribution.fragments.map(cloneFragment);
|
|
133
|
+
for (const refreshablePath of refreshablePaths) {
|
|
134
|
+
for (const fragment of fragments) {
|
|
135
|
+
if (!pathStartsWith(refreshablePath, fragment.path)) continue;
|
|
136
|
+
deletePath(fragment.value, refreshablePath.slice(fragment.path.length));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return fingerprint(canonicalContribution({ ...contribution, fragments }));
|
|
141
|
+
}
|
|
@@ -41,6 +41,16 @@ export interface OwnershipRecord {
|
|
|
41
41
|
fileFingerprint: string;
|
|
42
42
|
/** Hash of our contribution — detects catalog/port drift. */
|
|
43
43
|
blockFingerprint: string;
|
|
44
|
+
/**
|
|
45
|
+
* Hash of the fields the client must not rewrite. Present only when a
|
|
46
|
+
* client has explicitly declared runtime-derived paths below.
|
|
47
|
+
*/
|
|
48
|
+
protectedBlockFingerprint?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Exact document paths a client may derive after apply. These are recorded
|
|
51
|
+
* per operation so later catalog changes cannot widen an older grant.
|
|
52
|
+
*/
|
|
53
|
+
refreshablePaths?: readonly (readonly string[])[];
|
|
44
54
|
/** The exact paths we own. Removal touches these and nothing else. */
|
|
45
55
|
fragmentPaths: readonly (readonly string[])[];
|
|
46
56
|
/**
|
|
@@ -13,6 +13,11 @@ import type { OcxConfig } from "../types";
|
|
|
13
13
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
14
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
15
|
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
16
|
+
import {
|
|
17
|
+
protectedContributionFingerprint,
|
|
18
|
+
refreshablePathsOf,
|
|
19
|
+
validRefreshablePaths,
|
|
20
|
+
} from "./ownership-policy";
|
|
16
21
|
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
17
22
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
18
23
|
|
|
@@ -106,10 +111,10 @@ export function blockedContainerPath(
|
|
|
106
111
|
* values lets another integration or a user add a sibling without blocking a
|
|
107
112
|
* later refresh, while a change inside our block still fails closed.
|
|
108
113
|
*/
|
|
109
|
-
function
|
|
114
|
+
function recordedContribution(
|
|
110
115
|
doc: unknown,
|
|
111
116
|
record: OwnershipRecord,
|
|
112
|
-
):
|
|
117
|
+
): ManagedContribution | null {
|
|
113
118
|
if (
|
|
114
119
|
!Array.isArray(record.fragmentPaths)
|
|
115
120
|
|| record.fragmentPaths.length === 0
|
|
@@ -125,10 +130,44 @@ function recordedFragmentFingerprint(
|
|
|
125
130
|
if (value === undefined) return null;
|
|
126
131
|
fragments.push({ path, value });
|
|
127
132
|
}
|
|
128
|
-
return
|
|
133
|
+
return {
|
|
129
134
|
clientId: record.clientId,
|
|
130
135
|
fragments,
|
|
131
|
-
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Prove that every protected field still matches what OpenCodex wrote.
|
|
141
|
+
*
|
|
142
|
+
* New records carry an operation-scoped protected fingerprint and the exact
|
|
143
|
+
* paths excluded from it. Legacy records can recover only when the desired
|
|
144
|
+
* contribution has not moved since apply; otherwise catalog drift and a
|
|
145
|
+
* foreign edit are indistinguishable, so the classifier keeps failing closed.
|
|
146
|
+
*/
|
|
147
|
+
function recordedBlockIsOwned(
|
|
148
|
+
doc: unknown,
|
|
149
|
+
record: OwnershipRecord,
|
|
150
|
+
desired: ManagedContribution,
|
|
151
|
+
): boolean {
|
|
152
|
+
const observed = recordedContribution(doc, record);
|
|
153
|
+
if (!observed) return false;
|
|
154
|
+
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
typeof record.protectedBlockFingerprint === "string"
|
|
158
|
+
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
159
|
+
&& record.refreshablePaths.length > 0
|
|
160
|
+
) {
|
|
161
|
+
return protectedContributionFingerprint(observed, record.refreshablePaths)
|
|
162
|
+
=== record.protectedBlockFingerprint;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
166
|
+
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
167
|
+
const legacyPaths = refreshablePathsOf(desired);
|
|
168
|
+
return legacyPaths.length > 0
|
|
169
|
+
&& protectedContributionFingerprint(observed, legacyPaths)
|
|
170
|
+
=== protectedContributionFingerprint(desired, legacyPaths);
|
|
132
171
|
}
|
|
133
172
|
|
|
134
173
|
/**
|
|
@@ -191,7 +230,7 @@ export function classifyIntegration(input: {
|
|
|
191
230
|
* conflict no matter what the rest of the file looks like, so the sibling-
|
|
192
231
|
* edit exemption below can never mask it.
|
|
193
232
|
*/
|
|
194
|
-
if (
|
|
233
|
+
if (!recordedBlockIsOwned(input.parsed, input.record, input.contribution)) {
|
|
195
234
|
return { state: "conflict", reason: "foreign-edit" };
|
|
196
235
|
}
|
|
197
236
|
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
@@ -16,6 +16,7 @@ import { isLoopbackHostname } from "../codex/inject";
|
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
18
18
|
import { fingerprint, canonicalContribution, fragmentPathsOf, type OwnershipRecord } from "./ownership";
|
|
19
|
+
import { protectedContributionFingerprint, refreshablePathsOf } from "./ownership-policy";
|
|
19
20
|
import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
20
21
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
|
|
21
22
|
import { classifyIntegration, exportContextOf } from "./state";
|
|
@@ -353,12 +354,17 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
|
|
|
353
354
|
opId, clientId, kind: classified.state === "stale" ? "refresh" : "apply", at, configPath,
|
|
354
355
|
snapshot, resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
|
|
355
356
|
};
|
|
357
|
+
const refreshablePaths = refreshablePathsOf(contribution);
|
|
356
358
|
return commit({
|
|
357
359
|
io, store, clientId, configPath, before, nextText: text, state: "current",
|
|
358
360
|
priorRecord: record,
|
|
359
361
|
record: {
|
|
360
362
|
clientId, configPath, fileFingerprint: fingerprint(text),
|
|
361
363
|
blockFingerprint: fingerprint(canonicalContribution(contribution)),
|
|
364
|
+
...(refreshablePaths.length > 0 ? {
|
|
365
|
+
protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
|
|
366
|
+
refreshablePaths,
|
|
367
|
+
} : {}),
|
|
362
368
|
fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
|
|
363
369
|
appliedAt: at, opId,
|
|
364
370
|
},
|
|
@@ -3,7 +3,7 @@ import { jcsStringify } from "../digest";
|
|
|
3
3
|
import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
|
|
4
4
|
|
|
5
5
|
const CLOSED_KEYS = new Set([
|
|
6
|
-
"wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
|
|
6
|
+
"wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.commandCodeProjectContext", "wire.modelSuffixMode",
|
|
7
7
|
"auth.mode", "auth.transport",
|
|
8
8
|
"responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.fastWireKind", "responses.fastWireValue", "responses.snapshotRepair", "responses.itemIdRepair",
|
|
9
9
|
"limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
|
package/src/lib/bounded-body.ts
CHANGED
|
@@ -103,6 +103,16 @@ function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, r
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function cancelBodyWithoutWaiting(body: ReadableStream<Uint8Array>, reason?: unknown): void {
|
|
107
|
+
// A signal can already be aborted before a reader is attached. Still settle the
|
|
108
|
+
// original body so fetch-backed streams cannot retain a rejected read in that gap.
|
|
109
|
+
try {
|
|
110
|
+
void body.cancel(reason).catch(() => undefined);
|
|
111
|
+
} catch {
|
|
112
|
+
// A locked or non-conforming stream may throw synchronously from cancel().
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
/**
|
|
107
117
|
* Consume the original response body as raw bytes under a strict memory ceiling.
|
|
108
118
|
*
|
|
@@ -208,9 +218,11 @@ export async function readBoundedResponseBody(
|
|
|
208
218
|
options: BoundedBodyOptions = {},
|
|
209
219
|
): Promise<BoundedBodyResult> {
|
|
210
220
|
const signal = options.signal;
|
|
211
|
-
if (signal?.aborted) throw signal.reason;
|
|
212
|
-
|
|
213
221
|
const body = response.body;
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
if (body) cancelBodyWithoutWaiting(body, signal.reason);
|
|
224
|
+
throw signal.reason;
|
|
225
|
+
}
|
|
214
226
|
if (!body) {
|
|
215
227
|
return {
|
|
216
228
|
text: "",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { loadConfig
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { readRuntimePort } from "../config/process-state";
|
|
3
4
|
import { configuredAdminToken } from "./admin-secrets";
|
|
4
5
|
|
|
5
6
|
export function isProcessAlive(pid: number): boolean {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { reconcileCodexReauthState } from "../codex/account-runtime-state";
|
|
7
7
|
import { reconcileCatalogWarningMemos } from "../codex/catalog/aggregation";
|
|
8
8
|
import { reconcileProviderFetchWarnings } from "../codex/catalog/provider-fetch";
|
|
9
|
+
import { reconcileModelMetadataSnapshot } from "../codex/catalog/model-metadata";
|
|
9
10
|
import { reconcileModelCacheGeneration } from "../codex/model-cache";
|
|
10
11
|
import { reconcilePoolRotationState } from "../codex/pool-rotation";
|
|
11
12
|
import { reconcileCodexQuotaAccounts } from "../codex/quota";
|
|
@@ -103,6 +104,7 @@ export const STATE_STORE_REGISTRATIONS = [
|
|
|
103
104
|
{ name: "provider-quota-history", reconcileGeneration: reconcileProviderAccountQuotaRows },
|
|
104
105
|
{ name: "codex-routing-health", reconcileGeneration: reconcileCodexRoutingHealth },
|
|
105
106
|
{ name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration },
|
|
107
|
+
{ name: "model-metadata-snapshot", reconcileGeneration: reconcileModelMetadataSnapshot },
|
|
106
108
|
{ name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState },
|
|
107
109
|
{ name: "combo-rotation", reconcileGeneration: reconcileComboRotationState },
|
|
108
110
|
{ name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff },
|
|
@@ -59,6 +59,35 @@ function declaresString(schema: SchemaNode): boolean {
|
|
|
59
59
|
return Array.isArray(type) && type.includes("string");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Issue #2316: Codex advertises `multi_agent_v1__wait_agent`'s `timeout_ms` as a JSON
|
|
63
|
+
// Schema `number`, but its Rust runtime deserializes the field as `u64` and rejects
|
|
64
|
+
// `120000.0` with "invalid type: floating point `120000.0`, expected u64" before the
|
|
65
|
+
// tool ever runs. The schema lookup is not the problem — the error text comes from
|
|
66
|
+
// Codex's own deserializer, so the call reached it — the problem is that `number`
|
|
67
|
+
// alone never authorizes the integral-float repair below.
|
|
68
|
+
//
|
|
69
|
+
// This allowlist is deliberately one field wide. It names only what has a live
|
|
70
|
+
// reproduction against a real `u64`, because the repair is only unambiguous for a
|
|
71
|
+
// field that cannot legitimately hold a fraction. Generic names (`start`, `end`,
|
|
72
|
+
// `priority`, `port`) would silently rewrite a third-party tool's fractional value,
|
|
73
|
+
// so a new entry needs its own evidence, not a plausible-sounding name.
|
|
74
|
+
//
|
|
75
|
+
// Cursor's sibling `yield_time_ms` (src/adapters/cursor/tool-definitions.ts) is also
|
|
76
|
+
// declared `number`; it is NOT included here because no rejection has been captured
|
|
77
|
+
// against it. It gets its own change when it gets its own reproduction.
|
|
78
|
+
const U64_NUMBER_FIELDS = new Set(["timeout_ms"]);
|
|
79
|
+
|
|
80
|
+
// Issue #2443 / #2451: Codex Desktop's bare `wait` tool has the same schema/runtime
|
|
81
|
+
// split for `yield_time_ms` and `max_tokens`. Scope these names to that bare tool so a
|
|
82
|
+
// third-party or namespaced tool can still use fractional values legitimately.
|
|
83
|
+
// #2448 allowlisted the hyphenated `yield-time_ms`; live Grok 4.6 calls and the
|
|
84
|
+
// advertised wait schema both use the underscore form, so that name is the one
|
|
85
|
+
// with a captured u64 rejection. Cursor still uses the same underscore name on a
|
|
86
|
+
// namespaced tool; the wait-only map keeps that path byte-identical.
|
|
87
|
+
const U64_NUMBER_FIELDS_BY_TOOL = new Map<string, ReadonlySet<string>>([
|
|
88
|
+
["wait", new Set(["yield_time_ms", "max_tokens"])],
|
|
89
|
+
]);
|
|
90
|
+
|
|
62
91
|
/** True when the node accepts a JSON number (`integer` or `number`), so a numeric
|
|
63
92
|
* value is already schema-valid and must not be rewritten into a string. */
|
|
64
93
|
function declaresNumeric(schema: SchemaNode): boolean {
|
|
@@ -118,7 +147,14 @@ interface CoerceResult {
|
|
|
118
147
|
changed: boolean;
|
|
119
148
|
}
|
|
120
149
|
|
|
121
|
-
function coerceValue(
|
|
150
|
+
function coerceValue(
|
|
151
|
+
value: unknown,
|
|
152
|
+
schema: SchemaNode | undefined,
|
|
153
|
+
root: SchemaNode,
|
|
154
|
+
depth: number,
|
|
155
|
+
toolName?: string,
|
|
156
|
+
propertyName?: string,
|
|
157
|
+
): CoerceResult {
|
|
122
158
|
// A hostile or deeply nested schema must not blow the stack.
|
|
123
159
|
if (depth > 64) return { value, changed: false };
|
|
124
160
|
const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
|
|
@@ -126,7 +162,19 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
126
162
|
if (typeof value === "number") {
|
|
127
163
|
if (!resolved) return { value, changed: false };
|
|
128
164
|
const branches = compositionBranches(resolved);
|
|
129
|
-
|
|
165
|
+
// #2316: a known Codex-native u64 field counts as integer-declared even when the
|
|
166
|
+
// advertised schema says `number`, but only when the field really is numeric —
|
|
167
|
+
// an allowlisted name over a string-typed field is a disagreement, not a repair.
|
|
168
|
+
const nativeU64Field = propertyName !== undefined
|
|
169
|
+
&& (
|
|
170
|
+
U64_NUMBER_FIELDS.has(propertyName)
|
|
171
|
+
|| U64_NUMBER_FIELDS_BY_TOOL.get(toolName ?? "")?.has(propertyName) === true
|
|
172
|
+
);
|
|
173
|
+
const nativeU64Declared = nativeU64Field
|
|
174
|
+
&& (declaresNumeric(resolved) || branches.some(declaresNumeric));
|
|
175
|
+
const integerDeclared = declaresInteger(resolved)
|
|
176
|
+
|| branches.some(declaresInteger)
|
|
177
|
+
|| nativeU64Declared;
|
|
130
178
|
if (!integerDeclared && safelyIntegral(value)) {
|
|
131
179
|
// Issue #1938: a bare integer in a string-only field has exactly one faithful
|
|
132
180
|
// string reading. A field that also accepts a numeric type keeps the number.
|
|
@@ -148,7 +196,9 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
148
196
|
const itemSchema = resolved ? asSchema(resolved.items) : undefined;
|
|
149
197
|
let changed = false;
|
|
150
198
|
const next = value.map(entry => {
|
|
151
|
-
|
|
199
|
+
// Array items have no property name of their own; passing the array's key would
|
|
200
|
+
// let `timeout_ms: [1.5]` inherit the allowlist. Items are judged by schema only.
|
|
201
|
+
const result = coerceValue(entry, itemSchema, root, depth + 1, toolName);
|
|
152
202
|
if (result.changed) changed = true;
|
|
153
203
|
return result.value;
|
|
154
204
|
});
|
|
@@ -164,7 +214,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
164
214
|
const next: Record<string, unknown> = {};
|
|
165
215
|
for (const [key, entry] of Object.entries(object)) {
|
|
166
216
|
const childSchema = asSchema(properties?.[key]) ?? additional;
|
|
167
|
-
const result = coerceValue(entry, childSchema, root, depth + 1);
|
|
217
|
+
const result = coerceValue(entry, childSchema, root, depth + 1, toolName, key);
|
|
168
218
|
if (result.changed) changed = true;
|
|
169
219
|
next[key] = result.value;
|
|
170
220
|
}
|
|
@@ -182,6 +232,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
182
232
|
export function coerceIntegerToolArguments(
|
|
183
233
|
args: string,
|
|
184
234
|
parameters: Record<string, unknown> | undefined,
|
|
235
|
+
toolName?: string,
|
|
185
236
|
): string {
|
|
186
237
|
if (!parameters || !args) return args;
|
|
187
238
|
// Cheap reject: a payload with no digit cannot need either repair (integral-float
|
|
@@ -196,7 +247,7 @@ export function coerceIntegerToolArguments(
|
|
|
196
247
|
return args;
|
|
197
248
|
}
|
|
198
249
|
const root = parameters as SchemaNode;
|
|
199
|
-
const result = coerceValue(parsed, root, root, 0);
|
|
250
|
+
const result = coerceValue(parsed, root, root, 0, toolName);
|
|
200
251
|
if (!result.changed) return args;
|
|
201
252
|
return JSON.stringify(result.value);
|
|
202
253
|
}
|
package/src/oauth/health.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
|
|
|
3
3
|
import { isAccountNeedsReauth } from "../codex/account-runtime-state";
|
|
4
4
|
import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
|
|
5
5
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
6
|
-
import { readRuntimePort } from "../config";
|
|
6
|
+
import { readRuntimePort } from "../config/process-state";
|
|
7
7
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
8
8
|
import { maskAccountId } from "../lib/privacy";
|
|
9
9
|
import { findLiveProxy } from "../server/proxy-liveness";
|
package/src/oauth/index.ts
CHANGED
|
@@ -1073,6 +1073,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1073
1073
|
if (existing?.commandCodeVersion !== undefined) {
|
|
1074
1074
|
next.commandCodeVersion = existing.commandCodeVersion;
|
|
1075
1075
|
}
|
|
1076
|
+
if (existing?.projectContext !== undefined) {
|
|
1077
|
+
next.projectContext = existing.projectContext;
|
|
1078
|
+
}
|
|
1076
1079
|
// User-configured price overlays are operator data, not preset state; a
|
|
1077
1080
|
// re-login, add-account, or reauth must not silently drop them from the
|
|
1078
1081
|
// Logs/Usage estimates.
|
|
@@ -442,7 +442,7 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
|
|
|
442
442
|
max: "enabled",
|
|
443
443
|
};
|
|
444
444
|
const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
|
|
445
|
-
"mimo-v2.5", "mimo-v2.5-pro", "
|
|
445
|
+
"mimo-v2.5", "mimo-v2.5-pro", "glm-5", "glm-5.1",
|
|
446
446
|
];
|
|
447
447
|
/**
|
|
448
448
|
* Zhipu's domestic BigModel platform. Text families first, then the vision member: modalities are
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -14,6 +14,16 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] =
|
|
|
14
14
|
const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
|
|
15
15
|
const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sentinel wire value in reasoningEffortMap / modelReasoningEffortMap to explicitly
|
|
19
|
+
* omit the reasoning_effort field from the upstream wire request (issue #2356).
|
|
20
|
+
*/
|
|
21
|
+
export const REASONING_EFFORT_OMIT_SENTINEL = "__omit__";
|
|
22
|
+
|
|
23
|
+
export function isReasoningEffortOmitted(wireEffort: string | undefined): boolean {
|
|
24
|
+
return wireEffort === REASONING_EFFORT_OMIT_SENTINEL;
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */
|
|
18
28
|
export function isCodexReasoningEffort(effort: string): boolean {
|
|
19
29
|
return CODEX_REASONING_SET.has(effort);
|
|
@@ -170,7 +180,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
170
180
|
const boundary = requested === "ultra" ? "max" : requested;
|
|
171
181
|
|
|
172
182
|
const wireMap = reasoningEffortMapFor(provider, modelId);
|
|
173
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary))
|
|
183
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) {
|
|
184
|
+
const mapped = wireMap[boundary];
|
|
185
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
186
|
+
}
|
|
174
187
|
|
|
175
188
|
const supported = configuredReasoningEfforts(provider, modelId);
|
|
176
189
|
const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
|
|
@@ -178,6 +191,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
178
191
|
|
|
179
192
|
// Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
|
|
180
193
|
const wire = codexEffort === "ultra" ? "max" : codexEffort;
|
|
181
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire))
|
|
194
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) {
|
|
195
|
+
const mapped = wireMap[wire];
|
|
196
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
197
|
+
}
|
|
198
|
+
if (wire === REASONING_EFFORT_OMIT_SENTINEL) return undefined;
|
|
182
199
|
return wire;
|
|
183
200
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Representation repair for top-level Codex apply_patch custom-tool payloads.
|
|
2
|
+
//
|
|
3
|
+
// Some routed models decorate the first and last lines as
|
|
4
|
+
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
|
|
5
|
+
// valid custom-tool payloads. Repair is deliberately limited to a complete,
|
|
6
|
+
// structurally recognizable top-level patch: arbitrary `exec` JavaScript is
|
|
7
|
+
// caller-authored executable input and must remain byte-identical.
|
|
8
|
+
//
|
|
9
|
+
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
10
|
+
// repair the one faithful reading, leave genuine patch content alone.
|
|
11
|
+
|
|
12
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
13
|
+
const PATCH_END = "*** End Patch";
|
|
14
|
+
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
|
|
15
|
+
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
|
|
16
|
+
|
|
17
|
+
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
18
|
+
export function unwrapFreeformToolInput(argumentsText: unknown): string {
|
|
19
|
+
if (typeof argumentsText !== "string") return "";
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(argumentsText);
|
|
22
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
23
|
+
const input = (parsed as { input?: unknown }).input;
|
|
24
|
+
if (typeof input === "string") return input;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// The string is the freeform body, not nested JSON.
|
|
28
|
+
}
|
|
29
|
+
return argumentsText;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip trailing `***` only from the outer lines of one complete patch.
|
|
34
|
+
* Internal patch content, incomplete envelopes, and non-patch text are exact
|
|
35
|
+
* pass-throughs.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeApplyPatchDelimiters(text: string): string {
|
|
38
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
39
|
+
if (!match) return text;
|
|
40
|
+
const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
|
|
41
|
+
if (!PATCH_OPERATION_LINE.test(body)) return text;
|
|
42
|
+
if (begin === PATCH_BEGIN && end === PATCH_END) return text;
|
|
43
|
+
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Repair freeform input before Codex sees it.
|
|
48
|
+
*
|
|
49
|
+
* Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
|
|
50
|
+
* repair. Remote namespaces own their grammar; those bodies and every other
|
|
51
|
+
* freeform input are unwrapped and left byte-exact.
|
|
52
|
+
*/
|
|
53
|
+
export function repairFreeformToolInput(
|
|
54
|
+
argumentsText: unknown,
|
|
55
|
+
toolName = "",
|
|
56
|
+
namespace?: string,
|
|
57
|
+
): string {
|
|
58
|
+
const unwrapped = unwrapFreeformToolInput(argumentsText);
|
|
59
|
+
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
|
|
60
|
+
return ownsApplyPatchGrammar && toolName === "apply_patch"
|
|
61
|
+
? normalizeApplyPatchDelimiters(unwrapped)
|
|
62
|
+
: unwrapped;
|
|
63
|
+
}
|