Skip to content

fix(config): preserve built-in reserved-slug cloud_providers across settings saves#2457

Open
YellowSnnowmann wants to merge 8 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/cloud-providers-reserved-slug-filter
Open

fix(config): preserve built-in reserved-slug cloud_providers across settings saves#2457
YellowSnnowmann wants to merge 8 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/cloud-providers-reserved-slug-filter

Conversation

@YellowSnnowmann
Copy link
Copy Markdown
Contributor

@YellowSnnowmann YellowSnnowmann commented May 21, 2026

Summary

  • handle_inference_update_model_settings and handle_update_model_settings now silently drop reserved-slug cloud_providers entries (openhuman, cloud, pid) from incoming patches instead of failing the entire request with Err("slug 'X' is reserved...").
  • apply_model_settings now preserves reserved-slug entries from the stored config when replacing cloud_providers, so the migration-seeded built-in OpenHuman entry survives every settings save.
  • Empty-slug validation behavior is preserved — empty slugs still pass the filter and hit the explicit "slug must not be empty" error.
  • Added two regression tests pinning the preservation contract and the no-double-add defense.
  • Eliminates Sentry TAURI-RUST-5 (~7,183 events / 14 days, ~512/day) at the source.
  • Fixes user-facing bug: AI Settings saves were silently failing for every user post-migration, dropping their actual edits.

Problem

  • The migration unify_ai_provider_settings seeds a cloud_providers entry with slug: "openhuman" into every user's config — this is the built-in OpenHuman provider and is referenced by primary_cloud and the per-workload routing fields.

  • The frontend's AI Settings screen uses a read-modify-write pattern: it reads the full cloud_providers list (which includes the seeded built-in), the user edits one of their custom providers, and the frontend echoes the whole list back on save.

  • Both schema handlers then ran every incoming entry through is_slug_reserved():

if is_slug_reserved(&slug) {
    return Err(format!("slug '{}' is reserved and cannot be used for a custom provider", slug));
}
  • Because the entries were processed via .collect::<Result<Vec<_>, String>>().transpose()?, a single rejected entry failed the entire RPC call. The user's actual edit (e.g. a new API key) was discarded, and jsonrpc.rs reported the error to Sentry via report_error_or_expected since the message didn't match any expected-error classifier.

  • Result: every save-settings click → one Sentry event + a silently failed user edit. 7,183 events in 14 days, on track to be one of the noisiest issues on the dashboard. Sentry link: https://sentry.tinyhumans.ai/organizations/tinyhumans/issues/45

  • The system was rejecting data its own migration had seeded.

Solution

Two coordinated changes that together fix the user experience and the Sentry noise without altering the reserved-slug contract:

  1. Schema handlers (src/openhuman/inference/schemas.rs, src/openhuman/config/schemas.rs) — insert a .filter() step before .map() that drops non-empty reserved slugs silently:

    .filter(|entry| {
        let trimmed = entry.slug.trim();
        trimmed.is_empty() || !is_slug_reserved(trimmed)
    })

    The filter is intentionally constructed so empty slugs pass through and still hit the explicit "slug must not be empty" Err inside .map() — preserving the validation surface for genuinely malformed frontend payloads.

  2. apply_model_settings (src/openhuman/config/ops.rs) — before replacing config.cloud_providers with the patch, snapshot any existing reserved-slug entries from the stored config and re-inject them afterwards. A iter().any(|e| e.slug == entry.slug) guard prevents duplicates if a CLI/test path bypasses the schema filter.

Design decisions / tradeoffs

  • Silent filter vs. explicit error: The frontend has no good way to distinguish "I authored this openhuman entry on purpose" from "this came from the read-modify-write cycle." Returning a friendly error per-entry is more disruptive than silently filtering, and the user can't create a meaningful openhuman provider anyway — the slug is reserved. Silent filter wins.
  • Preserve-on-apply rather than merge-in-handler: The schema handler is shape-focused (param → patch); ownership of "what lives in stored config" belongs to apply_model_settings. Putting the preservation logic there means CLI / tests / future RPC paths that build a ModelSettingsPatch directly all benefit from the same invariant.
  • Why not classify the message as expected in expected_error_kind? That would silence Sentry but leave the user with a broken save. The real bug is the failing update, not the alert.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
    • apply_model_settings_preserves_existing_reserved_slug_cloud_providers — happy path: built-in survives a normal save
    • apply_model_settings_does_not_double_add_reserved_entries — edge case: defensive de-dup
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • Coverage matrix updated — added/removed/renamed feature rows in docs/TEST-COVERAGE-MATRIX.md reflect this change (or N/A: behaviour-only change)
    • N/A: behaviour-only fix to an existing feature row (AI Settings / cloud_providers)
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md)
    • N/A: bug fix on an existing flow, smoke step "Save AI settings without error" already covers the regression
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Runtime/platform: Desktop (Windows / macOS / Linux). No mobile / web / CLI behavior change beyond the shared core path.
  • Performance: Negligible. One additional iter().filter().cloned().collect() over cloud_providers per settings-save call. The list is tiny (typically < 10 entries).
  • Security: None. The reserved-slug contract (is_slug_reserved) is unchanged; users still cannot persist a user-controlled provider with a reserved slug. The filter drops such entries rather than allowing them through.
  • Migration / compatibility: Backward-compatible. No schema change, no on-disk format change. Configs already on disk continue to work — in fact, this fix is what makes them work correctly going forward.
  • Observability: Drops Sentry TAURI-RUST-5 to zero (~512 events/day eliminated). No new error-reporting paths added.
  • User-visible: AI Settings → Save now actually persists user edits for every user who has run the unify_ai_provider_settings migration (i.e. everyone on a current build).

Related

Summary by CodeRabbit

  • Bug Fixes

    • Cloud provider configurations are preserved during settings updates, preventing loss of reserved built-in entries and avoiding duplicate entries.
    • Requests that include reserved provider identifiers are now filtered rather than causing the entire update to be rejected; providers with empty names are still rejected.
  • Tests

    • Added tests covering cloud provider preservation and defensive duplicate handling; updated an end-to-end test to assert provider survival after updates.
  • Localization

    • Added new German translations for provider and developer-menu (MCP server) UI text.

Review Change Stack

…tions

- Updated  and  to silently drop entries with reserved slugs, allowing empty slugs to trigger validation errors.
- This change prevents reserved entries from being dropped on save while maintaining explicit error handling for actual frontend issues.
- Enhanced the apply_model_settings function to retain existing reserved-slug entries when updating cloud providers, preventing accidental deletion during routine saves.
- Added defensive checks to avoid duplicate entries for reserved slugs from the payload.
…ved cloud provider slugs

- Introduced new tests to ensure that existing reserved-slug entries are preserved during model settings updates.
- Added checks to prevent double-adding reserved entries when bypassing the schema handler, ensuring data integrity in cloud provider configurations.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2053f46b-d5a0-4d55-8670-0b8af895bd44

📥 Commits

Reviewing files that changed from the base of the PR and between 75b4f3e and e62fbb8.

📒 Files selected for processing (2)
  • app/src/lib/i18n/chunks/de-3.ts
  • app/src/lib/i18n/chunks/de-5.ts

📝 Walkthrough

Walkthrough

RPC handlers now drop reserved cloud-provider slugs from incoming model-settings updates, and apply_model_settings preserves and reinjects any reserved providers already present in stored config; tests, an e2e assertion, and German i18n strings were added.

Changes

Reserved Cloud Provider Handling

Layer / File(s) Summary
RPC schema-level filtering for reserved slugs
src/openhuman/config/schemas.rs, src/openhuman/inference/schemas.rs
Both handle_update_model_settings and handle_inference_update_model_settings count and silently drop cloud provider entries whose non-empty slug is reserved (logging the count); entries with empty/whitespace slugs still cause a validation error.
Config preservation of reserved entries + unit tests
src/openhuman/config/ops.rs, src/openhuman/config/ops_tests.rs
apply_model_settings now extracts preserved reserved-slug providers from the stored config, overwrites config.cloud_providers with incoming providers, and re-attaches any preserved reserved entries that are missing (avoiding duplicates). Two new unit tests validate preservation when the patch omits reserved entries and when the patch includes a reserved slug.
End-to-end test verification
tests/json_rpc_e2e.rs
E2E test updated to verify that a cloud_providers entry with slug == "proxy" still exists after model settings update, replacing an earlier exact-length assertion.
German i18n additions
app/src/lib/i18n/chunks/de-3.ts, app/src/lib/i18n/chunks/de-5.ts
Adds subconscious.providerUnavailableTitle and providerSettings keys plus MCP-Server developer-menu strings (titles, descriptions, client labels, clipboard/config messages).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2184: Related handling of reserved cloud_providers slugs; that PR filters reserved slugs on the TS load/save path while this PR preserves/drops them in Rust apply/update paths.

Suggested reviewers

  • senamakel

Poem

A rabbit guards the built-in key,
While user seeds flow wild and free.
We trim, we log, we gently paste—
No duplicate, no treasured waste. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main fix: preserving built-in reserved cloud_providers across settings saves. It directly reflects the core change across all modified files.
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.

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

Warning

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

🔧 ESLint

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

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


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

- Updated the test to assert that the user's proxy entry survives configuration updates by checking for the presence of the slug rather than relying on the count of cloud providers.
- This change ensures that reserved-slug entries are correctly maintained during model settings updates, aligning with recent enhancements to the apply_model_settings function.
@YellowSnnowmann YellowSnnowmann marked this pull request as ready for review May 21, 2026 18:44
@YellowSnnowmann YellowSnnowmann requested a review from a team May 21, 2026 18:44
@coderabbitai coderabbitai Bot added the bug label May 21, 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

🧹 Nitpick comments (3)
src/openhuman/config/ops.rs (1)

498-513: ⚡ Quick win

Add debug visibility for reserved-provider preservation/reinjection.

This block now performs a non-trivial state transition (preserved extraction + reinjection). A small debug log with preserved/reinjected counts would make routing regressions easier to diagnose.

As per coding guidelines: "Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions, and any branch that is hard to infer from tests alone."

🤖 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/openhuman/config/ops.rs` around lines 498 - 513, Add a debug log that
reports how many reserved providers were pulled out and how many were actually
reinjected: after computing let preserved: Vec<_> = ... log the preserved.len(),
and after the reinjection loop log how many entries were appended (or the
difference between config.cloud_providers length before/after). Use the existing
logging crate in the project (e.g. tracing::debug! or log::debug!) and reference
the symbols config.cloud_providers, preserved, is_slug_reserved and the
surrounding apply_model_settings / reinjection block so it's clear where to
insert the two debug statements.
src/openhuman/config/schemas.rs (1)

915-927: ⚡ Quick win

Add debug diagnostics for reserved-slug filtering in update_model_settings.

Line 915 introduces a silent behavioral branch that mutates incoming payloads. Add a debug log (count-only) when reserved entries are dropped so save-path behavior is traceable without surfacing user-facing errors.

As per coding guidelines: "Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions, and any branch that is hard to infer from tests alone."

🤖 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/openhuman/config/schemas.rs` around lines 915 - 927, In
update_model_settings, the filter that silently drops reserved slugs should emit
a debug-only diagnostic: compute how many incoming entries would be removed by
is_slug_reserved (e.g., by iterating once to count reserved entries or comparing
lengths before/after filtering), then call the crate's debug/tracing logger
(trace/debug) with a concise message like "update_model_settings dropped N
reserved slugs" including the count and context (request id or user id if
available). Keep behavior unchanged (still drop them) and place the log next to
the existing filter block so it runs on the incoming payload before
apply_model_settings re-injects stored entries.
src/openhuman/inference/schemas.rs (1)

561-573: ⚡ Quick win

Mirror debug diagnostics for reserved-slug filtering on inference RPC path.

