1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//! Generator information - configuration for LLM providers
use crate::provider::{
AnthropicProvider, AppIdentity, Auth, GenericProvider, OpenAiProvider, OpenRouterProvider,
Provider, TokenPrice,
};
use secrecy::SecretString;
use std::sync::Arc;
/// Configuration for an LLM provider/generator
#[derive(Debug, Clone)]
pub struct GeneratorInfo {
/// Display name for this generator
pub name: String,
/// Base URL for the API (e.g., `https://openrouter.ai/api/v1`)
pub base_url: String,
/// Model identifier (e.g., "anthropic/claude-3.5-sonnet")
pub model: String,
/// How this generator authenticates. The provider maps it to concrete headers
/// (OpenAI-wire `Authorization: Bearer`, Anthropic `x-api-key` or bearer).
pub auth: Auth,
/// Custom headers to include in requests
pub custom_headers: Vec<(String, String)>,
/// Whether this provider supports streaming
pub supports_streaming: bool,
/// Whether this provider supports vision/images
pub supports_vision: bool,
/// Whether this provider supports audio input
pub supports_audio: bool,
/// Maximum context length (tokens)
pub max_context_length: Option<usize>,
/// Provider implementation: the wire dialect (token-limit key, usage opt-in,
/// usage parsing, cost aggregation, out-of-band resolution, attribution
/// headers). Swap this to target a different provider.
pub provider: Arc<dyn Provider>,
/// Per-token price for this model, used to derive cost when the provider
/// returns token counts but no dollar amount (OpenAI, Anthropic, ...). When
/// `None` and the provider has no native cost, cost is reported `Unpriced`.
pub token_price: Option<TokenPrice>,
/// Calling-app identity for providers that attribute usage to an app (e.g.
/// OpenRouter rankings). The provider decides which headers express it.
pub app_attribution: Option<AppIdentity>,
/// Default completion parameters for this generator
pub default_params: super::CompletionParameters,
}
impl GeneratorInfo {
/// Create a new GeneratorInfo with minimal configuration (generic
/// OpenAI-compatible accounting; set `with_provider` for a specific provider).
pub fn new(
name: impl Into<String>,
base_url: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self {
name: name.into(),
base_url: base_url.into(),
model: model.into(),
auth: Auth::None,
custom_headers: Vec::new(),
supports_streaming: true,
supports_vision: false,
supports_audio: false,
max_context_length: None,
provider: Arc::new(GenericProvider::default()),
token_price: None,
app_attribution: None,
default_params: super::CompletionParameters::default(),
}
}
/// Set the calling-app identity for provider usage attribution.
pub fn with_app_attribution(
mut self,
url: impl Into<String>,
title: impl Into<String>,
) -> Self {
self.app_attribution = Some(AppIdentity {
url: url.into(),
title: title.into(),
});
self
}
/// Set the provider implementation (wire dialect: token-limit key, usage
/// opt-in, usage parsing, cost aggregation, out-of-band resolution,
/// attribution headers).
pub fn with_provider(mut self, provider: Arc<dyn Provider>) -> Self {
self.provider = provider;
self
}
/// Set the per-token price (USD per million tokens) used to derive cost for
/// providers that return token counts but no dollar amount.
pub fn with_token_price(mut self, price: TokenPrice) -> Self {
self.token_price = Some(price);
self
}
/// Set the API key (provider-issued; the provider chooses the header).
pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
self.auth = Auth::ApiKey(SecretString::from(key.into()));
self
}
/// Set the API key from an environment variable (no-op if unset).
pub fn with_api_key_from_env(mut self, env_var: &str) -> Self {
if let Ok(key) = std::env::var(env_var) {
self.auth = Auth::ApiKey(SecretString::from(key));
}
self
}
/// Set an OAuth/bearer token (e.g. a Claude subscription token). Always sent
/// as `Authorization: Bearer <token>`.
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
self.auth = Auth::BearerToken(SecretString::from(token.into()));
self
}
/// Set a bearer token from an environment variable (no-op if unset).
pub fn with_bearer_token_from_env(mut self, env_var: &str) -> Self {
if let Ok(token) = std::env::var(env_var) {
self.auth = Auth::BearerToken(SecretString::from(token));
}
self
}
/// Set the auth strategy directly.
pub fn with_auth(mut self, auth: Auth) -> Self {
self.auth = auth;
self
}
/// Add a custom header
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.custom_headers.push((name.into(), value.into()));
self
}
/// Enable vision support
pub fn with_vision(mut self) -> Self {
self.supports_vision = true;
self
}
/// Enable audio support
pub fn with_audio(mut self) -> Self {
self.supports_audio = true;
self
}
/// Set max context length
pub fn with_max_context(mut self, length: usize) -> Self {
self.max_context_length = Some(length);
self
}
/// Set default completion parameters
pub fn with_default_params(mut self, params: super::CompletionParameters) -> Self {
self.default_params = params;
self
}
/// Get the full completions endpoint URL (the provider owns the path suffix:
/// OpenAI-wire `/chat/completions`, Anthropic `/v1/messages`).
pub fn completions_url(&self) -> String {
self.provider.endpoint_url(&self.base_url)
}
}
// Pre-configured generators for common providers
impl GeneratorInfo {
/// Create an OpenRouter generator (native USD cost, `/generation` fallback).
///
/// Attribution defaults to the library's identity; override it with
/// [`with_app_attribution`](Self::with_app_attribution) (or a
/// `CompletionContext`) to attribute usage to your app. The OpenRouter
/// provider turns the attribution into `HTTP-Referer`/`X-Title` headers.
pub fn openrouter(model: impl Into<String>) -> Self {
Self::new("OpenRouter", "https://openrouter.ai/api/v1", model)
.with_provider(Arc::new(OpenRouterProvider))
.with_api_key_from_env("OPENROUTER_API_KEY")
.with_app_attribution("https://github.com/minillmlib", "MiniLLMLib")
}
/// Create an OpenAI generator.
///
/// OpenAI returns token counts but no dollar cost, so set a
/// [`with_token_price`](Self::with_token_price) to get a resolved cost;
/// otherwise cost tracking reports `Unpriced`.
pub fn openai(model: impl Into<String>) -> Self {
Self::new("OpenAI", "https://api.openai.com/v1", model)
.with_provider(Arc::new(OpenAiProvider))
.with_api_key_from_env("OPENAI_API_KEY")
}
/// Create a native Anthropic generator (`/v1/messages`, `content[]` envelope,
/// `x-api-key` auth from `ANTHROPIC_API_KEY`).
///
/// Anthropic returns token counts but no dollar cost, so set a
/// [`with_token_price`](Self::with_token_price) for a resolved cost; otherwise
/// cost tracking reports `Unpriced`.
pub fn anthropic(model: impl Into<String>) -> Self {
Self::new("Anthropic", "https://api.anthropic.com", model)
.with_provider(Arc::new(AnthropicProvider))
.with_api_key_from_env("ANTHROPIC_API_KEY")
}
/// Create a Claude **subscription** generator: native Anthropic wire,
/// authenticated with a Claude Pro/Max OAuth bearer token so usage draws on
/// the **subscription's** rolling quota rather than pay-as-you-go API billing.
///
/// The token is resolved by [`crate::resolve_claude_subscription_auth`]: the
/// `ANTHROPIC_AUTH_TOKEN` env var supersedes, otherwise the live Claude Code
/// credential at `~/.claude/.credentials.json` (which Claude Code keeps
/// refreshed) is used. NOTE: a Console/API OAuth token (e.g. from the `ant`
/// CLI) bills the API account, NOT the subscription; use an API key via
/// [`anthropic`](Self::anthropic) for Console, and this preset only for the
/// actual Pro/Max subscription token.
///
/// Cost is an ESTIMATE: Anthropic returns only token counts, so set a
/// [`with_token_price`](Self::with_token_price) reflecting the model's
/// published price for a `Resolved` USD estimate; otherwise `Unpriced`.
pub fn claude_subscription(model: impl Into<String>) -> Self {
Self::new("Claude (subscription)", "https://api.anthropic.com", model)
.with_provider(Arc::new(AnthropicProvider))
.with_auth(crate::provider::resolve_claude_subscription_auth())
}
/// Create a custom URL-based generator
pub fn custom(
name: impl Into<String>,
base_url: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self::new(name, base_url, model)
}
}