BAML 0.24.0
Native Rust compilation, a BAML-powered toolchain wrapper, custom model output shapes, reflection constructors, code quality reports, and SDK migrations.
Thanks to everyone who contributed! Special thanks to harivansh-afk for the Rust backend and runtime improvements, and to BenSpex for reporting Rust dependency and toolchain-selection issues.
This post records the changes in the 0.24.0 registry release since 0.23.0. That cut did not finish promoting the canary channel. Use the replacement 0.25.0 canary, which includes these changes, and follow the migrations here. Upgrade the language toolchain and bridge packages together, regenerate SDKs with baml generate, and rebuild packed executables. Upgrade the wrapper separately to 0.3.0 for its toolchain-management changes. Run baml ide install to refresh editor integration, and baml agent install to refresh the bundled skill. Reload that skill before writing more BAML. Read the migrations below before upgrading.
Compile packed programs to Rust
baml pack --rust compiles supported functions to Rust. Other functions keep running as bytecode. Both can call each other inside the same runtime.
# Use a BAML checkout at the baml-language-0.25.0 tag for the replacement canary.
baml pack main --rust --rust-workspace /path/to/baml/baml_language --output ./app
./app
# Run the bytecode bodies in the same executable for comparison.
BAML_COMPILED_BODIES=0 ./app
This mode requires Rust, Cargo, and a source checkout matching the compiler revision. It supports native builds on the current machine. Cross-target compilation, WebAssembly, and ARM64 Windows are not supported. The default release profile uses fat LTO. --rust-profile fast provides a quicker build for correctness checks.
The compiler can use unsynchronized memory access for arrays, maps, and objects proven local to one task. Shared or uncertain values keep synchronized access. The native backend also keeps common arithmetic, heap-access, and call checks small enough to inline. Error behavior remains the same.
The toolchain wrapper is written in BAML
Wrapper 0.3.0 ships as a packed BAML program. It selects, installs, and launches your language toolchain. Wrapper and language versions remain independent: this release pairs wrapper 0.3.0 with language 0.24.0.
baml --help
baml toolchain status --json
baml --project ./my-app check
Normal language commands install a missing managed toolchain on demand. Wrapper help works without one. The wrapper uses the project selected by --project when resolving its toolchain. See the command migrations below before updating scripts.
Features
Choose the shape a model writes for a type
Implement baml.FromLlmOutput to expose a model-friendly representation of a class or enum. Its associated Shape controls output formats, JSON schemas, structured outputs, parsing, and tool arguments. from_llm_output converts the completed shape to your type.
class Amount {
text: string,
implements baml.FromLlmOutput {
type Shape = string;
function from_llm_output(shape: string) -> Self throws baml.json.DecodeError {
Amount { text: shape }
}
}
@@description("An amount with its currency, such as 12.50 USD")
}
function parse_amount() -> Amount {
baml.sap.parse<Amount>(`"12.50 USD"`)
}
@@description supplies the model-facing hint. baml.FromJson remains the separate interface for ordinary JSON decoding. Time types also adopt model-facing string shapes; see their migration below.
(#5414)
Construct values through reflection
reflect.Class.instantiate<T> builds an instance from declared field names. Missing nullable fields become null. reflect.Enum.value_of<T> resolves a declared variant name. Both work with runtime types and preserve the supplied values' identities.
enum Team { Technical, Sales }
class Ticket { team: Team, urgent: bool, note: string? }
function make_ticket() -> Ticket {
let team = reflect.Enum.value_of<Team>("Technical");
reflect.Class.instantiate<Ticket>({ "team": team, "urgent": true })
}
Unknown fields, missing required fields, and incompatible values produce reflect.errors.InvalidArgument. Field aliases do not rename constructor keys.
(#5415)
Inspect code complexity
baml codequality reports project, file, and function metrics. Inspect complexity, nesting, NPath, callers, dependencies, and test and test-line counts. File and function views put the numbers first and show the ten worst rows by default. --all includes every row. --metrics selects columns and sorts by the first metric.
baml codequality
baml codequality --functions --all --metrics cognitive,nesting,npath
baml codequality --files baml_src/agents
baml codequality --output-format json
NPath counts stay exact beyond 64-bit integers, including in JSON output. These reports measure code structure; test counts do not establish executed line coverage.
Check formatting without writing files
baml fmt --check
baml fmt --check baml_src/main.baml
Check mode lists files that need formatting and exits with code 1. It writes nothing. Read or parse errors take precedence and exit with code 4. Use this mode in CI instead of --dry-run; the two options cannot be combined.
(#5455)
Filesystem, host, and path utilities
The standard library adds filesystem metadata, rename, canonical paths, atomic writes, and file locks. Path operations include join, parent, is_absolute, and absolute. Host helpers expose the current directory, executable, home directory, platform, and target triple.
function output_path() -> string {
baml.path.join(["build", baml.sys.host_target(), "app"])
}
function save_config(path: string, text: string) -> void {
let lock = baml.fs.lock(path + ".lock", timeout_ms = 2000);
defer { lock.close(); }
baml.fs.write_atomic(path, text.to_utf8());
}
absolute expands a leading tilde without requiring the path to exist. canonicalize requires an existing path and resolves symlinks. Atomic writes preserve an existing destination's permissions.
(#5457)
Inspect archives and edit TOML
Inspect an archive before extracting it. entries validates entry metadata and paths and rejects links. It does not check compressed ZIP member data; corruption may be detected only when reading or extracting a member. read_file reads a member without extraction. extract supports ZIP and gzip-compressed tar archives and requires an empty destination that is not a symlink.
function inspect_archive(data: uint8array) -> string[] {
baml.archive.entries(data, "tar.gz")
}
Direct extraction skips links. Call entries first when you need strict preflight, and use a staging directory because failed extraction can leave partial contents.
baml.toml.Table adds construction, lookup, mutation, removal, table access, and key renaming. Serialization preserves untouched formatting and comments.
function edit_config() -> string {
let config = baml.toml.Table.parse("# Keep this comment\nname = 'old'\n");
config.set("name", "new");
config.to_string()
}
(#5457)
Pack with a specific runtime host
baml pack main --host-binary /path/to/baml-pack-host --output ./app
The explicit host takes precedence over discovery and downloads. Supply a host matching the compiler source revision and chosen target. You can pair this with --target for an existing cross-target host. It cannot be combined with --rust.
(#5463)
Late-bound Vertex and Bedrock credentials
Vertex credential documents and files accept environment references. Bedrock region, profile, and static credential options do too. References resolve when a request is built; rendering a request preview does not load credentials.
client Vertex = google.VertexClient.new(credentials_json = env.GCP_CREDENTIALS_JSON);
client Bedrock = aws.BedrockClient.new(
model = "anthropic.claude-3-5-haiku-20241022-v1:0",
access_key_id = env.AWS_ACCESS_KEY_ID,
secret_access_key = env.AWS_SECRET_ACCESS_KEY,
session_token = env.AWS_SESSION_TOKEN,
);
Unset or blank references can fall through to another credential source. A configured but unusable source returns an authentication error. Partial explicit Bedrock credentials now fail; see the migration below.
(#5494)
Updated agent guidance
The bundled BAML skill includes style guidelines and a workflow for reviewing code complexity as you work. Refresh and reload it after upgrading:
baml agent install
baml codequality --functions
Python and Rust naming conventions
New Python and Rust generator configurations use host-language naming. Existing configurations with preserve-case keep their authored spelling. You can opt an existing generator into the new mode:
[generator.python]
output_type = "python/pydantic"
output_dir = "../generated"
naming_convention = "language"
For Rust, set the same option in its generator table. Functions, arguments, fields, and namespaces use snake case. Type names follow host conventions, including known compound words: getGcpOAuthProvider becomes get_gcp_oauth_provider, and oauth_token becomes OAuthToken. BAML wire names stay the same. If you switch modes, regenerate and update host identifiers and imports. If you used a nightly's language mode, update split compound names such as get_gcp_o_auth_provider.
Go generic functions with defaults and callbacks
The Go generator supports generic functions with defaulted parameters or callback arguments. Given:
function EchoTimes<T>(x: T, times: int = 1) -> T { x }
function EchoWith<T>(x: T, f: (T) -> T throws never) -> T { f(x) }
Generated calls look like this:
value, err := baml_sdk.EchoTimes[string](ctx, "hello", baml_sdk.WithEchoTimesTimes(2))
value, err = baml_sdk.EchoWith[string](ctx, "hello", func(_ context.Context, x string) string {
return x
})
Go and Rust generation now report unsupported local symbols instead of silently omitting them. Use baml generate --show-all-skips to expand dependency and standard-library skip summaries. Some signatures remain unsupported.
(#5430)
Rust constructors initialize optional fields
Generated Rust data structs gain a new constructor taking their required fields. Optional fields start as None. Given:
class CreateSetupLinkRequest {
project_id: string,
name: string,
max_deployments: int?,
}
Set only the optional values you need:
let request = CreateSetupLinkRequest {
max_deployments: Some(5),
..CreateSetupLinkRequest::new("p1".to_owned(), "preview".to_owned())
};
Required fields remain mandatory. A BAML method already named new keeps that name; the generated constructor gets a trailing underscore. Opaque runtime handles do not gain constructors.
(#5527)
Handle unobserved Python task errors
Servers can install a handler for errors from unobserved BAML tasks:
import logging
from baml_bridge import set_unhandled_spawn_error_handler
logger = logging.getLogger(__name__)
def report(error: BaseException, cancelled: bool) -> None:
logger.error("Unobserved BAML task failure", exc_info=error)
previous = set_unhandled_spawn_error_handler(report)
# Restore the previous policy when appropriate.
set_unhandled_spawn_error_handler(previous)
The callback runs on a runtime thread. Return promptly and do not call BAML from it. Forward work to an event loop with a thread-safe queue or scheduler. The default handler still prints and exits immediately for an unobserved, uncancelled failure. Passing None restores that default.
(#5359)
Measured runtime improvements
We compared the published macOS ARM64 releases at f0f0bb5135 (0.23.0) and dec4713a0e (0.24.0) on an Apple M3 Max with macOS 26.6.2 and Python 3.12. Release builds use optimization level 3 and fat LTO. Each version generated its own SDK from identical workload source. Bytecode used the default O2 optimization. Telemetry was off.
The figures below are medians from five interleaved release pairs. Timing excludes process startup, SDK initialization, and compilation. Kernel timings include the bridge call and the complete workload. These compare the complete releases; they do not isolate individual PR contributions or measure provider latency.
String formatting and joining
Primitive interpolation avoids generic conversion, longer concatenation chains join their parts in one step, and native join appends into one buffer. Custom ToString behavior and element order remain supported.
| Workload | 0.23.0 | 0.24.0 |
|---|---|---|
| 12-hole request-line template, 10,000 iterations | 329.341 ms | 8.501 ms |
| Request-line array join, 10,000 iterations | 973.800 ms | 15.895 ms |
These are string-heavy kernels, not a whole-application speedup. The PRs' isolated interpolation and join analyses identified repeated generic conversion and temporary string construction; the final-release comparison includes the later concatenation optimization.
Interface lookup
The runtime caches interface resolution and limits a runtime class's first lookup to that class's own implementations. The enum equality/state-machine workload fell from 146.088 ms to 31.430 ms. Ordinary polymorphic dispatch was near-neutral in the PR's isolated analysis; do not expect the enum result for every interface call.
Allocation and collection overhead
Default builds avoid deadlock-detector bookkeeping on every lock. Allocation avoids unnecessary slot-directory locks, and finalizer scans visit candidates instead of every dead object. The 20,000-step n-body workload fell from 231.829 ms to 138.645 ms across the complete releases. This allocation-heavy workload does not establish the same gain for other programs. The isolated allocation PR also reported a 7.8% regression in its shared-array-push workload.
Language server deadlock reports are now disabled by default.
Optimized array loops
Eligible array for loops use an index walk without allocating an iterator or performing virtual dispatch at every step. They continue to observe changes to the array length. O0 bytecode and native loop lowering are unchanged by this optimization.
An 8,192-element array workload fell from 10.423 ms to 3.573 ms per job. This measurement includes array setup, traversal, indexed summation, and rebuilding; it is not a traversal-only timing. Task-local access improvements also contribute to this release comparison.
(#5503)
Async Python completion
Python event loops with add_reader resolve completed BAML calls on the event-loop thread in batches. The runtime also reduces idle-GC wakeups between calls. A batch of 5,000 concurrent no-op calls fell from 46.231 to 16.625 microseconds per call, including Python task scheduling.
This measures completion overhead. It does not shorten the model's work. Sequential async timings were noisy. The sync no-op control had a 9.7% higher median with broad outliers, so these results do not establish a gain for all Python calls. Loops without add_reader, including the default Windows proactor loop, retain the earlier completion path. Read the worker-process migration below before using fork.
Breaking changes
Generated SDK APIs are async by default
Python, TypeScript, Rust, and Swift generated functions and methods now use the plain name for async calls. Blocking calls use _sync. The old _async names are removed. Regenerate SDKs and update every affected call, including spec and stream helpers. Java, C++, C#, Go, and Ruby keep their existing convention.
Check generated names when a declared BAML name collides with a blocking twin. In TypeScript, declaring both foo and foo_sync makes the blocking twin of foo become foo_sync_; foo_sync calls the separately declared function. Use the intended generated binding when migrating.
For BAML functions echo(value: string) -> string and make_adder(n: int) -> (x: int) -> int throws never, Python changes from:
result = echo("hello")
result = await echo_async("hello")
add = make_adder(10)
result = add(5)
result = await add.call_async(5)
To:
result = await echo("hello")
result = echo_sync("hello")
add = await make_adder(10)
result = await add(5)
result = add.call_sync(5)
Returned callables use the same async-first convention. TypeScript changes from:
const result = echo("hello");
const asyncResult = await echo_async("hello");
const add = make_adder(10);
const sum = add(5);
const asyncSum = await add.callAsync(5);
To:
const result = await echo("hello");
const syncResult = echo_sync("hello");
const add = await make_adder(10);
const sum = await add(5);
const syncSum = add.callSync(5);
Rust blocking calls change from:
// In a synchronous function.
let result = echo("hello".to_owned())?;
let add = make_adder(10)?;
let sum = add.call((5,))?;
To:
// In a synchronous function.
let result = echo_sync("hello".to_owned())?;
let add = make_adder_sync(10)?;
let sum = add.call_sync((5,))?;
Rust async calls change from:
// In an async function.
let result = echo_async("hello".to_owned()).await?;
let add = make_adder_async(10).await?;
let sum = add.call_async((5,)).await?;
To:
// In an async function.
let result = echo("hello".to_owned()).await?;
let add = make_adder(10).await?;
let sum = add.call((5,)).await?;
Keep blocking calls outside an entered Tokio runtime; otherwise the Rust bridge returns CalledSyncFromAsync. tokio::task::spawn_blocking still carries that runtime context. Use an async binding, or a separate OS thread without an entered Tokio runtime. For Rust options variants, replace a blocking echo_with_options(...) with echo_sync_with_options(...), or await echo_with_options(...).
Swift changes from:
let result = try Baml.lorem.echo(value: "hello")
let asyncResult = try await Baml.lorem.echo_async(value: "hello")
To:
let result = try await Baml.lorem.echo(value: "hello")
let syncResult = try Baml.lorem.echo_sync(value: "hello")
Runtime objects follow their host naming conventions:
| API | Async | Blocking |
|---|---|---|
| Python streams | next(), final() | next_sync(), final_sync() |
| Rust streams | next(), final_() | next_sync(), final_sync() |
| TypeScript/Swift streams | next(), final() | nextSync(), finalSync() |
| Python/Rust specs | call(), parse(), build_request() | call_sync(), parse_sync(), build_request_sync() |
| TypeScript/Swift specs | call(), parse(), buildRequest() | callSync(), parseSync(), buildRequestSync() |
| Python runtime values | to_data() | to_data_sync() |
Apply the same plain-name/Sync convention to prompt helpers and experimental invocation methods. async for stream iteration stays the same. Browser and workerd streams require the async methods. In Node, use an async entry point when host callbacks return Promises.
Typed bridge boundaries reject ambiguous values
Generated SDKs encode arguments against the declared BAML signature. An empty Python or TypeScript collection that fits multiple union arms no longer chooses the first arm by declaration order.
Before, this function accepted consume([]) by selecting the first array type:
function consume(x: string[] | int[]) -> int {
match (x) {
let s: string[] => s.length(),
let i: int[] => i.length(),
}
}
Expose a concrete parameter at the host boundary:
function consume_strings(x: string[]) -> int {
consume(x)
}
Then replace the host call:
# Before
count = consume([])
# After
count = await consume_strings([])
# Or: count = consume_strings_sync([])
For ambiguous class-shaped dictionaries, construct the appropriate generated class. Dynamic runtime-owned types cross the bridge as opaque handles; pass those handles back unchanged. There is no new host-side cast API for disambiguating empty collections.
Update bridges and the toolchain together, regenerate clients, and rebuild generated artifacts. Handwritten C integrations must adopt ABI revision 5 and the new completion payload. The completion callback signature changes from:
type BamlCompleteHostCallFn =
extern "C" fn(call_id: u32, is_error: i32, content: *const i8, length: usize);
To:
type BamlCompleteHostCallFn =
extern "C" fn(call_id: u32, content: *const i8, length: usize);
Encode the outcome in HostCallCompletion instead of the removed is_error argument. Old generated code is not wire-compatible with the new bridge.
(#5359)
Rust JSON dependency features are explicit
baml_bridge no longer enables serde_json's arbitrary_precision and preserve_order features for the consumer's whole build. This restores ordinary serde_json deserialization behavior, including untagged numeric enums.
If your application relied on exact wide JSON integers or source key ordering, change:
[dependencies]
serde_json = "1"
To:
[dependencies]
serde_json = { version = "1", features = ["arbitrary_precision", "preserve_order"] }
Without these features, JSON objects use serde_json's default sorted keys and numbers normalize their representation. JSON integers beyond i64/u64 can become approximate floats. BAML map insertion order is unchanged. Returning a BAML bigint outside those Rust integer bounds still fails. Remove bridge-only vendored patches after upgrading if they are no longer needed.
(#5509)
Start Python workers before using BAML, or use spawn
Sync and async Python calls now share one native runtime. A forked child can hang if its parent has already made any BAML call. The restriction previously affected some sync/async combinations and now applies across both.
Replace a fork-based worker context:
import multiprocessing
workers = multiprocessing.get_context("fork")
With fresh processes:
import multiprocessing
workers = multiprocessing.get_context("spawn")
Create runtime-dependent values inside each child, and guard process startup with if __name__ == "__main__":. Alternatively, start forked children before any BAML calls in the parent. This release does not fix fork-after-initialization hangs.
(#5508)
Time types use strings in model output
The model-facing shapes of Instant, ZonedDateTime, PlainDateTime, PlainDate, and PlainTime are now RFC 3339 or ISO 8601 strings.
For a field starts_at: baml.time.Instant, replace saved model replies such as:
{"starts_at":{"_nanoseconds":"1767225600000000000"}}
With:
{"starts_at":"2026-01-01T00:00:00Z"}
Update saved response fixtures, output-format and schema snapshots, custom model instructions, tool arguments, and structured-output integrations using these time types. Ordinary baml.json decoding already used their string representations and keeps that behavior.
(#5414)
Captured model response bodies retain raw text
Wire capture now preserves the original response body in ModelCall.http_response.body. It no longer applies URL-secret redaction to text inside that body. For example, a response containing https://example.test/?key=example-secret now retains that text in the captured body.
If you relied on the earlier body redaction, redact event payloads before storing or sharing them, or disable wire capture. Change:
client Model = openai.ResponsesClient.new(model = "gpt-4o-mini");
To:
client Model = openai.ResponsesClient.new(model = "gpt-4o-mini", capture_wire = false);
Direct ai.wire.send_as_with_wire callers can pass capture_wire = false too. Request URL and credential-header redaction remain. Raw provider error bodies can still carry provider text.
The separate [pack] http_bodies = false and [bridge] http_bodies = false settings suppress native network-span bodies. They do not replace the client capture policy for ModelCall events. Rebuild or regenerate artifacts to bake in those settings.
(#5494)
Toolchain selection and working-directory flags
Wrapper 0.3.0 replaces toolchain pin with project-local toolchain use. The former user-default behavior of toolchain use requires --global.
Before:
baml toolchain pin 0.23.0
baml toolchain use canary
After:
# Update the nearest existing baml.toml.
baml toolchain use 0.25.0
# Set the user default in $BAML_HOME/config.toml.
baml toolchain use --global canary
Update scripts to specify the intended scope. BAML_TOOLCHAIN takes precedence over project configuration, which takes precedence over the user default. Local relative paths resolve from the manifest directory for a project selection and from the invocation directory for an environment or user-default selection.
Replace --directory with a shell directory change:
# Before
baml --directory ./app check
# After
(cd ./app && baml check)
Use baml --project ./app check to select a source project while keeping the shell working directory. For query, place filesystem --project before the command; query --project selects a cloud project.
toolchain status and list now attempt to install the active selection before continuing. Status can require success with --install. Status JSON uses schema_version: 1; a local-path toolchain reports version: null and is not executed to discover its version. Recorded channels stay on their recorded version until toolchain update. Repair a damaged managed installation explicitly with toolchain install <version> --force.
The wrapper also adds usage telemetry and runtime recording. DO_NOT_TRACK=1 or baml telemetry disable disables usage collection. BAML_TELEMETRY=off disables the BAML wrapper's BCS runtime recording. These control separate recording paths.
(#5426)
Explicit subprocess options and lifetimes
Replace baml.sys.exec with run, start_process with subprocess, Process with Subprocess, and ShellOutput with ProcessOutput. ProcessOptions and StderrMode are removed. shell keeps its name and now uses named options.
Before:
function main() -> string {
let result = baml.sys.exec("printf", ["hello"], baml.sys.ProcessOptions {
cwd: null,
env: null,
timeout_ms: 2000,
stdin: null,
stderr: null,
});
result.stdout.to_string()
}
After:
function main() -> string {
let result = baml.sys.run(
"printf",
args = ["hello"],
timeout = baml.time.Duration.from_seconds(2),
capture_output = true,
);
result.stdout.to_string()
}
Set capture_output = true when you need returned stdout or stderr. Both inherit the parent's streams by default. Use input for complete stdin bytes or text, and timeout with a baml.time.Duration instead of timeout_ms. Nonzero exits return a result normally. ProcessExit.signal is now int?; Unix signal exits have code 128 + signal.
Environment values now overlay the parent environment. Null values remove variables. Set inherit_env = false to preserve the previous replacement behavior.
Background processes require explicit ownership. Subprocess.close(), garbage collection, and cancellation do not kill the child. Add an explicit kill, commonly in defer:
function run_child() -> int {
let child = baml.sys.subprocess("printf", args = ["hello"], stdout = "ignore");
defer { child.kill(); }
child.wait(timeout = baml.time.Duration.from_seconds(2)).exit_code
}
Configure stdin, stdout, or stderr as "pipe" before using the corresponding optional pipe handle. Other modes are "inherit" and "ignore". Flush buffered writes before closing a write pipe. A timeout on Subprocess.wait leaves the child alive; a timeout on foreground run or shell terminates that command. Set detached = true when OS detachment is required.
(#5456)
Custom streaming sources return batches
ai.stream.EventSource returns Event[] | Done instead of Event | Done. Update sources passed to TurnStream.from_event_source.
Before:
function source() -> ai.stream.Event | ai.stream.Done throws ai.errors.InvokeError {
ai.stream.TextDelta { text: "hello" }
}
After:
function source() -> ai.stream.Event[] | ai.stream.Done throws ai.errors.InvokeError {
[ai.stream.TextDelta { text: "hello" }]
}
Return all buffered events together, or wrap a single event in an array. Never return an empty array: wait for an event or return Done. Built-in adapters preserve buffered batches. TurnStream.next() keeps its existing batch return type.
(#5471)
Role markers belong in prompt templates
Interpolating ai.Role into an ordinary string now produces a compiler error. This also applies inside arrays, map values, unions, and aliases. Such markers previously became text instead of starting messages.
Before:
function history(past: string[], ctx: ai.Context) -> string {
let render = prompt`${past.map((p) -> { `${role("user")}${p}` })}`;
render(ctx).text()
}
After:
function history(past: string[], ctx: ai.Context) -> string {
let render = prompt`
${for (let p in past)}
${role("user")}${p}
${endfor}
`;
render(ctx).text()
}
Move role markers into the prompt template's loop. To display a role as ordinary text, use its .name, or .to_string() when you intend a debug representation.
(#5486)
Complete explicit Bedrock credentials
An explicit access key, secret, or session token now requires both key fields. Partial credentials produce an error instead of silently selecting another identity.
Before:
client Model = aws.BedrockClient.new(
model = "anthropic.claude-3-5-haiku-20241022-v1:0",
access_key_id = "example-id",
);
After, to use the normal provider chain:
client Model = aws.BedrockClient.new(
model = "anthropic.claude-3-5-haiku-20241022-v1:0",
);
Alternatively, supply both access_key_id and secret_access_key. Supply session_token too for temporary credentials. Remove every static override when using the provider chain.
Vertex project discovery now respects the selected credential identity. If an explicit credential document has no project ID, set project_id or GOOGLE_CLOUD_PROJECT instead of relying on another identity's ADC file.
(#5494)
Embed a scoped ingestion key explicitly
--embed-telemetry now verifies and embeds the API key you provide. It no longer mints a build token using a saved login or ambient BOUNDARY_API_KEY. Set the project in baml.toml, move the environment setting into the build table, and name the environment variable containing the key.
Before:
[boundary]
project = "acme/app"
[pack]
telemetry_environment = "Production"
baml pack main --embed-telemetry --telemetry-environment Production
After:
[boundary]
project = "acme/app"
[pack.build]
embed_telemetry_env = "Production"
embed_telemetry_api_key_from_env_var = "APP_INGESTION_KEY"
[pack.env_var_names]
BAML_TELEMETRY = "APP_TELEMETRY"
BOUNDARY_API_KEY = "APP_BOUNDARY_API_KEY"
BOUNDARY_PROJECT = "APP_BOUNDARY_PROJECT"
# Set APP_INGESTION_KEY in the build environment.
baml pack main --embed-telemetry
Use a key with write access to exactly one environment and no query permissions. The key is copied into the distributed artifact. For generated SDKs, use [bridge.build] and [bridge.env_var_names] with the same setting names, then run baml generate --embed-telemetry. Regenerate SDKs and rebuild existing executables.
The --telemetry-environment flag and the old pack.telemetry_environment and bridge.telemetry_environment settings are removed. Build-time project selection comes from boundary.project. The server must support the full JSON response from /v1/auth/verify; a legacy empty response cannot verify an embedded key.
Runtime destination overrides require explicit BOUNDARY_API_KEY and BOUNDARY_PROJECT mappings when a key is embedded. The example permits overrides through the APP_ variables. BAML_TELEMETRY can be mapped to a custom variable or set to false to prevent runtime control of the recording level. BOUNDARY_API_URL remains overrideable.
(#5491)
Bug fixes
- Fixed generic function values losing their inferred specialization when returned, passed through SDK handles, or invoked. (#5359)
- Fixed OpenAI Decisions and typesafeai enum results becoming strings when a union also contains an equal string literal. (#5415)
- Fixed native callback failures bypassing a
catchdirectly around the call. (#5414) - Fixed fresh Python bridge installations missing the Pydantic dependency needed by generated SDKs. Refresh the bridge package or lockfile. (#5482)
- Fixed local SDK recordings being written outside the current project. Run SDK code from inside the project so
baml query --localcan find them. A nonemptyBAML_HOMEstill overrides the store; old recordings are not moved. (#5485) - Fixed Go generic decoding of class, enum, and container union fields introduced during this release's bridge changes. Regenerate the SDK to get the decoder hook. (#5511)
- Fixed the Node bridge printing a misleading native-reference leak warning on early process exit. (#5518)
- Routed Rust bridge diagnostics through the standard
logfacade. Configure your application logger and enablebaml_bridge=debugwhen you need bridge debug output. (#5521) - Fixed the new escape analysis treating values reachable by a finalizer as task-local. Those values retain synchronized access while cleanup runs. (#5531)
- Fixed bound-method assignments to class fields, array elements, and map values that could panic during bytecode emission. (#5385)
- Fixed settled-future collection consuming a completed input before honoring the collector's pending cancellation. Ordinary
awaitbehavior is unchanged. (#5420) - Fixed continuation lines beginning with
- valuebeing discarded as separate negation statements. If you intended a separate negation, write-valueor terminate the preceding statement with;. Reformat after upgrading. (#5439) - Fixed false string-termination errors for code-like content inside multi-backtick strings. (#5466)
- Fixed formatting and editor edits changing CRLF files to LF. Compilation normalizes source CRLF and bare CR to LF, including multiline literals. Write
\rexplicitly if a literal must contain a carriage return. (#5467) - Fixed deferred cleanup being skipped on the first termination signal in
baml runand packed binaries. A second signal still terminates immediately. Rebuild existing executables for this behavior. (#5500) - Fixed credential-bearing URLs, headers, and provider error messages exposing sensitive values in diagnostics. Streaming HTTP failures retain their status classification instead of treating every failure as a retry-safe network error. Captured HTTP bodies can still contain application data. (#5494)
- Fixed installer bootstrapping with older wrappers when
toolchain use --helpis unsupported. The installers fall back to plaintoolchain use. (#5516) - Fixed stale agent-skill warnings to show the installed and required versions and the refresh action. Run
baml agent installand reload the skill. The existing validation bypass remains accepted but is hidden from help. (#5480) - Corrected bundled skill examples and client, event, and error names. Examples are now checked for compilation, formatting, and test execution. (#5446)
- Corrected static calls in standard-library examples to include their
baml.namespace. (#5502) - Published stable
@v1mirror references for the setup and check GitHub Actions. Keep usingBoundaryML/setup-baml@v1andBoundaryML/baml-action@v1; their maintained sources now live in this repository. (#5537)
