1

JEV na prática: decisões tipadas em FastAPI pelo Vercel AI Gateway

English summary: A production-minded walkthrough of using TypeSafe AI's JEV through Vercel AI Gateway to triage support tickets in a FastAPI vertical slice. It covers typed Choice, Score, and Noul questions, graceful degradation, validation, evals, cost, security, rollback, and the gap between a working proof of concept and a system you can trust.

Publicado: setembro de 2026
Tempo de leitura: 18 min
Palavras-chave: JEV, TypeSafe AI, Vercel AI Gateway, FastAPI, decisões tipadas, vertical slice, Python, evals


Um ticket diz que o checkout parou, há clientes esperando e o assunto veio marcado como “dúvida”. Seu backend precisa decidir três coisas: isso é urgente, qual time assume e qual o nível de prioridade.

Pedir a um LLM para “analisar o ticket e responder em JSON” funciona na demo. Em produção, a pergunta importante vem depois: quem garante que o departamento existe, que a probabilidade está no intervalo certo e que uma indisponibilidade do modelo não impede o ticket de ser criado?

Foi esse problema que eu usei para testar o JEV, modelo da TypeSafe AI voltado a decisões estruturadas, por meio do Vercel AI Gateway. A implementação entrou em uma fatia vertical FastAPI real: router, service, repository, contrato de avaliação e testes de degradação. Foi uma POC funcional feita durante algumas horas de trabalho, não um benchmark de produção nem uma promessa de acurácia.

Minha conclusão é específica: JEV é interessante quando seu software precisa de um julgamento estreito e probabilístico, mas a aplicação deve continuar dona do contrato, da política e do fallback. Tipar a resposta reduz uma classe de erros. Não transforma uma inferência em verdade.

TL;DR

  • JEV recebe um state e perguntas tipadas; devolve respostas que o código consegue consumir sem extrair texto livre.
  • Os três primitivos documentados são choice, score e noul. Noul representa a probabilidade de uma resposta “sim”.
  • No meu fluxo, uma chamada decide urgência, departamento e nível de urgência para um ticket.
  • O Vercel AI Gateway expõe JEV como typesafe-ai/jev no endpoint /v1/evaluate.
  • A cota gratuita documentada em 19 de setembro de 2026 era de US$ 5 por mês para cada time no free tier. Ela começa na primeira requisição; comprar créditos move o time para o tier pago e remove o crédito mensal. Isso é uma condição comercial mutável, não “API grátis para sempre”.
  • Timeout, erro HTTP, resposta inválida ou baixa confiança precisam levar a fallback ou revisão humana, não a uma ação silenciosa.
  • O exemplo mínimo abaixo ensina a integração. A seção de produção mostra o que ainda falta antes de colocar decisões no caminho crítico.

O que JEV é, e o que ele não é

A TypeSafe chama JEV de seu primeiro modelo “System One”: um modelo para decisões rápidas e estruturadas. Em vez de pedir prosa e tentar recuperar estrutura depois, você envia o estado do problema e perguntas com tipos conhecidos.

Os primitivos são:

TipoPergunta que resolveResposta útil ao código
choiceQual opção definida se aplica?opção escolhida, probabilidades por opção e confiança
scoreEm qual nível ordenado o caso cai?score, probabilidades por nível e confiança
noulQual a probabilidade de “sim”?número entre 0 e 1

O nome incomum noul importa porque copiar um contrato como boolean quebraria a integração. No Gateway, a página do modelo descreve capacidades como Choice, Score e Boolean; na API de avaliação e na documentação TypeSafe, o tipo usado no payload é noul. O contrato da API vence o rótulo de marketing.

JEV não é:

  • uma regra de negócio;
  • um mecanismo de autorização;
  • uma garantia de calibração no seu domínio;
  • um substituto automático para validação semântica;
  • a melhor escolha quando um if, regex, enum ou consulta ao banco resolve o caso de forma determinística.

Use código comum para fatos. Considere um modelo de decisão quando o input é ambíguo e a saída possível já é conhecida.

Rode o núcleo em poucos minutos

Este é um exemplo didático, adaptado da minha implementação. Requer Python 3.11+, httpx e uma chave do Vercel AI Gateway.

python -m venv .venv
source .venv/bin/activate
pip install "httpx>=0.27,<1"
export AI_GATEWAY_API_KEY="sua-chave"

Crie jev_demo.py:

import os
from typing import Any

import httpx

GATEWAY_URL = "https://ai-gateway.vercel.sh"
MODEL = "typesafe-ai/jev"

