Class: Zephira::Models::BaseModel

Inherits:
Object
  • Object
show all
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.

Class Method Summary collapse

Class Method Details

.backendObject

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_classObject



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_limitObject

Raises:

  • (NotImplementedError)


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: 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

    messages << {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|
      messages << {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_nameObject

Raises:

  • (NotImplementedError)


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.message}. 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: messages, agent: agent)["content"]
end