mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
The OpenAI-compat and Ollama engines exposed stream()/stream_full() as async def but iterated a synchronous httpx.Client.iter_lines() internally, blocking the single event loop on every inter-token read (serializing concurrent chats; one wedged upstream read froze the whole API). Convert both to a shared AsyncHTTPEngineMixin using httpx.AsyncClient + aiter_lines() with a per-event-loop pooled client and the configured timeout applied; map mid-stream transport errors (RemoteProtocolError/ReadError) to EngineConnectionError via a deliberately narrow set that keeps CancelledError/GeneratorExit propagating; handle non-2xx explicitly (incl. 3xx and a typed EngineContextLengthError for context-window overflow 400s); switch litellm streaming to acompletion; and offload the blocking non-streaming handlers and websocket generate() to asyncio.to_thread. No public API change. Strong MockTransport-based tests, including a pin that the async path never touches the sync client. Complements #618. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
"""Tests for agent error classification."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class TestErrorClassification:
|
|
def test_retryable_error(self):
|
|
from openjarvis.agents.errors import RetryableError
|
|
|
|
err = RetryableError("rate limit hit")
|
|
assert err.retryable is True
|
|
assert str(err) == "rate limit hit"
|
|
|
|
def test_fatal_error(self):
|
|
from openjarvis.agents.errors import FatalError
|
|
|
|
err = FatalError("invalid API key")
|
|
assert err.retryable is False
|
|
|
|
def test_escalate_error(self):
|
|
from openjarvis.agents.errors import EscalateError
|
|
|
|
err = EscalateError("agent uncertain about next step")
|
|
assert err.retryable is False
|
|
assert err.needs_human is True
|
|
|
|
def test_classify_rate_limit(self):
|
|
from openjarvis.agents.errors import classify_error
|
|
|
|
result = classify_error(Exception("rate limit exceeded"))
|
|
assert result.retryable is True
|
|
|
|
def test_classify_timeout(self):
|
|
from openjarvis.agents.errors import classify_error
|
|
|
|
result = classify_error(TimeoutError("connection timed out"))
|
|
assert result.retryable is True
|
|
|
|
def test_classify_permission(self):
|
|
from openjarvis.agents.errors import classify_error
|
|
|
|
result = classify_error(PermissionError("access denied"))
|
|
assert result.retryable is False
|
|
|
|
def test_classify_unknown_defaults_retryable(self):
|
|
from openjarvis.agents.errors import classify_error
|
|
|
|
result = classify_error(ValueError("something weird"))
|
|
assert result.retryable is True
|
|
|
|
def test_retry_delay_exponential(self):
|
|
from openjarvis.agents.errors import retry_delay
|
|
|
|
assert retry_delay(0) == 10
|
|
assert retry_delay(1) == 20
|
|
assert retry_delay(2) == 40
|
|
# Capped at 300 seconds
|
|
assert retry_delay(10) == 300
|
|
|
|
def test_classify_context_length_is_fatal(self):
|
|
# A context-window overflow is deterministic — retrying the identical
|
|
# over-length request can never succeed, so it must NOT be classified
|
|
# retryable (which would burn ~30s of backoff on guaranteed failures).
|
|
from openjarvis.agents.errors import classify_error
|
|
from openjarvis.engine._base import EngineContextLengthError
|
|
|
|
typed = classify_error(
|
|
EngineContextLengthError(
|
|
"The conversation is too long for the model's context window."
|
|
)
|
|
)
|
|
assert typed.retryable is False
|
|
|
|
# Same for untyped errors whose message reads like a context overflow
|
|
# (e.g. raw vendor errors from engines without the typed mapping).
|
|
untyped = classify_error(
|
|
Exception("This model's maximum context length is 4096 tokens.")
|
|
)
|
|
assert untyped.retryable is False
|
|
|
|
def test_suggest_action_context_length(self):
|
|
from openjarvis.agents.errors import FatalError, suggest_action
|
|
|
|
action = suggest_action(FatalError("prompt exceeds the model's context window"))
|
|
assert "context window" in action or "too long" in action.lower()
|