Skip to content

Commit 4f8fac6

Browse files
committed
Merge branch 'gh-pr-33'
2 parents 8b24d7c + 26e859c commit 4f8fac6

3 files changed

Lines changed: 98 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ Configuration values can be set from config file, env var or command line flag,
550550
| Safe yes | `safe_yes: true` | `RUNPROMPT_SAFE_YES=1` | `--safe-yes` |
551551
| Verbose | `verbose: true` | `RUNPROMPT_VERBOSE=1` | `--verbose` |
552552
| Chat | `chat: true` | `RUNPROMPT_CHAT=1` | `--chat` |
553+
| LLM timeout | `timeout: 120` | `RUNPROMPT_TIMEOUT=120` | `--timeout 120` |
553554

554555
### API keys
555556

@@ -572,6 +573,7 @@ model: openai/gpt-4o
572573
default_model: anthropic/claude-sonnet-4-20250514 # fallback if model not set anywhere
573574
cache: true
574575
safe_yes: true
576+
timeout: 120
575577
tool_path:
576578
- ./tools
577579
- /shared/tools

runprompt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ CONFIG_KEYS = {
4848
"model", "default_model", "tool_path", "base_url", "ollama_base_url", "ollama_api_base",
4949
"openai_base_url", "openai_api_base", "cache", "cache_dir",
5050
"safe_yes", "verbose", "anthropic_api_key", "openai_api_key",
51-
"google_api_key", "openrouter_api_key", "chat", "chat_history", "history_file",
51+
"google_api_key", "openrouter_api_key", "chat", "chat_history", "history_file", "timeout",
5252
}
5353

