Handle slow AI backend timeouts
This commit is contained in:
@@ -38,7 +38,8 @@ Copiez `config.json.template` vers `config.json` puis adaptez :
|
||||
"model": "llama3.1",
|
||||
"api_key": "",
|
||||
"server_url": "http://localhost:11434",
|
||||
"hotkey": "ctrl+alt+a"
|
||||
"hotkey": "ctrl+alt+a",
|
||||
"request_timeout_seconds": 300
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,6 +64,10 @@ Copiez `config.json.template` vers `config.json` puis adaptez :
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout IA
|
||||
|
||||
`request_timeout_seconds` vaut `300` par défaut. Si Ollama charge un gros modèle ou répond lentement, augmentez cette valeur. Mettez `0` pour désactiver le timeout côté application.
|
||||
|
||||
## Compilation
|
||||
|
||||
### Windows
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"api_key": "",
|
||||
"server_url": "http://localhost:11434",
|
||||
"hotkey": "ctrl+alt+a",
|
||||
"request_timeout_seconds": 120,
|
||||
"request_timeout_seconds": 300,
|
||||
"type_delay_seconds": 0,
|
||||
"system_prompt": "Réponds directement et de manière ultra-concise. Aucune phrase d'introduction, aucune salutation, aucun formatage superflu. Uniquement la réponse brute."
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ def _ask_ollama(prompt: str, config: AppConfig) -> str:
|
||||
response = requests.post(url, json=payload, timeout=config.request_timeout_seconds)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.Timeout as exc:
|
||||
timeout_label = "désactivé" if config.request_timeout_seconds is None else f"{config.request_timeout_seconds:.0f} s"
|
||||
raise AIClientError(
|
||||
"Ollama n'a pas répondu avant le délai configuré "
|
||||
f"({timeout_label}). Le modèle est peut-être en chargement ou trop lent; "
|
||||
"augmentez request_timeout_seconds dans config.json, ou mettez 0 pour désactiver le timeout."
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise AIClientError(f"Erreur Ollama: {exc}") from exc
|
||||
except ValueError as exc:
|
||||
@@ -65,6 +72,13 @@ def _ask_gemini(prompt: str, config: AppConfig) -> str:
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.Timeout as exc:
|
||||
timeout_label = "désactivé" if config.request_timeout_seconds is None else f"{config.request_timeout_seconds:.0f} s"
|
||||
raise AIClientError(
|
||||
"Gemini n'a pas répondu avant le délai configuré "
|
||||
f"({timeout_label}). Augmentez request_timeout_seconds dans config.json, "
|
||||
"ou mettez 0 pour désactiver le timeout."
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise AIClientError(f"Erreur Gemini: {exc}") from exc
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -15,7 +15,7 @@ class AppConfig:
|
||||
api_key: str = ""
|
||||
server_url: str = "http://localhost:11434"
|
||||
hotkey: str = "ctrl+alt+a"
|
||||
request_timeout_seconds: float = 120.0
|
||||
request_timeout_seconds: float | None = 300.0
|
||||
copy_wait_seconds: float = 1.0
|
||||
type_delay_seconds: float = 0.0
|
||||
restore_clipboard: bool = True
|
||||
@@ -33,6 +33,13 @@ def _coerce_provider(value: Any) -> Provider:
|
||||
return provider # type: ignore[return-value]
|
||||
|
||||
|
||||
def _coerce_timeout(value: Any) -> float | None:
|
||||
timeout = float(value)
|
||||
if timeout <= 0:
|
||||
return None
|
||||
return timeout
|
||||
|
||||
|
||||
def load_config(path: str | Path = "config.json") -> AppConfig:
|
||||
cfg_path = Path(path)
|
||||
if not cfg_path.exists():
|
||||
@@ -46,7 +53,7 @@ def load_config(path: str | Path = "config.json") -> AppConfig:
|
||||
api_key=str(data.get("api_key", "")),
|
||||
server_url=str(data.get("server_url", "http://localhost:11434")).rstrip("/"),
|
||||
hotkey=str(data.get("hotkey", "ctrl+alt+a")).lower(),
|
||||
request_timeout_seconds=float(data.get("request_timeout_seconds", 120)),
|
||||
request_timeout_seconds=_coerce_timeout(data.get("request_timeout_seconds", 300)),
|
||||
copy_wait_seconds=float(data.get("copy_wait_seconds", 1)),
|
||||
type_delay_seconds=float(data.get("type_delay_seconds", 0)),
|
||||
restore_clipboard=bool(data.get("restore_clipboard", True)),
|
||||
|
||||
+17
-1
@@ -1,4 +1,7 @@
|
||||
from ai_typewriter.ai_client import ask_ai
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from ai_typewriter.ai_client import AIClientError, ask_ai
|
||||
from ai_typewriter.config import AppConfig
|
||||
|
||||
|
||||
@@ -27,6 +30,19 @@ def test_ollama_payload_contains_strict_system_prompt(monkeypatch):
|
||||
assert "Uniquement la réponse brute" in seen["json"]["messages"][0]["content"]
|
||||
|
||||
|
||||
def test_ollama_timeout_message_suggests_config_change(monkeypatch):
|
||||
def fake_post(url, json, timeout, **kwargs):
|
||||
raise requests.Timeout("too slow")
|
||||
monkeypatch.setattr("ai_typewriter.ai_client.requests.post", fake_post)
|
||||
|
||||
with pytest.raises(AIClientError) as exc:
|
||||
ask_ai("texte", AppConfig(provider="ollama", model="m", request_timeout_seconds=300))
|
||||
|
||||
assert "300 s" in str(exc.value)
|
||||
assert "request_timeout_seconds" in str(exc.value)
|
||||
assert "0 pour désactiver" in str(exc.value)
|
||||
|
||||
|
||||
def test_gemini_payload(monkeypatch):
|
||||
seen = {}
|
||||
def fake_post(url, params, json, timeout):
|
||||
|
||||
@@ -16,6 +16,15 @@ def test_load_default_config(tmp_path):
|
||||
assert "Réponds directement" in cfg.system_prompt
|
||||
|
||||
|
||||
def test_timeout_zero_disables_timeout(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "ollama", "request_timeout_seconds": 0}), encoding="utf-8")
|
||||
|
||||
cfg = load_config(path)
|
||||
|
||||
assert cfg.request_timeout_seconds is None
|
||||
|
||||
|
||||
def test_reject_invalid_provider(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "bad"}), encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user