fix: detect low balance and prompt for top up#7166
Conversation
When the Tetrate provider detects that a user's credits have been
exhausted, this change:
1. Adds a new ProviderError::CreditsExhausted variant with details
and an optional top_up_url field
2. Detects credit exhaustion in the Tetrate provider via:
- HTTP 402 (Payment Required) status codes
- Error code 402 in response body
- Credit-related keywords in error messages (covers cases where
credit exhaustion arrives as 429 or in 200 OK error bodies)
3. Opens the user's default browser to the Tetrate dashboard
(https://router.tetrate.ai/dashboard) for easy credit top-up
4. Falls back to printing the URL if the browser can't be opened
5. Shows a clear message explaining what happened and how to continue
6. Does NOT retry on CreditsExhausted (it's not transient)
7. Adds generic HTTP 402 handling in openai_compatible for other providers
Uses the existing webbrowser crate (already a dependency for OAuth).
…assumption
Follow-up improvements to the credits-exhausted browser-open feature:
1. Unit tests (35 total):
- 29 tests in tetrate.rs covering is_credits_exhausted() positive
cases (all 16 phrases), case-insensitivity, embedded matches,
negative cases (rate limit, server error, auth, context length,
empty, generic, model-not-found), error variant fields, display
format, telemetry type, and retry exclusion.
- 6 tests in openai_compatible.rs covering map_http_error_to_provider_error
for HTTP 402 (with/without payload), 429, 401, 400+context, 500.
2. Verified HTTP 402 assumption:
- No documentation found confirming Tetrate's exact error format for
credit exhaustion. Added detailed code comment explaining we
defensively handle HTTP 402, error code 402 in response bodies,
AND keyword-based detection as a safety net.
3. Generalized the design:
- Removed hardcoded Tetrate dashboard URL fallback from agent.rs.
- When top_up_url is None (generic 402 from unknown provider), shows
a provider-agnostic message without attempting to open a browser.
- When top_up_url is Some (Tetrate or any future provider), opens
the browser as before.
4. Browser opening verification:
- Confirmed webbrowser crate v1.0 (same as Tetrate OAuth flow in
signup_tetrate). Works on macOS, Linux, Windows.
- Added code comment in agent.rs noting the cross-platform mechanism.
The agent is a core library and should not do UI actions like opening
browsers. Different consumers (CLI, desktop app, API) need to handle
credits-exhausted differently.
Changes:
- Add SystemNotificationType::CreditsExhausted variant to message.rs
with documented data schema ({top_up_url: string | null})
- agent.rs now yields a structured CreditsExhausted notification with
the user-facing message and top_up_url in the data field — no
browser opening, no webbrowser dependency
- goose-cli output.rs handles the new notification type: renders the
message in yellow and calls webbrowser::open() if a top_up_url is
present in the notification data
- goose-server passes the notification through as a serialized SSE
event (no changes needed) — desktop app can handle it however it
wants (dialog, button, etc.)
This follows the same pattern as ThinkingMessage and InlineMessage
notifications: the agent produces structured events, the UI layer
decides how to present them.
Add desktop app support for the CreditsExhausted system notification
that flows from the agent through goose-server SSE events.
Changes:
- openapi.json: Add 'creditsExhausted' to SystemNotificationType enum
- types.gen.ts: Add 'creditsExhausted' to the union type
- CreditsExhaustedNotification.tsx: New component that renders a
yellow warning banner with the error message and a 'Top Up Credits'
button that opens the provider's dashboard URL via
window.electron.openExternal()
- ProgressiveMessageList.tsx: Detect creditsExhausted notifications
and render CreditsExhaustedNotification before other system
notification checks
The notification data schema is {top_up_url: string | null}. When
top_up_url is present the button is shown; when null only the message
is displayed. goose-server required no changes — it already serializes
the full Message (including SystemNotificationContent with data) as
SSE JSON events.
DOsinga
left a comment
There was a problem hiding this comment.
This looks like a very good addition, but I think we should remove all the clutter from tetrate and rely on the http code which they do provide.
| // If the provider supplied a top-up URL, try to open | ||
| // the user's browser so they can add credits. Uses the | ||
| // `webbrowser` crate (same cross-platform mechanism as | ||
| // the Tetrate OAuth sign-up flow). |
There was a problem hiding this comment.
I could do without that comment (but also realizing I'm losing that battle0
There was a problem hiding this comment.
you know - I didn't look at a single bit of this PR, not even once (at least other than having AI.staged describe it too me - as wasn't sure if people cared!) but now they do, will tidy up
crates/goose/src/agents/agent.rs
Outdated
| "Your credits have been exhausted: {details}\n\n\ | ||
| Please check your account with your provider to add more \ | ||
| credits, then retry your last message to continue." | ||
| ) |
There was a problem hiding this comment.
could remove the one line duplication here
| InlineMessage, | ||
| /// Provider credits have been exhausted. The `data` field of the | ||
| /// notification may contain `{"top_up_url": "..."}` so the UI layer | ||
| /// can open the user's browser or show a clickable link. |
There was a problem hiding this comment.
this comment should go - or we need to document the others too
| use serde_json::json; | ||
|
|
||
| #[test] | ||
| fn http_402_maps_to_credits_exhausted() { |
There was a problem hiding this comment.
I don't see these tests fail unless we change functionality, but if you want to keep them, can we change them into the test_case pattern?
| if (hasCreditsExhaustedNotification(message)) { | ||
| return ( | ||
| <div | ||
| key={message.id ?? `msg-${index}-${message.created}`} |
There was a problem hiding this comment.
this (we already have this) leads to duplicate keys since we also render the main message with this id (and also message.id should really be set)
| }) => { | ||
| const notification = message.content.find( | ||
| (content): content is SystemNotificationContent & { type: 'systemNotification' } => | ||
| content.type === 'systemNotification' && content.notificationType === 'creditsExhausted' |
There was a problem hiding this comment.
this is probably also based on the previous way this works, but we really should refactor this to just go through the message content and render them based on their type
| "gpt-4.1", | ||
| ]; | ||
| pub const TETRATE_DOC_URL: &str = "https://router.tetrate.ai"; | ||
| pub const TETRATE_DASHBOARD_URL: &str = "https://router.tetrate.ai/dashboard"; |
There was a problem hiding this comment.
this reads like we changed our mind half way? like, the 402 that Tetrate returns should really be enough and we handle that using the openai compatible error handler, so all we really should need from tetrate is the top up url
…slop, use test_case - Remove comment slop from output.rs, agent.rs, message.rs, tetrate.rs - Deduplicate error logging in agent.rs CreditsExhausted arm - Remove doc comment from CreditsExhausted variant (consistency with others) - Convert openai_compatible.rs tests to test_case pattern - Simplify tetrate.rs: rely on openai_compatible 402 handling, just enrich with dashboard URL. Remove is_credits_exhausted and all its tests. - Fix duplicate React keys in ProgressiveMessageList by using index-based keys - Refactor notification rendering: components take notification content directly, unified getSystemNotification + renderSystemNotification dispatches by type
There was a problem hiding this comment.
Pull request overview
Adds end-to-end handling for “credits exhausted” across providers and frontends, so users are notified and can navigate to a top-up page (notably for the Tetrate provider).
Changes:
- Introduces a new
creditsExhaustedsystem notification type in the OpenAPI schema/typegen and Rust message models. - Maps HTTP
402 Payment Requiredto a newProviderError::CreditsExhausted, enriches it with a Tetrate dashboard URL, and emits a user-facing system notification from the agent. - Adds UI/CLI rendering for the new notification, including opening the top-up URL.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/components/context_management/SystemNotificationInline.tsx | Refactors inline notification rendering to accept a notification object + adds helper selector. |
| ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx | New UI component + selector for rendering “credits exhausted” with an optional top-up button. |
| ui/desktop/src/components/ProgressiveMessageList.tsx | Routes system notifications through type-based rendering (inline vs credits exhausted). |
| ui/desktop/src/api/types.gen.ts | Extends generated SystemNotificationType union with creditsExhausted. |
| ui/desktop/openapi.json | Extends OpenAPI enum for SystemNotificationType with creditsExhausted. |
| crates/goose/src/providers/tetrate.rs | Enriches credits-exhausted errors with a dashboard URL and wires enrichment into request paths. |
| crates/goose/src/providers/openai_compatible.rs | Maps HTTP 402 to ProviderError::CreditsExhausted + adds tests. |
| crates/goose/src/providers/errors.rs | Adds ProviderError::CreditsExhausted variant + telemetry mapping. |
| crates/goose/src/conversation/message.rs | Adds SystemNotificationType::CreditsExhausted for frontend notifications. |
| crates/goose/src/agents/agent.rs | Converts ProviderError::CreditsExhausted into a system notification with top_up_url data. |
| crates/goose-cli/src/session/output.rs | Renders credits-exhausted notification and attempts to open the top-up URL in a browser. |
ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx
Outdated
Show resolved
Hide resolved
ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx
Show resolved
Hide resolved
|
pushed some changes to simplify things |
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
2659d26 to
51ae6dd
Compare
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
85d1ae2 to
87f4c5a
Compare
* 'main' of github.com:block/goose: (40 commits) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) docs: remove ALPHA_FEATURES flag from documentation (#7315) docs: escape variable syntax in recipes (#7314) docs: update OTel environment variable and config guides (#7221) docs: system proxy settings (#7311) docs: add Summon extension tutorial and update Skills references (#7310) docs: agent session id (#7289) ...
* origin/main: fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316)
* origin/main: (29 commits) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) docs: remove ALPHA_FEATURES flag from documentation (#7315) docs: escape variable syntax in recipes (#7314) ... # Conflicts: # ui/desktop/src/components/McpApps/McpAppRenderer.tsx # ui/desktop/src/components/McpApps/types.ts
* 'main' of github.com:block/goose: (24 commits) Docs: claude code uses stream-json (#7358) Improve link confirmation modal (#7333) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) chore: upgrade to rmcp 0.16.0 (#7274) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) ...
* main: (46 commits) chore(deps): bump hono from 4.11.9 to 4.12.0 in /ui/desktop (#7369) Include 3rd-party license copy for JavaScript/CSS minified files (#7352) docs for reasoning env var (#7367) docs: update skills detail page to reference Goose Summon extension (#7350) fix(apps): restore MCP app sampling support reverted by #6933 (#7366) feat: TUI client of goose-acp (#7362) docs: agent variable (#7365) docs: pass env vars to shell (#7361) docs: update sandbox topic (#7336) feat: add local inference provider with llama.cpp backend and HuggingFace model management (#6933) Docs: claude code uses stream-json (#7358) Improve link confirmation modal (#7333) fix(ci): deflake smoke tests for Google models (#7344) feat: add Cerebras provider support (#7339) fix: skip whitespace-only text blocks in Anthropic message (#7343) fix(goose-acp): heap allocations (#7322) Remove trailing space from links (#7156) fix: detect low balance and prompt for top up (#7166) feat(apps): add support for MCP apps to sample (#7039) Typescript SDK for ACP extension methods (#7319) ...
attempt to detect if there is no credit and open a browser to tetrate so user can top it up
Goose app:
Goose.out.of.credits.mov
CLI:
CLI.out.of.credits.mov