legion-llm 0.3.2 → 0.3.4

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: 45ff0d9cdd07ee541c80dbac46e66d37542af95a96a614e31fc4af2c2bdf7833
4
- data.tar.gz: 1562706b98e0e6e301a76dec373cecbcf99a3eb3a8e74a0d258928fbbaaf5be7
3
+ metadata.gz: bd0b530095616abc383dcd06473a6c435753f021458c76c981da1d4e98583a5f
4
+ data.tar.gz: 21d8645355c14d591891c3484ca90957e99b0cb376b115eb61dd61f3e0721800
5
5
  SHA512:
6
- metadata.gz: e28b6c1e39599d1ecd16a60ff67bf4c7625b2d3cdcb2406b465751640b13d44d8a16e593644a4ee30f0bdcaff577580a19e7577e48a7bb07dc3fcfaaf28b3de9
7
- data.tar.gz: d8fe57b67e87f8035c9c78c9bc8489aea68d75d50e477ca0007cd471cd24254081cf6784d2350d128d721dbbf4e5332340ac8e5d8297e9feffb1efe826b87d11
6
+ metadata.gz: 53f3b6bd09f86625986e6f9d5c53f665e000e71d78dc5db36d599f1b5e5d7267d40ca1a2fe1e9f2b48cc54fb7ab6d272108869a02b327ff9669f978b83280e71
7
+ data.tar.gz: 381d707e3bdb75a1cf87d82404dc842140f98fa4bb5091e5a837685235327684b800de977efcd24857bb0ab8ab5bc75d42fdc22364aa4b024d6adf8f27cdab65
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.3.4] - 2026-03-18
4
+
5
+ ### Added
6
+ - Auto-configure LLM providers from environment variables (`AWS_BEARER_TOKEN_BEDROCK`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `CODEX_API_KEY`, `GEMINI_API_KEY`)
7
+ - `ANTHROPIC_MODEL` env var sets default model for Anthropic and Bedrock providers
8
+ - Import Claude CLI config from `~/.claude/settings.json` and `~/.claude.json`
9
+ - Auto-detect Ollama via local port probe (no env var needed)
10
+ - Auto-enable providers when credentials are found in environment
11
+
12
+ ## [0.3.3] - 2026-03-17
13
+
14
+ ### Added
15
+ - `Router::GatewayInterceptor`: optional gateway routing mode for cloud-tier LLM calls
16
+ - Gateway settings: endpoint, API key, model policy per risk tier, fallback_to_direct
17
+ - Identity header builder: X-Agent-Id, X-Tenant-Id, X-AIRB-Project-Id, X-Risk-Tier
18
+ - Model selection policy: fnmatch-based allowlist per risk tier
19
+ - Wired gateway interceptor into `chat_single` for automatic cloud-tier interception
20
+
3
21
  ## [0.3.2] - 2026-03-16
4
22
 
5
23
  ### 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 |
@@ -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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module LLM
5
+ module Router
6
+ module GatewayInterceptor
7
+ module_function
8
+
9
+ def intercept(resolution, context: {})
10
+ return resolution unless gateway_enabled?
11
+ return resolution unless resolution&.tier == :cloud
12
+
13
+ model = resolution.model
14
+ risk_tier = context[:risk_tier]&.to_sym
15
+
16
+ unless model_allowed?(model, risk_tier)
17
+ Legion::Logging.warn "[llm] gateway policy blocked model=#{model} risk_tier=#{risk_tier}"
18
+ return nil
19
+ end
20
+
21
+ Resolution.new(
22
+ tier: :cloud,
23
+ provider: :gateway,
24
+ model: model,
25
+ rule: 'gateway_intercept',
26
+ metadata: { original_provider: resolution.provider }
27
+ )
28
+ end
29
+
30
+ def gateway_enabled?
31
+ settings = gateway_settings
32
+ settings[:enabled] == true && !settings[:endpoint].nil?
33
+ end
34
+
35
+ def model_allowed?(model, risk_tier)
36
+ return true unless risk_tier
37
+
38
+ allowlist = gateway_settings.dig(:model_policy, risk_tier)
39
+ return true unless allowlist.is_a?(Array) && !allowlist.empty?
40
+
41
+ allowlist.any? { |pattern| File.fnmatch?(pattern, model.to_s) }
42
+ end
43
+
44
+ def gateway_headers(context)
45
+ {
46
+ 'X-Agent-Id' => context[:worker_id],
47
+ 'X-Tenant-Id' => context[:tenant_id],
48
+ 'X-AIRB-Project-Id' => context[:airb_project_id],
49
+ 'X-Risk-Tier' => context[:risk_tier]&.to_s,
50
+ 'X-Legion-Task-Id' => context[:task_id]&.to_s
51
+ }.compact
52
+ end
53
+
54
+ def gateway_settings
55
+ llm = Legion::Settings[:llm]
56
+ return {} unless llm.is_a?(Hash)
57
+
58
+ (llm[:gateway] || {}).transform_keys(&:to_sym)
59
+ rescue StandardError
60
+ {}
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -4,6 +4,7 @@ require_relative 'router/resolution'
4
4
  require_relative 'router/rule'
