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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa07486ef7216a0a1408d1711ea21ca521283673c06ca2845ad63197dd1dbe8d
4
- data.tar.gz: dd42552dfd7e3752dac0264b1c89159f04f230c6d067fa674dd4347aae01b0b1
3
+ metadata.gz: 58f15bbf9597b75e9dc32542801d07da3661a2e0477e13460ea9116e610501c5
4
+ data.tar.gz: '08f39b213898e52ee7fb378472e5e2ba0bc68a7823b000e086afeb9c95b14457'
5
5
  SHA512:
6
- metadata.gz: 1cd0846a2f725823ba691f7d761c5abee299291d9307a7a870cc086f17a02ead88913acaf70cf257221b82a5b8684655595cdc4a8952d4825fa19f23ab7004e5
7
- data.tar.gz: ea5880cc6e73795cac795189e6a6d192e27fdd11a252768f1b33933b13407d9e1f929e1aaf4020112164dfa880acbe4601708f202795737b434df0f5a63202d6
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; nil = disabled). 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.
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. Note that Huawei's models typically accept only `'high'` and `'max'` — `'high'` is the safe choice, and `'max'` is not expressible through the gem's universal levels.
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
@@ -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)
@@ -4,6 +4,7 @@ module Rasti
4
4
  class Provider < Rasti::AI::Provider
5
5
 
6
6
  THINKING_LEVELS = {
7
+ 'none' => {thinking_budget: 0}.freeze,
7
8
  'low' => {thinking_budget: 1_024}.freeze,
8
9
  'medium' => {thinking_budget: 8_192}.freeze,
9
10
  'high' => {thinking_budget: 24_576}.freeze
@@ -37,7 +37,7 @@ module Rasti
37
37
  end
38
38
 
39
39
  def thinking_config
40
- thinking
40
+ Provider::THINKING_LEVELS[thinking]
41
41
  end
42
42
 
43
43
  def parse_tool_calls(response)
@@ -4,6 +4,7 @@ module Rasti
4
4
  class Provider < Rasti::AI::Provider
5
5
 
6
6
  THINKING_LEVELS = {
7
+ 'none' => 'none'.freeze,
7
8
  'low' => 'low'.freeze,
8
9
  'medium' => 'medium'.freeze,
9
10
  'high' => 'high'.freeze
@@ -1,5 +1,5 @@
1
1
  module Rasti
2
2
  module AI
3
- VERSION = '3.2.0'
3
+ VERSION = '3.2.1'
4
4
  end
5
5
  end
@@ -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
@@ -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' => {key: 'OPENAI_API_KEY', klass: -> { Rasti::AI::OpenAI::Assistant }},
23
- 'gemini' => {key: 'GEMINI_API_KEY', klass: -> { Rasti::AI::Gemini::Assistant }},
24
- 'anthropic' => {key: 'ANTHROPIC_API_KEY', klass: -> { Rasti::AI::Anthropic::Assistant }},
25
- 'openrouter' => {key: 'OPENROUTER_API_KEY', klass: -> { Rasti::AI::OpenRouter::Assistant }},
26
- 'huawei_maas'=> {key: 'HUAWEI_MAAS_API_KEY', klass: -> { Rasti::AI::HuaweiMaaS::Assistant }}
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.0
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-07-28 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_require