Skip to content

Commit 37c4c75

Browse files
committed
OpenAI to Anthropic conversion using cache control
1 parent d1715d3 commit 37c4c75

File tree

16 files changed

+257
-142
lines changed

16 files changed

+257
-142
lines changed

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/JsonFormats.scala

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ trait JsonFormats {
4141

4242
implicit lazy val cacheControlFormat: Format[CacheControl] = new Format[CacheControl] {
4343
def reads(json: JsValue): JsResult[CacheControl] = json match {
44-
case JsObject(Seq(("type", JsString("ephemeral")))) => JsSuccess(CacheControl.Ephemeral)
45-
case _ => JsError("Invalid cache control")
44+
case JsObject(map) =>
45+
if (map == Map("type" -> JsString("ephemeral"))) JsSuccess(CacheControl.Ephemeral)
46+
else JsError(s"Invalid cache control $map")
47+
case x => {
48+
JsError(s"Invalid cache control ${x}")
49+
}
4650
}
4751

4852
def writes(cacheControl: CacheControl): JsValue = writeJsObject(cacheControl)
@@ -71,11 +75,8 @@ trait JsonFormats {
7175

7276
implicit lazy val textBlockFormat: Format[TextBlock] = Json.format[TextBlock]
7377

74-
// implicit lazy val contentBlockBaseFormat: Format[ContentBlockBase] =
75-
// Json.format[ContentBlockBase]
7678
implicit lazy val contentBlocksFormat: Format[ContentBlocks] = Json.format[ContentBlocks]
7779

78-
// implicit lazy val textBlockWrites: Writes[TextBlock] = Json.writes[TextBlock]
7980
implicit lazy val textBlockReads: Reads[TextBlock] = {
8081
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
8182
Json.reads[TextBlock]
@@ -85,27 +86,6 @@ trait JsonFormats {
8586
implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)
8687
Json.writes[TextBlock]
8788
}
88-
// implicit lazy val imageBlockWrites: Writes[ImageBlock] =
89-
// (block: ImageBlock) =>
90-
// Json.obj(
91-
// "type" -> "image",
92-
// "source" -> Json.obj(
93-
// "type" -> block.`type`,
94-
// "media_type" -> block.mediaType,
95-
// "data" -> block.data
96-
// )
97-
// )
98-
//
99-
// implicit lazy val documentBlockWrites: Writes[DocumentBlock] =
100-
// (block: DocumentBlock) =>
101-
// Json.obj(
102-
// "type" -> "document",
103-
// "source" -> Json.obj(
104-
// "type" -> block.`type`,
105-
// "media_type" -> block.mediaType,
106-
// "data" -> block.data
107-
// )
108-
// )
10989

11090
implicit lazy val mediaBlockWrites: Writes[MediaBlock] =
11191
(block: MediaBlock) =>
@@ -119,7 +99,7 @@ trait JsonFormats {
11999
)
120100

121101
private def cacheControlToJsObject(maybeCacheControl: Option[CacheControl]): JsObject =
122-
maybeCacheControl.fold(Json.obj())(cc => Json.obj("cache_control" -> Json.toJson(cc)))
102+
maybeCacheControl.fold(Json.obj())(cc => writeJsObject(cc))
123103

124104
implicit lazy val contentBlockWrites: Writes[ContentBlockBase] = {
125105
case ContentBlockBase(textBlock @ TextBlock(_), cacheControl) =>
@@ -128,8 +108,7 @@ trait JsonFormats {
128108
cacheControlToJsObject(cacheControl)
129109
case ContentBlockBase(media @ MediaBlock(_, _, _, _), maybeCacheControl) =>
130110
Json.toJson(media)(mediaBlockWrites).as[JsObject] ++
131-
// cacheControlToJsObject(maybeCacheControl)
132-
maybeCacheControl.map(cc => writeJsObject(cc)).getOrElse(Json.obj())
111+
cacheControlToJsObject(maybeCacheControl)
133112

134113
}
135114

@@ -160,20 +139,6 @@ trait JsonFormats {
160139
}
161140
}
162141

163-
// CacheControl Reads and Writes
164-
// implicit lazy val cacheControlReads: Reads[Option[CacheControl]] =
165-
// Reads[Option[CacheControl]] {
166-
// case JsObject(Seq("type", JsString("ephemeral"))) =>
167-
// JsSuccess(Some(CacheControl.Ephemeral))
168-
// case JsNull | JsUndefined() => JsSuccess(None)
169-
// case _ => JsError("Invalid cache control")
170-
// }
171-
//
172-
// implicit lazy val cacheControlWrites: Writes[CacheControl] =
173-
// Writes[CacheControl] { case CacheControl.Ephemeral =>
174-
// Json.obj("cache_control" -> Json.obj("type" -> "ephemeral"))
175-
// }
176-
177142
implicit lazy val contentReads: Reads[Content] = new Reads[Content] {
178143
def reads(json: JsValue): JsResult[Content] = json match {
179144
case JsString(str) => JsSuccess(SingleString(str))
@@ -182,11 +147,20 @@ trait JsonFormats {
182147
}
183148
}
184149

150+
implicit lazy val contentWrites: Writes[Content] = new Writes[Content] {
151+
def writes(content: Content): JsValue = content match {
152+
case SingleString(text, cacheControl) =>
153+
Json.obj("content" -> text) ++ cacheControlToJsObject(cacheControl)
154+
case ContentBlocks(blocks) =>
155+
Json.obj("content" -> Json.toJson(blocks)(Writes.seq(contentBlockWrites)))
156+
}
157+
}
158+
185159
implicit lazy val baseMessageWrites: Writes[Message] = new Writes[Message] {
186160
def writes(message: Message): JsValue = message match {
187161
case UserMessage(content, cacheControl) =>
188162
val baseObj = Json.obj("role" -> "user", "content" -> content)
189-
cacheControl.fold(baseObj)(cc => baseObj + ("cache_control" -> Json.toJson(cc)))
163+
baseObj ++ cacheControlToJsObject(cacheControl)
190164

191165
case UserMessageContent(content) =>
192166
Json.obj(
@@ -196,7 +170,7 @@ trait JsonFormats {
196170

197171
case AssistantMessage(content, cacheControl) =>
198172
val baseObj = Json.obj("role" -> "assistant", "content" -> content)
199-
cacheControl.fold(baseObj)(cc => baseObj + ("cache_control" -> Json.toJson(cc)))
173+
baseObj ++ cacheControlToJsObject(cacheControl)
200174

201175
case AssistantMessageContent(content) =>
202176
Json.obj(

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/domain/settings/AnthropicCreateMessageSettings.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ final case class AnthropicCreateMessageSettings(
55
// See [[models|https://docs.anthropic.com/claude/docs/models-overview]] for additional details and options.
66
model: String,
77

8-
// System prompt.
9-
// A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [[guide to system prompts|https://docs.anthropic.com/claude/docs/system-prompts]].
10-
system: Option[String] = None,
8+
// // System prompt.
9+
// // A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [[guide to system prompts|https://docs.anthropic.com/claude/docs/system-prompts]].
10+
// system: Option[String] = None,
1111

1212
// The maximum number of tokens to generate before stopping.
1313
// Note that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/AnthropicService.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package io.cequence.openaiscala.anthropic.service
22

33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
5-
import io.cequence.openaiscala.anthropic.domain.Message
5+
import io.cequence.openaiscala.anthropic.domain.{Content, Message}
66
import io.cequence.openaiscala.anthropic.domain.response.{
77
ContentBlockDelta,
88
CreateMessageResponse
@@ -32,6 +32,7 @@ trait AnthropicService extends CloseableService with AnthropicServiceConsts {
3232
* <a href="https://docs.anthropic.com/claude/reference/messages_post">Anthropic Doc</a>
3333
*/
3434
def createMessage(
35+
system: Option[Content],
3536
messages: Seq[Message],
3637
settings: AnthropicCreateMessageSettings = DefaultSettings.CreateMessage
3738
): Future[CreateMessageResponse]
@@ -54,6 +55,7 @@ trait AnthropicService extends CloseableService with AnthropicServiceConsts {
5455
* <a href="https://docs.anthropic.com/claude/reference/messages_post">Anthropic Doc</a>
5556
*/
5657
def createMessageStreamed(
58+
system: Option[Content],
5759
messages: Seq[Message],
5860
settings: AnthropicCreateMessageSettings = DefaultSettings.CreateMessage
5961
): Source[ContentBlockDelta, NotUsed]

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/AnthropicServiceFactory.scala

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,19 @@ object AnthropicServiceFactory extends AnthropicServiceConsts {
6161
*/
6262
def apply(
6363
apiKey: String = getAPIKeyFromEnv(),
64-
timeouts: Option[Timeouts] = None
64+
timeouts: Option[Timeouts] = None,
65+
withPdf: Boolean = false,
66+
withCache: Boolean = false
6567
)(
6668
implicit ec: ExecutionContext,
6769
materializer: Materializer
6870
): AnthropicService = {
6971
val authHeaders = Seq(
7072
("x-api-key", s"$apiKey"),
7173
("anthropic-version", apiVersion)
72-
)
73-
new AnthropicServiceClassImpl(defaultCoreUrl, authHeaders, timeouts)
74-
}
74+
) ++ (if (withPdf) Seq(("anthropic-beta", "pdfs-2024-09-25")) else Seq.empty) ++
75+
(if (withCache) Seq(("anthropic-beta", "prompt-caching-2024-07-31")) else Seq.empty)
7576

76-
def withPdf(
77-
apiKey: String = getAPIKeyFromEnv(),
78-
timeouts: Option[Timeouts] = None
79-
)(
80-
implicit ec: ExecutionContext,
81-
materializer: Materializer
82-
): AnthropicService = {
83-
val authHeaders = Seq(
84-
("x-api-key", s"$apiKey"),
85-
("anthropic-version", apiVersion),
86-
("anthropic-beta", "pdfs-2024-09-25")
87-
)
8877
new AnthropicServiceClassImpl(defaultCoreUrl, authHeaders, timeouts)
8978
}
9079

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/impl/AnthropicServiceImpl.scala

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import io.cequence.openaiscala.anthropic.domain.response.{
99
CreateMessageResponse
1010
}
1111
import io.cequence.openaiscala.anthropic.domain.settings.AnthropicCreateMessageSettings
12-
import io.cequence.openaiscala.anthropic.domain.{ChatRole, Message}
12+
import io.cequence.openaiscala.anthropic.domain.{ChatRole, Content, Message}
1313
import io.cequence.openaiscala.anthropic.service.{AnthropicService, HandleAnthropicErrorCodes}
1414
import io.cequence.wsclient.JsonUtil.JsonOps
1515
import io.cequence.wsclient.ResponseImplicits.JsonSafeOps
1616
import io.cequence.wsclient.service.WSClientWithEngineTypes.WSClientWithStreamEngine
1717
import org.slf4j.LoggerFactory
18-
import play.api.libs.json.{JsValue, Json}
18+
import play.api.libs.json.{JsString, JsValue, Json, Writes}
1919

2020
import scala.concurrent.Future
2121

@@ -33,17 +33,20 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
3333
private val logger = LoggerFactory.getLogger("AnthropicServiceImpl")
3434

3535
override def createMessage(
36+
system: Option[Content],
3637
messages: Seq[Message],
3738
settings: AnthropicCreateMessageSettings
3839
): Future[CreateMessageResponse] =
3940
execPOST(
4041
EndPoint.messages,
41-
bodyParams = createBodyParamsForMessageCreation(messages, settings, stream = false)
42+
bodyParams =
43+
createBodyParamsForMessageCreation(system, messages, settings, stream = false)
4244
).map(
4345
_.asSafeJson[CreateMessageResponse]
4446
)
4547

4648
override def createMessageStreamed(
49+
system: Option[Content],
4750
messages: Seq[Message],
4851
settings: AnthropicCreateMessageSettings
4952
): Source[ContentBlockDelta, NotUsed] =
@@ -52,7 +55,7 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
5255
EndPoint.messages.toString(),
5356
"POST",
5457
bodyParams = paramTuplesToStrings(
55-
createBodyParamsForMessageCreation(messages, settings, stream = true)
58+
createBodyParamsForMessageCreation(system, messages, settings, stream = true)
5659
)
5760
)
5861
.map { (json: JsValue) =>
@@ -80,6 +83,7 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
8083
.collect { case Some(delta) => delta }
8184

8285
private def createBodyParamsForMessageCreation(
86+
system: Option[Content],
8387
messages: Seq[Message],
8488
settings: AnthropicCreateMessageSettings,
8589
stream: Boolean
@@ -89,10 +93,57 @@ private[service] trait AnthropicServiceImpl extends Anthropic {
8993

9094
val messageJsons = messages.map(Json.toJson(_))
9195

96+
val systemMessages = Seq(
97+
Map(
98+
"type" -> "text",
99+
"text" -> "You respond in Slovak language."
100+
),
101+
Map(
102+
"type" -> "text",
103+
"text" -> "You make jokes about the question."
104+
)
105+
)
106+
107+
val system2 = Content.ContentBlocks(
108+
Seq(
109+
Content.ContentBlockBase(
110+
Content.ContentBlock.TextBlock("You respond in Slovak language.")
111+
),
112+
Content.ContentBlockBase(
113+
Content.ContentBlock.TextBlock("You make jokes about the question.")
114+
)
115+
)
116+
)
117+
val systemJson = system.map { x =>
118+
x match {
119+
case single @ Content.SingleString(text, cacheControl) =>
120+
if (cacheControl.isEmpty) JsString(text)
121+
else {
122+
val blocks =
123+
Seq(Content.ContentBlockBase(Content.ContentBlock.TextBlock(text), cacheControl))
124+
125+
Json.toJson(blocks)(Writes.seq(contentBlockWrites))
126+
}
127+
case Content.ContentBlocks(blocks) =>
128+
Json.toJson(blocks)(Writes.seq(contentBlockWrites))
129+
case Content.ContentBlockBase(content, cacheControl) => ???
130+
}
131+
// Json.toJson(x)(Writes.seq(contentBlockWrites))
132+
133+
}
134+
135+
println(s"systemJson: $systemJson")
136+
137+
val systemMessagesJson = systemMessages.map(Json.toJson(_))
138+
println(s"systemMessagesJson: $systemMessagesJson")
139+
92140
jsonBodyParams(
93141
Param.messages -> Some(messageJsons),
94142
Param.model -> Some(settings.model),
95-
Param.system -> settings.system,
143+
// Param.system -> settings.system,
144+
Param.system -> Some(
145+
systemJson
146+
),
96147
Param.max_tokens -> Some(settings.max_tokens),
97148
Param.metadata -> { if (settings.metadata.isEmpty) None else Some(settings.metadata) },
98149
Param.stop_sequences -> {

anthropic-client/src/main/scala/io/cequence/openaiscala/anthropic/service/impl/OpenAIAnthropicChatCompletionService.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.cequence.openaiscala.anthropic.service.impl
22

33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
5+
import io.cequence.openaiscala.anthropic.domain.Content
56
import io.cequence.openaiscala.anthropic.service.AnthropicService
67
import io.cequence.openaiscala.domain.BaseMessage
78
import io.cequence.openaiscala.domain.response.{
@@ -40,8 +41,9 @@ private[service] class OpenAIAnthropicChatCompletionService(
4041
): Future[ChatCompletionResponse] = {
4142
underlying
4243
.createMessage(
44+
toAnthropicSystemMessages(messages, settings),
4345
toAnthropicMessages(messages, settings),
44-
toAnthropic(settings, messages)
46+
toAnthropicSettings(settings)
4547
)
4648
.map(toOpenAI)
4749
// TODO: recover and wrap exceptions
@@ -64,8 +66,9 @@ private[service] class OpenAIAnthropicChatCompletionService(
6466
): Source[ChatCompletionChunkResponse, NotUsed] =
6567
underlying
6668
.createMessageStreamed(
69+
toAnthropicSystemMessages(messages, settings),
6770
toAnthropicMessages(messages, settings),
68-
toAnthropic(settings, messages)
71+
toAnthropicSettings(settings)
6972
)
7073
.map(toOpenAI)
7174

0 commit comments

Comments
 (0)