Class: Ask::Providers::Mistral

Inherits:
Ask::Provider
  • Object
show all
Includes:
LLM::ProviderConfig
Defined in:
lib/ask/provider/mistral.rb

Overview

Mistral AI provider. Uses OpenAI-compatible wire format.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ Mistral

Returns a new instance of Mistral.



9
10
11
12
13
# File 'lib/ask/provider/mistral.rb', line 9

def initialize(config = {})
  config = normalize_config(config)
  super(config)
  @http = build_http
end

Class Method Details

.capabilitiesObject



57
58
59
# File 'lib/ask/provider/mistral.rb', line 57

def capabilities
  { chat: true, streaming: true, tool_calls: true, structured_output: true, embed: true }
end

.configuration_optionsObject



61
# File 'lib/ask/provider/mistral.rb', line 61

def configuration_options; %i[api_key api_base]; end

.configuration_requirementsObject



62
# File 'lib/ask/provider/mistral.rb', line 62

def configuration_requirements; %i[api_key]; end

.slugObject



55
# File 'lib/ask/provider/mistral.rb', line 55

def slug; "mistral"; end

Instance Method Details

#api_baseObject



15
16
17
# File 'lib/ask/provider/mistral.rb', line 15

def api_base
  @config.api_base || "https://api.mistral.ai/v1"
end

#build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params) ⇒ Object

--- Config transformation contract ---



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ask/provider/mistral.rb', line 67

def build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params)
  payload = {
    model:,
    messages: messages.map { |m| format_message(m) },
    stream: stream || false
  }
  payload[:temperature] = temperature if temperature
  tool_defs = format_tools(tools) if tools&.any?
  payload[:tools] = tool_defs if tool_defs
  if schema
    payload[:response_format] = {
      type: "json_schema",
      json_schema: { name: "response", schema:, strict: true }
    }
  end
  payload.merge(params)
end

#chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block) ⇒ Object



23
24
25
26
27
28
29
30
31
# File 'lib/ask/provider/mistral.rb', line 23

def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
  msgs = messages.is_a?(Ask::Conversation) ? messages.to_a : messages
  payload = build_request(msgs, model:, tools:, temperature:, stream:, schema:, **params)
  if stream
    chat_stream(payload, model, &block)
  else
    chat_nonstream(payload, model)
  end
end

#embed(texts, model:) ⇒ Object



33
34
35
36
37
38
39
40
# File 'lib/ask/provider/mistral.rb', line 33

def embed(texts, model:)
  texts = Array(texts)
  response = @http.post("embeddings") { |r| r.body = { model:, input: texts } }
  raise LLM::HTTP.map_error(response.status, response.body, provider: "Mistral") unless response.success?

  embeddings = response.body["data"].map { |d| d["embedding"] }
  Ask::Result.success(embeddings.one? ? embeddings.first : embeddings)
end

#format_message(msg) ⇒ Object



121
122
123
# File 'lib/ask/provider/mistral.rb', line 121

def format_message(msg)
  { role: (msg[:role] || msg["role"]).to_s, content: msg[:content] || msg["content"] }
end

#format_tools(tools) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ask/provider/mistral.rb', line 125

def format_tools(tools)
  tools.map do |t|
    {
      type: "function",
      function: {
        name: t.respond_to?(:name) ? t.name : t[:name],
        description: t.respond_to?(:description) ? t.description : t[:description],
        parameters: t.respond_to?(:parameters) ? t.parameters : t[:parameters]
      }
    }
  end
end

#headersObject



19
20
21
# File 'lib/ask/provider/mistral.rb', line 19

def headers
  { "Content-Type" => "application/json", "Authorization" => "Bearer #{@config.api_key}" }.compact
end

#list_modelsObject



42
43
44
45
46
47
# File 'lib/ask/provider/mistral.rb', line 42

def list_models
  response = @http.get("models")
  return [] unless response.success?

  response.body["data"].map { |m| Ask::ModelInfo.new(id: m["id"], provider: slug) }
end

#parse_error(response) ⇒ Object



49
50
51
52
# File 'lib/ask/provider/mistral.rb', line 49

def parse_error(response)
  body = response.body rescue nil
  body&.dig("error", "message") || body&.dig("error", "type")
end

#parse_response(body, model) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/ask/provider/mistral.rb', line 85

def parse_response(body, model)
  choice = body.dig("choices", 0)
  return Ask::Message.new(role: :assistant, content: nil) unless choice

  msg = choice["message"]
  usage = body["usage"] || {}
  Ask::Message.new(
    role: :assistant,
    content: msg["content"],
    tool_calls: parse_tool_calls(msg["tool_calls"]),
    metadata: {
      model: body["model"] || model,
      finish_reason: choice["finish_reason"],
      input_tokens: usage["prompt_tokens"],
      output_tokens: usage["completion_tokens"],
      raw: body
    }
  )
end

#parse_stream(raw, stream, model, &block) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ask/provider/mistral.rb', line 105

def parse_stream(raw, stream, model, &block)
  each_sse_event(raw) do |data|
    parsed = JSON.parse(data) rescue next
    choice = parsed.dig("choices", 0) or next
    delta = choice["delta"] || {}
    chunk = Ask::Chunk.new(
      content: delta["content"],
      tool_calls: parse_stream_tool_calls(delta["tool_calls"]),
      finish_reason: choice["finish_reason"],
      usage: parsed["usage"]
    )
    stream.add(chunk)
    yield chunk if block_given?
  end
end