Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-codex-cli-gpt55-structured-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---

Fix Codex CLI structured object generation by normalizing JSON schemas for strict OpenAI structured outputs, and add GPT-5.5 to the Codex CLI model catalog.
46 changes: 23 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
"provenance": true,
"access": "public"
},
"workspaces": ["apps/*", "packages/*", "."],
"workspaces": [
"apps/*",
"packages/*",
"."
],
"scripts": {
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
"dev": "tsdown --watch",
Expand Down Expand Up @@ -83,7 +87,7 @@
"@types/turndown": "^5.0.6",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^2.2.4",
"ai-sdk-provider-codex-cli": "^0.7.0",
"ai-sdk-provider-codex-cli": "^0.7.3",
"ai-sdk-provider-gemini-cli": "^1.4.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
Expand Down
13 changes: 13 additions & 0 deletions scripts/modules/supported-models.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@
}
],
"codex-cli": [
{
"id": "gpt-5.5",
"name": "GPT-5.5",
"swe_score": 0.82,
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 128000,
"reasoning_efforts": ["none", "low", "medium", "high", "xhigh"],
"supported": true
},
{
"id": "gpt-5.2-codex",
"name": "GPT-5.2 Codex",
Expand Down
42 changes: 35 additions & 7 deletions src/ai-providers/base-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ const isIntegerType = (type) => {
return type === 'integer';
};

const sanitizeIntegerConstraints = (schema) => {
const normalizeSchemaForStructuredOutputs = (schema) => {
if (!schema || typeof schema !== 'object') {
return schema;
}

if (Array.isArray(schema)) {
return schema.map(sanitizeIntegerConstraints);
return schema.map(normalizeSchemaForStructuredOutputs);
}

const next = { ...schema };
Expand All @@ -75,28 +75,54 @@ const sanitizeIntegerConstraints = (schema) => {

for (const key of SCHEMA_OBJECT_KEYS) {
if (next[key]) {
next[key] = sanitizeIntegerConstraints(next[key]);
next[key] = normalizeSchemaForStructuredOutputs(next[key]);
}
}

for (const key of SCHEMA_ARRAY_KEYS) {
if (Array.isArray(next[key])) {
next[key] = next[key].map(sanitizeIntegerConstraints);
next[key] = next[key].map(normalizeSchemaForStructuredOutputs);
}
}

for (const key of SCHEMA_RECORD_KEYS) {
if (next[key] && typeof next[key] === 'object') {
const mapped = {};
for (const [entryKey, entryValue] of Object.entries(next[key])) {
mapped[entryKey] = sanitizeIntegerConstraints(entryValue);
mapped[entryKey] = normalizeSchemaForStructuredOutputs(entryValue);
}
next[key] = mapped;
}
}

if (next.items) {
next.items = sanitizeIntegerConstraints(next.items);
next.items = Array.isArray(next.items)
? next.items.map(normalizeSchemaForStructuredOutputs)
: normalizeSchemaForStructuredOutputs(next.items);
}

const propertyKeys =
next.properties && typeof next.properties === 'object'
? Object.keys(next.properties)
: [];
const isObjectSchema =
next.type === 'object' ||
(Array.isArray(next.type) && next.type.includes('object')) ||
propertyKeys.length > 0;
const hasCombinator = ['oneOf', 'anyOf', 'allOf'].some((key) => key in next);

if (isObjectSchema) {
if (!hasCombinator && !('additionalProperties' in next)) {
next.additionalProperties = false;
}

if (
!hasCombinator &&
propertyKeys.length > 0 &&
next.required === undefined
) {
next.required = propertyKeys;
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return next;
Expand All @@ -112,7 +138,9 @@ const buildSafeSchema = (schema) => {
return baseSchema;
}

const sanitizedSchema = sanitizeIntegerConstraints(baseSchema.jsonSchema);
const sanitizedSchema = normalizeSchemaForStructuredOutputs(
baseSchema.jsonSchema
);

if (typeof jsonSchemaHelper === 'function') {
return jsonSchemaHelper(sanitizedSchema, { validate: baseSchema.validate });
Expand Down
40 changes: 40 additions & 0 deletions src/ai-providers/codex-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const REASONING_EFFORT_SUPPORT = {
'gpt-5.1': ['none', 'low', 'medium', 'high'],
// GPT-5.1 Codex Max supports full range
'gpt-5.1-codex-max': ['none', 'low', 'medium', 'high', 'xhigh'],
// GPT-5.5 supports full range
'gpt-5.5': ['none', 'low', 'medium', 'high', 'xhigh'],
// GPT-5.2 supports full range
'gpt-5.2': ['none', 'low', 'medium', 'high', 'xhigh'],
// GPT-5.2 Pro only supports medium and above
Expand All @@ -41,6 +43,26 @@ const DEFAULT_REASONING_EFFORTS = ['none', 'low', 'medium', 'high'];
// Ordering for effort levels (lowest to highest)
const EFFORT_ORDER = ['none', 'low', 'medium', 'high', 'xhigh'];

function getSystemCodexPath() {
try {
// 'command -v' is POSIX-compliant and available in /bin/sh; prefer it over 'which' which may be missing in minimal containers.
const command =
process.platform === 'win32' ? 'where codex' : 'command -v codex';
const result = execSync(command, {
stdio: 'pipe',
timeout: 1000,
encoding: 'utf8'
})
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);

return result || null;
} catch {
return null;
}
}

export class CodexCliProvider extends BaseAIProvider {
constructor() {
super();
Expand All @@ -63,6 +85,9 @@ export class CodexCliProvider extends BaseAIProvider {
// CLI availability check cache
this._codexCliChecked = false;
this._codexCliAvailable = null;
// System Codex path detection cache
this._systemCodexPathChecked = false;
this._systemCodexPath = null;
}

/**
Expand Down Expand Up @@ -149,6 +174,15 @@ export class CodexCliProvider extends BaseAIProvider {
return highestSupported;
}

_resolveSystemCodexPath() {
if (!this._systemCodexPathChecked) {
this._systemCodexPath = getSystemCodexPath();
this._systemCodexPathChecked = true;
}

return this._systemCodexPath;
}

/**
* Creates a Codex CLI client instance
* @param {object} params
Expand All @@ -168,9 +202,15 @@ export class CodexCliProvider extends BaseAIProvider {
settings.reasoningEffort
);

// Prefer an explicitly configured Codex path; otherwise prefer the system
// Codex CLI over the provider's optional bundled @openai/codex dependency.
// The bundled CLI can lag behind newly released ChatGPT/OAuth models.
const codexPath = settings.codexPath || this._resolveSystemCodexPath();

// Inject API key only if explicitly provided; OAuth is the primary path
const defaultSettings = {
...settings,
...(codexPath ? { codexPath } : {}),
reasoningEffort: validatedReasoningEffort,
...(params.apiKey
? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } }
Expand Down
Loading