Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
573de7e
fix(providers): align ten vendor packages with their upstream counter…
cunninghamcard-bit Oct 5, 2026
99a476b
test(providers): follow the vendor alignment in the existing tests
cunninghamcard-bit Oct 5, 2026
b8e0cd8
test(providers): a small end-to-end set per vendor, taken from upstre…
cunninghamcard-bit Oct 5, 2026
5803a97
docs(changelog): record the visible changes of the vendor alignment
cunninghamcard-bit Oct 5, 2026
b2230d7
fix(providers): close review findings in the vendor alignment
cunninghamcard-bit Oct 5, 2026
c34eb40
Merge provider-factory: review fixes from the branches below
cunninghamcard-bit Oct 5, 2026
6498d0d
feat(providers): port the vendor features that were left open
cunninghamcard-bit Oct 5, 2026
92e6d30
Merge provider-options-types and emit the new content variants
cunninghamcard-bit Oct 5, 2026
a66f9e5
Merge provider-result-types: structured tool results in the prompt
cunninghamcard-bit Oct 5, 2026
03ad6dc
Merge provider-factory: completion models, skills, registry middleware
cunninghamcard-bit Oct 5, 2026
5cf8e47
test: fix three tests and drop hand-written tests the merges brought …
cunninghamcard-bit Oct 5, 2026
2b98964
Merge provider-factory: test removals from the branches below
cunninghamcard-bit Oct 5, 2026
5589a72
test: keep this branch's regression expectations and regenerate provi…
cunninghamcard-bit Oct 5, 2026
0fb7f32
fix(providers): restore upstream-ported tests and three vendor behavi…
cunninghamcard-bit Oct 5, 2026
8d03d9e
Merge output-content-types: source union, no thought signature, no co…
cunninghamcard-bit Oct 5, 2026
f08097d
Merge provider-factory: aligned factory settings, restored tests, cha…
cunninghamcard-bit Oct 5, 2026
e223370
test(bedrock): do not expect a response body upstream does not return
cunninghamcard-bit Oct 5, 2026
f217067
Merge provider-factory: ancestry of the split-out parts
cunninghamcard-bit Oct 5, 2026
c935bc7
fix(providers): close vendor divergences found in review
cunninghamcard-bit Oct 6, 2026
4f3a824
Merge the branch below: review fixes
cunninghamcard-bit Oct 6, 2026
e12021d
Merge the branch below: review fixes
cunninghamcard-bit Oct 6, 2026
f03ea87
fix(providers): stream error events as parts; Bedrock mock routes use…
cunninghamcard-bit Oct 6, 2026
68d436a
test(providers): restore the Bedrock and Anthropic upstream-translate…
cunninghamcard-bit Oct 6, 2026
4d2db55
Merge the branch below: review fixes
cunninghamcard-bit Oct 6, 2026
8f82c07
fix(providers): Azure user-agent uses the package namespace constant
cunninghamcard-bit Oct 6, 2026
b4d4639
Merge the branch below: formatting
cunninghamcard-bit Oct 6, 2026
5dc85be
Merge the branch below: ported test files kept
cunninghamcard-bit Oct 6, 2026
7db8f86
feat(groq): transcription model ported from upstream
cunninghamcard-bit Oct 6, 2026
76b7b6e
feat(mistral): speech and transcription models ported from upstream
cunninghamcard-bit Oct 6, 2026
5b6a120
feat(google): speech and transcription models ported from upstream
cunninghamcard-bit Oct 6, 2026
5ecef34
feat(core): evaluation model, registry evaluationModel lookup and cus…
cunninghamcard-bit Oct 6, 2026
ff8da04
Merge: d2q port into model-families
cunninghamcard-bit Oct 6, 2026
bd31566
Merge: d2m port into model-families
cunninghamcard-bit Oct 6, 2026
c994e24
Merge: d2g port into model-families
cunninghamcard-bit Oct 6, 2026
e83a788
Merge: d2r port into model-families
cunninghamcard-bit Oct 6, 2026
a0d04fc
feat(xai): speech, transcription and video models ported from upstream
cunninghamcard-bit Oct 6, 2026
dc37571
Merge: d2x port into model-families
cunninghamcard-bit Oct 6, 2026
809d47e
fix(providers): new models respect the provider boundary rules
cunninghamcard-bit Oct 6, 2026
dff3840
chore(node): regenerate ContentPart.ts for this branch's Rust type
cunninghamcard-bit Oct 6, 2026
11321e3
chore(web): regenerate WireContentPart.ts for this branch's Rust type
cunninghamcard-bit Oct 7, 2026
d3de71d
fix(core): evaluation types export resolvable TypeScript
cunninghamcard-bit Oct 7, 2026
9f007bf
test(mistral): name the pinned upstream source in the ported test hea…
cunninghamcard-bit Oct 7, 2026
4fe2794
Merge branch 'codex/restack-214' into codex/restack-220
cunninghamcard-bit Oct 7, 2026
a03dc20
chore: update model families with the refreshed RFC-0036 stack
cunninghamcard-bit Oct 7, 2026
d6cd80e
Merge branch 'codex/restack-214' into codex/restack-220
cunninghamcard-bit Oct 7, 2026
43d0af6
Merge branch 'codex/restack-220' into codex/restack-model-families
cunninghamcard-bit Oct 7, 2026
9b0b7a2
Merge branch 'codex/restack-214' into codex/restack-220
cunninghamcard-bit Oct 7, 2026
3eeece9
test(stream): assert terminal decoding failures through provider erro…
cunninghamcard-bit Oct 7, 2026
f7bcb79
Merge branch 'codex/restack-220' into codex/restack-model-families
cunninghamcard-bit Oct 7, 2026
c2c963c
Merge branch 'rfc-0036/provider-factory' into rfc-0036/vendor-alignment
cunninghamcard-bit Oct 8, 2026
ff339b5
Merge branch 'rfc-0036/provider-factory' into rfc-0036/vendor-alignment
cunninghamcard-bit Oct 9, 2026
3cd6893
Merge branch 'rfc-0036/provider-factory' into rfc-0036/vendor-alignment
cunninghamcard-bit Oct 10, 2026
13b3163
Merge branch 'rfc-0036/provider-factory' into rfc-0036/vendor-alignment
claude Oct 10, 2026
c2fa9aa
test(providers): pin OpenAI Chat replay of non-object tool input to u…
claude Oct 10, 2026
ae5cb12
Merge branch 'rfc-0036/provider-factory' into rfc-0036/vendor-alignment
claude Oct 11, 2026
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`available_providers`. Node and Python OpenAI constructors use Responses
by default.