QUESTIONS = {
    "is_urgent": {
        "type": "noul",
        "instructions": "Does this ticket require immediate attention?",
    },
    "department": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {
            "billing": "Payments, invoices, refunds, or charges",
            "technical": "Bugs, outages, integrations, or product errors",
            "shipping": "Delivery, tracking, or logistics",
            "general": "Anything outside the other departments",
        },
    },
    "urgency": {
        "type": "score",
        "instructions": "How urgent is this ticket?",
        "criteria": ["low", "medium", "high"],
    },
}


def evaluate_ticket(subject: str, message: str) -> dict[str, Any]:
    api_key = os.environ["AI_GATEWAY_API_KEY"]
    payload = {
        "model": MODEL,
        "state": {"subject": subject, "message": message},
        "questions": QUESTIONS,
    }

    with httpx.Client(timeout=10.0) as client:
        response = client.post(
            f"{GATEWAY_URL}/v1/evaluate",
            headers={"Authorization": f"Bearer {api_key}"},
            json=payload,
        )
        response.raise_for_status()
        return response.json()["answers"]


if __name__ == "__main__":
    answers = evaluate_ticket(
        subject="Checkout fora do ar",
        message="Pagamentos falham há 20 minutos; 42 pedidos estão bloqueados.",
    )
    print(answers)

Execute:

python jev_demo.py

O formato exato inclui metadados definidos pelo serviço, mas o consumo relevante segue este desenho:

{
  "is_urgent": {"noul": 0.97},
  "department": {
    "choice": "technical",
    "probabilities": {
      "billing": 0.01,
      "technical": 0.96,
      "shipping": 0.0,
      "general": 0.03
    }
  },
  "urgency": {"score": 2.8}
}

Valores acima são ilustrativos, não output prometido. O ganho estrutural está em conhecer os campos possíveis antes da requisição. Seu código ainda deve validar a resposta real.

Intuição: modelo propõe, política dispõe

Pense no JEV como um sensor. Um sensor mede e carrega incerteza; ele não decide sozinho se uma esteira industrial deve parar. O controlador lê a medição, verifica limites, considera outras condições e escolhe a ação segura.

Aqui, o modelo produz julgamento:

ticket → {urgente: 0.97, departamento: technical, score: 2.8}

A aplicação produz política:

if result.is_urgent >= 0.90 and result.confidence >= 0.80:
    page_on_call()
elif result.is_urgent >= 0.60:
    enqueue_human_review()
else:
    follow_normal_queue()

Os cortes pertencem ao seu domínio. Eles não devem ficar escondidos em prompt, resposta do modelo ou regra do provedor.

Minha fatia vertical em FastAPI

Na PR privada do POC FastAPI, implementei um CRUD de tickets no mesmo padrão vertical já usado pelo projeto. O link registra a origem do estudo, mas o conteúdo não é acessível sem permissão. Por isso, os trechos úteis e sanitizados estão reproduzidos aqui; nenhuma credencial ou configuração privada foi copiada.

flowchart LR
    C[Cliente HTTP] --> R[Ticket Router]
    R --> S[Ticket Service]
    S --> J[JevClient]
    J --> G[Vercel AI Gateway]
    G --> M[typesafe-ai/jev]
    S --> P[Ticket Repository]
    P --> D[(MongoDB)]
    J -. timeout ou erro .-> F[Fallback sem avaliação]
    F --> P

O JevClient é um adapter fino. Ele conhece URL, autenticação, timeout e envelope HTTP. O service conhece o caso de uso e decide o que fazer quando o adapter falha.

from typing import Any

import httpx


class JevEvaluationError(RuntimeError):
    pass


class JevClient:
    def __init__(
        self,
        *,
        api_key: str,
        model: str = "typesafe-ai/jev",
        base_url: str = "https://ai-gateway.vercel.sh",
        timeout: float = 10.0,
    ) -> None:
        self.api_key = api_key
        self.model = model
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    def evaluate(self, state: Any, questions: dict[str, Any]) -> dict[str, Any]:
        try:
            with httpx.Client(timeout=self.timeout) as client:
                response = client.post(
                    f"{self.base_url}/v1/evaluate",
                    headers={"Authorization": f"Bearer {self.api_key}"},
                    json={
                        "model": self.model,
                        "state": state,
                        "questions": questions,
                    },
                )
                response.raise_for_status()
                body = response.json()
        except (httpx.HTTPError, ValueError, KeyError) as error:
            raise JevEvaluationError("evaluation unavailable") from error

        answers = body.get("answers")
        if not isinstance(answers, dict):
            raise JevEvaluationError("invalid evaluation contract")
        return answers

O fluxo do POST /tickets preserva a função principal mesmo sem IA:

sequenceDiagram
    actor User as Cliente
    participant API as FastAPI Router
    participant Service as Ticket Service
    participant Jev as JevClient
    participant Gateway as Vercel AI Gateway
    participant DB as Repository

    User->>API: POST /tickets
    API->>Service: create(subject, message)
    Service->>Jev: evaluate(state, questions)
    Jev->>Gateway: POST /v1/evaluate
    alt resposta válida
        Gateway-->>Jev: answers tipadas
        Jev-->>Service: answers
        Service->>DB: save(ticket + evaluation)
    else timeout, HTTP error ou contrato inválido
        Gateway--xJev: falha
        Jev-->>Service: JevEvaluationError
        Service->>DB: save(ticket + evaluation null)
    end
    DB-->>API: ticket persistido
    API-->>User: 201 Created

Esse detalhe mudou o risco do experimento. JEV enriquece o ticket; não possui a criação. Em falha, o dado principal continua entrando no sistema e pode ser triado manualmente.

Contratos e ownership

Uma integração confiável precisa responder “quem é dono?” antes de responder “qual modelo?”.

ResponsabilidadeDonoEvidênciaReversão
esquema das perguntastime do domíniocontrato versionado e revisão de códigovoltar à versão anterior
chamada ao Gatewayadapter de infraestruturamétricas de latência/status e testes HTTP fakedesligar provider via flag
validação da respostaboundary da aplicaçãoerros de schema e fixtures canônicasencaminhar para fallback
cortes e açãoservice/policydecisão registrada com versão da policymodo shadow ou advisory
segredo do Gatewayplataforma/secret managerauditoria de acesso e rotaçãorevogar chave
qualidade do modeloowner do produtoeval por segmento e revisão de errosregra determinística/manual

O modelo nunca deve receber permissão implícita para executar uma ação sensível. Ele fornece evidência para uma policy que o código controla.

Do brinquedo para produção

O script mínimo ensina endpoint e payload. Ele omite pontos que importam sob carga ou incidente:

  1. Validação estrutural e semântica. Verifique IDs esperados, tipo de cada resposta, opções permitidas, limites [0, 1], soma das probabilidades e faixa do score. Pydantic ajuda, mas tipos válidos não provam decisão correta.
  2. Cliente reutilizável. Não abra uma conexão por request em carga alta. Injete httpx.AsyncClient com lifecycle controlado pelo FastAPI.
  3. Timeout e orçamento de retry. Retry cego pode duplicar custo e piorar congestionamento. Restrinja a erros transitórios, use backoff com jitter e mantenha deadline total.
  4. Idempotência. Persistência e avaliação podem terminar em ordens diferentes. Defina chave idempotente, status pending/evaluated/failed ou outbox se reprocessamento for necessário.
  5. Observabilidade sem vazar dados. Registre request ID, versão do contrato, modelo, latência, status, rota tomada e custo. Não grave ticket completo, bearer token ou PII por conveniência.
  6. Concorrência e escala horizontal. Estado de retry, locks e fila não podem morar na memória de uma única instância. Workers devem conseguir reprocessar por ID, com claim atômico e deduplicação.
  7. Versionamento. Guarde versão da pergunta, policy e modelo junto à decisão. typesafe-ai/jev é o ID do Gateway; a TypeSafe documentava jev-latest como alias de jev-1.13.0 em 19 de setembro de 2026. Aliases facilitam upgrades, mas reduzem reprodutibilidade.

Seis falhas que aparecem depois da demo

1. O Gateway está indisponível

Timeouts, 429, 5xx e falhas de rede acontecem. Se triagem é enriquecimento, salve o ticket com evaluation: null, emita métrica e reprocese depois. Se a decisão protege uma ação irreversível, falhe fechado ou exija humano.

2. A resposta passa no JSON, mas viola o contrato

department="sales" não deve entrar se o enum só aceita quatro times. Uma probabilidade 1.2 também não. Valide antes de persistir ou agir. Resposta inválida é falha de integração, não “melhor esforço”.

3. Confiança alta, decisão errada

Confiança não substitui acurácia. Meça falsos negativos de urgência e erros por idioma, produto e tipo de cliente. Um corte global pode esconder desempenho ruim num segmento pequeno.

4. O input contém instrução maliciosa

Texto do ticket é dado não confiável. A superfície é menor do que em geração com tools, mas prompt injection ainda pode influenciar classificação. Não inclua segredos no estado; mantenha ações fora do modelo; teste entradas adversariais.

5. Custo muda sem mudança de código

Em 19 de setembro de 2026, a página de pricing do Gateway documentava US$ 5 mensais para times no free tier, enquanto superfícies do catálogo mostravam JEV como “Free” e o detalhe do modelo exibia US$ 0,04 por milhão. Essa divergência é motivo para tratar preço como snapshot, medir uso real e configurar alertas. Comprar créditos remove o crédito mensal gratuito daquele time, segundo a documentação vigente.

6. Um alias muda o comportamento

Atualização de modelo pode alterar distribuição das respostas sem quebrar schema. Rode eval antes de promover nova versão. Guarde versão efetiva quando o provedor a expuser e mantenha rollback para modelo ou policy anterior.

Adoção incremental: observe antes de automatizar

Automação não precisa nascer com permissão de agir.

ModoComportamentoCritério para avançar
offlineroda eval em dataset históricobaseline e taxonomia aceitos
shadowavalia tráfego real, sem afetar rotacobertura, latência e erros conhecidos
advisorymostra sugestão ao operadorconcordância e override medidos
enforcepolicy executa ações limitadasSLO, cortes e rollback aprovados

Eu comecei com dependency injection. Nos testes FastAPI, get_jev_client é substituído por um fake determinístico e por outro que lança erro. Isso cobre caminho feliz e degradação sem consumir API ou depender de credencial real.

A mesma arquitetura apareceu em duas PRs públicas do meu hub de skills:

  • PR #38 — decision-native routing: adicionou modos heuristic, shadow, advisory e enforce, além de um envelope tipado para decisões entre estágios. Merge em 19 de setembro de 2026.
  • PR #40 — typed decision engineering: tornou o padrão independente de provedor, com schemas, validação semântica, composição determinística, roteamento por incerteza e benchmark de custo/tokens. Merge em 19 de setembro de 2026.

O ponto não é colocar JEV em todo lugar. É separar julgamento, contrato e autoridade de execução para poder trocar o provider sem redesenhar o domínio.

O mesmo padrão dentro do meu harness

Depois da POC FastAPI, levei a ideia para o harness que uso no dia a dia. O caso está documentado em duas PRs abertas, não em um exemplo inventado:

O PR do core inclui typecheck, 849 testes, 2.879 assertions, 50 testes focados e smoke live com cinco casos. O benchmark local mediu média de 0,070 ms antes e 72,177 ms depois; o smoke remoto, uma execução por caso, mediu média de 536,641 ms. São números da POC, datados de 20 de setembro de 2026, não SLO nem benchmark de produção. O smoke observou respostas tipadas e fallbacks; isso prova integração exercitada, não qualidade estatística suficiente para automatizar decisões críticas.

flowchart TD
    Q[Pedido do usuário] --> R[Auto Dispatch]
    R --> H{Modo JEV}
    H -->|offline| B[Builtin One determinístico]
    H -->|shadow| E[Avalia, não troca rota]
    H -->|advisory| J[JEV via Vercel AI Gateway]
    H -->|enforce| J
    J --> G{Resposta válida + gates}
    G -->|sim| D[Decisão candidata]
    G -->|não| B
    D --> P[Policy local]
    P --> F[Provider/model/runtime final]
    D --> T[RouteTrace + RPC]
    T --> A[App macOS: proveniência visível]

Human-in-the-loop como contrato, não como botão

O paradigma muda quando a incerteza vira parte do protocolo. O modelo não recebe autoridade para “escolher o próximo agente”; ele propõe uma decisão entre candidatos que o código já conhece. A policy local aplica dois gates no modo padrão do PR: confidence >= 0.65 e probability >= 0.55. Resposta inválida, baixa confiança, timeout ou erro remoto caem para Builtin One.

type JevGate = {
  mode: "shadow" | "advisory" | "enforce";
  confidenceFloor: number;
  probabilityFloor: number;
};

const defaultGate: JevGate = {
  mode: "advisory",
  confidenceFloor: 0.65,
  probabilityFloor: 0.55,
};

shadow mede sem alterar rota. advisory mostra sugestão para inspeção. enforce permite aplicar policy somente depois de evals e rollback aprovados. Cancelamento do caller aborta a avaliação sem inventar fallback; o erro remoto chega ao restante do sistema como classificação estável, não como stack trace de provider.

Essa separação preserva controle humano em três pontos: o conjunto de candidatos, os limites de confiança e a ação final. O ciclo real é JEV → sugestão → operador vê proveniência → aceita ou faz override → decisão fica auditável. A PR do App implementa visibilidade de proveniência, não um workflow completo de aprovação humana. A UI macOS recebe route.jev opcional; payloads legados continuam decodificando quando o core ainda não envia esse campo. Credencial, prompt bruto e decisão de provider permanecem no core. A interface apenas torna a proveniência auditável; o readiness router continua soberano.

O ganho não é “tirar humano do loop”. É tirar julgamento implícito do loop: quando o sistema está confiante e a policy permite, ele avança; quando não está, entrega evidência suficiente para uma pessoa revisar.

Eval: o que medir antes de confiar

Carregando publicação patrocinada...