@bitkyc08/opencodex 1.9.0 → 1.9.2
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/codex-catalog.ts +38 -13
- package/src/codex-inject.ts +7 -3
- 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 => ({
|
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
|
/**
|
|
@@ -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
|
@@ -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/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 false; set true to opt
|
|
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;
|