Repository navigation
feat(ai): community AI providers, credit balances & per-call cost (desktop + mobile) - #108
Conversation
Bring the desktop community-providers feature to mobile, reusing the shared providers.json schema and the existing keychain / meta-DB / ConnectorLogo infrastructure. - providers-registry.ts: fetch + cache the separate providers.json manifest (conditional GET + ETag, offline meta-DB cache, hard-refresh cache-buster), mirroring registry.ts and the desktop fetcher - ai-config.ts: SavedProvider gains source/communityId; installCommunityProvider (dedup by communityId/name, reuse id so the secure-store key survives, store key in expo-secure-store, no auto-select) + isCommunityProviderOutdated - app/settings/providers.tsx: Browse Providers screen — search + category chips, Add/Added/Update states, needsApiKey gating (iOS Alert.prompt / Android modal), "Get a key" link - AiSettingsForm.tsx: Browse Providers nav row; refresh the provider list on focus so a newly-added provider appears in the switcher - changelog: v0.1.4 entry
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change adds mobile community-provider browsing and installation, shared provider credit and model catalogs, provider balance probing, provider-reported usage-cost tracking, PDF attachments, model capability indicators, attachment gating, and Radix-based chat quick settings. ChangesProvider platform enhancements
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ProvidersSettingsScreen
participant fetchProvidersManifest
participant installCommunityProvider
participant SecureStorage
User->>ProvidersSettingsScreen: Open or filter provider catalog
ProvidersSettingsScreen->>fetchProvidersManifest: Load or refresh manifest
fetchProvidersManifest-->>ProvidersSettingsScreen: Return manifest or cached data
User->>ProvidersSettingsScreen: Install provider with API key
ProvidersSettingsScreen->>installCommunityProvider: Submit provider definition
installCommunityProvider->>SecureStorage: Store API key
installCommunityProvider-->>ProvidersSettingsScreen: Return saved provider ID
sequenceDiagram
participant Chat
participant runToolLoop
participant Provider
participant ContextRing
participant BalanceCache
Chat->>runToolLoop: Process streamed usage
runToolLoop->>Provider: Read provider-reported cost
Provider-->>runToolLoop: Return numeric cost
runToolLoop-->>Chat: Emit accumulated costUsd
Chat->>ContextRing: Pass persisted costUsd
ContextRing->>BalanceCache: Check cached provider balance
BalanceCache-->>ContextRing: Return balance or null
ContextRing-->>Chat: Render cost and balance
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile/app/settings/providers.tsx`:
- Around line 66-92: Guard the state updates in loadRegistry against completion
after the owning effect has been cleaned up. Pass or otherwise associate the
effect’s cancelled state with loadRegistry, and only call setProviders and
setError while still active; preserve the existing loading and refresh
completion behavior.
- Around line 223-232: Update the Pressable handler around def.apiKeyUrl to
handle rejected promises from Linking.openURL, ensuring failures are caught
rather than left unhandled while preserving the existing “Get a key” behavior.
- Around line 210-263: Update the row rendering logic around installedRow and
outdated to derive the outdated state directly from installedRow, using the same
version comparison semantics as isCommunityProviderOutdated. Remove the
per-entry call to isCommunityProviderOutdated(entry), while preserving the
existing installed and update-button behavior.
In `@mobile/src/chat/ai-config.ts`:
- Around line 275-305: Update the existing-provider lookup in
installCommunityProvider so the name-based fallback only matches providers whose
source is "community"; retain the communityId match behavior and leave manually
configured providers unmatched when names collide.
In `@mobile/src/chat/providers-registry.ts`:
- Around line 75-113: Update fetchProvidersManifest so the If-None-Match
conditional request is used only when getCachedProvidersManifest() returns a
valid manifest. When cached is null, omit the ETag header (and treat the request
as an unconditional fetch) so the server returns content that can be revalidated
and recached; preserve the existing 304 behavior for valid cached manifests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bbc1e3ad-265b-4ff4-bd09-8cd970ff0afb
📒 Files selected for processing (5)
mobile/app/settings/providers.tsxmobile/changelogs/v0.1.4.mdmobile/src/chat/ai-config.tsmobile/src/chat/providers-registry.tsmobile/src/components/AiSettingsForm.tsx
- Share credit/balance parsing across desktop + mobile via shared/chat/provider-credits.ts: manifest descriptor resolution and shape parsers for OpenRouter, DeepSeek, OpenAI grants and Neuralwatt. - Desktop: ai:fetchKeyInfo resolves the community manifest's credits descriptor per endpoint; settings endpoint rows show a CreditsBadge. - Mobile: AI-settings keyInfo card resolves the manifest descriptor too, so DeepSeek/Neuralwatt balances render alongside OpenRouter. - Per-call USD cost (usage.cost) flows through both context rings (desktop chat usage/done events + mobile persisted lastUsage). - Registry schema grows the credits descriptor (url + shape incl. neuralwatt); add live endpoint check and unit tests. - Changelogs: changelogs/v2.5.20.md, mobile/changelogs/v0.1.4.md.
…, a11y - providers.tsx: guard loadRegistry state updates against effect teardown (drop results when the owning effect was cancelled); catch rejected Linking.openURL promises for 'Get a key'; derive the Update/outdated state from the fetched installed row instead of a per-entry list scan. - ai-config.ts: installCommunityProvider name-based fallback now only matches community-sourced providers, so manually-configured same-name providers are left untouched. - providers-registry.ts: only send If-None-Match when a valid cached manifest exists; empty/corrupt cache triggers an unconditional fetch.
- provider-credits.test.ts: drop unused shape-parser imports (eslint). - provider-credits.live-check.test.ts: defer the manifest read into the test body so an absent cairn-community repo (CI/fresh clone) collects cleanly and skips instead of throwing ENOENT at suite collection.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@electron/ipc/chat.ts`:
- Line 177: Update costUsd in the chat usage flow to remain optional rather than
defaulting to 0, and only include it in emitted usage after validating that the
value is finite and non-negative. Preserve an explicitly reported cost of 0, and
adjust finalUsage() so cost-only usage is retained instead of being discarded by
the promptTokens > 0 condition.
- Line 262: Propagate subagent cost through the usage contract: add optional
costUsd to SubagentEvents.onSubagentUsage, preload onSubagentUsage,
ChatSubagent.lastUsage, and useChatStream.onSubagentUsage; update
electron/ipc/chat.ts chat:subagent-usage to pass subagent.lastUsage.costUsd and
include metrics.promptTokens when it represents the subagent’s own context-turn
total; update ChatSubagentBlock to accept costUsd and render it via ContextRing.
Apply the type change at src/types/index.ts:523 and the IPC change at
electron/ipc/chat.ts:262.
In `@electron/lib/provider-credits.live-check.test.ts`:
- Around line 31-34: Move the fs.readFileSync(manifestPath, "utf8") manifest
initialization out of the describe.skipIf(!hasManifest) factory and into the
guarded it.skipIf(!hasAnyKey) test callback. Keep manifest-dependent assertions
inside that callback so the file is never read when the manifest is unavailable.
In `@mobile/src/components/ContextRing.tsx`:
- Around line 263-266: Update formatUsd in
mobile/src/components/ContextRing.tsx:263-266 to preserve at least one non-zero
significant digit for small positive costs such as 0.000001, then use this
corrected formatter in the non-iOS alert at
mobile/src/components/ContextRing.tsx:128-130 and iOS popover at
mobile/src/components/ContextRing.tsx:240-246; apply the same formatting
behavior in src/components/agent/ContextRing.tsx:201-210.
In `@shared/chat/provider-credits.test.ts`:
- Around line 2-10: Remove the unused parseOpenRouterCredits,
parseDeepSeekCredits, and parseOpenAiGrantsCredits imports from the
provider-credits test file; retain the imports used by existing tests, including
parseNeuralwattCredits and parseCredits.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 65348933-5c14-4141-893b-dbaa62ddbba7
📒 Files selected for processing (29)
changelogs/v2.5.20.mdelectron/ipc/ai-handlers.tselectron/ipc/chat.tselectron/lib/chat-loop.tselectron/lib/provider-credits.live-check.test.tselectron/lib/provider-credits.tselectron/preload.tsmobile/app/(tabs)/chat/index.tsxmobile/app/settings/providers.tsxmobile/changelogs/v0.1.4.mdmobile/src/chat/ai-config.tsmobile/src/chat/providers-registry.tsmobile/src/chat/providers/openai.tsmobile/src/chat/providers/types.tsmobile/src/components/ContextRing.tsxmobile/src/db/chat-store.tsshared/chat/provider-credits.test.tsshared/chat/provider-credits.tsshared/chat/registry-schema.test.tsshared/chat/registry-schema.tssrc/components/agent/AgentChatPane.tsxsrc/components/agent/AgentMessageBubble.tsxsrc/components/agent/ContextRing.tsxsrc/components/chat/chat-panel/ChatSubagentBlock.tsxsrc/components/chat/chat-panel/index.tsxsrc/components/settings/endpoint-components.tsxsrc/hooks/useChatStream.tssrc/store/slices/chat.tssrc/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- mobile/src/chat/providers-registry.ts
- mobile/src/chat/ai-config.ts
- mobile/app/settings/providers.tsx
- mobile/changelogs/v0.1.4.md
…cost formatting - chat.ts: costUsd stays undefined until a provider reports it; only non-negative finite values accumulate (explicit 0 preserved), and finalUsage keeps cost-only usage instead of dropping it on the promptTokens > 0 gate. - subagent cost: SubagentEvents.onSubagentUsage + preload onSubagentUsage + useChatStream carry costUsd, and runSubagent accumulates each subagent's cost (incl. a forced final answer) onto its own context ring. The chat:subagent-usage passthrough and promptTokens-as-own-context semantics were already correct. - ContextRing (mobile + desktop agent): formatUsd keeps at least one significant digit for tiny costs (e.g. $0.000001) instead of collapsing to $0.
…ngs popover - Context ring gains an account-level Balance row (remaining credits or "Free tier"), TTL-cached (1min, shared via useSyncExternalStore) so ring instances don't re-probe; hidden when the provider exposes no balance or offline; excluded from subagent rings (showBalance=false). - Mobile chat-header ring shows the same Balance row (Alert + iOS popover) via getKeyInfo with the same TTL cache. - ChatQuickSettings dropdown is now a portaled Radix Popover so it no longer clips in narrow windows / docked panels.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
electron/lib/provider-credits.live-check.test.ts (1)
55-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPass an abort signal to bound each live probe.
probeCreditsaccepts an optionalAbortSignal, and the production path inelectron/ipc/ai-handlers.tsuses a 12-secondAbortController. This test passes no signal. If a provider endpoint hangs, the test blocks until the Vitest timeout and gives no per-provider diagnostic. Add a per-probe timeout so the loop reports a failed provider instead of stalling.♻️ Proposed change
- const probe = await probeCredits(resolved.url, key, resolved.shape); + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), 12_000); + let probe; + try { + probe = await probeCredits(resolved.url, key, resolved.shape, ac.signal); + } finally { + clearTimeout(timer); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@electron/lib/provider-credits.live-check.test.ts` at line 55, Update the live probe flow around probeCredits to create a per-provider AbortController with the same 12-second timeout used by electron/ipc/ai-handlers.ts, pass its signal as the optional argument, and clear the timeout after completion so hanging providers fail through the existing diagnostic path instead of blocking the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile/src/components/ContextRing.tsx`:
- Around line 19-21: Update the mobile balance cache definition and its
read/write logic in ContextRing to include active.id alongside the existing base
URL key. Ensure cache hits require the provider id, base URL, and API key to
match, and store all three values when refreshing the balance, preserving the
existing TTL behavior.
- Around line 71-90: Update the balance-loading useEffect in ContextRing to
depend on getActiveProviderId(), so it reruns when the active provider changes
while preserving the existing cache and cancellation behavior.
In `@src/components/agent/ContextRing.tsx`:
- Around line 112-126: Move the duplicated formatUsd and formatBalance helpers
into shared/chat/provider-credits.ts, then update both
src/components/agent/ContextRing.tsx lines 112-126 and
mobile/src/components/ContextRing.tsx lines 311-325 to reuse them. Preserve
negative signs by using Math.abs only for digit selection and placing the
currency symbol after the minus sign, while retaining existing formatting
behavior for positive values and balance states.
- Around line 72-107: Deduplicate concurrent balance probes in the ContextRing
balance-loading effect by adding a module-level in-flight promise keyed by the
balance key and reusing it when the cache is cold or expired. Store and clear
the shared promise around fetchKeyInfo, update the shared balance store from the
resolved result, and remove the per-effect cancelled guard since store
notifications already handle unmounted subscribers.
---
Nitpick comments:
In `@electron/lib/provider-credits.live-check.test.ts`:
- Line 55: Update the live probe flow around probeCredits to create a
per-provider AbortController with the same 12-second timeout used by
electron/ipc/ai-handlers.ts, pass its signal as the optional argument, and clear
the timeout after completion so hanging providers fail through the existing
diagnostic path instead of blocking the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c7c6ede-3a61-4c6e-9a94-f3c2e677d4f6
📒 Files selected for processing (13)
changelogs/v2.5.20.mdelectron/ipc/chat.tselectron/lib/chat-subagent-loop.tselectron/lib/provider-credits.live-check.test.tselectron/preload.tsmobile/changelogs/v0.1.4.mdmobile/src/components/ContextRing.tsxshared/chat/provider-credits.test.tssrc/components/agent/AgentMessageBubble.tsxsrc/components/agent/ContextRing.tsxsrc/components/chat/chat-panel/ChatQuickSettings.tsxsrc/components/chat/chat-panel/ChatSubagentBlock.tsxsrc/hooks/useChatStream.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/components/chat/chat-panel/ChatSubagentBlock.tsx
- src/components/agent/AgentMessageBubble.tsx
- electron/ipc/chat.ts
- electron/preload.ts
- shared/chat/provider-credits.test.ts
- src/hooks/useChatStream.ts
Fresh installs, resets, and unset-model fallbacks use gpt-5.6-luna instead of gpt-4o / gpt-4o-mini (chat, coding agent, notes AI actions, flow summaries); mobile's DEFAULT_OPENAI_MODEL switches from gpt-4.1-mini. UI placeholders and onboarding preset list updated to match; the model picker still lists the endpoint's real models.
…rs, probe dedup - Desktop: dedupe concurrent balance probes via a module-level in-flight promise keyed by provider key (cold/expired cache); drop the per-effect cancelled guard (the external-store subscription already handles unmounts). - Mobile: key the balance cache on provider id + base URL + API key (two providers can share a base URL with different keys) and rerun the probe when the active provider id changes. - Move formatUsd/formatBalance into shared/chat/provider-credits.ts and reuse from both context rings; negative amounts render the currency after the minus sign (-¥5). Cover both with unit tests. - Live-check test: probe each provider through a 12s AbortController like the ai:fetchKeyInfo handler, cleared on completion.
Extends the existing models.dev catalog lookup (context ring) into a full
per-model metadata layer shared by desktop + mobile:
- shared/models/model-catalog.ts: parse api.json into {context, input/output
per-1M cost, image-input, tool-call, provider}, id normalization/fuzzy
lookup, formatModelCost (tiny-cost safe), providerLogoUrl, supportsImageInput
- desktop + mobile models-dev: persist the full info map (localStorage /
app_settings), add a subscribe/version API so pickers re-render on load
- model pickers (desktop dropdown + trigger, mobile AI settings list) show the
provider logo (theme-aware: inverted on dark), $in/$out cost and a warning
for models without tool calling
- chat image attach gated per model: hidden/disabled when the active model is
known not to accept images; permissive default for unknown models
Extends the shared models.dev ModelInfo to carry the raw input modalities and renders compact capability chips next to each model: on the desktop picker trigger (closed state) and dropdown rows, and on the mobile AI-settings model list. Tooltips explain each chip (Text/Image/PDF/Video/Audio input).
- Accessors (supportsImageInput, supportsPdfInput, modelInputChips) no longer assume the cached ModelInfo has the modes array — a cache written by the previous build (pre-modes shape) crashed ChatPanel. Cache loads now normalize entries (normalizeModelInfo) and migrate the legacy image boolean. - Capability chips now render lucide icons (Type / Image / FileText / Video / AudioLines) instead of text letters, on the desktop picker trigger + rows and the mobile model list.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile/app/`(tabs)/chat/index.tsx:
- Around line 84-111: Include the value returned by useSyncExternalStore in the
allowImages useMemo dependency array so the memo recomputes when the model
catalog updates. Keep openAiModelActive as the existing input and preserve the
current permissive behavior for unknown or unavailable models.
In `@shared/chat/provider-credits.ts`:
- Around line 168-173: Update formatUsd to format based on the absolute cost
magnitude while preserving the negative sign separately, matching
formatBalance’s fmt helper; ensure large negative values such as -123.456 render
with the correct amount. Add a negative-cost test in provider-credits.test.ts,
and apply the same correction to the duplicate formatter in ContextRing.tsx if
present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 27d6e425-9df6-4c67-94ec-9805e4f5c126
📒 Files selected for processing (38)
changelogs/v2.5.20.mdelectron/ipc/ai-handlers.tselectron/ipc/chat.tselectron/ipc/flow-handlers.tselectron/ipc/pi-agent.tselectron/ipc/tool-builder.tselectron/lib/provider-credits.live-check.test.tsmobile/app/(tabs)/chat/index.tsxmobile/changelogs/v0.1.4.mdmobile/src/chat/ai-config.tsmobile/src/chat/models-dev.tsmobile/src/components/AiSettingsForm.tsxmobile/src/components/ContextRing.tsxmobile/src/components/ProviderLogo.tsxmobile/src/components/ai-settings/styles.tsmobile/src/components/chat/Composer.tsxshared/chat/provider-credits.test.tsshared/chat/provider-credits.tsshared/models/model-catalog.test.tsshared/models/model-catalog.tsshared/models/pdf-attach.test.tsshared/models/pdf-attach.tssrc/app/globals.csssrc/components/agent/ContextRing.tsxsrc/components/agent/SpawnAgentModal.tsxsrc/components/chat/ChatInput.tsxsrc/components/chat/chat-panel/ChatQuickSettings.tsxsrc/components/chat/chat-panel/index.tsxsrc/components/flow/nodes/AiSummaryNode.tsxsrc/components/notes/notes-view/PrdModal.tsxsrc/components/onboarding/StepAISetup.tsxsrc/components/onboarding/index.tsxsrc/components/settings/ProviderManager.tsxsrc/components/settings/endpoint-components.tsxsrc/components/ui/model-picker.tsxsrc/lib/constants.tssrc/lib/models-dev.tssrc/store/slices/notes.ts
🚧 Files skipped from review as they are similar to previous changes (10)
- electron/lib/provider-credits.live-check.test.ts
- mobile/changelogs/v0.1.4.md
- electron/ipc/ai-handlers.ts
- electron/ipc/chat.ts
- mobile/src/chat/ai-config.ts
- src/components/chat/chat-panel/ChatQuickSettings.tsx
- src/components/settings/endpoint-components.tsx
- shared/chat/provider-credits.test.ts
- mobile/src/components/ContextRing.tsx
- src/components/agent/ContextRing.tsx
…allback) The chat composer now accepts PDFs alongside images. Models known (via models.dev) to take PDF input receive the raw bytes as an Anthropic-style `document` base64 part — lossless, and smaller than rasterized pages for text-heavy files. Models that only take images get the PDF rasterized in-browser (pdf.js legacy build, main-thread fake worker, up to 8 pages) into image parts so the file still arrives. - shared/models/pdf-attach.ts: buildAttachmentParts emits document parts for kind:"pdf" (kind optional, defaults to image); pdfTokenEstimate bounds the context-ring cost of a document part. - src/lib/pdf-document.ts + test: node-safe pdf.js loader (page count, base64 decode) backed by a committed 4-page fixture. - src/lib/pdf-rasterize.ts: page->PNG data-URL rendering. Uses pdf.js's main-thread fake worker (importing pdf.worker.min.mjs self-registers globalThis.pdfjsWorker) instead of a ?url asset import, which Turbopack can't compile for the static export. - electron/ipc/chat.ts + lib/chat-subagent-loop.ts: user message content built via buildAttachmentParts; lib/llm.ts tokContent bills document parts via pdfTokenEstimate (not raw base64 length). - desktop UI: ChatInput accept=image/pdf + PDF chip in the preview strip and in sent bubbles; chat-panel gates attach on supportsPdfInput / supportsImageInput and rasterizes as the image-only fallback. - mobile: the OpenAI provider maps application/pdf file parts to document parts (latent — mobile has no document picker yet). changelog: changelogs/v2.5.20.md
…ating - shared/chat/provider-credits.ts: formatUsd now formats by absolute magnitude with the minus preserved before the symbol (matching formatBalance). Negative costs like -123.456 previously fell into the tiny-cost toPrecision(2) branch and rendered as "$-120"; they now show "-$123.456". No duplicate formatter in ContextRing — both desktop and mobile import the shared one. Added negative-cost cases to the tests. - mobile/app/(tabs)/chat/index.tsx: the allowImages memo now depends on the useSyncExternalStore catalog version, so image gating recomputes when the models.dev catalog loads/refreshes after the active model was resolved on focus (previously the memo stayed stale until the model name changed). Permissive behaviour for unknown/absent models unchanged.
The bare model text field in AI settings is now a dropdown-style selector matching the desktop picker: the current model shows on a tappable row with its provider logo, cost, and capability markers, expanding into a searchable list plus a "Custom model…" input for ids the endpoint didn't list. The list now populates itself — the core of the "first need to fetch" friction. Model ids are cached per endpoint (meta DB, 7-day TTL, mirroring desktop's endpoint-models-cache) and hydrated instantly on open; with an API key present, the endpoint is refreshed in the background (debounced + generation-guarded) so a fresh provider always lists its models without a manual tap. The "Fetch models" button becomes a "Refresh" affordance. - mobile/src/chat/ai-config.ts: readModelsCache/writeModelsCache (normCacheBaseUrl keying). - AiSettingsForm.tsx: dropdown trigger + expandable searchable list + custom-model toggle; auto-fetch effect (cache hydrate + debounced refresh); fetchModels now takes explicit base/key so auto-refresh can't mix a mid-switch provider's url with the old key. changelog: mobile/changelogs/v0.1.4.md
Each dropdown row gets a star (nested pressable on the leading edge) that toggles a favorite without selecting the model. Favorites are persisted as a global id set in the meta DB (mirroring desktop's localStorage favoriteModels) and pinned above the rest in a "Favorites" section at the top of the list, so usual models are one tap away instead of buried in the full list. - mobile/src/chat/ai-config.ts: getFavoriteModels/toggleFavoriteModel (key ai.favoriteModels, JSON array of ids). - AiSettingsForm.tsx: favorites state + shared renderModelRow with star, favModels/restModels split of the filtered list, section label + separator. - ai-settings/styles.ts: modelStar/modelSectionLabel/modelSectionSeparator. changelog: mobile/changelogs/v0.1.4.md
models.dev lists each model under every host provider, so the flattened api.json catalog can't tell who OWNS a model — `deepseek/deepseek-v4-flash` resolved to the hpc-ai logo and bare `deepseek-v4-flash` to cortecs. We now also fetch models.json (222KB canonical <provider>/<model> map) alongside api.json and resolve the logo from the canonical owner, with the brand-name heuristic as a mid-tier fallback (covers models not in the canonical list and ids the fuzzy match can't reach). - shared: parseCanonicalCatalog + canonicalProviderFor (exact + normalized fuzzy match like lookupModelInfo); logoProviderFor/providerLogoUrlFor take an optional canonical map and prefer it over the heuristic. - desktop + mobile models-dev: fetch + cache models.json (per-platform localStorage/SQLite, same 7-day TTL), expose getLogoProvider/modelLogoUrl. - model-picker.tsx + AiSettingsForm.tsx: resolve logos via the composed lookup. Verified against the live catalogs: deepseek-v4-flash→deepseek, openai/gpt-5.2→openai, zhipuai/glm-5→zhipuai, xai/grok-4.3→xai, gemma-3→google (were hpc-ai/perplexity-agent/cortecs/vercel/inference). changelogs: v2.5.20.md + mobile v0.1.4.md
Saved community AI providers now render their real brand icon (ConnectorLogo) in the saved-providers switcher instead of the generic Server glyph — on both desktop (trigger + dropdown rows) and mobile (provider chips). The logo is resolved from the community catalog by communityId, with a fallback to a normalized base-URL match so a provider added manually that happens to share an endpoint with a catalog entry (e.g. added before it was in the registry) still picks up its brand mark. - desktop ProviderManager.tsx: logoMap keyed by id + normBaseUrl; logoFor. - mobile AiSettingsForm.tsx: buildProviderLogoMap keyed by id + normBaseUrl; prewarm cache-first + network refresh; passed to ProviderList. - mobile ProviderList.tsx: renders ConnectorLogo on each chip, resolved by communityId → normalized baseUrl. changelogs: v2.5.20.md + mobile v0.1.4.md
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
electron/ipc/chat.ts (1)
266-266: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftKeep dispatcher costs in the main usage contract.
Subagent mode does not carry dispatcher-generated costs into the main
chat:doneusage. This underreports provider-reported USD usage.
electron/ipc/chat.ts#L266-L266: Return the dispatcher cost fromrunDispatchLoopand merge it before callingfinalUsage().electron/lib/chat-subagent-loop.ts#L520-L522: Pass the dispatcher cost accumulator toforceFinalAnswerso forced synthesis cost is recorded.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@electron/ipc/chat.ts` at line 266, Update electron/ipc/chat.ts lines 266-266 so runDispatchLoop returns its dispatcher cost and that cost is merged into the main usage before finalUsage() is called. Update electron/lib/chat-subagent-loop.ts lines 520-522 to pass the dispatcher cost accumulator to forceFinalAnswer, ensuring forced synthesis costs are included in the final chat:done usage.mobile/src/chat/providers/openai.ts (1)
180-188: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winValidate
usage.costbefore assigning tocostUsd.
mobile/src/chat/providers/openai.tscurrently assigns any numericchunk.usage.costat lines 263-265, so invalid values can reachusage.costUsd. Use the same guard aselectron/ipc/chat.ts:typeof cost === "number" && Number.isFinite(cost) && cost >= 0. Preserve0and leavecostUsdundefined when no valid cost is reported.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/src/chat/providers/openai.ts` around lines 180 - 188, Validate the cost value before assigning it to costUsd in the OpenAI streaming usage handling. Apply the guard typeof cost === "number", Number.isFinite(cost), and cost >= 0, preserving valid 0 values and leaving costUsd undefined for missing or invalid costs; use the existing costUsd and chunk.usage handling symbols.
🧹 Nitpick comments (2)
src/components/chat/chat-panel/index.tsx (1)
361-380: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winLimit the size of rasterized PDF attachments.
rasterizePdfToImagesreturns up to 8 PNG pages at up to 2048 px edge. Each page becomes a base64 data URL inpendingAttachments.addMessageat Line 412 persists these data URLs to the store, to disk, and over IPC. A multi-page PDF can therefore add tens of megabytes per message. Consider a total byte budget, JPEG output, or a smaller edge cap for the fallback path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/chat/chat-panel/index.tsx` around lines 361 - 380, Limit the fallback rasterization in the attachment-processing flow around rasterizePdfToImages before pushing items into pendingAttachments. Enforce a bounded total payload for generated page data, preferably by reducing image dimensions or using JPEG output, and stop or skip pages once the budget is reached while preserving existing PDF/image handling and error behavior.mobile/src/components/AiSettingsForm.tsx (1)
507-518: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the capability-icon mapping into one helper.
The same
c.key → iconchain exists inrenderModelRowand in the collapsed trigger. Duplicate mapping can diverge when a new capability key is added. Move the map to a module-level constant and render from it in both places.♻️ Proposed refactor
+const CAP_ICONS = { text: Type, image: ImageIcon, pdf: FileText, video: Video, audio: AudioLines } as const; + +function CapabilityIcons({ info, color }: { info: ModelInfo | null; color: string }) { + return ( + <> + {modelInputChips(info).map((c) => { + const CapIcon = CAP_ICONS[c.key as keyof typeof CAP_ICONS]; + return CapIcon ? <CapIcon key={c.key} size={10} color={color} /> : null; + })} + </> + ); +}Also applies to: 776-787
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/src/components/AiSettingsForm.tsx` around lines 507 - 518, Extract the capability-to-icon mapping used by renderModelRow and the collapsed trigger into a shared module-level constant. Replace both inline c.key conditional chains with lookups from that constant, preserving the existing icon rendering and null behavior for unsupported keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile/src/chat/ai-config.ts`:
- Around line 423-425: Update normCacheBaseUrl so normalization removes only
trailing slashes and preserves endpoint paths such as /v1, keeping root and /v1
model-discovery URLs in distinct cache entries; ensure the cache key or complete
model-list URL uses this preserved normalized path.
- Around line 454-474: The favorite storage keys are inconsistent between
getFavoriteModels/toggleFavoriteModel and the UI store's global favorites. Align
FAVORITE_MODELS_KEY and the corresponding mobile read/write flow with the
established "favoriteModels" key, or migrate existing "ai.favoriteModels" data
so current mobile favorites are preserved and both model-picker paths share one
set.
- Around line 429-446: Validate persisted entries in readModelsCache and
writeModelsCache before trusting or storing them: require models to be an array
of strings and ts to be a finite number, rejecting malformed entries by
returning null during reads and avoiding persistence during writes. Anchor the
validation around the ModelsCache entry accessed via normCacheBaseUrl, while
preserving the existing TTL behavior for valid entries.
In `@shared/models/model-catalog.ts`:
- Around line 71-112: Lowercase the model-id segment used by the
LOGO_BRAND_MATCHES loop in logoProviderFor, while preserving the existing token
lookup and fallback behavior. Ensure mixed-case brand prefixes such as Claude or
GPT match the same providers as their lowercase equivalents.
In `@src/components/chat/ChatInput.tsx`:
- Around line 302-310: The file picker and upload flow must enforce the selected
model’s media capabilities. In src/components/chat/ChatInput.tsx lines 302-310,
update the file input accept value to independently include image types only
when allowImages is true and PDF types only when allowPdf is true; in
src/components/chat/chat-panel/index.tsx lines 346-360, skip image files before
invoking FileReader when allowImages is false, while preserving PDF handling.
---
Outside diff comments:
In `@electron/ipc/chat.ts`:
- Line 266: Update electron/ipc/chat.ts lines 266-266 so runDispatchLoop returns
its dispatcher cost and that cost is merged into the main usage before
finalUsage() is called. Update electron/lib/chat-subagent-loop.ts lines 520-522
to pass the dispatcher cost accumulator to forceFinalAnswer, ensuring forced
synthesis costs are included in the final chat:done usage.
In `@mobile/src/chat/providers/openai.ts`:
- Around line 180-188: Validate the cost value before assigning it to costUsd in
the OpenAI streaming usage handling. Apply the guard typeof cost === "number",
Number.isFinite(cost), and cost >= 0, preserving valid 0 values and leaving
costUsd undefined for missing or invalid costs; use the existing costUsd and
chunk.usage handling symbols.
---
Nitpick comments:
In `@mobile/src/components/AiSettingsForm.tsx`:
- Around line 507-518: Extract the capability-to-icon mapping used by
renderModelRow and the collapsed trigger into a shared module-level constant.
Replace both inline c.key conditional chains with lookups from that constant,
preserving the existing icon rendering and null behavior for unsupported keys.
In `@src/components/chat/chat-panel/index.tsx`:
- Around line 361-380: Limit the fallback rasterization in the
attachment-processing flow around rasterizePdfToImages before pushing items into
pendingAttachments. Enforce a bounded total payload for generated page data,
preferably by reducing image dimensions or using JPEG output, and stop or skip
pages once the budget is reached while preserving existing PDF/image handling
and error behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4fe036cc-9510-4a5a-9056-af8b553bb3c0
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonsrc/lib/__fixtures__/markdown-parity-test.pdfis excluded by!**/*.pdf
📒 Files selected for processing (29)
changelogs/v2.5.20.mdelectron/ipc/chat.tselectron/lib/chat-subagent-loop.tselectron/lib/llm.tselectron/lib/tools.tsmobile/app/(tabs)/chat/index.tsxmobile/changelogs/v0.1.4.mdmobile/src/chat/ai-config.tsmobile/src/chat/models-dev.tsmobile/src/chat/providers/openai.tsmobile/src/components/AiSettingsForm.tsxmobile/src/components/ai-settings/styles.tspackage.jsonshared/chat/provider-credits.test.tsshared/chat/provider-credits.tsshared/models/model-catalog.test.tsshared/models/model-catalog.tsshared/models/pdf-attach.tssrc/components/chat/ChatInput.tsxsrc/components/chat/chat-panel/ChatMessageBubble.tsxsrc/components/chat/chat-panel/index.tsxsrc/components/ui/model-picker.tsxsrc/hooks/useChatStream.tssrc/lib/models-dev.tssrc/lib/pdf-document.test.tssrc/lib/pdf-document.tssrc/lib/pdf-rasterize.tssrc/types/index.tssrc/types/pdfjs.d.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/types/index.ts
- shared/chat/provider-credits.ts
- mobile/src/chat/models-dev.ts
- src/lib/models-dev.ts
- mobile/app/(tabs)/chat/index.tsx
- src/components/ui/model-picker.tsx
- mobile/changelogs/v0.1.4.md
- shared/models/pdf-attach.ts
- shared/chat/provider-credits.test.ts
…ints
Saved providers whose hostname maps to a direct vendor that models.dev serves
(OpenAI, Anthropic, Google, DeepSeek, OpenRouter, Together, Groq, Fireworks,
Neuralwatt, …) now render that vendor's models.dev logo when the community
catalog has no inline iconSvg for them — covering the 5 community providers
currently missing an icon, plus manually-added providers sharing those
endpoints.
Logo resolution priority for a saved provider:
1. community iconSvg (by communityId, then normalized baseUrl)
2. models.dev slug via curated hostname table (endpointLogoSlug)
3. generic Server glyph
- shared: ENDPOINT_LOGO_SLUGS hostname→slug table + endpointLogoSlug(baseUrl)
(parses the URL hostname, falls back to a bare-host regex). Tested.
- desktop ProviderManager.tsx: logoFor returns a discriminated
{iconSvg}|{url} union; renderProviderLogo draws ConnectorLogo or an <img>.
- mobile AiSettingsForm.tsx: builds a per-provider resolved logo map
(memoized) merging community iconSvg → endpoint slug; ProviderList renders
ConnectorLogo or ProviderLogo accordingly. Added ResolvedProviderLogo type.
changelogs: v2.5.20.md + mobile v0.1.4.md
Saved-provider chips and community-browse rows on both desktop and mobile now render models.dev provider SVGs through ConnectorLogo on the same fixed light chip background as community icons, instead of raw <img> (desktop) or theme-tinted glyphs (mobile). Adds getOrFetchLogoSvg to the mobile models-dev module (mirroring desktop's), with useSyncExternalStore subscriptions in ProviderManager, BrowseProvidersModal, ProviderList, and ProvidersSettingsScreen so rows pop in their brand mark once the lazy SVG fetch lands.
The selected provider's fields (Name, Base URL, API key, Model, Context window) are now hidden behind a read-only summary card until you tap Edit. Selecting another provider shows its summary; a freshly added provider opens straight in the editor. Declutters the screen and prevents accidental edits. Browse Providers is now hidden while the Apple Intelligence or Rork backend is selected, since community presets only apply to OpenAI- compatible providers; it reappears when you switch back to OpenAI.
- ChatInput: build file-input accept from allowImages/allowPdf independently; skip image files before FileReader when allowImages is false (no silent drop) - model-catalog: lowercase base segment before LOGO_BRAND_MATCHES so mixed-case ids (Claude/GPT/DeepSeek) resolve their brand mark; add tests - mobile ai-config: normCacheBaseUrl preserves /v1 (distinct cache entries); readModelsCache/writeModelsCache validate entry shape (array of strings + finite ts) before trusting persisted JSON - changelogs (desktop v2.5.20, mobile v0.1.4)
What does this PR do?
Browse Providers (mobile) — a new Browse Providers screen (Settings → AI) installs preset OpenAI-compatible providers from the cairn-community
providers.jsoncatalog with one tap — the user just enters their API key (stored in the device keychain). Mirrors the desktop feature shipped in v2.5.19.Provider credit balances + per-call cost (desktop & mobile) — remaining-credit badges in AI settings now resolve each provider's balance endpoint from the community manifest's
creditsdescriptor, not just OpenRouter's/v1/key. DeepSeek's/user/balance, Neuralwatt's/v1/quota, and OpenRouter's/keyall feed the badge. The parsing/resolution logic lives once inshared/chat/provider-credits.ts, used by both the desktop IPC handler (ai:fetchKeyInfo) and mobile'sgetKeyInfo. And when a provider reports a USD cost per call (usage.cost, e.g. Neuralwatt), both context rings show a Cost row alongside the token breakdown (mobile persists it inchat.lastUsage).Type of change
Screenshots / recording
Checklist
npx tsc --noEmitinmobile/, exit 0)npm run type-check:all)vitest run --project node)mobile/changelogs/v0.1.4.md,changelogs/v2.5.20.md)t.accent,t.textPrimary, etc.)expo-secure-storeonly (never SQLite, never synced)@cairn/shared/chat/registry-schema) — no duplicated typesNotes for reviewer
providers.json(newmobile/src/chat/providers-registry.ts), parallel toregistry.tsfor tools/commands — same fetch/cache/ETag shape, different URL + meta-DB keys. Hard-refresh cache-busts to dodge stale CDN 304s, matching the desktop fix.SavedProvidergainedsource/communityId(round-trip through the existingreadProviderstype-guard unchanged).installCommunityProviderdedups bycommunityId/name, reuses the row id so the keychain key survives a re-install, and — unlikeaddSavedProvider— does not auto-select the provider (matches desktop). The name fallback only matches community-sourced rows, so a manual provider with a colliding name is never hijacked.AiSettingsForm.tsx; auseFocusEffectrefreshes the provider list on return so a newly-added provider shows in the switcher (without clobbering in-progress edits to the active provider).Alert.prompt(secure-text); Android uses the in-app modal, copying the pattern fromsettings/tools.tsx.resolveCreditSpecmatches the configured endpoint against the manifest'screditsdescriptor (url+shape), thenparseCreditsinterprets the response (deepseek / openai-grants / neuralwatt / openrouter). Shared inshared/chat/provider-credits.ts; desktop probes viaprobeCredits(live-checked inelectron/lib/provider-credits.live-check.test.tsagainst real endpoints), mobile probes viaexpo/fetch. Unknown shapes fall back to the OpenRouter/v1/keyprobe and degrade to a hidden badge.usage.costflows throughchat:usage/chat:done→useChatStream→ContextRing(agent + subagent blocks). Mobile: captured in the OpenAI streamer, persisted inchat.lastUsage, shown in the context-ring popover..expo/types/router.d.tstyped-route for/settings/providersis regenerated by expo (git-ignored), so it's not in this diff.main; the credits descriptors for deepseek/neuralwatt/openrouter ship with it.Summary by CodeRabbit
New Features
gpt-5.6-luna.Bug Fixes