Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ tracing = "0.1.44"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
url = { version = "2.5.8", features = ["serde"] }
xxhash-rust = { version = "0.8.15", features = ["xxh64"] }
backon = "1.5.2"
eserde = "0.1.7"
uuid = { version = "1.23.0", features = [
Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ convert_case.workspace = true
backon.workspace = true
sha2.workspace = true
dashmap.workspace = true
xxhash-rust.workspace = true

url.workspace = true
reqwest.workspace = true
Expand Down
46 changes: 27 additions & 19 deletions crates/forge_app/src/dto/anthropic/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@ use derive_setters::Setters;
use forge_domain::{ContextMessage, Image};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Default, Setters)]
#[derive(Serialize, Deserialize, Default, Setters)]
#[setters(into, strip_option)]
pub struct Request {
pub max_tokens: u64,
pub messages: Vec<Message>,
// NOTE: Field declaration order controls JSON serialization order.
// `system` MUST appear before `messages` so that CCH signing (which
// computes a hash over the serialized body) produces the same byte
// sequence as the final outbound request body. Do NOT reorder these two
// fields.
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<Vec<SystemMessage>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub max_tokens: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<Vec<SystemMessage>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tools: Vec<ToolDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u64>,
Expand All @@ -35,9 +39,13 @@ pub struct Request {
pub output_format: Option<OutputFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anthropic_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub research_preview_2026_02: Option<String>,
// messages MUST come after system — see note above.
pub messages: Vec<Message>,
}

#[derive(Serialize, Default)]
#[derive(Serialize, Deserialize, Default)]
pub struct SystemMessage {
pub r#type: String,
pub text: String,
Expand All @@ -60,7 +68,7 @@ impl SystemMessage {
}
}

#[derive(Serialize, Default, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
pub struct Thinking {
pub r#type: ThinkingType,
pub budget_tokens: u64,
Expand All @@ -70,7 +78,7 @@ pub struct Thinking {
///
/// Only the variants officially supported by Anthropic's `output_config.effort`
/// field. Mutually exclusive with the `thinking` object.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputEffort {
Low,
Expand All @@ -81,19 +89,19 @@ pub enum OutputEffort {

/// Output configuration for newer Anthropic models that support effort-based
/// reasoning (e.g. `claude-opus-4-6`). Mutually exclusive with `thinking`.
#[derive(Serialize, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct OutputConfig {
pub effort: OutputEffort,
}

#[derive(Serialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputFormat {
#[serde(rename = "json_schema")]
JsonSchema { schema: schemars::Schema },
}

#[derive(Serialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingType {
#[default]
Expand Down Expand Up @@ -218,13 +226,13 @@ impl Request {
}
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct Message {
pub content: Vec<Content>,
pub role: Role,
Expand Down Expand Up @@ -341,7 +349,7 @@ impl From<Image> for Content {
}
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
pub type_: String,
Expand All @@ -353,7 +361,7 @@ pub struct ImageSource {
pub url: Option<String>,
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Content {
Image {
Expand Down Expand Up @@ -463,7 +471,7 @@ impl TryFrom<forge_domain::ToolResult> for Content {
}
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CacheControl {
Ephemeral,
Expand All @@ -476,7 +484,7 @@ pub enum Role {
Assistant,
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ToolChoice {
Auto {
Expand Down Expand Up @@ -510,7 +518,7 @@ impl From<forge_domain::ToolChoice> for ToolChoice {
}
}

#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
use std::borrow::Cow;

use forge_domain::Transformer;

use crate::dto::anthropic::{Request, SystemMessage};

/// Adds authentication system message when OAuth is enabled.
#[derive(Clone)]
pub struct AuthSystemMessage {
message: String,
message: Cow<'static, str>,
}

impl AuthSystemMessage {
const AUTH_MESSAGE: &'static str = include_str!("claude_code.md");

/// Creates a new AuthSystemMessage with the provided message.
pub fn new(message: String) -> Self {
Self { message }
/// Creates a new `AuthSystemMessage` with the provided message.
pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
Self { message: message.into() }
}
}

impl Default for AuthSystemMessage {
fn default() -> Self {
Self::new(Self::AUTH_MESSAGE.to_string())
Self::new(Self::AUTH_MESSAGE.trim())
}
}

Expand All @@ -29,7 +32,7 @@ impl Transformer for AuthSystemMessage {
fn transform(&mut self, mut request: Self::Value) -> Self::Value {
let auth_system_message = SystemMessage {
r#type: "text".to_string(),
text: self.message.trim().to_string(),
text: self.message.trim().to_owned(),
cache_control: None,
};

Expand Down
Loading
Loading