llm_conductor 1.8.2 → 1.8.3

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: 8a13bccd45a0d886de6bc88a531724a2e929912d21564f5c50296c000c28476a
4
- data.tar.gz: 1838b42538d448dc2373b1e7e9180b92b1f97fe87dfd8b50b18a164ac992ffae
3
+ metadata.gz: 669ae889bc5b65b4455617425a5c3628a66ed21741eca0930c78652f4b8bfbad
4
+ data.tar.gz: 396219406bf2db7236c223cc36200dc1be2739d6a87ab6cc50ccdc75be7482ff
5
5
  SHA512:
6
- metadata.gz: aeac7323ebde9d6327f9e536876d42f951d5e042f37888f6d0cb0dc64aaa50fd820c94cf4788a6ebde8b9e27423d67872f99a87c6ff6400f910cb07fbf37cfbc
7
- data.tar.gz: 20dcb4c95317a32eaa033bbf0dadfdfa3aef5934818fbc491fd13b9e467edbe5a2b332d3234ee9f7b7181900a0a35294d356417c759c5cb736a476592ac670cf
6
+ metadata.gz: 9e71aaa12abdf4c1bf1d750f4e28b41508d5e0b999d8373858e5ad0a16d8ea7f24c059553e59b5b38377b262bd5e6963f0a424b745d9f58e064d4135d4c05779
7
+ data.tar.gz: 2ab40251e03a5b8eb01bb6d7cfdbe0bac4b1137768b4c42b991da8649ec146698d17e012d076aa102643e37ddaa94951552e8e0c8f80b68ac14fd7875f7e8cdd
data/README.md CHANGED
@@ -108,7 +108,7 @@ end
108
108
  |----------|-------------|--------|---------------|
109
109
  | OpenAI (GPT) | ✅ `gpt-*` | ✅ | 🔜 |
110
110
  | Anthropic (Claude) | ✅ `claude-*` | ✅ | 🔜 |
111
- | Google (Gemini) | ✅ `gemini-*` | ✅ | 🔜 |
111
+ | Google (Gemini) | ✅ `gemini-*` | ✅ | |
112
112
  | Groq | ✅ `llama/mixtral` | ❌ | 🔜 |
113
113
  | Ollama | ✅ (default) | ❌ | ✅ |
114
114
  | OpenRouter | 🔧 Manual | ✅ | 🔜 |
@@ -150,6 +150,20 @@ response = LlmConductor.generate(
150
150
  )
151
151
  ```
152
152
 
153
+ ### Fast Gemini Calls (Minimal Thinking)
154
+
155
+ ```ruby
156
+ response = LlmConductor.generate(
157
+ model: 'gemini-3.5-flash-lite',
158
+ prompt: 'Extract the company name from: ...',
159
+ vendor: :gemini,
160
+ params: { thinking_level: 'minimal' }
161
+ )
162
+ ```
163
+
164
+ Sent to Gemini as `generationConfig.thinkingConfig.thinkingLevel`. See
165
+ [Custom Parameters](docs/custom-parameters.md#gemini-thinking-configuration).
166
+
153
167
  ### Vision Analysis
154
168
 
155
169
  ```ruby
@@ -328,6 +342,7 @@ Check the [examples/](examples/) directory for comprehensive examples:
328
342
 
329
343
  - `simple_usage.rb` - Basic text generation
330
344
  - `ollama_params_usage.rb` - Custom parameters with Ollama
345
+ - `gemini_thinking_usage.rb` - Gemini thinking configuration (`thinkingConfig`)
331
346
  - `gpt_vision_usage.rb` - Vision with OpenAI
332
347
  - `claude_vision_usage.rb` - Vision with Anthropic
333
348
  - `gemini_vision_usage.rb` - Vision with Gemini
data/docs/README.md CHANGED
@@ -11,8 +11,8 @@ Learn how to fine-tune LLM generation with parameters like `temperature`, `top_p
11
11
  - Provider-specific parameters
12
12
  - Best practices and use cases
13
13
 
14
- **Currently supported**: Ollama
15
- **Coming soon**: OpenAI, Anthropic, Gemini, Groq, OpenRouter, Z.ai
14
+ **Currently supported**: Ollama, Gemini (including Gemini 3.x `thinkingConfig`)
15
+ **Coming soon**: OpenAI, Anthropic, Groq, OpenRouter, Z.ai
16
16
 
17
17
  ### [Vision Support](vision-support.md)
18
18
  Complete guide to using vision/multimodal capabilities. Includes:
@@ -149,6 +149,13 @@ Below are common parameters supported by Google Gemini via `generationConfig`. F
149
149
  | `max_output_tokens` | `maxOutputTokens` | Integer | Alias for max_tokens |
150
150
  | `candidate_count` | `candidateCount` | Integer | Number of candidates to return |
151
151
  | `stop_sequences` | `stopSequences` | Array | Stop sequences that end generation |
152
+ | `thinking_config` | `thinkingConfig` | Hash | Gemini 3.x thinking controls (see below) |
153
+ | `thinking_level` | `thinkingConfig.thinkingLevel` | String | Thinking depth: `'minimal'`, `'low'`, `'high'` |
154
+ | `thinking_budget` | `thinkingConfig.thinkingBudget` | Integer | Token budget for thinking (`0` disables it) |
155
+ | `include_thoughts` | `thinkingConfig.includeThoughts` | Boolean | Return the thought summary alongside the answer |
156
+
157
+ Unsupported keys are skipped and logged as a warning through
158
+ `LlmConductor.configuration.logger` — they are never dropped silently.
152
159
 
153
160
  ### Gemini Usage Examples
154
161
 
@@ -170,6 +177,36 @@ response = LlmConductor.generate(
170
177
  )
171
178
  ```
172
179
 
180
+ ### Gemini Thinking Configuration
181
+
182
+ Gemini 3.x models think before answering. Cutting that budget down is the single
183
+ biggest latency lever available on the fast models, so the thinking controls are
184
+ exposed through the same `params` hash:
185
+
186
+ ```ruby
187
+ # Fastest path — minimal thinking (flat convenience key)
188
+ response = LlmConductor.generate(
189
+ model: 'gemini-3.5-flash-lite',
190
+ prompt: 'Extract the company name from: ...',
191
+ vendor: :gemini,
192
+ params: { thinking_level: 'minimal', temperature: 0.0 }
193
+ )
194
+
195
+ # Nested form — mirrors the API shape, accepts snake_case or camelCase
196
+ response = LlmConductor.generate(
197
+ model: 'gemini-3.5-flash-lite',
198
+ prompt: 'Extract the company name from: ...',
199
+ vendor: :gemini,
200
+ params: { thinking_config: { thinking_level: 'minimal', thinking_budget: 0 } }
201
+ )
202
+ ```
203
+
204
+ Both forms are merged into the request as:
205
+
206
+ ```json
207
+ { "generationConfig": { "thinkingConfig": { "thinkingLevel": "minimal" } } }
208
+ ```
209
+
173
210
  ## Ollama Parameters Reference
174
211
 
175
212
  Below are common parameters supported by Ollama. For a complete list, see the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
@@ -348,7 +385,8 @@ Always refer to your provider's documentation for supported parameters.
348
385
 
349
386
  Currently, custom parameters are fully supported for:
350
387
  - ✅ **Ollama**
351
- - ✅ **Google (Gemini)** — maps snake_case Ruby keys to camelCase `generationConfig`
388
+ - ✅ **Google (Gemini)** — maps snake_case Ruby keys to camelCase `generationConfig`,
389
+ including `thinkingConfig` for Gemini 3.x thinking controls
352
390
 
353
391
  Coming soon:
354
392
  - 🔜 OpenAI (GPT)
@@ -364,6 +402,8 @@ Coming soon:
364
402
  1. Check parameter spelling (case-sensitive)
365
403
  2. Verify your provider supports the parameter
366
404
  3. Check parameter value types (integer vs float vs string)
405
+ 4. Set `config.logger` — Gemini logs a warning for every param it cannot map,
406
+ e.g. `Skipping generationConfig param :reasoning_effort: no Gemini API mapping.`
367
407
 
368
408
  ### Unexpected Output
369
409
 
@@ -379,7 +419,9 @@ Coming soon:
379
419
 
380
420
  ## Examples
381
421
 
382
- See the complete example file: [examples/ollama_params_usage.rb](examples/ollama_params_usage.rb)
422
+ See the complete example files:
423
+ - [examples/ollama_params_usage.rb](../examples/ollama_params_usage.rb)
424
+ - [examples/gemini_thinking_usage.rb](../examples/gemini_thinking_usage.rb)
383
425
 
384
426
  ## Resources
385
427
 
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Gemini thinking configuration (generationConfig.thinkingConfig).
5
+ #
6
+ # Gemini 3.x models think before answering. Trimming that budget is the biggest
7
+ # latency lever on the fast models, so llm_conductor exposes it through params:
8
+ #
9
+ # params: { thinking_level: 'minimal' } # flat convenience key
10
+ # params: { thinking_config: { thinking_level: 'minimal' } } # nested, mirrors the API
11
+ #
12
+ # Both reach the API as generationConfig.thinkingConfig.thinkingLevel.
13
+ #
14
+ # GEMINI_API_KEY=... ruby examples/gemini_thinking_usage.rb
15
+ # GEMINI_API_KEY=... MODEL=gemini-3.5-flash-lite ruby examples/gemini_thinking_usage.rb
16
+
17
+ require_relative '../lib/llm_conductor'
18
+ require 'logger'
19
+
20
+ MODEL = ENV['MODEL'] || 'gemini-2.5-flash'
21
+ PROMPT = 'Name the capital of France. Answer with one word.'
22
+
23
+ LlmConductor.configure do |config|
24
+ config.gemini(api_key: ENV.fetch('GEMINI_API_KEY'))
25
+ # With a logger configured, any param the client cannot map is reported
26
+ # instead of being dropped silently.
27
+ config.logger = Logger.new($stdout)
28
+ end
29
+
30
+ CASES = {
31
+ 'no thinking config' => {},
32
+ 'flat thinking_level' => { thinking_level: 'minimal' },
33
+ 'nested thinking_config' => { thinking_config: { thinking_level: 'minimal' } },
34
+ 'thinking_budget: 0' => { thinking_config: { thinking_budget: 0 } },
35
+ 'unmapped param (expect a warning)' => { reasoning_effort: 'high' }
36
+ }.freeze
37
+
38
+ CASES.each do |label, params|
39
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
40
+ response = LlmConductor.generate(model: MODEL, prompt: PROMPT, vendor: :gemini, params:)
41
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
42
+
43
+ if response.success?
44
+ puts "[#{label}] #{elapsed.round(2)}s — #{response.output.to_s.strip}"
45
+ else
46
+ puts "[#{label}] FAILED — #{response.metadata[:error]}"
47
+ end
48
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmConductor
4
+ module Clients
5
+ module Concerns
6
+ # Builds Gemini's generationConfig payload from the generic snake_case +params+
7
+ # hash accepted by every client.
8
+ #
9
+ # The Gemini REST API expects camelCase keys inside generationConfig, and nests
10
+ # the Gemini 3.x thinking controls one level deeper, under
11
+ # generationConfig.thinkingConfig. Callers keep passing snake_case Ruby keys —
12
+ # the translation lives here.
13
+ #
14
+ # Keys without a mapping are skipped and reported through the configured logger
15
+ # instead of disappearing silently. Skipping rather than raising keeps existing
16
+ # callers that pass provider-agnostic params working unchanged.
17
+ module GeminiGenerationConfig
18
+ # Gemini REST API uses camelCase keys in generationConfig.
19
+ PARAM_KEY_MAP = {
20
+ temperature: :temperature,
21
+ top_p: :topP,
22
+ top_k: :topK,
23
+ max_tokens: :maxOutputTokens,
24
+ max_output_tokens: :maxOutputTokens,
25
+ candidate_count: :candidateCount,
26
+ stop_sequences: :stopSequences
27
+ }.freeze
28
+
29
+ # Keys nested under generationConfig.thinkingConfig (Gemini 3.x thinking
30
+ # controls). Accepted flat in params or inside a thinking_config hash.
31
+ THINKING_KEY_MAP = {
32
+ thinking_level: :thinkingLevel,
33
+ thinking_budget: :thinkingBudget,
34
+ include_thoughts: :includeThoughts
35
+ }.freeze
36
+
37
+ # Param keys carrying a nested thinkingConfig hash.
38
+ THINKING_CONFIG_KEYS = %i[thinking_config thinkingConfig].freeze
39
+
40
+ private
41
+
42
+ # Build Gemini generationConfig from the params hash.
43
+ #
44
+ # Thinking controls may be passed flat (thinking_level: 'minimal') or nested
45
+ # (thinking_config: { thinking_level: 'minimal' }); either way they end up in
46
+ # generationConfig.thinkingConfig, and both forms together are merged.
47
+ #
48
+ # @return [Hash, nil] generationConfig hash, or nil when nothing maps
49
+ def build_generation_config
50
+ return unless params.is_a?(Hash) && params.any?
51
+
52
+ gen_cfg = {}
53
+ thinking_cfg = {}
54
+ params.each { |key, value| assign_generation_param(key, value, gen_cfg, thinking_cfg) }
55
+ gen_cfg[:thinkingConfig] = thinking_cfg if thinking_cfg.any?
56
+ gen_cfg.any? ? gen_cfg : nil
57
+ end
58
+
59
+ # Route a single param into generationConfig or its nested thinkingConfig.
60
+ def assign_generation_param(key, value, gen_cfg, thinking_cfg)
61
+ if THINKING_CONFIG_KEYS.include?(key.to_sym)
62
+ thinking_cfg.merge!(normalize_thinking_config(value))
63
+ elsif (thinking_key = thinking_config_key_for(key))
64
+ thinking_cfg[thinking_key] = value
65
+ elsif (mapped = PARAM_KEY_MAP[key.to_sym])
66
+ gen_cfg[mapped] = value
67
+ else
68
+ warn_unmapped_param(key)
69
+ end
70
+ end
71
+
72
+ # Normalize a nested thinking_config hash to the API's camelCase keys.
73
+ # @param value [Hash] thinking config as given by the caller
74
+ # @return [Hash] camelCase thinkingConfig fragment
75
+ def normalize_thinking_config(value)
76
+ unless value.is_a?(Hash)
77
+ warn_unmapped_param(:thinking_config, reason: "expected a Hash, got #{value.class}")
78
+ return {}
79
+ end
80
+
81
+ value.each_with_object({}) do |(key, val), cfg|
82
+ mapped = thinking_config_key_for(key)
83
+ if mapped
84
+ cfg[mapped] = val
85
+ else
86
+ warn_unmapped_param(key, scope: 'thinkingConfig')
87
+ end
88
+ end
89
+ end
90
+
91
+ # Resolve a thinkingConfig key, accepting snake_case or the camelCase API name.
92
+ # @return [Symbol, nil] camelCase API key, or nil when unknown
93
+ def thinking_config_key_for(key)
94
+ sym_key = key.to_sym
95
+ THINKING_KEY_MAP[sym_key] || (sym_key if THINKING_KEY_MAP.value?(sym_key))
96
+ end
97
+
98
+ # Report a param the client cannot map. Non-fatal by design: unmapped keys are
99
+ # skipped, but never without a trace — configure LlmConductor.configuration.logger
100
+ # to surface them.
101
+ def warn_unmapped_param(key, scope: 'generationConfig', reason: 'no Gemini API mapping')
102
+ configuration.logger&.warn(
103
+ "[LlmConductor::Clients::GeminiClient] Skipping #{scope} param #{key.inspect}: #{reason}. " \
104
+ 'See docs/custom-parameters.md for the supported keys.'
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -5,6 +5,7 @@ require 'base64'
5
5
  require 'net/http'
6
6
  require 'uri'
7
7
  require_relative 'concerns/vision_support'
8
+ require_relative 'concerns/gemini_generation_config'
8
9
  require_relative '../patches/gemini_vertex_api_key'
9
10
 
10
11
  module LlmConductor
@@ -13,20 +14,10 @@ module LlmConductor
13
14
  # Supports both text-only and multimodal (vision) requests
14
15
  class GeminiClient < BaseClient
15
16
  include Concerns::VisionSupport
17
+ include Concerns::GeminiGenerationConfig
16
18
 
17
19
  private
18
20
 
19
- # Gemini REST API uses camelCase keys in generationConfig.
20
- PARAM_KEY_MAP = {
21
- temperature: :temperature,
22
- top_p: :topP,
23
- top_k: :topK,
24
- max_tokens: :maxOutputTokens,
25
- max_output_tokens: :maxOutputTokens,
26
- candidate_count: :candidateCount,
27
- stop_sequences: :stopSequences
28
- }.freeze
29
-
30
21
  def generate_content(prompt)
31
22
  content = format_content(prompt)
32
23
  parts = build_parts_for_gemini(content)
@@ -45,20 +36,6 @@ module LlmConductor
45
36
  response.dig('candidates', 0, 'content', 'parts', 0, 'text')
46
37
  end
47
38
 
48
- # Build Gemini generationConfig from params hash.
49
- # Maps snake_case Ruby keys to camelCase Gemini API keys.
50
- # @return [Hash, nil] generationConfig hash or nil if no mapped params
51
- def build_generation_config
52
- return unless params.is_a?(Hash) && params.any?
53
-
54
- gen_cfg = {}
55
- params.each do |key, value|
56
- mapped = PARAM_KEY_MAP[key.to_sym]
57
- gen_cfg[mapped] = value if mapped
58
- end
59
- gen_cfg.any? ? gen_cfg : nil
60
- end
61
-
62
39
  # Build parts array for Gemini API from formatted content
63
40
  # Converts VisionSupport format to Gemini's specific format
64
41
  # @param content [String, Array] Formatted content from VisionSupport
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmConductor
4
- VERSION = '1.8.2'
4
+ VERSION = '1.8.3'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_conductor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.2
4
+ version: 1.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Zheng
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-06-11 00:00:00.000000000 Z
10
+ date: 2026-08-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -172,6 +172,7 @@ files:
172
172
  - docs/vision-support.md
173
173
  - examples/claude_vision_usage.rb
174
174
  - examples/data_builder_usage.rb
175
+ - examples/gemini_thinking_usage.rb
175
176
  - examples/gemini_usage.rb
176
177
  - examples/gemini_vision_usage.rb
177
178
  - examples/gpt_vision_usage.rb
@@ -187,6 +188,7 @@ files:
187
188
  - lib/llm_conductor/client_factory.rb
188
189
  - lib/llm_conductor/clients/anthropic_client.rb
189
190
  - lib/llm_conductor/clients/base_client.rb
191
+ - lib/llm_conductor/clients/concerns/gemini_generation_config.rb
190
192
  - lib/llm_conductor/clients/concerns/vision_support.rb
191
193
  - lib/llm_conductor/clients/gemini_client.rb
192
194
  - lib/llm_conductor/clients/gpt_client.rb