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;