Leíste qué es Jev. Te convenció la idea de un modelo que no genera texto, solo decide. Y sigues sin una sola llamada en tu código.
Ese es el hueco entre entender una herramienta y saber cómo usar Jev de verdad: la primera llamada es la que te enseña dónde se tropieza.
Saber qué es una herramienta y tenerla respondiendo en tu terminal son cosas distintas. Con Jev se tropieza en sitios que no esperas: un noul que no es un número, un alias de modelo que cambia sin avisarte, preguntas que funcionan peor en castellano.
Así que vamos con el caso más pequeño que tiene sentido en un proyecto real: clasificar los comentarios que llegan a un blog. Paso a paso, en orden.
En corto: para empezar con Jev crea una API key en la consola de TypeSafe, guárdala en TYPESAFE_API_KEY y haz un POST a https://api.typesafe.ai/v1/systemone con un state y un mapa de questions tipadas. Con el SDK (@typesafe-ai/sdk o typesafe-sdk) es una sola llamada. El tropiezo más común es tratar el noul como un número: es un objeto y el valor está en .noul.
Jev es un modelo de TypeSafe AI al que le mandas un texto (state) y preguntas cerradas de tres tipos (noul, choice, score), y te devuelve respuestas tipadas con probabilidades en lugar de texto libre. Si quieres la parte conceptual (RLCD, por qué no genera texto, cuándo compite con un LLM), está en qué es Jev y por qué sus probabilidades están calibradas. Aquí vamos a lo práctico.
El caso: triaje de comentarios del blog
A un blog de developers llegan preguntas técnicas, feedback y spam. Solo algunos piden respuesta del autor, y alguno trae un tono que conviene revisar. Tres decisiones, una por primitiva:
kind(choice): pregunta técnica, feedback o spam.needs_reply(noul): ¿necesita respuesta del autor?tone(score): de amable a hostil.
Paso 1: pruébalo sin código en el Playground
Antes de instalar nada, abre el Playground e inicia sesión. Pega un comentario como state:
Muy buen post. Una duda: en el ejemplo del interceptor, ¿por qué usas inject() fuera del constructor? En mi proyecto con Angular 17 me da error.
Añade una pregunta noul:
{
"needs_reply": {
"type": "noul",
"instructions": "Does this comment need a reply from the author of the post?"
}
}
Después añade un choice y un score y mira las tres respuestas juntas.
Paso 2: la API key, en una variable de entorno
Crea la key en console.typesafe.ai/keys y guárdala como variable de entorno. Los SDK la leen de TYPESAFE_API_KEY sin que tengas que pasarla.
En bash o zsh:
export TYPESAFE_API_KEY="tu-api-key"
En PowerShell:
$env:TYPESAFE_API_KEY = "tu-api-key"
La key vive en el servidor. Nunca en el frontend. El SDK de TypeScript tiene una opción dangerouslyAllowBrowser que viene a false por algo: si lo activas en el navegador, cualquiera abre las DevTools y se lleva tu key.
Paso 3: tu primera llamada a Jev con cURL
Con la key en el entorno, esta es la llamada mínima. En Windows, lánzala desde Git Bash o WSL: en PowerShell 5.1 curl es un alias de Invoke-WebRequest y el heredoc no funciona.
curl -X POST https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"model": "jev-latest",
"state": { "comment": "Muy buen post. ¿Por qué usas inject() fuera del constructor? Me da error." },
"questions": {
"needs_reply": {
"type": "noul",
"instructions": "Does `comment` need a reply from the author of the post?"
}
}
}
EOF
Tres campos en el body: state (texto, objeto o array), model (obligatorio si vas por HTTP) y questions. La clave de cada pregunta (needs_reply) la eliges tú y no se envía al modelo: solo sirve para encontrar la respuesta después.
Paso 4: cómo usar Jev con el SDK de TypeScript
Necesitas Node 20 o superior. Este código es del SDK de TypeScript v0.6.0 (15 de septiembre de 2026), en el que los niveles de score pasan a ser un array ordenado:
npm install @typesafe-ai/sdk
# o
bun add @typesafe-ai/sdk
Y el triaje completo:
import { choice, noul, score, TypeSafeClient } from '@typesafe-ai/sdk'
const client = new TypeSafeClient() // lee TYPESAFE_API_KEY
export async function triageComment(postTitle: string, comment: string) {
return client.systemOne({
state: { post_title: postTitle, comment },
questions: {
kind: choice('What kind of message is `comment`?', {
technical_question: 'The reader asks a technical question about the post or its code',
feedback: 'Opinion, praise, criticism or a correction about the post',
spam: 'Promotion, links unrelated to `post_title`, or bot-generated text',
}),
needs_reply: noul('Does `comment` need a reply from the author of the post?', {
true: 'It asks something only the author can answer, or reports an error in the post',
false: 'It is a thank-you, a general opinion, or spam',
}),
tone: score('How hostile is the tone of `comment`?', [
'Friendly or neutral',
'Critical but respectful',
'Hostile or insulting',
]),
},
})
}
Fíjate en los backticks: `comment` y `post_title` le dicen a Jev qué campo del state mirar en cada pregunta. Y no hay model: el SDK usa jev-latest por defecto. Los tipos de answers se infieren de las preguntas, así que answers.kind.choice ya viene tipado como 'technical_question' | 'feedback' | 'spam'.
Separar el comentario en tres preguntas pequeñas en vez de pedir "analiza este comentario" es la decisión de diseño que más importa. En el hilo de lanzamiento en Hacker News, un usuario describía el antipatrón así: "A lot of people have become prompt maximalists, asking for complex multi-part solutions or dynamic workflows in a single prompt." Y otro, que todavía no lo había probado, contaba cómo rediseñaría con Jev su app de seguimiento de paquetes: "Using Jev I’d decompose the prompt into a bunch of smaller questions, then I’d combine the answers in software."
Paso 5: qué devuelve Jev y cómo leerlo
Jev devuelve un objeto con tres claves: answers (una entrada por pregunta, con el mismo nombre que le diste), model (la versión exacta que respondió) y usage (los tokens).
Esto es lo que devuelve la API para el comentario del Paso 1 pasado por triageComment (valores de ejemplo, forma exacta):
{
"model": "jev-1.13.0",
"answers": {
"kind": {
"type": "choice",
"choice": "technical_question",
"confidence": 0.91,
"probabilities": { "technical_question": 0.94, "feedback": 0.06, "spam": 0.0 }
},
"needs_reply": { "type": "noul", "noul": 0.97 },
"tone": {
"type": "score",
"score": 0.12,
"confidence": 0.82,
"legend": { "0": "Friendly or neutral", "1": "Critical but respectful", "2": "Hostile or insulting" },
"probabilities": { "0": 0.88, "1": 0.12, "2": 0.0 }
}
},
"usage": { "input_tokens": 214, "output_tokens": 58 }
}
| Campo | Qué es | Lo que suele confundir |
|---|---|---|
model |
La versión concreta que respondió | Pediste jev-latest y te dice jev-1.13.0. Guárdalo en tus logs |
noul |
Probabilidad de "sí", de 0 a 1 | Es answers.needs_reply.noul, no answers.needs_reply. No trae confidence |
choice |
La opción elegida | Mira también probabilities (suman 1) y confidence |
score |
Media ponderada sobre los índices 0..n-1 | Puede caer entre niveles: 0.12 no es "nivel 0", es "casi 0" |
legend |
Texto de cada nivel del score | Te evita guardar el array de niveles en otro sitio |
usage |
Tokens de entrada y salida | Pagas solo la entrada (0,042 $ por millón). output_tokens no es cero, pero es gratis |
Si consumes la API con fetch en vez del SDK, esa respuesta entra a tu sistema sin tipos. Es justo la frontera donde conviene validar con un schema: te lo explico en el curso de Zod para TypeScript, y el patrón es el mismo para cualquier API externa.
Paso 6: cómo usar Jev en Python
Python 3.10 o superior:
pip install typesafe-sdk
# o
uv add typesafe-sdk
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
comment = "Muy buen post. ¿Por qué usas inject() fuera del constructor? Me da error."
with TypeSafeClient() as client:
response = client.system_one(
state={"post_title": "Interceptores en Angular", "comment": comment},
questions={
"kind": Choice(
instructions="What kind of message is `comment`?",
criteria={"technical_question": None, "feedback": None, "spam": None},
),
"needs_reply": Noul(
instructions="Does `comment` need a reply from the author of the post?",
),
"tone": Score(
instructions="How hostile is the tone of `comment`?",
criteria=["Friendly or neutral", "Critical but respectful", "Hostile or insulting"],
),
},
)
print(response.choices["kind"].choice)
print(response.nouls["needs_reply"].noul)
print(response.scores["tone"].score)
Los argumentos son instructions= y criteria= en las tres clases. Si vienes de otro tutorial con options= o levels=, eso no existe.
Qué vía elegir para cada momento
| Playground | cURL | SDK TypeScript | SDK Python | |
|---|---|---|---|---|
| Para qué | Afinar preguntas | Comprobar key y endpoint | Integrarlo en tu backend Node/Bun | Scripts, notebooks, FastAPI |
| Necesitas | Cuenta | Key + terminal | Key + Node 20+ | Key + Python 3.10+ |
| Reintentos ante 429/5xx | No aplica | Los programas tú | Incluidos | Incluidos |
| Límite / riesgo | No ves cómo se comporta con tus datos reales en volumen | Sin tipos: es fácil leer mal la respuesta | Si lo ejecutas en el navegador, expones la key | Si lees response.answers[...] sin mirar el tipo, el error de .noul aparece en ejecución |
Los tropiezos del primer día
El noul es un objeto. if (answers.needs_reply > 0.5) no hace lo que crees: en TypeScript no compila y en Python lanza un TypeError al comparar. Es answers.needs_reply.noul > 0.5.
jev-latest se mueve. Hoy apunta a jev-1.13.0, pero el alias avanza con cada release estable. Mientras pruebas, da igual. En cuanto calibres umbrales, fija la versión con new TypeSafeClient({ defaultModel: 'jev-1.13.0' }). GET /v1/models hoy solo lista los alias; el ID versionado que te respondió lo tienes en el campo model de cada respuesta.
Preguntas en inglés. El inglés es el idioma principal de entrenamiento y donde mejor acierta. Las instructions y los criteria van en inglés. El state puede ir en castellano, pero mide antes de fiarte.
State limpio. Manda el comentario y el título del post, no el HTML de la página ni el hilo entero. Solo acepta texto: si el comentario lleva una captura, Jev no la ve.
Errores HTTP. Un 401 es la key y un 422, el body: el propio error te dice qué campo falla.
El SDK de TypeScript ya reintenta 408, 429 y 5xx por ti.
Límites de este primer setup
Unos cuantos comentarios en el Playground no validan nada. Que te devuelva confidence: 1.0 puede pasar, y no significa que acierte siempre en tu dominio. Otro comentario del hilo de HN lo dice sin rodeos: "It can't hallucinate, but it doesn't mean it can't make wrong decisions." Antes de automatizar nada necesitas una muestra etiquetada a mano y comparar. Es el mismo razonamiento que te cuento en evals deterministas para agentes de IA.
La latencia que ves en tu terminal no es la de la doc. La documentación de modelos habla de unos 100 ms por consulta, y eso es inferencia. Desde mi red, contra jev-1.13.0, medí una mediana de unos 258 ms reutilizando la conexión TLS y unos 628 ms abriendo una conexión nueva. Si lo llamas desde una función serverless que abre conexión en cada invocación, cuenta con la cifra alta.
Los rate limits no son un contrato. Hoy son 250.000 tokens por segundo y 1.200 peticiones por minuto, pero la propia doc avisa de que se están ajustando dinámicamente y pueden cambiar sin aviso. Si quieres pasar por Jev el histórico de comentarios de golpe, móntalo con cola y reintentos, no con un Promise.all de diez mil llamadas.
Siguiente paso: qué hacer con el confidence
¿Qué haces con un needs_reply de 0.62 o un kind con confidence 0.55? El patrón de umbrales (automatizar lo claro, mandar lo dudoso a revisión humana) está en el post sobre qué es Jev.
Lo que sí puedes hacer hoy: coge los últimos 30 comentarios de tu blog, tu canal o tu repo, etiquétalos a mano y pásalos por el script del Paso 4. En una hora sabrás si Jev acierta con tus datos, que es lo único que importa.
Y si trabajas con Claude Code, instala la skill oficial para que tu agente conozca la API sin que se la expliques:
claude plugin marketplace add typesafe-ai/skills
claude plugin install typesafe@typesafe-ai
Con otros agentes: npx skills add typesafe-ai/skills --skill typesafe-ai. Y si quieres llevar ese flujo con agentes de la idea a un producto en producción, es lo que hacemos en Construye con IA.
Qué hacer con esos números es el centro de Jev y las decisiones tipadas con IA: por qué lo que está calibrado son las probabilidades y no el confidence, cómo comprobarlo con casos de tu propio histórico y los patrones para llevarlo a producción.
Preguntas frecuentes
¿Qué necesito para empezar a usar Jev?
Una cuenta en la consola de TypeSafe y una API key. Para probar con cURL basta la terminal; para los SDK, Node 20 o superior (TypeScript) o Python 3.10 o superior. Si prefieres fetch sin SDK, funciona igual: es un POST con JSON, pero los tipos y los reintentos los programas tú.
¿Por qué mi noul nunca pasa del umbral?
Porque probablemente estás comparando el objeto entero. La respuesta de un noul es { "type": "noul", "noul": 0.97 }, así que el número está en answers.needs_reply.noul. Además, a diferencia de choice y score, el noul no trae campo confidence.
¿Tengo que pasar model en cada llamada?
En HTTP sí, es obligatorio. En los SDK no: usan jev-latest por defecto, o el que pongas en defaultModel o en la variable TYPESAFE_DEFAULT_MODEL. Cuando tengas umbrales ajustados, fija jev-1.13.0 para que un cambio de versión no te los mueva.
¿Puedo mandar el state en castellano?
Sí, lo acepta. Pero el inglés es donde mejor acierta, así que deja las preguntas y los criterios en inglés y mide el acierto con comentarios reales en castellano antes de automatizar decisiones.
¿Cómo pruebo Jev sin escribir código?
En el Playground: pegas un texto como state, añades preguntas noul, choice o score y ves las respuestas con sus probabilidades.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.









