ace-llm 0.39.2 → 0.41.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb59cc8e89a88f8c35c88148d6364c109cff56aa65393a8c6998636cfe3217f8
4
- data.tar.gz: 1f0850c65fcafd1bdda99e7e6c61f99267cd32a8414d7842ce315078a1ed5983
3
+ metadata.gz: 2fae0fd9e7709654101b452e68a39a32412c1788948f60ff6b3ea619ca4877d8
4
+ data.tar.gz: c5ecb76fe5e258e27e5c71ddd8d0f760637ca7a0f67766a7239fe2e60acf3dbf
5
5
  SHA512:
6
- metadata.gz: 9c71aed42c8a25be6d78cb5da76d61b833632767165b65c51249509667fd9d1bd7a5fd3be46e8d16f3f7b1e11715dd0b2654bbe74565dfefcda13f4d5bddc8f2
7
- data.tar.gz: 00bf1c6a53d6268a90613953c3434768ea82421b73e0bdf9814e4ab00cc885a59e7749c1efaa104bcde825bda577356fcc8dec2e0c073a87c010c5c34ac745f0
6
+ metadata.gz: a885b5933135a969eb4f91a4e013af1e9a35b1e79450c8f47b8ce2b9afa372226c482c1023bab1033438a04b2fc95fbc6ccf4f8592e5b5ce6b763d0820bfb429
7
+ data.tar.gz: b378d04c4280ede3e36d60b01fbe7ee821f28597ac0c9074745961126d642db7e822af19c44f0362d15eaf5067e3ed6b4ee9cf9a2779abf15652e979f9313114
@@ -1,5 +1,4 @@
1
1
  timeout: 600
2
2
  cli_args:
3
- - --full-auto
4
3
  - --sandbox
5
4
  - read-only
@@ -1,3 +1,4 @@
1
1
  timeout: 600
2
2
  cli_args:
3
- - --full-auto
3
+ - --sandbox
4
+ - workspace-write
@@ -1 +1,5 @@
1
1
  timeout: 600
2
+ cli_args:
3
+ - --tools
4
+ - read,grep,find,ls
5
+ - --no-extensions
@@ -0,0 +1,3 @@
1
+ cli_args:
2
+ - --thinking
3
+ - high
@@ -0,0 +1,3 @@
1
+ cli_args:
2
+ - --thinking
3
+ - low
@@ -0,0 +1,3 @@
1
+ cli_args:
2
+ - --thinking
3
+ - max
@@ -0,0 +1,3 @@
1
+ cli_args:
2
+ - --thinking
3
+ - medium
@@ -0,0 +1,3 @@
1
+ cli_args:
2
+ - --thinking
3
+ - xhigh
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.41.0] - 2026-09-27
11
+
12
+ ### Changed
13
+
14
+ - Query results now derive the top-level `usage` record from provider metadata token counts (model, input/output/cached/total). Absent counts stay absent; a measured zero cache read is kept so cost models can distinguish "no cache" from "unknown". Sessions no longer report "usage unavailable" when the provider supplied tokens.
15
+
16
+ ## [0.40.0] - 2026-09-24
17
+
18
+ ### Fixed
19
+
20
+ - Update Codex CLI presets for the new `codex exec` interface: replace the
21
+ removed `--full-auto` flag with explicit `--sandbox read-only` (ro) and
22
+ `--sandbox workspace-write` (rw); `yolo` keeps
23
+ `--dangerously-bypass-approvals-and-sandbox`.
24
+ - Preserve resolved provider, model, preset and reasoning metadata for the executed request, including fallback targets.
25
+ - Accept `max` reasoning in selectors and Pi reasoning configuration.
26
+
10
27
  ## [0.39.2] - 2026-09-13
11
28
 
12
29
  ### Fixed
data/README.md CHANGED
@@ -35,7 +35,7 @@ Deterministic coverage lives in `test/fast/` and `test/feat/`. Scenario assets s
35
35
 
36
36
  ## How It Works
37
37
 
38
- 1. Select a model -- by alias, `provider:model`, with a thinking level suffix (`:low`/`:medium`/`:high`), or an `@preset` -- and submit a prompt.
38
+ 1. Select a model -- by alias, `provider:model`, with a configured thinking level suffix (`:low`/`:medium`/`:high`/`:xhigh`/`:max`), or an `@preset` -- and submit a prompt.
39
39
  2. The provider router resolves the target through [ace-llm-providers-cli](../ace-llm-providers-cli) adapters, applying fallback and retry rules from the [config cascade](../ace-support-config).
40
40
  3. The response is returned as text, markdown, or JSON with optional token usage metadata.
41
41
 
@@ -43,7 +43,7 @@ Deterministic coverage lives in `test/fast/` and `test/feat/`. Scenario assets s
43
43
 
44
44
  **Switch providers with aliases** - use short names like `gflash`, `sonnet`, `opus` instead of full `provider:model` notation. Aliases resolve through versioned YAML in [`.ace-defaults/`](docs/usage.md).
45
45
 
46
- **Control reasoning depth** - append a thinking level (`codex:gpt:high`, `claude:sonnet:low`) to tune reasoning budgets. Supported CLI providers: `claude`, `codex` (levels: `low`, `medium`, `high`, `xhigh`).
46
+ **Control reasoning depth** - append a thinking level (`codex:gpt:high`, `claude:sonnet:low`, `pi:glm5:max`) to tune reasoning budgets. Supported CLI providers: `claude`, `codex` (`low`, `medium`, `high`, `xhigh`) and `pi` (including `max`). Each provider/level pair requires a matching thinking configuration.
47
47
 
48
48
  **Run preset-driven prompts** - apply execution profiles with `@preset` or `--preset`. Built-in presets for CLI providers: `@ro` (read-only), `@rw` (read-write), `@yolo` (full autonomy). Supported by: `claude`, `codex`, `gemini`, `opencode`, `pi`, `agy`.
49
49
 
@@ -33,6 +33,7 @@ module Ace
33
33
  # @raise [Error] If all providers and retries exhausted
34
34
  def execute(primary_provider:, registry:)
35
35
  @start_time = Time.now
36
+ @primary_provider = primary_provider
36
37
  @visited_providers.clear
37
38
 
38
39
  # If fallback disabled, just execute with primary
@@ -84,6 +85,7 @@ module Ace
84
85
  client = get_client(provider_name, registry)
85
86
  return yield client, provider_name
86
87
  rescue => error
88
+ raise if error.is_a?(Ace::LLM::ConfigurationError) && provider_name != @primary_provider
87
89
  last_error = error
88
90
 
89
91
  # Handle the error - returns :retry or :stop_and_fallback
@@ -10,7 +10,7 @@ module Ace
10
10
  # ProviderModelParser handles parsing and validation of provider:model syntax
11
11
  # for the unified LLM query interface.
12
12
  class ProviderModelParser
13
- THINKING_LEVELS = %w[low medium high xhigh].freeze
13
+ THINKING_LEVELS = %w[low medium high xhigh max].freeze
14
14
 
15
15
  # Result object for parsed provider:model combinations.
