ace-llm-providers-cli 0.27.2 → 0.31.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.
- checksums.yaml +4 -4
- data/.ace-defaults/llm/providers/codex.yml +12 -7
- data/.ace-defaults/llm/providers/gemini.yml +2 -0
- data/CHANGELOG.md +94 -0
- data/README.md +14 -3
- data/lib/ace/llm/providers/cli/atoms/command_formatters.rb +3 -0
- data/lib/ace/llm/providers/cli/atoms/interactive_startup_policy.rb +100 -0
- data/lib/ace/llm/providers/cli/claude_code_client.rb +129 -4
- data/lib/ace/llm/providers/cli/claude_oai_client.rb +4 -2
- data/lib/ace/llm/providers/cli/cli_args_support.rb +15 -0
- data/lib/ace/llm/providers/cli/codex_client.rb +73 -3
- data/lib/ace/llm/providers/cli/codex_oai_client.rb +2 -0
- data/lib/ace/llm/providers/cli/gemini_client.rb +16 -11
- data/lib/ace/llm/providers/cli/molecules/safe_capture.rb +3 -1
- data/lib/ace/llm/providers/cli/open_code_client.rb +2 -0
- data/lib/ace/llm/providers/cli/pi_client.rb +54 -2
- data/lib/ace/llm/providers/cli/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ae114fa300c121e8b2d358ae3ef2ad16e3d53bf981b11e2c2be59eb2c5c29d6
|
|
4
|
+
data.tar.gz: e3b9479472b714507dfb0bbb82dd10b19b3e403bfcd9fe0a107fd3194975e65a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68cf79d47bbdf06e62e8e8d79a72ed5a607d249ff57cbf767696f8580066b5188ac1f3920a3bf8f3b998de290067f75d58379670472cd5b57c61d613499e8de6
|
|
7
|
+
data.tar.gz: f3d14332bb1d44bdf0fc8906afd6576e7349bf2ea12ac1ebeedc32d535156bce4ff948bd1323e5ee0691b0d88f365d8a4b1ec9a0b8460849867c899a8af8cc4d
|
|
@@ -2,16 +2,21 @@ name: codex
|
|
|
2
2
|
class: Ace::LLM::Providers::CLI::CodexClient
|
|
3
3
|
gem: ace-llm-providers-cli
|
|
4
4
|
models:
|
|
5
|
-
- gpt-5
|
|
6
|
-
- gpt-5-
|
|
5
|
+
- gpt-5.3-codex
|
|
6
|
+
- gpt-5.3-codex-spark
|
|
7
|
+
- gpt-5.4
|
|
8
|
+
- gpt-5.4-mini
|
|
7
9
|
aliases:
|
|
8
10
|
global:
|
|
9
|
-
codex-gpt5: codex:gpt-5
|
|
10
|
-
codex-mini: codex:gpt-5-mini
|
|
11
|
+
codex-gpt5: codex:gpt-5.4
|
|
12
|
+
codex-mini: codex:gpt-5.4-mini
|
|
11
13
|
model:
|
|
12
|
-
5: gpt-5
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
5: gpt-5.4
|
|
15
|
+
gpt: gpt-5.4
|
|
16
|
+
codex: gpt-5.3-codex
|
|
17
|
+
spark: gpt-5.3-codex-spark
|
|
18
|
+
mini: gpt-5.4-mini
|
|
19
|
+
m: gpt-5.4-mini
|
|
15
20
|
capabilities:
|
|
16
21
|
- text_generation
|
|
17
22
|
- code_generation
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.31.2] - 2026-04-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Updated shipped Codex defaults from the rejected `gpt-5-mini` alias path to the current `gpt-5.4` / `gpt-5.4-mini` catalog and aligned the CLI client metadata with those model IDs.
|
|
14
|
+
- Added shipped `flash-latest` and `pro-latest` Gemini aliases backed by declared default models so fresh-provider fallback chains no longer target missing Gemini aliases.
|
|
15
|
+
|
|
16
|
+
## [0.31.1] - 2026-04-16
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Updated `TS-LLMCLI-001` sandbox setup to source `mise.toml` from `${ACE_E2E_SOURCE_ROOT:-$PROJECT_ROOT_PATH}`, keeping CLI smoke runs compatible with read-only source-root mounts.
|
|
20
|
+
|
|
21
|
+
## [0.31.0] - 2026-04-15
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Reworked `TS-LLMCLI-001` to public-surface goal-style guidance by removing hidden `which` interception,
|
|
25
|
+
introducing deterministic sandbox-safe runtime/stub setup, and clarifying provider-discovery harness rules.
|
|
26
|
+
|
|
27
|
+
### Technical
|
|
28
|
+
- Added E2E verification lifecycle metadata guidance (`Last verified`, `Verified by`, refresh trigger) in
|
|
29
|
+
scenario decision/verifier docs.
|
|
30
|
+
- Updated package README testing guidance for deterministic provider-discovery harness behavior.
|
|
31
|
+
|
|
32
|
+
## [0.30.0] - 2026-04-15
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Added a Codex tmux startup overlay that trusts the exact working directory through a temporary per-workspace Codex home, preventing fresh tmux sandboxes from blocking on the trust prompt before agent work begins.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Switched tmux-facing Codex launches to reuse the shared `ace-llm --interactive` entrypoint, so provider skill translation, alias resolution, preset handling, and startup policy all flow through one path.
|
|
39
|
+
|
|
40
|
+
## [0.29.0] - 2026-04-15
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Added native interactive invocation builders for Codex, Claude Code, and pi so CLI providers can be launched without their one-shot print/exec flags while still receiving translated ACE skill handoffs.
|
|
44
|
+
|
|
45
|
+
### Technical
|
|
46
|
+
- Added shared CLI-arg conflict guards plus Claude slash-command formatting support for the new interactive launch path.
|
|
47
|
+
|
|
48
|
+
## [0.28.6] - 2026-04-13
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Cleared `CLAUDECODE` during Claude help capability probes so nested Claude Code sessions no longer cache false negatives for `--max-tokens` support.
|
|
52
|
+
|
|
53
|
+
### Technical
|
|
54
|
+
- Added regression coverage for the help-probe environment handling.
|
|
55
|
+
|
|
56
|
+
## [0.28.5] - 2026-04-13
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Avoided eager Claude CLI capability probes during normal command construction so non-`max-tokens` requests no longer shell out to `claude --help`.
|
|
60
|
+
|
|
61
|
+
### Technical
|
|
62
|
+
- Restored deterministic command-builder coverage by guarding common-path unit tests from unexpected capability probing.
|
|
63
|
+
|
|
64
|
+
## [0.28.4] - 2026-04-13
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- Filter forwarded Claude `--max-tokens` CLI args when the installed Claude CLI does not advertise support, preventing explicit passthrough flags from reintroducing unsupported-option failures.
|
|
68
|
+
|
|
69
|
+
## [0.28.3] - 2026-04-13
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Guarded Claude CLI `--max-tokens` usage so commit-generation flows skip the flag on unsupported Claude versions while preserving it when available.
|
|
73
|
+
|
|
74
|
+
### Technical
|
|
75
|
+
- Added command-construction coverage for supported and unsupported Claude `--max-tokens` capability paths.
|
|
76
|
+
|
|
77
|
+
## [0.28.2] - 2026-04-13
|
|
78
|
+
|
|
79
|
+
### Technical
|
|
80
|
+
- Tightened the package version contract test to require a full semantic-version match instead of accepting partial matches.
|
|
81
|
+
## [0.28.1] - 2026-04-13
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
- Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
|
|
85
|
+
|
|
86
|
+
### Technical
|
|
87
|
+
- Included in the coordinated assignment-driven patch release for batch i05 package updates.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
- Migrated package tests to the `fast` / `feat` / `e2e` model and moved deterministic coverage into `test/fast` and `test/feat`.
|
|
92
|
+
- Updated `TS-LLMCLI-001` scenario metadata and decision records to reference deterministic fast coverage locations.
|
|
93
|
+
- Updated README testing guidance to teach the `ace-test ...`, `ace-test ... feat`, `ace-test ... all`, and `ace-test-e2e ...` contract.
|
|
94
|
+
|
|
95
|
+
## [0.27.3] - 2026-04-10
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
- Forwarded `subprocess_env` into Codex and Gemini `SafeCapture` execution.
|
|
99
|
+
- Updated Gemini prompt-cache and subprocess working-dir resolution to use execution context directly.
|
|
100
|
+
|
|
101
|
+
### Technical
|
|
102
|
+
- Added Codex and Gemini regression coverage to assert `SafeCapture` receives forwarded `env`.
|
|
103
|
+
|
|
10
104
|
## [0.27.2] - 2026-03-29
|
|
11
105
|
|
|
12
106
|
### Technical
|
data/README.md
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
> Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
|
|
16
16
|
|
|
17
|
-
[ace-llm Usage Guide](../ace-llm/docs/usage.md) | [ace-llm Handbook](../ace-llm/docs/handbook.md)
|
|
17
|
+
[ace-llm Usage Guide](../ace-llm/docs/usage.md) | [ace-llm Handbook](../ace-llm/docs/handbook.md) | Part of [ACE](https://github.com/cs3b/ace)
|
|
18
|
+
|
|
18
19
|
`ace-llm-providers-cli` extends [ace-llm](../ace-llm) with provider clients that execute through installed CLI tools (Claude, Codex, OpenCode, Gemini, pi, Codex OSS) while preserving the shared command interface. Provider defaults live in versioned YAML, and a health-check command verifies local readiness.
|
|
19
20
|
|
|
21
|
+
This package intentionally does not ship a package-local `docs/` directory. Public usage and operator guidance live in the shared [ace-llm Usage Guide](../ace-llm/docs/usage.md), while `ace-llm-providers-cli-check --help` remains the authoritative runtime help surface for local readiness checks.
|
|
22
|
+
|
|
20
23
|
## How It Works
|
|
21
24
|
|
|
22
25
|
1. The gem registers CLI provider clients on require, making them available to [ace-llm](../ace-llm) automatically.
|
|
@@ -31,6 +34,14 @@
|
|
|
31
34
|
|
|
32
35
|
**Diagnose local provider readiness** - run `ace-llm-providers-cli-check` to verify that CLI tools are installed, authenticated, and reachable before starting work.
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
## Testing
|
|
38
|
+
|
|
39
|
+
- `ace-test ace-llm-providers-cli` runs deterministic fast coverage from `test/fast/`.
|
|
40
|
+
- `ace-test ace-llm-providers-cli feat` runs deterministic feature coverage from `test/feat/`.
|
|
41
|
+
- `ace-test ace-llm-providers-cli all` runs fast + feat deterministic coverage.
|
|
42
|
+
- `ace-test-e2e ace-llm-providers-cli` runs retained workflow scenarios from `test/e2e/`.
|
|
43
|
+
- E2E provider-discovery scenarios avoid helper interception: the no-tools path uses a baseline PATH
|
|
44
|
+
(`/usr/bin:/bin`), and success-path stubs are kept as text scripts in a dedicated sandbox stub directory.
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
The package no longer uses legacy `test/integration` naming and does not use
|
|
47
|
+
`ace-test ace-llm-providers-cli e2e` as a deterministic test path.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module LLM
|
|
8
|
+
module Providers
|
|
9
|
+
module CLI
|
|
10
|
+
module Atoms
|
|
11
|
+
module InteractiveStartupPolicy
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def tmux_context?(subprocess_env = nil)
|
|
15
|
+
env = normalized_env(subprocess_env)
|
|
16
|
+
!tmux_session_name(env).empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def codex_trust_override(working_dir:, subprocess_env: nil)
|
|
20
|
+
return nil unless tmux_context?(subprocess_env)
|
|
21
|
+
|
|
22
|
+
path = working_dir.to_s.strip
|
|
23
|
+
return nil if path.empty?
|
|
24
|
+
|
|
25
|
+
%{projects."#{escape_toml_basic_string(path)}".trust_level="trusted"}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def codex_overlay_home(working_dir:, subprocess_env: nil)
|
|
29
|
+
return nil unless tmux_context?(subprocess_env)
|
|
30
|
+
|
|
31
|
+
root = working_dir.to_s.strip
|
|
32
|
+
return nil if root.empty?
|
|
33
|
+
|
|
34
|
+
overlay_home = File.join(root, ".ace-local", "llm", "codex-home", Digest::SHA256.hexdigest(root)[0, 12])
|
|
35
|
+
prepare_codex_overlay_home(overlay_home: overlay_home, working_dir: root)
|
|
36
|
+
overlay_home
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalized_env(subprocess_env)
|
|
40
|
+
env = ENV.to_h
|
|
41
|
+
return env unless subprocess_env
|
|
42
|
+
|
|
43
|
+
subprocess_env.each_with_object(env) do |(key, value), memo|
|
|
44
|
+
memo[key.to_s] = value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tmux_session_name(env)
|
|
49
|
+
explicit = env["ACE_TMUX_SESSION"].to_s.strip
|
|
50
|
+
return explicit unless explicit.empty?
|
|
51
|
+
|
|
52
|
+
env["TMUX"].to_s.strip
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def escape_toml_basic_string(value)
|
|
56
|
+
value.to_s.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prepare_codex_overlay_home(overlay_home:, working_dir:)
|
|
60
|
+
codex_dir = File.join(overlay_home, ".codex")
|
|
61
|
+
FileUtils.mkdir_p(codex_dir)
|
|
62
|
+
|
|
63
|
+
sync_codex_overlay_links(codex_dir)
|
|
64
|
+
write_codex_overlay_config(codex_dir: codex_dir, working_dir: working_dir)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sync_codex_overlay_links(codex_dir)
|
|
68
|
+
source_dir = File.expand_path("~/.codex")
|
|
69
|
+
return unless Dir.exist?(source_dir)
|
|
70
|
+
|
|
71
|
+
%w[auth.json installation_id version.json skills rules memories].each do |entry|
|
|
72
|
+
source = File.join(source_dir, entry)
|
|
73
|
+
next unless File.exist?(source)
|
|
74
|
+
|
|
75
|
+
target = File.join(codex_dir, entry)
|
|
76
|
+
next if File.exist?(target) || File.symlink?(target)
|
|
77
|
+
|
|
78
|
+
File.symlink(source, target)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_codex_overlay_config(codex_dir:, working_dir:)
|
|
83
|
+
source_config = File.expand_path("~/.codex/config.toml")
|
|
84
|
+
base = File.exist?(source_config) ? File.read(source_config) : ""
|
|
85
|
+
trust_block = <<~TOML
|
|
86
|
+
|
|
87
|
+
[projects."#{escape_toml_basic_string(working_dir)}"]
|
|
88
|
+
trust_level = "trusted"
|
|
89
|
+
TOML
|
|
90
|
+
|
|
91
|
+
target = File.join(codex_dir, "config.toml")
|
|
92
|
+
content = base.include?(trust_block.strip) ? base : "#{base.rstrip}#{trust_block}"
|
|
93
|
+
File.write(target, content)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -6,6 +6,9 @@ require "shellwords"
|
|
|
6
6
|
|
|
7
7
|
require_relative "cli_args_support"
|
|
8
8
|
require_relative "atoms/execution_context"
|
|
9
|
+
require_relative "atoms/command_rewriter"
|
|
10
|
+
require_relative "atoms/command_formatters"
|
|
11
|
+
require_relative "molecules/skill_name_reader"
|
|
9
12
|
|
|
10
13
|
module Ace
|
|
11
14
|
module LLM
|
|
@@ -33,6 +36,7 @@ module Ace
|
|
|
33
36
|
# Skip normal BaseClient initialization that requires API key
|
|
34
37
|
@options = options
|
|
35
38
|
@generation_config = options[:generation_config] || {}
|
|
39
|
+
@skill_name_reader = Molecules::SkillNameReader.new
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# Override to indicate this client doesn't need API credentials
|
|
@@ -60,7 +64,8 @@ module Ace
|
|
|
60
64
|
cmd,
|
|
61
65
|
prompt,
|
|
62
66
|
subprocess_env: subprocess_env,
|
|
63
|
-
working_dir: working_dir
|
|
67
|
+
working_dir: working_dir,
|
|
68
|
+
subprocess_command_prefix: options[:subprocess_command_prefix]
|
|
64
69
|
)
|
|
65
70
|
|
|
66
71
|
parse_claude_response(stdout, stderr, status, prompt, options)
|
|
@@ -79,6 +84,32 @@ module Ace
|
|
|
79
84
|
]
|
|
80
85
|
end
|
|
81
86
|
|
|
87
|
+
def interactive_supported?
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_interactive_invocation(messages, **options)
|
|
92
|
+
validate_claude_availability!
|
|
93
|
+
|
|
94
|
+
prompt = format_messages_as_prompt(messages)
|
|
95
|
+
subprocess_env = options[:subprocess_env]
|
|
96
|
+
working_dir = Atoms::ExecutionContext.resolve_working_dir(
|
|
97
|
+
working_dir: options[:working_dir],
|
|
98
|
+
subprocess_env: subprocess_env
|
|
99
|
+
)
|
|
100
|
+
prompt = rewrite_skill_commands(prompt, working_dir: working_dir)
|
|
101
|
+
|
|
102
|
+
cmd = build_claude_interactive_command(prompt, options)
|
|
103
|
+
env = {"CLAUDECODE" => nil}
|
|
104
|
+
env.merge!(subprocess_env) if subprocess_env
|
|
105
|
+
{
|
|
106
|
+
command: cmd,
|
|
107
|
+
env: env,
|
|
108
|
+
working_dir: working_dir,
|
|
109
|
+
prompt: prompt
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
82
113
|
private
|
|
83
114
|
|
|
84
115
|
def format_messages_as_prompt(messages)
|
|
@@ -146,19 +177,42 @@ module Ace
|
|
|
146
177
|
# (128KB per-argument limit). System content is already embedded in the
|
|
147
178
|
# formatted prompt via format_messages_as_prompt.
|
|
148
179
|
|
|
180
|
+
cli_args = normalized_cli_args(options)
|
|
181
|
+
max_tokens_requested = cli_args_include_max_tokens_flag?(cli_args)
|
|
182
|
+
|
|
149
183
|
# Add max tokens if provided
|
|
150
184
|
max_tokens = options[:max_tokens] || @generation_config[:max_tokens]
|
|
151
185
|
if max_tokens
|
|
152
|
-
|
|
186
|
+
max_tokens_requested = true
|
|
187
|
+
cmd.concat(["--max-tokens", max_tokens.to_s]) if supports_max_tokens_flag?
|
|
153
188
|
end
|
|
154
189
|
|
|
155
190
|
# User CLI args last so they take precedence (last-wins in most CLIs)
|
|
156
|
-
cmd.concat(
|
|
191
|
+
cmd.concat(filter_unsupported_cli_args(cli_args, max_tokens_requested: max_tokens_requested))
|
|
192
|
+
|
|
193
|
+
cmd
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def build_claude_interactive_command(prompt, options)
|
|
197
|
+
cmd = ["claude"]
|
|
157
198
|
|
|
199
|
+
if @model && @model != DEFAULT_MODEL
|
|
200
|
+
cmd << "--model" << @model
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
cmd.concat(
|
|
204
|
+
normalized_cli_args_without_conflicts(
|
|
205
|
+
options,
|
|
206
|
+
forbidden_flags: ["-p", "--print", "--output-format", "--input-format", "--json-schema",
|
|
207
|
+
"--include-partial-messages", "--include-hook-events", "--no-session-persistence"],
|
|
208
|
+
label: "Claude"
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
cmd << prompt.to_s unless prompt.to_s.empty?
|
|
158
212
|
cmd
|
|
159
213
|
end
|
|
160
214
|
|
|
161
|
-
def execute_claude_command(cmd, prompt, subprocess_env: nil, working_dir: nil)
|
|
215
|
+
def execute_claude_command(cmd, prompt, subprocess_env: nil, working_dir: nil, subprocess_command_prefix: nil)
|
|
162
216
|
timeout_val = @options[:timeout] || 120
|
|
163
217
|
# Clear CLAUDECODE env var so `claude -p` (non-interactive, one-shot mode)
|
|
164
218
|
# can run as a subprocess from within a Claude Code session.
|
|
@@ -173,10 +227,58 @@ module Ace
|
|
|
173
227
|
stdin_data: prompt.to_s,
|
|
174
228
|
chdir: working_dir,
|
|
175
229
|
env: env,
|
|
230
|
+
command_prefix: subprocess_command_prefix,
|
|
176
231
|
provider_name: "Claude"
|
|
177
232
|
)
|
|
178
233
|
end
|
|
179
234
|
|
|
235
|
+
def supports_max_tokens_flag?
|
|
236
|
+
return @supports_max_tokens_flag unless @supports_max_tokens_flag.nil?
|
|
237
|
+
|
|
238
|
+
@supports_max_tokens_flag = command_help_supports_flag?("--max-tokens")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def command_help_supports_flag?(flag)
|
|
242
|
+
[["claude", "-p", "--help"], ["claude", "--help"]].any? do |help_cmd|
|
|
243
|
+
stdout, stderr, status = Open3.capture3({"CLAUDECODE" => nil}, *help_cmd)
|
|
244
|
+
status.success? && [stdout, stderr].join("\n").include?(flag)
|
|
245
|
+
rescue
|
|
246
|
+
false
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def cli_args_include_max_tokens_flag?(cli_args)
|
|
251
|
+
cli_args.each_with_index.any? do |arg, index|
|
|
252
|
+
arg == "--max-tokens" || arg.start_with?("--max-tokens=") || (arg == "--max-tokens" && !cli_args[index + 1].nil?)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def filter_unsupported_cli_args(cli_args, max_tokens_requested: false)
|
|
257
|
+
return cli_args unless max_tokens_requested
|
|
258
|
+
return cli_args if supports_max_tokens_flag?
|
|
259
|
+
|
|
260
|
+
filtered = []
|
|
261
|
+
skip_next = false
|
|
262
|
+
|
|
263
|
+
cli_args.each do |arg|
|
|
264
|
+
if skip_next
|
|
265
|
+
skip_next = false
|
|
266
|
+
next
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if arg == "--max-tokens"
|
|
270
|
+
skip_next = true
|
|
271
|
+
next
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
next if arg.start_with?("--max-tokens=")
|
|
275
|
+
|
|
276
|
+
filtered << arg
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
filtered
|
|
280
|
+
end
|
|
281
|
+
|
|
180
282
|
def parse_claude_response(stdout, stderr, status, prompt, options)
|
|
181
283
|
unless status.success?
|
|
182
284
|
error_msg = stderr.empty? ? stdout : stderr
|
|
@@ -212,6 +314,29 @@ module Ace
|
|
|
212
314
|
}
|
|
213
315
|
end
|
|
214
316
|
|
|
317
|
+
def rewrite_skill_commands(prompt, working_dir: nil)
|
|
318
|
+
skills_dir = resolve_skills_dir(working_dir: working_dir)
|
|
319
|
+
return prompt unless skills_dir
|
|
320
|
+
|
|
321
|
+
skill_names = @skill_name_reader.call(skills_dir)
|
|
322
|
+
return prompt if skill_names.empty?
|
|
323
|
+
|
|
324
|
+
Atoms::CommandRewriter.call(
|
|
325
|
+
prompt,
|
|
326
|
+
skill_names: skill_names,
|
|
327
|
+
formatter: Atoms::CommandFormatters::CLAUDE_FORMATTER
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def resolve_skills_dir(working_dir: nil)
|
|
332
|
+
configured = @options[:skills_dir] || @generation_config[:skills_dir]
|
|
333
|
+
return configured if configured && Dir.exist?(configured)
|
|
334
|
+
|
|
335
|
+
working_dir ||= Atoms::ExecutionContext.resolve_working_dir
|
|
336
|
+
candidate_dir = File.join(working_dir, ".claude", "skills")
|
|
337
|
+
candidate_dir if Dir.exist?(candidate_dir)
|
|
338
|
+
end
|
|
339
|
+
|
|
215
340
|
def extract_claude_text(response)
|
|
216
341
|
direct_result = response["result"]
|
|
217
342
|
direct_response = response["response"]
|
|
@@ -61,7 +61,8 @@ module Ace
|
|
|
61
61
|
cmd,
|
|
62
62
|
prompt,
|
|
63
63
|
subprocess_env: subprocess_env,
|
|
64
|
-
working_dir: working_dir
|
|
64
|
+
working_dir: working_dir,
|
|
65
|
+
subprocess_command_prefix: options[:subprocess_command_prefix]
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
parse_claude_response(stdout, stderr, status, prompt, options)
|
|
@@ -154,7 +155,7 @@ module Ace
|
|
|
154
155
|
cmd
|
|
155
156
|
end
|
|
156
157
|
|
|
157
|
-
def execute_claude_command(cmd, prompt, subprocess_env: nil, working_dir: nil)
|
|
158
|
+
def execute_claude_command(cmd, prompt, subprocess_env: nil, working_dir: nil, subprocess_command_prefix: nil)
|
|
158
159
|
timeout_val = @options[:timeout] || 120
|
|
159
160
|
|
|
160
161
|
# Build env with backend-specific vars for Anthropic-compatible routing
|
|
@@ -169,6 +170,7 @@ module Ace
|
|
|
169
170
|
stdin_data: prompt.to_s,
|
|
170
171
|
chdir: working_dir,
|
|
171
172
|
env: env,
|
|
173
|
+
command_prefix: subprocess_command_prefix,
|
|
172
174
|
provider_name: "Claude OAI"
|
|
173
175
|
)
|
|
174
176
|
end
|
|
@@ -12,6 +12,21 @@ module Ace
|
|
|
12
12
|
def normalized_cli_args(options)
|
|
13
13
|
Atoms::ArgsNormalizer.new.normalize_cli_args(options[:cli_args])
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
def normalized_cli_args_without_conflicts(options, forbidden_flags:, label:)
|
|
17
|
+
args = normalized_cli_args(options)
|
|
18
|
+
conflict = find_conflicting_cli_arg(args, forbidden_flags)
|
|
19
|
+
return args unless conflict
|
|
20
|
+
|
|
21
|
+
raise Ace::LLM::ProviderError, "#{label} interactive mode does not support cli arg #{conflict}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_conflicting_cli_arg(args, forbidden_flags)
|
|
25
|
+
normalized_forbidden = Array(forbidden_flags).map(&:to_s)
|
|
26
|
+
args.find do |arg|
|
|
27
|
+
normalized_forbidden.any? { |flag| arg == flag || arg.start_with?("#{flag}=") }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
15
30
|
end
|
|
16
31
|
end
|
|
17
32
|
end
|
|
@@ -8,6 +8,7 @@ require_relative "cli_args_support"
|
|
|
8
8
|
require_relative "atoms/execution_context"
|
|
9
9
|
require_relative "atoms/command_rewriter"
|
|
10
10
|
require_relative "atoms/command_formatters"
|
|
11
|
+
require_relative "atoms/interactive_startup_policy"
|
|
11
12
|
require_relative "atoms/worktree_dir_resolver"
|
|
12
13
|
require_relative "molecules/skill_name_reader"
|
|
13
14
|
|
|
@@ -30,7 +31,7 @@ module Ace
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Default model (can be overridden by config)
|
|
33
|
-
DEFAULT_MODEL = "gpt-5"
|
|
34
|
+
DEFAULT_MODEL = "gpt-5.4"
|
|
34
35
|
|
|
35
36
|
def initialize(model: nil, **options)
|
|
36
37
|
@model = model || DEFAULT_MODEL
|
|
@@ -74,11 +75,48 @@ module Ace
|
|
|
74
75
|
# Return models based on what the CLI supports
|
|
75
76
|
# Actual models come from YAML config
|
|
76
77
|
[
|
|
77
|
-
{id: "gpt-5", name: "GPT-5", description: "
|
|
78
|
-
{id: "gpt-5-
|
|
78
|
+
{id: "gpt-5.3-codex", name: "GPT-5.3 Codex", description: "Code-specialized Codex model", context_size: 128_000},
|
|
79
|
+
{id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark", description: "Faster Codex model", context_size: 128_000},
|
|
80
|
+
{id: "gpt-5.4", name: "GPT-5.4", description: "Advanced Codex model", context_size: 128_000},
|
|
81
|
+
{id: "gpt-5.4-mini", name: "GPT-5.4 Mini", description: "Smaller, faster Codex model", context_size: 128_000}
|
|
79
82
|
]
|
|
80
83
|
end
|
|
81
84
|
|
|
85
|
+
def interactive_supported?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_interactive_invocation(messages, **options)
|
|
90
|
+
validate_codex_availability!
|
|
91
|
+
|
|
92
|
+
prompt = format_messages_as_prompt(messages)
|
|
93
|
+
subprocess_env = options[:subprocess_env]
|
|
94
|
+
working_dir = Atoms::ExecutionContext.resolve_working_dir(
|
|
95
|
+
working_dir: options[:working_dir],
|
|
96
|
+
subprocess_env: subprocess_env
|
|
97
|
+
)
|
|
98
|
+
prompt = rewrite_skill_commands(prompt, working_dir: working_dir)
|
|
99
|
+
|
|
100
|
+
env = subprocess_env ? subprocess_env.to_h.dup : {}
|
|
101
|
+
overlay_home = Atoms::InteractiveStartupPolicy.codex_overlay_home(
|
|
102
|
+
working_dir: working_dir,
|
|
103
|
+
subprocess_env: subprocess_env
|
|
104
|
+
)
|
|
105
|
+
env["HOME"] = overlay_home if overlay_home
|
|
106
|
+
|
|
107
|
+
cmd = build_codex_interactive_command(
|
|
108
|
+
prompt,
|
|
109
|
+
options,
|
|
110
|
+
working_dir: working_dir
|
|
111
|
+
)
|
|
112
|
+
{
|
|
113
|
+
command: cmd,
|
|
114
|
+
env: env,
|
|
115
|
+
working_dir: working_dir,
|
|
116
|
+
prompt: prompt
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
82
120
|
private
|
|
83
121
|
|
|
84
122
|
def format_messages_as_prompt(messages)
|
|
@@ -182,6 +220,36 @@ module Ace
|
|
|
182
220
|
cmd
|
|
183
221
|
end
|
|
184
222
|
|
|
223
|
+
def build_codex_interactive_command(prompt, options, working_dir: nil)
|
|
224
|
+
working_dir ||= Atoms::ExecutionContext.resolve_working_dir(
|
|
225
|
+
working_dir: options[:working_dir],
|
|
226
|
+
subprocess_env: options[:subprocess_env]
|
|
227
|
+
)
|
|
228
|
+
cmd = ["codex"]
|
|
229
|
+
|
|
230
|
+
if options[:sandbox]
|
|
231
|
+
cmd << "--sandbox" << options[:sandbox].to_s
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if @model && @model != DEFAULT_MODEL
|
|
235
|
+
cmd << "--model" << @model
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if (git_dir = Atoms::WorktreeDirResolver.call(working_dir: working_dir))
|
|
239
|
+
cmd << "--add-dir" << git_dir
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
cmd.concat(
|
|
243
|
+
normalized_cli_args_without_conflicts(
|
|
244
|
+
options,
|
|
245
|
+
forbidden_flags: ["exec", "review", "--output-last-message"],
|
|
246
|
+
label: "Codex"
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
cmd << prompt.to_s unless prompt.to_s.empty?
|
|
250
|
+
cmd
|
|
251
|
+
end
|
|
252
|
+
|
|
185
253
|
def execute_codex_command(cmd, prompt, options)
|
|
186
254
|
# Prepare the input - combine system prompt with user prompt if needed
|
|
187
255
|
input = prompt.to_s
|
|
@@ -206,6 +274,8 @@ module Ace
|
|
|
206
274
|
timeout: timeout_val,
|
|
207
275
|
stdin_data: input,
|
|
208
276
|
chdir: working_dir,
|
|
277
|
+
env: options[:subprocess_env],
|
|
278
|
+
command_prefix: options[:subprocess_command_prefix],
|
|
209
279
|
provider_name: "Codex"
|
|
210
280
|
)
|
|
211
281
|
end
|
|
@@ -245,7 +245,11 @@ module Ace
|
|
|
245
245
|
end
|
|
246
246
|
|
|
247
247
|
def create_prompt_cache_dir(working_dir = nil, subprocess_env: nil)
|
|
248
|
-
|
|
248
|
+
resolved_working_dir = Atoms::ExecutionContext.resolve_working_dir(
|
|
249
|
+
working_dir: working_dir,
|
|
250
|
+
subprocess_env: subprocess_env
|
|
251
|
+
)
|
|
252
|
+
cache_dir = File.join(resolved_working_dir, ".ace-local", "llm", "prompts")
|
|
249
253
|
FileUtils.mkdir_p(cache_dir) unless Dir.exist?(cache_dir)
|
|
250
254
|
cache_dir
|
|
251
255
|
end
|
|
@@ -256,17 +260,18 @@ module Ace
|
|
|
256
260
|
|
|
257
261
|
def execute_gemini_command(cmd, prompt, options)
|
|
258
262
|
timeout_val = options[:timeout] || @options[:timeout] || 120
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
working_dir = Atoms::ExecutionContext.resolve_working_dir(
|
|
264
|
+
working_dir: options[:working_dir],
|
|
265
|
+
subprocess_env: options[:subprocess_env]
|
|
266
|
+
)
|
|
267
|
+
Molecules::SafeCapture.call(
|
|
268
|
+
cmd,
|
|
269
|
+
timeout: timeout_val,
|
|
270
|
+
chdir: working_dir,
|
|
271
|
+
env: options[:subprocess_env],
|
|
272
|
+
command_prefix: options[:subprocess_command_prefix],
|
|
273
|
+
provider_name: "Gemini"
|
|
268
274
|
)
|
|
269
|
-
Ace::Support::Fs::Molecules::ProjectRootFinder.find(start_path: start_path) || start_path
|
|
270
275
|
end
|
|
271
276
|
|
|
272
277
|
def parse_gemini_response(stdout, stderr, status, prompt, options)
|
|
@@ -27,13 +27,15 @@ module Ace
|
|
|
27
27
|
# @return [Array(String, String, Process::Status)] [stdout, stderr, status]
|
|
28
28
|
# @raise [Ace::LLM::ProviderError] on timeout
|
|
29
29
|
def self.call(cmd, timeout:, stdin_data: nil, chdir: nil, env: nil, provider_name: "CLI",
|
|
30
|
+
command_prefix: nil,
|
|
30
31
|
isolate_process_group: true, cleanup_group_on_exit: true)
|
|
31
32
|
normalized_timeout = normalize_timeout(timeout)
|
|
32
33
|
opts = {}
|
|
33
34
|
opts[:chdir] = chdir if chdir
|
|
34
35
|
opts[:pgroup] = true if isolate_process_group
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
full_cmd = Array(command_prefix) + cmd
|
|
38
|
+
args = env ? [env, *full_cmd] : full_cmd
|
|
37
39
|
|
|
38
40
|
Open3.popen3(*args, **opts) do |stdin, stdout, stderr, wait_thr|
|
|
39
41
|
pid = wait_thr.pid
|
|
@@ -57,7 +57,7 @@ module Ace
|
|
|
57
57
|
full_prompt = rewrite_skill_commands(full_prompt, working_dir: working_dir)
|
|
58
58
|
|
|
59
59
|
cmd = build_pi_command(full_prompt, options, system_prompt: system_prompt)
|
|
60
|
-
stdout, stderr, status = execute_pi_command(cmd, working_dir: working_dir)
|
|
60
|
+
stdout, stderr, status = execute_pi_command(cmd, working_dir: working_dir, options: options)
|
|
61
61
|
|
|
62
62
|
parse_pi_response(stdout, stderr, status, full_prompt, options)
|
|
63
63
|
rescue => e
|
|
@@ -77,6 +77,31 @@ module Ace
|
|
|
77
77
|
]
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def interactive_supported?
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_interactive_invocation(messages, **options)
|
|
85
|
+
validate_pi_availability!
|
|
86
|
+
|
|
87
|
+
prompt = format_messages_as_prompt(messages)
|
|
88
|
+
full_prompt, system_prompt = build_full_prompt(prompt, options)
|
|
89
|
+
subprocess_env = options[:subprocess_env]
|
|
90
|
+
working_dir = Atoms::ExecutionContext.resolve_working_dir(
|
|
91
|
+
working_dir: options[:working_dir],
|
|
92
|
+
subprocess_env: subprocess_env
|
|
93
|
+
)
|
|
94
|
+
full_prompt = rewrite_skill_commands(full_prompt, working_dir: working_dir)
|
|
95
|
+
|
|
96
|
+
cmd = build_pi_interactive_command(full_prompt, options, system_prompt: system_prompt)
|
|
97
|
+
{
|
|
98
|
+
command: cmd,
|
|
99
|
+
env: subprocess_env,
|
|
100
|
+
working_dir: working_dir,
|
|
101
|
+
prompt: full_prompt
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
80
105
|
private
|
|
81
106
|
|
|
82
107
|
def format_messages_as_prompt(messages)
|
|
@@ -178,6 +203,31 @@ module Ace
|
|
|
178
203
|
cmd
|
|
179
204
|
end
|
|
180
205
|
|
|
206
|
+
def build_pi_interactive_command(full_prompt, options, system_prompt: nil)
|
|
207
|
+
cmd = ["pi"]
|
|
208
|
+
|
|
209
|
+
if system_prompt
|
|
210
|
+
cmd << "--system-prompt" << system_prompt
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
model_to_use = @model || @generation_config[:model] || DEFAULT_MODEL
|
|
214
|
+
provider_name, model_id = split_provider_model(model_to_use)
|
|
215
|
+
if provider_name && model_id
|
|
216
|
+
cmd << "--provider" << provider_name
|
|
217
|
+
cmd << "--model" << model_id
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
cmd.concat(
|
|
221
|
+
normalized_cli_args_without_conflicts(
|
|
222
|
+
options,
|
|
223
|
+
forbidden_flags: ["-p", "--print", "--no-session", "--no-skills", "--mode"],
|
|
224
|
+
label: "Pi"
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
cmd << full_prompt.to_s unless full_prompt.to_s.empty?
|
|
228
|
+
cmd
|
|
229
|
+
end
|
|
230
|
+
|
|
181
231
|
# Split "provider/model" into ["provider", "model"]
|
|
182
232
|
# Handles multi-segment providers like "google-gemini-cli/gemini-2.5-pro"
|
|
183
233
|
# Also handles nested providers like "openrouter:openai/gpt-oss-120b"
|
|
@@ -200,13 +250,15 @@ module Ace
|
|
|
200
250
|
[parts[0], parts[1]]
|
|
201
251
|
end
|
|
202
252
|
|
|
203
|
-
def execute_pi_command(cmd, timeout: nil, working_dir: nil)
|
|
253
|
+
def execute_pi_command(cmd, timeout: nil, working_dir: nil, options: {})
|
|
204
254
|
timeout_val = timeout || @options[:timeout] || 120
|
|
205
255
|
Molecules::SafeCapture.call(
|
|
206
256
|
cmd,
|
|
207
257
|
timeout: timeout_val,
|
|
208
258
|
stdin_data: "",
|
|
209
259
|
chdir: working_dir,
|
|
260
|
+
env: options[:subprocess_env],
|
|
261
|
+
command_prefix: options[:subprocess_command_prefix],
|
|
210
262
|
provider_name: "Pi"
|
|
211
263
|
)
|
|
212
264
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-llm-providers-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-llm
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- lib/ace/llm/providers/cli/atoms/command_formatters.rb
|
|
92
92
|
- lib/ace/llm/providers/cli/atoms/command_rewriter.rb
|
|
93
93
|
- lib/ace/llm/providers/cli/atoms/execution_context.rb
|
|
94
|
+
- lib/ace/llm/providers/cli/atoms/interactive_startup_policy.rb
|
|
94
95
|
- lib/ace/llm/providers/cli/atoms/provider_detector.rb
|
|
95
96
|
- lib/ace/llm/providers/cli/atoms/session_finders/claude_session_finder.rb
|
|
96
97
|
- lib/ace/llm/providers/cli/atoms/session_finders/codex_session_finder.rb
|