honour GRAPHIFY_API_TIMEOUT in claude-cli and Anthropic SDK backends (#1112)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
830f0396d8
commit
75c4de7759
@@ -366,7 +366,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) |
|
| `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_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_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 HTTP, claude-cli, and Anthropic SDK backends (default: 600) | optional — also `--api-timeout` flag |
|
||||||
| `GRAPHIFY_FORCE` | Force graph rebuild even with fewer nodes | optional — also `--force` 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_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 |
|
| `GRAPHIFY_TRIAGE_BACKEND` | Backend for `graphify prs --triage` | optional — auto-detected from available keys |
|
||||||
|
|||||||
+17
-13
@@ -214,6 +214,19 @@ def _resolve_max_tokens(default: int) -> int:
|
|||||||
pass
|
pass
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_api_timeout(default: float = 600.0) -> float:
|
||||||
|
"""Honour GRAPHIFY_API_TIMEOUT env var override, else use default (seconds)."""
|
||||||
|
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 = """\
|
_EXTRACTION_SYSTEM = """\
|
||||||
You are a graphify semantic extraction agent. Extract a knowledge graph fragment from the files provided.
|
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.
|
Output ONLY valid JSON — no explanation, no markdown fences, no preamble.
|
||||||
@@ -434,16 +447,7 @@ def _call_openai_compat(
|
|||||||
# default. Honour GRAPHIFY_API_TIMEOUT (seconds) for explicit override;
|
# default. Honour GRAPHIFY_API_TIMEOUT (seconds) for explicit override;
|
||||||
# default to 600s, which is long enough for a 31B model on a 16k chunk
|
# default to 600s, which is long enough for a 31B model on a 16k chunk
|
||||||
# but still bounds runaway connections (issue #792 addendum).
|
# but still bounds runaway connections (issue #792 addendum).
|
||||||
timeout_raw = os.environ.get("GRAPHIFY_API_TIMEOUT", "").strip()
|
client = OpenAI(api_key=api_key, base_url=base_url, timeout=_resolve_api_timeout())
|
||||||
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)
|
|
||||||
kwargs: dict = {
|
kwargs: dict = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [
|
"messages": [
|
||||||
@@ -550,7 +554,7 @@ def _call_claude(api_key: str, model: str, user_message: str, max_tokens: int =
|
|||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError(_backend_pkg_hint("anthropic", "anthropic")) from exc
|
raise ImportError(_backend_pkg_hint("anthropic", "anthropic")) from exc
|
||||||
|
|
||||||
client = anthropic.Anthropic(api_key=api_key)
|
client = anthropic.Anthropic(api_key=api_key, timeout=_resolve_api_timeout())
|
||||||
resp = client.messages.create(
|
resp = client.messages.create(
|
||||||
model=model,
|
model=model,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
@@ -637,7 +641,7 @@ def _call_claude_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bo
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
|
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
|
||||||
timeout=600,
|
timeout=_resolve_api_timeout(),
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
@@ -1200,7 +1204,7 @@ def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
|
encoding="utf-8", # Force UTF-8 — prevents UnicodeEncodeError on Windows cp1252
|
||||||
timeout=600,
|
timeout=_resolve_api_timeout(),
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
|
|||||||
@@ -193,3 +193,38 @@ def test_non_windows_uses_bare_claude(monkeypatch):
|
|||||||
|
|
||||||
argv = run.call_args.args[0]
|
argv = run.call_args.args[0]
|
||||||
assert argv[0] == "claude"
|
assert argv[0] == "claude"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- GRAPHIFY_API_TIMEOUT honoured by all backends ----------
|
||||||
|
|
||||||
|
|
||||||
|
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_extraction_honours_timeout(monkeypatch, fake_claude):
|
||||||
|
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "30")
|
||||||
|
llm._call_claude_cli("dummy", max_tokens=8192)
|
||||||
|
assert fake_claude.call_args.kwargs["timeout"] == 30.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_call_llm_claude_cli_branch_honours_timeout(monkeypatch, fake_claude):
|
||||||
|
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "30")
|
||||||
|
llm._call_llm(prompt="x", backend="claude-cli", max_tokens=10)
|
||||||
|
assert fake_claude.call_args.kwargs["timeout"] == 30.0
|
||||||
|
|||||||
Reference in New Issue
Block a user