Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b9b7a64

Browse files
committed
Improve exceptions handling
1 parent 41b0e82 commit b9b7a64

21 files changed

Lines changed: 1034 additions & 683 deletions

docs/features/core/exceptions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ title: Error Handling
66

77
For server-based models, Outlines provides a common exception hierarchy under `OutlinesError`.
88

9-
For local models, native runtime exceptions are preserved (see list of local and server-based models at [Model Types](../models/index.md#model-types)).
9+
For local models, native runtime exceptions are preserved (see the list of local and server-based models at [Model Types](../models/index.md#model-types)).
1010

11-
Exceptions normalization covers both sync and async modes, and both `generate` and `generate_stream()` (or equivalent) methods.
11+
Exception normalization covers both sync and async modes, and both `generate` and `generate_stream()` (or equivalent) methods.
1212

1313
## Exception Hierarchy
1414

@@ -46,7 +46,7 @@ OutlinesError
4646

4747
## Usage
4848

49-
Normalized `APIError` subclasses are raised by Outlines model wrappers; if you call a provider SDK directly, you should expect provider-native exceptions instead.
49+
Normalized `APIError` subclasses are raised by Outlines model wrappers. If you call a provider SDK directly, expect provider-native exceptions instead.
5050

5151
```python
5252
from outlines.exceptions import (

outlines/exceptions.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"""
77

88
from collections.abc import Mapping
9+
from contextlib import contextmanager
10+
from functools import lru_cache
11+
import importlib
12+
from typing import Iterator
913

1014
__all__ = [
1115
"OutlinesError",
@@ -21,6 +25,7 @@
2125
"ProviderResponseError",
2226
"GenerationError",
2327
"is_provider_exception",
28+
"normalize_provider_errors",
2429
"normalize_provider_exception",
2530
]
2631

@@ -122,7 +127,10 @@ class RateLimitError(APIError):
122127

123128
class BadRequestError(APIError):
124129
"""400, 409, 413, 422, other 4xx - malformed request."""
125-
hint = "Check prompt length, schema, unsupported parameters, etc."
130+
hint = (
131+
"Check prompt length, schema, unsupported parameters, etc. "
132+
"If this is a provider schema support error, try a local model or dottxt."
133+
)
126134

127135

128136
# --- Server errors (5xx) ---
@@ -179,7 +187,10 @@ def _extract_status_code(exc: Exception | None) -> int | None:
179187

180188

181189
def _extract_request_id(exc: Exception) -> str | None:
182-
for attr in ("request_id", "requestId", "x_request_id", "x-request-id"):
190+
# "x-request-id" is intentionally not in this list: dashes aren't valid in
191+
# Python identifiers, so getattr would never find it. The header fallback
192+
# below covers that case.
193+
for attr in ("request_id", "requestId", "x_request_id"):
183194
if isinstance(value := getattr(exc, attr, None), str) and value:
184195
return value
185196

@@ -214,6 +225,7 @@ def _extract_request_id(exc: Exception) -> str | None:
214225
# Per-provider SDK exception maps
215226
# ---------------------------------------------------------------------------
216227

228+
@lru_cache(maxsize=16)
217229
def _build_exception_map(provider: str) -> dict[type, type[APIError]]:
218230
"""Return the SDK-exception → Outlines-exception mapping for *provider*.
219231
@@ -252,26 +264,39 @@ def _build_exception_map(provider: str) -> dict[type, type[APIError]]:
252264
anthropic.UnprocessableEntityError: BadRequestError,
253265
anthropic.InternalServerError: ServerError,
254266
anthropic_exc.ServiceUnavailableError: ServerError,
255-
anthropic_exc.DeadlineExceededError: ServerError,
256267
anthropic_exc.OverloadedError: ServerError,
268+
anthropic_exc.DeadlineExceededError: APITimeoutError,
257269
anthropic.APITimeoutError: APITimeoutError,
258270
anthropic.APIConnectionError: APIConnectionError,
259271
anthropic.APIResponseValidationError: ProviderResponseError,
260272
}
261273

262274
if provider == "mistral":
263275
import httpx
264-
import mistralai.models as mm
265-
return {
266-
mm.HTTPValidationError: BadRequestError,
267-
mm.ValidationError: BadRequestError,
268-
mm.ResponseValidationError: ProviderResponseError,
269-
mm.NoResponseError: APIConnectionError,
276+
277+
mapping: dict[type, type[APIError]] = {
270278
httpx.TimeoutException: APITimeoutError,
271279
httpx.ConnectError: APIConnectionError,
272-
# MistralError / SDKError with .status_code handled by status-code fallback
273280
}
274281

282+
# Mistral SDK v2 exposes public exceptions from mistralai.client.errors.
283+
# Older SDKs are best-effort via transport and status-code fallback.
284+
try:
285+
mistral_errors = importlib.import_module("mistralai.client.errors")
286+
except ImportError:
287+
return mapping
288+
289+
for name, outlines_exc_cls in (
290+
("HTTPValidationError", BadRequestError),
291+
("ResponseValidationError", ProviderResponseError),
292+
("NoResponseError", APIConnectionError),
293+
):
294+
exc_cls = getattr(mistral_errors, name, None)
295+
if isinstance(exc_cls, type) and issubclass(exc_cls, Exception):
296+
mapping[exc_cls] = outlines_exc_cls
297+
298+
return mapping
299+
275300
if provider == "gemini":
276301
import httpx
277302
from google.genai import errors as genai_errors
@@ -285,8 +310,12 @@ def _build_exception_map(provider: str) -> dict[type, type[APIError]]:
285310
}
286311

287312
if provider == "ollama":
313+
import httpx
288314
import ollama
289315
return {
316+
ConnectionError: APIConnectionError,
317+
httpx.ConnectError: APIConnectionError,
318+
httpx.TimeoutException: APITimeoutError,
290319
ollama.RequestError: APIConnectionError,
291320
# ollama.ResponseError.status_code handled by status-code fallback
292321
# (.status_code defaults to -1 when unknown; range guard filters it out)
@@ -359,3 +388,20 @@ def normalize_provider_exception(exc: Exception, provider: str) -> APIError:
359388
return BadRequestError(provider=provider, original_exception=exc)
360389

361390
return APIError(provider=provider, original_exception=exc)
391+
392+
393+
@contextmanager
394+
def normalize_provider_errors(provider: str) -> Iterator[None]:
395+
"""Normalize provider exceptions raised inside this block.
396+
397+
This is a context manager instead of a decorator so wrappers can use the
398+
same helper around sync calls, awaited async calls, sync generators, async
399+
generators, and other provider SDK control-flow shapes without needing
400+
separate wrapper machinery for each function kind.
401+
"""
402+
try:
403+
yield
404+
except Exception as exc:
405+
if not is_provider_exception(exc, provider):
406+
raise
407+
raise normalize_provider_exception(exc, provider) from exc

outlines/models/anthropic.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import singledispatchmethod
44
from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
55

6-
from outlines.exceptions import is_provider_exception, normalize_provider_exception
6+
from outlines.exceptions import normalize_provider_errors
77
from outlines.inputs import Chat, Image
88
from outlines.models.base import Model, ModelTypeAdapter
99

@@ -189,15 +189,11 @@ def generate(
189189
):
190190
inference_kwargs["model"] = self.model_name
191191

192-
try:
192+
with normalize_provider_errors(PROVIDER):
193193
completion = self.client.messages.create(
194194
**messages,
195195
**inference_kwargs,
196196
)
197-
except Exception as e:
198-
if not is_provider_exception(e, PROVIDER):
199-
raise
200-
raise normalize_provider_exception(e, PROVIDER) from e
201197
return completion.content[0].text
202198

203199
def generate_batch(
@@ -248,28 +244,18 @@ def generate_stream(
248244
):
249245
inference_kwargs["model"] = self.model_name
250246

251-
try:
247+
with normalize_provider_errors(PROVIDER):
252248
stream = self.client.messages.create(
253249
**messages,
254250
stream=True,
255251
**inference_kwargs,
256252
)
257-
except Exception as e:
258-
if not is_provider_exception(e, PROVIDER):
259-
raise
260-
raise normalize_provider_exception(e, PROVIDER) from e
261-
262-
try:
263253
for chunk in stream:
264254
if (
265255
chunk.type == "content_block_delta"
266256
and chunk.delta.type == "text_delta"
267257
):
268258
yield chunk.delta.text
269-
except Exception as e:
270-
if not is_provider_exception(e, PROVIDER):
271-
raise
272-
raise normalize_provider_exception(e, PROVIDER) from e
273259

274260

275261
def from_anthropic(

outlines/models/dottxt.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
from typing import TYPE_CHECKING, Any, Optional, Union, cast
55

6-
from outlines.exceptions import APIError, is_provider_exception, normalize_provider_exception
6+
from outlines.exceptions import normalize_provider_errors
77
from outlines.models.base import AsyncModel, Model, ModelTypeAdapter
88
from outlines.types import CFG, JsonSchema, Regex
99

@@ -142,16 +142,12 @@ def generate(
142142
"or as a `model=` keyword argument at generation time."
143143
)
144144

145-
try:
145+
with normalize_provider_errors(PROVIDER):
146146
result = self.client.generate(
147147
input=prompt,
148148
response_format=json_schema,
149149
**inference_kwargs,
150150
)
151-
except Exception as e:
152-
if not is_provider_exception(e, PROVIDER):
153-
raise
154-
raise normalize_provider_exception(e, PROVIDER) from e
155151

156152
return json.dumps(result)
157153

@@ -242,14 +238,12 @@ async def generate(
242238
"or as a `model=` keyword argument at generation time."
243239
)
244240

245-
try:
241+
with normalize_provider_errors(PROVIDER):
246242
result = await self.client.generate(
247243
input=prompt,
248244
response_format=json_schema,
249245
**inference_kwargs,
250246
)
251-
except Exception as e:
252-
raise normalize_provider_exception(e, PROVIDER) from e
253247

254248
return json.dumps(result)
255249

outlines/models/gemini.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
get_args,
1111
)
1212

13-
from outlines.exceptions import is_provider_exception, normalize_provider_exception
13+
from outlines.exceptions import normalize_provider_errors
1414
from outlines.inputs import Image, Chat
1515
from outlines.models.base import Model, ModelTypeAdapter
1616
from outlines.types import CFG, Choice, JsonSchema, Regex
@@ -297,16 +297,12 @@ def generate(
297297
contents = self.type_adapter.format_input(model_input)
298298
generation_config = self.type_adapter.format_output_type(output_type)
299299

300-
try:
300+
with normalize_provider_errors(PROVIDER):
301301
completion = self.client.models.generate_content(
302302
**contents,
303303
model=inference_kwargs.pop("model", self.model_name),
304304
config={**generation_config, **inference_kwargs}
305305
)
306-
except Exception as e:
307-
if not is_provider_exception(e, PROVIDER):
308-
raise
309-
raise normalize_provider_exception(e, PROVIDER) from e
310306

311307
return completion.text
312308

@@ -348,25 +344,15 @@ def generate_stream(
348344
contents = self.type_adapter.format_input(model_input)
349345
generation_config = self.type_adapter.format_output_type(output_type)
350346

351-
try:
347+
with normalize_provider_errors(PROVIDER):
352348
stream = self.client.models.generate_content_stream(
353349
**contents,
354350
model=inference_kwargs.pop("model", self.model_name),
355351
config={**generation_config, **inference_kwargs},
356352
)
357-
except Exception as e:
358-
if not is_provider_exception(e, PROVIDER):
359-
raise
360-
raise normalize_provider_exception(e, PROVIDER) from e
361-
362-
try:
363353
for chunk in stream:
364354
if hasattr(chunk, "text") and chunk.text:
365355
yield chunk.text
366-
except Exception as e:
367-
if not is_provider_exception(e, PROVIDER):
368-
raise
369-
raise normalize_provider_exception(e, PROVIDER) from e
370356

371357

372358
def from_gemini(client: "Client", model_name: Optional[str] = None) -> Gemini:

outlines/models/lmstudio.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Integration with the `lmstudio` library."""
1+
"""Integration with the `lmstudio` library.
2+
3+
Local runtime calls intentionally bypass
4+
outlines.exceptions.normalize_provider_errors().
5+
"""
26

37
from functools import singledispatchmethod
48
from typing import (
@@ -218,7 +222,6 @@ def generate(
218222
if response_format is not None:
219223
kwargs["response_format"] = response_format
220224

221-
# Local runtime: intentionally bypasses outlines.exceptions.normalize_provider_exception().
222225
result = model.respond(formatted_input, **kwargs)
223226
return result.content
224227

@@ -269,7 +272,6 @@ def generate_stream(
269272
if response_format is not None:
270273
kwargs["response_format"] = response_format
271274

272-
# Local runtime: intentionally bypasses outlines.exceptions.normalize_provider_exception().
273275
stream = model.respond_stream(formatted_input, **kwargs)
274276
for fragment in stream:
275277
yield fragment.content
@@ -348,7 +350,6 @@ async def generate(
348350
if response_format is not None:
349351
kwargs["response_format"] = response_format
350352

351-
# Local runtime: intentionally bypasses outlines.exceptions.normalize_provider_exception().
352353
result = await model.respond(formatted_input, **kwargs)
353354
return result.content
354355

@@ -403,7 +404,6 @@ async def generate_stream( # type: ignore
403404
if response_format is not None:
404405
kwargs["response_format"] = response_format
405406

406-
# Local runtime: intentionally bypasses outlines.exceptions.normalize_provider_exception().
407407
stream = await model.respond_stream(formatted_input, **kwargs)
408408
async for fragment in stream:
409409
yield fragment.content

0 commit comments

Comments
 (0)