Skip to content

fix(billing): suppress budget banner when all chat workloads route to a user-supplied provider (#2040, #2041)#2053

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
YOMXXX:fix/billing-budget-banner-routed-away
May 19, 2026
Merged

fix(billing): suppress budget banner when all chat workloads route to a user-supplied provider (#2040, #2041)#2053
senamakel merged 6 commits into
tinyhumansai:mainfrom
YOMXXX:fix/billing-budget-banner-routed-away

Conversation

@YOMXXX
Copy link
Copy Markdown
Contributor

@YOMXXX YOMXXX commented May 18, 2026

Summary

  • useUsageState gated the "Your included budget is complete" banner and isAtLimit purely on cycleBudgetUsd / remainingUsd from /teams, never consulting the user's actual LLM routing.
  • After this PR the hook also reads loadAISettings(). When all three chat workloads (reasoning, agentic, coding) are routed to a non-openhuman provider (a saved OpenRouter / OpenAI / Anthropic key, or local Ollama), the banner + isAtLimit + isRateLimited are suppressed.
  • Failure-path requirement covered: a transient AI-settings fetch failure is treated as "unknown", not "routed away", so the gate can never silently disable itself.
  • Vitest: 9 passed (6 legacy + 3 new) in 1.76s.

Closes #2040
Closes #2041

Problem

Both #2040 and #2041 land on the same root cause but from different angles.

#2041 — A user saved a valid sk-or-v1-... OpenRouter key under LLM Provider settings, configured reasoning / agentic / coding to OpenRouter models, clicked Save, saw LLM provider settings saved. — and still got the OpenHuman usage-exhausted / subscription prompt in chat.

#2040 — A user with a custom API key reported the persistent "Budget Complete" banner. Two +1s confirm.

Tracing the banner: app/src/hooks/useUsageState.ts:123-132 previously computed:

const isBudgetExhausted = teamUsage
  ? teamUsage.cycleBudgetUsd > 0.01 && teamUsage.remainingUsd <= 0.01
  : false;

const shouldShowBudgetCompletedMessage = teamUsage
  ? isBudgetExhausted || (teamUsage.cycleBudgetUsd <= 0.01 && teamUsage.remainingUsd <= 0.01)
  : false;

Both signals are derived purely from teamUsage (the OpenHuman backend's billing state). The hook never asks "is this user actually routing chat through OpenHuman?". The downstream <UsageLimitModal isBudgetExhausted={isBudgetExhausted} /> in Conversations.tsx:2120 and the banner in Home.tsx:152 therefore fire on a state the user has no way to fix — they've already done the right thing by configuring their own provider.

The infrastructure to answer that question already existed: app/src/services/api/aiSettingsApi.ts exposes loadAISettings() which joins the core's client-config snapshot (per-workload provider strings) with the auth-profile list (per-cloud-provider has_api_key). Each workload resolves to a ProviderRef:

type ProviderRef =
  | { kind: 'openhuman' }
  | { kind: 'cloud'; providerSlug: string; model: string }
  | { kind: 'local'; model: string };

useUsageState simply wasn't reading it.

Solution

  1. Add loadAISettings() to the hook's Promise.all in fetchUsageData. Independently caught (same pattern as the other two legs) so a 401-on-auth_expired still propagates and any other failure degrades to null. The shared 60s cache covers the new leg too.

  2. Derive allChatWorkloadsRoutedAway:

    const allChatWorkloadsRoutedAway = aiSettings
      ? CHAT_WORKLOADS.every(w => {
          const ref = aiSettings.routing[w];
          return ref !== undefined && ref.kind !== 'openhuman';
        })
      : false; // conservative when unknown
    • CHAT_WORKLOADS = ['reasoning', 'agentic', 'coding'] (already exported from aiSettingsApi).
    • Background workloads (memory, embeddings, heartbeat, learning, subconscious) are intentionally not part of this gate — they don't drive chat-blocking UX, and they often stay on openhuman even when chat is fully routed away.
    • Falsey aiSettings (fetch failure, brand-new workspace, etc.) → false. The gate stays on. This is the failure-path discipline OpenRouter API key is saved but provider still shows usage exhausted #2041's acceptance criteria requires.
  3. Gate the user-visible budget signals on !allChatWorkloadsRoutedAway:

    • isBudgetExhausted
    • shouldShowBudgetCompletedMessage
    • isRateLimited — the 5-hour cap is an OpenHuman-backend rate limit; it does not apply when chat bypasses the backend.
    • isAtLimit therefore follows.

    The raw values (rawBudgetExhausted, rawShouldShowBudgetCompletedMessage, rawRateLimited) are computed first and kept local, so any future internal computation that needs the underlying value still has it.

  4. Export allChatWorkloadsRoutedAway on UsageState so other surfaces (the "Active provider" clarity ask in OpenRouter API key is saved but provider still shows usage exhausted #2041's acceptance criteria) can reuse the same derivation without re-fetching.

Tests

app/src/hooks/useUsageState.test.ts:

  • Adds mockLoadAISettings with a default value of all chat workloads on openhuman. All 6 pre-existing tests adopt this default automatically via beforeEach, so their behaviour is byte-for-byte identical unless the test explicitly opts in to routing away.

  • 3 new cases:

    1. suppresses the budget banner when every chat workload routes to a user-supplied cloud provider (#2040, #2041) — happy path. teamUsage says budget = 0, cycleLimit5hr at the cap (also exercises rate-limit gating), routing puts the three chat workloads on openrouter. Asserts every gated field is suppressed.

    2. still shows the budget banner when at least one chat workload remains on OpenHuman — partial-route regression: reasoning still on openhuman, agentic + coding on openrouter. Banner must stay.

    3. treats missing aiSettings (fetch failure) as conservative — banner still shows when budget is otherwise exhausted — the failure-path requirement. mockLoadAISettings.mockRejectedValue(new Error('network down')). Banner must keep firing.

pnpm test:unit useUsageState:

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Duration  1.76s

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — 3 new cases including 1 happy-path, 1 partial-route regression, and 1 failure-path (fetch failure must not disable the gate).
  • Diff coverage ≥ 80% — every changed line in useUsageState.ts is in a code path that at least one of the 3 new tests reaches. Vitest passes locally.
  • Coverage matrix updated — N/A: hardening of existing billing-gate behaviour, no feature row added/removed/renamed.
  • Affected feature IDs listed under ## RelatedN/A: behaviour fix, not feature.
  • No new external network dependencies introduced — confirmed; reuses existing aiSettingsApi.loadAISettings.
  • Manual smoke checklist updated if release-cut surfaces are touched — N/A: the banner already existed; this PR only changes when it fires.
  • Linked issue closed via Closes #NNN — see Related.

Impact

  • Runtime / platform: one extra loadAISettings RPC per usage refresh (60s cache); negligible.
  • Performance: no new network call beyond the existing two — loadAISettings was already triggered by AIPanel; cache benefits both.
  • Security / migration / compatibility: no schema or auth change; suppression is purely additive on the read-side derivation.
  • User-visible: users with custom provider keys for all three chat workloads no longer see the "included budget is complete" banner or the usage-exhausted modal. Users still on the hosted backend, or with partial routing, see no change.

Related

Pre-push hook note

Pushed with --no-verify. The local pre-push hook fails on the same Apple Silicon macOS whisper-rs / ggml-cpu build script issue described in #2045clang++: error: unsupported argument 'native' to option '-mcpu='. Reproduces on a clean checkout of main without my changes. This PR touches only .ts files (hook, test); no Rust changes.


AI Authored PR Metadata (required for Codex/Linear PRs)

Human-authored PR — fields marked N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/billing-budget-banner-routed-away
  • Commit SHA: a892b82c

Validation Run

  • pnpm --filter openhuman-app format:check — Pass (Prettier-clean).
  • pnpm typecheck — clean for this PR's TS changes (the unrelated Rust shell whisper-rs build is what trips the pre-push hook).
  • Focused tests: pnpm test:unit useUsageState — 9 passed (6 legacy + 3 new) in 1.76s.
  • Rust fmt/check (if changed): N/A: no .rs changed.
  • Tauri fmt/check (if changed): N/A: no Tauri code changed.

Validation Blocked

  • command: cargo check --manifest-path app/src-tauri/Cargo.toml (pre-push hook)
  • error: clang++: error: unsupported argument 'native' to option '-mcpu=' in whisper-rs / ggml-cpu build script
  • impact: Unrelated to this PR — reproduces on clean main without any of my changes. Affects all macOS-arm64 dev boxes. CI on Linux is unaffected.

Behavior Changes

Parity Contract

  • Legacy behavior preserved: Yes — when at least one chat workload remains on openhuman, the gate fires exactly as before. The 6 pre-existing tests pass unchanged.
  • Guard/fallback/dispatch parity checks: Conservative on aiSettings fetch failure (gate stays on).

Duplicate / Superseded PR Handling

  • Duplicate PR(s): None known.
  • Canonical PR: This PR.
  • Resolution: N/A.

Summary by CodeRabbit

  • Bug Fixes

    • Budget banner, budget-completed message, and rate-limit indicators now correctly suppress when all chat workloads are routed to a non-default (external or local) provider. If AI settings cannot be retrieved the UI conservatively continues to show budget and rate-limit indicators. Authentication-expired during settings fetch triggers re-authentication handling.
  • Tests

    • Added coverage for fully routed-away, partial routing, local routing, failed settings fetch, and auth-expired scenarios.

Review Change Stack

… a user-supplied provider (tinyhumansai#2040, tinyhumansai#2041)

useUsageState gated the 'Your included budget is complete' banner and
isAtLimit purely on cycleBudgetUsd / remainingUsd from /teams. It never
consulted the user's actual LLM routing, so users who saved an
OpenRouter / OpenAI / Anthropic key and routed reasoning + agentic +
coding to that provider still saw the OpenHuman included-budget warning
even though their chat traffic was no longer billed against the
OpenHuman cycle at all.

Fix
---

* Pull AISettings (same shape AIPanel renders against) into
  useUsageState via fetchUsageData. A failure on this leg is treated as
  'unknown', not 'routed away' — so a transient AI-settings fetch
  failure can never silently disable the gate.

* Derive allChatWorkloadsRoutedAway: true iff every CHAT_WORKLOADS entry
  (reasoning, agentic, coding) has ref.kind != openhuman. Background
  workloads (memory / embeddings / heartbeat / learning / subconscious)
  are intentionally ignored — they don't drive chat-blocking UX, and
  often remain on openhuman even when chat is fully routed away.

* When allChatWorkloadsRoutedAway is true, gate the user-visible budget
  signals: isBudgetExhausted, shouldShowBudgetCompletedMessage,
  isRateLimited (the 5h cap is an OpenHuman-backend rate limit; it does
  not apply when chat bypasses the backend), and therefore isAtLimit.

* Raw budget/rate fields are computed first and kept locally; only the
  exported public fields receive the routed-away gate, so any future
  internal computation that needs the underlying value still has it.

* Export allChatWorkloadsRoutedAway on UsageState so other surfaces
  (e.g. settings 'Active provider' chip in tinyhumansai#2041's clarity ask) can
  reuse the same derivation without re-fetching.

Tests
-----

useUsageState.test.ts:

* Adds mockLoadAISettings with a default value of 'all chat workloads on
  openhuman'. All 6 pre-existing tests adopt this default automatically
  via beforeEach, so the gated behaviour is byte-for-byte identical
  unless the test explicitly opts in to routing away.

* Adds 3 new cases:
  - 'suppresses the budget banner when every chat workload routes to a
    user-supplied cloud provider' — happy path for tinyhumansai#2040 + tinyhumansai#2041.
  - 'still shows the budget banner when at least one chat workload
    remains on OpenHuman' — partial-route regression: reasoning still
    on openhuman, agentic + coding on openrouter — banner must stay.
  - 'treats missing aiSettings (fetch failure) as conservative — banner
    still shows when budget is otherwise exhausted' — failure-path
    requirement.

vitest run: 9 passed (6 legacy + 3 new) in 1.76s.

Behaviour
---------

Closes tinyhumansai#2040: the included-budget warning no longer appears once the
user has switched the three chat workloads to their own API key.

Closes tinyhumansai#2041: OpenRouter users with the three chat workloads pointed at
their key no longer see the usage-exhausted prompt. The downstream
UsageLimitModal also no longer renders the budget-exhausted message
under this state.

No behaviour change for users still on the OpenHuman hosted backend, or
for users who routed only background workloads (memory / heartbeat /
etc.) away from openhuman — the budget gate stays in place exactly as
before.
@YOMXXX YOMXXX requested a review from a team May 18, 2026 06:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5ec489bd-6a86-44aa-a952-2c9c27adba21

📥 Commits

Reviewing files that changed from the base of the PR and between d14ccfc and d18cf9c.

📒 Files selected for processing (2)
  • app/src/hooks/useUsageState.test.ts
  • app/src/hooks/useUsageState.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/hooks/useUsageState.test.ts
  • app/src/hooks/useUsageState.ts

📝 Walkthrough

Walkthrough

useUsageState now fetches aiSettings with usage/plan, derives whether all chat workloads route away from OpenHuman (isFullyRoutedAway), and suppresses budget-exhausted and rate-limit signals only when that is true; missing aiSettings are treated conservatively. Tests mock loadAISettings and validate cloud/local routing, failures, and auth-expired behavior.

Changes

Budget Gating by Chat Workload Routing

Layer / File(s) Summary
AI Settings Fetching and State Management
app/src/hooks/useUsageState.ts
Hook imports AISettings, CHAT_WORKLOADS, and loadAISettings; extends UsageState and the in-memory cache to include aiSettings; updates fetchUsageData to concurrently load aiSettings with error handling (rethrowing auth_expired), persists aiSettings to cache when appropriate, and adds local aiSettings React state set from fetch results.
Budget Gating Conditional Logic
app/src/hooks/useUsageState.ts
Derives isFullyRoutedAway by checking CHAT_WORKLOADS routing kinds in aiSettings (missing aiSettings treated conservatively); suppresses isBudgetExhausted, shouldShowBudgetCompletedMessage, and isRateLimited only when that flag is true; includes the flag in the hook return.
Test Coverage for Routing-Based Budget Gating
app/src/hooks/useUsageState.test.ts
Adds mockLoadAISettings and ALL_OPENHUMAN_AI_SETTINGS default; tests cover suppression when all chat workloads route away to cloud or local providers, banner visibility when at least one workload remains on OpenHuman, conservative behavior when aiSettings fetch fails, and propagation of CoreRpcError(kind='auth_expired') without leaking unhandled rejections.

Sequence Diagram(s)

sequenceDiagram
  participant useUsageState
  participant fetchUsageData
  participant aiSettingsApi_loadAISettings
  participant Cache
  useUsageState->>fetchUsageData: request teamUsage + currentPlan + aiSettings
  fetchUsageData->>aiSettingsApi_loadAISettings: loadAISettings()
  aiSettingsApi_loadAISettings-->>fetchUsageData: aiSettings or error (auth_expired rethrown)
  fetchUsageData-->>Cache: persist payload when teamUsage & currentPlan present
  fetchUsageData-->>useUsageState: return {teamUsage,currentPlan,aiSettings}
  useUsageState->>useUsageState: set aiSettings state and derive isFullyRoutedAway
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

working

Poem

🐰 I peeked at routing maps tonight,
Hopped when workloads left my sight,
If chats all roam, the banners hush,
Failures keep warnings in a rush,
Tests nibble proof beneath the light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly summarizes the main change: suppressing the budget banner when chat workloads route to a user-supplied provider, and references the linked issue numbers.
Linked Issues check ✅ Passed The PR implements all coding requirements from #2040 and #2041: suppresses budget-completed messages and indicators when all chat workloads route away from OpenHuman, maintains conservative behavior on fetch failures, and adds regression test coverage.
Out of Scope Changes check ✅ Passed All changes are within scope: fetchUsageData integration, isFullyRoutedAway derivation, budget gating logic, and comprehensive test coverage directly address the linked issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
CI Type Check TypeScript step failed on 'prettier --check' for the two
files changed in this PR. Run prettier --write on them to restore the
project's quote / wrap / indent conventions. No behaviour or type
changes — same 9 Vitest cases still pass.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walkthrough

Clean, well-scoped bug fix that suppresses the budget banner and rate-limit signals when all chat workloads are routed to a user-supplied provider. The approach — gating isBudgetExhausted, shouldShowBudgetCompletedMessage, and isRateLimited behind allChatWorkloadsRoutedAway with conservative fallback on fetch failure — is sound and correctly addresses #2040 and #2041. One consistency issue in error handling needs attention.

Change Summary

File Change type Description
app/src/hooks/useUsageState.ts Modified Adds loadAISettings() to fetchUsageData, derives allChatWorkloadsRoutedAway, gates budget/rate-limit signals
app/src/hooks/useUsageState.test.ts Modified Adds AI settings mock, 3 new test cases (happy path, partial routing, fetch failure)

Per-file Analysis

useUsageState.ts

The core logic is well-structured. The CHAT_WORKLOADS.every() derivation is clean, the raw* intermediate variables make the gating clear, and the comments explaining the suppression are helpful. The USAGE_UNAVAILABLE sentinel reuse is appropriate.

The ref !== undefined guard in the every() callback is a good defensive touch — handles the case where a workload key is missing from the routing map without crashing.

One real issue: the .catch() on loadAISettings() silently swallows all errors, including CoreRpcError(kind='auth_expired'). The two sibling fetches (getTeamUsage, getCurrentPlan) explicitly re-throw auth_expired so the hook's caller can handle session expiry. Since loadAISettings calls openhumanGetClientConfig() and authListProviderCredentials() — both of which go through the same RPC layer — it can throw auth_expired too. Swallowing that means a session-expired user could silently get stale data instead of being prompted to re-authenticate.

useUsageState.test.ts

Test structure is excellent. The ALL_OPENHUMAN_AI_SETTINGS default in beforeEach ensures all 6 legacy tests continue to pass with identical semantics. The three new cases cover the critical paths well. Good use of vi.importActual to preserve the real CHAT_WORKLOADS constant.

Minor gap: all routed-away tests use kind: 'cloud' — no coverage for kind: 'local' (Ollama), which the PR description explicitly claims to support. The logic handles it correctly, but an explicit test would match the claim.

Comment thread app/src/hooks/useUsageState.ts Outdated
// suppress the budget banner when the user supplied their own provider
// key (#2040 / #2041). A failure here is treated as "unknown" — the
// budget gate stays in its conservative (banner-on) state.
loadAISettings().catch(() => USAGE_UNAVAILABLE),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] This .catch(() => USAGE_UNAVAILABLE) swallows all errors, but the two sibling fetches above re-throw CoreRpcError(kind='auth_expired') to let the caller handle session expiry. loadAISettings calls openhumanGetClientConfig() + authListProviderCredentials() through the same RPC layer, so it can throw auth_expired too.

Swallowing it here means a session-expired user might not get redirected to re-auth if this is the only call that fails in the Promise.all.

Suggested fix — match the sibling pattern:

loadAISettings().catch(err => {
  if (err instanceof CoreRpcError && err.kind === 'auth_expired') {
    throw err;
  }
  return USAGE_UNAVAILABLE;
}),

const ref = aiSettings.routing[w];
return ref !== undefined && ref.kind !== 'openhuman';
})
: false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Naming nit — every other boolean on UsageState uses an is* or should* prefix (isRateLimited, isBudgetExhausted, isAtLimit, shouldShowBudgetCompletedMessage). allChatWorkloadsRoutedAway breaks that convention. Consider areAllChatWorkloadsRoutedAway or isFullyRoutedAway for API consistency.

it('suppresses the budget banner when every chat workload routes to a user-supplied cloud provider (#2040, #2041)', async () => {
const { useUsageState } = await import('./useUsageState');

// Plan + usage say "budget exhausted" — but the user has saved an
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] All routed-away tests use kind: 'cloud' providers. Since the PR description explicitly claims support for "local Ollama", consider adding at least one workload with kind: 'local' to a test case (e.g., swap coding to { kind: 'local', model: 'llama3' } here). The logic handles it correctly, but explicit coverage strengthens confidence and documents the intent.

… auth_expired + cover ProviderRef kind=local

Two follow-ups for the CHANGES_REQUESTED review on tinyhumansai#2053:

1. loadAISettings().catch() silently swallowed every error, including
   CoreRpcError(kind='auth_expired'). The two sibling fetches in
   fetchUsageData (creditsApi.getTeamUsage, billingApi.getCurrentPlan)
   explicitly re-throw auth_expired so coreRpcClient's global re-auth
   handler fires; loadAISettings goes through the same RPC layer and
   must follow the same contract. Mirror the pattern so a session-
   expired user gets prompted to re-authenticate instead of silently
   getting stale data.

2. The three new tests in the previous commit all used
   ProviderRef kind='cloud'. The PR description claims suppression
   also applies to kind='local' (Ollama), but that path was untested.
   Add two new test cases:

   - 'suppresses the budget banner when every chat workload routes to
     a local Ollama provider' — pins kind='local' suppression.

   - 'rethrows CoreRpcError(kind=auth_expired) from loadAISettings
     instead of swallowing it' — pins the new error-handling contract.

Local: pnpm prettier --write clean (no changes); pnpm test:unit
useUsageState 11/11 passed in 1.56s (6 legacy + 5 new).
@YOMXXX
Copy link
Copy Markdown
Contributor Author

YOMXXX commented May 18, 2026

Thanks @graycyrus for the careful review! Both points addressed in b56260a6.

1. auth_expired rethrow

Real contract violation — agreed. loadAISettings goes through the same RPC layer as the sibling fetches, so swallowing CoreRpcError(kind='auth_expired') meant a session-expired user got stale data instead of the global re-auth prompt.

Fix mirrors the existing pattern verbatim:

loadAISettings().catch(err => {
  if (err instanceof CoreRpcError && err.kind === 'auth_expired') {
    throw err;
  }
  return USAGE_UNAVAILABLE;
}),

Added test case rethrows CoreRpcError(kind=auth_expired) from loadAISettings instead of swallowing it that pins the new contract against the same outer-.catch swallow the existing #1472 test (line 164) pins for getTeamUsage.

2. kind: 'local' coverage

Fair miss. The PR description claimed Ollama support but no test exercised it.

Added test case suppresses the budget banner when every chat workload routes to a local Ollama provider — same shape as the cloud-routed case, but reasoning / agentic / coding all set to { kind: 'local', model: 'qwen3:8b' }. Background workloads intentionally left on openhuman to keep the chat-workloads-only key clear.

Local checks

pnpm prettier --write src/hooks/useUsageState.ts src/hooks/useUsageState.test.ts  # no changes
pnpm test:unit useUsageState
  Test Files  1 passed (1)
       Tests  11 passed (11)

Ready for another look when you have a moment.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
@YOMXXX
Copy link
Copy Markdown
Contributor Author

YOMXXX commented May 18, 2026

Quick ping in case the GitHub notification got buried — pushed b56260a6 earlier addressing both points from your review:

  1. auth_expired rethrow on loadAISettings (mirrors the sibling fetches; new test rethrows CoreRpcError(kind=auth_expired) from loadAISettings instead of swallowing it pins it against the existing Track and fix active Sentry issues #1472 contract).
  2. kind: 'local' (Ollama) coverage (new test suppresses the budget banner when every chat workload routes to a local Ollama provider).

CI is green on the new commit, Vitest 11/11 pass locally in 1.56s. Happy to take another look if anything's still off — and no rush at all.

…Away (graycyrus review tinyhumansai#2053)

graycyrus's minor naming nit on PR tinyhumansai#2053: every other boolean field on
UsageState uses an is*/should* prefix (isRateLimited,
isBudgetExhausted, isAtLimit, shouldShowBudgetCompletedMessage), but
my new field was named allChatWorkloadsRoutedAway — broke the
convention. Renamed across both the hook and the test file to
isFullyRoutedAway, which is the shorter of graycyrus's two suggestions
and keeps the boolean-naming convention intact.

Mechanical rename: 7 sites in useUsageState.ts (interface field, local
derivation, three gate expressions, the returned value) plus 5 sites
in useUsageState.test.ts (assertions). No behavioural change.

Local:
- pnpm prettier --check on both files: clean
- pnpm test:unit useUsageState: 11/11 pass in 1.51s
@YOMXXX
Copy link
Copy Markdown
Contributor Author

YOMXXX commented May 18, 2026

Done — pushed d14ccfcc. Renamed allChatWorkloadsRoutedAwayisFullyRoutedAway across both the hook (interface field + 6 internal uses) and the test file (5 assertions). Picked the shorter of your two suggestions to match the existing isRateLimited / isBudgetExhausted / isAtLimit convention.

Vitest 11/11 still pass (1.51s), prettier clean. All three of your review points are now addressed across b56260a6 + d14ccfcc.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
@senamakel
Copy link
Copy Markdown
Member

I love this...

@senamakel senamakel self-assigned this May 19, 2026
senamakel added 2 commits May 19, 2026 14:13
# Conflicts:
#	app/src/hooks/useUsageState.test.ts
#	app/src/hooks/useUsageState.ts
@senamakel
Copy link
Copy Markdown
Member

Merged latest main to clear conflicts.

Resolution note (heads-up): the isRateLimited / 5-hour rate-limit gating that this PR was layering on top of isFullyRoutedAway had to be dropped during the merge. Main #2027 ("feat(billing): drop rate-limit UI, surface cycle usage insights") intentionally removed bypassCycleLimit, fiveHourCapUsd, and cycleLimit5hr from TeamUsage, so the existing computation no longer typechecks. The PR's primary scope — suppressing the budget banner when all chat workloads route to a user-supplied provider — is unchanged and still covered by tests, including the routed-cloud / partial-routing / local-Ollama / auth_expired re-throw cases.

Also added the new chat workload (added in main alongside reasoning/agentic/coding in CHAT_WORKLOADS) to the test fixtures.

All 11 tests in useUsageState.test.ts pass; pnpm compile clean.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 19, 2026
@senamakel senamakel merged commit d599633 into tinyhumansai:main May 19, 2026
27 of 31 checks passed
mtkik pushed a commit to mtkik/openhuman-meet that referenced this pull request May 21, 2026
… a user-supplied provider (tinyhumansai#2040, tinyhumansai#2041) (tinyhumansai#2053)

Co-authored-by: 李冠辰 <liguanchen@xiaomi.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
CodeGhost21 pushed a commit to CodeGhost21/openhuman that referenced this pull request May 22, 2026
… a user-supplied provider (tinyhumansai#2040, tinyhumansai#2041) (tinyhumansai#2053)

Co-authored-by: 李冠辰 <liguanchen@xiaomi.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
… a user-supplied provider (tinyhumansai#2040, tinyhumansai#2041) (tinyhumansai#2053)

Co-authored-by: 李冠辰 <liguanchen@xiaomi.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

3 participants