5454
PROVIDERS = {
@@ -178,6 +178,26 @@ def init_config(args):
178178
CONFIG["args"] = args_dict
179179

180180

181+
def get_timeout():
182+
"""Get configured LLM request timeout in seconds."""
183+
timeout = get_conf("timeout", TIMEOUT)
184+
if isinstance(timeout, bool):
185+
print("%sInvalid timeout: %s%s" % (RED, timeout, RESET), file=sys.stderr)
186+
print("Timeout must be a positive number of seconds.", file=sys.stderr)
187+
sys.exit(1)
188+
try:
189+
timeout = float(timeout)
190+
except (TypeError, ValueError):
191+
print("%sInvalid timeout: %s%s" % (RED, timeout, RESET), file=sys.stderr)
192+
print("Timeout must be a positive number of seconds.", file=sys.stderr)
193+
sys.exit(1)
194+
if timeout <= 0:
195+
print("%sInvalid timeout: %s%s" % (RED, timeout, RESET), file=sys.stderr)
196+
print("Timeout must be a positive number of seconds.", file=sys.stderr)
197+
sys.exit(1)
198+
return timeout
199+
200+
181201
# === MAIN ENTRY POINT ===
182202

183203

@@ -525,6 +545,7 @@ Config file example:
525545
- /shared/tools
526546
cache: true
527547
safe_yes: true
548+
timeout: 120
528549
openai_api_key: sk-...
529550
530551
Environment:
@@ -600,6 +621,8 @@ def parse_args(args):
600621
metavar="PATH", help="Add directory to tool import path")
601622
parser.add_argument("--chat", action="store_true",
602623
help="Interactive chat mode: prompt for input after each response")
624+
parser.add_argument("--timeout", type=float, metavar="SECONDS",
625+
help="LLM request timeout in seconds")
603626
parser.add_argument("--read", "--file", action="append", default=[],
604627
metavar="FILE",
605628
help="Read file(s) into context (supports globs)")
@@ -1150,7 +1173,7 @@ def make_request(url, api_key, model, messages, output_config, provider, tools=N
11501173
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
11511174
try:
11521175
start_time = time.time()
1153-
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
1176+
with urllib.request.urlopen(req, timeout=get_timeout()) as resp:
11541177
response_body = resp.read().decode("utf-8")
11551178
elapsed = time.time() - start_time
11561179
log("Response: %s" % response_body)

tests/test-config.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#!/usr/bin/env python3
22
"""Test configuration cascade functionality."""
3+
import importlib.machinery
4+
import importlib.util
35
import subprocess
46
import os
57
import json
68
import shutil
79
import tempfile
810
import threading
911
import time
12+
import urllib.request
1013
from http.server import HTTPServer, BaseHTTPRequestHandler
1114

1215
MOCK_PORT = 18820
@@ -41,6 +44,30 @@ def log_message(self, format, *args):
4144
pass
4245

4346

47+
class DummyResponse:
48+
def __enter__(self):
49+
return self
50+
51+
def __exit__(self, exc_type, exc, tb):
52+
pass
53+
54+
def read(self):
55+
response = {
56+
"choices": [{
57+
"message": {"content": "OK"}
58+
}]
59+
}
60+
return json.dumps(response).encode("utf-8")
61+
62+
63+
def load_runprompt():
64+
loader = importlib.machinery.SourceFileLoader("runprompt_module", RUNPROMPT)
65+
spec = importlib.util.spec_from_loader(loader.name, loader)
66+
module = importlib.util.module_from_spec(spec)
67+
loader.exec_module(module)
68+
return module
69+
70+
4471
def run_server(server):
4572
server.serve_forever()
4673

@@ -76,6 +103,48 @@ def clean_env():
76103
return env
77104

78105

106+
def test_timeout_cascade_for_provider_request():
107+
"""Test that timeout config reaches the provider request."""
108+
module = load_runprompt()
109+
captured = []
110+
original_urlopen = urllib.request.urlopen
111+
112+
def fake_urlopen(req, **kwargs):
113+
captured.append(kwargs.get("timeout"))
114+
return DummyResponse()
115+
116+
try:
117+
urllib.request.urlopen = fake_urlopen
118+
cases = [
119+
({}, {}, {}, 120.0, "default"),
120+
({"timeout": 30}, {}, {}, 30.0, "config file"),
121+
({"timeout": 30}, {"timeout": 45}, {}, 45.0, "env"),
122+
({"timeout": 30}, {"timeout": 45}, {"timeout": 60}, 60.0,
123+
"CLI args"),
124+
]
125+
for files, env, args, expected, source in cases:
126+
module.CONFIG["files"] = files
127+
module.CONFIG["env"] = env
128+
module.CONFIG["args"] = args
129+
module.make_request("https://example.test/chat/completions",
130+
"test-key", "gpt-4o",
131+
[{"role": "user", "content": "hi"}],
132+
{}, "openai")
133+
assert captured[-1] == expected, \
134+
"Expected timeout from %s, got: %s" % (source, captured[-1])
135+
finally:
136+
urllib.request.urlopen = original_urlopen
137+
138+
139+
def test_timeout_cli_flag():
140+
"""Test that --timeout is parsed as a config value."""
141+
module = load_runprompt()
142+
parsed = module.parse_args(["--timeout", "12.5", "test.prompt"])
143+
module.init_config(parsed)
144+
assert module.get_conf("timeout") == 12.5, \
145+
"Expected parsed timeout, got: %s" % module.get_conf("timeout")
146+
147+
79148
def test_config_file_model():
80149
"""Test that model can be set from config file."""
81150
server = start_server(MOCK_PORT)
@@ -320,6 +389,8 @@ def test_tool_path_from_config():
320389

321390

322391
if __name__ == '__main__':
392+
test("timeout cascade for provider request", test_timeout_cascade_for_provider_request)
393+
test("timeout CLI flag", test_timeout_cli_flag)
323394
test("config file model", test_config_file_model)
324395
test("env overrides config file", test_env_overrides_config_file)
325396
test("CLI overrides env", test_cli_overrides_env)

0 commit comments

Comments
 (0)