baml
BAML es un lenguaje de programación estáticamente tipado y orientado a expresiones: TypeScript con métodos en snake_case, campos name: type,, enums, interfaces, genéricos, clausuras, encadenamiento opcional, cadenas con backticks e interpolación ${...}, .to_string() en cualquier valor y una stdlib completa. También incluye un DSL declarativo para llamadas a LLM (function … { client: prompt: }, test) que se desugariza en él, por lo que la salida estructurada de un modelo es simplemente un valor de retorno tipado.
El CLI es la documentación. Descúbrelo con baml describe:
brew install baml # Binario CLI: `baml`
baml init # Nuevo proyecto (baml.toml + baml_src/)
baml help <comando> # Opciones del CLI y ejemplos
baml describe baml.json # Referencia para cualquier módulo/tipo/método/firma/palabra clave
baml describe Array --budget 120 # (Array, String, Map, assert, match, patterns, spawn, python, ...)
# termina con "N more lines"? vuelve a ejecutar con `--budget <N>`
baml check # Verificación de compilación del proyecto
baml run -e 'expr' # Evaluar una expresión; retroalimentación rápida + verificación de sintaxis
baml test --list && baml test # Ejecutar cada bloque test/testset
baml fmt baml_src/main.baml # Canonicalizar el formato del proyecto
baml describe <nombre> imprime el cuerpo fuente completo de las funciones de la stdlib, la forma más rápida de verificar el comportamiento y la única ruta para funciones integradas como assert (sin archivo en disco). Las funciones puras no necesitan test/cliente: verifícalas con baml run -e 'add(2, 3)'.
No describas APIs ya demostradas a continuación a menos que encuentres errores. Puedes comenzar basándote en los ejemplos y usarlo si encuentras más errores o deseas detalles reales de la stdlib.
En su mayor parte, se comporta como JavaScript/TypeScript, con una sintaxis muy similar, pero BAML es más sólido/estricto.
Mejores prácticas e información
- Función LLM = retorno tipado. EL TIPO DE RETORNO es el esquema que el modelo debe producir (
class,enum, unión literal,string[],T?). La salida estructurada es solo un valor tipado: entrégala al código ordinario. - Los prompts son cadenas con backticks e interpolación
${...}. Escribeprompt:… ${arg} …, y siempre inyecta${ctx.output_format()}para un retorno estructurado. Escapa con\`` /${`; anida con backticks adicionales. - Los clientes son valores, no bloques de configuración.
client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);— el bloque antiguoclient<llm> Name { provider: …, options: {…} }está eliminado. Cualquier cosa que implementeai.Clientfunciona (openai.ResponsesClient,openai.ChatClient,anthropic.Client, …; los parámetros del constructor varían según el proveedor).api_keyybase_urlaceptanai.Credential: una cadena literal,nullpara el valor predeterminado del proveedor, o una referenciaenv.NAMEenlazada tardíamente y resuelta en el momento de la solicitud. Compón confiabilidad mediante envoltura:ai.clients.Retry.new(inner = c, max_attempts = 3)yai.clients.RoundRobin.new(members = […])tienen.new, peroai.clients.Fallback { members: […] }no — construye como un literal de clase. Luego usaclient: Fasten la función, o la abreviaturaclient: "openai/gpt-4o-mini".baml describe openai/baml describe ai.Credential/baml describe ai.clients. - Da forma al esquema con atributos de campo.
@description("…")agrega una pista///que el modelo ve en${ctx.output_format()};@alias("name")renombra la clave JSON emitida. Encadena:tags: string[] @alias("labels") @description("…"). - Prueba el código puro, no el modelo. Prueba unitaria de orquestación/post-procesamiento en datos literales con
assert.*. Llamar a una función LLM en untestcon un cliente real realiza una solicitud real. Para probar prompts, solicitudes y análisis sin conexión, usaF@spec(args)o un cliente con script (ver Ejecución de funciones de IA). - Construye cadenas con interpolación, no coerción.
score=${n}stringifica cualquier valor (.to_string()implícito); llama a.to_string()solo para la cadena.+requiere que ambos lados ya sean cadenas ("n=" + 5no compilará). catchpara algunos,catch_allpara todos.expr catch (e) { baml.errors.ParseError => fallback }maneja un error específico;expr catch_all (e) { _ => fallback }es exhaustivo, para la parte superior/punto de entrada de un flujo de trabajo. Los errores se propagan implícitamente; los llamadores no necesitan redeclararlos. Lanza conthrow baml.errors.InvalidArgument { message: "…" }(los tipos de error son las clases integradasbaml.errors.*—InvalidArgument/ParseError/Io/Timeout/…;baml describe baml.errors); anota una firma fallible con-> T throws ErrType. Prefiere una unión de resultado tipada (type R = Ok | Err) sobre lanzar para flujo de control ordinario.- Interfaces = comportamiento compartido + despacho dinámico.
interface I { function m(self) -> T throws never }; cada método declara su cláusulathrows. Un método con cuerpo es una implementación predeterminada, que un implementador puede anular. Una clase opta medianteimplements I { … }; un valor tipado comoI(oI[]) despacha al implementador en tiempo de ejecución. Las interfaces también pueden declarar tipos asociados y límites genéricos.baml describe interfaces. requires, no herencia. BAML no tiene herencia: una clase no puede extender otra clase.interface Pet requires Animal, Named { … }convierte esas interfases en requisitos previos dePet. Los cuerpos predeterminados dePetpueden llamar a sus métodos, y un valor tipado comoPetlos expone. Cada implementador dePettambién escribe sus propios bloquesimplements Animaleimplements Named(de lo contrario, E0125). Un diamante derequirescompila, y la interfaz compartida se implementa una vez; un ciclo es un error. Cuando dos interfaces en una clase declaran el mismo nombre de método, una llamada sin calificar es un error (E0121); llamaobj.as<I>.m()en su lugar.- Pattern matching.
match (v) { … }sobre valores/tipos; los brazos sonpatrón => expr— literales,let x: T(vincular + estrechar), deestructuración de claseT { f: let y }, patrones ORA | B, guardas… if cond,_; debe ser exhaustivo. Tambiénv is T→ bool (estrecha) yif let x: T = v { … } else { … }.baml describe patterns. - Estrechamiento — solo locales.
v is T,v != null/v == nully una prueba de veracidad estrechavsolo cuandoves un local (unleto un parámetro) que ninguna clausura captura: en la ramaif/whilecon guarda, después de una salida temprana (if (!(v is T)) { return … }), y en el lado derecho de&&(lado izquierdo verdadero) y||(lado izquierdo falso) —v is T && v.f == 1,s == null || s.length() == 0. NO se estrecha: un local capturado, una ruta de campo (h.v is T && h.v.f→E0007) y una ruta de índice (xs[0] is T). Las tareas pueden intercalarse entre declaraciones, por lo que otra tarea o un alias puede cambiar un campo entre la prueba y la lectura. Para una escritura de campo, escribelet v = h.v;primero, oif let v: T = h.v { … }, omatch (h.v) { let v: T => … }(cada una lee el campo una vez). Una asignación al local reemplaza el hecho con el tipo asignado.baml describe is. - Concurrencia = green threads.
spawn { … }devuelve unFuture;awaitlo recoge. Combina muchos conbaml.future.all/all_settled/race/any.all_settleddevuelveSuccess<T>,Failure<E>oPanickedpara cada entrada; la cancelación de entrada es un resultadoPanicked. Configura un spawn con una cláusulawith:spawn with limit, token { … }— cada valor listado es unbaml.spawn.Modifierque transforma el plan que el spawn construye, de izquierda a derecha.baml.spawn.Limit.new(n)limita la concurrencia (los spawns excedentes se ponen en cola), unbaml.spawn.CancelTokencancela cooperativamente,baml.spawn.Root.new()desvincula la tarea de la cancelación de su generador.baml describe spawn/baml describe baml.future. - Seguridad de recursos —
defer,cleanup,catch (e, ctx).defer { … }ejecuta un bloque al salir del ámbito, LIFO, en cada ruta (return/throw/fall-through) — como Go. Un método de clase llamadofunction cleanup(self) -> voides un finalizador: se ejecuta como máximo una vez por instancia, ya sea que lo llames, lodefero el GC lo reclame.catch (e, ctx)vincula unErrorContextjunto con el error: un error lanzado mientras se maneja otro se encadena a él, por lo quectx.root_cause()/ctx.causeretroceden hasta el fallo original yctx.to_string()renderiza toda la cadena (estilo__context__de Python).while let PATTERN = expr { … }itera hasta que el patrón falla (por ejemplo, drenar un.pop()que devuelveT?). - Llama a BAML desde Python/TS. Declara un
[generator.<name>]enbaml.toml, ejecutabaml generate, luego importa elbaml_sdktipado. Instalación + uso:baml describe python/baml describe typescript/baml describe baml_sdk. - Acceso seguro sobre indexación. El subíndice entra en pánico en un índice/clave faltante; usa
.at(i)/.get(k)(→T?), pasa con?., valor predeterminado con??(parentiza:(m.get(k) ?? 0) + 1). - Los métodos de la stdlib son snake_case, llamados sobre un valor. Algunos devuelven nuevo, otros mutan en el lugar, algunos hacen ambas cosas (
sort_by_keyordena el receptor y lo devuelve) — para leer la documentación,baml describe <palabra/tipo/identificador/palabra clave/etc>. - Campos de clase
name: type,; construcciónType { field: val }. Los métodos toman unselfsimple; las fábricas son funciones libres. Los campos son mutables (como TS):obj.field = vyobj.field += nfuncionan, y un métodoselfpuede mutar en el lugar — un método con efectos secundarios devuelvevoid. Las clases son tipos de referencia:find/at(i)/subíndice devuelven un alias en vivo, no una copia, por lo que mutar el resultado muta ese elemento dentro del array (xs.find(p)?.n += 1actualizaxs), y una clase pasada a una función puede ser mutada por el que llama. Se admite la propagación de actualización de struct:User { ...u, tier: Tier.Free }. Las clases vacías son legales (class Marker {}) — útiles como variantes de unión. Los enums son variantes simples — sin métodos, sin datos asociados (E.A.foo()no compilará); coloca el comportamiento en funciones libres que haganmatch.enum E { A, B }, accesoE.A. - Los bloques son expresiones — la última expresión es el valor (sin
;);return x;para salida temprana. Una función solo de efectos secundarios devuelvevoid— otra forma de escribirnull, el tipo unidad — y puede omitir su expresión final oreturn;. Un valor dejado como expresión final de dicho cuerpo es un error, no se descarta: termínalo con;(xs.push(x);—pushdevuelve la nueva longitud).for (let x in xs)itera VALORES;while (cond) { … }itera. Las clausuras(x) -> { ... }infieren parámetro/retorno del contexto (anota(x: T) -> Rsolo cuando es ambiguo;->es obligatorio)..map/.filterdevuelven arrays directamente (sin.collect()). Un mapa vacío necesita un tipo:let m: map<string, int> = {};. - Sin ternario —
if/elsees la expresión. No haycond ? a : b;if (cond) { a } else { b }es una expresión que devuelve un valor, así que asígnalo directamente:let label = if (x > 3) { "big" } else { "small" };. Cada rama es un bloque cuya última expresión es su valor (sinreturn). Encadena conelse if, y combina conif let PATTERN = expr { … } else { … }para vincular y estrechar. - Las condiciones usan veracidad.
if,while, guardas de match,&&,||y!aceptan cualquier valor. Los valores falsos sonfalse,null, cero numérico, cadenas vacías, arrays/mapas vacíos y bytes vacíos; todo lo demás es verdadero.&&y||aún devuelvenbool, no un valor de operando; cortocircuitan. Un optional local verdadero se estrecha en la rama tomada y en el lado derecho de&&(s && s.length() > 0). - Los arrays tienen un conjunto de métodos similar a JS —
map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(), máspush/pop/shift/unshift/sort_by/sort_by_keyen el lugar. La mayoría toman clausuras que puedenthrows.baml describe Arrayda más información. - Las vinculaciones locales con
letson reasignables (x = x + 1) — no hay palabra clave mut (es TS let, no Rust); tampoco hay const. - Args: valores predeterminados con
=, llamadas por palabra clave con=(nunca:). Declara un valor predeterminado en la firma:function f(a: int, b: int = 10); llamaf(1)of(1, b = 2). Un parámetro con valor predeterminado debe pasarse por nombre —f(1, 2)es un error (el parámetro con valor predeterminado 'b' debe pasarse por nombre). Cualquier parámetro (incluso obligatorio) puede pasarse por nombre (f(a = 1, b = 2)), y puedes omitir un valor predeterminado del medio para establecer uno posterior (f(1, c = 9)). La sintaxis de palabra clave esnombre = valor;nombre: valorno se analizará (:es para tipos/campos).T?NO hace que un argumento sea opcional — a diferencia de TSb?: T, un parámetrob: T?sigue siendo obligatorio (debes pasarnull, de lo contrariose esperaban N argumento(s), se obtuvo …); agrega= nullpara hacerlo omisible. Las funciones integradas siguen esto:baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)). - Donde diverge de TS (las trampas silenciosas): la aritmética está basada en tipos, no al estilo TS.
int / intes división entera truncante (285 / 100 == 2, NO2.85) y%es el resto (285 % 100 == 85); esto compila bien y solo da un número incorrectamente silencioso, por lo que es la trampa de mayor valor. Mezcla un float para obtener división flotante (285 / 100.0 == 2.85,285.0 / 100 == 2.85); cualquier operación mixtaint/floatpromueve afloat(5 + 2.0 == 7.0). No hay.to_float()— convierte un int conn * 1.0(o divide por un float). Un resultadointno se autocoerciona afloaten la asignación (let x: float = 285 / 100es un error de compilación).+es solo numérico: la concatenación de cadenas requiere que ambos lados ya seanstring("n=" + 5no compilará — usa interpolación${...}). Las comparaciones (==,<, …,==estructural) y&&/||/!son similares a TS. - Tests:
test "nombre" { ... }solo (sin envoltorio);testsetsolo AGRUPA. Aserciones (solo 6):assert.equal/approx_equal/is_true/is_type/not_null/contains.assert.equalcompara estructuralmente (profundo, entre clases/arrays/mapas) — y también lo hace==simple, que es la forma booleana.assert.equales exacto en floats; usaassert.approx_equal(actual, esperado, eps)para los calculados. Última aserción: sin;final. Los IDs canónicos sonroot::NombreTestpara un test de nivel superior yroot::Testset::NombreTestdentro de un testset. Ejecuta uno conbaml test root::NombreTest. - Ejecución de funciones de IA. Construir una solicitud sin ejecutarla:
adddevuelve unbaml.llm.PendingCall. Pasarlo abaml.llm.runlo ejecuta una vez; abaml.llm.run_streampara iterar tokens incrementalmente. Una entrada de datos es un mapa de argumentos nombrados desde el que BAML lee (SSE o stdin).baml.llm.PendingCall { … }también es un valor construible manualmente.baml describe llm. - CLI, no lib. BAML es un lenguaje; todo (tests, generación de código, documentación) sale de
bamlybaml.toml. La liblibbamles la implementación del lenguaje, no una API pública para programación de aplicaciones (excepto los SDK generados para Python/TS). Solo el binariobamles compatible. - Los comentarios son de una sola línea.
//comienza un comentario. No hay comentarios de bloque (/* */); usa//para todo, incluso para bloques de documentación (ver@description).
Tabla de referencia rápida
| Característica | Sintaxis / Notas |
|---|---|
| Función LLM | function name(input: T) -> R { client: …, prompt: … } |
| Cliente LLM | client Name = openai.ResponsesClient.new(model = "…") |
| Prompt | Cadena con backticks: prompt: `… ${arg} … ${ctx.output_format()}` |
| Atributos de campo | @description("…") / @alias("json_name") |
| Tipo opcional | T? — null o valor |
| Pattern matching | match (v) { patrón => expr, … } |
| Concurrencia | spawn { … } → Future; await fut; baml.future.all([…]) |
| Manejo de errores | catch (e) { Tipo => fallback }; catch_all |
| Pruebas | test "nombre" { … } con assert.* |
| Generación de código | baml generate después de configurar [generator] |
| Atajo de cliente | client: "openai/gpt-4o-mini" |