diff --git a/src/providers/anthropic.zig b/src/providers/anthropic.zig index 03eb63d..2e5dbbd 100644 --- a/src/providers/anthropic.zig +++ b/src/providers/anthropic.zig @@ -1365,6 +1365,10 @@ test "encodes Anthropic web search and web fetch server tools" { defer std.testing.allocator.free(body); try std.testing.expect(std.mem.indexOf(u8, body, "\"type\":\"web_search_20250305\",\"name\":\"web_search\"") != null); try std.testing.expect(std.mem.indexOf(u8, body, "\"type\":\"web_fetch_20250910\",\"name\":\"web_fetch\"") != null); + try std.testing.expectError(error.UnsupportedBuiltinTool, encodeRequest(std.testing.allocator, "claude-test", 20, .{ + .messages = &.{}, + .builtin_tools = &.{.{ .x_search = .{} }}, + })); } test "encodes Anthropic rich inputs and preserves thinking" { diff --git a/src/providers/google.zig b/src/providers/google.zig index 7ec2fae..0220dd4 100644 --- a/src/providers/google.zig +++ b/src/providers/google.zig @@ -1123,6 +1123,10 @@ test "encodes Gemini Google Search and URL Context tools" { defer std.testing.allocator.free(body); try std.testing.expect(std.mem.indexOf(u8, body, "\"googleSearch\":{}") != null); try std.testing.expect(std.mem.indexOf(u8, body, "\"urlContext\":{}") != null); + try std.testing.expectError(error.UnsupportedBuiltinTool, encodeRequest(std.testing.allocator, .{ + .messages = &.{}, + .builtin_tools = &.{.{ .x_search = .{} }}, + })); } test "encodes complete Gemini settings and tool policy" { diff --git a/src/providers/openai.zig b/src/providers/openai.zig index 457eb37..5ec6481 100644 --- a/src/providers/openai.zig +++ b/src/providers/openai.zig @@ -1371,6 +1371,10 @@ test "encodes OpenAI web search and rejects standalone web fetch" { .messages = &.{}, .builtin_tools = &.{.{ .web_fetch = .{} }}, })); + try std.testing.expectError(error.UnsupportedBuiltinTool, encodeRequest(std.testing.allocator, "gpt-test", .{ + .messages = &.{}, + .builtin_tools = &.{.{ .web_search = .{ .allowed_domains = &.{"openai.com"} } }}, + })); } test "encodes complete OpenAI settings and tagged extensions" { diff --git a/src/providers/xai.zig b/src/providers/xai.zig index d816ebf..66593dc 100644 --- a/src/providers/xai.zig +++ b/src/providers/xai.zig @@ -153,10 +153,55 @@ test "native client owns the xAI Responses dialect" { try std.testing.expectEqual(model_types.ExtraBodyKind.xai, client.model().profile.extra_body_kind.?); } -test "native xAI tools reject malformed provider configuration before transport" { +test "native client streams through the xAI Responses dialect" { + const State = struct { + fn send(_: *anyopaque, allocator: std.mem.Allocator, _: transport.Request) !transport.Response { + return .{ + .status = 200, + .body = try allocator.dupe(u8, "{\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"buffered\"}]}]}"), + }; + } + + fn stream(_: *anyopaque, _: std.mem.Allocator, request: transport.Request, sink: transport.LineSink) !transport.StreamResponse { + try std.testing.expectEqualStrings("https://api.x.ai/v1/responses", request.url); + try std.testing.expect(std.mem.indexOf(u8, request.body, "\"stream\":true") != null); + try std.testing.expect(std.mem.indexOf(u8, request.body, "\"type\":\"x_search\"") != null); + try sink.start(.{ .status = 200 }); + try sink.line("data: {\"type\":\"response.output_text.delta\",\"delta\":\"pong\"}"); + try sink.line("data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}"); + return .{ .status = 200 }; + } + }; + const Sink = struct { + fn emit(_: *anyopaque, _: model_types.ModelStreamEvent) !void {} + }; + var marker: u8 = 0; + var provider = Provider.init("secret", .{ + .context = &marker, + .sendFn = State.send, + .streamLinesFn = State.stream, + }); + var client = Client{ .model_name = "grok-4.6", .provider = provider.provider() }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const response = try client.model().stream(arena.allocator(), .{ + .messages = &.{}, + .builtin_tools = &.{.{ .x_search = .{} }}, + }, .{ .context = &marker, .eventFn = Sink.emit }); + try std.testing.expectEqualStrings("pong", response.parts[0].text); + const buffered = try client.model().request(arena.allocator(), .{ .messages = &.{} }); + try std.testing.expectEqualStrings("buffered", buffered.parts[0].text); +} + +test "native xAI search tools encode exclusion filters" { const State = struct { - fn send(_: *anyopaque, _: std.mem.Allocator, _: transport.Request) !transport.Response { - return error.UnexpectedRequest; + fn send(_: *anyopaque, allocator: std.mem.Allocator, request: transport.Request) !transport.Response { + try std.testing.expect(std.mem.indexOf(u8, request.body, "\"excluded_domains\":[\"example.com\"]") != null); + try std.testing.expect(std.mem.indexOf(u8, request.body, "\"excluded_x_handles\":[\"spam\"]") != null); + return .{ + .status = 200, + .body = try allocator.dupe(u8, "{\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"pong\"}]}]}"), + }; } }; var marker: u8 = 0; @@ -164,6 +209,33 @@ test "native xAI tools reject malformed provider configuration before transport" var client = Client{ .model_name = "grok-4.6", .provider = provider.provider() }; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); + _ = try client.model().request(arena.allocator(), .{ + .messages = &.{}, + .builtin_tools = &.{ + .{ .web_search = .{ .excluded_domains = &.{"example.com"} } }, + .{ .x_search = .{ .excluded_x_handles = &.{"spam"} } }, + }, + }); +} + +test "native xAI tools reject malformed provider configuration before transport" { + const State = struct { + calls: usize = 0, + + fn send(context: *anyopaque, allocator: std.mem.Allocator, _: transport.Request) !transport.Response { + const self: *@This() = @ptrCast(@alignCast(context)); + self.calls += 1; + return .{ + .status = 200, + .body = try allocator.dupe(u8, "{\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"pong\"}]}]}"), + }; + } + }; + var state: State = .{}; + var provider = Provider.init("secret", .{ .context = &state, .sendFn = State.send }); + var client = Client{ .model_name = "grok-4.6", .provider = provider.provider() }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); const invalid = [_]model_types.BuiltinTool{ .{ .web_search = .{ .allowed_domains = &.{"x.ai"}, .excluded_domains = &.{"example.com"} } }, @@ -192,6 +264,9 @@ test "native xAI tools reject malformed provider configuration before transport" error.InvalidRequestEncoding, client.model().request(arena.allocator(), .{ .messages = &.{}, .builtin_tools = &.{tool} }), ); + try std.testing.expectEqual(@as(usize, 0), state.calls); + _ = try client.model().request(arena.allocator(), .{ .messages = &.{} }); + try std.testing.expectEqual(@as(usize, 1), state.calls); } test "Chat compatibility is explicit and has a separate profile" {