mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
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:
8
.changeset/afraid-rocks-add.md
Normal file
8
.changeset/afraid-rocks-add.md
Normal 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')
|
||||
7
.changeset/new-grapes-accept.md
Normal file
7
.changeset/new-grapes-accept.md
Normal 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
|
||||
@@ -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 } }
|
||||
: {})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user