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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 11 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
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
Loading