ace-llm 0.32.1 → 0.36.3
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/.ace-defaults/llm/config.yml +1 -1
- data/CHANGELOG.md +105 -0
- data/README.md +17 -2
- data/lib/ace/llm/cli/commands/query.rb +46 -5
- data/lib/ace/llm/molecules/client_registry.rb +2 -1
- data/lib/ace/llm/molecules/fallback_orchestrator.rb +14 -8
- data/lib/ace/llm/molecules/interactive_command_builder.rb +64 -0
- data/lib/ace/llm/molecules/role_resolver.rb +0 -1
- data/lib/ace/llm/query_interface.rb +106 -33
- data/lib/ace/llm/version.rb +1 -1
- data/lib/ace/llm.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d92ed4d8cab8d260ef4235ecf94f612af751abbf4d1f4a4cdd30951d8c33388
|
|
4
|
+
data.tar.gz: dd6ff444bed65aa46801197857f39c008087a41da8e01c6cafcfc3c88d781a43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66a5720b86973458a565c63fb36a37061d1e201b1f0be9b43cc3d6f4ecedad6785117d96ec8a53819ab35aacc70246c68780af5d82c24f8c52067a94b1714b69
|
|
7
|
+
data.tar.gz: b107b41c5221dcfda405a2073996184edf6f52b075a97f90c753b36eb283ce07403942c96e8f3116dfc2cbab3d58faa61c2630b378718c5a31aaf1c5ec8db304
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.36.3] - 2026-04-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Fixed `ace-llm --interactive` startup for Codex aliases so the `/as-onboard` and related interactive alias prompts are launched with a valid subprocess command shape instead of aborting with a missing keyword.
|
|
14
|
+
|
|
15
|
+
## [0.36.2] - 2026-04-19
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Refreshed Codex model examples in README, usage, getting-started, demo, and help output from stale `codex:gpt-5` forms to the current stable `codex:gpt` alias and updated current provider alias guidance.
|
|
19
|
+
|
|
20
|
+
## [0.36.1] - 2026-04-16
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Switched the fresh-default `commit` role to prefer `google:lite` before CLI fallbacks so new `ace-config init` projects keep generated commit messages working when stale CLI aliases or unavailable local CLIs would otherwise break message generation.
|
|
24
|
+
|
|
25
|
+
## [0.36.0] - 2026-04-15
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Reworked `TS-LLM-001` to public-surface goal style by tightening model-routing evidence checks, adding `TC-003` output-file-contract coverage, and prioritizing end-state/output artifacts over helper-only signals.
|
|
29
|
+
- Added `TS-LLM-002` provider-discovery coverage for `ace-llm --list-providers` so public setup-hint and provider-list output behavior is verified as a first-class E2E journey.
|
|
30
|
+
|
|
31
|
+
### Technical
|
|
32
|
+
- Updated runtime dependency constraint to `ace-support-models ~> 0.11` after the coordinated support-models minor release.
|
|
33
|
+
|
|
34
|
+
## [0.35.1] - 2026-04-15
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Updated the `cli_args` threading feature tests to match the normalized array contract and stub provider preset loading directly, so `ace-test ace-llm` no longer fails on stale scalar expectations or package-local preset lookup assumptions.
|
|
38
|
+
|
|
39
|
+
## [0.35.0] - 2026-04-15
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Merged preset CLI args with explicit `--cli-args` so interactive launches keep provider autonomy presets such as `@yolo` while still accepting additional flags like `--no-alt-screen`.
|
|
43
|
+
|
|
44
|
+
### Technical
|
|
45
|
+
- Threaded the current working directory and subprocess environment explicitly through `ace-llm --interactive` so provider-side startup policy hooks can make launch decisions from the real execution context.
|
|
46
|
+
|
|
47
|
+
## [0.34.0] - 2026-04-15
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Added `ace-llm --interactive` for supported CLI-backed providers so alias resolution, presets, thinking levels, and canonical skill handoff translation can launch the provider's native interactive terminal UI in one call.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- Documented interactive launch usage in the README, getting-started guide, and usage guide, including the tmux fork handoff pattern built around `/as-assign-drive <assignment>@<root>`.
|
|
54
|
+
|
|
55
|
+
## [0.33.6] - 2026-04-13
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- Switched `TS-LLM-001` basic-query coverage to the explicit `--prompt` form so the scenario executes with canonical prompt input and no longer misclassifies dropped positional input as provider behavior.
|
|
59
|
+
|
|
60
|
+
## [0.33.5] - 2026-04-13
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Hardened `TS-LLM-001` basic-query execution by persisting the exact prompt and command artifacts so empty-input/provider-contract failures are distinguished from valid model responses.
|
|
64
|
+
|
|
65
|
+
## [0.33.4] - 2026-04-13
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Added the missing `ace-support-models` runtime dependency so `ace-llm --list-providers` no longer fails on fresh setups with `cannot load such file -- ace/support/models`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- Updated quick-start onboarding guidance to include `ace-llm` in the base install command used before provider verification.
|
|
72
|
+
|
|
73
|
+
## [0.33.3] - 2026-04-13
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- Added `codex:mini` as a fallback model for the `commit` role after `glite`, so role-based commit-message generation tries a secondary model instead of failing when `glite` is unavailable.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### Technical
|
|
80
|
+
- Tightened the package version contract test to require a full semantic-version match instead of accepting partial matches.
|
|
81
|
+
## [0.33.2] - 2026-04-13
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
- Fixed provider credential availability checks so role-based model selection no longer crashes when a provider relies on fallback environment keys.
|
|
85
|
+
|
|
86
|
+
### Technical
|
|
87
|
+
- Added regression coverage for `ClientRegistry#provider_api_key_present?` using Google fallback API key resolution.
|
|
88
|
+
|
|
89
|
+
## [0.33.1] - 2026-04-13
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
- Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
|
|
93
|
+
|
|
94
|
+
### Technical
|
|
95
|
+
- Included in the coordinated assignment-driven patch release for batch i05 package updates.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## [0.33.0] - 2026-04-12
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
- Migrated package tests to the restarted `fast` / `feat` / `e2e` contract:
|
|
102
|
+
- moved deterministic test suites from legacy top-level folders into `test/fast/`
|
|
103
|
+
- moved legacy `test/integration/` coverage into `test/feat/`
|
|
104
|
+
- reduced `TS-LLM-001` E2E scenario from 3 to 2 TCs and added an E2E decision record
|
|
105
|
+
- Updated README and package docs to teach only `ace-test ace-llm`, `ace-test ace-llm feat`, `ace-test ace-llm all`, and `ace-test-e2e ace-llm`.
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- Preserved full fallback target selectors (including provider/model + suffix context) across fallback attempts and rebuilt generation options per target instead of reusing stale primary-provider options.
|
|
109
|
+
|
|
110
|
+
## [0.32.2] - 2026-04-10
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
- Updated the shared role catalog for restarted E2E execution with explicit `e2e-runner`, `e2e-verifier`, and `e2e-reporter` roles in the project-owned LLM configuration surface.
|
|
114
|
+
|
|
10
115
|
## [0.32.1] - 2026-04-01
|
|
11
116
|
|
|
12
117
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,7 +18,20 @@
|
|
|
18
18
|
|
|
19
19
|

|
|
20
20
|
|
|
21
|
-
`ace-llm` gives developers and coding agents one command surface for querying any LLM provider. Address models by alias (`gflash`, `sonnet`), explicit `provider:model` notation, or with thinking levels (`codex:gpt
|
|
21
|
+
`ace-llm` gives developers and coding agents one command surface for querying any LLM provider. Address models by alias (`gflash`, `sonnet`), explicit `provider:model` notation, or with thinking levels (`codex:gpt:high`) and execution presets (`cc@ro`). Pass prompts and system instructions inline or as file paths, or start supported CLI providers in native interactive mode with `--interactive`. Fallback routing and retry behavior keep prompt workflows resilient.
|
|
22
|
+
|
|
23
|
+
## Testing
|
|
24
|
+
|
|
25
|
+
Package-local verification contract:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ace-test ace-llm
|
|
29
|
+
ace-test ace-llm feat
|
|
30
|
+
ace-test ace-llm all
|
|
31
|
+
ace-test-e2e ace-llm
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Deterministic coverage lives in `test/fast/` and `test/feat/`. Scenario assets stay in `test/e2e/`.
|
|
22
35
|
|
|
23
36
|
## How It Works
|
|
24
37
|
|
|
@@ -30,10 +43,12 @@
|
|
|
30
43
|
|
|
31
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).
|
|
32
45
|
|
|
33
|
-
**Control reasoning depth** - append a thinking level (`codex:gpt
|
|
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`).
|
|
34
47
|
|
|
35
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`.
|
|
36
49
|
|
|
50
|
+
**Launch real interactive agents** - use `--interactive` for supported CLI providers so alias resolution, presets, and skill translation still flow through `ace-llm`, but the provider starts its native terminal UI instead of one-shot print mode.
|
|
51
|
+
|
|
37
52
|
**Build resilient prompt workflows** - configure fallback chains and retry behavior through the [config cascade](.ace-defaults/llm/config.yml) so transient provider issues do not block work.
|
|
38
53
|
|
|
39
54
|
**Power LLM-enhanced flows in sibling packages** - serve as the execution backend for [ace-git-commit](../ace-git-commit), [ace-idea](../ace-idea), [ace-review](../ace-review), [ace-sim](../ace-sim), [ace-prompt-prep](../ace-prompt-prep), and more.
|
|
@@ -32,6 +32,7 @@ module Ace
|
|
|
32
32
|
option :model, type: :string, desc: "Model name (overrides PROVIDER[:MODEL])"
|
|
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
|
+
option :interactive, type: :boolean, default: false, desc: "Start an interactive CLI session instead of one-shot query"
|
|
35
36
|
|
|
36
37
|
option :version, type: :boolean, desc: "Show version information"
|
|
37
38
|
option :list_providers, type: :boolean, desc: "List available LLM providers"
|
|
@@ -88,6 +89,7 @@ module Ace
|
|
|
88
89
|
puts " --timeout SECONDS Request timeout in seconds"
|
|
89
90
|
puts " --model MODEL Model name (overrides PROVIDER[:MODEL])"
|
|
90
91
|
puts " --prompt PROMPT Prompt text (overrides positional PROMPT)"
|
|
92
|
+
puts " --interactive Start an interactive CLI session"
|
|
91
93
|
puts " --force Force overwrite existing files"
|
|
92
94
|
puts " -q, --quiet Suppress config summary output"
|
|
93
95
|
puts " -d, --debug Enable debug output"
|
|
@@ -97,17 +99,19 @@ module Ace
|
|
|
97
99
|
puts ' ace-llm google:gemini-2.5-flash "What is Ruby?"'
|
|
98
100
|
puts ' ace-llm gflash "Quick question" # using alias'
|
|
99
101
|
puts ' ace-llm gflash@ro "Summarize this diff"'
|
|
100
|
-
puts ' ace-llm codex:gpt
|
|
102
|
+
puts ' ace-llm codex:gpt:high@ro "Review this code"'
|
|
101
103
|
puts ' ace-llm claude:sonnet "Summarize this diff" --preset rw'
|
|
102
104
|
puts ' ace-llm claude:sonnet "Hi" --cli-args "dangerously-skip-permissions"'
|
|
103
105
|
puts ' ace-llm claude:sonnet "Hi" --cli-args "--model=claude-sonnet-4-0 --verbose"'
|
|
106
|
+
puts ' ace-llm codex:gpt@yolo "/as-assign-drive abc123@010" --interactive'
|
|
104
107
|
puts ""
|
|
105
108
|
puts "Provider Aliases:"
|
|
106
109
|
puts " Short aliases for common provider:MODEL combinations:"
|
|
107
|
-
puts " gflash → google:gemini-
|
|
108
|
-
puts " glite → google:gemini-
|
|
109
|
-
puts "
|
|
110
|
-
puts "
|
|
110
|
+
puts " gflash → google:gemini-flash-latest"
|
|
111
|
+
puts " glite → google:gemini-flash-lite-latest"
|
|
112
|
+
puts " codex:gpt → codex:gpt-5.4"
|
|
113
|
+
puts " cc → claude:sonnet"
|
|
114
|
+
puts " sonnet → anthropic:claude-sonnet-4-5"
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
def show_provider_help
|
|
@@ -150,6 +154,23 @@ module Ace
|
|
|
150
154
|
system_append_text = options[:system_append] ? file_handler.read_content(options[:system_append]) : nil
|
|
151
155
|
normalized_timeout = normalize_timeout(options[:timeout])
|
|
152
156
|
|
|
157
|
+
if options[:interactive]
|
|
158
|
+
validate_interactive_options!(options)
|
|
159
|
+
invocation = Ace::LLM::Molecules::InteractiveCommandBuilder.new.build(
|
|
160
|
+
provider_model: @provider_model,
|
|
161
|
+
prompt: prompt_text,
|
|
162
|
+
system: system_text,
|
|
163
|
+
cli_args: options[:cli_args],
|
|
164
|
+
system_append: system_append_text,
|
|
165
|
+
preset: options[:preset],
|
|
166
|
+
model: (@model_from_option ? nil : options[:model]),
|
|
167
|
+
working_dir: Dir.pwd,
|
|
168
|
+
subprocess_env: ENV.to_h
|
|
169
|
+
)
|
|
170
|
+
execute_interactive_invocation(invocation)
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
|
|
153
174
|
resolved_model_override = @model_from_option ? nil : options[:model]
|
|
154
175
|
response = Ace::LLM::QueryInterface.query(
|
|
155
176
|
@provider_model,
|
|
@@ -178,6 +199,26 @@ module Ace
|
|
|
178
199
|
raise
|
|
179
200
|
end
|
|
180
201
|
|
|
202
|
+
def validate_interactive_options!(options)
|
|
203
|
+
conflicts = []
|
|
204
|
+
conflicts << "--output" if options[:output]
|
|
205
|
+
conflicts << "--force" if options[:force]
|
|
206
|
+
conflicts << "--temperature" unless options[:temperature].nil?
|
|
207
|
+
conflicts << "--max-tokens" unless options[:max_tokens].nil?
|
|
208
|
+
explicit_format = options[:format]
|
|
209
|
+
conflicts << "--format" if explicit_format && explicit_format != "text"
|
|
210
|
+
return if conflicts.empty?
|
|
211
|
+
|
|
212
|
+
raise Ace::Support::Cli::Error, "Interactive mode does not support #{conflicts.join(', ')}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def execute_interactive_invocation(invocation)
|
|
216
|
+
env = invocation[:env].respond_to?(:to_h) ? invocation[:env].to_h : {}
|
|
217
|
+
command = invocation[:command]
|
|
218
|
+
working_dir = invocation[:working_dir]
|
|
219
|
+
Kernel.exec(env, *command, chdir: working_dir)
|
|
220
|
+
end
|
|
221
|
+
|
|
181
222
|
def output_response(response, options)
|
|
182
223
|
format = options[:format] || "text"
|
|
183
224
|
handler = Ace::LLM::Molecules::FormatHandlers.get_handler(format)
|
|
@@ -142,7 +142,8 @@ module Ace
|
|
|
142
142
|
provider = get_provider(provider_name)
|
|
143
143
|
return false unless provider
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
normalized_name = normalize_provider_name(provider["name"] || provider_name)
|
|
146
|
+
credential_status(provider, normalized_name)[:present]
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
# Reload all configurations
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../atoms/error_classifier"
|
|
4
4
|
require_relative "../models/fallback_config"
|
|
5
|
+
require_relative "provider_model_parser"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module LLM
|
|
@@ -35,10 +36,12 @@ module Ace
|
|
|
35
36
|
@visited_providers.clear
|
|
36
37
|
|
|
37
38
|
# If fallback disabled, just execute with primary
|
|
38
|
-
return yield
|
|
39
|
+
return yield(get_client(primary_provider, registry), primary_provider) if @config.disabled?
|
|
39
40
|
|
|
40
41
|
# Try primary provider with retries
|
|
41
|
-
result = try_provider_with_retry(primary_provider, registry)
|
|
42
|
+
result = try_provider_with_retry(primary_provider, registry) do |client, provider_name|
|
|
43
|
+
yield client, provider_name
|
|
44
|
+
end
|
|
42
45
|
return result if result
|
|
43
46
|
|
|
44
47
|
# Try fallback providers in order (per-provider chain or default)
|
|
@@ -54,7 +57,9 @@ module Ace
|
|
|
54
57
|
|
|
55
58
|
report_status("ℹ Trying fallback provider #{fallback_provider}...")
|
|
56
59
|
|
|
57
|
-
result = try_provider_with_retry(fallback_provider, registry)
|
|
60
|
+
result = try_provider_with_retry(fallback_provider, registry) do |client, provider_name|
|
|
61
|
+
yield client, provider_name
|
|
62
|
+
end
|
|
58
63
|
return result if result
|
|
59
64
|
end
|
|
60
65
|
|
|
@@ -77,7 +82,7 @@ module Ace
|
|
|
77
82
|
|
|
78
83
|
loop do
|
|
79
84
|
client = get_client(provider_name, registry)
|
|
80
|
-
return yield client
|
|
85
|
+
return yield client, provider_name
|
|
81
86
|
rescue => error
|
|
82
87
|
last_error = error
|
|
83
88
|
|
|
@@ -145,10 +150,11 @@ module Ace
|
|
|
145
150
|
opts = {}
|
|
146
151
|
opts[:timeout] = @timeout if @timeout
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
parser = ProviderModelParser.new(registry: registry)
|
|
154
|
+
parsed = parser.parse(provider_name.to_s)
|
|
155
|
+
|
|
156
|
+
if parsed.valid?
|
|
157
|
+
registry.get_client(parsed.provider, model: parsed.model, **opts)
|
|
152
158
|
else
|
|
153
159
|
registry.get_client(provider_name, **opts)
|
|
154
160
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module LLM
|
|
5
|
+
module Molecules
|
|
6
|
+
class InteractiveCommandBuilder
|
|
7
|
+
def initialize(registry: ClientRegistry.new)
|
|
8
|
+
@registry = registry
|
|
9
|
+
@parser = ProviderModelParser.new(registry: registry)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build(provider_model:, prompt:, model: nil, preset: nil, system: nil, cli_args: nil,
|
|
13
|
+
system_append: nil, sandbox: nil, working_dir: nil, subprocess_env: nil)
|
|
14
|
+
parse_result = @parser.parse(provider_model)
|
|
15
|
+
raise Error, parse_result.error unless parse_result.valid?
|
|
16
|
+
|
|
17
|
+
resolved_preset = QueryInterface.send(:resolve_preset_name, parse_result.preset, preset)
|
|
18
|
+
final_model = model || parse_result.model
|
|
19
|
+
if final_model.nil? || final_model.empty?
|
|
20
|
+
raise Error, "No model specified and no default available for #{parse_result.provider}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
final_prompt = prompt.to_s
|
|
24
|
+
raise Error, "No prompt specified. Use positional prompt or --prompt." if final_prompt.empty?
|
|
25
|
+
|
|
26
|
+
generation_opts = QueryInterface.send(
|
|
27
|
+
:build_generation_opts,
|
|
28
|
+
provider: parse_result.provider,
|
|
29
|
+
preset: resolved_preset,
|
|
30
|
+
thinking_level: parse_result.thinking_level,
|
|
31
|
+
temperature: nil,
|
|
32
|
+
max_tokens: nil,
|
|
33
|
+
system_file: nil,
|
|
34
|
+
prompt_file: nil,
|
|
35
|
+
cli_args: cli_args,
|
|
36
|
+
system_append: system_append,
|
|
37
|
+
sandbox: sandbox,
|
|
38
|
+
working_dir: working_dir,
|
|
39
|
+
subprocess_env: subprocess_env,
|
|
40
|
+
subprocess_command_prefix: nil,
|
|
41
|
+
last_message_file: nil
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
client = @registry.get_client(parse_result.provider, model: final_model)
|
|
45
|
+
unless client.respond_to?(:interactive_supported?) && client.interactive_supported?
|
|
46
|
+
raise Error, "Provider '#{parse_result.provider}' does not support interactive mode"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
messages = []
|
|
50
|
+
messages << {role: "system", content: system} if system && !system.empty?
|
|
51
|
+
messages << {role: "user", content: final_prompt}
|
|
52
|
+
|
|
53
|
+
invocation = client.build_interactive_invocation(messages, **generation_opts)
|
|
54
|
+
invocation.merge(
|
|
55
|
+
provider: parse_result.provider,
|
|
56
|
+
model: final_model,
|
|
57
|
+
preset: resolved_preset,
|
|
58
|
+
thinking_level: parse_result.thinking_level
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -60,7 +60,6 @@ module Ace
|
|
|
60
60
|
return false if provider.nil? || provider.empty?
|
|
61
61
|
return false if @configuration.provider_inactive?(provider)
|
|
62
62
|
return false unless @registry.provider_available?(provider)
|
|
63
|
-
return false if @registry.provider_api_key_required?(provider) && !@registry.provider_api_key_present?(provider)
|
|
64
63
|
|
|
65
64
|
true
|
|
66
65
|
end
|
|
@@ -35,6 +35,7 @@ module Ace
|
|
|
35
35
|
sandbox: nil,
|
|
36
36
|
working_dir: nil,
|
|
37
37
|
subprocess_env: nil,
|
|
38
|
+
subprocess_command_prefix: nil,
|
|
38
39
|
last_message_file: nil)
|
|
39
40
|
registry = Molecules::ClientRegistry.new
|
|
40
41
|
parser = Molecules::ProviderModelParser.new(registry: registry)
|
|
@@ -43,12 +44,6 @@ module Ace
|
|
|
43
44
|
raise Error, parse_result.error unless parse_result.valid?
|
|
44
45
|
|
|
45
46
|
resolved_preset = resolve_preset_name(parse_result.preset, preset)
|
|
46
|
-
execution_overrides = load_execution_overrides(
|
|
47
|
-
provider: parse_result.provider,
|
|
48
|
-
preset: resolved_preset,
|
|
49
|
-
thinking_level: parse_result.thinking_level
|
|
50
|
-
)
|
|
51
|
-
|
|
52
47
|
final_model = model || parse_result.model
|
|
53
48
|
if final_model.nil? || final_model.empty?
|
|
54
49
|
raise Error, "No model specified and no default available for #{parse_result.provider}"
|
|
@@ -63,38 +58,41 @@ module Ace
|
|
|
63
58
|
messages << {role: "system", content: system} if system && !system.empty?
|
|
64
59
|
messages << {role: "user", content: final_prompt}
|
|
65
60
|
|
|
66
|
-
generation_opts =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
generation_opts = build_generation_opts(
|
|
62
|
+
provider: parse_result.provider,
|
|
63
|
+
preset: resolved_preset,
|
|
64
|
+
thinking_level: parse_result.thinking_level,
|
|
65
|
+
temperature: temperature,
|
|
66
|
+
max_tokens: max_tokens,
|
|
67
|
+
system_file: system_file,
|
|
68
|
+
prompt_file: prompt_file,
|
|
69
|
+
cli_args: cli_args,
|
|
70
|
+
system_append: system_append,
|
|
71
|
+
sandbox: sandbox,
|
|
72
|
+
working_dir: working_dir,
|
|
73
|
+
subprocess_env: subprocess_env,
|
|
74
|
+
subprocess_command_prefix: subprocess_command_prefix,
|
|
75
|
+
last_message_file: last_message_file
|
|
76
|
+
)
|
|
77
|
+
execution_overrides = load_execution_overrides(
|
|
78
|
+
provider: parse_result.provider,
|
|
79
|
+
preset: resolved_preset,
|
|
80
|
+
thinking_level: parse_result.thinking_level
|
|
81
|
+
)
|
|
85
82
|
|
|
86
83
|
if debug
|
|
87
84
|
warn "Provider: #{parse_result.provider}"
|
|
88
85
|
warn "Model: #{final_model}"
|
|
89
86
|
warn "Preset: #{resolved_preset}" if resolved_preset
|
|
90
87
|
warn "Thinking level: #{parse_result.thinking_level}" if parse_result.thinking_level
|
|
91
|
-
warn "Temperature: #{
|
|
92
|
-
warn "Max tokens: #{
|
|
88
|
+
warn "Temperature: #{generation_opts[:temperature]}" unless generation_opts[:temperature].nil?
|
|
89
|
+
warn "Max tokens: #{generation_opts[:max_tokens]}" unless generation_opts[:max_tokens].nil?
|
|
93
90
|
end
|
|
94
91
|
|
|
95
92
|
fallback_config = load_fallback_config(fallback, fallback_providers, parser: parser)
|
|
96
93
|
timeout_value = first_non_nil(timeout, execution_overrides["timeout"], Molecules::ConfigLoader.get("llm.timeout"), 120)
|
|
97
94
|
resolved_timeout = normalize_timeout(timeout_value)
|
|
95
|
+
parser_for_options = Molecules::ProviderModelParser.new(registry: registry)
|
|
98
96
|
|
|
99
97
|
response = execute_with_fallback(
|
|
100
98
|
provider: parse_result.provider,
|
|
@@ -105,7 +103,39 @@ module Ace
|
|
|
105
103
|
fallback_config: fallback_config,
|
|
106
104
|
timeout: resolved_timeout,
|
|
107
105
|
debug: debug,
|
|
108
|
-
role_fallbacks: parse_result.role_fallbacks
|
|
106
|
+
role_fallbacks: parse_result.role_fallbacks,
|
|
107
|
+
preset: resolved_preset,
|
|
108
|
+
thinking_level: parse_result.thinking_level,
|
|
109
|
+
option_builder: lambda { |target_selector|
|
|
110
|
+
parsed_target = parser_for_options.parse(target_selector.to_s)
|
|
111
|
+
target_provider = parsed_target.valid? ? parsed_target.provider : parse_result.provider
|
|
112
|
+
target_preset = if parsed_target.valid? && parsed_target.preset
|
|
113
|
+
parsed_target.preset
|
|
114
|
+
else
|
|
115
|
+
resolved_preset
|
|
116
|
+
end
|
|
117
|
+
target_thinking = if parsed_target.valid?
|
|
118
|
+
parsed_target.thinking_level
|
|
119
|
+
else
|
|
120
|
+
parse_result.thinking_level
|
|
121
|
+
end
|
|
122
|
+
build_generation_opts(
|
|
123
|
+
provider: target_provider,
|
|
124
|
+
preset: target_preset,
|
|
125
|
+
thinking_level: target_thinking,
|
|
126
|
+
temperature: temperature,
|
|
127
|
+
max_tokens: max_tokens,
|
|
128
|
+
system_file: system_file,
|
|
129
|
+
prompt_file: prompt_file,
|
|
130
|
+
cli_args: cli_args,
|
|
131
|
+
system_append: system_append,
|
|
132
|
+
sandbox: sandbox,
|
|
133
|
+
working_dir: working_dir,
|
|
134
|
+
subprocess_env: subprocess_env,
|
|
135
|
+
subprocess_command_prefix: subprocess_command_prefix,
|
|
136
|
+
last_message_file: last_message_file
|
|
137
|
+
)
|
|
138
|
+
}
|
|
109
139
|
)
|
|
110
140
|
|
|
111
141
|
text_content = extract_text_content(response)
|
|
@@ -174,6 +204,43 @@ module Ace
|
|
|
174
204
|
end
|
|
175
205
|
private_class_method :load_execution_overrides
|
|
176
206
|
|
|
207
|
+
def self.build_generation_opts(provider:, preset:, thinking_level:, temperature:, max_tokens:, system_file:,
|
|
208
|
+
prompt_file:, cli_args:, system_append:, sandbox:, working_dir:, subprocess_env:, subprocess_command_prefix:,
|
|
209
|
+
last_message_file:)
|
|
210
|
+
execution_overrides = load_execution_overrides(
|
|
211
|
+
provider: provider,
|
|
212
|
+
preset: preset,
|
|
213
|
+
thinking_level: thinking_level
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
generation_opts = {}
|
|
217
|
+
resolved_temperature = first_non_nil(temperature, execution_overrides["temperature"])
|
|
218
|
+
resolved_max_tokens = first_non_nil(max_tokens, execution_overrides["max_tokens"])
|
|
219
|
+
resolved_cli_args = append_cli_args(execution_overrides["cli_args"], cli_args)
|
|
220
|
+
resolved_system_append = first_non_empty(system_append, execution_overrides["system_append"])
|
|
221
|
+
resolved_sandbox = first_non_nil(sandbox, execution_overrides["sandbox"])
|
|
222
|
+
resolved_working_dir = first_non_nil(working_dir, execution_overrides["working_dir"])
|
|
223
|
+
resolved_subprocess_env = merge_hash_values(execution_overrides["subprocess_env"], subprocess_env)
|
|
224
|
+
resolved_subprocess_command_prefix = first_non_nil(
|
|
225
|
+
subprocess_command_prefix,
|
|
226
|
+
execution_overrides["subprocess_command_prefix"]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
generation_opts[:temperature] = resolved_temperature unless resolved_temperature.nil?
|
|
230
|
+
generation_opts[:max_tokens] = resolved_max_tokens unless resolved_max_tokens.nil?
|
|
231
|
+
generation_opts[:system_file] = system_file if system_file
|
|
232
|
+
generation_opts[:prompt_file] = prompt_file if prompt_file
|
|
233
|
+
generation_opts[:cli_args] = resolved_cli_args unless blank_value?(resolved_cli_args)
|
|
234
|
+
generation_opts[:system_append] = resolved_system_append unless blank_value?(resolved_system_append)
|
|
235
|
+
generation_opts[:sandbox] = resolved_sandbox if resolved_sandbox
|
|
236
|
+
generation_opts[:working_dir] = resolved_working_dir unless blank_value?(resolved_working_dir)
|
|
237
|
+
generation_opts[:subprocess_env] = resolved_subprocess_env unless resolved_subprocess_env.nil?
|
|
238
|
+
generation_opts[:subprocess_command_prefix] = resolved_subprocess_command_prefix unless blank_value?(resolved_subprocess_command_prefix)
|
|
239
|
+
generation_opts[:last_message_file] = last_message_file if last_message_file
|
|
240
|
+
generation_opts
|
|
241
|
+
end
|
|
242
|
+
private_class_method :build_generation_opts
|
|
243
|
+
|
|
177
244
|
def self.merge_execution_overrides(base, overlay)
|
|
178
245
|
left = base.respond_to?(:to_h) ? deep_stringify_keys(base.to_h) : deep_stringify_keys(base || {})
|
|
179
246
|
right = overlay.respond_to?(:to_h) ? deep_stringify_keys(overlay.to_h) : deep_stringify_keys(overlay || {})
|
|
@@ -291,7 +358,7 @@ module Ace
|
|
|
291
358
|
end
|
|
292
359
|
|
|
293
360
|
def self.execute_with_fallback(provider:, model:, messages:, generation_opts:,
|
|
294
|
-
registry:, fallback_config:, timeout:, debug:, role_fallbacks: nil)
|
|
361
|
+
registry:, fallback_config:, timeout:, debug:, role_fallbacks: nil, preset: nil, thinking_level: nil, option_builder:)
|
|
295
362
|
if fallback_config.disabled?
|
|
296
363
|
client = registry.get_client(provider, model: model, timeout: timeout)
|
|
297
364
|
return client.generate(messages, **generation_opts)
|
|
@@ -300,9 +367,10 @@ module Ace
|
|
|
300
367
|
primary_provider_string = model ? "#{provider}:#{model}" : provider
|
|
301
368
|
|
|
302
369
|
# Inject remaining role candidates ahead of the global fallback chain
|
|
370
|
+
normalized_role_fallbacks = normalize_fallback_providers(role_fallbacks, Molecules::ProviderModelParser.new(registry: registry))
|
|
303
371
|
if role_fallbacks&.any?
|
|
304
372
|
existing_chain = fallback_config.providers_for(primary_provider_string)
|
|
305
|
-
merged_chain =
|
|
373
|
+
merged_chain = normalized_role_fallbacks + (existing_chain - normalized_role_fallbacks)
|
|
306
374
|
fallback_config = fallback_config.merge(chains: {primary_provider_string => merged_chain})
|
|
307
375
|
end
|
|
308
376
|
|
|
@@ -314,8 +382,13 @@ module Ace
|
|
|
314
382
|
timeout: timeout
|
|
315
383
|
)
|
|
316
384
|
|
|
317
|
-
orchestrator.execute(primary_provider: primary_provider_string, registry: registry) do |client|
|
|
318
|
-
|
|
385
|
+
orchestrator.execute(primary_provider: primary_provider_string, registry: registry) do |client, target_selector|
|
|
386
|
+
opts = if target_selector.to_s == primary_provider_string
|
|
387
|
+
generation_opts
|
|
388
|
+
else
|
|
389
|
+
option_builder.call(target_selector)
|
|
390
|
+
end
|
|
391
|
+
client.generate(messages, **opts)
|
|
319
392
|
end
|
|
320
393
|
end
|
|
321
394
|
|
|
@@ -380,7 +453,7 @@ module Ace
|
|
|
380
453
|
|
|
381
454
|
parse_result = parser.parse(provider)
|
|
382
455
|
canonical_provider = if parse_result.valid?
|
|
383
|
-
|
|
456
|
+
parse_result.to_s
|
|
384
457
|
else
|
|
385
458
|
provider
|
|
386
459
|
end
|
data/lib/ace/llm/version.rb
CHANGED
data/lib/ace/llm.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "llm/molecules/provider_model_parser"
|
|
|
28
28
|
require_relative "llm/molecules/format_handlers"
|
|
29
29
|
require_relative "llm/molecules/client_registry"
|
|
30
30
|
require_relative "llm/molecules/fallback_orchestrator"
|
|
31
|
+
require_relative "llm/molecules/interactive_command_builder"
|
|
31
32
|
|
|
32
33
|
require_relative "llm/query_interface"
|
|
33
34
|
|
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.
|
|
4
|
+
version: 0.36.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.29'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ace-support-models
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.11'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.11'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: faraday
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -273,6 +287,7 @@ files:
|
|
|
273
287
|
- lib/ace/llm/molecules/fallback_orchestrator.rb
|
|
274
288
|
- lib/ace/llm/molecules/file_io_handler.rb
|
|
275
289
|
- lib/ace/llm/molecules/format_handlers.rb
|
|
290
|
+
- lib/ace/llm/molecules/interactive_command_builder.rb
|
|
276
291
|
- lib/ace/llm/molecules/llm_alias_resolver.rb
|
|
277
292
|
- lib/ace/llm/molecules/openai_compatible_params.rb
|
|
278
293
|
- lib/ace/llm/molecules/preset_loader.rb
|