From aadf6c370856e2a7faff396328550619be354cf4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:25:40 +0000 Subject: [PATCH 01/13] test(web): cover 402 billing exhaustion from gemini Add a test asserting that a 402 response from the Gemini API surfaces as Error::BillingExhausted with the BudgetExceeded code and a message mentioning credit, so the out-of-credit case is distinguished from a transient outage. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/tests/web/gemini.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/supermath-web/tests/web/gemini.rs b/crates/supermath-web/tests/web/gemini.rs index cc33162..e6cb37d 100644 --- a/crates/supermath-web/tests/web/gemini.rs +++ b/crates/supermath-web/tests/web/gemini.rs @@ -42,6 +42,16 @@ async fn a_404_says_the_model_may_be_retired_and_names_the_override() { assert!(message.contains("SUPERMATH_WEB_MODEL"), "{message}"); } +#[tokio::test] +async fn a_402_says_the_key_is_out_of_credit_and_is_not_a_transient_outage() { + let stub = gemini_stub(|_| Response::status(402)).await; + let client = GeminiClient::new(&config(&stub), Arc::new(SystemClock)).unwrap(); + let error = client.generate("hi").await.unwrap_err(); + assert_eq!(error, Error::BillingExhausted); + assert_eq!(error.code(), supermath_tool::ErrorCode::BudgetExceeded); + assert!(error.to_string().contains("credit"), "{error}"); +} + #[tokio::test] async fn generate_returns_the_decoded_response() { let stub = fixed_gemini("grounded.json").await; From 4a8066bde5790472768503e2b8f880afa71c0393 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:25:58 +0000 Subject: [PATCH 02/13] feat(error): add billing exhausted error variant Add a BillingExhausted variant for Gemini 402 responses so an exhausted key's billing or credit is reported as a spent budget rather than a transient outage, which callers should not retry. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/error/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/supermath-web/src/error/mod.rs b/crates/supermath-web/src/error/mod.rs index e8644fe..ada58ee 100644 --- a/crates/supermath-web/src/error/mod.rs +++ b/crates/supermath-web/src/error/mod.rs @@ -41,6 +41,13 @@ pub enum Error { /// The model name that was asked for. model: String, }, + /// Gemini answered 402: the key's billing or credit is exhausted. Not a + /// transient outage, so it reports as a spent budget, which a caller + /// does not retry. + #[error( + "gemini refused the request with 402: the key's billing or credit is exhausted, so web search is off until it is topped up" + )] + BillingExhausted, /// The upstream is down or refused the request. #[error("upstream unavailable: {0}")] Unavailable(String), From 1b5e54f20207367fbcf6b4faec193bcfa837e261 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:26:01 +0000 Subject: [PATCH 03/13] fix(error): map billing exhausted to budget exceeded The error code mapping now also covers the BillingExhausted variant, so it reports BudgetExceeded instead of falling through to the default code. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/error/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/supermath-web/src/error/mod.rs b/crates/supermath-web/src/error/mod.rs index ada58ee..d34ee7d 100644 --- a/crates/supermath-web/src/error/mod.rs +++ b/crates/supermath-web/src/error/mod.rs @@ -116,7 +116,7 @@ impl Error { Self::Forbidden(_) => ErrorCode::Forbidden, Self::RateLimited { .. } => ErrorCode::RateLimited, Self::Timeout => ErrorCode::Timeout, - Self::DailyCapSpent { .. } => ErrorCode::BudgetExceeded, + Self::DailyCapSpent { .. } | Self::BillingExhausted => ErrorCode::BudgetExceeded, Self::InvalidConfig { .. } | Self::Client(_) | Self::MissingApiKey From 3efacbbc4587383388c13003412d7fad369c82c6 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:26:03 +0000 Subject: [PATCH 04/13] fix(gemini): handle 402 billing exhausted responses Map HTTP 402 responses from the Gemini API to a dedicated BillingExhausted error so callers can distinguish an exhausted billing account from a generic service failure. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/gemini/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/supermath-web/src/gemini/mod.rs b/crates/supermath-web/src/gemini/mod.rs index 23787b1..2b25e5e 100644 --- a/crates/supermath-web/src/gemini/mod.rs +++ b/crates/supermath-web/src/gemini/mod.rs @@ -153,6 +153,9 @@ impl GeminiClient { model: self.model.clone(), }); } + if status.as_u16() == 402 { + return Err(Error::BillingExhausted); + } if !status.is_success() { return Err(Error::Unavailable(format!( "gemini answered with status {}", From 133ba17a3f3d063f248c14ca51cee0b95a8524a2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:26:06 +0000 Subject: [PATCH 05/13] docs(gemini): document billing exhausted error Add the missing doc entry for Error::BillingExhausted so the list of errors returned by the client covers the 402 case. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/gemini/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/supermath-web/src/gemini/mod.rs b/crates/supermath-web/src/gemini/mod.rs index 2b25e5e..fb45cb4 100644 --- a/crates/supermath-web/src/gemini/mod.rs +++ b/crates/supermath-web/src/gemini/mod.rs @@ -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. From 4c9417eb61af65b7d2820ca6711d2f76755bea38 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:26:08 +0000 Subject: [PATCH 06/13] test(web): cover billing exhausted error code mapping Add the BillingExhausted variant to the table-driven test that asserts every error maps to its documented code, so the new variant is verified to report BudgetExceeded. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/error/test.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/supermath-web/src/error/test.rs b/crates/supermath-web/src/error/test.rs index 101c064..7bc854d 100644 --- a/crates/supermath-web/src/error/test.rs +++ b/crates/supermath-web/src/error/test.rs @@ -32,6 +32,7 @@ fn maps_every_variant_to_its_documented_code() { ), (Error::Timeout, ErrorCode::Timeout), (Error::DailyCapSpent { limit: 1 }, ErrorCode::BudgetExceeded), + (Error::BillingExhausted, ErrorCode::BudgetExceeded), (Error::InvalidUrl("x".into()), ErrorCode::InvalidInput), (Error::PrivateAddress("x".into()), ErrorCode::InvalidInput), ( From 016caeb8c453725645943943b31a49f7f3b4d7a4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:26:58 +0000 Subject: [PATCH 07/13] test(web): cover gemini probe request shape and billing outcome Add tests asserting that a probe issues a single ungrounded request with a one-token output cap and surfaces a 402 as a billing error, and that a funded key probes successfully. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/tests/web/gemini.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/supermath-web/tests/web/gemini.rs b/crates/supermath-web/tests/web/gemini.rs index e6cb37d..3d1efab 100644 --- a/crates/supermath-web/tests/web/gemini.rs +++ b/crates/supermath-web/tests/web/gemini.rs @@ -52,6 +52,25 @@ async fn a_402_says_the_key_is_out_of_credit_and_is_not_a_transient_outage() { assert!(error.to_string().contains("credit"), "{error}"); } +#[tokio::test] +async fn a_probe_sends_one_ungrounded_token_request_and_reports_a_402() { + let stub = gemini_stub(|_| Response::status(402)).await; + let client = GeminiClient::new(&config(&stub), Arc::new(SystemClock)).unwrap(); + assert_eq!(client.probe().await.unwrap_err(), Error::BillingExhausted); + let requests = stub.requests(); + assert_eq!(requests.len(), 1); + let body = &requests[0].body; + assert!(!body.contains("google_search"), "{body}"); + assert!(body.contains("\"maxOutputTokens\":1"), "{body}"); +} + +#[tokio::test] +async fn a_probe_of_a_funded_key_succeeds() { + let stub = fixed_gemini("grounded.json").await; + let client = GeminiClient::new(&config(&stub), Arc::new(SystemClock)).unwrap(); + client.probe().await.unwrap(); +} + #[tokio::test] async fn generate_returns_the_decoded_response() { let stub = fixed_gemini("grounded.json").await; From 11b44d7f1fa9cf90df1e59ceca82996afab1afa6 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:27:17 +0000 Subject: [PATCH 08/13] test(mcp): cover gemini out-of-credit check failure Add a Gemini stub helper and a test asserting that a 402 from the generateContent endpoint fails the check with a credit-related detail and without leaking the API key. The existing reachable-backends test now also points the web base URL at a Gemini stub so the backend is exercised. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-mcp/tests/check.rs | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/supermath-mcp/tests/check.rs b/crates/supermath-mcp/tests/check.rs index 1bcb20f..219e984 100644 --- a/crates/supermath-mcp/tests/check.rs +++ b/crates/supermath-mcp/tests/check.rs @@ -18,6 +18,21 @@ async fn arxiv() -> Stub { Stub::start(|_| Reply::atom(EMPTY_FEED)).await } +/// A Gemini stub answering every `generateContent` with `status`, or with a +/// one-token reply when it is 200. +async fn gemini(status: u16) -> Stub { + Stub::start(move |_| { + if status == 200 { + Reply::json(&serde_json::json!({ + "candidates": [{"content": {"parts": [{"text": "ok"}]}, "finishReason": "MAX_TOKENS"}] + })) + } else { + Reply::status(status) + } + }) + .await +} + async fn python() -> Stub { Stub::start(|r| match r.path.as_str() { "/health" => Reply::json( @@ -41,13 +56,15 @@ async fn every_reachable_backend_is_reported_with_what_it_found() { let python = python().await; let arxiv = arxiv().await; let cortex = cortex().await; + let gemini = gemini(200).await; let env = Env::new() .set("LEAN_URL", &lean.stub.base) .set("PYTHON_URL", &python.base) .set("SUPERMATH_ARXIV_API_URL", &arxiv.base) .set("CORTEX_API_URL", cortex.url()) .set("CORTEX_API_KEY", "c-secret") - .set("GEMINI_API_KEY", "g-secret"); + .set("GEMINI_API_KEY", "g-secret") + .set("SUPERMATH_WEB_BASE_URL", &gemini.base); let config = env.config(); let report = run(&env.toolkit(), &config).await; assert!(report.passed(), "{report}"); @@ -98,3 +115,19 @@ async fn a_configured_backend_that_does_not_answer_fails_the_check() { ); } } + +#[tokio::test] +async fn a_gemini_key_out_of_credit_fails_the_check() { + let arxiv = arxiv().await; + let gemini = gemini(402).await; + let env = Env::new() + .set("SUPERMATH_ARXIV_API_URL", &arxiv.base) + .set("GEMINI_API_KEY", "g-secret") + .set("SUPERMATH_WEB_BASE_URL", &gemini.base); + let report = run(&env.toolkit(), &env.config()).await; + assert!(!report.passed(), "{report}"); + let gemini_line = line(&report, "gemini"); + assert_eq!(gemini_line.status, Status::Failed); + assert!(gemini_line.detail.contains("credit"), "{report}"); + assert!(!report.to_string().contains("g-secret")); +} From 4bcbe325115d4b34c9dcc78adbb28728e2db740f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:27:55 +0000 Subject: [PATCH 09/13] feat(gemini): add probe to check key and model with a minimal request Adds a probe that sends Gemini a single-token request with no grounding or cache, so callers can verify the API key and model without paying for a full turn. The status-to-error mapping is factored into a shared helper reused by generate, and the toolkit and engine expose the probe through their existing layers. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/src/engine/mod.rs | 4 ++ crates/supermath-web/src/gemini/mod.rs | 81 +++++++++++++++++-------- crates/supermath-web/src/toolkit/mod.rs | 10 +++ 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/crates/supermath-web/src/engine/mod.rs b/crates/supermath-web/src/engine/mod.rs index 857895d..0968d76 100644 --- a/crates/supermath-web/src/engine/mod.rs +++ b/crates/supermath-web/src/engine/mod.rs @@ -117,6 +117,10 @@ impl Engine { /// One grounded Gemini turn: ask, read the grounding, resolve each used /// source, and keep the sentences that still have a source. + pub(crate) async fn probe(&self) -> Result<()> { + self.gemini.probe().await + } + pub(crate) async fn grounded_turn(&self, prompt: &str) -> Result { let generation = self.gemini.generate(prompt).await?; let usage = generation.response.usage_metadata.unwrap_or_default(); diff --git a/crates/supermath-web/src/gemini/mod.rs b/crates/supermath-web/src/gemini/mod.rs index fb45cb4..d736fad 100644 --- a/crates/supermath-web/src/gemini/mod.rs +++ b/crates/supermath-web/src/gemini/mod.rs @@ -139,30 +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::().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.as_u16() == 402 { - return Err(Error::BillingExhausted); - } - 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 @@ -180,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)?; + 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 { + 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::().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 { diff --git a/crates/supermath-web/src/toolkit/mod.rs b/crates/supermath-web/src/toolkit/mod.rs index 47a982c..a2701de 100644 --- a/crates/supermath-web/src/toolkit/mod.rs +++ b/crates/supermath-web/src/toolkit/mod.rs @@ -43,6 +43,16 @@ impl WebToolkit { }) } + /// Sends Gemini the cheapest request that meets billing, to learn + /// whether the key and model answer. `check` uses it. + /// + /// # Errors + /// + /// The errors of [`GeminiClient::probe`](crate::GeminiClient::probe). + pub async fn probe(&self) -> Result<()> { + self.engine.probe().await + } + /// The `web_search` tool. #[must_use] pub fn search(&self) -> WebSearch { From 5a1fc75dd1c1e5426fb18ab6dd15d56f4e870827 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:28:15 +0000 Subject: [PATCH 10/13] fix(check): probe the gemini key instead of only reporting it set The check now sends one ungrounded one-token request through the web toolkit's Gemini client, so a key that is set but out of credit or bound to a retired model fails rather than passing. The toolkit exposes its web component to make that probe reachable, and the engine's probe documentation now describes the ungrounded request it actually makes. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-mcp/src/check/mod.rs | 12 ++++++++---- crates/supermath-mcp/src/toolkit/mod.rs | 8 ++++++++ crates/supermath-web/src/engine/mod.rs | 5 +++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/supermath-mcp/src/check/mod.rs b/crates/supermath-mcp/src/check/mod.rs index 67f117f..37a6729 100644 --- a/crates/supermath-mcp/src/check/mod.rs +++ b/crates/supermath-mcp/src/check/mod.rs @@ -1,14 +1,16 @@ //! `supermath-mcp check`: is each backend the toolkit needs reachable? //! //! [`run`] probes the Lean toolchain and Mathlib, the Python version and -//! lock, `CortexDB`, the Gemini key, and arXiv, and returns a [`Report`]. +//! lock, `CortexDB`, Gemini, and arXiv, and returns a [`Report`]. //! A backend whose variable is unset is [`Status::Skipped`], not failed: the //! toolkit runs without it and its tools answer `BACKEND_UNAVAILABLE`. A //! configured backend that does not answer is [`Status::Failed`], and //! [`Report::passed`] is then `false`. //! -//! The Gemini key is reported as present or absent. Its value is never read -//! into the report, and nothing here prints a secret. +//! A Gemini key is probed with one ungrounded, one-token request, so a key +//! that is set but out of credit (402) or names a retired model (404) fails +//! the check. The key's value is never read into the report, and nothing +//! here prints a secret. use std::fmt; @@ -121,7 +123,9 @@ pub async fn run(toolkit: &Toolkit, config: &Config) -> Report { Line::new("cortexdb", Status::Skipped, "CORTEX_API_KEY is not set") }; let gemini = if toolkit.has_gemini_key() { - Line::new("gemini", Status::Ok, "GEMINI_API_KEY is set") + Line::from_result("gemini", toolkit.web().probe().await, |()| { + format!("{} answered", config.web.model) + }) } else { Line::new( "gemini", diff --git a/crates/supermath-mcp/src/toolkit/mod.rs b/crates/supermath-mcp/src/toolkit/mod.rs index fced03c..e3ab186 100644 --- a/crates/supermath-mcp/src/toolkit/mod.rs +++ b/crates/supermath-mcp/src/toolkit/mod.rs @@ -63,6 +63,7 @@ pub struct Toolkit { python: Python, literature: Literature, memory: Memory, + web: WebToolkit, web_has_key: bool, } @@ -191,6 +192,7 @@ impl Toolkit { python, literature, memory, + web, web_has_key: config.web.has_api_key(), }) } @@ -271,6 +273,12 @@ impl Toolkit { &self.memory } + /// The web toolkit, whose Gemini client `check` probes. + #[must_use] + pub fn web(&self) -> &WebToolkit { + &self.web + } + /// Whether a Gemini key is configured (never the key itself). #[must_use] pub fn has_gemini_key(&self) -> bool { diff --git a/crates/supermath-web/src/engine/mod.rs b/crates/supermath-web/src/engine/mod.rs index 0968d76..0bb995f 100644 --- a/crates/supermath-web/src/engine/mod.rs +++ b/crates/supermath-web/src/engine/mod.rs @@ -115,12 +115,13 @@ impl Engine { } } - /// One grounded Gemini turn: ask, read the grounding, resolve each used - /// source, and keep the sentences that still have a source. + /// One ungrounded one-token Gemini request: does the key answer? pub(crate) async fn probe(&self) -> Result<()> { self.gemini.probe().await } + /// One grounded Gemini turn: ask, read the grounding, resolve each used + /// source, and keep the sentences that still have a source. pub(crate) async fn grounded_turn(&self, prompt: &str) -> Result { let generation = self.gemini.generate(prompt).await?; let usage = generation.response.usage_metadata.unwrap_or_default(); From 402ef813e4661abc2fba415de56883d13b17ad3c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:28:30 +0000 Subject: [PATCH 11/13] test(cli): cover gemini backend in check command test The check command test now starts a stub gemini service and points SUPERMATH_WEB_BASE_URL at it, so the test exercises every backend the command reports on rather than only lean and arxiv. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-mcp/tests/cli.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/supermath-mcp/tests/cli.rs b/crates/supermath-mcp/tests/cli.rs index c0f8a5e..836ed1d 100644 --- a/crates/supermath-mcp/tests/cli.rs +++ b/crates/supermath-mcp/tests/cli.rs @@ -50,11 +50,18 @@ fn a_bad_configuration_fails_on_stderr() { async fn check_reports_each_backend_and_exits_zero_when_none_failed() { let lean = LeanService::start().await; let arxiv = Stub::start(|_| Reply::atom(EMPTY_FEED)).await; + let gemini = Stub::start(|_| { + Reply::json(&serde_json::json!({ + "candidates": [{"content": {"parts": [{"text": "ok"}]}, "finishReason": "MAX_TOKENS"}] + })) + }) + .await; let out = binary() .args(["check", "demo"]) .env("LEAN_URL", &lean.stub.base) .env("SUPERMATH_ARXIV_API_URL", &arxiv.base) .env("GEMINI_API_KEY", "g-secret") + .env("SUPERMATH_WEB_BASE_URL", &gemini.base) .output() .unwrap(); let shown = String::from_utf8_lossy(&out.stdout); From d4838e957242729d1fe6d7be7fa07d5be43516f2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:28:46 +0000 Subject: [PATCH 12/13] docs(readme): clarify gemini check uses one-token probe The `check` example comment now notes that the Gemini key check runs a one-token probe, so readers understand what that step actually does. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d12dadc..903d4e3 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ so they can check a candidate checkout. ```sh cargo install --path crates/supermath-mcp claude mcp add supermath -- supermath-mcp serve my-project -supermath-mcp check # Lean, Python, CortexDB, Gemini key, arXiv +supermath-mcp check # Lean, Python, CortexDB, Gemini (one-token probe), arXiv ``` The project comes from the command line, never from a tool argument. A From dc4a59f7a07676749babb8c9408636a9dc914a1b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Tue, 27 Oct 2026 06:28:58 +0000 Subject: [PATCH 13/13] docs(supermath-web): document 402 budget exceeded handling Add a troubleshooting entry explaining that a 402 from Gemini is reported as BUDGET_EXCEEDED rather than BACKEND_UNAVAILABLE, so callers do not retry it as a transient outage. Also note that `supermath-mcp check` probes the key with a one-token request, so an unfunded key fails the check. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/supermath-web/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/supermath-web/README.md b/crates/supermath-web/README.md index b568316..d6051f0 100644 --- a/crates/supermath-web/README.md +++ b/crates/supermath-web/README.md @@ -36,6 +36,11 @@ the three, `register(&mut registry)` adds them to a `ToolRegistry`. - **Retired models.** Google retires model names. A 404 from Gemini is reported as `BACKEND_UNAVAILABLE` with a message naming the model and saying it may be retired; set `SUPERMATH_WEB_MODEL` to a current pinned name. +- **Out of credit.** A 402 from Gemini means the key's billing or credit is + exhausted. It is reported as `BUDGET_EXCEEDED`, not `BACKEND_UNAVAILABLE`, + so a caller does not mistake it for a transient outage and retry. + `supermath-mcp check` probes the key with one ungrounded, one-token request + (`WebToolkit::probe`), so a set but unfunded key fails the check. - **Cache and cap.** Responses are cached 24 h under `$SUPERMATH_CACHE/web`, keyed by model and prompt. `SUPERMATH_WEB_DAILY_CAP` calls per project per UTC day, counted in memory.