Commit 68397fc
Changed files (1)
lib
elelem
lib/elelem/openai.rb
@@ -5,48 +5,74 @@ module Elelem
class OpenAI
def initialize(model:, api_key:, base_url: "https://api.openai.com/v1", http: Elelem::Net.http)
@url = "#{base_url}/chat/completions"
- @model, @api_key, @http = model, api_key, http
+ @model = model
+ @api_key = api_key
+ @http = http
end
def fetch(messages, tools = [], &block)
tool_calls = {}
- body = { model: @model, messages:, stream: true, tools:, tool_choice: "auto" }
-
- stream(body) do |json|
- delta = json.dig("choices", 0, "delta") || {}
- block.call(content: delta["content"], thinking: nil) if delta["content"]
-
- delta["tool_calls"]&.each do |tool_call|
- idx = tool_call["index"]
- tool_calls[idx] ||= { id: nil, name: nil, args: "" }
- tool_calls[idx][:id] ||= tool_call["id"]
- tool_calls[idx][:name] ||= tool_call.dig("function", "name")
- tool_calls[idx][:args] += tool_call.dig("function", "arguments").to_s
- end
+ body = build_request_body(messages, tools)
+
+ stream(body) do |event|
+ handle_event(event, tool_calls, &block)
end
- finalize_tools(tool_calls)
+ finalize_tool_calls(tool_calls)
end
private
- 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
+ def build_request_body(messages, tools)
+ { model: @model, messages:, stream: true, tools:, tool_choice: "auto" }
+ end
+
+ def handle_event(event, tool_calls, &block)
+ delta = event.dig("choices", 0, "delta") || {}
+
+ block.call(content: delta["content"], thinking: nil) if delta["content"]
+
+ accumulate_tool_calls(delta["tool_calls"], tool_calls) if delta["tool_calls"]
+ end
+
+ def accumulate_tool_calls(incoming_tool_calls, tool_calls)
+ incoming_tool_calls.each do |tool_call|
+ index = tool_call["index"]
+ tool_calls[index] ||= { id: nil, name: nil, args: String.new }
+ tool_calls[index][:id] ||= tool_call["id"]
+ tool_calls[index][:name] ||= tool_call.dig("function", "name")
+ tool_calls[index][:args] << tool_call.dig("function", "arguments").to_s
+ end
+ end
+
+ def stream(body)
+ @http.post(@url, headers: headers, body:) do |response|
+ raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(::Net::HTTPSuccess)
+
+ read_sse_stream(response) { |event| yield event }
+ end
+ end
+
+ def headers
+ { "Authorization" => "Bearer #{@api_key}" }
+ end
+
+ def read_sse_stream(response)
+ buffer = String.new
+
+ response.read_body do |chunk|
+ buffer << chunk
+
+ while (index = buffer.index("\n"))
+ line = buffer.slice!(0, index + 1).strip
+ next unless line.start_with?("data: ") && line != "data: [DONE]"
+
+ yield JSON.parse(line.delete_prefix("data: "))
end
end
end
- def finalize_tools(tool_calls)
+ def finalize_tool_calls(tool_calls)
tool_calls.values.map do |tool_call|
{
id: tool_call[:id],