Commit cdfd491

mo khan <mo@mokhan.ca>
2026-01-20 18:04:43
refactor: shrink size of net/llm code
1 parent aef95da
Changed files (1)
lib
lib/elelem/openai.rb
@@ -3,168 +3,59 @@
 module Net
   module Llm
     class OpenAI
-      attr_reader :api_key, :base_url, :model, :http
-
-      def initialize(api_key: ENV.fetch("OPENAI_API_KEY"), base_url: ENV.fetch("OPENAI_BASE_URL", "https://api.openai.com/v1"), model: "gpt-4o-mini", http: Net::Llm.http)
-        @api_key = api_key
-        @base_url = base_url
-        @model = model
-        @http = http
-      end
-
-      def chat(messages, tools)
-        handle_response(http.post(
-          "#{base_url}/chat/completions",
-          headers: headers,
-          body: { model: model, messages: messages, tools: tools, tool_choice: "auto" }
-        ))
+      def initialize(model:, api_key: ENV.fetch("OPENAI_API_KEY"), base_url: ENV.fetch("OPENAI_BASE_URL", "https://api.openai.com/v1"), http: Net::Llm.http)
+        @url = "#{base_url}/chat/completions"
+        @model, @api_key, @http = model, api_key, http
       end
 
       def fetch(messages, tools = [], &block)
-        if block_given?
-          fetch_streaming(messages, tools, &block)
-        else
-          fetch_non_streaming(messages, tools)
-        end
-      end
+        content, tool_calls, stop = "", {}, :end_turn
+        body = { model: @model, messages:, stream: true }
+        body.merge!(tools:, tool_choice: "auto") unless tools.empty?
 
-      def models
-        handle_response(http.get("#{base_url}/models", headers: headers))
-      end
-
-      def embeddings(input, model: "text-embedding-ada-002")
-        handle_response(http.post(
-          "#{base_url}/embeddings",
-          headers: headers,
-          body: { model: model, input: input },
-        ))
-      end
+        stream(body) do |json|
+          delta = json.dig("choices", 0, "delta") || {}
 
-      private
+          if (text = delta["content"])
+            content += text
+            block.call(type: :delta, content: text, thinking: nil, tool_calls: nil)
+          end
 
-      def headers
-        { "Authorization" => Net::Hippie.bearer_auth(api_key) }
-      end
+          delta["tool_calls"]&.each do |tc|
+            idx = tc["index"]
+            tool_calls[idx] ||= { id: nil, name: nil, args: "" }
+            tool_calls[idx][:id] ||= tc["id"]
+            tool_calls[idx][:name] ||= tc.dig("function", "name")
+            tool_calls[idx][:args] += tc.dig("function", "arguments").to_s
+          end
 
-      def handle_response(response)
-        if response.is_a?(Net::HTTPSuccess)
-          JSON.parse(response.body)
-        else
-          { "code" => response.code, "body" => response.body }
+          stop = json.dig("choices", 0, "finish_reason")&.to_sym || stop
         end
-      end
-
-      def fetch_non_streaming(messages, tools)
-        body = { model: model, messages: messages }
-        body[:tools] = tools unless tools.empty?
-        body[:tool_choice] = "auto" unless tools.empty?
-
-        result = handle_response(http.post("#{base_url}/chat/completions", headers: headers, body: body))
-        return result if result["code"]
 
-        msg = result.dig("choices", 0, "message") || {}
-        {
-          type: :complete,
-          content: msg["content"],
-          thinking: nil,
-          tool_calls: normalize_tool_calls(msg["tool_calls"]),
-          stop_reason: map_stop_reason(result.dig("choices", 0, "finish_reason"))
-        }
+        block.call(type: :complete, content:, thinking: nil, tool_calls: finalize_tools(tool_calls))
       end
 
-      def fetch_streaming(messages, tools, &block)
-        body = { model: model, messages: messages, stream: true }
-        body[:tools] = tools unless tools.empty?
-        body[:tool_choice] = "auto" unless tools.empty?
-
-        content = ""
-        tool_calls = {}
-        stop_reason = :end_turn
-
-        http.post("#{base_url}/chat/completions", headers: headers, body: body) do |response|
-          raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-
-          buffer = ""
-          response.read_body do |chunk|
-            buffer += chunk
-
-            while (line = extract_line(buffer))
-              next if line.empty? || !line.start_with?("data: ")
-
-              data = line[6..]
-              break if data == "[DONE]"
-
-              json = JSON.parse(data)
-              delta = json.dig("choices", 0, "delta") || {}
-
-              if delta["content"]
-                content += delta["content"]
-                block.call({ type: :delta, content: delta["content"], thinking: nil, tool_calls: nil })
-              end
-
-              if delta["tool_calls"]
-                delta["tool_calls"].each do |tc|
-                  idx = tc["index"]
-                  tool_calls[idx] ||= { id: nil, name: nil, arguments_json: "" }
-                  tool_calls[idx][:id] = tc["id"] if tc["id"]
-                  tool_calls[idx][:name] = tc.dig("function", "name") if tc.dig("function", "name")
-                  tool_calls[idx][:arguments_json] += tc.dig("function", "arguments") || ""
-                end
-              end
+      private
 
-              if json.dig("choices", 0, "finish_reason")
-                stop_reason = map_stop_reason(json.dig("choices", 0, "finish_reason"))
-              end
+      def stream(body, &block)
+        @http.post(@url, headers: { "Authorization" => "Bearer #{@api_key}" }, body:) do |res|
+          raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
+          buf = ""
+          res.read_body do |chunk|
+            buf += chunk
+            while (i = buf.index("\n"))
+              line = buf.slice!(0, i + 1).strip
+              next unless line.start_with?("data: ") && line != "data: [DONE]"
+              block.call(JSON.parse(line[6..]))
             end
           end
         end
-
-        final_tool_calls = tool_calls.values.map do |tc|
-          args = begin
-            JSON.parse(tc[:arguments_json])
-          rescue
-            {}
-          end
-          { id: tc[:id], name: tc[:name], arguments: args }
-        end
-
-        block.call({
-          type: :complete,
-          content: content,
-          thinking: nil,
-          tool_calls: final_tool_calls,
-          stop_reason: stop_reason
-        })
       end
 
-      def extract_line(buffer)
-        line_end = buffer.index("\n")
-        return nil unless line_end
-
-        line = buffer[0...line_end]
-        buffer.replace(buffer[(line_end + 1)..] || "")
-        line
-      end
-
-      def normalize_tool_calls(tool_calls)
-        return [] if tool_calls.nil? || tool_calls.empty?
-
-        tool_calls.map do |tc|
-          args = tc.dig("function", "arguments")
-          {
-            id: tc["id"],
-            name: tc.dig("function", "name"),
-            arguments: args.is_a?(String) ? (JSON.parse(args) rescue {}) : (args || {})
-          }
-        end
-      end
-
-      def map_stop_reason(reason)
-        case reason
-        when "stop" then :end_turn
-        when "tool_calls" then :tool_use
-        when "length" then :max_tokens
-        else :end_turn
+      def finalize_tools(tcs)
+        tcs.values.map do |tc|
+          args = begin; JSON.parse(tc[:args]); rescue; {}; end
+          { id: tc[:id], name: tc[:name], arguments: args }
         end
       end
     end