@bitkyc08/opencodex 1.9.1 → 1.9.3
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/package.json +1 -1
- package/src/adapters/openai-chat.ts +28 -1
- package/src/adapters/openai-responses.ts +21 -1
- package/src/codex-catalog.ts +39 -14
- package/src/codex-inject.ts +8 -4
- package/src/config.ts +2 -2
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { contentPartsToText } from "./image";
|
|
|
7
7
|
function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
8
8
|
const out: unknown[] = [];
|
|
9
9
|
const { context, options } = parsed;
|
|
10
|
+
let pendingToolCallIds = new Set<string>();
|
|
10
11
|
|
|
11
12
|
if (context.systemPrompt && context.systemPrompt.length > 0) {
|
|
12
13
|
// Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
|
|
@@ -39,6 +40,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
|
39
40
|
out.push({ role: "user", content: chatParts });
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
pendingToolCallIds = new Set();
|
|
42
44
|
break;
|
|
43
45
|
}
|
|
44
46
|
case "assistant": {
|
|
@@ -61,14 +63,33 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
|
61
63
|
// like DeepSeek reject an assistant message with neither content nor tool_calls.
|
|
62
64
|
if (chatMsg.content === undefined && chatMsg.tool_calls === undefined) break;
|
|
63
65
|
out.push(chatMsg);
|
|
66
|
+
pendingToolCallIds = new Set(toolCalls.map(tc => tc.id).filter(Boolean));
|
|
64
67
|
break;
|
|
65
68
|
}
|
|
66
69
|
case "toolResult": {
|
|
70
|
+
let toolCallId = msg.toolCallId;
|
|
71
|
+
if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
|
|
72
|
+
if (!pendingToolCallIds.has(toolCallId)) {
|
|
73
|
+
// WS turns can arrive with only tool outputs; chat-completions providers reject a bare
|
|
74
|
+
// role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
|
|
75
|
+
const name = safeToolName(msg.toolName);
|
|
76
|
+
out.push({
|
|
77
|
+
role: "assistant",
|
|
78
|
+
content: null,
|
|
79
|
+
tool_calls: [{
|
|
80
|
+
id: toolCallId,
|
|
81
|
+
type: "function",
|
|
82
|
+
function: { name, arguments: "{}" },
|
|
83
|
+
}],
|
|
84
|
+
});
|
|
85
|
+
pendingToolCallIds = new Set([toolCallId]);
|
|
86
|
+
}
|
|
67
87
|
out.push({
|
|
68
88
|
role: "tool",
|
|
69
|
-
tool_call_id:
|
|
89
|
+
tool_call_id: toolCallId,
|
|
70
90
|
content: contentPartsToText(msg.content),
|
|
71
91
|
});
|
|
92
|
+
pendingToolCallIds.delete(toolCallId);
|
|
72
93
|
break;
|
|
73
94
|
}
|
|
74
95
|
}
|
|
@@ -77,6 +98,12 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
|
|
|
77
98
|
return out;
|
|
78
99
|
}
|
|
79
100
|
|
|
101
|
+
function safeToolName(name: string | undefined): string {
|
|
102
|
+
const raw = name && name.trim().length > 0 ? name : "tool_result";
|
|
103
|
+
const sanitized = raw.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
104
|
+
return sanitized.length > 0 ? sanitized : "tool_result";
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
function toolsToChatFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
81
108
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
82
109
|
return parsed.context.tools.map(t => ({
|
|
@@ -22,6 +22,26 @@ export const FORWARD_HEADERS = [
|
|
|
22
22
|
"x-responsesapi-include-timing-metrics",
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
+
function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
26
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
27
|
+
const raw = body as Record<string, unknown>;
|
|
28
|
+
if (!Array.isArray(raw.input)) return body;
|
|
29
|
+
|
|
30
|
+
let changed = false;
|
|
31
|
+
const input = raw.input.map(item => {
|
|
32
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
33
|
+
const rec = item as Record<string, unknown>;
|
|
34
|
+
if (rec.type !== "reasoning" || !Array.isArray(rec.content) || rec.content.length === 0) return item;
|
|
35
|
+
changed = true;
|
|
36
|
+
// Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
|
|
37
|
+
// native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
|
|
38
|
+
// `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
|
|
39
|
+
return { ...rec, content: [] };
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return changed ? { ...raw, input } : body;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
|
|
26
46
|
return {
|
|
27
47
|
name: "openai-responses",
|
|
@@ -49,7 +69,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
49
69
|
url,
|
|
50
70
|
method: "POST",
|
|
51
71
|
headers,
|
|
52
|
-
body: JSON.stringify(parsed._rawBody),
|
|
72
|
+
body: JSON.stringify(sanitizeReasoningInputContent(parsed._rawBody)),
|
|
53
73
|
};
|
|
54
74
|
},
|
|
55
75
|
|
package/src/codex-catalog.ts
CHANGED
|
@@ -57,6 +57,12 @@ function readCatalog(path: string): { models?: RawEntry[]; [k: string]: unknown
|
|
|
57
57
|
} catch { return null; }
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function findNativeTemplate(catalog: { models?: RawEntry[] } | null): RawEntry | null {
|
|
61
|
+
return catalog?.models?.find(
|
|
62
|
+
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
|
|
63
|
+
) ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
function normalizeServiceTiers(entry: RawEntry): RawEntry {
|
|
61
67
|
// Codex stores the user-facing config spelling as "fast", but the catalog/request
|
|
62
68
|
// service tier id is "priority" in current codex-rs. Keep legacy catalogs working.
|
|
@@ -83,6 +89,28 @@ function ensureAutoCompactTokenLimit(entry: RawEntry): RawEntry {
|
|
|
83
89
|
return entry;
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
function ensureStrictCatalogFields(entry: RawEntry): RawEntry {
|
|
93
|
+
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = true;
|
|
94
|
+
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
|
|
95
|
+
if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true;
|
|
96
|
+
if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low";
|
|
97
|
+
if (typeof entry.apply_patch_tool_type !== "string") entry.apply_patch_tool_type = "freeform";
|
|
98
|
+
if (!entry.truncation_policy || typeof entry.truncation_policy !== "object" || Array.isArray(entry.truncation_policy)) {
|
|
99
|
+
entry.truncation_policy = { mode: "tokens", limit: 10000 };
|
|
100
|
+
}
|
|
101
|
+
if (typeof entry.supports_parallel_tool_calls !== "boolean") entry.supports_parallel_tool_calls = true;
|
|
102
|
+
if (typeof entry.supports_image_detail_original !== "boolean") entry.supports_image_detail_original = false;
|
|
103
|
+
if (!Array.isArray(entry.experimental_supported_tools)) entry.experimental_supported_tools = [];
|
|
104
|
+
if (!Array.isArray(entry.input_modalities)) entry.input_modalities = ["text"];
|
|
105
|
+
if (typeof entry.context_window !== "number" || entry.context_window <= 0) entry.context_window = 128000;
|
|
106
|
+
if (typeof entry.max_context_window !== "number" || entry.max_context_window <= 0) {
|
|
107
|
+
entry.max_context_window = entry.context_window;
|
|
108
|
+
}
|
|
109
|
+
if (typeof entry.effective_context_window_percent !== "number") entry.effective_context_window_percent = 95;
|
|
110
|
+
if (typeof entry.comp_hash !== "string") entry.comp_hash = "opencodex";
|
|
111
|
+
return ensureAutoCompactTokenLimit(entry);
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
|
|
87
115
|
delete entry.model_messages;
|
|
88
116
|
delete entry.tool_mode;
|
|
@@ -97,7 +125,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
|
|
|
97
125
|
// runs through native gpt-5.4-mini, so image search is available and verbalized for text-only models.
|
|
98
126
|
entry.web_search_tool_type = "text_and_image";
|
|
99
127
|
entry.supports_search_tool = true;
|
|
100
|
-
return
|
|
128
|
+
return ensureStrictCatalogFields(entry);
|
|
101
129
|
}
|
|
102
130
|
|
|
103
131
|
function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
|
|
@@ -122,8 +150,8 @@ function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
|
|
|
122
150
|
|
|
123
151
|
function loadCatalogForSync(path: string): { models?: RawEntry[]; [k: string]: unknown } | null {
|
|
124
152
|
const catalog = readCatalog(path);
|
|
125
|
-
if (catalog) return catalog;
|
|
126
|
-
return readCatalog(CODEX_MODELS_CACHE_PATH);
|
|
153
|
+
if (catalog && findNativeTemplate(catalog)) return catalog;
|
|
154
|
+
return readCatalog(CATALOG_BACKUP_PATH) ?? readCatalog(CODEX_MODELS_CACHE_PATH) ?? catalog;
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
function readCurrentCatalogOrCache(): { models?: RawEntry[]; [k: string]: unknown } | null {
|
|
@@ -136,10 +164,9 @@ function readCurrentCatalogOrCache(): { models?: RawEntry[]; [k: string]: unknow
|
|
|
136
164
|
* Returns a deep copy, or null if no catalog/native entry exists.
|
|
137
165
|
*/
|
|
138
166
|
export function loadCatalogTemplate(): RawEntry | null {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
167
|
+
const native = findNativeTemplate(readCatalog(readCodexCatalogPath()))
|
|
168
|
+
?? findNativeTemplate(readCatalog(CATALOG_BACKUP_PATH))
|
|
169
|
+
?? findNativeTemplate(readCatalog(CODEX_MODELS_CACHE_PATH));
|
|
143
170
|
return native ? JSON.parse(JSON.stringify(native)) : null;
|
|
144
171
|
}
|
|
145
172
|
|
|
@@ -186,7 +213,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
186
213
|
normalizeRoutedCatalogEntry(e);
|
|
187
214
|
applyJawcodeCatalogMetadata(e, slug);
|
|
188
215
|
}
|
|
189
|
-
return
|
|
216
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(e));
|
|
190
217
|
}
|
|
191
218
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
192
219
|
const entry: RawEntry = {
|
|
@@ -198,7 +225,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
198
225
|
...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
|
|
199
226
|
};
|
|
200
227
|
applyJawcodeCatalogMetadata(entry, slug);
|
|
201
|
-
return
|
|
228
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(entry));
|
|
202
229
|
}
|
|
203
230
|
|
|
204
231
|
/**
|
|
@@ -206,7 +233,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
206
233
|
* catalog sync and the proxy `/v1/models?client_version` branch.
|
|
207
234
|
* Native gpt slugs stay bare; routed models are namespaced `<provider>/<model>`.
|
|
208
235
|
*/
|
|
209
|
-
export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[], goModels: CatalogModel[], featured?: string[], wsEnabled =
|
|
236
|
+
export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[], goModels: CatalogModel[], featured?: string[], wsEnabled = false): RawEntry[] {
|
|
210
237
|
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
211
238
|
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
212
239
|
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
@@ -351,9 +378,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
351
378
|
const catalog = loadCatalogForSync(catalogPath);
|
|
352
379
|
if (!catalog) return { added: 0, path: catalogPath };
|
|
353
380
|
|
|
354
|
-
const template = (catalog
|
|
355
|
-
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
|
|
356
|
-
) ?? null;
|
|
381
|
+
const template = findNativeTemplate(catalog);
|
|
357
382
|
|
|
358
383
|
const goModels = await gatherRoutedModels(config);
|
|
359
384
|
if (goModels.length === 0) return { added: 0, path: catalogPath };
|
|
@@ -383,7 +408,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
383
408
|
// native template can never leak supports_websockets while the flag is off.
|
|
384
409
|
const wsEnabled = websocketsEnabled(config);
|
|
385
410
|
catalog.models = [...native, ...goEntries].map(m => {
|
|
386
|
-
const e =
|
|
411
|
+
const e = ensureStrictCatalogFields(normalizeServiceTiers(m));
|
|
387
412
|
if (wsEnabled) e.supports_websockets = true;
|
|
388
413
|
else delete e.supports_websockets;
|
|
389
414
|
return e;
|
package/src/codex-inject.ts
CHANGED
|
@@ -14,7 +14,7 @@ const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
|
14
14
|
* whatever `[table]` happened to be open last (e.g. `[plugins."chrome@openai-bundled"]`), so Codex
|
|
15
15
|
* never saw a global model_provider and silently fell back to the `openai` (ChatGPT) provider.
|
|
16
16
|
*/
|
|
17
|
-
export function buildProviderTableBlock(port: number, supportsWebsockets =
|
|
17
|
+
export function buildProviderTableBlock(port: number, supportsWebsockets = false): string {
|
|
18
18
|
const lines = [
|
|
19
19
|
"",
|
|
20
20
|
OCX_SECTION_MARKER,
|
|
@@ -143,12 +143,16 @@ function ensureFastModeFeature(content: string): string {
|
|
|
143
143
|
return lines.join("\n");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function isOpencodexCatalogPath(path: string): boolean {
|
|
147
|
+
return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stripOpencodexCatalogPath(content: string): string {
|
|
147
151
|
return content
|
|
148
152
|
.split("\n")
|
|
149
153
|
.filter(line => {
|
|
150
154
|
const m = line.match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
|
|
151
|
-
return !m || parseTomlString(m[1])
|
|
155
|
+
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
152
156
|
})
|
|
153
157
|
.join("\n");
|
|
154
158
|
}
|
|
@@ -240,7 +244,7 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
240
244
|
// must match the detection regex above, or a detected line could survive un-removed.
|
|
241
245
|
out = out.split("\n").filter(l => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l)).join("\n");
|
|
242
246
|
out = stripRootContextWindowOverrides(out);
|
|
243
|
-
out =
|
|
247
|
+
out = stripOpencodexCatalogPath(out);
|
|
244
248
|
return out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
245
249
|
}
|
|
246
250
|
|
package/src/config.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function saveConfig(config: OcxConfig): void {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function websocketsEnabled(config: Pick<OcxConfig, "websockets">): boolean {
|
|
61
|
-
return config.websockets
|
|
61
|
+
return config.websockets === true;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function getDefaultConfig(): OcxConfig {
|
|
@@ -76,7 +76,7 @@ export function getDefaultConfig(): OcxConfig {
|
|
|
76
76
|
},
|
|
77
77
|
defaultProvider: "openai",
|
|
78
78
|
subagentModels: [...DEFAULT_SUBAGENT_MODELS],
|
|
79
|
-
websockets:
|
|
79
|
+
websockets: false,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
package/src/types.ts
CHANGED
|
@@ -175,7 +175,7 @@ export interface OcxConfig {
|
|
|
175
175
|
subagentModels?: string[];
|
|
176
176
|
/** Routed model ids ("<provider>/<model>") hidden from Codex (excluded from the catalog + /v1/models). */
|
|
177
177
|
disabledModels?: string[];
|
|
178
|
-
/** Advertise supports_websockets so Codex opens the WS endpoint. Default
|
|
178
|
+
/** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
|
|
179
179
|
websockets?: boolean;
|
|
180
180
|
/** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
|
|
181
181
|
modelCacheTtlMs?: number;
|