Skip to content

providers: coerce numeric config knobs to numbers at send time#191

Merged
jeffdafoe merged 1 commit into
mainfrom
providers-coerce-numeric-config-to-numbers
May 10, 2026
Merged

providers: coerce numeric config knobs to numbers at send time#191
jeffdafoe merged 1 commit into
mainfrom
providers-coerce-numeric-config-to-numbers

Conversation

@jeffdafoe
Copy link
Copy Markdown
Owner

Symptom

salem-visitor (anthropic / claude-haiku-4-5) chat fails on every dispatch:

```
Anthropic API error 400: {"type":"error","error":{"type":"invalid_request_error",
"message":"temperature: Input should be a valid number"}}
```

Engine logs the failure, visitor never decides anything, sits forever outside the tavern.

Cause

The admin Settings panel (agents.js:449) round-trips numeric knobs through `agentSettingsConfig.value`, which inherits the string-typed values `pg` returns for `numeric` columns. The save persists them into `agent_configuration.configuration` as strings (e.g. `{"temperature":"0.7"}`), then each provider passes them straight through to the upstream API.

OpenRouter / Llama happen to coerce strings on their end, so the bug stayed latent there. Anthropic does not — it rejects with HTTP 400. OpenAI, Google, Perplexity, xAI would all fail the same way for any agent saved through the Settings panel; it just hadn't been exercised yet.

Fix

New `providers/coerce.js` exports `asNumber(v)`: returns a finite number for valid inputs, `undefined` for null / empty / NaN / Infinity / non-numeric strings.

Lives in its own module rather than `providers/index.js` to avoid the circular require chain (index.js loads each provider by name; providers can't require index.js back).

Each provider calls `asNumber` on numeric body fields before assigning. Storage stays whatever the user typed (no migration needed); each provider enforces its own type contract at send time.

Coverage:

  • anthropic.js: temperature, max_tokens
  • openai.js: temperature, max_tokens, max_completion_tokens (both /v1/responses and chat/completions paths)
  • google.js: temperature, max_tokens, top_p, top_k, thinking_budget
  • openrouter.js: temperature, max_tokens
  • perplexity.js: temperature, max_tokens
  • xai.js: temperature, max_output_tokens

Blast radius

  • Strings now become numbers before reaching the provider API. No call sites outside the providers — coerce.js is provider-internal.
  • Bad input (non-numeric string, NaN, Infinity) is silently dropped instead of being passed through. Provider falls back to its own default. Acceptable: matches the existing behavior for empty-string + sanitizeAgentConfiguration in the admin save path, just extended to the runtime contract.
  • No change for already-numeric values — `asNumber(0.7)` returns `0.7` unchanged.

Test plan

  • Local smoke test: `require('./src/services/providers')` loads cleanly (no circular dep).
  • `asNumber` matrix: '0.7' → 0.7, 0.7 → 0.7, '' → undefined, null → undefined, undefined → undefined, 'abc' → undefined, '0' → 0, 'Infinity' → undefined.
  • Deploy. Verify next salem-visitor tick (Caleb or Elias, both currently parked outside the tavern) actually reaches Anthropic and produces a tool call or speech.
  • Spot-check an OpenAI agent — should still work (was already passing numbers, this is a no-op for numeric input).
  • Spot-check Hannah Boggs (salem-vendor / openrouter) — should still work; openrouter happens to coerce on its end but now we coerce locally too.

— Home

The admin Settings panel round-trips temperature / max_tokens / top_p
through agentSettingsConfig.value, which inherits the string-typed
values pg returns for numeric columns. The save persists them into
agent_configuration.configuration as strings (e.g. "temperature":"0.7"),
then each provider passes them straight through to the upstream API.

OpenRouter / Llama coerce on their end so the bug stayed latent there.
Anthropic does not — Claude Haiku 4.5 returned HTTP 400
'temperature: Input should be a valid number' on every salem-visitor
call, so visitors couldn't act after arriving at the tavern.

Storage stays whatever the user typed (no migration). Each provider
enforces its own type contract by calling asNumber on numeric body
fields. asNumber lives in a new providers/coerce.js module to avoid
the circular require chain that would result from putting it in
providers/index.js (which loads each provider by name).

Coverage: temperature in anthropic, openai, openrouter, perplexity,
xai; max_tokens / max_completion_tokens / max_output_tokens across
the same set; top_p / top_k / thinking_budget in google.
@jeffdafoe jeffdafoe merged commit c4bf523 into main May 10, 2026
6 checks passed
@jeffdafoe jeffdafoe deleted the providers-coerce-numeric-config-to-numbers branch May 10, 2026 15:33
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