Skip to main content

mermaid_cli/providers/model/
meta.rs

1//! Meta provider — wraps `models::adapters::meta::MetaAdapter`.
2//!
3//! Same pattern as its four siblings now: the adapter owns the wire format
4//! (the Responses endpoint, stateless encrypted-reasoning replay); this
5//! wrapper plumbs `ChatRequest` / `StreamContext` into it.
6//!
7//! Meta is the second provider that emits a `provider_continuation` which
8//! MUST round-trip — the encrypted reasoning items, replayed verbatim so
9//! thinking survives a tool turn. The adapter's
10//! `ModelResponse.provider_continuation` carries it; we forward that onto
11//! the `FinalResponse` so the reducer can commit it.
12
13use std::collections::HashMap;
14
15use async_trait::async_trait;
16
17use mermaid_domain::ChatRequest;
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, RejectionCache};
23
24pub use mermaid_model::models::adapters::meta::{DEFAULT_API_KEY_ENV, DEFAULT_BASE_URL};
25
26/// Meta adapter fronted by `ModelProvider`.
27pub struct MetaProvider {
28    adapter: MetaAdapter,
29    capabilities: ModelCapabilities,
30    /// Links the adapter's learned rejections to the cross-session cache.
31    rejections: RejectionCache,
32}
33
34impl MetaProvider {
35    /// Wrap a fresh [`MetaAdapter`] as a `ModelProvider`.
36    ///
37    /// # Errors
38    ///
39    /// Only [`MetaAdapter::new`]'s — the HTTP client build. The API is not
40    /// contacted here, so an invalid key or unreachable `base_url` still
41    /// constructs and fails on the first request.
42    pub fn new(
43        api_key: String,
44        model_name: String,
45        base_url: String,
46        extra_headers: HashMap<String, String>,
47    ) -> Result<Self> {
48        let adapter = MetaAdapter::new(api_key, model_name, base_url, extra_headers)?;
49        let capabilities = Model::capabilities(&adapter).clone();
50        Ok(Self {
51            adapter,
52            capabilities,
53            rejections: RejectionCache::default(),
54        })
55    }
56}
57
58#[async_trait]
59impl ModelProvider for MetaProvider {
60    fn capabilities(&self) -> &ModelCapabilities {
61        &self.capabilities
62    }
63
64    async fn chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse> {
65        let config = ModelConfig::from(&request);
66        let chat_fut = self
67            .adapter
68            .chat(&request.messages, &config, Some(ctx.sink.clone()));
69
70        // Seed what earlier sessions learned this model rejects, and persist
71        // whatever this turn learns (see `learning`).
72        let learned_model = Model::name(&self.adapter).to_string();
73        let memory = self.adapter.param_memory();
74        self.rejections.seed("meta", &learned_model, memory).await;
75
76        // Meta used to select on the token inside its own read loop. The
77        // outer race is equivalent and is what the other four do: dropping
78        // this future drops the response stream with it.
79        let response = tokio::select! {
80            biased;
81            _ = ctx.token.cancelled() => {
82                return Err(ModelError::Cancelled);
83            },
84            r = chat_fut => {
85                self.rejections.persist("meta", &learned_model, memory).await;
86                r?
87            },
88        };
89
90        let usage = response.usage.clone();
91        let provider_continuation = response.provider_continuation.clone();
92        let stop_reason = response.stop_reason.clone();
93        // F3: the terminal Done goes on the same sink the adapter just
94        // finished writing to, so it cannot overtake a still-queued ToolCall.
95        let _ = ctx
96            .sink
97            .send(StreamEvent::Done {
98                usage: usage.clone(),
99                provider_continuation: provider_continuation.clone(),
100                stop_reason: stop_reason.clone(),
101            })
102            .await;
103
104        Ok(FinalResponse {
105            usage,
106            provider_continuation,
107            tool_calls: response.tool_calls.unwrap_or_default(),
108            stop_reason,
109        })
110    }
111}