Skip to content

feat: user-facing temperature setting#2275

Merged
ilblackdragon merged 5 commits intostagingfrom
feat/user-facing-temperature
Apr 11, 2026
Merged

feat: user-facing temperature setting#2275
ilblackdragon merged 5 commits intostagingfrom
feat/user-facing-temperature

Conversation

@ilblackdragon
Copy link
Copy Markdown
Member

@ilblackdragon ilblackdragon commented Apr 10, 2026

Summary

  • Adds temperature to user settings (persisted via settings store, editable in web UI)
  • Flows the setting into ReasoningContextrespond_with_tools(), replacing the hardcoded 0.7 default for conversational turns
  • Admin-set defaults propagate to all members via a new admin-scoped fallback layer
  • Adds float input type support to the structured settings renderer (with configurable step/min/max)
  • i18n labels for EN, ZH-CN, KO

Details

Backend — temperature setting:

  • Settings.temperature: Option<f32> — new field under Inference Provider section
  • ReasoningContext.temperature: Option<f32> — propagated to CompletionRequest/ToolCompletionRequest
  • ChatDelegate::call_llm() reads temperature from settings store on iteration 0 (same pattern as selected_model)
  • Internal system calls (compaction, heartbeat, routines) keep their hardcoded temperatures — those are deliberate per-task values

Backend — admin-scoped settings fallback (multi-tenant):

  • TenantScope::get_setting_with_admin_fallback() — checks user scope, then falls back to __admin__ scope
  • Config::from_db_with_toml() — layers admin-scope settings between TOML and per-user DB. Priority: TOML < admin DB < per-user DB
  • GET/PUT/DELETE /api/settings/{key}?scope=admin — lets admins read/write shared defaults. Non-admins get 403.
  • Works for any setting, not just temperature — admin can set instance-wide selected_model, temperature, etc.

Frontend:

  • New "Inference" settings group with temperature slider (0.0-2.0, step 0.1)
  • Extended renderStructuredSettingsRow to handle float type (parseFloat, configurable step)

Not in scope (follow-up):

  • Per-request temperature on Responses API (requires threading through IncomingMessage)
  • V2 engine thread-level temperature default (orchestrator already reads it from explicit config)
  • Admin settings UI page (admin can use ?scope=admin via API; UI can be added later)

Test plan

  • cargo clippy --all --all-features — zero warnings
  • cargo test --lib -- reasoning — 176 tests pass
  • cargo test --lib -- dispatcher::tests — 62 tests pass
  • cargo test --lib -- settings — 114 tests pass
  • cargo test --lib -- tenant::tests — 10 tests pass (3 new admin-fallback tests)
  • Manual: open web UI settings, Inference section, set temperature, verify it persists and affects responses
  • Manual: admin sets temperature via PUT /api/settings/temperature?scope=admin, verify member inherits it

Add a configurable default sampling temperature (0.0–2.0) that users
can set via the web settings UI or API. The setting flows into the
main conversational agent loop via ReasoningContext, replacing the
hardcoded 0.7 default. Per-request temperature (e.g. from the
OpenAI-compatible endpoint) still takes precedence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added scope: agent Agent core (agent loop, router, scheduler) scope: channel/web Web gateway channel scope: llm LLM integration scope: config Configuration size: M 50-199 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Apr 10, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a user-configurable temperature setting for LLM inference, allowing users to adjust the sampling randomness between 0.0 and 2.0. The changes span the frontend UI (adding float-type setting support and translations), the settings storage, and the backend dispatcher which now applies this override during inference. Feedback was provided to clamp the temperature value in the backend to ensure robustness against invalid settings values that might cause API errors with LLM providers.

Comment thread src/llm/reasoning.rs Outdated
context.available_tools.clone()
};

let temperature = context.temperature.unwrap_or(0.7);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The temperature value should be clamped to a safe range (typically 0.0 to 2.0) before being passed to LLM providers. While the frontend UI enforces this range, the backend should also ensure robustness against malformed or manually injected settings values to prevent potential API errors from providers that have strict range requirements.

Suggested change
let temperature = context.temperature.unwrap_or(0.7);
let temperature = context.temperature.unwrap_or(0.7).clamp(0.0, 2.0);

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 f3f2d19. Clamped at the use site in respond_with_tools (reasoning.rs) and also at the read site in the dispatcher (dispatcher.rs) when loading from the DB, for defense in depth.

Admin-set defaults now propagate to all members who haven't overridden
the value themselves. Three layers of change:

1. TenantScope::get_setting_with_admin_fallback() — checks user scope
   first, then falls back to __admin__ scope. Used by the dispatcher
   for temperature and selected_model.

2. Config::from_db_with_toml() — layers admin-scope settings between
   TOML and per-user DB settings during resolution. Priority:
   TOML < admin DB < per-user DB.

3. Settings API — GET/PUT/DELETE /api/settings/{key}?scope=admin lets
   admins read/write to the shared default scope. Non-admins get 403.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added size: L 200-499 changed lines and removed size: M 50-199 changed lines labels Apr 10, 2026
ilblackdragon and others added 2 commits April 11, 2026 04:07
Address review comment on #2275: the backend must guard against
bad DB values and per-request overrides that bypass the frontend
range enforcement. Some providers reject out-of-range temperatures
outright.

Clamped at both the read site (dispatcher reading DB settings) and
the use site (reasoning.rs respond_with_tools) for defense in depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon ilblackdragon marked this pull request as ready for review April 11, 2026 04:19
Copilot AI review requested due to automatic review settings April 11, 2026 04:19
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a user-configurable default LLM sampling temperature and introduces an admin-default settings scope that members inherit unless overridden, wiring the value through the backend reasoning path and exposing it in the web UI.

Changes:

  • Add temperature to persisted settings and propagate it into ReasoningContextrespond_with_tools() (replacing the hardcoded 0.7 default for conversational turns).
  • Introduce admin-scope settings fallback (__admin__) for selected settings resolution and add ?scope=admin support to settings GET/PUT/DELETE endpoints.
  • Extend the web structured settings renderer to support float inputs and add i18n labels for the new inference settings group.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/tenant.rs Adds get_setting_with_admin_fallback() and unit tests for fallback behavior.
src/settings.rs Introduces Settings.temperature: Option<f32>.
src/llm/reasoning.rs Threads temperature through ReasoningContext and applies clamping before provider calls.
src/config/mod.rs Adds admin-scope DB settings layer between TOML and per-user DB settings.
src/channels/web/types.rs Adds query type for ?scope=admin on settings endpoints.
src/channels/web/responses_api.rs Updates error message for unsupported per-request temperature.
src/channels/web/handlers/settings.rs Implements scope=admin targeting for get/set/delete settings operations.
src/agent/dispatcher.rs Loads selected_model and temperature from settings (with admin fallback) on iteration 0.
crates/ironclaw_gateway/static/i18n/en.js Adds i18n strings for temperature + inference group.
crates/ironclaw_gateway/static/i18n/zh-CN.js Adds i18n strings for temperature + inference group.
crates/ironclaw_gateway/static/i18n/ko.js Adds i18n strings for temperature + inference group.
crates/ironclaw_gateway/static/app.js Adds temperature setting UI and float support in structured settings rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/config/mod.rs Outdated
Comment on lines +295 to +297
&& let Ok(admin_map) = store.get_all_settings(admin_scope).await
&& !admin_map.is_empty()
{
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 857d3b2. Applied strip_admin_only_llm_keys to the admin-scope map under !is_operator before merging, so non-operators no longer inherit admin-only LLM endpoint keys (ollama_base_url, openai_compatible_base_url, llm_builtin_overrides, llm_custom_providers) from admin defaults. Regression test: re_resolve_llm_strips_admin_scope_admin_only_keys_for_non_operator in src/config/mod.rs.

Comment thread src/config/mod.rs Outdated
Comment on lines +415 to +417
&& let Ok(admin_map) = store.get_all_settings(admin_scope).await
&& !admin_map.is_empty()
{
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 857d3b2. Same defense-in-depth strip applied to the admin-scope merge in re_resolve_llm_with_secrets when is_operator is false.

Comment thread src/agent/dispatcher.rs Outdated
Comment on lines +603 to +609
// Temperature override from user or admin settings.
// Clamped to the supported range to guard against bad DB values.
if let Ok(Some(value)) = store.get_setting_with_admin_fallback("temperature").await
&& let Some(t) = value.as_f64()
{
reason_ctx.temperature = Some((t as f32).clamp(0.0, 2.0));
}
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 857d3b2. Extracted resolve_settings_temperature(current, settings_value) helper that returns None when a per-request value is already on the context, so explicit API-caller temperatures keep precedence over user/admin defaults. Clamping is preserved. Regression tests: resolve_settings_temperature_keeps_per_request_value plus three companion cases in src/agent/dispatcher.rs.

Comment on lines +20 to +43
/// Resolve the effective user_id for a settings operation.
///
/// When `scope=admin`, the operation targets the shared admin-default scope
/// (`__admin__`). Only admin users may use this scope; non-admins get 403.
/// Without the scope parameter (or any other value), operations target the
/// calling user's own settings.
fn resolve_settings_scope(
user: &crate::channels::web::auth::UserIdentity,
query: &SettingScopeQuery,
) -> Result<String, StatusCode> {
if query.scope.as_deref() == Some("admin") {
if user.role != "admin" {
tracing::warn!(
user_id = %user.user_id,
role = %user.role,
"Non-admin attempted to use scope=admin on settings endpoint"
);
return Err(StatusCode::FORBIDDEN);
}
Ok(crate::tools::permissions::ADMIN_SETTINGS_USER_ID.to_string())
} else {
Ok(user.user_id.clone())
}
}
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.

Acknowledged — this is a real inconsistency, but it's a pre-existing partial implementation of SettingScopeQuery rather than something introduced by this PR. Extending scope=admin to list/export/import touches the wire types and the frontend, so I'd prefer to keep this PR scoped to the temperature work and address it in a dedicated follow-up. Tracking it on my side.

Copy link
Copy Markdown
Collaborator

@serrrfirat serrrfirat left a comment

Choose a reason for hiding this comment

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

Paranoid review findings:

  1. High - src/tenant.rs: get_setting_with_admin_fallback treats Some(Value::Null) as a user override and returns it before checking the __admin__ scope. That conflicts with the settings layer semantics where null means default/absent, and bulk settings writes can persist null values. A user row like temperature: null or selected_model: null will silently block the admin default. Please skip null user values and continue to the admin lookup, with a regression test covering user null + admin value.

  2. Medium - src/config/mod.rs / src/channels/web/handlers/settings.rs: admin-scoped secret-bearing settings are vaulted under the effective user id (__admin__), but re_resolve_llm_with_secrets merges admin settings and then hydrates secrets only for the member/operator user_id. If admin-scoped provider settings are intended, sanitized provider config can merge without the admin API key. Please either disallow admin-scoped secret-bearing keys or hydrate admin-scope secrets before/alongside per-user secrets using the same precedence as settings.

- Strip admin-only LLM keys (ollama_base_url, openai_compatible_base_url,
  llm_builtin_overrides, llm_custom_providers) from the admin-scope merge
  in `Config::from_db_with_toml` and `Config::re_resolve_llm_with_secrets`
  when the resolving user is not an operator. Defense-in-depth so a
  non-admin member never inherits private/loopback provider endpoints
  from admin defaults.
- Preserve per-request `reason_ctx.temperature` precedence in the
  dispatcher: settings-derived temperature only applies when no value
  was already set by the API caller. Extract `resolve_settings_temperature`
  helper for direct unit testing.
- Add regression tests covering admin-scope strip behavior for both
  operator and non-operator paths, plus the temperature precedence rule
  including range clamping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
user: &crate::channels::web::auth::UserIdentity,
query: &SettingScopeQuery,
) -> Result<String, StatusCode> {
if query.scope.as_deref() == Some("admin") {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Medium Severity — Missing handler-level test for admin scope authorization

resolve_settings_scope correctly gates ?scope=admin to admins, but there's no test that drives the actual handler with a non-admin UserIdentity + scope=admin to assert a 403 response. The project's CLAUDE.md rule "Test Through the Caller, Not Just the Helper" applies here — a unit test on the pure function alone doesn't cover the wiring between the extractor, the scope resolver, and the handler.

Suggested fix: Add a test in the settings.rs test module that constructs a non-admin UserIdentity (role: "member"), passes Query(SettingScopeQuery { scope: Some("admin".into()) }), and asserts the handler returns 403. Pattern already exists in the module for similar tests (e.g. test_settings_delete_rejects_active_custom_backend).

Comment thread src/config/mod.rs
crate::config::helpers::strip_admin_only_llm_keys(&mut admin_map);
}
let admin_settings = Settings::from_db_map(&admin_map);
settings.merge_from(&admin_settings);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Medium Severity — Duplicated admin-scope fallback logic

The admin-scope fallback pattern (fetch admin map → strip admin-only keys for non-operators → merge into settings) is duplicated verbatim between from_db_with_toml (line ~290) and re_resolve_llm_with_secrets (line ~420). If one is updated without the other (e.g., a new strip rule or changed merge order), they silently diverge.

Suggested fix: Extract into a private helper like:

async fn apply_admin_scope_defaults(
    store: &(dyn SettingsStore + Sync),
    settings: &mut Settings,
    user_id: &str,
    is_operator: bool,
) { ... }

Low-urgency — can be a follow-up.

Copy link
Copy Markdown
Collaborator

@serrrfirat serrrfirat left a comment

Choose a reason for hiding this comment

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

Reviewed as paranoid architect. Security model is sound — admin scope gated correctly, temperature clamped at multiple layers, internal system calls unaffected. Two medium findings posted as inline comments (missing handler-level test for admin scope authz, duplicated fallback logic in config). Neither is a blocker. Ship it.

This was referenced Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: channel/web Web gateway channel scope: config Configuration scope: llm LLM integration size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants