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