16
16
  ParseResult = Struct.new(:provider, :model, :preset, :thinking_level, :valid, :error,
@@ -8,7 +8,7 @@ module Ace
8
8
  module Molecules
9
9
  # RoleResolver maps role names to an available concrete selector.
10
10
  class RoleResolver
11
- THINKING_LEVELS = %w[low medium high xhigh].freeze
11
+ THINKING_LEVELS = %w[low medium high xhigh max].freeze
12
12
 
13
13
  def initialize(registry: nil, configuration: nil)
14
14
  @registry = registry || ClientRegistry.new
@@ -8,7 +8,7 @@ module Ace
8
8
  module Molecules
9
9
  # Loads provider-scoped thinking-level overrides from llm/thinking/*.yml.
10
10
  class ThinkingLevelLoader
11
- ALLOWED_LEVELS = %w[low medium high xhigh].freeze
11
+ ALLOWED_LEVELS = %w[low medium high xhigh max].freeze
12
12
 
13
13
  class << self
14
14
  def load_for_provider(provider, level)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "shellwords"
4
+
3
5
  require_relative "molecules/client_registry"
4
6
  require_relative "molecules/provider_model_parser"
5
7
  require_relative "molecules/preset_loader"
@@ -145,11 +147,13 @@ module Ace
145
147
 
146
148
  result = {
147
149
  text: text_content,
148
- model: final_model,
149
- provider: parse_result.provider,
150
- preset: resolved_preset,
151
- thinking_level: parse_result.thinking_level,
152
- usage: response[:usage],
150
+ model: response.dig(:execution, :model) || final_model,
151
+ provider: response.dig(:execution, :provider) || parse_result.provider,
152
+ preset: response.fetch(:execution, {}).fetch(:preset, resolved_preset),
153
+ thinking_level: response.fetch(:execution, {}).fetch(:thinking_level, parse_result.thinking_level),
154
+ execution: response[:execution],
155
+ requested_selector: provider_model,
156
+ usage: build_usage(response[:metadata]),
153
157
  metadata: response[:metadata]
154
158
  }
155
159
 
@@ -178,6 +182,27 @@ module Ace
178
182
 
179
183
  private
180
184
 
185
+ # Derive the measured usage record from client metadata. Providers hand
186
+ # token counts to session recording through metadata keys; absent counts
187
+ # stay absent (never zero) so cost models can tell "no cache" from
188
+ # "unknown". Returns nil when the client reported no token counts at all.
189
+ def self.build_usage(metadata)
190
+ meta = metadata.is_a?(Hash) ? metadata : {}
191
+ input = meta[:input_tokens] || meta["input_tokens"]
192
+ output = meta[:output_tokens] || meta["output_tokens"]
193
+ return nil if input.nil? && output.nil?
194
+
195
+ usage = {model: meta[:model] || meta["model"]}
196
+ usage[:input_tokens] = input unless input.nil?
197
+ usage[:output_tokens] = output unless output.nil?
198
+ cached = meta[:cached_tokens] || meta["cached_tokens"]
199
+ usage[:cached_tokens] = cached unless cached.nil?
200
+ total = meta[:total_tokens] || meta["total_tokens"]
201
+ usage[:total_tokens] = total.nil? ? (input.to_i + output.to_i) : total
202
+ usage
203
+ end
204
+ private_class_method :build_usage
205
+
181
206
  def self.resolve_preset_name(suffix_preset, explicit_preset)
182
207
  suffix = suffix_preset&.to_s&.strip
183
208
  explicit = explicit_preset&.to_s&.strip
@@ -364,7 +389,8 @@ module Ace
364
389
  registry:, fallback_config:, timeout:, debug:, role_fallbacks: nil, preset: nil, thinking_level: nil, option_builder:)
365
390
  if fallback_config.disabled?
366
391
  client = registry.get_client(provider, model: model, timeout: timeout)
367
- return client.generate(messages, **generation_opts)
392
+ response = client.generate(messages, **generation_opts)
393
+ return response.merge(execution: execution_identity(provider, model, preset, thinking_level, response, generation_opts))
368
394
  end
369
395
 
370
396
  primary_provider_string = model ? "#{provider}:#{model}" : provider
@@ -391,9 +417,60 @@ module Ace
391
417
  else
392
418
  option_builder.call(target_selector)
393
419
  end
