Skip to content

fix(bridges): more streaming work (test infra, ffi fixes)#3768

Merged
sxlijin merged 5 commits into
canaryfrom
sam/replay-llm
Jun 16, 2026
Merged

fix(bridges): more streaming work (test infra, ffi fixes)#3768
sxlijin merged 5 commits into
canaryfrom
sam/replay-llm

Conversation

@sxlijin

@sxlijin sxlijin commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
  • add tests to exercise outbound streaming in python and TS (more now works)
  • set up a new crate sdk_test_llm_recordings that allow powering streaming tests with recorded traces
  • hacky patch to recover TypeVar bindings for instances of generic classes (this only works for simple instances; nested generics likely break)
  • add the ability to create chunked HTTP responses
  • in node, handle decoding for ADT_TAGGED_HEAP_HANDLE (necessary for LLM streams)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added HTTP streaming response support with new_streaming(), write(), and end() for chunked server output.
    • Added keyless deterministic replay infrastructure for LLM streaming validation via checked-in recordings.
  • Tests

    • Reworked streaming end-to-end tests to run against replayed SSE recordings (no live API keys required), including new replay-server endpoint checks and additional class/doc streaming assertions.
    • Added an automated Rust SSE snapshot harness for replay fixtures.
  • Bug Fixes

    • Improved Node.js stream value transfer/decoding for tagged heap handles to prevent downstream stream failures.
  • Chores

    • Renamed streaming helper functions from PascalCase to snake_case.

@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 16, 2026 11:19pm
promptfiddle Ready Ready Preview, Comment Jun 16, 2026 11:19pm
promptfiddle2 Ready Ready Preview, Comment Jun 16, 2026 11:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a keyless LLM streaming replay harness: BAML HTTP Response gains new_streaming/write/end methods backed by a native mpsc-channel streaming body. A BAML replay server replays committed .snap.sse fixtures. A Rust insta-based crate records and validates SSE snapshots. Python and TypeScript test suites are rewritten to run against the replay server without API keys. The BEX engine gains generic type-var specialization for bound methods, and the Node.js bridge fixes tagged-heap-handle wire encoding and decoding.

Changes

LLM SSE Streaming Replay Harness

Layer / File(s) Summary
BAML HTTP streaming response API and platform implementations
baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml, baml_language/crates/sys_native/src/http_server.rs, baml_language/crates/sys_native/src/io_impls.rs, baml_language/crates/sys_ops/src/lib.rs, baml_language/crates/bridge_wasm/src/wasm_http.rs, baml_language/crates/baml_lsp_server/src/playground_http.rs
Response gains new_streaming, write, and end in the BAML stdlib. HttpBody::Streaming backed by an mpsc channel is added natively; wire_response emits WireBody (BoxBody) for both buffered and streaming outputs. The three new ops are wired into IoSysOpsBuilder and stubbed as HostUnavailable in WASM and LSP playground targets.
BEX engine generic type-var specialization for bound methods
baml_language/crates/bex_engine/src/conversion.rs, baml_language/crates/bex_engine/src/lib.rs
Adds collect_type_var_bindings, substitute_type_vars, and tagged_handle_runtime_ty. Updates call_function_bound_args and call_callable to resolve declared type variables from the concrete receiver's type args, fixing return-type coercion for generic bound methods.
Node.js bridge: BamlStream wire encoding and tagged-heap-handle decode
baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
setInboundValue clones the tagged heap handle key for BamlStream wire transfer. decodeValueHolder now constructs the typed generated class for ADT_TAGGED_HEAP_HANDLE via the typemap instead of falling back to bare handle handling.
BAML replay server and StreamStub client update
baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml, baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml, ...ns_lorem/types.baml
Adds ReplayHandler, replay_serve_until_shutdown, and replay_serve_detached. Reconfigures StreamStub to use BAML_REPLAY_API_KEY/BAML_REPLAY_BASE_URL. Renames streaming e2e BAML functions to snake_case and updates companion $stream invocations.
Committed SSE snapshot fixtures and documentation
baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore, ...recordings/README.md, ...recordings/replay_extract_string.snap.sse, ...recordings/replay_extract_doc.snap.sse
Gitignores insta transient artifacts. README documents recording file roles, insta-controlled network behavior, re-recording workflows, and header redaction guarantees. Populates complete chat.completion.chunk SSE streams as replay payloads.
Rust insta SSE recording harness crate
baml_language/Cargo.toml, baml_language/sdk_tests/harness/llm_recordings/Cargo.toml, ...harness/llm_recordings/src/lib.rs, ...harness/llm_recordings/data/replay_server.baml, ...harness/llm_recordings/tests/recordings.rs, ...harness/llm_recordings/tests/transport_pin.rs
New sdk_test_llm_recordings crate. recordings.rs compiles BAML fixtures, invokes $build_request_stream with replay env vars, captures live SSE via curl or reads checked-in files, validates SSE well-formedness, and asserts insta binary snapshots. transport_pin.rs exercises the BAML engine SSE roundtrip end-to-end.
Python replay harness and keyless streaming tests
baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py, ...customizable/test_streaming_e2e.py, ...customizable/test_streaming_class_e2e.py, ...customizable/test_main.py
Adds replay_harness.py with a context manager that spawns a replay server daemon thread, polls for the bound address, and injects replay env vars. Rewrites test_streaming_e2e.py and test_streaming_class_e2e.py as keyless @replay_server-decorated tests. Adds binding-availability checks to test_main.py.
TypeScript replay harness and keyless streaming tests
baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts, ...customizable/streaming_e2e.test.ts
Adds replay_harness.ts with withReplayServer that starts a detached server, sets process.env, runs the test body, and shuts down cooperatively. Rewrites streaming_e2e.test.ts against the lowercase lorem.stream_e2e_* APIs with next()/final() and BAML-driven collect assertions.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    note over TestSuite,ReplayHandler: Test setup
    TestSuite->>replay_serve_detached: call(recording_path)
    replay_serve_detached-->>TestSuite: bound addr (host:port)
    TestSuite->>TestSuite: set BAML_REPLAY_BASE_URL, BAML_REPLAY_API_KEY
  end

  rect rgba(60, 179, 113, 0.5)
    note over TestSuite,HttpBodyStreaming: Streaming call execution
    TestSuite->>stream_e2e_extract: invoke via BAML SDK
    stream_e2e_extract->>ReplayHandler: POST /chat/completions (BAML_REPLAY_BASE_URL)
    ReplayHandler->>SnapSSEFile: read .snap.sse fixture
    ReplayHandler->>HttpBodyStreaming: new_streaming → write chunks → end
    HttpBodyStreaming-->>stream_e2e_extract: SSE frames streamed via WireBody
    stream_e2e_extract-->>TestSuite: stream partials + final value
  end

  rect rgba(210, 105, 30, 0.5)
    note over TestSuite,ReplayHandler: Teardown
    TestSuite->>ReplayHandler: POST /__replay__/shutdown
    ReplayHandler->>replay_serve_detached: cancel token → reaper cancels serve task
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3508: The main PR's bex_engine/src/conversion.rs adds tagged_handle_runtime_ty/type-var substitution helpers that specifically operate on the new BexExternalAdt::TaggedHeapHandle plumbing introduced in the retrieved PR, so the changes are directly connected at the TaggedHeapHandle conversion/type-handling layer.
  • BoundaryML/baml#3717: The main PR's HTTP streaming-response changes (e.g., sys_native/src/http_server.rs adding/altering HttpBody streaming support and wiring through Response.new_streaming/write/end, plus related IO stubs) are directly tied to the retrieved PR's foundational baml.http.Server/HTTP server implementation work in the same modules.

Poem

🐇 Hippity-hop, no key in my paw,
The SSE fixtures replace what I saw!
ReplayHandler streams with a chunky delight,
insta snaps the responses so tests stay bright.
No curl at runtime—just playback for free,
A deterministic bunny 🎉 for you and for me!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(bridges): more streaming work (test infra, ffi fixes)' directly reflects the main changes: implementing streaming test infrastructure and fixing FFI-related issues across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sam/replay-llm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

Copy link
Copy Markdown

No description provided.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py`:
- Around line 95-125: The fixture has two lifecycle issues: startup failures
before yield skip the shutdown path, and environment variables are set without
restoration. Restructure the fixture to wrap the entire startup logic (the
deadline loop checking for error and addr_file, and the environment variable
assignments at lines 111-112) inside the try block before yield, so that the
finally block always executes even on startup timeouts or errors. Additionally,
before setting BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY environment
variables, save their original values using os.environ.get(), and in the finally
block restore them using os.environ.pop() or by setting them back to their
original values to prevent environment pollution across tests.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py`:
- Around line 262-264: The stream-draining loops at lines 262-264 and 349-350
that call ReplayStreamExtractDoc_stream and check for StreamFinished have no
termination bounds, which means if stream completion regresses, these tests will
hang indefinitely instead of failing fast. Add iteration limits or timeouts to
both loop locations to ensure the tests fail quickly if the stream never
completes, for example by wrapping the loop with a maximum iteration counter or
using a timeout mechanism that breaks after a reasonable number of attempts.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts`:
- Around line 95-110: The servePromise from ReplayServeUntilShutdown_async is
started but never monitored during the address file polling loop. If the serve
task fails early, that error remains undetected and the code continues polling
until the 10-second timeout expires, obscuring the actual failure. Modify the
polling loop to check if servePromise has rejected and immediately throw that
error rather than continuing to wait for the timeout. This can be done by using
Promise.race or by checking the promise's settlement status within each loop
iteration before checking the address file.
- Around line 118-124: The teardown code in the afterEach block awaits both the
shutdown fetch call to http://${h.addr}/__replay__/shutdown and the
h.servePromise without timeout limits, which can cause the test suite to hang
indefinitely if either operation gets stuck. Add timeout constraints to both
await statements to ensure they fail fast and allow the test suite to continue.
Use Promise.race or a similar timeout mechanism (such as Promise.all with
AbortSignal or setTimeout-based timeout wrappers) to enforce reasonable time
limits on both the fetch operation and the servePromise resolution.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs`:
- Around line 259-269: The curl command being constructed in the external
capture call lacks a timeout configuration, which can cause the test to hang
indefinitely if the network or provider stalls. Add an explicit timeout argument
to the curl command by including a cmd.arg() call with curl's timeout option
(such as "--max-time" followed by an appropriate timeout duration in seconds)
before the cmd.output().expect("spawn curl") call executes the command.
- Around line 138-143: Locate the `capture_via_curl()` function and add timeout
flags to its curl invocation to prevent CI hangs on network stalls.
Specifically, add `--max-time` or `--connect-timeout` flags to the curl command
to enforce a reasonable timeout limit. Additionally, note that if the test
harness transitions from single-threaded to multi-threaded execution in the
future, the environment variable mutations at the env::set_var calls (for
BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY) should be protected by a Mutex to
prevent race conditions across threads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0f2da415-8ba0-4a99-ac0f-855d98317649

📥 Commits

Reviewing files that changed from the base of the PR and between 55b4b6b and 9504cc2.

⛔ Files ignored due to path filters (7)
  • baml_language/Cargo.lock is excluded by !**/*.lock
  • baml_language/crates/baml_tests/snapshots/baml_src/_root.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/baml_src/replay_server.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc__request.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string__request.snap is excluded by !**/*.snap
📒 Files selected for processing (17)
  • baml_language/Cargo.toml
  • baml_language/crates/baml_tests/baml_src/ns_replay_server/replay_server.baml
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/src/lib.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs

Comment on lines +262 to +264
stream = ReplayStreamExtractDoc_stream("ignored-by-replay-server")
while not isinstance(stream.next(), StreamFinished):
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add termination guards to detached/async stream-drain loops

Line 263 and Line 349 drain until StreamFinished without a bound. If stream completion regresses, these tests can hang the suite instead of failing fast.

Suggested fix
@@
-        stream = ReplayStreamExtractDoc_stream("ignored-by-replay-server")
-        while not isinstance(stream.next(), StreamFinished):
-            pass
+        stream = ReplayStreamExtractDoc_stream("ignored-by-replay-server")
+        iterations = 0
+        while not isinstance(stream.next(), StreamFinished):
+            iterations += 1
+            assert iterations < 10_000, "detached stream.next() failed to terminate"
@@
-        stream = await ReplayStreamExtractDoc_stream_async("ignored-by-replay-server")
-        while not isinstance(await stream.next_async(), StreamFinished):
-            pass
+        stream = await ReplayStreamExtractDoc_stream_async("ignored-by-replay-server")
+        iterations = 0
+        while not isinstance(await stream.next_async(), StreamFinished):
+            iterations += 1
+            assert iterations < 10_000, "async stream.next_async() failed to terminate"

Also applies to: 349-350

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py`
around lines 262 - 264, The stream-draining loops at lines 262-264 and 349-350
that call ReplayStreamExtractDoc_stream and check for StreamFinished have no
termination bounds, which means if stream completion regresses, these tests will
hang indefinitely instead of failing fast. Add iteration limits or timeouts to
both loop locations to ensure the tests fail quickly if the stream never
completes, for example by wrapping the loop with a maximum iteration counter or
using a timeout mechanism that breaks after a reasonable number of attempts.

Comment on lines +95 to +110
const servePromise = replay.ReplayServeUntilShutdown_async(rec, addrFile);

let addr: string | undefined;
const deadline = performance.now() + 10_000;
while (performance.now() < deadline) {
if (fs.existsSync(addrFile)) {
const text = fs.readFileSync(addrFile, "utf8").trim();
if (text) {
addr = text;
break;
}
}
await sleep(20);
}
if (!addr) throw new Error("replay server did not bind within 10s");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Surface serve-task failures during bind polling (don’t wait for the 10s timeout).

Line 95 starts ReplayServeUntilShutdown_async, but Lines 97-109 only poll the addr-file. If the serve task rejects early, this path still waits until timeout and obscures the real failure.

Proposed fix
   const servePromise = replay.ReplayServeUntilShutdown_async(rec, addrFile);
+  let serveError: unknown;
+  servePromise.catch((e) => {
+    serveError = e;
+  });

   let addr: string | undefined;
   const deadline = performance.now() + 10_000;
   while (performance.now() < deadline) {
+    if (serveError) {
+      throw new Error(`replay server failed before bind: ${String(serveError)}`);
+    }
     if (fs.existsSync(addrFile)) {
       const text = fs.readFileSync(addrFile, "utf8").trim();
       if (text) {
         addr = text;
         break;
       }
     }
     await sleep(20);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const servePromise = replay.ReplayServeUntilShutdown_async(rec, addrFile);
let addr: string | undefined;
const deadline = performance.now() + 10_000;
while (performance.now() < deadline) {
if (fs.existsSync(addrFile)) {
const text = fs.readFileSync(addrFile, "utf8").trim();
if (text) {
addr = text;
break;
}
}
await sleep(20);
}
if (!addr) throw new Error("replay server did not bind within 10s");
const servePromise = replay.ReplayServeUntilShutdown_async(rec, addrFile);
let serveError: unknown;
servePromise.catch((e) => {
serveError = e;
});
let addr: string | undefined;
const deadline = performance.now() + 10_000;
while (performance.now() < deadline) {
if (serveError) {
throw new Error(`replay server failed before bind: ${String(serveError)}`);
}
if (fs.existsSync(addrFile)) {
const text = fs.readFileSync(addrFile, "utf8").trim();
if (text) {
addr = text;
break;
}
}
await sleep(20);
}
if (!addr) throw new Error("replay server did not bind within 10s");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts`
around lines 95 - 110, The servePromise from ReplayServeUntilShutdown_async is
started but never monitored during the address file polling loop. If the serve
task fails early, that error remains undetected and the code continues polling
until the 10-second timeout expires, obscuring the actual failure. Modify the
polling loop to check if servePromise has rejected and immediately throw that
error rather than continuing to wait for the timeout. This can be done by using
Promise.race or by checking the promise's settlement status within each loop
iteration before checking the address file.

Comment on lines +118 to +124
await fetch(`http://${h.addr}/__replay__/shutdown`, { method: "POST", body: "" });
} catch {
/* tolerate the response/cancel race */
}
try {
await h.servePromise;
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound teardown waits with timeouts to prevent suite hangs.

Lines 118-124 await both shutdown fetch and servePromise without timeout limits. A stuck local HTTP call or unresolved serve task can hang afterEach and stall the test run.

Proposed fix
 async function stopReplayServer(h: ReplayHandle): Promise<void> {
   try {
-    await fetch(`http://${h.addr}/__replay__/shutdown`, { method: "POST", body: "" });
+    await fetch(`http://${h.addr}/__replay__/shutdown`, {
+      method: "POST",
+      body: "",
+      signal: AbortSignal.timeout(5_000),
+    });
   } catch {
     /* tolerate the response/cancel race */
   }
   try {
-    await h.servePromise;
+    await Promise.race([
+      h.servePromise,
+      sleep(10_000).then(() => {
+        throw new Error("timed out waiting for replay serve task to finish");
+      }),
+    ]);
   } catch {
     /* serve task cancelled */
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts`
around lines 118 - 124, The teardown code in the afterEach block awaits both the
shutdown fetch call to http://${h.addr}/__replay__/shutdown and the
h.servePromise without timeout limits, which can cause the test suite to hang
indefinitely if either operation gets stuck. Add timeout constraints to both
await statements to ensure they fail fast and allow the test suite to continue.
Use Promise.race or a similar timeout mechanism (such as Promise.all with
AbortSignal or setTimeout-based timeout wrappers) to enforce reasonable time
limits on both the fetch operation and the servePromise resolution.

Comment on lines +138 to +143
// `env.VAR` in the client options desugars to `baml.env.get_or_panic`,
// read when the client is instantiated inside the builder — so set them
// before running. (Process-global; nextest isolates each test in its own
// process. Under plain `cargo test` these would race across threads.)
env::set_var("BAML_REPLAY_BASE_URL", OPENAI_BASE_URL);
env::set_var("BAML_REPLAY_API_KEY", api_key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add timeout to the curl invocation to prevent CI hangs.

The capture_via_curl() function at lines 335 makes an external network call without timeout flags, which can block CI indefinitely on network stalls or provider latency. Add --max-time or --connect-timeout to prevent hanging.

Regarding lines 142–143: The current test configuration uses #[tokio::test] with default single-threaded runtime, so env var mutations do not create practical concurrency issues. If multi-threaded execution is planned, serialization via a Mutex would be necessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs` around
lines 138 - 143, Locate the `capture_via_curl()` function and add timeout flags
to its curl invocation to prevent CI hangs on network stalls. Specifically, add
`--max-time` or `--connect-timeout` flags to the curl command to enforce a
reasonable timeout limit. Additionally, note that if the test harness
transitions from single-threaded to multi-threaded execution in the future, the
environment variable mutations at the env::set_var calls (for
BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY) should be protected by a Mutex to
prevent race conditions across threads.

Comment on lines +259 to +269
cmd.arg("--silent")
.arg("--no-buffer")
.arg("--fail-with-body")
.arg("-X")
.arg("POST")
.arg(&req.url);
for (k, v) in &req.headers {
cmd.arg("-H").arg(format!("{k}: {v}"));
}
cmd.arg("--data").arg(&req.body);
let out = cmd.output().expect("spawn curl");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an explicit curl timeout to prevent indefinite hangs in recording mode.

The external capture call has no timeout, so a stalled network/provider can block this test job indefinitely.

Suggested fix
 fn capture_via_curl(req: &RequestParts) -> Vec<u8> {
     let mut cmd = Command::new("curl");
     cmd.arg("--silent")
         .arg("--no-buffer")
+        .arg("--connect-timeout").arg("10")
+        .arg("--max-time").arg("120")
         .arg("--fail-with-body")
         .arg("-X")
         .arg("POST")
         .arg(&req.url);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cmd.arg("--silent")
.arg("--no-buffer")
.arg("--fail-with-body")
.arg("-X")
.arg("POST")
.arg(&req.url);
for (k, v) in &req.headers {
cmd.arg("-H").arg(format!("{k}: {v}"));
}
cmd.arg("--data").arg(&req.body);
let out = cmd.output().expect("spawn curl");
cmd.arg("--silent")
.arg("--no-buffer")
.arg("--connect-timeout").arg("10")
.arg("--max-time").arg("120")
.arg("--fail-with-body")
.arg("-X")
.arg("POST")
.arg(&req.url);
for (k, v) in &req.headers {
cmd.arg("-H").arg(format!("{k}: {v}"));
}
cmd.arg("--data").arg(&req.body);
let out = cmd.output().expect("spawn curl");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs` around
lines 259 - 269, The curl command being constructed in the external capture call
lacks a timeout configuration, which can cause the test to hang indefinitely if
the network or provider stalls. Add an explicit timeout argument to the curl
command by including a cmd.arg() call with curl's timeout option (such as
"--max-time" followed by an appropriate timeout duration in seconds) before the
cmd.output().expect("spawn curl") call executes the command.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs (1)

223-236: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit curl timeouts to prevent hanging recording jobs.

capture_via_curl calls an external provider without connection/overall timeout bounds, so network stalls can block this test indefinitely.

Suggested fix
 fn capture_via_curl(req: &RequestParts) -> Vec<u8> {
     let mut cmd = Command::new("curl");
     cmd.arg("--silent")
         .arg("--no-buffer")
+        .arg("--connect-timeout")
+        .arg("10")
+        .arg("--max-time")
+        .arg("120")
         .arg("--fail-with-body")
         .arg("-X")
         .arg(&req.method)
         .arg(&req.url);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs` around
lines 223 - 236, The `capture_via_curl` function executes curl without timeout
constraints, which can cause the test to hang indefinitely during network
stalls. Add explicit timeout arguments to the curl command before executing it
(before the cmd.output() call). Use curl's --connect-timeout flag for connection
establishment timeout and --max-time flag for overall operation timeout to
ensure the command terminates within a reasonable timeframe even if the external
provider is unresponsive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs`:
- Around line 223-236: The `capture_via_curl` function executes curl without
timeout constraints, which can cause the test to hang indefinitely during
network stalls. Add explicit timeout arguments to the curl command before
executing it (before the cmd.output() call). Use curl's --connect-timeout flag
for connection establishment timeout and --max-time flag for overall operation
timeout to ensure the command terminates within a reasonable timeframe even if
the external provider is unresponsive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d33c0614-2d81-4f11-af07-99439f23d755

📥 Commits

Reviewing files that changed from the base of the PR and between 9504cc2 and 62a52c7.

⛔ Files ignored due to path filters (1)
  • baml_language/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/data/replay_server.baml
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/transport_pin.rs
💤 Files with no reviewable changes (1)
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
✅ Files skipped from review due to trivial changes (2)
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
🚧 Files skipped from review as they are similar to previous changes (3)
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py (1)

131-156: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move startup polling inside the try block to guarantee cleanup.

The polling loop (lines 131-146) executes before the try block at line 156. If startup times out or error is raised (lines 134-135, 142-146), the finally block never runs, leaving the thread un-joined and env vars unrestored.

🔧 Proposed fix
     t = threading.Thread(target=_serve, daemon=True)
     t.start()
 
-    addr = None
-    deadline = time.time() + 10.0
-    while time.time() < deadline:
-        if error:
-            raise RuntimeError(f"replay server thread failed: {error[0]!r}")
-        if addr_file.exists():
-            text = addr_file.read_text().strip()
-            if text:
-                addr = text
-                break
-        time.sleep(0.02)
-    if addr is None:
-        raise TimeoutError(
-            f"replay server did not bind within 10s "
-            f"(thread alive={t.is_alive()}, error={error!r})"
-        )
-
     # Save + restore the client env so a torn-down server's (dead) address
     # never leaks into a later test that forgot to set it.
     saved = {
         k: os.environ.get(k)
         for k in ("BAML_REPLAY_BASE_URL", "BAML_REPLAY_API_KEY")
     }
-    os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
-    os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
     try:
+        addr = None
+        deadline = time.time() + 10.0
+        while time.time() < deadline:
+            if error:
+                raise RuntimeError(f"replay server thread failed: {error[0]!r}")
+            if addr_file.exists():
+                text = addr_file.read_text().strip()
+                if text:
+                    addr = text
+                    break
+            time.sleep(0.02)
+        if addr is None:
+            raise TimeoutError(
+                f"replay server did not bind within 10s "
+                f"(thread alive={t.is_alive()}, error={error!r})"
+            )
+
+        os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
+        os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
         yield addr
     finally:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py`
around lines 131 - 156, The startup polling loop that waits for the replay
server to bind (starting with the while loop checking time.time() < deadline and
ending with the TimeoutError raise) is currently positioned before the try
block. This means if a RuntimeError is raised due to the thread failing, or if a
TimeoutError is raised when the server doesn't bind within 10 seconds, the
finally block will never execute, leaving the thread un-joined and environment
variables unrestored. Move the entire polling section (the while loop and the
subsequent TimeoutError check) inside the try block so that any startup failures
will guarantee the cleanup code in the finally block runs.
♻️ Duplicate comments (3)
baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs (1)

208-228: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeout flags to prevent curl from hanging indefinitely.

The curl command lacks --connect-timeout and --max-time flags. Network stalls or provider latency can block test execution indefinitely.

⏱️ Proposed fix
 fn capture_via_curl(req: &RequestParts) -> Vec<u8> {
     let mut cmd = Command::new("curl");
     cmd.arg("--silent")
         .arg("--no-buffer")
+        .arg("--connect-timeout").arg("10")
+        .arg("--max-time").arg("120")
         .arg("--fail-with-body")
         .arg("-X")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs` around
lines 208 - 228, The capture_via_curl function's curl command lacks timeout
protection, which can cause tests to hang indefinitely during network stalls or
provider latency. Add --connect-timeout and --max-time flags to the curl command
builder in the capture_via_curl function to set both a connection timeout and a
maximum overall request time, placing these arguments with the other cmd.arg()
calls before executing cmd.output().
baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py (2)

199-200: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add iteration bound to prevent infinite loop in asyncio mode test.

The while loop draining stream.next_async() has no iteration counter. If stream completion regresses, the test will hang indefinitely instead of failing fast.

🔒 Proposed fix
         stream = await StreamE2EExtractDoc_stream_async("ignored-by-replay-server")
-        while not isinstance(await stream.next_async(), StreamFinished):
-            pass
+        iterations = 0
+        while not isinstance(await stream.next_async(), StreamFinished):
+            iterations += 1
+            assert iterations < 10_000, "async stream.next_async() failed to terminate"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py`
around lines 199 - 200, The while loop draining stream.next_async() in the test
lacks an iteration bound, which will cause the test to hang indefinitely if
stream completion regresses. Add a counter variable to track the number of
iterations and establish a reasonable maximum iteration limit. If the counter
exceeds this limit before encountering StreamFinished, raise an exception or
assertion failure to fail the test fast instead of hanging. This ensures the
test properly detects regressions in stream completion behavior.

119-121: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add iteration bound to prevent infinite loop in detached mode test.

The while loop draining stream.next() has no iteration counter. If stream completion regresses, the test will hang indefinitely instead of failing fast.

🔒 Proposed fix
         stream = StreamE2EExtractDoc_stream("ignored-by-replay-server")
-        while not isinstance(stream.next(), StreamFinished):
-            pass
+        iterations = 0
+        while not isinstance(stream.next(), StreamFinished):
+            iterations += 1
+            assert iterations < 10_000, "detached stream.next() failed to terminate"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py`
around lines 119 - 121, The while loop in the test_streaming_replay function
that drains stream.next() calls has no iteration limit, which will cause the
test to hang indefinitely if the stream fails to complete properly. Add an
iteration counter before the while loop and increment it on each iteration, then
assert or raise an exception if the counter exceeds a reasonable maximum value
(e.g., 1000) before StreamFinished is received. This ensures the test fails fast
with a clear error message rather than hanging when stream completion regresses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py`:
- Around line 102-103: Add iteration bounds to prevent infinite hangs in the
three while loops that drain streams until StreamFinished is received. For each
while loop that calls stream.next() without bounds (one at lines 102-103, one at
lines 115-116, and one at lines 137-138), introduce a counter variable
initialized to zero and increment it on each iteration. Set a reasonable maximum
limit (such as 1000 iterations) and assert or raise an exception if the counter
exceeds this limit before StreamFinished is received. This ensures tests fail
fast with a clear error message rather than hanging indefinitely if stream
completion regresses.
- Around line 81-82: The while loop that drains the stream by repeatedly calling
stream.next_async() lacks an iteration counter, which means the test will hang
indefinitely if stream completion regresses instead of failing quickly. Add an
iteration counter to the loop that tracks how many times stream.next_async() is
called and raise an exception or assertion failure if a reasonable maximum
iteration limit is exceeded without the stream returning a StreamFinished
instance.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py`:
- Around line 181-182: The test file has two `while` loops (at lines 181-182 and
lines 228-229) that drain streams by calling stream.next() until receiving a
StreamFinished event, but they lack iteration limits. If stream completion
regresses, these loops will hang indefinitely instead of failing fast. Add
iteration counters to both loops with a reasonable maximum iteration limit
(e.g., 1000 iterations), and raise an assertion error or fail the test if the
limit is exceeded before StreamFinished is received. This ensures the tests
timeout quickly during failures rather than hanging.

---

Outside diff comments:
In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py`:
- Around line 131-156: The startup polling loop that waits for the replay server
to bind (starting with the while loop checking time.time() < deadline and ending
with the TimeoutError raise) is currently positioned before the try block. This
means if a RuntimeError is raised due to the thread failing, or if a
TimeoutError is raised when the server doesn't bind within 10 seconds, the
finally block will never execute, leaving the thread un-joined and environment
variables unrestored. Move the entire polling section (the while loop and the
subsequent TimeoutError check) inside the try block so that any startup failures
will guarantee the cleanup code in the finally block runs.

---

Duplicate comments:
In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py`:
- Around line 199-200: The while loop draining stream.next_async() in the test
lacks an iteration bound, which will cause the test to hang indefinitely if
stream completion regresses. Add a counter variable to track the number of
iterations and establish a reasonable maximum iteration limit. If the counter
exceeds this limit before encountering StreamFinished, raise an exception or
assertion failure to fail the test fast instead of hanging. This ensures the
test properly detects regressions in stream completion behavior.
- Around line 119-121: The while loop in the test_streaming_replay function that
drains stream.next() calls has no iteration limit, which will cause the test to
hang indefinitely if the stream fails to complete properly. Add an iteration
counter before the while loop and increment it on each iteration, then assert or
raise an exception if the counter exceeds a reasonable maximum value (e.g.,
1000) before StreamFinished is received. This ensures the test fails fast with a
clear error message rather than hanging when stream completion regresses.

In `@baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs`:
- Around line 208-228: The capture_via_curl function's curl command lacks
timeout protection, which can cause tests to hang indefinitely during network
stalls or provider latency. Add --connect-timeout and --max-time flags to the
curl command builder in the capture_via_curl function to set both a connection
timeout and a maximum overall request time, placing these arguments with the
other cmd.arg() calls before executing cmd.output().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bdbe12af-5fed-40ab-b9b8-3c8a1a106f69

📥 Commits

Reviewing files that changed from the base of the PR and between 28ffbb2 and a0ad198.

📒 Files selected for processing (10)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/conftest.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_replay.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_e2e.test.ts
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs
✅ Files skipped from review due to trivial changes (1)
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_replay.test.ts

…riven streams)

Host-driven $stream handles failed to round-trip on the Node bridge because the inbound ADT_TAGGED_HEAP_HANDLE tag had no decode arm. Decode it via the typemap so Stream handles cross the bridge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@sxlijin sxlijin changed the title Sam/replay llm fix: streaming Jun 16, 2026
sxlijin and others added 3 commits June 16, 2026 15:44
Add incremental server responses to baml.http.Server: Response.new_streaming/.write/.end send the body in chunks (chunked transfer-encoding, channel-backed with backpressure), implemented across baml_std, sys_native, sys_ops, bridge_wasm, and the playground proxy. Regenerate baml_std snapshots for the new stdlib functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-call return boundary

A generic instance method invoked from the host (e.g. Stream.next, by name via call_function_bound_args or by handle via call_callable) kept its declared return-type class type vars unsubstituted, so a concrete value (a stream partial) matched no member of a union like TStream | StreamFinished and the FFI return conversion panicked.

Recover TypeVar->concrete bindings by zipping the declared self param type against the receiver concrete self (the inbound TaggedHeapHandle Class args, or the seeded class_type_args) and substitute the declared return type before conversion. New conversion.rs helpers: collect_type_var_bindings, substitute_type_vars, tagged_handle_runtime_ty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tures)

Recorder crate that captures real provider SSE via curl into insta binary snapshots, plus the checked-in recordings and an engine-level SSE transport pin. Registers the crate in the workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Python (conftest/replay/e2e/class-e2e) and TypeScript (replay/e2e) streaming tests driving the BAML-implemented keyless replay server, plus the StreamE2E fixtures and the env-driven replay server fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/bex_engine/src/lib.rs (1)

1867-1902: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Substitute bound class type vars in throws_type too.

run_entry_point also uses throws_type to type escaping throws. Leaving it generic preserves the same unbound-TypeVar failure for methods whose throws clause mentions the receiver’s class type params.

Apply the same bindings to `throws_type`
-        let throws_type = self.function_throws_type(function_name);
+        let mut throws_type = self.function_throws_type(function_name);
@@
                 crate::conversion::collect_type_var_bindings(
                     self_declared,
                     self_actual,
                     &mut bindings,
                 );
                 return_type = crate::conversion::substitute_type_vars(&return_type, &bindings);
+                if let Some(throws_type) = throws_type.as_mut() {
+                    *throws_type =
+                        crate::conversion::substitute_type_vars(throws_type, &bindings);
+                }
             }
         }
@@
-        let (mut return_type, throws_type, arity, param_types) =
+        let (mut return_type, mut throws_type, arity, param_types) =
             match thread.vm.get_object(func_ptr) {
@@
                 for (declared, concrete) in declared_args.iter().zip(seed_type_args.iter()) {
                     crate::conversion::collect_type_var_bindings(declared, concrete, &mut bindings);
                 }
                 return_type = crate::conversion::substitute_type_vars(&return_type, &bindings);
+                if let Some(throws_type) = throws_type.as_mut() {
+                    *throws_type =
+                        crate::conversion::substitute_type_vars(throws_type, &bindings);
+                }
             }
         }

Also applies to: 2154-2192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_engine/src/lib.rs` around lines 1867 - 1902, The
code collects type variable bindings from the receiver and applies them to the
`return_type` through the `crate::conversion::substitute_type_vars` function,
but fails to apply the same substitution to `throws_type`. This causes throws
clauses that reference the receiver's class type parameters to remain unbound.
Apply the same `crate::conversion::substitute_type_vars` substitution to
`throws_type` using the collected `bindings` HashMap in the same conditional
block where `return_type` is being substituted, ensuring both return and throws
types are properly specialized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml`:
- Around line 113-116: The _timeout_nanos function collapses two distinct
timeout states into the same value: the null case returns 0n for "no timeout"
and a zero Duration also returns 0n when to_nanoseconds() is called, making
fetch or send with a zero timeout run unbounded instead of timing out
immediately. To fix this, change the null pattern match case in _timeout_nanos
to return a negative value (such as -1n) instead of 0n to represent "no
deadline", while allowing zero Duration objects to return 0n for an actual
zero-duration timeout. Then update the native timeout decoding logic that
interprets these values to treat negative values as "no deadline" and preserve 0
as a real zero-duration timeout.

In `@baml_language/crates/bex_engine/src/conversion.rs`:
- Around line 762-783: The Class branch in collect_type_var_bindings silently
processes mismatched classes without verifying they are the same class and have
the same arity, and the Union branch silently truncates when member counts
differ. Add guard checks in both branches to validate class identity and ensure
matching arity in the Class case (comparing the class identifiers and the
lengths of the type argument vectors), and ensure matching union member counts
in the Union case before proceeding with zip iteration. Add a unit test for
collect_type_var_bindings that covers both mismatched class scenarios and
mismatched union scenarios to verify the guards work correctly, then run cargo
test --lib to confirm all tests pass.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`:
- Line 76: After the thread.join(timeout=10) call in the teardown, add a
verification check to ensure the replay server thread has actually terminated.
If the thread is still alive after the timeout expires, raise an exception to
fail fast instead of silently continuing, which would allow a leaked daemon
server to potentially cause flaky tests. Use the thread's is_alive() method to
check termination status and handle the case where the shutdown stalls.
- Around line 67-80: The teardown code in the finally block unconditionally
deletes BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY environment variables,
which corrupts test state if these variables were already set before entering
the harness. Save the original values of these environment variables before
setting them at the start of the context manager, and then restore them to their
original values in the finally block instead of unconditionally popping them. If
a variable had a prior value, restore it; if it didn't exist before, remove it
from the environment.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts`:
- Around line 43-55: The `withReplayServer` function unconditionally deletes the
replay environment variables without preserving their original values, which can
leak state across tests if those variables were preconfigured. Before setting
`process.env.BAML_REPLAY_BASE_URL` and `process.env.BAML_REPLAY_API_KEY`, store
their original values (if they exist). In the finally block, instead of using
delete operations on these variables, restore them to their original values (or
delete them only if they weren't previously set).

---

Outside diff comments:
In `@baml_language/crates/bex_engine/src/lib.rs`:
- Around line 1867-1902: The code collects type variable bindings from the
receiver and applies them to the `return_type` through the
`crate::conversion::substitute_type_vars` function, but fails to apply the same
substitution to `throws_type`. This causes throws clauses that reference the
receiver's class type parameters to remain unbound. Apply the same
`crate::conversion::substitute_type_vars` substitution to `throws_type` using
the collected `bindings` HashMap in the same conditional block where
`return_type` is being substituted, ensuring both return and throws types are
properly specialized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab6bbc4d-1d59-40a0-8de4-54d557a9e7fb

📥 Commits

Reviewing files that changed from the base of the PR and between 28ffbb2 and 013d4dd.

⛔ Files ignored due to path filters (14)
  • baml_language/Cargo.lock is excluded by !**/*.lock
  • baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap is excluded by !**/*.snap
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • baml_language/Cargo.toml
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml
  • baml_language/crates/baml_lsp_server/src/playground_http.rs
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bridge_wasm/src/wasm_http.rs
  • baml_language/crates/sys_native/src/http_server.rs
  • baml_language/crates/sys_native/src/io_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_e2e.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/data/replay_server.baml
  • baml_language/sdk_tests/harness/llm_recordings/src/lib.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/transport_pin.rs
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
💤 Files with no reviewable changes (1)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
✅ Files skipped from review due to trivial changes (6)
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
  • baml_language/sdk_tests/harness/llm_recordings/src/lib.rs
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
🚧 Files skipped from review as they are similar to previous changes (6)
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/tests/transport_pin.rs
  • baml_language/sdk_tests/harness/llm_recordings/data/replay_server.baml
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/bex_engine/src/lib.rs (1)

1867-1902: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Substitute bound class type vars in throws_type too.

run_entry_point also uses throws_type to type escaping throws. Leaving it generic preserves the same unbound-TypeVar failure for methods whose throws clause mentions the receiver’s class type params.

Apply the same bindings to `throws_type`
-        let throws_type = self.function_throws_type(function_name);
+        let mut throws_type = self.function_throws_type(function_name);
@@
                 crate::conversion::collect_type_var_bindings(
                     self_declared,
                     self_actual,
                     &mut bindings,
                 );
                 return_type = crate::conversion::substitute_type_vars(&return_type, &bindings);
+                if let Some(throws_type) = throws_type.as_mut() {
+                    *throws_type =
+                        crate::conversion::substitute_type_vars(throws_type, &bindings);
+                }
             }
         }
@@
-        let (mut return_type, throws_type, arity, param_types) =
+        let (mut return_type, mut throws_type, arity, param_types) =
             match thread.vm.get_object(func_ptr) {
@@
                 for (declared, concrete) in declared_args.iter().zip(seed_type_args.iter()) {
                     crate::conversion::collect_type_var_bindings(declared, concrete, &mut bindings);
                 }
                 return_type = crate::conversion::substitute_type_vars(&return_type, &bindings);
+                if let Some(throws_type) = throws_type.as_mut() {
+                    *throws_type =
+                        crate::conversion::substitute_type_vars(throws_type, &bindings);
+                }
             }
         }

Also applies to: 2154-2192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_engine/src/lib.rs` around lines 1867 - 1902, The
code collects type variable bindings from the receiver and applies them to the
`return_type` through the `crate::conversion::substitute_type_vars` function,
but fails to apply the same substitution to `throws_type`. This causes throws
clauses that reference the receiver's class type parameters to remain unbound.
Apply the same `crate::conversion::substitute_type_vars` substitution to
`throws_type` using the collected `bindings` HashMap in the same conditional
block where `return_type` is being substituted, ensuring both return and throws
types are properly specialized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml`:
- Around line 113-116: The _timeout_nanos function collapses two distinct
timeout states into the same value: the null case returns 0n for "no timeout"
and a zero Duration also returns 0n when to_nanoseconds() is called, making
fetch or send with a zero timeout run unbounded instead of timing out
immediately. To fix this, change the null pattern match case in _timeout_nanos
to return a negative value (such as -1n) instead of 0n to represent "no
deadline", while allowing zero Duration objects to return 0n for an actual
zero-duration timeout. Then update the native timeout decoding logic that
interprets these values to treat negative values as "no deadline" and preserve 0
as a real zero-duration timeout.

In `@baml_language/crates/bex_engine/src/conversion.rs`:
- Around line 762-783: The Class branch in collect_type_var_bindings silently
processes mismatched classes without verifying they are the same class and have
the same arity, and the Union branch silently truncates when member counts
differ. Add guard checks in both branches to validate class identity and ensure
matching arity in the Class case (comparing the class identifiers and the
lengths of the type argument vectors), and ensure matching union member counts
in the Union case before proceeding with zip iteration. Add a unit test for
collect_type_var_bindings that covers both mismatched class scenarios and
mismatched union scenarios to verify the guards work correctly, then run cargo
test --lib to confirm all tests pass.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`:
- Line 76: After the thread.join(timeout=10) call in the teardown, add a
verification check to ensure the replay server thread has actually terminated.
If the thread is still alive after the timeout expires, raise an exception to
fail fast instead of silently continuing, which would allow a leaked daemon
server to potentially cause flaky tests. Use the thread's is_alive() method to
check termination status and handle the case where the shutdown stalls.
- Around line 67-80: The teardown code in the finally block unconditionally
deletes BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY environment variables,
which corrupts test state if these variables were already set before entering
the harness. Save the original values of these environment variables before
setting them at the start of the context manager, and then restore them to their
original values in the finally block instead of unconditionally popping them. If
a variable had a prior value, restore it; if it didn't exist before, remove it
from the environment.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts`:
- Around line 43-55: The `withReplayServer` function unconditionally deletes the
replay environment variables without preserving their original values, which can
leak state across tests if those variables were preconfigured. Before setting
`process.env.BAML_REPLAY_BASE_URL` and `process.env.BAML_REPLAY_API_KEY`, store
their original values (if they exist). In the finally block, instead of using
delete operations on these variables, restore them to their original values (or
delete them only if they weren't previously set).

---

Outside diff comments:
In `@baml_language/crates/bex_engine/src/lib.rs`:
- Around line 1867-1902: The code collects type variable bindings from the
receiver and applies them to the `return_type` through the
`crate::conversion::substitute_type_vars` function, but fails to apply the same
substitution to `throws_type`. This causes throws clauses that reference the
receiver's class type parameters to remain unbound. Apply the same
`crate::conversion::substitute_type_vars` substitution to `throws_type` using
the collected `bindings` HashMap in the same conditional block where
`return_type` is being substituted, ensuring both return and throws types are
properly specialized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab6bbc4d-1d59-40a0-8de4-54d557a9e7fb

📥 Commits

Reviewing files that changed from the base of the PR and between 28ffbb2 and 013d4dd.

⛔ Files ignored due to path filters (14)
  • baml_language/Cargo.lock is excluded by !**/*.lock
  • baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap is excluded by !**/*.snap
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap is excluded by !**/*.snap
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.d.ts.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js is excluded by !**/dist/**
  • baml_language/sdks/nodejs/bridge_nodejs/dist/proto.js.map is excluded by !**/dist/**, !**/*.map
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • baml_language/Cargo.toml
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml
  • baml_language/crates/baml_lsp_server/src/playground_http.rs
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/src/lib.rs
  • baml_language/crates/bridge_wasm/src/wasm_http.rs
  • baml_language/crates/sys_native/src/http_server.rs
  • baml_language/crates/sys_native/src/io_impls.rs
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_e2e.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/data/replay_server.baml
  • baml_language/sdk_tests/harness/llm_recordings/src/lib.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs
  • baml_language/sdk_tests/harness/llm_recordings/tests/transport_pin.rs
  • baml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto.ts
💤 Files with no reviewable changes (1)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
✅ Files skipped from review due to trivial changes (6)
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/.gitignore
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
  • baml_language/sdk_tests/harness/llm_recordings/src/lib.rs
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/README.md
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_doc.snap.sse
  • baml_language/sdk_tests/fixtures/llm_functions/recordings/replay_extract_string.snap.sse
🚧 Files skipped from review as they are similar to previous changes (6)
  • baml_language/sdk_tests/harness/llm_recordings/Cargo.toml
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/Cargo.toml
  • baml_language/sdk_tests/harness/llm_recordings/tests/transport_pin.rs
  • baml_language/sdk_tests/harness/llm_recordings/data/replay_server.baml
  • baml_language/sdk_tests/harness/llm_recordings/tests/recordings.rs
🛑 Comments failed to post (5)
baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml (1)

113-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Zero-duration timeouts are silently converted into “no timeout.”

Line [115] encodes null as 0n, but Line [116] also yields 0n when callers pass a zero Duration. That collapses two distinct states and can make fetch(..., 0) / send(..., 0) run unbounded instead of timing out immediately.

Suggested contract-safe direction
 function _timeout_nanos(timeout: root.time.Duration?) -> bigint throws never {
   match (timeout) {
-    null => 0n,
+    null => -1n,
     let t: root.time.Duration => t.to_nanoseconds(),
   }
 }

Then update native timeout decoding to interpret < 0 as “no deadline,” preserving 0 as a real zero-duration timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_builtins2/baml_std/baml/ns_http/http.baml` around
lines 113 - 116, The _timeout_nanos function collapses two distinct timeout
states into the same value: the null case returns 0n for "no timeout" and a zero
Duration also returns 0n when to_nanoseconds() is called, making fetch or send
with a zero timeout run unbounded instead of timing out immediately. To fix
this, change the null pattern match case in _timeout_nanos to return a negative
value (such as -1n) instead of 0n to represent "no deadline", while allowing
zero Duration objects to return 0n for an actual zero-duration timeout. Then
update the native timeout decoding logic that interprets these values to treat
negative values as "no deadline" and preserve 0 as a real zero-duration timeout.
baml_language/crates/bex_engine/src/conversion.rs (1)

762-783: ⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and locate the function
cat -n baml_language/crates/bex_engine/src/conversion.rs | sed -n '740,800p'

Repository: BoundaryML/baml

Length of output: 3295


🏁 Script executed:

# Search for the function definition
rg -n "fn collect_type_var_bindings" baml_language/crates/bex_engine/src/conversion.rs -A 50

Repository: BoundaryML/baml

Length of output: 2388


🏁 Script executed:

# Check if there are tests in the same file or test modules
rg -n "#\[test\]|#\[cfg\(test\)\]" baml_language/crates/bex_engine/src/conversion.rs

Repository: BoundaryML/baml

Length of output: 435


🏁 Script executed:

# Look for test files related to conversion
fd "test" baml_language/crates/bex_engine/src/ | head -20

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Check what RuntimeTy looks like to understand the structure
rg -n "enum RuntimeTy|struct RuntimeTy" baml_language/crates/bex_engine/src/ -A 10

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Search for RuntimeTy definition
rg "enum RuntimeTy" baml_language/crates/bex_engine/src/ -A 40 | head -80

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Check what calls collect_type_var_bindings and how it's used
rg "collect_type_var_bindings" baml_language/crates/bex_engine/src/ -B 2 -A 2

Repository: BoundaryML/baml

Length of output: 4848


🏁 Script executed:

# Look at the test modules to understand what is tested
sed -n '1723,1800p' baml_language/crates/bex_engine/src/conversion.rs

Repository: BoundaryML/baml

Length of output: 2780


🏁 Script executed:

# Check the comment about intended use and understand the logic
sed -n '740,790p' baml_language/crates/bex_engine/src/conversion.rs

Repository: BoundaryML/baml

Length of output: 2255


🏁 Script executed:

# Look for RuntimeTy::Class definition to understand its structure
rg "Class\(" baml_language/crates/bex_engine/src/ -B 2 -A 2 | head -40

Repository: BoundaryML/baml

Length of output: 3149


🏁 Script executed:

# Check if there are type checking constraints before calling collect_type_var_bindings
sed -n '1,100p' baml_language/crates/bex_engine/src/lib.rs | grep -A 20 "collect_type_var_bindings"

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Look for where seed_type_args and declared_args come from in lib.rs
rg "seed_type_args|declared_args" baml_language/crates/bex_engine/src/lib.rs -B 5 -A 2

Repository: BoundaryML/baml

Length of output: 1605


🏁 Script executed:

# Check if there are any guards in the calling code before collect_type_var_bindings is invoked
rg -B 10 "collect_type_var_bindings" baml_language/crates/bex_engine/src/lib.rs | head -60

Repository: BoundaryML/baml

Length of output: 1648


🏁 Script executed:

# Search for any existing tests of collect_type_var_bindings
rg -n "collect_type_var_bindings" baml_language/crates/bex_engine/src/conversion.rs | grep -i test

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Look at the test modules at the end of conversion.rs to see what functions are tested
sed -n '1847,2000p' baml_language/crates/bex_engine/src/conversion.rs

Repository: BoundaryML/baml

Length of output: 4673


🏁 Script executed:

# Check substitute_type_vars to understand the full flow
rg -n "fn substitute_type_vars" baml_language/crates/bex_engine/src/conversion.rs -A 30

Repository: BoundaryML/baml

Length of output: 1274


🏁 Script executed:

# Verify if there are any assertions or validation of class matching before collect_type_var_bindings is called
rg -B 20 "collect_type_var_bindings" baml_language/crates/bex_engine/src/lib.rs | grep -A 20 "self_declared\|declared_args"

Repository: BoundaryML/baml

Length of output: 1420


🏁 Script executed:

# Look at where self_declared comes from and what guarantees exist
rg -B 30 "self_declared" baml_language/crates/bex_engine/src/lib.rs | head -80

Repository: BoundaryML/baml

Length of output: 2153


🏁 Script executed:

# Check how param_types is constructed to see if there are any type-checking constraints
rg -n "param_types" baml_language/crates/bex_engine/src/lib.rs -B 3 -A 1 | head -40

Repository: BoundaryML/baml

Length of output: 1893


🏁 Script executed:

# Look for RuntimeTy definition to understand Class structure
fd RuntimeTy baml_language/crates/ | head -5

Repository: BoundaryML/baml

Length of output: 41


🏁 Script executed:

# Find the actual RuntimeTy enum definition
rg "enum RuntimeTy" baml_language/crates/ -A 50 | head -100

Repository: BoundaryML/baml

Length of output: 4326


🏁 Script executed:

# Check what function_params returns and how it's guaranteed to match
rg -n "function_params" baml_language/crates/bex_engine/src/lib.rs -A 5 | head -40

Repository: BoundaryML/baml

Length of output: 1097


🏁 Script executed:

# Look at the definition of tagged_handle_runtime_ty to understand what it returns
rg -n "fn tagged_handle_runtime_ty" baml_language/crates/bex_engine/src/conversion.rs -A 20

Repository: BoundaryML/baml

Length of output: 1209


🏁 Script executed:

# Check if there are any assertions or validation when creating Class instances
rg "RuntimeTy::Class\(" baml_language/crates/bex_engine/src/ -B 2 -A 2 | head -60

Repository: BoundaryML/baml

Length of output: 5259


🏁 Script executed:

# Look at validate_bound_args to see if it ensures class matching
rg -n "fn validate_bound_args" baml_language/crates/bex_engine/src/lib.rs -A 30

Repository: BoundaryML/baml

Length of output: 1293


🏁 Script executed:

# Check what happens when classes don't match - is there any error handling upstream?
rg -n "TypeMismatch|class.*mismatch|type.*mismatch" baml_language/crates/bex_engine/src/lib.rs -i | head -20

Repository: BoundaryML/baml

Length of output: 586


🏁 Script executed:

# Verify the context where collect_type_var_bindings is called to understand if there's validation
sed -n '1880,1910p' baml_language/crates/bex_engine/src/lib.rs

Repository: BoundaryML/baml

Length of output: 1748


🏁 Script executed:

# Check the run_entry_point context more carefully
rg -n "fn run_entry_point" baml_language/crates/bex_engine/src/lib.rs -A 50 | head -80

Repository: BoundaryML/baml

Length of output: 2485


🏁 Script executed:

# Check if there's any type validation when tagged_handle_runtime_ty is used
rg -B 10 "tagged_handle_runtime_ty" baml_language/crates/bex_engine/src/lib.rs | grep -A 10 "self_declared"

Repository: BoundaryML/baml

Length of output: 293


🏁 Script executed:

# Look for how the self_actual is obtained and if there are any guarantees
sed -n '1895,1905p' baml_language/crates/bex_engine/src/lib.rs

Repository: BoundaryML/baml

Length of output: 498


🏁 Script executed:

# Check if there are existing tests in conversion.rs
sed -n '1723,1850p' baml_language/crates/bex_engine/src/conversion.rs

Repository: BoundaryML/baml

Length of output: 4072


🏁 Script executed:

# Examine if collect_type_var_bindings is currently tested
rg "collect_type_var_bindings" baml_language/crates/bex_engine/src/conversion.rs -A 2 -B 2 | grep -i test

Repository: BoundaryML/baml

Length of output: 41


Add guards for class identity and aggregate arity in collect_type_var_bindings.

The Class branch (line 762) ignores class names and relies on zip to handle arity, silently binding type vars from mismatched classes. Similarly, the Union branch (line 779) silently truncates on member-count mismatch. Without identity/arity checks, a mismatched or malformed receiver can cause substitute_type_vars to rewrite the return type incorrectly.

Add the following guards:

Suggested changes
-        (RuntimeTy::Class(_, da, _), RuntimeTy::Class(_, ca, _)) => {
-            for (d, c) in da.iter().zip(ca.iter()) {
-                collect_type_var_bindings(d, c, out);
+        (RuntimeTy::Class(dn, da, _), RuntimeTy::Class(cn, ca, _)) => {
+            if dn == cn && da.len() == ca.len() {
+                for (d, c) in da.iter().zip(ca.iter()) {
+                    collect_type_var_bindings(d, c, out);
+                }
             }
         }
@@
-        (RuntimeTy::Union(dm, _), RuntimeTy::Union(cm, _)) => {
-            for (d, c) in dm.iter().zip(cm.iter()) {
-                collect_type_var_bindings(d, c, out);
+        (RuntimeTy::Union(dm, _), RuntimeTy::Union(cm, _)) => {
+            if dm.len() == cm.len() {
+                for (d, c) in dm.iter().zip(cm.iter()) {
+                    collect_type_var_bindings(d, c, out);
+                }
             }
         }

Per the coding guidelines for **/*.rs files, add a unit test for this function and run cargo test --lib to confirm all tests pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/bex_engine/src/conversion.rs` around lines 762 - 783,
The Class branch in collect_type_var_bindings silently processes mismatched
classes without verifying they are the same class and have the same arity, and
the Union branch silently truncates when member counts differ. Add guard checks
in both branches to validate class identity and ensure matching arity in the
Class case (comparing the class identifiers and the lengths of the type argument
vectors), and ensure matching union member counts in the Union case before
proceeding with zip iteration. Add a unit test for collect_type_var_bindings
that covers both mismatched class scenarios and mismatched union scenarios to
verify the guards work correctly, then run cargo test --lib to confirm all tests
pass.

Source: Coding guidelines

baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py (2)

67-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore prior replay env values instead of always deleting them

This teardown clears BAML_REPLAY_BASE_URL / BAML_REPLAY_API_KEY unconditionally, which can corrupt surrounding test state when these vars were already set before entering the harness.

Proposed fix
 def _running_server(recording: str):
@@
-    os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
-    os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
+    prev_base_url = os.environ.get("BAML_REPLAY_BASE_URL")
+    prev_api_key = os.environ.get("BAML_REPLAY_API_KEY")
+    os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
+    os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
@@
-        for key in ("BAML_REPLAY_BASE_URL", "BAML_REPLAY_API_KEY"):
-            os.environ.pop(key, None)
+        if prev_base_url is None:
+            os.environ.pop("BAML_REPLAY_BASE_URL", None)
+        else:
+            os.environ["BAML_REPLAY_BASE_URL"] = prev_base_url
+
+        if prev_api_key is None:
+            os.environ.pop("BAML_REPLAY_API_KEY", None)
+        else:
+            os.environ["BAML_REPLAY_API_KEY"] = prev_api_key
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    prev_base_url = os.environ.get("BAML_REPLAY_BASE_URL")
    prev_api_key = os.environ.get("BAML_REPLAY_API_KEY")
    os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
    os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
    try:
        yield addr
    finally:
        with contextlib.suppress(Exception):
            urllib.request.urlopen(
                f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
            )
        thread.join(timeout=10)
        with contextlib.suppress(FileNotFoundError):
            addr_file.unlink()
        if prev_base_url is None:
            os.environ.pop("BAML_REPLAY_BASE_URL", None)
        else:
            os.environ["BAML_REPLAY_BASE_URL"] = prev_base_url

        if prev_api_key is None:
            os.environ.pop("BAML_REPLAY_API_KEY", None)
        else:
            os.environ["BAML_REPLAY_API_KEY"] = prev_api_key
🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 73-73: Do not make http calls without encryption
Context: f"http://{addr}/replay/shutdown"
Note: [CWE-319].

(requests-http)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`
around lines 67 - 80, The teardown code in the finally block unconditionally
deletes BAML_REPLAY_BASE_URL and BAML_REPLAY_API_KEY environment variables,
which corrupts test state if these variables were already set before entering
the harness. Save the original values of these environment variables before
setting them at the start of the context manager, and then restore them to their
original values in the finally block instead of unconditionally popping them. If
a variable had a prior value, restore it; if it didn't exist before, remove it
from the environment.

76-76: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast if replay server thread does not actually stop

After timed join, teardown does not verify thread termination. If shutdown stalls, a leaked daemon server can make later tests flaky.

Proposed fix
-        thread.join(timeout=10)
+        thread.join(timeout=10)
+        if thread.is_alive():
+            raise TimeoutError("replay server thread did not shut down within 10s")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        thread.join(timeout=10)
        if thread.is_alive():
            raise TimeoutError("replay server thread did not shut down within 10s")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`
at line 76, After the thread.join(timeout=10) call in the teardown, add a
verification check to ensure the replay server thread has actually terminated.
If the thread is still alive after the timeout expires, raise an exception to
fail fast instead of silently continuing, which would allow a leaked daemon
server to potentially cause flaky tests. Use the thread's is_alive() method to
check termination status and handle the case where the shutdown stalls.
baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts (1)

43-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve and restore prior replay env values in teardown

withReplayServer always deletes replay env vars, which can leak state across tests when those vars were preconfigured before this wrapper executes.

Proposed fix
 export function withReplayServer(
   recording: string,
   body: () => void | Promise<void>,
 ): () => Promise<void> {
   return async () => {
     const addr = replay_serve_detached(recordingPath(recording));
+    const prevBaseUrl = process.env.BAML_REPLAY_BASE_URL;
+    const prevApiKey = process.env.BAML_REPLAY_API_KEY;
     process.env.BAML_REPLAY_BASE_URL = `http://${addr}`;
     process.env.BAML_REPLAY_API_KEY = "replay-test-key";
     try {
       await body();
     } finally {
@@
-      delete process.env.BAML_REPLAY_BASE_URL;
-      delete process.env.BAML_REPLAY_API_KEY;
+      if (prevBaseUrl === undefined) {
+        delete process.env.BAML_REPLAY_BASE_URL;
+      } else {
+        process.env.BAML_REPLAY_BASE_URL = prevBaseUrl;
+      }
+      if (prevApiKey === undefined) {
+        delete process.env.BAML_REPLAY_API_KEY;
+      } else {
+        process.env.BAML_REPLAY_API_KEY = prevApiKey;
+      }
     }
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts`
around lines 43 - 55, The `withReplayServer` function unconditionally deletes
the replay environment variables without preserving their original values, which
can leak state across tests if those variables were preconfigured. Before
setting `process.env.BAML_REPLAY_BASE_URL` and
`process.env.BAML_REPLAY_API_KEY`, store their original values (if they exist).
In the finally block, instead of using delete operations on these variables,
restore them to their original values (or delete them only if they weren't
previously set).

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`:
- Around line 52-80: The cleanup block in the finally clause will be skipped if
an exception occurs before the yield statement (such as in the deadline loop or
at the TimeoutError), leaving the server thread and temp file behind.
Additionally, the environment variable cleanup using os.environ.pop discards
pre-existing values instead of restoring them. Store the original values of
"BAML_REPLAY_BASE_URL" and "BAML_REPLAY_API_KEY" before setting them, and
restore these saved values in the finally block instead of using pop. This
ensures cleanup always runs and environment variables are properly restored to
their original state regardless of when an exception occurs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0ccce4a7-1209-41fc-a90e-78b9256115cd

📥 Commits

Reviewing files that changed from the base of the PR and between 013d4dd and 0a6510c.

⛔ Files ignored due to path filters (1)
  • baml_language/sdks/python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_e2e.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml
💤 Files with no reviewable changes (1)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_class_e2e.py
✅ Files skipped from review due to trivial changes (1)
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/types.baml
🚧 Files skipped from review as they are similar to previous changes (6)
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_main.py
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_replay/replay_server.baml
  • baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_streaming_e2e.py
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/replay_harness.ts
  • baml_language/sdk_tests/crates/typescript_node/llm_functions/customizable/streaming_e2e.test.ts
  • baml_language/sdk_tests/fixtures/llm_functions/baml_src/ns_lorem/functions.baml

Comment on lines +52 to +80
thread = threading.Thread(target=_serve, daemon=True)
thread.start()

addr = None
deadline = time.time() + 10.0
while time.time() < deadline:
if error:
raise RuntimeError(f"replay server thread failed: {error[0]!r}")
if addr_file.exists() and (text := addr_file.read_text().strip()):
addr = text
break
time.sleep(0.02)
if addr is None:
raise TimeoutError("replay server did not bind within 10s")

os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
try:
yield addr
finally:
with contextlib.suppress(Exception):
urllib.request.urlopen(
f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
)
thread.join(timeout=10)
with contextlib.suppress(FileNotFoundError):
addr_file.unlink()
for key in ("BAML_REPLAY_BASE_URL", "BAML_REPLAY_API_KEY"):
os.environ.pop(key, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure teardown runs on startup failure and restore prior env values.

Line 59 and Line 65 can raise before yield, so the cleanup block at Line 72-80 is skipped; that can leave the server thread/temp file behind. Also, popping env keys at Line 79-80 discards any pre-existing values instead of restoring them.

Proposed fix
@@
-    thread = threading.Thread(target=_serve, daemon=True)
-    thread.start()
-
-    addr = None
-    deadline = time.time() + 10.0
-    while time.time() < deadline:
-        if error:
-            raise RuntimeError(f"replay server thread failed: {error[0]!r}")
-        if addr_file.exists() and (text := addr_file.read_text().strip()):
-            addr = text
-            break
-        time.sleep(0.02)
-    if addr is None:
-        raise TimeoutError("replay server did not bind within 10s")
-
-    os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
-    os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
-    try:
-        yield addr
-    finally:
-        with contextlib.suppress(Exception):
-            urllib.request.urlopen(
-                f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
-            )
-        thread.join(timeout=10)
-        with contextlib.suppress(FileNotFoundError):
-            addr_file.unlink()
-        for key in ("BAML_REPLAY_BASE_URL", "BAML_REPLAY_API_KEY"):
-            os.environ.pop(key, None)
+    thread = threading.Thread(target=_serve, daemon=True)
+    thread.start()
+    addr = None
+    previous_env = {
+        "BAML_REPLAY_BASE_URL": os.environ.get("BAML_REPLAY_BASE_URL"),
+        "BAML_REPLAY_API_KEY": os.environ.get("BAML_REPLAY_API_KEY"),
+    }
+    try:
+        deadline = time.time() + 10.0
+        while time.time() < deadline:
+            if error:
+                raise RuntimeError(f"replay server thread failed: {error[0]!r}")
+            if addr_file.exists() and (text := addr_file.read_text().strip()):
+                addr = text
+                break
+            time.sleep(0.02)
+        if addr is None:
+            raise TimeoutError("replay server did not bind within 10s")
+
+        os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
+        os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
+        yield addr
+    finally:
+        if addr is not None:
+            with contextlib.suppress(Exception):
+                urllib.request.urlopen(
+                    f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
+                )
+        thread.join(timeout=10)
+        with contextlib.suppress(FileNotFoundError):
+            addr_file.unlink()
+        for key, old_value in previous_env.items():
+            if old_value is None:
+                os.environ.pop(key, None)
+            else:
+                os.environ[key] = old_value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
thread = threading.Thread(target=_serve, daemon=True)
thread.start()
addr = None
deadline = time.time() + 10.0
while time.time() < deadline:
if error:
raise RuntimeError(f"replay server thread failed: {error[0]!r}")
if addr_file.exists() and (text := addr_file.read_text().strip()):
addr = text
break
time.sleep(0.02)
if addr is None:
raise TimeoutError("replay server did not bind within 10s")
os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
try:
yield addr
finally:
with contextlib.suppress(Exception):
urllib.request.urlopen(
f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
)
thread.join(timeout=10)
with contextlib.suppress(FileNotFoundError):
addr_file.unlink()
for key in ("BAML_REPLAY_BASE_URL", "BAML_REPLAY_API_KEY"):
os.environ.pop(key, None)
thread = threading.Thread(target=_serve, daemon=True)
thread.start()
addr = None
previous_env = {
"BAML_REPLAY_BASE_URL": os.environ.get("BAML_REPLAY_BASE_URL"),
"BAML_REPLAY_API_KEY": os.environ.get("BAML_REPLAY_API_KEY"),
}
try:
deadline = time.time() + 10.0
while time.time() < deadline:
if error:
raise RuntimeError(f"replay server thread failed: {error[0]!r}")
if addr_file.exists() and (text := addr_file.read_text().strip()):
addr = text
break
time.sleep(0.02)
if addr is None:
raise TimeoutError("replay server did not bind within 10s")
os.environ["BAML_REPLAY_BASE_URL"] = f"http://{addr}"
os.environ["BAML_REPLAY_API_KEY"] = "replay-test-key"
yield addr
finally:
if addr is not None:
with contextlib.suppress(Exception):
urllib.request.urlopen(
f"http://{addr}/__replay__/shutdown", data=b"", timeout=5
)
thread.join(timeout=10)
with contextlib.suppress(FileNotFoundError):
addr_file.unlink()
for key, old_value in previous_env.items():
if old_value is None:
os.environ.pop(key, None)
else:
os.environ[key] = old_value
🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 66-66: Do not make http calls without encryption
Context: f"http://{addr}"
Note: [CWE-319].

(requests-http)


[warning] 73-73: Do not make http calls without encryption
Context: f"http://{addr}/replay/shutdown"
Note: [CWE-319].

(requests-http)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/replay_harness.py`
around lines 52 - 80, The cleanup block in the finally clause will be skipped if
an exception occurs before the yield statement (such as in the deadline loop or
at the TimeoutError), leaving the server thread and temp file behind.
Additionally, the environment variable cleanup using os.environ.pop discards
pre-existing values instead of restoring them. Store the original values of
"BAML_REPLAY_BASE_URL" and "BAML_REPLAY_API_KEY" before setting them, and
restore these saved values in the finally block instead of using pop. This
ensures cleanup always runs and environment variables are properly restored to
their original state regardless of when an exception occurs.

@sxlijin sxlijin added this pull request to the merge queue Jun 16, 2026
Merged via the queue into canary with commit e7a66f0 Jun 16, 2026
52 checks passed
@sxlijin sxlijin deleted the sam/replay-llm branch June 16, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant