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

Commit ae3117d

Browse files
committed
tests
1 parent b67568b commit ae3117d

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed

spec/lib/completions/endpoints/anthropic_spec.rb

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,219 @@
449449
expect(log.request_tokens).to eq(10)
450450
expect(log.response_tokens).to eq(25)
451451
end
452+
453+
it "can send through thinking tokens via a completion prompt" do
454+
body = {
455+
id: "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY",
456+
type: "message",
457+
role: "assistant",
458+
content: [{ type: "text", text: "world" }],
459+
model: "claude-3-7-sonnet-20250219",
460+
stop_reason: "end_turn",
461+
usage: {
462+
input_tokens: 25,
463+
output_tokens: 40,
464+
},
465+
}.to_json
466+
467+
parsed_body = nil
468+
stub_request(:post, url).with(
469+
body: ->(req_body) { parsed_body = JSON.parse(req_body) },
470+
headers: {
471+
"Content-Type" => "application/json",
472+
"X-Api-Key" => "123",
473+
"Anthropic-Version" => "2023-06-01",
474+
},
475+
).to_return(status: 200, body: body)
476+
477+
prompt = DiscourseAi::Completions::Prompt.new("system prompt")
478+
prompt.push(type: :user, content: "hello")
479+
prompt.push(
480+
type: :model,
481+
id: "user1",
482+
content: "hello",
483+
thinking: "I am thinking",
484+
thinking_signature: "signature",
485+
redacted_thinking_signature: "redacted_signature",
486+
)
487+
488+
result = llm.generate(prompt, user: Discourse.system_user)
489+
expect(result).to eq("world")
490+
491+
expected_body = {
492+
"model" => "claude-3-opus-20240229",
493+
"max_tokens" => 4096,
494+
"messages" => [
495+
{ "role" => "user", "content" => "hello" },
496+
{
497+
"role" => "assistant",
498+
"content" => [
499+
{ "type" => "thinking", "thinking" => "I am thinking", "signature" => "signature" },
500+
{ "type" => "redacted_thinking", "data" => "redacted_signature" },
501+
{ "type" => "text", "text" => "hello" },
502+
],
503+
},
504+
],
505+
"system" => "system prompt",
506+
}
507+
508+
expect(parsed_body).to eq(expected_body)
509+
end
510+
511+
it "can handle a response with thinking blocks in non-streaming mode" do
512+
body = {
513+
id: "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY",
514+
type: "message",
515+
role: "assistant",
516+
content: [
517+
{
518+
type: "thinking",
519+
thinking: "This is my thinking process about prime numbers...",
520+
signature: "abc123signature",
521+
},
522+
{ type: "redacted_thinking", data: "abd456signature" },
523+
{ type: "text", text: "Yes, there are infinitely many prime numbers where n mod 4 = 3." },
524+
],
525+
model: "claude-3-7-sonnet-20250219",
526+
stop_reason: "end_turn",
527+
usage: {
528+
input_tokens: 25,
529+
output_tokens: 40,
530+
},
531+
}.to_json
532+
533+
stub_request(:post, url).with(
534+
headers: {
535+
"Content-Type" => "application/json",
536+
"X-Api-Key" => "123",
537+
"Anthropic-Version" => "2023-06-01",
538+
},
539+
).to_return(status: 200, body: body)
540+
541+
result =
542+
llm.generate(
543+
"hello",
544+
user: Discourse.system_user,
545+
feature_name: "testing",
546+
output_thinking: true,
547+
)
548+
549+
# Result should be an array with both thinking and text content
550+
expect(result).to be_an(Array)
551+
expect(result.length).to eq(3)
552+
553+
# First item should be a Thinking object
554+
expect(result[0]).to be_a(DiscourseAi::Completions::Thinking)
555+
expect(result[0].message).to eq("This is my thinking process about prime numbers...")
556+
expect(result[0].signature).to eq("abc123signature")
557+
558+
expect(result[1]).to be_a(DiscourseAi::Completions::Thinking)
559+
expect(result[1].signature).to eq("abd456signature")
560+
expect(result[1].redacted).to eq(true)
561+
562+
# Second item should be the text response
563+
expect(result[2]).to eq("Yes, there are infinitely many prime numbers where n mod 4 = 3.")
564+
565+
# Verify audit log
566+
log = AiApiAuditLog.order(:id).last
567+
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
568+
expect(log.feature_name).to eq("testing")
569+
expect(log.response_tokens).to eq(40)
570+
end
571+
572+
it "can stream a response with thinking blocks" do
573+
body = (<<~STRING).strip
574+
event: message_start
575+
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}}}
576+
577+
event: content_block_start
578+
data: {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}
579+
580+
event: content_block_delta
581+
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"}}
582+
583+
event: content_block_delta
584+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\\n2. 453 = 400 + 50 + 3"}}
585+
586+
event: content_block_delta
587+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}}
588+
589+
event: content_block_stop
590+
data: {"type": "content_block_stop", "index": 0}
591+
592+
event: content_block_start
593+
data: {"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":"AAA=="} }
594+
595+
event: ping
596+
data: {"type": "ping"}
597+
598+
event: content_block_stop
599+
data: {"type":"content_block_stop","index":0 }
600+
601+
event: content_block_start
602+
data: {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}
603+
604+
event: content_block_delta
605+
data: {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "27 * 453 = 12,231"}}
606+
607+
event: content_block_stop
608+
data: {"type": "content_block_stop", "index": 1}
609+
610+
event: message_delta
611+
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null, "usage": {"output_tokens": 30}}}
612+
613+
event: message_stop
614+
data: {"type": "message_stop"}
615+
STRING
616+
617+
parsed_body = nil
618+
619+
stub_request(:post, url).with(
620+
headers: {
621+
"Content-Type" => "application/json",
622+
"X-Api-Key" => "123",
623+
"Anthropic-Version" => "2023-06-01",
624+
},
625+
).to_return(status: 200, body: body)
626+
627+
thinking_chunks = []
628+
text_chunks = []
629+
630+
llm.generate(
631+
"hello there",
632+
user: Discourse.system_user,
633+
feature_name: "testing",
634+
output_thinking: true,
635+
) do |partial, cancel|
636+
if partial.is_a?(DiscourseAi::Completions::Thinking)
637+
thinking_chunks << partial
638+
else
639+
text_chunks << partial
640+
end
641+
end
642+
643+
expected_thinking = [
644+
DiscourseAi::Completions::Thinking.new(message: "", signature: "", partial: true),
645+
DiscourseAi::Completions::Thinking.new(
646+
message: "Let me solve this step by step:\n\n1. First break down 27 * 453",
647+
partial: true,
648+
),
649+
DiscourseAi::Completions::Thinking.new(message: "\n2. 453 = 400 + 50 + 3", partial: true),
650+
DiscourseAi::Completions::Thinking.new(
651+
message:
652+
"Let me solve this step by step:\n\n1. First break down 27 * 453\n2. 453 = 400 + 50 + 3",
653+
signature: "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds...",
654+
partial: false,
655+
),
656+
DiscourseAi::Completions::Thinking.new(message: nil, signature: "AAA==", redacted: true),
657+
]
658+
659+
expect(thinking_chunks).to eq(expected_thinking)
660+
expect(text_chunks).to eq(["27 * 453 = 12,231"])
661+
662+
log = AiApiAuditLog.order(:id).last
663+
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
664+
expect(log.feature_name).to eq("testing")
665+
expect(log.response_tokens).to eq(30)
666+
end
452667
end

0 commit comments

Comments
 (0)