Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit ca598bc

Browse files
committed
FEATURE: support Mistral models with presets
1 parent 6505006 commit ca598bc

File tree

9 files changed

+177
-0
lines changed

9 files changed

+177
-0
lines changed

app/models/llm_model.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def self.provider_params
2828
organization: :text,
2929
disable_native_tools: :checkbox,
3030
},
31+
mistral: {
32+
disable_native_tools: :checkbox,
33+
},
3134
google: {
3235
disable_native_tools: :checkbox,
3336
},

config/locales/client.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ en:
290290
open_ai-o1-preview: "Open AI's most capabale reasoning model"
291291
samba_nova-Meta-Llama-3-1-8B-Instruct: "Efficient lightweight multilingual model"
292292
samba_nova-Meta-Llama-3-1-70B-Instruct": "Powerful multipurpose model"
293+
mistral-mistral-large-latest: "Mistral's most powerful model"
294+
mistral-pixtral-large-latest: "Mistral's most powerful vision capable model"
293295

294296
configured:
295297
title: "Configured LLMs"
@@ -325,6 +327,7 @@ en:
325327
ollama: "Ollama"
326328
CDCK: "CDCK"
327329
samba_nova: "SambaNova"
330+
mistral: "Mistral"
328331
fake: "Custom"
329332

330333
provider_fields:

lib/completions/dialects/dialect.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def all_dialects
1616
DiscourseAi::Completions::Dialects::Claude,
1717
DiscourseAi::Completions::Dialects::Command,
1818
DiscourseAi::Completions::Dialects::Ollama,
19+
DiscourseAi::Completions::Dialects::Mistral,
1920
DiscourseAi::Completions::Dialects::OpenAiCompatible,
2021
]
2122
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
# basically the same as Open AI, except for no support for user names
4+
5+
module DiscourseAi
6+
module Completions
7+
module Dialects
8+
class Mistral < ChatGpt
9+
class << self
10+
def can_translate?(model_provider)
11+
model_provider == "mistral"
12+
end
13+
end
14+
15+
def translate
16+
corrected = super
17+
corrected.each do |msg|
18+
msg[:content] = "" if msg[:tool_calls] && msg[:role] == "assistant"
19+
end
20+
corrected
21+
end
22+
23+
private
24+
25+
def user_msg(msg)
26+
mapped = super
27+
if name = mapped.delete(:name)
28+
if mapped[:content].is_a?(String)
29+
mapped[:content] = "#{name}: #{mapped[:content]}"
30+
else
31+
mapped[:content].each do |inner|
32+
if inner[:text]
33+
inner[:text] = "#{name}: #{inner[:text]}"
34+
break
35+
end
36+
end
37+
end
38+
end
39+
mapped
40+
end
41+
end
42+
end
43+
end
44+
end

lib/completions/endpoints/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def endpoint_for(provider_name)
2020
DiscourseAi::Completions::Endpoints::Anthropic,
2121
DiscourseAi::Completions::Endpoints::Cohere,
2222
DiscourseAi::Completions::Endpoints::SambaNova,
23+
DiscourseAi::Completions::Endpoints::Mistral,
2324
]
2425

2526
endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module Completions
5+
module Endpoints
6+
class Mistral < OpenAi
7+
def self.can_contact?(model_provider)
8+
model_provider == "mistral"
9+
end
10+
end
11+
end
12+
end
13+
end

lib/completions/llm.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ def presets
9090
endpoint: "https://api.sambanova.ai/v1/chat/completions",
9191
provider: "samba_nova",
9292
},
93+
{
94+
id: "mistral",
95+
models: [
96+
{
97+
name: "mistral-large-latest",
98+
tokens: 128_000,
99+
display_name: "Mistral Large",
100+
},
101+
{
102+
name: "pixtral-large-latest",
103+
tokens: 128_000,
104+
display_name: "Pixtral Large",
105+
},
106+
],
107+
tokenizer: DiscourseAi::Tokenizer::MixtralTokenizer,
108+
endpoint: "https://api.mistral.ai/v1/chat/completions",
109+
provider: "mistral",
110+
},
93111
]
94112
end
95113
end
@@ -105,6 +123,7 @@ def provider_names
105123
google
106124
azure
107125
samba_nova
126+
mistral
108127
]
109128
if !Rails.env.production?
110129
providers << "fake"

