> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Session Affinity

> Mantenha todos os turnos de uma conversa no modelo que o Smart Router escolheu no primeiro turno, para que o cache de prompt do provedor seja preservado e os turnos seguintes dispensem a pontuação no caminho da requisição.

Um [**Smart Router**](/docs/pt-BR/ai-gateway/smart-router.md) escolhe um modelo por requisição. Em uma conversa com vários turnos, isso significa que cada turno é pontuado novamente e pode ir para um modelo diferente, o que reinicia o cache de prompt do provedor. A **Session affinity** fixa uma conversa no modelo escolhido no primeiro turno. Os turnos seguintes reutilizam esse modelo sem chamada de pontuação no caminho da requisição e mantêm o cache aquecido.

A session affinity vem desativada por padrão e é configurada por router. Só participam as requisições que levam um ID de thread ou, no endpoint Responses, um ID de conversa. Todo o resto continua sendo roteado por requisição, como antes.

## Casos de uso

* Assistentes de chat e copilots em que turnos consecutivos compartilham um prompt de sistema ou um conjunto de ferramentas longo e se beneficiam do cache de prompt do provedor.
* Loops de agentes que chamam o router várias vezes por turno do usuário e não devem trocar de modelo no meio do caminho.
* Conversas de suporte em que o tom e o nível de detalhe devem permanecer consistentes da primeira à última resposta.

## Habilitar a Session Affinity

### Na interface

1. Abra o **Smart Router** no **AI Gateway** e edite-o, ou crie um novo.
2. Em **Session affinity**, ative **Pin conversations to one model**.
3. Defina **Pin for** como a janela de inatividade: 1, 5, 10, 30 ou 60 minutos. O padrão é 10 minutos.
4. Clique em **Save changes**, ou em **Create** no caso de um router novo.

### Pela API

Envie `session_affinity` ao criar ou atualizar um **Smart Router**. `ttl_seconds` é a janela de inatividade em segundos, entre 60 e 3600, e o padrão é 600 quando omitido. Na atualização, o objeto é substituído por inteiro; omitir `ttl_seconds` redefine a janela para o padrão de 600.

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH https://router.toqan.ai/eu/v2/smart-routers/$SMART_ROUTER_ID \
  -H "Authorization: Bearer $TOQAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_affinity": { "enabled": true, "ttl_seconds": 600 }
  }'
```

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(
  `https://router.toqan.ai/eu/v2/smart-routers/${process.env.SMART_ROUTER_ID}`,
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.TOQAN_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      session_affinity: { enabled: true, ttl_seconds: 600 },
    }),
  },
);

console.log(await response.json());
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import requests

response = requests.patch(
    f"https://router.toqan.ai/eu/v2/smart-routers/{os.environ['SMART_ROUTER_ID']}",
    headers={"Authorization": f"Bearer {os.environ['TOQAN_API_KEY']}"},
    json={"session_affinity": {"enabled": True, "ttl_seconds": 600}},
)

print(response.json())
```

O mesmo objeto é aceito em `POST /v2/smart-routers`. Consulte a [referência da API de Smart Routers](/docs/pt-BR/reference/smart-routers/create-a-smart-router.md).

## Definir a origem da fixação

A fixação é indexada por um ID enviado por quem faz a chamada. Nada é derivado do conteúdo das mensagens.

| Endpoint | Origem | Como enviar |
| - | - | - |
| `/v3/router/chat/completions` | ID de thread | `thread.id` no corpo ou o header `X-TOQAN-THREAD-ID` |
| `/v3/anthropic/v1/messages` | ID de thread | `thread.id` no corpo ou o header `X-TOQAN-THREAD-ID` |
| `/v3/router/responses` | ID de thread; se ausente, ID de conversa | `thread.id` ou `X-TOQAN-THREAD-ID`; `conversation.id` quando nenhum ID de thread é enviado |
| `/v2/deployments/invoke` | ID de thread | `thread.id` no corpo ou o header `X-TOQAN-THREAD-ID` |
| **Agents** | ID de thread; se ausente, ID de conversa | Igual ao endpoint Responses |

Quando ambos estão presentes, o corpo prevalece sobre o header. Um ID de thread e um ID de conversa com o mesmo valor geram duas fixações independentes. Consulte [Request Metadata](/docs/pt-BR/ai-gateway/request-metadata.md) para o campo thread e os headers.

## Início rápido

Chame o **Smart Router** duas vezes com o mesmo ID de thread. O primeiro turno é pontuado e retorna `X-Orq-Session-Affinity: miss`; o segundo turno reutiliza o modelo fixado e retorna `hit`.

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
for turn in "What does the refund policy say about digital goods?" "And for physical goods?"; do
  curl -s -D - -o /dev/null -X POST https://router.toqan.ai/eu/v3/router/chat/completions \
    -H "Authorization: Bearer $TOQAN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"model\": \"workspace_name@orq/my-smart-router\",
      \"messages\": [{ \"role\": \"user\", \"content\": \"$turn\" }],
      \"thread\": { \"id\": \"conversation-abc123\" }
    }" | grep -i x-orq-session-affinity
done
```

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.TOQAN_API_KEY,
  baseURL: "https://router.toqan.ai/eu/v3/router",
});

const thread = { id: "conversation-abc123" };

for (const content of [
  "What does the refund policy say about digital goods?",
  "And for physical goods?",
]) {
  const { data, response } = await client.chat.completions
    .create({
      model: "workspace_name@orq/my-smart-router",
      messages: [{ role: "user", content }],
      // @ts-expect-error Toqan Router extension
      thread,
    })
    .withResponse();

  console.log(response.headers.get("x-orq-session-affinity"), data.model);
}
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from openai import OpenAI
import os

client = OpenAI(
    api_key=os.environ.get("TOQAN_API_KEY"),
    base_url="https://router.toqan.ai/eu/v3/router",
)

for content in [
    "What does the refund policy say about digital goods?",
    "And for physical goods?",
]:
    raw = client.chat.completions.with_raw_response.create(
        model="workspace_name@orq/my-smart-router",
        messages=[{"role": "user", "content": content}],
        extra_body={"thread": {"id": "conversation-abc123"}},
    )
    print(raw.headers.get("x-orq-session-affinity"), raw.parse().model)
```

O campo `model` na resposta indica o modelo concreto nos dois turnos.

## Como a fixação é decidida

O primeiro turno de uma thread é pontuado e roteado normalmente. A fixação só é gravada depois que a primeira chamada ao provedor desse turno é bem-sucedida, então um primeiro turno com falha ou repetido nunca fixa um modelo. Um turno que o avaliador não conseguiu classificar com confiança é atendido, mas não fixado; o turno seguinte tenta novamente.

Em um hit, a requisição vai direto para o modelo fixado, sem chamada de pontuação no caminho da requisição, e a janela de inatividade é renovada. Hits **Easy** e **Medium** ainda disparam uma chamada de pontuação em segundo plano para o [escalonamento](#escalation); hits **Hard** não disparam nenhuma.

## Escalonamento

Uma thread que começa fácil pode se tornar difícil. Em todo hit com uma fixação **Easy** ou **Medium**, o router pontua o turno novamente em segundo plano. Quando a nova pontuação cai em uma faixa superior, a fixação sobe para o modelo dessa faixa e permanece lá. Uma fixação nunca desce: um rebaixamento reiniciaria o cache do provedor em troca de uma economia pequena. Fixações **Hard** nunca são pontuadas novamente.

O escalonamento mantém o tempo de vida restante da fixação, de modo que dois turnos concorrentes na mesma thread não conseguem fazer a decisão retroceder.

## Tempo de vida da fixação

A janela de inatividade é deslizante. Cada hit a renova, então uma conversa ativa mantém seu modelo enquanto permanecer ativa, e uma thread que fica inativa por mais tempo que a janela é roteada do zero no turno seguinte. Não há limite absoluto.

Uma fixação é descartada, e a requisição passa a ser roteada por requisição, quando:

* O modelo fixado falha na primeira chamada ao provedor de um turno.
* O modelo fixado não está mais no pool do router ou não pode mais ser chamado no workspace.
* A fixação armazenada não pode ser interpretada.

Quando o armazenamento das fixações não pode ser lido, a fixação é ignorada naquele turno e o turno é roteado do zero. Uma fixação descartada ou ignorada nunca faz a requisição falhar.

## Onde se aplica

| Superfície | Session affinity | Header `X-Orq-Session-Affinity` |
| - | - | - |
| `/v3/router/chat/completions` | Sim | `hit` ou `miss` |
| `/v3/anthropic/v1/messages` | Sim | `hit` ou `miss` |
| `/v3/router/responses` | Sim | `hit` ou `miss` |
| `/v2/deployments/invoke` (JSON e streaming) | Sim, quando o modelo da variante é um **Smart Router** | `hit` ou `miss` |
| **Agents** cujo modelo principal é um **Smart Router** | Sim | Não definido |
| Respostas em segundo plano (`background: true`) | Apenas lê uma fixação existente | `hit` ou `miss` |

Nos endpoints Responses e Messages, um turno que executa um loop de ferramentas define a fixação apenas na primeira chamada ao provedor. Subagentes fazem a fixação sob seu próprio router e sua própria chave. Execuções agendadas de agentes não levam ID de thread nem de conversa e são roteadas por requisição.

## Observabilidade

Toda requisição roteada registra a decisão em seu span:

| Atributo | Significado |
| - | - |
| `orq.session_affinity.enabled` | A session affinity está ativada para este router |
| `orq.session_affinity.seed` | `thread` ou `conversation` |
| `orq.session_affinity.hit` | O turno reutilizou uma fixação |
| `orq.session_affinity.ttl_seconds` | A janela de inatividade do router |
| `orq.session_affinity.escalated` | A fixação subiu neste turno |
| `orq.session_affinity.escalated_from`, `orq.session_affinity.escalated_to` | As faixas envolvidas |

Os atributos de escalonamento são registrados em um span `auto-router.escalation` separado, no mesmo trace, porque a nova pontuação é executada depois que a resposta é enviada.

> [!NOTE]
>
> Uma fixação guarda o nome de um modelo, uma faixa e um timestamp. Nenhum conteúdo de mensagem ou dado de usuário é armazenado com ela.

## Limitações

| Limitação | Impacto | Alternativa |
| - | - | - |
| Exige um ID de thread ou ID de conversa | Requisições sem nenhum dos dois são roteadas por requisição | Envie `thread.id`, o header `X-TOQAN-THREAD-ID` ou `conversation.id` no endpoint Responses |
| As fixações são por router | Um [load balancer](/docs/pt-BR/ai-gateway/features/load-balancing.md) na frente de vários **Smart Routers** redistribui as requisições a cada turno | Ative a session affinity em um único **Smart Router** e chame-o diretamente |
| As fixações são por chave de API e identidade | A mesma thread chamada com outra chave de API ou identidade recebe sua própria fixação | Mantenha uma única chave de API e identidade por conversa |
| Respostas em segundo plano | Requisições `background: true` reutilizam uma fixação, mas nunca gravam nem descartam uma | Inicie a thread com uma requisição em primeiro plano |
| **Agents** não retornam o header | A decisão fica visível no span, não na resposta | Filtre os traces por `orq.session_affinity.hit` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.