Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/providers/local.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @file local.c
* @brief OpenAI-compatible Chat Completions client for llama-server; init probe marks unreachable without failing startup.
* A later chat probes again so a server that was down only at startup can still serve.
*/

#define _POSIX_C_SOURCE 200809L
Expand Down Expand Up @@ -184,6 +185,14 @@ static int local_chat(const provider_message_t *messages, size_t message_count,
provider_set_error(response, "Local provider not initialized");
return -1;
}
/* 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) {
const char *endpoint = config_provider_local_endpoint(s_local_cfg);
if (endpoint && endpoint[0] != '\0' && provider_local_endpoint_reachable(endpoint))
provider_local_recovery_probe_succeeded();
}
if (s_local_unavailable) {
provider_set_error(response, "Local inference server unreachable");
return -1;
Expand Down
2 changes: 1 addition & 1 deletion src/providers/provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ void provider_openai_set_live_config(const config_t *cfg);
const provider_t *provider_local_get(void);
/** After SIGHUP reload: refresh live config reads without re-init (PRD §9 Q4). */
void provider_local_set_live_config(const config_t *cfg);
/** Lightweight GET probe via `/v1/models` or `/health`; used by init and router recovery. */
/** Lightweight GET probe via `/v1/models` or `/health`; used by init, chat retry, and router recovery. */
int provider_local_endpoint_reachable(const char *endpoint);
/** After a successful recovery GET probe on local; clears init-time unreachable flag. */
void provider_local_recovery_probe_succeeded(void);
Expand Down
47 changes: 47 additions & 0 deletions tests/test_local_provider.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,52 @@ static int test_init_unreachable_sets_unavailable_then_chat_errors(void)
return 0;
}

/**
* Init probe failure must not stick for the process lifetime. llama-server
* often is not listening yet at startup; the next chat has to probe again.
*/
static int test_chat_reprobes_when_server_comes_up(void)
{
FILE *f = fopen(TMP_CFG, "w");
config_t *cfg = NULL;
char errbuf[256];
const provider_t *p;
provider_message_t msg = { .role = "user", .content = "ping", .tool_calls = NULL,
.tool_calls_count = 0, .tool_use_id = NULL };
provider_response_t response = {0};
ASSERT(f);
local_provider_test_reset();
unsetenv("SHELLCLAW_FALLBACK_CHAIN");
unsetenv("SHELLCLAW_LOCAL_ENDPOINT");
unsetenv("SHELLCLAW_LOCAL_MODEL");
fprintf(f, "[agent]\nmodel = \"test-model\"\n");
fprintf(f, "[providers.local]\n");
fprintf(f, "endpoint = \"http://127.0.0.1:1/v1/chat/completions\"\nmodel = \"tiny\"\n");
fclose(f);
ASSERT(config_load(TMP_CFG, &cfg, errbuf, sizeof(errbuf)) == 0);
p = provider_local_get();
ASSERT(p->init(cfg) == 0);
#ifdef SHELLCLAW_TEST
ASSERT(local_provider_is_unavailable_for_test() != 0);
#endif
local_provider_test_set_skip_probe(1);
local_provider_test_set_http_response(
"{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"back online\"}}]}");
ASSERT(p->chat(&msg, 1, NULL, 0, &response) == 0);
ASSERT(response.error == 0);
ASSERT(response.content != NULL);
ASSERT(strstr(response.content, "back online") != NULL);
#ifdef SHELLCLAW_TEST
ASSERT(local_provider_is_unavailable_for_test() == 0);
#endif
p->cleanup();
provider_response_clear(&response);
local_provider_test_reset();
config_free(cfg);
remove(TMP_CFG);
return 0;
}

static int test_chat_mock_http_happy_path(void)
{
FILE *f = fopen(TMP_CFG, "w");
Expand Down Expand Up @@ -113,6 +159,7 @@ int main(void)
RUN(test_local_provider_vtable());
RUN(test_init_requires_config());
RUN(test_init_unreachable_sets_unavailable_then_chat_errors());
RUN(test_chat_reprobes_when_server_comes_up());
RUN(test_chat_mock_http_happy_path());
curl_global_cleanup();
printf("test_local_provider: all tests passed\n");
Expand Down
Loading