Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- refactor(zeph-llm): remove redundant `schema` feature gate — `schemars` is now a mandatory dependency of `zeph-llm`; all `#[cfg(feature = "schema")]` / `#[cfg_attr(feature = "schema", ...)]` annotations removed; `chat_typed`, `chat_typed_erased`, structured output types, and the `extractor` module are always compiled (#2100)
- Promote `scheduler` and `guardrail` features to the default feature set; users with `default-features = false` are unaffected

### Fixed
Expand Down
4 changes: 1 addition & 3 deletions crates/zeph-llm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ description = "LLM provider abstraction with Ollama, Claude, OpenAI, and Candle
readme = "README.md"

[features]
default = ["schema"]
schema = ["dep:schemars"]
stt = ["reqwest/multipart"]
candle = ["dep:audioadapter-buffers", "dep:candle-core", "dep:candle-nn", "dep:candle-transformers", "dep:hf-hub", "dep:tokenizers", "dep:symphonia", "dep:rubato"]
cuda = ["candle", "candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"]
Expand All @@ -37,7 +35,7 @@ rand.workspace = true
rand_distr.workspace = true
reqwest = { workspace = true, features = ["json", "rustls", "stream"] }
rubato = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
symphonia = { workspace = true, optional = true, features = ["mp3", "ogg", "wav", "flac", "pcm"] }
Expand Down
4 changes: 0 additions & 4 deletions crates/zeph-llm/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ use crate::mock::MockProvider;
use crate::ollama::OllamaProvider;
use crate::openai::OpenAiProvider;
use crate::orchestrator::ModelOrchestrator;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[cfg(feature = "schema")]
use serde::de::DeserializeOwned;

use crate::provider::{
Expand Down Expand Up @@ -67,7 +65,6 @@ impl AnyProvider {
/// # Errors
///
/// Returns an error if the provider fails or the response cannot be parsed.
#[cfg(feature = "schema")]
pub async fn chat_typed_erased<T>(&self, messages: &[Message]) -> Result<T, crate::LlmError>
where
T: DeserializeOwned + JsonSchema + 'static,
Expand Down Expand Up @@ -610,7 +607,6 @@ mod tests {
assert!(debug.contains("OpenAi"));
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_erased_dispatches_to_mock() {
#[derive(Debug, serde::Deserialize, schemars::JsonSchema, PartialEq)]
Expand Down
1 change: 0 additions & 1 deletion crates/zeph-llm/src/claude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,6 @@ impl LlmProvider for ClaudeProvider {
true
}

#[cfg(feature = "schema")]
async fn chat_typed<T>(&self, messages: &[Message]) -> Result<T, LlmError>
where
T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static,
Expand Down
2 changes: 0 additions & 2 deletions crates/zeph-llm/src/claude/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,6 @@ pub(super) struct AnthropicTool<'a> {
pub input_schema: &'a serde_json::Value,
}

#[cfg(feature = "schema")]
#[derive(Serialize)]
pub(super) struct TypedToolRequestBody<'a> {
pub model: &'a str,
Expand All @@ -589,7 +588,6 @@ pub(super) struct TypedToolRequestBody<'a> {
pub context_management: Option<ContextManagement>,
}

#[cfg(feature = "schema")]
#[derive(Serialize)]
pub(super) struct ToolChoice<'a> {
pub r#type: &'a str,
Expand Down
1 change: 0 additions & 1 deletion crates/zeph-llm/src/compatible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ impl LlmProvider for CompatibleProvider {
self.inner.supports_structured_output()
}

#[cfg(feature = "schema")]
async fn chat_typed<T>(&self, messages: &[Message]) -> Result<T, LlmError>
where
T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static,
Expand Down
2 changes: 0 additions & 2 deletions crates/zeph-llm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub mod claude;
pub mod compatible;
pub mod ema;
pub mod error;
#[cfg(feature = "schema")]
pub mod extractor;
pub mod gemini;
pub mod http;
Expand All @@ -35,7 +34,6 @@ pub mod whisper;

pub use claude::{ThinkingConfig, ThinkingEffort};
pub use error::LlmError;
#[cfg(feature = "schema")]
pub use extractor::Extractor;
pub use gemini::ThinkingLevel as GeminiThinkingLevel;
pub use provider::{ChatStream, LlmProvider, StreamChunk, ThinkingBlock};
Expand Down
4 changes: 0 additions & 4 deletions crates/zeph-llm/src/openai/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,6 @@ impl LlmProvider for OpenAiProvider {
true
}

#[cfg(feature = "schema")]
async fn chat_typed<T>(&self, messages: &[Message]) -> Result<T, LlmError>
where
T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static,
Expand Down Expand Up @@ -1150,7 +1149,6 @@ struct EmbeddingData {
embedding: Vec<f32>,
}

#[cfg(feature = "schema")]
#[derive(Serialize)]
struct TypedChatRequest<'a> {
model: &'a str,
Expand Down Expand Up @@ -1179,14 +1177,12 @@ impl CompletionTokens {
}
}

#[cfg(feature = "schema")]
#[derive(Serialize)]
struct ResponseFormat<'a> {
r#type: &'a str,
json_schema: JsonSchemaFormat<'a>,
}

#[cfg(feature = "schema")]
#[derive(Serialize)]
struct JsonSchemaFormat<'a> {
name: &'a str,
Expand Down
1 change: 0 additions & 1 deletion crates/zeph-llm/src/openai/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ fn vision_chat_request_serialization_uses_gpt5_completion_tokens() {
assert!(!json.contains("\"max_tokens\":55"));
}

#[cfg(feature = "schema")]
#[test]
fn typed_chat_request_serialization_uses_gpt5_completion_tokens() {
let msgs = [ApiMessage {
Expand Down
3 changes: 1 addition & 2 deletions crates/zeph-llm/src/orchestrator/classifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

use crate::provider::{Message, Role};

#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, serde::Deserialize)]
#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
pub struct ModelSelection {
pub model: String,
pub reason: String,
Expand Down
7 changes: 0 additions & 7 deletions crates/zeph-llm/src/orchestrator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ impl ModelOrchestrator {
.expect("default provider must exist")
}

#[cfg(feature = "schema")]
async fn try_llm_routing(&self, messages: &[Message]) -> Option<String> {
if !self.llm_routing {
return None;
Expand Down Expand Up @@ -225,12 +224,6 @@ impl ModelOrchestrator {
}
}

#[cfg(not(feature = "schema"))]
#[allow(clippy::unused_async)]
async fn try_llm_routing(&self, _messages: &[Message]) -> Option<String> {
None
}

async fn chat_with_fallback(&self, messages: &[Message]) -> Result<String, LlmError> {
if let Some(selected) = self.try_llm_routing(messages).await
&& let Some(provider) = self.providers.get(&selected)
Expand Down
21 changes: 0 additions & 21 deletions crates/zeph-llm/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "schema")]
use std::{
any::TypeId,
collections::HashMap,
Expand All @@ -15,7 +14,6 @@ use serde::{Deserialize, Serialize};

use crate::error::LlmError;

#[cfg(feature = "schema")]
static SCHEMA_CACHE: LazyLock<Mutex<HashMap<TypeId, (serde_json::Value, String)>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));

Expand All @@ -24,7 +22,6 @@ static SCHEMA_CACHE: LazyLock<Mutex<HashMap<TypeId, (serde_json::Value, String)>
/// # Errors
///
/// Returns an error if schema serialization fails.
#[cfg(feature = "schema")]
pub(crate) fn cached_schema<T: schemars::JsonSchema + 'static>()
-> Result<(serde_json::Value, String), crate::LlmError> {
let type_id = TypeId::of::<T>();
Expand Down Expand Up @@ -577,7 +574,6 @@ pub trait LlmProvider: Send + Sync {
///
/// Default implementation injects JSON schema into the system prompt and retries once
/// on parse failure. Providers with native structured output should override this.
#[cfg(feature = "schema")]
#[allow(async_fn_in_trait)]
async fn chat_typed<T>(&self, messages: &[Message]) -> Result<T, LlmError>
where
Expand Down Expand Up @@ -621,7 +617,6 @@ pub trait LlmProvider: Send + Sync {
/// Strip markdown code fences from LLM output. Only handles outer fences;
/// JSON containing trailing triple backticks in string values may be
/// incorrectly trimmed (acceptable for MVP — see review R2).
#[cfg(feature = "schema")]
fn strip_json_fences(s: &str) -> &str {
s.trim()
.trim_start_matches("```json")
Expand Down Expand Up @@ -1144,37 +1139,31 @@ mod tests {

// --- M27: strip_json_fences tests ---

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_plain_json() {
assert_eq!(strip_json_fences(r#"{"a": 1}"#), r#"{"a": 1}"#);
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_with_json_fence() {
assert_eq!(strip_json_fences("```json\n{\"a\": 1}\n```"), r#"{"a": 1}"#);
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_with_plain_fence() {
assert_eq!(strip_json_fences("```\n{\"a\": 1}\n```"), r#"{"a": 1}"#);
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_whitespace() {
assert_eq!(strip_json_fences(" \n "), "");
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_empty() {
assert_eq!(strip_json_fences(""), "");
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_outer_whitespace() {
assert_eq!(
Expand All @@ -1183,26 +1172,22 @@ mod tests {
);
}

#[cfg(feature = "schema")]
#[test]
fn strip_json_fences_only_opening_fence() {
assert_eq!(strip_json_fences("```json\n{\"a\": 1}"), r#"{"a": 1}"#);
}

// --- M27: chat_typed tests ---

#[cfg(feature = "schema")]
#[derive(Debug, serde::Deserialize, schemars::JsonSchema, PartialEq)]
struct TestOutput {
value: String,
}

#[cfg(feature = "schema")]
struct SequentialStub {
responses: std::sync::Mutex<Vec<Result<String, LlmError>>>,
}

#[cfg(feature = "schema")]
impl SequentialStub {
fn new(responses: Vec<Result<String, LlmError>>) -> Self {
Self {
Expand All @@ -1211,7 +1196,6 @@ mod tests {
}
}

#[cfg(feature = "schema")]
impl LlmProvider for SequentialStub {
async fn chat(&self, _messages: &[Message]) -> Result<String, LlmError> {
let mut responses = self.responses.lock().unwrap();
Expand Down Expand Up @@ -1247,7 +1231,6 @@ mod tests {
}
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_happy_path() {
let provider = StubProvider {
Expand All @@ -1263,7 +1246,6 @@ mod tests {
);
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_retry_succeeds() {
let provider = SequentialStub::new(vec![
Expand All @@ -1275,7 +1257,6 @@ mod tests {
assert_eq!(result, TestOutput { value: "ok".into() });
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_both_fail() {
let provider = SequentialStub::new(vec![Ok("bad json".into()), Ok("still bad".into())]);
Expand All @@ -1285,7 +1266,6 @@ mod tests {
assert!(err.to_string().contains("parse failed after retry"));
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_chat_error_propagates() {
let provider = SequentialStub::new(vec![Err(LlmError::Unavailable)]);
Expand All @@ -1294,7 +1274,6 @@ mod tests {
assert!(matches!(result, Err(LlmError::Unavailable)));
}

#[cfg(feature = "schema")]
#[tokio::test]
async fn chat_typed_strips_fences() {
let provider = StubProvider {
Expand Down
Loading