Skip to content

Commit eb8a49d

Browse files
Peter MarreckPeter Marreck
authored andcommitted
feat: parse default_model from oMLX /health and write to config
Auto-detect now extracts the model name from oMLX's /health response and writes embedding_model to config.ini. oMLX detection stays model_available=false since auth can't be verified via /health alone, so init prompts for lexical-only and tells user to set API key.
1 parent 05042fc commit eb8a49d

1 file changed

Lines changed: 47 additions & 15 deletions

File tree

src/main.zig

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ pub fn main() !void {
282282
if (detected) |d| {
283283
if (d.model_available) {
284284
// Server found with model — write config and proceed with embeddings
285+
const model_name = d.default_model orelse settings.embedding_model;
285286
_ = stderr.print(" Detected {s} on {s} with model '{s}'. Saved to .codescan/config.ini.\n", .{
286-
if (d.dialect == .ollama) "Ollama" else "oMLX", d.url, settings.embedding_model,
287+
if (d.dialect == .ollama) "Ollama" else "oMLX", d.url, model_name,
287288
}) catch {};
288289
_ = stderr.flush() catch {};
289290
use_embeddings = true;
@@ -295,8 +296,13 @@ pub fn main() !void {
295296
d.url, settings.embedding_model, settings.embedding_model,
296297
}) catch {};
297298
} else {
298-
_ = stderr.print(" Found oMLX on {s}. Configure embedding_api_key in .codescan/config.ini\n" ++
299-
" then run 'codescan index' for semantic search.\n", .{d.url}) catch {};
299+
if (d.default_model) |dm| {
300+
_ = stderr.print(" Found oMLX on {s} with model '{s}'.\n" ++
301+
" Set embedding_api_key in .codescan/config.ini then run 'codescan index'.\n", .{ d.url, dm }) catch {};
302+
} else {
303+
_ = stderr.print(" Found oMLX on {s}. Configure embedding_api_key in .codescan/config.ini\n" ++
304+
" then run 'codescan index' for semantic search.\n", .{d.url}) catch {};
305+
}
300306
}
301307
_ = stderr.flush() catch {};
302308
_ = stderr.print(" Index in lexical-only mode? [Y/n] ", .{}) catch {};
@@ -306,8 +312,8 @@ pub fn main() !void {
306312
return;
307313
}
308314
}
309-
// Write detected server config
310-
writeDetectedConfig(allocator, config_root, d.url, d.dialect) catch |err| {
315+
// Write detected server config (including model name if discovered)
316+
writeDetectedConfig(allocator, config_root, d.url, d.dialect, d.default_model) catch |err| {
311317
_ = stderr.print(" warning: could not update config: {s}\n", .{@errorName(err)}) catch {};
312318
_ = stderr.flush() catch {};
313319
};
@@ -332,10 +338,11 @@ pub fn main() !void {
332338

333339
const emb_url = if (detected) |d| d.url else settings.embedding_url;
334340
const emb_dialect = if (detected) |d| d.dialect else settings.embedding_dialect;
341+
const emb_model = if (detected) |d| (d.default_model orelse settings.embedding_model) else settings.embedding_model;
335342
var embedder_adapter = embedding.HttpEmbedder{
336343
.transport = http_client.transport(),
337344
.base_url = emb_url,
338-
.model = settings.embedding_model,
345+
.model = emb_model,
339346
.dialect = emb_dialect,
340347
.auth_header = settings.embedding_auth_header,
341348
};
@@ -659,10 +666,11 @@ pub fn main() !void {
659666

660667
const emb_url = if (detected) |d| d.url else settings.embedding_url;
661668
const emb_dialect = if (detected) |d| d.dialect else settings.embedding_dialect;
669+
const emb_model = if (detected) |d| (d.default_model orelse settings.embedding_model) else settings.embedding_model;
662670
var embedder_adapter = embedding.HttpEmbedder{
663671
.transport = http_client.transport(),
664672
.base_url = emb_url,
665-
.model = settings.embedding_model,
673+
.model = emb_model,
666674
.dialect = emb_dialect,
667675
.auth_header = settings.embedding_auth_header,
668676
};
@@ -1713,6 +1721,7 @@ const DetectedServer = struct {
17131721
url: []const u8,
17141722
dialect: embedding_http.ApiDialect,
17151723
model_available: bool,
1724+
default_model: ?[]const u8 = null, // from oMLX /health response
17161725
};
17171726

17181727
/// Probes well-known embedding server ports and returns the first that responds.
@@ -1786,10 +1795,13 @@ fn probeOpenAI(
17861795
})) |response| {
17871796
defer allocator.free(response.body);
17881797
if (response.status == 200) {
1798+
// Try to parse default_model from health response
1799+
const model_name = parseHealthDefaultModel(allocator, response.body);
17891800
return .{
17901801
.url = base_url,
17911802
.dialect = .openai,
1792-
.model_available = false, // /health confirms server exists but not auth/model
1803+
.model_available = false, // can't verify auth via /health alone
1804+
.default_model = model_name,
17931805
};
17941806
}
17951807
} else |_| {}
@@ -1822,6 +1834,18 @@ fn buildUrl(allocator: std.mem.Allocator, base_url: []const u8, path: []const u8
18221834

18231835
/// Prompts the user with a yes/no question. Returns true for yes.
18241836
/// In non-TTY mode, returns `non_tty_default`.
1837+
/// Parse "default_model" from an oMLX /health JSON response.
1838+
/// Returns an allocator-owned string, or null if not found.
1839+
fn parseHealthDefaultModel(allocator: std.mem.Allocator, body: []const u8) ?[]const u8 {
1840+
var parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return null;
1841+
defer parsed.deinit();
1842+
1843+
if (parsed.value != .object) return null;
1844+
const model_value = parsed.value.object.get("default_model") orelse return null;
1845+
if (model_value != .string) return null;
1846+
return allocator.dupe(u8, model_value.string) catch null;
1847+
}
1848+
18251849
fn promptYesNo(stderr: *std.Io.Writer, non_tty_default: bool) bool {
18261850
_ = stderr.flush() catch {};
18271851
if (!std.fs.File.stdin().isTty()) return non_tty_default;
@@ -1832,17 +1856,21 @@ fn promptYesNo(stderr: *std.Io.Writer, non_tty_default: bool) bool {
18321856
return input_buf[0] == 'y' or input_buf[0] == 'Y';
18331857
}
18341858

1835-
fn writeDetectedConfig(allocator: std.mem.Allocator, config_root: []const u8, url: []const u8, dialect: embedding_http.ApiDialect) !void {
1859+
fn writeDetectedConfig(allocator: std.mem.Allocator, config_root: []const u8, url: []const u8, dialect: embedding_http.ApiDialect, detected_model: ?[]const u8) !void {
18361860
const cfg_path = try configPath(allocator, config_root);
18371861
defer allocator.free(cfg_path);
18381862
const content = try std.fs.cwd().readFileAlloc(allocator, cfg_path, 64 * 1024);
18391863
defer allocator.free(content);
18401864
const dialect_str = if (dialect == .ollama) "ollama" else "openai";
1841-
const kvs = [_]config.KV{
1842-
.{ .key = "embedding_url", .value = url },
1843-
.{ .key = "embedding_api", .value = dialect_str },
1844-
};
1845-
const updated = try config.writeConfigValues(allocator, content, &kvs);
1865+
var kvs_buf: [3]config.KV = undefined;
1866+
kvs_buf[0] = .{ .key = "embedding_url", .value = url };
1867+
kvs_buf[1] = .{ .key = "embedding_api", .value = dialect_str };
1868+
var kv_count: usize = 2;
1869+
if (detected_model) |m| {
1870+
kvs_buf[2] = .{ .key = "embedding_model", .value = m };
1871+
kv_count = 3;
1872+
}
1873+
const updated = try config.writeConfigValues(allocator, content, kvs_buf[0..kv_count]);
18461874
defer allocator.free(updated);
18471875
const file = try std.fs.cwd().createFile(cfg_path, .{ .truncate = true });
18481876
defer file.close();
@@ -5759,7 +5787,7 @@ pub fn runRegexSearch(
57595787
const OpenAIFallbackMock = struct {
57605788
fn send(_: *anyopaque, allocator: std.mem.Allocator, req: embedding_http.HttpRequest) !embedding_http.HttpResponse {
57615789
if (std.mem.endsWith(u8, req.url, "/health")) {
5762-
return .{ .status = 200, .body = try allocator.dupe(u8, "{\"status\":\"healthy\"}") };
5790+
return .{ .status = 200, .body = try allocator.dupe(u8, "{\"status\":\"healthy\",\"default_model\":\"test-model-mlx\"}") };
57635791
}
57645792
if (std.mem.endsWith(u8, req.url, "/v1/models")) {
57655793
return .{ .status = 200, .body = try allocator.dupe(u8, "{\"data\":[]}") };
@@ -5814,6 +5842,10 @@ test "detectEmbeddingServer finds oMLX when Ollama unavailable" {
58145842
try std.testing.expect(result != null);
58155843
try std.testing.expectEqualStrings("http://localhost:8000", result.?.url);
58165844
try std.testing.expectEqual(embedding_http.ApiDialect.openai, result.?.dialect);
5845+
try std.testing.expect(!result.?.model_available); // can't verify auth via /health
5846+
try std.testing.expect(result.?.default_model != null);
5847+
try std.testing.expectEqualStrings("test-model-mlx", result.?.default_model.?);
5848+
allocator.free(result.?.default_model.?);
58175849
}
58185850

58195851
test "detectEmbeddingServer returns null when nothing available" {

0 commit comments

Comments
 (0)