From 1b2bf0df3f2bbb5c42d1f0f4fe11b5d87ac35ab7 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 20:23:34 +0100 Subject: [PATCH] feat: Extend timeout handling for Codex model feature generation - Introduced a dedicated 5-minute timeout for Codex models during feature generation to accommodate slower response times when generating 50+ features. - Updated the CodexProvider to utilize this extended timeout based on the reasoning effort level. - Enhanced the feature generation logic in generate-features-from-spec.ts to detect Codex models and apply the appropriate timeout. - Modified the model resolver to include reasoning effort in the resolved phase model structure. This change improves the reliability of feature generation for Codex models, ensuring they have sufficient time to process requests effectively. --- apps/server/src/providers/codex-provider.ts | 14 ++++++++++- .../app-spec/generate-features-from-spec.ts | 24 +++++++++++++++++-- libs/model-resolver/src/resolver.ts | 11 +++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) 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, }; }