Como integrar o Jev sem ficar preso à TypeSafe (e poder trocar de IA de decisão depois)

diagrama mostrando como integrar o Jev usando um adaptador desacoplado da TypeSafe
Resposta rápida

Pra integrar o Jev sem ficar preso à TypeSafe, isole a decisão atrás de uma interface sua (ex: DecisionProvider com decide(estado, perguntas)), defina tipos de resposta neutros (escolha, nota e probabilidade de sim) e guarde os casos de teste versionados no seu próprio repositório. O adaptador do Jev chama POST https://api.typesafe.ai/v1/systemone com model, state e questions e traduz o mapa answers pros seus tipos. Com isso, trocar pela Decisions API da OpenAI (ou o contrário) vira um adaptador novo e uma variável de ambiente, não um refactor

O erro não é escolher o Jev

O erro é espalhar o formato dele pelo código inteiro 😛

Fala aí, beleza? Se liga no contexto: o Jev é o modelo de decisão da TypeSafe AI, apresentado em 15 de setembro de 2026 como o primeiro modelo público da linha System One

Ele não é um chatbot: recebe um contexto e perguntas tipadas e devolve valores estruturados (uma escolha, uma nota ou uma probabilidade), nunca texto livre

Se tu ainda tá decidindo se faz sentido sair do prompt de chat, vale ler sobre Jev vs LLM de chat antes de seguir

Agora o ponto: duas semanas depois do Jev, a OpenAI anunciou a Decisions API no DevDay, em 29 de setembro de 2026

E já apareceram outros modelos de decisão, como o CLM-8B (Stanford e NVIDIA) e o Strands Decider 2B (AWS), segundo este levantamento sobre modelos de decisão

Ou seja, o mercado tá mudando MUITO rápido

Então faz todo sentido integrar o Jev de um jeito que te deixe trocar de fornecedor depois sem destroçar o projeto por completo haha

Bora ver na prática?

O que você precisa antes de começar

Primeiro, um jeito de chamar o Jev. Tem três caminhos documentados:

O que importa entender agora é outra coisa…

Por que um wrapper de chat não serve pro Jev?

O Jev é chamado por um endpoint próprio de avaliação, fora do padrão /chat/completions

Então aquela camada genérica de "chat com qualquer LLM" que tu já tem no projeto não encaixa aqui

A entrada é diferente (estado + perguntas) e a saída também (valores tipados por pergunta)

Por isso a gente vai criar uma camada nova, pensada pra decisão =)

Passo 1: defina a interface de decisão do seu domínio

Antes de escrever qualquer fetch, define o contrato que o resto da aplicação vai enxergar

Pensa assim: se você conhece o padrão de repository pra banco de dados, é bem semelhante

O código de negócio pede "me dá essa decisão" e não faz ideia se quem responde é o Jev, a OpenAI ou um modelo rodando local

// decisao/contrato.ts

export type Estado = {
  texto?: string;
  json?: unknown;
  imagens?: string[]; // nem todo fornecedor aceita
};

export type Pergunta =
  | { id: string; tipo: "escolha"; texto: string; opcoes: string[] }
  | { id: string; tipo: "nota"; texto: string; niveis: string[] }
  | { id: string; tipo: "probabilidade_sim"; texto: string };

export interface DecisionProvider {
  readonly nome: string;
  readonly aceitaImagem: boolean;
  decide(estado: Estado, perguntas: Pergunta[]): Promise<Record<string, Resposta>>;
}

Repara que os nomes são SEUS: escolha, nota, probabilidade_sim

O Resposta vem no próximo passo

Erro comum deste passo:

Copiar os nomes do Jev pra interface (Noul, state, questions e etc)

Parece inofensivo, mas aí o seu código de negócio já nasce falando o dialeto da TypeSafe, e qualquer troca de fornecedor passa a mexer em tudo

Passo 2: padronize o formato de resposta com tipos neutros

O Jev tem três tipos de pergunta:

  • Choice: escolhe entre opções nomeadas e devolve as probabilidades de cada opção e a confiança
  • Score: usa uma rubrica ordenada e devolve nota ponderada por probabilidade, legenda de níveis, distribuição e confiança
  • Noul: devolve um número de 0 a 1 que é a probabilidade de "sim"

A ideia é ter um tipo interno pra cada um desses, sem herdar o nome de ninguém:

// decisao/contrato.ts (continuação)

export type Escolha = {
  tipo: "escolha";
  opcao: string;
  probabilidades: Record<string, number>;
  confianca: number;
};

export type Nota = {
  tipo: "nota";
  valor: number; // nota ponderada por probabilidade
  distribuicao: Record<string, number>;
  confianca: number;
};

export type ProbabilidadeSim = {
  tipo: "probabilidade_sim";
  probabilidade: number; // 0 a 1, já é a chance de "sim"
};

export type Resposta = Escolha | Nota | ProbabilidadeSim;

E por que tanto cuidado com nome?

Porque essa armadilha é real: o Jev chama esse último tipo de Noul e a OpenAI chama o mesmo conceito de predicate

Mesma coisa, dois nomes diferentes. Se o seu código usa Noul em todo canto, trocar pra OpenAI vira caça ao tesouro 😀

Erro comum deste passo:

Tratar o valor do Noul como confiança e inventar um campo confianca extra no ProbabilidadeSim

No Noul, o número de 0 a 1 JÁ é a probabilidade de "sim", não existe um campo de confiança separado

Se tu cria um campo que o fornecedor não manda, alguém vai preencher com valor fake e a lógica de negócio vai decidir em cima disso… Tome cuidado!

Passo 3: escreva o adaptador do Jev

Agora sim, a parte que fala com a TypeSafe

A requisição tem três campos obrigatórios no nível superior: model, state e questions

O state é a informação a avaliar (texto ou JSON) e o questions é o que decidir

A resposta traz o identificador do modelo, os campos de uso (usage) e um mapa answers indexado pelos mesmos ids usados em questions

Se liga no código: ele usa só os campos confirmados na documentação

A tradução campo a campo de cada pergunta e de cada resposta mora num arquivo separado, o jev-schema.ts, e o adaptador só cuida do transporte e da correspondência por id

// decisao/jev.ts
import { DecisionProvider, Estado, Pergunta, Resposta } from "./contrato";
import { montarPerguntasJev, traduzirRespostaJev } from "./jev-schema";

const JEV_URL = "https://api.typesafe.ai/v1/systemone";

export class JevProvider implements DecisionProvider {
  readonly nome = "jev";
  readonly aceitaImagem = false; // state é texto ou JSON

  constructor(private apiKey: string, private modelo: string) {}

  async decide(estado: Estado, perguntas: Pergunta[]) {
    if (estado.imagens?.length) {
      throw new Error("JevProvider não recebe imagem: envie o estado em texto ou JSON");
    }

    const res = await fetch(JEV_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: this.modelo,
        state: estado.json ?? estado.texto,
        questions: montarPerguntasJev(perguntas), // ids iguais aos seus
      }),
    });

    if (!res.ok) throw new Error(`Jev respondeu ${res.status}`);

    const data = await res.json(); // { model, answers, usage }

    const saida: Record<string, Resposta> = {};
    for (const p of perguntas) {
      const bruta = data.answers?.[p.id];
      if (bruta === undefined) {
        throw new Error(`Jev não devolveu resposta para "${p.id}"`);
      }
      saida[p.id] = traduzirRespostaJev(p, bruta);
    }
    return saida;
  }
}

E o que faz o jev-schema.ts?

Ele é o ÚNICO arquivo do projeto que conhece o schema da TypeSafe: o montarPerguntasJev transforma cada Pergunta numa pergunta Choice, Score ou Noul no formato de questions da documentação da API da TypeSafe, e o traduzirRespostaJev faz o caminho de volta, de cada item de answers pra Escolha, Nota ou ProbabilidadeSim

No caso do Noul a tradução é a mais simples de todas: o número de 0 a 1 vai direto pro campo probabilidade, sem inventar confiança nenhuma

Se a TypeSafe mudar o schema amanhã, só esse arquivo muda 🙂

O valor do model vem por parâmetro de propósito: assim tu não espalha id de modelo pelo código e confere o id certo na doc do canal que estiver usando

