# Arquitetura TIA Regional com Pool xAI Pré-Aquecido

## 1. Introdução

Este documento descreve a evolução do TIA para operar TTS xAI em alta disponibilidade e alta volumetria usando réplicas Kubernetes distribuídas por região. A solução parte da implementação atual do TIA/LiveKit e mantém seu contrato de TTS, suas métricas de underflow/TTFB e sua lógica de proteção contra repetição após áudio parcial.

A mudança principal é mover a manutenção do conjunto de conexões WebSocket xAI para um **pool persistente por Pod TIA**, implementado como sidecar. Cada Pod regional pode manter até `XAI_POOL_SIZE` conexões xAI pré-aquecidas e prontas para uso. O TIA continua enxergando um endpoint compatível com xAI, porém local (`127.0.0.1`).

## 2. Dificuldade do modelo anterior

Na versão anterior, cada instância `OraclexAITTS` mantinha uma única conexão reutilizável. Como o LiveKit executa chamadas em processos de job, essas conexões são naturalmente distribuídas por chamadas/processos e não formam um pool global do Pod.

Isso cria alguns riscos em alta volumetria:

1. **Burst de handshakes WebSocket.** Muitas chamadas podem abrir conexões ao xAI praticamente ao mesmo tempo.
2. **Capacidade não compartilhada.** Uma chamada pode manter uma conexão ociosa enquanto outra precisa abrir uma nova.
3. **Ausência de backpressure no balanceador.** O LB enxerga o TIA como saudável mesmo quando a capacidade de TTS daquele Pod está totalmente ocupada.
4. **Acoplamento entre sessão e conexão xAI.** A quantidade de chamadas pode virar, indiretamente, a quantidade de conexões abertas, mesmo quando apenas uma parte das chamadas está sintetizando naquele instante.
5. **Falha regional afeta novas chamadas.** Sem readiness orientada à saúde/capacidade do xAI, novas sessões podem continuar chegando a uma réplica cuja região está degradada.

O histórico de testes do TTS já mostrou que problemas de estabelecimento de WebSocket e jitter podem se manifestar de forma regional e em função de carga. A arquitetura proposta transforma esses sinais em capacidade operacional do Pod.

## 3. Objetivos

A solução tem os seguintes objetivos:

- manter conexões OCI xAI abertas e pré-aquecidas;
- reutilizar uma conexão entre diferentes sínteses;
- reservar conexão upstream apenas durante uma utterance;
- liberar a conexão imediatamente após `audio.done`;
- impedir bursts de abertura de WebSockets no caminho crítico da chamada;
- permitir TIA ativo/ativo em múltiplas regiões;
- retirar automaticamente uma réplica saturada do balanceamento de novas conexões;
- manter chamadas existentes durante draining/rollout;
- renovar sockets antes do TTL de forma escalonada, evitando reconexão simultânea;
- preservar o protocolo atual do `OraclexAITTS` e minimizar mudanças no código de voz;
- permitir escalabilidade horizontal em Kubernetes.

## 4. Arquitetura proposta

```text
                                Clientes / Telefonia
                                        |
                                        v
                               Load Balancer / Service
                           (novas conexões WebSocket)
                                        |
                   +--------------------+--------------------+
                   |                                         |
                   v                                         v
             Deployment ORD                            Deployment IAD
                   |                                         |
          +--------+--------+                       +--------+--------+
          |   Pod TIA ORD   |                       |   Pod TIA IAD   |
          |                 |                       |                 |
          | bridge          |                       | bridge          |
          | agent/livekit   |                       | agent/livekit   |
          |       |         |                       |       |         |
          |       v         |                       |       v         |
          | xAI pool sidecar|                       | xAI pool sidecar|
          | 50 WS warm      |                       | 50 WS warm      |
          +-------+---------+                       +-------+---------+
                  |                                         |
                  v                                         v
              OCI xAI ORD                               OCI xAI IAD
```

O mesmo `Service` Kubernetes seleciona Pods ORD e IAD. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, mas ambos compartilham `app=${APP_NAME}-regional`.

## 5. Componentes

### 5.1 Bridge TIA

Mantém o WebSocket de entrada e o comportamento existente do TIA. O código principal não precisa conhecer o endpoint xAI regional.

### 5.2 LiveKit Agent

Continua instanciando `OraclexAITTS`, porém passa a usar:

```text
XAI_WEBSOCKET_URL=ws://127.0.0.1:18100/xai/v1/tts
```

O adapter continua enviando:

```text
text.clear
text.delta
text.done
```

e recebendo:

```text
audio.clear
audio.delta
audio.done
```

Portanto, a lógica atual de streaming, TTFB, gaps, underflow e prevenção de replay permanece válida.

### 5.3 Sidecar `xai-pool`

Implementado em:

```text
src/app/livekit/adapters/xai_pool_proxy.py
```

Responsabilidades:

- abrir `XAI_POOL_SIZE` WebSockets no startup;
- usar abertura em ondas controladas (`XAI_POOL_PREWARM_CONCURRENCY`);
- manter os sockets vivos;
- recuperar automaticamente slots desconectados;
- renovar sockets antes do TTL;
- aplicar jitter no refresh para não reconectar todos simultaneamente;
- emprestar uma conexão a uma utterance;
- devolver a conexão ao pool depois de `audio.done`;
- expor health, readiness, status, drain e métricas.

### 5.4 OCI xAI regional

Cada deployment recebe seu endpoint próprio por `XAI_POOL_UPSTREAM_URL`.

Exemplo:

```text
ORD -> wss://...us-chicago-1.../xai/v1/tts
IAD -> wss://...us-ashburn-1.../xai/v1/tts
```

As credenciais reais ficam somente no sidecar de pool. O container `agent` usa uma credencial local dummy porque se conecta apenas a `localhost`.

### 5.5 Kubernetes Service / Load Balancer

O Service seleciona todas as réplicas regionais. O WebSocket funciona normalmente através do LB: o balanceador escolhe um Pod durante o HTTP Upgrade e aquela conexão permanece no mesmo backend durante sua vida.

A estratégia de capacidade não tenta migrar uma conexão existente. Ela afeta **novas conexões**.

## 6. Pool compartilhado por Pod

Uma chamada não reserva um socket xAI por toda sua duração.

```text
Call A falando ---------- sem TTS
Call B ouvindo ---------- sem nova síntese
Call C precisa falar ---- acquire WS #17
                          text.clear
                          text.delta
                          text.done
                          audio.delta...
                          audio.done
                          release WS #17
```

Portanto, 200 chamadas podem coexistir em um Pod com pool de 50, desde que não existam mais de 50 sínteses concorrentes naquele instante.

Essa é a principal diferença entre dimensionar por **chamadas simultâneas** e por **utterances TTS simultâneas**.

## 7. Readiness orientada à capacidade

O sidecar expõe:

```text
GET /healthz
GET /readyz
GET /pool/status
GET /metrics
POST /drain
```

`/healthz` responde se o processo está vivo.

`/readyz` responde se o Pod deve aceitar **novas chamadas**.

Exemplo padrão:

```text
XAI_POOL_SIZE=50
XAI_POOL_UNAVAILABLE_FREE=2
XAI_POOL_RECOVER_FREE=5
```

Com o Pod inicialmente pronto:

```text
free > 2   -> ready
free <= 2  -> not ready (HTTP 503)
```

Depois de sair da rotação, só retorna quando:

```text
free >= 5 -> ready novamente
```

Isso cria histerese e evita flapping de readiness em torno do limite.

Para a política estrita sugerida de somente sair quando todas estiverem ocupadas:

```text
XAI_POOL_UNAVAILABLE_FREE=0
XAI_POOL_RECOVER_FREE=5
```

Em produção recomenda-se uma pequena reserva (por exemplo 2 a 5 conexões), pois ela absorve rajadas e reduz a chance de uma sessão recém-chegada não encontrar capacidade.

## 8. Como o LB redireciona tráfego

Em Kubernetes, um Pod é Ready apenas quando todos os containers que possuem readiness probe estão Ready.

O sidecar `xai-pool` possui uma probe em `/readyz`.

Quando a capacidade acaba:

```text
xai-pool /readyz -> 503
          |
          v
Pod becomes NotReady
          |
          v
Pod removed from Service Endpoints
          |
          v
LB stops sending NEW connections
```

As conexões WebSocket já estabelecidas não são redirecionadas e continuam no Pod enquanto o processo continuar disponível.

## 9. Renovação escalonada das conexões

Manter um pool grande de sockets aberto indefinidamente sem renovação é arriscado porque serviços upstream normalmente aplicam TTL e renovação de autorização.

A configuração padrão usa:

```text
XAI_POOL_CONNECTION_TTL_S=540
XAI_POOL_REFRESH_JITTER_S=45
```

Cada conexão recebe um refresh deadline diferente:

```text
WS01 -> ~501s
WS02 -> ~527s
WS03 -> ~509s
...
```

Somente conexões livres são renovadas. Isso evita que todo o pool expire e faça handshake simultaneamente.

## 10. Barge-in e reutilização segura do pool

Barge-in ocorre quando o usuário interrompe a fala do TTS antes de `audio.done`. A versão corrigida não descarta automaticamente um WebSocket saudável nem permite que áudio residual contamine a próxima síntese.

Fluxo:

```text
usuário interrompe
      |
      v
TIA cancela a utterance e envia text.clear
      |
      v
sidecar cancela imediatamente o relay de audio.delta
      |
      v
envia text.clear ao xAI
      |
      v
drena e DESCARTA audio.delta/audio.done residuais
      |
      v
recebe audio.clear
      |
      +--> confirmado: devolve audio.clear ao TIA e libera o WS saudável ao pool
      |
      `--> timeout/erro: fecha o WS; maintenance cria substituto pré-aquecido
```

Enquanto aguarda `audio.clear`, nenhum frame residual é encaminhado ao LiveKit. Isso cria um boundary limpo entre a utterance cancelada e a próxima.

Configuração:

```text
XAI_POOL_BARGE_IN_CLEAR_TIMEOUT_S=1.0
```

Métricas específicas:

```text
tia_xai_pool_barge_ins_total
tia_xai_pool_barge_in_reuses_total
tia_xai_pool_barge_in_resets_total
tia_xai_pool_barge_in_discarded_messages_total
```

## 11. Alta disponibilidade regional

Operação normal:

```text
LB
|-- ORD Pod 1 -> ready
|-- ORD Pod 2 -> ready
|-- IAD Pod 1 -> ready
`-- IAD Pod 2 -> ready
```

Se ORD perder saúde/capacidade xAI, os slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods ORD saem dos endpoints e novas chamadas passam a ser atendidas pelos Pods IAD disponíveis.

Não há necessidade de alterar o cliente ou o LiveKit para escolher a região.

## 12. Escala horizontal

A capacidade teórica de pool é:

```text
capacidade regional de sockets = replicas_region * XAI_POOL_SIZE
```

Exemplo:

```text
ORD: 2 pods x 25 sockets = 50
IAD: 2 pods x 25 sockets = 50
TOTAL = 100 sockets prewarmed
```

ou, se a OCI conceder capacidade independente suficiente:

```text
ORD: 2 pods x 50 = 100
IAD: 2 pods x 50 = 100
TOTAL = 200
```

### Capacidade efetivamente provisionada

`XAI_POOL_SIZE` é **somente o tamanho configurado do pool por réplica TIA**; não representa um limite público do OCI/xAI. Neste ambiente, a capacidade foi negociada diretamente com xAI/OCI e pode ser muito superior aos exemplos de 50 conexões usados neste documento.

O dimensionamento correto é:

```text
conexões pré-aquecidas da região = réplicas_region * XAI_POOL_SIZE
```

e deve ser comparado com a **capacidade efetivamente negociada/provisionada** para aquela região/endpoint. Exemplos com 25/50 existem apenas para facilitar a leitura da arquitetura.

## 13. Escala visual

```text
Carga baixa
=========
LB
 |-- ORD-1 [pool 50: 10 leased / 40 free]
 `-- IAD-1 [pool 50:  8 leased / 42 free]

Carga aumenta
=============
LB
 |-- ORD-1 [47 leased / 3 free] READY
 `-- IAD-1 [30 leased /20 free] READY

ORD satura
===========
LB
 |-- ORD-1 [48 leased /2 free] NOT READY -> sem novas chamadas
 `-- IAD-1 [31 leased /19 free] READY     -> recebe novas chamadas

ORD recupera
=============
ORD-1 chega a 45 leased /5 free
/readyz volta a 200
LB volta a considerá-lo para novas conexões
```

## 13. Draining e rollout

O deployment usa:

```yaml
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1
```

O sidecar executa no `preStop`:

```text
POST /drain
```

Isso torna `/readyz` imediatamente 503, removendo o Pod da entrada de novas conexões antes de sua finalização.

`terminationGracePeriodSeconds` deve ser compatível com a duração/grace desejada para as chamadas existentes.

## 14. Segurança

As credenciais OCI/xAI reais ficam no Secret indicado por `XAI_SECRET_NAME` e são montadas apenas no sidecar.

O agent conecta a localhost e não precisa conhecer a chave real do xAI.

Para produção recomenda-se evoluir para `OKE_WORKLOAD_IDENTITY` sempre que suportado pela política do ambiente, eliminando API keys estáticas.

## 15. Observabilidade

`GET /pool/status` retorna:

```json
{
  "status": "ready",
  "region": "ord",
  "configured": 50,
  "healthy": 50,
  "leased": 17,
  "free": 33,
  "total_acquires": 845,
  "total_acquire_timeouts": 0,
  "total_proxy_failures": 0
}
```

`GET /metrics` expõe métricas Prometheus simples:

- `tia_xai_pool_connections{state="healthy"}`
- `tia_xai_pool_connections{state="leased"}`
- `tia_xai_pool_connections{state="free"}`
- `tia_xai_pool_acquires_total`
- `tia_xai_pool_acquire_timeouts_total`
- `tia_xai_pool_proxy_failures_total`

Estas métricas devem ser correlacionadas com as métricas já existentes no TIA, especialmente TTFB, gap e underflow.

## 16. O que esta versão resolve

| Problema | Solução |
|---|---|
| handshakes xAI no caminho crítico | pool pré-aquecido |
| burst de WebSockets | prewarm em ondas + refresh com jitter |
| conexão presa a uma chamada | lease somente durante utterance |
| LB envia tráfego a Pod sem capacidade TTS | `/readyz` baseado no pool |
| flapping de health | histerese unavailable/recover |
| indisponibilidade regional | Deployments ORD/IAD no mesmo Service |
| rollout derruba novas sessões | drain + RollingUpdate |
| chave xAI em todos os processos | credencial real somente no sidecar |
| observabilidade de capacidade | `/pool/status` + `/metrics` |

## 17. O que esta versão não resolve sozinha

A solução não cria capacidade de inferência no OCI xAI. Se todos os endpoints regionais terminarem no mesmo pool de inferência saturado, o TIA terá failover e melhor utilização de sockets, mas não multiplicará a capacidade real do modelo.

É necessário confirmar com a OCI:

1. limite de WebSockets por endpoint/região/tenancy;
2. se endpoints dedicados possuem capacidade independente;
3. se API servers/LB e inference workers são dedicados ou compartilhados;
4. quais limites podem ser reservados/negociados para a TIM.
