@oai404iao/pi-codex-minimal-tools 1.3.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/LICENSE +28 -0
- package/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
- package/README.md +410 -0
- package/THIRD_PARTY_NOTICES.md +97 -0
- package/config.schema.json +174 -0
- package/models.schema.json +217 -0
- package/package.json +87 -0
- package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
- package/src/activation.ts +56 -0
- package/src/background-image-generation.ts +574 -0
- package/src/capabilities.ts +146 -0
- package/src/codex-http.ts +133 -0
- package/src/codex-request-profile.ts +45 -0
- package/src/codex-reserved-tools.ts +323 -0
- package/src/codex-wire-identity.ts +182 -0
- package/src/fast-mode.ts +124 -0
- package/src/glyphs.ts +70 -0
- package/src/index.ts +332 -0
- package/src/model-catalog/catalog.ts +636 -0
- package/src/model-catalog/default-models.json +252 -0
- package/src/model-catalog/runtime.ts +113 -0
- package/src/model-catalog/types.ts +95 -0
- package/src/native-compaction.ts +393 -0
- package/src/patch/apply.ts +338 -0
- package/src/patch/parser.ts +224 -0
- package/src/patch/render.ts +201 -0
- package/src/provider-headers.ts +54 -0
- package/src/provider-native-tools.ts +71 -0
- package/src/provider-shim.ts +4338 -0
- package/src/providers/codex-apply-patch-tool.ts +23 -0
- package/src/providers/codex-apply-patch.lark +19 -0
- package/src/providers/openai-responses-shared.ts +1463 -0
- package/src/settings.ts +247 -0
- package/src/tools/apply-patch.ts +84 -0
- package/src/tools/image-generation.ts +274 -0
- package/src/tools/view-image.ts +98 -0
- package/src/tools/web-search.ts +524 -0
- package/src/utils/images.ts +73 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SETTINGS,
|
|
6
|
+
configDir,
|
|
7
|
+
getSettingsSource,
|
|
8
|
+
loadSettings,
|
|
9
|
+
type CodexMinimalToolsSettings,
|
|
10
|
+
} from "../settings.js";
|
|
11
|
+
import { resolveCodexRequestProfile } from "../codex-request-profile.js";
|
|
12
|
+
import type {
|
|
13
|
+
EffectiveModelProfile,
|
|
14
|
+
FastModeProfile,
|
|
15
|
+
ModelCatalogFile,
|
|
16
|
+
ModelIdentityLike,
|
|
17
|
+
ModelProfilePatch,
|
|
18
|
+
ModelProfileSource,
|
|
19
|
+
NativeCompactionMode,
|
|
20
|
+
ReasoningSummary,
|
|
21
|
+
ResolvedModelProfile,
|
|
22
|
+
ResponsesEndpoint,
|
|
23
|
+
ResponsesMode,
|
|
24
|
+
ResponsesProfilePatch,
|
|
25
|
+
ResponsesTransport,
|
|
26
|
+
SystemPromptPlacement,
|
|
27
|
+
WebSearchContentType,
|
|
28
|
+
WebSearchProfile,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
|
|
31
|
+
export const MODELS_FILE_NAME = "models.json";
|
|
32
|
+
|
|
33
|
+
type JsonRecord = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
interface CatalogEntry {
|
|
36
|
+
patch: ModelProfilePatch;
|
|
37
|
+
sources: ModelProfileSource[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface LoadedCatalog {
|
|
41
|
+
entries: Map<string, CatalogEntry>;
|
|
42
|
+
resolved: Map<string, ModelProfilePatch>;
|
|
43
|
+
diagnostics: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SAFE_PROFILE: EffectiveModelProfile = {
|
|
47
|
+
enabled: true,
|
|
48
|
+
responses: {
|
|
49
|
+
providerShim: false,
|
|
50
|
+
endpoint: "auto",
|
|
51
|
+
mode: "standard",
|
|
52
|
+
reasoningSummary: "auto",
|
|
53
|
+
systemPromptPlacement: "instructions",
|
|
54
|
+
transport: "sse",
|
|
55
|
+
websocketPrewarm: false,
|
|
56
|
+
},
|
|
57
|
+
tools: {
|
|
58
|
+
parallelCalls: true,
|
|
59
|
+
applyPatch: false,
|
|
60
|
+
webSearch: false,
|
|
61
|
+
imageGeneration: false,
|
|
62
|
+
viewImage: false,
|
|
63
|
+
},
|
|
64
|
+
compaction: "pi",
|
|
65
|
+
fast: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const BUNDLED_CATALOG = parseBundledCatalog();
|
|
69
|
+
|
|
70
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
71
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function diagnoseUnknownKeys(
|
|
75
|
+
value: JsonRecord,
|
|
76
|
+
allowed: readonly string[],
|
|
77
|
+
path: string,
|
|
78
|
+
diagnostics: string[],
|
|
79
|
+
): void {
|
|
80
|
+
const allowedKeys = new Set(allowed);
|
|
81
|
+
for (const key of Object.keys(value)) {
|
|
82
|
+
if (!allowedKeys.has(key)) diagnostics.push(`${path}: unknown property ${key}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeId(value: string): string {
|
|
87
|
+
return value.trim().toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validModelId(value: unknown): value is string {
|
|
91
|
+
return typeof value === "string" && /^[^\s/]+\/\S+$/.test(value.trim());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function stringEnum<T extends string>(value: unknown, values: readonly T[]): T | undefined {
|
|
95
|
+
return typeof value === "string" && values.includes(value as T) ? value as T : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sanitizeContentTypes(
|
|
99
|
+
value: unknown,
|
|
100
|
+
path: string,
|
|
101
|
+
diagnostics: string[],
|
|
102
|
+
): WebSearchContentType[] | undefined {
|
|
103
|
+
if (value === undefined) return undefined;
|
|
104
|
+
if (!Array.isArray(value)) {
|
|
105
|
+
diagnostics.push(`${path}: contentTypes must be an array`);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const result: WebSearchContentType[] = [];
|
|
109
|
+
for (const item of value) {
|
|
110
|
+
if (item !== "text" && item !== "image") {
|
|
111
|
+
diagnostics.push(`${path}: unsupported content type ${JSON.stringify(item)}`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!result.includes(item)) result.push(item);
|
|
115
|
+
}
|
|
116
|
+
if (result.length === 0) {
|
|
117
|
+
diagnostics.push(`${path}: contentTypes must contain text and/or image`);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizeWebSearch(
|
|
124
|
+
value: unknown,
|
|
125
|
+
path: string,
|
|
126
|
+
diagnostics: string[],
|
|
127
|
+
): false | WebSearchProfile | undefined {
|
|
128
|
+
if (value === undefined || value === false) return value;
|
|
129
|
+
if (!isRecord(value)) {
|
|
130
|
+
diagnostics.push(`${path}: webSearch must be false or an object`);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
diagnoseUnknownKeys(value, ["implementation", "contentTypes"], `${path}.tools.webSearch`, diagnostics);
|
|
134
|
+
const implementation = stringEnum(value.implementation, ["hosted", "standalone"] as const);
|
|
135
|
+
if (!implementation) {
|
|
136
|
+
diagnostics.push(`${path}: webSearch.implementation must be hosted or standalone`);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const contentTypes = sanitizeContentTypes(value.contentTypes, path, diagnostics);
|
|
140
|
+
if (value.contentTypes !== undefined && !contentTypes) return false;
|
|
141
|
+
return {
|
|
142
|
+
implementation,
|
|
143
|
+
...(contentTypes ? { contentTypes } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sanitizeResponses(
|
|
148
|
+
value: unknown,
|
|
149
|
+
path: string,
|
|
150
|
+
diagnostics: string[],
|
|
151
|
+
): ResponsesProfilePatch | undefined {
|
|
152
|
+
if (value === undefined) return undefined;
|
|
153
|
+
if (!isRecord(value)) {
|
|
154
|
+
diagnostics.push(`${path}: responses must be an object`);
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
diagnoseUnknownKeys(
|
|
158
|
+
value,
|
|
159
|
+
["providerShim", "endpoint", "mode", "reasoningSummary", "systemPromptPlacement", "transport", "websocketPrewarm"],
|
|
160
|
+
`${path}.responses`,
|
|
161
|
+
diagnostics,
|
|
162
|
+
);
|
|
163
|
+
const result: ResponsesProfilePatch = {};
|
|
164
|
+
if (typeof value.providerShim === "boolean") result.providerShim = value.providerShim;
|
|
165
|
+
else if (value.providerShim !== undefined) diagnostics.push(`${path}: responses.providerShim must be boolean`);
|
|
166
|
+
const endpoint = stringEnum<ResponsesEndpoint>(value.endpoint, ["auto", "openai", "codex"]);
|
|
167
|
+
if (endpoint) result.endpoint = endpoint;
|
|
168
|
+
else if (value.endpoint !== undefined) diagnostics.push(`${path}: invalid responses.endpoint`);
|
|
169
|
+
const mode = stringEnum<ResponsesMode>(value.mode, ["standard", "lite"]);
|
|
170
|
+
if (mode) result.mode = mode;
|
|
171
|
+
else if (value.mode !== undefined) diagnostics.push(`${path}: invalid responses.mode`);
|
|
172
|
+
const reasoningSummary = stringEnum<ReasoningSummary>(
|
|
173
|
+
value.reasoningSummary,
|
|
174
|
+
["auto", "concise", "detailed", "none"],
|
|
175
|
+
);
|
|
176
|
+
if (reasoningSummary) result.reasoningSummary = reasoningSummary;
|
|
177
|
+
else if (value.reasoningSummary !== undefined) diagnostics.push(`${path}: invalid responses.reasoningSummary`);
|
|
178
|
+
const placement = stringEnum<SystemPromptPlacement>(value.systemPromptPlacement, ["instructions", "developer"]);
|
|
179
|
+
if (placement) result.systemPromptPlacement = placement;
|
|
180
|
+
else if (value.systemPromptPlacement !== undefined) diagnostics.push(`${path}: invalid responses.systemPromptPlacement`);
|
|
181
|
+
const transport = stringEnum<ResponsesTransport>(value.transport, ["sse", "websocket", "websocket-cached", "auto"]);
|
|
182
|
+
if (transport) result.transport = transport;
|
|
183
|
+
else if (value.transport !== undefined) diagnostics.push(`${path}: invalid responses.transport`);
|
|
184
|
+
if (typeof value.websocketPrewarm === "boolean") result.websocketPrewarm = value.websocketPrewarm;
|
|
185
|
+
else if (value.websocketPrewarm !== undefined) diagnostics.push(`${path}: responses.websocketPrewarm must be boolean`);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sanitizeFast(
|
|
190
|
+
value: unknown,
|
|
191
|
+
path: string,
|
|
192
|
+
diagnostics: string[],
|
|
193
|
+
): false | FastModeProfile | undefined {
|
|
194
|
+
if (value === undefined || value === false) return value;
|
|
195
|
+
if (!isRecord(value) || typeof value.serviceTier !== "string" || !value.serviceTier.trim()) {
|
|
196
|
+
diagnostics.push(`${path}: fast must be false or an object with serviceTier`);
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
diagnoseUnknownKeys(value, ["serviceTier", "costMultiplier"], `${path}.fast`, diagnostics);
|
|
200
|
+
const result: FastModeProfile = { serviceTier: value.serviceTier.trim() };
|
|
201
|
+
if (value.costMultiplier !== undefined) {
|
|
202
|
+
if (typeof value.costMultiplier === "number" && Number.isFinite(value.costMultiplier) && value.costMultiplier > 0) {
|
|
203
|
+
result.costMultiplier = value.costMultiplier;
|
|
204
|
+
} else {
|
|
205
|
+
diagnostics.push(`${path}: fast.costMultiplier must be a positive number`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sanitizeProfile(
|
|
212
|
+
value: unknown,
|
|
213
|
+
path: string,
|
|
214
|
+
diagnostics: string[],
|
|
215
|
+
): ModelProfilePatch | undefined {
|
|
216
|
+
if (!isRecord(value)) {
|
|
217
|
+
diagnostics.push(`${path}: model entry must be an object`);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
diagnoseUnknownKeys(
|
|
221
|
+
value,
|
|
222
|
+
["id", "extends", "enabled", "responses", "tools", "compaction", "fast"],
|
|
223
|
+
path,
|
|
224
|
+
diagnostics,
|
|
225
|
+
);
|
|
226
|
+
if (!validModelId(value.id)) {
|
|
227
|
+
diagnostics.push(`${path}: id must be an exact provider/model id`);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const id = value.id.trim();
|
|
231
|
+
const result: ModelProfilePatch = { id };
|
|
232
|
+
if (value.extends !== undefined) {
|
|
233
|
+
if (validModelId(value.extends)) result.extends = value.extends.trim();
|
|
234
|
+
else diagnostics.push(`${path}: extends must be an exact provider/model id`);
|
|
235
|
+
}
|
|
236
|
+
if (typeof value.enabled === "boolean") result.enabled = value.enabled;
|
|
237
|
+
else if (value.enabled !== undefined) diagnostics.push(`${path}: enabled must be boolean`);
|
|
238
|
+
const responses = sanitizeResponses(value.responses, path, diagnostics);
|
|
239
|
+
if (responses) result.responses = responses;
|
|
240
|
+
if (value.tools !== undefined) {
|
|
241
|
+
if (!isRecord(value.tools)) {
|
|
242
|
+
diagnostics.push(`${path}: tools must be an object`);
|
|
243
|
+
} else {
|
|
244
|
+
diagnoseUnknownKeys(
|
|
245
|
+
value.tools,
|
|
246
|
+
["parallelCalls", "applyPatch", "webSearch", "imageGeneration", "viewImage"],
|
|
247
|
+
`${path}.tools`,
|
|
248
|
+
diagnostics,
|
|
249
|
+
);
|
|
250
|
+
const tools: NonNullable<ModelProfilePatch["tools"]> = {};
|
|
251
|
+
if (typeof value.tools.parallelCalls === "boolean") tools.parallelCalls = value.tools.parallelCalls;
|
|
252
|
+
else if (value.tools.parallelCalls !== undefined) diagnostics.push(`${path}: tools.parallelCalls must be boolean`);
|
|
253
|
+
if (value.tools.applyPatch === false || value.tools.applyPatch === "function" || value.tools.applyPatch === "custom") {
|
|
254
|
+
tools.applyPatch = value.tools.applyPatch;
|
|
255
|
+
} else if (value.tools.applyPatch !== undefined) {
|
|
256
|
+
diagnostics.push(`${path}: tools.applyPatch must be false, function, or custom`);
|
|
257
|
+
}
|
|
258
|
+
const webSearch = sanitizeWebSearch(value.tools.webSearch, path, diagnostics);
|
|
259
|
+
if (webSearch !== undefined) tools.webSearch = webSearch;
|
|
260
|
+
if (
|
|
261
|
+
value.tools.imageGeneration === false
|
|
262
|
+
|| value.tools.imageGeneration === "hosted"
|
|
263
|
+
|| value.tools.imageGeneration === "standalone"
|
|
264
|
+
) {
|
|
265
|
+
tools.imageGeneration = value.tools.imageGeneration;
|
|
266
|
+
} else if (value.tools.imageGeneration !== undefined) {
|
|
267
|
+
diagnostics.push(`${path}: tools.imageGeneration must be false, hosted, or standalone`);
|
|
268
|
+
}
|
|
269
|
+
if (typeof value.tools.viewImage === "boolean") tools.viewImage = value.tools.viewImage;
|
|
270
|
+
else if (value.tools.viewImage !== undefined) diagnostics.push(`${path}: tools.viewImage must be boolean`);
|
|
271
|
+
result.tools = tools;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const compaction = stringEnum<NativeCompactionMode>(value.compaction, ["pi", "responses", "responses-compact"]);
|
|
275
|
+
if (compaction) result.compaction = compaction;
|
|
276
|
+
else if (value.compaction !== undefined) diagnostics.push(`${path}: invalid compaction mode`);
|
|
277
|
+
const fast = sanitizeFast(value.fast, path, diagnostics);
|
|
278
|
+
if (fast !== undefined) result.fast = fast;
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseCatalog(value: unknown, path: string): { models: ModelProfilePatch[]; diagnostics: string[] } {
|
|
283
|
+
const diagnostics: string[] = [];
|
|
284
|
+
if (!isRecord(value)) return { models: [], diagnostics: [`${path}: root value must be an object`] };
|
|
285
|
+
diagnoseUnknownKeys(value, ["$schema", "version", "models"], path, diagnostics);
|
|
286
|
+
if (value.version !== 1) return { models: [], diagnostics: [`${path}: version must be 1`] };
|
|
287
|
+
if (!Array.isArray(value.models)) return { models: [], diagnostics: [...diagnostics, `${path}: models must be an array`] };
|
|
288
|
+
const models: ModelProfilePatch[] = [];
|
|
289
|
+
const seen = new Set<string>();
|
|
290
|
+
for (const [index, candidate] of value.models.entries()) {
|
|
291
|
+
const profile = sanitizeProfile(candidate, `${path}: models[${index}]`, diagnostics);
|
|
292
|
+
if (!profile) continue;
|
|
293
|
+
const key = normalizeId(profile.id);
|
|
294
|
+
if (seen.has(key)) {
|
|
295
|
+
diagnostics.push(`${path}: duplicate model id ${profile.id}`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
seen.add(key);
|
|
299
|
+
models.push(profile);
|
|
300
|
+
}
|
|
301
|
+
return { models, diagnostics };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parseBundledCatalog(): ModelProfilePatch[] {
|
|
305
|
+
const path = new URL("./default-models.json", import.meta.url);
|
|
306
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as ModelCatalogFile;
|
|
307
|
+
const result = parseCatalog(parsed, "bundled model catalog");
|
|
308
|
+
if (result.diagnostics.length > 0) {
|
|
309
|
+
throw new Error(result.diagnostics.join("\n"));
|
|
310
|
+
}
|
|
311
|
+
return result.models;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function deepMerge<T>(base: T, patch: unknown): T {
|
|
315
|
+
if (!isRecord(base) || !isRecord(patch)) return patch as T;
|
|
316
|
+
const output: JsonRecord = { ...base };
|
|
317
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
318
|
+
const current = output[key];
|
|
319
|
+
output[key] = isRecord(current) && isRecord(value)
|
|
320
|
+
? deepMerge(current, value)
|
|
321
|
+
: value;
|
|
322
|
+
}
|
|
323
|
+
return output as T;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function modelsPath(agentDir?: string): string {
|
|
327
|
+
return join(configDir(agentDir), MODELS_FILE_NAME);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function loadUserCatalog(): { models: ModelProfilePatch[]; diagnostics: string[] } {
|
|
331
|
+
const path = modelsPath();
|
|
332
|
+
if (!existsSync(path)) {
|
|
333
|
+
return { models: [], diagnostics: [] };
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
337
|
+
return parseCatalog(parsed, path);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const diagnostics = [`${path}: ${error instanceof Error ? error.message : String(error)}`];
|
|
340
|
+
return { models: [], diagnostics };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function buildCatalog(): LoadedCatalog {
|
|
345
|
+
const entries = new Map<string, CatalogEntry>();
|
|
346
|
+
for (const patch of BUNDLED_CATALOG) {
|
|
347
|
+
entries.set(normalizeId(patch.id), { patch, sources: ["bundled"] });
|
|
348
|
+
}
|
|
349
|
+
const user = loadUserCatalog();
|
|
350
|
+
for (const patch of user.models) {
|
|
351
|
+
const key = normalizeId(patch.id);
|
|
352
|
+
const existing = entries.get(key);
|
|
353
|
+
entries.set(key, {
|
|
354
|
+
patch: existing ? deepMerge(existing.patch, patch) : patch,
|
|
355
|
+
sources: existing ? ["bundled", "user"] : ["user"],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const diagnostics = [...user.diagnostics];
|
|
360
|
+
const resolved = new Map<string, ModelProfilePatch>();
|
|
361
|
+
const resolving: string[] = [];
|
|
362
|
+
const invalid = new Set<string>();
|
|
363
|
+
const resolveEntry = (key: string): ModelProfilePatch | undefined => {
|
|
364
|
+
if (resolved.has(key)) return resolved.get(key);
|
|
365
|
+
if (invalid.has(key)) return undefined;
|
|
366
|
+
const entry = entries.get(key);
|
|
367
|
+
if (!entry) return undefined;
|
|
368
|
+
const cycleIndex = resolving.indexOf(key);
|
|
369
|
+
if (cycleIndex >= 0) {
|
|
370
|
+
const cycle = [...resolving.slice(cycleIndex), key]
|
|
371
|
+
.map((item) => entries.get(item)?.patch.id ?? item)
|
|
372
|
+
.join(" -> ");
|
|
373
|
+
diagnostics.push(`model catalog: cyclic extends: ${cycle}`);
|
|
374
|
+
for (const item of resolving.slice(cycleIndex)) invalid.add(item);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
resolving.push(key);
|
|
378
|
+
let patch = entry.patch;
|
|
379
|
+
if (patch.extends) {
|
|
380
|
+
const parentKey = normalizeId(patch.extends);
|
|
381
|
+
const parent = resolveEntry(parentKey);
|
|
382
|
+
if (!parent) {
|
|
383
|
+
if (!invalid.has(key)) diagnostics.push(`model catalog: ${patch.id} extends missing or invalid profile ${patch.extends}`);
|
|
384
|
+
invalid.add(key);
|
|
385
|
+
resolving.pop();
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
patch = {
|
|
389
|
+
...deepMerge(parent, patch),
|
|
390
|
+
id: entry.patch.id,
|
|
391
|
+
extends: entry.patch.extends,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
resolving.pop();
|
|
395
|
+
if (!invalid.has(key)) resolved.set(key, patch);
|
|
396
|
+
return invalid.has(key) ? undefined : patch;
|
|
397
|
+
};
|
|
398
|
+
for (const key of entries.keys()) resolveEntry(key);
|
|
399
|
+
return { entries, resolved, diagnostics };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stableValue(value: unknown): unknown {
|
|
403
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
404
|
+
if (!isRecord(value)) return value;
|
|
405
|
+
return Object.fromEntries(
|
|
406
|
+
Object.entries(value)
|
|
407
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
408
|
+
.map(([key, item]) => [key, stableValue(item)]),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function profileHash(profile: EffectiveModelProfile): string {
|
|
413
|
+
return createHash("sha256")
|
|
414
|
+
.update(JSON.stringify(stableValue(profile)))
|
|
415
|
+
.digest("hex")
|
|
416
|
+
.slice(0, 16);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeProfile(
|
|
420
|
+
patch: ModelProfilePatch,
|
|
421
|
+
diagnostics: string[],
|
|
422
|
+
): EffectiveModelProfile {
|
|
423
|
+
const effective: EffectiveModelProfile = {
|
|
424
|
+
enabled: patch.enabled ?? SAFE_PROFILE.enabled,
|
|
425
|
+
responses: deepMerge(SAFE_PROFILE.responses, patch.responses ?? {}),
|
|
426
|
+
tools: deepMerge(SAFE_PROFILE.tools, patch.tools ?? {}),
|
|
427
|
+
compaction: patch.compaction ?? SAFE_PROFILE.compaction,
|
|
428
|
+
fast: patch.fast ?? SAFE_PROFILE.fast,
|
|
429
|
+
};
|
|
430
|
+
effective.responses.reasoningSummary = resolveCodexRequestProfile({
|
|
431
|
+
responsesMode: effective.responses.mode,
|
|
432
|
+
reasoningSummary: patch.responses?.reasoningSummary,
|
|
433
|
+
}).reasoningSummary;
|
|
434
|
+
|
|
435
|
+
if (effective.responses.mode === "lite") {
|
|
436
|
+
effective.responses.systemPromptPlacement = "developer";
|
|
437
|
+
effective.tools.parallelCalls = false;
|
|
438
|
+
if (effective.tools.webSearch && effective.tools.webSearch.implementation === "hosted") {
|
|
439
|
+
diagnostics.push(`${patch.id}: Responses Lite cannot use hosted web search; webSearch was disabled`);
|
|
440
|
+
effective.tools.webSearch = false;
|
|
441
|
+
}
|
|
442
|
+
if (effective.tools.imageGeneration === "hosted") {
|
|
443
|
+
diagnostics.push(`${patch.id}: Responses Lite cannot use hosted image generation; imageGeneration was disabled`);
|
|
444
|
+
effective.tools.imageGeneration = false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!effective.responses.providerShim) {
|
|
448
|
+
if (effective.tools.applyPatch === "custom") {
|
|
449
|
+
diagnostics.push(`${patch.id}: custom apply_patch requires responses.providerShim; applyPatch was disabled`);
|
|
450
|
+
effective.tools.applyPatch = false;
|
|
451
|
+
}
|
|
452
|
+
if (effective.tools.webSearch && effective.tools.webSearch.implementation === "hosted") {
|
|
453
|
+
diagnostics.push(`${patch.id}: hosted web search requires responses.providerShim; webSearch was disabled`);
|
|
454
|
+
effective.tools.webSearch = false;
|
|
455
|
+
}
|
|
456
|
+
if (effective.tools.imageGeneration === "hosted") {
|
|
457
|
+
diagnostics.push(`${patch.id}: hosted image generation requires responses.providerShim; imageGeneration was disabled`);
|
|
458
|
+
effective.tools.imageGeneration = false;
|
|
459
|
+
}
|
|
460
|
+
if (effective.compaction !== "pi") {
|
|
461
|
+
diagnostics.push(`${patch.id}: native compaction requires responses.providerShim; compaction was reset to pi`);
|
|
462
|
+
effective.compaction = "pi";
|
|
463
|
+
}
|
|
464
|
+
if (effective.fast) {
|
|
465
|
+
diagnostics.push(`${patch.id}: Fast service tiers require responses.providerShim; fast was disabled`);
|
|
466
|
+
effective.fast = false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return effective;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function modelId(model: ModelIdentityLike | undefined): string | undefined {
|
|
473
|
+
const provider = model?.provider?.trim();
|
|
474
|
+
const id = model?.id?.trim() || model?.name?.trim();
|
|
475
|
+
return provider && id ? `${provider}/${id}` : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function valuesDiffer(left: unknown, right: unknown): boolean {
|
|
479
|
+
return JSON.stringify(stableValue(left)) !== JSON.stringify(stableValue(right));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function legacySettingsRecord(settings: CodexMinimalToolsSettings): JsonRecord {
|
|
483
|
+
const source = getSettingsSource(settings);
|
|
484
|
+
if (source) return source;
|
|
485
|
+
const record: JsonRecord = {};
|
|
486
|
+
for (const key of [
|
|
487
|
+
"nativeProviderTools",
|
|
488
|
+
"openaiTransport",
|
|
489
|
+
"openaiWebSocketPrewarm",
|
|
490
|
+
"compactionMode",
|
|
491
|
+
"requestProfile",
|
|
492
|
+
"apiKeyMode",
|
|
493
|
+
"imageGeneration",
|
|
494
|
+
"webSearchEnabled",
|
|
495
|
+
"viewImage",
|
|
496
|
+
"applyPatchEnabled",
|
|
497
|
+
"additionalModelIds",
|
|
498
|
+
] as const) {
|
|
499
|
+
if (valuesDiffer(settings[key], DEFAULT_SETTINGS[key])) record[key] = settings[key];
|
|
500
|
+
}
|
|
501
|
+
return record;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function nativeProvider(model: ModelIdentityLike | undefined): boolean {
|
|
505
|
+
return model?.provider === "openai" || model?.provider === "openai-codex";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function legacyProfilePatch(
|
|
509
|
+
model: ModelIdentityLike,
|
|
510
|
+
settings: CodexMinimalToolsSettings,
|
|
511
|
+
existing: ModelProfilePatch | undefined,
|
|
512
|
+
): ModelProfilePatch | undefined {
|
|
513
|
+
const id = modelId(model);
|
|
514
|
+
if (!id) return undefined;
|
|
515
|
+
const raw = legacySettingsRecord(settings);
|
|
516
|
+
const additional = Array.isArray(raw.additionalModelIds)
|
|
517
|
+
&& raw.additionalModelIds.some((candidate) => typeof candidate === "string" && normalizeId(candidate) === normalizeId(id));
|
|
518
|
+
if (!existing && !additional) return undefined;
|
|
519
|
+
const legacyKeys = [
|
|
520
|
+
"nativeProviderTools",
|
|
521
|
+
"openaiTransport",
|
|
522
|
+
"openaiWebSocketPrewarm",
|
|
523
|
+
"compactionMode",
|
|
524
|
+
"requestProfile",
|
|
525
|
+
"apiKeyMode",
|
|
526
|
+
"imageGeneration",
|
|
527
|
+
"webSearchEnabled",
|
|
528
|
+
"viewImage",
|
|
529
|
+
"applyPatchEnabled",
|
|
530
|
+
"additionalModelIds",
|
|
531
|
+
];
|
|
532
|
+
const legacyMode = additional || legacyKeys.some((key) => Object.hasOwn(raw, key));
|
|
533
|
+
if (!legacyMode) return undefined;
|
|
534
|
+
|
|
535
|
+
const fullId = normalizeId(id);
|
|
536
|
+
const oldExtendedToolModel = /^openai\/gpt-5(?:$|[.-])/.test(fullId) || additional;
|
|
537
|
+
const requestProfile = resolveCodexRequestProfile(settings.requestProfile);
|
|
538
|
+
const rawRequestProfile = isRecord(raw.requestProfile) ? raw.requestProfile : undefined;
|
|
539
|
+
const explicitPatchTransport = rawRequestProfile?.patchTransport === "function"
|
|
540
|
+
|| rawRequestProfile?.patchTransport === "custom";
|
|
541
|
+
const patchSupportedByCatalog = existing?.tools?.applyPatch !== false
|
|
542
|
+
&& existing?.tools?.applyPatch !== undefined;
|
|
543
|
+
const hostedTools = settings.nativeProviderTools
|
|
544
|
+
&& requestProfile.supportsHostedTools
|
|
545
|
+
&& nativeProvider(model);
|
|
546
|
+
const existingWebSearch = existing?.tools?.webSearch;
|
|
547
|
+
const contentTypes: WebSearchContentType[] = existingWebSearch
|
|
548
|
+
? existingWebSearch.contentTypes ?? ["text"]
|
|
549
|
+
: ["text"];
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
enabled: true,
|
|
554
|
+
responses: {
|
|
555
|
+
providerShim: nativeProvider(model) || existing?.responses?.providerShim === true,
|
|
556
|
+
endpoint: model.provider === "openai" || settings.apiKeyMode ? "openai" : "codex",
|
|
557
|
+
mode: requestProfile.responsesMode,
|
|
558
|
+
reasoningSummary: requestProfile.reasoningSummary,
|
|
559
|
+
systemPromptPlacement: requestProfile.systemPromptPlacement,
|
|
560
|
+
transport: settings.openaiTransport,
|
|
561
|
+
websocketPrewarm: settings.openaiWebSocketPrewarm,
|
|
562
|
+
},
|
|
563
|
+
tools: {
|
|
564
|
+
parallelCalls: requestProfile.supportsParallelTools,
|
|
565
|
+
applyPatch: settings.applyPatchEnabled
|
|
566
|
+
&& (oldExtendedToolModel || (explicitPatchTransport && patchSupportedByCatalog))
|
|
567
|
+
? requestProfile.patchTransport
|
|
568
|
+
: false,
|
|
569
|
+
webSearch: settings.webSearchEnabled && hostedTools && oldExtendedToolModel
|
|
570
|
+
? { implementation: "hosted", contentTypes: [...contentTypes] }
|
|
571
|
+
: false,
|
|
572
|
+
imageGeneration: settings.imageGeneration && hostedTools
|
|
573
|
+
? "hosted"
|
|
574
|
+
: false,
|
|
575
|
+
viewImage: settings.viewImage,
|
|
576
|
+
},
|
|
577
|
+
compaction: settings.compactionMode,
|
|
578
|
+
fast: existing?.fast ?? false,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function resolveModelProfile(
|
|
583
|
+
model: ModelIdentityLike | undefined,
|
|
584
|
+
options: { settings?: CodexMinimalToolsSettings } = {},
|
|
585
|
+
): ResolvedModelProfile | undefined {
|
|
586
|
+
const id = modelId(model);
|
|
587
|
+
if (!id || !model) return undefined;
|
|
588
|
+
const catalog = buildCatalog();
|
|
589
|
+
const key = normalizeId(id);
|
|
590
|
+
const catalogPatch = catalog.resolved.get(key);
|
|
591
|
+
const settings = options.settings ?? loadSettings();
|
|
592
|
+
const legacy = legacyProfilePatch(model, settings, catalogPatch);
|
|
593
|
+
if (!catalogPatch && !legacy) return undefined;
|
|
594
|
+
const patch = legacy
|
|
595
|
+
? deepMerge(catalogPatch ?? { id }, legacy)
|
|
596
|
+
: catalogPatch as ModelProfilePatch;
|
|
597
|
+
const diagnostics = [...catalog.diagnostics];
|
|
598
|
+
const effective = normalizeProfile(patch, diagnostics);
|
|
599
|
+
const sources = [...(catalog.entries.get(key)?.sources ?? []), ...(legacy ? ["legacy" as const] : [])];
|
|
600
|
+
return {
|
|
601
|
+
id,
|
|
602
|
+
sources: [...new Set(sources)],
|
|
603
|
+
profileHash: profileHash(effective),
|
|
604
|
+
effective,
|
|
605
|
+
diagnostics,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function listResolvedModelProfiles(
|
|
610
|
+
options: { settings?: CodexMinimalToolsSettings } = {},
|
|
611
|
+
): ResolvedModelProfile[] {
|
|
612
|
+
const catalog = buildCatalog();
|
|
613
|
+
const settings = options.settings ?? loadSettings();
|
|
614
|
+
const profiles: ResolvedModelProfile[] = [];
|
|
615
|
+
for (const entry of catalog.resolved.values()) {
|
|
616
|
+
const slash = entry.id.indexOf("/");
|
|
617
|
+
const provider = entry.id.slice(0, slash);
|
|
618
|
+
const id = entry.id.slice(slash + 1);
|
|
619
|
+
const resolved = resolveModelProfile({ provider, id }, { settings });
|
|
620
|
+
if (resolved) profiles.push(resolved);
|
|
621
|
+
}
|
|
622
|
+
for (const additional of settings.additionalModelIds) {
|
|
623
|
+
if (profiles.some((profile) => normalizeId(profile.id) === normalizeId(additional))) continue;
|
|
624
|
+
const slash = additional.indexOf("/");
|
|
625
|
+
const resolved = resolveModelProfile({
|
|
626
|
+
provider: additional.slice(0, slash),
|
|
627
|
+
id: additional.slice(slash + 1),
|
|
628
|
+
}, { settings });
|
|
629
|
+
if (resolved) profiles.push(resolved);
|
|
630
|
+
}
|
|
631
|
+
return profiles;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function modelCatalogDiagnostics(): string[] {
|
|
635
|
+
return buildCatalog().diagnostics;
|
|
636
|
+
}
|