okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
794bda6
main
1// Bedrock's SigV4 header construction, the Anthropic-shaped request body it 2// shares with bedrock-mantle, and the AWS config/credentials file resolution 3// have edge cases the shared OpenAI E2E fake cannot exercise through the 4// fixed AWS endpoint policy. Native fixtures use temporary files/helpers, 5// never real AWS credentials or network. 6 7import albedo/harness/extension as harness_extension 8import albedo/harness/extensions/bedrock/aws_profile 9import albedo/harness/extensions/bedrock/extension 10import albedo/harness/extensions/bedrock/sigv4 11import albedo/harness/extensions/bedrock/wire 12import albedo/harness/extensions/claude/stream as claude_stream 13import albedo/openai_api 14import albedo/openai_api/stream as reducer 15import albedo/openai_api/types 16import gleam/bit_array 17import gleam/crypto 18import gleam/dict .{ type Dict } 19import gleam/dynamic .{ type Dynamic } 20import gleam/dynamic/decode 21import gleam/json 22import gleam/list 23import gleam/option .{ None , Some } 24import gleam/result 25import gleam/string 26import gleam/string_tree 27 28pub fn effective_base_url_defaults_to_runtime_in_the_given_region_test () -> Nil { 29assert extension . effective_base_url ( "" , "us-east-1" ) 30== Ok ( "https://bedrock-runtime.us-east-1.amazonaws.com" ) 31} 32 33pub fn effective_base_url_keeps_an_explicit_url_regardless_of_region_test () -> Nil { 34let mantle = "https://bedrock-mantle.eu-west-1.api.aws" 35assert extension . effective_base_url ( mantle , "us-east-1" ) == Ok ( mantle ) 36assert extension . effective_base_url ( mantle , "" ) == Ok ( mantle ) 37} 38 39pub fn effective_base_url_fails_with_neither_a_url_nor_a_region_test () -> Nil { 40let assert Error ( message ) = extension . effective_base_url ( "" , "" ) 41assert string . contains ( message , "AWS_REGION" ) 42} 43 44// An arbitrary fixed instant; independently cross-checked against the OS 45// calendar (`date -u -r 1700000000`), not derived from this code. 46const epoch = 1_700_000_000 47 48const runtime_host = "bedrock-runtime.us-east-1.amazonaws.com" 49 50const runtime_url = "https://bedrock-runtime.us-east-1.amazonaws.com" 51 52fn creds () -> sigv4 . Credentials { 53sigv4 . Credentials ( "AKIATEST" , "topsecret" , None ) 54} 55 56fn header ( headers : List (#( String , String )), name : String ) -> String { 57let assert Ok (#( _ , value )) = list . find ( headers , fn ( h ) { h . 0 == name }) 58value 59} 60 61fn sha256_hex ( text : String ) -> String { 62crypto . hash ( crypto . Sha256 , bit_array . from_string ( text )) 63|> bit_array . base16_encode 64|> string . lowercase 65} 66 67pub fn sigv4_dates_the_request_and_lists_signed_headers_in_order_test () -> Result ( 68#( String , String ), 69Nil , 70) { 71let headers = 72sigv4 . headers ( creds (), "us-east-1" , "bedrock" , runtime_host , epoch , "{}" ) 73assert header ( headers , "x-amz-date" ) == "20231114T221320Z" 74assert header ( headers , "x-amz-content-sha256" ) == sha256_hex ( "{}" ) 75let authorization = header ( headers , "authorization" ) 76assert string . starts_with ( 77authorization , 78"AWS4-HMAC-SHA256 Credential=AKIATEST/20231114/us-east-1/bedrock/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=" , 79) 80let assert Error ( Nil ) = 81list . find ( headers , fn ( h ) { h . 0 == "x-amz-security-token" }) 82} 83 84pub fn sigv4_adds_the_security_token_header_only_when_one_is_given_test () -> Nil { 85let with_token = 86sigv4 . headers ( 87sigv4 . Credentials ( "ak" , "sk" , Some ( "tok" )), 88"us-east-1" , 89"bedrock" , 90runtime_host , 91epoch , 92"" , 93) 94assert header ( with_token , "x-amz-security-token" ) == "tok" 95assert string . contains ( 96header ( with_token , "authorization" ), 97"x-amz-security-token" , 98) 99let without = 100sigv4 . headers ( 101sigv4 . Credentials ( "ak" , "sk" , None ), 102"us-east-1" , 103"bedrock" , 104runtime_host , 105epoch , 106"" , 107) 108assert ! string . contains ( 109header ( without , "authorization" ), 110"x-amz-security-token" , 111) 112assert list . find ( without , fn ( h ) { h . 0 == "x-amz-security-token" }) 113== Error ( Nil ) 114} 115 116pub fn sigv4_signature_is_sensitive_to_the_body_and_the_secret_test () -> Nil { 117let base = 118sigv4 . headers ( creds (), "us-east-1" , "bedrock" , runtime_host , epoch , "one" ) 119let different_body = 120sigv4 . headers ( creds (), "us-east-1" , "bedrock" , runtime_host , epoch , "two" ) 121let different_secret = 122sigv4 . headers ( 123sigv4 . Credentials ( "AKIATEST" , "othersecret" , None ), 124"us-east-1" , 125"bedrock" , 126runtime_host , 127epoch , 128"one" , 129) 130assert header ( base , "authorization" ) 131!= header ( different_body , "authorization" ) 132assert header ( base , "authorization" ) 133!= header ( different_secret , "authorization" ) 134assert header ( base , "x-amz-content-sha256" ) 135!= header ( different_body , "x-amz-content-sha256" ) 136} 137 138fn request ( model : String , input : List ( types . Input )) -> types . Request { 139types . Request ( model , None , input , [], Some ( 100 ), types . defaults ) 140} 141 142fn exchange ( 143auth : wire . Auth , 144base_url : String , 145input : List ( types . Input ), 146) -> Result ( openai_api . Exchange , types . Error ) { 147wire . encode ( 148auth , 149base_url , 150epoch , 151request ( "anthropic.claude-sonnet-5" , input ), 152) 153} 154 155fn body ( input : List ( types . Input )) -> Dynamic { 156let assert Ok ( openai_api . Exchange ( body : body , ..)) = 157exchange ( wire . Bearer ( "key" ), runtime_url , input ) 158let assert Ok ( value ) = json . parse ( string_tree . to_string ( body ), decode . dynamic ) 159value 160} 161 162fn at ( value : Dynamic , path : List ( String ), decoder : decode . Decoder ( a)) -> a{ 163let assert Ok ( found ) = decode . run ( value , decode . at ( path , decoder )) 164found 165} 166 167pub fn encode_rejects_a_host_that_is_not_bedrock_runtime_or_mantle_test () -> Nil { 168let assert Error ( types . InvalidRequest ( message )) = 169exchange ( wire . Bearer ( "key" ), "https://example.com" , [ types . User ( "hi" )]) 170assert string . contains ( message , "bedrock-runtime" ) 171} 172 173pub fn encode_accepts_both_bedrock_endpoints_and_keeps_their_region_test () -> Nil { 174let assert Ok ( openai_api . Exchange ( url : on_runtime , ..)) = 175exchange ( wire . Bearer ( "key" ), runtime_url , [ types . User ( "hi" )]) 176assert on_runtime == runtime_url <> "/anthropic/v1/messages" 177let assert Ok ( openai_api . Exchange ( url : on_mantle , ..)) = 178exchange ( wire . Bearer ( "key" ), "https://bedrock-mantle.eu-west-1.api.aws" , [ 179types . User ( "hi" ), 180]) 181assert on_mantle 182== "https://bedrock-mantle.eu-west-1.api.aws/anthropic/v1/messages" 183} 184 185pub fn encode_rejects_models_outside_the_claude_family_test () -> Nil { 186let assert Error ( types . InvalidRequest ( message )) = 187wire . encode ( 188wire . Bearer ( "key" ), 189runtime_url , 190epoch , 191request ( "meta.llama4-maverick-17b-instruct-v1:0" , [ types . User ( "hi" )]), 192) 193assert string . contains ( message , "Claude" ) 194} 195 196pub fn encode_sends_the_bearer_key_as_x_api_key_and_signs_with_sigv4_otherwise_test () -> Nil { 197let assert Ok ( openai_api . Exchange ( headers : bearer_headers , ..)) = 198exchange ( wire . Bearer ( "my-key" ), runtime_url , [ types . User ( "hi" )]) 199assert header ( bearer_headers , "x-api-key" ) == "my-key" 200assert list . find ( bearer_headers , fn ( h ) { h . 0 == "authorization" }) 201== Error ( Nil ) 202let assert Ok ( openai_api . Exchange ( headers : sigv4_headers , ..)) = 203exchange ( wire . SigV4 ( creds ()), runtime_url , [ types . User ( "hi" )]) 204assert string . starts_with ( 205header ( sigv4_headers , "authorization" ), 206"AWS4-HMAC-SHA256" , 207) 208} 209 210pub fn adjacent_same_role_turns_merge_into_one_message_test () -> Nil { 211let value = 212body ([ 213types . User ( "a" ), 214types . Assistant ( "b" ), 215types . ToolOutput ( "call-1" , "ok" , []), 216types . ToolOutput ( "call-2" , "ok2" , []), 217]) 218let assert [ first , second , third ] = 219at ( value , [ "messages" ], decode . list ( decode . dynamic )) 220assert at ( first , [ "role" ], decode . string ) == "user" 221assert at ( second , [ "role" ], decode . string ) == "assistant" 222assert at ( third , [ "role" ], decode . string ) == "user" 223let assert [ block_one , block_two ] = 224at ( third , [ "content" ], decode . list ( decode . dynamic )) 225assert at ( block_one , [ "tool_use_id" ], decode . string ) == "call-1" 226assert at ( block_two , [ "tool_use_id" ], decode . string ) == "call-2" 227} 228 229pub fn user_image_with_no_caption_carries_no_text_block_test () -> Nil { 230let assert Ok ( image ) = types . image ( "image/png" , "aGk=" , 2 , 3 , 2 ) 231let value = body ([ types . UserImage ( "" , [ image ])]) 232let assert [ message ] = at ( value , [ "messages" ], decode . list ( decode . dynamic )) 233let assert [ block ] = at ( message , [ "content" ], decode . list ( decode . dynamic )) 234assert at ( block , [ "type" ], decode . string ) == "image" 235assert at ( block , [ "source" , "media_type" ], decode . string ) == "image/png" 236} 237 238pub fn replay_keeps_text_and_tool_calls_but_drops_thinking_test () -> Nil { 239let assert Ok ( item ) = 240json . parse ( 241"{\"role\":\"assistant\",\"content\":\"done\",\"reasoning_content\":\"secret thoughts\",\"tool_calls\":[{\"id\":\"call-1\",\"type\":\"function\",\"function\":{\"name\":\"bash\",\"arguments\":\"{\\\"cmd\\\":\\\"ls\\\"}\"}}]}" , 242types . replay_decoder ( types . ChatCompletions ), 243) 244let value = body ([ types . User ( "go" ), types . Replay ( item )]) 245let assert [ _ , assistant ] = 246at ( value , [ "messages" ], decode . list ( decode . dynamic )) 247let assert [ text , call ] = 248at ( assistant , [ "content" ], decode . list ( decode . dynamic )) 249assert at ( text , [ "text" ], decode . string ) == "done" 250assert at ( call , [ "type" ], decode . string ) == "tool_use" 251assert at ( call , [ "id" ], decode . string ) == "call-1" 252assert at ( call , [ "name" ], decode . string ) == "bash" 253assert at ( call , [ "input" , "cmd" ], decode . string ) == "ls" 254let assert Ok ( openai_api . Exchange ( body : raw , ..)) = 255exchange ( wire . Bearer ( "key" ), runtime_url , [ 256types . User ( "go" ), 257types . Replay ( item ), 258]) 259assert ! string . contains ( string_tree . to_string ( raw ), "secret thoughts" ) 260} 261 262pub fn tool_choice_and_effort_encode_as_documented_fields_test () -> Nil { 263let tool = 264types . Tool ( 265"bash" , 266"run" , 267json . object ([#( "type" , json . string ( "object" ))]), 268False , 269) 270let configured = 271types . Request ( 272"anthropic.claude-sonnet-5" , 273Some ( "be nice" ), 274[ types . User ( "hi" )], 275[ tool ], 276Some ( 100 ), 277types . Options ( 278.. types . defaults , 279tool_choice : Some ( types . NamedTool ( "bash" )), 280effort : Some ( "high" ), 281), 282) 283let assert Ok ( openai_api . Exchange ( body : raw , ..)) = 284wire . encode ( wire . Bearer ( "key" ), runtime_url , epoch , configured ) 285let assert Ok ( value ) = json . parse ( string_tree . to_string ( raw ), decode . dynamic ) 286assert at ( value , [ "system" ], decode . string ) == "be nice" 287assert at ( value , [ "tool_choice" , "type" ], decode . string ) == "tool" 288assert at ( value , [ "tool_choice" , "name" ], decode . string ) == "bash" 289assert at ( value , [ "output_config" , "effort" ], decode . string ) == "high" 290let assert [ tool_entry ] = at ( value , [ "tools" ], decode . list ( decode . dynamic )) 291assert at ( tool_entry , [ "name" ], decode . string ) == "bash" 292} 293 294pub fn tool_schemas_reuse_claudes_combinator_flattening_test () -> Nil { 295let assert Ok ( schema ) = 296json . parse ( 297"{\"oneOf\":[{\"required\":[\"a\"]},{\"required\":[\"b\"]}],\"properties\":{\"a\":{\"type\":\"string\"},\"b\":{\"type\":\"string\"}}}" , 298decode . dynamic , 299) 300let tool = 301types . Tool ( "pick" , "pick a or b" , types . encode_value ( schema ), False ) 302let configured = 303types . Request ( 304"anthropic.claude-sonnet-5" , 305None , 306[ types . User ( "hi" )], 307[ tool ], 308Some ( 100 ), 309types . defaults , 310) 311let assert Ok ( openai_api . Exchange ( body : raw , ..)) = 312wire . encode ( wire . Bearer ( "key" ), runtime_url , epoch , configured ) 313let assert Ok ( parsed ) = json . parse ( string_tree . to_string ( raw ), decode . dynamic ) 314let assert [ tool_entry ] = at ( parsed , [ "tools" ], decode . list ( decode . dynamic )) 315assert at ( tool_entry , [ "input_schema" , "type" ], decode . string ) == "object" 316assert result . is_error ( decode . run ( 317tool_entry , 318decode . at ([ "input_schema" , "oneOf" ], decode . dynamic ), 319)) 320} 321 322fn feed_all ( 323state : reducer . Reducer , 324chunks : List ( String ), 325) -> Result ( types . Turn , types . Error ) { 326case chunks { 327[] -> Error ( types . UnexpectedEnd ) 328[ chunk , .. rest ] -> { 329use #( next , _ , turn ) <- result . try ( state . feed ( chunk )) 330case turn { 331Some ( turn ) -> Ok ( turn ) 332None -> feed_all ( next , rest ) 333} 334} 335} 336} 337 338pub fn a_bedrock_turn_replays_through_claudes_own_reducer_test () -> Nil { 339let chunks = [ 340"{\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"usage\":{\"input_tokens\":5,\"output_tokens\":0}}}" , 341"{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}" , 342"{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"pong\"}}" , 343"{\"type\":\"content_block_stop\",\"index\":0}" , 344"{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}" , 345"{\"type\":\"message_stop\"}" , 346] 347let assert Ok ( turn ) = 348feed_all ( claude_stream . reducer ( "anthropic.claude-sonnet-5" , []), chunks ) 349assert turn . finish == types . Complete 350let assert [ item ] = turn . output 351let value = body ([ types . User ( "ping" ), types . Replay ( item )]) 352let assert [ _ , assistant ] = 353at ( value , [ "messages" ], decode . list ( decode . dynamic )) 354let assert [ text ] = at ( assistant , [ "content" ], decode . list ( decode . dynamic )) 355assert at ( text , [ "text" ], decode . string ) == "pong" 356} 357 358pub fn parse_section_finds_default_and_named_profile_headers_test () -> Result ( 359Dict ( String , String ), 360Nil , 361) { 362let text = 363"[default]\nregion = us-east-1\n\n[profile other]\ncredential_process = /bin/echo hi\n" 364let assert Ok ( default_section ) = aws_profile . parse_section ( text , "default" ) 365assert dict . get ( default_section , "region" ) == Ok ( "us-east-1" ) 366let assert Ok ( other ) = aws_profile . parse_section ( text , "profile other" ) 367assert dict . get ( other , "credential_process" ) == Ok ( "/bin/echo hi" ) 368aws_profile . parse_section ( text , "profile missing" ) 369} 370 371pub fn parse_section_keeps_equals_signs_inside_values_test () -> Nil { 372let text = "[default]\naws_session_token = abc=def==\n" 373let assert Ok ( section ) = aws_profile . parse_section ( text , "default" ) 374assert dict . get ( section , "aws_session_token" ) == Ok ( "abc=def==" ) 375} 376 377pub fn config_header_only_prefixes_named_profiles_test () -> Nil { 378assert aws_profile . config_header ( "default" ) == "default" 379assert aws_profile . config_header ( "work" ) == "profile work" 380} 381 382pub fn credentials_from_section_names_the_missing_key_test () -> Nil { 383let assert Error ( message ) = 384aws_profile . credentials_from_section ( dict . new (), "work" ) 385assert string . contains ( message , "aws_access_key_id" ) 386let partial = dict . from_list ([#( "aws_access_key_id" , "ak" )]) 387let assert Error ( message ) = 388aws_profile . credentials_from_section ( partial , "work" ) 389assert string . contains ( message , "aws_secret_access_key" ) 390let complete = 391dict . from_list ([ 392#( "aws_access_key_id" , "ak" ), 393#( "aws_secret_access_key" , "sk" ), 394#( "aws_session_token" , "tok" ), 395]) 396assert aws_profile . credentials_from_section ( complete , "work" ) 397== Ok ( sigv4 . Credentials ( "ak" , "sk" , Some ( "tok" ))) 398} 399 400pub fn credentials_from_process_output_decodes_the_documented_json_test () -> Nil { 401assert aws_profile . credentials_from_process_output ( 402"{\"Version\":1,\"AccessKeyId\":\"AKIATEST\",\"SecretAccessKey\":\"topsecret\",\"SessionToken\":\"tok\"}" , 403) 404== Ok ( sigv4 . Credentials ( "AKIATEST" , "topsecret" , Some ( "tok" ))) 405let assert Ok ( sigv4 . Credentials ( _ , _ , None )) = 406aws_profile . credentials_from_process_output ( 407"{\"Version\":1,\"AccessKeyId\":\"a\",\"SecretAccessKey\":\"b\"}" , 408) 409assert result . is_error ( aws_profile . credentials_from_process_output ( "not json" )) 410} 411 412pub fn credential_process_argv_preserves_quoted_paths_arguments_and_literal_shell_text_test () -> Nil { 413assert aws_profile . command_argv ( 414"\"/opt/my helpers/aws\"\t--name \"team account\" escaped\\ space '' '$HOME;echo'" , 415) 416== Ok ([ 417"/opt/my helpers/aws" , 418"--name" , 419"team account" , 420"escaped space" , 421"" , 422"$HOME;echo" , 423]) 424assert aws_profile . command_argv ( "helper \"foo\\bar\"" ) 425== Ok ([ "helper" , "foo\\bar" ]) 426assert result . is_error ( aws_profile . command_argv ( "helper \"unfinished" )) 427assert result . is_error ( aws_profile . command_argv ( "helper trailing\\" )) 428} 429 430pub fn endpoint_validation_rejects_credential_disclosure_and_malformed_urls_test () -> Nil { 431list . each ( 432[ 433"http://bedrock-runtime.us-east-1.amazonaws.com" , 434"https://bedrock-runtime.us-east-1.attacker.example" , 435"https://key@bedrock-runtime.us-east-1.amazonaws.com" , 436"https://bedrock-runtime.us-east-1.amazonaws.com:443" , 437"https://bedrock-runtime.us-east-1.amazonaws.com/path" , 438"https://bedrock-runtime.us-east-1.amazonaws.com?query" , 439"https://bedrock-runtime.us-east-1.amazonaws.com#fragment" , 440"https://bedrock-runtime..amazonaws.com" , 441"https://bedrock-runtime.us-east-1.amazonaws.com:bad" , 442], 443fn ( url ) { 444assert result . is_error ( wire . target ( url )) 445}, 446) 447list . each ( 448[ 449"https://bedrock-runtime.cn-north-1.amazonaws.com.cn" , 450"https://bedrock-runtime.us-gov-west-1.amazonaws.com" , 451"https://bedrock-mantle.eu-west-1.api.aws/" , 452"https://bedrock-runtime.us-iso-east-1.c2s.ic.gov" , 453], 454fn ( url ) { 455assert result . is_ok ( wire . target ( url )) 456}, 457) 458} 459 460// Scoped native fixtures exercise the real file/process boundary while restoring 461// all changed env; they never read the user's AWS credentials or call a cloud. 462pub fn aws_profile_files_helpers_and_environment_regression_test () -> Nil { 463assert profile_fixtures () 464} 465 466@external ( erlang , "albedo_bedrock_test_support" , "profile_fixtures" ) 467fn profile_fixtures () -> Bool 468 469// Known answers independently generated by botocore 1.43.108 SigV4Auth; 470// no botocore dependency or cloud credential is needed to execute this test. 471pub fn sigv4_matches_independent_botocore_vectors_test () -> Nil { 472list . each ( 473[ 474#( 475None , 476"0b40d8c99ea39128fe28e151463083ce5861037d881135c08ec4b8e364c542ac" , 477), 478#( 479Some ( "tok" ), 480"8e48825d1fe3ee6e3d9beae49a80d460d21bcc1c14a501b2cc2de307e891b53d" , 481), 482], 483fn ( vector ) { 484let headers = 485sigv4 . headers ( 486sigv4 . Credentials ( "AKIATEST" , "secret" , vector . 0 ), 487"us-east-1" , 488"bedrock" , 489runtime_host , 490epoch , 491"{}" , 492) 493assert string . ends_with ( 494header ( headers , "authorization" ), 495"Signature=" <> vector . 1 , 496) 497}, 498) 499} 500 501// Called within the native fixture's scoped temporary home and AWS env. 502pub fn reused_upstream_refreshes ( 503home : String , 504remove_credentials : fn () -> Nil , 505) -> Nil { 506let assert [ harness_extension . ModelProviderPlugin ( provider )] = 507extension . extension (). plugins 508let assert Some ( Ok ( upstream )) = 509provider . resolve ( harness_extension . ModelContext ( 510home , 511"test-session" , 512"bedrock" , 513"bedrock" , 514"not-claude" , 515types . ChatCompletions , 516None , 517)) 518// Invalid model deliberately stops before HTTP, after authentication. 519let invalid = request ( "llama" , [ types . User ( "hello" )]) 520let assert Error ( types . InvalidRequest ( first )) = 521upstream . stream ( invalid , fn ( _ ) { types . Continue }) 522assert string . contains ( first , "Claude models" ) 523remove_credentials () 524let assert Error ( types . InvalidRequest ( second )) = 525upstream . stream ( invalid , fn ( _ ) { types . Continue }) 526assert string . contains ( second , "Bedrock credentials missing" ) 527}