diff --git a/apps/server/src/providers/codex-provider.ts b/apps/server/src/providers/codex-provider.ts index 18838cb8..5c200ea5 100644 --- a/apps/server/src/providers/codex-provider.ts +++ b/apps/server/src/providers/codex-provider.ts @@ -98,9 +98,14 @@ const TEXT_ENCODING = 'utf-8'; * This is the "no output" timeout - if the CLI doesn't produce any JSONL output * for this duration, the process is killed. For reasoning models with high * reasoning effort, this timeout is dynamically extended via calculateReasoningTimeout(). + * + * For feature generation (which can generate 50+ features), we use a much longer + * base timeout (5 minutes) since Codex models are slower at generating large JSON responses. + * * @see calculateReasoningTimeout from @automaker/types */ const CODEX_CLI_TIMEOUT_MS = DEFAULT_TIMEOUT_MS; +const CODEX_FEATURE_GENERATION_BASE_TIMEOUT_MS = 300000; // 5 minutes for feature generation const CONTEXT_WINDOW_256K = 256000; const MAX_OUTPUT_32K = 32000; const MAX_OUTPUT_16K = 16000; @@ -827,7 +832,14 @@ export class CodexProvider extends BaseProvider { // Higher reasoning effort (e.g., 'xhigh' for "xtra thinking" mode) requires more time // for the model to generate reasoning tokens before producing output. // This fixes GitHub issue #530 where features would get stuck with reasoning models. - const timeout = calculateReasoningTimeout(options.reasoningEffort, CODEX_CLI_TIMEOUT_MS); + // + // For feature generation with 'xhigh', use the extended 5-minute base timeout + // since generating 50+ features takes significantly longer than normal operations. + const baseTimeout = + options.reasoningEffort === 'xhigh' + ? CODEX_FEATURE_GENERATION_BASE_TIMEOUT_MS + : CODEX_CLI_TIMEOUT_MS; + const timeout = calculateReasoningTimeout(options.reasoningEffort, baseTimeout); const stream = spawnJSONLProcess({ command: commandPath, diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 95e550e0..6558256b 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -8,7 +8,7 @@ import * as secureFs from '../../lib/secure-fs.js'; import type { EventEmitter } from '../../lib/events.js'; import { createLogger } from '@automaker/utils'; -import { DEFAULT_PHASE_MODELS, supportsStructuredOutput } from '@automaker/types'; +import { DEFAULT_PHASE_MODELS, supportsStructuredOutput, isCodexModel } from '@automaker/types'; import { resolvePhaseModel } from '@automaker/model-resolver'; import { streamingQuery } from '../../providers/simple-query-service.js'; import { parseAndCreateFeatures } from './parse-and-create-features.js'; @@ -26,6 +26,12 @@ const logger = createLogger('SpecRegeneration'); const DEFAULT_MAX_FEATURES = 50; +/** + * Timeout for Codex models when generating features (5 minutes). + * Codex models are slower and need more time to generate 50+ features. + */ +const CODEX_FEATURE_GENERATION_TIMEOUT_MS = 300000; // 5 minutes + /** * Type for extracted features JSON response */ @@ -189,10 +195,23 @@ Generate ${featureCount} NEW features that build on each other logically. Rememb provider: undefined, credentials: undefined, }; - const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry); + const { model, thinkingLevel, reasoningEffort } = resolvePhaseModel(phaseModelEntry); logger.info('Using model:', model, provider ? `via provider: ${provider.name}` : 'direct API'); + // Codex models need extended timeout for generating many features. + // Use 'xhigh' reasoning effort to get 5-minute timeout (300s base * 1.0x = 300s). + // The Codex provider has a special 5-minute base timeout for feature generation. + const isCodex = isCodexModel(model); + const effectiveReasoningEffort = isCodex ? 'xhigh' : reasoningEffort; + + if (isCodex) { + logger.info('Codex model detected - using extended timeout (5 minutes for feature generation)'); + } + if (effectiveReasoningEffort) { + logger.info('Reasoning effort:', effectiveReasoningEffort); + } + // Determine if we should use structured output based on model type const useStructuredOutput = supportsStructuredOutput(model); logger.info( @@ -239,6 +258,7 @@ Your entire response should be valid JSON starting with { and ending with }. No allowedTools: ['Read', 'Glob', 'Grep'], abortController, thinkingLevel, + reasoningEffort: effectiveReasoningEffort, // Extended timeout for Codex models readOnly: true, // Feature generation only reads code, doesn't write settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined, claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration diff --git a/libs/model-resolver/src/resolver.ts b/libs/model-resolver/src/resolver.ts index d642ecde..ebb36c44 100644 --- a/libs/model-resolver/src/resolver.ts +++ b/libs/model-resolver/src/resolver.ts @@ -32,6 +32,7 @@ import { migrateModelId, type PhaseModelEntry, type ThinkingLevel, + type ReasoningEffort, } from '@automaker/types'; // Pattern definitions for Codex/OpenAI models @@ -162,8 +163,10 @@ export function getEffectiveModel( export interface ResolvedPhaseModel { /** Resolved model string (full model ID) */ model: string; - /** Optional thinking level for extended thinking */ + /** Optional thinking level for extended thinking (Claude models) */ thinkingLevel?: ThinkingLevel; + /** Optional reasoning effort for timeout calculation (Codex models) */ + reasoningEffort?: ReasoningEffort; /** Provider ID if using a ClaudeCompatibleProvider */ providerId?: string; } @@ -205,6 +208,7 @@ export function resolvePhaseModel( return { model: resolveModelString(undefined, defaultModel), thinkingLevel: undefined, + reasoningEffort: undefined, }; } @@ -214,12 +218,13 @@ export function resolvePhaseModel( return { model: resolveModelString(phaseModel, defaultModel), thinkingLevel: undefined, + reasoningEffort: undefined, }; } // Handle new PhaseModelEntry object format console.log( - `[ModelResolver] phaseModel is object format: model="${phaseModel.model}", thinkingLevel="${phaseModel.thinkingLevel}", providerId="${phaseModel.providerId}"` + `[ModelResolver] phaseModel is object format: model="${phaseModel.model}", thinkingLevel="${phaseModel.thinkingLevel}", reasoningEffort="${phaseModel.reasoningEffort}", providerId="${phaseModel.providerId}"` ); // If providerId is set, pass through the model string unchanged @@ -231,6 +236,7 @@ export function resolvePhaseModel( return { model: phaseModel.model, // Pass through unchanged thinkingLevel: phaseModel.thinkingLevel, + reasoningEffort: phaseModel.reasoningEffort, providerId: phaseModel.providerId, }; } @@ -239,5 +245,6 @@ export function resolvePhaseModel( return { model: resolveModelString(phaseModel.model, defaultModel), thinkingLevel: phaseModel.thinkingLevel, + reasoningEffort: phaseModel.reasoningEffort, }; }