Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/models/llm_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ def self.provider_params
access_key_id: :text,
region: :text,
disable_native_tools: :checkbox,
enable_reasoning: :checkbox,
reasoning_tokens: :number,
},
anthropic: {
disable_native_tools: :checkbox,
enable_reasoning: :checkbox,
reasoning_tokens: :number,
},
open_ai: {
organization: :text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export default class AiLlmEditorForm extends Component {
provider: model.provider,
enabled_chat_bot: model.enabled_chat_bot,
vision_enabled: model.vision_enabled,
provider_params: this.computeProviderParams(model.provider),
provider_params: this.computeProviderParams(
model.provider,
model.provider_params
),
llm_quotas: model.llm_quotas,
};
}
Expand Down Expand Up @@ -128,12 +131,12 @@ export default class AiLlmEditorForm extends Component {
return !this.args.model.isNew;
}

computeProviderParams(provider) {
computeProviderParams(provider, currentParams = {}) {
const params = this.args.llms.resultSetMeta.provider_params[provider] ?? {};
return Object.fromEntries(
Object.entries(params).map(([k, v]) => [
k,
v?.type === "enum" ? v.default : null,
currentParams[k] ?? (v?.type === "enum" ? v.default : null),
])
);
}
Expand Down
4 changes: 3 additions & 1 deletion config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ en:

model_description:
none: "General settings that work for most language models"
anthropic-claude-3-5-sonnet: "Anthropic's most intelligent model"
anthropic-claude-3-7-sonnet: "Anthropic's most intelligent model"
anthropic-claude-3-5-haiku: "Fast and cost-effective"
anthropic-claude-3-opus: "Excels at writing and complex tasks"
google-gemini-1-5-pro: "Mid-sized multimodal model capable of a wide range of tasks"
Expand Down Expand Up @@ -459,6 +459,8 @@ en:
provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"
disable_streaming: "Disable streaming completions (convert streaming to non streaming requests)"
reasoning_effort: "Reasoning effort (only applicable to reasoning models)"
enable_reasoning: "Enable reasoning (only applicable to Sonnet 3.7)"
reasoning_tokens: "Number of tokens used for reasoning"

related_topics:
title: "Related topics"
Expand Down
13 changes: 13 additions & 0 deletions lib/completions/endpoints/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ def default_options(dialect)

options = { model: mapped_model, max_tokens: max_tokens }

if llm_model.lookup_custom_param("enable_reasoning")
reasoning_tokens = llm_model.lookup_custom_param("reasoning_tokens").to_i
if reasoning_tokens < 100
reasoning_tokens = 100
elsif reasoning_tokens > 65_536
reasoning_tokens = 65_536
end

# this allows for lots of tokens beyond reasoning
options[:max_tokens] = reasoning_tokens + 30_000
options[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
end

options[:stop_sequences] = ["</function_calls>"] if !dialect.native_tool_support? &&
dialect.prompt.has_tools?

Expand Down
19 changes: 18 additions & 1 deletion lib/completions/endpoints/aws_bedrock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,22 @@ def default_options(dialect)
max_tokens = 4096
max_tokens = 8192 if bedrock_model_id.match?(/3.5/)

{ max_tokens: max_tokens, anthropic_version: "bedrock-2023-05-31" }
result = { anthropic_version: "bedrock-2023-05-31" }
if llm_model.lookup_custom_param("enable_reasoning")
reasoning_tokens = llm_model.lookup_custom_param("reasoning_tokens").to_i
if reasoning_tokens < 100
reasoning_tokens = 100
elsif reasoning_tokens > 65_536
reasoning_tokens = 65_536
end

# this allows for ample tokens beyond reasoning
max_tokens = reasoning_tokens + 30_000
result[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
end
result[:max_tokens] = max_tokens

result
else
{}
end
Expand Down Expand Up @@ -66,6 +81,8 @@ def bedrock_model_id
"anthropic.claude-3-5-sonnet-20241022-v2:0"
when "claude-3-5-haiku"
"anthropic.claude-3-5-haiku-20241022-v1:0"
when "claude-3-7-sonnet"
"anthropic.claude-3-7-sonnet-20250219-v1:0"
else
llm_model.name
end
Expand Down
4 changes: 2 additions & 2 deletions lib/completions/llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def presets
id: "anthropic",
models: [
{
name: "claude-3-5-sonnet",
name: "claude-3-7-sonnet",
tokens: 200_000,
display_name: "Claude 3.5 Sonnet",
display_name: "Claude 3.7 Sonnet",
},
{ name: "claude-3-5-haiku", tokens: 200_000, display_name: "Claude 3.5 Haiku" },
{ name: "claude-3-opus", tokens: 200_000, display_name: "Claude 3 Opus" },
Expand Down
62 changes: 62 additions & 0 deletions spec/lib/completions/endpoints/anthropic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,68 @@
expect(requested_body).to eq(request_body)
end

it "can support reasoning" do
body = <<~STRING
{
"content": [
{
"text": "Hello!",
"type": "text"
}
],
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
"model": "claude-3-opus-20240229",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
"type": "message",
"usage": {
"input_tokens": 10,
"output_tokens": 25
}
}
STRING

parsed_body = nil
stub_request(:post, url).with(
body:
proc do |req_body|
parsed_body = JSON.parse(req_body, symbolize_names: true)
true
end,
headers: {
"Content-Type" => "application/json",
"X-Api-Key" => "123",
"Anthropic-Version" => "2023-06-01",
},
).to_return(status: 200, body: body)

model.provider_params["enable_reasoning"] = true
model.provider_params["reasoning_tokens"] = 10_000
model.save!

proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")
result = proxy.generate(prompt, user: Discourse.system_user)
expect(result).to eq("Hello!")

expected_body = {
model: "claude-3-opus-20240229",
max_tokens: 40_000,
thinking: {
type: "enabled",
budget_tokens: 10_000,
},
messages: [{ role: "user", content: "user1: hello" }],
system: "You are hello bot",
}
expect(parsed_body).to eq(expected_body)

log = AiApiAuditLog.order(:id).last
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
expect(log.request_tokens).to eq(10)
expect(log.response_tokens).to eq(25)
end

it "can operate in regular mode" do
body = <<~STRING
{
Expand Down
51 changes: 51 additions & 0 deletions spec/lib/completions/endpoints/aws_bedrock_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,57 @@ def encode_message(message)
expect(log.response_tokens).to eq(20)
end

it "supports thinking" do
model.provider_params["enable_reasoning"] = true
model.provider_params["reasoning_tokens"] = 10_000
model.save!

proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")

request = nil

content = {
content: [text: "hello sam"],
usage: {
input_tokens: 10,
output_tokens: 20,
},
}.to_json

stub_request(
:post,
"https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke",
)
.with do |inner_request|
request = inner_request
true
end
.to_return(status: 200, body: content)

response = proxy.generate("hello world", user: user)

expect(request.headers["Authorization"]).to be_present
expect(request.headers["X-Amz-Content-Sha256"]).to be_present

expected = {
"max_tokens" => 40_000,
"thinking" => {
"type" => "enabled",
"budget_tokens" => 10_000,
},
"anthropic_version" => "bedrock-2023-05-31",
"messages" => [{ "role" => "user", "content" => "hello world" }],
"system" => "You are a helpful bot",
}
expect(JSON.parse(request.body)).to eq(expected)

expect(response).to eq("hello sam")

log = AiApiAuditLog.order(:id).last
expect(log.request_tokens).to eq(10)
expect(log.response_tokens).to eq(20)
end

it "supports claude 3 streaming" do
proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")

Expand Down
10 changes: 6 additions & 4 deletions spec/system/llms/ai_llm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@

context "when changing the provider" do
it "has the correct provider params when visiting the edit page" do
llm = Fabricate(:llm_model, provider: "open_ai", provider_params: {})
llm =
Fabricate(:llm_model, provider: "anthropic", provider_params: { enable_reasoning: true })
visit "/admin/plugins/discourse-ai/ai-llms/#{llm.id}/edit"

expect(form).to have_field_with_name("provider_params.organization")
expect(form).to have_field_with_name("provider_params.disable_native_tools")
expect(form).to have_field_with_name("provider_params.disable_streaming")
expect(form).to have_field_with_name("provider_params.reasoning_effort")
expect(form).to have_field_with_name("provider_params.reasoning_tokens")

reasoning = form.field("provider_params.enable_reasoning")
expect(reasoning).to be_checked
end
it "correctly changes the provider params" do
visit "/admin/plugins/discourse-ai/ai-llms"
Expand Down
Loading