Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ These are only needed for **headless / CI extraction** (`graphify extract`). Whe
| `AWS_*` / `~/.aws/credentials` | AWS Bedrock — standard credential chain | `--backend bedrock` (no API key, uses IAM) |
| `GRAPHIFY_MAX_WORKERS` | AST parallelism thread count | optional — also `--max-workers` flag |
| `GRAPHIFY_MAX_OUTPUT_TOKENS` | Raise output cap for dense corpora | optional — e.g. `32768` for large files |
| `GRAPHIFY_API_TIMEOUT` | HTTP timeout in seconds (default: 600) | optional — also `--api-timeout` flag |
| `GRAPHIFY_API_TIMEOUT` | Per-call timeout in seconds for every backend, HTTP and `claude-cli` alike (default: 600) | optional — also `--api-timeout` flag |
| `GRAPHIFY_FORCE` | Force graph rebuild even with fewer nodes | optional — also `--force` flag |
| `GRAPHIFY_GOOGLE_WORKSPACE` | Auto-enable Google Workspace export | optional — set to `1` |
| `GRAPHIFY_TRIAGE_BACKEND` | Backend for `graphify prs --triage` | optional — auto-detected from available keys |
Expand Down
40 changes: 25 additions & 15 deletions graphify/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ def _resolve_max_tokens(default: int) -> int:
pass
return default


def _resolve_api_timeout(default: float = 600.0) -> float:
"""Honour GRAPHIFY_API_TIMEOUT env var override (seconds), else use the default.

Applies uniformly across every backend — the OpenAI-compatible HTTP client
and the claude-cli subprocess paths alike — so a slow local model or a long
chunk can be given more headroom without patching the source. Non-positive
or unparseable values fall back to the default (issue #792 addendum).
"""
raw = os.environ.get("GRAPHIFY_API_TIMEOUT", "").strip()
if raw:
try:
v = float(raw)
if v > 0:
return v
except ValueError:
pass
return default

_EXTRACTION_SYSTEM = """\
You are a graphify semantic extraction agent. Extract a knowledge graph fragment from the files provided.
Output ONLY valid JSON — no explanation, no markdown fences, no preamble.
Expand Down Expand Up @@ -362,19 +381,10 @@ def _call_openai_compat(

# Local backends (ollama, llama.cpp, vLLM) routinely take >60s for a
# single chunk on a large model — far longer than the openai SDK's
# default. Honour GRAPHIFY_API_TIMEOUT (seconds) for explicit override;
# default to 600s, which is long enough for a 31B model on a 16k chunk
# but still bounds runaway connections (issue #792 addendum).
timeout_raw = os.environ.get("GRAPHIFY_API_TIMEOUT", "").strip()
timeout_s: float = 600.0
if timeout_raw:
try:
v = float(timeout_raw)
if v > 0:
timeout_s = v
except ValueError:
pass
client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout_s)
# default. GRAPHIFY_API_TIMEOUT (seconds) overrides; defaults to 600s,
# long enough for a 31B model on a 16k chunk but still bounding a runaway
# connection (issue #792 addendum).
client = OpenAI(api_key=api_key, base_url=base_url, timeout=_resolve_api_timeout())
kwargs: dict = {
"model": model,
"messages": [
Expand Down Expand Up @@ -571,7 +581,7 @@ def _call_claude_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bo
capture_output=True,
text=True,
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
timeout=600,
timeout=_resolve_api_timeout(),
check=False,
)
if proc.returncode != 0:
Expand Down Expand Up @@ -1134,7 +1144,7 @@ def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str:
capture_output=True,
text=True,
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
timeout=600,
timeout=_resolve_api_timeout(),
check=False,
)
if proc.returncode != 0:
Expand Down
35 changes: 35 additions & 0 deletions tests/test_claude_cli_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,38 @@ def test_non_windows_uses_bare_claude(monkeypatch):

argv = run.call_args.args[0]
assert argv[0] == "claude"


# ---------- timeout honouring: GRAPHIFY_API_TIMEOUT reaches claude-cli ----------


def test_resolve_api_timeout_default(monkeypatch):
monkeypatch.delenv("GRAPHIFY_API_TIMEOUT", raising=False)
assert llm._resolve_api_timeout() == 600.0


def test_resolve_api_timeout_env_override(monkeypatch):
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "45")
assert llm._resolve_api_timeout() == 45.0


def test_resolve_api_timeout_ignores_invalid(monkeypatch):
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "not-a-number")
assert llm._resolve_api_timeout() == 600.0


def test_resolve_api_timeout_ignores_nonpositive(monkeypatch):
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "0")
assert llm._resolve_api_timeout() == 600.0


def test_claude_cli_subprocess_uses_default_timeout(fake_claude, monkeypatch):
monkeypatch.delenv("GRAPHIFY_API_TIMEOUT", raising=False)
llm._call_claude_cli("dummy", max_tokens=8192)
assert fake_claude.call_args.kwargs["timeout"] == 600.0


def test_claude_cli_subprocess_honours_api_timeout_env(fake_claude, monkeypatch):
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "30")
llm._call_claude_cli("dummy", max_tokens=8192)
assert fake_claude.call_args.kwargs["timeout"] == 30.0