@@ -2060,6 +2060,392 @@ public async Task GetResponseAsync_WithMultipleSystemMessages()
20602060 Assert . NotNull ( response ) ;
20612061 }
20622062
2063+ [ Fact ]
2064+ public async Task GetResponseAsync_MidConversationSystemMessage_TrailingAfterUser_OnOpus48 ( )
2065+ {
2066+ VerbatimHttpHandler handler = new (
2067+ expectedRequest : """
2068+ {
2069+ "model": "claude-opus-4-8",
2070+ "messages": [
2071+ {
2072+ "role": "user",
2073+ "content": [{ "type": "text", "text": "Review the process function." }]
2074+ },
2075+ {
2076+ "role": "assistant",
2077+ "content": [{ "type": "text", "text": "Looks fine for small inputs." }]
2078+ },
2079+ {
2080+ "role": "user",
2081+ "content": [{ "type": "text", "text": "Now review the caller." }]
2082+ },
2083+ {
2084+ "role": "system",
2085+ "content": [{ "type": "text", "text": "From now on, require type annotations." }]
2086+ }
2087+ ],
2088+ "max_tokens": 1024,
2089+ "system": [{ "type": "text", "text": "You are a code reviewer." }]
2090+ }
2091+ """ ,
2092+ actualResponse : """
2093+ {
2094+ "id": "msg_mid_conv_01",
2095+ "type": "message",
2096+ "role": "assistant",
2097+ "model": "claude-opus-4-8",
2098+ "content": [{ "type": "text", "text": "Reviewing..." }],
2099+ "stop_reason": "end_turn",
2100+ "usage": { "input_tokens": 30, "output_tokens": 10 }
2101+ }
2102+ """
2103+ ) ;
2104+
2105+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2106+
2107+ List < ChatMessage > messages =
2108+ [
2109+ new ( ChatRole . System , "You are a code reviewer." ) ,
2110+ new ( ChatRole . User , "Review the process function." ) ,
2111+ new ( ChatRole . Assistant , "Looks fine for small inputs." ) ,
2112+ new ( ChatRole . User , "Now review the caller." ) ,
2113+ new ( ChatRole . System , "From now on, require type annotations." ) ,
2114+ ] ;
2115+
2116+ ChatResponse response = await chatClient . GetResponseAsync (
2117+ messages ,
2118+ new ( ) ,
2119+ TestContext . Current . CancellationToken
2120+ ) ;
2121+ Assert . NotNull ( response ) ;
2122+ }
2123+
2124+ [ Fact ]
2125+ public async Task GetResponseAsync_MidConversationSystemMessage_BetweenUserAndAssistant_OnOpus48 ( )
2126+ {
2127+ VerbatimHttpHandler handler = new (
2128+ expectedRequest : """
2129+ {
2130+ "model": "claude-opus-4-8",
2131+ "messages": [
2132+ {
2133+ "role": "user",
2134+ "content": [{ "type": "text", "text": "Review this." }]
2135+ },
2136+ {
2137+ "role": "system",
2138+ "content": [{ "type": "text", "text": "Be concise." }]
2139+ },
2140+ {
2141+ "role": "assistant",
2142+ "content": [{ "type": "text", "text": "Looks good." }]
2143+ }
2144+ ],
2145+ "max_tokens": 1024
2146+ }
2147+ """ ,
2148+ actualResponse : """
2149+ {
2150+ "id": "msg_mid_conv_02",
2151+ "type": "message",
2152+ "role": "assistant",
2153+ "model": "claude-opus-4-8",
2154+ "content": [{ "type": "text", "text": "Done." }],
2155+ "stop_reason": "end_turn",
2156+ "usage": { "input_tokens": 15, "output_tokens": 5 }
2157+ }
2158+ """
2159+ ) ;
2160+
2161+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2162+
2163+ List < ChatMessage > messages =
2164+ [
2165+ new ( ChatRole . User , "Review this." ) ,
2166+ new ( ChatRole . System , "Be concise." ) ,
2167+ new ( ChatRole . Assistant , "Looks good." ) ,
2168+ ] ;
2169+
2170+ ChatResponse response = await chatClient . GetResponseAsync (
2171+ messages ,
2172+ new ( ) ,
2173+ TestContext . Current . CancellationToken
2174+ ) ;
2175+ Assert . NotNull ( response ) ;
2176+ }
2177+
2178+ [ Fact ]
2179+ public async Task GetResponseAsync_MidConversationSystemMessage_OnUnsupportedModel_HoistsToTopLevel ( )
2180+ {
2181+ VerbatimHttpHandler handler = new (
2182+ expectedRequest : """
2183+ {
2184+ "model": "claude-haiku-4-5",
2185+ "messages": [
2186+ {
2187+ "role": "user",
2188+ "content": [{ "type": "text", "text": "Review the process function." }]
2189+ },
2190+ {
2191+ "role": "assistant",
2192+ "content": [{ "type": "text", "text": "Looks fine for small inputs." }]
2193+ },
2194+ {
2195+ "role": "user",
2196+ "content": [{ "type": "text", "text": "Now review the caller." }]
2197+ }
2198+ ],
2199+ "max_tokens": 1024,
2200+ "system": [
2201+ { "type": "text", "text": "You are a code reviewer." },
2202+ { "type": "text", "text": "From now on, require type annotations." }
2203+ ]
2204+ }
2205+ """ ,
2206+ actualResponse : """
2207+ {
2208+ "id": "msg_mid_conv_03",
2209+ "type": "message",
2210+ "role": "assistant",
2211+ "model": "claude-haiku-4-5",
2212+ "content": [{ "type": "text", "text": "Reviewing..." }],
2213+ "stop_reason": "end_turn",
2214+ "usage": { "input_tokens": 30, "output_tokens": 10 }
2215+ }
2216+ """
2217+ ) ;
2218+
2219+ IChatClient chatClient = CreateChatClient ( handler , "claude-haiku-4-5" ) ;
2220+
2221+ List < ChatMessage > messages =
2222+ [
2223+ new ( ChatRole . System , "You are a code reviewer." ) ,
2224+ new ( ChatRole . User , "Review the process function." ) ,
2225+ new ( ChatRole . Assistant , "Looks fine for small inputs." ) ,
2226+ new ( ChatRole . User , "Now review the caller." ) ,
2227+ new ( ChatRole . System , "From now on, require type annotations." ) ,
2228+ ] ;
2229+
2230+ ChatResponse response = await chatClient . GetResponseAsync (
2231+ messages ,
2232+ new ( ) ,
2233+ TestContext . Current . CancellationToken
2234+ ) ;
2235+ Assert . NotNull ( response ) ;
2236+ }
2237+
2238+ [ Fact ]
2239+ public async Task GetResponseAsync_SystemMessageAfterAssistant_OnOpus48_IsHoisted ( )
2240+ {
2241+ // A system message that follows an assistant turn is an invalid mid-conversation
2242+ // position, so it is hoisted to the top-level `system` property even on Opus 4.8.
2243+ VerbatimHttpHandler handler = new (
2244+ expectedRequest : """
2245+ {
2246+ "model": "claude-opus-4-8",
2247+ "messages": [
2248+ {
2249+ "role": "user",
2250+ "content": [{ "type": "text", "text": "Hello" }]
2251+ },
2252+ {
2253+ "role": "assistant",
2254+ "content": [{ "type": "text", "text": "Hi!" }]
2255+ },
2256+ {
2257+ "role": "user",
2258+ "content": [{ "type": "text", "text": "Tell me about AI" }]
2259+ }
2260+ ],
2261+ "max_tokens": 1024,
2262+ "system": [{ "type": "text", "text": "Be concise." }]
2263+ }
2264+ """ ,
2265+ actualResponse : """
2266+ {
2267+ "id": "msg_mid_conv_04",
2268+ "type": "message",
2269+ "role": "assistant",
2270+ "model": "claude-opus-4-8",
2271+ "content": [{ "type": "text", "text": "AI is..." }],
2272+ "stop_reason": "end_turn",
2273+ "usage": { "input_tokens": 20, "output_tokens": 10 }
2274+ }
2275+ """
2276+ ) ;
2277+
2278+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2279+
2280+ List < ChatMessage > messages =
2281+ [
2282+ new ( ChatRole . User , "Hello" ) ,
2283+ new ( ChatRole . Assistant , "Hi!" ) ,
2284+ new ( ChatRole . System , "Be concise." ) ,
2285+ new ( ChatRole . User , "Tell me about AI" ) ,
2286+ ] ;
2287+
2288+ ChatResponse response = await chatClient . GetResponseAsync (
2289+ messages ,
2290+ new ( ) ,
2291+ TestContext . Current . CancellationToken
2292+ ) ;
2293+ Assert . NotNull ( response ) ;
2294+ }
2295+
2296+ [ Fact ]
2297+ public async Task GetResponseAsync_ConsecutiveSystemMessages_AreMerged_OnOpus48 ( )
2298+ {
2299+ VerbatimHttpHandler handler = new (
2300+ expectedRequest : """
2301+ {
2302+ "model": "claude-opus-4-8",
2303+ "messages": [
2304+ {
2305+ "role": "user",
2306+ "content": [{ "type": "text", "text": "Hello" }]
2307+ },
2308+ {
2309+ "role": "system",
2310+ "content": [
2311+ { "type": "text", "text": "Rule A." },
2312+ { "type": "text", "text": "Rule B." }
2313+ ]
2314+ }
2315+ ],
2316+ "max_tokens": 1024
2317+ }
2318+ """ ,
2319+ actualResponse : """
2320+ {
2321+ "id": "msg_mid_conv_05",
2322+ "type": "message",
2323+ "role": "assistant",
2324+ "model": "claude-opus-4-8",
2325+ "content": [{ "type": "text", "text": "Understood." }],
2326+ "stop_reason": "end_turn",
2327+ "usage": { "input_tokens": 15, "output_tokens": 5 }
2328+ }
2329+ """
2330+ ) ;
2331+
2332+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2333+
2334+ List < ChatMessage > messages =
2335+ [
2336+ new ( ChatRole . User , "Hello" ) ,
2337+ new ( ChatRole . System , "Rule A." ) ,
2338+ new ( ChatRole . System , "Rule B." ) ,
2339+ ] ;
2340+
2341+ ChatResponse response = await chatClient . GetResponseAsync (
2342+ messages ,
2343+ new ( ) ,
2344+ TestContext . Current . CancellationToken
2345+ ) ;
2346+ Assert . NotNull ( response ) ;
2347+ }
2348+
2349+ [ Fact ]
2350+ public async Task GetResponseAsync_LeadingSystemMessage_OnOpus48_StaysTopLevel ( )
2351+ {
2352+ VerbatimHttpHandler handler = new (
2353+ expectedRequest : """
2354+ {
2355+ "model": "claude-opus-4-8",
2356+ "messages": [{
2357+ "role": "user",
2358+ "content": [{ "type": "text", "text": "Tell me about AI" }]
2359+ }],
2360+ "max_tokens": 1024,
2361+ "system": [{ "type": "text", "text": "You are helpful." }]
2362+ }
2363+ """ ,
2364+ actualResponse : """
2365+ {
2366+ "id": "msg_mid_conv_06",
2367+ "type": "message",
2368+ "role": "assistant",
2369+ "model": "claude-opus-4-8",
2370+ "content": [{ "type": "text", "text": "AI is..." }],
2371+ "stop_reason": "end_turn",
2372+ "usage": { "input_tokens": 20, "output_tokens": 10 }
2373+ }
2374+ """
2375+ ) ;
2376+
2377+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2378+
2379+ List < ChatMessage > messages =
2380+ [
2381+ new ( ChatRole . System , "You are helpful." ) ,
2382+ new ( ChatRole . User , "Tell me about AI" ) ,
2383+ ] ;
2384+
2385+ ChatResponse response = await chatClient . GetResponseAsync (
2386+ messages ,
2387+ new ( ) ,
2388+ TestContext . Current . CancellationToken
2389+ ) ;
2390+ Assert . NotNull ( response ) ;
2391+ }
2392+
2393+ [ Fact ]
2394+ public async Task GetResponseAsync_WithCacheControlOnMidConversationSystemMessage ( )
2395+ {
2396+ VerbatimHttpHandler handler = new (
2397+ expectedRequest : """
2398+ {
2399+ "model": "claude-opus-4-8",
2400+ "messages": [
2401+ {
2402+ "role": "user",
2403+ "content": [{ "type": "text", "text": "Hello" }]
2404+ },
2405+ {
2406+ "role": "system",
2407+ "content": [{
2408+ "type": "text",
2409+ "text": "Remember the rules.",
2410+ "cache_control": { "type": "ephemeral", "ttl": "1h" }
2411+ }]
2412+ }
2413+ ],
2414+ "max_tokens": 1024
2415+ }
2416+ """ ,
2417+ actualResponse : """
2418+ {
2419+ "id": "msg_mid_conv_07",
2420+ "type": "message",
2421+ "role": "assistant",
2422+ "model": "claude-opus-4-8",
2423+ "content": [{ "type": "text", "text": "Understood." }],
2424+ "stop_reason": "end_turn",
2425+ "usage": { "input_tokens": 15, "output_tokens": 5 }
2426+ }
2427+ """
2428+ ) ;
2429+
2430+ IChatClient chatClient = CreateChatClient ( handler , "claude-opus-4-8" ) ;
2431+
2432+ var systemContent = new TextContent ( "Remember the rules." ) . WithCacheControl (
2433+ Anthropic . Models . Messages . Ttl . Ttl1h
2434+ ) ;
2435+
2436+ List < ChatMessage > messages =
2437+ [
2438+ new ( ChatRole . User , "Hello" ) ,
2439+ new ( ChatRole . System , [ systemContent ] ) ,
2440+ ] ;
2441+
2442+ ChatResponse response = await chatClient . GetResponseAsync (
2443+ messages ,
2444+ cancellationToken : TestContext . Current . CancellationToken
2445+ ) ;
2446+ Assert . NotNull ( response ) ;
2447+ }
2448+
20632449 [ Fact ]
20642450 public async Task GetResponseAsync_WithMixedContentTypes ( )
20652451 {
0 commit comments