Skip to main content

a3s_code_core/llm/
mod.rs

1//! LLM client abstraction layer
2//!
3//! Provides a unified interface for interacting with LLM providers
4//! (Anthropic Claude, OpenAI, Zhipu AI GLM, and OpenAI-compatible providers).
5
6mod admission;
7pub mod anthropic;
8pub mod codex_login;
9mod error;
10pub mod factory;
11pub mod http;
12pub mod openai;
13pub mod structured;
14mod token_estimation;
15mod types;
16pub mod zhipu;
17
18// Re-export public types
19pub use admission::{
20    ModelGenerationAdmission, ModelGenerationAdmissionError, ModelGenerationConcurrency,
21    ModelGenerationPermit, ModelGenerationPool, ModelGenerationPoolError,
22    ModelGenerationPoolHealthSnapshot,
23};
24pub use anthropic::AnthropicClient;
25pub use codex_login::CodexLoginClient;
26pub(crate) use error::non_retryable_llm_error_message;
27pub use error::NonRetryableLlmError;
28pub use factory::{create_client_with_config, LlmConfig};
29pub use http::{
30    clear_http_metrics_callback, default_http_client, set_http_metrics_callback, HttpClient,
31    HttpClientError, HttpMetricsCallback, HttpMetricsRecord, HttpResponse, StreamingHttpResponse,
32};
33pub use openai::OpenAiClient;
34pub(crate) use token_estimation::{
35    estimate_message_tokens, estimate_prompt_tokens, estimate_tool_result_contents_tokens,
36};
37pub use types::*;
38pub use zhipu::ZhipuClient;
39
40use anyhow::Result;
41use async_trait::async_trait;
42use std::time::Duration;
43use tokio::sync::mpsc;
44use tokio_util::sync::CancellationToken;
45
46// `LlmConfig::retry_config` and provider builder methods already expose this
47// type in the public API. Re-export it from the same module so hosts can
48// configure retry authority without reaching into a crate-private module.
49pub use crate::retry::{RetryConfig, RetryExhaustedError, MAX_RETRIES};
50
51/// LLM client trait
52#[async_trait]
53pub trait LlmClient: Send + Sync {
54    /// Report the client's explicitly supported active-generation capacity.
55    ///
56    /// The conservative default is single-flight. Providers that can safely
57    /// serve more active generations must override this with a typed contract;
58    /// callers must not infer concurrency from provider names or endpoint
59    /// strings.
60    fn model_generation_concurrency(&self) -> ModelGenerationConcurrency {
61        ModelGenerationConcurrency::single_flight()
62    }
63
64    /// Describe the non-secret provider/model capacity pool shared by this
65    /// client. The default is `None` for custom clients that do not expose a
66    /// stable routing identity; they retain the existing per-client gate.
67    fn model_generation_pool(&self) -> Option<ModelGenerationPool> {
68        None
69    }
70
71    /// Rebind a governed model facade to an invocation-owned generation gate.
72    ///
73    /// This is primarily used when a nested workflow supplies a tighter
74    /// admission gate than the surrounding session (for example, a Flow step
75    /// with its own `maxConcurrentGenerations` limit). Raw provider clients do
76    /// not need to implement this hook; the agent runtime can wrap them. A
77    /// client that already owns model-generation admission should preserve the
78    /// provider transport while replacing the facade's gate and, when given,
79    /// consuming the one pre-admitted permit exactly once.
80    fn bind_model_generation_admission(
81        &self,
82        _admission: ModelGenerationAdmission,
83        _preadmitted: Option<std::sync::Arc<ModelGenerationPermit>>,
84    ) -> Option<std::sync::Arc<dyn LlmClient>> {
85        None
86    }
87
88    /// Whether this client already applies the model-generation admission
89    /// contract around every provider call. Built-in run-bound clients use
90    /// this marker so structured tools do not acquire the same permit twice.
91    fn model_generation_is_managed(&self) -> bool {
92        false
93    }
94
95    /// Take queue wait accumulated by a managed client since the previous
96    /// observation. Unmanaged clients report zero.
97    fn take_model_generation_queue_wait(&self) -> Duration {
98        Duration::ZERO
99    }
100
101    /// Derive a provider client bound to one logical agent session.
102    ///
103    /// Stateless providers can keep the default and share the existing client.
104    /// Account-backed providers whose transport uses a live session identity
105    /// should return an independent client so parallel child agents do not
106    /// contend for the parent's active operation.
107    fn fork_for_session(&self, _session_id: &str) -> Option<std::sync::Arc<dyn LlmClient>> {
108        None
109    }
110
111    /// Return a view of this client configured for one active generation
112    /// deadline. The caller still owns and enforces the outer deadline.
113    ///
114    /// Composite and account-backed clients can use this budget to configure
115    /// their underlying transport without inferring timeout intent from error
116    /// text. Stateless clients may keep the default.
117    fn with_active_generation_timeout(
118        &self,
119        _timeout: Duration,
120    ) -> Option<std::sync::Arc<dyn LlmClient>> {
121        None
122    }
123
124    /// Complete a conversation (non-streaming)
125    async fn complete(
126        &self,
127        messages: &[Message],
128        system: Option<&str>,
129        tools: &[ToolDefinition],
130    ) -> Result<LlmResponse>;
131
132    /// Complete a conversation with streaming
133    /// Returns a receiver for streaming events.
134    /// The cancel_token is checked during the HTTP request; if cancelled, the request is aborted.
135    async fn complete_streaming(
136        &self,
137        messages: &[Message],
138        system: Option<&str>,
139        tools: &[ToolDefinition],
140        cancel_token: CancellationToken,
141    ) -> Result<mpsc::Receiver<StreamEvent>>;
142
143    /// Report the strongest provider-native structured-output enforcement this
144    /// client supports. Used by [`structured`] to decide whether to force a
145    /// tool call, request a native `response_format`, or fall back to
146    /// prompt-and-parse. Defaults to no native support.
147    fn native_structured_support(&self) -> structured::NativeStructuredSupport {
148        structured::NativeStructuredSupport::None
149    }
150
151    /// Report whether [`LlmClient::complete_structured`] uses a transport that
152    /// is independent from the streaming implementation.
153    ///
154    /// The conservative default is false because several account-backed
155    /// clients implement `complete` by opening a stream and waiting for its
156    /// terminal event. Composite reliability layers use this capability to
157    /// avoid presenting the same streaming failure mode as a non-streaming
158    /// fallback.
159    fn has_distinct_non_streaming_transport(&self) -> bool {
160        false
161    }
162
163    /// Complete a conversation while honoring a structured-output directive
164    /// (forced `tool_choice` and/or native `response_format`).
165    ///
166    /// The default implementation ignores the directive and behaves exactly
167    /// like [`LlmClient::complete`], so existing clients keep working unchanged;
168    /// providers that support native structured output override this.
169    async fn complete_structured(
170        &self,
171        messages: &[Message],
172        system: Option<&str>,
173        tools: &[ToolDefinition],
174        _directive: &structured::StructuredDirective,
175    ) -> Result<LlmResponse> {
176        self.complete(messages, system, tools).await
177    }
178
179    /// Streaming counterpart of [`LlmClient::complete_structured`]. Defaults to
180    /// [`LlmClient::complete_streaming`], ignoring the directive.
181    async fn complete_streaming_structured(
182        &self,
183        messages: &[Message],
184        system: Option<&str>,
185        tools: &[ToolDefinition],
186        _directive: &structured::StructuredDirective,
187        cancel_token: CancellationToken,
188    ) -> Result<mpsc::Receiver<StreamEvent>> {
189        self.complete_streaming(messages, system, tools, cancel_token)
190            .await
191    }
192}
193
194// Include test modules — these reference internal types via crate paths
195#[cfg(test)]
196#[path = "tests.rs"]
197mod tests_file;