diff --git a/R/provider-openai.R b/R/provider-openai.R index d6e9003c..cadb6953 100644 --- a/R/provider-openai.R +++ b/R/provider-openai.R @@ -49,28 +49,17 @@ chat_openai <- function( api_key = openai_key(), model = NULL, params = NULL, - seed = lifecycle::deprecated(), api_args = list(), echo = c("none", "output", "all") ) { model <- set_default(model, "gpt-4.1") echo <- check_echo(echo) - params <- params %||% params() - if (lifecycle::is_present(seed) && !is.null(seed)) { - lifecycle::deprecate_warn( - when = "0.2.0", - what = "chat_openai(seed)", - with = "chat_openai(params)" - ) - params$seed <- seed - } - provider <- ProviderOpenAI( name = "OpenAI", base_url = base_url, model = model, - params = params, + params = params %||% params(), extra_args = api_args, api_key = api_key ) @@ -84,7 +73,6 @@ chat_openai_test <- function( echo = "none" ) { params <- params %||% params() - params$seed <- params$seed %||% 1014 params$temperature <- params$temperature %||% 0 chat_openai( @@ -145,10 +133,10 @@ method(base_request_error, ProviderOpenAI) <- function(provider, req) { # Chat endpoint ---------------------------------------------------------------- method(chat_path, ProviderOpenAI) <- function(provider) { - "/chat/completions" + "/responses" } -# https://platform.openai.com/docs/api-reference/chat/create +# https://platform.openai.com/docs/api-reference/responses method(chat_body, ProviderOpenAI) <- function( provider, stream = TRUE, @@ -156,33 +144,41 @@ method(chat_body, ProviderOpenAI) <- function( tools = list(), type = NULL ) { - messages <- compact(unlist(as_json(provider, turns), recursive = FALSE)) + input <- compact(unlist(as_json(provider, turns), recursive = FALSE)) tools <- as_json(provider, unname(tools)) if (!is.null(type)) { - response_format <- list( - type = "json_schema", - json_schema = list( + # https://platform.openai.com/docs/api-reference/responses/create#responses-create-text + text <- list( + format = list( + type = "json_schema", name = "structured_data", schema = as_json(provider, type), strict = TRUE ) ) } else { - response_format <- NULL + text <- NULL } + # https://platform.openai.com/docs/api-reference/responses/create#responses-create-include params <- chat_params(provider, provider@params) - params$seed <- params$seed %||% provider@seed + if (isTRUE(params$log_probs)) { + include <- list("message.output_text.logprobs") + } else { + include <- NULL + } + params$log_probs <- NULL compact(list2( - messages = messages, + input = input, + include = include, model = provider@model, !!!params, stream = stream, - stream_options = if (stream) list(include_usage = TRUE), tools = tools, - response_format = response_format + text = text, + store = FALSE )) } @@ -194,11 +190,8 @@ method(chat_params, ProviderOpenAI) <- function(provider, params) { temperature = "temperature", top_p = "top_p", frequency_penalty = "frequency_penalty", - presence_penalty = "presence_penalty", - seed = "seed", - max_tokens = "max_completion_tokens", - logprobs = "log_probs", - stop = "stop_sequences" + max_tokens = "max_output_tokens", + log_probs = "log_probs" ) ) } @@ -213,10 +206,9 @@ method(stream_parse, ProviderOpenAI) <- function(provider, event) { jsonlite::parse_json(event$data) } method(stream_text, ProviderOpenAI) <- function(provider, event) { - if (length(event$choices) == 0) { - NULL - } else { - event$choices[[1]]$delta[["content"]] + # https://platform.openai.com/docs/api-reference/responses-streaming/response/output_text/delta + if (event$type == "response.output_text.delta") { + event$delta } } method(stream_merge_chunks, ProviderOpenAI) <- function( @@ -224,56 +216,43 @@ method(stream_merge_chunks, ProviderOpenAI) <- function( result, chunk ) { - if (is.null(result)) { - chunk - } else { - merge_dicts(result, chunk) + # https://platform.openai.com/docs/api-reference/responses-streaming/response/completed + if (chunk$type == "response.completed") { + chunk$response } } + method(value_turn, ProviderOpenAI) <- function( provider, result, has_type = FALSE ) { - if (has_name(result$choices[[1]], "delta")) { - # streaming - message <- result$choices[[1]]$delta - } else { - message <- result$choices[[1]]$message - } - - if (has_type) { - if (is_string(message$content)) { - json <- jsonlite::parse_json(message$content[[1]]) + contents <- lapply(result$output, function(output) { + if (output$type == "message") { + if (has_type) { + ContentJson(jsonlite::parse_json(output$content[[1]]$text)) + } else { + ContentText(output$content[[1]]$text) + } + } else if (output$type == "function_call") { + arguments <- jsonlite::parse_json(output$arguments) + ContentToolRequest(output$id, output$name, arguments) } else { - json <- message$content - } - content <- list(ContentJson(json)) - } else { - content <- lapply(message$content, as_content) - } - if (has_name(message, "tool_calls")) { - calls <- lapply(message$tool_calls, function(call) { - name <- call$`function`$name - # TODO: record parsing error - args <- tryCatch( - jsonlite::parse_json(call$`function`$arguments), - error = function(cnd) list() + browser() + cli::cli_abort( + "Unknown content type {.str {content$type}}.", + .internal = TRUE ) - ContentToolRequest(name = name, arguments = args, id = call$id) - }) - content <- c(content, calls) - } + } + }) + + # cached_tokens <- result$usage$input_token_details$cached_tokens tokens <- tokens_log( provider, - input = result$usage$prompt_tokens, - output = result$usage$completion_tokens - ) - assistant_turn( - content, - json = result, - tokens = tokens + input = result$usage$input_tokens, + output = result$usage$output_tokens ) + assistant_turn(contents = contents, json = result, tokens = tokens) } # ellmer -> OpenAI -------------------------------------------------------------- @@ -284,51 +263,42 @@ method(as_json, list(ProviderOpenAI, Turn)) <- function(provider, x) { list(role = "system", content = x@contents[[1]]@text) ) } else if (x@role == "user") { - # Each tool result needs to go in its own message with role "tool" - is_tool <- map_lgl(x@contents, S7_inherits, ContentToolResult) - content <- as_json(provider, x@contents[!is_tool]) - if (length(content) > 0) { - user <- list(list(role = "user", content = content)) - } else { - user <- list() - } - - tools <- lapply(x@contents[is_tool], function(tool) { - list( - role = "tool", - content = tool_string(tool), - tool_call_id = tool@request@id - ) + lapply(x@contents, function(x) { + if (S7_inherits(x, ContentText)) { + list(role = "user", content = x@text) + } else { + as_json(provider, x) + } }) - - c(user, tools) } else if (x@role == "assistant") { - # Tool requests come out of content and go into own argument - is_tool <- map_lgl(x@contents, is_tool_request) - content <- as_json(provider, x@contents[!is_tool]) - tool_calls <- as_json(provider, x@contents[is_tool]) - - list( - compact(list( - role = "assistant", - content = content, - tool_calls = tool_calls - )) - ) + as_json(provider, x@contents) } else { cli::cli_abort("Unknown role {x@role}", .internal = TRUE) } } method(as_json, list(ProviderOpenAI, ContentText)) <- function(provider, x) { - list(type = "text", text = x@text) + # OpenAI uses a different format dependening on whether the text is provided + # by the user or generated by the assistant. Since ellmer content types don't + # distinguish, this method generates the assistant content and we special + # case the + list( + role = "assistant", + content = x@text + ) } method(as_json, list(ProviderOpenAI, ContentImageRemote)) <- function( provider, x ) { - list(type = "image_url", image_url = list(url = x@url)) + list( + type = "message", + role = "user", + content = list( + list(type = "input_image", image_url = x@url) + ) + ) } method(as_json, list(ProviderOpenAI, ContentImageInline)) <- function( @@ -336,9 +306,13 @@ method(as_json, list(ProviderOpenAI, ContentImageInline)) <- function( x ) { list( - type = "image_url", - image_url = list( - url = paste0("data:", x@type, ";base64,", x@data) + type = "message", + role = "user", + content = list( + list( + type = "input_image", + image_url = paste0("data:", x@type, ";base64,", x@data) + ) ) ) } @@ -347,23 +321,32 @@ method(as_json, list(ProviderOpenAI, ContentToolRequest)) <- function( provider, x ) { - json_args <- jsonlite::toJSON(x@arguments) list( - id = x@id, - `function` = list(name = x@name, arguments = json_args), - type = "function" + type = "function_call", + call_id = x@id, + name = x@name, + arguments = jsonlite::toJSON(x@arguments) + ) +} + +method(as_json, list(ProviderOpenAI, ContentToolResult)) <- function( + provider, + x +) { + list( + type = "function_call_output", + call_id = x@request@id, + output = tool_string(x) ) } method(as_json, list(ProviderOpenAI, ToolDef)) <- function(provider, x) { list( type = "function", - "function" = compact(list( - name = x@name, - description = x@description, - strict = TRUE, - parameters = as_json(provider, x@arguments) - )) + name = x@name, + description = x@description, + strict = TRUE, + parameters = as_json(provider, x@arguments) ) } @@ -422,7 +405,7 @@ method(batch_submit, ProviderOpenAI) <- function( list( custom_id = paste0("chat-", i), method = "POST", - url = "/v1/chat/completions", + url = "/v1/responses", body = body ) }) diff --git a/R/provider.R b/R/provider.R index f9b3a92f..d00f6cf9 100644 --- a/R/provider.R +++ b/R/provider.R @@ -137,6 +137,8 @@ stream_parse <- new_generic( S7_dispatch() } ) + +# Extract text that should be printed to the console stream_text <- new_generic( "stream_text", "provider", diff --git a/man/chat_openai.Rd b/man/chat_openai.Rd index 9c350270..84bfd64f 100644 --- a/man/chat_openai.Rd +++ b/man/chat_openai.Rd @@ -10,8 +10,7 @@ chat_openai( base_url = "https://api.openai.com/v1", api_key = openai_key(), model = NULL, - params = NULL, - seed = lifecycle::deprecated(), + params = params(), api_args = list(), echo = c("none", "output", "all") ) @@ -35,9 +34,6 @@ Use \code{models_openai()} to see all options.} \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} -\item{seed}{Optional integer seed that ChatGPT uses to try and make output -more reproducible.} - \item{api_args}{Named list of arbitrary extra arguments appended to the body of every chat API call. Combined with the body object generated by ellmer with \code{\link[=modifyList]{modifyList()}}.} @@ -51,6 +47,9 @@ when running at the console). } Note this only affects the \code{chat()} method.} + +\item{seed}{Optional integer seed that ChatGPT uses to try and make output +more reproducible.} } \value{ A \link{Chat} object. diff --git a/tests/testthat/_snaps/provider-openai.md b/tests/testthat/_snaps/provider-openai.md index 78b3be54..67b0cc15 100644 --- a/tests/testthat/_snaps/provider-openai.md +++ b/tests/testthat/_snaps/provider-openai.md @@ -13,12 +13,3 @@ Error in `method(as_json, list(ellmer::ProviderOpenAI, ellmer::TypeObject))`: ! `.additional_properties` not supported for OpenAI. -# seed is deprecated, but still honored - - Code - chat <- chat_openai_test(seed = 1) - Condition - Warning: - The `seed` argument of `chat_openai()` is deprecated as of ellmer 0.2.0. - i Please use the `params` argument instead. - diff --git a/tests/testthat/_vcr/openai-v2-image.yml b/tests/testthat/_vcr/openai-v2-image.yml new file mode 100644 index 00000000..4f4223c8 --- /dev/null +++ b/tests/testthat/_vcr/openai-v2-image.yml @@ -0,0 +1,192 @@ +http_interactions: +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Be terse."},{"role":"user","content":"What''s + in this image? (Be sure to mention the outside shape)"},{"type":"message","role":"user","content":[{"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAFaCAYAAACkBmCpAABoaElEQVR42u29eXgc5ZXv/+1N3eqW5DYotsxYtiRu1B4vCOFcJEd5IssLHhIvkAFjQy4BY5uZ30OCFwLJJIMlJslMCN4Ic28w2DEkgw0mEy/MDLK8SMx4US7GNl4uMliSLWFLij1udatbvdfvj+oq9VJdW1d3V7fe7/MkWOqqd6nq+ui8p857jg5ERArLWnl/men2r2423f5VjefmF59lejxEuSNNpgdAlDuyVt5vBbAGwLMArOFftwJYa7/0H2cyPT6i7BcBFpEislbe/wCAzQDKEhyyEzS47JkeK1H2igCLKClZK++/GzSoZos43A6gyX7pP7ZketxE2SkCLCJZCi//NgN4Qsbp3QCetF/6j9ZMz4Mou0SARSRZ1sr7GxHtp5KrVtDg6s70nIiyQwRYRKJlrbx/NoDfIrGfSq6aAGwh/i0iIRFgEQnKWnl/GWhQzU5hN3bQTvmdmZ4vkXpFgEWUUGE/1QbQoQrp0hnQ4GrN9PyJ1CcCLCJOWSvvXwMaVtYMDWEvaHB1Z/paEKlHBFhEUQr7qTYDuDvTYwG9TNwK4t8iCosAiwgA66faDOCBTI+FQ92g47d2ZnogRJkVAdYoV8R2mg2ZHosItYIGV2umB0KUGRFgjWJZK+9/ArRVZc30WCRqJ8g2n1EpAqxRqLCfagNSG6aQatkBbLVf+o/GTA+EKH0iwBpFCvupNkDedhq1qhu0tbU30wMhSr0IsEaJFNxOo1a1gqSxyXkRYOW4RKR9yTVtAe2Yt2d6IETKiwArRyUx7UuuyQ6SxiYnRYCVY0oy7UuuqRskjU1OiQArh6SC7TRqVStIGpucEAFWDiiFaV9yTSSNTZaLACuLlaa0L7kmO0gam6wVAVYWKkNpX3JNZ0DS2GSdCLCyTFm8nUat2gn6jWJ3pgdCJCwCrCyRytK+5JrsIGlsskIEWCqXytO+5Jq6QdLYqFoEWCpVgirKROlRK8g2H1WKAEuFCvupNoCEKWRQFADspCisHfz8Q3umR0NEiwBLRcqRtC85IIr+f/o/dgBbBz//sDHToyIiwFKFyHYaNSkKVpHqBrB28PMP92Z6hKNZBFgZ1ihI+5JFSgirSLUCeHLw8w+7Mz3a0SgCrAxpFKZ9UblEwSpSWwA0Ef9WekWAlWaN8rQvKpVkWDGyg4bWlkzPYLSIACtNIttp1CrZsIrUGdD+rdZMzybXRYCVBpG0L2qVIrCK1F6AWjv4eXN3pmeWqyLASqFI2hc1S3FYsW0inMZm8PNme6ZnmWsiwEqByHYatSulsGJkB7B28PPmnZmebS6JAEtBZVkV5VGqtMAqUq0UhSbHF82tmZ55LogASyGRtC/ZoLTDKrKvnQCaHF8Q/1YyIsBKUiTtS7Yoo7BiZEc4jY3jC+LfkiMCLJnK0SrKOSpVwCpS3QDWOr5o3pvJq5KNIsCSKJL2JdukOlhFqhU0uM6k+6pkqwiwJIikfck2qRpWkWftBLDW8cVBe1ouSxaLAEuEyHaabFTWwIr5hx1Ak+OLg1tSfGGyWgRYPCJpX7JVWQerSHUDeNLxxcHWFFyYrBcBVgKRtC/ZqqyGVaRaQYOrW8mZZLsIsGJEttNkq0YAkAOwitQW0EtFu5KzylYRYIVFqihns3IWVozsFIW1zssHdyo5u2zUqAcWSfuS7cp5WEX2dQbAWufl0evfGtXAImlfsl2jClaR2gsaXN1KzjobNCqBRbbT5IJGLawij2gCsMV5ucWu5BVQs0YVsEjal1wRgVVEu90AmpyXW3YqeSXUqlEBLJL2JZdEYJWg3VbQ4GpV8qqoTTkPLJL2JZdEYCWi3Z0A1ubqMjFngUWqKOeaRjusJLVpB7DVebmlMYkLo0rlHLBI2pdcFIGVTHWDtrb2ym1AbcopYJHtNLkoAisF1AoaXGeUaCyTyglgkSrKuSoCK8VmTDe1BUDTUGf2+reyGlgk7Ut2auKEcZh4xzhcvNQFh9OV4CgCK8VmHNUUZQfQNNR5aItiHaRRWQkskvYlMyoqtKCowILe6wOSz62dOR0PfbsB99Xfi6JCCxxOF7713fUJ2iKwUmzG0bCK/KEbwJNDnYdaFessDdJnegBSNRq200ycMA731d+LqZXlmHjHODicLhxsa8fBtj/xWCTKq6jQgvvq78V99TWYWlmGiRPG4f0PjuK5l34tuo2pleV4cd0K1N4zLer3O3Z/QGClYJvC/cS1WwbgaEHFvFbQ4OpWrOMUKmssrNGQ9mXihHFYs+oRPLSwgfPzi5e6sOxvX0wLtFYsW4g1qx5BUaGF/V3v9QF867vrRfd/X/29eOXF70e1wbTzjSV/w3EGgZViM+aHFZeaAGwZ6jxkV2wQKZAu0wMQkrXy/jLT7V/9I4BG5LBV9dDCBmz71Y9QPaMy4TFfuX0svD4/Tn5yIaVjeeXF7+Nvv/cdGI15Ub9/afMOnD5/SVQbUyvLse1XP4qDFdPOxUvdMb8lsFJsxtJhBdB+4L/JG1vR77vVeUaxwSgsbaYHkEjWyvut1sr7NwPoQo471V958fuclsjFS1147qVfR1k0tTOnp3wsXBZe7/UBvP/BUdHtbPvVC5ywope3f4r5LYGVYjOWBytGVgC/LaiYd7qgYt5sxQaloFTpwxpN22kSLQEjl38PLZzD+oBifUFK6qGFDQmXozt2fSCpnYkTxnF+Fu+Hyx5YTa0swzRbOaZWVmBaZTmKCi2YWlkedxzz9vPCpS5c7OjE8VPn0Xst0YsK1cAqUneD9m/tBP1GsVuxQSYpVQFrtKV9eWhhA9aseiTu9w6nC8+99FpaHewAOMfCjOf9fxNvXSVqB6Cd7SNSP6wWzK7BfbNrMGvm9IQQjhUDsUhr+MKlLmx/Zz/2HDgiepySZqwcrCL1BIAHCirmbQWoLUOdh+2KDVimVAGs0Zj2ZWplOV5cu4Lzsx27P8DFS10jx361jP13qiAmzSpKrFqeB/vipa6IeakXVhPvGIeHF87Bw4vmiIaUkKZVlmNT47N46tHFWL9hKy5c6lRuxqmBFSMrQG0A8L2CirlNQ52HdyrdgRRlFFijuYryKy8+w+nj6b0+gC1vvMv+PDW89GB08fPulIxnxbKFCT872NYuup2Hvt2Q8LMRH5g6YVVUaMGG9U/h4YVzEh7Te30A29/Zj+a29qhlXlGhBbNmTsd9s2t5z59WWY73tv0cjRvfxJ4Dh5OfcWphFdlmGUXht5byud8DsNbVdfhMCjoTVMaANZqrKK9Z9Qin7wNAFKyAeJ9VpOWllKZWliccD7eTnFtFhZaEPjAA4XbUCasFs2uwccMPOP+IMNdh87Zd2L7rQMLPm1vb0dzaju3/sh/vbft5wraKCi3Y1Pgseq8N4MSpc/JnnD5YRfY1G8BpS/ncnaDBZU9BxwmVdmCN9rQvTKwVl7jexMW+FTx56rziY+KzrqT4rvisq97rA5h4x1cw8Y6vABT/4+VwuiSCOTlYbVj/FJ5avijh5xcvdWHlc7/gcZxH93XhUheWrv4JL7QA4M2Nf4dZi1bKWuZnCFaRegLAA5byuVtdXYcbUzAATqUtcJRsp6G1+zf/kPBN35Y33o2ysIoKLfj08O+ijrlr7v9S1I9VVGjBf+39TcIH6/0PjqL3+kDU0nTqV8vw0uYdeP+Do5haWY6HFjag9p5pCa00qTrY1o5Vz/2TyKOTg9XGxh/wLuEuXurC0qd/Iuqax/a17unlWLt6Ge85ew4cxrrGrZKujwpgFatuAE+6ug63pmAwUUqLhUXSvtCqnTmdNywh+g0aHSkeKaW35kytLGf39iUS1xLv5CcXcLDtT9j2qx/FjTFZ9V4fwPomsVt/koPVw4vmpAxWAPDmO/sFgbVgdi0A8cBSIayA8DYfS/ncVtDg6k7BwACkGFgk7Uu0+F73v//B0bgH46GYh0mK8ztWzL5Axl+VTDzXljfexe7/85JiFhUjh9OFVc/9k0goJweriXeMw4Z1T/GOJRlYMW2cOHUes3iCfWln/QxRviyVwipSswF0WcrnbgHQlAr/VkqARaoox0vIuor1FU2cMC7qeIfTJSnSPLLfNaseUSzgdMsb7+LFtU8qDisAaNq0Q6TvKvnQhbWrl/FaluubtiYFK2acFzs6eYEFALO+Nl0QWFkAq0itAfCEpXxOk6vryBYlR6kosEgV5cQSckjHOtNjrbHY5aIYJdpmI1cH2/6EiRPGpQRWW954F+9/cETEkcnDqqjQwrsUPHnqPJpbha1ZMdttHEPJL+GzDFZMS1YAmy3lc8JhEEdalWhVsb2E4bQvXSCwitPECeN4wRG77YVZvkVKqnWlNKx6rw/gYFu7om2OzO0INm/bLeJIZSLYF8yu4f1887ZdwiMRuTewqMAi2Jb4frIGVpE/3A3gqKV8zh8t5XPKkm05aWBZK++fba28vwujZO+fHAk95LFxTiuWLYxarjBv6sTqxXUrFAWLw+nC6h/+ktcHJ1fvf3BEpJNdue02fNHrvdcHcEIgdETKRuaptgrZ1yYHYBWpBwB0mcvmNJrL5ljlti57STgat9PIFR88Tn5yIQpGRYWWuLio2GBSPtXOnM4bVyVHz730axQVWjBxAp3W+OQnF+BwutB7fSBqY3asHE4XVv1wJDxhamU5xkSAeNDpwo4EgZjRUnYjM9+Slm8pKLVmIBP9Lkc5BqvIfjYAeNZcNmetu/vITqk9SAYWqaIsTffV38v7Fz3Wb5OsdfXKi88kPeYduz/A+x8cjXOAl937naifiwoteOXF7yds52Bbe5RvTl7Qq/JZF8bwONsTOdrlFDilQxaEdeLj6OuSw7BiZAXwW3PZnO8BaHJ3i/dvSQLWaEr7opTuq+f3l0QuB5O1rrg2MDucLlz8vJtNeXLykwtYsWwhb/zUjl0fiIIk34sEAGhuFbelJ7FSk8+q5/oAEqHkJMfbOrnVmNetXi5qRJFvCEcBrCI1G8Bsc9mcnaDB1S3Ukyhgjba0L0pJzN66yL/oyVpXEyeMY/f+HWxrZ5dusdr28gsJ27h4qUt0n3xzY/LQy1fqku9d7OgEErwlrJ05I8qHJRtWTy/HxDuEMz00t55M0FfOwypSTwB4wFw2ZyuALe7uI/ZEB/ICi1RRTk5CUeCRD/TECeOSsq4AGnBC5zy0sIE3/khs+ATfhunYuUlXajOF7vngCNauXs55HSJ/JxdW0yrL8dTyxaJGxfjMRjGsGFkBbACo75nLGta6u4/u5TqI8y1hOD1xI4DTILCSLSnLwdiCD1veeFdyOS1Ryzie+KPYMfFJyLEvfzmY+rTGTBR75PWiszHsZkMa5MKqqNCCjU1reP8oMOq9NoA9Bw4TWEW3WQbgj+ayhqPmsoa7Y4+Is7BGc9oXJcUVSxWpyOVg7czpUcsrh9MlK1BUSLHR83xj4p+bmXdu8peD6cvBfvFSF76+aJXMfhIf8ObGn2CayMDaTdt2EVglbnM2gNPmsoadANa6u4/agQgLy1p5/93WyvuPIsdLaaVLUpaDsfFNL23ekZLMoiuW81tF4iBD4b76Gl4LQlzEeny7vJ+mqWBEMrDa1Pis6DCGE6fO4739kQn8CKwS6AkAXeayhjUAoCdpX1IjsctBJjULo4uXumTtGRQjobd6wstB+osltBzcI3n8uQGrhxfNFdWPw+nCug1bkhqrlPlnMawYWQFsNpc1PKtFeKOiUlMiEr8cLCq0xOV1f+6l11IyJqE0MsLLQfqLJeRs770+kNbke3LalNePMrACgKfW/wI9bDJAAisJKlNtXcJsltBykAmgjHW0xxafUFLCzna+5eDIF0vQShOxaZirXc5PcxBW6zZsxYmPmbgrAiupIsBKgWrv4fdjHGz7E6ZWlkctrWKLTyipiRPGiXCSJ1oORj8AQuATvxxUx0OQbli9xxaeILCSfBalkjJfuSY+ODCBmf/+q41Rv39pU2oc7YC4zdfcfcfDim9ZKX45qI6HIJnQhU2Nz4reegMQWIltU6gvAiyFFVuWK1YnP7kQVzWHjkz/E3v+Qwsb4rKCXrzUhS1vvCs6TipSwsDiWsbFPwAPC7UjajnI/YV9aCFdA/DkqfOC2RLEtsl7RhKwem/bL0SHLjicLjy1/hdkGZjE3CP7IsBSWGL8V5Ebhukqz79G7czpeOXFZxJulJ5aWY5tv/oRnnvp15LeIgptvuZeDsY/ABMnjIur4BMr4eVg/Be2duZ0bNzwA3aMza3tEoGVPlhNqyzHmxt/ImrLDQBc6OjCyvU/Jw72JOYe2xcBlsISAlaso/2lzTuw7Vc/Ep3C+MW1KyQVo5ASbU+L+wF4eBG/7yq6qjOX4r+wDy2cg40bfhAzHuWc9pxnyITVgtm12NT4rKgIdgDY9PoubHp9l2C7yWl0wQogwFJURYUWwfTBsZ+/uHaF6Icgsg8xqVqENl8DsYBI/AAItcNvXYmDVe/1Aew5cIQd+7Tw8pq5ZrH5tOh+D7PnCCmZjcxrRWZeuHCpC+s2bMGFji7BdpPT6IMVQIClqOQUepACq8h+xABLKAQhejmY+AG4r76Gd1kJ8PmvxMGK0YfvbJaUM7525nScOHVesMipHFhJca47nC5s2rYL29/ZT7bbJDl3vr5IWIOCElp+CSlROhi5Ep+amf8BWDCbf5mbOCVNfGNTK8sTlteSW+BCKDOCHFhNqyzHe9t+IQpW29/Zj1mLVhJYSWhTbl/EwlJQtTPlldLasfsDbHnjXTicLjy0sIE3iycgLiuDUEQ6ALz/b0cg9ADQy0o5sVfc37yNG34gy6rkE98bOzmwenjRXDSuXyk4zj0HDmPTtl2sdUdgldzcxdwrAiyFNHHCOMFlU6wuXurCcy+9FuWsFmNhXbzULXiM0H6/2NJiib4sQrACuJaD3I2tXb0sJSXCErUpJwd74/qVgsGgsaCK74vASvJZIu8VAZZCkmpdnfzkAlb/ML7KsdAD7XC6RAVnSskWwfdlEYq9il8OcjdGJyhcJOkaJSOpsJpWWY6NTWt4rTUuUMX3RWAl+SwJ94oASyFJ8V+d/OQClv3N33N+JgQsMYGjQllFgZE6h3xfFjHLyu1RNRUTN7Zi+SLFl4KMmmNCIaTCSihk4cSp89j8+i7O6swEVsnNXeq9IsBSSGLfEF681IXVEaWvpLZz8hPht4NC8GS20Ah9WYSsKyDSUkvcGEWBt9JyMqIzhY7EO0l9APhCFvhAFd8XgZXks2T4FwmwFJDQdhxGdFT7awn9VGLaEbKwhDY6A7TPScwXU0yGB3ou/LASe32kik11zOn05hwN+y++kAWH04XGjW9iz4HDiVsisEpq7nJj4giwFJBY60oofYxQGIKYsAchWAHiMioIZRUFmAIK/LAC+OsAcunipS70XBsIR893YsP6lZwvNJo2vcleTykPwMQ7xiVMZbz9nf3YtG0X73UmsEpu7slkyCDAUkBCe+wAceljxFhGQhKCnvAWGlpCsVcA/zaayC9lj0AYxvZdB3Dy1PkwpDqjPtvY+CwnrNY3bWUj3KU8AEx8VSyMe68NYF3j1oTLP655EVjJOCsJWAEEWIpIjIUlBCsxYREnP7nA+7kYJ7kYpz2dMVVoD2K76CrJvdcGsOeDI5x+rD0fHEHTxu3MmVGfPbxoDuc5cmGVKL5KjFUV3xeBleSzkoQVQICVtCZOGCe4dOq9PiCYYUHIuhKTa0rIugJG6uDxj0V4OZgoo0KiL+X6xldx8tR5bFj3FNs2U1orfGbU8XRE/Mq4djZv2y0bVpsan4361OF0YeX6XwhaVfF9KQerokILli6ai1lfm4ETH5/Dm+/sI7DiEQFWkhITfyUmHYzQsvLkqQuCbSgBPQB4SqC6DsC9PBX6UvZeG4gC4eZtu8MO8/gATq4wg5OnzsuqG8gFqxOnzmPl+p+LCtRNFazWPf0oVj66mJ3ngtm1OP7xOVzo6EyyZeFxZiOsAAKspCWUDhkQByzBPFoC4QwPLWwQXlKK2DBdO3O64LKy9/pA3PYgoS9lUaEFGxtHNjw3t7Zj+64D4Pqyrl29PG4MDqcLK5/7hai+Yn1WjeujLbU9Bw5jXeNWwWsR35cysCoqtGD7pp9g1swZcZ/9VUOtAsDKTVgBBFhJS8jCSrwxOLINYegJ+Z7EbKERU415bUyNRC7Fgk/MA/DmKz9mgXrxUhfWN70Kri/rrJnT8dTy+Ij4zWEfk9TQhVgHe6ZhNc1WgT0cTn/llLuwAki2hqQkxlEuajl4jzD0+JYusemUE0nISqudOV0UPE+I2IMYqY2NP2DbdThdWNf4KhzOoQTHPhv3u97rA9i+64DkB+DNjT9RFayWLporCKvBpLJ15DasAGJhJaWplWWCx4h5KyfovxJ4Oyi00RmgrSIhf40Y64ppCxAPK+ZNHx3o+dO40AW2/9XLueOtNr4p6gF4eNFcduNyc+vJuCrMvdcHsO7p5Rh0unCxowsXLnVyXpNUwKrxuVVY+ehiwePkLwdzH1YAAVZSEgKNmOUgAEz9aplgO4k0ccI4UW8HhZztYq0rgH7wlYZVUaGFcynYe30AHx7lf7NJb1x+NioQlCsolGsLzoVLXTj58TnsOXAYF+K2KyUPq2m2CmxqfBbTbBVJt5VYowNWAAFWUhJaholZDooJi+B7Q7hGpFV0gSclTVGhBRs3fF9UOydPnRflYH/v9Z+xjnMhWAH0XkOu6yCU/njd08viQNR7bUB0oYhpleWYVlmOpx5djN5rA3jvwGHsOXAYPdf6RZ3Pdw1WProE654Wl1o5cuzSNHpgBRBgyZaY/O1CSzlA3LIykZU2ccJXRFlXfG0A9FJQai4vvusSCauLl7qw9OmfJvRZMXoqwXJppDxWtGbNnI5Njc/GgWld49a4t4JiNfGOcVj39HKse3o5Tpw6hz37D+PD1pOSs8AuXTQX655+VDQ0I8WAcnyIQq2Xwgw/hYoghXHB+IfdpQHOGTQ4Z9DgpFGL/hiPdK7BCiDAki0h60ps3ir50KPwikiriE/31ddgxXJl8lRNrSzHe6//jLWUxMJqamV5QmDO+tqMKCf/SJK9+Lei6xq3wuF0KfIGbtbMGZg1cwY2gfaHNR89iQuXuhL6mErvGIdZM2fIBhVA+69qfSEsGQ5hhl/4AbdQQK2PQq2PwipXCOcMGrxj1uKcQZOTsCKVn5OQoP/q825R7cjLwEnhvvoaUTFg7Hg44Dm1sjxuKdh7fUCytcX4n9auXhbVnxhYAeDNm7529TIUFVhw4tQ5zJo5Aw8v4l46rmvcSifY43jLyOjCpS6MKbBIBsqC2bVRY4yNjC+dMF42pBgNnfgUuqbf4KeOIOfnwWIrAqXjEZxUguDtVgSLrQAA/dU+GDq6YTzdgRl+Cv84GMQ5gwb/kk+DS7zUDyuAAEu2BPNWiSwGKmQNxIOGwtTK8rhirELtTJwwDhedI21NrSzHu7/5h6jzmjZtx5hCC9asWpawHSaw9OKlLiyYXYP7ZtdgQcxWHimwAiD4sD/16CI89WhiK5CBFQBMTeDcjgxpmDVzBtY+vTzuLaJYcQV8JqPext/gz9v/GPW7YLEVvmob/LYy+GyTQZlNnOf6bZMxPL8Guht2WHY3s+D6J38Q+0xabLOIiVzKDlgBBFiypJT/CqD9ULx9FUSCiEJRoQWvvPj9GNDsEHSaM5BhrKFYZ/37HxzBjl0HoqykRPrwnc0JP5MKKwAoTcJ/FgkrgPvtIB37NRJ/deLUORxfdQ6zvjYD655epjiA+HShoxOld4xHUaEFQccQPl/6PIYvXAYwAilPXRUCpSWS2g0WW+F45hEYT3egcMc+aNweLPGEUBGk8A+FOrgSGlvZAytShEKmxARpivFfARBcfo045alwOfvvR52z5Y13RYVObNzwfTy8sIEzmd7FS11o2rRdsA0xc5YKq2QUC6tEam49GfUz8xCc+PgcHv74U8z62gxsblyT9LJOSG/+yz5cuNSFzU1romAVLLbCvbgenroq2W3re/pg3tcG17IFsD//OKwvvw2N20NbW44gflTEBa3sghVALCxZEvJf9V4fUKy+4NTKcvz77zeiqNASBzcmAjzaCpM2bofThVXP/WPS4z156jxWPvePsmB14tR50TFgzJjXNW6NA1EiXYj448H1EJz4+BxqFz4VtxlZKTmcLqzdsBkfHj2Jk/+2AwBYWAVKS2B//vG4JZ/G7UFexxUESsezv9PeHARlNiJ4uxVjm7bBvbgegUn054HSEhhPd8B4ugOeuio4VyxB0Wt0SqOKAIXVrhA2F0QuD7MPVgABliwJ+6/ELQfFKtHys2njDjicLtmwcThdeORvfhploUWXWBenPR8cwfpG7r2BQqIoSIp5cjhdWLr676IgFPt5LHAYsAkFhW56/R3sOXAIjc+tElVAVYw+PHoSazdspse9eB5K7xiH3sbf8MIKACz72wAA5n1tCEwaD9Oxs+xnwWIrdDfsKNyxD5TZBI3bE3Wu6dhZGDquwFttg/F0BwBgnjeEk3kanMjTyLpP8deP8whFrhnffSJ7CSVKjP9K7HIwGR1sa4/K+CkVWgysYscqdezJwgoQl6MLoC2xWYtWJoQVADRufDN6fAcOo/fagOgI9p5rA3hq3c/x8Oq/kxHEGdvOz/DUup+xEG16bhWGTnyKP2//IyizCY5nlnLCyni6A/kt7chvaYe+py8KVgCgu2Fn/x0Lq8hjGFgxWuUKIZthBRALS7LEbTIWb2HJCSPovT6A9U2/junzvOhSYw6nC+ubXo2DE23t0AVWxSzRlIAVM57N23bzOvw3b9uFTa/vEmx3z4HDuNjRiaLCAjicQ7K32zDLRKlBoBc6OvHmO/vx4dETUX9E1j/9KIoKLfh88+8AAMPza9jQBOPpDnirbchvaYenrgqFO/ZJvp5iND5EYZ6XwiGjlHAH9cAKIMCSLKEHWWzAKKOLl7olAYv2OcUXYG1u/ZMoYCWyrCK/LJu37ca7r/+Mt531Ta+Gt80kBytGm17fhaICS1z4QqLipXwS8llJ0XsHDuO9A4cxzVaBBbNrMc0W/dLiQge9gfrEx+dwvoN7M/Wsr83AyseWYOjEpxg68SkAwPN12sGucXuQ39KOkNmEgt3NMHR0J7SalFCtTxqw1AQrgABLsoTrBkrzX508dV5UpRuAgc3fcwKRzrG+gtdhzOSh4oMVQC+91je9io0bfhDXxslT59G4cXu4DWVgxahx45t4c9c+lE6gHcmJsinI6yu5B+tCRydHlLtwDvbSO8Zjx6afAgAGwrFW3moba12ZD7VD4/ZgTNhBHruMU1qzfBQsFHjCHBJdP84jFBmTlPtEgCVBqfBfvf9vR7Fm1SOCb6b4YBX5eWwwKKMduw5g8xu74wCQ6Eu558ARnDh1PiootLm1PaJ/ZWHFtNl7bSAp3xF3X6krGME3JyazKBNzNdh8HADgq54CYMS6SqVFxaWKACUYBa9GWAEEWJIkxhKSamE5nC4s+9u/x+7/8w8JofX+B0fQtGmHoLVx8VIX7n9sHR5eNAe190yDY8iFE6fO42BrO2eslpgc7HQa47gzJV+7dD0A8X1lDlbvv/GPbFqZ65t+z37mt00GQL8JTDesAOCuAHDOwDM7lcIKIMCSJDF798RuyYnUxUtd+MYDT+Ohb89B7cxpKCqw4OKlLly41I2Tp86LCgxl1Ht9IFyJJhVpRyDYrry+cgtW02wV2L7ppygNO+r/e08Lu/UmWEzvAzR0XEF+i7i3o0qrPEAB4Law1AwrgABLknqvD7AWVGza4ouXuuAYcstolb5Zgw4Xtu86kMCikddmwk8JrJKaP9+cVj62BE3PrQIABB1D+PP2vbi+6Xfs50wgqGVfWwrGJ04FCcavdlgBBFiStOWNdwGBgqjSJPwAyG0z4ac5D6tUgGqk3URzmvW1GWh6bhWm2Srg6+3H9U2/w3/vaWE/Z5zsjPQ9fSkap8zZZQGsAAKsDIrASrFZZxBWf9VQi5WPLsGsr81A0DGEvs2/j7KoIvcJWl9+G8bTHTAdO4uhZQtSFm8lpNhkgNkCK4AAK0MisFJs1hmAVekd47F08Vw8vGge66cavnAZnSub4OultxnxbWgu3LEP7iX1cDzzCJtZIZ0aH+K6fvzzT/oqKrRcJ8BKuwisFJt1GmE1zVaBv2qoDQePRufc8vX24/OlzyPooDd+u5fUw7W4Pq61YPEYGMJhVuZ9baDMJvhsk5HXcSUjbwuzDVYAAVaalS2wUvd+s+i+UgOrabZyOk3y1+hUyXxxctc3/Y6FleOZR+CttnEeF4rxY2ncnpQHiiZSNsIKIMBKowisFJt1CmA1zVaBWTOniwJUrJhlIACEEmQGzS6pE1YAAVaaRGCl2KwVglUygIpV/tQ72T2CWo6lHZPbSuP2cKaDSbf4o9zVCyuAACsNIrBSbNYyYVVUaME0WwWmVZYrAqhYjVv5IBsYat7XFrcktOxvg+GzK6oJZdibcPOzumEFEGClWARWis1aBKym2SpQVGhhgTTNVo5plRWKZxCN1ImPz2Hj6++g3KzDo+4g9D19KNyxD84VSwDQ8VYatwe6m/aUjUGKOnVMEr9YqR9WAAFWCkVgpdisKbruH5OTiikawaR6STWUYuVwuvDh0ZN48519bAaHE2Ytan0hVAQomI6dhe7GIJwrFqNg10EYOrrTNjYh9eu4fpsdsCJFKFImAis+TassR1FhAfvzVFs5xoSBU1RgYUt10TCSU7cxNfrw6Ek0t57Ee/sPcX5+2KhFRYCuK2jo6MbYpm3wVtswtHx1XBUcjdsDfU8/9Ff7ULC7OaXjDpSWQHfTDo3bg4pA7L3JHlgBxMJKgbIXVnx1+ujUOhUJP0+U2HAaR5WebFHPtQGc+PgcmltP4PjH5xJmy5jnDeFRdyguglzj9sB07Cyb4pgymxAym6JSHKdDlNmEweWPwPryWxgfAh4bpvAv+fJzu8e1nyZYAQRYCiv9sIrMIDHxjnFR2Usn3jEuruafmJxeo1UMoC5c6qT/m6AsPaNaXwirXfGgCpSWIFg8BsFJJbQldbV/xJeVgTeE+p4+tuBqfks7HhsO4ZxBi08VePrTCStSql5RKQerEahQrNUztbKcLeclpSQWEbd6rw3gfEcnLl7qDGcT7RJdvWeGn8Kj7iBm+Edutt9WBk9dFbzVNrawhL6nD1Q+/e/bXngVflsZtDftrIXlqauKKzCRCjFhFEPLFrBvK//eGcIzY7ToT6IMTbphBRBgKSTpsJo4gXYiT62k/Te190wj1o/CcjhdbJrlCx1d6L3Wj57r/Tjx8TlZf1i4QOWpq4J7cX1cNgYAKNy+ny4jf9OOYLEVg88sxe0vvMp+7queAkPHlbQtEY2nO+B8ajHGNm6DhQLWuSi8UCitIAWjTMAKIMBSQMKwqp05HVMryzGtsgwTJ4wjFpIC6r02gJ7rtEV0oYPOTUYDaSAMKOn51xMpkUXlWlLPZg+Nlb6nD/qePnirbRjbtA2+ahubv31o2QIU7G5G4Y598NRVpSWRn6HjCsz72nCrcTXcS+ph3teGGX4K87xIsopO+mAFEGAlqXhYTa0sR+3M6ZhWWYapleXEYpKoE6fOAQgDKZzbnfkdN4jESB6suEAlVFbedOwsC6FAaQnywhZUoLQEBbubESgtYQNI0x3xzkDUtbgexmNnobthx9PuZKropBdWAAFWEqKv5tSvlqNm5nTMmjkNtfdMz9o3YukQvUTrgsM5hIuXujAYXqo5nEPhitPpfwC4xAUqgK4lOLRsAe+5+S3tbMVlT10VjMfOAKBBpnF74Jtvgzki26jhs+gKzakSs2XIeLoDgdISuBfXo3DHPlgoiK5VmGlYAQRYkkUv6abhvvp7CaAS6MIl2l908VIXu1xjrKRIqeEBiFSi8ARGpmNn4Vpcz1mtmfmcMptgPN0BjduD4O1WdrmXaFuOvqcPw/NrUg4spn/DZ1eAxbTvzby/DbobdjzgEQaWWu4VAZYITa0sx3319+K++nvJEi9CjMV08tQ59FwbwMWOTt4y8pFSywNgoYAlwyHM80aDimuTssbtQeGOfXA88whnW+b9bfBG+KQit+No3B4ESktoYMSIKUyRDud7JDh94WrTFUE6qV+iN4ZquVcAAVZCTa0sx0MLG3Bf/b2SS8nnqi5c6sLFjk6cOHVeEpxipYYHYHyIwqPuEGq9IVhijgkWW+FcsQR+2+Rw/JQXxtOfIb+lHcbTHchvacfw/Ogq26awT4iOYO+j/VcxVlNg0njOMAbj6c8wPK8mpRHv2ggY6nv6oLsxCG/1FBauM/wU+jmsLDXcq0gRYEVo4oRxuK/+Xjy0sIFYUqAd3ydOnUdz60mcOHUuqSrMjDL9ANBvxkKY6wklbEF3ww7ry2+xbwIDpeNRsGsEJgW7m+GfMjlquw3z4DMWFLMVRowMn12B6/n6lAKLWXL6bJNh+OwKtMOeqKyo4zkuR6bvFZcIsIAwpOaILhmfy7pwqQt7DhzGyY/PR1hQ2beFI/YBSORI55OhoxvWl7s5Pyvcvh/25x8HFd5qwyy1mGUgF6wS+an0PX2skz5VgaTMeHxhq4oym4DFI5/fFQD+JfJKqhBWwCgGVlGhBSuWLcRDCxtG/ZLvwqUubH9nP5pb2zmsqOyGlZAjXa70PfSmZeeKJdD3jETI81lVfJ+lo4KO31aGYLGVjsDnSSSoVlgBoxBYEyeMw5pVj+ChhQ2ZHkpG1XttAHs+OIL3DhxG77VElaWzF1ZcoPLUVcFvK0Ng0ngESkvYTKDmfW2ykuuZjp2Fr3qKahLz8V4hswnD82sw5jW6rqbG7YH50EjAKm15alQNK2AUAat25nSsWfUIau+ZlumhZFTNre3Y88FhNB8Viq7OTlhxgcq9pB7ueTVsOILuhh2mY2eRd/qzpCvWFO7Yl7DohNpU9Fp0EWBjzPJT7bACRgGwplaW48V1K0Y9qPZ8cASbt+1C77WBrK2YwjfOuZ4QVg0Fo974Mfv8ANp/ZOjoVnzvXiYr30gdZ6wSXwd1wgoYBcACgJOnzuPkqfNRv5saztM09atlOR38GQkqIHvLOyUa51xPvEXlrbbBtWwBtDcHUbhjf8ozfma6qISyUi+sgFEArIuXunBRIF5o4oRxmBre+1cb3mKT7WpubUfTpjej/FO5BCsuUDFhCJTZmBZQKaFgsRV+22TobgxGpZ7JjNQNK2AUAEuMeq8PoPd6Pw62tYPaRr9BrL1nOhbMvhf31ddklQXWe30A6xu34kSMRZkrsJrhD2HlUDAq1S+zIdlbbUPB7ua05JhKRsFiK3zVNvhtZTB0dLP7DDOpTl3ybcRLWVgBgMZaeX8jgA1puCYqFn+KmIcWzsHDCxtUnxZm87bd2LxtV/zscgBW44IUVrmCqPWORDhSZhNci+sxPL8Glv1tyG9pz/iDzye/rQzD82tYJ71lfxvyPulQxVvGT/WQnRuLW8rDihShCF8EgP+ivv/BEbz/wRFMnDAOa1c/ojqrq/f6AFau/wXn0jfbYUXv9QtiuSsY9XvmzZ++px+3vfBqhpdS/EqUOyt4u1U15b+UVWpgBYz6JaG0TKG91wewvunXKCrcgaeWL8KKZQszDq7m1nasb9rKuW0m22FV6w1hlSsYF0vFvPkr3LFP1W/oIvckRkrj9rAWoVrkUsy4Sh2sgFG9JEw+B3tRoQUb1q3AQwvnZGQGTRvfxPZdB7hnl8WwmuGnsNwVxAz/yPLPbyuDc8ViBIutWbH8cy+pj9qrx8h07Cyb1iVSflsZQmYjgpNKErapu9oHrdubkpcJ/5Kvwe9NybaSWlgBoxZYyla3mVpZjg3rVqTVx7W+aSv2HDjCPbsshJWFoi2quZ5QHKiY5ZTxdAcsu5tVvfwLlJbA+dTiqI3Ruht2mI6fZbN8BkpL4J8ymY66Lx0flQ9e39OHotfeg+6GHZTZFFfPEKCzPmjcHkVjypIHVuphBYxKYKWuFNeK5YuwdtWylC4THU4XVj33i7i3gOxIsghW44IUan0h1HqpKEgB0aDS3bBnPEzhnIEu8T4+CCxJkOlheH4Nm+CP2dDMONX9tjJ4q23wVds4C1YYT3cgFJMYUH+1D6Hw3j/tDTt0NwbZcmFK66UCDU4Y5J6dHlgBo86Hldq6gTt2HcDB1nZs3PCDlFlb2Q6rccGR9C6xG5IpswneahtbhUbj9qBgd3NGfD0uDXAiT8uCyqUBKgIUHnMH446lzCYMLVsAymxCwe5m1vIJFlvhrauC45mlUZBiqj5H+rZCZhNMx86ktYpO7HzlKX2wAkaVhZXeIqdrVi3D2tXLFJ1BNi8DZ/gpLB6ODktgRFseU6Jq+pmOnUXB7ua0+qk69Rqc02twyKhBpz76CbZQwK/tQYwPxc/ZW22DvqefBU1s+EKs9D19KNy+nw1nCJSWgDKbECweg1AY1Nobduh7+qF1e9JyDR6yamRAK72wAkYNsNILK6avWTOn441XfqzIEjFbHexcDnTGkmKWSZE50g0dV1CwqzktsUkDOg0+1WuirCguWSjgnxzRwapc8tRVwVN3N2fpLyYzROy8dFfpVC/6q3SKmkzFZN0/Viqt0g+rUVL5OTOwAoATp87jrx5bizdf+XFSGUybW9uzDlYWCnjUFcTiYXoJRZlN4Qe6inUkMxuH805/Bq3by6YXTpVcGtoXdc6gwUkjXfVY6PqJgVXkG0zONlT+VlN6yfrMwArIeWBlDlaMeq8NYOnTP8V7r/9MFrTo2K+tovqSOlbRM5YIq1pvCGucATZzQqC0hM3Oych4ugOFO/al9CGOBNQ5Q/wyT+j6jQ8BP3UmhhWTY8pnK4P25iC0Nwc5j9O4PQiZTdCpFFjSytVnDlZATgMr87Bi5HC6sPTpn2LD+qfwsMSYrfWN2RMUaqGANc5A3PaZWFiZjp1NSYZNGlDaMKAQBygp12+Wj8LamHQ1sdK4PTDva4MZbchm8V2naGUWVkDOAks9sGLkcLqwvvFVABANrT0fHOF8I6hGWFUEKPzEEYh78+epq2Jr9QVKx8PQcUUxWA3oNOjUcVlQ8u/V+BCwyhXELF8qMheoU+I2PmceVkBOAkt9sIrU+sZXMU1ECXuH04WmjW/K6Cv9sFo8HMSqoSDnZ97qKQAAy+5mUPmmpHxUDJTOGTTo0mvQr+WyDOTdKwsFPOAJYclwfNkvJoAzMGl8wiKqqYxCT7WEfVjqgBWQc8BSN6wYLX36J3jv9Z/zQmv7rgNxS0G1wcpCAauGAlElsyizCb5wVDoA+G2T2Zp9UkVbTlrWghIxcgnzojXLR6HWR8eGRYoJtfDbJid0pieSvqcP+qv9KclwqrSErSv1wArIKWBlB6wAKuzT+gmO73+DM+TB4XRh+679EvtKv2X1C7ufdUgzDmj3vBpYX34bANhCD1Lq7bk0wBsFOpzI00qMCxJ3r8aH6FCLGX4Ks3whTh9V7D5A3Q07NMMezm0ywEggaKS81TZ46qrY8/PCKZrVtlmb37pSF6yAnAFW9sCKEQOtD9/ZEndUrHWlRlitikii5622wbliCRvpHVmj7/YXXhX9JrBTr8HPi3QJlnrS52+haN/aDD+F8gCFigDiAj+ZmLBQsRW6q/R2GuOxs1HAyj/UDtOxs7A//3gUtPQ9fTB8dgX+KZNhffkt7pGFl5PB4jEITiqBo+5uaG/YYejoTroAhhL6NKHlqj5YATkBrMSwGqfVYpxWg65gCC5JVz21sGJ08VIXmja+iQ3rV0b9fs8HhyX0lX5YMVHrAO1Ud65YAoAO+ozcRiPlYXRpgB+P0ScVbW2hRqynGX5KMNCTMptw49fPR403L5xby3TsLGsh6a/2Q+P2oHD7ftxqXM0ey2xSZur9cS396E3K3TDEGFbBYit84dTImmFPxpaN3BaWOmEFZD2womE1TqtFjUGPBoMB5br44JLzgSD+FAjgiM/PA7D0wIrR9l0HUDtzBhbMrgFAB4mqvWAEk0yPDphcwv4+mbd/PyuSDisLRYkCFJM3PRT2RRk+uwJDRzebl4qxpiL9b+b9bSywGOl7+tjjC3fsYyEjx9Guu5Hp/O20/yr+mqsXVkBWA2sEVhaNBouMeXjEmMd7xnS9DtP1OjxizMO7Xh8OeH2cbSbsUWFYMVrftBWzZtL+rINtJ0X2lRlYjQuOZFYYnl+DotfehXsJ/cDLfQD35WtFOtXpJV6tL8RCinNOYfAkdJovBqwvvw1DRzfyW9rh+XoVgsVWGE93QOv2snNhrKxIIJn3tUF7w646X5QcxVtX6oYVkLXAiobVy2V/gYmuYYTiABQvQ1EBxn1lLH7yl3dikXsYa/ceDltbmYEVQPuzNm/bhQ3rV6K5tV21sAIQBQlvtQ1Fr72LvI4rnMnqALD7BRM53gd0GrxjTvyqanyIBhOTgobLSc4Aym8rg3/K5ITOcUPHFeiv9tHpWsKpiTVuD8b86m3OTcb5Le1xVhYA1Re5EKsTeZF/JNQPKyArgRUNq20PzsNd07+KgGMIQ5d7oDPmwe90IeAYQjACYDpjHiwVpfA7hpA/sQQ6owE1xvHYdJcN6z79DC6ei5dKWDHavusATpw6j0GHS+DIzGZdiHRaMxZV7JtAZgnGlHHne0u4zRL/NrAiQGGuN4RZPiouEJURAyefrYxzszETWsA4xvnivxJZhvqevpyBU6xcmkgLKztghewrQhHts9p0lw3TvzoZAccQtMY85E8sgaFoJEzA++dbUT9rjXnQOwoQ8vrC/7uFqZMmYMWlbvx6eJi7xzTAitGFjq6UtMs/J/ltmo7HP8xMqhTdjcG40uixOmfQ4GQe7Wus9dGAqvVyW1GRpbF8tslRAZy6cCoWBk5KBm9KCcnIJh1nk/VlD6yArLKwomG1yJiHyYEA7Gc+AwAYi8di+Mt+aI15MBRaoC8qgPP/XYbWmAdj8VgEvT74btyCNuzn8juGMNzbj5DXhzl5Bhz1+3E+EFDswko+S8XLwEhFWjxcifWkRLKfM2ixdijICalIP1Rg0njobgzC0NENn20y9D39yOvohu5qX1QeqlQo02EHqdI5A5BtsAKyBljxoQuPGPMw3NuP4d5+3jO1xjxRvq1HjMYoYBFYRSsy5zqjZB/mR2OydzIxUb7qKQgWj4HhsyswHTsLw45u9hg1VZrJZh2PSIecLbACsgJY8bCqMehh0Yh7qyQGVgD9BrFcp0NXMEhgFda4IIUlwyHU+kIJfUlKiIEUQIcIpDrtzGjXCcNIOEM2wQpQPbC4g0Lv1adm2NN0OnQGgjLOzC1YJSpeCow4u4O3W2Ho6AZlNoEym2DeR6dYYSK7hfxIlNmE4O1WBIvHQHdjMCXpZoi4dSIc/ZNtsAJUDSz+CHYAGPedBezv8kqKMXz5KvLvnISbzf+J0v/vu+j537+P+m/3r7Zh4t8+hj//60F85Tv3YfD4J8i/cxKGL1/Fzeb/xHS9niM2S96FFTxLpbCqCFB41hmICsKMzbkeWVvP/vz3AAD55nYMz69BoLRE1LJN4/ZA7+5TRZn20SSXBmjJy05YAaoFFv/eQCaKPehys9DKv3MS7MdOYfjyVQxfvoqCqimY8PiD7H/zSorh67uBoMuNr3znPljrZkJfYEH+nZPQuYHO6Cl2mSnmwvKepVJYzfBT+Mmgn3WAM1WWmQo25kPtcal+/bbJ0Pf04daG1TDvb0PRvuxOZpfrOm7IXlgBqgSW8EZmBizOs/8Pk3+4Cr7+Gxi+fBXWupn4878eBAAMnf0MhVV/CfuxU7DWzcTNg//J/n7yD1fBfuwUCqv+km1H7jgln6VSWM310GmNgegc5ZFl1WP9SkyApvF0h6pzlhONqCUv+TZoZSbQWmXAEpd1oSsYQrlOC18fDSpv35/h67+BvPHFLHzsx06hoGoK/vyvB2Gtm4nBY58AGIHT0NnPYCz5Crx9f5Y9TslnqRRWa5wjOa2Y1Cp08dJ9MJ7uSAgizbAHt4U3CxOpX/1aOQUnuJS5XSEqKvMlPkXMj8z5qDEoz9rdHi92e7yixil5diqFVWS2UPeSeni+XgXz/racjfAezXo9H/ijMdlWMgcrQDUWlrR8VheCwZQAqyso9IYwt2AFAAURh+mu9uE24oMCgITpYrJZyS8HMwsrAJBU4Cc1kp58r90f4P1ca8xj/2coKoChqECwTRdFCbSbe7ACgJqI1MC5kIFArs4Z6IIWjIbn1eBW4+qEG6mzTS15wJDscvSAGmAFZNzCkpcpdCAUwhGfH/f/xTjkTyyBvtCCkM+P4d4+GIoKoC8qQP5fjI/amgMAw1/2w1JRCu+NW/TPvX1spDx/OENuwmquJySY5E4JuTTAIZOW3TfIjHN8CKIDUssDVJQ1KFX9WjozBEDngRrS0D9HFlOd5aPw90P0D4FJJWw9RevLb2d9+EVy1pU6YAVkFFjyYMVoh8eLb//lnbDeWQp/OMOBpaI0arMzcCcAwFBkCW+OHk9POmxxGYvHovib/xOnfr+fB1i5B6txQbroAldgqJIa0GlwyKjFvnzu/OznAADxH8h/7S79xMi+KiIuR6CU/q5QZhMczyzF2KZtWfsWtFOXjLNdPbDKYKn65GAF0Eu4pqPt+M3UO5E/kc7A4He42A3N3hu34P3zfwOgc2DlFY9lU88YCi3InzgehonjcaOnD//oGk6QgTS3YMWkNq6NqRCjtDr1GuzP1+KQMZHHIfFYMwUrAKgIW3vBYiubDYJJUzM8v4aN5s82yXe0qwtWQEaAlTysGJ0YcuOF3+3HupJiBBxD8P75FudxwxjZIE0vGS3wO11werx45j9PJXC25wasmE3Lj7qDUcsvymxCKPxQKuVc7tRr8IZFJ5A9VJ2wAkZeQDDWFaPCHftgf/57yDdnX6wZE9kuXeqDFZB2YCkHK0YfXutHR/8N/Micz27Z4ZPfMQS/YwgXr1zDq25PzsKKWfYtdkeXWw8WWxG63RouTaWMX8alAd4x67AvX+j6qxdWAFh/XnDSiKM9UFoC9xI6h7trcX3W5ceSZ12pE1ZAWoGlPKyYNruCQTztHMIiYx4W5uXxgqsrGMQBrw9HfH7eNiWPRCWwmuGnMNcTjEoDA9APnu8eG1vKSikx1W5GysRLn78aYAWABXvsm0HX4nrkfULXFcymcAeXBtgrGVjqhRWQNmClDlaROhAuLFGu06FMq40CV1cwiK5gCAOhkKQ2RY1EBbCa66FzVTEFIhgxG5cNHd0JfTADupHKyjP8lOg3d+LrCKofVpFiloQat4f1ZTmfWoyxjdvgrbZlDbCOG6SGMqgbVkDKgZWKjIbCF6EzEEQnpL4Byz5YMWlg5npCcf4puvLw3TAdO5MwdYtLA7xj0WF/vi5qnNPDpdsTpSumC0fwOdXFzV9dsBo5gKmyYz7Uzlav9lbbMDy/Bvkt9O+yIczh9yYpR6sfVkBKgZUZWKnlwibTLn8/tAX0qDs+tXCw2ApvXRW81Tbkt7QnrEYMACeNWmwpZGoBRt+rcwYNzhl0QIEOFQEqqg+XBiKWf8LzVxus7grHDAdKS6C7YYf25iBdPHXHPgwtWwDry2/D8cxS5J3uyApYteTRsWcirxD/pyp6plIELAKrVMBqhp+OnYpd9vltZfDU0bX18ltOYizP63eXBthSqMdJ1jriv1fi4SR+/mqDVdRP4SXgmNfeZd8I5re0Q9/Th6LX3oM7XERV7RJvXWXXM5UCYBFYKQ2ruZ5gXFgCQOer8tTdDQCw7GsTzPIZbVVFjzNd90rNsAJov1Ww2IqhZQtYMDEWFVOyzG8rU7Qyj9ISb11l3zOlMLAIrJSClTlE+6cWuwNRSzLKbIKnrgrD82qg7+kXBap4qyp6nARWI2JqEXrqqpB3+rO4N6oat0fVsALEWlfZ+UwpCCwCKyVgNS5IYbkriLme6JcGjH/KPa8GxtMdGPOrt0W9rdqfr8M7Fl3M1hgCKz4V7G5GsNgK54olyHvh1awKFhVnXWXvM6UQsAiskoUV45+a7ouPnxqeXwNvtQ2mY2cxtmmbKFAN6DTYUqjniDonsOJS5CZwjdsD68tv0ZZslm3JEbausvuZUgBYBFbJwGquJ4jF7iDKY7Im+G1lcC2pR6B0PMyH2nG7hL/03FZV9DgJrEZ0VwBY7Y7/LNuSGApbV9n/TCUJrNEOK3lt0vv76DJaXI509+J6AIB5fxtviuJYJbaqosdKYBWttUOp3QyeLvFbV9nyTPG3mwSwCKykykIBi93cjvTh+TXwfL0KgHRQAXxWVfRYCayiVREExod5NbRsAXQ37cg73ZE10eyM+K2rbHmmhNuVCSwCKykaF6TTuswdDnIGerrDb/wKd+yH9qY9LlsAnzr1Gmwt5NvLR2DFp1m+kc8Dk0oQmFSCoWUL2LirbAFXYusqW54pce3KABaBlVjxvfFzL66Hp66KfuP32nvh348BID5V8S6LDu+YdaLGSmDFLSZpX7DYyoaI2J//Hvy2ySyshufXwDf9Tphb/y80N5yqi3Tfa0xkXWXLMyW+XYnAIrASo0SgYt74eeqqYDp2Fre98CoCpePZn8U6eYWtquixElglFpMDS+v2QHejm/29oeMK+29PXRWMn3dC73Zg+Jsz4LiL3qKjhlQzLk0i6ypbnilp7UoAFoGVkBKBinnj57dNZkHlt02Gc8US3s3JXBK2qqLHSmAlTpH+wryOboTyaQoESunc7kX/fgQAkP/xOfi+Wgb/lMlyJ6yomixcGRmy5ZmS3q5IYBFY8ak8QGG5KxCXejg2NGHMa+/CW22D45mlMJ7u4N2cHKtzBi3eLNBJyjtFYCWsGX7u43U37QBo60o36IR20Ml+Zrh6HZQ7IKb5lGqjmStXe7Y8U/LaFQEsAqtEErKoYkFlf/7xMKjeFv0GMDoFjPj5E1gJa50r+ngmLU8o3wTjZ/SS0FNXBWPnlbhzMxmj5dLQltVogxUgCCwCKy5JBdWtDath6LgiCVQAbVVtKdSx5anEzl+5e5WK+yTcrry+pLW52k1hnpc+hwkrcc+rYbM1AEDIbARlNkE7OBR1rv8vSmDJgOPdpaFTHu81jq5lYKR4gEVgFSu5oBK774+RNKsqeqwEVsKa56XwgIc+J1hsheOZpXFpkYfn12B4fg0AwDu9EvkffwqN1wfvdBt0X96QO3lZ+lRPQ+q4Qd781fNMyW+XUQJgEVhFykIBy10BLHaLB5X25iAKd+yXvLNfmlUVPVYCK2HN81LsUpCpOShU3Tk4phD//TePQevxIjimEMbTHWnL7f6ShQ9UwvNXzzMlv91IcQCLwIpRosj02Ld+kaDSDHtkgYo7BYz4+RNYCasiiChY2Z9/XHQp+ryLXchvaYd/ymQESksw+MPHYei4wib3S4V+byKwilUMsAisGM31xO/1iwVV4Y59CN1uxeAzjyB0+xiY97fJcsbGJ9aTNn8CK2FVBIFfOkbe4g4tWyAIq9teeBWh260spDx1VTDvb4P5Bp29gTKb4LNNBmU2KZYji8kbDwhVa86WZ0p+u1z9RFwSAiuATvOy0hmIyp4QLLbCtWwBvNW2KOe5c8USBErHo2B3syxQybOqoudPYCUsBlaMlcwE73KJWeYFi624tWE1zIfakd8yUkA10imvcXsULZkWKC2Ba3E9Cyy581fPMyW/3UT96GMbHa2wGheksHIoOpYqcguN7oYdRa+9C31PP9yL6+GttrF+KzkJ3vg3K4ubP4GVsCwUsG5oBFbBYitc4WwYXDJ0XEHhjn0IFlvhq7bBbyujwRW2nlOZzM93j40NrTCe7sCdwdEZusDXj360w8pCUVjsDmK5ayQQMPI1NwDWghqeXwPHM48gv0VafqpI8aeAET9/AithWSjasqqIeFfiXlwfZSUx0rg9KNjdjOH5NfjvX/4AhTv2I7+lnbV2/LYyFiSplq96CoynO/CAN7Zyc3Y8U8m0y99PRBGK0QiruZ4gVjr9UQ51T10VhpYtAGU2wXTsLPslvvnLH8B4ugO3vfCq7LdD8q2q6PkTWInT024qClbBYmvCpSCjsY3b2CpEriX1KNjVDH1PX1ryuOuu0s57v43e9jM+BMz30aljsuWZSqZd/n7oH/TyJytvsGq4sOUBCquG/FHpiP22MjhXLEaw2EpXR3ntIILFY9gQBbGpibnUqdfgzQK5VlX0/AmsxGmdKzowVOP2sIkRuaS7aYevego8dXejcMc+dlnorauC7x4bjMfOpjyMIS+84TpYbGWLtdLAUv8zlWy7/P2M/KAfTbAaiacaWf5FOtSZZYH+aj9cS+gvt5wQhUiJ26wsbv4EVuI0zzsCq0BpCYaWL8CY194VtK7M+9qgGfZgeF4NdDftyG9pZ/O5p6NEvcbtYSv2eOqqULC7D3cFaEsrUXK+TD9TSrTL3090mzrT7f9jNoDZCnST5GSVuwBcfc31BNFo97NFSCmzCcPfqoPjmUcQnFBMO1t/9+/w3jsdntkzYd7fhoI/HGY3wUpVp16DxjEGfCT5DSD3/AmsxKkiCPyDc8SyutW4GlqHC9SYAvhtZVHHGjquwNDTj+CEYoTGFMB77zQYz19GftspaB0ueOuqoPEHoXUMQd93U+6FkSR9Tz+G59eAGlOA/EO0/6xTR/9P+vVT5pqK6yv1sAIUA5Z6YVUeoPC8w4/F7iDywr+jMyY8Am/1FLoc+e/+DQAw/K06mNpOoXDHftlfUJcG+INFh18V6XFLK3cJGD1/dcNKXQ/AJkeIzXE1uPYxBCcUQwPAZysDDNGv3HQ3B1H0+h/gvXca7Yg36Ok3wjftyOu4AkPnl6Dy9PR+wjSV+tK6PdBoAG/Y8a51DMGl0eBETABpLtwr/n6421S0ao68ySp3ASL7oqPUA1Fv/5hac4xT03i6A9obdrgX18N0nM5TlYykb6sRnj+BlXjN81JsfnZ3OMAXoO87l/RX++g/WDv2w/48Hblu2deGYPEY1oeUiRTJxmNn4VpcD989Nuh7+nBn9I6wnLhX/P0kblOxqjnyJqvcBYjsq9Ybwsohf1SUuntJfVz8jc82GeaePoxt2pZUfI30zcri5k9gJU13hf82UWYTG5LCJ+0wfc8NHd2w7G+De14NApPGiwjcTK10N+zQ9/TBZyuDGW1Rbzpz5V4l7oe/TUWq5sibrDIXILKfkeDPkTsc+fYvUqZjZ2He35b0X1B522qE509gJV1MMj5vtY0z1opP5n1t8FbbMLRsAe3w3nUwoyXpDZ9dQWBS9NahXLpX3P0It5l01Rx5k1XmAkT2wwR/WqgRhyvz5YuUUqCSv61GeP4EVtLFvE0DEOdcj1XB7mZ46qrirOqi196D45mlsL78NoK3W5MaT7JirD/26uTQveLuR1ybSVXNkTdZZS4A08+4IIU1zuiYKm+1Dc4VS6L+yjL+CSX+aiYXAMo/fwIreWLCGAD6/vMpv6Ud3uop0F/tj/q97oYdprD/SA0FJpL7o6reexXfj/g2ZVfNkTdZZS4A0095gMIvbvmirCrniiVRX1h9T59i5n1y22qE509gJU8WagRYcpaDkcpvaYfjmUfgt5VldEkYyjfJDqlR872K70dam7Kq5sibrDIXINKyioSV31aGwWeWsl9W3Q277HQvXEo+AJR//gRW8sVkDwWA4fm1vMeKsVoKd+zD4DOPwPpytyLjk6PApBJYwkGrn0oyK9R9r5KBFQCIdMCo4yGI7Gfl0Ii/Chh508P4JcY2bVMEVucMWjx1m4HASupZaXoALBSi0h0zoQyJpL05yP47UeI9jdsDy7420cn9lBZlNsFvm8yO77Lor56671WysAJEAUsdD0FsP5FvAhkxWRT0PX1sPm65cmmAnxcZ8HdWvQJxVYnnT2CVnB7wUOzmdb69glziC2UxdHRnrMIzkxWCGd+nOjHXS933SglYAYLAUsdDENsPs72GSxq3B9aX34bPVibbl7E/X4enbs/DSaPSoIqeP4FVchoXHLGumIygXNLdsLOVnPMy6JcSK1/1FOSd/gwAXR2HP00yoPZ7pRSsAF5gqeMh4OpnSMMPEo3bgzGvvYuQRGCdM2jx7Ng8vFGgVFxV4vkTWCU5awp4bHjEuhpaviDhsdqbg1kBKoBe1vpsk9m8Wy15Qmeo+14pCStARNUceZNV5gIk6qdLr4FLo4nyYcVK4/ZAJ6FY6RsFehw2Mc4C5R4srvkTWCU5a4qOu5rvG3nhIuS7YqQNO93VCrDheTUwHxpJyfxH3tQy6r5XSsMK4LSw1PEQCPWjVMAmvfwzElglMfd0PgBMX0+7R9wCzhWLec/TXx3xReluDEKtoswm+Kpt7Nag44bEaWWy4V4p3SbAUzVH3mSVuQBiLuqRfG1cQVMpOp+nxRsFenRGOdQJrCSflYEH4AHvSCZR95L6hJubGcVGjatVw/NrYNndzFpXvzcmunbZc6+UfqY4q+bIm6wyF0DsRT1n0OKkURtVNEKMBnQa7LLocNikS+mFjW2TwCqJGUc0Y6EofHd4JFCY2eSscXtEvWRhgkGNCsXnKSXKbEIo3wRzhO+KO5whe+5VKp4pLd2oOh4CqRd1a5FBUsjBLosez96WR2AloU35faXmAfhupKM9Ivc+X74qw2dX4n6XibQxfAqZTex2oCEN8BsT1/VLxb0Sfv7l9ZOKZ0pEHJZaYQWEY6XGGNCl54fWSaMWK2/Pw67w/j8Cq+TmnilYjQ8BD3jpnyLDGAwd3YLLQkB9kEo0to35FIbivtKpgpUySgesAIGtOWqGFaMuvQZ/NzYPi91B1HqDbAFUlwY4adThSL4W5wzaBH0RWEk+K4NLC6bMPDASxqDv6RPMZcZ8Hhnlrla15HHFXRFYMUoIrGyAFSOXht7rt8vCv4eBwCq5uWcSVncFRhL0RYYx5Le0IziJfwuNvqcPvnv4MzioQZd1wCv5sdeQwCpSnEvCbIKVvL4IrCSflWGn7WPDI/92LamH8XQHmw7GJ5D/ipFaY68AGlbPWwishBQHLAKr5OZPYJXEjBPcp4pgvHWl7+nDmF+9DQAI3T4mYZtq9lsxYmAV7bcisOJSFLAIrJKbP4FVEjPmuU+R6WOYIFGfrYyFEZfDndk/GOm30qoQXp/qCaykKMlS9QRW4uclvU3hOSnTpvy+Ug8rplw7AHjqqhAstsJ07CybSSFROmRm/2DkclFtUe6/NwG/M5JloBQlUfmZwEr8vKS3KTwnZdqU31d6YncifVeR6WOY7SvB4sTLQSB6W45a1K+lnevxifkIrISUkiIUwpNVpk1xfRFYST5LJbDisq70PX1RS8AQT81BIHpbTqbyWzEa0gB7jVxWFff8xWg0wQpIQREK4ckq06a4vgisJJ+lElgB3NZVwa6D8E+ZzP4+trqN6dhZ+G2TOfcPJlN7MhkxoPpjHldAaOL5C2m0wQpQuAiF8GSVaVNcXwRWks9SEawqgvHWFQCEzEaYw7nOAXDWm4z8nS5saWUKVi159DaboYSbMQispEhCjhYCK/Hzkt6m8JyUaVN+X+ndb/a0m/6MqS9ZsLsZho4rgkGisdK66b08sSW9Uq0hDdBkpvBKPoGVkhJpYRFYiZ+X9DaF56RMm/L7Si+sZvlH4q6G59eAMpuQF85xHlsYIlHiPt3VPslwU0pDGjpUgb94BIGVHIkAFoGV+HlJb1N4Tsq0Kb+v9MLKQkVbV0z6mOF5NSjY3QzK3CGqP63biyBG0smky+FOYJVaaQHsBdCt1EUgsEpu/qMZVgDw3WGKLTk/tGwB9D39sOxvgy9cHFeuLypdPqzfGwmsUimt44vmM44vmssBNAGwJzNYAqvk5j/aYXVXYCR9TLDYyqaPMe9rg6HjSlyQqFDdwHQ72j/VA3808l4NWe0SWAEAWgFUs053xxfNjQDKAewksCKwUmTGEu6ThYpOH+NcsQQA7aOizCYU7tgX3z5PhlF9T1/aHe2/M/LNkcBKproBPBi4drwhcO34mai3hI4vmu2OL5qfBFANmmgyJst5hGKjJ7BKbu5qhBUQvRT0Vtvgt02G9eW3YTp2loWXgSfbQuwm53RbV5d1fCXlCaxkyA6gKXDteHng2vG9zC85wxocXxw84/jiYAOAB5HQv8U1Wc4jFJsBgVVyc1crrCKXgpTZBOeKJchvaYe32obCHfuQd/qzhEVSGXEl59PetCsyHzFqMSSaJ4GVDO0EUB24drwx9gPeOCzHFwf3gra2YvxbXJPlvByKzYDAKrm5qxVWFgp4cWjkONfielBmE/Q9fbDspwNETcfOQuP28KZBNoYrJUcqnalluKszE1hJVCuAhsC1408Grh3v5jpAMHDU8cVBu+OLg42gwbWTe7Kcl0OxWRBYJTd3tcIKoGHF5K3zVtvgnzIZt73wKjx1d0dtu2ES9iVSpL8q3RueL+u46gcSWEmQHcCTYT9VK9+BoiPdHV8c7HZ8cfBJAA0UJeTfyqYLS2ClyIxl3KfvekYCRJmlIAOpMa+9i6HlC0SV7gKifVbprkNIsi4kpSYA5YFrx3eKOVhy+WTHFwdbnZcPNgB4EhzLxOy6sARWisxYxn26KxCT9nhxPVvmyvHMUmjcHhTsasbQsgWi2stkJoZo/5W8N+yjEFZ7QYOqMXDtuF3sSbLrvTsvH9wJOgyiKRUXgMAqubmrGVaxIQzeahs8dVXQX+2H9eW3QeWb4F5SD31PHwwd3YIO91hx1SFMlYY0kQVPR3s4kCidAe2nejCRn4pPsoEFAM7LB+3OywcbAZQD1F6lZkRgldzc1QwrIDqEgTKb4LeVwXyoHfbnH4fuph1jm7bBZytDoLQEJgkVmhkry5DGYhMnWGc7gZWA7ADWBq4drxbyU/EpKWAxcl4+2O283PIggAbQBJUtAqvk5q52WEUWQwXoANHApBKY97WhYHczBp95BABgffkt+KdMZis7cyk2bEHj9iLdOq6nJM2f+/pxHqHYGFUAqy2gl39bkm1IEWAxcl5uaXVebqkGsBac/i1+EVglN3e1wwqILijhrbYhWDwGxtOfwbliCUzHzkb5rfJb2nkDQGPfGmozkPPqUz2BFY9aQYNqrRQ/FZ8UBRYj5+WWLaD9W1vEnkNgldzcswFWAJ06BqCXgq5lC6Bxe5Hf0g5DR3d4s3MfzPvbMDy/Rtq4wrFb6dRxA1+uKzHXj/MIxcaXQVh1g/ZTNcjxU/EpJcACAOflFrvzcsta0OBq5TuWwCq5uWcLrMaHwPqumCyifttkuJfUs5VwPHVVbIFUvkDRWAlthE6FPtVJO34UwMqOke00ranoIGXAYuS83NLtvNyScJsPgVVyc88WWAEjsAKiLSLX4nr4bWVsRLu32gaN2yMqUt3QMfJGUJfmgNHjBvHXYRTAaifCYQqp7CTlwGLkvNyy13m5JSqNDYFVcnPPJljFKpRvwtjGbbjthVdhOnYWg88sBWU2wXi6A/qefknWFSNtGp3u3NHt3MpxWLWC3vf3pFJ+Kj6lDViMnJdbGgGUU9TINh8CKxlnZTGsACAwqQT+cPXmwh37MOa199h4K8ayEhPlHmmFpTNDg1hnew7Dqhsj22nOpLozRmkHFkD7t4Y6W8JpbKhW5XsgsFJkxgo/AJaYJhirCqBjp/Jb2tmfdTfsogCkC4c2hMzGtDrdW0QsB3MUVnbQq6RqsdtplFRGgMVoqLPlzFDnIWabT7cyrdI3S3i7g/Q2E35KYCVK3x2Obsd4uoONu2Ik1UrShi2sdBaciI5u51aOwmonwmlf0rH841JGgcVoqPPQTvCksRGvEVgpJwIrJfRdD11rEAD8tjL4bZNh6OiG6dgZyVtvIqW7EZ0HKx1W1glDst+JrIPVGQikfUmXVAEsABjqPGQf6jzUiJg0NuJFYKXIjFPwAFQERzY601kZFkN3ww734nA4QxKpjBlAMWEN6fBj8cVe5Ris7KD9VEltp1FSqgEWo6HOQ91DnYeehKRtPgRWisw4RQ/A+oiNzq7F9QgWW2E6fhaW3c3wVtuSsooYQAVKxys2XiF93c9NrByDlaS0L+mS6oDFaKjzUOtQ56FqJExjw4jASpEZp+gBiFwKBkpLoiLYjac7YDwtrs4gnwwdVxAstsoKhZCj8SHgueFoaOUQrPZCRtqXdElirG765bvVeSZvbMXrALwAZkd/SmClyIxT9ABUBIEfRVhXg+seRWhMAQAgdLsV+YfaFeknUFqCwJ0T6Uo5PemplHNnUIP7/BqMp4ApAQ2qghoUQAMfAFecAZYVsOoGXZ3mlyFnjz0lF00BqR5YAOC71enx3epszRtb8RaAuwGUEVipG1YWCnjZSaEg3KR7ST28906H7oYdY157D/4pkxEsKUbe+ctJ9xWcUAzf9P+BULEV+a2nFJuDkAoo4C+DNKyqghrM9mvwHR/97yEN0KNV9pqm6F7ZAfw4cO348pCzpzs9V06+sgJYjHy3Ou2+W51v5Y0tbwMwm6JgVa51AisltWKYwtfC6Y8DpSVwPP3X7Gf5radg2d+GwJ0TESy2Jm8VGQzw1FUhNKYAeR1X2NisTGl8CJjtpy0wAOjVAj6Jm6RjlaJ7tQW0VdWa7mskV1kFLEa+W13dvltdW/PGVgwCqAUgLvF3QhFYKam7AsD33eGWzSY4vr8UoTEFMB07C0PnlxhavgB55y/D+KcLADQIjSmA1jEkuz/dTTvcS+oB0IVXTcfPQuMPKDonzmtoNmH4W3UITCqB997pGFz7GIKTSkAZ9ND39KOAAv5nQINv+7W4jdKgR0txLBdF9KP8vWoFDaq3Qs6e9OfkSUJZCSxGfnvXybB/ywQaXDJEYKWkLBTws6GRpaDrr+fCVz0Fuht2UBYTinbsR17HFQw9/i0YLn8JfU9fUrBi5LvHBvN/HIO+7yY8s78WhmHqNDy/Bt7qKTC1nULe+cvw3jsdgUkl0PfdhP/OiXAvrodm2AN9Tz/yQC8dv+PTYnxIg8s68eBS+F51gw5T+HHImcEk+Ekoq4EFAH57l8dv72rOG1uxD8AUAGXizyawUlo/dlGYEhEgOvT4twEAY5u2QTs4hKHHvw3LHw4jv/UUvDXToPEHFQFWsKQYlNkEyx8OQzvogvcbVdD39KfE0goWW6HruwnTny5AAxrKGn8ARdv+FfmtpxAqtsJbPQXB0hJAg6g53hmiwVUV1KBfy7+BWsF7ZQfwy8C14w+GnD2fJdNQppXkylp9spTPfQDAZgiCi8BKac3yU3gxzB7KbMLNX/6ATXEcmDQe1pffhs82GX5bGVshJ1hsVaTgqaeuCu7F9bjthVfZ/gOlJSnP7x4oLWHjyDx1VfBVT4G+pw95n3RA39OHYLEVgz98HNqbg7Dsa4sbz6d6Cr8zUjiri74fCt6rnaBzqSd/kVWgrLewYuW3d33mt3dtzRtboQH9RpHDv0VgpbTGhyj8bAjIC//sfPqvEQjv79P39MP878cxPHsmzIfaoR10wf2tOuSdv6xYWmPNsBfuJfWs013jD6TF+c5YToHSEuj6biK/9RQMHVegdQzBW23D8Le+AX3nl4BBD9dDc+GfUgb91X72vPEh2jl/Z1CD/xdeKip0r1pBL/+2Zpufik85ByxGfntXa9i/VQIaXGERWCkv2rIqDSfoG55fwwaIGk93QOMPQN/TD0Pnl/DWTIPxdAe0gy747/wL6Ptuyu71Uz1dIv4vg3Q+d29dFQKTpFXaUUpax1AcfPV9N2H80wUYOr9kY8V0NwfhmT2T9nf19LPnlIaXioAGl7VU+K2irHvVDdqiWpsNYQpSlbPAAlj/1r68sRVtoGO3yviOJ7CSN/8HvMC3w7nzAqUlcK5YAhj0KNyxD6ExBSjY3Rx+c9YHjT+I0JgCehllMCA4oViWJdSvBZ4P56uZHQ4fCEwqga/aBt1Ne9oCSMUoNKYApj9dgGV/G51VddgD95J6DM+vQSgc1sGAqyqoQUNAiz4tFY7jEi07gF+CtqpOZnrOqVLO+bD4ZCmf8wSADeDwbxFYyZt/RRD4Z0f4J7MJ9ucfD/uO6NTFY157F95qW0KrhzKbJG9YHtLQsLqso4M3/+Cgv8aeuiqEwttzGB+SmuSttsFbdzdCZhN0N+zQ3bRDe8M+UjFod3PUtTiro/A7YyjOv8WhvaCtqu5MzzHVymkLK1Z+e9eZvLHlbyFmmw+Blbz509HsYEMYhv7Xt+Gb/j8AAJb9bdAOe6Druwmt25sw1krOW7xX8yl8HC5g6tMAXw9ocBtF+7FCxVaY//0YghOKAU1mSn9xKVhshb7vJkzHzsJ07CzyOq4gNKaAtrLGFCAwqQTDs2cCeXr27WYJpcF9fi3qAhrkQQOfBrgVbWKcAbBc7dtplNSosrAiZSmfUwZgM0XhAelnE1gBwItDI2W7vNU2OMLJ+MY2boN7ST2KXnsXw/NrkN/SrtjbwD8agd+Youf0oBf4Gw/9VZZjsWVCgdISzhTQgUnj4VpcD/OhdhiPneW8Zp/rKPvuvNDaIwPHdmZ6HunWqAUWI3PZnNmgwyDuFncGgRUAzPcB61z0b4LFVtzasBpUeKlj6LjCVsAB6Eh0JSBy3AA0mePnVEABbzk1rKWXbaLMJnirbfDbyuCttkWBzHi6A3mnP4P+aj/0PX34swZbvkKhqcH5kT3T486ERj2wGJnL5jwBGlzWxEcRWAF0FoaXnSM52m81rgZAWw23vfAqHM8sRdFr7wGAIlYVQKckft6SuHBppJWVTfLbyuCfMhm6q33Qur3QuD2s7+3P21+MPLQVwJNLl87rzvSYM6nsu8MplLlsjhW0U35N/KcEVgANqX92jNQYHFq2AMPzazC2cRuG59dAd9MO47GzGJ5XwwaHJqtIJzufnhvWYL4vBVNPo7zVNviqp4Aym2jn/C1Ht6d2xpNLl85rzfTY1CACLA6Zy+aUAfgtWMc8gRUjLr8VbRX0w/ryW3CuWILCHfvoQEqFloJNZgrHDcLHFVDAyy4N7gym4BKkQTH+NzuApgbnR1syPS41aVS9JRQrv73L7rd3vWWwlrcB1GxAehqbXITVAx7ggXC8VbDYisG1jwEGPYpe/wOoMQX0G7r/OEbHWfXdUGQf3+9NwL/liTvWp6Hwb3kUCkBvNs42RVyvnQDub3B+1JrpMalNBFg88tu7uv327q0Ga7mkNDa5CKsZfuDH7pFPBp9/HBrQ4Queursx5rV3EbhzIrQ3BxXzW7Xkxb8RFDPWj/V0FHxVMOsc8a0AHmxwfvT6W74r6n/VmQFl35+hDMlc1mAF7ZR/gu+4XISVJQS85RhxsjtXLGFLc1lffptdxuh7+hQLKxBysicaa6zuCmjwoC9x4QiVqBvA2gbnR3szPRC1S9V3UY0ylzXcDRpcs2M/44eV6lPlcrZJUcD/do4UkvDUVbF+KoDeNxgJLSU0pAG+V5g8rCI1PkRDa75fVT4uO4CtDc6PGjM9kGwRAZZMmcsaHkBEGptchdV6N9g3b4HSEtiff5yNtxrbtA0at0exoFBA/BtBufOnKNo5f2dIg7sC9LKxIohMLB13gnaqd6e95ywWAVYSCi8T11AUnkVCx3z2wmq+jwYWEL1PEKDrARbsblY8M8Ir+RRaRDnZ5cEqkcaHgKogXQ3nrvB/U6RW0KBqTVUHuSwCLAWUP7mhDHT81hPRn2QvrO4MAi8PxfutNG4PzIfakd/SHrcMDJSWsBuQLbubJVtdmYIVV5sjVpgGVUHaF5aMhjSwF1BY2+D8aGdSDY1yEWApqPzJDbNBg2t2NsOqgAL+2TkSHDo8vwbD82pgDqdHiVSw2ApftQ2euiror/bDvL9N1vKwJY8GlpSxyrt+8tusCsoGWBOALaN1O42SIsBKgfInNzwBUALbfMQp3bACaMvqrnBIELPPLRJUzO88dXcjdPsYmI6f5bS4xCobYMUlEQDbC/rtX7fsToiiRICVIuVPnm0FvcVng9w2MgGrSCd7rJhtI566qvAG5zNJ+7CyFVaxsoSAu4I0vP5nQHOmNKRZS/xUyosAK8XKnzy7DPTbxAeknJduWH3dT0ex3xUTnM74pXzVNgSLrWw+JyWKO+QKrCL6sgNUU+Da8S2KNU4UJQKsNCl/8uzZEJnGJp2wemyYznRgieiGWfINz69BoLSEdbQnys8kRzkIqy1hWNkVa5woTgRYaVb+5NlrQC8TrVyfpxNW61zRy79AaQmG59ewUey6G3aY97fRhSQUDAzNMVi1AngycO1Yt2INEyUUAVYGFPZvxaWxSfdG5qeHRz6J3G5j6LjCWUNPCeUQrLpBg6pVuatDJCQCrAwq7N/6LYDZ6fZZvRWR08q5Ygm81TYYT3fIDksQoxyBlZ2isDVw7VijoheHSJQIsFQg06TZD4Dd5pOeoNB/dtI/e6ttCE4qSSosQYxyBFY7KQprA9eO2RW9OESiRYClIpkm1TcCfNt85Cg+dOFBb/RyMNXKAVi1Aljr//LYGYUvDZFEEWCpTKZJ9VaISGPDpbsCwDwvha/7R9769WtpYOw1gs1+ELkcTLWyHFbdoEG1NwWXhkiGCLBUKtOk+ruRII0Nl+iCpokfUJeGLpHl0qTPuspiWNkBbAWwxf8lWf6pSQRYKpdpUv0TSFCtOlJ3BYBfOumH1P7896C/2gftsCdjFZDVtJFZonYCaPJ/ScIU1CiSIlnlCgxeOaMfU8ZUq74bCdI0D2mApWGfue7GIPR9NwGAzq/e05/WMWcprM4AWO7/8tjW0VJFORtFLKwskmlSfRk409jQet9ORUWsZ0JZCCs7aD/VztReGSIlRICVhTJNqp+N8DafWX5gli/a0Z4pZSGsmkD8VFklAqws1lvWbz4xKShUrTo9EudgV019x72graruNFwaIgWlzfQAiOTre/aPdgIoB9AUKC1BsNiasbGc1WUFrLoBNPi/PPYggVV2igAry9Xg/Mje4Pyo8Vbj6nLkGVozNY5+3m9SxmFlB21Rlfu/JHv/sllkSZhjOlr4zdmg9yeWpbPf35go/NHI9UnGYbUFdJiCPZ3Xgyg1IsDKUR0t/OYa8KSxUVrcPqyMwqoVwJNk6ZdbIkvCHFWD86MtoP1bW9LRX2ym0gzCqhvAg/4vjzUQWOWeiIU1CnS08Jt3Q8I2H7n66yKmWnNGYGUHsNX/JUn7kssiwBpFOlr4zQcQUa1aaf3QQuFTfUZgtRO0U92einkRqUcEWKNQRwu/2QjF09gATWYKxw1pDQptBe1Qb1X2ChGpVcSHNQrV4PyoEbR/a6eS7c73Sz9HJqy6QTvUGwisRpeIhTXKFQ6DCFerTl7HDRSazOKsLBmwsoOkfRnVIsAiAgAcLfzmExCRxkaMHi8MCQSSyoLVXpDtNKNeZElIBABocH60E0A16A3B9mTaSlC2nZVEWJ0B2U5DFBaxsIjidLTwm2WQUa2aEd+yUAKs7CBpX4hiRCwsojg1OD/qbnB+9CCABtAWjiQVUNx/ByXAqglAOYEVUayIhUUkqKbbvvlEIYXNOp4wiH4txfqt+jRAS140nUTCai+In4qIRwRYRKJ0tPCbVtCVqjcIHRu7JBQBq27QYQqtmZ4nkbpFgEUkSUcLv1l2S4PNPbrE/q3fGUci3gVgZQeoJv+Xx7Zkel5E2SECLCJZypv4jdkQSGMjAKstAJr8X/6XPdNzIcoeEWARJaW8id9YA440NjywagXwpP/L/+rO9NiJsk8EWERJK2/iN6ygobUGSAirbtCgas30eImyVwRYRIopb+I3yigKv0X0Nh87gK3+L/+rMdPjI8p+/f9RABURCvfGCwAAAABJRU5ErkJggg=="}]}],"model":"gpt-4.1-mini","temperature":0,"stream":false}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b5d9da3fbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:27 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '1609' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=zHPF_HVxmXn4KrxM3QnvQe_YZObUibuNDkpX6DeO6LU-1752092007-1.0.1.1-K68.JK3Gr4CHhNSiwypJBoh8SlXmN09NjgeG4Z9Eio090u9AfEOktfKIin4OC2T7poCYpcVW4baR7Sz1qQf4gBeZ8DT.eg.sVFLS1NDrDvU; + path=/; expires=Wed, 09-Jul-25 20:43:27 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=n2oPW9AhUbrVqcjpmLWZOw5pxQ0uW9ia7x5ADUNvlsI-1752092007742-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-reset-requests: 2ms + x-request-id: req_154ff4d0d7a1e6226cb1600186802af1 + body: + string: |- + { + "id": "resp_686ecd6616d4819a998835d2109773a6098389edbd29dc68", + "object": "response", + "created_at": 1752092006, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-mini-2025-04-14", + "output": [ + { + "id": "msg_686ecd669164819abff00918809caf7e098389edbd29dc68", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The image is inside a hexagon shape. It shows a red silhouette of a baseball player swinging a bat at a ball with \"www\" on it. Above the player is the text \"htr2\" in white cursive. The background is dark blue." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 213, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 54, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 267 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:27 +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Be terse."},{"role":"user","content":"What''s + in this image? (Be sure to mention the outside shape)"},{"type":"message","role":"user","content":[{"type":"input_image","image_url":"https://httr2.r-lib.org/logo.png"}]}],"model":"gpt-4.1-mini","temperature":0,"stream":false}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b6979c4fbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:29 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '1466' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=N.yCHntMW_BbXjTGLKtVRYnmj532DjrqrbEt7OG8e1Y-1752092009-1.0.1.1-oPYdg.B80WyV41ZuttNvHo2kpf8Y6bFHFFEpH4HD_lhQvOQ2EN56mBaor3AMrWyzsSyrkazegngd2dwbwbNcjd6W16iBhjw8BRycpdJotlk; + path=/; expires=Wed, 09-Jul-25 20:43:29 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=C4HEucpbuQHVOb5UZJ9vhv9qJGIJVBfxcucsyyWT2BI-1752092009480-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-reset-requests: 2ms + x-request-id: req_782cb6f9801f888542cccc6f4ed0eb9c + body: + string: |- + { + "id": "resp_686ecd67fbb08198aefcd3b0c40c452104413787094ed5c1", + "object": "response", + "created_at": 1752092008, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-mini-2025-04-14", + "output": [ + { + "id": "msg_686ecd68a2408198b209e5d3096b5eca04413787094ed5c1", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The image is inside a hexagon shape. It shows a red silhouette of a baseball player swinging a bat, with the text \"httr2\" above and a small ball labeled \"www\" near the bat. The background is dark blue." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 151, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 50, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 201 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:29 +recorded_with: VCR-vcr/1.7.0.91, webmockr/2.1.0 diff --git a/tests/testthat/_vcr/openai-v2-tool.yml b/tests/testthat/_vcr/openai-v2-tool.yml new file mode 100644 index 00000000..dac97d64 --- /dev/null +++ b/tests/testthat/_vcr/openai-v2-tool.yml @@ -0,0 +1,478 @@ +http_interactions: +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Always use a tool to answer. + Reply with ''It is ____.''."},{"role":"user","content":"What''s the current + date in Y-M-D format?"}],"model":"gpt-4.1-nano","temperature":0,"stream":false,"tools":[{"type":"function","name":"current_date","description":"Return + the current date","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}}]}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b355ffefbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:20 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '655' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=dEZLZ7JJO01ysLMlFZkiulBT_MURBo21xWXvlbfhGrc-1752092000-1.0.1.1-o_N3qQEumHJg_.RtBolD8mxG2t9wVOnEvn6bs2x8VNiAQnrwwYXFhguaQa4QjlZWIyWoNk5W3PP1Wj4ili_RenSPdGrmmGwzqrA6FP5wTxw; + path=/; expires=Wed, 09-Jul-25 20:43:20 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=0QZ8BzsIngM4fTc0IyAFkEAgdFPoDBvMHrKqew0xbeE-1752092000348-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-limit-tokens: '150000000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-remaining-tokens: '149999724' + x-ratelimit-reset-requests: 2ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_8f800070b1c98e15cc948fcdd03b3409 + body: + string: |- + { + "id": "resp_686ecd5fa7708198b54d73f42d3575e903f0dc7b529e5cc4", + "object": "response", + "created_at": 1752091999, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-nano-2025-04-14", + "output": [ + { + "id": "fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4", + "type": "function_call", + "status": "completed", + "arguments": "{}", + "call_id": "call_NVX5fmiDJcbNNksOv1ZLZpKd", + "name": "current_date" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Return the current date", + "name": "current_date", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 59, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 11, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 70 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:20 +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Always use a tool to answer. + Reply with ''It is ____.''."},{"role":"user","content":"What''s the current + date in Y-M-D format?"},{"type":"function_call","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","name":"current_date","arguments":"{}"},{"type":"function_call_output","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","output":"2024-01-01"}],"model":"gpt-4.1-nano","temperature":0,"stream":false,"tools":[{"type":"function","name":"current_date","description":"Return + the current date","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}}]}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b3bccccfbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:21 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '499' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=6HYLyA43jpx2_qbGyqK1bgEl_c5p3xPwcPrS9aaTSLI-1752092001-1.0.1.1-_zLVg1UtyhWABsZ8h442mXmh8kH8MgPcWbp6P2CTcgZ_qJqBRpvwWiKQpwcSZZbHZvLJi8I5yb73giA6gm0nk_AoCIii29UIyjazPj1l.Ss; + path=/; expires=Wed, 09-Jul-25 20:43:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=mDXOTZuG33jSJzFolMT_XRET5LIBzVgppvVW5tNNcyw-1752092001220-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-limit-tokens: '150000000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-remaining-tokens: '149999702' + x-ratelimit-reset-requests: 2ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_e4f2dae041ccd4d98ed439b899123393 + body: + string: |- + { + "id": "resp_686ecd60b1148199a1705bb6349b461908913f2ae1ee9fd1", + "object": "response", + "created_at": 1752092000, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-nano-2025-04-14", + "output": [ + { + "id": "msg_686ecd60f994819982c5e41636f7eb7f08913f2ae1ee9fd1", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "It is 2024-01-01." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Return the current date", + "name": "current_date", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 81, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 12, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 93 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:21 +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Always use a tool to answer. + Reply with ''It is ____.''."},{"role":"user","content":"What''s the current + date in Y-M-D format?"},{"type":"function_call","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","name":"current_date","arguments":"{}"},{"type":"function_call_output","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","output":"2024-01-01"},{"role":"assistant","content":"It + is 2024-01-01."},{"role":"user","content":"What month is it? Provide the full + name"}],"model":"gpt-4.1-nano","temperature":0,"stream":false,"tools":[{"type":"function","name":"current_date","description":"Return + the current date","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}},{"type":"function","name":"current_month","description":"Return + the full name of the current month","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}}]}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b41d92bfbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:22 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '1080' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=Fr.ZCC_iuBwD.Gcqe7kNssqd1DDRhaM7WyufIV2P8VI-1752092002-1.0.1.1-o8lL0pufSaYNQtmJndgqoinSxAG8R_XO8XCan1FeJhFqasyhkBRx76uSBXbyt3nBiojRnwtKGGYe2e0jVLHL5NORSY2IaFDYmbTmL5A.APM; + path=/; expires=Wed, 09-Jul-25 20:43:22 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=o7BVmzjPXPVtjEgv2Hr9sW3W4x1lGlqcqdVLsw.C9TI-1752092002776-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-limit-tokens: '150000000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-remaining-tokens: '149999658' + x-ratelimit-reset-requests: 2ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_2ca939f5e035903f941c4e629da0554b + body: + string: |- + { + "id": "resp_686ecd61a7e4819ab1b4c1fe76efbf810d79be5ad2e4c9f9", + "object": "response", + "created_at": 1752092001, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-nano-2025-04-14", + "output": [ + { + "id": "fc_686ecd626ef4819aa92fa44a81f469780d79be5ad2e4c9f9", + "type": "function_call", + "status": "completed", + "arguments": "{}", + "call_id": "call_ywiVsy2WsoQDgndJCNJGlITT", + "name": "current_month" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Return the current date", + "name": "current_date", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "description": "Return the full name of the current month", + "name": "current_month", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 125, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 11, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 136 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:23 +- request: + method: POST + uri: https://api.openai.com/v1/responses + body: + string: '{"input":[{"role":"system","content":"Always use a tool to answer. + Reply with ''It is ____.''."},{"role":"user","content":"What''s the current + date in Y-M-D format?"},{"type":"function_call","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","name":"current_date","arguments":"{}"},{"type":"function_call_output","call_id":"fc_686ecd602d648198912c8251e607a78803f0dc7b529e5cc4","output":"2024-01-01"},{"role":"assistant","content":"It + is 2024-01-01."},{"role":"user","content":"What month is it? Provide the full + name"},{"type":"function_call","call_id":"fc_686ecd626ef4819aa92fa44a81f469780d79be5ad2e4c9f9","name":"current_month","arguments":"{}"},{"type":"function_call_output","call_id":"fc_686ecd626ef4819aa92fa44a81f469780d79be5ad2e4c9f9","output":"February"}],"model":"gpt-4.1-nano","temperature":0,"stream":false,"tools":[{"type":"function","name":"current_date","description":"Return + the current date","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}},{"type":"function","name":"current_month","description":"Return + the full name of the current month","strict":true,"parameters":{"type":"object","description":"","properties":{},"required":[],"additionalProperties":false}}]}' + response: + status: 200 + headers: + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + cf-ray: 95ca7b4c8a5bfbc2-IAH + content-encoding: gzip + content-type: application/json + date: Wed, 09 Jul 2025 20:13:23 GMT + openai-organization: user-wfrajlxs4zxa4ixjljhmt9vx + openai-processing-ms: '514' + openai-version: '2020-10-01' + server: cloudflare + set-cookie: + - __cf_bm=7C_SnpkDbGY5UE1B9R4Xl5aTYnR7ZfdafniZHk9eSxM-1752092003-1.0.1.1-gmDOz153YcR4aK_l3Xv6Ne45.0N4kTJMDhF4iB0TBC4iupgyyLC1kwGFYIYcBDVaKZ08K.2v.5Q9TSMDdJXDf9hKvop3JZfUFIUK_7UJlWg; + path=/; expires=Wed, 09-Jul-25 20:43:23 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=7D6A9t2qxmCAvbnPOkFV2dI1._VBT1OQuITvQvVvV3Y-1752092003908-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + strict-transport-security: max-age=31536000; includeSubDomains; preload + x-content-type-options: nosniff + x-ratelimit-limit-requests: '30000' + x-ratelimit-limit-tokens: '150000000' + x-ratelimit-remaining-requests: '29999' + x-ratelimit-remaining-tokens: '149999641' + x-ratelimit-reset-requests: 2ms + x-ratelimit-reset-tokens: 0s + x-request-id: req_10603c5fb3997c1f17abfc7ddcc8ceeb + body: + string: |- + { + "id": "resp_686ecd635f50819b953d823ae8db8f3e06801cb0f54bbb71", + "object": "response", + "created_at": 1752092003, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-nano-2025-04-14", + "output": [ + { + "id": "msg_686ecd63af7c819b89fdcf9ee80a23b406801cb0f54bbb71", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "It is February." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "service_tier": "default", + "store": true, + "temperature": 0.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Return the current date", + "name": "current_date", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "description": "Return the full name of the current month", + "name": "current_month", + "parameters": { + "type": "object", + "description": "", + "properties": {}, + "required": [], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 142, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 148 + }, + "user": null, + "metadata": {} + } + recorded_at: 2025-07-09 20:13:23 +recorded_with: VCR-vcr/1.7.0.91, webmockr/2.1.0 diff --git a/tests/testthat/helper-provider.R b/tests/testthat/helper-provider.R index 84cb9a43..b352de43 100644 --- a/tests/testthat/helper-provider.R +++ b/tests/testthat/helper-provider.R @@ -36,15 +36,15 @@ test_tools_simple <- function(chat_fun) { name = "current_date", description = "Return the current date" )) + result <- chat$chat("What's the current date in Y-M-D format?") + expect_match(result, "2024-01-01") + chat$register_tool(tool( function() "February", name = "current_month", description = "Return the full name of the current month" )) - result <- chat$chat("What's the current date in Y-M-D format?") - expect_match(result, "2024-01-01") - result <- chat$chat("What month is it? Provide the full name") expect_match(result, "February") } diff --git a/tests/testthat/test-provider-openai.R b/tests/testthat/test-provider-openai.R index 92fe5e91..d06d27db 100644 --- a/tests/testthat/test-provider-openai.R +++ b/tests/testthat/test-provider-openai.R @@ -5,6 +5,9 @@ test_that("can make simple request", { resp <- chat$chat("What is 1 + 1?", echo = FALSE) expect_match(resp, "2") expect_equal(chat$last_turn()@tokens > 0, c(TRUE, TRUE)) + + resp <- chat$chat("Double that", echo = FALSE) + expect_match(resp, "4") }) test_that("can make simple streaming request", { @@ -17,21 +20,21 @@ test_that("can list models", { test_models(models_openai) }) - # Common provider interface ----------------------------------------------- test_that("defaults are reported", { expect_snapshot(. <- chat_openai()) }) -test_that("supports standard parameters", { - chat_fun <- chat_openai_test +# No longer supports stop parameter +# test_that("supports standard parameters", { +# chat_fun <- chat_openai_test - test_params_stop(chat_fun) -}) +# test_params_stop(chat_fun) +# }) test_that("supports tool calling", { - vcr::local_cassette("openai-tool") + # vcr::local_cassette("openai-v2-tool") chat_fun <- chat_openai_test test_tools_simple(chat_fun) @@ -44,7 +47,7 @@ test_that("can extract data", { }) test_that("can use images", { - vcr::local_cassette("openai-image") + # vcr::local_cassette("openai-v2-image") # Needs mini to get shape correct chat_fun <- \(...) chat_openai_test(model = "gpt-4.1-mini", ...) @@ -56,13 +59,8 @@ test_that("can use images", { test_that("can retrieve log_probs (#115)", { chat <- chat_openai_test(params = params(log_probs = TRUE)) - pieces <- coro::collect(chat$stream("Hi")) - - logprobs <- chat$last_turn()@json$choices[[1]]$logprobs$content - expect_equal( - length(logprobs), - length(pieces) - 2 # leading "" + trailing \n - ) + chat$chat("Hi") + expect_length(chat$last_turn()@json$output[[1]]$content[[1]]$logprobs, 2) }) # Custom ----------------------------------------------------------------- @@ -87,11 +85,3 @@ test_that("as_json specialised for OpenAI", { ) ) }) - -test_that("seed is deprecated, but still honored", { - expect_snapshot(chat <- chat_openai_test(seed = 1)) - expect_equal(chat$get_provider()@params$seed, 1) - - # NULL is also ignored since that's what subclasses use - expect_no_warning(chat_openai_test(seed = NULL)) -})