Metadata-Version: 2.4
Name: ai-generator
Version: 0.6.0
Summary: Roteador de chamadas LLM por model:provider, multi-provider, com retorno consistente.
Author-email: Stamatios Stamou Jr <bushier.outsets.0c@icloud.com>
License: MIT
Keywords: llm,router,openai,gemini,anthropic,multi-provider
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24
Requires-Dist: requests>=2.28
Provides-Extra: openai
Requires-Dist: openai>=1.30; extra == "openai"
Provides-Extra: gemini
Requires-Dist: google-genai>=2.10; extra == "gemini"
Provides-Extra: claude
Requires-Dist: anthropic>=0.40; extra == "claude"
Provides-Extra: all
Requires-Dist: openai>=1.30; extra == "all"
Requires-Dist: google-genai>=2.10; extra == "all"
Requires-Dist: anthropic>=0.40; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# aigen

Roteador multi-provider para chamadas de IA com retorno consistente.

O caller escolhe explicitamente a key `model:provider`; o pacote instancia o
provider correspondente, executa a chamada e devolve um dataclass padronizado
com resposta, metadados técnicos e tokens reportados pelo provider.

Decisões de produto como tiers, créditos, planos, cobrança, limites comerciais
ou cálculo de custo não pertencem ao `aigen`. Quem chama o pacote pode usar
`total_tokens`, `input_tokens`, `output_tokens` e `reasoning_tokens` para fazer
qualquer cálculo externo.

## Instalação

SDKs de provider são extras opcionais. Instale só os que usar:

```bash
pip install "ai-generator[openai]"        # OpenAI, Groq, z.ai, Kimi, Alibaba, DeepSeek
pip install "ai-generator[gemini]"        # Google Gemini
pip install "ai-generator[claude]"        # Anthropic Claude
pip install "ai-generator[cerebras]"      # Cerebras
pip install "ai-generator[all]"           # tudo

# Desenvolvimento:
pip install -e ".[all]"
```

Providers HTTP puros funcionam só com o core (`httpx`, `requests`).

## Uso

Sempre passe `model='model:provider'`.

```python
import aigen

r = aigen.generate_text(
    'Resuma o texto X',
    model='gemini-3.8-flash:gemini',
)

print(r.content)
print(r.total_tokens)
```

Outras mídias usam o mesmo padrão:

```python
img = aigen.generate_image('um gato astronauta', model='gpt-image-2:openai')
video = aigen.generate_video('clipe contínuo de...', model='bytedance/seedance-1.5-pro:kie')
music = aigen.generate_music('lo-fi chill', model='music-3.0:minimax')
text = aigen.transcribe(audio_url, model='whisper-large-v3:groq')
audio = aigen.synthesize('Olá', model='elevenlabs/text-to-speech-turbo-2-5:kie')
vecs = aigen.generate_embeddings(['texto a'], model='text-embedding-3-large:openai')
ranked = aigen.rerank('query', ['doc a', 'doc b'], model='cohere/rerank-4-pro:openrouter')
```

### Imagem pelo Codex CLI

O comando `aigen-image` usa o login local do Codex e invoca o skill `$imagegen`.
Não requer `OPENAI_API_KEY`:

```bash
aigen-image "Um gato astronauta em Marte"
aigen-image --output assets/hero.png "Banner horizontal futurista para uma landing page"
cat prompt.txt | aigen-image --output assets/hero.png
```

O padrão é salvar em `./image.png`. O modelo disponível neste provider é
`gpt-image-2:codex_cli`.

### Imagens de entrada nos CLIs de texto

`claude_cli` e `codex_cli` aceitam imagens em `files=` como Data URI Base64
(`{'src': 'data:image/png;base64,...'}`). Qualquer anexo que não seja imagem
levanta `ProviderError`.

```python
import base64
from pathlib import Path

src = 'data:image/png;base64,' + base64.b64encode(Path('foto.png').read_bytes()).decode()
r = aigen.generate_text(
    'Descreva a imagem',
    model='gpt-6.1-sol:codex_cli',
    files=[{'src': src}],
)
```

Vídeo com first frame ou imagens de referência na fal.ai aceita `bytes`, caminho
local, URL ou Data URI. Arquivos locais e `bytes` são enviados como Data URI Base64:

```python
from pathlib import Path

first_frame = aigen.generate_video(
    'A personagem olha para a câmera',
    model='google/gemini-omni-flash/image-to-video:fal',
    first_frame_image=Path('personagem.png').read_bytes(),
)

references = aigen.generate_video(
    'Image 1 é a personagem; Image 2 define o figurino',
    model='minimax/h3-max/reference-to-video:fal',
    reference_images=[
        Path('personagem.png').read_bytes(),
        Path('figurino.jpg').read_bytes(),
    ],
)

seedance_text = aigen.generate_video(
    'Plano cinematográfico contínuo com áudio ambiente',
    model='bytedance/seedance-2.5/text-to-video:fal',
)

omni_text = aigen.generate_video(
    'Farol ao entardecer com ondas quebrando nas pedras',
    model='google/gemini-omni-flash:fal',
)
```

`first_frame_image` e `reference_images` são mutuamente exclusivos.

O Gemini Omni Flash direto retorna o MP4 em `videos[0].data`. O
`provider_task_id` permite estender o vídeo em novos trechos, até 40 segundos:

```python
clip = aigen.generate_video(
    'Plano cinematográfico de uma cidade ao amanhecer',
    model='gemini-omni-1.1-flash:gemini',
    duration=10,
    resolution='720p',
    aspect_ratio='16:9',
)

extended = aigen.generate_video(
    'Continue a cena por mais dez segundos',
    model='gemini-omni-1.1-flash:gemini',
    duration=10,
    previous_interaction_id=clip.provider_task_id,
)
```

Entry genérico:

```python
r = aigen.generate(prompt, mode='text', model='glm-5.2:zai')
r = aigen.generate(prompt, mode='video', model='bytedance/seedance-1.5-pro:kie')
r = aigen.generate(prompt, mode='video', model='bytedance/seedance-2.0-mini/text-to-video:atlascloud')
r = aigen.generate(audio_url, mode='stt', model='whisper-large-v3:groq')
```

`mode`: `text` · `image` · `video` · `music` · `stt` · `tts` · `embedding` · `rerank`.

## Tool calling

`generate_text` aceita tools em um formato neutro e mensagens OpenAI-like. O
caller continua responsável por executar as funções; o `aigen` traduz chamadas,
resultados e histórico para cada provider.

```python
import json
import aigen

TOOLS = [{
    'name': 'get_weather',
    'description': 'Consulta o clima atual de uma cidade',
    'parameters': {
        'type': 'object',
        'properties': {
            'city': {'type': 'string'},
        },
        'required': ['city'],
        'additionalProperties': False,
    },
}]


def get_weather(city):
    return {'city': city, 'temp_c': 28}


messages = [{'role': 'user', 'content': 'Como está o tempo em São Paulo?'}]

for _ in range(8):
    response = aigen.generate_text(
        model='gemini-3.8-flash:gemini',
        messages=messages,
        tools=TOOLS,
        tool_choice='auto',
    )
    messages.append(response.assistant_message)

    if not response.tool_calls:
        print(response.raw_content)
        break

    for call in response.tool_calls:
        if call.arguments is None:
            result = {
                'ok': False,
                'error': 'invalid_arguments',
                'arguments_raw': call.arguments_raw,
            }
        elif call.name == 'get_weather':
            result = {'ok': True, 'data': get_weather(**call.arguments)}
        else:
            result = {'ok': False, 'error': f'unknown_tool: {call.name}'}

        messages.append({
            'role': 'tool',
            'tool_call_id': call.id,
            'content': json.dumps(result, ensure_ascii=False),
        })
else:
    raise RuntimeError('limite de tool calls excedido')
```

### Contrato de tools

Cada tool precisa de `name` string não vazia e `parameters` como JSON Schema.
Nomes duplicados e listas vazias são rejeitados com `ProviderBadRequest`.

`tool_choice` aceita:

- `'auto'`: o modelo decide entre texto e tools;
- `'none'`: desabilita chamadas naquele request;
- `'required'`: exige ao menos uma chamada;
- `{'name': 'get_weather'}`: exige uma função específica.

`tool_choice` exige uma lista `tools` não vazia, e uma função nomeada precisa
existir nessa lista.

Cada item de `response.tool_calls` contém `id`, `name`, `arguments` e
`arguments_raw`. O parse de `arguments` é estrito: JSON inválido resulta em
`arguments=None` e nunca deve ser executado. `arguments_raw` preserva o valor
original para diagnóstico.

O modelo pode retornar várias chamadas no mesmo turno. Execute todas e acrescente
uma mensagem `role='tool'` para cada `id`; os adapters Claude e Gemini agrupam
automaticamente resultados paralelos no formato nativo esperado.

