Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
12a6006
feat(providers): add /models probe for custom endpoint validation
vernonstinebaker May 4, 2026
832743c
fix(providers): sanitize test data and fix state.zig load() dropping …
vernonstinebaker May 4, 2026
77ad598
feat(providers): wire openai-compatible through install wizard
vernonstinebaker May 4, 2026
f2b454a
fix(orchestrator): replace custom provider with 'openai' placeholder …
vernonstinebaker May 4, 2026
7310fa1
fix(orchestrator): patch model, remove openai placeholder after custo…
vernonstinebaker May 4, 2026
74efb83
perf(ui): pre-fetch revealed providers on mount so Use Saved is instant
vernonstinebaker May 4, 2026
a24af8d
fix(orchestrator): disable vision probe for custom provider models on…
vernonstinebaker May 4, 2026
73707ad
fix(ui): route openai-compatible model fetch to probe endpoint
vernonstinebaker May 4, 2026
6f4f375
fix(ui): fix wizard validation and stale provider health display
vernonstinebaker May 4, 2026
32b3076
chore: replace debug prints with std.log, fix hardcoded provider string
vernonstinebaker May 4, 2026
b827813
fix(installer): patch a2a.enabled false→true on install
vernonstinebaker May 4, 2026
5667c56
chore(pr): drop incorrect nullclaw startup workaround
vernonstinebaker May 5, 2026
dcf9f05
fix(providers): give health probes longer to finish
vernonstinebaker May 5, 2026
4bec3de
fix(providers): harden custom models probe
DonPrus May 5, 2026
ab69616
Merge origin/main into feat/models-probe
DonPrus May 5, 2026
0988dbd
fix(installer): preserve custom provider fallback order
DonPrus May 5, 2026
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
4 changes: 2 additions & 2 deletions src/api/instances.zig
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,9 @@ fn probeProviderViaComponentHealth(
model: []const u8,
) ProviderProbeResult {
const args: []const []const u8 = if (model.len > 0)
&.{ "--probe-provider-health", "--provider", provider, "--model", model, "--timeout-secs", "10" }
&.{ "--probe-provider-health", "--provider", provider, "--model", model, "--timeout-secs", "30" }
else
&.{ "--probe-provider-health", "--provider", provider, "--timeout-secs", "10" };
&.{ "--probe-provider-health", "--provider", provider, "--timeout-secs", "30" };
const result = component_cli.runWithComponentHome(
allocator,
component,
Expand Down
662 changes: 631 additions & 31 deletions src/api/providers.zig

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/api/wizard.zig
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,9 @@ fn probeProviderViaComponentBinary(
model: []const u8,
) ProviderProbeResult {
const args: []const []const u8 = if (model.len > 0)
&.{ "--probe-provider-health", "--provider", provider, "--model", model, "--timeout-secs", "10" }
&.{ "--probe-provider-health", "--provider", provider, "--model", model, "--timeout-secs", "30" }
else
&.{ "--probe-provider-health", "--provider", provider, "--timeout-secs", "10" };
&.{ "--probe-provider-health", "--provider", provider, "--timeout-secs", "30" };

const result = component_cli.runWithComponentHome(
allocator,
Expand Down
112 changes: 72 additions & 40 deletions src/core/state.zig
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ pub const State = struct {
for (self.saved_channels.items) |sc| {
if (std.mem.eql(u8, sc.channel_type, channel_type)) count += 1;
}
if (count == 0) return std.fmt.allocPrint(self.allocator, "{s}", .{label});
return std.fmt.allocPrint(self.allocator, "{s} #{d}", .{ label, count + 1 });
}

Expand All @@ -827,6 +828,10 @@ pub const State = struct {
for (self.saved_providers.items) |sp| {
if (std.mem.eql(u8, sp.provider, provider)) count += 1;
}
// Only append a numeric suffix when there is already at least one
// provider of this type — avoids the awkward "My Provider #1" for
// the common single-instance case.
if (count == 0) return std.fmt.allocPrint(self.allocator, "{s}", .{label});
return std.fmt.allocPrint(self.allocator, "{s} #{d}", .{ label, count + 1 });
}
};
Expand Down Expand Up @@ -1182,7 +1187,7 @@ test "add saved provider, save, load, verify round-trip" {
try std.testing.expectEqualStrings("openrouter", providers[0].provider);
try std.testing.expectEqualStrings("sk-or-xxx", providers[0].api_key);
try std.testing.expectEqualStrings("anthropic/claude-sonnet-4", providers[0].model);
try std.testing.expectEqualStrings("OpenRouter #1", providers[0].name);
try std.testing.expectEqualStrings("OpenRouter", providers[0].name);
try std.testing.expectEqual(@as(u32, 1), providers[0].id);

try s.save();
Expand All @@ -1196,7 +1201,7 @@ test "add saved provider, save, load, verify round-trip" {
try std.testing.expectEqual(@as(usize, 1), providers.len);
try std.testing.expectEqualStrings("openrouter", providers[0].provider);
try std.testing.expectEqualStrings("sk-or-xxx", providers[0].api_key);
try std.testing.expectEqualStrings("OpenRouter #1", providers[0].name);
try std.testing.expectEqualStrings("OpenRouter", providers[0].name);
try std.testing.expectEqual(@as(u32, 1), providers[0].id);
}
}
Expand All @@ -1216,9 +1221,9 @@ test "auto-generated name increments per provider type" {

const providers = s.savedProviders();
try std.testing.expectEqual(@as(usize, 3), providers.len);
try std.testing.expectEqualStrings("OpenRouter #1", providers[0].name);
try std.testing.expectEqualStrings("OpenRouter", providers[0].name);
try std.testing.expectEqualStrings("OpenRouter #2", providers[1].name);
try std.testing.expectEqualStrings("Anthropic #1", providers[2].name);
try std.testing.expectEqualStrings("Anthropic", providers[2].name);
}

test "update saved provider name only" {
Expand Down Expand Up @@ -1408,19 +1413,19 @@ test "add saved provider with base_url, save, load, verify round-trip" {
defer s.deinit();

try s.addSavedProvider(.{
.provider = "infini-ai",
.api_key = "sk-cp-test",
.model = "minimax-m2.7",
.base_url = "https://cloud.infini-ai.com/maas/coding/v1",
.provider = "custom-llm",
.api_key = "sk-test-key",
.model = "test-model",
.base_url = "https://example.com/v1",
});

const providers = s.savedProviders();
try std.testing.expectEqual(@as(usize, 1), providers.len);
try std.testing.expectEqualStrings("infini-ai", providers[0].provider);
try std.testing.expectEqualStrings("sk-cp-test", providers[0].api_key);
try std.testing.expectEqualStrings("minimax-m2.7", providers[0].model);
try std.testing.expectEqualStrings("https://cloud.infini-ai.com/maas/coding/v1", providers[0].base_url);
try std.testing.expectEqualStrings("infini-ai #1", providers[0].name);
try std.testing.expectEqualStrings("custom-llm", providers[0].provider);
try std.testing.expectEqualStrings("sk-test-key", providers[0].api_key);
try std.testing.expectEqualStrings("test-model", providers[0].model);
try std.testing.expectEqualStrings("https://example.com/v1", providers[0].base_url);
try std.testing.expectEqualStrings("custom-llm #1", providers[0].name);

try s.save();
}
Expand All @@ -1431,11 +1436,11 @@ test "add saved provider with base_url, save, load, verify round-trip" {

const providers = s.savedProviders();
try std.testing.expectEqual(@as(usize, 1), providers.len);
try std.testing.expectEqualStrings("infini-ai", providers[0].provider);
try std.testing.expectEqualStrings("sk-cp-test", providers[0].api_key);
try std.testing.expectEqualStrings("minimax-m2.7", providers[0].model);
try std.testing.expectEqualStrings("https://cloud.infini-ai.com/maas/coding/v1", providers[0].base_url);
try std.testing.expectEqualStrings("infini-ai #1", providers[0].name);
try std.testing.expectEqualStrings("custom-llm", providers[0].provider);
try std.testing.expectEqualStrings("sk-test-key", providers[0].api_key);
try std.testing.expectEqualStrings("test-model", providers[0].model);
try std.testing.expectEqualStrings("https://example.com/v1", providers[0].base_url);
try std.testing.expectEqualStrings("custom-llm #1", providers[0].name);
}
}

Expand All @@ -1449,7 +1454,7 @@ test "update saved provider base_url" {
defer s.deinit();

try s.addSavedProvider(.{
.provider = "infini-ai",
.provider = "custom-llm",
.api_key = "key1",
.base_url = "https://old.example.com/v1",
});
Expand All @@ -1472,9 +1477,9 @@ test "update saved provider clears base_url" {
defer s.deinit();

try s.addSavedProvider(.{
.provider = "infini-ai",
.provider = "custom-llm",
.api_key = "key1",
.base_url = "https://cloud.infini-ai.com/v1",
.base_url = "https://example.com/v1",
});
const updated = try s.updateSavedProvider(1, .{ .base_url = "" });
try std.testing.expect(updated);
Expand All @@ -1493,33 +1498,60 @@ test "multiple openai-compatible providers with different names" {
defer s.deinit();

try s.addSavedProvider(.{
.provider = "infini-ai",
.provider = "custom-llm",
.api_key = "key1",
.model = "minimax-m2.7",
.base_url = "https://cloud.infini-ai.com/maas/coding/v1",
.model = "test-model",
.base_url = "https://example.com/v1",
});
try s.addSavedProvider(.{
.provider = "infini-ai",
.provider = "custom-llm",
.api_key = "key1",
.model = "deepseek-v3",
.base_url = "https://cloud.infini-ai.com/maas/coding/v1",
.base_url = "https://example.com/v1",
});
try s.addSavedProvider(.{
.provider = "xiaomi-mimo",
.provider = "another-llm",
.api_key = "key2",
.model = "mimo-7b",
.base_url = "https://api.xiaomi.com/v1",
.model = "another-model",
.base_url = "https://other.example.com/v1",
});

const providers = s.savedProviders();
try std.testing.expectEqual(@as(usize, 3), providers.len);
try std.testing.expectEqualStrings("infini-ai #1", providers[0].name);
try std.testing.expectEqualStrings("infini-ai #2", providers[1].name);
try std.testing.expectEqualStrings("xiaomi-mimo #1", providers[2].name);
try std.testing.expectEqualStrings("infini-ai", providers[0].provider);
try std.testing.expectEqualStrings("xiaomi-mimo", providers[2].provider);
try std.testing.expectEqualStrings("https://cloud.infini-ai.com/maas/coding/v1", providers[0].base_url);
try std.testing.expectEqualStrings("https://api.xiaomi.com/v1", providers[2].base_url);
try std.testing.expectEqualStrings("custom-llm", providers[0].name);
try std.testing.expectEqualStrings("custom-llm #2", providers[1].name);
try std.testing.expectEqualStrings("another-llm", providers[2].name);
try std.testing.expectEqualStrings("custom-llm", providers[0].provider);
try std.testing.expectEqualStrings("another-llm", providers[2].provider);
try std.testing.expectEqualStrings("https://example.com/v1", providers[0].base_url);
try std.testing.expectEqualStrings("https://other.example.com/v1", providers[2].base_url);
}

test "find saved provider distinguishes base_url" {
const allocator = std.testing.allocator;
const path = try testPath(allocator, "state.json");
defer allocator.free(path);
defer cleanupTestDir();

var s = State.init(allocator, path);
defer s.deinit();

try s.addSavedProvider(.{
.provider = "custom-llm",
.api_key = "key1",
.model = "test-model",
.base_url = "https://one.example.com/v1",
});
try s.addSavedProvider(.{
.provider = "custom-llm",
.api_key = "key1",
.model = "test-model",
.base_url = "https://two.example.com/v1",
});

try std.testing.expectEqual(@as(?u32, 1), s.findSavedProviderId("custom-llm", "key1", "test-model", "https://one.example.com/v1"));
try std.testing.expectEqual(@as(?u32, 2), s.findSavedProviderId("custom-llm", "key1", "test-model", "https://two.example.com/v1"));
try std.testing.expectEqual(@as(?u32, null), s.findSavedProviderId("custom-llm", "key1", "test-model", "https://three.example.com/v1"));
}

test "openai-compatible provider base_url defaults to empty" {
Expand Down Expand Up @@ -1561,7 +1593,7 @@ test "add saved channel, save, load, verify round-trip" {
try std.testing.expectEqualStrings("telegram", channels[0].channel_type);
try std.testing.expectEqualStrings("@mybot", channels[0].account);
try std.testing.expectEqualStrings("{\"token\":\"abc\"}", channels[0].config);
try std.testing.expectEqualStrings("Telegram #1", channels[0].name);
try std.testing.expectEqualStrings("Telegram", channels[0].name);
try std.testing.expectEqual(@as(u32, 1), channels[0].id);

try s.save();
Expand All @@ -1575,7 +1607,7 @@ test "add saved channel, save, load, verify round-trip" {
try std.testing.expectEqual(@as(usize, 1), channels.len);
try std.testing.expectEqualStrings("telegram", channels[0].channel_type);
try std.testing.expectEqualStrings("@mybot", channels[0].account);
try std.testing.expectEqualStrings("Telegram #1", channels[0].name);
try std.testing.expectEqualStrings("Telegram", channels[0].name);
try std.testing.expectEqual(@as(u32, 1), channels[0].id);
}
}
Expand All @@ -1595,9 +1627,9 @@ test "channel auto-generated name increments per type" {

const channels = s.savedChannels();
try std.testing.expectEqual(@as(usize, 3), channels.len);
try std.testing.expectEqualStrings("Telegram #1", channels[0].name);
try std.testing.expectEqualStrings("Telegram", channels[0].name);
try std.testing.expectEqualStrings("Telegram #2", channels[1].name);
try std.testing.expectEqualStrings("Discord #1", channels[2].name);
try std.testing.expectEqualStrings("Discord", channels[2].name);
}

test "update saved channel name only" {
Expand Down
Loading
Loading