5
5
  require_relative 'router/health_tracker'
6
6
  require_relative 'router/escalation_chain'
7
+ require_relative 'router/gateway_interceptor'
7
8
  require_relative 'discovery/ollama'
8
9
  require_relative 'discovery/system'
9
10
 
@@ -4,14 +4,16 @@ 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,
14
- discovery: discovery_defaults
15
+ discovery: discovery_defaults,
16
+ gateway: gateway_defaults
15
17
  }
16
18
  end
17
19
 
@@ -47,6 +49,18 @@ module Legion
47
49
  }
48
50
  end
49
51
 
52
+ def self.gateway_defaults
53
+ {
54
+ enabled: false,
55
+ endpoint: nil,
56
+ api_key: nil,
57
+ timeout_seconds: 30,
58
+ model_policy: {},
59
+ headers: {},
60
+ fallback_to_direct: true
61
+ }
62
+ end
63
+
50
64
  def self.providers
51
65
  {
52
66
  bedrock: {
@@ -55,23 +69,23 @@ module Legion
55
69
  api_key: nil,
56
70
  secret_key: nil,
57
71
  session_token: nil,
58
- bearer_token: nil,
72
+ bearer_token: 'env://AWS_BEARER_TOKEN_BEDROCK',
59
73
  region: 'us-east-2'
60
74
  },
61
75
  anthropic: {
62
76
  enabled: false,
63
77
  default_model: 'claude-sonnet-4-6',
64
- api_key: nil
78
+ api_key: 'env://ANTHROPIC_API_KEY'
65
79
  },
66
80
  openai: {
67
81
  enabled: false,
68
82
  default_model: 'gpt-4o',
69
- api_key: nil
83
+ api_key: ['env://OPENAI_API_KEY', 'env://CODEX_API_KEY']
70
84
  },
71
85
  gemini: {
72
86
  enabled: false,
73
87
  default_model: 'gemini-2.0-flash',
74
- api_key: nil
88
+ api_key: 'env://GEMINI_API_KEY'
75
89
  },
76
90
  ollama: {
77
91
  enabled: false,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.3.2'
5
+ VERSION = '0.3.4'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -19,6 +19,9 @@ module Legion
19
19
  def start
20
20
  Legion::Logging.debug 'Legion::LLM is running start'
21
21
 
22
+ require 'legion/llm/claude_config_loader'
23
+ ClaudeConfigLoader.load
24
+
22
25
  configure_providers
23
26
  run_discovery
24
27
  set_defaults
@@ -114,6 +117,7 @@ module Legion
114
117
  if (intent || tier) && Router.routing_enabled?
115
118
  resolution = Router.resolve(intent: intent, tier: tier, model: model, provider: provider)
116
119
  if resolution
120
+ resolution = Router::GatewayInterceptor.intercept(resolution, context: kwargs.fetch(:context, {}))
117
121
  model = resolution.model
118
122
  provider = resolution.provider
119
123
  end
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.2
4
+ version: 0.3.4
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
@@ -99,6 +100,7 @@ files:
99
100
  - lib/legion/llm/quality_checker.rb
100
101
  - lib/legion/llm/router.rb
101
102
  - lib/legion/llm/router/escalation_chain.rb
103
+ - lib/legion/llm/router/gateway_interceptor.rb
102
104
  - lib/legion/llm/router/health_tracker.rb
103
105
  - lib/legion/llm/router/resolution.rb
104
106
  - lib/legion/llm/router/rule.rb