E o usage? Vale logar, porque é ele que te deixa comparar custo entre fornecedores depois

Erro comum deste passo:

Perder a correspondência entre os ids de questions e as chaves de answers

Quem itera as respostas "na ordem" em vez de buscar pelo id acaba misturando a resposta de uma pergunta com outra

Por isso o loop acima percorre as SUAS perguntas e busca cada uma pelo id, estourando erro se faltar alguma

Passo 4: deixe o segundo fornecedor pronto (mesmo sem usar)

Esse passo parece exagero, mas é ele que prova que a interface tá certa

O candidato óbvio é a Decisions API da OpenAI:

  • Beta público aberto a todos os devs, anunciado em 6 de outubro de 2026
  • Endpoint POST /v1/decisions
  • Só suporta o modelo gpt-6-luna
  • Aceita texto e imagem como entrada
  • Devolve três formatos: predicate (probabilidade entre 0 e 1), choice (uma opção de uma lista fixa) e score (média ponderada por probabilidade de níveis ordenados)

O esboço fica assim:

// decisao/openai-decisions.ts
import { DecisionProvider, Estado, Pergunta, Resposta } from "./contrato";

// a parte que muda de fornecedor pra fornecedor: o nome de cada tipo
const TIPO_OPENAI: Record<Pergunta["tipo"], string> = {
  escolha: "choice",
  nota: "score",
  probabilidade_sim: "predicate", // o mesmo conceito do Noul no Jev
};

export class OpenAIDecisionsProvider implements DecisionProvider {
  readonly nome = "openai-decisions";
  readonly aceitaImagem = true; // texto e imagem
  readonly modelo = "gpt-6-luna"; // único modelo suportado

  async decide(estado: Estado, perguntas: Pergunta[]): Promise<Record<string, Resposta>> {
    // POST /v1/decisions
    // payload e nomes de campos: conferir na doc oficial da OpenAI antes de implementar
    throw new Error("OpenAIDecisionsProvider ainda não implementado");
  }
}

Não escrevi o payload de propósito: o formato exato de requisição e resposta tu confere na doc oficial da OpenAI na hora de implementar

O ponto aqui é ver que a classe encaixa na MESMA interface e que predicate vira o mesmo ProbabilidadeSim que o Noul vira no adaptador do Jev

O resto da aplicação nem fica sabendo 🙂

Erro comum deste passo:

Assumir que os dois aceitam a mesma entrada

A OpenAI aceita imagem e o state do Jev é texto ou JSON

Por isso a interface tem o aceitaImagem: ou o seu fluxo prevê imagem e escolhe um fornecedor que aceita, ou o adaptador recusa de forma explícita (como o JevProvider faz), nunca descarta a imagem em silêncio

Passo 5: mantenha os casos de teste no seu repositório

Aqui mora a parte que mais gente esquece

Sem casos de teste, trocar de fornecedor é no feeling: "parece que tá decidindo igual" haha

Guarda os casos num arquivo versionado do próprio projeto, com estado, perguntas e resposta esperada (ou faixa aceitável):

[
  {
    "id": "ticket-reembolso-01",
    "estado": { "texto": "Cliente pede reembolso logo após a compra, produto não foi usado" },
    "perguntas": [
      { "id": "categoria", "tipo": "escolha", "texto": "Qual a categoria do ticket?", "opcoes": ["reembolso", "suporte_tecnico", "outro"] },
      { "id": "urgente", "tipo": "probabilidade_sim", "texto": "O ticket é urgente?" }
    ],
    "esperado": {
      "categoria": { "opcao": "reembolso" },
      "urgente": { "min": 0, "max": 0.4 }
    }
  }
]

Os valores do exemplo são ilustrativos: a faixa aceitável quem define é você, conforme o risco da decisão no seu produto

E um runner simples que roda contra QUALQUER adaptador:

// decisao/avaliar.ts
import casos from "./casos/decisoes.json";
import { DecisionProvider } from "./contrato";

export async function avaliar(provider: DecisionProvider) {
  let falhas = 0;

  for (const caso of casos as any[]) {
    const respostas = await provider.decide(caso.estado, caso.perguntas);

    for (const [id, esperado] of Object.entries<any>(caso.esperado)) {
      const r = respostas[id];
      const ok =
        r.tipo === "escolha" ? r.opcao === esperado.opcao :
        r.tipo === "nota" ? r.valor >= esperado.min && r.valor <= esperado.max :
        r.probabilidade >= esperado.min && r.probabilidade <= esperado.max;

      if (!ok) {
        falhas++;
        console.log(`[${provider.nome}] ${caso.id}/${id} falhou`, r);
      }
    }
  }

  return falhas;
}

Pra quem quer uma ferramenta de evals em cima disso, o Langfuse publicou um guia de uso do Jev para evals em 18 de setembro de 2026

É uma opção massa, mas a fonte da verdade continua sendo o arquivo no seu repo

Erro comum deste passo:

Deixar o dataset só dentro de uma ferramenta externa

Aí a troca de fornecedor (ou de ferramenta de eval) vira migração de dados, e o que era uma tarde vira uma sprint…

Passo 6: escolha o fornecedor por configuração

Último passo: o fornecedor sai de uma variável de ambiente e é montado num lugar só, uma factory

// decisao/factory.ts
import { DecisionProvider } from "./contrato";
import { JevProvider } from "./jev";
import { OpenAIDecisionsProvider } from "./openai-decisions";

export function criarProvider(): DecisionProvider {
  switch (process.env.DECISION_PROVIDER) {
    case "jev":
      return new JevProvider(process.env.TYPESAFE_API_KEY!, process.env.JEV_MODEL!);
    case "openai":
      return new OpenAIDecisionsProvider();
    default:
      throw new Error(`DECISION_PROVIDER inválido: ${process.env.DECISION_PROVIDER}`);
  }
}

Antes de virar a chave em produção, roda o avaliar() do passo 5 com os dois valores de DECISION_PROVIDER e compara as falhas

E sobre velocidade: a TypeSafe afirma que o Jev conclui decisões entre 70 e 500 milissegundos

Isso é dado da própria empresa, então mede no SEU caso (com o seu estado, as suas perguntas e a sua rede) antes de montar timeout ou SLA em cima desse número

Erro comum deste passo:

Espalhar if (fornecedor === "jev") pela aplicação

Cada if desses é um pedacinho do fornecedor vazando de volta pro código de negócio, ou seja, tudo que a gente evitou nos passos anteriores

Troca de fornecedor tem que ser: um adaptador novo, um case na factory e uma variável de ambiente. Só isso

Jev x Decisions API da OpenAI: o que o seu adaptador precisa traduzir

Essa tabela não é pra dizer qual é melhor, é pra mostrar o que cada adaptador traduz e qual tipo interno recebe isso:

Item Jev (TypeSafe) Decisions API (OpenAI) Tipo interno sugerido
Endpoint POST https://api.typesafe.ai/v1/systemone POST /v1/decisions DecisionProvider.decide()
Modelo typesafe/jev-1.13 (no OpenRouter) gpt-6-luna (único suportado) Configuração do adaptador
Entrada state em texto ou JSON + questions Texto e imagem Estado (com aceitaImagem)
Escolha entre opções Choice choice Escolha
Nota em níveis ordenados Score score Nota
Probabilidade de sim (0 a 1) Noul predicate ProbabilidadeSim
Preço de entrada US$ 0,042 por milhão de tokens US$ 0,10 por milhão de tokens Log de usage por fornecedor
Preço de saída Grátis Grátis (não precisa rastrear)
Disponibilidade API própria, OpenRouter, LiteLLM pass-through Beta público Variável DECISION_PROVIDER

Repara na linha do Noul x predicate: é literalmente o motivo de existir o passo 2 😀

Quando essa arquitetura vale o esforço (e quando é exagero)

Nem todo projeto precisa de interface, dois adaptadores e suíte de casos, né?

Vale a pena quando:

  • O produto está em produção e depende da decisão: se o Jev decide algo que afeta usuário real, conseguir trocar de fornecedor rápido é seguro de vida
  • Você quer comparar custo entre fornecedores: com US$ 0,042 (preço do Jev no OpenRouter) contra US$ 0,10 (anúncio da Decisions API) por milhão de tokens de entrada (saída grátis nos dois), a diferença em volume alto justifica medir, e medir só é justo com a mesma suíte de testes
  • A equipe quer testar outros modelos depois: CLM-8B, Strands Decider 2B e o que mais aparecer viram só mais um adaptador

