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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ object Main:
response.body match {
case Right(messageResponse) =>
messageResponse.content.foreach {
case ContentBlock.TextContent(text, _) => println(text)
case ContentBlock.TextContent(text, _, _) => println(text)
case _ => // Handle other content types if needed
}
println(s"Usage: ${messageResponse.usage}")
Expand Down Expand Up @@ -290,6 +290,7 @@ val request = MessageRequest(
stopSequences = Some(List("\n\n")), // Stop generation at sequences
system = Some("Be concise and helpful."),
tools = Some(tools) // Tool calling support
cacheControl = Some(CacheControl.Ephemeral()) // Optional cache control
)
```

Expand Down Expand Up @@ -391,7 +392,7 @@ object StructuredOutputExample:
response.body match {
case Right(messageResponse) =>
messageResponse.content.foreach {
case ContentBlock.TextContent(text, _) =>
case ContentBlock.TextContent(text, _, _) =>
println("Structured JSON output:")
println(text)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ClaudeSyncClient(config: ClaudeConfig, backend: SyncBackend = DefaultSyncB

val response = createMessage(withSchema)

val text = response.content.collect { case ContentBlock.TextContent(t, _) => t }.mkString
val text = response.content.collect { case ContentBlock.TextContent(t, _, _) => t }.mkString

try SnakePickle.read[T](text)
catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private[claude] class ClaudeAgentBackend[F[_]](
monad.flatMap(monad.map(client.createMessage(request).send(backend))(_.body)) {
case Right(response) =>
val textContent = response.content
.collectFirst { case ContentBlock.TextContent(text, _) => text }
.collectFirst { case ContentBlock.TextContent(text, _, _) => text }
.getOrElse("")

val toolCalls = response.content.collect { case ContentBlock.ToolUseContent(id, name, input) =>
Expand Down
18 changes: 18 additions & 0 deletions claude/src/main/scala/sttp/ai/claude/models/CacheControl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package sttp.ai.claude.models

import sttp.ai.core.json.SnakePickle
import upickle.implicits.key

@key("type")
sealed trait CacheControl

object CacheControl {
@key("ephemeral")
final case class Ephemeral(ttl: Option[String] = None) extends CacheControl

object Ephemeral {
implicit val rw: SnakePickle.ReadWriter[Ephemeral] = SnakePickle.macroRW
}

implicit val rw: SnakePickle.ReadWriter[CacheControl] = SnakePickle.macroRW
}
14 changes: 9 additions & 5 deletions claude/src/main/scala/sttp/ai/claude/models/ContentBlock.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ sealed trait ContentBlock {

object ContentBlock {
@key("text")
case class TextContent(text: String, citations: Option[List[Citation]] = None) extends ContentBlock {
case class TextContent(text: String, citations: Option[List[Citation]] = None, cacheControl: Option[CacheControl] = None)
extends ContentBlock {
val `type`: String = "text"
}

Expand All @@ -21,7 +22,7 @@ object ContentBlock {
}

@key("image")
case class ImageContent(source: ImageSource) extends ContentBlock {
case class ImageContent(source: ImageSource, cacheControl: Option[CacheControl] = None) extends ContentBlock {
val `type`: String = "image"
}

Expand All @@ -38,7 +39,8 @@ object ContentBlock {
case class ToolResultContent(
toolUseId: String,
content: String,
isError: Option[Boolean] = None
isError: Option[Boolean] = None,
cacheControl: Option[CacheControl] = None
) extends ContentBlock {
val `type`: String = "tool_result"
}
Expand All @@ -48,7 +50,8 @@ object ContentBlock {
source: DocumentSource,
title: Option[String] = None,
context: Option[String] = None,
citations: Option[CitationsConfig] = None
citations: Option[CitationsConfig] = None,
cacheControl: Option[CacheControl] = None
) extends ContentBlock {
val `type`: String = "document"
}
Expand All @@ -75,7 +78,8 @@ object ContentBlock {
url: String,
title: String,
pageAge: Option[String] = None,
encryptedContent: Option[String] = None
encryptedContent: Option[String] = None,
cacheControl: Option[CacheControl] = None
) {
val `type`: String = "web_search_result"
}
Expand Down
3 changes: 2 additions & 1 deletion claude/src/main/scala/sttp/ai/claude/models/Tool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ sealed trait Tool
case class ToolInputSchema(
`type`: String,
properties: Map[String, PropertySchema],
required: Option[List[String]] = None
required: Option[List[String]] = None,
cacheControl: Option[CacheControl] = None
)

case class PropertySchema(
Expand Down
7 changes: 5 additions & 2 deletions claude/src/main/scala/sttp/ai/claude/models/Usage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import sttp.ai.core.json.SnakePickle.{macroRW, ReadWriter}

case class Usage(
inputTokens: Int,
outputTokens: Int
outputTokens: Int,
cacheReadInputTokens: Option[Int] = None,
cacheCreationInputTokens: Option[Int] = None
) {
def totalTokens: Int = inputTokens + outputTokens
def totalInputTokens: Int = inputTokens + cacheReadInputTokens.getOrElse(0) + cacheCreationInputTokens.getOrElse(0)
def totalTokens: Int = totalInputTokens + outputTokens
}

object Usage {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sttp.ai.claude.requests

import sttp.ai.claude.models.{Effort, Message, OutputConfig, OutputFormat, Tool}
import sttp.ai.claude.models.{CacheControl, Effort, Message, OutputConfig, OutputFormat, Tool}
import sttp.ai.core.json.SnakePickle.{macroRW, ReadWriter}

case class MessageRequest(
Expand All @@ -14,7 +14,8 @@ case class MessageRequest(
stopSequences: Option[List[String]] = None,
stream: Option[Boolean] = None,
tools: Option[List[Tool]] = None,
outputConfig: Option[OutputConfig] = None
outputConfig: Option[OutputConfig] = None,
cacheControl: Option[CacheControl] = None
) {
def usesStructuredOutput: Boolean = outputConfig.exists(_.format.exists(_.isInstanceOf[OutputFormat.JsonSchema]))

Expand All @@ -23,6 +24,9 @@ case class MessageRequest(
this.copy(outputConfig = Some(updated))
}

def withCacheControl(cacheControl: CacheControl): MessageRequest =
this.copy(cacheControl = Some(cacheControl))

def withEffort(effort: Effort): MessageRequest = {
val updated = outputConfig.getOrElse(OutputConfig()).copy(effort = Some(effort))
this.copy(outputConfig = Some(updated))
Expand Down
Loading