Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cef1608
refactor(providers)!: remove the by-name provider entries; bindings g…
cunninghamcard-bit Oct 6, 2026
55487cb
refactor(core)!: call-layer tool result carries upstream's structured…
cunninghamcard-bit Oct 6, 2026
837919b
Merge into wire-format
cunninghamcard-bit Oct 6, 2026
13c3282
Merge into wire-format
cunninghamcard-bit Oct 6, 2026
d0a7739
Merge into wire-format
cunninghamcard-bit Oct 6, 2026
f623304
Merge model-families: boundary fixes
cunninghamcard-bit Oct 6, 2026
c7f21fc
refactor!: public JSON equals the AI SDK JSON
cunninghamcard-bit Oct 7, 2026
1cb1ba6
Merge branch 'rfc-0036/model-families' into rfc-0036/wire-format
cunninghamcard-bit Oct 7, 2026
32df21f
fix(core): Size and AspectRatio serialize as upstream's template strings
cunninghamcard-bit Oct 7, 2026
26952dd
refactor(bindings)!: bindings follow the AI SDK JSON and the reduced …
cunninghamcard-bit Oct 7, 2026
3771082
Merge branch 'rfc-0036/model-families' into rfc-0036/wire-format
cunninghamcard-bit Oct 7, 2026
4894b5f
chore: update wire format on the refreshed model-family stack
cunninghamcard-bit Oct 7, 2026
5fe5c0b
test(decision): migrate recording envelope and shared types to curren…
cunninghamcard-bit Oct 7, 2026
54488ac
chore(node): regenerate decision recording types for wire format
cunninghamcard-bit Oct 7, 2026
8339908
Merge branch 'codex/restack-model-families' into codex/restack-wire-f…
cunninghamcard-bit Oct 7, 2026
39f28f1
docs(decision): use current recording wire field names
cunninghamcard-bit Oct 7, 2026
415e37e
Merge branch 'codex/restack-model-families' into codex/restack-wire-f…
cunninghamcard-bit Oct 7, 2026
8f5265c
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
cunninghamcard-bit Oct 8, 2026
2a5f7d6
feat(error): suggest close names in the NoSuchProvider message
cunninghamcard-bit Oct 8, 2026
ed7c52b
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
cunninghamcard-bit Oct 9, 2026
117df5c
fix(openai)!: drive the chat tool-call tracker with @ai-sdk/openai's …
cunninghamcard-bit Oct 9, 2026
7fc99ee
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
cunninghamcard-bit Oct 10, 2026
2423267
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
claude Oct 10, 2026
b0c632f
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
claude Oct 10, 2026
c9817ee
Merge branch 'rfc-0036/vendor-alignment' into rfc-0036/wire-format
claude Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking

- aimux's public JSON equals the AI SDK's JSON. Struct fields serialize
camelCase (`toolCallId`, `providerOptions`, `inputTokens`); unions carry the
AI SDK's tag and strings (`{"type":"text-delta"}`, `"tool-call"`,
`"reasoning-file"`, Source `sourceType`, Warning/FileData/ResponseFormat/
stream parts no longer externally tagged); byte payloads are untagged
(array of numbers or base64 string); optional fields are omitted instead of
`null` and generate as `field?: T`; `AiMuxError` is tagged by `name`
(`AI_APICallError`, `AI_RetryError`, ...). Vendor request and response types
and the OpenAI chat-completion output keep their vendor wire formats.
Persisted recordings and trace records change with the same rule.

- Middleware wraps generation and streaming with callbacks for both operations,
parameter transforms and provider/model/supported-URL overrides. Explicit
model and provider ids take precedence over middleware overrides.
Expand Down Expand Up @@ -166,6 +177,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
67 changes: 30 additions & 37 deletions aimux-core/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use ts_rs::TS;

use crate::language_model_message::ToolResultOutput;
use crate::shared::SharedProviderOptions;

