Class: Zephira::Models::BaseModel
- Inherits:
-
Object
- Object
- Zephira::Models::BaseModel
- Defined in:
- lib/zephira/models/base_model.rb
Overview
Base class for all model definitions.
To add a new model:
1. Drop a new file in `lib/zephira/models/<name>.rb` — it is auto-loaded.
2. Subclass `BaseModel` and implement `model_name` and `context_limit`.
3. Optionally override `backend` to point at a specific backend class.
Defaults to `Backends::OpenAiCompatible` (works for any provider with an
OpenAI-compatible API). For provider-specific quirks (Mistral, Anthropic
tool-call shape, etc.) define a dedicated backend class and return it
from `backend`.
‘ENV` overrides per-model `backend` for debugging.
Direct Known Subclasses
ChatGpt41, ChatGpt41Mini, Claude35Sonnet, Gpt54, Gpt55, GptO4Mini, Llama4
Class Method Summary collapse
-
.backend ⇒ Object
Override in subclasses to bind a model to a specific backend class.
- .backend_class ⇒ Object
- .context_limit ⇒ Object
-
.dispatch_tool_calls(tool_calls, agent:) ⇒ Object
Returns an array of [call, content] pairs in the original order.
- .format_tools(tools) ⇒ Object
- .inference(api_key:, agent:, messages: [], base_url: nil) ⇒ Object
- .model_name ⇒ Object
- .parse_tool_arguments(call, agent:) ⇒ Object
- .serialize_tool_result(result) ⇒ Object
- .simple_inference(api_key:, messages:, agent: nil, base_url: nil) ⇒ Object
Class Method Details
.backend ⇒ Object
Override in subclasses to bind a model to a specific backend class.
29 30 31 |
# File 'lib/zephira/models/base_model.rb', line 29 def self.backend Zephira::Backends::OpenAiCompatible end |
.backend_class ⇒ Object
33 34 35 36 37 38 39 40 |
# File 'lib/zephira/models/base_model.rb', line 33 def self.backend_class identifier = ENV["ZEPHIRA_BACKEND"] if identifier found = Zephira::Backends.find_by_name(identifier) return found if found end backend end |
.context_limit ⇒ Object
24 25 26 |
# File 'lib/zephira/models/base_model.rb', line 24 def self.context_limit raise NotImplementedError, "You must implement the context_limit method" end |
.dispatch_tool_calls(tool_calls, agent:) ⇒ Object
Returns an array of [call, content] pairs in the original order. Read-only tools are run concurrently via threads (network/disk I/O releases the GVL); mutating tools run sequentially after, in original order.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/zephira/models/base_model.rb', line 87 def self.dispatch_tool_calls(tool_calls, agent:) results = Array.new(tool_calls.size) read_only_calls = [] mutating_calls = [] tool_calls.each_with_index do |call, index| if agent.tools.read_only?(call["function"]["name"]) read_only_calls << [index, call] else mutating_calls << [index, call] end end threads = read_only_calls.map do |index, call| Thread.new do args = parse_tool_arguments(call, agent: agent) result = agent.run_tool(name: call["function"]["name"], args: args) results[index] = [call, serialize_tool_result(result)] end end threads.each(&:join) mutating_calls.each do |index, call| args = parse_tool_arguments(call, agent: agent) result = agent.run_tool(name: call["function"]["name"], args: args) results[index] = [call, serialize_tool_result(result)] end results end |
.format_tools(tools) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/zephira/models/base_model.rb', line 42 def self.format_tools(tools) tools.to_h.map do |tool| { type: "function", function: { name: tool[:name], description: tool[:description], parameters: tool[:parameters] } } end end |
.inference(api_key:, agent:, messages: [], base_url: nil) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/zephira/models/base_model.rb', line 55 def self.inference(api_key:, agent:, messages: [], base_url: nil) client = backend_class.new(api_key: api_key, base_url: base_url) loop do agent.thinking(self) response = client.chat( model_name: model_name, messages: , agent: agent, tools: format_tools(agent.tools) ) tool_calls = Array(response["tool_calls"]).select { |tool_call| tool_call["type"] == "function" } if tool_calls.empty? content = response["content"] return (content.nil? || content.empty?) ? nil : content end << {role: "assistant", content: response["content"], tool_calls: response["tool_calls"]} agent.history.append(role: "assistant", content: response["content"], tool_calls: response["tool_calls"]) dispatch_tool_calls(tool_calls, agent: agent).each do |call, content| << {role: "tool", tool_call_id: call["id"], content: content} agent.history.append(role: "tool", tool_call_id: call["id"], content: content) end end end |
.model_name ⇒ Object
20 21 22 |
# File 'lib/zephira/models/base_model.rb', line 20 def self.model_name raise NotImplementedError, "You must implement the model_name method" end |
.parse_tool_arguments(call, agent:) ⇒ Object
124 125 126 127 128 129 130 |
# File 'lib/zephira/models/base_model.rb', line 124 def self.parse_tool_arguments(call, agent:) raw = call["function"]["arguments"] || "{}" JSON.parse(raw, symbolize_names: true) rescue JSON::ParserError => exception agent&.logger&.error("Failed to parse tool arguments for #{call["function"]["name"]}: #{exception.}. Raw: #{raw.inspect}") {} end |
.serialize_tool_result(result) ⇒ Object
132 133 134 135 136 137 138 139 140 |
# File 'lib/zephira/models/base_model.rb', line 132 def self.serialize_tool_result(result) return result unless result.is_a?(Hash) && result.key?(:outcome) if result[:outcome] == "success" result[:data].is_a?(String) ? result[:data] : JSON.pretty_generate([result[:data]]) else result[:error].to_s end end |
.simple_inference(api_key:, messages:, agent: nil, base_url: nil) ⇒ Object
118 119 120 121 122 |
# File 'lib/zephira/models/base_model.rb', line 118 def self.simple_inference(api_key:, messages:, agent: nil, base_url: nil) client = backend_class.new(api_key: api_key, base_url: base_url) agent.thinking(self) if agent.respond_to?(:thinking) client.chat(model_name: model_name, messages: , agent: agent)["content"] end |