@@ -154,98 +154,16 @@ def __init__(
154154 def _map_api_exception (self , error : ApiException ) -> OtariError :
155155 """Map a generated ``ApiException`` to a typed otari exception.
156156
157- ``ApiException`` carries ``.status`` (int) and ``.body`` (the raw JSON
158- string the gateway returned) plus ``.headers``. The gateway encodes the
159- human-readable reason under the ``detail`` key (FastAPI convention).
160-
161- Most status mappings only apply in platform mode; in non-platform mode
162- the generic :class:`OtariError` is raised so the caller still gets a
163- single SDK exception type. The one cross-mode case is
164- :class:`UnsupportedCapabilityError`, surfaced in both modes.
157+ Thin wrapper over the module-level :func:`map_api_exception` so the
158+ control-plane resources can reuse the same mapping without a client
159+ instance.
165160 """
166- status = error .status if isinstance (error .status , int ) else 0
167- headers = error .headers or {}
168- detail = self ._extract_detail (error )
169- correlation_id = _header_get (headers , "x-correlation-id" )
170- retry_after = _header_get (headers , "retry-after" )
171-
172- full = f"{ detail } (correlation_id={ correlation_id } )" if correlation_id else detail
173-
174- # Unsupported-capability is surfaced regardless of mode.
175- if status == 400 and _UNSUPPORTED_MODERATION_RE .search (detail ):
176- provider = _parse_unsupported_provider (detail )
177- capability = "multimodal_moderation" if "multimodal" in detail else "moderation"
178- return UnsupportedCapabilityError (
179- full ,
180- status_code = status ,
181- original_error = error ,
182- provider_name = PROVIDER_NAME ,
183- provider = provider ,
184- capability = capability ,
185- )
186-
187- if status in (401 , 403 ):
188- return AuthenticationError (
189- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
190- )
191- if status == 402 :
192- return InsufficientFundsError (
193- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
194- )
195- if status == 404 :
196- return ModelNotFoundError (
197- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
198- )
199- if status == 409 :
200- return BatchNotCompleteError (
201- full ,
202- status_code = status ,
203- original_error = error ,
204- provider_name = PROVIDER_NAME ,
205- batch_id = _extract_batch_id (detail ),
206- batch_status = _extract_status (detail ),
207- )
208- if status == 429 :
209- return RateLimitError (
210- full ,
211- status_code = status ,
212- original_error = error ,
213- provider_name = PROVIDER_NAME ,
214- retry_after = retry_after ,
215- )
216- if status == 504 :
217- return GatewayTimeoutError (
218- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
219- )
220- # 502 and any other 5xx are upstream-provider failures.
221- if status == 502 or 500 <= status < 600 :
222- return UpstreamProviderError (
223- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
224- )
225-
226- return OtariError (
227- full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
228- )
161+ return map_api_exception (error )
229162
230163 @staticmethod
231164 def _extract_detail (error : ApiException ) -> str :
232165 """Pull the gateway's human-readable detail from an ``ApiException`` body."""
233- body = error .body
234- if isinstance (body , (bytes , bytearray )):
235- body = body .decode ("utf-8" , "replace" )
236- if isinstance (body , str ) and body :
237- try :
238- parsed = json .loads (body )
239- except (ValueError , TypeError ):
240- return body
241- if isinstance (parsed , dict ):
242- detail = parsed .get ("detail" ) or parsed .get ("message" ) or parsed .get ("error" )
243- if isinstance (detail , str ):
244- return detail
245- if detail is not None :
246- return str (detail )
247- return body
248- return error .reason or "An error occurred"
166+ return extract_detail (error )
249167
250168 def _map_streaming_response (self , response : httpx .Response , body : bytes ) -> OtariError :
251169 """Map a failed raw streaming response to a typed otari exception.
@@ -331,3 +249,102 @@ def _extract_status(message: str) -> str | None:
331249def _url_encode (value : str ) -> str :
332250 """Percent-encode a single URL component."""
333251 return urllib .parse .quote (value , safe = "" )
252+
253+
254+ def extract_detail (error : ApiException ) -> str :
255+ """Pull the gateway's human-readable detail from an ``ApiException`` body."""
256+ body = error .body
257+ if isinstance (body , (bytes , bytearray )):
258+ body = body .decode ("utf-8" , "replace" )
259+ if isinstance (body , str ) and body :
260+ try :
261+ parsed = json .loads (body )
262+ except (ValueError , TypeError ):
263+ return body
264+ if isinstance (parsed , dict ):
265+ detail = parsed .get ("detail" ) or parsed .get ("message" ) or parsed .get ("error" )
266+ if isinstance (detail , str ):
267+ return detail
268+ if detail is not None :
269+ return str (detail )
270+ return body
271+ return error .reason or "An error occurred"
272+
273+
274+
275+ def map_api_exception (error : ApiException ) -> OtariError :
276+ """Map a generated ``ApiException`` to a typed otari exception.
277+
278+ ``ApiException`` carries ``.status`` (int) and ``.body`` (the raw JSON
279+ string the gateway returned) plus ``.headers``. The gateway encodes the
280+ human-readable reason under the ``detail`` key (FastAPI convention).
281+
282+ Most status mappings only apply in platform mode; in non-platform mode
283+ the generic :class:`OtariError` is raised so the caller still gets a
284+ single SDK exception type. The one cross-mode case is
285+ :class:`UnsupportedCapabilityError`, surfaced in both modes.
286+ """
287+ status = error .status if isinstance (error .status , int ) else 0
288+ headers = error .headers or {}
289+ detail = extract_detail (error )
290+ correlation_id = _header_get (headers , "x-correlation-id" )
291+ retry_after = _header_get (headers , "retry-after" )
292+
293+ full = f"{ detail } (correlation_id={ correlation_id } )" if correlation_id else detail
294+
295+ # Unsupported-capability is surfaced regardless of mode.
296+ if status == 400 and _UNSUPPORTED_MODERATION_RE .search (detail ):
297+ provider = _parse_unsupported_provider (detail )
298+ capability = "multimodal_moderation" if "multimodal" in detail else "moderation"
299+ return UnsupportedCapabilityError (
300+ full ,
301+ status_code = status ,
302+ original_error = error ,
303+ provider_name = PROVIDER_NAME ,
304+ provider = provider ,
305+ capability = capability ,
306+ )
307+
308+ if status in (401 , 403 ):
309+ return AuthenticationError (
310+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
311+ )
312+ if status == 402 :
313+ return InsufficientFundsError (
314+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
315+ )
316+ if status == 404 :
317+ return ModelNotFoundError (
318+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
319+ )
320+ if status == 409 :
321+ return BatchNotCompleteError (
322+ full ,
323+ status_code = status ,
324+ original_error = error ,
325+ provider_name = PROVIDER_NAME ,
326+ batch_id = _extract_batch_id (detail ),
327+ batch_status = _extract_status (detail ),
328+ )
329+ if status == 429 :
330+ return RateLimitError (
331+ full ,
332+ status_code = status ,
333+ original_error = error ,
334+ provider_name = PROVIDER_NAME ,
335+ retry_after = retry_after ,
336+ )
337+ if status == 504 :
338+ return GatewayTimeoutError (
339+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
340+ )
341+ # 502 and any other 5xx are upstream-provider failures.
342+ if status == 502 or 500 <= status < 600 :
343+ return UpstreamProviderError (
344+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
345+ )
346+
347+ return OtariError (
348+ full , status_code = status , original_error = error , provider_name = PROVIDER_NAME
349+ )
350+
0 commit comments