Repository navigation
refactor(providers): move the ROCm tool schema, local HTTP transport and wire formats out of providers.rs - #887
Conversation
…hema.rs providers.rs mixes provider dispatch with the ROCm tool catalog it advertises to models. Move rocm_openai_tool_definitions, rocm_openai_tool and rocm_anthropic_tool_definitions into a leaf child module, providers/tool_schema.rs. This is a pure move: the bodies are byte-identical, and the two definition functions become pub(super) because the request-body builders that read them stay in providers.rs for now. rocm_openai_tool stays private. No tool is added, renamed or re-described, and no test changes (the tests that assert on the catalog go through the request-body builders and move with them later). docs/architecture.md gains the providers/tool_schema.rs entry and a fuller providers.rs entry. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…/local_http.rs The local provider talks to an engine over a hand-rolled HTTP/1.1 client: a blocking JSON POST and a streamed POST that decodes chunked, Content-Length and close-delimited SSE bodies. It sat in providers.rs next to provider dispatch and the wire formats. Move it into a child module, providers/local_http.rs: post_json_to_local_endpoint, local_bearer_header, post_json_to_local_endpoint_body, stream_json_from_local_endpoint, read_sse_body, header_value, read_fixed_sse_body, read_close_delimited_sse_body, LOCAL_STREAM_TRUNCATED, body_truncated_at_eof, read_chunked_sse_body and parse_http_endpoint. This is a pure move: the bodies are byte-identical. The two entry points and LOCAL_STREAM_TRUNCATED (which providers.rs's tests read) become pub(super); the rest stays private. providers/proptests.rs moves to providers/local_http/proptests.rs and is declared from local_http.rs, so the property tests of the SSE readers sit with the readers; the one example test of parse_http_endpoint moves with them. The tests that drive LocalProvider against a mock server stay in providers.rs with their server helpers. No test is added, removed or renamed. local_http reaches OpenAiSseEmitter, SseBytesEmitter and ProviderStreamEvent through super; nothing in providers depends back on its private items. docs/architecture.md gains the providers/local_http.rs entry. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…re.rs The OpenAI-compatible and Anthropic request bodies, the parsers for their responses and the SSE emitters that decode a streamed answer were interleaved with provider dispatch in providers.rs. Move them into a child module, providers/wire.rs: the openai_*_request_body* and anthropic_*_request_body* builders, apply_local_model_request_defaults, local_model_should_disable_thinking, local_openai_compatible_request, the parse_openai_* and parse_anthropic_* parsers (including the SSE line parsers and the test-only content helpers), and OpenAiSseEmitter and AnthropicSseEmitter with their SseBytesEmitter impls. The SseBytesEmitter trait stays in providers.rs, with the ureq remote transport. This is a pure move: the bodies are byte-identical apart from rustfmt re-wrapping signatures that gained pub(super). The builders, the two chat-message parsers and the two emitters (with new and finish) become pub(super); everything else stays private. local_http now reaches OpenAiSseEmitter through wire, and wire reaches only the shared types in providers and tool_schema, so the dependency between them is one-way. The ten tests of the builders and parsers move into wire.rs's own test module with their leaf names unchanged, and the emitter property tests (sse_emitter_proptests.rs) are declared from wire.rs so they sit with the emitters and line parsers they test. The tests that drive LocalProvider or the remote providers against a mock server stay in providers.rs, with spawn_local_provider_test_server and spawn_sequenced_local_provider_test_server. docs/architecture.md gains the providers/wire.rs entry. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
proptest looks up its saved-failure file by the test's source path, so after providers/proptests.rs moved to providers/local_http/proptests.rs the three seeds in proptest-regressions/providers/proptests.txt were no longer replayed, silently. Move the file to the path that matches the new source location. The file's contents are unchanged. Checked that the new path is the one read: with a deliberately unparsable line appended, the local_http property tests printed 'unparsable line, ignoring' for the new path; the line was removed again. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues were identified, though tests and builds were not rerun in this review environment.
No open issues 🎉
What changed in this PR
This PR splits ROCm CLI’s provider implementation into focused child modules, continuing the assistant-module cleanup without changing provider behavior or CLI output.
Changes:
- Extracts the tool catalog, local HTTP/SSE transport, and OpenAI/Anthropic wire formats while keeping provider selection and dispatch in
providers.rs. - Moves existing tests alongside their implementations and relocates saved property-test seeds to preserve replay.
- Documents module responsibilities, visibility, and dependencies.
Tests and builds were not rerun in this review environment.
| File | Description |
|---|---|
docs/architecture.md |
Documents the provider module split and dependency boundaries. |
apps/rocm/src/providers/wire/sse_emitter_proptests.rs |
Relocates emitter property tests and updates explanatory comments. |
apps/rocm/src/providers/wire.rs |
Houses request builders, response parsers, SSE emitters, and their unit tests. |
apps/rocm/src/providers/tool_schema.rs |
Isolates the shared ROCm tool catalog and provider-specific schema formatting. |
apps/rocm/src/providers/local_http/proptests.rs |
Relocates transport property tests beside the body readers. |
apps/rocm/src/providers/local_http.rs |
Houses local HTTP transport, SSE body readers, and endpoint parsing. |
apps/rocm/src/providers.rs |
Imports extracted functionality while retaining provider orchestration and integration tests. |
apps/rocm/proptest-regressions/providers/local_http/proptests.txt |
Relocates the three saved failure seeds to match the test source path. |
Review effort: Balanced
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · e023bd1
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Review — no blocking findings
Full review of the whole change (base 3944380...head e023bd1, 4 commits, 8 files).
No ticket named. The description is self-contained: it claims a pure mechanical move of the ROCm tool schema, local HTTP/SSE transport and provider wire formats out of providers.rs into three new child modules, with no function-body or behaviour changes.
Blocking
None
Non-blocking
None
Decisions for the author
wire.rsstill bundles both the OpenAI and Anthropic wire formats (855 lines) — tradeoff. Splitting by provider (openai.rs/anthropic.rs) instead of by layer (schema/transport/wire) was a reasonable alternative grouping; the chosen layer-based split keeps the OpenAI/Anthropic request builders next to the parsers and emitters that share their shapes, which has its own value for reviewing request/response round-trips together. Not a defect either way.providers.rsstill keepsProviderAdapter, theprovider_*dispatch,LocalProvider/RemoteProvider, and the remoteureqtransport (2308 lines after this PR) — tradeoff, explicitly scoped out by the author ("a follow-up... would belong inwire.rsorlocal_http.rsafter this lands"). This PR's description accurately states what stays and why, so this is a deliberate boundary rather than an incomplete extraction.
Positive signals
- The "pure move" claim held up under independent verification: a normalized line-multiset diff (ignoring whitespace,
pub(super), andsuper::/self::prefixes) between the oldproviders.rsand the four new files showed only license headers,mod/usedeclarations, the visibility keywords the PR claims to add, new module-level doc comments, and the handful of rustfmt re-wrapped multi-line signatures — no unexplained content on either side. - Commit structure is genuinely incremental and independently buildable (tool_schema → local_http → wire → proptest-regression rename), each with a commit message that says what moved and why, which made this an easy diff to review honestly.
- The proptest regression-seed relocation (
proptest-regressions/providers/proptests.txt→.../providers/local_http/proptests.txt) is confirmed byte-identical content, just renamed, and the PR description explains why this file had to move (proptest keys saved failures by test source path) rather than leaving that as a silent side effect. docs/architecture.mdis updated in the same commit as the code it describes, and its new bullets fortool_schema.rs/local_http.rs/wire.rsaccurately match thepub(super)surface and one-way dependency edge (local_http → wire → tool_schema, no edge back) actually implemented.
Deployment notes
None — this is an internal module reorganization with no behavioural or interface change, confirmed by four independent passes plus an original-vs-moved line-multiset comparison.
What this covered
Read completely and independently verified in four parallel passes grouped by coupling: (1) providers/tool_schema.rs plus its old location and the docs/architecture.md bullet; (2) providers/local_http.rs + providers/local_http/proptests.rs plus the old providers/proptests.rs and the regression-seed rename; (3) providers/wire.rs + providers/wire/sse_emitter_proptests.rs plus the old providers/sse_emitter_proptests.rs; (4) the full remaining providers.rs diff (old 3572 lines → new 2308 lines) for completeness, orphaned code, and import/export matching against the three new modules' pub(super) surfaces. Each pass independently found the moved code byte-identical to its old location modulo visibility keywords, doc comments, and rustfmt re-wrapping. I separately re-ran this as a normalized line-multiset comparison myself (not delegated) as an additional independent check. I also checked: AGENTS.md's module-splitting guardrails (consistent — "full domain extraction" via child modules with docs/architecture.md as the module map is the documented pattern), MANIFEST.md (does not reference providers.rs, no update needed), the codebase for stale references to the old file paths (none found), and that xtask/src/ascii_output_contract.rs's proptests.rs path-fragment match still covers both new proptest files as the PR claims. CI at this commit: every completed check (build, clippy, fmt/lint, Windows build/test, CodeQL, commit sign-off, affected-crate tests, E2E consolidated report) is green; four hardware-runner E2E jobs (MI300X, W7900, Strix Halo Ubuntu nightly/release) were still queued at review time rather than failed.
Did not run: no local cargo toolchain was available in this environment, so the build/clippy/test commands the PR author reports were not independently re-executed here — this relies on the PR's own verification log and on CI's green checks for that. Prior-changes pass (whether earlier review comments on providers.rs apply here) was not run as a dedicated search of historical review threads. Not reconciled against any discussion on this PR.
🤖 Generated with Claude Code
juhovainio
left a comment
There was a problem hiding this comment.
I went through the saved diff line by line and this is a clean move. The only changes are use lines, mod declarations, file headers, pub(super) widenings, rustfmt re-wraps and doc-comment path updates; no function body changed. rocm_openai_tool stays private, and LOCAL_STREAM_TRUNCATED is imported under #[cfg(test)] only. Both proptest files and the regression seed file are renamed into the matching module directories, so the saved seeds keep replaying. The new docs/architecture.md entries link to files that exist.
This reflects a diff-only read, not a runtime check. The Strix Halo nightly and release E2E checks were red when I looked and their logs weren't available; they look like a runner issue, but worth a re-run or a comparison against main before merging.
Summary
apps/rocm/src/providers.rshad grown to about 1,600 production lines that mix four unrelated jobs: choosing and calling a provider, the ROCm tool catalog advertised to models, a hand-rolled HTTP/1.1 and SSE client for local engines, and the OpenAI-compatible and Anthropic request and response formats. Reading or reviewing any one of them meant scrolling past the others. This change moves the ROCm tool schema, the local HTTP/SSE transport and the provider wire formats into three child modules underproviders/, one commit each, plus a small fourth commit that keeps the saved property-test seeds replaying. It is part of the follow-up work on splitting the assistant modules.It is a pure move. No function body changes, no behaviour changes, no CLI output changes. The only edits beyond relocation are
uselines,pub(super)where a moved item has a caller in another module, rustfmt re-wrapping of the five signatures that became longer than a line, and doc comments.What moved
rocm_openai_tool_definitions,rocm_openai_tool,rocm_anthropic_tool_definitionsproviders.rsproviders/tool_schema.rspost_json_to_local_endpoint,local_bearer_header,post_json_to_local_endpoint_body,stream_json_from_local_endpoint,read_sse_body,header_value,read_fixed_sse_body,read_close_delimited_sse_body,LOCAL_STREAM_TRUNCATED,body_truncated_at_eof,read_chunked_sse_body,parse_http_endpointproviders.rsproviders/local_http.rsproviders/proptests.rs(property tests of the SSE body readers)providers/proptests.rsproviders/local_http/proptests.rs, declared fromlocal_http.rsproptest-regressions/providers/proptests.txt(three saved failure seeds, contents unchanged)apps/rocm/proptest-regressions/providers/apps/rocm/proptest-regressions/providers/local_http/proptests.txt(commit 4)openai_chat_request_body,openai_stream_chat_request_body,openai_local_chat_request_body,openai_local_stream_chat_request_body,openai_chat_request_body_with_stream,anthropic_chat_request_body,anthropic_stream_chat_request_body,anthropic_chat_request_body_with_stream,apply_local_model_request_defaults,local_model_should_disable_thinking,local_openai_compatible_requestproviders.rsproviders/wire.rsparse_openai_chat_message,parse_openai_tool_calls,parse_openai_sse_line,parse_anthropic_chat_message,parse_anthropic_sse_line, and the test-onlyparse_openai_chat_content,parse_anthropic_chat_content,parse_openai_sse_chat_eventsproviders.rsproviders/wire.rsOpenAiSseEmitter,AnthropicSseEmitterand theirSseBytesEmitterimplsproviders.rsproviders/wire.rsproviders/sse_emitter_proptests.rs(split-invariance and panic-guard properties of the emitters and line parsers, added by #557 while this branch was open)providers/sse_emitter_proptests.rsproviders/wire/sse_emitter_proptests.rs, declared fromwire.rsproviders.rskeeps the request/response types,ProviderAdapter, theprovider_*dispatch, theLocalProviderandRemoteProviderimpls,select_local_chat_service,ready_local_services,resolve_remote_model,remote_endpoint, theureqremote transport (post_json_with_headers,stream_json_with_headers) and theSseBytesEmittertrait.Visibility widened only where a moved item has a caller outside its new module:
post_json_to_local_endpoint,stream_json_from_local_endpointandLOCAL_STREAM_TRUNCATED(the last only read byproviders.rs's test module, imported under#[cfg(test)]), the six request-body builders,parse_openai_chat_message,parse_anthropic_chat_message, the two emitter structs with theirnewandfinish, and the two tool-definition functions. Each ispub(super); nothing becomespub(crate), and no other module in the crate referenced a moved item.Symbols that differ from the older list this work started from: none moved, disappeared or were renamed. New since that list: the test-only helpers
parse_openai_chat_content,parse_anthropic_chat_contentandparse_openai_sse_chat_events(moved with the parsers they wrap),parse_openai_tool_calls,parse_openai_sse_lineandparse_anthropic_sse_line(the SSE and tool-call parsers behind the namedparse_*families), andproviders/sse_emitter_proptests.rsfrom #557.Tests
providers.rs's test module had 35#[test]functions on this base (the older count of 34 is stale), plus the two property-test files. They are partitioned by primary subject; no test is added, removed or renamed, and the leaf-name list ofcargo test -p rocm --bin rocm -- --listand-- --list --ignoredis identical toorigin/mainafter each commit.tool_schema.rsopenai_request_body_includes_rocm_tools_only_for_non_streaming_tool_mode,anthropic_request_body_includes_rocm_tools_and_system_only_for_non_streaming_tool_mode) go through the request-body builders, so they move with the builders in commit 3 rather than twice.local_http.rslocal_provider_endpoint_parser_supports_ipv6_loopback(intolocal_http.rs'smod tests), and all ofproviders/proptests.rs(nowlocal_http/proptests.rs)wire.rsparses_openai_chat_content,parses_openai_chat_tool_calls_without_text,parses_anthropic_chat_content,parses_anthropic_tool_use_without_text,openai_and_anthropic_request_bodies_include_model_and_messages,request_bodies_include_temperature_and_top_p_only_when_set,openai_local_qwen3_request_disables_thinking_for_visible_answers, the two ROCm-tools body tests above,parses_openai_sse_chat_events, and all ofsse_emitter_proptests.rs(nowwire/sse_emitter_proptests.rs)The remaining 24 tests stay in
providers.rs: they driveLocalProvider,RemoteProvideror theprovider_*dispatch end to end against a mock server, so their primary subject is the provider rather than the transport or the format. That keepsspawn_local_provider_test_serverandspawn_sequenced_local_provider_test_serverinproviders.rs's test module; neither helper moves.Module edges
Each child's
useedges, read from the final tree. Test-only imports are marked.use super::/use crate::edgesproviders/tool_schema.rsproviders/local_http.rssuper::wire::OpenAiSseEmitter;super::{ProviderStreamEvent, SseBytesEmitter}. Test-only:super::*, and inlocal_http/proptests.rscrate::test_support::{proptest_cases, split_at_offsets}providers/wire.rssuper::tool_schema::{rocm_anthropic_tool_definitions, rocm_openai_tool_definitions};super::{ChatMessage, ChatRequest, ChatToolCall, ProviderStreamEvent, SseBytesEmitter}. Test-only:super::*, and inwire/sse_emitter_proptests.rscrate::test_support::{proptest_cases, split_at_offsets}Among the children the order is
local_http -> wire -> tool_schema, with no edge back: nothing inwire.rsortool_schema.rsnameslocal_http, andtool_schema.rsnames nothing. Every child also reads the shared types that stay inproviders.rsthroughsuper, andproviders.rsimports the children it calls, which is the ordinary parent/child relationship and adds no crate-level edge: none of the three children usescrate::outside its test-only files, socargo xtask check-crate-edgesis unaffected.Verification
Ran locally, on each of the three commits individually (checked out in turn) and again on the final head:
cargo fmt --all --checkcargo clippy -p rocm -p xtask --all-targets -- -D warningscargo test -p rocm --bin rocm -- --listand-- --list --ignored, againstorigin/main: empty after each commitcargo test -p rocm --bin rocm providers(the 24 staying tests, the moved tests and both property-test files)cargo test -p rocm --bin rocm advised_commandsandcargo test -p xtask ascii(no path pin namesproviders.rsor its children; theproptests.rsfragment inxtask/src/ascii_output_contract.rsmatches by file name and still covers the moved files)cargo xtask check-crate-edges,cargo xtask manifest --checkcargo test -p rocm --bin rocm --no-fail-fastonce on the final head: 1467 passed, 0 failed, 8 ignoredproviders.rsagainst the four new files, ignoringpub(super),super::and whitespace: the only differences areuselines, module headers and the five re-wrapped signaturesNot run here: the full workspace gate,
cargo xtask e2eandscripts/smoke_local.py(CI carries them), and the Windows cross-clippy. No moved item is behind acfg(windows)gate, and the moved code has no platform-specific imports, so the Windows lane is the check.User-visible behaviour
None, so no new scenario. This is an internal module move; the existing chat and serve scenarios that reach the local provider and its streaming path exercise the moved code unchanged.
Notes for reviewers
proptest finds its saved-failure file by the test's source path, so moving
proptests.rsalone would have silently stopped replaying the three saved seeds. Commit 4 moves the regression file to match; I checked the new path is the one read by appending an unparsable line and seeing proptest report it for the new path, then removing the line.wire/sse_emitter_proptests.rshas no regression file, here or onorigin/main.Read the commits in order. Commit 1 is the leaf (no dependencies); commit 2 moves the transport and the SSE-reader property tests, importing
OpenAiSseEmitterfrom the parent for now; commit 3 moves the wire formats and the emitter property tests and re-pointslocal_httpatwire, which makes the edge one-way. Commits 1 to 3 each build, pass clippy and pass the tests on their own; commit 4 only renames a data file.docs/architecture.mdis updated in the same commit as the code it describes.The branch was rebased once onto #557 (merged while this work was open), which added
providers/sse_emitter_proptests.rs; that file moved with the emitters in commit 3.Other open PRs that touch
providers.rs(read-only check of their current diffs): none of them edits anything this change moves, so none should conflict textually with any of the three commits. #678, #723 and #883 each add one field to a struct literal insideserve_smoke_test_reports_no_metrics_when_engine_dies_mid_chunked_stream, which stays inproviders.rs. #724 and #725 (drafts, stacked on #723) editLocalProvider::status,select_local_chat_service,ready_local_servicesand add or extend tests that drive those against a mock server, all of which stay inproviders.rs. A follow-up that touches the request-body builders, the SSE emitters or the local transport would belong inwire.rsorlocal_http.rsafter this lands.