`response.assistant_message` deve ser acrescentada ao histórico sem alterações.
Ela preserva metadados opacos necessários ao replay de thinking do Claude e
thought signatures do Gemini, mas continua serializável com `json.dumps`.

### Suporte e limitações

- Suportado pela API Claude, Gemini e adapters OpenAI-compatible non-stream.
- Providers CLI (`claude_cli`, `codex_cli`) não expõem este contrato.
- Tool calling em adapters configurados com streaming ainda não é suportado.
- `files=` não pode ser combinado com `messages=` nesta versão.
- No Claude com thinking ativo, use `tool_choice='auto'` ou `'none'`; escolhas
  `'required'` ou nomeadas são rejeitadas cedo pelo `aigen` porque a API
  Anthropic não aceita essa combinação.
- Passe a mesma lista `tools` em todas as etapas do loop.

## Contrato

Sucesso retorna o dataclass do tipo. Falha levanta exception tipada.

| Mídia | Função | Retorno |
|-------|--------|---------|
| Texto | `generate_text` | `TextResponse` |
| Imagem | `generate_image` | `ProviderResponse` |
| Vídeo | `generate_video` | `VideoResponse` |
| Música | `generate_music` | `TTMResponse` |
| STT | `transcribe` | `STTResponse` |
| TTS | `synthesize` | `TTSResponse` |
| Embedding | `generate_embeddings` | `EmbeddingResponse` |
| Rerank | `rerank` | `RerankResponse` |

Texto retorna `content`, `raw_content`, `input_tokens`, `output_tokens`,
`total_tokens`, `reasoning_tokens`, `response_time`, `model`, `tool_calls`,
`finish_reason` e `assistant_message`. `finish_reason='tool_calls'` indica que o
caller deve executar as funções e continuar o loop. Outras mídias seguem o mesmo
padrão de resposta padronizada e metadados técnicos quando o provider fornece.

Áudio segue o modelo do `VideoResult` (`url` ou `data`, conforme o provider):
TTS retorna `audio: AudioResult` (`url`/`data`/`mime_type`/`duration`) e TTM
retorna `tracks: list[TrackResult]` (mais `title`, `image_url` e `lyrics`).

## Erros

```text
ProviderError
├─ ProviderAuthError
├─ ProviderBadRequest
├─ ProviderBlocked
└─ ProviderRetryable
   ├─ ProviderRateLimit
   └─ ProviderTimeout
```

Cada exception carrega `.provider`, `.model`, `.status_code`, `.response_time`
e `.raw_error`.

## Catálogo

O catálogo em `aigen/constants/{text,image,audio,video,embedding,rerank}.py` contém fatos técnicos:
providers suportados, variável de ambiente da API key, modelos conhecidos e
capabilities. O dispatch considera `capabilities.tools` quando o campo está
declarado para o modelo. O catálogo não contém preço, crédito, tier ou regra de
cobrança.

Para registrar um modelo novo de um provider já suportado:

```python
import aigen

aigen.register_model('text', 'openai', 'gpt-5.4')
```

O catálogo runtime default pode remover modelos e voltar ao estado inicial:

```python
aigen.unregister_model('text', 'openai', 'gpt-5.4')
aigen.reset_registry()
```

Para manter configurações isoladas, crie um registry próprio e passe-o às
factories de provider:

```python
registry = aigen.CatalogRegistry()
registry.register_model('text', 'openai', 'gpt-custom')
provider = aigen.get_text_provider(
    'gpt-custom:openai',
    catalog_registry=registry,
)
```

Providers novos exigem implementação de classe e registro no código do pacote.

## API keys

Cada provider lê a key de uma variável de ambiente (`api_key_env` no catálogo).
Defaults incluem `GOOGLE_API_KEY`, `OPENAI_API_KEY`, `CLAUDE_API_KEY`,
`GROQ_API_KEY`, `CEREBRAS_API_KEY`, `ZAI_API_KEY`, `MOONSHOT_AI_API_KEY`,
`DASHSCOPE_API_KEY`, `DEEPSEEK_API_KEY`, `MINIMAX_API_KEY`, `KIE_API_KEY`,
`DEAPI_API_KEY` e `ATLASCLOUD_API_KEY`.

## Logs

O pacote usa `logging.getLogger(__name__)` e não configura handler. Para ver logs,
o app habilita logging no boot:

```python
import logging
logging.basicConfig(level=logging.INFO)
```

Stats de fim de chamada podem ser desligadas com `LOG_PROVIDER_STATS=0`.
