Metadata-Version: 2.4
Name: aibridgecore
Version: 1.5.15.dev1
Summary: Bridge for LLM"s
Home-page: https://github.com/23ventures/aibridge-core
Author: Ashish Tilekar
Author-email: developer.tools@23v.co
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.9.0
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai<3.0.0,>=1.104.2
Requires-Dist: SQLAlchemy>=2.0.19
Requires-Dist: redis>=4.6.0
Requires-Dist: PyYAML>=6.0.1
Requires-Dist: Jinja2>=3.1.2
Requires-Dist: pymongo>=4.4.1
Requires-Dist: sqlparse>=0.4.4
Requires-Dist: jsonschema>=4.18.4
Requires-Dist: Pillow>=10.0.0
Requires-Dist: google-genai>=1.62.0
Requires-Dist: cohere>=5.13.11
Requires-Dist: ai21>=2.13.0
Requires-Dist: xmltodict>=0.13.0
Requires-Dist: anthropic>=0.45.2
Requires-Dist: ollama<=1.2.2
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary


# aibridgecore

A unified Python SDK for working with multiple AI providers through a consistent set of utilities — text generation (sync + streaming), prompt management, reusable variables, structured outputs, queue-backed async execution, image generation, video generation — all with **client-side validation**, **typed exceptions**, and **provider-normalized error handling**.

## Overview

- **Multi-provider text generation** with a shared high-level interface — same call shape across OpenAI, Anthropic, Gemini, Cohere, Mistral, Grok, Deepseek, Kimi, Alibaba (Qwen), AI21, and self-hosted Ollama
- **Streaming responses** (`generate_stream`) on every provider that supports it
- **Function calling** (`execute_prompt_function_calling`) for tool use across providers
- **Structured outputs** — JSON, CSV, XML workflows with format validation
- **Stored prompts and reusable variables** — backed by SQL or MongoDB
- **Redis-based async queue** for fire-and-forget execution at scale
- **Provider-specific image generation** APIs (DALL·E, Stable Diffusion)
- **Provider-specific video generation** modules
- **Client-side validation** before any network call (model, prompts, temperature, output format, context, etc.)
- **Provider-agnostic exception model** — every error has the same shape with the same attributes regardless of which provider failed
- **Automatic categorisation** of errors into typed Python exceptions (`BadRequestException`, `AuthenticationException`, `RateLimitException`, `TimeoutException`, `ServerException`)
- **Inheritance-based validator system** — 90% of validation logic lives in `BaseValidator`, providers override only what differs
- **OpenAI-style printed errors** — uncaught exceptions render like `openai.BadRequestError: Error code: 400 - {'error': {...}}` so they're immediately useful in tracebacks

---

## Table of Contents

1. [Installation & Configuration](#installation--configuration)
2. [Quick Start](#quick-start)
3. [Architecture](#architecture)
4. [Multi-Provider Text Generation](#multi-provider-text-generation)
5. [Streaming](#streaming)
6. [Function Calling](#function-calling)
7. [Structured Outputs (JSON / CSV / XML)](#structured-outputs-json--csv--xml)
8. [Stored Prompts & Variables](#stored-prompts--variables)
9. [Asynchronous Queue Execution (Redis)](#asynchronous-queue-execution-redis)
10. [Image Generation](#image-generation)
11. [Video Generation](#video-generation)
12. [The Validator System](#the-validator-system)
13. [The Exception Model](#the-exception-model)
14. [The Wrapper (`@provider_wrapper`)](#the-wrapper-provider_wrapper)
15. [The Error Normalizer](#the-error-normalizer)
16. [End-to-End Error Flow](#end-to-end-error-flow)
17. [Error Handling](#error-handling)
18. [Provider Catalog](#provider-catalog)
19. [Adding a New Provider](#adding-a-new-provider)
20. [Web Integration (Flask / FastAPI)](#web-integration-flask--fastapi)
21. [Testing](#testing)
22. [File Map](#file-map)
23. [Glossary](#glossary)

---

## Installation & Configuration

```bash
pip install aibridgecore
```

Python 3.9+. Set the config file path:

```bash
export AIBRIDGE_CONFIG=/absolute/path/to/aibridge_config.yaml
```

Minimal `aibridge_config.yaml`:

```yaml
open_ai:
  equal:
    - YOUR_OPENAI_API_KEY

kimi:
  equal:
    - YOUR_KIMI_API_KEY

anthropic:
  equal:
    - YOUR_ANTHROPIC_API_KEY

database: sql                  # or "mongo"
message_queue: redis
redis_host: localhost
redis_port: 6379
```

Each provider has its own top-level key. The `equal` list supports key rotation across multiple keys.

---

## Quick Start

```python
from aibridgecore.ai_services.openai_services import OpenAIService
from aibridgecore.exceptions import AIBridgeException

try:
    response = OpenAIService.generate(
        prompts=["Summarise this paragraph in 50 words..."],
        model="gpt-3.5-turbo",
        variation_count=1,
        max_tokens=200,
        temperature=0.5,
    )
    print(response)

except AIBridgeException as e:
    print(f"{type(e).__name__}: {e.user_message}")
```

Every exception in the SDK is a subclass of `AIBridgeException` and carries the same attributes: `e.status_code` (int HTTP status), `e.code` (string error code), `e.category`, `e.message`, `e.user_message`, `e.param`, `e.expected`, `e.received`, `e.provider`, `e.retryable`, `e.developer_message`, and the full flat `e.details` dict (plus `e.to_dict()`).

---

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  Caller (script / Flask / FastAPI / worker)                 │
└────────────────────┬────────────────────────────────────────┘
                     │ OpenAIService.generate(prompts=..., ...)
                     ▼
┌─────────────────────────────────────────────────────────────┐
│  @provider_wrapper(validator=OpenAIValidator, ...)          │
│    1. OpenAIValidator.validate_generate(kwargs)             │
│    2. func(*args, **kwargs)  if validation passes           │
│    3. On any exception → normalize → re-raise typed         │
└────────────────────┬────────────────────────────────────────┘
                     │ HTTPS (only if validation passed)
                     ▼
┌─────────────────────────────────────────────────────────────┐
│  Provider API (OpenAI, Anthropic, Kimi, ...)                │
└─────────────────────────────────────────────────────────────┘
```

Three pieces do the heavy lifting:

| Piece | File | Responsibility |
|---|---|---|
| **Validators** | `aibridgecore/validators/provider_validator.py` | Check kwargs before the call. Raise `BadRequestException` with rich details on bad input. |
| **Wrapper** | `aibridgecore/wrappers/provider_wrapper.py` | Run the validator, run the function, catch any exception, transform it into the right category class. |
| **Normalizer** | `aibridgecore/error_hanldling/error_normalizer.py` | Inspect any non-typed exception and produce a rich details dict. |

---

## Multi-Provider Text Generation

Every provider exposes the same method signature for `generate()`:

```python
SomeService.generate(
    prompts=[...],                 # or prompt_ids=[...] (from stored prompts)
    prompt_data=[...],             # variable substitutions
    variables=[...],
    output_format=["json"],        # "json", "xml", "csv", "string"
    format_strcture=["..."],       # JSON schema / XML template / CSV header
    model="...",
    variation_count=1,
    max_tokens=200,
    temperature=0.5,
    message_queue=False,           # if True, enqueue and return response_id
    api_key=None,                  # falls back to AIBRIDGE_CONFIG
    output_format_parse=True,
    context=[{"role": "system", "context": "..."}],
)
```

Swap the service class to switch providers:

```python
from aibridgecore.ai_services.openai_services import OpenAIService
from aibridgecore.ai_services.anthropic_ai import AnthropicService
from aibridgecore.ai_services.geminin_services import GeminiAIService
from aibridgecore.ai_services.cohere_llm import CohereApi
from aibridgecore.ai_services.kimi_services import KimiService
from aibridgecore.ai_services.grok_services import GrokService
from aibridgecore.ai_services.mistral_services import MistralService
from aibridgecore.ai_services.deepseek_services import DeepseekService
from aibridgecore.ai_services.alibaba_services import AlibabaService
from aibridgecore.ai_services.ai21labs_text import AI21labsText
from aibridgecore.ai_services.ollama_services import OllamaService
```

The validation rules and exception types are uniform across all of them.

---

## Streaming

```python
from aibridgecore.ai_services.openai_services import OpenAIService
from aibridgecore.exceptions import AIBridgeException

try:
    stream = OpenAIService.generate_stream(
        prompts=["Tell me a 3-sentence story."],
        model="gpt-3.5-turbo",
        temperature=0.5,
        max_tokens=200,
    )
    for chunk in stream:
        if chunk["type"] == "content":
            print(chunk["data"], end="", flush=True)
        elif chunk["type"] == "usage":
            print(f"\n[input={chunk['input_tokens']} output={chunk['output_tokens']}]")

except AIBridgeException as e:
    print(f"\n{type(e).__name__}: {e}")
```

Chunks come in two shapes:
- `{"type": "content", "data": "<text fragment>"}`
- `{"type": "usage", "input_tokens": N, "output_tokens": M}` (at the end)

Validation runs **before** the generator is created, so bad input raises a `BadRequestException` immediately (no API round-trip). Errors raised **mid-stream** are also caught by `@provider_wrapper` and normalized into the right typed exception, just like non-streaming methods. `kimi_services.py` and `ollama_services.py` do not expose `generate_stream`.

---

## Function Calling

```python
result = OpenAIService.execute_prompt_function_calling(
    api_key="sk-...",
    model="gpt-4",
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
    n=1,
    functions_call=[{
        "name": "get_weather",
        "description": "Get current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }],
    max_tokens=200,
    temperature=0.3,
)
```

The validator checks: `api_key` required, `model` required and string, `messages` must be a non-empty list of dicts, `n` is a positive integer, `functions_call` is a list of dicts.

Supported on every LLM provider except AI21.

---

## Structured Outputs (JSON / CSV / XML)

Pass `output_format` and `format_strcture` to constrain provider responses to machine-parseable shapes:

```python
response = OpenAIService.generate(
    prompts=["List 3 fruits with their colors."],
    model="gpt-4",
    variation_count=1,
    output_format=["json"],
    format_strcture=['{"fruits": [{"name": "string", "color": "string"}]}'],
)
```

Allowed `output_format` values: `"json"`, `"xml"`, `"csv"`, `"string"`. Lengths of `output_format` and `format_strcture` must match `len(prompts)`. JSON templates are checked for parse-ability; XML/CSV templates are sanity-checked at validation time.

Parsers live in `aibridgecore/output_validation/`.

---

## Stored Prompts & Variables

Store frequently-used prompts in your database and reference them by ID, with placeholder substitution:

```python
response = OpenAIService.generate(
    prompt_ids=["product_description_v3"],
    prompt_data=[{
        "product_name": "Acme Widget",
        "feature_list": ["A", "B", "C"],
    }],
    variables=[{"tone": "playful"}],
    model="gpt-4",
)
```

Backed by SQL or MongoDB depending on `database:` in `aibridge_config.yaml`. Helpers in `aibridgecore/prompts/`.

---

## Asynchronous Queue Execution (Redis)

For long jobs or fan-out workloads, enqueue the call and return a response ID immediately:

```python
result = OpenAIService.generate(
    prompts=["Long-running multi-step task..."],
    model="gpt-4",
    message_queue=True,                # ← off by default
)

print(result)                          # {"response_id": "<uuid>"}
```

A worker (using `aibridgecore.ai_services.process_mq`) drains the Redis queue, executes the generate call, and stores the result for later retrieval. Configure Redis via `redis_host` / `redis_port` in the yaml.

---

## Image Generation

```python
from aibridgecore.ai_services.openai_images import OpenAIImage

result = OpenAIImage.generate(
    prompts=["A cyberpunk samurai in Tokyo at night."],
    model="dall-e-3",
    size="1024x1024",
    process_type="create",             # "create", "variation", or "edit"
    variation_count=1,
    api_key="sk-...",
)
```

For `process_type="variation"` or `"edit"`, also pass `image_data=[...]` (and `mask_image=[...]` for edits). Edits and variations require `model="dall-e-2"`.

Stable Diffusion has the same shape — see `aibridgecore.ai_services.stable_diffusion_image.StableDiffusion`.

---

## Video Generation

Provider-specific modules live in `aibridgecore/video/`. Same overall pattern as image generation — pass a prompt, get a generation response or queue ID back.

---

## The Validator System

### Design

A single file — `validators/provider_validator.py` — holds:

- One `BaseValidator` with ~90% of the validation logic
- One subclass per provider that only overrides what differs
- A `VALIDATORS` registry mapping provider name → class

```
BaseValidator
├── OpenAIValidator
├── KimiValidator              (TEMP_MAX = 1.0)
├── AnthropicValidator
├── GeminiValidator
├── CohereValidator
├── GrokValidator
├── MistralValidator
├── DeepseekValidator
├── AlibabaValidator
├── OllamaValidator
└── Ai21Validator
```

```python
from aibridgecore.validators.provider_validator import VALIDATORS

VALIDATORS["kimi"]    # <class 'KimiValidator'>
VALIDATORS["openai"]  # <class 'OpenAIValidator'>
```

### What `BaseValidator` checks

Per-field methods on `BaseValidator`:

| Method | Validates |
|---|---|
| `validate_prompt_source(kwargs)` | exactly one of `prompts` / `prompt_ids` is provided |
| `validate_prompts(prompts)` | list of non-empty strings |
| `validate_prompt_ids(prompt_ids)` | list |
| `validate_prompt_data(prompt_data)` | list of dicts |
| `validate_variables(variables)` | list |
| `validate_model(model)` | required, must be a string |
| `validate_temperature(temperature)` | numeric, `TEMP_MIN <= temp <= TEMP_MAX` |
| `validate_max_tokens(max_tokens)` | positive integer |
| `validate_variation_count(variation_count)` | positive integer |
| `validate_output_format(output_format, prompts)` | list of `"json" / "xml" / "csv" / "string"`, length matches prompts |
| `validate_format_structure(structure, output_format, prompts)` | structured format string is parseable; length matches prompts |
| `validate_context(context)` | list of `{role, context}` dicts; role is `user / system / assistant` |
| `validate_api_key(api_key)` | non-empty string when provided |

### Class-level constants subclasses can override

```python
class BaseValidator:
    PROVIDER = "base"
    ALLOWED_FORMATS = ["json", "xml", "csv", "string"]
    ALLOWED_ROLES = ["user", "system", "assistant"]
    TEMP_MIN = 0.0
    TEMP_MAX = 2.0
```

Real overrides today:

```python
class KimiValidator(BaseValidator):
    PROVIDER = "kimi"
    TEMP_MAX = 1.0
```

### Method-level validators (full surface)

Every LLM service is wrapped on six entry points:

| Service method | validation_method classmethod |
|---|---|
| `generate` | `validate_generate` |
| `generate_stream` | `validate_generate_stream` |
| `execute_text_prompt` | `validate_execute_text_prompt_method` |
| `execute_prompt_function_calling` | `validate_execute_prompt_function_calling_method` |
| `get_prompt_context` | `validate_get_prompt_context_method` |
| `get_response` | `validate_get_response_method` |

### Error codes the validator emits

| `e.code` | Meaning |
|---|---|
| `MISSING_REQUIRED_FIELD` | A required arg was `None` or absent |
| `EMPTY_VALUE` | A required string is empty / whitespace |
| `INVALID_TYPE` | Argument was of the wrong Python type |
| `INVALID_VALUE` | Value didn't match an enum (output_format, role, etc.) |
| `OUT_OF_RANGE` | Numeric value outside allowed range |
| `CONFLICTING_FIELDS` | Mutually exclusive args both provided |
| `LENGTH_MISMATCH` | List lengths don't match (e.g. output_format vs prompts) |

---

## The Exception Model

### Hierarchy

```
Exception
└── AIBridgeException                (.code, .status_code, .message, .details, to_dict(), __str__, __repr__)
    │
    ├── BadRequestException          (HTTP 400 — generic bad request)
    │   └── (ValidationException can be made to inherit — currently sibling)
    ├── AuthenticationException      (HTTP 401)
    ├── TimeoutException             (HTTP 408)
    ├── RateLimitException           (HTTP 429)
    ├── ServerException              (HTTP 5xx)
    ├── ValidationException          (HTTP 400 — client-side validator caught it)
    │
    └── Provider-specific (internal layer, rarely surfaced):
        OpenAIException, KimiException, AnthropicsException, GeminiException,
        CohereException, Ai21Exception, GrokException, DeepseekException,
        MistralException, AlibabaException, OllamaException
```

### Per-category-class HTTP status

| Class | `e.status_code` |
|---|---|
| `ValidationException` / `BadRequestException` | 400 |
| `AuthenticationException` | 401, 403 |
| `TimeoutException` | 408 |
| `RateLimitException` | 429 |
| `ServerException` | 500–599 (preserves upstream) |

### `e.code` vs `e.status_code`

These are two different things, matching the OpenAI SDK:

- **`e.status_code`** — `int` HTTP status (`401`, `429`, `500`, ...).
- **`e.code`** — `str` machine-readable error code (`"rate_limit_exceeded"`, `"invalid_api_key"`, ...) or `None` when unknown.

### Attributes on every exception

```python
e.status_code          # int — HTTP status (400, 401, 403, 429, ...)
e.code                 # str / None — semantic error code ("rate_limit_exceeded", ...)
e.category             # str — error category (rate_limit_error, authentication_error, ...)
e.message              # str — raw machine-friendly message
e.user_message         # str — human-friendly version
e.param                # str / None — which input parameter, if applicable
e.expected             # str / None — what was expected
e.received             # any  / None — what was received
e.provider             # str / None — which provider triggered
e.retryable            # bool — whether retrying may succeed
e.developer_message    # str — pre-formatted developer log line
e.details              # dict — all of the above as a flat dict (always present)
e.to_dict()            # method — returns e.details
```

### Backward compatibility

`AiBridgeValidationException` is an alias of `BadRequestException`.

---

## The Wrapper (`@provider_wrapper`)

Every public method on every service is decorated with `@provider_wrapper(validator=..., validation_method=...)`. The wrapper:

1. **Argument binding** — binds the call against the function signature and applies declared defaults, so the validator sees the *effective* values (an omitted `variation_count=1` validates as `1`, not `None`). Mutable defaults (`prompts=[]`, `context=[]`) are copied per call so state never leaks between invocations.
2. **Pre-validation** — runs the validator. On failure, the validator raises `BadRequestException`; wrapper sees it's a category class and lets it propagate.
3. **Function execution** — runs the wrapped function. Generator methods (`generate_stream`) are detected automatically and their iteration is wrapped too, so errors raised mid-stream are caught the same way.
4. **Exception transformation** — typed exceptions pass through (with `e.details` filled in to the full normalized shape); anything else gets normalized and transformed into the right category class.

Because the wrapper covers every method shape — sync, generator, validation-time and mid-stream errors — service methods do **not** need their own `try/except`. It is the single error-handling layer.

### `_class_for_code()` mapping

```python
400        → BadRequestException
401, 403   → AuthenticationException
408        → TimeoutException
429        → RateLimitException
500-599    → ServerException
other      → BadRequestException
```

### Provider auto-injection

The wrapper reads `PROVIDER` from the validator class and stamps it onto `e.details["provider"]`.

---

## The Error Normalizer

`ErrorNormalizer.normalize(e, provider=None)` inspects any exception and returns a
structured details dict. Normalization is **status-code-driven**: `_GENERIC_BY_STATUS`
covers the **12 HTTP status codes** below, and a small `ERROR_CATALOG` holds only the
handful of **provider-specific** errors that need a non-generic mapping.

### Resolution order

| Step | Trigger | Result |
|---|---|---|
| 1 | `isinstance(e, ValidationException / BadRequestException)` | `validation_error` / 400, passes param details through |
| 2 | `provider` known and `e.code` matches a catalog entry | provider-specific entry |
| 3 | a provider catalog `match_keywords` entry is found in the message | provider-specific entry |
| 4 | `e.status_code` is set | `_GENERIC_BY_STATUS` entry for that status |
| 5 | nothing matched | `internal_server_error` / 500 |

### Status codes covered (12)

`400` invalid request · `401` authentication · `402` insufficient balance ·
`403` permission / region blocked · `404` not found · `408` timeout ·
`422` unprocessable entity · `429` rate limit · `500` server error ·
`502` bad gateway · `503` service unavailable / overloaded · `529` overloaded (Anthropic).

### `ERROR_CATALOG` — provider-specific overrides only (7)

Common errors (`invalid_api_key`, `rate_limit_exceeded`, `model_not_found`,
`internal_server_error`, ...) are **not** catalogued — they resolve straight from
`_GENERIC_BY_STATUS` by HTTP status. The catalog holds only entries whose behavior
differs from the generic mapping:

| Provider | Code | Why it overrides the generic |
|----------|------|------------------------------|
| openai | `insufficient_quota` | 429 but `retryable: False` (billing, not rate) |
| openai | `context_length_exceeded` | 400 with `param: messages` + specific guidance |
| openai | `engine_overloaded` | 503 with provider-specific wording |
| anthropic | `overloaded_error` | non-standard `529` |
| gemini | `safety_blocked` | content-filter — no generic equivalent |
| ollama | `out_of_memory` | 500 but `retryable: False` |
| ollama | `connection_refused` | local-daemon reachability |

### Categories (6)

`authentication_error` · `rate_limit_error` · `validation_error` ·
`provider_error` · `server_error` · `unknown_error`.

`ErrorNormalizer.status_codes()` returns the sorted list of covered statuses;
`ErrorNormalizer.lookup(provider, code)` returns a single catalog entry.

---

## End-to-End Error Flow

### Scenario A — client-side validation fails

1. `@provider_wrapper` runs the validator.
2. Validator's `_raise()` constructs `BadRequestException` with full details.
3. Exception propagates. Wrapper sees a category class → re-raises it, filling in any missing `details` keys.
4. Caller catches `BadRequestException`. No HTTPS round-trip.

### Scenario B — provider rate-limits

1. Validation passes. Function calls provider SDK.
2. Provider raises `openai.RateLimitError`.
3. The raw exception propagates to the wrapper (no per-function `try/except` needed).
4. Wrapper catches it, the normalizer reads `status_code` / `code` / `"rate limit"` → 429.
5. Wrapper picks `RateLimitException`, attaches full `details`, raises `from e`.
6. Caller catches `RateLimitException`.

### Scenario C — typed exception inside function body

A service body may raise a typed exception directly:

```python
raise OpenAIException("No prompts provided")
```

It propagates to the wrapper, which normalizes it into the right category class and fills in the full `details`. Provider methods do not need their own `try/except` — the wrapper is the single error layer.

---

## Error Handling

The single most important section for SDK users.

### Catching by category

```python
from aibridgecore.exceptions import (
    AIBridgeException,
    ValidationException,
    BadRequestException,
    AuthenticationException,
    RateLimitException,
    TimeoutException,
    ServerException,
)

try:
    response = OpenAIService.generate(...)

except ValidationException as e:
    pass
except AuthenticationException as e:
    pass
except RateLimitException as e:
    pass
except TimeoutException as e:
    pass
except ServerException as e:
    pass
except BadRequestException as e:
    pass
except AIBridgeException as e:
    pass
```

Catch most-specific first.

### Catching everything

```python
try:
    response = OpenAIService.generate(...)
except AIBridgeException as e:
    log.error(e.developer_message)
    return {"error": e.to_dict()}, e.status_code
```

### Accessing exception data

```python
except AIBridgeException as e:
    e.status_code       # int — 400, 401, 403, 408, 429, 5xx
    e.code              # str / None — semantic error code ("rate_limit_exceeded")
    e.category          # str — error category
    e.message
    e.user_message
    e.param
    e.expected
    e.received
    e.provider
    e.retryable
    e.developer_message
    e.details           # flat dict (always present)
    e.to_dict()         # returns e.details
```

### Example outputs

**Pre-validation — temperature out of range:**

```python
try:
    OpenAIService.generate(prompts=["hi"], variation_count=1, model="gpt-4", temperature=99)
except BadRequestException as e:
    e.code         # OUT_OF_RANGE       (str — semantic error code)
    e.status_code  # 400                (int — HTTP status)
    e.param        # temperature
    e.expected     # 0.0 <= temperature <= 2.0
    e.received     # 99
    e.user_message # Please use a temperature between 0.0 and 2.0.
```

Uncaught traceback:

```text
aibridgecore.exceptions.BadRequestException: {'error': {'code': 'OUT_OF_RANGE',
'status_code': 400, 'category': 'validation_error',
'message': 'temperature must be between 0.0 and 2.0',
'user_message': 'Please use a temperature between 0.0 and 2.0.',
'param': 'temperature', 'expected': '0.0 <= temperature <= 2.0',
'received': 99, 'provider': 'openai',
'developer_message': '[openai] [OUT_OF_RANGE] param=temperature ...'}}
```

**Provider rate limit:**

```python
except RateLimitException as e:
    e.code         # rate_limit_exceeded   (str — semantic error code)
    e.status_code  # 429                   (int — HTTP status)
    e.category     # rate_limit_error
    e.retryable    # True
    e.user_message # Too many requests. Please slow down and try again shortly.
    e.provider     # openai
```

**Provider auth failure:**

```python
except AuthenticationException as e:
    e.code         # invalid_api_key       (str — semantic error code)
    e.status_code  # 401                   (int — HTTP status)
    e.category     # authentication_error
    e.user_message # Authentication failed. Please verify your API key.
```

### Serializing for an API response or log line

```python
import json

try:
    OpenAIService.generate(...)
except AIBridgeException as e:
    json.dumps(e.to_dict())        # safe over the wire
    log.error(e.developer_message)  # pre-formatted log line
```

### Retry recipe

```python
import time
from aibridgecore.exceptions import (
    AIBridgeException, RateLimitException, TimeoutException, ServerException,
)

def call_with_retry(svc, **kwargs):
    delay = 1
    for attempt in range(5):
        try:
            return svc.generate(**kwargs)
        except (RateLimitException, TimeoutException, ServerException):
            time.sleep(delay)
            delay *= 2
        except AIBridgeException:
            raise
    raise
```

### Cheat sheet — which class, when

| Class | When you'll see it |
|---|---|
| `ValidationException` | Bad input. Our validator caught it before any network call. Fix the input. |
| `BadRequestException` | Provider returned 400. Inspect `e.message`. |
| `AuthenticationException` | Provider returned 401. API key bad / missing / revoked. |
| `TimeoutException` | Provider returned 408 or the request timed out. Retry. |
| `RateLimitException` | Provider returned 429. Backoff and retry. |
| `ServerException` | Provider returned 5xx. Retry or fall back. |

---

## Provider Catalog

| Provider | Service class | Validator | File |
|---|---|---|---|
| OpenAI | `OpenAIService` | `OpenAIValidator` | `ai_services/openai_services.py` |
| Anthropic (Claude) | `AnthropicService` | `AnthropicValidator` | `ai_services/anthropic_ai.py` |
| Google Gemini | `GeminiAIService` | `GeminiValidator` | `ai_services/geminin_services.py` |
| Cohere | `CohereApi` | `CohereValidator` | `ai_services/cohere_llm.py` |
| Grok | `GrokService` | `GrokValidator` | `ai_services/grok_services.py` |
| Mistral | `MistralService` | `MistralValidator` | `ai_services/mistral_services.py` |
| Deepseek | `DeepseekService` | `DeepseekValidator` | `ai_services/deepseek_services.py` |
| Kimi (Moonshot) | `KimiService` | `KimiValidator` (TEMP_MAX=1.0) | `ai_services/kimi_services.py` |
| Alibaba (Qwen) | `AlibabaService` | `AlibabaValidator` | `ai_services/alibaba_services.py` |
| AI21 | `AI21labsText` | `Ai21Validator` | `ai_services/ai21labs_text.py` |
| Ollama (self-hosted) | `OllamaService` | `OllamaValidator` | `ai_services/ollama_services.py` |
| OpenAI Images (DALL·E) | `OpenAIImage` | not yet | `ai_services/openai_images.py` |
| Stable Diffusion | `StableDiffusion` | not yet | `ai_services/stable_diffusion_image.py` |

### Method coverage

| File | generate | stream | exec_txt | exec_fn | prompt_ctx | get_resp |
|---|---|---|---|---|---|---|
| openai_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| kimi_services | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| anthropic_ai | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| geminin_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| cohere_llm | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| grok_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| mistral_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| deepseek_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| alibaba_services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| ollama_services | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| ai21labs_text | ✓ | ✓ | — | — | ✓ | ✓ |

---

## Adding a New Provider

Four steps.

### 1. Subclass `BaseValidator`

```python
class NewProviderValidator(BaseValidator):
    PROVIDER = "newprovider"
    TEMP_MAX = 1.5
```

### 2. Register

```python
VALIDATORS = {
    ...
    "newprovider": NewProviderValidator,
}
```

### 3. Import in the service

```python
from aibridgecore.validators.provider_validator import NewProviderValidator
from aibridgecore.wrappers.provider_wrapper import provider_wrapper
from aibridgecore.exceptions import AIBridgeException, NewProviderException
```

### 4. Wrap each public method

```python
class NewProviderService(AIInterface):

    @classmethod
    @provider_wrapper(
        validator=NewProviderValidator,
        validation_method="validate_generate",
    )
    def generate(self, prompts=[], model="np-default", variation_count=1, ...):
        try:
            ...
        except AIBridgeException:
            raise
        except Exception as e:
            raise NewProviderException(e)
```

Repeat for `generate_stream`, `execute_text_prompt`, `execute_prompt_function_calling`, `get_prompt_context`, `get_response`.

---

## Web Integration (Flask / FastAPI)

### FastAPI

```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from aibridgecore.ai_services.openai_services import OpenAIService
from aibridgecore.exceptions import AIBridgeException

app = FastAPI()

@app.exception_handler(AIBridgeException)
def aibridge_handler(request, exc: AIBridgeException):
    return JSONResponse(status_code=exc.status_code, content={"error": exc.to_dict()})

@app.post("/generate")
def generate(payload: dict):
    return OpenAIService.generate(**payload)
```

### Flask

```python
from flask import Flask, jsonify, request
from aibridgecore.ai_services.openai_services import OpenAIService
from aibridgecore.exceptions import AIBridgeException

app = Flask(__name__)

@app.errorhandler(AIBridgeException)
def handle_aibridge(exc):
    return jsonify({"error": exc.to_dict()}), exc.status_code

@app.post("/generate")
def generate():
    return jsonify(OpenAIService.generate(**request.json))
```

A 429 from a provider, a 401 for a bad key, or a 400 from the validator all flow into the same handler.

---

## Testing

```bash
python -m pytest tests/test_detailed_errors.py -v
```

The test file loads modules via `importlib.util` to exercise validator + normalizer + exception layers without importing every provider SDK.

### Smoke-testing without an API key

```python
from aibridgecore.validators.provider_validator import OpenAIValidator
from aibridgecore.exceptions import BadRequestException

try:
    OpenAIValidator.validate_generate({
        "prompts": ["hello"],
        "variation_count": 1,
        "model": "gpt-4",
        "temperature": 99,
    })
except BadRequestException as e:
    assert e.code == "OUT_OF_RANGE"
    assert e.param == "temperature"
```

---

## File Map

```
aibridgecore/
├── ai_services/
│   ├── openai_services.py          OpenAIService
│   ├── kimi_services.py            KimiService
│   ├── anthropic_ai.py             AnthropicService
│   ├── geminin_services.py         GeminiAIService
│   ├── cohere_llm.py               CohereApi
│   ├── grok_services.py            GrokService
│   ├── mistral_services.py         MistralService
│   ├── deepseek_services.py        DeepseekService
│   ├── alibaba_services.py         AlibabaService
│   ├── ai21labs_text.py            AI21labsText
│   ├── ollama_services.py          OllamaService
│   ├── openai_images.py            OpenAIImage (DALL·E)
│   ├── stable_diffusion_image.py   StableDiffusion
│   ├── image_optimisaton.py        (internal helper)
│   ├── ai_services_response.py     response store
│   ├── process_mq.py               Redis queue worker
│   └── ai_abstraction.py           AIInterface base contract
│
├── validators/
│   └── provider_validator.py       BaseValidator + subclasses + VALIDATORS
│
├── wrappers/
│   └── provider_wrapper.py         @provider_wrapper + _class_for_code
│
├── error_hanldling/
│   └── error_normalizer.py         ErrorNormalizer + inline ERROR_CATALOG
│
├── output_validation/              JSON / XML / CSV parsers
├── prompts/                        stored prompts + variables
├── queue_integration/              Redis integration
├── database/                       SQL / MongoDB layer
├── constant/                       provider URLs, allowed sizes, etc.
├── image/                          image utilities
├── video/                          video generation modules
├── exceptions.py                   exception hierarchy
└── setconfig.py                    config loader
```

---

## Glossary

- **Category class** — one of `ValidationException`, `BadRequestException`, `AuthenticationException`, `TimeoutException`, `RateLimitException`, `ServerException`. What callers `except` against.
- **Provider class** — internal type like `OpenAIException`, `KimiException`. The wrapper transforms these into category classes.
- **Details dict** — flat dict built by `_raise()` or `_build_error()` and attached as `e.details`. Always present.
- **Error code (`e.code`)** — semantic string like `OUT_OF_RANGE`, `rate_limit_exceeded`, `invalid_api_key`. Independent of HTTP status.
- **HTTP status (`e.status_code`)** — numeric HTTP status (`400`, `401`, `403`, `429`, ...). Fixed per category class except `ServerException` which preserves the upstream value.
- **Category (`e.category`)** — one of `authentication_error`, `rate_limit_error`, `validation_error`, `provider_error`, `server_error`, `unknown_error`.
- **Developer message** — pre-formatted log line like `[openai] [OUT_OF_RANGE] param=temperature ...`. Render `user_message` to humans instead.
- **`_CATEGORY_CLASSES`** — tuple in `provider_wrapper.py` of category classes. The wrapper checks `isinstance(e, _CATEGORY_CLASSES)` to decide whether to transform or pass through.

---

## License

See `LICENSE`.
