rasti-ai 3.1.0 → 3.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.features/api-normalization/api-design.md +184 -0
  3. data/.features/api-normalization/overview.md +230 -0
  4. data/AGENTS.md +125 -15
  5. data/README.md +143 -19
  6. data/lib/rasti/ai/anthropic/assistant.rb +1 -11
  7. data/lib/rasti/ai/anthropic/client.rb +6 -28
  8. data/lib/rasti/ai/anthropic/provider.rb +87 -0
  9. data/lib/rasti/ai/assistant.rb +10 -11
  10. data/lib/rasti/ai/client.rb +13 -10
  11. data/lib/rasti/ai/errors.rb +8 -0
  12. data/lib/rasti/ai/gemini/assistant.rb +1 -11
  13. data/lib/rasti/ai/gemini/client.rb +2 -24
  14. data/lib/rasti/ai/gemini/provider.rb +90 -0
  15. data/lib/rasti/ai/huawei_maas/assistant.rb +0 -7
  16. data/lib/rasti/ai/huawei_maas/client.rb +0 -19
  17. data/lib/rasti/ai/huawei_maas/provider.rb +25 -0
  18. data/lib/rasti/ai/huawei_maas/roles.rb +1 -8
  19. data/lib/rasti/ai/open_ai/assistant.rb +0 -4
  20. data/lib/rasti/ai/open_ai/client.rb +3 -33
  21. data/lib/rasti/ai/open_ai/provider.rb +74 -0
  22. data/lib/rasti/ai/open_router/assistant.rb +0 -7
  23. data/lib/rasti/ai/open_router/client.rb +0 -19
  24. data/lib/rasti/ai/open_router/provider.rb +25 -0
  25. data/lib/rasti/ai/open_router/roles.rb +1 -8
  26. data/lib/rasti/ai/provider.rb +197 -0
  27. data/lib/rasti/ai/provider_aware.rb +17 -0
  28. data/lib/rasti/ai/result.rb +11 -0
  29. data/lib/rasti/ai/roles.rb +11 -0
  30. data/lib/rasti/ai/version.rb +1 -1
  31. data/lib/rasti/ai.rb +93 -0
  32. data/spec/anthropic/provider_spec.rb +106 -0
  33. data/spec/gemini/provider_spec.rb +90 -0
  34. data/spec/open_ai/provider_spec.rb +101 -0
  35. data/spec/rasti_ai_spec.rb +322 -0
  36. data/spec/resources/open_ai/conversation_request.json +1 -0
  37. data/spec/resources/open_ai/system_request.json +1 -0
  38. metadata +25 -2
