@@ -21,27 +21,43 @@ vi.mock("@anthropic-ai/sdk", () => ({
2121
2222import { streamAnthropic } from "./anthropic.js" ;
2323
24+ function createSseResponse ( events : Record < string , unknown > [ ] = [ ] ) : Response {
25+ const body = events . map ( ( event ) => `data: ${ JSON . stringify ( event ) } \n\n` ) . join ( "" ) ;
26+ return new Response ( body , {
27+ status : 200 ,
28+ headers : { "content-type" : "text/event-stream" } ,
29+ } ) ;
30+ }
31+
32+ function makeAnthropicModel ( overrides : Partial < Model < "anthropic-messages" > > = { } ) {
33+ return {
34+ id : "claude-sonnet-4-6" ,
35+ name : "Claude Sonnet 4.6" ,
36+ provider : "anthropic" ,
37+ api : "anthropic-messages" ,
38+ baseUrl : "https://api.anthropic.com" ,
39+ reasoning : true ,
40+ input : [ "text" ] ,
41+ cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 } ,
42+ contextWindow : 200_000 ,
43+ maxTokens : 4096 ,
44+ ...overrides ,
45+ } satisfies Model < "anthropic-messages" > ;
46+ }
47+
2448describe ( "Anthropic provider" , ( ) => {
2549 beforeEach ( ( ) => {
2650 anthropicMockState . configs = [ ] ;
2751 } ) ;
2852
2953 it ( "keeps Cloudflare AI Gateway upstream provider auth on the Anthropic API key" , async ( ) => {
30- const model = {
31- id : "claude-sonnet-4-6" ,
32- name : "Claude Sonnet 4.6" ,
54+ const model = makeAnthropicModel ( {
3355 provider : "cloudflare-ai-gateway" ,
34- api : "anthropic-messages" ,
3556 baseUrl : "https://gateway.ai.cloudflare.com/v1/account/gateway/anthropic/v1/messages" ,
36- reasoning : true ,
37- input : [ "text" ] ,
38- cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 } ,
39- contextWindow : 200_000 ,
40- maxTokens : 4096 ,
4157 headers : {
4258 "cf-aig-authorization" : "Bearer gateway-token" ,
4359 } ,
44- } satisfies Model < "anthropic-messages" > ;
60+ } ) ;
4561 const context = {
4662 messages : [ { role : "user" , content : "hello" , timestamp : 1 } ] ,
4763 } satisfies Context ;
@@ -62,4 +78,93 @@ describe("Anthropic provider", () => {
6278 expect ( config . defaultHeaders ?. [ "x-api-key" ] ) . toBeUndefined ( ) ;
6379 expect ( config . defaultHeaders ?. [ "cf-aig-authorization" ] ) . toBe ( "Bearer gateway-token" ) ;
6480 } ) ;
81+
82+ it ( "preserves provider-signed Anthropic thinking text on replay" , async ( ) => {
83+ const highSurrogate = String . fromCharCode ( 0xd83d ) ;
84+ const signedThinking = `keep${ highSurrogate } signed` ;
85+ let capturedPayload : unknown ;
86+ const client = {
87+ messages : {
88+ create : vi . fn ( ( ) => ( {
89+ asResponse : ( ) =>
90+ Promise . resolve (
91+ createSseResponse ( [
92+ {
93+ type : "message_start" ,
94+ message : { id : "msg_1" , usage : { input_tokens : 1 , output_tokens : 0 } } ,
95+ } ,
96+ {
97+ type : "message_delta" ,
98+ delta : { stop_reason : "end_turn" } ,
99+ usage : { input_tokens : 1 , output_tokens : 1 } ,
100+ } ,
101+ { type : "message_stop" } ,
102+ ] ) ,
103+ ) ,
104+ } ) ) ,
105+ } ,
106+ } ;
107+
108+ const stream = streamAnthropic (
109+ makeAnthropicModel ( ) ,
110+ {
111+ messages : [
112+ { role : "user" , content : "hello" , timestamp : 0 } ,
113+ {
114+ role : "assistant" ,
115+ provider : "anthropic" ,
116+ api : "anthropic-messages" ,
117+ model : "claude-sonnet-4-6" ,
118+ stopReason : "stop" ,
119+ timestamp : 0 ,
120+ usage : {
121+ input : 0 ,
122+ output : 0 ,
123+ cacheRead : 0 ,
124+ cacheWrite : 0 ,
125+ totalTokens : 0 ,
126+ cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 , total : 0 } ,
127+ } ,
128+ content : [
129+ {
130+ type : "thinking" ,
131+ thinking : signedThinking ,
132+ thinkingSignature : "sig_1" ,
133+ } ,
134+ {
135+ type : "thinking" ,
136+ thinking : `sanitize${ highSurrogate } synthetic` ,
137+ thinkingSignature : "reasoning_content" ,
138+ } ,
139+ ] ,
140+ } ,
141+ { role : "user" , content : "again" , timestamp : 0 } ,
142+ ] ,
143+ } ,
144+ {
145+ apiKey : "sk-ant-provider" ,
146+ client : client as never ,
147+ onPayload : ( payload ) => {
148+ capturedPayload = payload ;
149+ } ,
150+ } ,
151+ ) ;
152+
153+ await stream . result ( ) ;
154+
155+ const payload = capturedPayload as { messages : Array < { role : string ; content : unknown [ ] } > } ;
156+ const assistantMessage = payload . messages . find ( ( message ) => message . role === "assistant" ) ;
157+ expect ( assistantMessage ?. content ) . toEqual ( [
158+ {
159+ type : "thinking" ,
160+ thinking : signedThinking ,
161+ signature : "sig_1" ,
162+ } ,
163+ {
164+ type : "thinking" ,
165+ thinking : "sanitizesynthetic" ,
166+ signature : "reasoning_content" ,
167+ } ,
168+ ] ) ;
169+ } ) ;
65170} ) ;
0 commit comments