mermaid_cli/providers/model/
meta.rs1use std::collections::HashMap;
14
15use async_trait::async_trait;
16
17use mermaid_domain::{ChatRequest, ToolDefinition};
18use mermaid_model::models::adapters::meta::MetaAdapter;
19use mermaid_model::models::{Model, ModelCapabilities, ModelConfig, ModelError, Result};
20
21use super::super::ctx::{FinalResponse, StreamContext, StreamEvent};
22use super::ModelProvider;
23
24pub use mermaid_model::models::adapters::meta::{DEFAULT_API_KEY_ENV, DEFAULT_BASE_URL};
25
26pub struct MetaProvider {
28 adapter: MetaAdapter,
29 capabilities: ModelCapabilities,
30}
31
32impl MetaProvider {
33 pub fn new(
41 api_key: String,
42 model_name: String,
43 base_url: String,
44 extra_headers: HashMap<String, String>,
45 ) -> Result<Self> {
46 let adapter = MetaAdapter::new(api_key, model_name, base_url, extra_headers)?;
47 let capabilities = Model::capabilities(&adapter).clone();
48 Ok(Self {
49 adapter,
50 capabilities,
51 })
52 }
53}
54
55#[async_trait]
56impl ModelProvider for MetaProvider {
57 fn capabilities(&self) -> &ModelCapabilities {
58 &self.capabilities
59 }
60
61 async fn chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse> {
62 let config = build_model_config(&request);
63 let chat_fut = self
64 .adapter
65 .chat(&request.messages, &config, Some(ctx.sink.clone()));
66
67 let response = tokio::select! {
71 biased;
72 _ = ctx.token.cancelled() => {
73 return Err(ModelError::Cancelled);
74 },
75 r = chat_fut => r?,
76 };
77
78 let usage = response.usage.clone();
79 let provider_continuation = response.provider_continuation.clone();
80 let stop_reason = response.stop_reason.clone();
81 let _ = ctx
84 .sink
85 .send(StreamEvent::Done {
86 usage: usage.clone(),
87 provider_continuation: provider_continuation.clone(),
88 stop_reason: stop_reason.clone(),
89 })
90 .await;
91
92 Ok(FinalResponse {
93 usage,
94 provider_continuation,
95 tool_calls: response.tool_calls.unwrap_or_default(),
96 stop_reason,
97 })
98 }
99}
100
101fn build_model_config(request: &ChatRequest) -> ModelConfig {
102 ModelConfig {
103 model: request.model_id.clone(),
104 temperature: request.temperature,
105 max_tokens: request.max_tokens,
106 reasoning: request.reasoning,
107 system_prompt: Some(request.system_prompt.clone()),
108 dynamic_system_suffix: request.instructions.clone(),
109 tools: request
110 .tools
111 .iter()
112 .map(ToolDefinition::to_openai_json)
113 .collect(),
114 resolved_context_window: request.resolved_context_window,
115 resolved_max_output: request.resolved_max_output,
116 output_schema: request.output_schema.clone(),
117 ..Default::default()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use mermaid_domain::ToolDefinition;
125
126 #[test]
127 fn build_model_config_maps_fields() {
128 let req = ChatRequest {
129 model_id: "meta/muse-spark-1.1".to_string(),
130 messages: vec![],
131 system_prompt: "system".to_string(),
132 instructions: Some("project".to_string()),
133 reasoning: mermaid_model::models::ReasoningLevel::Max,
134 temperature: 0.7,
135 max_tokens: 200_000,
136 tools: vec![ToolDefinition {
137 name: "read_file".to_string(),
138 description: "Read a file".to_string(),
139 input_schema: serde_json::json!({"type": "object"}),
140 }],
141 ollama_num_ctx: None,
142 ollama_allow_ram_offload: None,
143 resolved_context_window: Some(mermaid_model::constants::META_MUSE_SPARK_CONTEXT_WINDOW),
144 resolved_max_output: Some(mermaid_model::constants::META_MUSE_SPARK_MAX_OUTPUT_TOKENS),
145 output_schema: None,
146 suppress_auto_compact: false,
147 suppressed_builtin_tools: Vec::new(),
148 };
149 let cfg = build_model_config(&req);
150 assert_eq!(cfg.dynamic_system_suffix.as_deref(), Some("project"));
151 assert_eq!(
152 cfg.resolved_max_output,
153 Some(mermaid_model::constants::META_MUSE_SPARK_MAX_OUTPUT_TOKENS)
154 );
155 assert_eq!(cfg.tools[0]["function"]["name"], "read_file");
159 }
160}