Skip to content

th-1cc9fa: model-output ceiling clamp + raise starvation defaults (TypeScript parity)#195

Open
brentrager wants to merge 1 commit into
localflavor-hardeningfrom
th-1cc9fa-ts-ceiling
Open

th-1cc9fa: model-output ceiling clamp + raise starvation defaults (TypeScript parity)#195
brentrager wants to merge 1 commit into
localflavor-hardeningfrom
th-1cc9fa-ts-ceiling

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Why

A budget/policy max_tokens can exceed what a model can physically emit. When it does, reasoning models burn the whole budget on reasoning_content and return empty content, or the upstream 400s (e.g. groq-compound caps output at 8192). Separately, the server's chat-widget-sized defaults (max_tokens=512, max_iterations=6) starve reasoning / multi-step turns.

What (TypeScript server)

Mirrors the already-merged Rust reference on this branch — rust/smooth-operator-server/src/admin.rs (model_output_ceiling / fetch_model_costs / map_model_info), config.rs (DEFAULT_MAX_TOKENS / DEFAULT_MAX_ITERATIONS), handler.rs + runner.rs (thread the ceiling into the turn) — in TS idioms:

  • modelCeiling.ts (new): best-effort per-model output ceiling from the gateway's /model/info. extractModelCeilings(payload) (pure, the map_model_info analog, narrowed to model_info.max_output_tokens) + createGatewayModelCeilingResolver(url, key, fetch?), fetched at most once per process and cached; any error / unknown model ⇒ undefined ⇒ the engine leaves max_tokens unclamped (graceful, no behaviour change).
  • TurnRunner: raise DEFAULT_MAX_TOKENS 512→8192 and DEFAULT_MAX_ITERATIONS 6→20 (safe now that the clamp bounds per-model output; a cap doesn't lengthen concise answers). Look up the resolved model's ceiling per turn and thread it into the engine via AgentOptions.modelMaxOutput. An explicit DEFAULT_MODEL (= the engine's own default) is now set so the request model and the ceiling-lookup model are always the same.
  • Thread model + modelCeiling through FrameDispatcherServerOptions / serveLocal; main.ts builds the resolver from SMOOAI_GATEWAY_URL / SMOOAI_GATEWAY_KEY (absent on the keyless local path ⇒ unclamped, behaviour unchanged).
  • Bump the @smooai/smooth-operator-core pin to ^0.20.1 — the release that introduces modelMaxOutput / effectiveMaxTokens (th-1cc9fa: model-output ceiling clamp + raise starvation defaults (TypeScript parity) smooth-operator-core#71).

Mirroring Rust

undefined/unset ceiling ⇒ no clamp, identical to the Rust None passthrough. The ceiling is sourced by the consumer (this server) from /model/info and passed into the engine, keeping LiteLLM-specific HTTP out of the published engine — same split as Rust.

Tests

  • modelCeiling: payload extraction (skips missing name / no ceiling / non-integer), resolver caching (one fetch), {url}/model/info + bearer header, no-key path, best-effort undefined on non-ok / thrown fetch (not cached → retries), single-flight across concurrent first lookups.
  • TurnRunner: raised default budget sent; clamp-down when ceiling < budget; passthrough when ceiling ≥ budget; unclamped on unknown model; ceiling looked up for the model the turn actually uses.
  • Full server suite green (57 tests), tsc build + typecheck clean (verified against a local link to the modified core).

Caveat — release ordering (see companion core PR)

The modelMaxOutput option ships in @smooai/smooth-operator-core 0.20.1 (SmooAI/smooth-operator-core#71). Until that publishes, a fresh pnpm install here can't resolve the bumped ^0.20.1 pin (the committed lockfile still points at the stale prior core). The TS server is a private package and is not built/tested in PR CI, so this doesn't gate the PR — but land/publish the core PR first, then refresh the lockfile.

🤖 Generated with Claude Code

… server)

TypeScript parity with the merged Rust reference (rust/smooth-operator-server:
admin.rs model_output_ceiling/map_model_info + config.rs DEFAULT_MAX_TOKENS/
ITERATIONS).

- modelCeiling.ts: best-effort per-model output ceiling from the gateway's
  /model/info (extractModelCeilings + createGatewayModelCeilingResolver), cached
  once per process, undefined on any error => engine leaves max_tokens unclamped.
  The TS analog of admin.rs's fetch_model_costs/model_output_ceiling/map_model_info.
- TurnRunner: raise DEFAULT_MAX_TOKENS 512->8192 and DEFAULT_MAX_ITERATIONS 6->20
  (chat-widget sizing starved reasoning models), thread the per-turn ceiling into
  the engine via AgentOptions.modelMaxOutput, and set an explicit DEFAULT_MODEL
  (= engine default) that both the request and the ceiling lookup share.
- Thread model + modelCeiling through FrameDispatcher, ServerOptions, serveLocal;
  main.ts builds the resolver from SMOOAI_GATEWAY_URL/KEY (undefined on the keyless
  local path => unclamped, behaviour unchanged).
- Bump @smooai/smooth-operator-core pin to ^0.20.1 (the release introducing
  modelMaxOutput / effectiveMaxTokens).

Tests: modelCeiling extract/resolve (caching, best-effort, auth header, single-flight)
+ TurnRunner clamp-down / passthrough / unclamped. Server suite green (57 tests).

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

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9844fdd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

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