spec/fabricators/llm_model_fabricator.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@
9090
provider_params { { enable_native_tool: true } }
9191
end
9292

93+
Fabricator(:mistral_model, from: :llm_model) do
94+
display_name "Mistral Large"
95+
name "mistral-large-latest"
96+
provider "mistral"
97+
api_key "ABC"
98+
tokenizer "DiscourseAi::Tokenizer::MixtralTokenizer"
99+
url "https://api.mistral.ai/v1/chat/completions"
100+
provider_params { { disable_native_tools: false } }
101+
end
102+
93103
Fabricator(:seeded_model, from: :llm_model) do
94104
id "-2"
95105
display_name "CDCK Hosted Model"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
require_relative "dialect_context"
5+
6+
RSpec.describe DiscourseAi::Completions::Dialects::Mistral do
7+
fab!(:model) { Fabricate(:mistral_model) }
8+
let(:context) { DialectContext.new(described_class, model) }
9+
let(:image100x100) { plugin_file_from_fixtures("100x100.jpg") }
10+
let(:upload100x100) do
11+
UploadCreator.new(image100x100, "image.jpg").create_for(Discourse.system_user.id)
12+
end
13+
14+
it "does not include user names" do
15+
prompt =
16+
DiscourseAi::Completions::Prompt.new(
17+
messages: [type: :user, content: "Hello, I am Bob", id: "bob"],
18+
)
19+
20+
dialect = described_class.new(prompt, model)
21+
22+
# mistral has no support for name
23+
expect(dialect.translate).to eq([{ role: "user", content: "bob: Hello, I am Bob" }])
24+
end
25+
26+
it "can properly encode images" do
27+
model.update!(vision_enabled: true)
28+
29+
prompt =
30+
DiscourseAi::Completions::Prompt.new(
31+
"You are image bot",
32+
messages: [type: :user, id: "user1", content: "hello", upload_ids: [upload100x100.id]],
33+
)
34+
35+
encoded = prompt.encoded_uploads(prompt.messages.last)
36+
37+
image = "data:image/jpeg;base64,#{encoded[0][:base64]}"
38+
39+
dialect = described_class.new(prompt, model)
40+
41+
content = dialect.translate[1][:content]
42+
43+
expect(content).to eq(
44+
[{ type: "image_url", image_url: { url: image } }, { type: "text", text: "user1: hello" }],
45+
)
46+
end
47+
48+
it "can properly map tool calls to mistral format" do
49+
result = [
50+
{
51+
role: "system",
52+
content:
53+
"I want you to act as a title generator for written pieces. I will provide you with a text,\nand you will generate five attention-grabbing titles. Please keep the title concise and under 20 words,\nand ensure that the meaning is maintained. Replies will utilize the language type of the topic.\n",
54+
},
55+
{ role: "user", content: "user1: This is a message by a user" },
56+
{ role: "assistant", content: "I'm a previous bot reply, that's why there's no user" },
57+
{ role: "user", content: "user1: This is a new message by a user" },
58+
{
59+
role: "assistant",
60+
content: "",
61+
tool_calls: [
62+
{
63+
type: "function",
64+
function: {
65+
arguments: "{\"location\":\"Sydney\",\"unit\":\"c\"}",
66+
name: "get_weather",
67+
},
68+
id: "tool_id",
69+
},
70+
],
71+
},
72+
{
73+
role: "tool",
74+
tool_call_id: "tool_id",
75+
content: "\"I'm a tool result\"",
76+
name: "get_weather",
77+
},
78+
]
79+
expect(context.multi_turn_scenario).to eq(result)
80+
81+
bang
82+
end
83+
end

0 commit comments

Comments
 (0)