Repository navigation
fix(providers): Anthropic reload and parallel results, local probe retry - #124
adriannoes wants to merge 6 commits into
Conversation
Dashboard and SIGHUP reloads updated OpenAI and the local provider but left Anthropic on the config captured at init. The default provider then kept sending the previous model and max_tokens. Supersedes #106.
Claude often returns several tool_use blocks in one assistant turn. Each result was posted as its own user message, which the Messages API rejects, so the model never saw the tool output after the tools had already run. Supersedes #107.
Init marks llama-server unreachable once. The default chain puts local last, so router recovery never clears that flag and offline chat stays dead until restart. The next chat probes again before refusing. Supersedes #110.
There was a problem hiding this comment.
Summary
Three small, independent provider fixes that supersede #106, #107, #110: Anthropic now follows live config reload (try_config_reload -> provider_anthropic_set_live_config, same guard as OpenAI), parallel tool results are folded into one user message, and the local provider re-probes on chat after a startup miss. The Anthropic message builder is also split into short, checkable helpers. I read the diff and surrounding code; I could not build or run the tests here (no libcurl headers in this image), so test status is from reading only.
Must Fix
None.
Should Fix
src/providers/local.c:188-195: the re-probe runs on everychatwhiles_local_unavailableis set, synchronously, andagent_runholdsagent_lockat that point. A down server costs up to twocurlprobes per turn (/v1/models, then/health;LOCAL_PROBE_CONNECT_SEC2s,LOCAL_PROBE_TIMEOUT_SEC3s each). When local is the last backend in the chain and everything before it failed, that is up to a few seconds of added latency per message, plus a blocked gateway/channel for the duration. Consider a minimum interval between retries (for example one probe per 10-30s, using a statictime_tlike the router'sg_recovery_last_wall). Test code can reset it inlocal_provider_test_reset.CHANGELOG.md(Unreleased, Fixed): no entries for the three fixes. Other consolidation PRs in this tree add them.docs/LOCAL_INFERENCE.md:21: still says that if the server is unreachable at startup the provider marks itself unavailable. Add that the next request probes again.tests/test_local_provider.c:82:local_provider_test_set_skip_probe(1)makeslocal_probe_endpointreturn 1 unconditionally, so the test proves the flag clears, but not thatlocal_chatactually callsprovider_local_endpoint_reachableagainst the configured endpoint. Fine as a first cut; a loopback listener (or a probe hook) would cover the real path. The unchangedtest_init_unreachable_...does cover the still-down case.tests/test_anthropic.c:238: the grouping test only covers two results at the end of the list. Add: a single tool result (no regression of the old path), results followed by a plainusermessage (the loop must stop at the first message withouttool_use_id), and two assistant/tool rounds in a row (no merging across the assistant message). Those are the boundary conditions ofappend_tool_result_message.
Nice to Have
src/providers/anthropic.c:333: iftc->argumentsis non-empty but not valid JSON,cJSON_Parsereturns NULL and thetool_useblock is sent withoutinput, which the API rejects with a 400 that will repeat every turn for that session. Now that this is its own function it is cheap to fall back to{}(or fail closed). Pre-existing behavior, moved as-is.src/providers/anthropic.c:481:anthropic_live_config_for_testis compiled into production and declared unconditionally inprovider.h, unlike the other_for_testhooks. I see why (test_reloadlinks the non-testanthropic.o), but a short comment inprovider.hsaying so would stop someone from "fixing" it with an#ifdef.tests/test_reload.c:260: the new test is moved to the front ofmainwith no comment, and it does not cover the no-key case (provider_anthropic_set_live_configmust stay a no-op wheninitfailed). One assertion would lock thes_anth_api_keyguard.- The
cJSON_AddItemToObject(msg, "role", cJSON_CreateString(...))calls inappend_assistant_tool_contentandappend_anthropic_messagesstill ignore allocation failure (missingrolewould produce an invalid request). Same pattern as before;append_one_tool_resultalready usescJSON_AddStringToObjectwith checks, so the rest could follow.
Positive Highlights
- The tool-result fix is correct against the Messages API: all
tool_resultblocks for one assistant turn go into a singleusermessage, and the old per-result messages (a 400 for parallel tool calls) are gone. Ownership on error paths is clean:msgis attached tomsg_arrbefore the loop, so a mid-loop failure does not leak. - Splitting the 50-line inline loop in
build_and_sendintoappend_one_tool_result,append_tool_result_message,append_assistant_tool_content, andappend_anthropic_messagesmakes the code reviewable, and theanthropic_messages_json_for_testhook lets the test assert on JSON structure instead ofstrstr. Failure on OOM now returns an error instead of silently dropping a block. - The comment in
local_chatexplains why router recovery cannot clear the flag (it only probes backends before the active index, and local is last in the default chain). I checkedprovider_router_periodic_recovery_tick, and that reasoning holds.
Sent by Cursor Automation: Code review
| /* Init probes once, often before llama-server has loaded the model. | ||
| * Default fallback order puts local last, so router recovery (which | ||
| * only probes backends before the active index) never clears the flag. */ | ||
| if (s_local_unavailable) { |
There was a problem hiding this comment.
This probe is synchronous and repeats on every chat while the server is down (models probe, then health probe, up to ~2s connect / 3s total each). It runs under agent_lock. Please add a minimum retry interval so a dead llama-server does not add seconds to every turn.
| @@ -265,6 +265,111 @@ static int do_request(const char *body, provider_response_t *response) | |||
| return ret; | |||
There was a problem hiding this comment.
The loop stops at the first following message without tool_use_id, which is right. Add a test for results followed by a plain user message and for a single result, so this boundary is locked.
agent_run holds agent_lock while local_chat probes synchronously, so a down llama-server cost up to two curl probes (about 5 s) on every turn, blocking the gateway and channels for the duration. While the startup miss flag is set, re-probe at most once per LOCAL_REPROBE_MIN_SEC (15 s), tracked with a wall-clock timestamp in the router's recovery-timer style; local_provider_test_reset() clears it so tests control the window.
The grouping test only covered two results at the end of the message list, so regressions at the loop edges of append_tool_result_message would pass unnoticed. Add the boundary cases: a single tool result still yields one user message (old path unregressed), results followed by a plain user message stop grouping at the first message without a tool_use_id, and two assistant/tool rounds in a row keep each round's result message separate.
LOCAL_INFERENCE.md still ended the unreachable-startup story at the provider marking itself unavailable, with no word on recovery; state that the next request re-probes, at most once every 15 seconds. Add the CHANGELOG entries for the three fixes this PR consolidates (live config reload reaching Anthropic, parallel tool results in one user message, local re-probe after a startup miss) so the Unreleased section reflects the tree.
|
Review triage — every item mapped: Must Fix — none raised. Should Fix
Nice to Have — none changed
|


Summary
Consolidates three cursor high-severity provider fixes as operator-authored commits:
Testing
make test_reload— passedmake test_anthropic— passedmake test_local_provider— passedCI=true make test— passedmake static— passedSupersedes #106, #107 and #110. #118 (model-id gateway mapping) also touches
src/providers/anthropic.c/provider.h; it will be rebased on top of this PR as a follow-up.