Commit Diff


commit - 2b7e8021abe55fdf0bb84b568215ea14f6185f98
commit + bb6838b9c104b4902178e2dc8a89be358400c386
blob - 1855a11ac15917996b9fcb9e25fe772d44430c1b
blob + 10c9a0070028473452cdc4b04d9337df264db448
--- README.md
+++ README.md
@@ -293,9 +293,11 @@ is funds ready to spend, and a positive one is money o
 
 The Ollama provider reports Ollama Cloud usage. It takes the key from
 `$OLLAMA_API_KEY` and defaults to `https://ollama.com`. You can override this
-with `$OLLAMA_API_ENDPOINT`. alpaca prints the 5 hour and weekly usage windows
-with locally computed reset boundaries, a table of per-model request counts
-for each window, and any extra spend the account has accrued.
+with `$OLLAMA_API_ENDPOINT`. It shows the monthly usage pool as the share of
+the plan's credits used, with per-model request counts and any extra spend
+beyond the plan. The endpoint reports no reset time, so no reset is shown.
+Accounts on the old billing get the 5 hour and weekly windows instead, with
+locally computed reset boundaries.
 
 The Codex provider reports your ChatGPT plan usage, not organization spend.
 alpaca reads the OAuth token the Codex CLI stores in `$CODEX_HOME/auth.json`,
blob - bf801bce530c8d8bd207c669acc80f6755d06c39
blob + 3054cefdd7e70d720bfe12a8f5da5d42dfc7672f
--- man/alpaca-quota.1.scd
+++ man/alpaca-quota.1.scd
@@ -67,9 +67,11 @@ all providers.
 *ollama*
 	Ollama Cloud. It takes the key from *OLLAMA_API_KEY*. The base URL is
 	*https://ollama.com*, or *$OLLAMA_API_ENDPOINT* when set. It prints the
-	5 hour and weekly usage windows with locally computed reset boundaries,
-	a table of per-model request counts for each window, and any extra spend
-	the account has accrued.
+	monthly usage pool as the share of the plan's credits used, a table of
+	per-model request counts, and any extra spend beyond the plan. It shows
+	no reset, because the endpoint reports no reset time. Accounts on the
+	old billing get the 5 hour and weekly windows instead, with locally
+	computed reset boundaries.
 
 The *anthropic* endpoint is not a documented public API, so it may change
 without notice. It expects the short-lived OAuth token the Claude Code CLI
blob - 3d408a6d306c759b7b2d2e5deb9df9661db0ce78
blob + e133bb147b6c894ab6b24ff1746faee228cd1130
--- src/usage/ollama.rs
+++ src/usage/ollama.rs
@@ -15,9 +15,11 @@ const WEEKLY_OFFSET: TimeDelta = TimeDelta::days(4);
 
 #[derive(Debug, Default, Deserialize)]
 struct Limit {
+    /// Used share of the window. The new billing only reports the monthly
+    /// pool, so a window is drawn for the limits the response actually
+    /// carries, and none are defaulted.
+    usage: Option<f64>,
     #[serde(default)]
-    usage: f64,
-    #[serde(default)]
     models: Vec<ModelCount>,
 }
 
@@ -37,6 +39,8 @@ struct Activity {
     cost: String,
     #[serde(default)]
     period: Period,
+    #[serde(default)]
+    models: Vec<ModelCount>,
 }
 
 #[derive(Debug, Default, Deserialize)]
@@ -45,6 +49,8 @@ struct Limits {
     session: Limit,
     #[serde(default)]
     weekly: Limit,
+    #[serde(default)]
+    monthly: Limit,
 }
 
 #[derive(Debug, Default, Deserialize)]
