riffer 0.28.0 → 0.29.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.
- checksums.yaml +4 -4
- data/.agents/architecture.md +18 -11
- data/.agents/code-style.md +1 -1
- data/.agents/rbs-inline.md +2 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +19 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +87 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +17 -17
- data/docs/08_MESSAGES.md +25 -32
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +7 -18
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/06_MOCK_PROVIDER.md +2 -1
- data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
- data/docs/providers/08_GEMINI.md +2 -2
- data/docs/providers/09_OPENROUTER.md +242 -0
- data/lib/riffer/agent/config.rb +173 -0
- data/lib/riffer/agent/context.rb +125 -0
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +268 -0
- data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
- data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
- data/lib/riffer/agent.rb +234 -923
- data/lib/riffer/config.rb +14 -7
- data/lib/riffer/evals/evaluator.rb +13 -3
- data/lib/riffer/evals/judge.rb +2 -2
- data/lib/riffer/evals/run_result.rb +2 -1
- data/lib/riffer/evals/scenario_result.rb +2 -1
- data/lib/riffer/guardrails/runner.rb +3 -2
- data/lib/riffer/helpers/call_or_value.rb +16 -0
- data/lib/riffer/helpers.rb +0 -1
- data/lib/riffer/mcp/authenticated_tool.rb +4 -0
- data/lib/riffer/mcp/client.rb +1 -1
- data/lib/riffer/mcp/registration.rb +2 -3
- data/lib/riffer/mcp/registry.rb +3 -1
- data/lib/riffer/mcp/tool_factory.rb +5 -0
- data/lib/riffer/messages/assistant.rb +9 -3
- data/lib/riffer/messages/base.rb +22 -0
- data/lib/riffer/messages/converter.rb +6 -6
- data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
- data/lib/riffer/messages/tool.rb +1 -1
- data/lib/riffer/messages/user.rb +4 -4
- data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
- data/lib/riffer/{param.rb → params/param.rb} +6 -6
- data/lib/riffer/params.rb +27 -21
- data/lib/riffer/providers/amazon_bedrock.rb +19 -20
- data/lib/riffer/providers/anthropic.rb +27 -28
- data/lib/riffer/providers/base.rb +10 -9
- data/lib/riffer/providers/gemini.rb +15 -12
- data/lib/riffer/providers/mock.rb +41 -13
- data/lib/riffer/providers/open_ai.rb +24 -22
- data/lib/riffer/providers/open_router.rb +318 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
- data/lib/riffer/providers.rb +1 -0
- data/lib/riffer/runner/fibers.rb +4 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +1 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +1 -1
- data/lib/riffer/skills/context.rb +3 -3
- data/lib/riffer/skills/filesystem_backend.rb +7 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +1 -1
- data/lib/riffer/stream_events/token_usage_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +1 -1
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
- data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
- data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
- data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -1
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +91 -0
- data/sig/generated/riffer/agent/run.rbs +144 -0
- data/sig/generated/riffer/agent/session/repair.rbs +51 -0
- data/sig/generated/riffer/agent/session.rbs +145 -0
- data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
- data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
- data/sig/generated/riffer/agent.rbs +143 -342
- data/sig/generated/riffer/config.rbs +17 -5
- data/sig/generated/riffer/evals/judge.rbs +2 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/messages/assistant.rbs +7 -3
- data/sig/generated/riffer/messages/base.rbs +18 -0
- data/sig/generated/riffer/messages/converter.rbs +4 -4
- data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
- data/sig/generated/riffer/messages/user.rbs +4 -4
- data/sig/generated/riffer/params/boolean.rbs +10 -0
- data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
- data/sig/generated/riffer/params.rbs +15 -15
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +10 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +25 -5
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +85 -0
- data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
- data/sig/generated/riffer/providers.rbs +1 -0
- data/sig/generated/riffer/runner/fibers.rbs +2 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +2 -2
- data/sig/generated/riffer/runner.rbs +2 -2
- data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
- data/sig/generated/riffer/skills/config.rbs +1 -1
- data/sig/generated/riffer/skills/context.rbs +2 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
- data/sig/stubs/agent_ivars.rbs +7 -0
- data/sig/stubs/async.rbs +24 -0
- data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/stubs/extend_self.rbs +11 -0
- data/sig/stubs/lib_ivars.rbs +101 -0
- data/sig/stubs/mcp_sdk.rbs +22 -0
- data/sig/stubs/provider_ivars.rbs +36 -0
- data/sig/stubs/provider_sdk_methods.rbs +50 -0
- data/sig/stubs/zeitwerk.rbs +12 -0
- metadata +54 -33
- data/lib/riffer/core.rb +0 -28
- data/lib/riffer/helpers/validations.rb +0 -18
- data/sig/generated/riffer/boolean.rbs +0 -10
- data/sig/generated/riffer/core.rbs +0 -19
- data/sig/generated/riffer/helpers/validations.rbs +0 -12
|
@@ -63,7 +63,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
63
63
|
system: partitioned_messages[:system],
|
|
64
64
|
messages: partitioned_messages[:conversation],
|
|
65
65
|
**options.except(:tools, :structured_output)
|
|
66
|
-
}
|
|
66
|
+
} #: Hash[Symbol, untyped]
|
|
67
67
|
|
|
68
68
|
if tools && !tools.empty?
|
|
69
69
|
params[:tool_config] = {
|
|
@@ -92,18 +92,17 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
#--
|
|
95
|
-
#: (Hash[Symbol, untyped]) -> Aws::BedrockRuntime::
|
|
95
|
+
#: (Hash[Symbol, untyped]) -> Aws::BedrockRuntime::Client::_ConverseResponseSuccess
|
|
96
96
|
def execute_generate(params)
|
|
97
97
|
@client.converse(**params)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
#--
|
|
101
|
-
#: (Aws::BedrockRuntime::
|
|
101
|
+
#: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> Riffer::Providers::TokenUsage?
|
|
102
102
|
def extract_token_usage(response)
|
|
103
103
|
usage = response.usage
|
|
104
|
-
return nil unless usage
|
|
105
104
|
|
|
106
|
-
Riffer::TokenUsage.new(
|
|
105
|
+
Riffer::Providers::TokenUsage.new(
|
|
107
106
|
input_tokens: usage.input_tokens,
|
|
108
107
|
output_tokens: usage.output_tokens,
|
|
109
108
|
cache_creation_tokens: usage.cache_write_input_tokens,
|
|
@@ -112,7 +111,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
112
111
|
end
|
|
113
112
|
|
|
114
113
|
#--
|
|
115
|
-
#: (Aws::BedrockRuntime::
|
|
114
|
+
#: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> String
|
|
116
115
|
def extract_content(response)
|
|
117
116
|
content_blocks = response.output&.message&.content
|
|
118
117
|
return "" if content_blocks.nil? || content_blocks.empty?
|
|
@@ -127,12 +126,12 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
127
126
|
end
|
|
128
127
|
|
|
129
128
|
#--
|
|
130
|
-
#: (Aws::BedrockRuntime::
|
|
129
|
+
#: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
131
130
|
def extract_tool_calls(response)
|
|
132
131
|
content_blocks = response.output&.message&.content
|
|
133
132
|
return [] if content_blocks.nil? || content_blocks.empty?
|
|
134
133
|
|
|
135
|
-
tool_calls = []
|
|
134
|
+
tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
136
135
|
|
|
137
136
|
content_blocks.each do |block|
|
|
138
137
|
if block.respond_to?(:tool_use) && block.tool_use
|
|
@@ -153,7 +152,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
153
152
|
current_state = {
|
|
154
153
|
text: nil,
|
|
155
154
|
tool_call: nil
|
|
156
|
-
}
|
|
155
|
+
} #: Hash[Symbol, untyped]
|
|
157
156
|
|
|
158
157
|
@client.converse_stream(**params) do |stream|
|
|
159
158
|
stream.on_event do |event|
|
|
@@ -196,7 +195,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
196
195
|
end
|
|
197
196
|
|
|
198
197
|
#--
|
|
199
|
-
#: (Aws::BedrockRuntime::Types::ContentBlockStartEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
198
|
+
#: (Aws::BedrockRuntime::Types::ContentBlockStartEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
200
199
|
def handle_content_block_start_tool_use(event, state:, yielder:)
|
|
201
200
|
state[:tool_call] = {
|
|
202
201
|
id: event.start.tool_use.tool_use_id,
|
|
@@ -206,7 +205,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
206
205
|
end
|
|
207
206
|
|
|
208
207
|
#--
|
|
209
|
-
#: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
208
|
+
#: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
210
209
|
def handle_content_block_delta_text_delta(event, state:, yielder:)
|
|
211
210
|
delta_text = event.delta.text
|
|
212
211
|
state[:text] ||= ""
|
|
@@ -215,7 +214,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
215
214
|
end
|
|
216
215
|
|
|
217
216
|
#--
|
|
218
|
-
#: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
217
|
+
#: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
219
218
|
def handle_content_block_delta_tool_use(event, state:, yielder:)
|
|
220
219
|
input_delta = event.delta.tool_use.input
|
|
221
220
|
|
|
@@ -229,14 +228,14 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
229
228
|
end
|
|
230
229
|
|
|
231
230
|
#--
|
|
232
|
-
#: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
231
|
+
#: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
233
232
|
def handle_content_block_stop_text_delta(_event, state:, yielder:)
|
|
234
233
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text])
|
|
235
234
|
state[:text] = nil
|
|
236
235
|
end
|
|
237
236
|
|
|
238
237
|
#--
|
|
239
|
-
#: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
238
|
+
#: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
240
239
|
def handle_content_block_stop_tool_use(_event, state:, yielder:)
|
|
241
240
|
tool_call = state[:tool_call]
|
|
242
241
|
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
@@ -249,10 +248,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
249
248
|
end
|
|
250
249
|
|
|
251
250
|
#--
|
|
252
|
-
#: (Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent, state: Hash[Symbol, untyped], yielder: Enumerator
|
|
251
|
+
#: (Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
253
252
|
def handle_metadata_usage(event, state:, yielder:)
|
|
254
253
|
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
255
|
-
token_usage: Riffer::TokenUsage.new(
|
|
254
|
+
token_usage: Riffer::Providers::TokenUsage.new(
|
|
256
255
|
input_tokens: event.usage.input_tokens,
|
|
257
256
|
output_tokens: event.usage.output_tokens,
|
|
258
257
|
cache_creation_tokens: event.usage.cache_write_input_tokens,
|
|
@@ -264,8 +263,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
264
263
|
#--
|
|
265
264
|
#: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
266
265
|
def partition_messages(messages)
|
|
267
|
-
system_prompts = []
|
|
268
|
-
conversation_messages = []
|
|
266
|
+
system_prompts = [] #: Array[Hash[Symbol, untyped]]
|
|
267
|
+
conversation_messages = [] #: Array[Hash[Symbol, untyped]]
|
|
269
268
|
|
|
270
269
|
messages.each do |message|
|
|
271
270
|
case message
|
|
@@ -291,7 +290,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
291
290
|
#--
|
|
292
291
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
293
292
|
def convert_assistant_to_bedrock_format(message)
|
|
294
|
-
content = []
|
|
293
|
+
content = [] #: Array[Hash[Symbol, untyped]]
|
|
295
294
|
content << {text: message.content} if message.content && !message.content.empty?
|
|
296
295
|
|
|
297
296
|
message.tool_calls.each do |tc|
|
|
@@ -326,7 +325,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
326
325
|
end
|
|
327
326
|
|
|
328
327
|
#--
|
|
329
|
-
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
328
|
+
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
330
329
|
def convert_file_part_to_bedrock_format(file)
|
|
331
330
|
format = bedrock_format(file.media_type)
|
|
332
331
|
|
|
@@ -26,7 +26,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
26
26
|
|
|
27
27
|
api_key ||= Riffer.config.anthropic.api_key
|
|
28
28
|
|
|
29
|
-
@client = Anthropic::Client.new(api_key: api_key, **options)
|
|
29
|
+
@client = ::Anthropic::Client.new(api_key: api_key, **options)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
private
|
|
@@ -46,11 +46,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
46
46
|
messages: partitioned_messages[:conversation],
|
|
47
47
|
max_tokens: max_tokens,
|
|
48
48
|
**options.except(:tools, :max_tokens, :structured_output, :web_search)
|
|
49
|
-
}
|
|
49
|
+
} #: Hash[Symbol, untyped]
|
|
50
50
|
|
|
51
51
|
params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
|
|
52
52
|
|
|
53
|
-
anthropic_tools = []
|
|
53
|
+
anthropic_tools = [] #: Array[Hash[Symbol, untyped]]
|
|
54
54
|
anthropic_tools.concat(tools.map { |t| convert_tool_to_anthropic_format(t) }) if tools && !tools.empty?
|
|
55
55
|
|
|
56
56
|
if web_search
|
|
@@ -83,12 +83,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
#--
|
|
86
|
-
#: (Anthropic::Models::Message) -> Riffer::TokenUsage?
|
|
86
|
+
#: (Anthropic::Models::Message) -> Riffer::Providers::TokenUsage?
|
|
87
87
|
def extract_token_usage(response)
|
|
88
88
|
usage = response.usage
|
|
89
|
-
return nil unless usage
|
|
90
89
|
|
|
91
|
-
Riffer::TokenUsage.new(
|
|
90
|
+
Riffer::Providers::TokenUsage.new(
|
|
92
91
|
input_tokens: usage.input_tokens,
|
|
93
92
|
output_tokens: usage.output_tokens,
|
|
94
93
|
cache_creation_tokens: usage.cache_creation_input_tokens,
|
|
@@ -105,7 +104,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
105
104
|
text_content = ""
|
|
106
105
|
|
|
107
106
|
content_blocks.each do |block|
|
|
108
|
-
text_content = block.text if block.
|
|
107
|
+
text_content = block.text if block.is_a?(::Anthropic::Models::TextBlock)
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
text_content
|
|
@@ -117,10 +116,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
117
116
|
content_blocks = response.content
|
|
118
117
|
return [] if content_blocks.nil? || content_blocks.empty?
|
|
119
118
|
|
|
120
|
-
tool_calls = []
|
|
119
|
+
tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
121
120
|
|
|
122
121
|
content_blocks.each do |block|
|
|
123
|
-
if block.
|
|
122
|
+
if block.is_a?(::Anthropic::Models::ToolUseBlock)
|
|
124
123
|
tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
125
124
|
call_id: block.id,
|
|
126
125
|
name: decode_tool_name(block.name, tools: @current_tools),
|
|
@@ -142,7 +141,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
142
141
|
web_search_index: nil,
|
|
143
142
|
web_search_json: nil,
|
|
144
143
|
web_search_query: nil
|
|
145
|
-
}
|
|
144
|
+
} #: Hash[Symbol, untyped]
|
|
146
145
|
|
|
147
146
|
# Workaround for anthropics/anthropic-sdk-ruby#182: force identity
|
|
148
147
|
# encoding so Net::HTTP/Zlib doesn't buffer SSE chunks until EOF.
|
|
@@ -154,24 +153,24 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
154
153
|
begin
|
|
155
154
|
stream.each do |event|
|
|
156
155
|
case event
|
|
157
|
-
when Anthropic::Models::RawContentBlockStartEvent
|
|
156
|
+
when ::Anthropic::Models::RawContentBlockStartEvent
|
|
158
157
|
handle_raw_content_block_start(event, state: current_state)
|
|
159
|
-
when Anthropic::Models::RawContentBlockDeltaEvent
|
|
158
|
+
when ::Anthropic::Models::RawContentBlockDeltaEvent
|
|
160
159
|
handle_raw_content_block_delta(event, state: current_state)
|
|
161
|
-
when Anthropic::Streaming::TextEvent
|
|
160
|
+
when ::Anthropic::Helpers::Streaming::TextEvent
|
|
162
161
|
handle_text_event(event, state: current_state, yielder: yielder)
|
|
163
|
-
when Anthropic::Streaming::ThinkingEvent
|
|
162
|
+
when ::Anthropic::Helpers::Streaming::ThinkingEvent
|
|
164
163
|
handle_thinking_event(event, state: current_state, yielder: yielder)
|
|
165
|
-
when Anthropic::Streaming::InputJsonEvent
|
|
164
|
+
when ::Anthropic::Helpers::Streaming::InputJsonEvent
|
|
166
165
|
handle_input_json_event(event, state: current_state, yielder: yielder)
|
|
167
|
-
when Anthropic::Streaming::ContentBlockStopEvent
|
|
168
|
-
|
|
169
|
-
handle_content_block_stop_text(event, state: current_state, yielder: yielder) if
|
|
170
|
-
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if
|
|
171
|
-
handle_content_block_stop_thinking(event, state: current_state, yielder: yielder) if
|
|
172
|
-
handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder) if
|
|
173
|
-
handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder) if
|
|
174
|
-
when Anthropic::Streaming::MessageStopEvent
|
|
166
|
+
when ::Anthropic::Helpers::Streaming::ContentBlockStopEvent
|
|
167
|
+
block = event.content_block
|
|
168
|
+
handle_content_block_stop_text(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::TextBlock) && current_state[:text]
|
|
169
|
+
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ToolUseBlock)
|
|
170
|
+
handle_content_block_stop_thinking(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ThinkingBlock) && current_state[:reasoning]
|
|
171
|
+
handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ServerToolUseBlock)
|
|
172
|
+
handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::WebSearchToolResultBlock)
|
|
173
|
+
when ::Anthropic::Helpers::Streaming::MessageStopEvent
|
|
175
174
|
handle_message_stop(event, accumulated_message: stream.accumulated_message, yielder: yielder)
|
|
176
175
|
end
|
|
177
176
|
end
|
|
@@ -292,7 +291,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
292
291
|
return unless usage
|
|
293
292
|
|
|
294
293
|
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
295
|
-
token_usage: Riffer::TokenUsage.new(
|
|
294
|
+
token_usage: Riffer::Providers::TokenUsage.new(
|
|
296
295
|
input_tokens: usage.input_tokens,
|
|
297
296
|
output_tokens: usage.output_tokens,
|
|
298
297
|
cache_creation_tokens: usage.cache_creation_input_tokens,
|
|
@@ -304,8 +303,8 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
304
303
|
#--
|
|
305
304
|
#: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
306
305
|
def partition_messages(messages)
|
|
307
|
-
system_prompts = []
|
|
308
|
-
conversation_messages = []
|
|
306
|
+
system_prompts = [] #: Array[Hash[Symbol, untyped]]
|
|
307
|
+
conversation_messages = [] #: Array[Hash[Symbol, untyped]]
|
|
309
308
|
|
|
310
309
|
messages.each do |message|
|
|
311
310
|
case message
|
|
@@ -342,7 +341,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
342
341
|
#--
|
|
343
342
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
344
343
|
def convert_assistant_to_anthropic_format(message)
|
|
345
|
-
content = []
|
|
344
|
+
content = [] #: Array[Hash[Symbol, untyped]]
|
|
346
345
|
content << {type: "text", text: message.content} if message.content && !message.content.empty?
|
|
347
346
|
|
|
348
347
|
message.tool_calls.each do |tc|
|
|
@@ -358,7 +357,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
358
357
|
end
|
|
359
358
|
|
|
360
359
|
#--
|
|
361
|
-
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
360
|
+
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
362
361
|
def convert_file_part_to_anthropic_format(file)
|
|
363
362
|
type = file.image? ? "image" : "document"
|
|
364
363
|
|
|
@@ -39,10 +39,10 @@ class Riffer::Providers::Base
|
|
|
39
39
|
# Generates text using the provider.
|
|
40
40
|
#
|
|
41
41
|
#--
|
|
42
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
42
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
43
43
|
def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
44
44
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
45
|
-
@current_tools = options[:tools] || []
|
|
45
|
+
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
46
46
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
47
47
|
validate_normalized_messages!(messages)
|
|
48
48
|
messages = merge_consecutive_messages(messages)
|
|
@@ -65,10 +65,10 @@ class Riffer::Providers::Base
|
|
|
65
65
|
# Streams text from the provider.
|
|
66
66
|
#
|
|
67
67
|
#--
|
|
68
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
68
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
69
69
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
70
70
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
71
|
-
@current_tools = options[:tools] || []
|
|
71
|
+
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
72
72
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
73
73
|
validate_normalized_messages!(messages)
|
|
74
74
|
messages = merge_consecutive_messages(messages)
|
|
@@ -87,7 +87,7 @@ class Riffer::Providers::Base
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
#--
|
|
90
|
-
#: (String, tools: Array[Riffer::Tool]) -> String
|
|
90
|
+
#: (String, tools: Array[singleton(Riffer::Tool)]) -> String
|
|
91
91
|
def decode_tool_name(wire_name, tools:)
|
|
92
92
|
tool = tools.find { |t| encode_tool_name(t.name) == wire_name }
|
|
93
93
|
tool ? tool.name : wire_name
|
|
@@ -112,7 +112,7 @@ class Riffer::Providers::Base
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
#--
|
|
115
|
-
#: (untyped) -> Riffer::TokenUsage?
|
|
115
|
+
#: (untyped) -> Riffer::Providers::TokenUsage?
|
|
116
116
|
def extract_token_usage(response)
|
|
117
117
|
raise NotImplementedError, "Subclasses must implement #extract_token_usage"
|
|
118
118
|
end
|
|
@@ -166,7 +166,7 @@ class Riffer::Providers::Base
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
#--
|
|
169
|
-
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
169
|
+
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
170
170
|
def normalize_messages(prompt:, system:, messages:, files: nil)
|
|
171
171
|
if messages && files && !files.empty?
|
|
172
172
|
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead"
|
|
@@ -176,10 +176,11 @@ class Riffer::Providers::Base
|
|
|
176
176
|
return messages.map { |msg| convert_to_message_object(msg) }
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
-
result = []
|
|
179
|
+
result = [] #: Array[Riffer::Messages::Base]
|
|
180
180
|
result << Riffer::Messages::System.new(system) if system
|
|
181
181
|
file_parts = (files || []).map { |f| convert_to_file_part(f) }
|
|
182
|
-
|
|
182
|
+
prompt_text = prompt #: String
|
|
183
|
+
result << Riffer::Messages::User.new(prompt_text, files: file_parts)
|
|
183
184
|
result
|
|
184
185
|
end
|
|
185
186
|
|
|
@@ -36,7 +36,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
36
36
|
params = {
|
|
37
37
|
model: model,
|
|
38
38
|
contents: partitioned[:contents]
|
|
39
|
-
}
|
|
39
|
+
} #: Hash[Symbol, untyped]
|
|
40
40
|
|
|
41
41
|
params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction]
|
|
42
42
|
|
|
@@ -96,12 +96,12 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
#--
|
|
99
|
-
#: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
|
|
99
|
+
#: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage?
|
|
100
100
|
def extract_token_usage(response)
|
|
101
101
|
usage = response[:usageMetadata]
|
|
102
102
|
return nil unless usage
|
|
103
103
|
|
|
104
|
-
Riffer::TokenUsage.new(
|
|
104
|
+
Riffer::Providers::TokenUsage.new(
|
|
105
105
|
input_tokens: usage[:promptTokenCount] || 0,
|
|
106
106
|
output_tokens: usage[:candidatesTokenCount] || 0
|
|
107
107
|
)
|
|
@@ -114,6 +114,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
114
114
|
body = params.except(:model)
|
|
115
115
|
|
|
116
116
|
uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse")
|
|
117
|
+
host = uri.hostname #: String
|
|
117
118
|
request = Net::HTTP::Post.new(uri)
|
|
118
119
|
request["Content-Type"] = "application/json"
|
|
119
120
|
request["x-goog-api-key"] = @api_key
|
|
@@ -126,7 +127,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
126
127
|
buffer << chunk
|
|
127
128
|
|
|
128
129
|
while (match = buffer.match(/\r?\n\r?\n/))
|
|
129
|
-
|
|
130
|
+
match_end = match.end(0) #: Integer
|
|
131
|
+
frame = buffer.slice!(0, match_end).to_s.strip
|
|
130
132
|
next unless frame.start_with?("data: ")
|
|
131
133
|
|
|
132
134
|
json_str = frame.delete_prefix("data: ").strip
|
|
@@ -155,7 +157,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
155
157
|
usage = parsed[:usageMetadata]
|
|
156
158
|
if usage && usage[:candidatesTokenCount]
|
|
157
159
|
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
158
|
-
token_usage: Riffer::TokenUsage.new(
|
|
160
|
+
token_usage: Riffer::Providers::TokenUsage.new(
|
|
159
161
|
input_tokens: usage[:promptTokenCount] || 0,
|
|
160
162
|
output_tokens: usage[:candidatesTokenCount] || 0
|
|
161
163
|
)
|
|
@@ -164,7 +166,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
164
166
|
end
|
|
165
167
|
end
|
|
166
168
|
|
|
167
|
-
Net::HTTP.start(
|
|
169
|
+
Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
|
|
168
170
|
http.request(request) do |response|
|
|
169
171
|
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
170
172
|
|
|
@@ -182,8 +184,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
182
184
|
#--
|
|
183
185
|
#: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
184
186
|
def partition_messages(messages)
|
|
185
|
-
system_parts = []
|
|
186
|
-
contents = []
|
|
187
|
+
system_parts = [] #: Array[Hash[Symbol, untyped]]
|
|
188
|
+
contents = [] #: Array[Hash[Symbol, untyped]]
|
|
187
189
|
|
|
188
190
|
messages.each do |message|
|
|
189
191
|
case message
|
|
@@ -212,7 +214,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
212
214
|
end
|
|
213
215
|
end
|
|
214
216
|
|
|
215
|
-
result = {contents: contents}
|
|
217
|
+
result = {contents: contents} #: Hash[Symbol, untyped]
|
|
216
218
|
result[:system_instruction] = {parts: system_parts} unless system_parts.empty?
|
|
217
219
|
result
|
|
218
220
|
end
|
|
@@ -220,7 +222,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
220
222
|
#--
|
|
221
223
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
222
224
|
def convert_assistant_to_gemini_format(message)
|
|
223
|
-
parts = []
|
|
225
|
+
parts = [] #: Array[Hash[Symbol, untyped]]
|
|
224
226
|
parts << {text: message.content} if message.content && !message.content.empty?
|
|
225
227
|
|
|
226
228
|
message.tool_calls.each do |tc|
|
|
@@ -236,7 +238,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
236
238
|
end
|
|
237
239
|
|
|
238
240
|
#--
|
|
239
|
-
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
241
|
+
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
240
242
|
def convert_file_part_to_gemini_format(file)
|
|
241
243
|
if file.url?
|
|
242
244
|
raise Riffer::ArgumentError,
|
|
@@ -268,11 +270,12 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
268
270
|
#: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
|
|
269
271
|
def post_request(path, body)
|
|
270
272
|
uri = URI("#{BASE_URI}/#{path}")
|
|
273
|
+
host = uri.hostname #: String
|
|
271
274
|
request = Net::HTTP::Post.new(uri)
|
|
272
275
|
request["Content-Type"] = "application/json"
|
|
273
276
|
request["x-goog-api-key"] = @api_key
|
|
274
277
|
request.body = body.to_json
|
|
275
|
-
Net::HTTP.start(
|
|
278
|
+
Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
|
|
276
279
|
end
|
|
277
280
|
|
|
278
281
|
#--
|
|
@@ -25,10 +25,21 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
25
25
|
|
|
26
26
|
# Initializes the mock provider.
|
|
27
27
|
#
|
|
28
|
+
# +responses:+ accepts an array of response hashes in the same shape
|
|
29
|
+
# +#stub_response+ takes — raw +tool_calls:+ hashes are normalised to
|
|
30
|
+
# +Riffer::Messages::Assistant::ToolCall+ instances. This is the canonical
|
|
31
|
+
# way to pre-configure canned LLM responses on an agent via
|
|
32
|
+
# +provider_options responses: [...]+.
|
|
33
|
+
#
|
|
34
|
+
# Riffer::Providers::Mock.new(responses: [
|
|
35
|
+
# {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
|
|
36
|
+
# {content: "Final answer"}
|
|
37
|
+
# ])
|
|
38
|
+
#
|
|
28
39
|
#--
|
|
29
40
|
#: (**untyped) -> void
|
|
30
41
|
def initialize(**options)
|
|
31
|
-
@responses = options[:responses] || []
|
|
42
|
+
@responses = (options[:responses] || []).map { |r| normalize_response(r) }
|
|
32
43
|
@current_index = 0
|
|
33
44
|
@calls = []
|
|
34
45
|
@stubbed_responses = []
|
|
@@ -40,19 +51,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
40
51
|
#
|
|
41
52
|
# provider.stub_response("Hello")
|
|
42
53
|
# provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
|
|
43
|
-
# provider.stub_response("Final response", token_usage: Riffer::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
54
|
+
# provider.stub_response("Final response", token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
44
55
|
#
|
|
45
56
|
#--
|
|
46
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::TokenUsage?) -> void
|
|
57
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?) -> void
|
|
47
58
|
def stub_response(content, tool_calls: [], token_usage: nil)
|
|
48
|
-
|
|
49
|
-
Riffer::Messages::Assistant::ToolCall.new(
|
|
50
|
-
call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
|
|
51
|
-
name: tc[:name],
|
|
52
|
-
arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
@stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls, token_usage: token_usage}
|
|
59
|
+
@stubbed_responses << normalize_response(content: content, tool_calls: tool_calls, token_usage: token_usage)
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# Clears all stubbed responses.
|
|
@@ -65,6 +69,30 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
65
69
|
|
|
66
70
|
private
|
|
67
71
|
|
|
72
|
+
# Normalises a response hash into Mock's internal format. Accepts the
|
|
73
|
+
# +#stub_response+ kwargs shape (+content:+, +tool_calls:+, +token_usage:+)
|
|
74
|
+
# or a pre-built hash with already-converted ToolCall instances. Raw
|
|
75
|
+
# +tool_calls:+ hashes are wrapped in +Riffer::Messages::Assistant::ToolCall+.
|
|
76
|
+
#
|
|
77
|
+
#--
|
|
78
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
79
|
+
def normalize_response(response)
|
|
80
|
+
formatted_tool_calls = (response[:tool_calls] || []).map.with_index do |tc, idx|
|
|
81
|
+
next tc if tc.is_a?(Riffer::Messages::Assistant::ToolCall)
|
|
82
|
+
Riffer::Messages::Assistant::ToolCall.new(
|
|
83
|
+
call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
|
|
84
|
+
name: tc[:name],
|
|
85
|
+
arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
{
|
|
89
|
+
role: response[:role] || "assistant",
|
|
90
|
+
content: response[:content] || "",
|
|
91
|
+
tool_calls: formatted_tool_calls,
|
|
92
|
+
token_usage: response[:token_usage]
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
#--
|
|
69
97
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
70
98
|
def build_request_params(messages, model, options)
|
|
@@ -82,7 +110,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
82
110
|
end
|
|
83
111
|
|
|
84
112
|
#--
|
|
85
|
-
#: (untyped) -> Riffer::TokenUsage?
|
|
113
|
+
#: (untyped) -> Riffer::Providers::TokenUsage?
|
|
86
114
|
def extract_token_usage(response)
|
|
87
115
|
response[:token_usage]
|
|
88
116
|
end
|
|
@@ -146,7 +174,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
146
174
|
def next_response
|
|
147
175
|
if @stubbed_responses.any?
|
|
148
176
|
@stubbed_responses.shift
|
|
149
|
-
elsif @responses
|
|
177
|
+
elsif @current_index < @responses.size
|
|
150
178
|
response = @responses[@current_index]
|
|
151
179
|
@current_index += 1
|
|
152
180
|
response
|