/// A part of a multi-part message.
///
/// Shared between `ModelMessage` (user-facing) and `LanguageModelPrompt` (provider-facing).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[serde(
tag = "type",
rename_all = "kebab-case",
rename_all_fields = "camelCase"
)]
#[ts(export)]
pub enum ContentPart {
/// A text segment.
Text {
text: String,
/// Provider-specific options for this part (e.g. `openai.promptCacheBreakpoint`).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},

/// Provider-specific custom content.
Custom {
kind: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -32,6 +39,7 @@ pub enum ContentPart {
ReasoningFile {
data: crate::shared::GeneratedFileData,
media_type: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -40,12 +48,16 @@ pub enum ContentPart {
ToolApprovalRequest {
approval_id: String,
tool_call_id: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
is_automatic: Option<bool>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
input_schema_input: Option<Value>,
},
Expand All @@ -54,6 +66,7 @@ pub enum ContentPart {
Image {
image: Vec<u8>,
media_type: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -62,8 +75,10 @@ pub enum ContentPart {
File {
data: Vec<u8>,
media_type: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -77,8 +92,10 @@ pub enum ContentPart {
FileBase64 {
data: String,
media_type: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -89,6 +106,7 @@ pub enum ContentPart {
FileUrl {
url: String,
media_type: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -101,8 +119,10 @@ pub enum ContentPart {
FileReference {
media_type: String,
reference: Value,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -116,10 +136,13 @@ pub enum ContentPart {
/// `redacted_thinking` block is emitted).
Reasoning {
text: String,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
/// Provider-specific options for this part (e.g.
/// `anthropic.signature`, `anthropic.redactedData`,
/// `anthropic.cacheControl`).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand All @@ -133,41 +156,24 @@ pub enum ContentPart {
/// Whether the tool call is executed by the provider rather than by
/// the client. This is part of the prompt contract because providers
/// need it to replay server tool calls on a later turn.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_executed: Option<bool>,
/// Provider-specific options for this part (e.g.
/// `anthropic.cacheControl`).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},

/// The result of executing a tool call.
/// The result of executing a tool call (upstream `ToolResultPart`).
ToolResult {
tool_call_id: String,
/// The tool's output (usually a JSON value or plain text).
///
/// Accepts the legacy `output` field name (used by the Vercel AI SDK
/// and the 0.1.0 TypeScript bindings) as an alias during deserialization,
/// so multi-part tool messages constructed either way round-trip
/// correctly. Serialization always emits `result`.
#[serde(alias = "output")]
result: Value,
/// The name of the tool that produced this result (optional on the
/// user-input side; providers that need it can look it up from the
/// preceding tool call).
#[serde(default, skip_serializing_if = "Option::is_none")]
tool_name: Option<String>,
/// Whether the result is an error or error message.
#[serde(default, skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
/// Whether the result is preliminary (replaces prior, e.g. image previews).
#[serde(default, skip_serializing_if = "Option::is_none")]
preliminary: Option<bool>,
/// Whether the tool is dynamic (defined at runtime, e.g. MCP tools).
#[serde(default, skip_serializing_if = "Option::is_none")]
dynamic: Option<bool>,
tool_name: String,
output: ToolResultOutput,
/// Provider-specific options for this part (e.g.
/// `anthropic.cacheControl`).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_options: Option<SharedProviderOptions>,
},
Expand Down Expand Up @@ -197,19 +203,6 @@ impl ContentPart {
}
}

/// Convenience constructor for a tool-result part (no provider options).
pub fn tool_result(tool_call_id: impl Into<String>, result: Value) -> Self {
ContentPart::ToolResult {
tool_call_id: tool_call_id.into(),
result,
tool_name: None,
is_error: None,
preliminary: None,
dynamic: None,
provider_options: None,
}
}

/// Convenience constructor for a reasoning/thinking part (no provider
/// options, no signature).
pub fn reasoning(text: impl Into<String>) -> Self {
Expand Down
22 changes: 22 additions & 0 deletions aimux-core/src/embedding_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub type Embedding = Vec<f32>;
///
/// Aligned with V4 `EmbeddingModelV4CallOptions`.
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct EmbeddingCallOptions {
/// List of text values to generate embeddings for.
Expand All @@ -36,15 +37,23 @@ pub struct EmbeddingCallOptions {
pub abort_signal: Option<AbortSignal>,

/// Per-call retry override. `None` uses the model default.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_retries: Option<u32>,

/// Per-call operation timeout.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<crate::options::TimeoutConfiguration>,

/// Additional provider-specific options, keyed by provider name.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_options: Option<SharedProviderOptions>,

/// Additional HTTP headers to send with the request.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub headers: Option<SharedHeaders>,
}

Expand All @@ -64,6 +73,7 @@ impl EmbeddingCallOptions {

/// Token usage for an embedding call. Embeddings only report input tokens.
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct EmbeddingUsage {
/// Number of input tokens consumed.
Expand All @@ -74,18 +84,25 @@ pub struct EmbeddingUsage {
///
/// Aligned with V4 `EmbeddingModelV4Result`.
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct EmbeddingResult {
/// Generated embeddings, in the same order as the input `values`.
pub embeddings: Vec<Embedding>,

/// Token usage (input tokens only).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<EmbeddingUsage>,

/// Additional provider-specific metadata.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<SharedProviderMetadata>,

/// Optional response information for debugging.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response: Option<EmbeddingResponse>,

/// Warnings for the call, e.g. unsupported settings.
Expand All @@ -94,11 +111,16 @@ pub struct EmbeddingResult {

/// Debugging response info specific to embedding calls.
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct EmbeddingResponse {
/// Response headers.
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub headers: Option<SharedHeaders>,
/// The response body (opaque JSON).
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
}

Expand Down
Loading