Commit 9a291e7

mo khan <mo@mokhan.ca>
2025-10-08 16:54:23
refactor: use net-hippie as http client
1 parent 339e0ae
lib/net/llm/anthropic.rb
@@ -11,13 +11,13 @@ module Net
       end
 
       def messages(messages, system: nil, max_tokens: 1024, tools: nil, &block)
-        uri = URI("https://api.anthropic.com/v1/messages")
+        url = "https://api.anthropic.com/v1/messages"
         payload = build_payload(messages, system, max_tokens, tools, block_given?)
 
         if block_given?
-          stream_request(uri, payload, &block)
+          stream_request(url, payload, &block)
         else
-          post_request(uri, payload)
+          post_request(url, payload)
         end
       end
 
@@ -35,51 +35,43 @@ module Net
         payload
       end
 
-      def post_request(uri, payload)
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = true
+      def client
+        @client ||= Net::Hippie::Client.new(read_timeout: 3600, open_timeout: 10)
+      end
+
+      def anthropic_headers
+        {
+          "x-api-key" => api_key,
+          "anthropic-version" => "2023-06-01"
+        }
+      end
 
-        request = Net::HTTP::Post.new(uri)
-        request["x-api-key"] = api_key
-        request["anthropic-version"] = "2023-06-01"
-        request["Content-Type"] = "application/json"
-        request.body = payload.to_json
+      def post_request(url, payload)
+        response = client.post(url, headers: anthropic_headers, body: payload)
+        handle_response(response)
+      end
 
-        response = http.start { |h| h.request(request) }
+      def handle_response(response)
         raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
         JSON.parse(response.body)
       end
 
-      def stream_request(uri, payload, &block)
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = true
-        http.read_timeout = 3600
-
-        request = Net::HTTP::Post.new(uri)
-        request["x-api-key"] = api_key
-        request["anthropic-version"] = "2023-06-01"
-        request["Content-Type"] = "application/json"
-        request.body = payload.to_json
-
-        http.start do |h|
-          h.request(request) do |response|
-            unless response.is_a?(Net::HTTPSuccess)
-              raise "HTTP #{response.code}: #{response.body}"
-            end
+      def stream_request(url, payload, &block)
+        client.post(url, headers: anthropic_headers, body: payload) do |response|
+          raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
 
-            buffer = ""
-            response.read_body do |chunk|
-              buffer += chunk
+          buffer = ""
+          response.read_body do |chunk|
+            buffer += chunk
 
-              while (event = extract_sse_event(buffer))
-                next if event[:data].nil? || event[:data].empty?
-                next if event[:data] == "[DONE]"
+            while (event = extract_sse_event(buffer))
+              next if event[:data].nil? || event[:data].empty?
+              next if event[:data] == "[DONE]"
 
-                json = JSON.parse(event[:data])
-                block.call(json)
+              json = JSON.parse(event[:data])
+              block.call(json)
 
-                break if json["type"] == "message_stop"
-              end
+              break if json["type"] == "message_stop"
             end
           end
         end
lib/net/llm/ollama.rb
@@ -11,103 +11,81 @@ module Net
       end
 
       def chat(messages, &block)
-        uri = build_uri("/api/chat")
+        url = build_url("/api/chat")
         payload = { model: model, messages: messages, stream: block_given? }
 
         if block_given?
-          stream_request(uri, payload, &block)
+          stream_request(url, payload, &block)
         else
-          post_request(uri, payload)
+          post_request(url, payload)
         end
       end
 
       def generate(prompt, &block)
-        uri = build_uri("/api/generate")
+        url = build_url("/api/generate")
         payload = { model: model, prompt: prompt, stream: block_given? }
 
         if block_given?
-          stream_request(uri, payload, &block)
+          stream_request(url, payload, &block)
         else
-          post_request(uri, payload)
+          post_request(url, payload)
         end
       end
 
       def embeddings(input)
-        uri = build_uri("/api/embed")
+        url = build_url("/api/embed")
         payload = { model: model, input: input }
-        post_request(uri, payload)
+        post_request(url, payload)
       end
 
       def tags
-        uri = build_uri("/api/tags")
-        get_request(uri)
+        url = build_url("/api/tags")
+        response = client.get(url)
+        handle_response(response)
       end
 
       def show(name)
-        uri = build_uri("/api/show")
+        url = build_url("/api/show")
         payload = { name: name }
-        post_request(uri, payload)
+        post_request(url, payload)
       end
 
       private
 
-      def build_uri(path)
+      def build_url(path)
         base = host.start_with?("http://", "https://") ? host : "http://#{host}"
-        URI("#{base}#{path}")
+        "#{base}#{path}"
       end
 
-      def get_request(uri)
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = uri.scheme == "https"
-        request = Net::HTTP::Get.new(uri)
-        request["Accept"] = "application/json"
-
-        response = http.start { |h| h.request(request) }
-        raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-        JSON.parse(response.body)
+      def client
+        @client ||= Net::Hippie::Client.new(read_timeout: 3600, open_timeout: 10)
       end
 
-      def post_request(uri, payload)
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = uri.scheme == "https"
-        request = Net::HTTP::Post.new(uri)
-        request["Accept"] = "application/json"
-        request["Content-Type"] = "application/json"
-        request.body = payload.to_json
+      def post_request(url, payload)
+        response = client.post(url, body: payload)
+        handle_response(response)
+      end
 
-        response = http.start { |h| h.request(request) }
+      def handle_response(response)
         raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
         JSON.parse(response.body)
       end
 
-      def stream_request(uri, payload, &block)
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = uri.scheme == "https"
-        http.read_timeout = 3600
-
-        request = Net::HTTP::Post.new(uri)
-        request["Accept"] = "application/json"
-        request["Content-Type"] = "application/json"
-        request.body = payload.to_json
-
-        http.start do |h|
-          h.request(request) do |response|
-            unless response.is_a?(Net::HTTPSuccess)
-              raise "HTTP #{response.code}: #{response.body}"
-            end
+      def stream_request(url, payload, &block)
+        client.post(url, body: payload) do |response|
+          raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
 
-            buffer = ""
-            response.read_body do |chunk|
-              buffer += chunk
+          buffer = ""
+          response.read_body do |chunk|
+            buffer += chunk
 
-              while (message = extract_message(buffer))
-                next if message.empty?
+            while (message = extract_message(buffer))
+              next if message.empty?
 
-                json = JSON.parse(message)
-                block.call(json)
+              json = JSON.parse(message)
+              block.call(json)
 
-                break if json["done"]
-              end
+              break if json["done"]
             end
           end
         end
lib/net/llm/version.rb
@@ -2,6 +2,6 @@
 
 module Net
   module Llm
-    VERSION = "0.2.0"
+    VERSION = "0.3.0"
   end
 end
lib/net/llm.rb
@@ -3,9 +3,8 @@
 require_relative "llm/version"
 require_relative "llm/ollama"
 require_relative "llm/anthropic"
-require "net/http"
+require "net/hippie"
 require "json"
-require "uri"
 
 module Net
   module Llm
@@ -22,52 +21,45 @@ module Net
       end
 
       def chat(messages, tools, timeout: DEFAULT_TIMEOUT)
-        uri = URI("#{base_url}/chat/completions")
-        request = Net::HTTP::Post.new(uri)
-        request["Authorization"] = "Bearer #{api_key}"
-        request["Content-Type"] = "application/json"
-        request.body = { model: model, messages: messages, tools: tools, tool_choice: "auto" }.to_json
-
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = true
-        http.open_timeout = timeout
-        http.read_timeout = timeout
-        http.write_timeout = timeout if http.respond_to?(:write_timeout=)
-
-        response = http.start { |h| h.request(request) }
-        raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-        JSON.parse(response.body)
+        response = client(timeout).post(
+          "#{base_url}/chat/completions",
+          headers: auth_headers,
+          body: { model: model, messages: messages, tools: tools, tool_choice: "auto" }
+        )
+        handle_response(response)
       end
 
       def models(timeout: DEFAULT_TIMEOUT)
-        uri = URI("#{base_url}/models")
-        request = Net::HTTP::Get.new(uri)
-        request["Authorization"] = "Bearer #{api_key}"
-
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = true
-        http.open_timeout = timeout
-        http.read_timeout = timeout
-
-        response = http.start { |h| h.request(request) }
-        raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-        JSON.parse(response.body)
+        response = client(timeout).get(
+          "#{base_url}/models",
+          headers: auth_headers
+        )
+        handle_response(response)
       end
 
       def embeddings(input, model: "text-embedding-ada-002", timeout: DEFAULT_TIMEOUT)
-        uri = URI("#{base_url}/embeddings")
-        request = Net::HTTP::Post.new(uri)
-        request["Authorization"] = "Bearer #{api_key}"
-        request["Content-Type"] = "application/json"
-        request.body = { model: model, input: input }.to_json
+        response = client(timeout).post(
+          "#{base_url}/embeddings",
+          headers: auth_headers,
+          body: { model: model, input: input }
+        )
+        handle_response(response)
+      end
+
+      private
 
-        http = Net::HTTP.new(uri.hostname, uri.port)
-        http.use_ssl = true
-        http.open_timeout = timeout
-        http.read_timeout = timeout
-        http.write_timeout = timeout if http.respond_to?(:write_timeout=)
+      def client(timeout)
+        Net::Hippie::Client.new(
+          read_timeout: timeout,
+          open_timeout: timeout
+        )
+      end
+
+      def auth_headers
+        { "Authorization" => Net::Hippie.bearer_auth(api_key) }
+      end
 
-        response = http.start { |h| h.request(request) }
+      def handle_response(response)
         raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
         JSON.parse(response.body)
       end
CHANGELOG.md
@@ -1,5 +1,14 @@
 ## [Unreleased]
 
+## [0.3.0] - 2025-10-08
+### Changed
+- Refactored all providers to use net-hippie HTTP client
+- Reduced code duplication across providers
+- Improved error handling consistency
+
+### Added
+- Added net-hippie dependency for cleaner HTTP interactions
+
 ## [0.2.0] - 2025-10-08
 
 - Add Ollama provider with streaming support
Gemfile
@@ -11,3 +11,5 @@ gem "rake", "~> 13.0"
 gem "rspec", "~> 3.0"
 
 gem "webmock", "~> 3.25", group: :development
+
+gem "net-hippie", "~> 1.4"
Gemfile.lock
@@ -11,6 +11,7 @@ GEM
   specs:
     addressable (2.8.7)
       public_suffix (>= 2.0.2, < 7.0)
+    base64 (0.3.0)
     bigdecimal (3.3.0)
     cgi (0.4.2)
     crack (1.0.0)
@@ -26,8 +27,16 @@ GEM
       rdoc (>= 4.0.0)
       reline (>= 0.4.2)
     json (2.13.2)
+    logger (1.7.0)
+    net-hippie (1.4.0)
+      base64 (~> 0.1)
+      json (~> 2.0)
+      logger (~> 1.0)
+      net-http (~> 0.6)
+      openssl (~> 3.0)
     net-http (0.6.0)
       uri
+    openssl (3.3.1)
     psych (5.2.2)
       date
       stringio
@@ -65,6 +74,7 @@ PLATFORMS
 
 DEPENDENCIES
   irb
+  net-hippie (~> 1.4)
   net-llm!
   rake (~> 13.0)
   rspec (~> 3.0)