From d05602112e21d4b8aaea9b7e668673aa5cfcba6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 06:58:18 +0000 Subject: [PATCH] fix: remove default wiki llm timeout ceiling Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/af129945-1e0e-4d94-8676-877470c4574c --- README.md | 2 +- .../skills/gitnexus-cli/SKILL.md | 2 +- gitnexus/src/cli/index.ts | 2 +- gitnexus/src/core/wiki/llm-client.ts | 15 ++--- gitnexus/test/unit/wiki-llm-client.test.ts | 57 +++++++++++++++++++ 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5909554e9..5e470221f 100644 --- a/README.md +++ b/README.md @@ -725,7 +725,7 @@ gitnexus wiki --force # Increase the timeout or retries for large codebase or slow LLM providers -gitnexus wiki --timeout # Per-attempt LLM request timeout in seconds (default: 60) +gitnexus wiki --timeout # Per-attempt LLM request timeout in seconds (default: disabled) gitnexus wiki --retries # Max LLM retry attempts per request (default: 3) ``` diff --git a/gitnexus-claude-plugin/skills/gitnexus-cli/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-cli/SKILL.md index 11945b8cc..6e3ceb753 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-cli/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-cli/SKILL.md @@ -62,7 +62,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir | `--api-key ` | LLM API key | | `--concurrency ` | Parallel LLM calls (default: 3) | | `--gist` | Publish wiki as a public GitHub Gist | -| `--timeout ` | Per-attempt LLM request timeout in seconds (default: 60) | +| `--timeout ` | Per-attempt LLM request timeout in seconds (default: disabled) | | `--retries ` | Max LLM retry attempts per request (default: 3) | ### list — Show all indexed repos diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index 4b009e4aa..7ae8d2736 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -161,7 +161,7 @@ program ) .option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)') .option('--concurrency ', 'Parallel LLM calls (default: 3)', '3') - .option('--timeout ', 'Per-attempt LLM request timeout in seconds (default: 60)') + .option('--timeout ', 'Per-attempt LLM request timeout in seconds (default: disabled)') .option('--retries ', 'Max LLM retry attempts per request (default: 3)') .option('--gist', 'Publish wiki as a public GitHub Gist after generation') .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)') diff --git a/gitnexus/src/core/wiki/llm-client.ts b/gitnexus/src/core/wiki/llm-client.ts index 40ef831bf..fe73dad85 100644 --- a/gitnexus/src/core/wiki/llm-client.ts +++ b/gitnexus/src/core/wiki/llm-client.ts @@ -23,7 +23,7 @@ export interface LLMConfig { apiVersion?: string; /** When true, strips sampling params and uses max_completion_tokens instead of max_tokens */ isReasoningModel?: boolean; - /** Per-attempt fetch timeout in ms (default: 60_000). */ + /** Per-attempt fetch timeout in ms. Omit to disable request timeouts. */ requestTimeoutMs?: number; /** Max fetch attempts before giving up (default: 3). */ maxAttempts?: number; @@ -237,12 +237,13 @@ export async function callLLM( ...authHeaders, }, body: JSON.stringify(body), - // Per-attempt timeout. Without this each retry can hang - // indefinitely on a frozen TCP connection — the per-call - // signal is the only timeout `resilientFetch` honors; - // `capDelayMs` only bounds the *backoff* between attempts. - // Default 60s; raise via --timeout for slow models or large pages. - signal: AbortSignal.timeout(config.requestTimeoutMs ?? 60_000), + // Per-attempt timeout is opt-in for wiki generation. Large local + // model runs can legitimately take well over a minute, so the + // default runtime path must not impose a hidden 60s ceiling. + signal: + config.requestTimeoutMs !== undefined + ? AbortSignal.timeout(config.requestTimeoutMs) + : undefined, }, { breakerKey: `wiki-llm-${new URL(url).host}`, diff --git a/gitnexus/test/unit/wiki-llm-client.test.ts b/gitnexus/test/unit/wiki-llm-client.test.ts index 52b633566..8a20ee4d6 100644 --- a/gitnexus/test/unit/wiki-llm-client.test.ts +++ b/gitnexus/test/unit/wiki-llm-client.test.ts @@ -237,6 +237,63 @@ describe('callLLM — reasoning model params', () => { }); }); +describe('callLLM — timeout handling', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it('does not apply a default timeout when requestTimeoutMs is omitted', async () => { + const fetchSpy = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ choices: [{ message: { content: 'answer' } }], usage: {} }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchSpy); + const timeoutSpy = vi.spyOn(AbortSignal, 'timeout'); + + const { callLLM } = await import('../../src/core/wiki/llm-client.js'); + await callLLM('test', { + apiKey: 'sk-test', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-4o', + maxTokens: 500, + temperature: 0, + }); + + expect(timeoutSpy).not.toHaveBeenCalled(); + const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit]; + expect(init.signal).toBeUndefined(); + }); + + it('applies an explicit timeout when requestTimeoutMs is provided', async () => { + const fetchSpy = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ choices: [{ message: { content: 'answer' } }], usage: {} }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchSpy); + const timeoutSignal = new AbortController().signal; + const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockReturnValue(timeoutSignal); + + const { callLLM } = await import('../../src/core/wiki/llm-client.js'); + await callLLM('test', { + apiKey: 'sk-test', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-4o', + maxTokens: 500, + temperature: 0, + requestTimeoutMs: 120_000, + }); + + expect(timeoutSpy).toHaveBeenCalledWith(120_000); + const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit]; + expect(init.signal).toBe(timeoutSignal); + }); +}); + describe('callLLM — Azure content_filter error', () => { afterEach(() => vi.unstubAllGlobals());