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;
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}
31
32impl MetaProvider {
33    /// Wrap a fresh [`MetaAdapter`] as a `ModelProvider`.
34    ///
35    /// # Errors
36    ///
37    /// Only [`MetaAdapter::new`]'s — the HTTP client build. The API is not
38    /// contacted here, so an invalid key or unreachable `base_url` still
39    /// constructs and fails on the first request.
40    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 = ModelConfig::from(&request);
63        let chat_fut = self
64            .adapter
65            .chat(&request.messages, &config, Some(ctx.sink.clone()));
66
67        // Meta used to select on the token inside its own read loop. The
68        // outer race is equivalent and is what the other four do: dropping
69        // this future drops the response stream with it.
70        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        // F3: the terminal Done goes on the same sink the adapter just
82        // finished writing to, so it cannot overtake a still-queued ToolCall.
83        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}