Skip to content

Commit 76a4e7f

Browse files
authored
Merge pull request #80 from dr-frmr/main
feat: add support for message-list system prompts to anthropic provider
2 parents ac1fb01 + 184ef04 commit 76a4e7f

File tree

3 files changed

+161
-41
lines changed

3 files changed

+161
-41
lines changed

src/backends/anthropic.rs

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::collections::HashMap;
66
use std::sync::Arc;
77

88
use crate::{
9-
builder::LLMBackend,
9+
builder::{LLMBackend, SystemContent, SystemPrompt},
1010
chat::{
1111
ChatMessage, ChatProvider, ChatResponse, ChatRole, MessageType, StreamChunk, Tool,
1212
ToolChoice, Usage,
@@ -44,7 +44,7 @@ pub struct AnthropicConfig {
4444
/// Request timeout in seconds.
4545
pub timeout_seconds: u64,
4646
/// System prompt to guide model behavior.
47-
pub system: String,
47+
pub system: SystemPrompt,
4848
/// Top-p (nucleus) sampling parameter.
4949
pub top_p: Option<f32>,
5050
/// Top-k sampling parameter.
@@ -91,6 +91,14 @@ struct ThinkingConfig {
9191
budget_tokens: u32,
9292
}
9393

94+
/// System prompt in the request - can be either a string or vector of content objects
95+
#[derive(Serialize, Debug)]
96+
#[serde(untagged)]
97+
enum RequestSystemPrompt<'a> {
98+
String(&'a str),
99+
Messages(&'a [SystemContent]),
100+
}
101+
94102
/// Request payload for Anthropic's messages API endpoint.
95103
#[derive(Serialize, Debug)]
96104
struct AnthropicCompleteRequest<'a> {
@@ -101,7 +109,7 @@ struct AnthropicCompleteRequest<'a> {
101109
#[serde(skip_serializing_if = "Option::is_none")]
102110
temperature: Option<f32>,
103111
#[serde(skip_serializing_if = "Option::is_none")]
104-
system: Option<&'a str>,
112+
system: Option<RequestSystemPrompt<'a>>,
105113
#[serde(skip_serializing_if = "Option::is_none")]
106114
stream: Option<bool>,
107115
#[serde(skip_serializing_if = "Option::is_none")]
@@ -487,6 +495,14 @@ impl Anthropic {
487495
(anthropic_tools, final_tool_choice)
488496
}
489497

498+
/// Converts a SystemPrompt to the request format
499+
fn system_to_request(system: &SystemPrompt) -> RequestSystemPrompt<'_> {
500+
match system {
501+
SystemPrompt::String(s) => RequestSystemPrompt::String(s),
502+
SystemPrompt::Messages(msgs) => RequestSystemPrompt::Messages(msgs),
503+
}
504+
}
505+
490506
/// Creates a new Anthropic client with the specified configuration.
491507
///
492508
/// # Arguments
@@ -505,7 +521,7 @@ impl Anthropic {
505521
max_tokens: Option<u32>,
506522
temperature: Option<f32>,
507523
timeout_seconds: Option<u64>,
508-
system: Option<String>,
524+
system: Option<SystemPrompt>,
509525
top_p: Option<f32>,
510526
top_k: Option<u32>,
511527
tools: Option<Vec<Tool>>,
@@ -551,36 +567,6 @@ impl Anthropic {
551567
/// * `timeout_seconds` - Request timeout in seconds (defaults to 30)
552568
/// * `system` - System prompt (defaults to "You are a helpful assistant.")
553569
/// * `thinking_budget_tokens` - Budget tokens for thinking (optional)
554-
///
555-
/// # Examples
556-
///
557-
/// ```rust
558-
/// use reqwest::Client;
559-
/// use std::time::Duration;
560-
///
561-
/// // Create a shared client with custom settings
562-
/// let shared_client = Client::builder()
563-
/// .timeout(Duration::from_secs(120))
564-
/// .build()
565-
/// .unwrap();
566-
///
567-
/// // Use the shared client for multiple Anthropic instances
568-
/// let anthropic = llm::backends::anthropic::Anthropic::with_client(
569-
/// shared_client.clone(),
570-
/// "your-api-key",
571-
/// Some("claude-3-opus-20240229".to_string()),
572-
/// Some(1000),
573-
/// Some(0.7),
574-
/// Some(120),
575-
/// Some("You are a helpful assistant.".to_string()),
576-
/// None,
577-
/// None,
578-
/// None,
579-
/// None,
580-
/// None,
581-
/// None,
582-
/// );
583-
/// ```
584570
#[allow(clippy::too_many_arguments)]
585571
pub fn with_client(
586572
client: Client,
@@ -589,7 +575,7 @@ impl Anthropic {
589575
max_tokens: Option<u32>,
590576
temperature: Option<f32>,
591577
timeout_seconds: Option<u64>,
592-
system: Option<String>,
578+
system: Option<SystemPrompt>,
593579
top_p: Option<f32>,
594580
top_k: Option<u32>,
595581
tools: Option<Vec<Tool>>,
@@ -603,7 +589,9 @@ impl Anthropic {
603589
model: model.unwrap_or_else(|| "claude-3-sonnet-20240229".to_string()),
604590
max_tokens: max_tokens.unwrap_or(300),
605591
temperature: temperature.unwrap_or(0.7),
606-
system: system.unwrap_or_else(|| "You are a helpful assistant.".to_string()),
592+
system: system.unwrap_or_else(|| {
593+
SystemPrompt::String("You are a helpful assistant.".to_string())
594+
}),
607595
timeout_seconds: timeout_seconds.unwrap_or(30),
608596
top_p,
609597
top_k,
@@ -636,7 +624,7 @@ impl Anthropic {
636624
self.config.timeout_seconds
637625
}
638626

639-
pub fn system(&self) -> &str {
627+
pub fn system(&self) -> &SystemPrompt {
640628
&self.config.system
641629
}
642630

@@ -706,12 +694,14 @@ impl ChatProvider for Anthropic {
706694
None
707695
};
708696

697+
let system_prompt = Self::system_to_request(&self.config.system);
698+
709699
let req_body = AnthropicCompleteRequest {
710700
messages: anthropic_messages,
711701
model: &self.config.model,
712702
max_tokens: Some(self.config.max_tokens),
713703
temperature: Some(self.config.temperature),
714-
system: Some(&self.config.system),
704+
system: Some(system_prompt),
715705
stream: Some(false),
716706
top_p: self.config.top_p,
717707
top_k: self.config.top_k,
@@ -833,12 +823,14 @@ impl ChatProvider for Anthropic {
833823
})
834824
.collect();
835825

826+
let system_prompt = Self::system_to_request(&self.config.system);
827+
836828
let req_body = AnthropicCompleteRequest {
837829
messages: anthropic_messages,
838830
model: &self.config.model,
839831
max_tokens: Some(self.config.max_tokens),
840832
temperature: Some(self.config.temperature),
841-
system: Some(&self.config.system),
833+
system: Some(system_prompt),
842834
stream: Some(true),
843835
top_p: self.config.top_p,
844836
top_k: self.config.top_k,
@@ -901,12 +893,14 @@ impl ChatProvider for Anthropic {
901893
&self.config.tool_choice,
902894
);
903895

896+
let system_prompt = Self::system_to_request(&self.config.system);
897+
904898
let req_body = AnthropicCompleteRequest {
905899
messages: anthropic_messages,
906900
model: &self.config.model,
907901
max_tokens: Some(self.config.max_tokens),
908902
temperature: Some(self.config.temperature),
909-
system: Some(&self.config.system),
903+
system: Some(system_prompt),
910904
stream: Some(true),
911905
top_p: self.config.top_p,
912906
top_k: self.config.top_k,

src/builder.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,128 @@ mod embedding;
3434
#[path = "builder/voice.rs"]
3535
mod voice;
3636

37+
use serde::Serialize;
38+
use serde_json::Value;
39+
40+
/// Content object for structured system prompts with optional cache control.
41+
///
42+
/// This allows fine-grained control over system prompt components, particularly
43+
/// useful for providers like Anthropic that support prompt caching.
44+
#[derive(Debug, Clone, Serialize)]
45+
pub struct SystemContent {
46+
pub text: String,
47+
#[serde(rename = "type")]
48+
pub content_type: String,
49+
#[serde(skip_serializing_if = "Option::is_none")]
50+
pub cache_control: Option<Value>,
51+
}
52+
53+
impl SystemContent {
54+
/// Creates a new text content segment for system prompts.
55+
pub fn text(text: String) -> Self {
56+
Self {
57+
text,
58+
content_type: "text".to_string(),
59+
cache_control: None,
60+
}
61+
}
62+
63+
/// Creates a new text content segment with cache control.
64+
pub fn text_with_cache(text: String, cache_control: Value) -> Self {
65+
Self {
66+
text,
67+
content_type: "text".to_string(),
68+
cache_control: Some(cache_control),
69+
}
70+
}
71+
}
72+
73+
/// System prompt configuration supporting both simple strings and structured message formats.
74+
///
75+
/// This enum allows system prompts to be specified either as a simple string or a vector
76+
/// of content objects with fine-grained control, particularly useful for caching parts
77+
/// of the system prompt with certain providers.
78+
#[derive(Debug, Clone)]
79+
pub enum SystemPrompt {
80+
String(String),
81+
Messages(Vec<SystemContent>),
82+
}
83+
84+
impl SystemPrompt {
85+
/// Converts the system prompt to a string representation.
86+
///
87+
/// For `SystemPrompt::String`, returns the string directly.
88+
/// For `SystemPrompt::Messages`, concatenates all message text with newlines.
89+
pub fn to_string_representation(self) -> String {
90+
match self {
91+
SystemPrompt::String(s) => s,
92+
SystemPrompt::Messages(messages) => messages
93+
.into_iter()
94+
.map(|m| m.text)
95+
.collect::<Vec<_>>()
96+
.join("\n"),
97+
}
98+
}
99+
}
100+
101+
impl From<SystemPrompt> for String {
102+
fn from(prompt: SystemPrompt) -> String {
103+
prompt.to_string_representation()
104+
}
105+
}
106+
107+
pub trait IntoSystemMessage {
108+
fn into_system_message(self) -> SystemPrompt;
109+
}
110+
111+
impl IntoSystemMessage for String {
112+
fn into_system_message(self) -> SystemPrompt {
113+
SystemPrompt::String(self)
114+
}
115+
}
116+
117+
impl IntoSystemMessage for &str {
118+
fn into_system_message(self) -> SystemPrompt {
119+
SystemPrompt::String(self.to_string())
120+
}
121+
}
122+
123+
impl IntoSystemMessage for Vec<String> {
124+
fn into_system_message(self) -> SystemPrompt {
125+
if self.len() == 1 {
126+
SystemPrompt::String(self.into_iter().next().unwrap())
127+
} else {
128+
SystemPrompt::Messages(self.into_iter().map(SystemContent::text).collect())
129+
}
130+
}
131+
}
132+
133+
impl IntoSystemMessage for Vec<&str> {
134+
fn into_system_message(self) -> SystemPrompt {
135+
if self.len() == 1 {
136+
SystemPrompt::String(self[0].to_string())
137+
} else {
138+
SystemPrompt::Messages(
139+
self.into_iter()
140+
.map(|s| SystemContent::text(s.to_string()))
141+
.collect(),
142+
)
143+
}
144+
}
145+
}
146+
147+
impl IntoSystemMessage for Vec<SystemContent> {
148+
fn into_system_message(self) -> SystemPrompt {
149+
SystemPrompt::Messages(self)
150+
}
151+
}
152+
153+
impl IntoSystemMessage for SystemPrompt {
154+
fn into_system_message(self) -> SystemPrompt {
155+
self
156+
}
157+
}
158+
37159
pub use backend::LLMBackend;
38160
pub use llm_builder::LLMBuilder;
39161
pub use tools::{FunctionBuilder, ParamBuilder};

src/builder/build/backends/anthropic.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
builder::SystemPrompt,
23
chat::{Tool, ToolChoice},
34
error::LLMError,
45
LLMProvider,
@@ -16,13 +17,16 @@ pub(super) fn build_anthropic(
1617
let api_key = helpers::require_api_key(state, "Anthropic")?;
1718
let timeout = helpers::timeout_or_default(state);
1819

20+
// Convert String to SystemPrompt for Anthropic (supports structured prompts)
21+
let system_prompt = state.system.take().map(SystemPrompt::String);
22+
1923
let provider = crate::backends::anthropic::Anthropic::new(
2024
api_key,
2125
state.model.take(),
2226
state.max_tokens,
2327
state.temperature,
2428
timeout,
25-
state.system.take(),
29+
system_prompt,
2630
state.top_p,
2731
state.top_k,
2832
tools,

0 commit comments

Comments
 (0)