Skip to content

feat: add Ollama zero-setup usage provider (ollama.local)#27

Merged
ElbertePlinio merged 2 commits into
mainfrom
feat/ollama-local-provider
Jul 9, 2026
Merged

feat: add Ollama zero-setup usage provider (ollama.local)#27
ElbertePlinio merged 2 commits into
mainfrom
feat/ollama-local-provider

Conversation

@ElbertePlinio

Copy link
Copy Markdown
Member

Adds the ollama.local zero-setup provider: PickGauge asks the running local Ollama daemon (POST http://localhost:11434/api/me) for the signed-in account's plan and shows a plan-only card — no cookies, no browser, nothing leaves the machine.

Refs #26 (PR 2 of the 4-PR plan; PR 1 = Grok zero-setup is in flight in parallel, PR 3 web full-gauges and PR 4 headless export remain).

What changed

  • New src-tauri/src/ollama_provider.rs collector; UsageProviderId::OllamaLocal (ollama.local); registered when Ollama + local readings are enabled.
  • Plan-only snapshot contract: null percentages, confidence: medium, details.plan only — identity fields (email/name/id/avatar) are dropped, never stored or logged.
  • Graceful states: daemon not running → not_configured ("Ollama isn't running"), signed out → login_required.
  • Default enabledServices.ollama flips to true for fresh configs; existing saved configs keep their value.
  • Dashboard plan-only card + "Local daemon" source label; Settings copy updated; README privacy section extended.

Tested

  • cargo test --locked --all-targets: 232 passed, 1 ignored (live-daemon test — run separately, passed against the real daemon, plan pro).
  • bun run check clean; bun run test: 61 + 15 sidecar passed.

Not tested yet

  • Tray behavior over a long session with the daemon stopping/starting (covered by unit-level error mapping only).

Risks

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bb66883bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +49 to +51
let client = reqwest::blocking::Client::builder()
.timeout(HTTP_TIMEOUT)
.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep redirected Ollama probes local-only

When anything listening on localhost:11434 returns a 30x (for example a non-Ollama service on that port, or a daemon response that redirects unauthenticated users), the default reqwest client follows the Location before this code checks the status. That lets the supposedly local-only plan probe make an outbound request to an arbitrary URL, which breaks the privacy guarantee that the Ollama read stays on the machine; disable redirects for this client and treat 30x as an error instead.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/config.rs
codex: true,
claude: true,
ollama: false,
ollama: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve old configs' disabled Ollama default

For existing pre-v6/partial config files that do not contain enabledServices.ollama, parse_config_raw fills missing fields from AppConfig::default(), so this new default migrates those saved configs to ollama: true. With localEnabled also defaulting true, upgrading users who never opted into Ollama will start seeing the Ollama card and periodic localhost:11434 probes; add an explicit migration/default for missing legacy ollama to keep it false if only fresh configs should opt in.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/usage.rs
Comment on lines +1356 to +1358
if config.providers.local_enabled {
providers.push(Box::new(OllamaLocalProvider::new()));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid downgrading successful Ollama web readings

When Ollama web readings are enabled and the local daemon also returns a plan, registering this local provider makes the existing merge path treat the plan-only snapshot as a local delta source. Because it has no deltaBaselineAt/deltaUnit, a successful official web snapshot is cloned with lowered confidence and mergeStatus: local_delta_unavailable, even though the web usage data is valid; exclude Ollama plan-only snapshots from the web+local delta merge path or do not register both as mergeable providers.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/ollama_provider.rs Outdated

fn map_status_error(status: reqwest::StatusCode) -> UsageProviderError {
match status.as_u16() {
401 | 403 => UsageProviderError::LoginRequired,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not route daemon sign-in to the web profile login

When the local Ollama daemon returns 401/403, this login_required status feeds the generic loginPromptVisible / startProviderLogin flow, which launches PickGauge's app-owned browser profile for ollama.com rather than signing the user's local daemon in. A signed-out daemon will therefore keep returning 401 after the user clicks the dashboard's Start login button; use a daemon-specific status/action or suppress the generic browser login prompt for ollama.local.

Useful? React with 👍 / 👎.

Service, UsageConfidence, UsageProvider, UsageProviderError, UsageProviderId, UsageSnapshot,
UsageSource,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor non-default local Ollama hosts

When Ollama is configured to listen on a non-default local address or port (for example via OLLAMA_HOST), the CLI and other clients use that configured daemon, but this provider always probes localhost:11434. Those users will get an Ollama isn't running snapshot even though their local daemon is healthy; read the configured local host (while rejecting non-loopback values if the provider must stay local-only) or expose a setting instead of hard-coding the default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid edge case, deliberately deferred: this slice keeps the zero-setup probe pinned to the loopback default; non-default OLLAMA_HOST setups get an honest "isn't running" state rather than wrong data. Tracked in #29 (with the loopback-only guard you suggest).

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7c47fdd58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +49 to +52
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(HTTP_TIMEOUT)
.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable proxies for daemon reads

When HTTP_PROXY/ALL_PROXY is set and the user has not excluded localhost with NO_PROXY, reqwest's default client will route this http://localhost:11434/api/me call through that proxy. For the new Ollama provider that is supposed to be a local-only daemon read, this can either send the request off-machine or make plan detection fail behind corporate proxies; build this client with proxies disabled, e.g. no_proxy(), before sending the daemon request.

Useful? React with 👍 / 👎.

Comment on lines +70 to +72
fn map_request_error(error: reqwest::Error) -> UsageProviderError {
if error.is_connect() || error.is_timeout() {
UsageProviderError::NotConfigured

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid backing off a stopped daemon

When Ollama is not running, this returns NotConfigured, but the generic refresh path records every provider error into the exponential backoff state; after a few daemon-down refreshes, even a user-triggered Refresh Now is skipped by try_begin_refresh until retry_after expires. For users who start Ollama after PickGauge has already observed it stopped, the card can keep saying “Ollama isn't running” for up to 15 minutes, so this local daemon state needs to avoid failure backoff or allow manual refresh to bypass it.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/usage.rs
}

if let Some(local) = local {
if let Some(local) = local.filter(|snapshot| !is_plan_only_local_snapshot(snapshot)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve daemon plan when web login fails

This filter only excludes plan-only local snapshots after the web snapshot has real percentages; in the earlier !web_has_usage_percent branch, an Ollama web login_required/missing_data snapshot still falls back to the daemon plan via local.or(fake), lowers its confidence, and adds webStatus, causing the valid local plan card to render with a spurious “Login required” note when web readings are enabled but the browser profile is not signed in. Apply the plan-only handling before the unavailable-web fallback as well.

Useful? React with 👍 / 👎.

UsageSource,
};

const DEFAULT_BASE_URL: &str = "http://localhost:11434";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect configured Ollama hosts

This hard-codes only the default daemon address, so users who run Ollama on a different local port or bind address via OLLAMA_HOST will get not_configured even though the CLI and local API are working. For those setups, the zero-setup provider never finds the daemon; derive the endpoint from the same Ollama host configuration, normalizing local bind addresses as needed, before falling back to this default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Duplicate of the round-1 finding on the same line — deliberately deferred and tracked in #29 (with the loopback-only guard). This slice stays pinned to the zero-setup default.

Comment thread src-tauri/src/ollama_provider.rs Outdated
Comment on lines +63 to +65
let body: OllamaMeResponse = response
.json()
.map_err(|_| UsageProviderError::ParseFailed)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat empty /api/me bodies as signed out

If the daemon responds successfully but with no account payload, such as null or an omitted plan while the local install is not signed in, this deserialization path turns it into parse_failed. That shows “Usage data could not be parsed” instead of the intended Ollama sign-in guidance and also makes the provider look broken; handle an empty/missing plan response as login_required before treating malformed JSON as a parser drift.

Useful? React with 👍 / 👎.

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b8d5d42c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/views/Settings.svelte Outdated
Comment on lines +302 to +305
{loggingIn === "ollama" ? "Opening…" : "Sign in to Ollama"}
</button>
<p class="hint">
Reads the session and weekly limits from your signed-in ollama.com page — no API key,
nothing leaves this machine except the page load.
The local daemon reports only your plan. Usage-limit readings are not available yet, and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate the Ollama sign-in button on web readings

With the new local-daemon copy/default, this Ollama section is visible on fresh configs while providers.webEnabled is still false. The button immediately above still calls startProviderLogin("ollama"), and that backend only prepares a managed browser profile when web readings are enabled, so clicking “Sign in to Ollama” takes the managed-login-unavailable path and tells the user to enable web readings instead of the required ollama signin. Hide/disable this browser sign-in affordance unless web readings are enabled, or replace it with CLI guidance for the local daemon.

Useful? React with 👍 / 👎.

Comment thread src/lib/views/Dashboard.svelte Outdated
}

function sourceLabel(snapshot: UsageSnapshot) {
return snapshot.details.via === "daemon" ? "Local daemon" : sourceLabels[snapshot.source];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Label Ollama daemon failures as daemon checks

When the local Ollama provider fails (for example, daemon stopped or signed out), the error snapshot still has providerId: "ollama.local" but does not include details.via = "daemon", so this helper falls back to sourceLabels.local and displays those new daemon states as “Local estimate.” That is misleading for the not_configured/login_required scenarios because no local estimate was produced; key this label off the Ollama local provider as well, or add the daemon marker to its error snapshots.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/ollama_provider.rs Outdated
Comment on lines +72 to +73
if error.is_connect() || error.is_timeout() {
UsageProviderError::NotConfigured

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Report hung Ollama daemon requests as timeouts

When the Ollama daemon accepts the connection but does not answer within the 3s client timeout, reqwest reports is_timeout() and this branch maps it to NotConfigured. The frontend special-cases that status as “Ollama isn't running,” which sends users down the wrong path for a running but hung or overloaded daemon; return TimedOut for timeout errors and reserve NotConfigured for connection failures.

Useful? React with 👍 / 👎.

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de5c7af116

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fn map_status_error(status: reqwest::StatusCode) -> UsageProviderError {
match status.as_u16() {
401 | 403 => UsageProviderError::LoginRequired,
_ => UsageProviderError::ParseFailed,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map Ollama account outages as unavailable

When a signed-in daemon cannot verify the account or plan, Ollama's /api/me handler returns a 503 account unavailable response rather than malformed JSON; this catch-all turns that into parse_failed, so users see “Usage data could not be parsed” during offline/provider-outage cases instead of an unavailable/network status. Please map 5xx responses, at least 503, to NetworkUnavailable or another unavailable state instead of ParseFailed.

Useful? React with 👍 / 👎.

Comment thread README.md Outdated
- **Usage requests stay provider-only.** To compute real remaining quota, PickGauge calls the same official endpoints the CLIs use — `chatgpt.com/backend-api/codex/usage` and `api.anthropic.com/api/oauth/usage`, plus the providers' OAuth refresh endpoints. No usage measurements, project files, account data, tokens, or provider responses are sent to Pickforge or third-party analytics services.
- **Anonymous crash reports.** Crash and error reporting is on by default in release builds and can be turned off in Settings → Crash reports. Reports go to Sentry with crash stack traces, OS version, and app version. Native crash dumps include a snapshot of process memory, which may contain fragments of recent in-memory data. Reports never intentionally include usage measurements, project files, or personal data; the hostname is stripped. Crash reporting is disabled in development builds unless `PICKGAUGE_SENTRY_DEBUG=1` is set.
- **Web reads are opt-in and isolated.** Browser-based reading of the official usage pages is disabled by default. When enabled, it runs only in dedicated, app-owned browser profiles (under `com.pickforge.pickgauge`) that you log into yourself — never your personal browser, never a shared cookie jar.
- **Ollama plan reads stay local.** When Ollama is enabled, PickGauge sends a read-only request to the local daemon at `http://localhost:11434/api/me`. It retains only the returned plan name; account IDs, email addresses, names, and avatars are never stored, logged, or included in snapshots.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clarify that Ollama plan checks can leave the machine

This privacy note says the Ollama plan read “stay[s] local”, but the local daemon's /api/me implementation calls https://ollama.com/api/me to resolve the signed-in user and plan, so enabling this default provider can still trigger provider-network traffic via the daemon. That mismatch is important for privacy/offline expectations; please reword this to say PickGauge only talks to the local daemon but the daemon may contact Ollama, or avoid the remote-backed check unless the user opts in.

Useful? React with 👍 / 👎.

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@ElbertePlinio ElbertePlinio force-pushed the feat/ollama-local-provider branch from de5c7af to c563f70 Compare July 9, 2026 22:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c563f7088f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

UsageSource,
};

const DEFAULT_BASE_URL: &str = "http://localhost:11434";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect configured Ollama hosts

When a user runs Ollama with the documented OLLAMA_HOST setting pointed at a non-default local address or port (for example Docker/WSL or 127.0.0.1:11500), the Ollama CLI can be working while this provider still always posts to localhost:11434; those installs will be reported as not_configured/“Ollama isn't running” and the new zero-setup card never works. Resolve the daemon base URL from OLLAMA_HOST or app config before falling back to the default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Third occurrence of this finding — deliberately deferred, tracked in #29 (loopback-only OLLAMA_HOST resolution). The failure mode for non-default hosts is an honest "isn't running" state, never wrong data.

@@ -319,12 +325,12 @@
</span>
</header>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Handle Grok errors without an empty gauge area

When the Grok CLI auth is missing or expired, the backend emits a Grok snapshot with no details.plan and no percentages. This new condition falls through to the gauge branch, but windowsFor intentionally returns no rows for Grok, so the card shows a blank gauge area instead of the previous “Plan unavailable” row plus the login note. Treat Grok no-plan snapshots as a plan-unavailable state or render an explicit unavailable row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b78577b: planOnly() now falls back to "Plan unavailable" for Grok snapshots without a plan, restoring the pre-rebase card (plan row + login note) instead of a blank gauge area.

Comment thread README.md Outdated
- **Usage requests stay provider-only.** To compute real remaining quota or detect a plan, PickGauge calls the same official endpoints the CLIs use — `chatgpt.com/backend-api/codex/usage`, `api.anthropic.com/api/oauth/usage`, and one authenticated `GET grok.com/rest/subscriptions`. Grok never makes an OAuth refresh request. No usage measurements, project files, account data, tokens, or provider responses are sent to Pickforge or third-party analytics services.
- **Anonymous crash reports.** Crash and error reporting is on by default in release builds and can be turned off in Settings → Crash reports. Reports go to Sentry with crash stack traces, OS version, and app version. Native crash dumps include a snapshot of process memory, which may contain fragments of recent in-memory data. Reports never intentionally include usage measurements, project files, or personal data; the hostname is stripped. Crash reporting is disabled in development builds unless `PICKGAUGE_SENTRY_DEBUG=1` is set.
- **Web reads are opt-in and isolated.** Browser-based reading of the official usage pages is disabled by default. When enabled, it runs only in dedicated, app-owned browser profiles (under `com.pickforge.pickgauge`) that you log into yourself — never your personal browser, never a shared cookie jar.
- **Ollama plan reads use your local daemon.** When Ollama is enabled, PickGauge sends a read-only request only to the local daemon at `http://localhost:11434/api/me` and never contacts ollama.com itself. The daemon resolves the account with ollama.com using its own credentials. PickGauge retains only the returned plan name; account IDs, email addresses, names, and avatars are never stored, logged, or included in snapshots.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clarify Ollama web reads in the privacy note

With Official web readings enabled, the refresh path still calls the Ollama web provider (refresh_web_provider_headless(..., Service::Ollama) in src-tauri/src/lib.rs), so PickGauge can contact ollama.com via the sidecar even when this new paragraph says it “never contacts ollama.com itself.” This privacy guarantee should be scoped to the local-daemon provider or the web path should be disabled for Ollama.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b78577b: the privacy bullet now scopes the no-ollama.com claim to the zero-setup daemon read and explicitly notes that the separate opt-in web readings feature does load ollama.com in the isolated profile.

@ElbertePlinio ElbertePlinio merged commit f23638d into main Jul 9, 2026
1 check passed
@ElbertePlinio ElbertePlinio deleted the feat/ollama-local-provider branch July 9, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant