ace-llm 0.36.3 → 0.38.2

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.
@@ -11,6 +11,7 @@ module Ace
11
11
  # Optional fields with expected types
12
12
  OPTIONAL_FIELDS = {
13
13
  "models" => Array,
14
+ "limits" => Hash,
14
15
  "api_key" => [Hash, NilClass],
15
16
  "capabilities" => Array,
16
17
  "default_options" => Hash,
@@ -69,6 +70,7 @@ module Ace
69
70
  validate_class(config["class"], errors) if config["class"]
70
71
  validate_gem(config["gem"], errors) if config["gem"]
71
72
  validate_models(config["models"], warnings) if config["models"]
73
+ validate_limits(config["limits"], errors, warnings) if config["limits"]
72
74
  validate_api_key(config["api_key"], errors, warnings) if config["api_key"]
73
75
  validate_capabilities(config["capabilities"], warnings) if config["capabilities"]
74
76
  validate_default_options(config["default_options"], warnings) if config["default_options"]
@@ -147,6 +149,57 @@ module Ace
147
149
  end
148
150
  end
149
151
 
152
+ def validate_limits(limits, errors, warnings)
153
+ unless limits.is_a?(Hash)
154
+ errors << "Limits must be a Hash, got #{limits.class}"
155
+ return
156
+ end
157
+
158
+ valid_limit_keys = %w[default models]
159
+ unknown_limit_keys = limits.keys - valid_limit_keys
160
+ unless unknown_limit_keys.empty?
161
+ warnings << "Unknown limits keys: #{unknown_limit_keys.join(", ")}"
162
+ end
163
+
164
+ validate_limit_entry(limits["default"], "limits.default", errors) if limits["default"]
165
+
166
+ return unless limits["models"]
167
+
168
+ unless limits["models"].is_a?(Hash)
169
+ errors << "limits.models must be a Hash, got #{limits["models"].class}"
170
+ return
171
+ end
172
+
173
+ limits["models"].each do |model_name, entry|
174
+ if model_name.to_s.strip.empty?
175
+ errors << "limits.models entries must have non-empty model names"
176
+ next
177
+ end
178
+
179
+ validate_limit_entry(entry, "limits.models.#{model_name}", errors)
180
+ end
181
+ end
182
+
183
+ def validate_limit_entry(entry, path, errors)
184
+ unless entry.is_a?(Hash)
185
+ errors << "#{path} must be a Hash, got #{entry.class}"
186
+ return
187
+ end
188
+
189
+ valid_keys = %w[context output]
190
+ unknown_keys = entry.keys - valid_keys
191
+ unless unknown_keys.empty?
192
+ errors << "#{path} contains unknown keys: #{unknown_keys.join(", ")}"
193
+ end
194
+
195
+ entry.each do |field, value|
196
+ next unless valid_keys.include?(field)
197
+ next if value.is_a?(Integer) && value >= 0
198
+
199
+ errors << "#{path}.#{field} must be a non-negative Integer"
200
+ end
201
+ end
202
+
150
203
  # Validate API key configuration
151
204
  def validate_api_key(api_key_config, errors, warnings)
152
205
  if api_key_config.is_a?(Hash)
@@ -33,6 +33,8 @@ module Ace
33
33
  option :prompt, type: :string, desc: "Prompt text (overrides positional PROMPT)"
34
34
  option :force, type: :boolean, default: false, desc: "Force overwrite existing files"
35
35
  option :interactive, type: :boolean, default: false, desc: "Start an interactive CLI session instead of one-shot query"
36
+ option :fallback, type: :boolean, default: true, desc: "Use provider fallback for this query"
37
+ option :json, type: :boolean, default: false, desc: "Output command results as JSON"
36
38
 
37
39
  option :version, type: :boolean, desc: "Show version information"
38
40
  option :list_providers, type: :boolean, desc: "List available LLM providers"
@@ -55,6 +57,7 @@ module Ace
55
57
  end
56
58
 
57
59
  @prompt = options[:prompt] || prompt_text
60
+ options[:format] ||= "json" if options[:json]
58
61
 
59
62
  return show_help if @provider_model.nil? && @prompt.nil?
60
63
  return show_provider_help if @prompt.nil? || @prompt.empty?
@@ -90,6 +93,8 @@ module Ace
90
93
  puts " --model MODEL Model name (overrides PROVIDER[:MODEL])"
91
94
  puts " --prompt PROMPT Prompt text (overrides positional PROMPT)"
92
95
  puts " --interactive Start an interactive CLI session"
96
+ puts " --no-fallback Disable provider fallback for this query"
97
+ puts " --json Output command results as JSON"
93
98
  puts " --force Force overwrite existing files"
94
99
  puts " -q, --quiet Suppress config summary output"
95
100
  puts " -d, --debug Enable debug output"
@@ -104,6 +109,7 @@ module Ace
104
109
  puts ' ace-llm claude:sonnet "Hi" --cli-args "dangerously-skip-permissions"'
105
110
  puts ' ace-llm claude:sonnet "Hi" --cli-args "--model=claude-sonnet-4-0 --verbose"'
106
111
  puts ' ace-llm codex:gpt@yolo "/as-assign-drive abc123@010" --interactive'
112
+ puts ' ace-llm gemini:pro "ping" --no-fallback --timeout 15 --max-tokens 4'
107
113
  puts ""
108
114
  puts "Provider Aliases:"
109
115
  puts " Short aliases for common provider:MODEL combinations:"
@@ -136,7 +142,7 @@ module Ace
136
142
  return if quiet?(options)
137
143
 
138
144
  require "ace/core"
139
- summary_keys = %w[provider_model preset temperature max_tokens format timeout system_append cli_args]
145
+ summary_keys = %w[provider_model preset temperature max_tokens format timeout system_append cli_args fallback]
140
146
  Ace::Core::Atoms::ConfigSummary.display(
141
147
  command: "query",
142
148
  config: options.merge(provider_model: @provider_model),
@@ -183,7 +189,8 @@ module Ace
183
189
  model: resolved_model_override,
184
190
  cli_args: options[:cli_args],
185
191
  system_append: system_append_text,
186
- preset: options[:preset]
192
+ preset: options[:preset],
193
+ fallback: (options[:fallback] == false ? false : nil)
187
194
  )
188
195
 
189
196
  output_response(response, options)
@@ -207,6 +214,7 @@ module Ace
207
214
  conflicts << "--max-tokens" unless options[:max_tokens].nil?
208
215
  explicit_format = options[:format]
209
216
  conflicts << "--format" if explicit_format && explicit_format != "text"
217
+ conflicts << "--no-fallback" if options[:fallback] == false
210
218
  return if conflicts.empty?
211
219
 
212
220
  raise Ace::Support::Cli::Error, "Interactive mode does not support #{conflicts.join(', ')}"
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/models"
4
+ require_relative "client_registry"
5
+ require_relative "provider_model_parser"
6
+
7
+ module Ace
8
+ module LLM
9
+ module Molecules
10
+ # Resolves model context/output limits against the expanded concrete target.
11
+ class ModelLimitResolver
12
+ DEFAULT_CONTEXT_LIMIT = 200_000
13
+
14
+ ResolveResult = Struct.new(
15
+ :provider,
16
+ :model,
17
+ :context_limit,
18
+ :output_limit,
19
+ :source,
20
+ :original_target,
21
+ keyword_init: true
22
+ ) do
23
+ def full_model
24
+ return nil if provider.to_s.empty? || model.to_s.empty?
25
+
26
+ "#{provider}:#{model}"
27
+ end
28
+ end
29
+
30
+ def self.resolve(target, **kwargs)
31
+ new(**kwargs).resolve(target)
32
+ end
33
+
34
+ def initialize(registry: nil, parser: nil)
35
+ @registry = registry || ClientRegistry.new
36
+ @parser = parser || ProviderModelParser.new(registry: @registry)
37
+ end
38
+
39
+ def resolve(target)
40
+ original_target = target.to_s
41
+ return fallback_result(original_target) if original_target.strip.empty?
42
+
43
+ parsed = @parser.parse(original_target)
44
+ return fallback_result(original_target) if parsed.invalid?
45
+
46
+ provider_config = @registry.get_provider(parsed.provider)
47
+ return fallback_result(original_target, provider: parsed.provider, model: parsed.model) unless provider_config
48
+
49
+ limits = Ace::Support::Models::Atoms::ProviderConfigReader.extract_limits(provider_config)
50
+ default_limits = limits["default"] || {}
51
+ model_limits = limits.dig("models", parsed.model) || {}
52
+ merged_limits = default_limits.merge(model_limits)
53
+
54
+ ResolveResult.new(
55
+ provider: parsed.provider,
56
+ model: parsed.model,
57
+ context_limit: merged_limits["context"] || DEFAULT_CONTEXT_LIMIT,
58
+ output_limit: merged_limits["output"],
59
+ source: limit_source(default_limits, model_limits),
60
+ original_target: original_target
61
+ )
62
+ rescue
63
+ fallback_result(original_target)
64
+ end
65
+
66
+ private
67
+
68
+ def limit_source(default_limits, model_limits)
69
+ return :model_override if model_limits.any?
70
+ return :provider_default if default_limits.any?
71
+
72
+ :fallback
73
+ end
74
+
75
+ def fallback_result(original_target, provider: nil, model: nil)
76
+ ResolveResult.new(
77
+ provider: provider,
78
+ model: model,
79
+ context_limit: DEFAULT_CONTEXT_LIMIT,
80
+ output_limit: nil,
81
+ source: :fallback,
82
+ original_target: original_target
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module LLM
5
- VERSION = '0.36.3'
5
+ VERSION = '0.38.2'
6
6
  end
7
7
  end
data/lib/ace/llm.rb CHANGED
@@ -25,6 +25,7 @@ require_relative "llm/molecules/file_io_handler"
25
25
  require_relative "llm/molecules/llm_alias_resolver"
26
26
  require_relative "llm/molecules/role_resolver"
27
27
  require_relative "llm/molecules/provider_model_parser"
28
+ require_relative "llm/molecules/model_limit_resolver"
28
29
  require_relative "llm/molecules/format_handlers"
29
30
  require_relative "llm/molecules/client_registry"
30
31
  require_relative "llm/molecules/fallback_orchestrator"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.36.3
4
+ version: 0.38.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-20 00:00:00.000000000 Z
10
+ date: 2026-04-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-cli
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.11'
60
+ version: '0.12'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.11'
67
+ version: '0.12'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: faraday
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -289,6 +289,7 @@ files:
289
289
  - lib/ace/llm/molecules/format_handlers.rb
290
290
  - lib/ace/llm/molecules/interactive_command_builder.rb
291
291
  - lib/ace/llm/molecules/llm_alias_resolver.rb
292
+ - lib/ace/llm/molecules/model_limit_resolver.rb
292
293
  - lib/ace/llm/molecules/openai_compatible_params.rb
293
294
  - lib/ace/llm/molecules/preset_loader.rb
294
295
  - lib/ace/llm/molecules/provider_loader.rb