FEATURE: first class support for OpenRouter (#1011)
* 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 * Better support for full screen artifacts on iPad Support back button to close full screen
This commit is contained in:
parent
51e0a96e51
commit
7ca21cc329
|
@ -15,6 +15,7 @@ class AiApiAuditLog < ActiveRecord::Base
|
||||||
Ollama = 7
|
Ollama = 7
|
||||||
SambaNova = 8
|
SambaNova = 8
|
||||||
Mistral = 9
|
Mistral = 9
|
||||||
|
OpenRouter = 10
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_log_id
|
def next_log_id
|
||||||
|
|
|
@ -47,6 +47,11 @@ class LlmModel < ActiveRecord::Base
|
||||||
disable_system_prompt: :checkbox,
|
disable_system_prompt: :checkbox,
|
||||||
enable_native_tool: :checkbox,
|
enable_native_tool: :checkbox,
|
||||||
},
|
},
|
||||||
|
open_router: {
|
||||||
|
disable_native_tools: :checkbox,
|
||||||
|
provider_order: :text,
|
||||||
|
provider_quantizations: :text,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import htmlClass from "discourse/helpers/html-class";
|
import htmlClass from "discourse/helpers/html-class";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
|
// note the panel for artifact full screen can not be at position 0,0
|
||||||
|
// otherwise this hack will not activate.
|
||||||
|
// https://github.com/discourse/discourse/blob/b8325f2190a8c0a9022405c219faeac6f0f98ca5/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js#L77-L77
|
||||||
|
// this will cause post stream to navigate to a different post
|
||||||
|
|
||||||
export default class AiArtifactComponent extends Component {
|
export default class AiArtifactComponent extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@tracked expanded = false;
|
@tracked expanded = false;
|
||||||
|
@ -15,17 +19,29 @@ export default class AiArtifactComponent extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.keydownHandler = this.handleKeydown.bind(this);
|
this.keydownHandler = this.handleKeydown.bind(this);
|
||||||
|
this.popStateHandler = this.handlePopState.bind(this);
|
||||||
|
window.addEventListener("popstate", this.popStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
super.willDestroy(...arguments);
|
super.willDestroy(...arguments);
|
||||||
window.removeEventListener("keydown", this.keydownHandler);
|
window.removeEventListener("keydown", this.keydownHandler);
|
||||||
|
window.removeEventListener("popstate", this.popStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleKeydown(event) {
|
handleKeydown(event) {
|
||||||
if (event.key === "Escape" || event.key === "Esc") {
|
if (event.key === "Escape" || event.key === "Esc") {
|
||||||
this.expanded = false;
|
history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handlePopState(event) {
|
||||||
|
const state = event.state;
|
||||||
|
this.expanded = state?.artifactId === this.args.artifactId;
|
||||||
|
if (!this.expanded) {
|
||||||
|
window.removeEventListener("keydown", this.keydownHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,12 +68,17 @@ export default class AiArtifactComponent extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleView() {
|
toggleView() {
|
||||||
this.expanded = !this.expanded;
|
if (!this.expanded) {
|
||||||
if (this.expanded) {
|
window.history.pushState(
|
||||||
|
{ artifactId: this.args.artifactId },
|
||||||
|
"",
|
||||||
|
window.location.href + "#artifact-fullscreen"
|
||||||
|
);
|
||||||
window.addEventListener("keydown", this.keydownHandler);
|
window.addEventListener("keydown", this.keydownHandler);
|
||||||
} else {
|
} else {
|
||||||
window.removeEventListener("keydown", this.keydownHandler);
|
history.back();
|
||||||
}
|
}
|
||||||
|
this.expanded = !this.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
get wrapperClasses() {
|
get wrapperClasses() {
|
||||||
|
@ -66,25 +87,12 @@ export default class AiArtifactComponent extends Component {
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
artifactPanelHover() {
|
|
||||||
// retrrigger animation
|
|
||||||
const panel = document.querySelector(".ai-artifact__panel");
|
|
||||||
panel.style.animation = "none"; // Stop the animation
|
|
||||||
setTimeout(() => {
|
|
||||||
panel.style.animation = ""; // Re-trigger the animation by removing the none style
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.expanded}}
|
{{#if this.expanded}}
|
||||||
{{htmlClass "ai-artifact-expanded"}}
|
{{htmlClass "ai-artifact-expanded"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class={{this.wrapperClasses}}>
|
<div class={{this.wrapperClasses}}>
|
||||||
<div
|
<div class="ai-artifact__panel--wrapper">
|
||||||
class="ai-artifact__panel--wrapper"
|
|
||||||
{{on "mouseleave" this.artifactPanelHover}}
|
|
||||||
>
|
|
||||||
<div class="ai-artifact__panel">
|
<div class="ai-artifact__panel">
|
||||||
<DButton
|
<DButton
|
||||||
class="btn-flat btn-icon-text"
|
class="btn-flat btn-icon-text"
|
||||||
|
|
|
@ -37,27 +37,21 @@ html.ai-artifact-expanded {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-artifact__panel--wrapper {
|
.ai-artifact__panel--wrapper {
|
||||||
display: block;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 2em;
|
||||||
right: 0;
|
right: 2em;
|
||||||
height: 4em;
|
height: 2em;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
&:hover {
|
animation: vanishing 0.5s 3s forwards;
|
||||||
.ai-artifact__panel {
|
|
||||||
transform: translateY(0) !important;
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-artifact__panel {
|
.ai-artifact__panel {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 2em;
|
||||||
right: 0;
|
right: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
transition: transform 0.5s ease-in-out;
|
transition: transform 0.5s ease-in-out;
|
||||||
animation: slideUp 0.5s 3s forwards;
|
animation: slideUp 0.5s 3s forwards;
|
||||||
|
@ -75,7 +69,6 @@ html.ai-artifact-expanded {
|
||||||
.d-icon {
|
.d-icon {
|
||||||
color: var(--secondary-high);
|
color: var(--secondary-high);
|
||||||
}
|
}
|
||||||
//color: var(--secondary-vary-low);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +78,12 @@ html.ai-artifact-expanded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes vanishing {
|
||||||
|
to {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -351,6 +351,7 @@ en:
|
||||||
CDCK: "CDCK"
|
CDCK: "CDCK"
|
||||||
samba_nova: "SambaNova"
|
samba_nova: "SambaNova"
|
||||||
mistral: "Mistral"
|
mistral: "Mistral"
|
||||||
|
open_router: "OpenRouter"
|
||||||
fake: "Custom"
|
fake: "Custom"
|
||||||
|
|
||||||
provider_fields:
|
provider_fields:
|
||||||
|
@ -360,6 +361,8 @@ en:
|
||||||
disable_system_prompt: "Disable system message in prompts"
|
disable_system_prompt: "Disable system message in prompts"
|
||||||
enable_native_tool: "Enable native tool support"
|
enable_native_tool: "Enable native tool support"
|
||||||
disable_native_tools: "Disable native tool support (use XML based tools)"
|
disable_native_tools: "Disable native tool support (use XML based tools)"
|
||||||
|
provider_order: "Provider order (comma delimited list)"
|
||||||
|
provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"
|
||||||
|
|
||||||
related_topics:
|
related_topics:
|
||||||
title: "Related topics"
|
title: "Related topics"
|
||||||
|
@ -436,7 +439,7 @@ en:
|
||||||
|
|
||||||
ai_artifact:
|
ai_artifact:
|
||||||
expand_view_label: "Expand view"
|
expand_view_label: "Expand view"
|
||||||
collapse_view_label: "Exit Fullscreen (ESC)"
|
collapse_view_label: "Exit Fullscreen (ESC or Back button)"
|
||||||
click_to_run_label: "Run Artifact"
|
click_to_run_label: "Run Artifact"
|
||||||
|
|
||||||
ai_bot:
|
ai_bot:
|
||||||
|
|
|
@ -252,6 +252,7 @@ en:
|
||||||
failed_to_share: "Failed to share the conversation"
|
failed_to_share: "Failed to share the conversation"
|
||||||
conversation_deleted: "Conversation share deleted successfully"
|
conversation_deleted: "Conversation share deleted successfully"
|
||||||
ai_bot:
|
ai_bot:
|
||||||
|
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]"
|
||||||
default_pm_prefix: "[Untitled AI bot PM]"
|
default_pm_prefix: "[Untitled AI bot PM]"
|
||||||
personas:
|
personas:
|
||||||
default_llm_required: "Default LLM model is required prior to enabling Chat"
|
default_llm_required: "Default LLM model is required prior to enabling Chat"
|
||||||
|
|
|
@ -533,14 +533,26 @@ module DiscourseAi
|
||||||
reply_post.post_custom_prompt.update!(custom_prompt: prompt)
|
reply_post.post_custom_prompt.update!(custom_prompt: prompt)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
reply_post
|
||||||
|
rescue => e
|
||||||
|
if reply_post
|
||||||
|
details = e.message.to_s
|
||||||
|
reply = "#{reply}\n\n#{I18n.t("discourse_ai.ai_bot.reply_error", details: details)}"
|
||||||
|
reply_post.revise(
|
||||||
|
bot.bot_user,
|
||||||
|
{ raw: reply },
|
||||||
|
skip_validations: true,
|
||||||
|
skip_revision: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
raise e
|
||||||
|
ensure
|
||||||
# since we are skipping validations and jobs we
|
# since we are skipping validations and jobs we
|
||||||
# may need to fix participant count
|
# may need to fix participant count
|
||||||
if reply_post.topic.private_message? && reply_post.topic.participant_count < 2
|
if reply_post && reply_post.topic && reply_post.topic.private_message? &&
|
||||||
|
reply_post.topic.participant_count < 2
|
||||||
reply_post.topic.update!(participant_count: 2)
|
reply_post.topic.update!(participant_count: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
reply_post
|
|
||||||
ensure
|
|
||||||
post_streamer&.finish(skip_callback: true)
|
post_streamer&.finish(skip_callback: true)
|
||||||
publish_final_update(reply_post) if stream_reply
|
publish_final_update(reply_post) if stream_reply
|
||||||
if reply_post && post.post_number == 1 && post.topic.private_message?
|
if reply_post && post.post_number == 1 && post.topic.private_message?
|
||||||
|
|
|
@ -6,7 +6,8 @@ module DiscourseAi
|
||||||
class ChatGpt < Dialect
|
class ChatGpt < Dialect
|
||||||
class << self
|
class << self
|
||||||
def can_translate?(llm_model)
|
def can_translate?(llm_model)
|
||||||
llm_model.provider == "open_ai" || llm_model.provider == "azure"
|
llm_model.provider == "open_router" || llm_model.provider == "open_ai" ||
|
||||||
|
llm_model.provider == "azure"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ module DiscourseAi
|
||||||
DiscourseAi::Completions::Endpoints::Cohere,
|
DiscourseAi::Completions::Endpoints::Cohere,
|
||||||
DiscourseAi::Completions::Endpoints::SambaNova,
|
DiscourseAi::Completions::Endpoints::SambaNova,
|
||||||
DiscourseAi::Completions::Endpoints::Mistral,
|
DiscourseAi::Completions::Endpoints::Mistral,
|
||||||
|
DiscourseAi::Completions::Endpoints::OpenRouter,
|
||||||
]
|
]
|
||||||
|
|
||||||
endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
|
endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Completions
|
||||||
|
module Endpoints
|
||||||
|
class OpenRouter < OpenAi
|
||||||
|
def self.can_contact?(model_provider)
|
||||||
|
%w[open_router].include?(model_provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_request(payload)
|
||||||
|
headers = { "Content-Type" => "application/json" }
|
||||||
|
api_key = llm_model.api_key
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer #{api_key}"
|
||||||
|
headers["X-Title"] = "Discourse AI"
|
||||||
|
headers["HTTP-Referer"] = "https://www.discourse.org/ai"
|
||||||
|
|
||||||
|
Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_payload(prompt, model_params, dialect)
|
||||||
|
payload = super
|
||||||
|
|
||||||
|
if quantizations = llm_model.provider_params["provider_quantizations"].presence
|
||||||
|
options = quantizations.split(",").map(&:strip)
|
||||||
|
|
||||||
|
payload[:provider] = { quantizations: options }
|
||||||
|
end
|
||||||
|
|
||||||
|
if order = llm_model.provider_params["provider_order"].presence
|
||||||
|
options = order.split(",").map(&:strip)
|
||||||
|
payload[:provider] ||= {}
|
||||||
|
payload[:provider][:order] = options
|
||||||
|
end
|
||||||
|
|
||||||
|
payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -108,6 +108,24 @@ module DiscourseAi
|
||||||
endpoint: "https://api.mistral.ai/v1/chat/completions",
|
endpoint: "https://api.mistral.ai/v1/chat/completions",
|
||||||
provider: "mistral",
|
provider: "mistral",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "open_router",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
name: "meta-llama/llama-3.3-70b-instruct",
|
||||||
|
tokens: 128_000,
|
||||||
|
display_name: "Llama 3.3 70B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google/gemini-flash-1.5-exp",
|
||||||
|
tokens: 1_000_000,
|
||||||
|
display_name: "Gemini Flash 1.5 Exp",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||||
|
endpoint: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
provider: "open_router",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -124,6 +142,7 @@ module DiscourseAi
|
||||||
azure
|
azure
|
||||||
samba_nova
|
samba_nova
|
||||||
mistral
|
mistral
|
||||||
|
open_router
|
||||||
]
|
]
|
||||||
if !Rails.env.production?
|
if !Rails.env.production?
|
||||||
providers << "fake"
|
providers << "fake"
|
||||||
|
|
|
@ -64,7 +64,7 @@ module DiscourseAi
|
||||||
.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
|
.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
|
||||||
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
|
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
|
||||||
You are an advanced summarization bot. Your task is to update an existing single-sentence summary by integrating new developments from a conversation.
|
You are an advanced summarization bot. Your task is to update an existing single-sentence summary by integrating new developments from a conversation.
|
||||||
Analyze the most recent messages to identify key updates or shifts in the main topic and reflect these in the updated summary.
|
Analyze the most recent messages to identify key updates or shifts in the main topic and reflect these in the updated summary.
|
||||||
Emphasize new significant information or developments within the context of the initial conversation theme.
|
Emphasize new significant information or developments within the context of the initial conversation theme.
|
||||||
|
@ -103,7 +103,7 @@ module DiscourseAi
|
||||||
statements =
|
statements =
|
||||||
contents.to_a.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
|
contents.to_a.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
|
||||||
|
|
||||||
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
|
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
|
||||||
You are an advanced summarization bot. Analyze a given conversation and produce a concise,
|
You are an advanced summarization bot. Analyze a given conversation and produce a concise,
|
||||||
single-sentence summary that conveys the main topic and current developments to someone with no prior context.
|
single-sentence summary that conveys the main topic and current developments to someone with no prior context.
|
||||||
|
|
||||||
|
@ -124,9 +124,9 @@ module DiscourseAi
|
||||||
### Context:
|
### Context:
|
||||||
|
|
||||||
#{content_title.present? ? "The discussion title is: " + content_title + ". (DO NOT REPEAT THIS IN THE SUMMARY)\n" : ""}
|
#{content_title.present? ? "The discussion title is: " + content_title + ". (DO NOT REPEAT THIS IN THE SUMMARY)\n" : ""}
|
||||||
|
|
||||||
The conversation began with the following statement:
|
The conversation began with the following statement:
|
||||||
|
|
||||||
#{statements.shift}\n
|
#{statements.shift}\n
|
||||||
TEXT
|
TEXT
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ module DiscourseAi
|
||||||
input =
|
input =
|
||||||
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]})" }.join
|
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]})" }.join
|
||||||
|
|
||||||
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT)
|
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT, topic_id: target.id)
|
||||||
You are an advanced summarization bot tasked with enhancing an existing summary by incorporating additional posts.
|
You are an advanced summarization bot tasked with enhancing an existing summary by incorporating additional posts.
|
||||||
|
|
||||||
### Guidelines:
|
### Guidelines:
|
||||||
|
@ -76,7 +76,7 @@ module DiscourseAi
|
||||||
input =
|
input =
|
||||||
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }.join
|
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }.join
|
||||||
|
|
||||||
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
|
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
|
||||||
You are an advanced summarization bot that generates concise, coherent summaries of provided text.
|
You are an advanced summarization bot that generates concise, coherent summaries of provided text.
|
||||||
|
|
||||||
- Only include the summary, without any additional commentary.
|
- Only include the summary, without any additional commentary.
|
||||||
|
|
|
@ -28,6 +28,15 @@ Fabricator(:hf_model, from: :llm_model) do
|
||||||
provider "hugging_face"
|
provider "hugging_face"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Fabricator(:open_router_model, from: :llm_model) do
|
||||||
|
display_name "OpenRouter"
|
||||||
|
name "openrouter-1.0"
|
||||||
|
provider "open_router"
|
||||||
|
tokenizer "DiscourseAi::Tokenizer::OpenAiTokenizer"
|
||||||
|
max_prompt_tokens 64_000
|
||||||
|
url "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
end
|
||||||
|
|
||||||
Fabricator(:vllm_model, from: :llm_model) do
|
Fabricator(:vllm_model, from: :llm_model) do
|
||||||
display_name "Llama 3.1 vLLM"
|
display_name "Llama 3.1 vLLM"
|
||||||
name "meta-llama/Meta-Llama-3.1-70B-Instruct"
|
name "meta-llama/Meta-Llama-3.1-70B-Instruct"
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe DiscourseAi::Completions::Endpoints::OpenRouter do
|
||||||
|
fab!(:user)
|
||||||
|
fab!(:open_router_model)
|
||||||
|
|
||||||
|
subject(:endpoint) { described_class.new(open_router_model) }
|
||||||
|
|
||||||
|
it "supports provider quantization and order selection" do
|
||||||
|
open_router_model.provider_params["provider_quantizations"] = "int8,int16"
|
||||||
|
open_router_model.provider_params["provider_order"] = "Google, Amazon Bedrock"
|
||||||
|
open_router_model.save!
|
||||||
|
|
||||||
|
parsed_body = nil
|
||||||
|
stub_request(:post, open_router_model.url).with(
|
||||||
|
body: proc { |body| parsed_body = JSON.parse(body, symbolize_names: true) },
|
||||||
|
headers: {
|
||||||
|
"Content-Type" => "application/json",
|
||||||
|
"X-Title" => "Discourse AI",
|
||||||
|
"HTTP-Referer" => "https://www.discourse.org/ai",
|
||||||
|
"Authorization" => "Bearer 123",
|
||||||
|
},
|
||||||
|
).to_return(
|
||||||
|
status: 200,
|
||||||
|
body: { "choices" => [message: { role: "assistant", content: "world" }] }.to_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy = DiscourseAi::Completions::Llm.proxy("custom:#{open_router_model.id}")
|
||||||
|
result = proxy.generate("hello", user: user)
|
||||||
|
|
||||||
|
expect(result).to eq("world")
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
model: "openrouter-1.0",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: "You are a helpful bot" },
|
||||||
|
{ role: "user", content: "hello" },
|
||||||
|
],
|
||||||
|
provider: {
|
||||||
|
quantizations: %w[int8 int16],
|
||||||
|
order: ["Google", "Amazon Bedrock"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(parsed_body).to eq(expected)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue