rasti-ai 3.2.0 → 3.2.1
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.md +11 -4
- data/README.md +9 -1
- data/lib/rasti/ai/anthropic/provider.rb +1 -0
- data/lib/rasti/ai/assistant.rb +1 -1
- data/lib/rasti/ai/gemini/provider.rb +1 -0
- data/lib/rasti/ai/open_ai/assistant.rb +1 -1
- data/lib/rasti/ai/open_ai/provider.rb +1 -0
- data/lib/rasti/ai/version.rb +1 -1
- data/spec/anthropic/assistant_spec.rb +14 -0
- data/spec/anthropic/provider_spec.rb +15 -0
- data/spec/gemini/assistant_spec.rb +15 -0
- data/spec/gemini/provider_spec.rb +14 -0
- data/spec/open_ai/assistant_spec.rb +15 -0
- data/spec/open_ai/provider_spec.rb +10 -0
- data/spec/rasti_ai_spec.rb +1 -1
- data/tasks/assistant.rake +8 -8
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58f15bbf9597b75e9dc32542801d07da3661a2e0477e13460ea9116e610501c5
|
|
4
|
+
data.tar.gz: '08f39b213898e52ee7fb378472e5e2ba0bc68a7823b000e086afeb9c95b14457'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6237690a28cea19cfcd156f67dbf45674619dacfb0f8dd5a540da30aadf2020ec2a838a3a04439c959446faef65718ee8f83fff58524ed6674fbe4547cc55d81
|
|
7
|
+
data.tar.gz: e5e6f685ab358ddc17b006de0885c343ee370b8769b7cae8fb2a6031878643c19565e8b8994d7baaece9a050ada387ef59e4e3b1df8b52881d15d73fafb4e7cf
|
data/AGENTS.md
CHANGED
|
@@ -272,10 +272,11 @@ Thinking is inherited too: `reasoning_effort` is passed through as-is (same as O
|
|
|
272
272
|
|
|
273
273
|
## Thinking levels
|
|
274
274
|
|
|
275
|
-
The base `Assistant` accepts `thinking: 'low' | 'medium' | 'high'` (validated on construction
|
|
275
|
+
The base `Assistant` accepts `thinking: 'none' | 'low' | 'medium' | 'high'` (validated on construction). `nil` means the gem sends nothing and the model uses its own default — which for reasoning models (e.g. `gpt-6-luna`, `gemini-2.5-flash`, Huawei's DeepSeek/GLM) means reasoning *on*. `'none'` explicitly asks the provider to disable reasoning. Each provider translates it in a private `thinking_config` method and passes the result to the client. The client includes it in the request body only if present.
|
|
276
276
|
|
|
277
277
|
| Level | OpenAI `reasoning_effort` | Anthropic `budget_tokens` | Gemini `thinking_budget` |
|
|
278
278
|
|---|---|---|---|
|
|
279
|
+
| `'none'` | `'none'` | _(field omitted)_ | `0` |
|
|
279
280
|
| `'low'` | `'low'` | `1_024` | `1_024` |
|
|
280
281
|
| `'medium'` | `'medium'` | `8_000` | `8_192` |
|
|
281
282
|
| `'high'` | `'high'` | `16_000` | `24_576` |
|
|
@@ -284,7 +285,11 @@ The `THINKING_LEVELS` table lives in each **`Provider`** class (it's provider me
|
|
|
284
285
|
|
|
285
286
|
For Gemini, `thinking_config` goes inside `generation_config` — the client doesn't need a new param. For OpenAI and Anthropic, it's a separate top-level param in the client method (`reasoning_effort:` and `thinking:` respectively).
|
|
286
287
|
|
|
287
|
-
OpenRouter and Huawei MaaS inherit OpenAI's behavior: `thinking` is passed through as `reasoning_effort` with the same string values (`'low'`/`'medium'`/`'high'`). No `thinking_config` override is needed.
|
|
288
|
+
OpenRouter and Huawei MaaS inherit OpenAI's behavior: `thinking` is passed through as `reasoning_effort` with the same string values (`'low'`/`'medium'`/`'high'`). No `thinking_config` override is needed. Huawei MaaS rejects `reasoning_effort: 'none'` with a 400 (`deepseek-v4-flash` accepts `low|medium|high|xhigh|max`, `glm-5.3` only `low|high|max`), so `thinking: 'none'` fails there by design. Disabling reasoning on Huawei needs `thinking: {type: 'disabled'}` instead (works on DeepSeek and GLM ≤ 5.2, rejected by `glm-5.3`); the gem does not send it.
|
|
289
|
+
|
|
290
|
+
`'none'` matters for `gpt-6-luna`: it reasons by default and rejects function tools on `/v1/chat/completions` unless `reasoning_effort: 'none'` is sent. Combining reasoning with tools there requires `/v1/responses`, which the gem does not support.
|
|
291
|
+
|
|
292
|
+
Gemini's `thinking_budget: 0` is rejected by models that cannot turn reasoning off (e.g. `gemini-2.5-pro`).
|
|
288
293
|
|
|
289
294
|
The loop does not change. Anthropic thinking blocks (`type: 'thinking'`) in responses are ignored by `parse_content` (looks for `type == 'text'`) and preserved automatically by `build_assistant_tool_calls_message` (passes full `response['content']` array).
|
|
290
295
|
|
|
@@ -315,10 +320,12 @@ Add an entry to the `PROVIDERS` table in `tasks/assistant.rake` so the interacti
|
|
|
315
320
|
```ruby
|
|
316
321
|
PROVIDERS = {
|
|
317
322
|
# existing providers ...
|
|
318
|
-
'<provider>' => {key: '<PROVIDER>_API_KEY', klass: -> { Rasti::AI::<Provider>::Assistant }}
|
|
323
|
+
'<provider>' => {key: '<PROVIDER>_API_KEY', klass: -> { Rasti::AI::<Provider>::Assistant }, thinking: 'none'}
|
|
319
324
|
}.freeze
|
|
320
325
|
```
|
|
321
326
|
|
|
327
|
+
`thinking:` is optional and passed as-is to the assistant. All providers use `'none'` except `huawei_maas`, which rejects it (see [Thinking levels](#thinking-levels)).
|
|
328
|
+
|
|
322
329
|
The task name, description, env-key check, logger path and banner are all derived automatically from this entry.
|
|
323
330
|
|
|
324
331
|
Add to `spec/minitest_helper.rb`:
|
|
@@ -392,7 +399,7 @@ All string constants use `.freeze`. Integer and array/hash literals that are alr
|
|
|
392
399
|
USER = 'user'.freeze
|
|
393
400
|
ASSISTANT = 'assistant'.freeze
|
|
394
401
|
|
|
395
|
-
VALID_THINKING_LEVELS = %w[low medium high].freeze
|
|
402
|
+
VALID_THINKING_LEVELS = %w[none low medium high].freeze
|
|
396
403
|
|
|
397
404
|
THINKING_LEVELS = {
|
|
398
405
|
'low' => {thinking_budget: 1_024}.freeze,
|
data/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Both methods accept the same options:
|
|
|
146
146
|
| `http_connect_timeout:`, `http_read_timeout:`, `http_max_retries:` | Per-call HTTP settings | ✓ | ✓ |
|
|
147
147
|
| `client:` | A pre-built client (skips building one from the options above) | ✓ | ✓ |
|
|
148
148
|
| `json_schema:` | Schema for structured responses | ✓ | ✓ |
|
|
149
|
-
| `thinking:` | `'low'`, `'medium'` or `'high'` | ✓ | ✓ |
|
|
149
|
+
| `thinking:` | `'none'`, `'low'`, `'medium'` or `'high'` | ✓ | ✓ |
|
|
150
150
|
| `prompt:` | A single user message | ✓ | |
|
|
151
151
|
| `messages:` | Conversation with generic roles — mutually exclusive with `prompt:` | ✓ | |
|
|
152
152
|
| `system:` | System prompt. On `create_assistant` it's a shortcut for `state: AssistantState.new(context: ...)` | ✓ | ✓ |
|
|
@@ -330,6 +330,14 @@ assistant.call 'Solve this step by step: ...'
|
|
|
330
330
|
|
|
331
331
|
The level controls how much computation the model can spend reasoning before responding. Higher levels may improve answer quality at the cost of more tokens and latency. Not all models support thinking — check your provider's documentation.
|
|
332
332
|
|
|
333
|
+
Omitting `thinking:` leaves the decision to the model, and many current models reason by default. Pass `thinking: 'none'` to disable reasoning explicitly. Some models require it — `gpt-6-luna`, for example, rejects function tools unless reasoning is disabled:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
assistant = Rasti::AI.create_assistant provider: :open_ai, model: 'gpt-6-luna', tools: [GetCurrentWeather.new], thinking: 'none'
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Huawei MaaS does not accept `'none'` and returns an error.
|
|
340
|
+
|
|
333
341
|
### Usage tracking
|
|
334
342
|
|
|
335
343
|
Track token consumption across API calls (including tool calls):
|
|
@@ -6,6 +6,7 @@ module Rasti
|
|
|
6
6
|
STRUCTURED_OUTPUT_TOOL = 'structured_output'.freeze
|
|
7
7
|
|
|
8
8
|
THINKING_LEVELS = {
|
|
9
|
+
'none' => nil,
|
|
9
10
|
'low' => {type: 'enabled', budget_tokens: 1_024}.freeze,
|
|
10
11
|
'medium' => {type: 'enabled', budget_tokens: 8_000}.freeze,
|
|
11
12
|
'high' => {type: 'enabled', budget_tokens: 16_000}.freeze
|
data/lib/rasti/ai/assistant.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Rasti
|
|
|
6
6
|
|
|
7
7
|
attr_reader :state, :model, :thinking
|
|
8
8
|
|
|
9
|
-
VALID_THINKING_LEVELS = %w[low medium high].freeze
|
|
9
|
+
VALID_THINKING_LEVELS = %w[none low medium high].freeze
|
|
10
10
|
|
|
11
11
|
def initialize(client:nil, provider:nil, json_schema:nil, state:nil, model:nil, thinking:nil, tools:[], mcp_servers:{}, logger:nil)
|
|
12
12
|
raise ArgumentError, "Invalid thinking level '#{thinking}'. Valid: #{VALID_THINKING_LEVELS.join(', ')}" if thinking && !VALID_THINKING_LEVELS.include?(thinking)
|
data/lib/rasti/ai/version.rb
CHANGED
|
@@ -135,6 +135,20 @@ describe Rasti::AI::Anthropic::Assistant do
|
|
|
135
135
|
assert_equal answer, response
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
it 'Thinking none' do
|
|
139
|
+
body = read_json_resource('anthropic/basic_request.json', model: Rasti::AI.anthropic_default_model, prompt: question)
|
|
140
|
+
|
|
141
|
+
stub_request(:post, api_url)
|
|
142
|
+
.with(body: JSON.dump(body))
|
|
143
|
+
.to_return(body: read_resource('anthropic/basic_response.json', content: answer))
|
|
144
|
+
|
|
145
|
+
assistant = Rasti::AI::Anthropic::Assistant.new thinking: 'none'
|
|
146
|
+
|
|
147
|
+
response = assistant.call question
|
|
148
|
+
|
|
149
|
+
assert_equal answer, response
|
|
150
|
+
end
|
|
151
|
+
|
|
138
152
|
it 'JSON Schema' do
|
|
139
153
|
json_schema = {'answer' => 'Response answer'}
|
|
140
154
|
|
|
@@ -58,6 +58,21 @@ describe Rasti::AI::Anthropic::Provider do
|
|
|
58
58
|
assert_equal answer, result.content
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
it 'Generate text with thinking none' do
|
|
62
|
+
stub_messages model: Rasti::AI.anthropic_default_model,
|
|
63
|
+
max_tokens: 4096,
|
|
64
|
+
messages: [
|
|
65
|
+
{
|
|
66
|
+
role: 'user',
|
|
67
|
+
content: question
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
result = provider.generate_text prompt: question, thinking: 'none'
|
|
72
|
+
|
|
73
|
+
assert_equal answer, result.content
|
|
74
|
+
end
|
|
75
|
+
|
|
61
76
|
it 'Generate text with json schema' do
|
|
62
77
|
json_schema = {player: 'string'}
|
|
63
78
|
|
|
@@ -125,6 +125,21 @@ describe Rasti::AI::Gemini::Assistant do
|
|
|
125
125
|
assert_equal answer, response
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
it 'Thinking none' do
|
|
129
|
+
body = read_json_resource('gemini/basic_request.json', prompt: question)
|
|
130
|
+
body['generation_config'] = {'thinking_config' => {'thinking_budget' => 0}}
|
|
131
|
+
|
|
132
|
+
stub_request(:post, api_url)
|
|
133
|
+
.with(body: JSON.dump(body))
|
|
134
|
+
.to_return(body: read_resource('gemini/basic_response.json', content: answer))
|
|
135
|
+
|
|
136
|
+
assistant = Rasti::AI::Gemini::Assistant.new thinking: 'none'
|
|
137
|
+
|
|
138
|
+
response = assistant.call question
|
|
139
|
+
|
|
140
|
+
assert_equal answer, response
|
|
141
|
+
end
|
|
142
|
+
|
|
128
143
|
it 'JSON Schema' do
|
|
129
144
|
json_schema = {answer: 'Response answer'}
|
|
130
145
|
json_answer = "{\\\"answer\\\": \\\"#{answer}\\\"}"
|
|
@@ -87,4 +87,18 @@ describe Rasti::AI::Gemini::Provider do
|
|
|
87
87
|
assert_equal answer, provider.generate_text(prompt: question, json_schema: json_schema, thinking: 'medium').content
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
it 'Generate text with thinking none' do
|
|
91
|
+
stub_generate_content contents: [
|
|
92
|
+
{
|
|
93
|
+
role: 'user',
|
|
94
|
+
parts: [{text: question}]
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
generation_config: {
|
|
98
|
+
thinking_config: {thinking_budget: 0}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
assert_equal answer, provider.generate_text(prompt: question, thinking: 'none').content
|
|
102
|
+
end
|
|
103
|
+
|
|
90
104
|
end
|
|
@@ -127,6 +127,21 @@ describe Rasti::AI::OpenAI::Assistant do
|
|
|
127
127
|
assert_equal answer, response
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
it 'Thinking none' do
|
|
131
|
+
body = read_json_resource('open_ai/basic_request.json', model: Rasti::AI.openai_default_model, prompt: question)
|
|
132
|
+
body['reasoning_effort'] = 'none'
|
|
133
|
+
|
|
134
|
+
stub_request(:post, api_url)
|
|
135
|
+
.with(body: JSON.dump(body))
|
|
136
|
+
.to_return(body: read_resource('open_ai/basic_response.json', content: answer))
|
|
137
|
+
|
|
138
|
+
assistant = Rasti::AI::OpenAI::Assistant.new thinking: 'none'
|
|
139
|
+
|
|
140
|
+
response = assistant.call question
|
|
141
|
+
|
|
142
|
+
assert_equal answer, response
|
|
143
|
+
end
|
|
144
|
+
|
|
130
145
|
it 'JSON Schema' do
|
|
131
146
|
json_schema = {answer: 'Response answer'}
|
|
132
147
|
json_answer = "{\\\"answer\\\": \\\"#{answer}\\\"}"
|
|
@@ -62,6 +62,16 @@ describe Rasti::AI::OpenAI::Provider do
|
|
|
62
62
|
assert_equal answer, result.content
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
it 'Generate text with thinking none' do
|
|
66
|
+
body = basic_body(Rasti::AI.openai_default_model).merge(reasoning_effort: 'none')
|
|
67
|
+
|
|
68
|
+
stub_chat_completions 'https://api.openai.com/v1/chat/completions', body
|
|
69
|
+
|
|
70
|
+
result = Rasti::AI.provider(:open_ai).generate_text prompt: question, thinking: 'none'
|
|
71
|
+
|
|
72
|
+
assert_equal answer, result.content
|
|
73
|
+
end
|
|
74
|
+
|
|
65
75
|
describe 'OpenAI compatible providers' do
|
|
66
76
|
|
|
67
77
|
it 'OpenRouter' do
|
data/spec/rasti_ai_spec.rb
CHANGED
|
@@ -203,7 +203,7 @@ describe Rasti::AI do
|
|
|
203
203
|
Rasti::AI.provider(:open_ai).generate_text prompt: question, thinking: 'invalid'
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
assert_equal "Invalid thinking level 'invalid'. Valid: low, medium, high", error.message
|
|
206
|
+
assert_equal "Invalid thinking level 'invalid'. Valid: none, low, medium, high", error.message
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
end
|
data/tasks/assistant.rake
CHANGED
|
@@ -19,11 +19,11 @@ require 'logger'
|
|
|
19
19
|
WEATHER_MCP_URL = 'https://gateway.pipeworx.io/weather/mcp'.freeze
|
|
20
20
|
|
|
21
21
|
PROVIDERS = {
|
|
22
|
-
'openai'
|
|
23
|
-
'gemini'
|
|
24
|
-
'anthropic'
|
|
25
|
-
'openrouter' => {key: 'OPENROUTER_API_KEY',
|
|
26
|
-
'huawei_maas'=> {key: 'HUAWEI_MAAS_API_KEY',
|
|
22
|
+
'openai' => {key: 'OPENAI_API_KEY', klass: -> { Rasti::AI::OpenAI::Assistant }, thinking: 'none'},
|
|
23
|
+
'gemini' => {key: 'GEMINI_API_KEY', klass: -> { Rasti::AI::Gemini::Assistant }, thinking: 'none'},
|
|
24
|
+
'anthropic' => {key: 'ANTHROPIC_API_KEY', klass: -> { Rasti::AI::Anthropic::Assistant }, thinking: 'none'},
|
|
25
|
+
'openrouter' => {key: 'OPENROUTER_API_KEY', klass: -> { Rasti::AI::OpenRouter::Assistant }, thinking: 'none'},
|
|
26
|
+
'huawei_maas' => {key: 'HUAWEI_MAAS_API_KEY', klass: -> { Rasti::AI::HuaweiMaaS::Assistant }}
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
29
|
def build_weather_mcp
|
|
@@ -70,7 +70,7 @@ def chat_loop(assistant)
|
|
|
70
70
|
puts "\nGoodbye!"
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
def start_assistant(provider, env_key, assistant_klass)
|
|
73
|
+
def start_assistant(provider, env_key, assistant_klass, thinking)
|
|
74
74
|
require 'rasti-ai'
|
|
75
75
|
|
|
76
76
|
abort "[Error] #{env_key} is not set." unless ENV[env_key]
|
|
@@ -81,7 +81,7 @@ def start_assistant(provider, env_key, assistant_klass)
|
|
|
81
81
|
mcp = build_weather_mcp
|
|
82
82
|
print_banner provider.capitalize, mcp.list_tools.map { |t| t['name'] }
|
|
83
83
|
|
|
84
|
-
chat_loop assistant_klass.call.new(mcp_servers: {weather: mcp})
|
|
84
|
+
chat_loop assistant_klass.call.new(mcp_servers: {weather: mcp}, thinking: thinking)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
namespace :assistant do
|
|
@@ -89,7 +89,7 @@ namespace :assistant do
|
|
|
89
89
|
PROVIDERS.each do |provider, config|
|
|
90
90
|
desc "Chat with #{provider.capitalize} assistant using Pipeworx weather MCP (requires #{config[:key]})"
|
|
91
91
|
task provider.to_sym do
|
|
92
|
-
start_assistant(provider, config[:key], config[:klass])
|
|
92
|
+
start_assistant(provider, config[:key], config[:klass], config[:thinking])
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rasti-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.2.
|
|
4
|
+
version: 3.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Naiman
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: multi_require
|