394
- client.generate(messages, **opts)
420
+ parsed = Molecules::ProviderModelParser.new(registry: registry).parse(target_selector)
421
+ raise ConfigurationError, parsed.error unless parsed.valid?
422
+ primary = target_selector.to_s == primary_provider_string
423
+ response = client.generate(messages, **opts)
424
+ response.merge(execution: execution_identity(
425
+ parsed.provider, parsed.model,
426
+ primary ? preset : (parsed.preset || preset),
427
+ primary ? thinking_level : parsed.thinking_level,
428
+ response,
429
+ opts
430
+ ))
431
+ end
432
+ end
433
+
434
+ # Transport identity records what ACE executed, not a provider attestation.
435
+ def self.execution_identity(provider, model, preset, thinking_level, response, options)
436
+ model ||= response.dig(:metadata, :model) || response.dig(:metadata, "model")
437
+ finish_reason = response.dig(:metadata, :finish_reason) || response.dig(:metadata, "finish_reason") ||
438
+ response.dig(:metadata, :stop_reason) || response.dig(:metadata, "stop_reason")
439
+ status = if Ace::LLM::SUCCESSFUL_FINISH_REASONS.include?(finish_reason.to_s.downcase)
440
+ "succeeded"
441
+ else
442
+ "incomplete"
443
+ end
444
+ {provider: provider, model: model, preset: preset, thinking_level: effective_thinking_level(provider, thinking_level, options),
445
+ identity_source: "resolved_request", status: status}
446
+ end
447
+ private_class_method :execution_identity
448
+
449
+ def self.effective_thinking_level(provider, default, options)
450
+ return default unless %w[pi codex].include?(provider)
451
+
452
+ args = Array(options[:cli_args]).flat_map { |arg| Shellwords.split(arg.to_s) }
453
+ effective = default
454
+ args.each_with_index do |arg, index|
455
+ if provider == "pi"
456
+ candidate = if arg == "--thinking"
457
+ args[index + 1]
458
+ elsif arg.start_with?("--thinking=")
459
+ arg.delete_prefix("--thinking=")
460
+ end
461
+ effective = candidate if %w[off minimal low medium high xhigh max].include?(candidate)
462
+ else
463
+ config = if %w[-c --config].include?(arg)
464
+ args[index + 1]
465
+ elsif arg.match?(/\A(?:-c|--config)=?model_reasoning_effort=/)
466
+ arg.sub(/\A(?:-c|--config)=?/, "")
467
+ end
468
+ effective = config.split("=", 2).last if config&.start_with?("model_reasoning_effort=")
469
+ end
395
470
  end
471
+ effective
396
472
  end
473
+ private_class_method :effective_thinking_level
397
474
 
398
475
  def self.extract_text_content(response)
399
476
  if response[:text]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module LLM
5
- VERSION = "0.39.2"
5
+ VERSION = "0.41.0"
6
6
  end
7
7
  end
data/lib/ace/llm.rb CHANGED
@@ -55,6 +55,8 @@ module Ace
55
55
  class ConfigurationError < Error; end
56
56
  class AuthenticationError < Error; end
57
57
 
58
+ SUCCESSFUL_FINISH_REASONS = %w[success stop end_turn complete completed stop_sequence].freeze
59
+
58
60
  # Define module namespaces
59
61
  module Atoms; end
60
62
  module Molecules; end
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.39.2
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-13 00:00:00.000000000 Z
10
+ date: 2026-09-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-cli
@@ -266,6 +266,11 @@ files:
266
266
  - ".ace-defaults/llm/thinking/codex/low.yml"
267
267
  - ".ace-defaults/llm/thinking/codex/medium.yml"
268
268
  - ".ace-defaults/llm/thinking/codex/xhigh.yml"
269
+ - ".ace-defaults/llm/thinking/pi/high.yml"
270
+ - ".ace-defaults/llm/thinking/pi/low.yml"
271
+ - ".ace-defaults/llm/thinking/pi/max.yml"
272
+ - ".ace-defaults/llm/thinking/pi/medium.yml"
273
+ - ".ace-defaults/llm/thinking/pi/xhigh.yml"
269
274
  - ".ace-defaults/nav/protocols/guide-sources/ace-llm.yml"
270
275
  - CHANGELOG.md
271
276
  - LICENSE