rasti-ai 2.0.1 → 3.0.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -20
  3. data/AGENTS.md +614 -0
  4. data/README.md +133 -25
  5. data/Rakefile +2 -0
  6. data/lib/rasti/ai/anthropic/assistant.rb +139 -0
  7. data/lib/rasti/ai/anthropic/client.rb +58 -0
  8. data/lib/rasti/ai/anthropic/roles.rb +12 -0
  9. data/lib/rasti/ai/assistant.rb +8 -15
  10. data/lib/rasti/ai/client.rb +16 -3
  11. data/lib/rasti/ai/gemini/assistant.rb +42 -25
  12. data/lib/rasti/ai/gemini/client.rb +14 -0
  13. data/lib/rasti/ai/mcp/client.rb +60 -9
  14. data/lib/rasti/ai/mcp/{errors.rb → constants.rb} +4 -1
  15. data/lib/rasti/ai/mcp/server.rb +42 -47
  16. data/lib/rasti/ai/mcp/tools_registry.rb +64 -0
  17. data/lib/rasti/ai/open_ai/assistant.rb +9 -17
  18. data/lib/rasti/ai/open_ai/client.rb +17 -2
  19. data/lib/rasti/ai/tool_serializer.rb +35 -62
  20. data/lib/rasti/ai/usage.rb +2 -1
  21. data/lib/rasti/ai/version.rb +1 -1
  22. data/lib/rasti/ai.rb +10 -0
  23. data/rasti-ai.gemspec +4 -1
  24. data/spec/anthropic/assistant_spec.rb +349 -0
  25. data/spec/anthropic/client_spec.rb +203 -0
  26. data/spec/gemini/assistant_spec.rb +15 -66
  27. data/spec/gemini/client_spec.rb +50 -0
  28. data/spec/mcp/client_spec.rb +3 -1
  29. data/spec/mcp/server_spec.rb +195 -136
  30. data/spec/mcp/tools_registry_spec.rb +226 -0
  31. data/spec/minitest_helper.rb +29 -0
  32. data/spec/open_ai/assistant_spec.rb +20 -70
  33. data/spec/open_ai/client_spec.rb +53 -0
  34. data/spec/resources/anthropic/basic_request.json +1 -0
  35. data/spec/resources/anthropic/basic_response.json +20 -0
  36. data/spec/resources/anthropic/tool_request.json +1 -0
  37. data/spec/resources/anthropic/tool_response.json +22 -0
  38. data/spec/resources/gemini/basic_response.json +10 -3
  39. data/spec/tool_serializer_spec.rb +31 -6
  40. data/tasks/assistant.rake +94 -0
  41. metadata +46 -6
@@ -0,0 +1,349 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI::Anthropic::Assistant do
4
+
5
+ let(:api_url) { 'https://api.anthropic.com/v1/messages' }
6
+
7
+ let(:question) { 'How many goals has Messi scored for Barca?' }
8
+
9
+ let(:answer) { 'Lionel Messi scored 672 goals in 778 official matches for FC Barcelona.' }
10
+
11
+ def stub_anthropic_messages(question:, answer:, model:nil, json_schema:nil)
12
+ model ||= Rasti::AI.anthropic_default_model
13
+
14
+ body = read_json_resource('anthropic/basic_request.json', model: model, prompt: question)
15
+
16
+ if json_schema
17
+ body['tools'] = [{
18
+ 'name' => 'structured_output',
19
+ 'description' => 'Return the structured response',
20
+ 'input_schema' => {'type' => 'object', 'properties' => json_schema}
21
+ }]
22
+ body['tool_choice'] = {'type' => 'tool', 'name' => 'structured_output'}
23
+
24
+ stub_request(:post, api_url)
25
+ .with(body: JSON.dump(body))
26
+ .to_return(body: read_resource('anthropic/tool_response.json', name: 'structured_output', arguments: json_schema))
27
+ else
28
+ stub_request(:post, api_url)
29
+ .with(body: JSON.dump(body))
30
+ .to_return(body: read_resource('anthropic/basic_response.json', content: answer))
31
+ end
32
+ end
33
+
34
+ it 'Default' do
35
+ stub_anthropic_messages question: question, answer: answer
36
+
37
+ assistant = Rasti::AI::Anthropic::Assistant.new
38
+
39
+ response = assistant.call question
40
+
41
+ assert_equal answer, response
42
+ end
43
+
44
+ describe 'Customized' do
45
+
46
+ it 'Client' do
47
+ client_arguments = [
48
+ {
49
+ model: nil,
50
+ system: nil,
51
+ tools: [],
52
+ tool_choice: nil,
53
+ thinking: nil,
54
+ messages: [
55
+ {
56
+ role: Rasti::AI::Anthropic::Roles::USER,
57
+ content: question
58
+ }
59
+ ]
60
+ }
61
+ ]
62
+
63
+ client_response = read_json_resource 'anthropic/basic_response.json', content: answer
64
+
65
+ client = Minitest::Mock.new
66
+ client.expect :messages, client_response, client_arguments
67
+
68
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client
69
+
70
+ response = assistant.call question
71
+
72
+ assert_equal answer, response
73
+
74
+ client.verify
75
+ end
76
+
77
+ it 'State' do
78
+ context = 'Act as sports journalist'
79
+ state = Rasti::AI::AssistantState.new context: context
80
+
81
+ request_body = {
82
+ model: Rasti::AI.anthropic_default_model,
83
+ max_tokens: 4096,
84
+ messages: [
85
+ {
86
+ role: Rasti::AI::Anthropic::Roles::USER,
87
+ content: question
88
+ }
89
+ ],
90
+ system: context
91
+ }
92
+
93
+ stub_request(:post, api_url)
94
+ .with(body: JSON.dump(request_body))
95
+ .to_return(body: read_resource('anthropic/basic_response.json', content: answer))
96
+
97
+ assistant = Rasti::AI::Anthropic::Assistant.new state: state
98
+
99
+ response = assistant.call question
100
+
101
+ expected_assistant_message = {
102
+ role: Rasti::AI::Anthropic::Roles::ASSISTANT,
103
+ content: answer
104
+ }
105
+
106
+ assert_equal answer, response
107
+ assert_equal 2, state.messages.count
108
+ assert_equal expected_assistant_message, state.messages.last
109
+ end
110
+
111
+ it 'Model' do
112
+ model = SecureRandom.uuid
113
+
114
+ stub_anthropic_messages question: question, answer: answer, model: model
115
+
116
+ assistant = Rasti::AI::Anthropic::Assistant.new model: model
117
+
118
+ response = assistant.call question
119
+
120
+ assert_equal answer, response
121
+ end
122
+
123
+ it 'Thinking' do
124
+ body = read_json_resource('anthropic/basic_request.json', model: Rasti::AI.anthropic_default_model, prompt: question)
125
+ body['thinking'] = {'type' => 'enabled', 'budget_tokens' => 8_000}
126
+
127
+ stub_request(:post, api_url)
128
+ .with(body: JSON.dump(body))
129
+ .to_return(body: read_resource('anthropic/basic_response.json', content: answer))
130
+
131
+ assistant = Rasti::AI::Anthropic::Assistant.new thinking: 'medium'
132
+
133
+ response = assistant.call question
134
+
135
+ assert_equal answer, response
136
+ end
137
+
138
+ it 'JSON Schema' do
139
+ json_schema = {'answer' => 'Response answer'}
140
+
141
+ stub_anthropic_messages question: question, answer: answer, json_schema: json_schema
142
+
143
+ assistant = Rasti::AI::Anthropic::Assistant.new json_schema: json_schema
144
+
145
+ response = assistant.call question
146
+
147
+ assert_equal 'Response answer', JSON.parse(response)['answer']
148
+ end
149
+
150
+ end
151
+
152
+
153
+
154
+ describe 'Tools' do
155
+
156
+ let(:client) { Minitest::Mock.new }
157
+
158
+ let(:tool_response) do
159
+ read_json_resource(
160
+ 'anthropic/tool_response.json',
161
+ name: 'goals_by_player',
162
+ arguments: {
163
+ player: 'Lionel Messi',
164
+ team: 'Barcelona'
165
+ }
166
+ )
167
+ end
168
+
169
+ let(:tool_result) { '672' }
170
+
171
+ let(:error_message) { 'There was an error using a tool' }
172
+
173
+ def basic_response(content)
174
+ read_json_resource 'anthropic/basic_response.json', content: content
175
+ end
176
+
177
+ def stub_client_request(role:, content:, response:, tools:[])
178
+ serialized_tools = tools.map do |tool|
179
+ raw = Rasti::AI::ToolSerializer.serialize(tool.class)
180
+ result = raw.dup
181
+ if result.key?(:inputSchema)
182
+ result[:input_schema] = result.delete(:inputSchema)
183
+ end
184
+ result
185
+ end
186
+
187
+ client.expect :messages, response do |params|
188
+ last_message = params[:messages].last
189
+ last_message[:role] == role &&
190
+ last_message[:content] == content &&
191
+ params[:tools] == serialized_tools
192
+ end
193
+ end
194
+
195
+ it 'Call function' do
196
+ tool = GoalsByPlayer.new
197
+
198
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
199
+ content: question,
200
+ tools: [tool],
201
+ response: tool_response
202
+
203
+ expected_tool_result_content = [{
204
+ type: 'tool_result',
205
+ tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
206
+ content: tool_result
207
+ }]
208
+
209
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
210
+ content: expected_tool_result_content,
211
+ tools: [tool],
212
+ response: basic_response(answer)
213
+
214
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
215
+
216
+ response = assistant.call question
217
+
218
+ assert_equal answer, response
219
+
220
+ client.verify
221
+ end
222
+
223
+ it 'Tool failure' do
224
+ tool = GoalsByPlayer.new
225
+ tool.define_singleton_method :call do |*args|
226
+ raise 'Broken tool'
227
+ end
228
+
229
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
230
+ content: question,
231
+ tools: [tool],
232
+ response: tool_response
233
+
234
+ expected_tool_result_content = [{
235
+ type: 'tool_result',
236
+ tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
237
+ content: 'Error: Broken tool'
238
+ }]
239
+
240
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
241
+ content: expected_tool_result_content,
242
+ tools: [tool],
243
+ response: basic_response(error_message)
244
+
245
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
246
+
247
+ response = assistant.call question
248
+
249
+ assert_equal error_message, response
250
+
251
+ client.verify
252
+ end
253
+
254
+ it 'Undefined tool' do
255
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
256
+ content: question,
257
+ response: tool_response
258
+
259
+ expected_tool_result_content = [{
260
+ type: 'tool_result',
261
+ tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
262
+ content: 'Error: Undefined tool goals_by_player'
263
+ }]
264
+
265
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
266
+ content: expected_tool_result_content,
267
+ response: basic_response(error_message)
268
+
269
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: []
270
+
271
+ response = assistant.call question
272
+
273
+ assert_equal error_message, response
274
+
275
+ client.verify
276
+ end
277
+
278
+ it 'Cached result' do
279
+ mock = Minitest::Mock.new
280
+ mock.expect :call, tool_result, [{'player' => 'Lionel Messi', 'team' => 'Barcelona'}]
281
+
282
+ tool = GoalsByPlayer.new
283
+ tool.define_singleton_method :call do |*args|
284
+ mock.call(*args)
285
+ end
286
+
287
+ expected_tool_result_content = [{
288
+ type: 'tool_result',
289
+ tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
290
+ content: tool_result
291
+ }]
292
+
293
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
294
+
295
+ 5.times do
296
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
297
+ content: question,
298
+ tools: [tool],
299
+ response: tool_response
300
+
301
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
302
+ content: expected_tool_result_content,
303
+ tools: [tool],
304
+ response: basic_response(answer)
305
+
306
+ response = assistant.call question
307
+
308
+ assert_equal answer, response
309
+ end
310
+
311
+ client.verify
312
+ end
313
+
314
+ it 'Custom logger' do
315
+ log_output = StringIO.new
316
+ logger = Logger.new log_output
317
+
318
+ tool = GoalsByPlayer.new
319
+
320
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
321
+ content: question,
322
+ tools: [tool],
323
+ response: tool_response
324
+
325
+ expected_tool_result_content = [{
326
+ type: 'tool_result',
327
+ tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
328
+ content: tool_result
329
+ }]
330
+
331
+ stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
332
+ content: expected_tool_result_content,
333
+ tools: [tool],
334
+ response: basic_response(answer)
335
+
336
+ assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool], logger: logger
337
+
338
+ response = assistant.call question
339
+
340
+ assert_equal answer, response
341
+
342
+ refute_empty log_output.string
343
+
344
+ client.verify
345
+ end
346
+
347
+ end
348
+
349
+ end
@@ -0,0 +1,203 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI::Anthropic::Client do
4
+
5
+ let(:api_url) { 'https://api.anthropic.com/v1/messages' }
6
+
7
+ def user_message(content)
8
+ {
9
+ role: Rasti::AI::Anthropic::Roles::USER,
10
+ content: content
11
+ }
12
+ end
13
+
14
+ describe 'Basic message' do
15
+
16
+ let(:question) { 'who is Messi?' }
17
+
18
+ let(:answer) { 'Lionel Messi is the best player ever' }
19
+
20
+ def stub_anthropic_messages(api_key:nil, model:nil)
21
+ api_key ||= Rasti::AI.anthropic_api_key
22
+ model ||= Rasti::AI.anthropic_default_model
23
+
24
+ stub_request(:post, api_url)
25
+ .with(
26
+ headers: {
27
+ 'x-api-key' => api_key,
28
+ 'anthropic-version' => '2023-06-01'
29
+ },
30
+ body: read_resource('anthropic/basic_request.json', model: model, prompt: question)
31
+ )
32
+ .to_return(body: read_resource('anthropic/basic_response.json', content: answer))
33
+ end
34
+
35
+ def assert_response_content(response, expected_content)
36
+ text_block = response['content'].find { |b| b['type'] == 'text' }
37
+ assert_equal expected_content, text_block['text']
38
+ end
39
+
40
+ it 'Default API key, model and logger' do
41
+ stub_anthropic_messages
42
+
43
+ client = Rasti::AI::Anthropic::Client.new
44
+
45
+ response = client.messages messages: [user_message(question)]
46
+
47
+ assert_response_content response, answer
48
+ end
49
+
50
+ it 'Custom API key' do
51
+ custom_api_key = SecureRandom.uuid
52
+
53
+ stub_anthropic_messages api_key: custom_api_key
54
+
55
+ client = Rasti::AI::Anthropic::Client.new api_key: custom_api_key
56
+
57
+ response = client.messages messages: [user_message(question)]
58
+
59
+ assert_response_content response, answer
60
+ end
61
+
62
+ it 'Custom model' do
63
+ custom_model = SecureRandom.uuid
64
+
65
+ stub_anthropic_messages model: custom_model
66
+
67
+ client = Rasti::AI::Anthropic::Client.new
68
+
69
+ response = client.messages messages: [user_message(question)], model: custom_model
70
+
71
+ assert_response_content response, answer
72
+ end
73
+
74
+ it 'Custom logger' do
75
+ log_output = StringIO.new
76
+ logger = Logger.new log_output
77
+
78
+ stub_anthropic_messages
79
+
80
+ client = Rasti::AI::Anthropic::Client.new logger: logger
81
+
82
+ response = client.messages messages: [user_message(question)]
83
+
84
+ assert_response_content response, answer
85
+
86
+ refute_empty log_output.string
87
+ end
88
+
89
+ describe 'Usage tracker' do
90
+
91
+ it 'Track usage' do
92
+ stub_anthropic_messages
93
+
94
+ tracked = []
95
+ tracker = ->(usage) { tracked << usage }
96
+
97
+ client = Rasti::AI::Anthropic::Client.new usage_tracker: tracker
98
+
99
+ client.messages messages: [user_message(question)]
100
+
101
+ assert_equal 1, tracked.count
102
+
103
+ expected_raw = {
104
+ 'input_tokens' => 25,
105
+ 'output_tokens' => 11,
106
+ 'cache_creation_input_tokens' => 0,
107
+ 'cache_read_input_tokens' => 0
108
+ }
109
+
110
+ usage = tracked[0]
111
+ assert_instance_of Rasti::AI::Usage, usage
112
+ assert_equal 'anthropic', usage.provider
113
+ assert_equal 'claude-test', usage.model
114
+ assert_equal 25, usage.input_tokens
115
+ assert_equal 11, usage.output_tokens
116
+ assert_equal 0, usage.cached_tokens
117
+ assert_equal 0, usage.reasoning_tokens
118
+ assert_equal expected_raw, usage.raw
119
+ end
120
+
121
+ it 'Without tracker' do
122
+ stub_anthropic_messages
123
+
124
+ client = Rasti::AI::Anthropic::Client.new
125
+
126
+ response = client.messages messages: [user_message(question)]
127
+
128
+ assert_response_content response, answer
129
+ end
130
+
131
+ end
132
+
133
+ end
134
+
135
+ it 'Request error' do
136
+ stub_request(:post, api_url)
137
+ .to_return(status: 400, body: '{"type":"error","error":{"type":"invalid_request_error","message":"Test error"}}')
138
+
139
+ client = Rasti::AI::Anthropic::Client.new
140
+
141
+ error = assert_raises(Rasti::AI::Errors::RequestFail) do
142
+ client.messages messages: ['invalid message']
143
+ end
144
+
145
+ assert_includes error.message, 'Response: 400'
146
+ end
147
+
148
+ it 'Tool call' do
149
+ question = 'how many goals did messi for barca'
150
+ tool_name = 'player_goals'
151
+
152
+ tool = {
153
+ name: tool_name,
154
+ description: 'Gets the number of goals scored by a player for a specific team',
155
+ input_schema: {
156
+ type: 'object',
157
+ properties: {
158
+ name: {
159
+ type: 'string',
160
+ description: 'Full name of the player'
161
+ },
162
+ team: {
163
+ type: 'string',
164
+ description: 'Name of the team the player was part of'
165
+ }
166
+ },
167
+ required: ['name', 'team']
168
+ }
169
+ }
170
+
171
+ arguments = {
172
+ name: 'Lionel Messi',
173
+ team: 'FC Barcelona'
174
+ }
175
+
176
+ stub_request(:post, api_url)
177
+ .with(
178
+ headers: {
179
+ 'x-api-key' => Rasti::AI.anthropic_api_key,
180
+ 'anthropic-version' => '2023-06-01'
181
+ },
182
+ body: read_resource(
183
+ 'anthropic/tool_request.json',
184
+ model: Rasti::AI.anthropic_default_model,
185
+ prompt: question,
186
+ tools: [tool]
187
+ )
188
+ )
189
+ .to_return(body: read_resource('anthropic/tool_response.json', name: tool_name, arguments: arguments))
190
+
191
+ client = Rasti::AI::Anthropic::Client.new
192
+
193
+ response = client.messages messages: [{role: 'user', content: question}],
194
+ tools: [tool],
195
+ tool_choice: {type: 'auto'}
196
+
197
+ tool_use = response['content'].find { |b| b['type'] == 'tool_use' }
198
+
199
+ assert_equal tool_name, tool_use['name']
200
+ assert_equal({'name' => 'Lionel Messi', 'team' => 'FC Barcelona'}, tool_use['input'])
201
+ end
202
+
203
+ end
@@ -110,6 +110,21 @@ describe Rasti::AI::Gemini::Assistant do
110
110
  assert_equal answer, response
111
111
  end
112
112
 
113
+ it 'Thinking' do
114
+ body = read_json_resource('gemini/basic_request.json', prompt: question)
115
+ body['generation_config'] = {'thinking_config' => {'thinking_budget' => 8_192}}
116
+
117
+ stub_request(:post, api_url)
118
+ .with(body: JSON.dump(body))
119
+ .to_return(body: read_resource('gemini/basic_response.json', content: answer))
120
+
121
+ assistant = Rasti::AI::Gemini::Assistant.new thinking: 'medium'
122
+
123
+ response = assistant.call question
124
+
125
+ assert_equal answer, response
126
+ end
127
+
113
128
  it 'JSON Schema' do
114
129
  json_schema = {answer: 'Response answer'}
115
130
  json_answer = "{\\\"answer\\\": \\\"#{answer}\\\"}"
@@ -132,73 +147,7 @@ describe Rasti::AI::Gemini::Assistant do
132
147
 
133
148
  end
134
149
 
135
- describe 'Usage tracker' do
136
-
137
- it 'Track usage' do
138
- stub_gemini_generate_content question: question, answer: answer
139
-
140
- tracked = []
141
- tracker = ->(usage) { tracked << usage }
142
-
143
- assistant = Rasti::AI::Gemini::Assistant.new usage_tracker: tracker
144
-
145
- assistant.call question
146
-
147
- assert_equal 1, tracked.count
148
-
149
- usage = tracked[0]
150
- assert_instance_of Rasti::AI::Usage, usage
151
- assert_equal :gemini, usage.provider
152
- assert_equal 'gemini-test', usage.model
153
- assert_equal 10, usage.input_tokens
154
- assert_equal 50, usage.output_tokens
155
- assert_equal 0, usage.cached_tokens
156
- assert_equal 0, usage.reasoning_tokens
157
- end
158
-
159
- it 'Track usage with tool calls' do
160
- client = Minitest::Mock.new
161
-
162
- tool_response = read_json_resource(
163
- 'gemini/tool_response.json',
164
- name: 'goals_by_player',
165
- arguments: {player: 'Lionel Messi', team: 'Barcelona'}
166
- )
167
-
168
- basic_resp = read_json_resource('gemini/basic_response.json', content: answer)
169
-
170
- client.expect :generate_content, tool_response do |params| true end
171
- client.expect :generate_content, basic_resp do |params| true end
172
-
173
- tool = GoalsByPlayer.new
174
150
 
175
- tracked = []
176
- tracker = ->(usage) { tracked << usage }
177
-
178
- assistant = Rasti::AI::Gemini::Assistant.new client: client, tools: [tool], usage_tracker: tracker
179
-
180
- assistant.call question
181
-
182
- assert_equal 2, tracked.count
183
- assert_equal 20, tracked[0].input_tokens
184
- assert_equal 10, tracked[0].output_tokens
185
- assert_equal 10, tracked[1].input_tokens
186
- assert_equal 50, tracked[1].output_tokens
187
-
188
- client.verify
189
- end
190
-
191
- it 'Without tracker' do
192
- stub_gemini_generate_content question: question, answer: answer
193
-
194
- assistant = Rasti::AI::Gemini::Assistant.new
195
-
196
- response = assistant.call question
197
-
198
- assert_equal answer, response
199
- end
200
-
201
- end
202
151
 
203
152
  describe 'Tools' do
204
153
 
@@ -83,6 +83,56 @@ describe Rasti::AI::Gemini::Client do
83
83
  refute_empty log_output.string
84
84
  end
85
85
 
86
+ describe 'Usage tracker' do
87
+
88
+ it 'Track usage' do
89
+ stub_gemini_generate_content
90
+
91
+ tracked = []
92
+ tracker = ->(usage) { tracked << usage }
93
+
94
+ client = Rasti::AI::Gemini::Client.new usage_tracker: tracker
95
+
96
+ client.generate_content contents: [user_content(question)]
97
+
98
+ assert_equal 1, tracked.count
99
+
100
+ expected_raw = {
101
+ 'promptTokenCount' => 4,
102
+ 'candidatesTokenCount' => 18,
103
+ 'totalTokenCount' => 275,
104
+ 'promptTokensDetails' => [
105
+ {
106
+ 'modality' => 'TEXT',
107
+ 'tokenCount' => 4
108
+ }
109
+ ],
110
+ 'thoughtsTokenCount' => 253
111
+ }
112
+
113
+ usage = tracked[0]
114
+ assert_instance_of Rasti::AI::Usage, usage
115
+ assert_equal 'gemini', usage.provider
116
+ assert_equal 'gemini-test', usage.model
117
+ assert_equal 4, usage.input_tokens
118
+ assert_equal 18, usage.output_tokens
119
+ assert_equal 0, usage.cached_tokens
120
+ assert_equal 253, usage.reasoning_tokens
121
+ assert_equal expected_raw, usage.raw
122
+ end
123
+
124
+ it 'Without tracker' do
125
+ stub_gemini_generate_content
126
+
127
+ client = Rasti::AI::Gemini::Client.new
128
+
129
+ response = client.generate_content contents: [user_content(question)]
130
+
131
+ assert_response_content response, answer
132
+ end
133
+
134
+ end
135
+
86
136
  end
87
137
 
88
138
  it 'Request error' do