llm_conductor 1.6.0 → 1.7.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +10 -1
- data/examples/gemini_usage.rb +83 -11
- data/lib/llm_conductor/client_factory.rb +2 -0
- data/lib/llm_conductor/clients/gemini_client.rb +25 -5
- data/lib/llm_conductor/configuration.rb +27 -5
- data/lib/llm_conductor/patches/gemini_vertex_api_key.rb +52 -0
- data/lib/llm_conductor/version.rb +1 -1
- data/lib/llm_conductor.rb +0 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a132b52551949cd8cd7446e2cf9a26f1dacba997dbae0965494b4ee174cf5905
|
|
4
|
+
data.tar.gz: 89d87446edea2d31c7194f84e1c98cbb11d241a0a1cd57b2878b387c94b441a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21204a00bc9d437fa702dc67f6224c8ef325e32daa94b252421fdff430929a03ca9903634df739b72d11f4b689320d7bfb1b626e800443aa90d0393ab5d11b01
|
|
7
|
+
data.tar.gz: 490ded3cf0e013a0edaed1bb66b4c91750a6503419c37e58b8fd4e5d53db464ff0785a4de4a76c8b3b87b787684fa327d78a7c8c872dc30608a0225de829c59d
|
data/.rubocop.yml
CHANGED
|
@@ -30,7 +30,7 @@ Lint/ConstantDefinitionInBlock:
|
|
|
30
30
|
Enabled: false
|
|
31
31
|
|
|
32
32
|
Metrics/ClassLength:
|
|
33
|
-
Max:
|
|
33
|
+
Max: 125
|
|
34
34
|
|
|
35
35
|
Metrics/MethodLength:
|
|
36
36
|
Max: 15
|
|
@@ -38,6 +38,8 @@ Metrics/MethodLength:
|
|
|
38
38
|
- 'lib/llm_conductor/prompts.rb'
|
|
39
39
|
- 'lib/llm_conductor/clients/openrouter_client.rb'
|
|
40
40
|
- 'lib/llm_conductor/clients/zai_client.rb'
|
|
41
|
+
- 'lib/llm_conductor/client_factory.rb'
|
|
42
|
+
- 'examples/*.rb'
|
|
41
43
|
|
|
42
44
|
RSpec/ExampleLength:
|
|
43
45
|
Enabled: false
|
|
@@ -96,6 +98,12 @@ Metrics/AbcSize:
|
|
|
96
98
|
- 'lib/llm_conductor/prompts.rb'
|
|
97
99
|
- 'lib/llm_conductor/clients/openrouter_client.rb'
|
|
98
100
|
- 'lib/llm_conductor/clients/zai_client.rb'
|
|
101
|
+
- 'examples/*.rb'
|
|
102
|
+
|
|
103
|
+
Metrics/ParameterLists:
|
|
104
|
+
Exclude:
|
|
105
|
+
- 'lib/llm_conductor.rb'
|
|
106
|
+
- 'lib/llm_conductor/configuration.rb'
|
|
99
107
|
|
|
100
108
|
Metrics/CyclomaticComplexity:
|
|
101
109
|
Exclude:
|
|
@@ -103,6 +111,7 @@ Metrics/CyclomaticComplexity:
|
|
|
103
111
|
- 'lib/llm_conductor/prompts.rb'
|
|
104
112
|
- 'lib/llm_conductor/clients/openrouter_client.rb'
|
|
105
113
|
- 'lib/llm_conductor/clients/zai_client.rb'
|
|
114
|
+
- 'examples/*.rb'
|
|
106
115
|
|
|
107
116
|
Metrics/PerceivedComplexity:
|
|
108
117
|
Exclude:
|
data/examples/gemini_usage.rb
CHANGED
|
@@ -1,18 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
4
|
+
# Connectivity test for all Gemini auth methods.
|
|
5
|
+
# Omit SCENARIO to run all scenarios that have the required env vars set.
|
|
6
|
+
#
|
|
7
|
+
# Scenario A — Generative Language API (api_key)
|
|
8
|
+
# SCENARIO=api_key GEMINI_API_KEY=... ruby examples/gemini_usage.rb
|
|
9
|
+
#
|
|
10
|
+
# Scenario B — Vertex AI with account-bound API key
|
|
11
|
+
# SCENARIO=vertex_api_key GEMINI_API_KEY=... GOOGLE_VERTEX_PROJECT_ID=... ruby examples/gemini_usage.rb
|
|
12
|
+
#
|
|
13
|
+
# Scenario C — Vertex AI via Application Default Credentials
|
|
14
|
+
# SCENARIO=adc \
|
|
15
|
+
# GOOGLE_VERTEX_PROJECT_ID=... \
|
|
16
|
+
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json \
|
|
17
|
+
# ruby examples/gemini_usage.rb
|
|
18
|
+
#
|
|
19
|
+
# Scenario C — Vertex AI via credentials file contents
|
|
20
|
+
# SCENARIO=file_contents \
|
|
21
|
+
# GOOGLE_VERTEX_PROJECT_ID=... \
|
|
22
|
+
# GOOGLE_CREDENTIALS_FILE_CONTENTS=$(cat /path/to/sa.json) \
|
|
23
|
+
# ruby examples/gemini_usage.rb
|
|
24
|
+
|
|
3
25
|
require_relative '../lib/llm_conductor'
|
|
4
26
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
27
|
+
MODEL = 'gemini-2.5-flash'
|
|
28
|
+
PROMPT = 'Say hello.'
|
|
29
|
+
|
|
30
|
+
SCENARIOS = {
|
|
31
|
+
'api_key' => -> { ENV['GEMINI_API_KEY'] && !ENV['GOOGLE_VERTEX_PROJECT_ID'] },
|
|
32
|
+
'vertex_api_key' => -> { ENV['GEMINI_API_KEY'] && ENV['GOOGLE_VERTEX_PROJECT_ID'] },
|
|
33
|
+
'adc' => -> { ENV['GOOGLE_VERTEX_PROJECT_ID'] && ENV['GOOGLE_APPLICATION_CREDENTIALS'] },
|
|
34
|
+
'file_contents' => -> { ENV['GOOGLE_VERTEX_PROJECT_ID'] && ENV['GOOGLE_CREDENTIALS_FILE_CONTENTS'] }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def run_scenario(name)
|
|
38
|
+
case name
|
|
39
|
+
when 'api_key'
|
|
40
|
+
LlmConductor.configure { |c| c.gemini(api_key: ENV.fetch('GEMINI_API_KEY')) }
|
|
41
|
+
label = 'api_key'
|
|
42
|
+
|
|
43
|
+
when 'vertex_api_key'
|
|
44
|
+
LlmConductor.configure do |c|
|
|
45
|
+
c.gemini(api_key: ENV.fetch('GEMINI_API_KEY'),
|
|
46
|
+
project_id: ENV.fetch('GOOGLE_VERTEX_PROJECT_ID'),
|
|
47
|
+
region: ENV['GOOGLE_VERTEX_REGION'])
|
|
48
|
+
end
|
|
49
|
+
label = 'vertex_ai/api_key'
|
|
50
|
+
|
|
51
|
+
when 'adc'
|
|
52
|
+
LlmConductor.configure do |c|
|
|
53
|
+
c.gemini(project_id: ENV.fetch('GOOGLE_VERTEX_PROJECT_ID'),
|
|
54
|
+
region: ENV['GOOGLE_VERTEX_REGION'])
|
|
55
|
+
end
|
|
56
|
+
label = 'vertex_ai/adc'
|
|
57
|
+
|
|
58
|
+
when 'file_contents'
|
|
59
|
+
LlmConductor.configure do |c|
|
|
60
|
+
c.gemini(project_id: ENV.fetch('GOOGLE_VERTEX_PROJECT_ID'),
|
|
61
|
+
region: ENV['GOOGLE_VERTEX_REGION'],
|
|
62
|
+
file_contents: ENV.fetch('GOOGLE_CREDENTIALS_FILE_CONTENTS'))
|
|
63
|
+
end
|
|
64
|
+
label = 'vertex_ai/file_contents'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
response = LlmConductor.generate(model: MODEL, prompt: PROMPT)
|
|
68
|
+
raise 'Empty response' if response.output.nil? || response.output.strip.empty?
|
|
69
|
+
|
|
70
|
+
puts "[#{label}] OK — #{response.output.strip}"
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
puts "[#{label}] FAILED — #{e.message}"
|
|
73
|
+
false
|
|
8
74
|
end
|
|
9
75
|
|
|
10
|
-
|
|
11
|
-
response = LlmConductor.generate(
|
|
12
|
-
model: 'gemini-2.5-flash',
|
|
13
|
-
prompt: 'Explain how AI works in a few words'
|
|
14
|
-
)
|
|
76
|
+
scenario = ENV['SCENARIO']
|
|
15
77
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
78
|
+
if scenario
|
|
79
|
+
abort "Unknown SCENARIO=#{scenario}. Use: api_key | vertex_api_key | adc | file_contents" unless SCENARIOS.key?(scenario)
|
|
80
|
+
run_scenario(scenario)
|
|
81
|
+
else
|
|
82
|
+
ran = 0
|
|
83
|
+
SCENARIOS.each do |name, available|
|
|
84
|
+
next unless available.call
|
|
85
|
+
|
|
86
|
+
run_scenario(name)
|
|
87
|
+
ran += 1
|
|
88
|
+
end
|
|
89
|
+
puts '(no scenarios ran — set the required env vars)' if ran.zero?
|
|
90
|
+
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 '../patches/gemini_vertex_api_key'
|
|
8
9
|
|
|
9
10
|
module LlmConductor
|
|
10
11
|
module Clients
|
|
@@ -32,7 +33,7 @@ module LlmConductor
|
|
|
32
33
|
|
|
33
34
|
payload = {
|
|
34
35
|
contents: [
|
|
35
|
-
{ parts: }
|
|
36
|
+
{ role: 'user', parts: }
|
|
36
37
|
]
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -156,14 +157,33 @@ module LlmConductor
|
|
|
156
157
|
@client ||= begin
|
|
157
158
|
config = LlmConductor.configuration.provider_config(:gemini)
|
|
158
159
|
Gemini.new(
|
|
159
|
-
credentials:
|
|
160
|
-
service: 'generative-language-api',
|
|
161
|
-
api_key: config[:api_key]
|
|
162
|
-
},
|
|
160
|
+
credentials: build_credentials(config),
|
|
163
161
|
options: { model: }
|
|
164
162
|
)
|
|
165
163
|
end
|
|
166
164
|
end
|
|
165
|
+
|
|
166
|
+
def build_credentials(config)
|
|
167
|
+
if config[:project_id] && config[:api_key]
|
|
168
|
+
{ service: 'vertex-ai-api', region: config[:region], project_id: config[:project_id],
|
|
169
|
+
api_key: config[:api_key] }
|
|
170
|
+
elsif config[:project_id]
|
|
171
|
+
vertex_ai_credentials(config)
|
|
172
|
+
else
|
|
173
|
+
{ service: 'generative-language-api', api_key: config[:api_key] }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def vertex_ai_credentials(config)
|
|
178
|
+
creds = {
|
|
179
|
+
service: 'vertex-ai-api',
|
|
180
|
+
region: config[:region],
|
|
181
|
+
project_id: config[:project_id]
|
|
182
|
+
}
|
|
183
|
+
creds[:file_path] = config[:file_path] if config[:file_path]
|
|
184
|
+
creds[:file_contents] = config[:file_contents] if config[:file_contents]
|
|
185
|
+
creds
|
|
186
|
+
end
|
|
167
187
|
end
|
|
168
188
|
end
|
|
169
189
|
end
|
|
@@ -56,12 +56,23 @@ module LlmConductor
|
|
|
56
56
|
}
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Configure Google Gemini provider
|
|
60
|
-
|
|
59
|
+
# Configure Google Gemini provider (Generative Language API or Vertex AI)
|
|
60
|
+
#
|
|
61
|
+
# For the standard Generative Language API, provide api_key.
|
|
62
|
+
# For Vertex AI, provide project_id and optionally region (defaults to 'global').
|
|
63
|
+
# Authentication falls back to Application Default Credentials
|
|
64
|
+
# (ADC / GOOGLE_APPLICATION_CREDENTIALS) when neither file_path nor file_contents is supplied.
|
|
65
|
+
# Env vars (GEMINI_API_KEY, GOOGLE_VERTEX_PROJECT_ID, etc.) are only applied automatically
|
|
66
|
+
# on boot via setup_defaults_from_env — explicit calls use only what is passed.
|
|
67
|
+
def gemini(api_key: nil, project_id: nil, region: nil, file_path: nil, file_contents: nil, **options)
|
|
61
68
|
@providers[:gemini] = {
|
|
62
|
-
api_key
|
|
69
|
+
api_key:,
|
|
70
|
+
project_id:,
|
|
71
|
+
region: region || 'global',
|
|
72
|
+
file_path:,
|
|
73
|
+
file_contents:,
|
|
63
74
|
**options
|
|
64
|
-
}
|
|
75
|
+
}.compact
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
# Configure Groq provider
|
|
@@ -149,11 +160,22 @@ module LlmConductor
|
|
|
149
160
|
anthropic if ENV['ANTHROPIC_API_KEY']
|
|
150
161
|
openai if ENV['OPENAI_API_KEY']
|
|
151
162
|
openrouter if ENV['OPENROUTER_API_KEY']
|
|
152
|
-
|
|
163
|
+
setup_gemini_from_env
|
|
153
164
|
groq if ENV['GROQ_API_KEY']
|
|
154
165
|
zai if ENV['ZAI_API_KEY']
|
|
155
166
|
ollama # Always configure Ollama with default URL
|
|
156
167
|
end
|
|
168
|
+
|
|
169
|
+
def setup_gemini_from_env
|
|
170
|
+
return unless ENV.values_at('GEMINI_API_KEY', 'GOOGLE_VERTEX_PROJECT_ID').any?
|
|
171
|
+
|
|
172
|
+
gemini(
|
|
173
|
+
api_key: ENV['GEMINI_API_KEY'],
|
|
174
|
+
project_id: ENV['GOOGLE_VERTEX_PROJECT_ID'],
|
|
175
|
+
region: ENV['GOOGLE_VERTEX_REGION'],
|
|
176
|
+
file_contents: ENV['GOOGLE_CREDENTIALS_FILE_CONTENTS']
|
|
177
|
+
)
|
|
178
|
+
end
|
|
157
179
|
end
|
|
158
180
|
|
|
159
181
|
def self.configuration
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patches the gemini-ai gem for two Vertex AI issues:
|
|
4
|
+
#
|
|
5
|
+
# 1. api_key + Vertex AI: the gem natively supports api_key only for
|
|
6
|
+
# generative-language-api. When both project_id and api_key are present,
|
|
7
|
+
# this patch rebuilds @base_address so the key is appended as ?key=... to
|
|
8
|
+
# the correct Vertex AI endpoint.
|
|
9
|
+
#
|
|
10
|
+
# 2. ADC (Application Default Credentials): the gem calls
|
|
11
|
+
# Google::Auth.get_application_default without a scope, which causes
|
|
12
|
+
# `invalid_scope` when exchanging service-account credentials for a token.
|
|
13
|
+
# This patch re-fetches the authorizer with the required cloud-platform scope.
|
|
14
|
+
module Gemini
|
|
15
|
+
module Controllers
|
|
16
|
+
class Client
|
|
17
|
+
module VertexAiPatch
|
|
18
|
+
CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
super
|
|
22
|
+
fix_vertex_api_key_base_address(config) if @authentication == :api_key && @service == 'vertex-ai-api'
|
|
23
|
+
fix_adc_scope if @authentication == :default_credentials
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fix_vertex_api_key_base_address(config)
|
|
29
|
+
project_id = config.dig(:credentials, :project_id)
|
|
30
|
+
|
|
31
|
+
if project_id.nil?
|
|
32
|
+
raise Errors::MissingProjectIdError,
|
|
33
|
+
'project_id is required for vertex-ai-api with api_key'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
region = config.dig(:credentials, :region) || 'global'
|
|
37
|
+
@base_address = if region == 'global'
|
|
38
|
+
"https://aiplatform.googleapis.com/#{@service_version}/projects/#{project_id}/locations/#{region}"
|
|
39
|
+
else
|
|
40
|
+
"https://#{region}-aiplatform.googleapis.com/#{@service_version}/projects/#{project_id}/locations/#{region}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fix_adc_scope
|
|
45
|
+
@authorizer = ::Google::Auth.get_application_default(CLOUD_PLATFORM_SCOPE)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
prepend VertexAiPatch
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/llm_conductor.rb
CHANGED
|
@@ -29,7 +29,6 @@ module LlmConductor
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Unified generate method supporting both simple prompts and legacy template-based generation
|
|
32
|
-
# rubocop:disable Metrics/ParameterLists
|
|
33
32
|
def self.generate(model: nil, prompt: nil, type: nil, data: nil, vendor: nil, params: {})
|
|
34
33
|
if prompt && !type && !data
|
|
35
34
|
generate_simple_prompt(model:, prompt:, vendor:, params:)
|
|
@@ -40,7 +39,6 @@ module LlmConductor
|
|
|
40
39
|
"Invalid arguments. Use either: generate(prompt: 'text') or generate(type: :custom, data: {...})"
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
|
-
# rubocop:enable Metrics/ParameterLists
|
|
44
42
|
|
|
45
43
|
class << self
|
|
46
44
|
private
|
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.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Zheng
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activesupport
|
|
@@ -181,6 +181,7 @@ files:
|
|
|
181
181
|
- lib/llm_conductor/clients/zai_client.rb
|
|
182
182
|
- lib/llm_conductor/configuration.rb
|
|
183
183
|
- lib/llm_conductor/data_builder.rb
|
|
184
|
+
- lib/llm_conductor/patches/gemini_vertex_api_key.rb
|
|
184
185
|
- lib/llm_conductor/prompt_manager.rb
|
|
185
186
|
- lib/llm_conductor/prompts.rb
|
|
186
187
|
- lib/llm_conductor/prompts/base_prompt.rb
|