Skip to content

feat(ai-panel): per-workload temperature + endpoint URL UX for local runtimes#2165

Merged
senamakel merged 13 commits into
tinyhumansai:mainfrom
senamakel:fix/local-models
May 19, 2026
Merged

feat(ai-panel): per-workload temperature + endpoint URL UX for local runtimes#2165
senamakel merged 13 commits into
tinyhumansai:mainfrom
senamakel:fix/local-models

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 19, 2026

Summary

  • Add a per-workload temperature override (0..2, optional) in the Custom routing dialog so each workload (reasoning/agentic/coding/memory/…) can pin its own temperature.
  • Replace the misleading "API key" hack for Ollama / LM Studio with a proper Endpoint URL field (defaults to localhost, validates http(s)). For Ollama, also persists local_ai.base_url so the Rust factory actually routes chat to the chosen host.
  • Extend the provider-string wire grammar to <slug>:<model>[@<temp>] (both UI parse/serialize and Rust factory).
  • Strip the unsupported {label} interpolation token from settings.ai.connectProvider (was rendering as "Connect {label} OpenAI").

Problem

The LLM settings page had three gaps that came up while wiring custom models:

  1. The Ollama / LM Studio chip toggle reused the "API key" field as an endpoint URL — confusing UX, no validation, no localhost default, and the value was never written to local_ai.base_url so the Rust factory kept routing to http://localhost:11434 regardless of what the user typed.
  2. There was no per-workload temperature knob — only a single global default_temperature. Different workloads (reasoning vs. memory summarisation vs. coding) want different temperatures.
  3. The dialog title rendered as "Connect {label} OpenAI" because the en string contained a placeholder that the simple t() doesn't interpolate.

Solution

Frontend (app/src/services/api/aiSettingsApi.ts, app/src/components/settings/panels/AIPanel.tsx)

  • ProviderRef gains optional temperature?: number | null for cloud/local kinds.
  • New splitModelAndTemp / joinModelAndTemp helpers handle <model>[@<temp>] parse/serialize round-trips. parseProviderString only emits temperature when set, preserving existing test snapshots.
  • ProviderKeyDialog now takes an isLocalRuntime prop. For local runtimes it renders a proper URL input with defaultEndpointFor(slug) defaults (http://localhost:11434/v1, http://localhost:1234/v1) and rejects non-http(s) values.
  • The parent toggle handler persists Ollama's local_ai.base_url via openhumanUpdateLocalAiSettings (stripping a trailing /v1 since make_ollama_provider appends it) and flips runtime_enabled / opt_in_confirmed in tandem so chat actually uses the chosen host.
  • CustomRoutingDialog adds a checkbox-gated slider + numeric input for temperature. diffSummary surfaces temperature changes in the unsaved-changes bar.

Rust (src/openhuman/inference/provider/)

  • factory.rs parses @<temp> off both ollama: and <slug>: strings via split_model_and_temperature. The clean model id is what reaches the upstream API; the temperature is attached to the constructed provider via a builder method. Malformed @<garbage> is treated as part of the model id rather than silently dropped.
  • compatible.rs adds temperature_override: Option<f64> and with_temperature_override(..). effective_temperature() honours the override after the existing temperature_unsupported_models glob filter.

i18n

  • settings.ai.connectProvider: Connect {label}Connect (concatenation now produces a sane title).

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • N/A: pnpm test:coverage / pnpm test:rust not run locally; CI coverage gate will enforce. Targeted Vitest suites (aiSettingsApi, AIPanel) and Rust factory tests all pass.
  • N/A: behaviour-only change to existing AI settings panel; no new feature row in the coverage matrix.
  • N/A: no feature-matrix IDs touched.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: AI settings is not on the release-cut smoke checklist surface (no native windowing / install / launch change).
  • N/A: no linked issue.

Impact

  • Desktop only (renderer + Tauri core). No mobile, no CLI changes.
  • Wire format change is backwards-compatible: existing <slug>:<model> strings parse identically; @<temp> is opt-in per workload.
  • No new external network dependencies; /models discovery already exists and is unchanged.
  • Security: base_url is user-supplied, validated http(s)://; secrets remain in auth-profiles.json (no change).

Related

  • Closes:
  • Follow-up PR(s)/TODOs:
    • Remote-Ollama support in the model dropdown: CustomRoutingDialog local branch still uses the installed-Ollama-models list; switching it to listProviderModels('ollama') would let a remote Ollama host populate the dropdown.

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

Linear Issue

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

Commit & Branch

  • Branch: fix/local-models
  • Commit SHA: 31fab6b85284339f51ff98bcff879ae1969a9e48

Validation Run

  • pnpm --filter openhuman-app format:check (ran Prettier write on changed files)
  • pnpm typecheck
  • Focused tests: pnpm debug unit aiSettingsApi (37/37), pnpm debug unit AIPanel (11/11), cargo test … temperature (19/19 incl. 2 new factory tests)
  • Rust fmt/check: cargo check --manifest-path Cargo.toml --bin openhuman-core (only pre-existing warnings)
  • N/A: Tauri shell crate not touched.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: per-workload temperature override; local runtime endpoint URL is now a proper field and actually applied via local_ai.base_url.
  • User-visible effect: AI settings → LLM dialog shows a URL field for Ollama/LM Studio and a temperature slider in Custom routing.

Parity Contract

  • Legacy behavior preserved: provider strings without @<temp> parse and route identically to before.
  • Guard/fallback/dispatch parity checks: malformed temperature suffix degrades gracefully (model kept verbatim); temperature_unsupported_models glob filter still applied after the override.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution: N/A

Summary by CodeRabbit

  • New Features
    • Per-workload temperature overrides: checkbox + bounded slider/number, validation, shown in unsaved-change summaries and routing strings.
    • Provider connect UX split: cloud API-key vs local Endpoint URL with scheme/URL validation; local endpoints normalized/persisted and certain local runtimes (e.g., ollama, lmstudio) get sensible default endpoints and auto-configuration.
  • Tests
    • Unit tests for provider-string parsing and routing with @ suffix.
  • Chore
    • Minor i18n text update for the provider connect label.

Review Change Stack

…or local runtimes

- Extend provider-string grammar to `<slug>:<model>[@<temp>]`; the Rust
  factory parses and strips the suffix, attaching it as a temperature
  override on the built OpenAI-compatible provider so the model id sent
  upstream stays clean.
- Add a temperature slider (0..2, optional) to the Custom routing dialog so
  each workload can pin its own temperature.
- Replace the API-key hack in the Ollama/LM Studio connect dialog with a
  proper Endpoint URL field, with localhost defaults and http(s) validation.
  Ollama also persists `local_ai.base_url` (and flips the runtime/opt-in
  flags) so the Rust factory routes chat to the chosen host.
- Fix the "Connect {label} OpenAI" dialog title by dropping the unsupported
  interpolation token from the en string.
@senamakel senamakel requested a review from a team May 19, 2026 01:03
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 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
📝 Walkthrough

Walkthrough

Adds per-workload temperature override support encoded as @<temp> in provider strings, threads temperature through parsing, serialization, factory construction, Rust providers, and UI; refactors provider connect dialog to support cloud API keys vs local runtime endpoints with persistence.

Changes

Temperature Override and Provider Connection

Layer / File(s) Summary
ProviderRef schema and temperature contract
app/src/services/api/aiSettingsApi.ts, app/src/components/settings/panels/AIPanel.tsx
ProviderRef extended to carry optional temperature for cloud/local variants. Helpers added (splitModelAndTemp, joinModelAndTemp) and parse/serialize updated to round-trip numeric temperature suffixes.
Provider connection dialog: cloud API-key vs local endpoint
app/src/components/settings/panels/AIPanel.tsx, app/src/lib/i18n/en.ts
ProviderKeyDialog refactored to dual-mode for cloud API-key or local endpoint URL with validation; local endpoints persisted to provider settings and Ollama base URL persisted via openhumanUpdateLocalAiSettings (stripping trailing /v1). Default endpoints added for Ollama and LMStudio; i18n Connect label adjusted.
Custom routing dialog: temperature override controls
app/src/components/settings/panels/AIPanel.tsx
CustomRoutingDialog adds optional per-workload temperature override state (checkbox + range/number inputs for 0–2) and includes temperature in saved ProviderRef and unsaved-change summaries.
Rust provider temperature override implementation
src/openhuman/inference/provider/compatible.rs
OpenAiCompatibleProvider adds temperature_override and with_temperature_override(); effective temperature prefers override when present while honoring models that don't support temperature.
Factory parsing and provider construction with temperature
src/openhuman/inference/provider/factory.rs, src/openhuman/inference/provider/factory_test.rs
Factory parsing recognizes optional @<temp> suffix for ollama and slug forms via split_model_and_temperature and threads temperature_override into cloud and Ollama provider construction. Tests added for numeric/non-numeric suffix handling and workload routing updates.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AIPanel_CustomRouting
  participant aiSettingsApi
  participant ProviderFactory
  participant OpenAiCompatibleProvider
  User->>AIPanel_CustomRouting: Enable temp override / save (`@0.2`)
  AIPanel_CustomRouting->>aiSettingsApi: serializeProviderRef("ollama:model@0.2")
  aiSettingsApi->>ProviderFactory: create provider with temperature_override=0.2
  ProviderFactory->>OpenAiCompatibleProvider: with_temperature_override(0.2)
  OpenAiCompatibleProvider->>OpenAiCompatibleProvider: use override for requests
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2152: Related changes to AI settings routing UI and cloud-provider persistence/model resolution affecting AIPanel and provider lookup flows.

Poem

🐰 I hopped through strings with a tiny hat,
Tagging models with warmth—an @ and that,
Keys and endpoints now split and clear,
Per-workload temps snug and near,
A tiny rabbit cheers—hop, deploy, yay!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: per-workload temperature override feature and endpoint URL UX for local runtimes (Ollama/LM Studio).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label May 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/components/settings/panels/AIPanel.tsx (1)

2078-2090: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear kind: 'local' routes when disconnecting Ollama.

This branch only resets cloud refs keyed by the local slug. Persisted Ollama routes round-trip back as { kind: 'local' }, so after a reload the UI can say “Disconnected” while workloads still point at Ollama. Reset ref.kind === 'local' here as well when localKind === 'ollama'.

Suggested fix
                           const nextRouting = Object.fromEntries(
                             Object.entries(draft.routing).map(([wid, ref]) => [
                               wid,
-                              ref.kind === 'cloud' && ref.providerSlug === localKind
+                              (ref.kind === 'cloud' && ref.providerSlug === localKind) ||
+                              (localKind === 'ollama' && ref.kind === 'local')
                                 ? ({ kind: 'openhuman' } as const)
                                 : ref,
                             ])
                           ) as typeof draft.routing;
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 2078 - 2090,
When removing a cloud provider (inside the branch that checks enabled &&
existing), the routing mapping created in nextRouting should also clear refs
that became { kind: 'local' } for Ollama; update the mapping in the
Object.entries(draft.routing).map callback used to build nextRouting so the
replacement condition is true when either (ref.kind === 'cloud' &&
ref.providerSlug === localKind) OR (localKind === 'ollama' && ref.kind ===
'local'), and then set those entries to ({ kind: 'openhuman' } as const); keep
the rest of the logic and the setDraft call as-is.
app/src/services/api/aiSettingsApi.ts (1)

134-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reserve ollama from the cloud route grammar.

serializeProviderRef({ kind: 'cloud', providerSlug: 'ollama', model }) and serializeProviderRef({ kind: 'local', model }) both emit ollama:..., but parseProviderString always reads that back as kind: 'local'. That makes the contract non-round-trippable and changes behavior after save/reload. Either reject providerSlug === 'ollama' on the cloud path or give local routes a distinct wire prefix.

🤖 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 `@app/src/services/api/aiSettingsApi.ts` around lines 134 - 167, The wire token
"ollama:" must be reserved for local providers to preserve round-trip parity:
update parseProviderString to keep treating any "ollama:..." input as local (no
change) and add validation in serializeProviderRef (and any code that constructs
cloud ProviderRef) to reject or disallow a cloud provider with providerSlug ===
'ollama' (e.g., throw an error or convert callers to use kind: 'local'), so
serializeProviderRef never emits `ollama:` for a cloud ref; modify functions
parseProviderString and serializeProviderRef and ensure ProviderRef construction
is validated so cloud refs with providerSlug 'ollama' are not allowed.
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 2296-2315: The current catch for openhumanUpdateLocalAiSettings
silently logs and continues, leaving the UI marked connected while Ollama will
still use the old local_ai.base_url; change this to abort the connect flow
instead: in the block where isLocalRuntime && slug === 'ollama' and you compute
baseUrl, stop swallowing the error in the catch for
openhumanUpdateLocalAiSettings (refer to openhumanUpdateLocalAiSettings,
baseUrl, and the Ollama branch check) — surface the failure by rethrowing the
error (or return a rejected Promise) and ensure the outer handler that adds the
provider/dialog close respects that and does not add the provider or close the
dialog when the update fails so the UI remains unconnected.

---

Outside diff comments:
In `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 2078-2090: When removing a cloud provider (inside the branch that
checks enabled && existing), the routing mapping created in nextRouting should
also clear refs that became { kind: 'local' } for Ollama; update the mapping in
the Object.entries(draft.routing).map callback used to build nextRouting so the
replacement condition is true when either (ref.kind === 'cloud' &&
ref.providerSlug === localKind) OR (localKind === 'ollama' && ref.kind ===
'local'), and then set those entries to ({ kind: 'openhuman' } as const); keep
the rest of the logic and the setDraft call as-is.

In `@app/src/services/api/aiSettingsApi.ts`:
- Around line 134-167: The wire token "ollama:" must be reserved for local
providers to preserve round-trip parity: update parseProviderString to keep
treating any "ollama:..." input as local (no change) and add validation in
serializeProviderRef (and any code that constructs cloud ProviderRef) to reject
or disallow a cloud provider with providerSlug === 'ollama' (e.g., throw an
error or convert callers to use kind: 'local'), so serializeProviderRef never
emits `ollama:` for a cloud ref; modify functions parseProviderString and
serializeProviderRef and ensure ProviderRef construction is validated so cloud
refs with providerSlug 'ollama' are not allowed.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d7be75c-9c51-44ed-8f9c-2fc199beb416

📥 Commits

Reviewing files that changed from the base of the PR and between a719d78 and 31fab6b.

📒 Files selected for processing (6)
  • app/src/components/settings/panels/AIPanel.tsx
  • app/src/lib/i18n/en.ts
  • app/src/services/api/aiSettingsApi.ts
  • src/openhuman/inference/provider/compatible.rs
  • src/openhuman/inference/provider/factory.rs
  • src/openhuman/inference/provider/factory_test.rs

Comment thread app/src/components/settings/panels/AIPanel.tsx
senamakel added 3 commits May 18, 2026 18:21
`chat_provider` was added to the config schema and UI in tinyhumansai#2152 but the
chat-factory's `provider_for_role` was never extended with a matching
arm, so the `chat` role fell through to `None` and every chat message
silently routed to the OpenHuman backend regardless of the user's
configured provider (e.g. `openai:gpt-4`).

Verified via `inference-probe --mode raw --role chat`:

  before: create_chat_provider role=chat resolved_string=openhuman
  after:  create_chat_provider role=chat resolved_string=openai:gpt-4
          outbound chat/completions -> https://api.openai.com/v1/chat/completions

Add `chat_workload_override_respected` and extend
`all_workloads_default_to_openhuman` so the arm can't drop out again.
Swallowing the openhumanUpdateLocalAiSettings error left the UI marked
"connected" while chat would keep using the default localhost host —
because the Rust factory's Ollama branch reads `local_ai.base_url`, not
`cloud_providers[].endpoint`. Let the error propagate to
ProviderKeyDialog.handleSave so the user sees the failure and the dialog
stays open instead.

Caught in CodeRabbit review on tinyhumansai#2165.
…works

When the user toggled Ollama on, the chip handler added an `ollama` entry
to React's draft `cloud_providers`, but the eager-flush effect filtered
it out and the Rust `is_slug_reserved` check would have rejected it
anyway. Result: opening the Custom routing dialog and selecting Ollama
hit `list_configured_models` which returned

  "no cloud provider with id or slug 'ollama' found"

`ollama` doesn't need to stay reserved — the factory's chat routing
short-circuits via the `ollama:<model>` prefix branch before any
`cloud_providers` lookup happens, so a synthetic `ollama` entry is
benign for chat dispatch and unblocks the model-list dropdown.

Drop `ollama` from both `is_slug_reserved` (Rust) and the frontend
eager-flush filter, and add regression tests for the reserved-slug set.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/components/settings/panels/AIPanel.tsx (1)

466-595: 🏗️ Heavy lift

Extract the new dialogs out of AIPanel.

This PR adds more self-contained modal state, validation, and submit flow logic to a file that is already ~2.5k lines long. Pulling ProviderKeyDialog and CustomRoutingDialog into dedicated modules would make this panel much easier to navigate and test.

As per coding guidelines: **/*.{js,ts,tsx,jsx}: Prefer files ≤ ~500 lines per source file; split modules when growing to maintain readability and single responsibility.

Also applies to: 1551-1869, 2260-2325

🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 466 - 595, The
file AIPanel.tsx contains large inline modal components; extract
ProviderKeyDialog (and similarly CustomRoutingDialog) into their own files to
reduce file size and improve testability: create ProviderKeyDialog.tsx exporting
the ProviderKeyDialog component (keeping props shape, useT hook, state/handlers
like handleSave, placeholder logic, and classNames intact), update AIPanel.tsx
to import ProviderKeyDialog and remove the in-file definition, and do the same
for CustomRoutingDialog (preserve prop types and any referenced helpers like
defaultEndpointFor); ensure exports/imports paths are updated and run type
checks.
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 2277-2283: When persisting a local-runtime endpoint (where
isLocalRuntime is true) normalize and validate the URL instead of storing the
raw trimmed value: parse trimmed with new URL(...), throw/reject on parse
errors, and ensure the pathname is not empty or just "/" by appending "/v1" when
needed; then set upserted.endpoint to the normalized URL.toString(). Update the
endpoint assignment in the upsert logic that constructs the CloudProvider (the
block that creates the upserted object using id `p_${slug}_...`, slug, label,
endpoint, authStyle) and apply the same normalization in the other similar
upsert locations noted in the comment (the other blocks that construct
upserted/cloudProviders entries).
- Around line 1819-1841: The temperature slider and number input in AIPanel are
missing accessible names; update the inputs that use the temperature state and
setTemperature handler to include explicit accessible names by either adding
aria-label attributes (e.g., aria-label="Temperature" and
aria-label="Temperature value") or by linking each input to a
visible/visually-hidden <label> via aria-labelledby or id/for pairing; ensure
the labels describe the control and match the inputs that reference temperature
and setTemperature so screen readers announce them correctly.

---

Nitpick comments:
In `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 466-595: The file AIPanel.tsx contains large inline modal
components; extract ProviderKeyDialog (and similarly CustomRoutingDialog) into
their own files to reduce file size and improve testability: create
ProviderKeyDialog.tsx exporting the ProviderKeyDialog component (keeping props
shape, useT hook, state/handlers like handleSave, placeholder logic, and
classNames intact), update AIPanel.tsx to import ProviderKeyDialog and remove
the in-file definition, and do the same for CustomRoutingDialog (preserve prop
types and any referenced helpers like defaultEndpointFor); ensure
exports/imports paths are updated and run type checks.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3491fc2-3e36-4b37-aae4-682055a968a2

📥 Commits

Reviewing files that changed from the base of the PR and between 556b18a and d9d6a12.

📒 Files selected for processing (1)
  • app/src/components/settings/panels/AIPanel.tsx

Comment thread app/src/components/settings/panels/AIPanel.tsx
Comment thread app/src/components/settings/panels/AIPanel.tsx
Two CodeRabbit catches on tinyhumansai#2165:

1. Temperature override slider/number inputs had no accessible name —
   screen readers were announcing an unnamed slider/spinbutton inside
   the dialog. Add aria-label to both.
2. Local-runtime endpoints like `http://host:11434` passed validation
   but were stored verbatim, so the /models probe silently failed
   until the user manually appended `/v1`. Parse with `new URL(...)`,
   reject non-http(s), append `/v1` when the path is empty or `/`, and
   feed the normalised endpoint to both the cloud_providers entry and
   the local_ai.base_url derivation.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 19, 2026
…ure override

Coverage gate failed on the previous diff at 41% — well below the
80% diff-cover threshold for tinyhumansai#2165. Bring up the changed-line coverage
by exercising the actual new surfaces:

- aiSettingsApi.test.ts: @temp parse/serialize round-trips, malformed
  suffix degrades to model id, two-decimal rounding pinned, non-finite
  temperatures treated as unset, and the omitted-key contract for
  pre-existing toEqual snapshots.
- AIPanel.test.tsx: Ollama chip → endpoint dialog renders a URL field
  with a localhost default (not an API-key field); non-http endpoints
  are rejected and the dialog stays open; a successful save normalises
  the URL and calls openhumanUpdateLocalAiSettings with the stripped
  base_url; the Custom routing dialog wires a per-workload temperature
  override into the saved provider string.
senamakel added 7 commits May 18, 2026 19:54
AppearancePanel was entirely un-i18n'd; MascotPanel had a handful of
hardcoded strings (header title, color-swatch aria + labels, character
metadata "states"/"visemes", "Preview ·" caption, library-unavailable
error). Wire both panels to useT and add 21 new keys to en.ts +
chunks/{locale}-5.ts for all 11 locales (non-English values default to
the English text for the translator queue).
Save flow for both the chip toggle dialog (ProviderKeyDialog) and the
full editor (CloudProviderEditor) now:

1. Persists the credential (or local_ai.base_url for Ollama).
2. Flushes the new cloud_providers list to disk so the Rust controller
   can resolve the slug.
3. Calls `openhuman.inference_list_models` (i.e. hits the provider's
   `/models` endpoint with the configured auth header).
4. On failure: rolls back — restores the prior cloud_providers list and
   clears the API key — and surfaces the error inline in the dialog so
   the user can fix the value and retry. The provider never lands in
   the saved state.

OpenHuman backend is exempt (session JWT, no probe-able /models).
The chip toggle + full editor already probe `/models` at add-time, but
the global SaveBar was committing the draft without re-verifying. Now
the save() callback diffs draft vs saved cloud_providers and re-probes
any entry that is new or whose endpoint changed before calling
`saveAISettings`. A probe failure blocks the save and surfaces the
error in the existing top-of-panel alert ("Could not reach X: ...
Settings were not saved.") so a stale/unreachable provider can never
slip into the saved config and start routing chat traffic to a dead
host. OpenHuman backend stays exempt.
…rror field

LM Studio returns HTTP 200 at unknown paths like `/v11/models` with a
JSON body `{"error":"Unexpected endpoint or method..."}` instead of a
4xx. The previous `list_configured_models` happily parsed that as
`data: []` and reported success, so the AI-panel /models probe silently
accepted a typo'd endpoint.

Tighten the response check: any top-level `error` field, or a body
missing the `data` array entirely, is now a probe failure. A valid
empty list (`{"data": []}`) is still accepted — that's the legitimate
"endpoint reached, no models loaded yet" case for fresh Ollama.

Repro on a real LM Studio install:
  curl http://localhost:1234/v11/models
  → HTTP 200, {"error":"Unexpected endpoint or method..."}
The lead/orchestrator's `provider_role` was inferred from
`config.default_model` — bootstrap pinned that to `reasoning-v1`, so
setting `chat_provider` in Settings → LLM → Routing had no effect on
the main chat turn (it kept routing to `reasoning_provider`, which most
users leave on the OpenHuman backend).

Three coupled changes so `chat_provider` actually drives the
user-facing chat:

- Introduce `MODEL_CHAT_V1 = "chat-v1"` and re-export it; flip
  `DEFAULT_MODEL` from `reasoning-v1` to `chat-v1`.
- session/builder.rs: only the explicit `hint:<role>` form routes to a
  specialised workload now; everything else (including legacy
  `reasoning-v1` on disk for existing users) falls through to `chat`.
  Subagents still set their own role via `ModelSpec::Hint(...)`.
- factory::make_openhuman_backend: `hint:chat` now translates to
  `MODEL_CHAT_V1` (was `MODEL_REASONING_QUICK_V1`), keeping the
  backend tier name in sync with the new constant.

Verified live with chat_provider="lmstudio:google/gemma-4-e4b":
  [chat-factory] role=chat slug=lmstudio model=google/gemma-4-e4b
  [provider:cloud] outbound chat/completions -> http://localhost:1234/v1/chat/completions
@senamakel senamakel merged commit 16b35af into tinyhumansai:main May 19, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant