summaryrefslogtreecommitdiff
path: root/libpanto
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto')
-rw-r--r--libpanto/src/provider.zig45
-rw-r--r--libpanto/src/provider_anthropic_messages.zig10
-rw-r--r--libpanto/src/provider_openai_chat.zig13
3 files changed, 65 insertions, 3 deletions
diff --git a/libpanto/src/provider.zig b/libpanto/src/provider.zig
index e83012d..43c882a 100644
--- a/libpanto/src/provider.zig
+++ b/libpanto/src/provider.zig
@@ -26,6 +26,24 @@ pub const ContentBlockType = enum {
/// Markers (case-sensitive substrings, as the wire emits them):
/// - OpenAI: `context_length_exceeded`, `maximum context length`
/// - Anthropic: `prompt is too long`
+/// Merge a provider's built-in request headers with caller-supplied
+/// `extra_headers` (config `Header`s) into one `[]std.http.Header`, allocated
+/// with `alloc`. The caller owns the result and must free it once the request
+/// has been sent. `base` comes first; `extra` is appended in order, so an
+/// `extra` header with the same name as a base header is sent as a second
+/// occurrence (provider-identity headers in practice never collide with the
+/// fixed content-type/accept/authorization set).
+pub fn mergeHeaders(
+ alloc: std.mem.Allocator,
+ base: []const std.http.Header,
+ extra: []const config_mod.Header,
+) ![]std.http.Header {
+ const out = try alloc.alloc(std.http.Header, base.len + extra.len);
+ @memcpy(out[0..base.len], base);
+ for (extra, 0..) |h, i| out[base.len + i] = .{ .name = h.name, .value = h.value };
+ return out;
+}
+
pub fn isContextOverflowBody(body: []const u8) bool {
const markers = [_][]const u8{
"context_length_exceeded",
@@ -311,6 +329,33 @@ pub const OpenStreamFn = *const fn (
diag: ?*ProviderDiagnostic,
) anyerror!ProviderStream;
+test "mergeHeaders - base first, extra appended, converted to http.Header" {
+ const t2 = std.testing;
+ const base = [_]std.http.Header{
+ .{ .name = "content-type", .value = "application/json" },
+ .{ .name = "authorization", .value = "Bearer x" },
+ };
+ const extra = [_]config_mod.Header{
+ .{ .name = "Copilot-Integration-Id", .value = "vscode-chat" },
+ .{ .name = "X-Initiator", .value = "user" },
+ };
+ const merged = try mergeHeaders(t2.allocator, &base, &extra);
+ defer t2.allocator.free(merged);
+ try t2.expectEqual(@as(usize, 4), merged.len);
+ try t2.expectEqualStrings("content-type", merged[0].name);
+ try t2.expectEqualStrings("Copilot-Integration-Id", merged[2].name);
+ try t2.expectEqualStrings("user", merged[3].value);
+}
+
+test "mergeHeaders - empty extra yields a copy of base" {
+ const t2 = std.testing;
+ const base = [_]std.http.Header{.{ .name = "accept", .value = "text/event-stream" }};
+ const merged = try mergeHeaders(t2.allocator, &base, &.{});
+ defer t2.allocator.free(merged);
+ try t2.expectEqual(@as(usize, 1), merged.len);
+ try t2.expectEqualStrings("accept", merged[0].name);
+}
+
test "isContextOverflowBody - matches known markers, rejects others" {
const t2 = std.testing;
try t2.expect(isContextOverflowBody("{\"error\":{\"code\":\"context_length_exceeded\"}}"));
diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig
index be4268e..8f5d200 100644
--- a/libpanto/src/provider_anthropic_messages.zig
+++ b/libpanto/src/provider_anthropic_messages.zig
@@ -96,7 +96,15 @@ pub const AnthropicMessagesRequest = struct {
.value = "interleaved-thinking-2025-05-14",
};
}
- const extra_headers = headers_buf[0 .. if (send_interleaved) @as(usize, 5) else @as(usize, 4)];
+ const base_headers = headers_buf[0 .. if (send_interleaved) @as(usize, 5) else @as(usize, 4)];
+ // Merge any provider `extra_headers` onto the base set. Freed at the
+ // end of `open` — after the request body has been flushed.
+ const extra_headers = try provider_mod.mergeHeaders(
+ self.allocator,
+ base_headers,
+ self.config.extra_headers,
+ );
+ defer self.allocator.free(extra_headers);
rr.req = try self.http_client.request(.POST, uri, .{
.extra_headers = extra_headers,
diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig
index 5713b5d..b236a60 100644
--- a/libpanto/src/provider_openai_chat.zig
+++ b/libpanto/src/provider_openai_chat.zig
@@ -103,18 +103,27 @@ pub const OpenAIChatRequest = struct {
);
defer self.allocator.free(auth_value);
- const extra_headers = [_]http.Header{
+ const base_headers = [_]http.Header{
.{ .name = "content-type", .value = "application/json" },
.{ .name = "accept", .value = "text/event-stream" },
.{ .name = "authorization", .value = auth_value },
};
+ // Merge provider `extra_headers` (e.g. Copilot editor identity, or
+ // auth-exchange-derived headers) onto the base set. Freed at the end
+ // of `open` — after the request body has been flushed.
+ const extra_headers = try provider_mod.mergeHeaders(
+ self.allocator,
+ &base_headers,
+ self.config.extra_headers,
+ );
+ defer self.allocator.free(extra_headers);
// Open the request. We can't use `fetch()` because it buffers the
// response; we want to stream the body as it arrives. The request
// is moved into the heap struct so the body reader (which borrows
// `&rr.response`) stays valid across `produce` calls.
rr.req = try self.http_client.request(.POST, uri, .{
- .extra_headers = &extra_headers,
+ .extra_headers = extra_headers,
// Disable compression: gzip buffers small SSE frames, defeating
// the streaming property we paid for `stream: true` to get.
.headers = .{ .accept_encoding = .{ .override = "identity" } },