Skip to content

Commit 11192bf

Browse files
committed
Add LLMProviderError for provider-level error detection
- Introduce LLMProviderError to distinguish provider API errors (rate limits, auth failures, safety blocks) from response format issues - Add _raise_provider_error helper that checks error dict, error string, and Gemini promptFeedback.blockReason before content extraction
1 parent 23bcef6 commit 11192bf

3 files changed

Lines changed: 42 additions & 2 deletions

File tree

teaagent/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
LLMConfigurationError,
4040
LLMHTTPError,
4141
LLMMessage,
42+
LLMProviderError,
4243
LLMRequest,
4344
LLMResponse,
4445
LLMResponseFormatError,
@@ -144,6 +145,7 @@
144145
'LLMConfigurationError',
145146
'LLMHTTPError',
146147
'LLMMessage',
148+
'LLMProviderError',
147149
'LLMRequest',
148150
'LLMResponse',
149151
'LLMResponseFormatError',

teaagent/llm.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class LLMHTTPError(LLMAdapterError):
2121
pass
2222

2323

24+
class LLMProviderError(LLMAdapterError):
25+
pass
26+
27+
2428
class LLMResponseFormatError(LLMAdapterError):
2529
pass
2630

@@ -330,6 +334,7 @@ def complete(self, request: LLMRequest) -> LLMResponse:
330334

331335

332336
def _extract_openai_content(provider: str, response: dict[str, Any]) -> str:
337+
_raise_provider_error(provider, response)
333338
choices = response.get('choices')
334339
if not isinstance(choices, list) or not choices:
335340
raise LLMResponseFormatError(f'{provider} response missing choices')
@@ -359,6 +364,7 @@ def _first_choice_delta(provider: str, response: dict[str, Any]) -> dict[str, An
359364

360365

361366
def _extract_claude_content(response: dict[str, Any]) -> str:
367+
_raise_provider_error('claude', response)
362368
content_blocks = response.get('content')
363369
if not isinstance(content_blocks, list):
364370
raise LLMResponseFormatError('claude response missing content blocks')
@@ -374,6 +380,7 @@ def _extract_claude_content(response: dict[str, Any]) -> str:
374380

375381

376382
def _extract_gemini_content(response: dict[str, Any]) -> str:
383+
_raise_provider_error('gemini', response)
377384
candidates = response.get('candidates')
378385
if not isinstance(candidates, list) or not candidates:
379386
raise LLMResponseFormatError('gemini response missing candidates')
@@ -396,6 +403,20 @@ def _extract_gemini_content(response: dict[str, Any]) -> str:
396403
return text
397404

398405

406+
def _raise_provider_error(provider: str, response: dict[str, Any]) -> None:
407+
error = response.get('error')
408+
if isinstance(error, dict):
409+
message = error.get('message') or error.get('status') or error
410+
raise LLMProviderError(f'{provider} provider error: {message}')
411+
if isinstance(error, str):
412+
raise LLMProviderError(f'{provider} provider error: {error}')
413+
prompt_feedback = response.get('promptFeedback')
414+
if isinstance(prompt_feedback, dict) and prompt_feedback.get('blockReason'):
415+
raise LLMProviderError(
416+
f'{provider} provider blocked prompt: {prompt_feedback["blockReason"]}'
417+
)
418+
419+
399420
PROVIDER_CONFIGS = {
400421
'claude': ProviderConfig(
401422
name='claude',

tests/test_llm.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from teaagent import (
1111
LLMMessage,
12+
LLMProviderError,
1213
LLMRequest,
1314
LLMResponseFormatError,
1415
available_providers,
@@ -223,11 +224,19 @@ def test_openai_streaming_reads_sse_lines_incrementally(self) -> None:
223224
self.assertEqual(result.input_tokens, 2)
224225
self.assertEqual(result.output_tokens, 3)
225226

226-
def test_openai_adapter_rejects_malformed_response(self) -> None:
227+
def test_openai_adapter_reports_provider_error_payload(self) -> None:
227228
transport = FakeTransport({'error': {'message': 'blocked'}})
228229
with patch.dict(os.environ, {'OPENAI_API_KEY': 'key'}, clear=True):
229230
adapter = create_llm_adapter('gpt', transport=transport, model='gpt-test')
230231

232+
with self.assertRaises(LLMProviderError):
233+
adapter.complete(LLMRequest(messages=[LLMMessage('user', 'hi')]))
234+
235+
def test_openai_adapter_rejects_malformed_response(self) -> None:
236+
transport = FakeTransport({'choices': []})
237+
with patch.dict(os.environ, {'OPENAI_API_KEY': 'key'}, clear=True):
238+
adapter = create_llm_adapter('gpt', transport=transport, model='gpt-test')
239+
231240
with self.assertRaises(LLMResponseFormatError):
232241
adapter.complete(LLMRequest(messages=[LLMMessage('user', 'hi')]))
233242

@@ -240,13 +249,21 @@ def test_claude_adapter_rejects_malformed_response(self) -> None:
240249
adapter.complete(LLMRequest(messages=[LLMMessage('user', 'hi')]))
241250

242251
def test_gemini_adapter_rejects_malformed_response(self) -> None:
243-
transport = FakeTransport({'promptFeedback': {'blockReason': 'SAFETY'}})
252+
transport = FakeTransport({'candidates': [{'content': {'parts': []}}]})
244253
with patch.dict(os.environ, {'GEMINI_API_KEY': 'key'}, clear=True):
245254
adapter = create_llm_adapter('gemini', transport=transport)
246255

247256
with self.assertRaises(LLMResponseFormatError):
248257
adapter.complete(LLMRequest(messages=[LLMMessage('user', 'hi')]))
249258

259+
def test_gemini_adapter_reports_safety_block(self) -> None:
260+
transport = FakeTransport({'promptFeedback': {'blockReason': 'SAFETY'}})
261+
with patch.dict(os.environ, {'GEMINI_API_KEY': 'key'}, clear=True):
262+
adapter = create_llm_adapter('gemini', transport=transport)
263+
264+
with self.assertRaises(LLMProviderError):
265+
adapter.complete(LLMRequest(messages=[LLMMessage('user', 'hi')]))
266+
250267

251268
if __name__ == '__main__':
252269
unittest.main()

0 commit comments

Comments
 (0)