legion-llm 0.3.3 → 0.3.5

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: 7faf26458139d4c0e585e5c30e42602e85402e20abcbe2cd73ef8449ae17f947
4
- data.tar.gz: 73fecb93dbfd407891e64a4278c21c9e9f7fcc2af8313660de4e075a3195bbc7
3
+ metadata.gz: e3e10dfcd60fe722290bec30017671fb261f3baf151b416c82defb082d4445f4
4
+ data.tar.gz: 0c0b062649f8ede281fada374681d551d437126a2efa0e604a069610e14b7069
5
5
  SHA512:
6
- metadata.gz: 067c7e99927b675df13517a6ca5aa12b494fdedd8e277e7a42ae060e7597033597fbd0f88f595d5368937ba4a00aa7f6e2e02c14e56d01763f4253eb1cd3f421
7
- data.tar.gz: 9dc754975461db838b49d1f9826d54a80c7bdf106fa44fa6fe3d3693eefe70a1d503ed5025e551a458b367ab53580c67b7c9cf1fb9ee01e69cd5c2394181f150
6
+ metadata.gz: eeb2cd074c2eb1c3b63ccb7644adbcf7cac6bab62f8d5cc966e318b2185267ab73fae920726b83e1f72cbf8753ba1245ab84ae914baa092aebdbef08e0548cd3
7
+ data.tar.gz: ccc52360f869421100f0bbda503570168f2f7eb86c5fda9069e6ee29bdaa4c60553d202a1ad2e2109e98ba451b9abf9c00dc8210b50cf5d0a925192cada2ab9d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.3.5] - 2026-03-18
4
+
5
+ ### Added
6
+ - Gateway integration: `chat`, `embed`, `structured` delegate to `lex-llm-gateway` when loaded for automatic metering and fleet dispatch
7
+ - `chat_direct`, `embed_direct`, `structured_direct` methods bypass gateway (used by gateway runners to avoid recursion)
8
+ - Gateway integration spec (8 examples)
9
+
10
+ ## [0.3.4] - 2026-03-18
11
+
12
+ ### Added
13
+ - Auto-configure LLM providers from environment variables (`AWS_BEARER_TOKEN_BEDROCK`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `CODEX_API_KEY`, `GEMINI_API_KEY`)
14
+ - `ANTHROPIC_MODEL` env var sets default model for Anthropic and Bedrock providers
15
+ - Import Claude CLI config from `~/.claude/settings.json` and `~/.claude.json`
16
+ - Auto-detect Ollama via local port probe (no env var needed)
17
+ - Auto-enable providers when credentials are found in environment
18
+
3
19
  ## [0.3.3] - 2026-03-17
4
20
 
5
21
  ### Added
data/CLAUDE.md CHANGED
@@ -284,7 +284,7 @@ In-memory signal consumer with pluggable handlers. Adjusts effective priorities
284
284
  | `lib/legion/llm/embeddings.rb` | Embeddings module: generate, generate_batch, default_model |
285
285
  | `lib/legion/llm/shadow_eval.rb` | Shadow evaluation: enabled?, should_sample?, evaluate, compare |
286
286
  | `lib/legion/llm/structured_output.rb` | JSON schema enforcement with native response_format and prompt fallback |
287
- | `lib/legion/llm/version.rb` | Version constant (0.3.2) |
287
+ | `lib/legion/llm/version.rb` | Version constant (0.3.3) |
288
288
  | `lib/legion/llm/quality_checker.rb` | QualityChecker module with QualityResult struct |
289
289
  | `lib/legion/llm/escalation_history.rb` | EscalationHistory mixin: `escalation_history`, `escalated?`, `final_resolution`, `escalation_chain` |
290
290
  | `lib/legion/llm/router/escalation_chain.rb` | EscalationChain value object |
data/Gemfile CHANGED
@@ -4,6 +4,8 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'lex-llm-gateway', path: '../extensions-core/lex-llm-gateway' if File.directory?('../extensions-core/lex-llm-gateway')
8
+
7
9
  group :test do
8
10
  gem 'rake'
9
11
  gem 'rspec'
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module LLM
5
+ module ClaudeConfigLoader
6
+ CLAUDE_SETTINGS = File.expand_path('~/.claude/settings.json')
7
+ CLAUDE_CONFIG = File.expand_path('~/.claude.json')
8
+
9
+ module_function
10
+
11
+ def load
12
+ config = read_json(CLAUDE_SETTINGS).merge(read_json(CLAUDE_CONFIG))
13
+ return if config.empty?
14
+
15
+ apply_claude_config(config)
16
+ end
17
+
18
+ def read_json(path)
19
+ return {} unless File.exist?(path)
20
+
21
+ require 'json'
22
+ ::JSON.parse(File.read(path), symbolize_names: true)
23
+ rescue StandardError
24
+ {}
25
+ end
26
+
27
+ def apply_claude_config(config)
28
+ apply_api_keys(config)
29
+ apply_model_preference(config)
30
+ end
31
+
32
+ def apply_api_keys(config)
33
+ llm = Legion::LLM.settings
34
+ providers = llm[:providers]
35
+
36
+ if config[:anthropicApiKey] && providers.dig(:anthropic, :api_key).nil?
37
+ providers[:anthropic][:api_key] = config[:anthropicApiKey]
38
+ Legion::Logging.debug 'Imported Anthropic API key from Claude CLI config'
39
+ end
40
+
41
+ return unless config[:openaiApiKey] && providers.dig(:openai, :api_key).nil?
42
+
43
+ providers[:openai][:api_key] = config[:openaiApiKey]
44
+ Legion::Logging.debug 'Imported OpenAI API key from Claude CLI config'
45
+ end
46
+
47
+ def apply_model_preference(config)
48
+ return unless config[:preferredModel] || config[:model]
49
+
50
+ model = config[:preferredModel] || config[:model]
51
+ llm = Legion::LLM.settings
52
+ return if llm[:default_model]
53
+
54
+ llm[:default_model] = model
55
+ Legion::Logging.debug "Imported model preference from Claude CLI config: #{model}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -4,6 +4,7 @@ module Legion
4
4
  module LLM
5
5
  module Providers
6
6
  def configure_providers
7
+ auto_enable_from_resolved_credentials
7
8
  settings[:providers].each do |provider, config|
8
9
  next unless config[:enabled]
9
10
 
@@ -11,6 +12,37 @@ module Legion
11
12
  end
12
13
  end
13
14
 
15
+ def auto_enable_from_resolved_credentials
16
+ settings[:providers].each do |provider, config|
17
+ next if config[:enabled]
18
+
19
+ has_creds = case provider
20
+ when :bedrock
21
+ config[:bearer_token] || (config[:api_key] && config[:secret_key])
22
+ when :ollama
23
+ ollama_running?(config)
24
+ else
25
+ config[:api_key]
26
+ end
27
+ next unless has_creds
28
+
29
+ config[:enabled] = true
30
+ Legion::Logging.info "Auto-enabled #{provider} provider (credentials found)"
31
+ end
32
+ end
33
+
34
+ def ollama_running?(config)
35
+ require 'socket'
36
+ url = config[:base_url] || 'http://localhost:11434'
37
+ host_part = url.gsub(%r{^https?://}, '').split(':')
38
+ addr = host_part[0]
39
+ port = (host_part[1] || '11434').to_i
40
+ Socket.tcp(addr, port, connect_timeout: 1).close
41
+ true
42
+ rescue StandardError
43
+ false
44
+ end
45
+
14
46
  def apply_provider_config(provider, config)
15
47
  case provider
16
48
  when :bedrock
@@ -4,10 +4,11 @@ module Legion
4
4
  module LLM
5
5
  module Settings
6
6
  def self.default
7
+ model_override = ENV.fetch('ANTHROPIC_MODEL', nil)
7
8
  {
8
9
  enabled: true,
9
10
  connected: false,
10
- default_model: nil,
11
+ default_model: model_override,
11
12
  default_provider: nil,
12
13
  providers: providers,
13
14
  routing: routing_defaults,
@@ -68,23 +69,23 @@ module Legion
68
69
  api_key: nil,
69
70
  secret_key: nil,
70
71
  session_token: nil,
71
- bearer_token: nil,
72
+ bearer_token: 'env://AWS_BEARER_TOKEN_BEDROCK',
72
73
  region: 'us-east-2'
73
74
  },
74
75
  anthropic: {
75
76
  enabled: false,
76
77
  default_model: 'claude-sonnet-4-6',
77
- api_key: nil
78
+ api_key: 'env://ANTHROPIC_API_KEY'
78
79
  },
79
80
  openai: {
80
81
  enabled: false,
81
82
  default_model: 'gpt-4o',
82
- api_key: nil
83
+ api_key: ['env://OPENAI_API_KEY', 'env://CODEX_API_KEY']
83
84
  },
84
85
  gemini: {
85
86
  enabled: false,
86
87
  default_model: 'gemini-2.0-flash',
87
- api_key: nil
88
+ api_key: 'env://GEMINI_API_KEY'
88
89
  },
89
90
  ollama: {
90
91
  enabled: false,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.3.3'
5
+ VERSION = '0.3.5'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -9,6 +9,12 @@ require 'legion/llm/compressor'
9
9
  require 'legion/llm/quality_checker'
10
10
  require 'legion/llm/escalation_history'
11
11
 
12
+ begin
13
+ require 'legion/extensions/llm/gateway'
14
+ rescue LoadError
15
+ nil
16
+ end
17
+
12
18
  module Legion
13
19
  module LLM
14
20
  class EscalationExhausted < StandardError; end
@@ -19,6 +25,9 @@ module Legion
19
25
  def start
20
26
  Legion::Logging.debug 'Legion::LLM is running start'
21
27
 
28
+ require 'legion/llm/claude_config_loader'
29
+ ClaudeConfigLoader.load
30
+
22
31
  configure_providers
23
32
  run_discovery
24
33
  set_defaults
@@ -47,20 +56,24 @@ module Legion
47
56
  end
48
57
  end
49
58
 
50
- # Create a new chat session
51
- # @param model [String] model ID (e.g., "us.anthropic.claude-sonnet-4-6-v1")
52
- # @param provider [Symbol] provider slug (e.g., :bedrock, :anthropic)
53
- # @param intent [Hash, nil] routing intent (capability, privacy, etc.)
54
- # @param tier [Symbol, nil] explicit tier override — skips rule matching
55
- # @param escalate [Boolean, nil] enable escalation retry loop (nil = auto from settings)
56
- # @param max_escalations [Integer, nil] max escalation attempts override
57
- # @param quality_check [Proc, nil] custom quality check callable
58
- # @param message [String, nil] message to send (required for escalation)
59
- # @param kwargs [Hash] additional options passed to RubyLLM.chat
60
- # @return [RubyLLM::Chat]
61
- # TODO: fleet tier dispatch via Transport (Phase 3)
59
+ # Create a new chat session — delegates to lex-llm-gateway when available
60
+ # for automatic metering and fleet dispatch
62
61
  def chat(model: nil, provider: nil, intent: nil, tier: nil, escalate: nil,
63
62
  max_escalations: nil, quality_check: nil, message: nil, **)
63
+ if gateway_loaded? && message
64
+ return gateway_chat(model: model, provider: provider, intent: intent,
65
+ tier: tier, message: message, escalate: escalate,
66
+ max_escalations: max_escalations, quality_check: quality_check, **)
67
+ end
68
+
69
+ chat_direct(model: model, provider: provider, intent: intent, tier: tier,
70
+ escalate: escalate, max_escalations: max_escalations,
71
+ quality_check: quality_check, message: message, **)
72
+ end
73
+
74
+ # Direct chat bypassing gateway — used by gateway runners to avoid recursion
75
+ def chat_direct(model: nil, provider: nil, intent: nil, tier: nil, escalate: nil,
76
+ max_escalations: nil, quality_check: nil, message: nil, **)
64
77
  escalate = escalation_enabled? if escalate.nil?
65
78
 
66
79
  if escalate && message
@@ -74,11 +87,15 @@ module Legion
74
87
  end
75
88
  end
76
89
 
77
- # Generate embeddings via Embeddings module
78
- # @param text [String, Array<String>] text to embed
79
- # @param model [String] embedding model ID
80
- # @return [Hash] { vector:, model:, dimensions:, tokens: }
90
+ # Generate embeddings delegates to gateway when available
81
91
  def embed(text, **)
92
+ return Legion::Extensions::LLM::Gateway::Runners::Inference.embed(text: text, **) if gateway_loaded?
93
+
94
+ embed_direct(text, **)
95
+ end
96
+
97
+ # Direct embed bypassing gateway
98
+ def embed_direct(text, **)
82
99
  require 'legion/llm/embeddings'
83
100
  Embeddings.generate(text: text, **)
84
101
  end
@@ -91,11 +108,19 @@ module Legion
91
108
  Embeddings.generate_batch(texts: texts, **)
92
109
  end
93
110
 
94
- # Generate structured JSON output from LLM
95
- # @param messages [Array<Hash>] conversation messages
96
- # @param schema [Hash] JSON schema to enforce
97
- # @return [Hash] { data:, raw:, model:, valid: }
111
+ # Generate structured JSON output delegates to gateway when available
98
112
  def structured(messages:, schema:, **)
113
+ if gateway_loaded?
114
+ return Legion::Extensions::LLM::Gateway::Runners::Inference.structured(
115
+ messages: messages, schema: schema, **
116
+ )
117
+ end
118
+
119
+ structured_direct(messages: messages, schema: schema, **)
120
+ end
121
+
122
+ # Direct structured bypassing gateway
123
+ def structured_direct(messages:, schema:, **)
99
124
  require 'legion/llm/structured_output'
100
125
  StructuredOutput.generate(messages: messages, schema: schema, **)
101
126
  end
@@ -110,6 +135,14 @@ module Legion
110
135
 
111
136
  private
112
137
 
138
+ def gateway_loaded?
139
+ defined?(Legion::Extensions::LLM::Gateway::Runners::Inference)
140
+ end
141
+
142
+ def gateway_chat(**)
143
+ Legion::Extensions::LLM::Gateway::Runners::Inference.chat(**)
144
+ end
145
+
113
146
  def chat_single(model:, provider:, intent:, tier:, **kwargs)
114
147
  if (intent || tier) && Router.routing_enabled?
115
148
  resolution = Router.resolve(intent: intent, tier: tier, model: model, provider: provider)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -89,6 +89,7 @@ files:
89
89
  - legion-llm.gemspec
90
90
  - lib/legion/llm.rb
91
91
  - lib/legion/llm/bedrock_bearer_auth.rb
92
+ - lib/legion/llm/claude_config_loader.rb
92
93
  - lib/legion/llm/compressor.rb
93
94
  - lib/legion/llm/discovery/ollama.rb
94
95
  - lib/legion/llm/discovery/system.rb