@@ -57,10 +63,15 @@ struct Usage {
 
 /// Normalise the Ollama Cloud usage response
 ///
-/// Ollama reports no reset time, so the windows are computed locally. Session
-/// resets align to UTC multiples of 5 hours from the epoch. Weekly resets are
-/// offset by 4 days so all accounts share the same boundary. This matches the
-/// formula in ollama/ollama issue #12532.
+/// Since the transparent pricing of August 2026, the limits carry one monthly
+/// pool, consumed per token against the credits a plan includes. It reports
+/// no reset time: the pool renews on the day the plan started, which the
+/// endpoint does not expose. Accounts still on the old billing instead get
+/// session and weekly windows, whose resets are computed locally because
+/// Ollama reports no reset time there either: session resets align to UTC
+/// multiples of 5 hours from the epoch, and weekly resets are offset by 4
+/// days so all accounts share the same boundary. This matches the formula in
+/// ollama/ollama issue #12532.
 ///
 /// # Errors
 ///
@@ -83,7 +94,54 @@ pub fn view(body: &[u8]) -> Result<View, Error> {
 
 fn build(usage: &Usage, now: DateTime<Utc>) -> View {
     let period = &usage.activity.period;
+    let limits = &usage.limits;
 
+    let mut windows = Vec::new();
+    let mut tables = Vec::new();
+
+    // Only the windows the response carries are drawn. Under the new billing
+    // the monthly pool has no reported reset: it renews on the day the plan
+    // started, which the endpoint does not expose.
+    if let Some(used) = limits.session.usage {
+        windows.push(Window::new(
+            "session",
+            used,
+            Some(next_boundary(now, SESSION, TimeDelta::zero())),
+        ));
+        tables.push(Table {
+            heading: "session models".to_string(),
+            unit: "reqs",
+            rows: sorted(&limits.session.models),
+        });
+    }
+    if let Some(used) = limits.weekly.usage {
+        windows.push(Window::new(
+            "weekly",
+            used,
+            Some(next_boundary(now, WEEKLY, WEEKLY_OFFSET)),
+        ));
+        tables.push(Table {
+            heading: "weekly models".to_string(),
+            unit: "reqs",
+            rows: sorted(&limits.weekly.models),
+        });
+    }
+    if let Some(used) = limits.monthly.usage {
+        windows.push(Window::new("monthly", used, None));
+        tables.push(Table {
+            heading: "monthly models".to_string(),
+            unit: "reqs",
+            rows: sorted(&limits.monthly.models),
+        });
+    }
+    if !usage.activity.models.is_empty() {
+        tables.push(Table {
+            heading: "activity models".to_string(),
+            unit: "reqs",
+            rows: sorted(&usage.activity.models),
+        });
+    }
+
     View {
         title: "ollama cloud usage".to_string(),
         subtitle: Some(format!(
@@ -92,30 +150,8 @@ fn build(usage: &Usage, now: DateTime<Utc>) -> View {
         )),
         note: (!usage.activity.cost.is_empty())
             .then(|| format!("extra {} USD", usage.activity.cost)),
-        windows: vec![
-            Window::new(
-                "session",
-                usage.limits.session.usage,
-                Some(next_boundary(now, SESSION, TimeDelta::zero())),
-            ),
-            Window::new(
-                "weekly",
-                usage.limits.weekly.usage,
-                Some(next_boundary(now, WEEKLY, WEEKLY_OFFSET)),
-            ),
-        ],
-        tables: vec![
-            Table {
-                heading: "session models".to_string(),
-                unit: "reqs",
-                rows: sorted(&usage.limits.session.models),
-            },
-            Table {
-                heading: "weekly models".to_string(),
-                unit: "reqs",
-                rows: sorted(&usage.limits.weekly.models),
-            },
-        ],
+        windows,
+        tables,
     }
 }
 
@@ -195,6 +231,90 @@ mod test {
     }
 
     #[test]
+    fn reads_the_monthly_pool_of_the_new_billing() -> Result<()> {
+        let view = view(
+            br#"{
+            "activity": {
+                "cost": "0.00000",
+                "period": {
+                    "type": "last_4_weeks",
+                    "starting_at": "2026-08-17T00:00:00Z",
+                    "ending_at": "2026-09-07T18:18:40.065660066Z"
+                },
+                "models": []
+            },
+            "limits": {
+                "monthly": {
+                    "usage": 0.876,
+                    "models": [
+                        {"name": "kimi-k3", "request_count": 329},
+                        {"name": "web search", "request_count": 4}
+                    ]
+                }
+            }
+        }"#,
+        )?;
+
+        assert_eq!(view.windows.len(), 1);
+        assert_eq!(view.windows[0].name, "monthly");
+        assert!((view.windows[0].used - 0.876).abs() < f64::EPSILON);
+        assert_eq!(
+            view.windows[0].resets_in_secs, None,
+            "the monthly pool renews on the plan start day, which is not reported"
+        );
+        assert_eq!(view.tables.len(), 1);
+        assert_eq!(view.tables[0].heading, "monthly models");
+        assert_eq!(
+            view.tables[0].rows,
+            vec![
+                ModelCount {
+                    name: "kimi-k3".to_string(),
+                    requests: 329
+                },
+                ModelCount {
+                    name: "web search".to_string(),
+                    requests: 4
+                }
+            ],
+            "models are sorted busiest first"
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn omits_windows_the_response_does_not_report() -> Result<()> {
+        let view = view(
+            br#"{
+                "activity": {"cost": "0.00000", "period": {"type": "last_4_weeks"}},
+                "limits": {"session": {"models": []}}
+            }"#,
+        )?;
+
+        assert!(view.windows.is_empty(), "no bar without a usage share");
+        assert!(view.tables.is_empty(), "no table without a window");
+        Ok(())
+    }
+
+    #[test]
+    fn reads_activity_models_when_the_window_has_none() -> Result<()> {
+        let view = view(
+            br#"{
+            "activity": {
+                "cost": "0.00000",
+                "period": {"type": "last_4_weeks"},
+                "models": [{"name": "kimi-k3", "request_count": 329}]
+            },
+            "limits": {"monthly": {"usage": 0.876, "models": []}}
+        }"#,
+        )?;
+
+        assert_eq!(view.tables[0].heading, "monthly models");
+        assert_eq!(view.tables[1].heading, "activity models");
+        assert_eq!(view.tables[1].rows[0].name, "kimi-k3");
+        Ok(())
+    }
+
+    #[test]
     fn rejects_non_usage_bodies() {
         assert!(view(b"not json").is_err());
         assert!(view(b"{}").is_err());
blob - 1a4fb19f07f3acdda32348b6b532af40fef88430
blob + 409a19f4c2734c1d40d110e90edd994f97125775
--- tests/quota.rs
+++ tests/quota.rs
@@ -634,6 +634,42 @@ fn ollama_quota_reports_session_and_weekly_windows() {
 }
 
 #[test]
+fn ollama_quota_reports_the_monthly_pool_of_the_new_billing() {
+    let mut server = mockito::Server::new();
+    let mock = server
+        .mock("GET", "/api/usage")
+        .match_header("authorization", "KEY")
+        .with_body(
+            r#"{"activity":{"cost":"0.00000","period":{"type":"last_4_weeks","starting_at":"2026-08-17T00:00:00Z","ending_at":"2026-09-07T18:18:40.065660066Z"}},
+                "limits":{"monthly":{"usage":0.876,"models":[{"name":"kimi-k3","request_count":329}]}}}"#,
+        )
+        .create();
+
+    offline(alpaca().args([
+        "quota",
+        "-p",
+        "ollama",
+        "--apikey",
+        "KEY",
+        "--base-url",
+        &server.url(),
+    ]))
+    .assert()
+    .success()
+    .stdout(
+        predicate::str::contains("ollama cloud usage")
+            .and(predicate::str::contains("monthly"))
+            .and(predicate::str::contains("87.6%"))
+            .and(predicate::str::contains("monthly models"))
+            .and(predicate::str::contains("kimi-k3"))
+            .and(predicate::str::contains("extra 0.00000 USD"))
+            .and(predicate::str::contains("resets in").not()),
+    );
+
+    mock.assert();
+}
+
+#[test]
 fn ollama_quota_without_credentials_fails() {
     offline(alpaca().args(["quota", "-p", "ollama"]))
         .assert()