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 @@ -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` |
Expand Down
25 changes: 23 additions & 2 deletions graphify/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,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(
Expand All @@ -1618,7 +1619,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(
Expand Down Expand Up @@ -2565,12 +2578,20 @@ def _rec(inp, out) -> None:
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}]}],
Expand Down
50 changes: 49 additions & 1 deletion tests/test_image_vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,37 @@ def converse(self, **kw):
"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) ────────────────────────────────────────────
Expand Down Expand Up @@ -330,6 +352,32 @@ def test_call_bedrock_sends_raw_image_bytes(tmp_path, monkeypatch):
assert img_block["image"]["source"]["bytes"] == _PNG_BYTES


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) ──────────────────────────────────────────


Expand Down