feat(ai): lower reasoning into bedrock requests#342
Conversation
Override `stream_simple` on the Bedrock backend so the unified `reasoning`/`thinking_budgets` controls reach the ConverseStream request, faithfully porting pi's `bedrock-converse-stream.ts` `streamSimple` (:392-438). The lowering is model-family-aware: - No reasoning: the backend short-circuits to the raw `stream`, so the request stays byte-identical to the pre-seam path (no thinking config). - Claude + adaptive (supportsAdaptiveThinking): pass reasoning + budgets through; the driver emits `thinking.type = "adaptive"` + `output_config`. - Claude, non-adaptive: fit thinking inside the output cap via the shared `adjustMaxTokensForThinking`, re-clamp to the context window, and override the clamped level's budget with `min(adjusted, max(0, maxTokens - 1024))`. - Non-Claude: reasoning passes through but the driver ignores it (`build_additional_model_request_fields` returns None), matching pi. Promote anthropic's private `clamp_reasoning` to `pub(crate)` (behavior-preserving for anthropic) and reuse the existing `adjust_max_tokens_for_thinking`; port a `BedrockModel`-typed `clamp_max_tokens_to_context` (add `context_window` to the model slice) since the anthropic one is typed to `Model<AnthropicMessagesCompat>`. Tests mirror #309 + pi's `bedrock-thinking-payload.test.ts`: adaptive Claude passthrough, non-adaptive Claude maxTokens/budget adjustment with exact numbers, non-Claude passthrough (no thinking config), and the no-reasoning byte-identical guard. Follow-up to #309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkuR3Xbx7yeqvEv6MPbFg7
straitjacket's [duplication] rule flagged the new BedrockModel-typed `clamp_max_tokens_to_context` as a 9-line/58-token clone of the Anthropic one. pi has a single generic `clampMaxTokensToContext`; the two Rust copies existed only because the model types differ. Extract the shared arithmetic into a window-keyed `clamp_max_tokens_to_context_window(context_window, ...)` core in anthropic/simple_options.rs; both dialects' typed wrappers now delegate to it. Genuine de-duplication (one implementation, as in pi), not a suppression. Behavior-preserving; all tests, clippy, and fmt stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkuR3Xbx7yeqvEv6MPbFg7
Conformance smoke: agent + ai + tui
Headline is rust-backed: passing cases in files whose module-under-test is a native (Rust addon) module. Raw all-pass is shown secondary — it is inflated by unflipped TypeScript that passes without touching any Rust.
agent: 73/180 (40.6%) · ai: 79/1296 (6.1%) · tui: 375/678 (55.3%) rust-backed = passing / total tests run, per the manifest's per-native-row AttributionA pi test file is rust-backed iff the module it primarily exercises — its module-under-test — is Per-file decisions (from each native manifest row's
CLI conformance (black-box, against the pidgin binary)
The four repointed coding-agent CLI test files spawn the compiled pidgin binary via |
Follow-up to #309. PR D of the per-driver reasoning-lowering series: wires the bedrock dialect so
SimpleStreamOptions.reasoning/thinking_budgetsreach the ConverseStream request, usingmistral_backend.rs/anthropic_backend.rs(#309) as the override template.Before / After
Before:
BedrockBackendinherited the defaultProvider::stream_simple, which extracts.baseand dropsreasoning. A caller asking for a thinking level got a request with no thinking config — the portedBedrockOptions.reasoning/thinking_budgetsfields were orphaned.After:
BedrockBackendoverridesstream_simple, mapping the seam options intoBedrockOptionsand routing through a newdriver::stream_simplethat lowers reasoning into the request exactly as pi does.How
New
driver::stream_simplefaithfully ports pi'sbedrock-converse-stream.tsstreamSimple(:392-438), which is model-family-aware::398): the backend short-circuits to the rawstream, so the request is byte-identical to the pre-seam path (no thinking config added).:403-408, gated onsupportsAdaptiveThinking): passreasoning+thinkingBudgetsstraight through; the driver'sbuild_additional_model_request_fieldsemitsthinking.type = "adaptive"+output_config.effort.:413-430): fit thinking inside the output cap via the sharedadjustMaxTokensForThinking(simple-options.ts:50), re-clamp to the context window withclampMaxTokensToContext(simple-options.ts:15), and override the clamped level's budget withmin(adjusted.thinkingBudget, max(0, maxTokens - 1024))(:428).:433-437):reasoningpasses through but the driver ignores it (build_additional_model_request_fieldsreturnsNonefor non-Claude), so no thinking config reaches the request — matching pi.base.maxTokensin every branch is pi'sbuildBaseOptionsoutput, i.e.clampMaxTokensToContext(model, context, options.maxTokens ?? model.maxTokens)(simple-options.ts:29).Helpers
clamp_reasoning(xhigh/max->high) from private topub(crate)inapi/anthropic/simple_options.rsso Bedrock can override the clamped level's budget with the same collapse pi applies (:428). Behavior-preserving for Anthropic — it still calls the helper unchanged fromadjust_max_tokens_for_thinking.pubadjust_max_tokens_for_thinking(api/anthropic/simple_options.rs).BedrockModel-typedclamp_max_tokens_to_contextinapi/bedrock.rs(the Anthropic one is typed toModel<AnthropicMessagesCompat>); it reuses the shared context-token estimator pi imports fromutils/estimate.ts. Added acontext_windowfield to the leanBedrockModelslice to feed it.is_anthropic_claude_model,supports_adaptive_thinking) were already ported for the driver'sbuild_additional_model_request_fields; promoted from private topub(crate)so the siblingstream_simplecan reuse them (no re-port).Tests (param-exact; mirror #309 + pi
bedrock-thinking-payload.test.ts)anthropic.claude-opus-4-8) +high->thinking.type = "adaptive"+output_config.effort = "high".claude-3-5-sonnet) +medium, caller cap 2000, model cap 32000 ->inferenceConfig.maxTokens = 10192,thinking.budget_tokens = 8192.mediumbudget 20000, model cap 16000 -> budget override bites:maxTokens = 16000,budget_tokens = 14976(= min(14976, 16000-1024)).meta.llama3) + reasoning -> noadditionalModelRequestFields(driver ignores).streampath.Verification
The mapping was verified directly against pi source (
vendor/pi/packages/ai/src/api/bedrock-converse-stream.ts:392-438andsimple-options.ts:15,29,47-77) since the providers co-sign session is dormant. Each branch and helper is cited against those.tslines in the code comments.CI
cargo build,cargo test -p pidgin-ai(794 lib + 9 integration passing, incl. 5 new bedrock reasoning tests),cargo clippy --all-targets -- -D warnings, andcargo fmt --checkall clean. No touched file crosses the 1500-line straitjacket file-size limit; existing duplication markers preserved.🤖 Generated with Claude Code
https://claude.ai/code/session_01GkuR3Xbx7yeqvEv6MPbFg7
Generated by Claude Code