Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`speech::google_speech_input` become crate-internal, with no public
replacement. `google::files` and `google::transcription` keep their paths.

- `aimux_providers::openai::OpenAICompletionModel`, what
`OpenAIProvider::completion` returns, is its own struct instead of an alias
of `openai_compatible::OpenAICompatibleCompletionModel` (RFC-0046 R2, #391).
Code that used the two names as one type, such as passing an OpenAI
completion model where an `OpenAICompatibleCompletionModel` is expected,
names `OpenAICompletionModel` instead; both still implement
`LanguageModel`, and the requests and results are unchanged.

- `aimux_providers::xai::responses::resolve_provider_reference` is removed;
use `aimux_provider_utils::resolve_provider_reference`, the AI SDK's
`resolveProviderReference`, which takes the reference and the provider
Expand Down
19 changes: 5 additions & 14 deletions aimux-providers/src/openai_compatible/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use aimux_provider_utils::{
use futures::future::BoxFuture;
use serde_json::Value;

use crate::preset::PresetDescriptor;
use crate::shared::aisdk_pins::Package;
use crate::shared::{AuthScheme, Credential, ProviderHeaders};

Expand Down Expand Up @@ -360,19 +359,14 @@ pub(crate) trait ResolveFetchHeaders: Send + Sync {

/// Where the base URL comes from.
///
/// A plain provider has a [`Fixed`](Self::Fixed) URL. A preset whose URL is
/// read from the environment or expanded from template parameters is
/// [`Preset`](Self::Preset): it is evaluated per request, so creating the
/// provider (and the preset default instances) never reads the environment
/// and never fails.
/// A plain provider has a [`Fixed`](Self::Fixed) URL. A URL read from the
/// environment or built from settings (a preset's template, a Vertex AI
/// partner's project and location) is [`Dynamic`](Self::Dynamic): it is
/// evaluated per request, so creating the provider never reads the
/// environment and never fails.
#[derive(Clone, Debug)]
pub(crate) enum BaseUrl {
Fixed(String),
Preset {
descriptor: &'static PresetDescriptor,
/// The explicit template parameters of the settings.
params: Arc<HashMap<String, String>>,
},
/// A package's own base URL, resolved when a request is made.
Dynamic(Arc<dyn ResolveBaseUrl>),
}
Expand All @@ -381,9 +375,6 @@ impl BaseUrl {
pub(crate) fn resolve(&self) -> Result<String, AiMuxError> {
match self {
Self::Fixed(url) => Ok(url.clone()),
Self::Preset { descriptor, params } => {
crate::preset::resolve_base_url(descriptor, params)
}
Self::Dynamic(resolver) => resolver.resolve(),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4669,15 +4669,26 @@ mod error_structure_and_base_url {
}
}

/// A preset base URL is data the request resolves: creating the provider
/// and the model reads nothing, a request that cannot resolve it fails,
/// and the next request resolves it again.
/// A dynamic base URL (a preset's template, a Vertex AI partner's project
/// and location) is data the request resolves: creating the provider and
/// the model reads nothing, a request that cannot resolve it fails, and
/// the next request resolves it again.
#[tokio::test]
#[serial_test::serial]
async fn a_lazy_base_url_is_evaluated_per_request_and_its_error_fails_the_request() {
use crate::preset::{AuthMode, ParamSpec, PresetDescriptor};

const VAR: &str = "AIMUX_TEST_COMPAT_LAZY_BASE_URL";

/// The base URL in `VAR`, read on every request.
#[derive(Debug)]
struct EnvBaseUrl;

impl crate::openai_compatible::ResolveBaseUrl for EnvBaseUrl {
fn resolve(&self) -> Result<String, AiMuxError> {
std::env::var(VAR)
.map_err(|_| AiMuxError::invalid_argument(format!("{VAR} is not set")))
}
}

let server = MockServer::start().await;
serve(
&server,
Expand All @@ -4689,28 +4700,7 @@ mod error_structure_and_base_url {
.await;
// SAFETY: the test is `#[serial]` and no other test reads `VAR`.
unsafe { std::env::remove_var(VAR) };
let descriptor: &'static PresetDescriptor = Box::leak(Box::new(PresetDescriptor {
name: "acme".to_string(),
display: "Acme".to_string(),
base_url: "http://{host}/v1".to_string(),
env_var: String::new(),
auth: AuthMode::None,
base_url_env: Some(VAR.to_string()),
max_tokens_key: None,
convert_usage: None,
params: vec![ParamSpec {
name: "host".to_string(),
env: Vec::new(),
default: None,
derive: None,
}],
user_agent: ("openai-compatible", "3.0.62"),
structured_output_models: None,
}));
let lazy = BaseUrl::Preset {
descriptor,
params: Arc::new(std::collections::HashMap::new()),
};
let lazy = BaseUrl::Dynamic(Arc::new(EnvBaseUrl));
let provider = provider(lazy, ChatSettings::default());
let model = provider.chat("m");
let error = model.do_generate(&hello()).await.unwrap_err();
Expand Down
28 changes: 23 additions & 5 deletions aimux-providers/src/preset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ use aimux_provider_utils::{FetchFunction, HeaderMapOpt, Resolvable, validate_bas

use crate::openai_compatible::chat::usage_from_raw;
use crate::openai_compatible::{
Assembly, BaseUrl, ChatSettings, ConvertUsage, OpenAICompatibleProvider, TransformRequestBody,
Assembly, BaseUrl, ChatSettings, ConvertUsage, OpenAICompatibleProvider, ResolveBaseUrl,
TransformRequestBody,
};
use crate::shared::Credential;

Expand Down Expand Up @@ -423,10 +424,10 @@ fn assemble(
None if descriptor.params.is_empty() && descriptor.base_url_env.is_none() => {
BaseUrl::Fixed(validate_base_url(&descriptor.base_url)?)
}
None => BaseUrl::Preset {
None => BaseUrl::Dynamic(Arc::new(PresetBaseUrl {
descriptor,
params: Arc::new(settings.params),
},
params: settings.params,
})),
};

let credential = match (descriptor.auth, settings.api_key) {
Expand Down Expand Up @@ -526,9 +527,26 @@ fn env_value(names: &[impl AsRef<str>]) -> Option<String> {
})
}

/// The base URL of a preset whose URL is read from the environment or
/// expanded from template parameters, evaluated per request so creating the
/// provider (and the preset default instances) never reads the environment
/// and never fails.
#[derive(Debug)]
struct PresetBaseUrl {
descriptor: &'static PresetDescriptor,
/// The explicit template parameters of the settings.
params: HashMap<String, String>,
}

impl ResolveBaseUrl for PresetBaseUrl {
fn resolve(&self) -> Result<String, AiMuxError> {
resolve_base_url(self.descriptor, &self.params)
}
}

/// The base URL a request goes to: the descriptor's base-URL variable, else
/// the template expanded from `explicit`, the environment and the defaults.
pub(crate) fn resolve_base_url(
fn resolve_base_url(
descriptor: &PresetDescriptor,
explicit: &HashMap<String, String>,
) -> Result<String, AiMuxError> {
Expand Down
Loading