- Vendor packages follow their upstream counterparts more closely. Visible
changes: Bedrock percent-encodes the model id in request URLs and writes
provider metadata under both `amazonBedrock` and `bedrock`; OpenAI embeddings
request `encoding_format: "float"` and no longer decode base64 vectors; the
Google provider result no longer carries a model id unless the API returns
one; an Anthropic mid-stream `error` event is forwarded as an error part
without a synthetic finish; each package sends its own `user-agent` suffix;
Mistral has no `transform_request_body` setting; the deprecated
`text_embedding` and `text_embedding_model` provider methods are removed.

- Public surface (`aimux-core`, `aimux-provider-utils`): removed
`recording::init_recording_from_env`; made private the retry preparation
helpers and default constants, `composite::{add_usage,
Expand Down
1 change: 1 addition & 0 deletions aimux-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ rand = { workspace = true }
base64 = "0.22"
jsonschema = { workspace = true }
regex = "1"
indexmap = { version = "2", features = ["serde"] }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "test-util"] }
Expand Down
158 changes: 158 additions & 0 deletions aimux-core/src/custom_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! `custom_provider`: a provider assembled from alias tables, with an optional
//! fallback provider.
//!
//! Mirrors the AI SDK's `customProvider` (`ai/src/registry/custom-provider.ts`).
//! A lookup consults the alias table of its modality first, then the fallback
//! provider; with neither, it is [`AiMuxError::NoSuchModel`]. There are no
//! string aliases that resolve through a global default provider: Rust has
//! none.

use std::collections::HashMap;
use std::sync::Arc;

use crate::embedding_model::EmbeddingModel;
use crate::error::AiMuxError;
use crate::evaluation_model::EvaluationModel;
use crate::files_model::Files;
use crate::image_model::ImageModel;
use crate::language_model::LanguageModel;
use crate::provider::Provider;
use crate::reranking_model::RerankingModel;
use crate::search_model::SearchModel;
use crate::skills_model::Skills;
use crate::speech_model::SpeechModel;
use crate::transcription_model::TranscriptionModel;
use crate::video_model::VideoModel;

/// Options of [`custom_provider`]; every field is optional.
#[derive(Default)]
pub struct CustomProviderOptions {
pub language_models: HashMap<String, Arc<dyn LanguageModel>>,
pub embedding_models: HashMap<String, Arc<dyn EmbeddingModel>>,
pub image_models: HashMap<String, Arc<dyn ImageModel>>,
pub transcription_models: HashMap<String, Arc<dyn TranscriptionModel>>,
pub speech_models: HashMap<String, Arc<dyn SpeechModel>>,
pub reranking_models: HashMap<String, Arc<dyn RerankingModel>>,
pub video_models: HashMap<String, Arc<dyn VideoModel>>,
pub search_models: HashMap<String, Arc<dyn SearchModel>>,
pub evaluation_models: HashMap<String, Arc<dyn EvaluationModel>>,
/// A files interface for uploading files.
pub files: Option<Arc<dyn Files>>,
/// A skills interface for uploading skills.
pub skills: Option<Arc<dyn Skills>>,
/// Consulted when a requested model is not in the alias tables.
pub fallback_provider: Option<Arc<dyn Provider>>,
}

/// A provider built by [`custom_provider`].
pub struct CustomProvider {
options: CustomProviderOptions,
}

/// Create a custom provider from alias tables and an optional fallback.
#[must_use]
pub fn custom_provider(options: CustomProviderOptions) -> CustomProvider {
CustomProvider { options }
}

/// The aliased model, else what the fallback offers, else `NoSuchModel`.
fn resolve<T: ?Sized>(
models: &HashMap<String, Arc<T>>,
id: &str,
model_type: &str,
fallback: impl FnOnce() -> Option<Result<Arc<T>, AiMuxError>>,
) -> Result<Arc<T>, AiMuxError> {
models
.get(id)
.map(|model| Ok(Arc::clone(model)))
.or_else(fallback)
.unwrap_or_else(|| Err(AiMuxError::no_such_model(id, model_type)))
}

impl Provider for CustomProvider {
fn language_model(&self, id: &str) -> Result<Arc<dyn LanguageModel>, AiMuxError> {
let o = &self.options;
resolve(&o.language_models, id, "languageModel", || {
o.fallback_provider.as_ref().map(|p| p.language_model(id))
})
}

fn embedding_model(&self, id: &str) -> Result<Arc<dyn EmbeddingModel>, AiMuxError> {
let o = &self.options;
resolve(&o.embedding_models, id, "embeddingModel", || {
o.fallback_provider.as_ref().map(|p| p.embedding_model(id))
})
}

fn image_model(&self, id: &str) -> Result<Arc<dyn ImageModel>, AiMuxError> {
let o = &self.options;
resolve(&o.image_models, id, "imageModel", || {
o.fallback_provider.as_ref().map(|p| p.image_model(id))
})
}

// The optional modalities are always defined here, as upstream: an id
// neither table nor fallback serves is `NoSuchModel`, never `None`.

fn transcription_model(
&self,
id: &str,
) -> Option<Result<Arc<dyn TranscriptionModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(
&o.transcription_models,
id,
"transcriptionModel",
|| o.fallback_provider.as_ref()?.transcription_model(id),
))
}

fn speech_model(&self, id: &str) -> Option<Result<Arc<dyn SpeechModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(&o.speech_models, id, "speechModel", || {
o.fallback_provider.as_ref()?.speech_model(id)
}))
}

fn reranking_model(&self, id: &str) -> Option<Result<Arc<dyn RerankingModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(&o.reranking_models, id, "rerankingModel", || {
o.fallback_provider.as_ref()?.reranking_model(id)
}))
}

fn video_model(&self, id: &str) -> Option<Result<Arc<dyn VideoModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(&o.video_models, id, "videoModel", || {
o.fallback_provider.as_ref()?.video_model(id)
}))
}

fn search_model(&self, id: &str) -> Option<Result<Arc<dyn SearchModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(&o.search_models, id, "searchModel", || {
o.fallback_provider.as_ref()?.search_model(id)
}))
}

fn evaluation_model(&self, id: &str) -> Option<Result<Arc<dyn EvaluationModel>, AiMuxError>> {
let o = &self.options;
Some(resolve(&o.evaluation_models, id, "evaluationModel", || {
o.fallback_provider.as_ref()?.evaluation_model(id)
}))
}

fn files(&self) -> Option<Arc<dyn Files>> {
let o = &self.options;
o.files
.clone()
.or_else(|| o.fallback_provider.as_ref()?.files())
}

fn skills(&self) -> Option<Arc<dyn Skills>> {
let o = &self.options;
o.skills
.clone()
.or_else(|| o.fallback_provider.as_ref()?.skills())
}
}
Loading