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

Commit 4e85240

Browse files
committed
FEATURE: first class support for OpenRouter
This new implementation supports picking quantization and provider pref Also: - Improve logging for summary generation - Improve error message when contacting LLMs fails
1 parent 6ce14a7 commit 4e85240

File tree

13 files changed

+152
-11
lines changed

13 files changed

+152
-11
lines changed

app/models/ai_api_audit_log.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module Provider
1515
Ollama = 7
1616
SambaNova = 8
1717
Mistral = 9
18+
OpenRouter = 10
1819
end
1920

2021
def next_log_id

app/models/llm_model.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def self.provider_params
4747
disable_system_prompt: :checkbox,
4848
enable_native_tool: :checkbox,
4949
},
50+
open_router: {
51+
disable_native_tools: :checkbox,
52+
provider_order: :text,
53+
provider_quantizations: :text,
54+
},
5055
}
5156
end
5257

config/locales/client.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ en:
351351
CDCK: "CDCK"
352352
samba_nova: "SambaNova"
353353
mistral: "Mistral"
354+
open_router: "OpenRouter"
354355
fake: "Custom"
355356

356357
provider_fields:
@@ -360,6 +361,8 @@ en:
360361
disable_system_prompt: "Disable system message in prompts"
361362
enable_native_tool: "Enable native tool support"
362363
disable_native_tools: "Disable native tool support (use XML based tools)"
364+
provider_order: "Provider order (comma delimited list)"
365+
provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"
363366

364367
related_topics:
365368
title: "Related topics"

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ en:
252252
failed_to_share: "Failed to share the conversation"
253253
conversation_deleted: "Conversation share deleted successfully"
254254
ai_bot:
255+
reply_error: "Sorry, it looks like our system encountered an unexpected issue while trying to reply.\n\n[details='Error details']\n%{details}\n[/details]"
255256
default_pm_prefix: "[Untitled AI bot PM]"
256257
personas:
257258
default_llm_required: "Default LLM model is required prior to enabling Chat"

lib/ai_bot/playground.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,26 @@ def reply_to(post, custom_instructions: nil, &blk)
533533
reply_post.post_custom_prompt.update!(custom_prompt: prompt)
534534
end
535535

536+
reply_post
537+
rescue => e
538+
if reply_post
539+
details = e.message.to_s
540+
reply = "#{reply}\n\n#{I18n.t("discourse_ai.ai_bot.reply_error", details: details)}"
541+
reply_post.revise(
542+
bot.bot_user,
543+
{ raw: reply },
544+
skip_validations: true,
545+
skip_revision: true,
546+
)
547+
end
548+
raise e
549+
ensure
536550
# since we are skipping validations and jobs we
537551
# may need to fix participant count
538-
if reply_post.topic.private_message? && reply_post.topic.participant_count < 2
552+
if reply_post && reply_post.topic && reply_post.topic.private_message? &&
553+
reply_post.topic.participant_count < 2
539554
reply_post.topic.update!(participant_count: 2)
540555
end
541-
542-
reply_post
543-
ensure
544556
post_streamer&.finish(skip_callback: true)
545557
publish_final_update(reply_post) if stream_reply
546558
if reply_post && post.post_number == 1 && post.topic.private_message?

lib/completions/dialects/chat_gpt.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ module Dialects
66
class ChatGpt < Dialect
77
class << self
88
def can_translate?(llm_model)
9-
llm_model.provider == "open_ai" || llm_model.provider == "azure"
9+
llm_model.provider == "open_router" || llm_model.provider == "open_ai" ||
10+
llm_model.provider == "azure"
1011
end
1112
end
1213

lib/completions/endpoints/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def endpoint_for(provider_name)
2121
DiscourseAi::Completions::Endpoints::Cohere,
2222
DiscourseAi::Completions::Endpoints::SambaNova,
2323
DiscourseAi::Completions::Endpoints::Mistral,
24+
DiscourseAi::Completions::Endpoints::OpenRouter,
2425
]
2526

2627
endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module Completions
5+
module Endpoints
6+
class OpenRouter < OpenAi
7+
def self.can_contact?(model_provider)
8+
%w[open_router].include?(model_provider)
9+
end
10+
11+
def prepare_request(payload)
12+
headers = { "Content-Type" => "application/json" }
13+
api_key = llm_model.api_key
14+
15+
headers["Authorization"] = "Bearer #{api_key}"
16+
headers["X-Title"] = "Discourse AI"
17+
headers["HTTP-Referer"] = "https://www.discourse.org/ai"
18+
19+
Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
20+
end
21+
22+
def prepare_payload(prompt, model_params, dialect)
23+
payload = super
24+
25+
if quantizations = llm_model.provider_params["provider_quantizations"].presence
26+
options = quantizations.split(",").map(&:strip)
27+
28+
payload[:provider] = { quantizations: options }
29+
end
30+
31+
if order = llm_model.provider_params["provider_order"].presence
32+
options = order.split(",").map(&:strip)
33+
payload[:provider] ||= {}
34+
payload[:provider][:order] = options
35+
end
36+
37+
payload
38+
end
39+
end
40+
end
41+
end
42+
end

lib/completions/llm.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ def presets
108108
endpoint: "https://api.mistral.ai/v1/chat/completions",
109109
provider: "mistral",
110110
},
111+
{
112+
id: "open_router",
113+
models: [
114+
{
115+
name: "meta-llama/llama-3.3-70b-instruct",
116+
tokens: 128_000,
117+
display_name: "Llama 3.3 70B",
118+
},
119+
{
120+
name: "google/gemini-flash-1.5-exp",
121+
tokens: 1_000_000,
122+
display_name: "Gemini Flash 1.5 Exp",
123+
},
124+
],
125+
tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
126+
endpoint: "https://openrouter.ai/api/v1/chat/completions",
127+
provider: "open_router",
128+
},
111129
]
112130
end
113131
end
@@ -124,6 +142,7 @@ def provider_names
124142
azure
125143
samba_nova
126144
mistral
145+
open_router
127146
]
128147
if !Rails.env.production?
129148
providers << "fake"

lib/summarization/strategies/hot_topic_gists.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def summary_extension_prompt(summary, contents)
6464
.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
6565
.join("\n")
6666

67-
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
67+
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
6868
You are an advanced summarization bot. Your task is to update an existing single-sentence summary by integrating new developments from a conversation.
6969
Analyze the most recent messages to identify key updates or shifts in the main topic and reflect these in the updated summary.
7070
Emphasize new significant information or developments within the context of the initial conversation theme.
@@ -103,7 +103,7 @@ def first_summary_prompt(contents)
103103
statements =
104104
contents.to_a.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
105105

106-
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
106+
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
107107
You are an advanced summarization bot. Analyze a given conversation and produce a concise,
108108
single-sentence summary that conveys the main topic and current developments to someone with no prior context.
109109
@@ -124,9 +124,9 @@ def first_summary_prompt(contents)
124124
### Context:
125125
126126
#{content_title.present? ? "The discussion title is: " + content_title + ". (DO NOT REPEAT THIS IN THE SUMMARY)\n" : ""}
127-
127+
128128
The conversation began with the following statement:
129-
129+
130130
#{statements.shift}\n
131131
TEXT
132132

0 commit comments

Comments
 (0)