Skip to content

Commit 18ef4e4

Browse files
authored
feat: responses API integration (#2103)
- This PR adds support for the [Responses API](https://platform.openai.com/docs/api-reference/responses) by OpenAI by adding a new provider called `openai-responses`
1 parent 7eb1450 commit 18ef4e4

67 files changed

Lines changed: 33373 additions & 14311 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"Bash(uv run:*)",
99
"Bash(find:*)",
1010
"Bash(rg:*)",
11-
"Bash(cargo check:*)"
11+
"Bash(cargo check:*)",
12+
"Bash(cargo build:*)"
1213
],
1314
"deny": []
1415
}

engine/baml-lib/llm-client/src/clients/helpers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,14 @@ impl<Meta: Clone> PropertyHandler<Meta> {
332332
if let StringOr::Value(value) = value {
333333
Some(match value.as_str() {
334334
"openai" => UnresolvedResponseType::OpenAI,
335+
"openai-responses" => UnresolvedResponseType::OpenAIResponses,
335336
"anthropic" => UnresolvedResponseType::Anthropic,
336337
"google" => UnresolvedResponseType::Google,
337338
"vertex" => UnresolvedResponseType::Vertex,
338339
other => {
339340
self.push_error(
340341
format!(
341-
"client_response_type must be one of \"openai\", \"anthropic\", \"google\", or \"vertex\". Got: {other}"
342+
"client_response_type must be one of \"openai\", \"openai-responses\", \"anthropic\", \"google\", or \"vertex\". Got: {other}"
342343
),
343344
key_span,
344345
);
@@ -347,7 +348,7 @@ impl<Meta: Clone> PropertyHandler<Meta> {
347348
})
348349
} else {
349350
self.push_error(
350-
"client_response_type must be one of \"openai\", \"anthropic\", \"google\", or \"vertex\" and not an environment variable",
351+
"client_response_type must be one of \"openai\", \"openai-responses\", \"anthropic\", \"google\", or \"vertex\" and not an environment variable",
351352
key_span,
352353
);
353354
None

engine/baml-lib/llm-client/src/clients/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ impl crate::OpenAIClientProviderVariant {
184184
crate::OpenAIClientProviderVariant::Generic => {
185185
openai::UnresolvedOpenAI::create_generic(properties)
186186
}
187+
crate::OpenAIClientProviderVariant::Responses => {
188+
openai::UnresolvedOpenAI::create_responses(properties)
189+
}
187190
}
188191
}
189192
}

engine/baml-lib/llm-client/src/clients/openai.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ impl<Meta> UnresolvedOpenAI<Meta> {
6161
pub struct ResolvedOpenAI {
6262
pub base_url: String,
6363
pub api_key: Option<ApiKeyWithProvenance>,
64-
role_selection: RolesSelection,
64+
pub role_selection: RolesSelection,
6565
pub allowed_metadata: AllowedRoleMetadata,
66-
supported_request_modes: SupportedRequestModes,
66+
pub supported_request_modes: SupportedRequestModes,
6767
pub headers: IndexMap<String, String>,
6868
pub properties: IndexMap<String, serde_json::Value>,
6969
pub query_params: IndexMap<String, String>,
@@ -349,6 +349,24 @@ impl<Meta: Clone> UnresolvedOpenAI<Meta> {
349349
Ok(instance)
350350
}
351351

352+
pub fn create_responses(
353+
mut properties: PropertyHandler<Meta>,
354+
) -> Result<Self, Vec<Error<Meta>>> {
355+
let base_url = properties
356+
.ensure_base_url_with_default(UnresolvedUrl::new_static("https://api.openai.com/v1"));
357+
358+
let api_key = Some(
359+
properties
360+
.ensure_api_key()
361+
.unwrap_or_else(|| StringOr::EnvVar("OPENAI_API_KEY".to_string())),
362+
);
363+
364+
let instance =
365+
Self::create_common(properties, Some(either::Either::Left(base_url)), api_key)?;
366+
367+
Ok(instance)
368+
}
369+
352370
fn create_common(
353371
mut properties: PropertyHandler<Meta>,
354372
base_url: Option<either::Either<UnresolvedUrl, (StringOr, StringOr)>>,

engine/baml-lib/llm-client/src/clientspec.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ pub enum OpenAIClientProviderVariant {
6161
Ollama,
6262
/// The Azure client provider variant
6363
Azure,
64+
/// The OpenAI Responses API variant
65+
Responses,
6466
/// The generic client provider variant
6567
Generic,
6668
}
@@ -93,6 +95,7 @@ impl std::fmt::Display for OpenAIClientProviderVariant {
9395
OpenAIClientProviderVariant::Base => write!(f, "openai"),
9496
OpenAIClientProviderVariant::Ollama => write!(f, "ollama"),
9597
OpenAIClientProviderVariant::Azure => write!(f, "azure-openai"),
98+
OpenAIClientProviderVariant::Responses => write!(f, "openai-responses"),
9699
OpenAIClientProviderVariant::Generic => write!(f, "openai-generic"),
97100
}
98101
}
@@ -117,6 +120,9 @@ impl std::str::FromStr for ClientProvider {
117120
"openai-generic" => Ok(ClientProvider::OpenAI(OpenAIClientProviderVariant::Generic)),
118121
"azure-openai" => Ok(ClientProvider::OpenAI(OpenAIClientProviderVariant::Azure)),
119122
"baml-azure-chat" => Ok(ClientProvider::OpenAI(OpenAIClientProviderVariant::Azure)),
123+
"openai-responses" => Ok(ClientProvider::OpenAI(
124+
OpenAIClientProviderVariant::Responses,
125+
)),
120126
"baml-ollama-chat" => Ok(ClientProvider::OpenAI(OpenAIClientProviderVariant::Ollama)),
121127
"ollama" => Ok(ClientProvider::OpenAI(OpenAIClientProviderVariant::Ollama)),
122128
"anthropic" => Ok(ClientProvider::Anthropic),
@@ -141,6 +147,7 @@ impl std::str::FromStr for OpenAIClientProviderVariant {
141147
"openai" => Ok(OpenAIClientProviderVariant::Base),
142148
"ollama" => Ok(OpenAIClientProviderVariant::Ollama),
143149
"azure-openai" => Ok(OpenAIClientProviderVariant::Azure),
150+
"openai-responses" => Ok(OpenAIClientProviderVariant::Responses),
144151
"openai-generic" => Ok(OpenAIClientProviderVariant::Generic),
145152
_ => Err(anyhow::anyhow!(
146153
"Invalid OpenAI client provider variant: {}",
@@ -171,6 +178,7 @@ impl ClientProvider {
171178
"openai",
172179
"openai-generic",
173180
"azure-openai",
181+
"openai-responses",
174182
"anthropic",
175183
"ollama",
176184
"round-robin",
@@ -191,7 +199,7 @@ impl std::fmt::Display for ClientSpec {
191199
}
192200
}
193201

194-
#[derive(Clone, Debug, Hash)]
202+
#[derive(Clone, Debug, Hash, Default)]
195203
pub struct SupportedRequestModes {
196204
// If unset, treat as auto
197205
pub stream: Option<bool>,
@@ -325,7 +333,7 @@ impl UnresolvedRolesSelection {
325333
}
326334
}
327335

328-
#[derive(Clone, Debug)]
336+
#[derive(Clone, Debug, Default)]
329337
pub struct RolesSelection {
330338
allowed: Option<Vec<String>>,
331339
default: Option<String>,
@@ -409,6 +417,7 @@ impl AllowedRoleMetadata {
409417
#[derive(Clone, Debug, Hash)]
410418
pub enum UnresolvedResponseType {
411419
OpenAI,
420+
OpenAIResponses,
412421
Anthropic,
413422
Google,
414423
Vertex,
@@ -417,6 +426,7 @@ pub enum UnresolvedResponseType {
417426
#[derive(Clone, Debug, Hash)]
418427
pub enum ResponseType {
419428
OpenAI,
429+
OpenAIResponses,
420430
Anthropic,
421431
Google,
422432
Vertex,
@@ -430,9 +440,116 @@ impl UnresolvedResponseType {
430440
pub fn resolve(&self, _: &impl GetEnvVar) -> Result<ResponseType> {
431441
match self {
432442
Self::OpenAI => Ok(ResponseType::OpenAI),
443+
Self::OpenAIResponses => Ok(ResponseType::OpenAIResponses),
433444
Self::Anthropic => Ok(ResponseType::Anthropic),
434445
Self::Google => Ok(ResponseType::Google),
435446
Self::Vertex => Ok(ResponseType::Vertex),
436447
}
437448
}
438449
}
450+
451+
#[cfg(test)]
452+
mod tests {
453+
use std::str::FromStr;
454+
455+
use super::*;
456+
457+
#[test]
458+
fn test_client_provider_parsing() {
459+
// Test parsing of openai-responses provider
460+
let provider = ClientProvider::from_str("openai-responses");
461+
assert!(provider.is_ok());
462+
463+
let provider = provider.unwrap();
464+
match provider {
465+
ClientProvider::OpenAI(OpenAIClientProviderVariant::Responses) => {
466+
// Success!
467+
}
468+
_ => panic!("Expected OpenAI Responses variant, got {:?}", provider),
469+
}
470+
}
471+
472+
#[test]
473+
fn test_openai_client_provider_variant_parsing() {
474+
let variant = OpenAIClientProviderVariant::from_str("openai-responses");
475+
assert!(variant.is_ok());
476+
assert_eq!(variant.unwrap(), OpenAIClientProviderVariant::Responses);
477+
}
478+
479+
#[test]
480+
fn test_openai_responses_display() {
481+
let variant = OpenAIClientProviderVariant::Responses;
482+
assert_eq!(variant.to_string(), "openai-responses");
483+
}
484+
485+
#[test]
486+
fn test_openai_responses_in_allowed_providers() {
487+
let allowed = ClientProvider::allowed_providers();
488+
assert!(allowed.contains(&"openai-responses"));
489+
}
490+
491+
#[test]
492+
fn test_response_type_parsing() {
493+
// Test UnresolvedResponseType
494+
let unresolved = match "openai-responses" {
495+
"openai" => UnresolvedResponseType::OpenAI,
496+
"openai-responses" => UnresolvedResponseType::OpenAIResponses,
497+
"anthropic" => UnresolvedResponseType::Anthropic,
498+
"google" => UnresolvedResponseType::Google,
499+
"vertex" => UnresolvedResponseType::Vertex,
500+
_ => panic!("Unknown response type"),
501+
};
502+
503+
assert!(matches!(
504+
unresolved,
505+
UnresolvedResponseType::OpenAIResponses
506+
));
507+
}
508+
509+
#[test]
510+
fn test_response_type_resolution() {
511+
use baml_types::GetEnvVar;
512+
513+
struct MockEnvContext;
514+
impl GetEnvVar for MockEnvContext {
515+
fn get_env_var(&self, _name: &str) -> Result<String, anyhow::Error> {
516+
Err(anyhow::anyhow!("No env var"))
517+
}
518+
519+
fn set_allow_missing_env_var(&self, _: bool) -> Self {
520+
MockEnvContext
521+
}
522+
}
523+
524+
let unresolved = UnresolvedResponseType::OpenAIResponses;
525+
let ctx = MockEnvContext;
526+
let resolved = unresolved.resolve(&ctx);
527+
528+
assert!(resolved.is_ok());
529+
assert!(matches!(resolved.unwrap(), ResponseType::OpenAIResponses));
530+
}
531+
532+
#[test]
533+
fn test_provider_roundtrip() {
534+
// Test that we can convert to string and back
535+
let original = ClientProvider::OpenAI(OpenAIClientProviderVariant::Responses);
536+
let string_repr = match &original {
537+
ClientProvider::OpenAI(variant) => variant.to_string(),
538+
_ => panic!("Expected OpenAI provider"),
539+
};
540+
541+
assert_eq!(string_repr, "openai-responses");
542+
543+
let parsed_back = ClientProvider::from_str(&string_repr).unwrap();
544+
assert_eq!(original, parsed_back);
545+
}
546+
547+
#[test]
548+
fn test_invalid_provider_parsing() {
549+
let result = ClientProvider::from_str("invalid-provider");
550+
assert!(result.is_err());
551+
552+
let result = OpenAIClientProviderVariant::from_str("invalid-variant");
553+
assert!(result.is_err());
554+
}
555+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ impl TryFrom<(&ClientProperty, &RuntimeContext)> for LLMPrimitiveProvider {
123123
OpenAIClientProviderVariant::Generic => {
124124
OpenAIClient::dynamic_new_generic(value, ctx).map(Into::into)
125125
}
126+
OpenAIClientProviderVariant::Responses => {
127+
OpenAIClient::dynamic_new_responses(value, ctx).map(Into::into)
128+
}
126129
}
127130
}
128131
ClientProvider::Anthropic => AnthropicClient::dynamic_new(value, ctx).map(Into::into),
@@ -181,6 +184,9 @@ impl TryFrom<(&ClientWalker<'_>, &RuntimeContext)> for LLMPrimitiveProvider {
181184
OpenAIClientProviderVariant::Generic => {
182185
OpenAIClient::new_generic(client, ctx).map(Into::into)
183186
}
187+
OpenAIClientProviderVariant::Responses => {
188+
OpenAIClient::new_responses(client, ctx).map(Into::into)
189+
}
184190
}
185191
}
186192
ClientProvider::Anthropic => AnthropicClient::new(client, ctx).map(Into::into),

0 commit comments

Comments
 (0)