Line 561 adds the same silent filter behavior on a second RPC entrypoint. Emit a debug-level count of dropped reserved entries here too, to keep troubleshooting parity across both handlers.

As per coding guidelines: "Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions, and any branch that is hard to infer from tests alone."

🤖 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/openhuman/inference/schemas.rs` around lines 561 - 573, The reserved-slug
filter is silently dropping entries in the RPC handler; calculate how many
non-empty slugs are being dropped (use is_slug_reserved on trimmed slug)
immediately before applying the .filter(...) and emit a debug-level log (e.g.,
tracing::debug! or log::debug!) with that count and brief context so diagnostics
match the other RPC entrypoint; keep the filter logic unchanged and reference
the same trimmed.is_empty() || !is_slug_reserved(trimmed) check and the
surrounding comment about apply_model_settings.
🤖 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 `@src/openhuman/config/ops.rs`:
- Around line 510-511: The duplicate-check is using raw slugs so whitespace
variants slip through; update the comparison to use the same canonical form as
earlier by trimming (and any other normalization used) before comparing—i.e.,
when checking config.cloud_providers.iter().any(|e| e.slug == entry.slug)
normalize both e.slug and entry.slug (trim) so the push of entry only happens if
the trimmed slugs differ.

---

Nitpick comments:
In `@src/openhuman/config/ops.rs`:
- Around line 498-513: Add a debug log that reports how many reserved providers
were pulled out and how many were actually reinjected: after computing let
preserved: Vec<_> = ... log the preserved.len(), and after the reinjection loop
log how many entries were appended (or the difference between
config.cloud_providers length before/after). Use the existing logging crate in
the project (e.g. tracing::debug! or log::debug!) and reference the symbols
config.cloud_providers, preserved, is_slug_reserved and the surrounding
apply_model_settings / reinjection block so it's clear where to insert the two
debug statements.

In `@src/openhuman/config/schemas.rs`:
- Around line 915-927: In update_model_settings, the filter that silently drops
reserved slugs should emit a debug-only diagnostic: compute how many incoming
entries would be removed by is_slug_reserved (e.g., by iterating once to count
reserved entries or comparing lengths before/after filtering), then call the
crate's debug/tracing logger (trace/debug) with a concise message like
"update_model_settings dropped N reserved slugs" including the count and context
(request id or user id if available). Keep behavior unchanged (still drop them)
and place the log next to the existing filter block so it runs on the incoming
payload before apply_model_settings re-injects stored entries.

In `@src/openhuman/inference/schemas.rs`:
- Around line 561-573: The reserved-slug filter is silently dropping entries in
the RPC handler; calculate how many non-empty slugs are being dropped (use
is_slug_reserved on trimmed slug) immediately before applying the .filter(...)
and emit a debug-level log (e.g., tracing::debug! or log::debug!) with that
count and brief context so diagnostics match the other RPC entrypoint; keep the
filter logic unchanged and reference the same trimmed.is_empty() ||
!is_slug_reserved(trimmed) check and the surrounding comment about
apply_model_settings.
🪄 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: 6576f48a-dae8-4f7a-a8f7-98585f91cf6c

📥 Commits

Reviewing files that changed from the base of the PR and between ec9708a and 563ba74.

📒 Files selected for processing (5)
  • src/openhuman/config/ops.rs
  • src/openhuman/config/ops_tests.rs
  • src/openhuman/config/schemas.rs
  • src/openhuman/inference/schemas.rs
  • tests/json_rpc_e2e.rs

Comment thread src/openhuman/config/ops.rs Outdated
- Added debug logging to track the number of reserved cloud providers preserved before and after updates.
- Enhanced the slug comparison logic to ensure trimmed slugs are correctly checked, preventing duplicates during configuration updates.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 21, 2026
- Added new translation keys for subconscious provider status and settings in de-3.ts.
- Expanded MCP server settings translations in de-5.ts, including configuration options and error messages.
@M3gA-Mind
Copy link
Copy Markdown
Contributor

@YellowSnnowmann this PR has merge conflicts with main — please rebase/resolve before review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants