Skip to content

Commit b0dfd42

Browse files
committed
refactor(llm): share json post request building
1 parent ef8039e commit b0dfd42

9 files changed

Lines changed: 94 additions & 57 deletions

File tree

Sources/AgentRunKit/LLM/Providers/Anthropic/AnthropicClient.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,11 +404,11 @@ extension AnthropicClient {
404404

405405
func buildURLRequest(_ request: AnthropicRequest) throws -> URLRequest {
406406
let url = baseURL.appendingPathComponent("messages")
407-
var headers = additionalHeaders()
408-
headers["x-api-key"] = apiKey
409-
headers["anthropic-version"] = Self.anthropicAPIVersion
410-
applyBetaHeaders(for: request, into: &headers)
411-
return try buildJSONPostRequest(url: url, body: request, headers: headers)
407+
var headerMap = additionalHeaders()
408+
headerMap["x-api-key"] = apiKey
409+
headerMap["anthropic-version"] = Self.anthropicAPIVersion
410+
applyBetaHeaders(for: request, into: &headerMap)
411+
return try buildJSONPostRequest(url: url, body: request, headers: headerMap)
412412
}
413413

414414
func parseResponse(_ data: Data) throws -> AssistantMessage {

Sources/AgentRunKit/LLM/Providers/Gemini/GeminiClient.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,7 @@ extension GeminiClient {
227227
throw AgentError.llmError(.other("Failed to construct URL with query items"))
228228
}
229229

230-
let headers = additionalHeaders()
231-
return try buildJSONPostRequest(url: finalURL, body: request, headers: headers)
230+
return try buildJSONPostRequest(url: finalURL, body: request, headers: additionalHeaders())
232231
}
233232
}
234233

Sources/AgentRunKit/LLM/Providers/OpenAIChat/OpenAIClient.swift

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,22 +216,12 @@ extension OpenAIClient {
216216

217217
func buildURLRequest(_ request: ChatCompletionRequest) throws -> URLRequest {
218218
let url = baseURL.appendingPathComponent(chatCompletionPath)
219-
var urlRequest = URLRequest(url: url)
220-
urlRequest.httpMethod = "POST"
221-
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
219+
var headers: [(String, String)] = []
222220
if let apiKey {
223-
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
224-
}
225-
for (field, value) in additionalHeaders() {
226-
urlRequest.setValue(value, forHTTPHeaderField: field)
221+
headers.append(("Authorization", "Bearer \(apiKey)"))
227222
}
228-
229-
do {
230-
urlRequest.httpBody = try JSONEncoder().encode(request)
231-
} catch {
232-
throw AgentError.llmError(.encodingFailed(error))
233-
}
234-
return urlRequest
223+
headers.append(contentsOf: additionalHeaders().map { ($0.key, $0.value) })
224+
return try buildJSONPostRequest(url: url, body: request, headers: headers)
235225
}
236226

237227
func buildTranscriptionURLRequest(

Sources/AgentRunKit/LLM/Providers/Responses/ResponsesAPIClient.swift

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,22 +341,12 @@ extension ResponsesAPIClient.ResponsesTurnProjection {
341341
extension ResponsesAPIClient {
342342
func buildURLRequest(_ request: ResponsesRequest) throws -> URLRequest {
343343
let url = baseURL.appendingPathComponent(responsesPath)
344-
var urlRequest = URLRequest(url: url)
345-
urlRequest.httpMethod = "POST"
346-
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
344+
var headers: [(String, String)] = []
347345
if let apiKey {
348-
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
346+
headers.append(("Authorization", "Bearer \(apiKey)"))
349347
}
350-
for (field, value) in additionalHeaders() {
351-
urlRequest.setValue(value, forHTTPHeaderField: field)
352-
}
353-
354-
do {
355-
urlRequest.httpBody = try JSONEncoder().encode(request)
356-
} catch {
357-
throw AgentError.llmError(.encodingFailed(error))
358-
}
359-
return urlRequest
348+
headers.append(contentsOf: additionalHeaders().map { ($0.key, $0.value) })
349+
return try buildJSONPostRequest(url: url, body: request, headers: headers)
360350
}
361351
}
362352

Sources/AgentRunKit/LLM/Providers/Vertex/VertexAnthropicClient.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,10 @@ public struct VertexAnthropicClient: LLMClient, Sendable {
241241
}
242242
let url = baseURL.appendingPathComponent(basePath)
243243

244-
var headers = ["Authorization": "Bearer \(token)"]
245-
anthropic.applyBetaHeaders(for: request.inner, into: &headers)
246-
return try buildJSONPostRequest(url: url, body: request, headers: headers)
244+
var headerMap: [String: String] = [:]
245+
headerMap["Authorization"] = "Bearer \(token)"
246+
anthropic.applyBetaHeaders(for: request.inner, into: &headerMap)
247+
return try buildJSONPostRequest(url: url, body: request, headers: headerMap)
247248
}
248249
}
249250

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
func buildJSONPostRequest(
4+
url: URL,
5+
body: some Encodable,
6+
headers: [String: String]
7+
) throws -> URLRequest {
8+
try buildJSONPostRequest(url: url, body: body, headers: Array(headers))
9+
}
10+
11+
func buildJSONPostRequest(
12+
url: URL,
13+
body: some Encodable,
14+
headers: [(String, String)]
15+
) throws -> URLRequest {
16+
var request = URLRequest(url: url)
17+
request.httpMethod = "POST"
18+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
19+
for (field, value) in headers {
20+
request.setValue(value, forHTTPHeaderField: field)
21+
}
22+
do {
23+
request.httpBody = try JSONEncoder().encode(body)
24+
} catch {
25+
throw AgentError.llmError(.encodingFailed(error))
26+
}
27+
return request
28+
}

Sources/AgentRunKit/LLM/Transport/SSEParser.swift

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,6 @@ struct SSEEventParser {
6969
}
7070
}
7171

72-
func buildJSONPostRequest(
73-
url: URL,
74-
body: some Encodable,
75-
headers: [String: String]
76-
) throws -> URLRequest {
77-
var request = URLRequest(url: url)
78-
request.httpMethod = "POST"
79-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
80-
for (field, value) in headers {
81-
request.setValue(value, forHTTPHeaderField: field)
82-
}
83-
do {
84-
request.httpBody = try JSONEncoder().encode(body)
85-
} catch {
86-
throw AgentError.llmError(.encodingFailed(error))
87-
}
88-
return request
89-
}
90-
9172
@discardableResult
9273
func processSSEStream<S: AsyncSequence & Sendable>(
9374
bytes: S,

Tests/AgentRunKitTests/LLM/Core/LLMClientTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,21 @@ struct OpenAIClientURLRequestTests {
481481
#expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer sk-test-key-123")
482482
}
483483

484+
@Test
485+
func additionalAuthorizationHeaderOverridesApiKeyCaseInsensitively() throws {
486+
let client = OpenAIClient(
487+
apiKey: "sk-test-key-123",
488+
model: "test/model",
489+
baseURL: OpenAIClient.openRouterBaseURL,
490+
additionalHeaders: { ["authorization": "Bearer override"] }
491+
)
492+
let messages: [ChatMessage] = [.user("Hello")]
493+
let request = try client.buildRequest(messages: messages, tools: [])
494+
let urlRequest = try client.buildURLRequest(request)
495+
496+
#expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer override")
497+
}
498+
484499
@Test
485500
func buildURLRequestWithCustomBaseURL() throws {
486501
guard let customURL = URL(string: "https://custom.api.example.com/v2") else {

Tests/AgentRunKitTests/LLM/Responses/ResponsesAPIClientTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,39 @@ struct ResponsesURLRequestTests {
793793
#expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer sk-test-123")
794794
}
795795

796+
@Test
797+
func buildURLRequestAppliesAdditionalHeaders() async throws {
798+
let client = ResponsesAPIClient(
799+
apiKey: "sk-test-123",
800+
model: "gpt-4.1",
801+
baseURL: ResponsesAPIClient.openAIBaseURL,
802+
additionalHeaders: { ["X-Custom-Header": "custom-value"] }
803+
)
804+
let request = try await client.buildRequest(
805+
messages: [.user("Hello")], tools: []
806+
)
807+
let urlRequest = try await client.buildURLRequest(request)
808+
809+
#expect(urlRequest.value(forHTTPHeaderField: "X-Custom-Header") == "custom-value")
810+
#expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer sk-test-123")
811+
}
812+
813+
@Test
814+
func additionalAuthorizationHeaderOverridesApiKeyCaseInsensitively() async throws {
815+
let client = ResponsesAPIClient(
816+
apiKey: "sk-test-123",
817+
model: "gpt-4.1",
818+
baseURL: ResponsesAPIClient.openAIBaseURL,
819+
additionalHeaders: { ["authorization": "Bearer override"] }
820+
)
821+
let request = try await client.buildRequest(
822+
messages: [.user("Hello")], tools: []
823+
)
824+
let urlRequest = try await client.buildURLRequest(request)
825+
826+
#expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer override")
827+
}
828+
796829
@Test
797830
func buildURLRequestWithoutApiKeyOmitsAuth() async throws {
798831
let client = ResponsesAPIClient(

0 commit comments

Comments
 (0)