From fc1a79f2565b0d8c24f009aec2c473a335262ae2 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:19:19 +0100 Subject: [PATCH] fix: Add .strict() to all Zod schemas for OpenAI structured outputs compatibility (#1523) Co-authored-by: Ralph Khreish Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Fixes #1522 Fixes #1541 --- .changeset/afraid-rocks-add.md | 8 ++++ .changeset/new-grapes-accept.md | 7 +++ src/ai-providers/codex-cli.js | 77 +++++++++++++++++++++++++++++++ src/schemas/add-task.js | 40 ++++++++-------- src/schemas/analyze-complexity.js | 26 ++++++----- src/schemas/base-schemas.js | 60 +++++++++++++++--------- src/schemas/expand-task.js | 8 ++-- src/schemas/parse-prd.js | 30 ++++++------ src/schemas/update-subtask.js | 8 ++-- src/schemas/update-tasks.js | 10 ++-- 10 files changed, 199 insertions(+), 75 deletions(-) create mode 100644 .changeset/afraid-rocks-add.md create mode 100644 .changeset/new-grapes-accept.md diff --git a/.changeset/afraid-rocks-add.md b/.changeset/afraid-rocks-add.md new file mode 100644 index 00000000..3d1d13c4 --- /dev/null +++ b/.changeset/afraid-rocks-add.md @@ -0,0 +1,8 @@ +--- +"task-master-ai": patch +--- + +Codex cli Validate reasoning effort against model capabilities + +- Add provider-level reasoning effort validation for OpenAI models +- Automatically cap unsupported effort levels (e.g., 'xhigh' on gpt-5.1 and gpt-5 becomes 'high') diff --git a/.changeset/new-grapes-accept.md b/.changeset/new-grapes-accept.md new file mode 100644 index 00000000..28bf604f --- /dev/null +++ b/.changeset/new-grapes-accept.md @@ -0,0 +1,7 @@ +--- +"task-master-ai": patch +--- + +Improve json schemas for ai-related commands making it more compatible with openai models + +- Fixes #1541 #1542 diff --git a/src/ai-providers/codex-cli.js b/src/ai-providers/codex-cli.js index dbcf9365..fdaa607b 100644 --- a/src/ai-providers/codex-cli.js +++ b/src/ai-providers/codex-cli.js @@ -15,6 +15,32 @@ import { import { log } from '../../scripts/modules/utils.js'; import { BaseAIProvider } from './base-provider.js'; +/** + * OpenAI model reasoning effort support. + * Different models support different reasoning effort levels. + * This is provider-specific logic that belongs here, not in the general model catalog. + * + * See: https://platform.openai.com/docs/guides/reasoning + */ +const REASONING_EFFORT_SUPPORT = { + // GPT-5.1 base does not support xhigh + '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.2 supports full range + 'gpt-5.2': ['none', 'low', 'medium', 'high', 'xhigh'], + // GPT-5.2 Pro only supports medium and above + 'gpt-5.2-pro': ['medium', 'high', 'xhigh'], + // GPT-5 supports full range + 'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'] +}; + +// Default for models not explicitly listed +const DEFAULT_REASONING_EFFORTS = ['none', 'low', 'medium', 'high']; + +// Ordering for effort levels (lowest to highest) +const EFFORT_ORDER = ['none', 'low', 'medium', 'high', 'xhigh']; + export class CodexCliProvider extends BaseAIProvider { constructor() { super(); @@ -80,10 +106,54 @@ export class CodexCliProvider extends BaseAIProvider { } } + /** + * Gets a validated reasoningEffort for the model. + * If no effort is specified, returns the model's highest supported effort. + * If an unsupported effort is specified, caps it to the highest supported. + * @param {string} modelId - The model ID to check + * @param {string} [requestedEffort] - The requested reasoning effort (optional) + * @returns {string} The validated reasoning effort + */ + _getValidatedReasoningEffort(modelId, requestedEffort) { + // Get supported efforts for this model, or use defaults + const supportedEfforts = + REASONING_EFFORT_SUPPORT[modelId] || DEFAULT_REASONING_EFFORTS; + + // Get the highest supported effort for this model + const highestSupported = supportedEfforts.reduce((highest, effort) => { + const currentIndex = EFFORT_ORDER.indexOf(effort); + const highestIndex = EFFORT_ORDER.indexOf(highest); + return currentIndex > highestIndex ? effort : highest; + }, supportedEfforts[0]); + + // If no effort requested, use the model's highest supported + if (!requestedEffort) { + log( + 'debug', + `No reasoning effort specified for ${modelId}. Using '${highestSupported}'.` + ); + return highestSupported; + } + + // If the requested effort is supported, use it + if (supportedEfforts.includes(requestedEffort)) { + return requestedEffort; + } + + // Cap to the highest supported effort + log( + 'warn', + `Reasoning effort '${requestedEffort}' not supported by ${modelId}. Using '${highestSupported}' instead.` + ); + + return highestSupported; + } + /** * Creates a Codex CLI client instance * @param {object} params * @param {string} [params.commandName] - Command name for settings lookup + * @param {string} [params.modelId] - Model ID for capability validation * @param {string} [params.apiKey] - Optional API key (injected as OPENAI_API_KEY for Codex CLI) * @returns {Function} */ @@ -92,9 +162,16 @@ export class CodexCliProvider extends BaseAIProvider { // Merge global + command-specific settings from config const settings = getCodexCliSettingsForCommand(params.commandName) || {}; + // Get validated reasoningEffort - always pass to override Codex CLI global config + const validatedReasoningEffort = this._getValidatedReasoningEffort( + params.modelId, + settings.reasoningEffort + ); + // Inject API key only if explicitly provided; OAuth is the primary path const defaultSettings = { ...settings, + reasoningEffort: validatedReasoningEffort, ...(params.apiKey ? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } } : {}) diff --git a/src/schemas/add-task.js b/src/schemas/add-task.js index ccca666e..2b398eb3 100644 --- a/src/schemas/add-task.js +++ b/src/schemas/add-task.js @@ -1,21 +1,25 @@ import { z } from 'zod'; // Schema that matches the inline AiTaskDataSchema from add-task.js -export const AddTaskResponseSchema = z.object({ - title: z.string().describe('Clear, concise title for the task'), - description: z - .string() - .describe('A one or two sentence description of the task'), - details: z - .string() - .describe('In-depth implementation details, considerations, and guidance'), - testStrategy: z - .string() - .describe('Detailed approach for verifying task completion'), - dependencies: z - .array(z.number()) - .nullable() - .describe( - 'Array of task IDs that this task depends on (must be completed before this task can start)' - ) -}); +export const AddTaskResponseSchema = z + .object({ + title: z.string().describe('Clear, concise title for the task'), + description: z + .string() + .describe('A one or two sentence description of the task'), + details: z + .string() + .describe( + 'In-depth implementation details, considerations, and guidance' + ), + testStrategy: z + .string() + .describe('Detailed approach for verifying task completion'), + dependencies: z + .array(z.number()) + .nullable() + .describe( + 'Array of task IDs that this task depends on (must be completed before this task can start)' + ) + }) + .strict(); diff --git a/src/schemas/analyze-complexity.js b/src/schemas/analyze-complexity.js index 621e488a..eecc5b30 100644 --- a/src/schemas/analyze-complexity.js +++ b/src/schemas/analyze-complexity.js @@ -1,14 +1,18 @@ import { z } from 'zod'; -export const ComplexityAnalysisItemSchema = z.object({ - taskId: z.number().int().positive(), - taskTitle: z.string(), - complexityScore: z.number().min(1).max(10), - recommendedSubtasks: z.number().int().nonnegative(), - expansionPrompt: z.string(), - reasoning: z.string() -}); +export const ComplexityAnalysisItemSchema = z + .object({ + taskId: z.number().int().positive(), + taskTitle: z.string(), + complexityScore: z.number().min(1).max(10), + recommendedSubtasks: z.number().int().nonnegative(), + expansionPrompt: z.string(), + reasoning: z.string() + }) + .strict(); -export const ComplexityAnalysisResponseSchema = z.object({ - complexityAnalysis: z.array(ComplexityAnalysisItemSchema) -}); +export const ComplexityAnalysisResponseSchema = z + .object({ + complexityAnalysis: z.array(ComplexityAnalysisItemSchema) + }) + .strict(); diff --git a/src/schemas/base-schemas.js b/src/schemas/base-schemas.js index ef8901c6..f6777da5 100644 --- a/src/schemas/base-schemas.js +++ b/src/schemas/base-schemas.js @@ -1,6 +1,16 @@ import { z } from 'zod'; -// Base schemas that will be reused across commands +/** + * Base schemas that will be reused across commands. + * + * IMPORTANT: All object schemas use .strict() to add "additionalProperties: false" + * to the generated JSON Schema. This is REQUIRED for OpenAI's Structured Outputs API, + * which mandates that every object type explicitly includes additionalProperties: false. + * Without .strict(), OpenAI API returns 400 Bad Request errors. + * + * Other providers (Anthropic, Google, etc.) safely ignore this constraint. + * See: https://platform.openai.com/docs/guides/structured-outputs + */ export const TaskStatusSchema = z.enum([ 'pending', 'in-progress', @@ -10,26 +20,30 @@ export const TaskStatusSchema = z.enum([ 'deferred' ]); -export const BaseTaskSchema = z.object({ - id: z.number().int().positive(), - title: z.string().min(1).max(200), - description: z.string().min(1), - status: TaskStatusSchema, - dependencies: z.array(z.union([z.number().int(), z.string()])).default([]), - priority: z - .enum(['low', 'medium', 'high', 'critical']) - .nullable() - .default(null), - details: z.string().nullable().default(null), - testStrategy: z.string().nullable().default(null) -}); +export const BaseTaskSchema = z + .object({ + id: z.number().int().positive(), + title: z.string().min(1).max(200), + description: z.string().min(1), + status: TaskStatusSchema, + dependencies: z.array(z.union([z.number().int(), z.string()])).default([]), + priority: z + .enum(['low', 'medium', 'high', 'critical']) + .nullable() + .default(null), + details: z.string().nullable().default(null), + testStrategy: z.string().nullable().default(null) + }) + .strict(); -export const SubtaskSchema = z.object({ - id: z.number().int().positive(), - title: z.string().min(5).max(200), - description: z.string().min(10), - dependencies: z.array(z.number().int()).default([]), - details: z.string().min(20), - status: z.enum(['pending', 'done', 'completed']).default('pending'), - testStrategy: z.string().nullable().default(null) -}); +export const SubtaskSchema = z + .object({ + id: z.number().int().positive(), + title: z.string().min(5).max(200), + description: z.string().min(10), + dependencies: z.array(z.number().int()).default([]), + details: z.string().min(20), + status: z.enum(['pending', 'done', 'completed']).default('pending'), + testStrategy: z.string().nullable().default(null) + }) + .strict(); diff --git a/src/schemas/expand-task.js b/src/schemas/expand-task.js index b7b8a3bf..0e5bda4a 100644 --- a/src/schemas/expand-task.js +++ b/src/schemas/expand-task.js @@ -1,6 +1,8 @@ import { z } from 'zod'; import { SubtaskSchema } from './base-schemas.js'; -export const ExpandTaskResponseSchema = z.object({ - subtasks: z.array(SubtaskSchema) -}); +export const ExpandTaskResponseSchema = z + .object({ + subtasks: z.array(SubtaskSchema) + }) + .strict(); diff --git a/src/schemas/parse-prd.js b/src/schemas/parse-prd.js index 1b6faf24..a4f9f3bc 100644 --- a/src/schemas/parse-prd.js +++ b/src/schemas/parse-prd.js @@ -1,18 +1,22 @@ import { z } from 'zod'; // Schema for a single task from PRD parsing -const PRDSingleTaskSchema = z.object({ - id: z.number().int().positive(), - title: z.string().min(1), - description: z.string().min(1), - details: z.string().nullable(), - testStrategy: z.string().nullable(), - priority: z.enum(['high', 'medium', 'low']).nullable(), - dependencies: z.array(z.number().int().positive()).nullable(), - status: z.string().nullable() -}); +const PRDSingleTaskSchema = z + .object({ + id: z.number().int().positive(), + title: z.string().min(1), + description: z.string().min(1), + details: z.string().nullable(), + testStrategy: z.string().nullable(), + priority: z.enum(['high', 'medium', 'low']).nullable(), + dependencies: z.array(z.number().int().positive()).nullable(), + status: z.string().nullable() + }) + .strict(); // Schema for the AI response - only expects tasks array since metadata is generated by the code -export const ParsePRDResponseSchema = z.object({ - tasks: z.array(PRDSingleTaskSchema) -}); +export const ParsePRDResponseSchema = z + .object({ + tasks: z.array(PRDSingleTaskSchema) + }) + .strict(); diff --git a/src/schemas/update-subtask.js b/src/schemas/update-subtask.js index e510a764..bf4a3613 100644 --- a/src/schemas/update-subtask.js +++ b/src/schemas/update-subtask.js @@ -1,6 +1,8 @@ import { z } from 'zod'; import { SubtaskSchema } from './base-schemas.js'; -export const UpdateSubtaskResponseSchema = z.object({ - subtask: SubtaskSchema -}); +export const UpdateSubtaskResponseSchema = z + .object({ + subtask: SubtaskSchema + }) + .strict(); diff --git a/src/schemas/update-tasks.js b/src/schemas/update-tasks.js index 85e9f461..61d93524 100644 --- a/src/schemas/update-tasks.js +++ b/src/schemas/update-tasks.js @@ -3,8 +3,10 @@ import { BaseTaskSchema, SubtaskSchema } from './base-schemas.js'; export const UpdatedTaskSchema = BaseTaskSchema.extend({ subtasks: z.array(SubtaskSchema).nullable().default(null) -}); +}).strict(); -export const UpdateTasksResponseSchema = z.object({ - tasks: z.array(UpdatedTaskSchema) -}); +export const UpdateTasksResponseSchema = z + .object({ + tasks: z.array(UpdatedTaskSchema) + }) + .strict();