diff --git a/R/provider-claude.R b/R/provider-claude.R index 10212ade..9cbcb48e 100644 --- a/R/provider-claude.R +++ b/R/provider-claude.R @@ -201,10 +201,17 @@ method(chat_body, ProviderAnthropic) <- function( })) if (!is.null(type)) { + # Build description based on whether the type is required + if (type@required) { + description <- "Extract structured data from the conversation." + } else { + description <- "Extract structured data from the conversation. The data parameter is optional - if you cannot find the requested information in the conversation, do not include the data parameter in your tool call at all and instead return an empty object {}." + } + tool_def <- ToolDef( function(...) {}, name = "_structured_tool_call", - description = "Extract structured data", + description = description, arguments = type_object(data = type) ) tools[[tool_def@name]] <- tool_def diff --git a/tests/testthat/_vcr/anthropic-optional-array.yml b/tests/testthat/_vcr/anthropic-optional-array.yml new file mode 100644 index 00000000..d6401dd9 --- /dev/null +++ b/tests/testthat/_vcr/anthropic-optional-array.yml @@ -0,0 +1,42 @@ +http_interactions: +- request: + method: POST + uri: https://api.anthropic.com/v1/messages + body: + string: '{"model":"claude-sonnet-4-5-20250929","messages":[{"role":"user","content":[{"type":"text","text":"\n Apples + are tasty.\n Oranges are orange in colour.\n Bananas are nutritious, + and their colour is yellow.\n ","cache_control":{"type":"ephemeral","ttl":"5m"}}]}],"stream":false,"tools":[{"name":"_structured_tool_call","description":"Extract + structured data from the conversation.","input_schema":{"type":"object","description":"","properties":{"data":{"type":"array","description":"","items":{"type":"object","description":"","properties":{"name":{"type":"string","description":"Name + of the fruit"},"adjective":{"type":"string","description":"Descriptive adjective"},"colour":{"type":"string","description":"Colour + of the fruit"}},"required":[],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}],"tool_choice":{"type":"tool","name":"_structured_tool_call"},"temperature":0,"max_tokens":4096}' + response: + status: 200 + headers: + date: Fri, 05 Dec 2025 08:12:17 GMT + content-type: application/json + content-encoding: gzip + anthropic-ratelimit-input-tokens-limit: '800000' + anthropic-ratelimit-input-tokens-remaining: '800000' + anthropic-ratelimit-input-tokens-reset: '2025-12-05T08:12:16Z' + anthropic-ratelimit-output-tokens-limit: '160000' + anthropic-ratelimit-output-tokens-remaining: '160000' + anthropic-ratelimit-output-tokens-reset: '2025-12-05T08:12:17Z' + anthropic-ratelimit-requests-limit: '2000' + anthropic-ratelimit-requests-remaining: '1999' + anthropic-ratelimit-requests-reset: '2025-12-05T08:12:14Z' + retry-after: '44' + anthropic-ratelimit-tokens-limit: '960000' + anthropic-ratelimit-tokens-remaining: '960000' + anthropic-ratelimit-tokens-reset: '2025-12-05T08:12:16Z' + request-id: req_011CVoCkZTT5FhrTPHNZZGk8 + strict-transport-security: max-age=31536000; includeSubDomains; preload + anthropic-organization-id: 67bb8feb-7bca-4369-9e04-339d73646b40 + x-envoy-upstream-service-time: '2437' + cf-cache-status: DYNAMIC + x-robots-tag: none + server: cloudflare + cf-ray: 9a9213d02dc5bed7-LHR + body: + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01TuUbEjV1DaYmCNGAnJKvDe","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DhZa16HfD6kPuTfWGd8L4z","name":"_structured_tool_call","input":{"data":[{"name":"Apples","adjective":"tasty","colour":""},{"name":"Oranges","adjective":"","colour":"orange"},{"name":"Bananas","adjective":"nutritious","colour":"yellow"}]}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":798,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":97,"service_tier":"standard"}}' + recorded_at: 2025-12-05 08:12:17 +recorded_with: VCR-vcr/2.0.0 diff --git a/tests/testthat/_vcr/anthropic-optional-types.yml b/tests/testthat/_vcr/anthropic-optional-types.yml new file mode 100644 index 00000000..a0669d58 --- /dev/null +++ b/tests/testthat/_vcr/anthropic-optional-types.yml @@ -0,0 +1,86 @@ +http_interactions: +- request: + method: POST + uri: https://api.anthropic.com/v1/messages + body: + string: '{"model":"claude-sonnet-4-5-20250929","messages":[{"role":"user","content":[{"type":"text","text":"\n # + Apples are tasty\n By Hadley Wickham\n\n Apples are delicious and tasty + and I like to eat them.\n ","cache_control":{"type":"ephemeral","ttl":"5m"}}]}],"stream":false,"tools":[{"name":"_structured_tool_call","description":"Extract + structured data from the conversation. The data parameter is optional - if + you cannot find the requested information in the conversation, do not include + the data parameter in your tool call at all and instead return an empty object + {}.","input_schema":{"type":"object","description":"","properties":{"data":{"type":"string","description":"The + publication year"}},"additionalProperties":false}}],"tool_choice":{"type":"tool","name":"_structured_tool_call"},"temperature":0,"max_tokens":4096}' + response: + status: 200 + headers: + date: Fri, 05 Dec 2025 07:39:22 GMT + content-type: application/json + content-encoding: gzip + anthropic-ratelimit-input-tokens-limit: '800000' + anthropic-ratelimit-input-tokens-remaining: '800000' + anthropic-ratelimit-input-tokens-reset: '2025-12-05T07:39:22Z' + anthropic-ratelimit-output-tokens-limit: '160000' + anthropic-ratelimit-output-tokens-remaining: '160000' + anthropic-ratelimit-output-tokens-reset: '2025-12-05T07:39:22Z' + anthropic-ratelimit-requests-limit: '2000' + anthropic-ratelimit-requests-remaining: '1999' + anthropic-ratelimit-requests-reset: '2025-12-05T07:39:19Z' + retry-after: '41' + anthropic-ratelimit-tokens-limit: '960000' + anthropic-ratelimit-tokens-remaining: '960000' + anthropic-ratelimit-tokens-reset: '2025-12-05T07:39:22Z' + request-id: req_011CVoAEycSKZ6jciJ3j2A2u + strict-transport-security: max-age=31536000; includeSubDomains; preload + anthropic-organization-id: 67bb8feb-7bca-4369-9e04-339d73646b40 + x-envoy-upstream-service-time: '2555' + cf-cache-status: DYNAMIC + x-robots-tag: none + server: cloudflare + cf-ray: 9a91e3989b9ad8f4-LHR + body: + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_013d28qAN2EqrYqqzD1VAZx4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FaYVmT1FMbLYrnPKbH3hSM","name":"_structured_tool_call","input":{}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":747,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":16,"service_tier":"standard"}}' + recorded_at: 2025-12-05 07:39:22 +- request: + method: POST + uri: https://api.anthropic.com/v1/messages + body: + string: '{"model":"claude-sonnet-4-5-20250929","messages":[{"role":"user","content":[{"type":"text","text":"\n # + Apples are tasty\n By Hadley Wickham\n\n Apples are delicious and tasty + and I like to eat them.\n ","cache_control":{"type":"ephemeral","ttl":"5m"}}]}],"stream":false,"tools":[{"name":"_structured_tool_call","description":"Extract + structured data from the conversation. The data parameter is optional - if + you cannot find the requested information in the conversation, do not include + the data parameter in your tool call at all and instead return an empty object + {}.","input_schema":{"type":"object","description":"","properties":{"data":{"type":"integer","description":"Number + of citations"}},"additionalProperties":false}}],"tool_choice":{"type":"tool","name":"_structured_tool_call"},"temperature":0,"max_tokens":4096}' + response: + status: 200 + headers: + date: Fri, 05 Dec 2025 07:39:24 GMT + content-type: application/json + content-encoding: gzip + anthropic-ratelimit-input-tokens-limit: '800000' + anthropic-ratelimit-input-tokens-remaining: '800000' + anthropic-ratelimit-input-tokens-reset: '2025-12-05T07:39:24Z' + anthropic-ratelimit-output-tokens-limit: '160000' + anthropic-ratelimit-output-tokens-remaining: '160000' + anthropic-ratelimit-output-tokens-reset: '2025-12-05T07:39:24Z' + anthropic-ratelimit-requests-limit: '2000' + anthropic-ratelimit-requests-remaining: '1999' + anthropic-ratelimit-requests-reset: '2025-12-05T07:39:22Z' + retry-after: '36' + anthropic-ratelimit-tokens-limit: '960000' + anthropic-ratelimit-tokens-remaining: '960000' + anthropic-ratelimit-tokens-reset: '2025-12-05T07:39:24Z' + request-id: req_011CVoAFBtdKmapGxb4cYxBC + strict-transport-security: max-age=31536000; includeSubDomains; preload + anthropic-organization-id: 67bb8feb-7bca-4369-9e04-339d73646b40 + x-envoy-upstream-service-time: '2018' + cf-cache-status: DYNAMIC + x-robots-tag: none + server: cloudflare + cf-ray: 9a91e3aa8cb4d8f4-LHR + body: + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01LBMNcH1UUFuERrsJRbcokH","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016sKGcJdpuPz717qks1YX8f","name":"_structured_tool_call","input":{}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":747,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":16,"service_tier":"standard"}}' + recorded_at: 2025-12-05 07:39:24 +recorded_with: VCR-vcr/2.0.0 diff --git a/tests/testthat/_vcr/anthropic-structured-data.yml b/tests/testthat/_vcr/anthropic-structured-data.yml index 9d606e8f..e6a9329b 100644 --- a/tests/testthat/_vcr/anthropic-structured-data.yml +++ b/tests/testthat/_vcr/anthropic-structured-data.yml @@ -7,40 +7,40 @@ http_interactions: Apples are tasty\n By Hadley Wickham\n\n Apples are delicious and tasty and I like to eat them.\n Except for red delicious, that is. They are NOT delicious.\n ","cache_control":{"type":"ephemeral","ttl":"5m"}}]}],"stream":false,"tools":[{"name":"_structured_tool_call","description":"Extract - structured data","input_schema":{"type":"object","description":"","properties":{"data":{"type":"object","description":"Summary + structured data from the conversation.","input_schema":{"type":"object","description":"","properties":{"data":{"type":"object","description":"Summary of the article. Preserve existing case.","properties":{"title":{"type":"string","description":"Content title"},"author":{"type":"string","description":"Name of the author"}},"required":["title","author"],"additionalProperties":false}},"required":["data"],"additionalProperties":false}}],"tool_choice":{"type":"tool","name":"_structured_tool_call"},"temperature":0,"max_tokens":4096}' response: status: 200 headers: - date: Thu, 06 Nov 2025 14:56:32 GMT + date: Fri, 05 Dec 2025 07:43:54 GMT content-type: application/json content-encoding: gzip - anthropic-ratelimit-input-tokens-limit: '2000000' - anthropic-ratelimit-input-tokens-remaining: '2000000' - anthropic-ratelimit-input-tokens-reset: '2025-11-06T14:56:31Z' - anthropic-ratelimit-output-tokens-limit: '400000' - anthropic-ratelimit-output-tokens-remaining: '400000' - anthropic-ratelimit-output-tokens-reset: '2025-11-06T14:56:32Z' - anthropic-ratelimit-requests-limit: '4000' - anthropic-ratelimit-requests-remaining: '3999' - anthropic-ratelimit-requests-reset: '2025-11-06T14:56:29Z' - retry-after: '30' - anthropic-ratelimit-tokens-limit: '2400000' - anthropic-ratelimit-tokens-remaining: '2400000' - anthropic-ratelimit-tokens-reset: '2025-11-06T14:56:31Z' - request-id: req_011CUrq7wbHdfAWEpWUMRV2b + anthropic-ratelimit-input-tokens-limit: '800000' + anthropic-ratelimit-input-tokens-remaining: '800000' + anthropic-ratelimit-input-tokens-reset: '2025-12-05T07:43:54Z' + anthropic-ratelimit-output-tokens-limit: '160000' + anthropic-ratelimit-output-tokens-remaining: '160000' + anthropic-ratelimit-output-tokens-reset: '2025-12-05T07:43:54Z' + anthropic-ratelimit-requests-limit: '2000' + anthropic-ratelimit-requests-remaining: '1999' + anthropic-ratelimit-requests-reset: '2025-12-05T07:43:51Z' + retry-after: '6' + anthropic-ratelimit-tokens-limit: '960000' + anthropic-ratelimit-tokens-remaining: '960000' + anthropic-ratelimit-tokens-reset: '2025-12-05T07:43:54Z' + request-id: req_011CVoAayqdyJJYXTPU3ERa4 strict-transport-security: max-age=31536000; includeSubDomains; preload - anthropic-organization-id: bf3687d8-1b59-41eb-9761-a1fd70b24e7e - x-envoy-upstream-service-time: '2715' + anthropic-organization-id: 67bb8feb-7bca-4369-9e04-339d73646b40 + x-envoy-upstream-service-time: '3661' cf-cache-status: DYNAMIC x-robots-tag: none server: cloudflare - cf-ray: 99a570198c6569d0-DFW + cf-ray: 9a91ea38afbb9425-LHR body: - string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01G8c8rPFym6gBoDELWsWGnz","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TL6NrFX7XEQzBDcx1Vsvf8","name":"_structured_tool_call","input":{"data":{"title":"Apples - are tasty","author":"Hadley Wickham"}}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":792,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":54,"service_tier":"standard"}}' - recorded_at: 2025-11-06 14:56:32 + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01RTMVVq1U5Uc2pg5AbYgGW1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Mw9Xb4RHj828fCu8e2FE77","name":"_structured_tool_call","input":{"data":{"title":"Apples + are tasty","author":"Hadley Wickham"}}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":795,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":54,"service_tier":"standard"}}' + recorded_at: 2025-12-05 07:43:54 - request: method: POST uri: https://api.anthropic.com/v1/messages @@ -53,38 +53,38 @@ http_interactions: Apples are tasty\n By Hadley Wickham\n\n Apples are delicious and tasty and I like to eat them.\n Except for red delicious, that is. They are NOT delicious.\n ","cache_control":{"type":"ephemeral","ttl":"5m"}}]}],"stream":false,"tools":[{"name":"_structured_tool_call","description":"Extract - structured data","input_schema":{"type":"object","description":"","properties":{"data":{"type":"object","description":"Summary + structured data from the conversation.","input_schema":{"type":"object","description":"","properties":{"data":{"type":"object","description":"Summary of the article. Preserve existing case.","properties":{"title":{"type":"string","description":"Content title"},"author":{"type":"string","description":"Name of the author"}},"required":["title","author"],"additionalProperties":false}},"required":["data"],"additionalProperties":false}}],"tool_choice":{"type":"tool","name":"_structured_tool_call"},"temperature":0,"max_tokens":4096}' response: status: 200 headers: - date: Thu, 06 Nov 2025 14:56:35 GMT + date: Fri, 05 Dec 2025 07:43:57 GMT content-type: application/json content-encoding: gzip - anthropic-ratelimit-input-tokens-limit: '2000000' - anthropic-ratelimit-input-tokens-remaining: '2000000' - anthropic-ratelimit-input-tokens-reset: '2025-11-06T14:56:35Z' - anthropic-ratelimit-output-tokens-limit: '400000' - anthropic-ratelimit-output-tokens-remaining: '400000' - anthropic-ratelimit-output-tokens-reset: '2025-11-06T14:56:35Z' - anthropic-ratelimit-requests-limit: '4000' - anthropic-ratelimit-requests-remaining: '3999' - anthropic-ratelimit-requests-reset: '2025-11-06T14:56:32Z' - retry-after: '25' - anthropic-ratelimit-tokens-limit: '2400000' - anthropic-ratelimit-tokens-remaining: '2400000' - anthropic-ratelimit-tokens-reset: '2025-11-06T14:56:35Z' - request-id: req_011CUrq89vwiaG9F6D4Q6WLN + anthropic-ratelimit-input-tokens-limit: '800000' + anthropic-ratelimit-input-tokens-remaining: '800000' + anthropic-ratelimit-input-tokens-reset: '2025-12-05T07:43:57Z' + anthropic-ratelimit-output-tokens-limit: '160000' + anthropic-ratelimit-output-tokens-remaining: '160000' + anthropic-ratelimit-output-tokens-reset: '2025-12-05T07:43:57Z' + anthropic-ratelimit-requests-limit: '2000' + anthropic-ratelimit-requests-remaining: '1999' + anthropic-ratelimit-requests-reset: '2025-12-05T07:43:55Z' + retry-after: '3' + anthropic-ratelimit-tokens-limit: '960000' + anthropic-ratelimit-tokens-remaining: '960000' + anthropic-ratelimit-tokens-reset: '2025-12-05T07:43:57Z' + request-id: req_011CVoAbGhu9WXeAHDimiYbf strict-transport-security: max-age=31536000; includeSubDomains; preload - anthropic-organization-id: bf3687d8-1b59-41eb-9761-a1fd70b24e7e - x-envoy-upstream-service-time: '3197' + anthropic-organization-id: 67bb8feb-7bca-4369-9e04-339d73646b40 + x-envoy-upstream-service-time: '2765' cf-cache-status: DYNAMIC x-robots-tag: none server: cloudflare - cf-ray: 99a5702baf0d69d0-DFW + cf-ray: 9a91ea515f339425-LHR body: - string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01GoxxFUPozvUG66BGrR8Z6b","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BGXTSbLiVWVFax94GWPaje","name":"_structured_tool_call","input":{"data":{"title":"Apples - are tasty","author":"Hadley Wickham"}}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":874,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":54,"service_tier":"standard"}}' - recorded_at: 2025-11-06 14:56:35 + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01Q8ATv6AGSKM31BRC6FxBSf","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01M18dY77VQWkpExzqhHYp5a","name":"_structured_tool_call","input":{"data":{"title":"Apples + are tasty","author":"Hadley Wickham"}}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":877,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":54,"service_tier":"standard"}}' + recorded_at: 2025-12-05 07:43:57 recorded_with: VCR-vcr/2.0.0 diff --git a/tests/testthat/test-provider-claude.R b/tests/testthat/test-provider-claude.R index 24c26b7c..95b55864 100644 --- a/tests/testthat/test-provider-claude.R +++ b/tests/testthat/test-provider-claude.R @@ -145,3 +145,56 @@ test_that("batch chat works", { ) expect_equal(out, c("Des Moines", "Albany", "Sacramento", "Austin")) }) + +test_that("optional types return NULL when data unavailable", { + vcr::local_cassette("anthropic-optional-types") + + prompt <- " + # Apples are tasty + By Hadley Wickham + + Apples are delicious and tasty and I like to eat them. + " + + chat <- chat_anthropic_test() + result <- chat$chat_structured( + prompt, + type = type_string("The publication year", required = FALSE) + ) + expect_null(result) + + chat2 <- chat_anthropic_test() + result2 <- chat2$chat_structured( + prompt, + type = type_integer("Number of citations", required = FALSE) + ) + expect_null(result2) +}) + +test_that("optional fields in arrays become NA when missing", { + vcr::local_cassette("anthropic-optional-array") + + prompt <- " + Apples are tasty. + Oranges are orange in colour. + Bananas are nutritious, and their colour is yellow. + " + + chat <- chat_anthropic_test() + result <- chat$chat_structured( + prompt, + type = type_array( + type_object( + name = type_string("Name of the fruit", required = FALSE), + adjective = type_string("Descriptive adjective", required = FALSE), + colour = type_string("Colour of the fruit", required = FALSE) + ) + ) + ) + + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 3) + expect_equal(result$name, c("Apples", "Oranges", "Bananas")) + expect_equal(result$adjective, c("tasty", "", "nutritious")) + expect_equal(result$colour, c("", "orange", "yellow")) +})