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;