Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Each release's page on GitHub has its notes and measurements. See [the 1.0.0 rel

- Docs: the pull request pre-flight asks a PR that adds, finishes or drops a family's optimization to update that family's cell in the optimization ledger (#2243). Thanks to @jschmied.
- Server: a refused `--api-key-file` logs the path it was given, the mode the file system reported and which test failed, once at startup and once when a reload is refused (#2365). Thanks to @Whq02.
- CLI: `tensorfold models --json` prints the cached checkpoints as one document (`schema` 1, each checkpoint's `repo`, `family` and `model_type`); the lines are unchanged (#2364). Thanks to @Whq02.

- Server: tool calls completed just before the thinking-close marker are returned when no answer follows, whole and
streamed; calls followed by more reasoning or an answer stay in reasoning (#2313, fixes #2310). Thanks to @AdrianBinDC.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,14 @@ On Metal, each of up to 128 streams in the 27B's shared rounds drafts its own DF
```sh
tensorfold pull TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit
tensorfold models
tensorfold models --json
tensorfold info TensorFold/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit
```

`pull` downloads a checkpoint into the Hugging Face cache and checks what it downloads against the Hub: large files by sha256, small files by their git blob sha1.
Large files come down in pieces over up to 16 connections, and an interrupted pull resumes with the pieces it lacks.
It refuses a checkpoint whose `model_type` has no native family, and fetches the 27B's DFlash2 drafter for `--drafter`. `models` lists the cached checkpoints with a model type it knows, and `info` shows one checkpoint's family, format, context and memory floor.
`models --json` prints the same list as one document for programs: `schema` 1, and each checkpoint's `repo`, `family` and `model_type`; `family` is a display title and `model_type` is the key to match on.
`tensorfold serve REPO` serves a cached checkpoint by its repository name.

## Context compaction
Expand Down
7 changes: 4 additions & 3 deletions zig/src/cli/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const pull_exlr8 = @import("pull_exlr8.zig");
const lora_merge = @import("core").lora_merge;

const usage =
\\usage: tensorfold models
\\usage: tensorfold models [--json]
\\ tensorfold info MODEL
\\ tensorfold pull REPO[@REVISION]
\\ tensorfold pull REPO[@REVISION] --exlr8 PROFILE --node R/N --dir DIR
Expand Down Expand Up @@ -38,11 +38,12 @@ pub fn main(init: std.process.Init, argv: []const [:0]const u8) !u8 {
for (args, argv) |*x, y| x.* = y;
const env = init.environ_map;
if (std.mem.eql(u8, args[0], "models")) {
if (args.len != 1) {
const json = args.len == 2 and std.mem.eql(u8, args[1], "--json");
if (args.len != 1 and !json) {
try err.interface.writeAll(usage);
return 2;
}
return models.run(a, init.io, &out.interface, env, null);
return models.run(a, init.io, &out.interface, env, null, json);
}
if (std.mem.eql(u8, args[0], "pull")) {
if (args.len < 2) {
Expand Down
69 changes: 56 additions & 13 deletions zig/src/cli/models.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,38 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const hub = @import("hub.zig");

/// Lists each cache checkpoint whose model_type a Zig family serves. Returns the process exit code.
pub fn run(a: Allocator, io: std.Io, out: *std.Io.Writer, env: ?*const std.process.Environ.Map, override: ?[]const u8) !u8 {
/// One listed checkpoint: the facts a line prints, and the fields of a `--json` entry.
const Listed = struct { repo: []const u8, family: []const u8, model_type: []const u8 };

/// The cache checkpoints a Zig family serves, as lines or as one JSON document. Returns the process exit code.
pub fn run(a: Allocator, io: std.Io, out: *std.Io.Writer, env: ?*const std.process.Environ.Map, override: ?[]const u8, json: bool) !u8 {
const root = try hub.cacheDir(a, env, override);
var dir = std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch {
if (json) return printJson(out, &.{});
try out.print("no checkpoints on disk: the Hugging Face cache at {s} does not exist yet; run: tensorfold pull OWNER/NAME\n", .{root});
return 0;
};
defer dir.close(io);
var found: usize = 0;
var listed: std.ArrayList(Listed) = .empty;
var it = dir.iterate();
while (try it.next(io)) |entry| {
if (entry.kind != .directory) continue;
const repo = (repoFromDirName(a, entry.name) catch continue) orelse continue;
const snapshot = (try hub.cachedSnapshot(a, io, root, repo)) orelse continue;
const model_type = hub.modelType(a, io, snapshot);
const family = hub.family(model_type) orelse continue;
try out.print("{s}: {s} ({s})\n", .{ repo, family.title, family.model_type });
found += 1;
try listed.append(a, .{ .repo = repo, .family = family.title, .model_type = family.model_type });
}
if (found == 0) try out.print("no checkpoints on disk that a registered Zig family can serve; run: tensorfold pull OWNER/NAME\n", .{});
if (json) return printJson(out, listed.items);
for (listed.items) |m| try out.print("{s}: {s} ({s})\n", .{ m.repo, m.family, m.model_type });
if (listed.items.len == 0) try out.print("no checkpoints on disk that a registered Zig family can serve; run: tensorfold pull OWNER/NAME\n", .{});
return 0;
}

/// `{"schema": 1, "models": [...]}`: the same checkpoints the lines print, one object each, for programs.
fn printJson(out: *std.Io.Writer, listed: []const Listed) !u8 {
try std.json.Stringify.value(.{ .schema = 1, .models = listed }, .{}, out);
try out.writeByte('\n');
return 0;
}

Expand All @@ -36,29 +48,60 @@ fn repoFromDirName(a: Allocator, name: []const u8) !?[]const u8 {
return try std.fmt.allocPrint(a, "{s}/{s}", .{ rest[0..sep], rest[sep + 2 ..] });
}

test "models lists serve-able checkpoints and skips families Zig cannot serve" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const a = arena_state.allocator();
const io = std.testing.io;
/// A cache with one checkpoint a Zig family serves (Org/Flash) and one none does (Org/Draft); the caller deletes it.
fn syntheticCache(a: Allocator, io: std.Io) ![]const u8 {
const root = try std.fmt.allocPrint(a, ".tf-models-test-{d}", .{std.Io.Clock.awake.now(io).toNanoseconds()});
defer std.Io.Dir.cwd().deleteTree(io, root) catch {};
const w = std.Io.Dir.cwd();
try w.createDirPath(io, try std.fs.path.join(a, &.{ root, "models--Org--Flash/snapshots/rev1" }));
try w.createDirPath(io, try std.fs.path.join(a, &.{ root, "models--Org--Flash/refs" }));
try w.writeFile(io, .{ .sub_path = try std.fs.path.join(a, &.{ root, "models--Org--Flash/refs/main" }), .data = "rev1" });
try w.writeFile(io, .{ .sub_path = try std.fs.path.join(a, &.{ root, "models--Org--Flash/snapshots/rev1/config.json" }), .data = "{\"model_type\": \"qwen4_exp\", \"quantization\": {\"bits\": 6, \"group_size\": 32}}" });
try w.createDirPath(io, try std.fs.path.join(a, &.{ root, "models--Org--Draft/snapshots/rev2" }));
try w.writeFile(io, .{ .sub_path = try std.fs.path.join(a, &.{ root, "models--Org--Draft/snapshots/rev2/config.json" }), .data = "{\"model_type\": \"gemma3n\"}" });
return root;
}

test "models lists serve-able checkpoints and skips families Zig cannot serve" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const a = arena_state.allocator();
const io = std.testing.io;
const root = try syntheticCache(a, io);
defer std.Io.Dir.cwd().deleteTree(io, root) catch {};

var out: std.Io.Writer.Allocating = .init(a);
const code = try run(a, io, &out.writer, null, root);
const code = try run(a, io, &out.writer, null, root, false);
try std.testing.expectEqual(@as(u8, 0), code);
const listed = out.written();
try std.testing.expect(std.mem.indexOf(u8, listed, "Org/Flash: Qwen3.8 Flash Next (qwen4_exp)") != null);
try std.testing.expect(std.mem.indexOf(u8, listed, "Org/Draft") == null);
}

test "models --json prints the listed checkpoints as one document, and an absent cache as none" {
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const a = arena_state.allocator();
const io = std.testing.io;
const root = try syntheticCache(a, io);
defer std.Io.Dir.cwd().deleteTree(io, root) catch {};

var out: std.Io.Writer.Allocating = .init(a);
try std.testing.expectEqual(@as(u8, 0), try run(a, io, &out.writer, null, root, true));
var parsed = try std.json.parseFromSlice(std.json.Value, a, out.written(), .{});
defer parsed.deinit();
try std.testing.expectEqual(@as(i64, 1), parsed.value.object.get("schema").?.integer);
const models = parsed.value.object.get("models").?.array.items;
try std.testing.expectEqual(@as(usize, 1), models.len);
try std.testing.expectEqualStrings("Org/Flash", models[0].object.get("repo").?.string);
try std.testing.expectEqualStrings("Qwen3.8 Flash Next", models[0].object.get("family").?.string);
try std.testing.expectEqualStrings("qwen4_exp", models[0].object.get("model_type").?.string);

var none: std.Io.Writer.Allocating = .init(a);
const missing = try std.fs.path.join(a, &.{ root, "missing" });
try std.testing.expectEqual(@as(u8, 0), try run(a, io, &none.writer, null, missing, true));
try std.testing.expectEqualStrings("{\"schema\":1,\"models\":[]}\n", none.written());
}

fn has(text: []const u8, needle: []const u8) bool {
if (std.mem.indexOf(u8, text, needle) != null) return true;
std.debug.print("missing \"{s}\" in:\n{s}\n", .{ needle, text });
Expand Down
4 changes: 2 additions & 2 deletions zig/src/server/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub fn main(init: std.process.Init) !u8 {
return 0;
}
if (argv.len == 2 and (std.mem.eql(u8, argv[1], "--help") or std.mem.eql(u8, argv[1], "-h"))) {
try std.Io.File.stdout().writeStreamingAll(io, "usage: tensorfold-native --version | capabilities --json | models | info MODEL | pull REPO[@REVISION] | slide rollback FOLDER | slide audit [flags] | serve MODEL [flags] | benchmark MODEL [flags]\n" ++ usage_line);
try std.Io.File.stdout().writeStreamingAll(io, "usage: tensorfold-native --version | capabilities --json | models [--json] | info MODEL | pull REPO[@REVISION] | slide rollback FOLDER | slide audit [flags] | serve MODEL [flags] | benchmark MODEL [flags]\n" ++ usage_line);
return 0;
}
if (argv.len >= 3 and std.mem.eql(u8, argv[1], "serve")) {
Expand Down Expand Up @@ -87,7 +87,7 @@ pub fn main(init: std.process.Init) !u8 {
} else return benchmark.main(init, argv[2..], engines.capabilities(a));
}
if (argv.len < 2 or !std.mem.eql(u8, argv[1], "serve")) {
std.debug.print("usage: tensorfold-native capabilities --json | models | info MODEL | pull REPO[@REVISION] | slide rollback FOLDER | slide audit [flags] | serve MODEL [flags] | benchmark MODEL [flags]\n", .{});
std.debug.print("usage: tensorfold-native capabilities --json | models [--json] | info MODEL | pull REPO[@REVISION] | slide rollback FOLDER | slide audit [flags] | serve MODEL [flags] | benchmark MODEL [flags]\n", .{});
return 2;
}
const started = std.Io.Clock.awake.now(io).toNanoseconds();
Expand Down
Loading