@@ -0,0 +1,106 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI::Anthropic::Provider do
4
+
5
+ let(:provider) { Rasti::AI.provider :anthropic }
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_messages(request_body, response_body=nil)
12
+ stub_request(:post, 'https://api.anthropic.com/v1/messages')
13
+ .with(headers: {'x-api-key' => Rasti::AI.anthropic_api_key}, body: JSON.dump(request_body))
14
+ .to_return(body: response_body || read_resource('anthropic/basic_response.json', content: answer))
15
+ end
16
+
17
+ it 'Name, model and classes' do
18
+ assert_equal :anthropic, provider.name
19
+ assert_equal Rasti::AI.anthropic_default_model, provider.model
20
+ assert_instance_of Rasti::AI::Anthropic::Client, provider.build_client
21
+ assert_instance_of Rasti::AI::Anthropic::Assistant, provider.create_assistant
22
+ end
23
+
24
+ it 'Generate text' do
25
+ stub_messages model: Rasti::AI.anthropic_default_model,
26
+ max_tokens: 4096,
27
+ messages: [
28
+ {
29
+ role: 'user',
30
+ content: question
31
+ }
32
+ ]
33
+
34
+ result = provider.generate_text prompt: question
35
+
36
+ assert_equal answer, result.content
37
+ assert_equal 'anthropic', result.usage.provider
38
+ assert_equal 'end_turn', result.raw['stop_reason']
39
+ end
40
+
41
+ it 'Generate text with system, thinking and model override' do
42
+ stub_messages model: 'claude-sonnet-4-5',
43
+ max_tokens: 4096,
44
+ messages: [
45
+ {
46
+ role: 'user',
47
+ content: question
48
+ }
49
+ ],
50
+ thinking: {type: 'enabled', budget_tokens: 16_000},
51
+ system: 'Act as sports journalist'
52
+
53
+ result = provider.generate_text prompt: question,
54
+ model: 'claude-sonnet-4-5',
55
+ system: 'Act as sports journalist',
56
+ thinking: 'high'
57
+
58
+ assert_equal answer, result.content
59
+ end
60
+
61
+ it 'Generate text with json schema' do
62
+ json_schema = {player: 'string'}
63
+
64
+ structured_response = {
65
+ 'content' => [
66
+ {
67
+ 'type' => 'tool_use',
68
+ 'id' => 'toolu_1',
69
+ 'name' => 'structured_output',
70
+ 'input' => {'player' => 'Lionel Messi'}
71
+ }
72
+ ],
73
+ 'stop_reason' => 'tool_use'
74
+ }
75
+
76
+ stub_messages(
77
+ {
78
+ model: Rasti::AI.anthropic_default_model,
79
+ max_tokens: 4096,
80
+ messages: [
81
+ {
82
+ role: 'user',
83
+ content: question
84
+ }
85
+ ],
86
+ tools: [
87
+ {
88
+ name: 'structured_output',
89
+ description: 'Return the structured response',
90
+ input_schema: {
91
+ type: 'object',
92
+ properties: json_schema
93
+ }
94
+ }
95
+ ],
96
+ tool_choice: {type: 'tool', name: 'structured_output'}
97
+ },
98
+ JSON.dump(structured_response)
99
+ )
100
+
101
+ result = provider.generate_text prompt: question, json_schema: json_schema
102
+
103
+ assert_equal '{"player":"Lionel Messi"}', result.content
104
+ end
105
+
106
+ end
@@ -0,0 +1,90 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI::Gemini::Provider do
4
+
5
+ let(:provider) { Rasti::AI.provider :gemini }
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 api_url(model:nil)
12
+ "https://generativelanguage.googleapis.com/v1beta/models/#{model || Rasti::AI.gemini_default_model}:generateContent?key=#{Rasti::AI.gemini_api_key}"
13
+ end
14
+
15
+ def stub_generate_content(request_body)
16
+ stub_request(:post, api_url)
17
+ .with(body: JSON.dump(request_body))
18
+ .to_return(body: read_resource('gemini/basic_response.json', content: answer))
19
+ end
20
+
21
+ it 'Name, model and classes' do
22
+ assert_equal :gemini, provider.name
23
+ assert_equal Rasti::AI.gemini_default_model, provider.model
24
+ assert_instance_of Rasti::AI::Gemini::Client, provider.build_client
25
+ assert_instance_of Rasti::AI::Gemini::Assistant, provider.create_assistant
26
+ end
27
+
28
+ it 'Generate text' do
29
+ stub_generate_content contents: [
30
+ {
31
+ role: 'user',
32
+ parts: [{text: question}]
33
+ }
34
+ ]
35
+
36
+ result = provider.generate_text prompt: question
37
+
38
+ assert_equal answer, result.content
39
+ assert_equal 'gemini', result.usage.provider
40
+ assert_equal 'gemini-test', result.raw['modelVersion']
41
+ end
42
+
43
+ it 'Generate text with generic roles and system' do
44
+ stub_generate_content contents: [
45
+ {
46
+ role: 'user',
47
+ parts: [{text: 'who is the best player'}]
48
+ },
49
+ {
50
+ role: 'model',
51
+ parts: [{text: 'Lionel Messi'}]
52
+ },
53
+ {
54
+ role: 'user',
55
+ parts: [{text: question}]
56
+ }
57
+ ],
58
+ system_instruction: {
59
+ parts: [{text: 'Act as sports journalist'}]
60
+ }
61
+
62
+ messages = [
63
+ {role: Rasti::AI::Roles::SYSTEM, content: 'Act as sports journalist'},
64
+ {role: Rasti::AI::Roles::USER, content: 'who is the best player'},
65
+ {role: Rasti::AI::Roles::ASSISTANT, content: 'Lionel Messi'},
66
+ {role: Rasti::AI::Roles::USER, content: question}
67
+ ]
68
+
69
+ assert_equal answer, provider.generate_text(messages: messages).content
70
+ end
71
+
72
+ it 'Generate text with json schema and thinking' do
73
+ json_schema = {type: 'object', properties: {player: {type: 'string'}}}
74
+
75
+ stub_generate_content contents: [
76
+ {
77
+ role: 'user',
78
+ parts: [{text: question}]
79
+ }
80
+ ],
81
+ generation_config: {
82
+ thinking_config: {thinking_budget: 8_192},
83
+ response_mime_type: 'application/json',
84
+ response_schema: json_schema
85
+ }
86
+
87
+ assert_equal answer, provider.generate_text(prompt: question, json_schema: json_schema, thinking: 'medium').content
88
+ end
89
+
90
+ end
@@ -0,0 +1,101 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI::OpenAI::Provider do
4
+
5
+ let(:question) { 'How many goals has Messi scored for Barca?' }
6
+
7
+ let(:answer) { 'Lionel Messi scored 672 goals in 778 official matches for FC Barcelona.' }
8
+
9
+ def stub_chat_completions(url, request_body)
10
+ stub_request(:post, url)
11
+ .with(body: JSON.dump(request_body))
12
+ .to_return(body: read_resource('open_ai/basic_response.json', content: answer))
13
+ end
14
+
15
+ def basic_body(model)
16
+ {
17
+ model: model,
18
+ messages: [
19
+ {
20
+ role: 'user',
21
+ content: question
22
+ }
23
+ ],
24
+ tools: [],
25
+ tool_choice: 'none'
26
+ }
27
+ end
28
+
29
+ it 'Name, model and classes' do
30
+ provider = Rasti::AI.provider :open_ai
31
+
32
+ assert_equal :open_ai, provider.name
33
+ assert_equal Rasti::AI.openai_default_model, provider.model
34
+ assert_instance_of Rasti::AI::OpenAI::Client, provider.build_client
35
+ assert_instance_of Rasti::AI::OpenAI::Assistant, provider.create_assistant
36
+ end
37
+
38
+ it 'Generate text' do
39
+ stub_chat_completions 'https://api.openai.com/v1/chat/completions', basic_body(Rasti::AI.openai_default_model)
40
+
41
+ result = Rasti::AI.provider(:open_ai).generate_text prompt: question
42
+
43
+ assert_equal answer, result.content
44
+ assert_equal 'open_ai', result.usage.provider
45
+ end
46
+
47
+ it 'Generate text with json schema and thinking' do
48
+ json_schema = {player: 'string'}
49
+
50
+ body = basic_body(Rasti::AI.openai_default_model).merge(
51
+ response_format: {
52
+ type: 'json_schema',
53
+ json_schema: json_schema
54
+ },
55
+ reasoning_effort: 'low'
56
+ )
57
+
58
+ stub_chat_completions 'https://api.openai.com/v1/chat/completions', body
59
+
60
+ result = Rasti::AI.provider(:open_ai).generate_text prompt: question, json_schema: json_schema, thinking: 'low'
61
+
62
+ assert_equal answer, result.content
63
+ end
64
+
65
+ describe 'OpenAI compatible providers' do
66
+
67
+ it 'OpenRouter' do
68
+ provider = Rasti::AI.provider :open_router
69
+
70
+ assert_equal :open_router, provider.name
71
+ assert_equal Rasti::AI.openrouter_default_model, provider.model
72
+ assert_instance_of Rasti::AI::OpenRouter::Client, provider.build_client
73
+ assert_instance_of Rasti::AI::OpenRouter::Assistant, provider.create_assistant
74
+
75
+ stub_chat_completions 'https://openrouter.ai/api/v1/chat/completions', basic_body(Rasti::AI.openrouter_default_model)
76
+
77
+ result = provider.generate_text prompt: question
78
+
79
+ assert_equal answer, result.content
80
+ assert_equal 'open_router', result.usage.provider
81
+ end
82
+
83
+ it 'Huawei MaaS' do
84
+ provider = Rasti::AI.provider :huawei_maas
85
+
86
+ assert_equal :huawei_maas, provider.name
87
+ assert_equal Rasti::AI.huawei_maas_default_model, provider.model
88
+ assert_instance_of Rasti::AI::HuaweiMaaS::Client, provider.build_client
89
+ assert_instance_of Rasti::AI::HuaweiMaaS::Assistant, provider.create_assistant
90
+
91
+ stub_chat_completions 'https://api-ap-southeast-1.modelarts-maas.com/v2/chat/completions', basic_body(Rasti::AI.huawei_maas_default_model)
92
+
93
+ result = provider.generate_text prompt: question
94
+
95
+ assert_equal answer, result.content
96
+ assert_equal 'huawei_maas', result.usage.provider
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,322 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::AI do
4
+
5
+ let(:question) { 'How many goals has Messi scored for Barca?' }
6
+
7
+ let(:answer) { 'Lionel Messi scored 672 goals in 778 official matches for FC Barcelona.' }
8
+
9
+ def stub_open_ai(model:nil, prompt:nil, content:nil)
10
+ stub_request(:post, 'https://api.openai.com/v1/chat/completions')
11
+ .with(body: read_resource('open_ai/basic_request.json', model: model || Rasti::AI.openai_default_model, prompt: prompt || question))
12
+ .to_return(body: read_resource('open_ai/basic_response.json', content: content || answer))
13
+ end
14
+
15
+ def with_default_provider(provider)
16
+ previous = Rasti::AI.default_provider
17
+ Rasti::AI.default_provider = provider
18
+ yield
19
+ ensure
20
+ Rasti::AI.default_provider = previous
21
+ end
22
+
23
+ describe 'Create assistant' do
24
+
25
+ it 'Provider' do
26
+ assistant = Rasti::AI.create_assistant provider: :open_ai
27
+
28
+ assert_instance_of Rasti::AI::OpenAI::Assistant, assistant
29
+ end
30
+
31
+ it 'Provider alias' do
32
+ assistant = Rasti::AI.create_assistant provider: 'openai'
33
+
34
+ assert_instance_of Rasti::AI::OpenAI::Assistant, assistant
35
+ end
36
+
37
+ it 'Provider and model' do
38
+ assistant = Rasti::AI.create_assistant provider: :anthropic, model: 'claude-sonnet-4-5'
39
+
40
+ assert_instance_of Rasti::AI::Anthropic::Assistant, assistant
41
+ assert_equal 'claude-sonnet-4-5', assistant.model
42
+ end
43
+
44
+ it 'Provider and model as single string' do
45
+ assistant = Rasti::AI.create_assistant model: 'gemini:gemini-2.0-flash'
46
+
47
+ assert_instance_of Rasti::AI::Gemini::Assistant, assistant
48
+ assert_equal 'gemini-2.0-flash', assistant.model
49
+ end
50
+
51
+ it 'Default provider from config' do
52
+ with_default_provider :anthropic do
53
+ assistant = Rasti::AI.create_assistant
54
+
55
+ assert_instance_of Rasti::AI::Anthropic::Assistant, assistant
56
+ end
57
+ end
58
+
59
+ it 'Explicit provider takes precedence over config' do
60
+ with_default_provider :anthropic do
61
+ assistant = Rasti::AI.create_assistant provider: :gemini
62
+
63
+ assert_instance_of Rasti::AI::Gemini::Assistant, assistant
64
+ end
65
+ end
66
+
67
+ it 'Undefined provider' do
68
+ with_default_provider nil do
69
+ error = assert_raises ArgumentError do
70
+ Rasti::AI.create_assistant
71
+ end
72
+
73
+ assert_match /\AUndefined provider/, error.message
74
+ end
75
+ end
76
+
77
+ it 'Unknown provider' do
78
+ assert_raises Rasti::AI::Errors::UnknownProvider do
79
+ Rasti::AI.create_assistant provider: :invalid
80
+ end
81
+ end
82
+
83
+ it 'Thinking, tools and json schema' do
84
+ assistant = Rasti::AI.create_assistant provider: :open_ai,
85
+ thinking: 'high',
86
+ tools: [GoalsByPlayer.new],
87
+ json_schema: {player: 'string'}
88
+
89
+ assert_equal 'high', assistant.thinking
90
+ end
91
+
92
+ it 'System' do
93
+ assistant = Rasti::AI.create_assistant provider: :open_ai, system: 'Act as sports journalist'
94
+
95
+ assert_equal 'Act as sports journalist', assistant.state.context
96
+ end
97
+
98
+ it 'State' do
99
+ state = Rasti::AI::AssistantState.new context: 'Act as sports journalist'
100
+
101
+ assistant = Rasti::AI.create_assistant provider: :open_ai, state: state
102
+
103
+ assert_same state, assistant.state
104
+ end
105
+
106
+ it 'State and system are mutually exclusive' do
107
+ error = assert_raises ArgumentError do
108
+ Rasti::AI.create_assistant provider: :open_ai, state: Rasti::AI::AssistantState.new, system: 'Act as sports journalist'
109
+ end
110
+
111
+ assert_equal 'Use state or system, not both', error.message
112
+ end
113
+
114
+ it 'Custom client' do
115
+ client = Rasti::AI::OpenAI::Client.new
116
+
117
+ assistant = Rasti::AI.create_assistant provider: :open_ai, client: client
118
+
119
+ stub_open_ai
120
+
121
+ assert_equal answer, assistant.call(question)
122
+ end
123
+
124
+ it 'Client options' do
125
+ stub_open_ai
126
+
127
+ tracked_usage = []
128
+
129
+ assistant = Rasti::AI.create_assistant provider: :open_ai,
130
+ api_key: 'custom_api_key',
131
+ usage_tracker: ->(usage) { tracked_usage << usage },
132
+ http_read_timeout: 120,
133
+ http_max_retries: 5
134
+
135
+ assistant.call question
136
+
137
+ assert_requested :post, 'https://api.openai.com/v1/chat/completions',
138
+ headers: {'Authorization' => 'Bearer custom_api_key'}
139
+
140
+ assert_equal 1, tracked_usage.count
141
+ assert_equal 'open_ai', tracked_usage.first.provider
142
+ end
143
+
144
+ end
145
+
146
+ describe 'Provider' do
147
+
148
+ it 'Build from name' do
149
+ provider = Rasti::AI.provider :open_ai
150
+
151
+ assert_instance_of Rasti::AI::OpenAI::Provider, provider
152
+ assert_equal :open_ai, provider.name
153
+ assert_equal Rasti::AI.openai_default_model, provider.model
154
+ end
155
+
156
+ it 'Build from alias' do
157
+ assert_instance_of Rasti::AI::OpenRouter::Provider, Rasti::AI.provider('openrouter')
158
+ assert_instance_of Rasti::AI::HuaweiMaaS::Provider, Rasti::AI.provider(:huawei)
159
+ assert_instance_of Rasti::AI::OpenAI::Provider, Rasti::AI.provider(:OpenAI)
160
+ end
161
+
162
+ it 'Build from provider and model as single string' do
163
+ provider = Rasti::AI.provider model: 'anthropic:claude-sonnet-4-5'
164
+
165
+ assert_instance_of Rasti::AI::Anthropic::Provider, provider
166
+ assert_equal 'claude-sonnet-4-5', provider.model
167
+ end
168
+
169
+ it 'Names' do
170
+ assert_equal [:open_ai, :gemini, :anthropic, :open_router, :huawei_maas], Rasti::AI::Provider.names
171
+ end
172
+
173
+ it 'Unknown provider' do
174
+ error = assert_raises Rasti::AI::Errors::UnknownProvider do
175
+ Rasti::AI.provider :invalid
176
+ end
177
+
178
+ assert_equal 'Unknown provider :invalid. Valid providers: open_ai, gemini, anthropic, open_router, huawei_maas', error.message
179
+ end
180
+
181
+ it 'Builds client and assistant of its own provider' do
182
+ provider = Rasti::AI.provider :gemini
183
+
184
+ assert_instance_of Rasti::AI::Gemini::Client, provider.build_client
185
+ assert_instance_of Rasti::AI::Gemini::Assistant, provider.create_assistant
186
+ end
187
+
188
+ it 'Reusable instance' do
189
+ provider = Rasti::AI.provider :open_ai, api_key: 'tenant_api_key'
190
+
191
+ stub_open_ai
192
+
193
+ provider.generate_text prompt: question
194
+
195
+ assert_requested :post, 'https://api.openai.com/v1/chat/completions',
196
+ headers: {'Authorization' => 'Bearer tenant_api_key'}
197
+
198
+ assert_instance_of Rasti::AI::OpenAI::Assistant, provider.create_assistant
199
+ end
200
+
201
+ it 'Invalid thinking level' do
202
+ error = assert_raises ArgumentError do
203
+ Rasti::AI.provider(:open_ai).generate_text prompt: question, thinking: 'invalid'
204
+ end
205
+
206
+ assert_equal "Invalid thinking level 'invalid'. Valid: low, medium, high", error.message
207
+ end
208
+
209
+ end
210
+
211
+ describe 'Generate text' do
212
+
213
+ it 'Provider' do
214
+ stub_open_ai
215
+
216
+ result = Rasti::AI.generate_text prompt: question, provider: :open_ai
217
+
218
+ assert_instance_of Rasti::AI::Result, result
219
+ assert_equal answer, result.content
220
+ assert_equal 'open_ai', result.usage.provider
221
+ assert_equal 27, result.usage.input_tokens
222
+ assert_equal 229, result.usage.output_tokens
223
+ assert_equal 'stop', result.raw['choices'][0]['finish_reason']
224
+ end
225
+
226
+ it 'Provider and model' do
227
+ stub_open_ai model: 'gpt-4o'
228
+
229
+ result = Rasti::AI.generate_text prompt: question, provider: :open_ai, model: 'gpt-4o'
230
+
231
+ assert_equal answer, result.content
232
+ end
233
+
234
+ it 'Provider and model as single string' do
235
+ stub_open_ai model: 'gpt-4o'
236
+
237
+ result = Rasti::AI.generate_text prompt: question, model: 'open_ai:gpt-4o'
238
+
239
+ assert_equal answer, result.content
240
+ end
241
+
242
+ it 'Default provider from config' do
243
+ with_default_provider :open_ai do
244
+ stub_open_ai
245
+
246
+ assert_equal answer, Rasti::AI.generate_text(prompt: question).content
247
+ end
248
+ end
249
+
250
+ it 'Messages with generic roles' do
251
+ messages = [
252
+ {role: Rasti::AI::Roles::USER, content: 'who is the best player'},
253
+ {role: Rasti::AI::Roles::ASSISTANT, content: 'Lionel Messi'},
254
+ {role: Rasti::AI::Roles::USER, content: question}
255
+ ]
256
+
257
+ stub_request(:post, 'https://api.openai.com/v1/chat/completions')
258
+ .with(body: read_resource('open_ai/conversation_request.json', model: Rasti::AI.openai_default_model, prompt: question))
259
+ .to_return(body: read_resource('open_ai/basic_response.json', content: answer))
260
+
261
+ result = Rasti::AI.generate_text messages: messages, provider: :open_ai
262
+
263
+ assert_equal answer, result.content
264
+ end
265
+
266
+ it 'System message inside messages' do
267
+ messages = [
268
+ {role: Rasti::AI::Roles::SYSTEM, content: 'Act as sports journalist'},
269
+ {role: Rasti::AI::Roles::USER, content: question}
270
+ ]
271
+
272
+ stub_request(:post, 'https://api.openai.com/v1/chat/completions')
273
+ .with(body: read_resource('open_ai/system_request.json', model: Rasti::AI.openai_default_model, system: 'Act as sports journalist', prompt: question))
274
+ .to_return(body: read_resource('open_ai/basic_response.json', content: answer))
275
+
276
+ assert_equal answer, Rasti::AI.generate_text(messages: messages, provider: :open_ai).content
277
+ end
278
+
279
+ it 'Prompt and messages are mutually exclusive' do
280
+ error = assert_raises ArgumentError do
281
+ Rasti::AI.generate_text prompt: question, messages: [], provider: :open_ai
282
+ end
283
+
284
+ assert_equal 'Use prompt or messages, not both', error.message
285
+ end
286
+
287
+ it 'Undefined prompt and messages' do
288
+ error = assert_raises ArgumentError do
289
+ Rasti::AI.generate_text provider: :open_ai
290
+ end
291
+
292
+ assert_equal 'Undefined prompt or messages', error.message
293
+ end
294
+
295
+ it 'System' do
296
+ stub_request(:post, 'https://api.openai.com/v1/chat/completions')
297
+ .with(body: read_resource('open_ai/system_request.json', model: Rasti::AI.openai_default_model, system: 'Act as sports journalist', prompt: question))
298
+ .to_return(body: read_resource('open_ai/basic_response.json', content: answer))
299
+
300
+ result = Rasti::AI.generate_text prompt: question,
301
+ provider: :open_ai,
302
+ system: 'Act as sports journalist'
303
+
304
+ assert_equal answer, result.content
305
+ end
306
+
307
+ it 'Usage tracker' do
308
+ stub_open_ai
309
+
310
+ tracked_usage = []
311
+
312
+ result = Rasti::AI.generate_text prompt: question,
313
+ provider: :open_ai,
314
+ usage_tracker: ->(usage) { tracked_usage << usage }
315
+
316
+ assert_equal 1, tracked_usage.count
317
+ assert_equal tracked_usage.first.to_h, result.usage.to_h
318
+ end
319
+
320
+ end
321
+
322
+ end
@@ -0,0 +1 @@
1
+ {"model":"<%= model %>","messages":[{"role":"user","content":"who is the best player"},{"role":"assistant","content":"Lionel Messi"},{"role":"user","content":"<%= prompt %>"}],"tools":[],"tool_choice":"none"}
@@ -0,0 +1 @@
1
+ {"model":"<%= model %>","messages":[{"role":"system","content":"<%= system %>"},{"role":"user","content":"<%= prompt %>"}],"tools":[],"tool_choice":"none"}