Metadata-Version: 2.4
Name: zibloh-ai
Version: 0.2.0
Summary: SDK Python para el router de IA de Zibloh: un endpoint (/request) y tiers basic/medium/premium/eu.
Author: Milmoh
License-Expression: Apache-2.0
Project-URL: Homepage, https://zibloh.com
Keywords: zibloh,ai,llm,openai,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# zibloh-ai

SDK Python del **router de IA de Zibloh**. Tiers públicos `basic` / `medium` / `premium` / `eu`.

Zibloh tiene **un solo endpoint de trabajo**, `POST /request`, para chat, embeddings y OCR: el SDK es un cliente fino sobre `httpx` con un método por endpoint. El cuerpo y la respuesta son OpenAI-compatibles, pero **el SDK oficial de `openai` no sirve** contra Zibloh porque apunta a `/chat/completions` y `/embeddings`, que no existen aquí.

## Instalación

```bash
pip install zibloh-ai
```

## Uso

```python
from zibloh_ai import Zibloh

client = Zibloh(api_key="zbl_...")   # o variable de entorno ZIBLOH_API_KEY

resp = client.request(
    model="premium",                 # basic | medium | premium | eu
    messages=[{"role": "user", "content": "Hola, ¿qué tal?"}],
)
print(resp["choices"][0]["message"]["content"])
```

Las respuestas son `dict` con la forma de OpenAI. Cualquier parámetro extra
(`temperature`, `max_tokens`, `tools`, `response_format`…) se pasa tal cual.

### Streaming

```python
for chunk in client.stream(model="medium", messages=[{"role": "user", "content": "Un chiste"}]):
    print(chunk["choices"][0]["delta"].get("content", ""), end="")
```

Un corte a mitad de stream se convierte en `ZiblohError` en vez de pasar inadvertido.

### Embeddings

Mismo método, mismo endpoint: cambia el cuerpo.

```python
emb = client.request(model="basic", input="texto a vectorizar")
print(emb["data"][0]["embedding"][:5])
```

Tiers disponibles en embeddings: `basic` y `eu`.

### OCR de documentos

Mismo método, mismo endpoint: pasa `document` en vez de `messages`. Acepta una URL o un data URI
base64 (para un fichero local, sin multipart):

```python
import base64, pathlib

pdf = base64.b64encode(pathlib.Path("factura.pdf").read_bytes()).decode()
res = client.request(
    model="eu",
    document={"type": "document_url", "document_url": f"data:application/pdf;base64,{pdf}"},
)
print(res["pages"][0]["markdown"])
print(res["usage_info"]["pages_processed"])   # hojas que se te facturan
```

**OCR y extracción de campos en una sola llamada**: con `document_annotation_format` (un JSON
schema) y/o `document_annotation_prompt`, la respuesta trae además `document_annotation` con los
campos ya extraídos, sin un segundo paso de chat.

```python
res = client.request(
    model="eu",
    document={"type": "document_url", "document_url": "https://…/factura.pdf"},
    document_annotation_prompt="Extrae número de factura, NIF del emisor y total.",
    table_format="markdown",
)
```

Límites: **50 MB** por fichero (más → 413) y **1000 páginas**; PDF/PPTX/DOCX e imágenes
PNG/JPEG/AVIF. No hay subida multipart: el fichero va en el JSON. **Para documentos grandes, pasa
una URL prefirmada de S3** en vez de base64 — el fichero no atraviesa el router y evitas tanto el
33 % que infla el base64 como el timeout de 60 s del edge.

Solo el tier **`eu`** hace OCR (Mistral). Pedirlo en otro tier da un 400 explicándolo, y **no hay
fallback**: si el proveedor europeo falla, el error llega en claro en vez de sacar el documento de
la UE en silencio. Aquí las hojas **no** se declaran: se cobran las que reporte el proveedor.

### Documentos en chat (PDF e imágenes)

Pasa los ficheros en `files`: el SDK los adjunta al último mensaje `user` y el gateway los manda a
cada proveedor en **su formato nativo** (el mismo código vale para todos los tiers):

```python
resp = client.request(
    model="premium",
    messages=[{"role": "user", "content": "Extrae los datos de la factura en JSON"}],
    files=["factura.pdf", "anexo.png"],          # rutas, bytes o content parts ya montados
    response_format={"type": "json_schema", "json_schema": {"name": "factura", "schema": SCHEMA}},
)
```

- Formatos: **PDF, PNG, JPEG, WebP, GIF** (el tipo se detecta por los bytes, no por la extensión).
- **TIFF no**: ningún proveedor lo acepta. Conviértelo antes a PNG (una imagen por página).
- Las hojas se cuentan solas (páginas reales del PDF, 1 por imagen): no hace falta `zibloh_pages`.
- Respuestas largas (muchas líneas, modelos que razonan): `request()` ya pide el chat en
  streaming por debajo y te devuelve el `dict` completo, así que no le afecta el corte del
  edge a los 60 s. No hace falta cambiar nada.
- Para montar el mensaje a mano: `zibloh_ai.file_part("factura.pdf")` devuelve el content part.

### Hojas de documento (cuota)

Si el texto sale de un documento y la petición **no** es de OCR (p. ej. mandas texto ya extraído a
chat), declara las hojas para el metering de Zibloh:

```python
client.request(model="eu", messages=[...], zibloh_pages=12)
```

### Errores

```python
from zibloh_ai import ZiblohError

try:
    client.request(model="eu", messages=[...])
except ZiblohError as e:
    e.status_code   # 429 = cuota agotada o proveedor saturado; 502/503 = caído
    e.retry_after   # segundos que pide el router, si los manda
```

### Asíncrono

```python
import asyncio
from zibloh_ai import AsyncZibloh

async def main():
    async with AsyncZibloh(api_key="zbl_...") as client:
        r = await client.request(model="basic", messages=[{"role": "user", "content": "ping"}])
        print(r["choices"][0]["message"]["content"])

asyncio.run(main())
```

### Tiers

```python
from zibloh_ai import Models
Models.BASIC    # "basic"   — rápido y barato
Models.MEDIUM   # "medium"  — equilibrado
Models.PREMIUM  # "premium" — máxima calidad
Models.EU       # "eu"      — proveedor europeo

client.models()  # los tiers que el router expone ahora mismo
```

## Configuración

| Parámetro | Variable de entorno | Por defecto |
|---|---|---|
| `api_key` | `ZIBLOH_API_KEY` | — (obligatorio) |
| `base_url` | `ZIBLOH_BASE_URL` | `https://ai.zibloh.com/api/v1` |

> `https://ai.zibloh.com/api/v1` es la base URL **de producción** y el valor por defecto.
> Para **dev** usa `base_url="https://ai.devapp.zibloh.com/api/v1"` (o `ZIBLOH_BASE_URL`) con
> una key creada en dev: cada entorno tiene sus propias keys, y una de prod da 401 en dev.

La API Key (`zbl_...`) se crea desde el panel de Zibloh y requiere el scope `ai:chat`, `ai:embeddings` y/o `ai:ocr` (`ai:*` cubre los tres).

## Compatibilidad con el ecosistema OpenAI

El cuerpo y la respuesta tienen la forma de OpenAI, pero `Zibloh` **no** es un `openai.OpenAI`:
Zibloh solo expone `POST /request`, así que el SDK de `openai` (y las librerías que lo usan por
debajo, como LangChain o LlamaIndex) no funcionan apuntándolos a Zibloh. Usa `Zibloh` /
`AsyncZibloh`, o llama a `/request` directamente por HTTP.

## Licencia

Apache-2.0 (el texto completo va en el paquete, fichero `LICENSE`).
