diff --git a/CHANGELOG.md b/CHANGELOG.md index bb937557..a4210ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/zeph-llm/Cargo.toml b/crates/zeph-llm/Cargo.toml index 11561cf1..3c7bb1fe 100644 --- a/crates/zeph-llm/Cargo.toml +++ b/crates/zeph-llm/Cargo.toml @@ -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"] @@ -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"] } diff --git a/crates/zeph-llm/src/any.rs b/crates/zeph-llm/src/any.rs index 5d3bfa22..6034255f 100644 --- a/crates/zeph-llm/src/any.rs +++ b/crates/zeph-llm/src/any.rs @@ -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::{ @@ -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(&self, messages: &[Message]) -> Result where T: DeserializeOwned + JsonSchema + 'static, @@ -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)] diff --git a/crates/zeph-llm/src/claude/mod.rs b/crates/zeph-llm/src/claude/mod.rs index edb409bc..4f252ce3 100644 --- a/crates/zeph-llm/src/claude/mod.rs +++ b/crates/zeph-llm/src/claude/mod.rs @@ -818,7 +818,6 @@ impl LlmProvider for ClaudeProvider { true } - #[cfg(feature = "schema")] async fn chat_typed(&self, messages: &[Message]) -> Result where T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static, diff --git a/crates/zeph-llm/src/claude/types.rs b/crates/zeph-llm/src/claude/types.rs index 07a5f524..beb6ab59 100644 --- a/crates/zeph-llm/src/claude/types.rs +++ b/crates/zeph-llm/src/claude/types.rs @@ -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, @@ -589,7 +588,6 @@ pub(super) struct TypedToolRequestBody<'a> { pub context_management: Option, } -#[cfg(feature = "schema")] #[derive(Serialize)] pub(super) struct ToolChoice<'a> { pub r#type: &'a str, diff --git a/crates/zeph-llm/src/compatible.rs b/crates/zeph-llm/src/compatible.rs index 230962f0..c65d5048 100644 --- a/crates/zeph-llm/src/compatible.rs +++ b/crates/zeph-llm/src/compatible.rs @@ -113,7 +113,6 @@ impl LlmProvider for CompatibleProvider { self.inner.supports_structured_output() } - #[cfg(feature = "schema")] async fn chat_typed(&self, messages: &[Message]) -> Result where T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static, diff --git a/crates/zeph-llm/src/lib.rs b/crates/zeph-llm/src/lib.rs index 0591344e..c1e14e18 100644 --- a/crates/zeph-llm/src/lib.rs +++ b/crates/zeph-llm/src/lib.rs @@ -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; @@ -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}; diff --git a/crates/zeph-llm/src/openai/mod.rs b/crates/zeph-llm/src/openai/mod.rs index 0e10d985..87ad5f5d 100644 --- a/crates/zeph-llm/src/openai/mod.rs +++ b/crates/zeph-llm/src/openai/mod.rs @@ -552,7 +552,6 @@ impl LlmProvider for OpenAiProvider { true } - #[cfg(feature = "schema")] async fn chat_typed(&self, messages: &[Message]) -> Result where T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static, @@ -1150,7 +1149,6 @@ struct EmbeddingData { embedding: Vec, } -#[cfg(feature = "schema")] #[derive(Serialize)] struct TypedChatRequest<'a> { model: &'a str, @@ -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, diff --git a/crates/zeph-llm/src/openai/tests.rs b/crates/zeph-llm/src/openai/tests.rs index 553d6f23..50593a4f 100644 --- a/crates/zeph-llm/src/openai/tests.rs +++ b/crates/zeph-llm/src/openai/tests.rs @@ -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 { diff --git a/crates/zeph-llm/src/orchestrator/classifier.rs b/crates/zeph-llm/src/orchestrator/classifier.rs index d8cece65..43bc2f0f 100644 --- a/crates/zeph-llm/src/orchestrator/classifier.rs +++ b/crates/zeph-llm/src/orchestrator/classifier.rs @@ -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, diff --git a/crates/zeph-llm/src/orchestrator/mod.rs b/crates/zeph-llm/src/orchestrator/mod.rs index 56c3e148..6138207f 100644 --- a/crates/zeph-llm/src/orchestrator/mod.rs +++ b/crates/zeph-llm/src/orchestrator/mod.rs @@ -182,7 +182,6 @@ impl ModelOrchestrator { .expect("default provider must exist") } - #[cfg(feature = "schema")] async fn try_llm_routing(&self, messages: &[Message]) -> Option { if !self.llm_routing { return None; @@ -225,12 +224,6 @@ impl ModelOrchestrator { } } - #[cfg(not(feature = "schema"))] - #[allow(clippy::unused_async)] - async fn try_llm_routing(&self, _messages: &[Message]) -> Option { - None - } - async fn chat_with_fallback(&self, messages: &[Message]) -> Result { if let Some(selected) = self.try_llm_routing(messages).await && let Some(provider) = self.providers.get(&selected) diff --git a/crates/zeph-llm/src/provider.rs b/crates/zeph-llm/src/provider.rs index 8ebf24f0..306b2f35 100644 --- a/crates/zeph-llm/src/provider.rs +++ b/crates/zeph-llm/src/provider.rs @@ -3,7 +3,6 @@ use std::future::Future; use std::pin::Pin; -#[cfg(feature = "schema")] use std::{ any::TypeId, collections::HashMap, @@ -15,7 +14,6 @@ use serde::{Deserialize, Serialize}; use crate::error::LlmError; -#[cfg(feature = "schema")] static SCHEMA_CACHE: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); @@ -24,7 +22,6 @@ static SCHEMA_CACHE: LazyLock /// # Errors /// /// Returns an error if schema serialization fails. -#[cfg(feature = "schema")] pub(crate) fn cached_schema() -> Result<(serde_json::Value, String), crate::LlmError> { let type_id = TypeId::of::(); @@ -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(&self, messages: &[Message]) -> Result where @@ -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") @@ -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!( @@ -1183,7 +1172,6 @@ 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}"#); @@ -1191,18 +1179,15 @@ mod tests { // --- 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>>, } - #[cfg(feature = "schema")] impl SequentialStub { fn new(responses: Vec>) -> Self { Self { @@ -1211,7 +1196,6 @@ mod tests { } } - #[cfg(feature = "schema")] impl LlmProvider for SequentialStub { async fn chat(&self, _messages: &[Message]) -> Result { let mut responses = self.responses.lock().unwrap(); @@ -1247,7 +1231,6 @@ mod tests { } } - #[cfg(feature = "schema")] #[tokio::test] async fn chat_typed_happy_path() { let provider = StubProvider { @@ -1263,7 +1246,6 @@ mod tests { ); } - #[cfg(feature = "schema")] #[tokio::test] async fn chat_typed_retry_succeeds() { let provider = SequentialStub::new(vec![ @@ -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())]); @@ -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)]); @@ -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 {