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
use crate::error::{Error, Result};
use crate::types::generation::GenerationResponse;
use crate::utils::{
retry::execute_with_retry_builder, retry::handle_response_json,
retry::operations::GET_GENERATION,
};
use reqwest::Client;
/// API endpoint for generation management.
/// API endpoint for generation information.
pub struct GenerationApi {
pub(crate) client: Client,
pub(crate) config: crate::client::ApiConfig,
}
impl GenerationApi {
/// Creates a new GenerationApi with the given reqwest client and configuration.
#[must_use = "returns an API client that should be used for API calls"]
pub fn new(client: Client, config: &crate::client::ClientConfig) -> Result<Self> {
Ok(Self {
client,
config: config.to_api_config()?,
})
}
/// Retrieves metadata about a specific generation request.
///
/// This endpoint returns detailed information about a generation including
/// cost, token usage, latency, provider information, and more.
///
/// # Arguments
///
/// * `id` - The unique identifier of the generation to retrieve
///
/// # Returns
///
/// Returns a `GenerationResponse` containing comprehensive metadata about the generation:
/// - Basic info: id, model, created_at, origin
/// - Cost info: total_cost, cache_discount, effective_cost
/// - Token usage: tokens_prompt, tokens_completion, total_tokens
/// - Performance: latency, generation_time, moderation_latency
/// - Provider details: provider_name, upstream_id
/// - Features: streamed, cancelled, web_search, media, reasoning
/// - Finish reasons: finish_reason, native_finish_reason
///
/// # Errors
///
/// Returns an error if:
/// - The generation ID is empty or invalid
/// - The API request fails (network issues, authentication, etc.)
/// - The generation is not found
/// - The response cannot be parsed
/// - The server returns an error status code
///
/// # Example
///
/// ```rust,no_run
/// use openrouter_api::OpenRouterClient;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = OpenRouterClient::from_env()?;
/// let generation = client.generation()?.get_generation("gen-123456789").await?;
///
/// println!("Generation ID: {}", generation.id());
/// println!("Model: {}", generation.model());
/// println!("Total cost: ${:.6}", generation.total_cost());
/// println!("Effective cost: ${:.6}", generation.effective_cost());
///
/// if let Some(tokens) = generation.total_tokens() {
/// println!("Total tokens: {}", tokens);
/// if let Some(cost_per_token) = generation.cost_per_token() {
/// println!("Cost per token: ${:.8}", cost_per_token);
/// }
/// }
///
/// if let Some(latency) = generation.latency_seconds() {
/// println!("Latency: {:.2}s", latency);
/// }
///
/// println!("Successful: {}", generation.is_successful());
/// println!("Streamed: {}", generation.was_streamed());
/// println!("Used web search: {}", generation.used_web_search());
/// println!("Included media: {}", generation.included_media());
/// println!("Used reasoning: {}", generation.used_reasoning());
///
/// Ok(())
/// }
/// ```
pub async fn get_generation(&self, id: &str) -> Result<GenerationResponse> {
// Validate the generation ID
if id.trim().is_empty() {
return Err(Error::ConfigError(
"Generation ID cannot be empty".to_string(),
));
}
// Build the URL with query parameter.
let url = self
.config
.base_url
.join("generation")
.map_err(|e| Error::ApiError {
code: 400,
message: format!("Invalid URL for generation endpoint: {e}"),
metadata: None,
})?;
// Execute request with retry logic
let response =
execute_with_retry_builder(&self.config.retry_config, GET_GENERATION, || {
self.client
.get(url.clone())
.query(&[("id", id)])
.headers((*self.config.headers).clone())
})
.await?;
// Handle response with consistent error parsing
handle_response_json::<GenerationResponse>(response, GET_GENERATION).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generation_api_new() {
use crate::tests::test_helpers::test_client_config;
let config = test_client_config();
let client = Client::new();
let generation_api = GenerationApi::new(client, &config).unwrap();
// Verify that the API config was created successfully
// The API key should NOT be stored in the API config for security reasons
// headers is now Arc<HeaderMap>, but Arc implements Deref so methods work the same
assert!(!generation_api.config.headers.is_empty());
assert!(generation_api.config.headers.contains_key("authorization"));
}
#[test]
fn test_generation_id_validation() {
use crate::error::Error;
use crate::tests::test_helpers::test_client_config;
let config = test_client_config();
let client = Client::new();
let generation_api = GenerationApi::new(client, &config).unwrap();
// Test empty ID
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async { generation_api.get_generation("").await });
assert!(result.is_err());
// Test whitespace-only ID
let result = rt.block_on(async { generation_api.get_generation(" ").await });
assert!(result.is_err());
assert!(matches!(
result,
Err(Error::ConfigError(message)) if message == "Generation ID cannot be empty"
));
}
#[test]
fn test_generation_api_base_url_resolves_correct_path() {
use crate::tests::test_helpers::test_client_config;
let config = test_client_config();
let client = Client::new();
let generation_api = GenerationApi::new(client, &config).unwrap();
let url = generation_api.config.base_url.join("generation").unwrap();
assert!(
url.path().ends_with("/generation"),
"Expected path ending with /generation, got: {}",
url.path()
);
assert!(
!url.path().contains("/api/v1/api/v1/"),
"generation endpoint must not duplicate /api/v1/: {}",
url.path()
);
}
}