fix: Add .strict() to all Zod schemas for OpenAI structured outputs compatibility (#1523)

Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Fixes #1522
Fixes #1541
This commit is contained in:
Ralph Khreish
2025-12-22 10:19:19 +01:00
committed by GitHub
parent 74f9c2e27b
commit fc1a79f256
10 changed files with 199 additions and 75 deletions

View File

@@ -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')

View File

@@ -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

View File

@@ -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 } }
: {})

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();