Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [m, "claude/**"]
pull_request:
branches: [m]

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings

jobs:
check:
name: cargo check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace --all-targets

test:
name: cargo test
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace

clippy:
name: clippy
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets -- -D warnings

fmt:
name: rustfmt
runs-on: ubuntu-latest
env:
RUSTFLAGS: ""
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check

doc:
name: cargo doc
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo doc --workspace --no-deps
env:
RUSTDOCFLAGS: -Dwarnings

msrv:
name: minimum supported rust version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/target
Cargo.lock
*.swp
*.swo
.DS_Store
.idea/
.vscode/
86 changes: 86 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[workspace]
resolver = "2"
members = [
"crates/rusty_ai",
"crates/rusty_middleware",
"crates/rusty_ui_stream",
"crates/rusty_testing",
"crates/rusty_chatgpt",
"crates/rusty_claude",
"crates/rusty_gemini",
"crates/rusty_openai_compatible",
"crates/rusty_gemini_nano",
"crates/rusty_foundationmodels",
"crates/rusty_phi_silica",
"crates/rusty_browser",
"crates/rusty_ollama",
"examples/basic_text",
"examples/stream_text",
"examples/generate_object",
"examples/stream_object",
"examples/tool_loop",
"examples/multimodal",
"examples/local_android",
"examples/local_apple",
"examples/local_windows",
"examples/router",
]

[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.85"
license = "MPL-2.0"
repository = "https://github.com/undivisible/rusty_ai"

[workspace.dependencies]
# Core
rusty_ai = { path = "crates/rusty_ai" }
rusty_middleware = { path = "crates/rusty_middleware" }
rusty_ui_stream = { path = "crates/rusty_ui_stream" }
rusty_testing = { path = "crates/rusty_testing" }

# Cloud providers
rusty_chatgpt = { path = "crates/rusty_chatgpt" }
rusty_claude = { path = "crates/rusty_claude" }
rusty_gemini = { path = "crates/rusty_gemini" }
rusty_openai_compatible = { path = "crates/rusty_openai_compatible" }

# Local / platform runtimes
rusty_gemini_nano = { path = "crates/rusty_gemini_nano" }
rusty_foundationmodels = { path = "crates/rusty_foundationmodels" }
rusty_phi_silica = { path = "crates/rusty_phi_silica" }
rusty_browser = { path = "crates/rusty_browser" }
rusty_ollama = { path = "crates/rusty_ollama" }

# Async / streaming
tokio = { version = "1", features = ["full"] }
futures = "0.3"
async-trait = "0.1"
pin-project-lite = "0.2"
tokio-stream = "0.1"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
schemars = "0.8"

# HTTP
reqwest = { version = "0.12", features = ["json", "stream"] }
reqwest-eventsource = "0.6"
eventsource-stream = "0.2"

# Observability
tracing = "0.1"
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

# Error handling
thiserror = "2"

# Misc
bytes = "1"
url = "2"
secrecy = "0.10"
base64 = "0.22"
mime = "0.3"
25 changes: 25 additions & 0 deletions crates/rusty_ai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "rusty_ai"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Core traits, types, and abstractions for the Rusty AI SDK"

[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
pin-project-lite = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
schemars = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
bytes = { workspace = true }
url = { workspace = true }
base64 = { workspace = true }
mime = { workspace = true }
secrecy = { workspace = true }
67 changes: 67 additions & 0 deletions crates/rusty_ai/src/capability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

/// A capability that a model may support.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
TextInput,
TextOutput,
ImageInput,
ImageOutput,
Streaming,
ToolCalling,
StructuredOutput,
Embeddings,
LocalExecution,
SessionSupport,
PlatformNative,
ExtendedThinking,
VideoInput,
AudioInput,
AudioOutput,
}

/// An ordered set of capabilities.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilitySet {
inner: BTreeSet<Capability>,
}

impl CapabilitySet {
/// Create an empty capability set.
pub fn new() -> Self {
Self {
inner: BTreeSet::new(),
}
}

/// Builder-style: add a capability and return self.
pub fn with(mut self, cap: Capability) -> Self {
self.inner.insert(cap);
self
}

/// Check whether the set contains a specific capability.
pub fn has(&self, cap: &Capability) -> bool {
self.inner.contains(cap)
}

/// Returns `true` if every capability in the slice is present.
pub fn supports_all(&self, caps: &[Capability]) -> bool {
caps.iter().all(|c| self.inner.contains(c))
}

/// Merge another set into this one.
pub fn merge(&mut self, other: &CapabilitySet) {
for cap in &other.inner {
self.inner.insert(cap.clone());
}
}

/// Iterate over the capabilities.
pub fn iter(&self) -> impl Iterator<Item = &Capability> {
self.inner.iter()
}
}
51 changes: 51 additions & 0 deletions crates/rusty_ai/src/content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};

use crate::tool::{ToolCallRequest, ToolCallResult};

/// A single part of a message's content.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text { text: String },
Image { data: ImageData },
File { data: FileData },
ToolCall { call: ToolCallRequest },
ToolResult { result: ToolCallResult },
}

/// Image payload — either a URL reference or inline base64.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum ImageData {
Url {
url: String,
detail: Option<ImageDetail>,
},
Base64 {
media_type: String,
data: String,
},
}

/// Requested level of detail for image understanding.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageDetail {
Auto,
Low,
High,
}

/// File payload — either a URL reference or inline base64.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum FileData {
Url {
url: String,
media_type: Option<String>,
},
Base64 {
media_type: String,
data: String,
},
}
47 changes: 47 additions & 0 deletions crates/rusty_ai/src/embedding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// Compute the cosine similarity between two embedding vectors.
///
/// Returns 0.0 if either vector has zero magnitude.
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(
a.len(),
b.len(),
"embedding vectors must have the same length"
);

let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();

if mag_a == 0.0 || mag_b == 0.0 {
return 0.0;
}

dot / (mag_a * mag_b)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn identical_vectors() {
let v = vec![1.0, 2.0, 3.0];
let sim = cosine_similarity(&v, &v);
assert!((sim - 1.0).abs() < 1e-6);
}

#[test]
fn orthogonal_vectors() {
let a = vec![1.0, 0.0];
let b = vec![0.0, 1.0];
let sim = cosine_similarity(&a, &b);
assert!(sim.abs() < 1e-6);
}

#[test]
fn zero_vector() {
let a = vec![0.0, 0.0];
let b = vec![1.0, 2.0];
assert_eq!(cosine_similarity(&a, &b), 0.0);
}
}
Loading
Loading