Skip to content

R.2: remove model-level retry_config; decisions use core maxRetries (A6, stacked on #19) - #20

Open
cunninghamcard-bit wants to merge 11 commits into
exp/restartfrom
exp/u1R.2-remove-retry-config
Open

cunninghamcard-bit wants to merge 11 commits into
exp/restartfrom
exp/u1R.2-remove-retry-config

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Phase 1R unit R.2 (A6 item 2; D5/D6). Stacked on #19 (exp/u1R.1-remove-body-overrides@6f0aa367). Both units edit ProviderOptions and ProviderHandle::decision_model in provider_handle.rs, so until #19 merges this diff includes #19. The R.2-only change is commit 5ac114d.

Removed (line numbers at 8455db9)

  • aimux-core/src/decision_model.rs:562-566 DecisionModel::retry_config(). At :651-655, decide() now calls retry::prepare_retries(options.max_retries, abort). That is the same core path generate/moa use: call-level maxRetries, default 2, fixed delay and backoff.
  • aimux-core/src/retry.rs:18-41 RetryConfig (including the :19 doc reference) and :73-89 prepare_retries_with_config.
  • aimux-providers/src/jev.rs:26-27,38,116-118 (JevConfig.retry_config and its impl).
  • aimux-providers/src/openai/decision.rs:17,35,39-44,410-412,417 (field, with_retry_config, impl, snapshot argument).
  • aimux-providers/src/runtime_decision.rs:32,55,637-639,644.
  • aimux-providers/src/decision_support.rs:54,78: the snapshot no longer takes a retry config or records max_retries in provider_options.
  • aimux-providers/src/provider_handle.rs:15,39-40 (the handle-level ProviderOptions.max_retries default; docs: full-chain AI SDK alignment design and impact map arcships/aimux#200:1025 says not to reintroduce provider default max_retries), :195-201, and the HandleDecisionModel retry field, retry_config() and the max_retries snapshot insert. HandleDecisionModel stays only for api_key_source (R.3/R.4).
  • Not touched: scripts/check_provider_boundaries.sh (R.5) and the bindings. Node/Python/FFI already reject max_retries in the provider config and pass maxRetries per call. Node keeps its "call-level option" message (R.1: remove body_overrides; reject as unknown field (A6) #19).

Tests

  • New runtime_decision_test::call_level_max_retries_sets_the_exact_retry_count: a wiremock always returns 503 with retry-after: 0. max_retries = 0/1/3/None makes exactly 1/2/4/3 requests (N retries; the default is 2). The error is ApiCall for 0 and Retry otherwise.
  • decision_handle_test::handle_options_reach_decisions was replaced by handle_has_no_max_retries_option: {"max_retries":0} → unknown field `max_retries` .
  • jev_test / openai_decision_test: removed the initial_delay = 0 / with_retry_config setup. The retrying tests use retry-after: 0. jev_test now takes about 10s because its other retrying cases wait the default 2s backoff.

Acceptance

  • git grep -n "retry_config\|RetryConfig" -- aimux-providers aimux-core/src/decision_model.rs: no hits. The remaining repo hits are only in scripts/check_provider_boundaries.sh (rules plus the D16 allowance, R.5).

Results vs R.0 baseline (8455db9, local linux x64; test binaries rebuilt, new test names appear in the log)

R.0 R.1 (#19) R.2
cargo test --workspace 2871 / 0 failed / 7 ignored 2872 / 0 / 7 2873 / 0 / 7
fmt --check / clippy -D warnings — 0 / 0 0 / 0
check_provider_boundaries.sh 0 0 0
Python pytest 93 passed, 5 failed 95 passed, same 5 95 passed, same 5 (same KeyErrors)
Node ava 87 passed, 1 failed 93 passed, same 1 93 passed, same 1

Go/Java/Kotlin/Swift/Flutter were not run locally.

Update (8cfdf46): tests without real backoff

  • Retry counting now also lives in aimux-core decision_model::retry_tests. The first retry delay is set to zero through the crate-internal #[cfg(test)] PreparedRetries::with_initial_delay; there is no public API, feature or binding change. These tests check max_retries 1/3/None → 2/4/3 attempts for 429/503/529, with RetryErrorReason::MaxRetriesExceeded and the last error's status. max_retries=0 and non-retryable errors make one attempt.
  • jev_test: status classification runs with max_retries=0, so there's no wait. One max_retries=1 run on 503 asserts 2 requests, retries exhausted, and a 503 unavailable last error. jev_test went from 10.14s to 0.95s.
  • runtime_decision_test::call_level_max_retries_sets_the_exact_retry_count now also asserts retries-exhausted with a 503 cause.
  • cargo test: 2875 passed / 0 failed / 7 ignored. fmt, clippy and boundaries all pass.

Review follow-up

  • Review fix (a59fdbc): jev retry test runs max_retries=1 end-to-end for 429, 529 and 503 (2 requests + MaxRetriesExceeded each).

Drop ProviderOptions.body_overrides and both arcships#226 rejections in
provider_handle.rs; ProviderOptions now denies unknown fields. Node
ProviderConfig loses bodyOverrides and reports any unknown key as
InvalidArgumentError 'unknown field `x`'. Python docs updated; Python
already rejects unknown keys via serde. Tests in Rust/Python/Node.
Adds docs/amendments/A6-remove-226-per-rfc.md verbatim.
…keys)

Replace the hand-written key allowlist: the JS object is read as a raw
JSON map (undefined values skipped), maxRetries is rejected first with
the call-level message, then the map deserializes into ProviderConfig
with #[serde(deny_unknown_fields, rename_all = "camelCase")]. The
max_retries struct field is gone (index.d.ts loses maxRetries?). Tests:
maxRetries gets the call-level message, undefined keys are absent.
…ies only (A6 item 2)

Drop RetryConfig and prepare_retries_with_config from aimux-core,
DecisionModel::retry_config, the retry fields/overrides in jev,
openai/decision (with_retry_config), runtime_decision and
decision_support (snapshot no longer records max_retries), and the
handle-level ProviderOptions.max_retries default in provider_handle.
decide() uses retry::prepare_retries(options.max_retries, ...).
New mock test: max_retries = 0/1/3/None makes exactly 1/2/4/3 requests.
…n jev_test

Retry counting moves into aimux-core decision_model::retry_tests, using a
crate-internal #[cfg(test)] PreparedRetries::with_initial_delay(ZERO);
asserts attempts = max_retries + 1 and RetryErrorReason::MaxRetriesExceeded
with the last 503/429/529 error. jev_test keeps status classification at
max_retries = 0 plus one max_retries = 1 run on 503 asserting 2 requests
and retry exhaustion. No public API, feature or binding change.
… the struct

Drop #[napi(skip)] invalid: Option<String>; from_napi_value now returns the
InvalidArgumentError directly. The constructors' 3rd arg becomes ConfigArg
(dispatch on JS type) because napi's Either swallowed the member error.
Constructors accept the config argument as serde_json::Value (d.ts unchanged),
so conversion never throws synchronously; maxRetries, unknown keys and wrong
types are refused inside the async body as InvalidArgumentError. ConfigArg and
the custom ProviderConfig FromNapiValue are gone.
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