Skip to content

feat: per-model inference parameters with extended thinking#203

Merged
philmerrell merged 18 commits into
developfrom
feature/per-model-inference-params
May 2, 2026
Merged

feat: per-model inference parameters with extended thinking#203
philmerrell merged 18 commits into
developfrom
feature/per-model-inference-params

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the global temperature/max_tokens knobs with a per-model supportedParams map keyed by canonical name (temperature, top_p, top_k, max_tokens, thinking, reasoning_effort, …). Admins author it on the model form, the runtime translates canonical names into provider-native shapes (Bedrock / OpenAI / Gemini), and users can override per-request from a new Settings → Advanced panel.
  • Anthropic extended thinking on Bedrock works end-to-end: the canonical thinking value is an int budget that the translator wraps into {type: "enabled", budget_tokens: N} under the field Strands' BedrockConfig actually forwards (additional_request_fields, not the originally-attempted additional_model_request_fields). Sampling params that conflict with thinking are suppressed before dispatch.
  • Validation at four layers so an admin or user can't ship a request Bedrock will reject: per-row form validators (min ≤ max, default ∈ [min, max]), thinking-specific FormArray validator (budget ≥ 1024, budget < max_tokens.default), Pydantic SupportedParams._check_thinking_invariants on save, and a final cross-param safety drop in _merge_inference_params at request time.

Backend

  • backend/src/apis/shared/models/models.py — new ModelParamSpec + SupportedParams types added to ManagedModel/Create/Update. _check_thinking_invariants model_validator catches budget < 1024 / >= max_tokens at config time.
  • backend/src/agents/main_agent/core/model_config.py — per-provider canonical → native translation tables, dot-path nested writes, thinking value shaping, and incompatible-sampling-param suppression. top_k and thinking route through additional_request_fields for Bedrock.
  • backend/src/apis/inference_api/chat/routes.py_merge_inference_params layers admin defaults + request overrides, respects locked: true (admin default wins), clamps numerics to bounds, and now drops thinking if >= max_tokens after the merge so direct API callers can't trigger a ValidationException from Bedrock.
  • backend/src/apis/inference_api/chat/models.pyInvocationRequest.inference_params field.
  • backend/scripts/seed_bootstrap_data.py — seeds default Claude chat models with a temperature/top_p/max_tokens supportedParams block.

Frontend

  • frontend/ai.client/src/app/admin/manage-models/model-form.page.{ts,html} — full editor for supportedParams with per-row + cross-row validators (paramRowBoundsValidator, thinkingInvariantsValidator), inline error display, and a new thinkingBudget input kind. Save button is gated on modelForm.invalid so bad rows can't ship.
  • frontend/ai.client/src/app/components/model-settings/model-settings.{ts,html} — collapsible Advanced section that renders inputs from selectedModel().supportedParams. Bounds are merged from catalog defaults + provider-specific bounds + admin spec; the thinking row's max is tightened to max_tokens − 1 and the toggle is disabled with an explanation when that window collapses below 1024. Auto-clears the thinking override if max_tokens drops below the budget. Transient amber clamp notices appear when typed values get coerced.
  • frontend/ai.client/src/app/admin/manage-models/models/managed-model.model.tsModelParamSpec, SupportedParams, KNOWN_PARAMS catalog, and the new kind: 'thinkingBudget' + incompatibleWith fields.
  • frontend/ai.client/src/app/session/services/model/model.service.ts_inferenceOverrides signal keyed by modelId, persisted in sessionStorage.
  • frontend/ai.client/src/app/session/services/chat/chat-request.service.ts — sends inference_params on the chat request body when overrides exist.

Test plan

  • Backend: cd backend && uv run python -m pytest tests/agents/main_agent/core/test_model_config.py tests/agents/main_agent/property/ tests/agents/main_agent/test_fixtures_smoke.py (76 passing, includes new tests for the Bedrock thinking shape, top_k routing, sampling-param suppression, and disabled-thinking passthrough)
  • Frontend: cd frontend/ai.client && npx ng build --configuration development (template type-check clean)
  • Admin form: add a model, configure temperature/top_p/max_tokens/thinking with bad bounds (min > max, default out of range, thinking < 1024, thinking >= max_tokens); verify each shows an inline red bullet and Save is disabled.
  • Admin form: save a Claude Sonnet 4.5 model with thinking default: 4096 and max_tokens: 8192. Confirm DDB round-trips supportedParams.
  • Chat: with that model selected, open Settings → Advanced. Toggle Extended Thinking on; send a non-trivial prompt. Verify the response renders a reasoning content block before the text (proves additional_request_fields.thinking reaches Bedrock).
  • Chat: lower Max Output Tokens to 800 → thinking toggle disables with caption "Set Max Output Tokens above 1024 to enable extended thinking (currently 800)."
  • Chat: set Max Output Tokens to 5000, enable thinking with budget 4000, then drop Max Output Tokens to 3000 → thinking auto-clamps to 2999 with amber caption.
  • Backend safety net: edit sessionStorage.inferenceParamOverrides directly to {"<modelId>": {"max_tokens": 1000, "thinking": 1024}}, send a prompt. Should succeed without reasoning, with Dropping thinking budget 1024 ... not less than max_tokens 1000 warning in inference-api logs.

Notes / follow-ups

  • Tool-use round-tripping: this PR doesn't touch the agent loop, so if Strands' BedrockModel doesn't preserve Anthropic thinking content blocks (with signature) across tool-use turns, enabling thinking on a tool-using prompt will 400 from Bedrock on the second call. Worth a separate ticket — verify behavior before turning thinking on for any prod model.
  • Phase 2 (per-user override surface) hooks already in place via ModelParamSpec.locked and the _inferenceOverrides signal — this PR is the admin-authored side; locked-respecting user UX is built but only exposed via the Advanced panel today.

🤖 Generated with Claude Code

Replaces the global temperature/max_tokens knobs with a per-model
`supportedParams` map keyed by canonical name (temperature, top_p,
top_k, max_tokens, thinking, reasoning_effort, ...). Admins author
which params apply to each model, the runtime translates canonical
names into the provider-native shape (Bedrock/OpenAI/Gemini), and
users can override per-request from a new Settings → Advanced panel.

Extended thinking on Anthropic Bedrock is the headline use case:
- Stored as an int budget per model; runtime wraps it into the
  `{type, budget_tokens}` Anthropic request shape under the field
  Strands' BedrockConfig actually forwards (`additional_request_fields`,
  not the previously-attempted `additional_model_request_fields`).
- Suppresses temperature/top_p/top_k while on (Anthropic constraint).
- Validated up front: budget >= 1024 and < max_tokens, with inline
  errors on the admin form, an "unsatisfiable" disabled state on the
  user panel when max_tokens drops below the floor, and a final
  cross-param safety drop in the merge step so direct API callers
  never ship a Bedrock-rejecting request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread backend/src/apis/inference_api/chat/routes.py Fixed
philmerrell and others added 2 commits May 1, 2026 13:35
Three tests in chat-request.service.spec.ts started failing after the
new inference_params plumbing because the mock ModelService didn't
declare the new method buildChatRequestObject now calls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses CodeQL log-injection alert (Boise-State-Development/agentcore-public-stack#628)
on the warning emitted when a model lookup fails. `model_id` is sourced
from the chat request body, so a CRLF in the value could forge extra
log lines downstream.

Adds a local _sanitize_log helper (mirroring voice_routes._sanitize_log)
and applies it to all three new log sites that interpolate model_id —
including two where the value reaches us via the managed_model record
but originated as user input on create.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread backend/src/apis/inference_api/chat/routes.py Fixed
@philmerrell

Copy link
Copy Markdown
Contributor Author

Tracking issue created: #204 — captures the user-facing approach this PR ships and the follow-up work (per-user defaults, locked-indicator UX, tool-use × thinking round-tripping, convergence with #202). Marked as related to #201 and #202.

philmerrell and others added 13 commits May 1, 2026 14:46
…ng + tool use

The persistence-side _filter_empty_text in TurnBasedSessionManager dropped
reasoningContent blocks. Anthropic requires the prior thinking block (with
its signature) to be replayed verbatim while a tool-use cycle is open;
losing it triggers `messages.X.content.Y.thinking.signature: Field required`
on subsequent Bedrock calls.

Replace the narrow allowlist with the full set of Bedrock Converse content
block keys mirrored from Strands' BedrockModel._format_request_message_content,
and warn when an unrecognized block is dropped so future Bedrock additions
don't fail silently. Add a SessionMessage round-trip test through
AgentCoreMemoryConverter that asserts the signature survives JSON
serialization.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Phil Merrell <philmerrell@boisestate.edu>
…updates (#149)

Bumps the angular group with 10 updates in the /frontend/ai.client directory:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `21.2.5` | `21.2.9` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.7` | `21.2.11` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.7` | `21.2.11` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.7` | `21.2.11` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.7` | `21.2.11` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.7` | `21.2.11` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.7` | `21.2.11` |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.6` | `21.2.9` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.6` | `21.2.9` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.7` | `21.2.11` |



Updates `@angular/cdk` from 21.2.5 to 21.2.9
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](angular/components@v21.2.5...v21.2.9)

Updates `@angular/common` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/common)

Updates `@angular/compiler` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/compiler)

Updates `@angular/core` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/core)

Updates `@angular/forms` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/forms)

Updates `@angular/platform-browser` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/platform-browser)

Updates `@angular/router` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/router)

Updates `@angular/build` from 21.2.6 to 21.2.9
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.6...v21.2.9)

Updates `@angular/cli` from 21.2.6 to 21.2.9
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.6...v21.2.9)

Updates `@angular/compiler-cli` from 21.2.7 to 21.2.11
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.11/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cli"
  dependency-version: 21.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the infra-minor-patch group with 1 update in the /infrastructure directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 25.5.2 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: infra-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ingestion

- Add xlsx_chunker.py module to convert Excel sheets to CSV and chunk by rows
- Integrate XLSX detection and routing in docling_processor.py to bypass Docling's slow table parsing
- Prepend sheet names to chunks for multi-sheet workbooks to preserve context in embeddings
- Skip empty sheets and handle None values during row conversion
- Use existing row-based CSV chunker for consistent token-based chunking across tabular formats
- Improves performance and memory usage for Excel files while maintaining header-per-chunk structure for better RAG embeddings
- Replace single-batch vector upload with batched processing (50 vectors per batch)
- Add batch size constant to prevent exceeding S3 Vectors request body limits
- Replace print statement with logger.info for consistent logging
- Add progress logging at 500-vector intervals and batch completion
- Improve docstring to clarify batch processing behavior
- Refactor vector payload construction to use list comprehension within batch loop
- Prevents request failures when storing large numbers of embeddings in a single operation
- Add 'backend/src/apis/shared/embeddings/**' to push trigger paths
- Add 'backend/src/apis/shared/embeddings/**' to pull_request trigger paths
- Ensures RAG ingestion workflow runs when shared embeddings module changes
- Add _is_likely_header() helper to identify real column headers using heuristics (text-dominant cells, non-empty threshold)
- Add _find_header_row_index_from_rows() to locate the first actual header row, skipping sparse title/banner rows at sheet start
- Refactor chunk_xlsx() to collect all non-empty rows first, then detect header offset before CSV conversion
- Update logging to report actual data row count instead of total non-empty rows
- Improves chunking accuracy for XLSX files with metadata, titles, or banner rows before the actual data table
…isn't orphaned (#207)

* fix(chat): align resume cache key with original turn so paused agent isn't orphaned

The agent cache keyed on the unbuilt `system_prompt` parameter, but the
construction snapshot persisted the *built* prompt. Resume requests passed
the built form back into `get_agent`, hashing to a different cache slot
than the original turn. The resume rebuilt a fresh agent (cache MISS),
leaving the original (paused) agent stuck under the original key. The
next non-resume turn then cache-hit the paused agent, and Strands raised
"prompt_type=<class 'str'> | must resume from interrupt with list of
interruptResponse's".

Snapshot the unbuilt prompt so resume hashes to the same key as the
original turn. Add defense-in-depth: when `get_agent` cache-hits a paused
agent on a non-resume request, evict and rebuild instead of serving the
stale state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(chat): cover OAuth-resume cache fix and clean up eviction helper

- Add `tests/apis/inference_api/test_chat_service.py` exercising the
  `get_agent` cache behavior the fix depends on: snapshot replay lands
  on the same cache slot, paused-agent eviction fires on non-resume,
  resume preserves the paused agent, healthy agents still cache-hit.
- Extract `_is_paused_on_interrupt` helper. `getattr` with defaults
  cannot raise, so the broad try/except around the check was dead and
  hid real bugs behind a `logger.exception`. Replace with a direct call.
- Expand the snapshot comment in `BaseAgent.__init__` to flag the
  cache-evicted-resume date-drift trade-off (rebuilt agent re-renders
  `Current date:` rather than preserving the original turn's date).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…208)

MessageMetadata.cost is Optional[Union[float, Dict[str, float]]]. The
streaming path produces a breakdown dict ({"total": ..., "inputCost": ...}),
which flowed through `cost = message_metadata.cost or 0.0` unchanged and
hit `Decimal(str(cost_delta))` in the DynamoDB summary writer, raising
decimal.InvalidOperation. The per-message cost record was unaffected
(its dict goes through _convert_floats_to_decimal recursively); only
the rollup path crashed, leaving the summary silently stale.

Two layers of defense:
- Upstream: _coerce_cost_total normalizes dict/float/None/NaN/inf to a
  finite float total before the summary call. Also use `or 0` (not
  .get(..., 0)) on pricing_dict reads, since managed-model rows can
  store an explicit None for cache_read pricing.
- Boundary: _safe_decimal in dynamodb_storage replaces every
  Decimal(str(cost_delta | cache_savings_delta)) site (5 total) so a
  bad value collapses to Decimal("0") instead of aborting the rollup.
…des, fix host listener

Review follow-ups for #203:

- Remove `lastTemperature` from SessionPreferences/UpdateSessionMetadataRequest
  and the writer in stream_coordinator. The read site was reaching for a
  ModelConfig field that no longer exists, so every turn was overwriting the
  stored value with None. With per-model inference-param overrides now
  persisted in sessionStorage, the field is redundant rather than just
  broken — drop it. Existing DDB rows are silently ignored on read via
  Pydantic `extra="allow"`.

- Gate the request-side passthrough in `_merge_inference_params` against a
  new `KNOWN_CANONICAL_PARAMS` allow-list (union of all provider mapping
  keys). Without this, a user could submit a future canonical key the admin
  hasn't yet bounded — or one a future provider mapping starts forwarding —
  and bypass per-model bounds.

- Replace `@HostListener` in model-settings with the `host` decorator map
  per the frontend project conventions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Phil Merrell <philmerrell@boisestate.edu>
Comment thread backend/src/apis/inference_api/chat/routes.py Fixed
philmerrell and others added 2 commits May 1, 2026 21:56
The flag had no behavioral consumer — every reference was a schema
definition, CRUD passthrough, or form binding. Reasoning capability is
now expressed per-param via `supportedParams.thinking` (Anthropic) and
`supportedParams.reasoning_effort` (OpenAI o-series), so a UI badge
that wants to show "reasoning model" can derive it from the spec.

Pydantic v2 defaults to `extra="ignore"` on ManagedModel, so existing
DDB rows with `isReasoningModel: false` load cleanly post-removal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Phil Merrell <philmerrell@boisestate.edu>
@philmerrell philmerrell merged commit c70b4af into develop May 2, 2026
33 checks passed
@philmerrell philmerrell deleted the feature/per-model-inference-params branch May 2, 2026 04:11
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.

3 participants