Skip to content
Closed
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
16 changes: 16 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 Expand Up @@ -156,6 +166,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`tool-call-N` id instead of an empty string; a new call without a function
name ends the stream with `InvalidResponseData` (previously it started a
call with an empty name). `DeltaToolCall.index` is now `Option<usize>`.
- Once compatible servers moved to `openai_compatible`, the OpenAI (and
Azure) chat stream drives the tracker with `@ai-sdk/openai`'s options:
`type` is validated when present, ids missing from the wire come from
`generate_id` instead of `tool-call` / `tool-call-N`, and
`DeltaToolCall.index` is required again, so a chunk without it fails to
parse.

**Rust (provider factory, RFC-0036)**

Expand Down
6 changes: 3 additions & 3 deletions aimux-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ pub mod prelude {
init_session_store, list_sessions, session_calls,
};
pub use crate::shared::{
AspectRatio, FileBytes, FileData, GeneratedFileData, JsonObject, SharedHeaders,
SharedProviderMetadata, SharedProviderOptions, SharedProviderReference, Size,
provider_namespace,
AspectRatio, FileBytes, FileData, GeneratedFileData, JsonObject, ResponseInfo,
SharedHeaders, SharedProviderMetadata, SharedProviderOptions, SharedProviderReference,
Size, StreamResponseInfo, provider_namespace,
};
pub use crate::skills_model::{SkillFile, Skills, UploadSkillCallOptions, UploadSkillResult};
pub use crate::speech_model::{SpeechCallOptions, SpeechModel, SpeechResult, generate_speech};
Expand Down
1 change: 1 addition & 0 deletions aimux-core/src/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ pub struct ReplayOverrides {
/// - `recording.input.options` 中的 headers/provider_options 已脱敏,
/// 重发会用脱敏后的 `[REDACTED]` 值——需要真实头时用 `overrides` 或
/// 重建 provider 时补充。
/// - 逐消息和逐内容部分的 `provider_options` 原样保留。
///
/// # Errors
///
Expand Down
4 changes: 3 additions & 1 deletion aimux-core/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use std::pin::Pin;
use ts_rs::TS;

use crate::error::AiMuxError;
use crate::shared::{GeneratedFileData, RequestInfo, ResponseInfo, StreamResponseInfo};
pub use crate::shared::GeneratedFileData;
use crate::shared::{RequestInfo, ResponseInfo, StreamResponseInfo};

use crate::stream_part::StreamPart;
use crate::tool::{RawToolCall, ToolResult};
use crate::types::{FinishReason, ProviderMetadata, Usage, Warning};
Expand Down
11 changes: 0 additions & 11 deletions aimux-core/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,17 +297,6 @@ mod tests {
assert_eq!(attempts.load(Ordering::SeqCst), 0);
}

#[test]
fn prepared_retries_use_the_fixed_delay_and_backoff() {
let retries = prepare_retries(Some(4), None);

assert_eq!(retries.max_retries, 4);
assert_eq!(retries.initial_delay_ms, DEFAULT_INITIAL_DELAY_MS);
assert_eq!(retries.backoff_factor, u64::from(DEFAULT_BACKOFF_FACTOR));
assert_eq!(prepare_retries(None, None).max_retries, DEFAULT_MAX_RETRIES);
assert_eq!(prepare_retries(Some(0), None).max_retries, 0);
}

#[tokio::test]
async fn first_non_retryable_error_is_not_wrapped() {
let error = retry_with_exponential_backoff(
Expand Down
33 changes: 10 additions & 23 deletions aimux-ffi/tests/exports_smoke_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ use aimux_ffi::{
aimux_openai_image_new, aimux_openai_image_new_with_base, aimux_openai_new,
aimux_openai_new_with_base, aimux_openai_speech_new, aimux_openai_speech_new_with_base,
aimux_openai_transcription_new, aimux_openai_transcription_new_with_base,
aimux_provider_from_env, aimux_provider_handle_new, aimux_provider_list_models,
aimux_provider_model, aimux_provider_new, aimux_recording_flush, aimux_recording_stop,
aimux_recording_try_flush, aimux_register_providers, aimux_rerank, aimux_router_new,
aimux_search, aimux_session_calls, aimux_session_infer_init, aimux_session_store_init,
aimux_speech_generate, aimux_stream_text, aimux_stream_text_as_openai,
aimux_stream_text_as_openai_with_abort, aimux_stream_text_with_abort, aimux_tavily_search_new,
aimux_tavily_search_new_with_base, aimux_tool_call_repair_context, aimux_trace_aggregate,
aimux_trace_clear, aimux_trace_export_jsonl, aimux_trace_new, aimux_trace_new_audited,
aimux_trace_session_chain, aimux_trace_session_trajectory, aimux_transcription_generate,
aimux_transcription_input_done, aimux_transcription_next_part, aimux_transcription_push_audio,
aimux_provider_handle_new, aimux_provider_list_models, aimux_provider_model,
aimux_provider_new, aimux_recording_flush, aimux_recording_stop, aimux_recording_try_flush,
aimux_register_providers, aimux_rerank, aimux_router_new, aimux_search, aimux_session_calls,
aimux_session_infer_init, aimux_session_store_init, aimux_speech_generate, aimux_stream_text,
aimux_stream_text_as_openai, aimux_stream_text_as_openai_with_abort,
aimux_stream_text_with_abort, aimux_tavily_search_new, aimux_tavily_search_new_with_base,
aimux_tool_call_repair_context, aimux_trace_aggregate, aimux_trace_clear,
aimux_trace_export_jsonl, aimux_trace_new, aimux_trace_new_audited, aimux_trace_session_chain,
aimux_trace_session_trajectory, aimux_transcription_generate, aimux_transcription_input_done,
aimux_transcription_next_part, aimux_transcription_push_audio,
aimux_transcription_session_drop, aimux_transcription_session_new, aimux_vertex_new,
aimux_vertex_new_with_base, aimux_video_generate, aimux_xai_new, aimux_xai_new_with_base,
};
Expand Down Expand Up @@ -569,19 +569,6 @@ fn constructor_exports_build_and_release_handles() {
}
}

/// `provider_from_env` defers reading the key until request time.
#[test]
fn provider_from_env_handle() {
let mut h = 0;
let e = aimux_provider_from_env(
c("groq").as_ptr(),
c("llama-3.3-70b-versatile").as_ptr(),
&mut h,
);
expect_handle(e, h, "provider_from_env");
aimux_drop_handle(h);
}

// ── session class: text generation (4 exports) ──────────────────────────────

#[test]
Expand Down
20 changes: 20 additions & 0 deletions aimux-provider-utils/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ pub struct WebSocketRequest {
pub connector: Option<Arc<dyn WsConnector>>,
}

impl WebSocketRequest {
/// Build a transcription connection with the operation's cancellation and timeouts.
#[must_use]
pub fn for_transcription(
url: String,
headers: Vec<(String, String)>,
options: &aimux_core::transcription_model::TranscriptionStreamOptions,
connector: Option<Arc<dyn WsConnector>>,
) -> Self {
Self {
url,
headers,
subprotocols: Vec::new(),
abort_signal: options.abort_signal.clone(),
timeout: options.timeout,
connector,
}
}
}

/// Opens WebSocket connections. The built-in implementation is
/// tungstenite plus the RFC-0034 proxy tunnel; a custom one returns a
/// [`WsConnection`] built with [`WsConnection::from_transport`].
Expand Down
1 change: 1 addition & 0 deletions aimux-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ chrono = "0.4"
url = { workspace = true }
percent-encoding = "2"
regex = "1"
ring = "0.17"

[features]
default = ["realtime"]
Expand Down
4 changes: 4 additions & 0 deletions aimux-providers/src/anthropic/cache_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(crate) fn extract_cache_control_for(
let anthropic = anthropic_options(provider_options, options_name)?;
anthropic
.get("cacheControl")
.filter(|value| !value.is_null())
.or_else(|| anthropic.get("cache_control"))
.cloned()
}
Expand Down Expand Up @@ -97,6 +98,9 @@ impl CacheControlValidator {
let cache_control_value = extract_cache_control_for(provider_options, &self.options_name);

let cc = cache_control_value?;
if cc.is_null() || cc == false || cc == 0 || cc == "" {
return None;
}

if !can_cache {
self.warnings.push(Warning::Unsupported {
Expand Down
32 changes: 22 additions & 10 deletions aimux-providers/src/anthropic/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ impl AnthropicModelConfig {
}

/// Resolve the provider headers, add the `anthropic-beta` header for
/// `betas`, layer the per-call headers over both (case-insensitive, later
/// wins, `None` removes) and return the list that goes on the wire.
/// `betas`, merge beta values with per-call headers (case-insensitive) and return the list that goes on the wire.
///
/// # Errors
///
Expand All @@ -164,13 +163,6 @@ impl AnthropicModelConfig {
betas: &BTreeSet<String>,
) -> Result<Vec<(String, String)>, AiMuxError> {
let provider = self.headers.resolve().await?;
let mut beta_layer = HeaderMapOpt::new();
if !betas.is_empty() {
beta_layer.insert(
"anthropic-beta".to_string(),
Some(betas.iter().cloned().collect::<Vec<_>>().join(",")),
);
}
let call: HeaderMapOpt = call_headers
.map(|headers| {
headers
Expand All @@ -179,10 +171,30 @@ impl AnthropicModelConfig {
.collect()
})
.unwrap_or_default();
let mut merged_betas = betas.clone();
for headers in [&provider, &call] {
for (name, value) in headers {
if name.eq_ignore_ascii_case("anthropic-beta") {
for beta in value.as_deref().unwrap_or_default().split(',') {
let beta = beta.trim().to_ascii_lowercase();
if !beta.is_empty() {
merged_betas.insert(beta);
}
}
}
}
}
let mut beta_layer = HeaderMapOpt::new();
if !merged_betas.is_empty() {
beta_layer.insert(
"anthropic-beta".to_string(),
Some(merged_betas.into_iter().collect::<Vec<_>>().join(",")),
);
}
Ok(normalize_headers(combine_headers(&[
&provider,
&beta_layer,
&call,
&beta_layer,
])))
}

Expand Down
Loading
Loading