baml
BAML est un langage typé statiquement, orienté expression — du TypeScript avec des méthodes en snake_case, des champs name: type,, des énumérations, des interfaces, des génériques, des fermetures, du chaînage optionnel, des chaînes backtick avec interpolation ${...}, .to_string() sur n'importe quelle valeur, une vraie bibliothèque standard. Et un DSL déclaratif pour les appels LLM (function … { client: prompt: }, test) qui se désucre en lui-même, de sorte que la sortie structurée d'un modèle n'est qu'une valeur typée retournée.
La CLI est la documentation. Découvrez via baml describe :
brew install baml # Binaire CLI : `baml`
baml init # Nouveau projet (baml.toml + baml_src/)
baml help <commande> # Options CLI et exemples
baml describe baml.json # Référence pour tout module/type/méthode/signature/mot-clé
baml describe Array --budget 120 # (Array, String, Map, assert, match, patterns, spawn, python, ...)
# se termine par "N more lines" ? relancez avec `--budget <N>`
baml check # Vérification à la compilation du projet
baml run -e 'expr' # Évalue une expression ; feedback rapide + vérification syntaxique
baml test --list && baml test # Exécute chaque bloc test/testset
baml fmt baml_src/main.baml # Canonise le formatage du projet
baml describe <nom> affiche le corps source complet des fonctions de la bibliothèque standard — le moyen le plus rapide de vérifier le comportement, et le seul chemin pour les fonctions intégrées comme assert (pas de fichier sur disque). Les fonctions pures n'ont besoin ni de test ni de client — vérifiez-les avec baml run -e 'add(2, 3)'.
Ne décrivez pas les API déjà démontrées ci-dessous, sauf si vous rencontrez des erreurs. Vous pouvez commencer avec les exemples et les utiliser si vous rencontrez des erreurs supplémentaires ou si vous souhaitez des détails concrets sur la bibliothèque standard.
Il se comporte principalement comme JavaScript/TypeScript, avec une syntaxe très similaire — mais BAML est plus sûr/strict.
Bonnes pratiques et informations
- Fonction LLM = retour typé. Le TYPE DE RETOUR est le schéma que le modèle doit produire (
class,enum, union littérale,string[],T?). La sortie structurée n'est qu'une valeur typée — transmettez-la à du code ordinaire. - Les prompts sont des chaînes backtick avec interpolation
${...}. Écrivezprompt:… ${arg} …, et injectez toujours${ctx.output_format()}pour un retour structuré. Échappez avec\`` /${` ; imbriquez avec des backticks supplémentaires. - Les clients sont des valeurs, pas des blocs de configuration.
client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);— l'ancien blocclient<llm> Nom { provider: …, options: {…} }est supprimé. Tout ce qui implémenteai.Clientfonctionne (openai.ResponsesClient,openai.ChatClient,anthropic.Client, … ; les paramètres du constructeur diffèrent selon le fournisseur).api_keyetbase_urlacceptentai.Credential: une chaîne littérale,nullpour la valeur par défaut du fournisseur, ou une référenceenv.NOMrésolue au moment de la requête. Composez la fiabilité en encapsulant :ai.clients.Retry.new(inner = c, max_attempts = 3)etai.clients.RoundRobin.new(members = […])ont.new, maisai.clients.Fallback { members: […] }n'en a pas — construisez-le comme un littéral de classe. Utilisez ensuiteclient: Fastdans la fonction, ou le raccourciclient: "openai/gpt-4o-mini".baml describe openai/baml describe ai.Credential/baml describe ai.clients. - Façonnez le schéma avec des attributs de champ.
@description("…")ajoute une indication///que le modèle voit dans${ctx.output_format()};@alias("name")renomme la clé JSON émise. Enchaînez :tags: string[] @alias("labels") @description("…"). - Testez le code pur, pas le modèle. Testez unitairement l'orchestration/le post-traitement sur des données littérales avec
assert.*. Appeler une fonction LLM dans untestavec un client réel fait une vraie requête. Pour tester les prompts, les requêtes et l'analyse hors ligne, utilisezF@spec(args)ou un client scripté (voir Exécution de fonctions IA). - Construisez des chaînes avec interpolation, pas coercition.
score=${n}transforme n'importe quelle valeur en chaîne (.to_string()implicite) ; appelez.to_string()pour la chaîne seule.+nécessite que les deux côtés soient déjà des chaînes ("n=" + 5ne compile pas). catchpour certains,catch_allpour tous.expr catch (e) { baml.errors.ParseError => fallback }gère une erreur spécifique ;expr catch_all (e) { _ => fallback }est exhaustif — pour un sommet de workflow / point d'entrée. Les erreurs se propagent implicitement ; les appelants n'ont pas besoin de les redéclarer. Lancez avecthrow baml.errors.InvalidArgument { message: "…" }(les types d'erreur sont les classesbaml.errors.*intégrées —InvalidArgument/ParseError/Io/Timeout/… ;baml describe baml.errors) ; annotez une signature faillible avec-> T throws ErrType. Préférez une union de résultat typée (type R = Ok | Err) authrowpour le flux de contrôle ordinaire.- Interfaces = comportement partagé + dispatch dynamique.
interface I { function m(self) -> T throws never }; chaque méthode déclare sa clausethrows. Une méthode avec un corps est une implémentation par défaut, qu'un implémenteur peut surcharger. Une classe adhère viaimplements I { … }; une valeur typéeI(ouI[]) dispatche vers l'implémenteur à l'exécution. Les interfaces peuvent aussi déclarer des types associés et des bornes génériques.baml describe interfaces. requires, pas d'héritage. BAML n'a pas d'héritage : une classe ne peut pas étendre une classe.interface Pet requires Animal, Named { … }fait de ces interfaces des prérequis dePet. Les corps par défaut dePetpeuvent appeler leurs méthodes, et une valeur typéePetles expose. Chaque implémenteur dePetécrit aussi ses propres blocsimplements Animaletimplements Named(sinon E0125). Un diamant derequirescompile, et l'interface partagée est implémentée une fois ; un cycle est une erreur. Lorsque deux interfaces sur une même classe déclarent le même nom de méthode, un appel non qualifié est une erreur (E0121) ; appelezobj.as<I>.m()à la place.- Filtrage par motif (pattern matching).
match (v) { … }sur des valeurs/types ; les branches sontmotif => expr— littéraux,let x: T(lier + affiner), déstructuration de classeT { f: let y }, motifs ouA | B, gardes… if cond,_; doit être exhaustif. Aussiv is T→ bool (affine) etif let x: T = v { … } else { … }.baml describe patterns. - Affinement — uniquement les variables locales.
v is T,v != null/v == nullet un test de vérité affinentvuniquement lorsquevest une variable locale (unletou un paramètre) qui n'est capturée par aucune fermeture : dans la brancheif/whilegardée, après une sortie anticipée (if (!(v is T)) { return … }), et à droite de&&(côté gauche vrai) et||(côté gauche faux) —v is T && v.f == 1,s == null || s.length() == 0. PAS affiné : une variable locale capturée, un chemin de champ (h.v is T && h.v.f→E0007) et un chemin d'index (xs[0] is T). Les tâches peuvent s'intercaler entre les instructions, donc une autre tâche ou un alias peut changer un champ entre le test et la lecture. Pour un champ, écrivez d'abordlet v = h.v;, ouif let v: T = h.v { … }, oumatch (h.v) { let v: T => … }(chacun lit le champ une fois). Une affectation à la variable locale remplace le fait par le type assigné.baml describe is. - Concurrence = threads verts.
spawn { … }retourne unFuture;awaitle collecte. Combinez-en plusieurs avecbaml.future.all/all_settled/race/any.all_settledretourneSuccess<T>,Failure<E>, ouPanickedpour chaque entrée ; l'annulation d'une entrée est un résultatPanicked. Configurez un spawn avec une clausewith:spawn with limit, token { … }— chaque valeur listée est unbaml.spawn.Modifierqui transforme le plan que le spawn construit, de gauche à droite.baml.spawn.Limit.new(n)limite la concurrence (les spawns en excès sont mis en file d'attente), unbaml.spawn.CancelTokenannule coopérativement,baml.spawn.Root.new()détache la tâche de l'annulation de son spawner.baml describe spawn/baml describe baml.future. - Sécurité des ressources —
defer,cleanup,catch (e, ctx).defer { … }exécute un bloc à la sortie de la portée, LIFO, sur tous les chemins (return / throw / fall-through) — comme Go. Une méthode de classe nomméefunction cleanup(self) -> voidest un finaliseur : elle s'exécute au plus une fois par instance, que vous l'appeliez, ladeferiez, ou que le GC la récupère.catch (e, ctx)lie unErrorContexten plus de l'erreur — une erreur levée lors de la gestion d'une autre s'enchaîne dessus, doncctx.root_cause()/ctx.causeremontent jusqu'à l'échec original etctx.to_string()affiche toute la chaîne (style Python__context__).while let MOTIF = expr { … }boucle jusqu'à ce que le motif échoue (par exemple, vider un.pop()retournantT?). - Appelez BAML depuis Python/TS. Déclarez
[generator.<nom>]dansbaml.toml, exécutezbaml generate, puis importez lebaml_sdktypé. Installation + utilisation :baml describe python/baml describe typescript/baml describe baml_sdk. - Accès sécurisé plutôt qu'indexation. L'indice panique sur un index/clé manquant ; utilisez
.at(i)/.get(k)(→T?), traversez avec?., valeur par défaut avec??(parenthésé :(m.get(k) ?? 0) + 1). - Les méthodes de la stdlib sont en snake_case, appelées sur une valeur. Certaines retournent une nouvelle, d'autres mutent sur place, quelques-unes font les deux (
sort_by_keytrie le récepteur et le retourne) — pour lire la documentation,baml describe <mot/type/identifiant/mot-clé/etc>. - Champs de classe
name: type,; constructionType { field: val }. Les méthodes prennent unselfnu ; les factories sont des fonctions libres. Les champs sont mutables (comme TS) :obj.field = vetobj.field += nfonctionnent, et une méthodeselfpeut muter sur place — une méthode avec effet de bord retournevoid. Les classes sont des types référence :find/at(i)/indice retournent un alias vivant, pas une copie, donc muter le résultat mute cet élément dans le tableau (xs.find(p)?.n += 1met à jourxs), et une classe passée à une fonction peut être mutée par l'appelé. La propagation de mise à jour de structure est supportée :User { ...u, tier: Tier.Free }. Les classes vides sont légales (class Marker {}) — pratiques comme variantes d'union. Les énumérations sont des variantes simples — pas de méthodes, pas de données associées (E.A.foo()ne compile pas) ; mettez le comportement dans des fonctions libres qui fontmatch.enum E { A, B }, accèsE.A. - Les blocs sont des expressions — la dernière expression est la valeur (pas de
;) ;return x;pour une sortie anticipée. Une fonction à effet de bord uniquement retournevoid— une autre orthographe denull, le type unité — et peut omettre son expression finale oureturn;. Une valeur laissée comme expression finale d'un tel corps est une erreur, pas ignorée : terminez-la par;(xs.push(x);—pushretourne la nouvelle longueur).for (let x in xs)itère sur les VALEURS ;while (cond) { … }boucle. Les fermetures(x) -> { ... }infèrent le paramètre/retour du contexte (annotez(x: T) -> Runiquement en cas d'ambiguïté ; le->est requis)..map/.filterretournent directement des tableaux (pas de.collect()). Une carte vide nécessite un type :let m: map<string, int> = {};. - Pas d'opérateur ternaire —
if/elseest l'expression. Il n'y a pas decond ? a : b;if (cond) { a } else { b }est une expression qui retourne une valeur, donc assignez-la directement :let label = if (x > 3) { "big" } else { "small" };. Chaque branche est un bloc dont la dernière expression est sa valeur (pas dereturn). Enchaînez avecelse if, et associez avecif let MOTIF = expr { … } else { … }pour lier et affiner. - Les conditions utilisent la vérité.
if,while, les gardes de match,&&,||, et!acceptent n'importe quelle valeur. Les valeurs fausses sontfalse,null, zéro numérique, les chaînes vides, les tableaux/cartes vides, et les bytes vides ; tout le reste est vrai.&&et||retournentbool, pas une valeur d'opérande ; ils court-circuitent. Une variable locale optionnelle vraie est affinée dans la branche prise et à droite de&&(s && s.length() > 0). - Les tableaux ont un ensemble de méthodes similaire à JS —
map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(), plus les mutations sur placepush/pop/shift/unshift/sort_by/sort_by_key. La plupart prennent des fermetures qui peuventthrows.baml describe Arraydonne plus d'informations. - Les liaisons locales
letsont réassignables (x = x + 1) — pas de mot-clé mut (c'est TS let, pas Rust) ; il n'y a pas non plus de const. - Args : valeurs par défaut avec
=, appels par mots-clés avec=(jamais:). Déclarez une valeur par défaut dans la signature :function f(a: int, b: int = 10); appelezf(1)ouf(1, b = 2). Un paramètre avec valeur par défaut doit être passé par nom —f(1, 2)est une erreur (le paramètre par défaut 'b' doit être passé par nom). Tout paramètre (même requis) peut être passé par nom (f(a = 1, b = 2)), et vous pouvez sauter une valeur par défaut du milieu pour en définir une plus tard (f(1, c = 9)). La syntaxe des mots-clés estname = value;name: valuene sera pas analysé (:est pour les types/champs).T?NE rend PAS un argument optionnel — contrairement à TSb?: T, un paramètreb: T?est toujours requis (vous devez passernull, sinonN argument(s) attendu(s), reçu(s) …) ; ajoutez= nullpour le rendre omissible. Les fonctions intégrées suivent cela :baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)). - Où il diverge de TS (les pièges silencieux) : l'arithmétique est guidée par le type, pas à la TS.
int / intest une division entière tronquante (285 / 100 == 2, PAS2.85) et%est le reste (285 % 100 == 85) ; cela compile très bien et donne un nombre discrètement faux, donc c'est le piège le plus important. Ajoutez un float pour obtenir une division flottante (285 / 100.0 == 2.85,285.0 / 100 == 2.85) ; toute opération mixteint/floatpromeut enfloat(5 + 2.0 == 7.0). Il n'y a pas de.to_float()— convertissez un int avecn * 1.0(ou divisez par un float). Un résultatintne se coerce pas automatiquement enfloatlors de l'assignation (let x: float = 285 / 100est une erreur de compilation).+est uniquement numérique : la concaténation de chaînes nécessite que les deux côtés soient déjàstring("n=" + 5ne compile pas — utilisez l'interpolation${...}). Les comparaisons (==,<, …,==structurelle) et&&/||/!sont comme en TS. - Tests :
test "nom" { ... }seul (pas de wrapper) ;testsetsert seulement à GROUPER. Assertions (seulement 6) :assert.equal/approx_equal/is_true/is_type/not_null/contains.assert.equalcompare structurellement (profondément, à travers classes/tableaux/maps) — et==simple aussi, qui est la forme booléenne.assert.equalest exact sur les floats ; utilisezassert.approx_equal(réel, attendu, eps)pour les valeurs calculées. Dernière assertion : pas de;final. Les ID canoniques sontroot::NomTestpour un test de premier niveau etroot::Testset::NomTestà l'intérieur d'un testset. Exécutez-en un avecbaml test <id>. Testez du code pur directement :baml run -e 'assert.equal(add(2, 3), 5)'. - Exécution de fonctions IA :
f(args)— appelle la fonction LLM normale.f@spec(args)— réexécute uniquement le post-traitement avec le résultat en cache d'une précédente exécution def; cela contourne le LLM.f@spec(args, spec_id = "ma_spec")— utilise un bloctestnommé comme source du résultat en cache.baml describe aidonnerun,spec,full_spec,stream,chat. - Environnement et variables globales.
env.NOM_VARrésout une variable d'environnement au moment de l'appel ;ctxest le contexte d'exécution implicite (.output_format(),.session_id,.callbacks).$E.Nomest une constante de compilation.baml describe env/baml describe ctx/baml describe dollar. - Généralisation/méta-programmation :
E.Nompour les variantes d'énumération. Les métadonnées sur les énumérations se font via@descriptionsur le type ou le champ.baml describe $/baml describe E. useexplicite :use stdlib.io.{ read_file, write_file };useimporte dans la portée locale.baml describe use.- Les
aliascréent des noms synonymes :alias MiCadena = string;.baml describe alias. - Types littéraux :
let x: "rouge" | "bleu" = "rouge";.baml describe types. - Valeurs par défaut des champs :
class C { val: int = 10, }.baml describe classes. - Tâches =
asyncimplicite + parallélisme memory-safe : Les fonctions marquéestasks'exécutent sur un pool de threads ; les tâches partagent l'espace d'adressage (pas de mémoire partagée concurrente). Fonctions marquéestask(concurrentes, pas de mémoire partagée). Tâches de typethread(vrai parallélisme, sansasync/awaitsur le fil Python) ; les fonctionstasksontasyncpar défaut.baml describe tasks. - Valeurs volatiles —
atomic:class Shared { @atomic counter: int, }— les accèscountersont atomiques ; pas besoin de verrouillage manuel. Les lectures/écrituresatomicsont cohérentes entre les threads.baml describe atomic. type= alias d'union :type MonUnion = A | B;— oùA,Bsont des classes.typeest aussi utilisé pour les types littéraux.baml describe type.- Mots-clés de contrôle :
break,continuedans les boucles.breakprend une valeur optionnelle (break val).baml describe break. - Commentaires de documentation
///: attachés aux déclarations (classes, fonctions, interfaces).baml describe ///. - Le module
baml.json:baml define+baml describegénèrent/consultent la documentation de la bibliothèque standard.