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

Commit 791ab19

Browse files
committed
FEATURE: full support for Sonnet 3.7
- Adds support for Sonnet 3.7 with reasoning on bedrock and anthropic - Fixes regression where provider params were not populated Note. reasoning tokens are hardcoded to minimum of 100 maximum of 65536
1 parent 84e791a commit 791ab19

File tree

9 files changed

+165
-11
lines changed

9 files changed

+165
-11
lines changed

app/models/llm_model.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ def self.provider_params
2626
access_key_id: :text,
2727
region: :text,
2828
disable_native_tools: :checkbox,
29+
enable_reasoning: :checkbox,
30+
reasoning_tokens: :number,
2931
},
3032
anthropic: {
3133
disable_native_tools: :checkbox,
34+
enable_reasoning: :checkbox,
35+
reasoning_tokens: :number,
3236
},
3337
open_ai: {
3438
organization: :text,

assets/javascripts/discourse/components/ai-llm-editor-form.gjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ export default class AiLlmEditorForm extends Component {
6161
provider: model.provider,
6262
enabled_chat_bot: model.enabled_chat_bot,
6363
vision_enabled: model.vision_enabled,
64-
provider_params: this.computeProviderParams(model.provider),
64+
provider_params: this.computeProviderParams(
65+
model.provider,
66+
model.provider_params
67+
),
6568
llm_quotas: model.llm_quotas,
6669
};
6770
}
@@ -128,12 +131,12 @@ export default class AiLlmEditorForm extends Component {
128131
return !this.args.model.isNew;
129132
}
130133

131-
computeProviderParams(provider) {
134+
computeProviderParams(provider, currentParams = {}) {
132135
const params = this.args.llms.resultSetMeta.provider_params[provider] ?? {};
133136
return Object.fromEntries(
134137
Object.entries(params).map(([k, v]) => [
135138
k,
136-
v?.type === "enum" ? v.default : null,
139+
currentParams[k] ?? (v?.type === "enum" ? v.default : null),
137140
])
138141
);
139142
}

config/locales/client.en.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ en:
390390

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

463465
related_topics:
464466
title: "Related topics"

lib/completions/endpoints/anthropic.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ def default_options(dialect)
3838

3939
options = { model: mapped_model, max_tokens: max_tokens }
4040

41+
if llm_model.lookup_custom_param("enable_reasoning")
42+
reasoning_tokens = llm_model.lookup_custom_param("reasoning_tokens").to_i
43+
if reasoning_tokens < 100
44+
reasoning_tokens = 100
45+
elsif reasoning_tokens > 65_536
46+
reasoning_tokens = 65_536
47+
end
48+
49+
# this allows for lots of tokens beyond reasoning
50+
options[:max_tokens] = reasoning_tokens + 30_000
51+
options[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
52+
end
53+
4154
options[:stop_sequences] = ["</function_calls>"] if !dialect.native_tool_support? &&
4255
dialect.prompt.has_tools?
4356

lib/completions/endpoints/aws_bedrock.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,22 @@ def default_options(dialect)
2626
max_tokens = 4096
2727
max_tokens = 8192 if bedrock_model_id.match?(/3.5/)
2828

29-
{ max_tokens: max_tokens, anthropic_version: "bedrock-2023-05-31" }
29+
result = { anthropic_version: "bedrock-2023-05-31" }
30+
if llm_model.lookup_custom_param("enable_reasoning")
31+
reasoning_tokens = llm_model.lookup_custom_param("reasoning_tokens").to_i
32+
if reasoning_tokens < 100
33+
reasoning_tokens = 100
34+
elsif reasoning_tokens > 65_536
35+
reasoning_tokens = 65_536
36+
end
37+
38+
# this allows for ample tokens beyond reasoning
39+
max_tokens = reasoning_tokens + 30_000
40+
result[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
41+
end
42+
result[:max_tokens] = max_tokens
43+
44+
result
3045
else
3146
{}
3247
end
@@ -66,6 +81,8 @@ def bedrock_model_id
6681
"anthropic.claude-3-5-sonnet-20241022-v2:0"
6782
when "claude-3-5-haiku"
6883
"anthropic.claude-3-5-haiku-20241022-v1:0"
84+
when "claude-3-7-sonnet"
85+
"anthropic.claude-3-7-sonnet-20250219-v1:0"
6986
else
7087
llm_model.name
7188
end

lib/completions/llm.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ def presets
2727
id: "anthropic",
2828
models: [
2929
{
30-
name: "claude-3-5-sonnet",
30+
name: "claude-3-7-sonnet",
3131
tokens: 200_000,
32-
display_name: "Claude 3.5 Sonnet",
32+
display_name: "Claude 3.7 Sonnet",
3333
},
3434
{ name: "claude-3-5-haiku", tokens: 200_000, display_name: "Claude 3.5 Haiku" },
3535
{ name: "claude-3-opus", tokens: 200_000, display_name: "Claude 3 Opus" },

spec/lib/completions/endpoints/anthropic_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,68 @@
334334
expect(requested_body).to eq(request_body)
335335
end
336336

337+
it "can support reasoning" do
338+
body = <<~STRING
339+
{
340+
"content": [
341+
{
342+
"text": "Hello!",
343+
"type": "text"
344+
}
345+
],
346+
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
347+
"model": "claude-3-opus-20240229",
348+
"role": "assistant",
349+
"stop_reason": "end_turn",
350+
"stop_sequence": null,
351+
"type": "message",
352+
"usage": {
353+
"input_tokens": 10,
354+
"output_tokens": 25
355+
}
356+
}
357+
STRING
358+
359+
parsed_body = nil
360+
stub_request(:post, url).with(
361+
body:
362+
proc do |req_body|
363+
parsed_body = JSON.parse(req_body, symbolize_names: true)
364+
true
365+
end,
366+
headers: {
367+
"Content-Type" => "application/json",
368+
"X-Api-Key" => "123",
369+
"Anthropic-Version" => "2023-06-01",
370+
},
371+
).to_return(status: 200, body: body)
372+
373+
model.provider_params["enable_reasoning"] = true
374+
model.provider_params["reasoning_tokens"] = 10_000
375+
model.save!
376+
377+
proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")
378+
result = proxy.generate(prompt, user: Discourse.system_user)
379+
expect(result).to eq("Hello!")
380+
381+
expected_body = {
382+
model: "claude-3-opus-20240229",
383+
max_tokens: 40_000,
384+
thinking: {
385+
type: "enabled",
386+
budget_tokens: 10_000,
387+
},
388+
messages: [{ role: "user", content: "user1: hello" }],
389+
system: "You are hello bot",
390+
}
391+
expect(parsed_body).to eq(expected_body)
392+
393+
log = AiApiAuditLog.order(:id).last
394+
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
395+
expect(log.request_tokens).to eq(10)
396+
expect(log.response_tokens).to eq(25)
397+
end
398+
337399
it "can operate in regular mode" do
338400
body = <<~STRING
339401
{

spec/lib/completions/endpoints/aws_bedrock_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,57 @@ def encode_message(message)
335335
expect(log.response_tokens).to eq(20)
336336
end
337337

338+
it "supports thinking" do
339+
model.provider_params["enable_reasoning"] = true
340+
model.provider_params["reasoning_tokens"] = 10_000
341+
model.save!
342+
343+
proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")
344+
345+
request = nil
346+
347+
content = {
348+
content: [text: "hello sam"],
349+
usage: {
350+
input_tokens: 10,
351+
output_tokens: 20,
352+
},
353+
}.to_json
354+
355+
stub_request(
356+
:post,
357+
"https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke",
358+
)
359+
.with do |inner_request|
360+
request = inner_request
361+
true
362+
end
363+
.to_return(status: 200, body: content)
364+
365+
response = proxy.generate("hello world", user: user)
366+
367+
expect(request.headers["Authorization"]).to be_present
368+
expect(request.headers["X-Amz-Content-Sha256"]).to be_present
369+
370+
expected = {
371+
"max_tokens" => 40_000,
372+
"thinking" => {
373+
"type" => "enabled",
374+
"budget_tokens" => 10_000,
375+
},
376+
"anthropic_version" => "bedrock-2023-05-31",
377+
"messages" => [{ "role" => "user", "content" => "hello world" }],
378+
"system" => "You are a helpful bot",
379+
}
380+
expect(JSON.parse(request.body)).to eq(expected)
381+
382+
expect(response).to eq("hello sam")
383+
384+
log = AiApiAuditLog.order(:id).last
385+
expect(log.request_tokens).to eq(10)
386+
expect(log.response_tokens).to eq(20)
387+
end
388+
338389
it "supports claude 3 streaming" do
339390
proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")
340391

spec/system/llms/ai_llm_spec.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@
7373

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

79-
expect(form).to have_field_with_name("provider_params.organization")
8080
expect(form).to have_field_with_name("provider_params.disable_native_tools")
81-
expect(form).to have_field_with_name("provider_params.disable_streaming")
82-
expect(form).to have_field_with_name("provider_params.reasoning_effort")
81+
expect(form).to have_field_with_name("provider_params.reasoning_tokens")
82+
83+
reasoning = form.field("provider_params.enable_reasoning")
84+
expect(reasoning).to be_checked
8385
end
8486
it "correctly changes the provider params" do
8587
visit "/admin/plugins/discourse-ai/ai-llms"

0 commit comments

Comments
 (0)