Skip to content

th-c127d1: send temperature 1.0 — 0 is rejected by many frontier models - #333

Closed
brentrager wants to merge 7 commits into
mainfrom
th-c127d1-temperature-pinned
Closed

th-c127d1: send temperature 1.0 — 0 is rejected by many frontier models#333
brentrager wants to merge 7 commits into
mainfrom
th-c127d1-temperature-pinned

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Branched from e9ce68c (the rev smooth pins) rather than main, so smooth can consume this without a 1.23 → 1.36 version jump. Retarget/forward-port to main as a follow-up.

The bug

A growing set of frontier models accept only their default temperature and 400 the entire request:

Unsupported value: 'temperature' does not support 0 with this model.
Only the default (1) value is supported.

The symptom doesn't look like a config error. The server boots, accepts the turn, every LLM call 400s, and the user sees an assistant that silently says nothing. Downstream in Big Smooth this made the whole model picker a no-op — every model except the default produced an empty reply.

Why not a per-model allowlist

Provably wrong. Measured against llm.smoo.ai on 2026-08-07:

rejects temperature: 0 accepts it
gpt-5.1, gpt-5.4-pro, gpt-5.5 gpt-5, gpt-5.2, gpt-5.4
claude-opus-4-7, claude-opus-4-8, claude-sonnet-5, claude-fable-5 claude-haiku-4-5, claude-sonnet-4-5/4-6, claude-opus-4-6
gemini-3.5-flash, deepseek-v4-flash/pro, glm-5.1, minimax-m2.7, groq-gpt-oss-20b

gpt-5.1 rejects while gpt-5.2 accepts. gpt-5.4 accepts while gpt-5.4-pro rejects. There is no prefix rule to write.

1.0 was accepted by all 12 models tested across 6 families — the single value that works everywhere. Cost: temperature-0 determinism on models that would allow it. Fair trade against "only the default model works at all".

Verification

llm_config_with_key (handler.rs:1112) is the turn's config, so this is the load-bearing site. Verified end-to-end against a real gateway from this exact commit:

  • gpt-5.5: produced nothing at all → now PASSes two Big Smooth conversation scenarios at 5/5/5/5, zero LLM errors.
  • claude-sonnet-5: now runs and passes.
  • claude-opus-4-8: now runs and fails a scenario on merit (fabrication) rather than on infrastructure — real signal instead of an empty reply.

112 server tests pass. The long-term shape is Option<f32> on LlmConfig so we send nothing and take each provider's default.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P83HrAKaqp2ehxQcW7m6QU

brentrager and others added 7 commits July 12, 2026 09:43
…one step

The conversation-workflow judge advances only on "yes". When a step's criteria
demand evidence the judge never accepts, the step re-asks indefinitely and a
multi-step guided flow (the public Transformation Posture agent) never reaches
its posture-summary / lead-capture step — it just re-asks one dimension until
the conversation runs out. The step pointer itself persists and advances
correctly; the missing piece is an escape hatch.

apply_step_cap force-advances to the next step after WORKFLOW_STEP_ATTEMPT_CAP
(3) consecutive non-advancing turns, resetting on any real advance. The counter
persists in session metadata (stepAttempts) alongside the existing currentStepId
pointer, read/written in the send_message handler exactly like the pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…per turn

LocalServer/AppState now carry host-supplied `Vec<Arc<dyn ToolHook>>`, plumbed
into every per-turn ToolRegistry BEFORE the per-agent auth gate and confirmation
hooks — so a host permission/surveillance hook gets first say on every call.
This is the prerequisite seam for Big Smooth's narc-judge + auto-mode: the host
hands the server its own ToolHooks (auto-mode allow/deny/ask gate + LLM judge)
without forking the runner.

- LocalServerBuilder::tool_hooks(Vec<Arc<dyn ToolHook>>) → AppState::with_tool_hooks
  → TurnRequest::tool_hooks, registered in run_streaming_turn.
- SharedToolHook newtype forwards all 5 hook methods so an Arc<dyn ToolHook> can
  be registered via ToolRegistry::add_hook (which takes impl ToolHook by value;
  the orphan rule forbids a blanket impl here).
- Tests: spy-hook fires pre_call/post_call through the real runner turn; builder
  seam installs hooks onto AppState.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USBgWKcgpkWAugmHPE7Bc9
…mission system + DenyPolicy)

Bumps the workspace core pin 0.16.2 -> 1.7.0 so the server the Big Smooth
daemon pins is built against the engine that ships the permission system.
This lets the daemon install `PermissionHook::with_deny_policy(...)` via the
existing tool_hooks seam (th-1f694a) without forking the runner.

- rust/Cargo.toml: smooai-smooth-operator-core = "1.7.0" (+ updated note).
- Merged the tool_hooks seam branch (th-1f694a, rev 5661772) which was not on
  main; resolved handler.rs/runner.rs merge with main's newer
  persist_workflow_step (th-c12df5) path, dropped the seam's stale duplicate
  cap_* binding, and unioned the runner import (LlmClient + ToolCall/ToolResult).
- injection_seams test: add the multimodal `images` field main introduced after
  the seam branch was cut.

Core 0.16.2 -> 1.7.0 required no server source changes beyond the merge —
ToolHook::post_call already took `&mut ToolResult` at 0.16.2. Engine 1.7.0
exports PermissionHook / AutoMode / DenyPolicy (+ with_deny_policy) from the
crate root for the daemon to use.

cargo build + cargo test green for smooth-operator-server and
smooai-smooth-operator (210+ tests, 0 failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y turn (#330)

The engine core already supported auto-recall (AgentConfig::with_memory →
build_context_messages → memory.recall(msg, 5)), but the server never wired
it: StorageAdapter had checkpoints()/knowledge() but no memory accessor, and
runner.rs built each AgentConfig without .with_memory().

Add StorageAdapter::memory_for_access(&access) -> Option<Arc<dyn Memory>>
(default None → every backend byte-for-byte unchanged; hosted auto-recall
stays a deliberate opt-in). runner.rs now calls .with_memory() when the adapter
returns Some. The in-memory conformance adapter gains a with_memory() builder +
override so the seam is exercised end-to-end (2 new injection_seams tests, 8/8
pass). Lights up Big Smooth: its SQLite adapter overrides memory_for_access to
return its store, injecting remembered preferences into every turn.


Claude-Session: https://claude.ai/code/session_017QkUKdJ4Xbx7WXV2FAg555

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Turns were spawned detached (tokio::spawn, no handle) and the canonical WS
protocol had no cancel message, so a turn that went off the rails could not be
stopped: the user's only recourse was another message, which spawned a SECOND
concurrent turn racing the first. That is the "Big Smooth is being weird and I
can't stop it" report.

AppState now tracks a per-conversation CancellationToken, registered BEFORE the
turn is spawned so there is no window where a turn runs uninterruptibly. The
turn task races the token against its turn future in a tokio::select!, so
cancelling drops the turn future at its next await point and tears down the
session's confirmation/interaction parks. Registrations carry a monotonic
sequence number so a straggler turn's cleanup cannot disarm the Stop button for
the turn that replaced it.

The stopped turn closes out with a normal eventual_response on its OWN
requestId, so clients that know nothing about `interrupt` still resolve their
streaming state — no new event type, no client codegen needed.
A growing set of frontier models accept only their default temperature
and 400 the entire request:

  Unsupported value: 'temperature' does not support 0 with this model.
  Only the default (1) value is supported.

The symptom does not look like a config error. The server boots, accepts
the turn, every LLM call 400s, and the user sees an assistant that
silently says nothing. Downstream (Big Smooth) this made the whole model
picker a no-op: every model except the default produced an empty reply.

A per-model allowlist was the obvious fix and is provably wrong — the
behaviour does not follow the names. Measured against llm.smoo.ai:

  rejects temperature 0 : gpt-5.1, gpt-5.4-pro, gpt-5.5,
                          claude-opus-4-7, claude-opus-4-8,
                          claude-sonnet-5, claude-fable-5
  accepts it            : gpt-5, gpt-5.2, gpt-5.4, claude-haiku-4-5,
                          claude-sonnet-4-5/4-6, claude-opus-4-6,
                          gemini-3.5-flash, deepseek-v4-flash/pro,
                          glm-5.1, minimax-m2.7, groq-gpt-oss-20b

gpt-5.1 rejects while gpt-5.2 accepts; gpt-5.4 accepts while
gpt-5.4-pro rejects. There is no prefix rule to write. 1.0 was accepted
by all 12 models tested across 6 families, so it is the single value
that works everywhere — at the cost of temperature-0 determinism on the
models that would allow it, which is a fair trade against "only the
default model works".

`llm_config_with_key` (handler.rs:1112) is the turn's config, so this is
the load-bearing site. Verified end-to-end against a real gateway: with
this change gpt-5.5 goes from producing nothing at all to PASSing a
Big Smooth conversation scenario 5/5/5/5 with zero LLM errors, and
claude-sonnet-5 likewise runs.

Branched from e9ce68c (the rev smooth pins) rather than main, so smooth
can consume the fix without a 1.23 -> 1.36 version jump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P83HrAKaqp2ehxQcW7m6QU
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b8acb3b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@smooai/smooth-operator Patch
@smooai/smooth-operator-web-chat-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager

Copy link
Copy Markdown
Contributor Author

Not for merge into main — this branch is the pin anchor smooth consumes (b8acb3b), deliberately branched from e9ce68c to avoid a 1.23 → 1.36 version jump. It conflicts with main by construction. The forward-port to main is a separate PR; keep this branch alive as long as smooth pins it.

brentrager added a commit that referenced this pull request Aug 8, 2026
…ls (#334)

Forward-port of the fix already shipped on the pinned lineage (b8acb3b,
PR #333, branched from e9ce68c so smooth could consume it without a
1.23 -> 1.36 jump). This is the same change on main so future releases
carry it.

A growing set of frontier models accept only their default temperature
and 400 the entire request:

  Unsupported value: 'temperature' does not support 0 with this model.
  Only the default (1) value is supported.

The symptom does not look like a config error. The server boots, accepts
the turn, every LLM call 400s, and the user sees an assistant that
silently says nothing. Downstream this made Big Smooth's entire model
picker a no-op — every model except the default returned an empty reply.

A per-model allowlist was the obvious fix and is provably wrong; the
behaviour does not follow the names. Measured against llm.smoo.ai:

  rejects temperature 0 : gpt-5.1, gpt-5.4-pro, gpt-5.5,
                          claude-opus-4-7, claude-opus-4-8,
                          claude-sonnet-5, claude-fable-5
  accepts it            : gpt-5, gpt-5.2, gpt-5.4, claude-haiku-4-5,
                          claude-sonnet-4-5/4-6, claude-opus-4-6,
                          gemini-3.5-flash, deepseek-v4-flash/pro,
                          glm-5.1, minimax-m2.7, groq-gpt-oss-20b

gpt-5.1 rejects while gpt-5.2 accepts; gpt-5.4 accepts while
gpt-5.4-pro rejects. 1.0 was accepted by all 12 models tested across 6
families.

Verified end-to-end on the pinned lineage against a real gateway:
gpt-5.5 went from producing nothing at all to passing Big Smooth
conversation scenarios 5/5/5/5 with zero LLM errors.

112 server tests pass.


Claude-Session: https://claude.ai/code/session_01P83HrAKaqp2ehxQcW7m6QU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brentrager

brentrager commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded — all four things on this branch are resolved, so there is nothing left to extract.

This is a mixed branch: the title names the temperature fix, but the diff carries 26 files and three unrelated changesets. Judged against current main, piece by piece:

piece verdict
temperature 1.0 (the headline) Landed via #334 (ce26442). DEFAULT_TEMPERATURE is in config.rs with the measured model table and a pinning test. Nothing to extract.
memory-recall-seam Landed. memory_for_access is on main in smooth-operator/src/adapter.rs, adapters/in-memory, server/src/runner.rs and tests/injection_seams.rs.
workflow-step-attempt-cap Landedapply_step_cap is called from handler.rs. (It landed with a duplicated-binding wart, filed separately as th-fc07ac; not a reason to hold this PR open.)
interrupt-running-turn Not on main — and superseded by design. spec/actions/interrupt.schema.json and tests/interrupt_turn.rs are absent, but main ships spec/actions/cancel.schema.json for the same feature. Both schemas describe themselves as the protocol's "Stop button", both abort the running turn at its next await point.

On that last row: cancel and interrupt are the same feature with two different terminal contracts. cancel (shipped) emits a terminal cancelled event and treats no-active-turn as a silent no-op. interrupt (this branch) closes out with a normal eventual_response and reports NO_ACTIVE_TURN. Main picked the cancelled variant and the Rust/TS servers implement it end-to-end (TurnCancelledError, turn-cancel tests). Landing interrupt now would add a second, near-duplicate stop action to the protocol.

If anyone still wants interrupt's two behavioural differences, that is a small delta on top of the existing cancel action — not this 26-file branch. Recording it here rather than filing a speculative pearl, since main has already made the design call.

Closing. Triage pearl th-b093c7.

@brentrager brentrager closed this Aug 16, 2026
@brentrager

Copy link
Copy Markdown
Contributor Author

⚠️ Do not delete the th-c127d1-temperature-pinned branch.

Closing this PR is only about the PR — per the pin-anchor note above, smooth consumes b8acb3b off this branch, and closing (unlike merging) does not delete it. Verified after closing: refs/heads/th-c127d1-temperature-pinned still resolves to b8acb3b46eb07f75711d7c39d2d8f4db183dddd4 on origin.

The close is in fact consistent with that note — "not for merge into main" means this PR should never have been a merge candidate. It stays closed; the branch stays alive as long as smooth pins it.

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