okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
794bda6
main
1//// Vertex's foreign wire options, raw tool schemas, malformed SSE fields, 2//// terminal-call safety and replay catch protocol regressions that the E2E 3//// OpenAI-shaped fake provider cannot exercise. These tests need neither 4//// Google credentials nor network calls; ADC tests cover shape selection only. 5 6import albedo/harness/extensions/vertex/auth 7import albedo/harness/extensions/vertex/stream 8import albedo/harness/extensions/vertex/wire 9import albedo/openai_api 10import albedo/openai_api/types 11import gleam/dynamic .{ type Dynamic } 12import gleam/dynamic/decode 13import gleam/json 14import gleam/list 15import gleam/option .{ None , Some } 16import gleam/result 17import gleam/string 18import gleam/string_tree 19 20const project = "my-project" 21 22const location = "us-central1" 23 24fn request ( model : String , input : List ( types . Input )) -> types . Request { 25types . Request ( model , None , input , [], Some ( 100 ), types . defaults ) 26} 27 28fn gemini_request ( input : List ( types . Input )) -> types . Request { 29request ( "gemini-2.5-flash" , input ) 30} 31 32fn body ( input : List ( types . Input )) -> Dynamic { 33let assert Ok ( openai_api . Exchange ( body : body , ..)) = 34wire . encode ( "token" , project , location , gemini_request ( input )) 35let assert Ok ( value ) = json . parse ( string_tree . to_string ( body ), decode . dynamic ) 36value 37} 38 39fn at ( value : Dynamic , path : List ( String ), decoder : decode . Decoder ( a)) -> a{ 40let assert Ok ( found ) = decode . run ( value , decode . at ( path , decoder )) 41found 42} 43 44pub fn encode_rejects_models_outside_the_gemini_family_test () -> Nil { 45let assert Error ( types . InvalidRequest ( message )) = 46wire . encode ( 47"token" , 48project , 49location , 50request ( "claude-opus-5@default" , [ types . User ( "hi" )]), 51) 52assert string . contains ( message , "Gemini" ) 53} 54 55pub fn encode_builds_the_project_and_location_scoped_url_and_bearer_header_test () -> Nil { 56let assert Ok ( openai_api . Exchange ( url : url , headers : headers , ..)) = 57wire . encode ( 58"my-token" , 59project , 60location , 61gemini_request ([ types . User ( "hi" )]), 62) 63assert url 64== "https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse" 65let assert Ok (#( _ , authorization )) = 66list . find ( headers , fn ( h ) { h . 0 == "authorization" }) 67assert authorization == "Bearer my-token" 68} 69 70pub fn encode_uses_the_bare_host_for_the_global_location_test () -> Nil { 71let assert Ok ( openai_api . Exchange ( url : url , ..)) = 72wire . encode ( "t" , project , "global" , gemini_request ([ types . User ( "hi" )])) 73assert string . starts_with ( url , "https://aiplatform.googleapis.com/" ) 74} 75 76pub fn tool_output_looks_up_its_calls_name_from_the_replayed_history_test () -> Nil { 77let assert Ok ( item ) = 78json . parse ( 79"{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\",\"function\":{\"name\":\"bash\",\"arguments\":\"{}\"}}]}" , 80types . replay_decoder ( types . ChatCompletions ), 81) 82let value = 83body ([ 84types . User ( "a" ), 85types . Replay ( item ), 86types . ToolOutput ( "c1" , "done" , []), 87]) 88let assert [ user , model , result ] = 89at ( value , [ "contents" ], decode . list ( decode . dynamic )) 90assert at ( user , [ "role" ], decode . string ) == "user" 91assert at ( model , [ "role" ], decode . string ) == "model" 92assert at ( result , [ "role" ], decode . string ) == "user" 93let assert [ part ] = at ( result , [ "parts" ], decode . list ( decode . dynamic )) 94assert at ( part , [ "functionResponse" , "name" ], decode . string ) == "bash" 95assert at ( part , [ "functionResponse" , "response" , "output" ], decode . string ) 96== "done" 97} 98 99pub fn tool_output_without_a_preceding_call_is_refused_test () -> Nil { 100let assert Error ( types . InvalidRequest ( message )) = 101wire . encode ( 102"t" , 103project , 104location , 105gemini_request ([ types . ToolOutput ( "missing" , "x" , [])]), 106) 107assert string . contains ( message , "no preceding call" ) 108} 109 110pub fn replayed_calls_always_carry_the_foreign_signature_placeholder_test () -> Nil { 111let assert Ok ( item ) = 112json . parse ( 113"{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\",\"function\":{\"name\":\"bash\",\"arguments\":\"{}\"}}]}" , 114types . replay_decoder ( types . ChatCompletions ), 115) 116let value = body ([ types . User ( "a" ), types . Replay ( item )]) 117let assert [ _ , model ] = at ( value , [ "contents" ], decode . list ( decode . dynamic )) 118let assert [ call ] = at ( model , [ "parts" ], decode . list ( decode . dynamic )) 119assert at ( call , [ "thoughtSignature" ], decode . string ) 120== "skip_thought_signature_validator" 121} 122 123pub fn tool_choice_modes_map_to_function_calling_config_test () -> Nil { 124let tool = types . Tool ( "bash" , "run" , json . object ([]), False ) 125let named = 126types . Request ( 127"gemini-2.5-flash" , 128None , 129[ types . User ( "hi" )], 130[ tool ], 131Some ( 100 ), 132types . Options ( 133.. types . defaults , 134tool_choice : Some ( types . NamedTool ( "bash" )), 135), 136) 137let assert Ok ( openai_api . Exchange ( body : raw , ..)) = 138wire . encode ( "t" , project , location , named ) 139let assert Ok ( value ) = json . parse ( string_tree . to_string ( raw ), decode . dynamic ) 140assert at ( 141value , 142[ "toolConfig" , "functionCallingConfig" , "mode" ], 143decode . string , 144) 145== "ANY" 146assert at ( 147value , 148[ "toolConfig" , "functionCallingConfig" , "allowedFunctionNames" ], 149decode . list ( decode . string ), 150) 151== [ "bash" ] 152} 153 154/// Feeds every chunk, then ends the body: Gemini chunks carry no explicit 155/// terminal event, so `finish` (not `feed`) settles the turn. 156fn reduce ( chunks : List ( String )) -> Result ( types . Turn , types . Error ) { 157let final = 158list . fold ( chunks , stream . reducer (), fn ( state , data ) { 159let assert Ok (#( next , _ , None )) = state . feed ( data ) 160next 161}) 162final . finish () 163} 164 165pub fn stream_decodes_text_thought_and_usage_test () -> Nil { 166let chunks = [ 167"{\"candidates\":[{\"content\":{\"role\":\"model\",\"parts\":[{\"text\":\"thinking\",\"thought\":true}]}}]}" , 168"{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"pong\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":3,\"candidatesTokenCount\":1,\"thoughtsTokenCount\":2}}" , 169] 170let assert Ok ( turn ) = reduce ( chunks ) 171assert turn . finish == types . Complete 172let assert [ item ] = turn . output 173let assert Ok ( value ) = 174json . parse ( json . to_string ( types . replay_json ( item )), decode . dynamic ) 175assert at ( value , [ "content" ], decode . string ) == "pong" 176assert at ( value , [ "reasoning_content" ], decode . string ) == "thinking" 177let assert Some ( usage ) = turn . usage 178assert usage . input_tokens == 3 179assert usage . output_tokens == 3 180assert usage . reasoning_tokens == Some ( 2 ) 181} 182 183pub fn stream_decodes_a_function_call_with_an_indexed_id_test () -> Nil { 184let chunks = [ 185"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"bash\",\"args\":{\"cmd\":\"ls\"}}}]},\"finishReason\":\"STOP\"}]}" , 186] 187let assert Ok ( turn ) = reduce ( chunks ) 188assert turn . finish == types . ToolCalls 189let assert [ call ] = turn . tool_calls 190assert call . id == "call_0" 191assert call . name == "bash" 192assert call . arguments == "{\"cmd\":\"ls\"}" 193} 194 195pub fn stream_maps_finish_reasons_to_albedo_finish_values_test () -> Nil { 196let finish = fn ( reason ) { 197let assert Ok ( turn ) = 198reduce ([ 199"{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"x\"}]},\"finishReason\":\"" 200<> reason 201<> "\"}]}" , 202]) 203turn . finish 204} 205assert finish ( "MAX_TOKENS" ) == types . LengthLimit 206assert finish ( "SAFETY" ) == types . ContentFiltered 207assert finish ( "WEIRD" ) == types . OtherFinish ( "WEIRD" ) 208} 209 210pub fn stream_errors_surface_the_provider_message_test () -> Nil { 211let assert Error ( types . ProviderError ( message )) = 212stream . reducer (). feed ( "{\"error\":{\"message\":\"quota\"}}" ) 213assert message == "quota" 214} 215 216pub fn stream_reports_blocked_prompts_test () -> Nil { 217let assert Error ( types . ProviderError ( message )) = 218stream . reducer (). feed ( "{\"promptFeedback\":{\"blockReason\":\"SAFETY\"}}" ) 219assert string . contains ( message , "SAFETY" ) 220} 221 222pub fn from_text_recognizes_a_service_account_key_test () -> Nil { 223assert auth . from_text ( 224"{\"type\":\"service_account\",\"client_email\":\"sa@example.com\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\nkey\\n-----END PRIVATE KEY-----\\n\"}" , 225) 226== Ok ( auth . ServiceAccount ( 227"sa@example.com" , 228"-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n" , 229"https://oauth2.googleapis.com/token" , 230)) 231} 232 233pub fn from_text_recognizes_a_plain_refresh_token_and_defaults_its_endpoint_test () -> Nil { 234assert auth . from_text ( 235"{\"type\":\"authorized_user\",\"client_id\":\"client-id\",\"client_secret\":\"client-secret\",\"refresh_token\":\"refresh\"}" , 236) 237== Ok ( auth . RefreshToken ( 238"client-id" , 239"client-secret" , 240"refresh" , 241"https://oauth2.googleapis.com/token" , 242False , 243)) 244} 245 246pub fn from_text_uses_basic_auth_only_when_the_file_names_its_own_token_url_test () -> Nil { 247assert auth . from_text ( 248"{\"type\":\"external_account_authorized_user\",\"client_id\":\"id\",\"client_secret\":\"secret\",\"refresh_token\":\"r\",\"token_url\":\"https://sts.example.com/token\"}" , 249) 250== Ok ( auth . RefreshToken ( 251"id" , 252"secret" , 253"r" , 254"https://sts.example.com/token" , 255True , 256)) 257} 258 259pub fn from_text_rejects_a_credential_with_neither_shape_test () -> Nil { 260let assert Error ( message ) = auth . from_text ( "{\"type\":\"authorized_user\"}" ) 261assert string . contains ( 262message , 263"neither a service-account key nor a refresh-token credential" , 264) 265} 266 267pub fn from_text_rejects_malformed_json_test () -> Nil { 268assert result . is_error ( auth . from_text ( "not json" )) 269} 270 271fn encoded ( request : types . Request ) -> Dynamic { 272let assert Ok ( openai_api . Exchange ( body : raw , ..)) = 273wire . encode ( "t" , project , location , request ) 274let assert Ok ( value ) = json . parse ( string_tree . to_string ( raw ), decode . dynamic ) 275value 276} 277 278pub fn raw_json_schema_keeps_refs_unions_and_constraints_test () -> Nil { 279// A realistic MCP tool input: named reusable definition, nullable union, 280// required properties and constraints must survive without normalization. 281let schema_text = 282"{\"type\":\"object\",\"$defs\":{\"path\":{\"type\":\"string\",\"minLength\":1}},\"properties\":{\"path\":{\"$ref\":\"#/$defs/path\"},\"limit\":{\"anyOf\":[{\"type\":\"integer\",\"minimum\":1},{\"type\":\"null\"}]}},\"required\":[\"path\"],\"additionalProperties\":false}" 283let assert Ok ( schema_value ) = json . parse ( schema_text , decode . dynamic ) 284let schema = types . encode_value ( schema_value ) 285let req = 286types . Request (.. gemini_request ([ types . User ( "list" )]), tools : [ 287types . Tool ( "list_files" , "List files" , schema , False ), 288]) 289let value = encoded ( req ) 290let assert [ tool ] = at ( value , [ "tools" ], decode . list ( decode . dynamic )) 291let assert [ declaration ] = 292at ( tool , [ "functionDeclarations" ], decode . list ( decode . dynamic )) 293assert at ( declaration , [ "parametersJsonSchema" ], decode . dynamic ) 294== schema_value 295assert result . is_error ( decode . run ( 296declaration , 297decode . at ([ "parameters" ], decode . dynamic ), 298)) 299let formatted = 300encoded ( 301types . Request ( 302.. req , 303tools : [], 304options : types . Options ( 305.. types . defaults , 306format : Some ( types . JsonSchema ( "files" , schema , True )), 307), 308), 309) 310assert at ( 311formatted , 312[ "generationConfig" , "responseJsonSchema" ], 313decode . dynamic , 314) 315== schema_value 316assert at ( formatted , [ "generationConfig" , "responseMimeType" ], decode . string ) 317== "application/json" 318} 319 320pub fn json_object_format_requests_json_mime_type_test () -> Nil { 321let value = 322encoded ( 323types . Request ( 324.. gemini_request ([ types . User ( "hi" )]), 325options : types . Options (.. types . defaults , format : Some ( types . JsonObject )), 326), 327) 328assert at ( value , [ "generationConfig" , "responseMimeType" ], decode . string ) 329== "application/json" 330} 331 332fn with_effort ( model : String , effort : String ) -> types . Request { 333types . Request ( 334.. request ( model , [ types . User ( "hi" )]), 335options : types . Options (.. types . defaults , effort : Some ( effort )), 336) 337} 338 339pub fn effort_uses_budgets_for_25_and_levels_for_3_test () -> Nil { 340list . each ( 341[ 342#( "gemini-2.5-pro" , "low" , 1024 ), 343#( "gemini-2.5-pro" , "high" , 32_768 ), 344#( "gemini-2.5-flash" , "medium" , 8192 ), 345#( "gemini-2.5-flash" , "minimal" , 0 ), 346#( "gemini-2.5-flash-lite" , "minimal" , 0 ), 347], 348fn ( case_ ) { 349let value = encoded ( with_effort ( case_ . 0 , case_ . 1 )) 350assert at ( 351value , 352[ "generationConfig" , "thinkingConfig" , "thinkingBudget" ], 353decode . int , 354) 355== case_ . 2 356assert at ( value , [ "generationConfig" , "maxOutputTokens" ], decode . int ) 357== 100 358}, 359) 360list . each ( 361[ 362#( "gemini-3-pro-preview" , "low" , "LOW" ), 363#( "gemini-3-flash-preview" , "minimal" , "MINIMAL" ), 364#( "gemini-3-flash-preview" , "medium" , "MEDIUM" ), 365#( "gemini-3.1-pro-preview" , "medium" , "MEDIUM" ), 366#( "gemini-3.1-flash-lite" , "minimal" , "MINIMAL" ), 367#( "gemini-3.5-flash" , "minimal" , "MINIMAL" ), 368#( "gemini-3.8-flash" , "medium" , "MEDIUM" ), 369], 370fn ( case_ ) { 371let value = encoded ( with_effort ( case_ . 0 , case_ . 1 )) 372assert at ( 373value , 374[ "generationConfig" , "thinkingConfig" , "thinkingLevel" ], 375decode . string , 376) 377== case_ . 2 378}, 379) 380} 381 382pub fn unsupported_effort_is_not_silently_ignored_test () -> Nil { 383list . each ( 384[ 385#( "gemini-2.0-flash" , "high" ), 386#( "gemini-2.5-pro" , "minimal" ), 387#( "gemini-3-pro-preview" , "medium" ), 388#( "gemini-3-flash-preview" , "xhigh" ), 389#( "gemini-30-pro" , "high" ), 390#( "gemini-3.1-flash-image" , "medium" ), 391#( "gemini-3.8-flash" , "minimal" ), 392], 393fn ( case_ ) { 394let assert Error ( types . InvalidRequest ( _ )) = 395wire . encode ( "t" , project , location , with_effort ( case_ . 0 , case_ . 1 )) 396}, 397) 398} 399 400pub fn malformed_present_fields_fail_instead_of_becoming_empty_output_test () -> Nil { 401list . each ( 402[ 403"null" , "[]" , "{\"candidates\":null}" , "{\"candidates\":{}}" , 404"{\"candidates\":[null]}" , "{\"candidates\":[{\"content\":null}]}" , 405"{\"candidates\":[{\"content\":{\"parts\":{}}}]}" , 406"{\"candidates\":[{\"content\":{\"parts\":[null]}}]}" , 407"{\"candidates\":[{\"content\":{\"parts\":[{\"text\":1}]}}]}" , 408"{\"candidates\":[{\"content\":{\"parts\":[{\"thought\":\"yes\"}]}}]}" , 409"{\"candidates\":[{\"finishReason\":null}]}" , 410"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":null}]}}]}" , 411"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"args\":{}}}]}}]}" , 412"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"run\",\"args\":[]}}]}}]}" , 413"{\"usageMetadata\":null}" , 414"{\"usageMetadata\":{\"thoughtsTokenCount\":\"2\"}}" , 415"{\"promptFeedback\":false}" , "{\"error\":{\"message\":12}}" , 416], 417fn ( data ) { 418let assert Error ( types . InvalidEvent ( _ )) = stream . reducer (). feed ( data ) 419}, 420) 421} 422 423pub fn eof_without_finish_reason_rejects_text_and_calls_test () -> Nil { 424list . each ( 425[ 426"{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"partial\"}]}}]}" , 427"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"run\",\"args\":{}}}]}}]}" , 428], 429fn ( data ) { 430assert reduce ([ data ]) == Error ( types . UnexpectedEnd ) 431}, 432) 433} 434 435pub fn terminal_limits_and_errors_never_publish_executable_calls_test () -> Nil { 436list . each ( 437[ 438#( "MAX_TOKENS" , types . LengthLimit ), 439#( "SAFETY" , types . ContentFiltered ), 440#( "MALFORMED_FUNCTION_CALL" , types . OtherFinish ( "MALFORMED_FUNCTION_CALL" )), 441], 442fn ( case_ ) { 443let assert Ok ( turn ) = 444reduce ([ 445"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"run\",\"args\":{}}}]}}]}" , 446"{\"candidates\":[{\"finishReason\":\"" <> case_ . 0 <> "\"}]}" , 447]) 448assert turn . finish == case_ . 1 449assert turn . tool_calls == [] 450assert turn . call_indices == [] 451}, 452) 453} 454 455pub fn streamed_parallel_calls_replay_into_second_step_test () -> Nil { 456let assert Ok ( turn ) = 457reduce ([ 458"{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"first\",\"args\":{\"path\":\"a\"}}},{\"functionCall\":{\"name\":\"second\",\"args\":{}}}]},\"finishReason\":\"STOP\"}]}" , 459"{\"usageMetadata\":{\"candidatesTokenCount\":2,\"thoughtsTokenCount\":3}}" , 460]) 461let assert [ item ] = turn . output 462let assert [ first , second ] = turn . tool_calls 463let value = 464body ([ 465types . User ( "do both" ), 466types . Replay ( item ), 467types . ToolOutput ( first . id , "one" , []), 468types . ToolOutput ( second . id , "two" , []), 469]) 470let assert [ _ , model , results ] = 471at ( value , [ "contents" ], decode . list ( decode . dynamic )) 472let assert [ a , b ] = at ( model , [ "parts" ], decode . list ( decode . dynamic )) 473assert at ( a , [ "functionCall" , "name" ], decode . string ) == "first" 474assert at ( b , [ "functionCall" , "name" ], decode . string ) == "second" 475assert at ( a , [ "functionCall" , "args" , "path" ], decode . string ) == "a" 476assert at ( b , [ "thoughtSignature" ], decode . string ) 477== "skip_thought_signature_validator" 478let assert [ a , b ] = at ( results , [ "parts" ], decode . list ( decode . dynamic )) 479assert at ( a , [ "functionResponse" , "name" ], decode . string ) == "first" 480assert at ( b , [ "functionResponse" , "response" , "output" ], decode . string ) 481== "two" 482let assert Some ( usage ) = turn . usage 483assert usage . output_tokens == 5 484}