2024-03-19 06:48:46 +11:00
# frozen_string_literal: true
2024-04-03 16:06:27 +11:00
require_relative " endpoint_compliance "
2023-11-23 12:58:54 -03:00
2024-03-19 06:48:46 +11:00
RSpec . describe DiscourseAi :: Completions :: Endpoints :: Anthropic do
2024-07-24 16:29:47 -03:00
let ( :url ) { " https://api.anthropic.com/v1/messages " }
2024-07-30 13:44:57 -03:00
fab! ( :model ) { Fabricate ( :anthropic_model , name : " claude-3-opus " , vision_enabled : true ) }
2024-07-24 16:29:47 -03:00
let ( :llm ) { DiscourseAi :: Completions :: Llm . proxy ( " custom: #{ model . id } " ) }
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
let ( :image100x100 ) { plugin_file_from_fixtures ( " 100x100.jpg " ) }
let ( :upload100x100 ) do
UploadCreator . new ( image100x100 , " image.jpg " ) . create_for ( Discourse . system_user . id )
end
2024-03-19 06:48:46 +11:00
let ( :prompt ) do
DiscourseAi :: Completions :: Prompt . new (
" You are hello bot " ,
messages : [ type : :user , id : " user1 " , content : " hello " ] ,
)
end
2024-01-02 11:21:13 -03:00
2024-03-19 06:48:46 +11:00
let ( :echo_tool ) do
2023-11-23 12:58:54 -03:00
{
2024-03-19 06:48:46 +11:00
name : " echo " ,
description : " echo something " ,
parameters : [ { name : " text " , type : " string " , description : " text to echo " , required : true } ] ,
2023-11-23 12:58:54 -03:00
}
end
2024-03-19 06:48:46 +11:00
let ( :google_tool ) do
{
name : " google " ,
description : " google something " ,
parameters : [
{ name : " query " , type : " string " , description : " text to google " , required : true } ,
] ,
}
2023-11-23 12:58:54 -03:00
end
2024-03-19 06:48:46 +11:00
let ( :prompt_with_echo_tool ) do
prompt_with_tools = prompt
prompt . tools = [ echo_tool ]
prompt_with_tools
2023-11-23 12:58:54 -03:00
end
2024-03-19 06:48:46 +11:00
let ( :prompt_with_google_tool ) do
prompt_with_tools = prompt
prompt . tools = [ echo_tool ]
prompt_with_tools
2023-11-23 12:58:54 -03:00
end
2024-01-17 15:08:49 -03:00
2024-03-19 06:48:46 +11:00
it " does not eat spaces with tool calls " do
body = << ~ STRING
2024-06-06 08:34:23 +10:00
event : message_start
data : { " type " :" message_start " , " message " : { " id " :" msg_01Ju4j2MiGQb9KV9EEQ522Y3 " , " type " :" message " , " role " :" assistant " , " model " :" claude-3-haiku-20240307 " , " content " :[] , " stop_reason " :null , " stop_sequence " :null , " usage " : { " input_tokens " : 1293 , " output_tokens " : 1 } } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_start
data : { " type " :" content_block_start " , " index " : 0 , " content_block " : { " type " :" tool_use " , " id " :" toolu_01DjrShFRRHp9SnHYRFRc53F " , " name " :" search " , " input " : { } } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : ping
data : { " type " : " ping " }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" { \\ " searc " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" h_qu " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" er " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" y \\ " : \ \ " s " } }
2024-01-17 15:08:49 -03:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
2024-11-04 10:07:17 +11:00
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" <a>m " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" sam \\ " " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" , \\ " cate " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" gory " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" \\ " : \ \ " gene " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_delta
data : { " type " :" content_block_delta " , " index " : 0 , " delta " : { " type " :" input_json_delta " , " partial_json " :" ral \\ " } " } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : content_block_stop
data : { " type " :" content_block_stop " , " index " : 0 }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : message_delta
data : { " type " :" message_delta " , " delta " : { " stop_reason " :" tool_use " , " stop_sequence " :null } , " usage " : { " output_tokens " : 70 } }
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
event : message_stop
data : { " type " :" message_stop " }
2024-03-19 06:48:46 +11:00
STRING
2024-11-12 08:14:30 +11:00
result = [ ]
2024-04-03 16:06:27 +11:00
body = body . scan ( / .* \ n / )
EndpointMock . with_chunk_array_support do
2024-07-24 16:29:47 -03:00
stub_request ( :post , url ) . to_return ( status : 200 , body : body )
2024-04-03 16:06:27 +11:00
2024-11-14 06:58:24 +11:00
llm . generate (
prompt_with_google_tool ,
user : Discourse . system_user ,
partial_tool_calls : true ,
) { | partial | result << partial . dup }
2024-01-17 15:08:49 -03:00
end
2024-03-19 06:48:46 +11:00
2024-11-12 08:14:30 +11:00
tool_call =
DiscourseAi :: Completions :: ToolCall . new (
name : " search " ,
id : " toolu_01DjrShFRRHp9SnHYRFRc53F " ,
parameters : {
search_query : " s<a>m sam " ,
category : " general " ,
} ,
)
2024-11-14 06:58:24 +11:00
expect ( result . last ) . to eq ( tool_call )
search_queries = result . filter ( & :partial ) . map { | r | r . parameters [ :search_query ] }
categories = result . filter ( & :partial ) . map { | r | r . parameters [ :category ] }
expect ( categories ) . to eq ( [ nil , nil , nil , nil , " gene " , " general " ] )
expect ( search_queries ) . to eq ( [ " s " , " s<a>m " , " s<a>m " , " s<a>m sam " , " s<a>m sam " , " s<a>m sam " ] )
2024-03-19 06:48:46 +11:00
end
it " can stream a response " do
body = ( << ~ STRING ) . strip
event : message_start
data : { " type " : " message_start " , " message " : { " id " : " msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY " , " type " : " message " , " role " : " assistant " , " content " : [ ] , " model " : " claude-3-opus-20240229 " , " stop_reason " : null , " stop_sequence " : null , " usage " : { " input_tokens " : 25 , " output_tokens " : 1 } } }
event : content_block_start
data : { " type " : " content_block_start " , " index " : 0 , " content_block " : { " type " : " text " , " text " : " " } }
event : ping
data : { " type " : " ping " }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " Hello " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " ! " } }
event : content_block_stop
data : { " type " : " content_block_stop " , " index " : 0 }
event : message_delta
data : { " type " : " message_delta " , " delta " : { " stop_reason " : " end_turn " , " stop_sequence " :null , " usage " : { " output_tokens " : 15 } } }
event : message_stop
data : { " type " : " message_stop " }
STRING
parsed_body = nil
2024-07-24 16:29:47 -03:00
stub_request ( :post , url ) . with (
2024-03-19 06:48:46 +11:00
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 )
result = + " "
2024-05-14 13:28:46 +10:00
llm . generate ( prompt , user : Discourse . system_user , feature_name : " testing " ) do | partial , cancel |
result << partial
end
2024-03-19 06:48:46 +11:00
expect ( result ) . to eq ( " Hello! " )
expected_body = {
model : " claude-3-opus-20240229 " ,
2024-11-19 09:22:39 +11:00
max_tokens : 4096 ,
2024-03-19 06:48:46 +11:00
messages : [ { role : " user " , content : " user1: hello " } ] ,
system : " You are hello bot " ,
stream : true ,
}
expect ( parsed_body ) . to eq ( expected_body )
log = AiApiAuditLog . order ( :id ) . last
expect ( log . provider_id ) . to eq ( AiApiAuditLog :: Provider :: Anthropic )
2024-05-14 13:28:46 +10:00
expect ( log . feature_name ) . to eq ( " testing " )
2024-06-06 08:34:23 +10:00
expect ( log . response_tokens ) . to eq ( 15 )
expect ( log . request_tokens ) . to eq ( 25 )
2024-11-12 08:14:30 +11:00
expect ( log . raw_request_payload ) . to eq ( expected_body . to_json )
expect ( log . raw_response_payload . strip ) . to eq ( body . strip )
2024-03-19 06:48:46 +11:00
end
2024-06-06 08:34:23 +10:00
it " supports non streaming tool calls " do
tool = {
name : " calculate " ,
description : " calculate something " ,
parameters : [
{
name : " expression " ,
type : " string " ,
description : " expression to calculate " ,
required : true ,
} ,
] ,
}
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
prompt =
DiscourseAi :: Completions :: Prompt . new (
" You a calculator " ,
messages : [ { type : :user , id : " user1 " , content : " calculate 2758975 + 21.11 " } ] ,
tools : [ tool ] ,
)
2024-03-19 06:48:46 +11:00
2024-06-06 08:34:23 +10:00
body = {
id : " msg_01RdJkxCbsEj9VFyFYAkfy2S " ,
type : " message " ,
role : " assistant " ,
model : " claude-3-haiku-20240307 " ,
content : [
{ type : " text " , text : " Here is the calculation: " } ,
{
type : " tool_use " ,
id : " toolu_012kBdhG4eHaV68W56p4N94h " ,
name : " calculate " ,
input : {
expression : " 2758975 + 21.11 " ,
} ,
} ,
] ,
stop_reason : " tool_use " ,
stop_sequence : nil ,
usage : {
input_tokens : 345 ,
output_tokens : 65 ,
} ,
} . to_json
2024-07-24 16:29:47 -03:00
stub_request ( :post , url ) . to_return ( body : body )
2024-03-19 06:48:46 +11:00
2024-07-30 13:44:57 -03:00
result = llm . generate ( prompt , user : Discourse . system_user )
2024-03-19 06:48:46 +11:00
2024-11-12 08:14:30 +11:00
tool_call =
DiscourseAi :: Completions :: ToolCall . new (
name : " calculate " ,
id : " toolu_012kBdhG4eHaV68W56p4N94h " ,
parameters : {
expression : " 2758975 + 21.11 " ,
} ,
)
expect ( result ) . to eq ( [ " Here is the calculation: " , tool_call ] )
log = AiApiAuditLog . order ( :id ) . last
expect ( log . request_tokens ) . to eq ( 345 )
expect ( log . response_tokens ) . to eq ( 65 )
2024-03-19 06:48:46 +11:00
end
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
it " can send images via a completion prompt " do
prompt =
DiscourseAi :: Completions :: Prompt . new (
" You are image bot " ,
2025-04-01 02:39:07 +11:00
messages : [ type : :user , id : " user1 " , content : [ " hello " , { upload_id : upload100x100 . id } ] ] ,
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
)
encoded = prompt . encoded_uploads ( prompt . messages . last )
request_body = {
model : " claude-3-opus-20240229 " ,
2024-11-19 09:22:39 +11:00
max_tokens : 4096 ,
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
messages : [
{
role : " user " ,
content : [
2025-04-01 02:39:07 +11:00
{ type : " text " , text : " user1: hello " } ,
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
{
type : " image " ,
source : {
type : " base64 " ,
media_type : " image/jpeg " ,
data : encoded [ 0 ] [ :base64 ] ,
} ,
} ,
] ,
} ,
] ,
system : " You are image bot " ,
}
response_body = << ~ STRING
{
" content " : [
{
" text " : " What a cool image " ,
" 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
requested_body = nil
2024-07-24 16:29:47 -03:00
stub_request ( :post , url ) . with (
FEATURE: Add vision support to AI personas (Claude 3) (#546)
This commit adds the ability to enable vision for AI personas, allowing them to understand images that are posted in the conversation.
For personas with vision enabled, any images the user has posted will be resized to be within the configured max_pixels limit, base64 encoded and included in the prompt sent to the AI provider.
The persona editor allows enabling/disabling vision and has a dropdown to select the max supported image size (low, medium, high). Vision is disabled by default.
This initial vision support has been tested and implemented with Anthropic's claude-3 models which accept images in a special format as part of the prompt.
Other integrations will need to be updated to support images.
Several specs were added to test the new functionality at the persona, prompt building and API layers.
- Gemini is omitted, pending API support for Gemini 1.5. Current Gemini bot is not performing well, adding images is unlikely to make it perform any better.
- Open AI is omitted, vision support on GPT-4 it limited in that the API has no tool support when images are enabled so we would need to full back to a different prompting technique, something that would add lots of complexity
---------
Co-authored-by: Martin Brennan <martin@discourse.org>
2024-03-27 14:30:11 +11:00
body :
proc do | req_body |
requested_body = JSON . parse ( req_body , symbolize_names : true )
true
end ,
) . to_return ( status : 200 , body : response_body )
result = llm . generate ( prompt , user : Discourse . system_user )
expect ( result ) . to eq ( " What a cool image " )
expect ( requested_body ) . to eq ( request_body )
end
2025-02-25 17:32:12 +11:00
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
2024-03-19 06:48:46 +11:00
it " can operate in regular mode " 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
2024-07-24 16:29:47 -03:00
stub_request ( :post , url ) . with (
2024-03-19 06:48:46 +11:00
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 )
2024-07-30 13:44:57 -03:00
proxy = DiscourseAi :: Completions :: Llm . proxy ( " custom: #{ model . id } " )
result = proxy . generate ( prompt , user : Discourse . system_user )
2024-03-19 06:48:46 +11:00
expect ( result ) . to eq ( " Hello! " )
expected_body = {
model : " claude-3-opus-20240229 " ,
2024-11-19 09:22:39 +11:00
max_tokens : 4096 ,
2024-03-19 06:48:46 +11:00
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 )
2024-01-17 15:08:49 -03:00
end
2025-03-04 12:22:30 +11:00
it " can send through thinking tokens via a completion prompt " do
body = {
id : " msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY " ,
type : " message " ,
role : " assistant " ,
content : [ { type : " text " , text : " world " } ] ,
model : " claude-3-7-sonnet-20250219 " ,
stop_reason : " end_turn " ,
usage : {
input_tokens : 25 ,
output_tokens : 40 ,
} ,
} . to_json
parsed_body = nil
stub_request ( :post , url ) . with (
body : - > ( req_body ) { parsed_body = JSON . parse ( req_body ) } ,
headers : {
" Content-Type " = > " application/json " ,
" X-Api-Key " = > " 123 " ,
" Anthropic-Version " = > " 2023-06-01 " ,
} ,
) . to_return ( status : 200 , body : body )
prompt = DiscourseAi :: Completions :: Prompt . new ( " system prompt " )
prompt . push ( type : :user , content : " hello " )
prompt . push (
type : :model ,
id : " user1 " ,
content : " hello " ,
thinking : " I am thinking " ,
thinking_signature : " signature " ,
redacted_thinking_signature : " redacted_signature " ,
)
result = llm . generate ( prompt , user : Discourse . system_user )
expect ( result ) . to eq ( " world " )
expected_body = {
" model " = > " claude-3-opus-20240229 " ,
" max_tokens " = > 4096 ,
" messages " = > [
{ " role " = > " user " , " content " = > " hello " } ,
{
" role " = > " assistant " ,
" content " = > [
{ " type " = > " thinking " , " thinking " = > " I am thinking " , " signature " = > " signature " } ,
{ " type " = > " redacted_thinking " , " data " = > " redacted_signature " } ,
{ " type " = > " text " , " text " = > " hello " } ,
] ,
} ,
] ,
" system " = > " system prompt " ,
}
expect ( parsed_body ) . to eq ( expected_body )
end
it " can handle a response with thinking blocks in non-streaming mode " do
body = {
id : " msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY " ,
type : " message " ,
role : " assistant " ,
content : [
{
type : " thinking " ,
thinking : " This is my thinking process about prime numbers... " ,
signature : " abc123signature " ,
} ,
{ type : " redacted_thinking " , data : " abd456signature " } ,
{ type : " text " , text : " Yes, there are infinitely many prime numbers where n mod 4 = 3. " } ,
] ,
model : " claude-3-7-sonnet-20250219 " ,
stop_reason : " end_turn " ,
usage : {
input_tokens : 25 ,
output_tokens : 40 ,
} ,
} . to_json
stub_request ( :post , url ) . with (
headers : {
" Content-Type " = > " application/json " ,
" X-Api-Key " = > " 123 " ,
" Anthropic-Version " = > " 2023-06-01 " ,
} ,
) . to_return ( status : 200 , body : body )
result =
llm . generate (
" hello " ,
user : Discourse . system_user ,
feature_name : " testing " ,
output_thinking : true ,
)
# Result should be an array with both thinking and text content
expect ( result ) . to be_an ( Array )
expect ( result . length ) . to eq ( 3 )
# First item should be a Thinking object
expect ( result [ 0 ] ) . to be_a ( DiscourseAi :: Completions :: Thinking )
expect ( result [ 0 ] . message ) . to eq ( " This is my thinking process about prime numbers... " )
expect ( result [ 0 ] . signature ) . to eq ( " abc123signature " )
expect ( result [ 1 ] ) . to be_a ( DiscourseAi :: Completions :: Thinking )
expect ( result [ 1 ] . signature ) . to eq ( " abd456signature " )
expect ( result [ 1 ] . redacted ) . to eq ( true )
# Second item should be the text response
expect ( result [ 2 ] ) . to eq ( " Yes, there are infinitely many prime numbers where n mod 4 = 3. " )
# Verify audit log
log = AiApiAuditLog . order ( :id ) . last
expect ( log . provider_id ) . to eq ( AiApiAuditLog :: Provider :: Anthropic )
expect ( log . feature_name ) . to eq ( " testing " )
expect ( log . response_tokens ) . to eq ( 40 )
end
it " can stream a response with thinking blocks " do
body = ( << ~ STRING ) . strip
event : message_start
data : { " type " : " message_start " , " message " : { " id " : " msg_01... " , " type " : " message " , " role " : " assistant " , " content " : [ ] , " model " : " claude-3-opus-20240229 " , " stop_reason " : null , " stop_sequence " : null , " usage " : { " input_tokens " : 25 } } }
event : content_block_start
data : { " type " : " content_block_start " , " index " : 0 , " content_block " : { " type " : " thinking " , " thinking " : " " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " thinking_delta " , " thinking " : " Let me solve this step by step: \\ n \\ n1. First break down 27 * 453 " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " thinking_delta " , " thinking " : " \\ n2. 453 = 400 + 50 + 3 " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " signature_delta " , " signature " : " EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds... " } }
event : content_block_stop
data : { " type " : " content_block_stop " , " index " : 0 }
event : content_block_start
2025-05-06 10:09:39 -03:00
data : { " type " :" content_block_start " , " index " : 0 , " content_block " : { " type " :" redacted_thinking " , " data " :" AAA== " } }
2025-03-04 12:22:30 +11:00
event : ping
data : { " type " : " ping " }
event : content_block_stop
data : { " type " :" content_block_stop " , " index " : 0 }
event : content_block_start
data : { " type " : " content_block_start " , " index " : 1 , " content_block " : { " type " : " text " , " text " : " " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 1 , " delta " : { " type " : " text_delta " , " text " : " 27 * 453 = 12,231 " } }
event : content_block_stop
data : { " type " : " content_block_stop " , " index " : 1 }
event : message_delta
data : { " type " : " message_delta " , " delta " : { " stop_reason " : " end_turn " , " stop_sequence " : null , " usage " : { " output_tokens " : 30 } } }
event : message_stop
data : { " type " : " message_stop " }
STRING
parsed_body = nil
stub_request ( :post , url ) . with (
headers : {
" Content-Type " = > " application/json " ,
" X-Api-Key " = > " 123 " ,
" Anthropic-Version " = > " 2023-06-01 " ,
} ,
) . to_return ( status : 200 , body : body )
thinking_chunks = [ ]
text_chunks = [ ]
llm . generate (
" hello there " ,
user : Discourse . system_user ,
feature_name : " testing " ,
output_thinking : true ,
) do | partial , cancel |
if partial . is_a? ( DiscourseAi :: Completions :: Thinking )
thinking_chunks << partial
else
text_chunks << partial
end
end
expected_thinking = [
DiscourseAi :: Completions :: Thinking . new ( message : " " , signature : " " , partial : true ) ,
DiscourseAi :: Completions :: Thinking . new (
message : " Let me solve this step by step: \n \n 1. First break down 27 * 453 " ,
partial : true ,
) ,
DiscourseAi :: Completions :: Thinking . new ( message : " \n 2. 453 = 400 + 50 + 3 " , partial : true ) ,
DiscourseAi :: Completions :: Thinking . new (
message :
" Let me solve this step by step: \n \n 1. First break down 27 * 453 \n 2. 453 = 400 + 50 + 3 " ,
signature : " EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds... " ,
partial : false ,
) ,
DiscourseAi :: Completions :: Thinking . new ( message : nil , signature : " AAA== " , redacted : true ) ,
]
expect ( thinking_chunks ) . to eq ( expected_thinking )
expect ( text_chunks ) . to eq ( [ " 27 * 453 = 12,231 " ] )
log = AiApiAuditLog . order ( :id ) . last
expect ( log . provider_id ) . to eq ( AiApiAuditLog :: Provider :: Anthropic )
expect ( log . feature_name ) . to eq ( " testing " )
expect ( log . response_tokens ) . to eq ( 30 )
end
2025-03-11 16:54:02 +11:00
describe " parameter disabling " do
it " excludes disabled parameters from the request " do
model . update! ( provider_params : { disable_top_p : true , disable_temperature : true } )
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 : {
id : " msg_123 " ,
type : " message " ,
role : " assistant " ,
content : [ { type : " text " , text : " test response " } ] ,
model : " claude-3-opus-20240229 " ,
usage : {
input_tokens : 10 ,
output_tokens : 5 ,
} ,
} . to_json ,
)
# Request with parameters that should be ignored
llm . generate (
prompt ,
user : Discourse . system_user ,
top_p : 0 . 9 ,
temperature : 0 . 8 ,
max_tokens : 500 ,
)
# Verify disabled parameters aren't included
expect ( parsed_body ) . not_to have_key ( :top_p )
expect ( parsed_body ) . not_to have_key ( :temperature )
# Verify other parameters still work
expect ( parsed_body ) . to have_key ( :max_tokens )
expect ( parsed_body [ :max_tokens ] ) . to eq ( 500 )
end
end
2025-03-25 08:06:43 +11:00
describe " disabled tool use " do
it " can properly disable tool use with :none " do
prompt =
DiscourseAi :: Completions :: Prompt . new (
" You are a bot " ,
messages : [ type : :user , id : " user1 " , content : " don't use any tools please " ] ,
tools : [ echo_tool ] ,
tool_choice : :none ,
)
response_body = {
id : " msg_01RdJkxCbsEj9VFyFYAkfy2S " ,
type : " message " ,
role : " assistant " ,
model : " claude-3-haiku-20240307 " ,
content : [
{ type : " text " , text : " I won't use any tools. Here's a direct response instead. " } ,
] ,
stop_reason : " end_turn " ,
stop_sequence : nil ,
usage : {
input_tokens : 345 ,
output_tokens : 65 ,
} ,
} . to_json
parsed_body = nil
stub_request ( :post , url ) . with (
body :
proc do | req_body |
parsed_body = JSON . parse ( req_body , symbolize_names : true )
true
end ,
) . to_return ( status : 200 , body : response_body )
result = llm . generate ( prompt , user : Discourse . system_user )
# Verify that tool_choice is set to { type: "none" }
expect ( parsed_body [ :tool_choice ] ) . to eq ( { type : " none " } )
# Verify that an assistant message with no_more_tool_calls_text was added
messages = parsed_body [ :messages ]
expect ( messages . length ) . to eq ( 2 ) # user message + added assistant message
last_message = messages . last
expect ( last_message [ :role ] ) . to eq ( " assistant " )
expect ( last_message [ :content ] ) . to eq (
DiscourseAi :: Completions :: Dialects :: Dialect . no_more_tool_calls_text ,
)
expect ( result ) . to eq ( " I won't use any tools. Here's a direct response instead. " )
end
end
2025-05-06 10:09:39 -03:00
2025-05-23 10:36:52 -03:00
describe " forced tool use " do
it " can properly force tool use " do
prompt =
DiscourseAi :: Completions :: Prompt . new (
" You are a bot " ,
messages : [ type : :user , id : " user1 " , content : " echo hello " ] ,
tools : [ echo_tool ] ,
tool_choice : " echo " ,
)
response_body = {
id : " msg_01RdJkxCbsEj9VFyFYAkfy2S " ,
type : " message " ,
role : " assistant " ,
model : " claude-3-haiku-20240307 " ,
content : [
{
type : " tool_use " ,
id : " toolu_bdrk_014CMjxtGmKUtGoEFPgc7PF7 " ,
name : " echo " ,
input : {
text : " hello " ,
} ,
} ,
] ,
stop_reason : " end_turn " ,
stop_sequence : nil ,
usage : {
input_tokens : 345 ,
output_tokens : 65 ,
} ,
} . to_json
parsed_body = nil
stub_request ( :post , url ) . with (
body :
proc do | req_body |
parsed_body = JSON . parse ( req_body , symbolize_names : true )
true
end ,
) . to_return ( status : 200 , body : response_body )
llm . generate ( prompt , user : Discourse . system_user )
# Verify that tool_choice: "echo" is present
expect ( parsed_body . dig ( :tool_choice , :name ) ) . to eq ( " echo " )
end
end
2025-05-06 10:09:39 -03:00
describe " structured output via prefilling " do
it " forces the response to be a JSON and using the given JSON schema " do
schema = {
type : " json_schema " ,
json_schema : {
name : " reply " ,
schema : {
type : " object " ,
properties : {
key : {
type : " string " ,
} ,
} ,
required : [ " key " ] ,
additionalProperties : false ,
} ,
strict : true ,
} ,
}
body = ( << ~ STRING ) . strip
event : message_start
data : { " type " : " message_start " , " message " : { " id " : " msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY " , " type " : " message " , " role " : " assistant " , " content " : [ ] , " model " : " claude-3-opus-20240229 " , " stop_reason " : null , " stop_sequence " : null , " usage " : { " input_tokens " : 25 , " output_tokens " : 1 } } }
event : content_block_start
data : { " type " : " content_block_start " , " index " : 0 , " content_block " : { " type " : " text " , " text " : " " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " \\ " " }}
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " key " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " \\ " : \ \ " " } }
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " Hello! " } }
2025-05-21 11:25:59 -03:00
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " \\ n there " } }
2025-05-06 10:09:39 -03:00
event : content_block_delta
data : { " type " : " content_block_delta " , " index " : 0 , " delta " : { " type " : " text_delta " , " text " : " \\ " } " }}
event : content_block_stop
data : { " type " : " content_block_stop " , " index " : 0 }
event : message_delta
data : { " type " : " message_delta " , " delta " : { " stop_reason " : " end_turn " , " stop_sequence " :null , " usage " : { " output_tokens " : 15 } } }
event : message_stop
data : { " type " : " message_stop " }
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 )
structured_output = nil
llm . generate (
prompt ,
user : Discourse . system_user ,
feature_name : " testing " ,
response_format : schema ,
) { | partial , cancel | structured_output = partial }
2025-05-21 11:25:59 -03:00
expect ( structured_output . read_buffered_property ( :key ) ) . to eq ( " Hello! \n there " )
2025-05-06 10:09:39 -03:00
expected_body = {
model : " claude-3-opus-20240229 " ,
max_tokens : 4096 ,
messages : [ { role : " user " , content : " user1: hello " } , { role : " assistant " , content : " { " } ] ,
system : " You are hello bot " ,
stream : true ,
}
expect ( parsed_body ) . to eq ( expected_body )
end
end
2023-11-23 12:58:54 -03:00
end