mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 01:06:36 +00:00
fix(llm): honor GRAPHIFY_API_TIMEOUT in the bedrock backend
The two bedrock-runtime clients (primary extraction in _call_bedrock and the secondary dispatch path in _call_llm) were built with no botocore config, so Converse used botocore's 60s default read timeout and ignored GRAPHIFY_API_TIMEOUT / --api-timeout entirely. A long opus-class generation then died with "Read timeout on endpoint URL" no matter how high the timeout was set. Both client constructions now pass a botocore.config.Config wiring read_timeout to _resolve_api_timeout() (default 600s), a 10s connect_timeout, and retries from _resolve_max_retries() in adaptive mode. This mirrors the fixes that closed the same gap for the claude-cli subprocess (#1112/#1111) and the secondary LLM dispatch path (#1442) -- bedrock was the last cloud backend still ignoring the knob. Also updates the README env-var row, which listed the timeout as applying to HTTP/claude-cli/Anthropic only, and the _fake_boto3 test fixture to register botocore.config and capture the client config so the new coverage can assert the timeout is wired.
This commit is contained in:
@@ -513,7 +513,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` | Per-call timeout in seconds for HTTP, claude-cli, and Anthropic SDK backends (default: 600) | optional — also `--api-timeout` flag |
|
||||
| `GRAPHIFY_API_TIMEOUT` | Per-call timeout in seconds for HTTP, claude-cli, Anthropic SDK, and Bedrock backends (default: 600) | optional — also `--api-timeout` flag |
|
||||
| `GRAPHIFY_MAX_RETRIES` | How many times to retry a rate-limited (429) request before giving up (default: 6; honors `Retry-After`) | optional — raise for strict per-org limits (e.g. kimi); `0` disables |
|
||||
| `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` |
|
||||
|
||||
+23
-2
@@ -1633,6 +1633,7 @@ def _call_bedrock(model: str, user_message: str, max_tokens: int = 8192, *, deep
|
||||
"""Call AWS Bedrock via boto3 Converse API using the standard AWS credential chain."""
|
||||
try:
|
||||
import boto3
|
||||
import botocore.config
|
||||
import botocore.exceptions
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
@@ -1642,7 +1643,19 @@ def _call_bedrock(model: str, user_message: str, max_tokens: int = 8192, *, deep
|
||||
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
|
||||
profile = os.environ.get("AWS_PROFILE")
|
||||
session = boto3.Session(profile_name=profile, region_name=region)
|
||||
client = session.client("bedrock-runtime")
|
||||
# Wire GRAPHIFY_API_TIMEOUT into the botocore read timeout. Without an
|
||||
# explicit config, Converse uses botocore's 60s default and a long
|
||||
# generation dies with "Read timeout on endpoint URL" no matter what the
|
||||
# env var / --api-timeout is set to — the same gap #1112/#1442 closed for
|
||||
# the claude-cli and secondary-dispatch paths, on the last cloud backend.
|
||||
client = session.client(
|
||||
"bedrock-runtime",
|
||||
config=botocore.config.Config(
|
||||
read_timeout=_resolve_api_timeout(),
|
||||
connect_timeout=10,
|
||||
retries={"max_attempts": _resolve_max_retries(), "mode": "adaptive"},
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
resp = client.converse(
|
||||
@@ -2589,12 +2602,20 @@ def _call_llm(
|
||||
if backend == "bedrock":
|
||||
try:
|
||||
import boto3
|
||||
import botocore.config
|
||||
except ImportError as exc:
|
||||
raise ImportError(_backend_pkg_hint("boto3", "bedrock")) from exc
|
||||
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
|
||||
profile = os.environ.get("AWS_PROFILE")
|
||||
session = boto3.Session(profile_name=profile, region_name=region)
|
||||
client = session.client("bedrock-runtime")
|
||||
client = session.client(
|
||||
"bedrock-runtime",
|
||||
config=botocore.config.Config(
|
||||
read_timeout=_resolve_api_timeout(),
|
||||
connect_timeout=10,
|
||||
retries={"max_attempts": _resolve_max_retries(), "mode": "adaptive"},
|
||||
),
|
||||
)
|
||||
resp = client.converse(
|
||||
modelId=mdl,
|
||||
messages=[{"role": "user", "content": [{"text": prompt}]}],
|
||||
|
||||
@@ -279,15 +279,37 @@ def _fake_boto3(monkeypatch, captured):
|
||||
"usage": {"inputTokens": 1, "outputTokens": 2},
|
||||
"stopReason": "end_turn",
|
||||
}
|
||||
|
||||
def _make_client(svc, **kw):
|
||||
# Record the client-construction kwargs (notably config=) separately so
|
||||
# they don't collide with the converse() call kwargs captured above.
|
||||
captured["_client_service"] = svc
|
||||
captured["_client_config"] = kw.get("config")
|
||||
return _Client()
|
||||
|
||||
boto3 = types.ModuleType("boto3")
|
||||
boto3.Session = lambda **kw: SimpleNamespace(client=lambda svc: _Client())
|
||||
boto3.Session = lambda **kw: SimpleNamespace(client=_make_client)
|
||||
monkeypatch.setitem(sys.modules, "boto3", boto3)
|
||||
|
||||
botocore = types.ModuleType("botocore")
|
||||
exc = types.ModuleType("botocore.exceptions")
|
||||
exc.ClientError = type("ClientError", (Exception,), {})
|
||||
config_mod = types.ModuleType("botocore.config")
|
||||
|
||||
class _Config:
|
||||
def __init__(self, **kw):
|
||||
# Mirror botocore.config.Config: expose the kwargs as attributes so
|
||||
# tests can assert read_timeout/connect_timeout/retries were wired.
|
||||
self.read_timeout = kw.get("read_timeout")
|
||||
self.connect_timeout = kw.get("connect_timeout")
|
||||
self.retries = kw.get("retries")
|
||||
|
||||
config_mod.Config = _Config
|
||||
botocore.exceptions = exc
|
||||
botocore.config = config_mod
|
||||
monkeypatch.setitem(sys.modules, "botocore", botocore)
|
||||
monkeypatch.setitem(sys.modules, "botocore.exceptions", exc)
|
||||
monkeypatch.setitem(sys.modules, "botocore.config", config_mod)
|
||||
|
||||
|
||||
# ── backend payload shape (mocked) ────────────────────────────────────────────
|
||||
@@ -407,6 +429,30 @@ def test_call_bedrock_parses_reasoning_model_response(monkeypatch):
|
||||
assert len(result["nodes"]) == 1
|
||||
# Hard-indexing block 0 yielded "{}" -> zero nodes -> relabelled "length".
|
||||
assert result["finish_reason"] == "stop"
|
||||
def test_call_bedrock_honors_api_timeout(monkeypatch):
|
||||
# GRAPHIFY_API_TIMEOUT must reach the botocore client's read_timeout; else
|
||||
# Converse falls back to botocore's 60s default and a long generation dies
|
||||
# with "Read timeout on endpoint URL" regardless of the env var.
|
||||
monkeypatch.setenv("GRAPHIFY_API_TIMEOUT", "1800")
|
||||
monkeypatch.delenv("GRAPHIFY_MAX_RETRIES", raising=False)
|
||||
captured: dict = {}
|
||||
_fake_boto3(monkeypatch, captured)
|
||||
llm._call_bedrock("model", "CORPUS")
|
||||
cfg = captured["_client_config"]
|
||||
assert cfg is not None, "bedrock client built without a botocore config"
|
||||
assert cfg.read_timeout == 1800.0
|
||||
assert cfg.connect_timeout == 10
|
||||
assert cfg.retries == {"max_attempts": 6, "mode": "adaptive"}
|
||||
|
||||
|
||||
def test_call_bedrock_api_timeout_defaults_when_unset(monkeypatch):
|
||||
# With no override the client still gets an explicit 600s read timeout,
|
||||
# not botocore's silent 60s default.
|
||||
monkeypatch.delenv("GRAPHIFY_API_TIMEOUT", raising=False)
|
||||
captured: dict = {}
|
||||
_fake_boto3(monkeypatch, captured)
|
||||
llm._call_bedrock("model", "CORPUS")
|
||||
assert captured["_client_config"].read_timeout == 600.0
|
||||
|
||||
|
||||
# ── CLI backends (mocked subprocess) ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user