@bitkyc08/opencodex 2.54.0-preview.20260914 → 2.55.0-preview.20260914
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-B4VYfZcY.js → index-DH2PUHqr.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +57 -0
- package/src/adapters/anthropic-image-normalize.ts +28 -1
- package/src/adapters/anthropic.ts +68 -6
- package/src/adapters/base.ts +8 -0
- package/src/adapters/coding-agent/protocol.ts +41 -16
- package/src/adapters/cursor/cursor-errors.ts +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/native-exec-fs.ts +10 -10
- package/src/adapters/cursor/native-exec-network.ts +2 -2
- package/src/adapters/cursor/native-exec-shell.ts +13 -12
- package/src/adapters/cursor/native-exec.ts +51 -10
- package/src/adapters/cursor/policy-error.ts +75 -0
- package/src/adapters/cursor/protobuf-request.ts +105 -1
- package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
- package/src/adapters/devin/live-models.ts +33 -2
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +46 -0
- package/src/adapters/input-media-guard.ts +45 -0
- package/src/adapters/kiro/adapter.ts +8 -0
- package/src/adapters/kiro/payload.ts +28 -6
- package/src/adapters/kiro-events.ts +25 -6
- package/src/adapters/kiro-images.ts +30 -0
- package/src/adapters/kiro-retry.ts +8 -0
- package/src/adapters/openai-chat.ts +33 -4
- package/src/adapters/openai-responses.ts +26 -0
- package/src/adapters/registry.ts +4 -0
- package/src/bridge.ts +163 -116
- package/src/chat/image-parts.ts +151 -0
- package/src/chat/inbound.ts +70 -33
- package/src/cli/connect.ts +30 -9
- package/src/cli/dispatch.ts +7 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/runtime-api.ts +25 -0
- package/src/cli/status.ts +21 -19
- package/src/cli/system-restart-client.ts +25 -0
- package/src/clients/config-export.ts +14 -4
- package/src/codex/app-server-processes.ts +25 -0
- package/src/codex/auth-context.ts +8 -0
- package/src/codex/autostart-health.ts +36 -2
- package/src/codex/catalog/provider-fetch.ts +41 -0
- package/src/codex/catalog-auto-refresh.ts +182 -0
- package/src/codex/catalog-refresh-status.ts +93 -0
- package/src/codex/history-provider.ts +55 -0
- package/src/codex/model-entitlements.ts +78 -0
- package/src/codex/native-profile-processes.ts +114 -15
- package/src/codex/prompt-text-probe.ts +274 -41
- package/src/codex/routing-adoption.ts +189 -0
- package/src/codex/routing.ts +520 -48
- package/src/codex/runtime.ts +249 -7
- package/src/combos/failover.ts +45 -0
- package/src/config.ts +124 -4
- package/src/generated/compatibility-version.json +110 -74
- package/src/generated/model-metadata.ts +1 -0
- package/src/lib/request-execution-budget.ts +202 -0
- package/src/lib/upstream-retry.ts +95 -8
- package/src/lib/workflow-budget.ts +172 -0
- package/src/oauth/devin.ts +57 -12
- package/src/providers/quota.ts +37 -6
- package/src/providers/registry.ts +53 -6
- package/src/responses/input-media.ts +65 -0
- package/src/responses/parser-content.ts +42 -0
- package/src/responses/schema.ts +12 -2
- package/src/server/audio-live.ts +1 -2
- package/src/server/audio-transcriptions.ts +1 -2
- package/src/server/auth-cors.ts +1 -1
- package/src/server/background-lifecycle.ts +18 -0
- package/src/server/chat-completions.ts +23 -8
- package/src/server/chat-native.ts +17 -17
- package/src/server/index.ts +24 -0
- package/src/server/management/request-history-routes.ts +5 -0
- package/src/server/request-log.ts +8 -2
- package/src/server/responses/compact.ts +51 -3
- package/src/server/responses/core.ts +238 -28
- package/src/server/search.ts +7 -9
- package/src/types/config.ts +51 -6
- package/src/usage/log.ts +37 -0
- package/src/vision/eligibility.ts +37 -4
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +45 -10
- package/src/web-search/alpha-search.ts +324 -0
- package/src/web-search/index.ts +13 -22
- package/src/web-search/passthrough-bridge.ts +195 -22
- package/src/web-search/sidecar-providers.ts +22 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound Chat Completions image parts, recognized once for every consumer.
|
|
3
|
+
*
|
|
4
|
+
* Two call sites used to answer "does this body carry an image?" independently and
|
|
5
|
+
* gave different answers: the translated path understood Pi/MCP and Anthropic-shaped
|
|
6
|
+
* parts, while the native fast path's route-eligibility predicate matched only
|
|
7
|
+
* `image_url`. A text-only routed model therefore kept an image-bearing body and
|
|
8
|
+
* forwarded a non-OpenAI part verbatim to an OpenAI-compatible upstream.
|
|
9
|
+
*
|
|
10
|
+
* Normalization runs before route selection so the diversion decision and the
|
|
11
|
+
* forwarded wire see the same parts. This module deliberately imports nothing: it is
|
|
12
|
+
* shared by `src/chat/` and `src/server/` and must not create an edge between them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Rec = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
function isRec(v: unknown): v is Rec {
|
|
18
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The image reference a Chat content part carries, as a URL or data URI.
|
|
23
|
+
*
|
|
24
|
+
* Accepts OpenAI `image_url` (string or `{url}`), Pi/MCP-style
|
|
25
|
+
* `{type:"image", data, mimeType}` (Aside read_file tool results), and
|
|
26
|
+
* Anthropic-shaped `{type:"image", source:{...}}` in both base64 and url form.
|
|
27
|
+
* Returns null for anything else — including a part with no usable reference, which
|
|
28
|
+
* must be left alone rather than turned into a claim of an attachment.
|
|
29
|
+
*/
|
|
30
|
+
export function chatImageUrlFromPart(part: Rec): string | null {
|
|
31
|
+
if (part.type === "image_url") {
|
|
32
|
+
const imageUrl = part.image_url;
|
|
33
|
+
if (typeof imageUrl === "string" && imageUrl.length > 0) return imageUrl;
|
|
34
|
+
if (isRec(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.length > 0) return imageUrl.url;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (part.type === "image") {
|
|
38
|
+
const data = part.data;
|
|
39
|
+
if (typeof data === "string" && data.length > 0) {
|
|
40
|
+
if (data.startsWith("data:")) return data;
|
|
41
|
+
const media = typeof part.mimeType === "string" && part.mimeType.length > 0 ? part.mimeType
|
|
42
|
+
: typeof part.mediaType === "string" && part.mediaType.length > 0 ? part.mediaType
|
|
43
|
+
: "image/png";
|
|
44
|
+
return "data:" + media + ";base64," + data;
|
|
45
|
+
}
|
|
46
|
+
const source = part.source;
|
|
47
|
+
if (isRec(source)) {
|
|
48
|
+
if (source.type === "base64" && typeof source.data === "string" && source.data.length > 0) {
|
|
49
|
+
const media = typeof source.media_type === "string" && source.media_type.length > 0 ? source.media_type : "image/png";
|
|
50
|
+
return "data:" + media + ";base64," + source.data;
|
|
51
|
+
}
|
|
52
|
+
if (source.type === "url" && typeof source.url === "string" && source.url.length > 0) return source.url;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The fidelity hint a recognized part carries, when it is one the wire accepts. */
|
|
59
|
+
export function chatImageDetailFromPart(part: Rec): "auto" | "low" | "high" | undefined {
|
|
60
|
+
const raw = isRec(part.image_url) ? part.image_url.detail : part.detail;
|
|
61
|
+
return raw === "auto" || raw === "low" || raw === "high" ? raw : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* True when any `messages[].content[]` part carries a recognized image, in any of
|
|
66
|
+
* the accepted shapes. This is the predicate native-route eligibility depends on, so
|
|
67
|
+
* widening `chatImageUrlFromPart` widens the text-only diversion with it.
|
|
68
|
+
*/
|
|
69
|
+
export function chatBodyCarriesImage(rawBody: Rec): boolean {
|
|
70
|
+
const messages = rawBody.messages;
|
|
71
|
+
if (!Array.isArray(messages)) return false;
|
|
72
|
+
for (const message of messages) {
|
|
73
|
+
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
74
|
+
for (const part of message.content) {
|
|
75
|
+
if (isRec(part) && chatImageUrlFromPart(part) !== null) return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rewrite every recognized non-OpenAI image part into `image_url` form.
|
|
83
|
+
*
|
|
84
|
+
* Copy-on-write, and genuinely lazy: replacement arrays are allocated only after a
|
|
85
|
+
* part actually needs rewriting. An ordinary text or native-Chat request walks the
|
|
86
|
+
* messages and allocates nothing, and the original object reference is returned.
|
|
87
|
+
* An earlier revision mapped every message and content array eagerly and only then
|
|
88
|
+
* compared — identity was preserved, but the transient arrays were not, so the
|
|
89
|
+
* "only rewritten paths are rebuilt" claim was false for the common path.
|
|
90
|
+
*
|
|
91
|
+
* Every sibling part, every other message field and every top-level body field keep
|
|
92
|
+
* their exact value: the native path is a whitelist passthrough, so an incidental
|
|
93
|
+
* deep clone would itself be a behavior change.
|
|
94
|
+
*
|
|
95
|
+
* Each rewritten Pi/Anthropic base64 part costs one copy of its payload string,
|
|
96
|
+
* bounded by the inbound body limit `readChatBody` already enforces.
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeChatImageParts(rawBody: Rec): Rec {
|
|
99
|
+
const messages = rawBody.messages;
|
|
100
|
+
if (!Array.isArray(messages)) return rawBody;
|
|
101
|
+
let nextMessages: unknown[] | undefined;
|
|
102
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
103
|
+
const message = messages[messageIndex];
|
|
104
|
+
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
105
|
+
const content = message.content;
|
|
106
|
+
let nextContent: unknown[] | undefined;
|
|
107
|
+
for (let partIndex = 0; partIndex < content.length; partIndex++) {
|
|
108
|
+
const part = content[partIndex];
|
|
109
|
+
// Already-OpenAI parts are left byte-identical; only foreign shapes are rewritten.
|
|
110
|
+
if (!isRec(part) || part.type === "image_url") continue;
|
|
111
|
+
const url = chatImageUrlFromPart(part);
|
|
112
|
+
if (url === null) continue;
|
|
113
|
+
const detail = chatImageDetailFromPart(part);
|
|
114
|
+
nextContent ??= content.slice();
|
|
115
|
+
nextContent[partIndex] = { type: "image_url", image_url: { url, ...(detail ? { detail } : {}) } };
|
|
116
|
+
}
|
|
117
|
+
if (!nextContent) continue;
|
|
118
|
+
nextMessages ??= messages.slice();
|
|
119
|
+
nextMessages[messageIndex] = { ...message, content: nextContent };
|
|
120
|
+
}
|
|
121
|
+
return nextMessages ? { ...rawBody, messages: nextMessages } : rawBody;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* True when a `role: "tool"` message carries a recognized image, in any accepted shape.
|
|
126
|
+
*
|
|
127
|
+
* Shape normalization alone does NOT make such a request safe on the native fast path.
|
|
128
|
+
* A standard Chat tool message accepts a string or text parts only — not `image_url` —
|
|
129
|
+
* so rewriting a Pi/Anthropic tool image into `image_url` still leaves an image part
|
|
130
|
+
* inside a tool message, which a standard-enforcing endpoint rejects.
|
|
131
|
+
*
|
|
132
|
+
* The translated openai-chat adapter already solves placement: it collects tool-result
|
|
133
|
+
* images and flushes them into a following `user` carrier after the complete paired
|
|
134
|
+
* tool-result batch. Diverting these requests there is narrower than reimplementing
|
|
135
|
+
* that carrier on the native path, and it leaves ordinary user images and text-only
|
|
136
|
+
* tool results on the native fast path untouched.
|
|
137
|
+
*/
|
|
138
|
+
export function chatBodyCarriesToolResultImage(rawBody: Rec): boolean {
|
|
139
|
+
const messages = rawBody.messages;
|
|
140
|
+
if (!Array.isArray(messages)) return false;
|
|
141
|
+
for (const message of messages) {
|
|
142
|
+
// The legacy `function` role carries a tool result under the same schema constraint,
|
|
143
|
+
// so it needs the same diversion.
|
|
144
|
+
if (!isRec(message) || (message.role !== "tool" && message.role !== "function")) continue;
|
|
145
|
+
if (!Array.isArray(message.content)) continue;
|
|
146
|
+
for (const part of message.content) {
|
|
147
|
+
if (isRec(part) && chatImageUrlFromPart(part) !== null) return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
package/src/chat/inbound.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Same translate-and-replay pattern as Claude Messages: the produced body must pass
|
|
6
6
|
* responsesRequestSchema so routing/OAuth/pool/sidecars are inherited unchanged.
|
|
7
7
|
*/
|
|
8
|
+
import { chatImageUrlFromPart } from "./image-parts";
|
|
9
|
+
import { untranslatedChatInputMedia, untranslatedInputMediaMessage } from "../responses/input-media";
|
|
10
|
+
|
|
8
11
|
export class ChatCompletionsRequestError extends Error {}
|
|
9
12
|
|
|
10
13
|
type Rec = Record<string, unknown>;
|
|
@@ -25,7 +28,12 @@ export function assertChatCompletionsRoutingBody(raw: unknown): asserts raw is C
|
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
// "none" is the runtime's disable sentinel, not an unknown value: src/reasoning-effort.ts
|
|
32
|
+
// accepts it and maps it to "omit the reasoning parameter", and the Pi client export maps
|
|
33
|
+
// Pi's "off" thinking level onto it (src/clients/config-export.ts). Dropping it here let a
|
|
34
|
+
// provider default re-enable thinking the caller had explicitly turned off — and for the
|
|
35
|
+
// Anthropic families that think by default, omission is not the same as disabled.
|
|
36
|
+
const OUTPUT_CONFIG_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
29
37
|
const OUTPUT_CONFIG_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
|
|
30
38
|
|
|
31
39
|
function contentToText(content: unknown): string {
|
|
@@ -45,38 +53,9 @@ function contentToText(content: unknown): string {
|
|
|
45
53
|
return parts.join("\n");
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (typeof imageUrl === "string" && imageUrl.length > 0) return imageUrl;
|
|
52
|
-
if (isRec(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.length > 0) return imageUrl.url;
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
// Agent clients whose native wire shape is not OpenAI's still send images over
|
|
56
|
-
// Chat Completions: Pi/MCP-style parts carry {type:"image", data, mimeType}
|
|
57
|
-
// (Aside read_file tool results), Anthropic-shaped clients carry a source
|
|
58
|
-
// object. Dropping either silently blinds a vision model, so normalize both
|
|
59
|
-
// to the URL/data-URI form the Responses pipeline already understands.
|
|
60
|
-
if (part.type === "image") {
|
|
61
|
-
const data = part.data;
|
|
62
|
-
if (typeof data === "string" && data.length > 0) {
|
|
63
|
-
if (data.startsWith("data:")) return data;
|
|
64
|
-
const media = typeof part.mimeType === "string" && part.mimeType.length > 0 ? part.mimeType
|
|
65
|
-
: typeof part.mediaType === "string" && part.mediaType.length > 0 ? part.mediaType
|
|
66
|
-
: "image/png";
|
|
67
|
-
return "data:" + media + ";base64," + data;
|
|
68
|
-
}
|
|
69
|
-
const source = part.source;
|
|
70
|
-
if (isRec(source)) {
|
|
71
|
-
if (source.type === "base64" && typeof source.data === "string" && source.data.length > 0) {
|
|
72
|
-
const media = typeof source.media_type === "string" && source.media_type.length > 0 ? source.media_type : "image/png";
|
|
73
|
-
return "data:" + media + ";base64," + source.data;
|
|
74
|
-
}
|
|
75
|
-
if (source.type === "url" && typeof source.url === "string" && source.url.length > 0) return source.url;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
56
|
+
// Recognition moved to src/chat/image-parts.ts so the native fast path's
|
|
57
|
+
// route-eligibility predicate and this translator cannot drift apart again.
|
|
58
|
+
const imageUrlFromPart = chatImageUrlFromPart;
|
|
80
59
|
|
|
81
60
|
function videoUrlFromPart(part: Rec): string | null {
|
|
82
61
|
if (part.type !== "video_url") return null;
|
|
@@ -118,6 +97,31 @@ function userContentToBlocks(content: unknown): Rec[] {
|
|
|
118
97
|
return blocks;
|
|
119
98
|
}
|
|
120
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The assistant's prior thinking, as plaintext, from either Chat spelling.
|
|
102
|
+
*
|
|
103
|
+
* The outbound direction already reconstructs these for providers listed in
|
|
104
|
+
* `preserveReasoningContentModels` (src/adapters/openai-chat.ts), so a client
|
|
105
|
+
* replaying a turn sends them back. Dropping them here made the round trip lossy and
|
|
106
|
+
* left interleaved-thinking providers seeing a bare continuation.
|
|
107
|
+
*
|
|
108
|
+
* Only representable plaintext is read. No signature, encrypted payload or
|
|
109
|
+
* provider-issued item id is reconstructed — see the reasoning item built below.
|
|
110
|
+
*/
|
|
111
|
+
function assistantReasoningText(msg: Rec): string | undefined {
|
|
112
|
+
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
|
|
113
|
+
return msg.reasoning_content;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(msg.reasoning_details)) {
|
|
116
|
+
const segments: string[] = [];
|
|
117
|
+
for (const raw of msg.reasoning_details) {
|
|
118
|
+
if (isRec(raw) && typeof raw.text === "string" && raw.text.length > 0) segments.push(raw.text);
|
|
119
|
+
}
|
|
120
|
+
if (segments.length > 0) return segments.join("");
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
function assistantContentToBlocks(content: unknown): Rec[] {
|
|
122
126
|
if (typeof content === "string") {
|
|
123
127
|
return content.length > 0 ? [{ type: "output_text", text: content }] : [];
|
|
@@ -274,6 +278,13 @@ function resolveReasoningSummary(raw: Rec): string | undefined {
|
|
|
274
278
|
*/
|
|
275
279
|
export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
276
280
|
assertChatCompletionsRoutingBody(raw);
|
|
281
|
+
// Only the translated path reaches this function. Native Chat can retain its
|
|
282
|
+
// provider-specific file/audio blocks; projecting them here would discard them.
|
|
283
|
+
const unsupportedMedia = untranslatedChatInputMedia(raw);
|
|
284
|
+
if (unsupportedMedia) {
|
|
285
|
+
throw new ChatCompletionsRequestError(untranslatedInputMediaMessage(unsupportedMedia));
|
|
286
|
+
}
|
|
287
|
+
|
|
277
288
|
|
|
278
289
|
const systemParts: string[] = [];
|
|
279
290
|
const input: Rec[] = [];
|
|
@@ -295,11 +306,30 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
295
306
|
break;
|
|
296
307
|
}
|
|
297
308
|
case "assistant": {
|
|
309
|
+
// A reasoning item precedes the assistant message it belongs to: the
|
|
310
|
+
// Responses assistant item schema admits only output content blocks, so there
|
|
311
|
+
// is no attachment point on the message itself, and the parser buffers a
|
|
312
|
+
// reasoning item and prepends it to the NEXT assistant message. Emitting it
|
|
313
|
+
// here keeps that adjacency intact.
|
|
314
|
+
const reasoningText = assistantReasoningText(msg);
|
|
315
|
+
if (reasoningText !== undefined) {
|
|
316
|
+
input.push({ type: "reasoning", content: [{ type: "reasoning_text", text: reasoningText }] });
|
|
317
|
+
}
|
|
298
318
|
const blocks = assistantContentToBlocks(msg.content);
|
|
299
319
|
if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
|
|
300
320
|
if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input, knownNameByCallId);
|
|
301
321
|
break;
|
|
302
322
|
}
|
|
323
|
+
case "function": {
|
|
324
|
+
// Native eligibility diverts legacy image results too, but this translator
|
|
325
|
+
// has no legacy function_call/name pairing. Never silently discard them.
|
|
326
|
+
if (Array.isArray(msg.content) && msg.content.some(part => isRec(part) && imageUrlFromPart(part))) {
|
|
327
|
+
throw new ChatCompletionsRequestError(
|
|
328
|
+
"Legacy function-result image translation is not implemented. Use tool_calls and role:tool with tool_call_id.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
303
333
|
case "tool": {
|
|
304
334
|
const callId = typeof msg.tool_call_id === "string" ? msg.tool_call_id
|
|
305
335
|
: typeof msg.tool_use_id === "string" ? msg.tool_use_id
|
|
@@ -342,6 +372,13 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
342
372
|
if (typeof maxTokens === "number") body.max_output_tokens = maxTokens;
|
|
343
373
|
if (typeof raw.temperature === "number") body.temperature = raw.temperature;
|
|
344
374
|
if (typeof raw.top_p === "number") body.top_p = raw.top_p;
|
|
375
|
+
// responsesRequestSchema accepts both, parser.ts reads them into
|
|
376
|
+
// options.presencePenalty/frequencyPenalty, and the openai-chat adapter writes them
|
|
377
|
+
// back to the wire. Only this first link was missing, so a Chat caller's penalties
|
|
378
|
+
// never reached a provider that supports them. Per-model noPenaltyModels opt-outs
|
|
379
|
+
// still apply at the adapter.
|
|
380
|
+
if (typeof raw.presence_penalty === "number") body.presence_penalty = raw.presence_penalty;
|
|
381
|
+
if (typeof raw.frequency_penalty === "number") body.frequency_penalty = raw.frequency_penalty;
|
|
345
382
|
if (raw.stop !== undefined) body.stop = raw.stop;
|
|
346
383
|
if (typeof raw.user === "string") body.user = raw.user;
|
|
347
384
|
if (typeof raw.parallel_tool_calls === "boolean") body.parallel_tool_calls = raw.parallel_tool_calls;
|
package/src/cli/connect.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
takeFlag,
|
|
30
30
|
takeIntegerOption,
|
|
31
31
|
takeOption,
|
|
32
|
+
terminalSafeError,
|
|
33
|
+
terminalSafeText,
|
|
32
34
|
type RuntimeApiDeps,
|
|
33
35
|
} from "./runtime-api";
|
|
34
36
|
|
|
@@ -40,6 +42,15 @@ export interface ClientCommandDeps extends RuntimeApiDeps {
|
|
|
40
42
|
export interface ClientCatalogProbeDeps extends CatalogCompatibilityDeps {
|
|
41
43
|
/** Injected in tests; defaults to reading the materialized client catalog off disk. */
|
|
42
44
|
readCatalogBody?: () => string | null;
|
|
45
|
+
/**
|
|
46
|
+
* A Codex command the caller already resolved, handed over so readiness skips resolving it
|
|
47
|
+
* again. General `ocx status` resolves the full runtime for its diagnostics block; the resolver
|
|
48
|
+
* memo is keyed by discovery scope and holds one entry, so a priority-only readiness resolve
|
|
49
|
+
* and the full one miss each other and re-probe the same command with `--version` — up to eight
|
|
50
|
+
* seconds apiece. Passing the command across adds no cache state, and it cannot disagree with
|
|
51
|
+
* what status prints because it is the selection status is printing.
|
|
52
|
+
*/
|
|
53
|
+
selectedCodexCommand?: string;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export const CONNECT_USAGE = `Usage:
|
|
@@ -101,11 +112,15 @@ function readInstalledCatalogBody(): string | null {
|
|
|
101
112
|
* `codexSupportedReasoningEfforts()` with no deps reaches `resolveAndPersistCodexRuntime`, which
|
|
102
113
|
* writes codex-runtime.json. `ocx status` deliberately resolves without persisting, and a
|
|
103
114
|
* read-only diagnostics command should not start writing runtime selection state because a
|
|
104
|
-
* readiness check was added to it.
|
|
105
|
-
*
|
|
115
|
+
* readiness check was added to it. Stop at the first valid runtime, then hand only that command
|
|
116
|
+
* to the catalog probe: readiness does not consume alternative-runtime diagnostics. The resolver
|
|
117
|
+
* keeps this priority-only cache separate from the full discovery used by `ocx status`.
|
|
118
|
+
*
|
|
119
|
+
* A caller that has already resolved passes its selection in through `selectedCodexCommand` rather
|
|
120
|
+
* than paying for a second `--version` probe of the command it just resolved.
|
|
106
121
|
*/
|
|
107
|
-
function observeLocalCodexEffortLadder(): ReadonlySet<string> | null {
|
|
108
|
-
const command = resolveCodexRuntime().runtime.command;
|
|
122
|
+
function observeLocalCodexEffortLadder(selected?: string): ReadonlySet<string> | null {
|
|
123
|
+
const command = selected ?? resolveCodexRuntime({ discoverAlternatives: false }).runtime.command;
|
|
109
124
|
return codexSupportedReasoningEfforts({ commandCandidates: () => [command] });
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -117,7 +132,8 @@ function observeLocalCodexEffortLadder(): ReadonlySet<string> | null {
|
|
|
117
132
|
* a single `ocx connect`.
|
|
118
133
|
*/
|
|
119
134
|
function catalogObserver(deps: ClientCatalogProbeDeps | undefined): CatalogCompatibilityDeps {
|
|
120
|
-
|
|
135
|
+
const selected = deps?.selectedCodexCommand;
|
|
136
|
+
return { supportedEfforts: deps?.supportedEfforts ?? (() => observeLocalCodexEffortLadder(selected)) };
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
/** The stat half of the catalog verdict, shared by the status collector and `ocx connect`. */
|
|
@@ -212,7 +228,7 @@ function readinessLine(status: ClientConnectionStatus): string {
|
|
|
212
228
|
: status.readiness === "incompatible"
|
|
213
229
|
? "not ready"
|
|
214
230
|
: "unverified";
|
|
215
|
-
return `Local Codex CLI: ${label}${status.readinessReason ? ` (${status.readinessReason})` : ""}`;
|
|
231
|
+
return `Local Codex CLI: ${label}${status.readinessReason ? ` (${terminalSafeText(status.readinessReason)})` : ""}`;
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
export type ConnectCompletionReport = {
|
|
@@ -249,9 +265,10 @@ export function connectCompletionReport(
|
|
|
249
265
|
if (readiness.kind === "unverified") {
|
|
250
266
|
// Not a failure. A client with no observable Codex CLI is a working configuration, and the
|
|
251
267
|
// write-time gate deliberately lets it through; saying so is the honest middle report.
|
|
252
|
-
return { lines: [connected, `Local Codex CLI: unverified (${readiness.reason}).`], failure: null };
|
|
268
|
+
return { lines: [connected, `Local Codex CLI: unverified (${terminalSafeText(readiness.reason)}).`], failure: null };
|
|
253
269
|
}
|
|
254
|
-
const
|
|
270
|
+
const safeReason = terminalSafeText(readiness.reason);
|
|
271
|
+
const verdict = `Local Codex CLI: not ready (${safeReason})`;
|
|
255
272
|
if (!selectedClients.includes("codex")) {
|
|
256
273
|
return {
|
|
257
274
|
lines: [connected, `${verdict} This connection selected ${selectedClients.join(", ")}, so nothing here launches Codex.`],
|
|
@@ -260,7 +277,7 @@ export function connectCompletionReport(
|
|
|
260
277
|
}
|
|
261
278
|
return {
|
|
262
279
|
lines: [verdict, `The connection to ${connection.serverUrl} as key ${connection.apiKeyId} was saved; run 'ocx connect status' to see it.`],
|
|
263
|
-
failure: `client_not_ready: ${
|
|
280
|
+
failure: `client_not_ready: ${safeReason}`,
|
|
264
281
|
};
|
|
265
282
|
}
|
|
266
283
|
|
|
@@ -344,6 +361,10 @@ async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void
|
|
|
344
361
|
// production would let the gate fall back to its own probing, persisting default, so one
|
|
345
362
|
// command could run two probes and act on two different ladders.
|
|
346
363
|
catalogCompatibility: catalogObserver(deps.catalogProbeDeps),
|
|
364
|
+
}).catch((error: unknown) => {
|
|
365
|
+
// Compatibility refusals happen before the completion report and reach stderr.
|
|
366
|
+
// Keep the domain error untouched; render its message only at the CLI boundary.
|
|
367
|
+
throw terminalSafeError(error);
|
|
347
368
|
});
|
|
348
369
|
// The hub and the credential are proven at this point; the local runtime is not. Reporting
|
|
349
370
|
// only the first half is what #4207 was filed for, so the catalog now on disk is checked
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { restoreNativeCodexAsync } from "../codex/inject";
|
|
|
28
28
|
import { stripGrokConfig } from "../grok/inject";
|
|
29
29
|
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
|
|
30
30
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
31
|
-
import { isJsonOption, takeFlag } from "./runtime-api";
|
|
31
|
+
import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
|
|
32
32
|
import type { ClientConnectionState } from "../client/state";
|
|
33
33
|
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
|
|
34
34
|
|
|
@@ -70,7 +70,7 @@ export function selectDefaultGuiUrl(
|
|
|
70
70
|
probeHostname: (hostname: string | undefined) => string,
|
|
71
71
|
): string {
|
|
72
72
|
const ingress = config.runtimeRole === "hub" ? config.hub?.managementIngress : undefined;
|
|
73
|
-
if (ingress?.enabled) return `http://
|
|
73
|
+
if (ingress?.enabled) return `http://127.0.0.1:${ingress.port}`;
|
|
74
74
|
|
|
75
75
|
const guiHost = probeHostname(live?.hostname ?? config.hostname);
|
|
76
76
|
const hostname = guiHost === "127.0.0.1" ? "localhost" : guiHost;
|
|
@@ -405,7 +405,11 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
405
405
|
// types it as `number | string`; only a numeric code means anything here.
|
|
406
406
|
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
407
407
|
} catch (error) {
|
|
408
|
-
|
|
408
|
+
// The refresh path reaches the same hub catalog `ocx connect` validates, so a rejected
|
|
409
|
+
// reasoning level arrives here as hub-supplied text. Rendering it through the shared
|
|
410
|
+
// terminal boundary is what keeps the routine refresh from forging output; the domain
|
|
411
|
+
// error itself is left alone for callers that inspect it.
|
|
412
|
+
console.error(`Connected sync failed without local fallback: ${terminalSafeError(error).message}`);
|
|
409
413
|
return 1;
|
|
410
414
|
}
|
|
411
415
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -732,6 +732,9 @@ function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>
|
|
|
732
732
|
if (code === "restart_capability_unsupported") {
|
|
733
733
|
console.error("❌ The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
|
|
734
734
|
console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
|
|
735
|
+
} else if (code === "restart_version_skew") {
|
|
736
|
+
console.error("❌ The running proxy reports a different OpenCodex version than this CLI; restarting in place would respawn the old installation.");
|
|
737
|
+
console.error(" Run `ocx stop` and then `ocx start` from this installation instead.");
|
|
735
738
|
} else {
|
|
736
739
|
console.error("❌ Proxy restart request could not be confirmed; no fallback stop/start was attempted.");
|
|
737
740
|
}
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -350,6 +350,31 @@ export function printData(value: unknown, wantsJson: boolean, lines?: string[]):
|
|
|
350
350
|
else for (const line of lines) console.log(line);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Render untrusted diagnostic text without letting it control the operator's terminal. Catalog
|
|
355
|
+
* values are hub-supplied and surface on more than one CLI path -- first-time `ocx connect` and the
|
|
356
|
+
* connected `ocx sync` refresh both print them -- so the escaping sits beside `printData`, at the
|
|
357
|
+
* one boundary that already separates human output from structured output. Structured output keeps
|
|
358
|
+
* the exact value: escaping is a rendering decision for a tty, not a change to the data.
|
|
359
|
+
*/
|
|
360
|
+
export function terminalSafeText(value: string): string {
|
|
361
|
+
return value.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, character => {
|
|
362
|
+
const code = character.charCodeAt(0);
|
|
363
|
+
return code <= 0x7f
|
|
364
|
+
? `\\x${code.toString(16).padStart(2, "0")}`
|
|
365
|
+
: `\\u${code.toString(16).padStart(4, "0")}`;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The same rendering for a failure about to be printed or rethrown. The original is kept as
|
|
371
|
+
* `cause` rather than discarded, so a caller that inspects the domain error still reads the exact
|
|
372
|
+
* message and fields it threw.
|
|
373
|
+
*/
|
|
374
|
+
export function terminalSafeError(error: unknown): Error {
|
|
375
|
+
return new Error(terminalSafeText(error instanceof Error ? error.message : String(error)), { cause: error });
|
|
376
|
+
}
|
|
377
|
+
|
|
353
378
|
/** Compact human view for safe management DTOs; JSON remains available for complete fidelity. */
|
|
354
379
|
export function summaryLines(value: unknown, prefix = "", depth = 0): string[] {
|
|
355
380
|
if (!value || typeof value !== "object" || depth > 1) return [`${prefix || "value"}: ${String(value)}`];
|
package/src/cli/status.ts
CHANGED
|
@@ -509,7 +509,27 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
509
509
|
desiredEnabled: claudeDesktopIntegrationEnabled(config),
|
|
510
510
|
policy: claudeDesktopPolicyHealth(probeClaudeDesktopPolicy()),
|
|
511
511
|
};
|
|
512
|
-
const
|
|
512
|
+
const resolvedRuntime = (() => {
|
|
513
|
+
try {
|
|
514
|
+
return resolveCodexRuntime();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
const redacted = redactUserPath(redactSecretString(message)).slice(0, 160);
|
|
518
|
+
return {
|
|
519
|
+
runtime: { command: "codex", version: null, source: "fallback" as const },
|
|
520
|
+
failures: [{
|
|
521
|
+
command: "codex",
|
|
522
|
+
source: "fallback" as const,
|
|
523
|
+
reason: `resolve threw: ${redacted}`,
|
|
524
|
+
}],
|
|
525
|
+
replacedConfigured: undefined,
|
|
526
|
+
newerAvailable: undefined,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
})();
|
|
530
|
+
const clientConnection = collectClientConnectionStatus(Date.now(), undefined, {
|
|
531
|
+
selectedCodexCommand: resolvedRuntime.runtime.command,
|
|
532
|
+
});
|
|
513
533
|
// Asked before the local probes below so a connected client's report is hub-sourced from its
|
|
514
534
|
// first line. Bounded and failure-tolerant: an offline hub degrades the remoteHub block, it
|
|
515
535
|
// does not fail `ocx status`.
|
|
@@ -567,24 +587,6 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
567
587
|
routingKind: getCodexRoutingKind(),
|
|
568
588
|
});
|
|
569
589
|
const codexPlugins = diagnoseCodexBundledPlugins();
|
|
570
|
-
const resolvedRuntime = (() => {
|
|
571
|
-
try {
|
|
572
|
-
return resolveCodexRuntime();
|
|
573
|
-
} catch (error) {
|
|
574
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
575
|
-
const redacted = redactUserPath(redactSecretString(message)).slice(0, 160);
|
|
576
|
-
return {
|
|
577
|
-
runtime: { command: "codex", version: null, source: "fallback" as const },
|
|
578
|
-
failures: [{
|
|
579
|
-
command: "codex",
|
|
580
|
-
source: "fallback" as const,
|
|
581
|
-
reason: `resolve threw: ${redacted}`,
|
|
582
|
-
}],
|
|
583
|
-
replacedConfigured: undefined,
|
|
584
|
-
newerAvailable: undefined,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
})();
|
|
588
590
|
const lastClamp = loadLastEffortClamp();
|
|
589
591
|
const clampActive = effortClampAppliesToRuntime(lastClamp, resolvedRuntime.runtime);
|
|
590
592
|
const codexHome = collectOrcaCodexHomeDiagnostic();
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
type LiveProxy,
|
|
23
23
|
} from "../server/proxy-liveness";
|
|
24
24
|
import type { ProxyRestartRequestOutcome } from "./tray-proxy";
|
|
25
|
+
import { packageVersion } from "./help";
|
|
26
|
+
import { computeVersionSkew } from "./version-skew";
|
|
25
27
|
|
|
26
28
|
export const SYSTEM_RESTART_REQUEST_TIMEOUT_MS = 5_000;
|
|
27
29
|
export const SYSTEM_RESTART_ATTESTATION_TIMEOUT_MS = 4_000;
|
|
@@ -32,12 +34,23 @@ export interface BoundSystemRestartDeps {
|
|
|
32
34
|
findLive?: typeof findLiveProxy;
|
|
33
35
|
createChallenge?: () => string;
|
|
34
36
|
now?: () => number;
|
|
37
|
+
/** Invoking CLI version for the skew guard; defaults to this bundle's package version. */
|
|
38
|
+
cliVersion?: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function rejected(code: string): ProxyRestartRequestOutcome {
|
|
38
42
|
return { accepted: false, uncertain: false, error: new Error(code) };
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/** Own-bundle version for the skew comparison; an unreadable bundle is "cannot compare", not a crash. */
|
|
46
|
+
function ownCliVersion(): string {
|
|
47
|
+
try {
|
|
48
|
+
return packageVersion();
|
|
49
|
+
} catch {
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
function uncertain(code: string): ProxyRestartRequestOutcome {
|
|
42
55
|
return { accepted: false, uncertain: true, error: new Error(code) };
|
|
43
56
|
}
|
|
@@ -107,6 +120,18 @@ export async function requestBoundSystemRestart(
|
|
|
107
120
|
return rejected("restart_capability_unsupported");
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
// An in-place restart respawns the live process from its own installation
|
|
124
|
+
// (selfLaunchArgv in server/management/system-restart.ts), so a restart accepted
|
|
125
|
+
// from a different-version CLI would keep the OLD build serving while reporting
|
|
126
|
+
// success (#4522). Both sides already publish exactly the data doctor's skew
|
|
127
|
+
// diagnosis compares (packageVersion vs the /healthz version), so reuse that
|
|
128
|
+
// comparison and refuse before POST. Placeholder versions (unknown/0.0.0) are
|
|
129
|
+
// "cannot compare", not mismatch, and keep the existing behavior.
|
|
130
|
+
const proxyVersion = typeof body.version === "string" ? body.version : undefined;
|
|
131
|
+
if (computeVersionSkew(deps.cliVersion ?? ownCliVersion(), proxyVersion).skewed) {
|
|
132
|
+
return rejected("restart_version_skew");
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
let observed: LiveProxy | null;
|
|
111
136
|
try {
|
|
112
137
|
observed = await (deps.findLive ?? findLiveProxy)({ deadlineAt, nowFn: now });
|
|
@@ -627,10 +627,20 @@ function opencodeProviderConnection(baseURL: string, config: OcxConfig): Opencod
|
|
|
627
627
|
* override a default the user controls in opencodex. Variants are opt-in per selection,
|
|
628
628
|
* which is the same reason we never emit `defaultModel` for MCode.
|
|
629
629
|
*
|
|
630
|
-
* `none` is dropped even when a ladder declares it.
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
630
|
+
* `none` is dropped even when a ladder declares it.
|
|
631
|
+
*
|
|
632
|
+
* The original reason no longer holds and is recorded here so it is not repeated: the chat
|
|
633
|
+
* ingress `OUTPUT_CONFIG_EFFORTS` allowlist DID omit `none`, so selecting it sent no effort
|
|
634
|
+
* at all and fell back to the proxy default. That allowlist now accepts `none` (audit F7),
|
|
635
|
+
* because it is the runtime's disable sentinel and dropping it let a provider default
|
|
636
|
+
* re-enable thinking a caller had turned off.
|
|
637
|
+
*
|
|
638
|
+
* The variant stays filtered anyway, deliberately and narrowly: emitting it would change
|
|
639
|
+
* what this exporter writes into a user's opencode config, and whether opencode's own
|
|
640
|
+
* picker round-trips `reasoningEffort: "none"` to the wire this proxy reads has not been
|
|
641
|
+
* verified here. Re-enabling it is a scoped follow-up that needs that check first, not a
|
|
642
|
+
* side effect of an ingress fix. MCode and ZCode filter `none` for their own separate
|
|
643
|
+
* reasons, documented at their call sites.
|
|
634
644
|
*/
|
|
635
645
|
function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[] | undefined {
|
|
636
646
|
if (model.reasoningEfforts === undefined) return undefined;
|
|
@@ -524,6 +524,31 @@ function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number |
|
|
|
524
524
|
return listUnixProcSnapshots(getuid());
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
export interface ListProcessSnapshotsOptions {
|
|
528
|
+
platform?: NodeJS.Platform;
|
|
529
|
+
getuid?: () => number | undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Raw process snapshots for callers that need to match their own predicate.
|
|
534
|
+
*
|
|
535
|
+
* Throws on enumeration failure. That is the contract routing-adoption needs:
|
|
536
|
+
* a thrown read is "could not enumerate" and must never collapse to an empty
|
|
537
|
+
* list. listCodexAppServerProcesses maps the same failure to [] for the #476
|
|
538
|
+
* kill path, which would otherwise print a false adopted for #4550.
|
|
539
|
+
*/
|
|
540
|
+
export function listProcessSnapshots(options: ListProcessSnapshotsOptions = {}): ProcessSnapshot[] {
|
|
541
|
+
const platform = options.platform ?? process.platform;
|
|
542
|
+
const getuid = options.getuid ?? (() => {
|
|
543
|
+
try {
|
|
544
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
545
|
+
} catch {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
return defaultListSnapshots(platform, getuid);
|
|
550
|
+
}
|
|
551
|
+
|
|
527
552
|
export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcess[] {
|
|
528
553
|
const platform = io.platform ?? process.platform;
|
|
529
554
|
const getuid = io.getuid ?? (() => {
|