Se tu ainda nem sabe por onde começar dentro do produto, vale pensar em qual decisão migrar primeiro antes de montar toda essa estrutura

É exagero quando:

  • Protótipo descartável: se o código vai pro lixo em uma semana, chama o Jev direto e segue a vida
  • Script pontual: classificar uma planilha uma vez não precisa de factory nem de adaptador reserva

O critério é simples: se trocar de fornecedor um dia vai doer, a interface vale. Se não vai, é next, next e finish haha

Próximo passo: comece pela interface, não pelo fornecedor

Recapitulando o que faz a escolha do modelo de decisão ser reversível:

  • Uma interface sua (DecisionProvider), escrita em termos do seu negócio
  • Tipos de resposta neutros (Escolha, Nota, ProbabilidadeSim), sem Noul nem predicate no código de negócio
  • Casos de teste versionados no seu repositório, rodando contra qualquer adaptador
  • Um adaptador por fornecedor e uma factory escolhendo por configuração

O próximo passo concreto é bem pequeno: escreve a interface e cinco casos de teste do seu domínio ANTES de qualquer chamada ao Jev

Depois pluga o adaptador e roda a suíte

O mercado de modelos de decisão acabou de nascer, e em poucas semanas já tem Jev, Decisions API e modelos menores aparecendo

Nesse cenário, manter a porta aberta é a decisão mais barata que tu pode tomar agora =)

Até o próximo post!

Perguntas frequentes

Como integrar o Jev sem travar meu código no formato da TypeSafe?

Criando uma camada própria com tipos neutros (escolha, nota, probabilidade_sim) entre o seu código de negócio e o adaptador do Jev. O adaptador conversa com o formato da TypeSafe (state, questions, answers), mas o resto da aplicação nunca vê esses nomes. Assim, se precisar trocar de fornecedor, só reescreve o adaptador.

É possível usar o Jev pelo OpenRouter em vez da API própria da TypeSafe?

Sim, o Jev está disponível no OpenRouter com o ID typesafe/jev-1.13, pra qualquer pessoa com chave de API do OpenRouter. Nesse caso a cobrança acontece na conta do OpenRouter, e não direto na TypeSafe. A configuração específica fica na página de documentação do Jev no próprio OpenRouter.

Qual a diferença entre o Noul do Jev e o predicate da Decisions API da OpenAI?

Nenhuma na prática: são o mesmo conceito com nomes diferentes. No Jev, o Noul devolve um número de 0 a 1 que já é a probabilidade de ‘sim’. Na OpenAI, esse formato se chama predicate. É exatamente esse tipo de divergência de nome que a interface neutra do passo 2 evita propagar pelo código.

Preciso de um wrapper de chat genérico pra chamar o Jev?

Não, e tentar usar um wrapper de chat aqui só complica as coisas. O Jev é chamado por um endpoint próprio de avaliação (POST para /v1/systemone), fora do padrão /chat/completions. A entrada também é diferente: em vez de mensagens, são os campos state e questions.

Quanto custa usar o Jev comparado com a Decisions API da OpenAI?

Segundo a página do Jev no OpenRouter, ele cobra US$ 0,042 por milhão de tokens de entrada, com tokens de saída grátis. Pelo anúncio da Decisions API da OpenAI, ela cobra US$ 0,10 por milhão de tokens de entrada, também com saída grátis. A diferença de preço de entrada é relevante se o seu volume de chamadas de decisão for alto.

Dá pra trocar o Jev por outro modelo de decisão depois, como o CLM-8B ou o Strands Decider?

Dá, e é justamente pra isso que serve a interface de domínio do passo 1. Depois do Jev já surgiram outros modelos de decisão, como o CLM-8B (Stanford e NVIDIA) e o Strands Decider 2B (AWS), conforme o levantamento sobre modelos de decisão citado no post. Se o seu código de negócio só conhece os tipos neutros, basta escrever um novo adaptador pra esses modelos sem tocar no resto do projeto.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares