diff options
| -rw-r--r-- | libpanto/src/provider.zig | 45 | ||||
| -rw-r--r-- | libpanto/src/provider_anthropic_messages.zig | 10 | ||||
| -rw-r--r-- | libpanto/src/provider_openai_chat.zig | 13 |
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" } }, |
