Skip to content

Commit 9c7800a

Browse files
Add HTTPBody to HTTPResponse (#1684)
<!-- ELLIPSIS_HIDDEN --> > [!IMPORTANT] > Replaces `serde_json::Value` with `HTTPBody` for HTTP response bodies across multiple components, updating handling, logging, and tests. > > - **Behavior**: > - Replaces `serde_json::Value` with `HTTPBody` for `body` in `HTTPResponse` in `events.rs` and `request.rs`. > - Updates `log_http_response()` and `execute_request()` in `request.rs` to use `HTTPBody`. > - Modifies `HTTPResponse` class in Python, Ruby, and TypeScript to use `HTTPBody`. > - **Tests**: > - Updates Python, Ruby, and TypeScript integration tests to handle `HTTPBody` in request and response validation. > - Ensures tests verify JSON parsing and content extraction from `HTTPBody`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for edc4312. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent e5ebaa8 commit 9c7800a

File tree

10 files changed

+81
-58
lines changed

10 files changed

+81
-58
lines changed

engine/baml-lib/baml-types/src/tracing/events.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ pub struct HTTPResponse {
197197
pub request_id: HttpRequestId,
198198
pub status: u16,
199199
pub headers: serde_json::Value,
200-
pub body: serde_json::Value,
200+
pub body: HTTPBody,
201201
}
202202

203203
#[derive(Debug, Serialize, Deserialize)]

engine/baml-runtime/src/internal/llm_client/primitive/request.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async fn log_http_response(
126126
http_request_id: HttpRequestId,
127127
status: u16,
128128
headers: serde_json::Value,
129-
body: serde_json::Value,
129+
body: HTTPBody,
130130
) {
131131
if let Some(span_id) = runtime_context.span_id {
132132
BAML_TRACER.lock().unwrap().put(Arc::new(TraceEvent {
@@ -253,7 +253,7 @@ pub async fn execute_request(
253253
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR)
254254
.as_u16(),
255255
serde_json::Value::Null,
256-
serde_json::Value::String(format!("No response. Error: {:?}", e)),
256+
HTTPBody::new(format!("No response. Error: {:?}", e).into_bytes()),
257257
)
258258
.await;
259259

@@ -295,7 +295,7 @@ pub async fn execute_request(
295295
http_request_id.clone(),
296296
0,
297297
serde_json::Value::Null,
298-
serde_json::Value::String(format!("Could not read response body: {:?}", e)),
298+
HTTPBody::new(format!("Could not read response body: {:?}", e).into_bytes()),
299299
)
300300
.await;
301301
return Err(LLMResponse::LLMFailure(LLMErrorResponse {
@@ -317,15 +317,17 @@ pub async fn execute_request(
317317
Ok(s) if !s.is_empty() => s.to_string(),
318318
_ => "<no response or invalid utf-8>".to_string(),
319319
};
320+
320321
log_http_response(
321322
runtime_context,
322323
TraceLevel::Error,
323324
http_request_id.clone(),
324325
logged_res.status.as_u16(),
325326
json_headers(&logged_res.headers),
326-
serde_json::Value::String(resp_body.clone()),
327+
HTTPBody::new(resp_body.clone().into_bytes()),
327328
)
328329
.await;
330+
329331
return Err(LLMResponse::LLMFailure(LLMErrorResponse {
330332
client: client.context().name.to_string(),
331333
model: None,
@@ -335,8 +337,7 @@ pub async fn execute_request(
335337
latency: instant_now.elapsed(),
336338
message: format!(
337339
"Request failed with status code: {}, \n{}",
338-
logged_res.status,
339-
resp_body.clone()
340+
logged_res.status, resp_body
340341
),
341342
code: ErrorCode::from_status(logged_res.status),
342343
}));
@@ -352,7 +353,7 @@ pub async fn execute_request(
352353
http_request_id.clone(),
353354
0,
354355
serde_json::Value::Null,
355-
serde_json::Value::String(format!("Could not read response body: {:?}", e)),
356+
HTTPBody::new(format!("Could not read response body: {:?}", e).into_bytes()),
356357
)
357358
.await;
358359
return Err(LLMResponse::LLMFailure(LLMErrorResponse {
@@ -380,7 +381,7 @@ pub async fn execute_request(
380381
http_request_id.clone(),
381382
logged_response.status.as_u16(),
382383
json_headers(&logged_response.headers),
383-
json_body(JsonBodyInput::String(resp_body)).unwrap_or_default(),
384+
HTTPBody::new(resp_body.into_bytes()),
384385
)
385386
.await;
386387

engine/language_client_python/python_src/baml_py/baml_py.pyi

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ class BamlRuntime:
174174
cr: Optional[ClientRegistry],
175175
is_stream: bool,
176176
) -> HTTPRequest: ...
177+
def parse_llm_response(
178+
self,
179+
function_name: str,
180+
llm_response: str,
181+
enum_module: Any,
182+
cls_module: Any,
183+
partial_cls_module: Any,
184+
allow_partials: bool,
185+
ctx: RuntimeContextManager,
186+
tb: Optional[TypeBuilder],
187+
cr: Optional[ClientRegistry],
188+
) -> Any: ...
189+
177190

178191
class LogEventMetadata:
179192
event_id: str
@@ -354,7 +367,7 @@ class HTTPResponse:
354367
@property
355368
def headers(self) -> Dict[str, Any]: ...
356369
@property
357-
def body(self) -> Union[Dict[str, Any], str]: ...
370+
def body(self) -> HTTPBody: ...
358371

359372
class ClientRegistry:
360373
def __init__(self) -> None: ...

engine/language_client_python/src/types/response.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use pyo3::{
22
prelude::{pymethods, Py},
33
types::{PyDict, PyDictMethods},
4-
PyObject, PyResult, Python,
4+
PyResult, Python,
55
};
66

7-
use super::log_collector::serde_value_to_py;
7+
use super::request::HTTPBody;
88

99
crate::lang_wrapper!(
1010
HTTPResponse,
@@ -20,7 +20,7 @@ impl HTTPResponse {
2020
"HTTPResponse(status={}, headers={}, body={})",
2121
self.inner.status,
2222
serde_json::to_string_pretty(&self.inner.headers).unwrap(),
23-
serde_json::to_string_pretty(&self.inner.body).unwrap()
23+
serde_json::to_string_pretty(&self.inner.body.as_serde_value()).unwrap()
2424
)
2525
}
2626

@@ -40,9 +40,9 @@ impl HTTPResponse {
4040
Ok(dict.into())
4141
}
4242

43-
// note the body may be an error string, not a dict
4443
#[getter]
45-
pub fn body(&self, py: Python<'_>) -> PyResult<PyObject> {
46-
serde_value_to_py(py, &self.inner.body)
44+
pub fn body(&self) -> HTTPBody {
45+
// TODO: Avoid clone.
46+
HTTPBody::from(self.inner.body.clone())
4747
}
4848
}

engine/language_client_ruby/ext/ruby_ffi/src/types/response.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::Result;
22
use magnus::{class, method, Error, Module, RModule, Ruby};
33

4+
use super::request::HTTPBody;
5+
46
crate::lang_wrapper!(
57
HTTPResponse,
68
"Baml::Ffi::HTTPResponse",
@@ -14,7 +16,7 @@ impl HTTPResponse {
1416
"HTTPResponse(status={}, headers={}, body={})",
1517
self.inner.status,
1618
serde_json::to_string_pretty(&self.inner.headers).unwrap_or_default(),
17-
serde_json::to_string_pretty(&self.inner.body).unwrap_or_default()
19+
serde_json::to_string_pretty(&self.inner.body.as_serde_value()).unwrap_or_default()
1820
)
1921
}
2022

@@ -28,9 +30,9 @@ impl HTTPResponse {
2830
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{:?}", e)))
2931
}
3032

31-
pub fn body(ruby: &Ruby, rb_self: &Self) -> Result<magnus::Value> {
32-
serde_magnus::serialize(&rb_self.inner.body)
33-
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{:?}", e)))
33+
pub fn body(&self) -> HTTPBody {
34+
// TODO: Avoid clone.
35+
HTTPBody::from(self.inner.body.clone())
3436
}
3537

3638
pub fn define_in_ruby(module: &RModule) -> Result<()> {

engine/language_client_typescript/native.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export declare class HttpResponse {
128128
toString(): string
129129
get status(): number
130130
get headers(): object
131-
get body(): any
131+
get body(): HttpBody
132132
}
133133
export type HTTPResponse = HttpResponse
134134

engine/language_client_typescript/src/types/response.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use napi::{bindgen_prelude::Env, JsObject};
22
use napi_derive::napi;
33

4+
use super::request::HTTPBody;
5+
46
crate::lang_wrapper!(
57
HTTPResponse,
68
baml_types::tracing::events::HTTPResponse,
@@ -15,7 +17,7 @@ impl HTTPResponse {
1517
"HTTPResponse(status={}, headers={}, body={})",
1618
self.inner.status,
1719
serde_json::to_string_pretty(&self.inner.headers).unwrap(),
18-
serde_json::to_string_pretty(&self.inner.body).unwrap()
20+
serde_json::to_string_pretty(&self.inner.body.as_serde_value()).unwrap()
1921
)
2022
}
2123

@@ -37,7 +39,8 @@ impl HTTPResponse {
3739
}
3840

3941
#[napi(getter)]
40-
pub fn body(&self) -> napi::Result<serde_json::Value> {
41-
Ok(self.inner.body.clone())
42+
pub fn body(&self) -> HTTPBody {
43+
// TODO: Avoid clone.
44+
HTTPBody::from(self.inner.body.clone())
4245
}
4346
}

integ-tests/python/tests/test_collector.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ async def test_collector_async_no_stream_success():
8282
# Verify http response
8383
response = call.http_response
8484
assert response is not None
85+
response_body = response.body.json()
8586
assert response.status == 200
86-
assert response.body is not None
87-
assert isinstance(response.body, dict)
88-
completion = ChatCompletion(**response.body)
89-
assert "choices" in response.body
90-
assert len(response.body["choices"]) > 0
91-
assert "message" in response.body["choices"][0]
92-
assert "content" in response.body["choices"][0]["message"]
87+
assert response_body is not None
88+
assert isinstance(response_body, dict)
89+
completion = ChatCompletion(**response_body)
90+
assert "choices" in response_body
91+
assert len(response_body["choices"]) > 0
92+
assert "message" in response_body["choices"][0]
93+
assert "content" in response_body["choices"][0]["message"]
9394
assert completion.choices[0].message.content is not None
9495

9596
# Verify call timing

integ-tests/ruby/test_collector.rb

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
assert_equal 0, Baml::Collector.__function_span_count if Baml::Collector.respond_to?(:__function_span_count)
1818
end
1919

20-
after do
20+
after do
2121
# Force garbage collection and check collector is empty
2222
GC.start
2323
# GC.start(full_mark: true, immediate_sweep: true);
@@ -35,7 +35,7 @@
3535
b.TestOpenAIGPT4oMini(input: "hi there", baml_options: {collector: collector})
3636

3737
puts "func called"
38-
38+
3939

4040
puts "#{Baml::Collector.__function_span_count}"
4141
puts "#{Baml::Collector.__print_storage}"
@@ -71,24 +71,26 @@
7171

7272
# Verify request/response
7373
request = call.http_request
74-
refute_nil request
75-
assert_kind_of Hash, request.body
76-
assert_includes request.body, "messages"
77-
assert_includes request.body["messages"][0], "content"
78-
refute_nil request.body["messages"][0]["content"]
79-
assert_equal "gpt-4o-mini", request.body["model"]
74+
body = request.body.json()
75+
refute_nil body
76+
assert_kind_of Hash, body
77+
assert_includes body, "messages"
78+
assert_includes body["messages"][0], "content"
79+
refute_nil body["messages"][0]["content"]
80+
assert_equal "gpt-4o-mini", body["model"]
8081

8182
# Verify http response
8283
response = call.http_response
8384
refute_nil response
85+
body = response.body.json()
8486
assert_equal 200, response.status
85-
refute_nil response.body
86-
assert_kind_of Hash, response.body
87-
assert_includes response.body, "choices"
88-
assert response.body["choices"].length > 0
89-
assert_includes response.body["choices"][0], "message"
90-
assert_includes response.body["choices"][0]["message"], "content"
91-
refute_nil response.body["choices"][0]["message"]["content"]
87+
refute_nil body
88+
assert_kind_of Hash, body
89+
assert_includes body, "choices"
90+
assert body["choices"].length > 0
91+
assert_includes body["choices"][0], "message"
92+
assert_includes body["choices"][0]["message"], "content"
93+
refute_nil body["choices"][0]["message"]["content"]
9294

9395
puts "call.body.headers: #{call.http_response.headers}"
9496
# Verify response headers contain openai-version
@@ -109,7 +111,7 @@
109111
assert call_usage.input_tokens > 0
110112
refute_nil call_usage.output_tokens
111113
assert call_usage.output_tokens > 0
112-
114+
113115
# Matches log usage
114116
assert_equal call_usage.input_tokens, log.usage.input_tokens
115117
assert_equal call_usage.output_tokens, log.usage.output_tokens
@@ -149,7 +151,7 @@
149151
assert_equal 0, function_logs.length
150152

151153
stream = b.stream.TestOpenAIGPT4oMini(input: "hi there", baml_options: {collector: collector})
152-
154+
153155
chunks = []
154156
stream.each do |chunk|
155157
puts "### chunk: #{chunk}"
@@ -158,7 +160,7 @@
158160

159161
res = stream.get_final_response
160162
puts "### res: #{res}"
161-
163+
162164
function_logs = collector.logs
163165
assert_equal 1, function_logs.length
164166

@@ -329,7 +331,7 @@
329331
threads << Thread.new { b.TestOpenAIGPT4oMini(input: "call #1", baml_options: {collector: collector}) }
330332
threads << Thread.new { b.TestOpenAIGPT4oMini(input: "call #2", baml_options: {collector: collector}) }
331333
threads.each(&:join)
332-
334+
333335
puts "------------------------- ended parallel calls"
334336

335337
# Verify the collector has two function logs

integ-tests/typescript/tests/collector.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ describe('Collector Tests', () => {
7373

7474
// Verify http response
7575
const response = call.httpResponse;
76+
const responseBody = response?.body.json();
7677
expect(response).not.toBeNull();
7778
expect(response?.status).toBe(200);
78-
expect(response?.body).not.toBeNull();
79-
expect(response?.body.choices).toBeDefined();
80-
expect(response?.body.choices.length).toBeGreaterThan(0);
81-
expect(response?.body.choices[0].message.content).not.toBeNull();
79+
expect(responseBody).not.toBeNull();
80+
expect(responseBody?.choices).toBeDefined();
81+
expect(responseBody?.choices.length).toBeGreaterThan(0);
82+
expect(responseBody?.choices[0].message.content).not.toBeNull();
8283

8384
// Verify call timing
8485
const callTiming = call.timing;
@@ -157,7 +158,7 @@ describe('Collector Tests', () => {
157158
const request = call.httpRequest;
158159
expect(request).not.toBeNull();
159160
expect(typeof request?.body).toBe('object');
160-
expect((request?.body.json() as any).messages).toBeDefined();
161+
expect((request?.body.json()).messages).toBeDefined();
161162

162163
// For streaming, httpResponse might be null since it's streaming
163164
const response = call.httpResponse;
@@ -292,13 +293,13 @@ describe('Collector Tests', () => {
292293
it('should handle sync calls correctly', async () => {
293294
const collector = new Collector("sync-collector");
294295
const result = b_sync.TestOpenAIGPT4oMini("sync call", { collector });
295-
296+
296297
const logs = collector.logs;
297298
expect(logs.length).toBe(1);
298299
expect(logs[0].functionName).toBe("TestOpenAIGPT4oMini");
299300
expect(logs[0].logType).toBe("call");
300301
expect(logs[0].usage).not.toBeNull();
301302
});
302-
303-
303+
304+
304305
});

0 commit comments

Comments
 (0)