Commit 1418c7c

mo khan <mo@mokhan.ca>
2025-10-08 18:10:38
refactor: extract singleton http client
1 parent 81a284c
lib/net/llm/anthropic.rb
@@ -3,11 +3,12 @@
 module Net
   module Llm
     class Anthropic
-      attr_reader :api_key, :model
+      attr_reader :api_key, :model, :http
 
-      def initialize(api_key:, model: "claude-3-5-sonnet-20241022")
+      def initialize(api_key:, model: "claude-3-5-sonnet-20241022", http: Net::Llm.http)
         @api_key = api_key
         @model = model
+        @http = http
       end
 
       def messages(messages, system: nil, max_tokens: 1024, tools: nil, &block)
@@ -35,11 +36,7 @@ module Net
         payload
       end
 
-      def client
-        @client ||= Net::Hippie::Client.new(read_timeout: 3600, open_timeout: 10)
-      end
-
-      def anthropic_headers
+      def headers
         {
           "x-api-key" => api_key,
           "anthropic-version" => "2023-06-01"
@@ -47,8 +44,7 @@ module Net
       end
 
       def post_request(url, payload)
-        response = client.post(url, headers: anthropic_headers, body: payload)
-        handle_response(response)
+        handle_response(http.post(url, headers: headers, body: payload))
       end
 
       def handle_response(response)
@@ -57,7 +53,7 @@ module Net
       end
 
       def stream_request(url, payload, &block)
-        client.post(url, headers: anthropic_headers, body: payload) do |response|
+        http.post(url, headers: headers, body: payload) do |response|
           raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
 
           buffer = ""
lib/net/llm/ollama.rb
@@ -3,11 +3,12 @@
 module Net
   module Llm
     class Ollama
-      attr_reader :host, :model
+      attr_reader :host, :model, :http
 
-      def initialize(host: "localhost:11434", model: "llama2")
+      def initialize(host: "localhost:11434", model: "llama2", http: Net::Llm.http)
         @host = host
         @model = model
+        @http = http
       end
 
       def chat(messages, &block)
@@ -40,7 +41,7 @@ module Net
 
       def tags
         url = build_url("/api/tags")
-        response = client.get(url)
+        response = http.get(url)
         handle_response(response)
       end
 
@@ -57,12 +58,8 @@ module Net
         "#{base}#{path}"
       end
 
-      def client
-        @client ||= Net::Hippie::Client.new(read_timeout: 3600, open_timeout: 10)
-      end
-
       def post_request(url, payload)
-        response = client.post(url, body: payload)
+        response = http.post(url, body: payload)
         handle_response(response)
       end
 
@@ -72,7 +69,7 @@ module Net
       end
 
       def stream_request(url, payload, &block)
-        client.post(url, body: payload) do |response|
+        http.post(url, body: payload) do |response|
           raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
 
           buffer = ""
lib/net/llm/openai.rb
@@ -3,38 +3,36 @@
 module Net
   module Llm
     class OpenAI
-      DEFAULT_TIMEOUT = 60 * 2
+      attr_reader :api_key, :base_url, :model, :http
 
-      attr_reader :base_url, :model, :client
-
-      def initialize(api_key:, base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", timeout: DEFAULT_TIMEOUT)
+      def initialize(api_key:, base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", http: Net::Llm.http)
+        @api_key = api_key
         @base_url = base_url
-        @client = Net::Hippie::Client.new(
-          headers: Net::Hippie::Client::DEFAULT_HEADERS.merge({ "Authorization" => Net::Hippie.bearer_auth(api_key) }),
-          open_timeout: timeout,
-          read_timeout: timeout,
-        )
         @model = model
+        @http = http
       end
 
       def chat(messages, tools)
-        response = client.post(
+        response = http.post(
           "#{base_url}/chat/completions",
+          headers: headers,
           body: { model: model, messages: messages, tools: tools, tool_choice: "auto" }
         )
         handle_response(response)
       end
 
       def models
-        response = client.get(
+        response = http.get(
           "#{base_url}/models",
+          headers: headers
         )
         handle_response(response)
       end
 
       def embeddings(input, model: "text-embedding-ada-002")
-        response = client.post(
+        response = http.post(
           "#{base_url}/embeddings",
+          headers: headers,
           body: { model: model, input: input }
         )
         handle_response(response)
@@ -42,6 +40,10 @@ module Net
 
       private
 
+      def headers
+        { "Authorization" => Net::Hippie.bearer_auth(api_key) }
+      end
+
       def handle_response(response)
         raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
         JSON.parse(response.body)
lib/net/llm.rb
@@ -10,5 +10,12 @@ require "json"
 module Net
   module Llm
     class Error < StandardError; end
+
+    def self.http
+      @http ||= Net::Hippie::Client.new(
+        read_timeout: 3600,
+        open_timeout: 10
+      )
+    end
   end
 end