ace-llm 0.30.2 → 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 +93 -0
- data/CHANGELOG.md +147 -0
- data/README.md +17 -2
- data/lib/ace/llm/cli/commands/query.rb +60 -9
- data/lib/ace/llm/configuration.rb +9 -1
- data/lib/ace/llm/models/role_config.rb +79 -0
- data/lib/ace/llm/molecules/client_registry.rb +89 -9
- 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/provider_model_parser.rb +85 -24
- data/lib/ace/llm/molecules/role_resolver.rb +100 -0
- data/lib/ace/llm/query_interface.rb +115 -34
- data/lib/ace/llm/version.rb +1 -1
- data/lib/ace/llm.rb +3 -0
- metadata +19 -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
|
|
@@ -25,6 +25,99 @@ llm:
|
|
|
25
25
|
chains: {}
|
|
26
26
|
providers: []
|
|
27
27
|
|
|
28
|
+
# Named model roles for indirection across consumer configs.
|
|
29
|
+
# Each role lists candidates in priority order; RoleResolver returns
|
|
30
|
+
# the first available one. Every role covers codex, claude, and gemini
|
|
31
|
+
# so resolution succeeds even when a provider is unconfigured.
|
|
32
|
+
# Override in .ace/llm/config.yml for project-specific role assignments.
|
|
33
|
+
roles:
|
|
34
|
+
# --- Reusable candidate lists ---
|
|
35
|
+
_utility-lite: &utility-lite
|
|
36
|
+
- google:lite
|
|
37
|
+
- codex:mini
|
|
38
|
+
- claude:haiku
|
|
39
|
+
_assign-worker: &assign-worker
|
|
40
|
+
- codex:codex@yolo
|
|
41
|
+
- claude:sonnet@yolo
|
|
42
|
+
- gemini:pro-latest@yolo
|
|
43
|
+
|
|
44
|
+
# --- Diagnostic ---
|
|
45
|
+
doctor:
|
|
46
|
+
- gemini:flash-latest@yolo
|
|
47
|
+
- claude:haiku@yolo
|
|
48
|
+
- codex:mini@yolo
|
|
49
|
+
|
|
50
|
+
# --- Planning ---
|
|
51
|
+
planner:
|
|
52
|
+
- codex:gpt@ro
|
|
53
|
+
- claude:sonnet@ro
|
|
54
|
+
- gemini:pro-latest@ro
|
|
55
|
+
|
|
56
|
+
# --- Review (single-model and synthesis) ---
|
|
57
|
+
review-default:
|
|
58
|
+
- codex:codex@ro
|
|
59
|
+
- claude:sonnet@ro
|
|
60
|
+
- gemini:pro-latest@ro
|
|
61
|
+
review-synthesizer:
|
|
62
|
+
- codex:gpt@ro
|
|
63
|
+
- claude:sonnet@ro
|
|
64
|
+
- gemini:pro-latest@ro
|
|
65
|
+
|
|
66
|
+
# --- Review (provider-specific, for parallel multi-model review) ---
|
|
67
|
+
review-claude:
|
|
68
|
+
- claude:opus@ro
|
|
69
|
+
- codex:gpt@ro
|
|
70
|
+
- gemini:pro-latest@ro
|
|
71
|
+
review-codex:
|
|
72
|
+
- codex:gpt@ro
|
|
73
|
+
- claude:sonnet@ro
|
|
74
|
+
- gemini:pro-latest@ro
|
|
75
|
+
review-gemini:
|
|
76
|
+
- gemini:pro-latest@ro
|
|
77
|
+
- claude:sonnet@ro
|
|
78
|
+
- codex:gpt@ro
|
|
79
|
+
|
|
80
|
+
# --- Assignment ---
|
|
81
|
+
assign-executor: *assign-worker
|
|
82
|
+
assign-engineer: *assign-worker
|
|
83
|
+
assign-orchestrator:
|
|
84
|
+
- claude:sonnet
|
|
85
|
+
- codex:gpt
|
|
86
|
+
- gemini:pro-latest
|
|
87
|
+
|
|
88
|
+
# --- E2E testing ---
|
|
89
|
+
e2e-executor:
|
|
90
|
+
- claude:haiku@yolo
|
|
91
|
+
- codex:mini@yolo
|
|
92
|
+
- gemini:flash-latest@yolo
|
|
93
|
+
e2e-reporter:
|
|
94
|
+
- claude:haiku
|
|
95
|
+
- codex:mini
|
|
96
|
+
- gemini:flash-latest
|
|
97
|
+
|
|
98
|
+
# --- Utility ---
|
|
99
|
+
prompt-enhance: *utility-lite
|
|
100
|
+
commit:
|
|
101
|
+
- google:lite
|
|
102
|
+
- codex:mini
|
|
103
|
+
- claude:haiku
|
|
104
|
+
docs-analysis: *utility-lite
|
|
105
|
+
idea-enhance:
|
|
106
|
+
- gemini:flash-latest
|
|
107
|
+
- codex:mini
|
|
108
|
+
- claude:haiku
|
|
109
|
+
compressor: *utility-lite
|
|
110
|
+
|
|
111
|
+
# --- Simulation ---
|
|
112
|
+
sim-primary:
|
|
113
|
+
- google:flash-preview
|
|
114
|
+
- claude:haiku
|
|
115
|
+
- codex:mini
|
|
116
|
+
sim-synthesis:
|
|
117
|
+
- claude:haiku
|
|
118
|
+
- codex:mini
|
|
119
|
+
- gemini:flash-latest
|
|
120
|
+
|
|
28
121
|
# Default context limit for unknown models (in tokens)
|
|
29
122
|
# Provider-specific limits are defined in providers/*.yml
|
|
30
123
|
context_limit:
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,153 @@ 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
|
+
|
|
115
|
+
## [0.32.1] - 2026-04-01
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
- Made role-based fallback chain-aware so remaining role candidates are tried before global fallback providers on query failure.
|
|
119
|
+
|
|
120
|
+
## [0.32.0] - 2026-03-31
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
- Shipped canonical role catalog with gem defaults so `role:*` selectors resolve without project-level config.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
- Added type guard in `RoleConfig.from_hash` to raise `ConfigurationError` for non-Hash role config input.
|
|
127
|
+
|
|
128
|
+
## [0.31.3] - 2026-03-31
|
|
129
|
+
|
|
130
|
+
### Technical
|
|
131
|
+
- Clarified provider fallback environment-key coupling in `ClientRegistry` to align credential resolution guidance with current setup behavior.
|
|
132
|
+
|
|
133
|
+
## [0.31.2] - 2026-03-30
|
|
134
|
+
|
|
135
|
+
### Technical
|
|
136
|
+
- Removed the unused `api_key_present?` helper from `ClientRegistry` to keep provider credential routing focused on active call sites.
|
|
137
|
+
|
|
138
|
+
## [0.31.1] - 2026-03-30
|
|
139
|
+
|
|
140
|
+
### Technical
|
|
141
|
+
- Added regression coverage for Google provider credential fallback when only `GOOGLE_API_KEY` is configured.
|
|
142
|
+
|
|
143
|
+
## [0.31.0] - 2026-03-29
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
- Extended `ace-llm --list-providers` output with provider credential setup hints, including required environment variable names.
|
|
147
|
+
- Added named model-role resolution via `role:<name>` selectors in `ProviderModelParser`, including strict runtime availability checks across candidate providers.
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
- Made provider setup failures actionable by surfacing supported provider lists, ignored-provider guidance, and an explicit recovery path via `ace-llm --list-providers`.
|
|
151
|
+
- Updated onboarding and usage documentation to align runtime errors with provider discovery and environment-variable setup guidance.
|
|
152
|
+
- Added `llm.roles` configuration support and a dedicated `RoleResolver`/`RoleConfig` pipeline so callers can centralize provider-model selection and keep caller `:thinking` and `@preset` overrides authoritative.
|
|
153
|
+
|
|
154
|
+
### Technical
|
|
155
|
+
- Added focused coverage for role config validation, resolver behavior, and parser integration in `test/models/role_config_test.rb`, `test/molecules/role_resolver_test.rb`, and `test/molecules/provider_model_parser_test.rb`.
|
|
156
|
+
|
|
10
157
|
## [0.30.2] - 2026-03-29
|
|
11
158
|
|
|
12
159
|
### Technical
|
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)
|
|
@@ -215,17 +256,27 @@ module Ace
|
|
|
215
256
|
|
|
216
257
|
providers.each do |provider|
|
|
217
258
|
status = provider[:available] ? "\u2713" : "\u2717"
|
|
218
|
-
|
|
219
|
-
provider[:api_key_present] ? "
|
|
259
|
+
credential_status = if provider[:api_key_required]
|
|
260
|
+
provider[:api_key_present] ? "Credentials configured" : "Credentials required"
|
|
220
261
|
else
|
|
221
|
-
"No
|
|
262
|
+
"No credentials required"
|
|
222
263
|
end
|
|
223
264
|
|
|
224
265
|
models = provider[:models] || []
|
|
225
266
|
model_count = models.empty? ? "" : " \u00b7 #{models.length} models"
|
|
226
|
-
puts "#{status} #{provider[:name]}#{model_count} (#{
|
|
267
|
+
puts "#{status} #{provider[:name]}#{model_count} (#{credential_status})"
|
|
227
268
|
|
|
228
269
|
print_wrapped_list(models, indent: " ") unless models.empty?
|
|
270
|
+
env_keys = provider[:credential_env_keys] || []
|
|
271
|
+
if provider[:api_key_required]
|
|
272
|
+
if env_keys.empty?
|
|
273
|
+
puts " Setup hint: credentials are required for this provider."
|
|
274
|
+
else
|
|
275
|
+
puts " Setup hint: set #{env_keys.join(' or ')}"
|
|
276
|
+
end
|
|
277
|
+
else
|
|
278
|
+
puts " Setup hint: no credential environment variable required."
|
|
279
|
+
end
|
|
229
280
|
puts " Gem required: #{provider[:gem]}" unless provider[:available]
|
|
230
281
|
puts ""
|
|
231
282
|
end
|
|
@@ -59,6 +59,11 @@ module Ace
|
|
|
59
59
|
Molecules::ConfigLoader.get(path)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Get configured role map (llm.roles) from config cascade.
|
|
63
|
+
def roles
|
|
64
|
+
get("llm.roles") || {}
|
|
65
|
+
end
|
|
66
|
+
|
|
62
67
|
# Check if configuration exists
|
|
63
68
|
def configured?
|
|
64
69
|
!config.empty?
|
|
@@ -122,7 +127,10 @@ module Ace
|
|
|
122
127
|
unknown = active_allow_list - available
|
|
123
128
|
return if unknown.empty?
|
|
124
129
|
|
|
125
|
-
warn "Unknown providers in llm.providers.active: #{unknown.join(", ")} (ignored)"
|
|
130
|
+
warn "Unknown providers in llm.providers.active: #{unknown.join(", ")} (ignored). " \
|
|
131
|
+
"These names do not match configured providers and were skipped. " \
|
|
132
|
+
"Update llm.providers.active to use supported provider names, or run " \
|
|
133
|
+
"`ace-llm --list-providers` for available providers and configuration guidance."
|
|
126
134
|
end
|
|
127
135
|
|
|
128
136
|
def active_provider_allow_list
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module LLM
|
|
5
|
+
module Models
|
|
6
|
+
# RoleConfig represents llm.roles configuration with validation.
|
|
7
|
+
class RoleConfig
|
|
8
|
+
attr_reader :roles
|
|
9
|
+
|
|
10
|
+
def initialize(roles: {})
|
|
11
|
+
@roles = normalize_roles(roles).freeze
|
|
12
|
+
validate!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_hash(hash)
|
|
16
|
+
return new unless hash
|
|
17
|
+
|
|
18
|
+
unless hash.is_a?(Hash)
|
|
19
|
+
raise Ace::LLM::ConfigurationError, "llm.roles config must be a Hash, got: #{hash.class}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
roles = hash.fetch(:roles, hash.fetch("roles", hash))
|
|
23
|
+
new(roles: roles)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def role_names
|
|
27
|
+
@roles.keys.sort
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def candidates_for(role_name)
|
|
31
|
+
normalized = normalize_role_name(role_name)
|
|
32
|
+
@roles[normalized]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalize_roles(roles)
|
|
38
|
+
return {} if roles.nil?
|
|
39
|
+
|
|
40
|
+
unless roles.is_a?(Hash)
|
|
41
|
+
raise Ace::LLM::ConfigurationError, "llm.roles must be a hash, got: #{roles.class}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
roles.each_with_object({}) do |(name, candidates), acc|
|
|
45
|
+
normalized_name = normalize_role_name(name)
|
|
46
|
+
acc[normalized_name] = Array(candidates).map { |candidate| candidate.to_s.strip }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize_role_name(name)
|
|
51
|
+
name.to_s.strip
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate!
|
|
55
|
+
@roles.each do |name, candidates|
|
|
56
|
+
if name.empty?
|
|
57
|
+
raise Ace::LLM::ConfigurationError, "role name cannot be empty"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless candidates.is_a?(Array) && !candidates.empty?
|
|
61
|
+
raise Ace::LLM::ConfigurationError, "role '#{name}' must define at least one candidate"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
candidates.each do |candidate|
|
|
65
|
+
if candidate.empty?
|
|
66
|
+
raise Ace::LLM::ConfigurationError, "role '#{name}' contains an empty candidate"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if candidate.start_with?("role:")
|
|
70
|
+
raise Ace::LLM::ConfigurationError,
|
|
71
|
+
"role '#{name}' cannot reference nested role candidate '#{candidate}'"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -112,17 +112,40 @@ module Ace
|
|
|
112
112
|
# @return [Hash] Provider status information
|
|
113
113
|
def list_providers_with_status
|
|
114
114
|
@providers.map do |name, config|
|
|
115
|
+
credential = credential_status(config, name)
|
|
115
116
|
{
|
|
116
117
|
name: name,
|
|
117
118
|
models: config["models"] || [],
|
|
118
119
|
gem: config["gem"],
|
|
119
120
|
available: provider_available?(name),
|
|
120
|
-
api_key_required:
|
|
121
|
-
api_key_present:
|
|
121
|
+
api_key_required: credential[:required],
|
|
122
|
+
api_key_present: credential[:present],
|
|
123
|
+
credential_env_keys: credential[:env_keys]
|
|
122
124
|
}
|
|
123
125
|
end
|
|
124
126
|
end
|
|
125
127
|
|
|
128
|
+
# Whether provider config marks API key as required.
|
|
129
|
+
# @param provider_name [String]
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def provider_api_key_required?(provider_name)
|
|
132
|
+
provider = get_provider(provider_name)
|
|
133
|
+
return false unless provider
|
|
134
|
+
|
|
135
|
+
provider.dig("api_key", "required") || false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Whether provider has a present API key according to its api_key config.
|
|
139
|
+
# @param provider_name [String]
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def provider_api_key_present?(provider_name)
|
|
142
|
+
provider = get_provider(provider_name)
|
|
143
|
+
return false unless provider
|
|
144
|
+
|
|
145
|
+
normalized_name = normalize_provider_name(provider["name"] || provider_name)
|
|
146
|
+
credential_status(provider, normalized_name)[:present]
|
|
147
|
+
end
|
|
148
|
+
|
|
126
149
|
# Reload all configurations
|
|
127
150
|
def reload!
|
|
128
151
|
@providers.clear
|
|
@@ -299,14 +322,30 @@ module Ace
|
|
|
299
322
|
end
|
|
300
323
|
end
|
|
301
324
|
|
|
302
|
-
#
|
|
303
|
-
# @param
|
|
304
|
-
# @
|
|
305
|
-
|
|
306
|
-
|
|
325
|
+
# Build normalized credential status for provider output.
|
|
326
|
+
# @param provider_config [Hash] provider configuration hash
|
|
327
|
+
# @param provider_name [String] normalized provider name
|
|
328
|
+
# @return [Hash] credential status
|
|
329
|
+
def credential_status(provider_config, provider_name)
|
|
330
|
+
env_keys = extract_credential_env_keys(provider_config, provider_name)
|
|
331
|
+
explicit_required = provider_config.dig("api_key", "required")
|
|
332
|
+
required = explicit_required.nil? ? !env_keys.empty? : explicit_required
|
|
333
|
+
|
|
334
|
+
present = if provider_config["api_key"].is_a?(String) && !provider_config["api_key"].empty?
|
|
335
|
+
true
|
|
336
|
+
elsif provider_config["api_key"].is_a?(Hash) && provider_config["api_key"]["value"]
|
|
337
|
+
!provider_config["api_key"]["value"].to_s.empty?
|
|
338
|
+
elsif provider_config["api_key"].is_a?(Hash) && provider_config["api_key"]["env"]
|
|
339
|
+
!ENV[provider_config["api_key"]["env"]].to_s.empty?
|
|
340
|
+
else
|
|
341
|
+
env_keys.any? { |key| !ENV[key].to_s.empty? }
|
|
342
|
+
end
|
|
307
343
|
|
|
308
|
-
|
|
309
|
-
|
|
344
|
+
{
|
|
345
|
+
required: required,
|
|
346
|
+
present: present,
|
|
347
|
+
env_keys: env_keys
|
|
348
|
+
}
|
|
310
349
|
end
|
|
311
350
|
|
|
312
351
|
# Build alias maps from provider configurations
|
|
@@ -330,6 +369,47 @@ module Ace
|
|
|
330
369
|
end
|
|
331
370
|
end
|
|
332
371
|
end
|
|
372
|
+
|
|
373
|
+
def extract_credential_env_keys(provider_config, provider_name)
|
|
374
|
+
env_keys = []
|
|
375
|
+
|
|
376
|
+
api_key_env = provider_config.dig("api_key", "env")
|
|
377
|
+
env_keys << api_key_env if api_key_env
|
|
378
|
+
|
|
379
|
+
backends = provider_config["backends"]
|
|
380
|
+
if backends.is_a?(Hash)
|
|
381
|
+
backends.each_value do |backend|
|
|
382
|
+
next unless backend.is_a?(Hash)
|
|
383
|
+
|
|
384
|
+
env_key = backend["env_key"]
|
|
385
|
+
Array(env_key).each do |key|
|
|
386
|
+
env_keys << key if key
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Keep parity with BaseClient/provider.yml defaults for providers that rely on
|
|
392
|
+
# fallback keys. If new providers are added, update this fallback map as well.
|
|
393
|
+
env_keys.concat(default_env_keys_for_provider(provider_name)) if env_keys.empty?
|
|
394
|
+
env_keys.map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def default_env_keys_for_provider(provider_name)
|
|
398
|
+
case provider_name.to_s.downcase
|
|
399
|
+
when "google"
|
|
400
|
+
%w[GEMINI_API_KEY GOOGLE_API_KEY]
|
|
401
|
+
when "openai"
|
|
402
|
+
["OPENAI_API_KEY"]
|
|
403
|
+
when "anthropic"
|
|
404
|
+
["ANTHROPIC_API_KEY"]
|
|
405
|
+
when "mistral"
|
|
406
|
+
["MISTRAL_API_KEY"]
|
|
407
|
+
when "togetherai"
|
|
408
|
+
%w[TOGETHER_API_KEY TOGETHERAI_API_KEY]
|
|
409
|
+
else
|
|
410
|
+
[]
|
|
411
|
+
end
|
|
412
|
+
end
|
|
333
413
|
end
|
|
334
414
|
end
|
|
335
415
|
end
|