# Guia OpenAI-Compatible

## Quando usar este guia

Use este documento quando voce quer apontar um cliente que fala o contrato OpenAI para o ChatAgent.

Exemplos:

- OpenClaw
- gateways internos
- clientes customizados que ja esperam `/v1/models`
- integracoes que preferem `chat/completions` ou `responses` em vez do contrato nativo do ChatAgent

## Escopo atual

Nesta primeira versao, o ChatAgent expoe:

- `GET /v1/models`
- `POST /v1/chat/completions`
- `POST /v1/responses`

Tool calling (function calling) ja esta disponivel em `POST /v1/chat/completions`.
Veja a secao [Tool calling](#tool-calling).

Ainda nao faz parte do primeiro release:

- `POST /v1/embeddings`
- tool calling em `POST /v1/responses` (so em `chat/completions`)
- tool calling nativo do modelo (a implementacao atual e por prompt; veja "Como funciona por baixo")
- multimodal

## Autenticacao

Use a API key do tenant:

```http
Authorization: Bearer SUA_API_KEY
```

Este endpoint nao aceita a API key do numero conectado.

## Modelos disponiveis

A camada compativel expoe modelos virtuais que selecionam o "cerebro" por tras:

```text
chatagent-default    -> ChatGPT (padrao, via openai-oauth)
chatagent-deepseek   -> DeepSeek web
```

Use o campo `model` para escolher o provider. `chatagent-default` mantem o
comportamento historico (ChatGPT). `chatagent-deepseek` roteia a mesma requisicao
para o DeepSeek web. O contrato de request/response e identico nos dois; muda
apenas o `model`. Isso desacopla o cliente da implementacao interna do ChatAgent.

> `chatagent-deepseek` so responde quando o tenant tem o DeepSeek habilitado no
> servidor (`DEEPSEEK_ENABLED=true` + sessao logada). Caso contrario retorna
> `503`. O DeepSeek e texto-only: tool calling continua disponivel (por prompt),
> mas geracao de imagem e audio seguem so no ChatGPT.

## Listar modelos

```http
GET /v1/models
Authorization: Bearer SUA_API_KEY
```

## Chat Completions

```http
POST /v1/chat/completions
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
```

Body minimo:

```json
{
  "model": "chatagent-default",
  "user": "crm-user-42",
  "messages": [
    { "role": "system", "content": "Priorize objetividade." },
    { "role": "user", "content": "Quero um orcamento" }
  ]
}
```

## Responses

```http
POST /v1/responses
Authorization: Bearer SUA_API_KEY
Content-Type: application/json
```

Body minimo:

```json
{
  "model": "chatagent-default",
  "user": "crm-user-42",
  "input": "Preciso remarcar meu horario"
}
```

## Tool calling

A camada compativel aceita o contrato de tool calling da OpenAI em
`POST /v1/chat/completions`. Voce define as ferramentas no campo `tools`, o
ChatAgent decide quando chama-las e devolve `tool_calls` com `finish_reason:
"tool_calls"`. Seu cliente executa a ferramenta de verdade e manda o resultado
de volta, fechando o loop.

> O `tool_choice` da OpenAI ainda nao e' respeitado: o modelo decide sozinho
> quando chamar uma ferramenta. Tool calling vale so para `chat/completions`,
> nao para `responses`.

### Passo 1 - enviar a pergunta com as tools

```json
{
  "model": "chatagent-default",
  "user": "crm-user-42",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Retorna o clima atual de uma cidade",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "Nome da cidade" }
          },
          "required": ["city"]
        }
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "Como esta o clima em Recife?" }
  ]
}
```

### Passo 2 - o ChatAgent pede a ferramenta

Quando o modelo decide chamar a tool, a resposta vem assim:

```json
{
  "object": "chat.completion",
  "model": "chatagent-default",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_ab12cd34",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Recife\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}
```

### Passo 3 - executar a tool e devolver o resultado

Seu codigo roda a funcao e reenvia o historico, agora com a mensagem
`role: "tool"` contendo o resultado. Mantenha o mesmo `user` para reaproveitar a
thread:

```json
{
  "model": "chatagent-default",
  "user": "crm-user-42",
  "tools": [ /* as mesmas tools do passo 1 */ ],
  "messages": [
    { "role": "user", "content": "Como esta o clima em Recife?" },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_ab12cd34",
          "type": "function",
          "function": { "name": "get_weather", "arguments": "{\"city\":\"Recife\"}" }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_ab12cd34",
      "content": "{\"temp_c\":30,\"condition\":\"ensolarado\"}"
    }
  ]
}
```

### Passo 4 - resposta final em texto

```json
{
  "object": "chat.completion",
  "model": "chatagent-default",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Em Recife esta 30 graus e ensolarado."
      },
      "finish_reason": "stop"
    }
  ]
}
```

### Loop completo em Python (SDK da OpenAI)

Como o contrato e' o mesmo da OpenAI, o SDK oficial funciona apontando o
`base_url` para o ChatAgent:

```python
from openai import OpenAI
import json

client = OpenAI(base_url="https://chatagentai.online/v1", api_key="chatag_sua_key")

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Retorna o clima atual de uma cidade",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

def get_weather(city):
    # aqui voce chamaria sua API real
    return {"temp_c": 30, "condition": "ensolarado"}

messages = [{"role": "user", "content": "Como esta o clima em Recife?"}]

resp = client.chat.completions.create(
    model="chatagent-default", user="crm-user-42", tools=tools, messages=messages,
)
msg = resp.choices[0].message

if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })
    final = client.chat.completions.create(
        model="chatagent-default", user="crm-user-42", tools=tools, messages=messages,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)
```

### Streaming com tools

Com `"stream": true`, a chamada de ferramenta chega como chunks SSE com
`delta.tool_calls` e encerra com `finish_reason: "tool_calls"`.

### Como funciona por baixo

O "cerebro" padrao do ChatAgent responde em texto livre, entao o tool calling
e' feito por prompt: o ChatAgent injeta a descricao das tools e instrui o modelo
a emitir um JSON quando quiser chamar uma ferramenta, faz o parse desse JSON e o
traduz para o formato `tool_calls` da OpenAI. Efeitos praticos:

- a confiabilidade depende de o modelo seguir o formato pedido
- se o modelo "chamar" uma tool que nao esta em `tools`, o ChatAgent trata como
  texto normal (nao inventa `tool_calls`)
- nada de execucao acontece no servidor: quem roda a ferramenta e' sempre o seu
  cliente

## Memoria

A memoria continua no ChatAgent.

Para manter contexto entre chamadas, envie um identificador estavel, de preferencia em:

- `user`

Quando o mesmo tenant chama o endpoint com o mesmo identificador estavel, o ChatAgent reaproveita a mesma thread interna.

## Override de perfil por numero

Por padrao, a camada compativel usa o perfil principal do tenant.

Se quiser responder com o perfil de um numero conectado especifico, envie:

```http
X-ChatAgent-Number-Id: 6
```

O numero precisa pertencer ao tenant autenticado.

## Streaming

`chat/completions` e `responses` aceitam:

```json
{
  "stream": true
}
```

O ChatAgent responde em `text/event-stream`.

## Erros esperados

- `401`: API key ausente ou invalida
- `403`: token de numero conectado usado no endpoint compativel
- `400`: payload invalido ou `model` invalido
- `402`: sem creditos
- `429`: rate limit

## Recomendacoes

- use `user` com um identificador estavel do seu sistema
- escolha o `model`: `chatagent-default` (ChatGPT) ou `chatagent-deepseek` (DeepSeek)
- so use `X-ChatAgent-Number-Id` quando realmente quiser trocar de perfil
- comece validando `GET /v1/models` antes de integrar o fluxo completo
- ao usar tool calling, reenvie sempre o array `tools` em todas as chamadas do loop e mantenha o mesmo `user` para preservar a thread
