diff --git a/gitnexus/src/core/wiki/generator.ts b/gitnexus/src/core/wiki/generator.ts index a396e9048..aaf7bb048 100644 --- a/gitnexus/src/core/wiki/generator.ts +++ b/gitnexus/src/core/wiki/generator.ts @@ -222,6 +222,7 @@ export class WikiGenerator { const localConfig = resolveLocalCLIConfig({ model: this.llmConfig.model, workingDirectory: this.repoPath, + requestTimeoutMs: this.llmConfig.requestTimeoutMs, }); return this.llmConfig.provider === 'claude' ? callClaudeLLM(prompt, localConfig, systemPrompt, options) diff --git a/gitnexus/src/core/wiki/llm-client.ts b/gitnexus/src/core/wiki/llm-client.ts index b9cb3eddd..6e88f8e74 100644 --- a/gitnexus/src/core/wiki/llm-client.ts +++ b/gitnexus/src/core/wiki/llm-client.ts @@ -79,7 +79,7 @@ export async function resolveLLMConfig(overrides?: Partial): Promise< 'https://openrouter.ai/api/v1', model: overrides?.model || - process.env.GITNEXUS_MODEL || + (localProvider ? undefined : process.env.GITNEXUS_MODEL) || savedLocalModel || (localProvider ? '' : savedConfig.model || 'minimax/minimax-m2.5'), maxTokens: overrides?.maxTokens ?? 16_384, diff --git a/gitnexus/src/core/wiki/local-cli-client.ts b/gitnexus/src/core/wiki/local-cli-client.ts index c7a55d0d2..0ff22f56d 100644 --- a/gitnexus/src/core/wiki/local-cli-client.ts +++ b/gitnexus/src/core/wiki/local-cli-client.ts @@ -19,6 +19,7 @@ export type LocalAgentProvider = 'claude' | 'codex'; export interface LocalCLIConfig { model?: string; workingDirectory?: string; + requestTimeoutMs?: number; } const COMMANDS: Record = { @@ -62,6 +63,7 @@ export function resolveLocalCLIConfig(overrides?: Partial): Loca return { model: overrides?.model, workingDirectory: overrides?.workingDirectory, + requestTimeoutMs: overrides?.requestTimeoutMs, }; } @@ -156,6 +158,7 @@ function runLocalCLI( const child = spawn(commandInfo.command, finalArgs, { cwd: config.workingDirectory || process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, env: { ...process.env, CI: '1', @@ -168,19 +171,38 @@ function runLocalCLI( let stderr = ''; let stdinError: Error | undefined; let settled = false; + let killTimer: ReturnType | undefined; const rejectOnce = (error: Error) => { if (settled) return; settled = true; + if (killTimer !== undefined) clearTimeout(killTimer); reject(error); }; const resolveOnce = (response: LLMResponse) => { if (settled) return; settled = true; + if (killTimer !== undefined) clearTimeout(killTimer); resolve(response); }; + if (config.requestTimeoutMs !== undefined && config.requestTimeoutMs > 0) { + killTimer = setTimeout(() => { + child.kill(); + const duration = + config.requestTimeoutMs! >= 60_000 + ? `${Math.round(config.requestTimeoutMs! / 60_000)}m` + : `${Math.round(config.requestTimeoutMs! / 1_000)}s`; + rejectOnce( + new Error( + `${provider} CLI timed out after ${duration}. ` + + 'Increase --timeout or omit it to disable the request timeout.', + ), + ); + }, config.requestTimeoutMs); + } + child.stdout.on('data', (chunk: Buffer) => { const chunkStr = chunk.toString(); stdout += chunkStr; @@ -212,7 +234,12 @@ function runLocalCLI( rejectOnce(new Error(`${provider} CLI stdin error: ${stdinError.message}`)); return; } - resolveOnce({ content: stdout.trim() }); + const output = stdout.trim(); + if (!output) { + rejectOnce(new Error(`${provider} CLI returned empty output`)); + return; + } + resolveOnce({ content: output }); }); child.on('error', (err) => {