返回 CodeWhale
model.rs
根目录 / crates / tui / src / core / runtime_contract / model.rs
1 use std::sync::Arc;
2
3 use anyhow::Result;
4 use async_trait::async_trait;
5
6 use crate::llm_client::LlmClient;
7 use crate::llm_client::StreamEventBox;
8 use codewhale_models::{MessageRequest, MessageResponse};
9
10 /// Object-safe model boundary for Engine dependency injection.
11 ///
12 /// The existing `LlmClient` uses return-position `impl Future`, which is
13 /// efficient for concrete providers but cannot be placed behind `dyn`. This
14 /// adapter preserves that provider trait while giving deterministic Engine
15 /// tests and alternate adapters one injectable boundary.
16 #[async_trait]
17 pub trait ModelClient: Send + Sync {
18 fn provider_name(&self) -> &str;
19 fn model(&self) -> &str;
20 /// Concrete route base for billing classification, when this client can
21 /// prove one. Provider-neutral injected clients leave it unknown.
22 fn billing_base_url(&self) -> Option<&str> {
23 None
24 }
25 fn route_limits(&self) -> Option<codewhale_config::route::RouteLimits> {
26 None
27 }
28 fn effective_max_output_tokens(&self, requested_model: &str) -> u32 {
29 let route = self.effective_route_envelope(requested_model, chrono::Utc::now());
30 crate::route_budget::effective_max_output_tokens_for_route(
31 route.provider,
32 &route.model,
33 self.route_limits(),
34 )
35 }
36 fn effective_route_envelope(
37 &self,
38 requested_model: &str,
39 dispatched_at: chrono::DateTime<chrono::Utc>,
40 ) -> crate::cost_status::EffectiveRouteEnvelope {
41 let provider = crate::config::ApiProvider::parse(self.provider_name())
42 .unwrap_or(crate::config::ApiProvider::Custom);
43 crate::cost_status::EffectiveRouteEnvelope::capture(
44 None,
45 provider,
46 self.provider_name(),
47 requested_model,
48 self.billing_base_url(),
49 dispatched_at,
50 )
51 }
52 async fn create_message(&self, request: MessageRequest) -> Result<MessageResponse>;
53 /// Fresh authorization evidence; cache-owning adapters must bypass it.
54 async fn create_message_uncached(&self, request: MessageRequest) -> Result<MessageResponse> {
55 self.create_message(request).await
56 }
57 async fn create_message_stream(&self, request: MessageRequest) -> Result<StreamEventBox>;
58 #[expect(dead_code)]
59 async fn health_check(&self) -> Result<bool>;
60 }
61
62 pub type SharedModelClient = Arc<dyn ModelClient>;
63
64 /// Every existing provider client automatically satisfies the injectable
65 /// boundary. This keeps provider-specific HTTP/routing code behind
66 /// `LlmClient` while the Engine owns only the object-safe contract.
67 #[async_trait]
68 impl<T> ModelClient for T
69 where
70 T: LlmClient + Send + Sync,
71 {
72 fn provider_name(&self) -> &str {
73 LlmClient::provider_name(self)
74 }
75
76 fn model(&self) -> &str {
77 LlmClient::model(self)
78 }
79
80 fn billing_base_url(&self) -> Option<&str> {
81 LlmClient::billing_base_url(self)
82 }
83
84 fn route_limits(&self) -> Option<codewhale_config::route::RouteLimits> {
85 LlmClient::route_limits(self)
86 }
87
88 fn effective_max_output_tokens(&self, requested_model: &str) -> u32 {
89 LlmClient::effective_max_output_tokens(self, requested_model)
90 }
91
92 fn effective_route_envelope(
93 &self,
94 requested_model: &str,
95 dispatched_at: chrono::DateTime<chrono::Utc>,
96 ) -> crate::cost_status::EffectiveRouteEnvelope {
97 LlmClient::effective_route_envelope(self, requested_model, dispatched_at)
98 }
99
100 async fn create_message(&self, request: MessageRequest) -> Result<MessageResponse> {
101 LlmClient::create_message(self, request).await
102 }
103
104 async fn create_message_uncached(&self, request: MessageRequest) -> Result<MessageResponse> {
105 LlmClient::create_message_uncached(self, request).await
106 }
107
108 async fn create_message_stream(&self, request: MessageRequest) -> Result<StreamEventBox> {
109 LlmClient::create_message_stream(self, request).await
110 }
111
112 async fn health_check(&self) -> Result<bool> {
113 LlmClient::health_check(self).await
114 }
115 }
116
117 #[cfg(test)]
118 mod tests {
119 use super::*;
120
121 #[test]
122 fn model_client_is_object_safe() {
123 fn accepts_dyn(_: Option<SharedModelClient>) {}
124 accepts_dyn(None);
125 }
126 }
127
127 lines RUST