Repository navigation
Report Gemini 402 as out of credit and probe the key in check #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aadf6c3
4a8066b
1b5e54f
3efacbb
133ba17
4c9417e
016caeb
11b44d7
4bcbe32
5a1fc75
402ef81
d4838e9
dc4a59f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,7 @@ impl GeminiClient { | |
| /// - [`Error::MissingApiKey`] without `GEMINI_API_KEY`. | ||
| /// - [`Error::RateLimited`] on a 429, with the `Retry-After` it sent. | ||
| /// - [`Error::ModelNotFound`] on a 404: the model name is likely retired. | ||
| /// - [`Error::BillingExhausted`] on a 402: the key is out of credit. | ||
| /// - [`Error::Unavailable`] on any other failure status or a failed | ||
| /// request. | ||
| /// - [`Error::Timeout`] if the request itself times out. | ||
|
|
@@ -138,27 +139,7 @@ impl GeminiClient { | |
| .await | ||
| .map_err(request_error)?; | ||
|
|
||
| let status = reply.status(); | ||
| if status.as_u16() == 429 { | ||
| let retry_after_ms = reply | ||
| .headers() | ||
| .get(reqwest::header::RETRY_AFTER) | ||
| .and_then(|v| v.to_str().ok()) | ||
| .and_then(|v| v.trim().parse::<u64>().ok()) | ||
| .map_or(DEFAULT_RETRY_AFTER_MS, |secs| secs.saturating_mul(1000)); | ||
| return Err(Error::RateLimited { retry_after_ms }); | ||
| } | ||
| if status.as_u16() == 404 { | ||
| return Err(Error::ModelNotFound { | ||
| model: self.model.clone(), | ||
| }); | ||
| } | ||
| if !status.is_success() { | ||
| return Err(Error::Unavailable(format!( | ||
| "gemini answered with status {}", | ||
| status.as_u16() | ||
| ))); | ||
| } | ||
| let reply = self.success(reply)?; | ||
| let raw: Value = reply.json().await.map_err(|e| { | ||
| if e.is_timeout() { | ||
| Error::Timeout | ||
|
|
@@ -176,6 +157,62 @@ impl GeminiClient { | |
| cached_age: None, | ||
| }) | ||
| } | ||
|
|
||
| /// Asks Gemini for one token, with no search grounding and no cache, to | ||
| /// learn whether the key and model answer: the cheapest request that | ||
| /// still meets billing. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// The errors of [`generate`](Self::generate) for a failure status or a | ||
| /// failed request; [`Error::BillingExhausted`] when the key is out of | ||
| /// credit. | ||
| pub async fn probe(&self) -> Result<()> { | ||
| let key = self.api_key.as_deref().ok_or(Error::MissingApiKey)?; | ||
| let url = format!( | ||
| "{}/v1beta/models/{}:generateContent", | ||
| self.base_url, self.model | ||
| ); | ||
| let body = json!({ | ||
| "contents": [{"role": "user", "parts": [{"text": "ping"}]}], | ||
| "generationConfig": {"temperature": 0, "maxOutputTokens": 1}, | ||
| }); | ||
| let reply = self | ||
| .http | ||
| .post(url) | ||
| .header("x-goog-api-key", key) | ||
| .json(&body) | ||
| .send() | ||
| .await | ||
| .map_err(request_error)?; | ||
|
Comment on lines
+185
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Gemini or a configured proxy accepts the connection but never returns response headers, this request can wait indefinitely: Useful? React with 👍 / 👎. |
||
| self.success(reply).map(|_| ()) | ||
| } | ||
|
|
||
| /// `reply` itself when its status is a success, or the error its status | ||
| /// stands for. | ||
| fn success(&self, reply: reqwest::Response) -> Result<reqwest::Response> { | ||
| let status = reply.status(); | ||
| match status.as_u16() { | ||
| 429 => { | ||
| let retry_after_ms = reply | ||
| .headers() | ||
| .get(reqwest::header::RETRY_AFTER) | ||
| .and_then(|v| v.to_str().ok()) | ||
| .and_then(|v| v.trim().parse::<u64>().ok()) | ||
| .map_or(DEFAULT_RETRY_AFTER_MS, |secs| secs.saturating_mul(1000)); | ||
| Err(Error::RateLimited { retry_after_ms }) | ||
| } | ||
| 404 => Err(Error::ModelNotFound { | ||
| model: self.model.clone(), | ||
| }), | ||
| 402 => Err(Error::BillingExhausted), | ||
| _ if !status.is_success() => Err(Error::Unavailable(format!( | ||
| "gemini answered with status {}", | ||
| status.as_u16() | ||
| ))), | ||
| _ => Ok(reply), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn request_error(error: reqwest::Error) -> Error { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set a deadline on the probe request.
If Gemini accepts a connection but does not answer,
send().awaitcan remain pending. The reqwest 0.12 asynchronous client has no timeout by default, andcheck::runawaits this probe before it checks arXiv or returns a report. Set a request-specific timeout so the check can report a failure. (raw.githubusercontent.com)