anthropic 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +1 -1
  4. data/lib/anthropic/client.rb +8 -0
  5. data/lib/anthropic/helpers/bedrock/client.rb +5 -0
  6. data/lib/anthropic/helpers/bedrock/mantle_client.rb +5 -0
  7. data/lib/anthropic/helpers/stainless_helper_header.rb +1 -0
  8. data/lib/anthropic/helpers/threads/beta_message_thread.rb +635 -0
  9. data/lib/anthropic/helpers/tools/runner.rb +79 -11
  10. data/lib/anthropic/helpers/vertex/client.rb +5 -0
  11. data/lib/anthropic/internal/report_page_cursor.rb +2 -2
  12. data/lib/anthropic/internal/tracing.rb +2 -0
  13. data/lib/anthropic/models/anthropic_beta.rb +6 -0
  14. data/lib/anthropic/models/beta/beta_message_thread_param.rb +61 -0
  15. data/lib/anthropic/models/beta/beta_thread_continue_params.rb +29 -0
  16. data/lib/anthropic/models/beta/beta_thread_create_params.rb +21 -0
  17. data/lib/anthropic/models/beta/message_create_params.rb +13 -1
  18. data/lib/anthropic/models/beta/messages/batch_create_params.rb +13 -1
  19. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_cost_group.rb +271 -0
  20. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_cost_group_by.rb +40 -0
  21. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_cost_item.rb +57 -0
  22. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_cost_metrics.rb +57 -0
  23. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_cost_order_by.rb +22 -0
  24. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_usage_group.rb +247 -0
  25. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_usage_group_by.rb +38 -0
  26. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_usage_item.rb +57 -0
  27. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_usage_metrics.rb +79 -0
  28. data/lib/anthropic/models/beta/organization/analytics/beta_analytics_usage_order_by.rb +24 -0
  29. data/lib/anthropic/models/beta/organization/analytics/cost_list_params.rb +360 -0
  30. data/lib/anthropic/models/beta/organization/analytics/usage_list_params.rb +353 -0
  31. data/lib/anthropic/models/beta/organization/beta_analytics_actor.rb +81 -0
  32. data/lib/anthropic/models/beta/organization/beta_analytics_actor_type.rb +22 -0
  33. data/lib/anthropic/models/beta/organization/beta_analytics_api_actor.rb +27 -0
  34. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor.rb +42 -0
  35. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor_aws_provider.rb +35 -0
  36. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor_azure_provider.rb +27 -0
  37. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor_gcp_provider.rb +27 -0
  38. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor_oidc_provider.rb +27 -0
  39. data/lib/anthropic/models/beta/organization/beta_analytics_federated_actor_provider.rb +72 -0
  40. data/lib/anthropic/models/beta/organization/beta_analytics_grouped_cost_type.rb +21 -0
  41. data/lib/anthropic/models/beta/organization/beta_analytics_report_user_actor.rb +65 -0
  42. data/lib/anthropic/models/beta/organization/beta_analytics_service_account_actor.rb +27 -0
  43. data/lib/anthropic/models/beta/organization/beta_analytics_service_tier.rb +23 -0
  44. data/lib/anthropic/models/beta/organization/beta_analytics_speed.rb +19 -0
  45. data/lib/anthropic/models/beta/organization/beta_analytics_time_bucket_width.rb +20 -0
  46. data/lib/anthropic/models/beta/organization/beta_analytics_usage_cost_inference_geo.rb +19 -0
  47. data/lib/anthropic/models/beta/organization/beta_analytics_usage_cost_product_filter.rb +34 -0
  48. data/lib/anthropic/models/beta/organization/beta_analytics_usage_cost_sort_order.rb +19 -0
  49. data/lib/anthropic/models/beta/organization/beta_organization_rate_limit.rb +2 -1
  50. data/lib/anthropic/models/beta/organization/beta_plugin.rb +12 -6
  51. data/lib/anthropic/models/beta/organization/beta_plugin_content_scan.rb +11 -5
  52. data/lib/anthropic/models/beta/organization/workspaces/beta_workspace_rate_limit.rb +2 -1
  53. data/lib/anthropic/models/organization/organization_rate_limit.rb +2 -1
  54. data/lib/anthropic/models/organization/workspaces/workspace_rate_limit.rb +2 -1
  55. data/lib/anthropic/resources/beta/messages.rb +78 -4
  56. data/lib/anthropic/resources/beta/organization/analytics/cost.rb +175 -0
  57. data/lib/anthropic/resources/beta/organization/analytics/cost_report.rb +13 -6
  58. data/lib/anthropic/resources/beta/organization/analytics/usage.rb +176 -0
  59. data/lib/anthropic/resources/beta/organization/analytics/usage_report.rb +9 -2
  60. data/lib/anthropic/resources/beta/organization/analytics/user_cost_report.rb +12 -5
  61. data/lib/anthropic/resources/beta/organization/analytics/user_usage_report.rb +15 -8
  62. data/lib/anthropic/resources/beta/organization/analytics.rb +8 -0
  63. data/lib/anthropic/resources/beta/organization/plugin_marketplaces.rb +6 -6
  64. data/lib/anthropic/resources/beta/organization/plugins.rb +12 -9
  65. data/lib/anthropic/version.rb +1 -1
  66. data/lib/anthropic.rb +36 -0
  67. data/rbi/anthropic/client.rbi +5 -0
  68. data/rbi/anthropic/helpers/bedrock/client.rbi +5 -0
  69. data/rbi/anthropic/helpers/bedrock/mantle_client.rbi +5 -0
  70. data/rbi/anthropic/helpers/threads/beta_message_thread.rbi +100 -0
  71. data/rbi/anthropic/helpers/vertex/client.rbi +5 -0
  72. data/rbi/anthropic/internal/tracing.rbi +4 -0
  73. data/rbi/anthropic/models/anthropic_beta.rbi +10 -0
  74. data/rbi/anthropic/models/beta/beta_message_thread_param.rbi +80 -0
  75. data/rbi/anthropic/models/beta/beta_thread_continue_params.rbi +43 -0
  76. data/rbi/anthropic/models/beta/beta_thread_create_params.rbi +31 -0
  77. data/rbi/anthropic/models/beta/message_create_params.rbi +37 -0
  78. data/rbi/anthropic/models/beta/messages/batch_create_params.rbi +37 -0
  79. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_cost_group.rbi +439 -0
  80. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_cost_group_by.rbi +140 -0
  81. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_cost_item.rbi +110 -0
  82. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_cost_metrics.rbi +79 -0
  83. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_cost_order_by.rbi +50 -0
  84. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_usage_group.rbi +399 -0
  85. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_usage_group_by.rbi +130 -0
  86. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_usage_item.rbi +110 -0
  87. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_usage_metrics.rbi +120 -0
  88. data/rbi/anthropic/models/beta/organization/analytics/beta_analytics_usage_order_by.rbi +60 -0
  89. data/rbi/anthropic/models/beta/organization/analytics/cost_list_params.rbi +624 -0
  90. data/rbi/anthropic/models/beta/organization/analytics/usage_list_params.rbi +612 -0
  91. data/rbi/anthropic/models/beta/organization/beta_analytics_actor.rbi +134 -0
  92. data/rbi/anthropic/models/beta/organization/beta_analytics_actor_type.rbi +54 -0
  93. data/rbi/anthropic/models/beta/organization/beta_analytics_api_actor.rbi +40 -0
  94. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor.rbi +75 -0
  95. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor_aws_provider.rbi +54 -0
  96. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor_azure_provider.rbi +42 -0
  97. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor_gcp_provider.rbi +42 -0
  98. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor_oidc_provider.rbi +42 -0
  99. data/rbi/anthropic/models/beta/organization/beta_analytics_federated_actor_provider.rbi +107 -0
  100. data/rbi/anthropic/models/beta/organization/beta_analytics_grouped_cost_type.rbi +53 -0
  101. data/rbi/anthropic/models/beta/organization/beta_analytics_report_user_actor.rbi +94 -0
  102. data/rbi/anthropic/models/beta/organization/beta_analytics_service_account_actor.rbi +42 -0
  103. data/rbi/anthropic/models/beta/organization/beta_analytics_service_tier.rbi +63 -0
  104. data/rbi/anthropic/models/beta/organization/beta_analytics_speed.rbi +40 -0
  105. data/rbi/anthropic/models/beta/organization/beta_analytics_time_bucket_width.rbi +48 -0
  106. data/rbi/anthropic/models/beta/organization/beta_analytics_usage_cost_inference_geo.rbi +43 -0
  107. data/rbi/anthropic/models/beta/organization/beta_analytics_usage_cost_product_filter.rbi +98 -0
  108. data/rbi/anthropic/models/beta/organization/beta_analytics_usage_cost_sort_order.rbi +43 -0
  109. data/rbi/anthropic/models/beta/organization/beta_organization_rate_limit.rbi +4 -2
  110. data/rbi/anthropic/models/beta/organization/beta_plugin.rbi +20 -8
  111. data/rbi/anthropic/models/beta/organization/beta_plugin_content_scan.rbi +15 -6
  112. data/rbi/anthropic/models/beta/organization/workspaces/beta_workspace_rate_limit.rbi +4 -2
  113. data/rbi/anthropic/models/organization/organization_rate_limit.rbi +4 -2
  114. data/rbi/anthropic/models/organization/workspaces/workspace_rate_limit.rbi +4 -2
  115. data/rbi/anthropic/resources/beta/messages.rbi +191 -0
  116. data/rbi/anthropic/resources/beta/organization/analytics/cost.rbi +245 -0
  117. data/rbi/anthropic/resources/beta/organization/analytics/cost_report.rbi +12 -5
  118. data/rbi/anthropic/resources/beta/organization/analytics/usage.rbi +240 -0
  119. data/rbi/anthropic/resources/beta/organization/analytics/usage_report.rbi +9 -2
  120. data/rbi/anthropic/resources/beta/organization/analytics/user_cost_report.rbi +12 -5
  121. data/rbi/anthropic/resources/beta/organization/analytics/user_usage_report.rbi +14 -7
  122. data/rbi/anthropic/resources/beta/organization/analytics.rbi +10 -0
  123. data/rbi/anthropic/resources/beta/organization/plugin_marketplaces.rbi +6 -6
  124. data/rbi/anthropic/resources/beta/organization/plugins.rbi +12 -9
  125. data/rbi/lib/anthropic/helpers/stainless_helper_header.rbi +2 -0
  126. data/sig/anthropic/client.rbs +2 -0
  127. data/sig/anthropic/helpers/bedrock/client.rbs +2 -0
  128. data/sig/anthropic/helpers/threads/beta_message_thread.rbs +40 -0
  129. data/sig/anthropic/helpers/tools/runner.rbs +40 -0
  130. data/sig/anthropic/helpers/vertex/client.rbs +2 -0
  131. data/sig/anthropic/internal/tracing.rbs +2 -0
  132. data/sig/anthropic/models/anthropic_beta.rbs +4 -0
  133. data/sig/anthropic/models/beta/beta_message_thread_param.rbs +40 -0
  134. data/sig/anthropic/models/beta/beta_thread_continue_params.rbs +20 -0
  135. data/sig/anthropic/models/beta/beta_thread_create_params.rbs +17 -0
  136. data/sig/anthropic/models/beta/message_create_params.rbs +5 -0
  137. data/sig/anthropic/models/beta/messages/batch_create_params.rbs +5 -0
  138. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_cost_group.rbs +126 -0
  139. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_cost_group_by.rbs +60 -0
  140. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_cost_item.rbs +41 -0
  141. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_cost_metrics.rbs +41 -0
  142. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_cost_order_by.rbs +21 -0
  143. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_usage_group.rbs +116 -0
  144. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_usage_group_by.rbs +56 -0
  145. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_usage_item.rbs +41 -0
  146. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_usage_metrics.rbs +56 -0
  147. data/sig/anthropic/models/beta/organization/analytics/beta_analytics_usage_order_by.rbs +28 -0
  148. data/sig/anthropic/models/beta/organization/analytics/cost_list_params.rbs +188 -0
  149. data/sig/anthropic/models/beta/organization/analytics/usage_list_params.rbs +188 -0
  150. data/sig/anthropic/models/beta/organization/beta_analytics_actor.rbs +67 -0
  151. data/sig/anthropic/models/beta/organization/beta_analytics_actor_type.rbs +21 -0
  152. data/sig/anthropic/models/beta/organization/beta_analytics_api_actor.rbs +19 -0
  153. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor.rbs +34 -0
  154. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor_aws_provider.rbs +30 -0
  155. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor_azure_provider.rbs +20 -0
  156. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor_gcp_provider.rbs +20 -0
  157. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor_oidc_provider.rbs +20 -0
  158. data/sig/anthropic/models/beta/organization/beta_analytics_federated_actor_provider.rbs +60 -0
  159. data/sig/anthropic/models/beta/organization/beta_analytics_grouped_cost_type.rbs +21 -0
  160. data/sig/anthropic/models/beta/organization/beta_analytics_report_user_actor.rbs +44 -0
  161. data/sig/anthropic/models/beta/organization/beta_analytics_service_account_actor.rbs +26 -0
  162. data/sig/anthropic/models/beta/organization/beta_analytics_service_tier.rbs +28 -0
  163. data/sig/anthropic/models/beta/organization/beta_analytics_speed.rbs +18 -0
  164. data/sig/anthropic/models/beta/organization/beta_analytics_time_bucket_width.rbs +19 -0
  165. data/sig/anthropic/models/beta/organization/beta_analytics_usage_cost_inference_geo.rbs +18 -0
  166. data/sig/anthropic/models/beta/organization/beta_analytics_usage_cost_product_filter.rbs +40 -0
  167. data/sig/anthropic/models/beta/organization/beta_analytics_usage_cost_sort_order.rbs +18 -0
  168. data/sig/anthropic/resources/beta/messages.rbs +33 -0
  169. data/sig/anthropic/resources/beta/organization/analytics/cost.rbs +46 -0
  170. data/sig/anthropic/resources/beta/organization/analytics/usage.rbs +46 -0
  171. data/sig/anthropic/resources/beta/organization/analytics.rbs +4 -0
  172. metadata +111 -2
@@ -0,0 +1,635 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anthropic
4
+ module Helpers
5
+ module Threads
6
+ # A conversation that the API keeps between requests, so that each turn sends only the
7
+ # messages that are new. Create one with `client.beta.messages.thread(...)`.
8
+ #
9
+ # A thread runs one turn at a time and is not safe for concurrent use.
10
+ class BetaMessageThread
11
+ THREADS_BETA = Anthropic::Models::AnthropicBeta::MESSAGE_THREADS_2026_08_12.to_s
12
+ NOT_FOUND_ERROR_CODE = "thread_not_found"
13
+ # A `continue` rejected with one of these is sent again as a `create`.
14
+ REPLAY_ERROR_CODES = Set[NOT_FOUND_ERROR_CODE, "thread_already_continued", "thread_continue_in_flight"].freeze
15
+ UNSUPPORTED_REQUEST_ERROR_CODE = "thread_unsupported_request"
16
+ UNSUPPORTED_REQUEST_BEHAVIORS = Set[:error, :continue_unthreaded].freeze
17
+
18
+ # How the next turn relates to what the API holds. It is always exactly one of these.
19
+ #
20
+ # The next turn starts a new thread with the whole history. `after_not_found` is whether it
21
+ # does so because the API answered a `continue` with `thread_not_found`.
22
+ Create = Data.define(:after_not_found) do
23
+ def initialize(after_not_found: false) = super
24
+ end
25
+ # The next turn continues from `previous_message_id` and sends what follows the recorded
26
+ # messages. `created_with` is the `system` and `tools` that the API keeps for the thread.
27
+ # `after_not_found` is that of the `Create` the thread began with, until a `continue` succeeds.
28
+ Continue = Data.define(:previous_message_id, :created_with, :after_not_found)
29
+ # Every turn sends the whole history and no `thread`.
30
+ Unthreaded = Data.define
31
+
32
+ # One message of the history, as the API receives it and frozen.
33
+ Entry = Data.define(:param, :reply_id, :partial) do
34
+ # @param message [Anthropic::Models::Beta::BetaUserMessageParam, Anthropic::Models::Beta::BetaAssistantMessageParam, Anthropic::Models::Beta::BetaSystemMessageParam, Anthropic::Models::Beta::BetaMessage, Hash{Symbol=>Object}]
35
+ def self.given(message) = build(message, reply_id: nil, partial: false)
36
+
37
+ # @param reply [Anthropic::Models::Beta::BetaMessage] a reply that a turn of the thread received
38
+ # @param partial [Boolean] whether its stream ended before `message_stop`
39
+ def self.received(reply, partial: false) = build(reply, reply_id: reply.id, partial:)
40
+
41
+ def self.build(message, **)
42
+ message = {role: :assistant, content: message.content} if message.is_a?(Anthropic::Models::Beta::BetaMessage)
43
+ param = Anthropic::Internal::Type::Converter.dump(Anthropic::Models::Beta::BetaMessageParam, message)
44
+ new(param: Anthropic::Internal::Util.deep_frozen_copy(param), **)
45
+ end
46
+ private_class_method :new, :[], :build
47
+
48
+ # A typed message or block has its `role` or `type` last, and a Hash has it where the caller put it.
49
+ #
50
+ # @return [Hash{String=>Object}] two messages are sent alike exactly when these are equal
51
+ def wire = JSON.parse(param.to_json)
52
+ end
53
+
54
+ # One request of a turn: the params for `create`, its `system_` and `tools` as given and as
55
+ # sent, and the classes that its reply parses into.
56
+ Request = Data.define(:params, :create_only, :created_with, :tools, :models)
57
+
58
+ private_constant :THREADS_BETA,
59
+ :NOT_FOUND_ERROR_CODE,
60
+ :REPLAY_ERROR_CODES,
61
+ :UNSUPPORTED_REQUEST_ERROR_CODE,
62
+ :UNSUPPORTED_REQUEST_BEHAVIORS,
63
+ :Create,
64
+ :Continue,
65
+ :Unthreaded,
66
+ :Entry,
67
+ :Request
68
+
69
+ # The whole history: every message that was sent, every reply, and the messages staged
70
+ # for the next turn.
71
+ #
72
+ # @return [Array<Hash{Symbol=>Object}>] frozen
73
+ def messages = @history.map(&:param).freeze
74
+
75
+ # The reply the next turn continues from, or `nil` when the next turn starts a new thread.
76
+ #
77
+ # @return [String, nil]
78
+ def previous_message_id = @anchor.is_a?(Continue) ? @anchor.previous_message_id : nil
79
+
80
+ # What `client.beta.messages.thread` takes to create a thread that behaves the same, with
81
+ # `messages` set to the whole history.
82
+ #
83
+ # @return [Hash{Symbol=>Object}] a frozen copy
84
+ def params
85
+ options = @request_options.empty? ? {} : {request_options: @request_options}
86
+ given = {**@fields, **options, unsupported_request_behavior: @unsupported_request_behavior}
87
+ Anthropic::Internal::Util.deep_frozen_copy(given).merge(messages:).freeze
88
+ end
89
+
90
+ # Stages messages for the next turn without sending them.
91
+ #
92
+ # @param messages [Array<Anthropic::Models::Beta::BetaUserMessageParam, Anthropic::Models::Beta::BetaAssistantMessageParam, Anthropic::Models::Beta::BetaSystemMessageParam, Anthropic::Models::Beta::BetaMessage, Hash{Symbol=>Object}>]
93
+ #
94
+ # @return [void]
95
+ def append(*messages)
96
+ messages.each { @history << Entry.given(_1) unless repeats_last_reply?(_1) }
97
+ nil
98
+ end
99
+
100
+ # Changes the thread's request fields for every later turn, where `nil` removes a field. It
101
+ # sends no request. `system_` and `tools` cannot change once a turn has created the thread.
102
+ #
103
+ # @param params [Hash{Symbol=>Object}]
104
+ #
105
+ # @raise [ArgumentError] if `messages`, `thread` or `stream` is given
106
+ #
107
+ # @return [void]
108
+ def update(**params)
109
+ fields, options = split_params(params)
110
+ reject_key!(fields, :messages, "Use `#append` to add messages to a message thread.")
111
+
112
+ @fields = overlay(@fields, fields)
113
+ @request_options = overlay(@request_options, options)
114
+ nil
115
+ end
116
+
117
+ # Returns a new, independent thread with this thread's history and request fields. It sends
118
+ # no request.
119
+ #
120
+ # @return [Anthropic::Helpers::Threads::BetaMessageThread]
121
+ def fork = dup.tap(&:start_new_thread)
122
+
123
+ # Creates a tool runner that runs over this thread. It sends no request.
124
+ #
125
+ # @param max_iterations [Integer, nil]
126
+ #
127
+ # @return [Anthropic::Helpers::Tools::Runner]
128
+ def tool_runner(max_iterations: nil)
129
+ # As in `#start_turn`.
130
+ history = @history.last&.partial ? @history[...-1] : @history
131
+ Anthropic::Helpers::Tools::Runner.new(
132
+ @client,
133
+ # The runner's params are those of its requests, and the thread applies its own behavior.
134
+ params: {**params.except(:unsupported_request_behavior), messages: history.map(&:param)},
135
+ max_iterations:,
136
+ thread: self
137
+ )
138
+ end
139
+
140
+ # Sends the messages staged with {#append}, followed by `messages`, as one turn and returns
141
+ # the reply. `params` takes the keywords of {#update} and applies to this turn only.
142
+ #
143
+ # @param messages [Array<Anthropic::Models::Beta::BetaUserMessageParam, Anthropic::Models::Beta::BetaAssistantMessageParam, Anthropic::Models::Beta::BetaSystemMessageParam, Anthropic::Models::Beta::BetaMessage, Hash{Symbol=>Object}>]
144
+ #
145
+ # @param params [Hash{Symbol=>Object}]
146
+ #
147
+ # @raise [Anthropic::Errors::Error] if another turn is in progress on this thread, or if
148
+ # the turn continues the thread and changes `system_` or `tools`
149
+ #
150
+ # @return [Anthropic::Models::Beta::BetaMessage]
151
+ def send_message(*messages, **params)
152
+ overrides = start_turn(messages, params)
153
+ begin
154
+ request, reply = send_request(*overrides) { @client.beta.messages.create(_1) }
155
+ parse_reply!(reply, request)
156
+ record_reply(request, reply)
157
+ reply
158
+ ensure
159
+ end_turn
160
+ end
161
+ end
162
+
163
+ # Sends the next turn like {#send_message} and returns the stream of the reply. The turn is
164
+ # in progress until the stream ends or is closed.
165
+ #
166
+ # @param messages [Array<Anthropic::Models::Beta::BetaUserMessageParam, Anthropic::Models::Beta::BetaAssistantMessageParam, Anthropic::Models::Beta::BetaSystemMessageParam, Anthropic::Models::Beta::BetaMessage, Hash{Symbol=>Object}>]
167
+ #
168
+ # @param params [Hash{Symbol=>Object}]
169
+ #
170
+ # @raise [Anthropic::Errors::Error] if another turn is in progress on this thread, or if
171
+ # the turn continues the thread and changes `system_` or `tools`
172
+ #
173
+ # @return [Anthropic::Streaming::MessageStream]
174
+ def stream(*messages, **params)
175
+ overrides = start_turn(messages, params)
176
+ begin
177
+ request, raw_stream = send_request(*overrides) { @client.beta.messages.stream_raw(_1) }
178
+ stream = Stream.new(raw_stream:, thread: self, request:)
179
+ ensure
180
+ end_turn if stream.nil?
181
+ end
182
+ end
183
+
184
+ # @api private
185
+ #
186
+ # Replaces the request fields, the request options and the history. `messages` that begin
187
+ # with the recorded messages keep the thread, and the rest is staged. Any other list is
188
+ # staged as a whole and starts a new thread.
189
+ #
190
+ # @param params [Hash{Symbol=>Object}]
191
+ #
192
+ # @return [void]
193
+ def replace_params(params)
194
+ check_idle!
195
+ fields, options = split_params(params)
196
+ history = fields.delete(:messages).to_a.map { Entry.given(_1) }
197
+ if history.first(recorded.length).map(&:wire) == recorded.map(&:wire)
198
+ history[0, recorded.length] = recorded
199
+ else
200
+ start_new_thread
201
+ end
202
+
203
+ @fields = fields
204
+ @request_options = options
205
+ @history = history
206
+ nil
207
+ end
208
+
209
+ # @api private
210
+ #
211
+ # The messages after the first `count` and after the reply to those, if one was recorded.
212
+ # As in {#append}, the last reply appended again does not count.
213
+ #
214
+ # @param count [Integer]
215
+ #
216
+ # @return [Array<Hash{Symbol=>Object}>]
217
+ def appended_after(count)
218
+ rest = @history.drop(count)
219
+ rest.shift unless rest.first&.reply_id.nil?
220
+ last_reply = @history.first(count).reverse_each.find { _1.param[:role].to_s == "assistant" }
221
+ rest.reject { _1.wire == last_reply&.wire }.map(&:param)
222
+ end
223
+
224
+ # @api private
225
+ #
226
+ # Makes the next turn start a new thread with the whole history.
227
+ #
228
+ # @return [void]
229
+ def start_new_thread
230
+ @anchor = Create.new unless @anchor.is_a?(Unthreaded)
231
+ nil
232
+ end
233
+
234
+ # @api private
235
+ #
236
+ # A `continue` leaves out `system` and `tools`, so the API cannot reject a change.
237
+ #
238
+ # @param params [Hash{Symbol=>Object}]
239
+ #
240
+ # @raise [Anthropic::Errors::Error] if a next turn with `params` continues the thread and
241
+ # changes `system_` or `tools`
242
+ #
243
+ # @return [void]
244
+ def check_create_only_fields!(params)
245
+ refuse_change!(create_only_fields(wire_request(params.slice(:system, :system_, :tools)).first))
246
+ end
247
+
248
+ # @api private
249
+ #
250
+ # Ends the turn in progress with `reply`, from which the next turn continues. The API may
251
+ # hold a refused turn, which a `continue` cannot remove, so such a turn ends with no reply
252
+ # and the next turn starts a new thread.
253
+ #
254
+ # @param request [Request]
255
+ #
256
+ # @param reply [Anthropic::Models::Beta::BetaMessage]
257
+ #
258
+ # @return [void]
259
+ def record_reply(request, reply)
260
+ if reply.stop_reason == Anthropic::Models::Beta::BetaStopReason::REFUSAL
261
+ start_new_thread
262
+ return end_turn
263
+ end
264
+
265
+ @history.insert(@turn, Entry.received(reply))
266
+ unless @anchor.is_a?(Unthreaded)
267
+ @anchor = Continue.new(
268
+ previous_message_id: reply.id,
269
+ created_with: request.created_with,
270
+ after_not_found: @anchor.is_a?(Create) && @anchor.after_not_found
271
+ )
272
+ # What the turn that created the thread sent is the thread's from then on.
273
+ @fields = {**@fields.except(:system_, :tools), **request.create_only}
274
+ # A thread keeps one model, so the API does not hold a reply that a fallback model wrote.
275
+ start_new_thread if reply.usage.iterations.to_a.any? { _1 in {type: :fallback_message} }
276
+ end
277
+ adopt_container(reply)
278
+ @turn = nil
279
+ end
280
+
281
+ # @api private
282
+ #
283
+ # Ends a turn whose stream ended after `message_start` and before `message_stop`. The API
284
+ # holds either the whole reply or none of it, so the next turn starts a new thread.
285
+ #
286
+ # @param reply [Anthropic::Models::Beta::BetaMessage, nil] the part of the reply worth keeping
287
+ #
288
+ # @return [void]
289
+ def record_partial_reply(reply)
290
+ @history.insert(@turn, Entry.received(reply, partial: true)) unless reply.nil?
291
+ start_new_thread
292
+ @turn = nil
293
+ end
294
+
295
+ # @api private
296
+ #
297
+ # Ends the turn in progress, if any, with no reply. The thread takes back the messages that
298
+ # were passed to the call.
299
+ #
300
+ # @return [void]
301
+ def end_turn
302
+ @history.slice!(@turn - @passed, @passed) unless @turn.nil?
303
+ @turn = nil
304
+ end
305
+
306
+ # @api private
307
+ #
308
+ # @param client [Anthropic::Client]
309
+ #
310
+ # @param params [Hash{Symbol=>Object}] the fields of `create`, with `messages` as the seed
311
+ #
312
+ # @param unsupported_request_behavior [Symbol] `:error` or `:continue_unthreaded`
313
+ def initialize(client, params:, unsupported_request_behavior: :error)
314
+ unless UNSUPPORTED_REQUEST_BEHAVIORS.include?(unsupported_request_behavior)
315
+ raise ArgumentError.new(
316
+ "`unsupported_request_behavior` must be `:error` or `:continue_unthreaded`, " \
317
+ "got #{unsupported_request_behavior.inspect}"
318
+ )
319
+ end
320
+
321
+ @client = client
322
+ @unsupported_request_behavior = unsupported_request_behavior
323
+ @fields, @request_options = split_params(params)
324
+ @history = @fields.delete(:messages).to_a.map { Entry.given(_1) }
325
+ @anchor = Create.new
326
+ # The index in the history at which the reply of the turn in progress goes, `nil` when none is.
327
+ @turn = nil
328
+ # How many of the messages before that index were passed to the call that started the turn.
329
+ @passed = 0
330
+ end
331
+
332
+ private
333
+
334
+ def initialize_copy(source)
335
+ super
336
+ @history = @history.dup
337
+ @turn = nil
338
+ end
339
+
340
+ # @return [Array(Hash{Symbol=>Object}, Hash{Symbol=>Object})] the fields and the request options
341
+ def split_params(params)
342
+ fields = params.to_h.transform_keys(&:to_sym)
343
+ reject_key!(fields, :thread, "The message thread sets `thread` for each request itself.")
344
+ reject_key!(fields, :stream, "Use `#stream` to stream a turn of a message thread.")
345
+ # `create` takes the system prompt as `system_`; one name keeps an override from sending both.
346
+ fields[:system_] = fields.delete(:system) if fields.key?(:system)
347
+
348
+ options = Anthropic::Internal::Util.coerce_hash!(fields.delete(:request_options)).to_h
349
+ [fields, options.transform_keys(&:to_sym)]
350
+ end
351
+
352
+ def reject_key!(fields, key, hint)
353
+ raise ArgumentError.new("`#{key}` cannot be given here. #{hint}") if fields.key?(key)
354
+ end
355
+
356
+ def overlay(base, overrides)
357
+ overrides.each_with_object(base.dup) do |(key, value), merged|
358
+ value.nil? ? merged.delete(key) : merged.store(key, value)
359
+ end
360
+ end
361
+
362
+ def repeats_last_reply?(message)
363
+ last = @history.last
364
+ return false if last&.reply_id.nil?
365
+ return message.id == last.reply_id if message.is_a?(Anthropic::Models::Beta::BetaMessage)
366
+
367
+ Entry.given(message).wire == last.wire
368
+ end
369
+
370
+ def check_idle!
371
+ return if @turn.nil?
372
+
373
+ raise Anthropic::Errors::Error.new(
374
+ "Another turn is in progress on this message thread. Wait for it to end, or close its stream."
375
+ )
376
+ end
377
+
378
+ # @return [Array(Hash{Symbol=>Object}, Hash{Symbol=>Object})] the turn's fields and request options
379
+ def start_turn(messages, params)
380
+ check_idle!
381
+ fields, options = split_params(params)
382
+ reject_key!(fields, :messages, "Pass the messages of a turn as positional arguments.")
383
+
384
+ staged = @history.length
385
+ append(*messages)
386
+ @passed = @history.length - staged
387
+ # With nothing after it, a partial reply gives way to the whole reply of the same turn.
388
+ @history.pop if @history.last&.partial
389
+ @turn = @history.length
390
+ [fields, options]
391
+ end
392
+
393
+ # Sends the request of the turn in progress, and again for as long as the thread recovers
394
+ # from an error that rejected it.
395
+ #
396
+ # @return [Array(Request, Object)] the request that went through and what the block returned
397
+ def send_request(fields, options)
398
+ loop do
399
+ request = plan_request(fields, options)
400
+ begin
401
+ return [request, yield(request.params)]
402
+ rescue Anthropic::Errors::APIStatusError => e
403
+ raise unless recovers_from?(e)
404
+ end
405
+ end
406
+ end
407
+
408
+ # @return [Request]
409
+ def plan_request(fields, options)
410
+ fields = overlay(@fields, fields)
411
+ wire, tools, models = wire_request(fields)
412
+ refuse_change!(created_with = create_only_fields(wire))
413
+ create_only = fields.slice(:system_, :tools)
414
+ # Sent as schemas, because `create` names another beta for a class. The thread reads the reply.
415
+ fields = {**fields.except(:output_format), **wire.slice(:tools, :output_config, :output_format)}
416
+ betas = [*fields[:betas], THREADS_BETA].uniq(&:to_s)
417
+
418
+ params =
419
+ case @anchor
420
+ in Create
421
+ {**fields, betas:, thread: {type: :create}, messages: whole_history}
422
+ in Continue[previous_message_id:]
423
+ thread = {type: :continue, previous_message_id:}
424
+ {**fields.except(:system_, :tools), betas:, thread:, messages: messages.drop(recorded.length)}
425
+ in Unthreaded
426
+ {**fields, messages: whole_history}
427
+ end
428
+ options = overlay(@request_options, options)
429
+ headers = options[:extra_headers].to_h
430
+ helpers = StainlessHelperHeader.merged_value(headers, StainlessHelperHeader::MESSAGE_THREAD)
431
+ params[:request_options] = {**options, extra_headers: {**headers, StainlessHelperHeader::HEADER => helpers}}
432
+
433
+ Request.new(params:, create_only:, created_with:, tools:, models:)
434
+ end
435
+
436
+ # The messages that were sent and answered: those up to the last reply that a turn received.
437
+ #
438
+ # @return [Array<Entry>]
439
+ def recorded = @history.take((@history.rindex(&:reply_id) || -1) + 1)
440
+
441
+ # The history as a request that sends all of it carries it. The cache breakpoints of the
442
+ # recorded messages add up over the turns, so those messages go without them. The other
443
+ # messages go as given.
444
+ #
445
+ # @return [Array<Hash{Symbol=>Object}>]
446
+ def whole_history
447
+ sent_before = recorded.map { without_cache_control(_1.param) }
448
+ sent_before + messages.drop(sent_before.length)
449
+ end
450
+
451
+ # Enters only the keys that lead to a marker. Any other value may hold any key, so it is kept whole.
452
+ def without_cache_control(value)
453
+ return value unless value.is_a?(Hash)
454
+
455
+ value.except(:cache_control, "cache_control").to_h do |key, nested|
456
+ next [key, nested] unless %w[content source tool definition tool_references].include?(key.to_s)
457
+
458
+ [key, nested.is_a?(Array) ? nested.map { without_cache_control(_1) } : without_cache_control(nested)]
459
+ end
460
+ end
461
+
462
+ # @return [Array(Hash{Symbol=>Object}, Hash{String=>Class}, Hash{String=>Class})] the request
463
+ # as the API would receive it, and the classes that its tool inputs and output parse into
464
+ def wire_request(fields)
465
+ wire, = Anthropic::Models::Beta::MessageCreateParams.dump_request(fields)
466
+ tools, models = Anthropic::Helpers::Messages.distill_input_schema_models!(wire, strict: nil, is_beta: true)
467
+ [wire, tools, models]
468
+ end
469
+
470
+ def create_only_fields(wire) = {system_: wire[:system], tools: wire[:tools]}.transform_values(&:to_json)
471
+
472
+ def refuse_change!(created_with)
473
+ return if !@anchor.is_a?(Continue) || created_with == @anchor.created_with
474
+
475
+ changed = created_with.reject { |field, value| value == @anchor.created_with[field] }.keys
476
+ message =
477
+ "Cannot change #{changed.map { "`#{_1}`" }.join(' and ')} on a thread: the server keeps the " \
478
+ "`system_` and `tools` the thread was created with. To change them, start a new thread from " \
479
+ "`thread.params`."
480
+ if changed.include?(:tools)
481
+ message +=
482
+ " To add or remove tools on this thread, send `tool_addition` or `tool_removal` blocks in a " \
483
+ "`system` message."
484
+ end
485
+ raise Anthropic::Errors::Error.new(message)
486
+ end
487
+
488
+ # Sets `parsed` on the content blocks of `reply`, as `create` does for a request that holds
489
+ # the classes to parse them into.
490
+ def parse_reply!(reply, request)
491
+ reply.content.to_a.each_with_index do |block, index|
492
+ parsed =
493
+ begin
494
+ case block
495
+ in Anthropic::Models::Beta::BetaToolUseBlock[type: :tool_use] if (tool = request.tools[block.name])
496
+ Anthropic::Internal::Type::Converter.coerce(tool, block.input)
497
+ in Anthropic::Models::Beta::BetaTextBlock if (model = request.models.first&.last)
498
+ Anthropic::Internal::Type::Converter.coerce(model, JSON.parse(block.text, symbolize_names: true))
499
+ else
500
+ next
501
+ end
502
+ rescue StandardError => e
503
+ {error: e.message}
504
+ end
505
+ reply.content[index] = Anthropic::Internal::Type::Converter.coerce(block.class, {**block.to_h, parsed:})
506
+ end
507
+ end
508
+
509
+ # Container-bound server tools reject a request that leaves out the container the turn
510
+ # before it ran in, so its id is sent on later turns unless the caller named one.
511
+ def adopt_container(reply)
512
+ id = reply.container&.id
513
+ return if id.nil? || id.empty?
514
+
515
+ case (pinned = @fields[:container])
516
+ in nil
517
+ @fields = {**@fields, container: id}
518
+ in Hash | Anthropic::Internal::Type::BaseModel if pinned.to_h.transform_keys(&:to_sym)[:id].nil?
519
+ @fields = {**@fields, container: {**pinned.to_h, id:}}
520
+ else
521
+ nil
522
+ end
523
+ end
524
+
525
+ # Whether the turn should be sent again because the API rejected its `thread` in a way the
526
+ # thread has now adjusted for.
527
+ def recovers_from?(error)
528
+ body = error.body
529
+ code = body.is_a?(Hash) ? body.dig(:error, :details, :error_code) : nil
530
+ unsupported = code == UNSUPPORTED_REQUEST_ERROR_CODE
531
+ not_found = code == NOT_FOUND_ERROR_CODE
532
+ # The API may be unable to store a conversation whose new thread it does not hold either.
533
+ unsupported ||= not_found && @anchor.is_a?(Continue) && @anchor.after_not_found
534
+
535
+ case @anchor
536
+ in Create | Continue if unsupported && @unsupported_request_behavior == :continue_unthreaded
537
+ @anchor = Unthreaded.new
538
+ in Continue if REPLAY_ERROR_CODES.include?(code)
539
+ @anchor = Create.new(after_not_found: not_found)
540
+ else
541
+ return false
542
+ end
543
+ true
544
+ end
545
+
546
+ # @api private
547
+ #
548
+ # The stream of one turn. It tells the thread how the turn ended.
549
+ class Stream < Anthropic::Helpers::Streaming::MessageStream
550
+ # Ends the stream. When the reply has started and `message_stop` has not arrived, the
551
+ # thread records the part of the reply that arrived.
552
+ #
553
+ # @return [void]
554
+ def close
555
+ finish
556
+ @raw_stream.close
557
+ super
558
+ end
559
+
560
+ # @param raw_stream [Anthropic::Internal::Type::BaseStream]
561
+ #
562
+ # @param thread [Anthropic::Helpers::Threads::BetaMessageThread]
563
+ #
564
+ # @param request [Request]
565
+ def initialize(raw_stream:, thread:, request:)
566
+ @thread = thread
567
+ @request = request
568
+ @ended = false
569
+ @snapshot = nil
570
+ @finished_blocks = 0
571
+ super(raw_stream:, tools: request.tools, models: request.models)
572
+ end
573
+
574
+ private
575
+
576
+ def iterator
577
+ events = super
578
+ @iterator = Anthropic::Internal::Util.chain_fused(events) do |y|
579
+ events.each(&y)
580
+ ensure
581
+ finish
582
+ end
583
+ end
584
+
585
+ def build_events(event:, message_snapshot:)
586
+ @snapshot = message_snapshot
587
+ case event.type
588
+ in :content_block_stop
589
+ @finished_blocks += 1
590
+ in :message_stop unless @ended
591
+ @ended = true
592
+ @thread.record_reply(@request, message_snapshot)
593
+ else
594
+ nil
595
+ end
596
+ super
597
+ end
598
+
599
+ # Tells the thread, once, that the stream ended before `message_stop`.
600
+ def finish
601
+ return if @ended
602
+
603
+ @ended = true
604
+ case @snapshot
605
+ in nil
606
+ @thread.end_turn
607
+ in {stop_reason: Anthropic::Models::Beta::BetaStopReason::REFUSAL}
608
+ @thread.record_reply(@request, @snapshot)
609
+ else
610
+ @thread.record_partial_reply(partial_reply)
611
+ end
612
+ end
613
+
614
+ # The part of the reply worth keeping: the content blocks that finished, and the block
615
+ # after them if it is a text block with text. Any other unfinished block is dropped,
616
+ # because its input or signature is incomplete.
617
+ #
618
+ # @return [Anthropic::Models::Beta::BetaMessage, nil]
619
+ def partial_reply
620
+ content = @snapshot.content.to_a.first(@finished_blocks)
621
+ open = @snapshot.content.to_a[@finished_blocks]
622
+ content << open if open&.type == :text && !open.text.empty?
623
+ return nil if content.empty?
624
+
625
+ Anthropic::Internal::Type::Converter.coerce(
626
+ Anthropic::Models::Beta::BetaMessage,
627
+ {**@snapshot.to_h, content:}
628
+ )
629
+ end
630
+ end
631
+ private_constant :Stream
632
+ end
633
+ end
634
+ end
635
+ end