ace-llm-providers-cli 0.33.0 → 0.34.1
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 +45 -15
- data/.ace-defaults/llm/providers/pi.yml +1 -0
- data/CHANGELOG.md +24 -0
- data/lib/ace/llm/providers/cli/cli_args_support.rb +1 -1
- data/lib/ace/llm/providers/cli/codex_client.rb +77 -28
- data/lib/ace/llm/providers/cli/pi_client.rb +70 -38
- data/lib/ace/llm/providers/cli/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9aee773ea6aac47cb8aeaa00ca6926a81f5d9eaac29f9b968ba098d81da0117d
|
|
4
|
+
data.tar.gz: ac05f84f681e6316ac4662750e923e06a03692ee88201456f56da5afae904079
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 707c07914d35d16e3e868b4891a0be562930edd9ce5d79a41eb0a99f6628bc90b3cc5eff2f9ad7adc122b3e674431e4825ae2a2602354e0386bd1c0cf737e0d7
|
|
7
|
+
data.tar.gz: d10514be9fbb31940a1cd96c7ba35807eddedaa047aacf8fd09d35085d82c4e742db8efdf6261889236839056493305ec6f781836f38180a69fc3c9af2332c18
|
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
name: codex
|
|
2
|
-
last_synced: 2026-04-
|
|
2
|
+
last_synced: 2026-04-24
|
|
3
3
|
class: Ace::LLM::Providers::CLI::CodexClient
|
|
4
4
|
gem: ace-llm-providers-cli
|
|
5
5
|
models_dev_id: openai
|
|
6
|
-
context_limit: 1050000
|
|
7
6
|
models:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
- gpt-5.6-terra
|
|
8
|
+
- gpt-6-astra
|
|
9
|
+
- gpt-5.6-sol
|
|
10
|
+
- gpt-5.6-luna
|
|
11
|
+
- gpt-5.3-chat-latest
|
|
12
|
+
- gpt-5.3-codex
|
|
13
|
+
- gpt-5.3-codex-spark
|
|
14
|
+
- gpt-5.4
|
|
15
|
+
- gpt-5.4-mini
|
|
16
|
+
- gpt-5.4-nano
|
|
17
|
+
- gpt-5.4-pro
|
|
18
|
+
limits:
|
|
19
|
+
models:
|
|
20
|
+
gpt-5.3-chat-latest:
|
|
21
|
+
context: 128000
|
|
22
|
+
output: 16384
|
|
23
|
+
gpt-5.3-codex:
|
|
24
|
+
context: 400000
|
|
25
|
+
output: 128000
|
|
26
|
+
gpt-5.3-codex-spark:
|
|
27
|
+
context: 128000
|
|
28
|
+
output: 32000
|
|
29
|
+
gpt-5.4:
|
|
30
|
+
context: 1050000
|
|
31
|
+
output: 128000
|
|
32
|
+
gpt-5.4-mini:
|
|
33
|
+
context: 400000
|
|
34
|
+
output: 128000
|
|
35
|
+
gpt-5.4-nano:
|
|
36
|
+
context: 400000
|
|
37
|
+
output: 128000
|
|
38
|
+
gpt-5.4-pro:
|
|
39
|
+
context: 1050000
|
|
40
|
+
output: 128000
|
|
15
41
|
aliases:
|
|
16
42
|
model:
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
astra: gpt-6-astra
|
|
44
|
+
sol: gpt-5.6-sol
|
|
45
|
+
terra: gpt-5.6-terra
|
|
46
|
+
luna: gpt-5.6-luna
|
|
47
|
+
gpt: gpt-5.6-terra
|
|
48
|
+
codex: gpt-5.6-terra
|
|
49
|
+
mini: gpt-5.6-luna
|
|
19
50
|
spark: gpt-5.3-codex-spark
|
|
20
|
-
mini: gpt-5.4-mini
|
|
21
51
|
capabilities:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
- text_generation
|
|
53
|
+
- code_generation
|
|
54
|
+
- function_calling
|
|
25
55
|
default_options:
|
|
26
56
|
temperature: 0.7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.34.1] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Preserve completed Codex responses when copying the optional last-message diagnostic fails, and handle Pi retry streams without accepting partial output.
|
|
15
|
+
- Reject unresolved Pi model selectors before CLI execution and keep the `glm5` alias synchronized across provider registries.
|
|
16
|
+
- Preserve Codex JSON response text containing a preset path without mistaking it for the CLI banner.
|
|
17
|
+
- Reject directory destinations for Codex last-message capture and preserve the final Pi stop reason when earlier turns appear in terminal events.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Restrict Pi `@ro` to read-only tools (`read`, `grep`, `find`, `ls`) and disable extensions.
|
|
22
|
+
|
|
23
|
+
## [0.34.0] - 2026-09-13
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Register Codex Astra, Sol, Terra and Luna with Terra as the generic default and Luna as mini; retain all existing explicit IDs and omit unverified new-model limits.
|
|
28
|
+
- Derive Codex client defaults and model listings from provider configuration instead of a separate hardcoded catalog.
|
|
29
|
+
|
|
30
|
+
### Technical
|
|
31
|
+
|
|
32
|
+
- Isolate subprocess cleanup test fixtures from login-shell startup while retaining timeout and descendant-termination assertions.
|
|
33
|
+
|
|
10
34
|
### Fixed
|
|
11
35
|
- Hardened `SafeCapture` process group termination for container PID namespaces where background processes reparent to PID 1, ensuring reliable cleanup of descendant processes without ESRCH failures.
|
|
12
36
|
|
|
@@ -18,7 +18,7 @@ module Ace
|
|
|
18
18
|
conflict = find_conflicting_cli_arg(args, forbidden_flags)
|
|
19
19
|
return args unless conflict
|
|
20
20
|
|
|
21
|
-
raise Ace::LLM::ProviderError, "#{label}
|
|
21
|
+
raise Ace::LLM::ProviderError, "#{label} does not support cli arg #{conflict} in this invocation"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def find_conflicting_cli_arg(args, forbidden_flags)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "open3"
|
|
5
5
|
require "shellwords"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
require "fileutils"
|
|
6
8
|
|
|
7
9
|
require_relative "cli_args_support"
|
|
8
10
|
require_relative "atoms/execution_context"
|
|
@@ -30,11 +32,9 @@ module Ace
|
|
|
30
32
|
"codex"
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
# Default model (can be overridden by config)
|
|
34
|
-
DEFAULT_MODEL = "gpt-5.4"
|
|
35
|
-
|
|
36
35
|
def initialize(model: nil, **options)
|
|
37
|
-
@model = model ||
|
|
36
|
+
@model = model || Ace::LLM::Molecules::ClientRegistry.new.models_for_provider("codex").first
|
|
37
|
+
raise Ace::LLM::ConfigurationError, "No default model configured for codex" if @model.to_s.empty?
|
|
38
38
|
# Skip normal BaseClient initialization that requires API key
|
|
39
39
|
@options = options
|
|
40
40
|
@generation_config = options[:generation_config] || {}
|
|
@@ -61,25 +61,49 @@ module Ace
|
|
|
61
61
|
subprocess_env: subprocess_env
|
|
62
62
|
)
|
|
63
63
|
prompt = rewrite_skill_commands(prompt, working_dir: working_dir)
|
|
64
|
+
requested_last_message = File.expand_path(options[:last_message_file], working_dir) if options[:last_message_file]
|
|
65
|
+
if requested_last_message && File.directory?(requested_last_message)
|
|
66
|
+
raise Ace::LLM::ProviderError, "last_message_file must name a file, not a directory: #{requested_last_message}"
|
|
67
|
+
end
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
Tempfile.create(["ace-codex-last-", ".md"]) do |last_message|
|
|
70
|
+
last_message.close
|
|
71
|
+
direct_capture = requested_last_message && File.writable?(File.dirname(requested_last_message)) && !File.directory?(requested_last_message)
|
|
72
|
+
execution_options = options.merge(last_message_file: direct_capture ? requested_last_message : last_message.path)
|
|
73
|
+
cmd = build_codex_command(prompt, execution_options, working_dir: working_dir)
|
|
74
|
+
if direct_capture && File.exist?(requested_last_message)
|
|
75
|
+
begin
|
|
76
|
+
File.delete(requested_last_message)
|
|
77
|
+
rescue SystemCallError
|
|
78
|
+
direct_capture = false
|
|
79
|
+
execution_options = options.merge(last_message_file: last_message.path)
|
|
80
|
+
cmd = build_codex_command(prompt, execution_options, working_dir: working_dir)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
completed = false
|
|
84
|
+
result = nil
|
|
85
|
+
begin
|
|
86
|
+
stdout, stderr, status = execute_codex_command(cmd, prompt, execution_options)
|
|
87
|
+
result = parse_codex_response(stdout, stderr, status, prompt, execution_options)
|
|
88
|
+
completed = true
|
|
89
|
+
result
|
|
90
|
+
ensure
|
|
91
|
+
if requested_last_message && !direct_capture && (completed || File.size?(last_message.path))
|
|
92
|
+
begin
|
|
93
|
+
FileUtils.cp(last_message.path, requested_last_message)
|
|
94
|
+
rescue => copy_error
|
|
95
|
+
result[:metadata][:last_message_copy_error] = copy_error.message if completed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
69
100
|
rescue => e
|
|
70
101
|
handle_codex_error(e)
|
|
71
102
|
end
|
|
72
103
|
|
|
73
104
|
# List available Codex models
|
|
74
105
|
def list_models
|
|
75
|
-
|
|
76
|
-
# Actual models come from YAML config
|
|
77
|
-
[
|
|
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}
|
|
82
|
-
]
|
|
106
|
+
Ace::LLM::Molecules::ClientRegistry.new.models_for_provider("codex").map { |id| {id: id, name: id} }
|
|
83
107
|
end
|
|
84
108
|
|
|
85
109
|
def interactive_supported?
|
|
@@ -209,8 +233,15 @@ module Ace
|
|
|
209
233
|
cmd << "--output-last-message" << options[:last_message_file]
|
|
210
234
|
end
|
|
211
235
|
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
cli_args = normalized_cli_args_without_conflicts(
|
|
237
|
+
options,
|
|
238
|
+
forbidden_flags: ["--model", "-m", "--profile", "--output-last-message"],
|
|
239
|
+
label: "Codex"
|
|
240
|
+
)
|
|
241
|
+
if codex_cli_args_override_model?(cli_args)
|
|
242
|
+
raise Ace::LLM::ProviderError, "Codex cli args cannot override the resolved model"
|
|
243
|
+
end
|
|
244
|
+
cmd.concat(cli_args)
|
|
214
245
|
|
|
215
246
|
cmd
|
|
216
247
|
end
|
|
@@ -242,17 +273,23 @@ module Ace
|
|
|
242
273
|
cmd << "-c" << trust_override
|
|
243
274
|
end
|
|
244
275
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
label: "Codex"
|
|
250
|
-
)
|
|
276
|
+
cli_args = normalized_cli_args_without_conflicts(
|
|
277
|
+
options,
|
|
278
|
+
forbidden_flags: ["exec", "review", "-C", "--cd", "--output-last-message", "--model", "-m", "--profile"],
|
|
279
|
+
label: "Codex"
|
|
251
280
|
)
|
|
281
|
+
raise Ace::LLM::ProviderError, "Codex cli args cannot override the resolved model" if codex_cli_args_override_model?(cli_args)
|
|
282
|
+
cmd.concat(cli_args)
|
|
252
283
|
cmd << prompt.to_s unless prompt.to_s.empty?
|
|
253
284
|
cmd
|
|
254
285
|
end
|
|
255
286
|
|
|
287
|
+
def codex_cli_args_override_model?(args)
|
|
288
|
+
args.any? { |arg| arg.match?(/\A-m.+/) } ||
|
|
289
|
+
args.each_cons(2).any? { |flag, value| %w[-c --config].include?(flag) && value.match?(/\A(?:model|model_provider|profiles\.[^.]+\.(?:model|model_provider))\s*=/) } ||
|
|
290
|
+
args.any? { |arg| arg.match?(/\A(?:-c|--config)=?(?:model|model_provider|profiles\.[^.]+\.(?:model|model_provider))\s*=/) }
|
|
291
|
+
end
|
|
292
|
+
|
|
256
293
|
def execute_codex_command(cmd, prompt, options)
|
|
257
294
|
# Prepare the input - combine system prompt with user prompt if needed
|
|
258
295
|
input = prompt.to_s
|
|
@@ -289,16 +326,28 @@ module Ace
|
|
|
289
326
|
raise Ace::LLM::ProviderError, "Codex CLI failed: #{error_msg}"
|
|
290
327
|
end
|
|
291
328
|
|
|
292
|
-
#
|
|
329
|
+
# The native last-message file contains only the final response,
|
|
330
|
+
# unlike stdout's transcript of tools and status messages.
|
|
331
|
+
last_message_path = options[:last_message_file]
|
|
332
|
+
if last_message_path
|
|
333
|
+
text = File.file?(last_message_path) ? File.read(last_message_path).strip : ""
|
|
334
|
+
raise Ace::LLM::ProviderError, "Codex CLI produced no final message" if text.empty?
|
|
335
|
+
|
|
336
|
+
return {text: text, metadata: build_synthetic_metadata(text, prompt)}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Older CLI wrappers may supply stdout only. Parse its transcript
|
|
340
|
+
# only when a real CLI banner is present, not when report prose
|
|
341
|
+
# happens to contain a standalone `codex` line.
|
|
293
342
|
# Codex output includes metadata lines and the actual response
|
|
294
343
|
lines = stdout.split("\n")
|
|
295
344
|
|
|
296
|
-
|
|
297
|
-
response_start = lines.find_index { |line| line.
|
|
345
|
+
has_cli_banner = lines.first(20).any? { |line| line.start_with?("OpenAI Codex v") }
|
|
346
|
+
response_start = lines.find_index { |line| line.strip == "codex" } if has_cli_banner
|
|
298
347
|
|
|
299
348
|
if response_start && response_start < lines.length - 1
|
|
300
349
|
# Extract text after the "codex" line, skipping empty lines
|
|
301
|
-
response_lines = lines[(response_start + 1)
|
|
350
|
+
response_lines = lines[(response_start + 1)..]
|
|
302
351
|
# Remove token usage lines at the end
|
|
303
352
|
response_lines = response_lines.reject { |line| line.include?("tokens used") }
|
|
304
353
|
text = response_lines.join("\n").strip
|
|
@@ -183,6 +183,10 @@ module Ace
|
|
|
183
183
|
# No session (stateless)
|
|
184
184
|
cmd << "--no-session"
|
|
185
185
|
|
|
186
|
+
# Keep the terminal stop reason so truncated responses cannot pass
|
|
187
|
+
# as successful plain-text output.
|
|
188
|
+
cmd << "--mode" << "json"
|
|
189
|
+
|
|
186
190
|
# No skills (we handle skill content ourselves in one-shot mode)
|
|
187
191
|
cmd << "--no-skills"
|
|
188
192
|
|
|
@@ -194,13 +198,16 @@ module Ace
|
|
|
194
198
|
# Provider/model from the model string (format: "provider/model")
|
|
195
199
|
model_to_use = @model || @generation_config[:model] || DEFAULT_MODEL
|
|
196
200
|
provider_name, model_id = split_provider_model(model_to_use)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
cmd << "--model" << model_id
|
|
200
|
-
end
|
|
201
|
+
cmd << "--provider" << provider_name
|
|
202
|
+
cmd << "--model" << model_id
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
cmd.concat(
|
|
205
|
+
normalized_cli_args_without_conflicts(
|
|
206
|
+
options,
|
|
207
|
+
forbidden_flags: ["--mode", "--provider", "--model", "--models"],
|
|
208
|
+
label: "Pi"
|
|
209
|
+
)
|
|
210
|
+
)
|
|
204
211
|
|
|
205
212
|
cmd
|
|
206
213
|
end
|
|
@@ -214,15 +221,13 @@ module Ace
|
|
|
214
221
|
|
|
215
222
|
model_to_use = @model || @generation_config[:model] || DEFAULT_MODEL
|
|
216
223
|
provider_name, model_id = split_provider_model(model_to_use)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
cmd << "--model" << model_id
|
|
220
|
-
end
|
|
224
|
+
cmd << "--provider" << provider_name
|
|
225
|
+
cmd << "--model" << model_id
|
|
221
226
|
|
|
222
227
|
cmd.concat(
|
|
223
228
|
normalized_cli_args_without_conflicts(
|
|
224
229
|
options,
|
|
225
|
-
forbidden_flags: ["-p", "--print", "--no-session", "--no-skills", "--mode"],
|
|
230
|
+
forbidden_flags: ["-p", "--print", "--no-session", "--no-skills", "--mode", "--provider", "--model", "--models"],
|
|
226
231
|
label: "Pi"
|
|
227
232
|
)
|
|
228
233
|
)
|
|
@@ -234,20 +239,25 @@ module Ace
|
|
|
234
239
|
# Handles multi-segment providers like "google-gemini-cli/gemini-2.5-pro"
|
|
235
240
|
# Also handles nested providers like "openrouter:openai/gpt-oss-120b"
|
|
236
241
|
def split_provider_model(model_string)
|
|
237
|
-
|
|
242
|
+
raise Ace::LLM::ProviderError, "Pi model must resolve to provider/model" unless model_string
|
|
243
|
+
raise Ace::LLM::ProviderError, "Pi model must resolve to provider/model: #{model_string}" if model_string.start_with?(":")
|
|
238
244
|
|
|
239
245
|
# Check for nested provider pattern (e.g., "openrouter:openai/model")
|
|
240
|
-
|
|
246
|
+
colon = model_string.index(":")
|
|
247
|
+
slash = model_string.index("/")
|
|
248
|
+
if colon && (!slash || colon < slash)
|
|
241
249
|
parts = model_string.split(":", 2)
|
|
242
|
-
|
|
250
|
+
nested_parts = parts[1]&.split("/", 2)
|
|
251
|
+
if parts.length == 2 && !parts[0].empty? && nested_parts&.length == 2 && nested_parts.all? { |part| !part.empty? }
|
|
243
252
|
# Nested provider: "openrouter:openai/model" -> ["openrouter", "openai/model"]
|
|
244
253
|
return [parts[0], parts[1]]
|
|
245
254
|
end
|
|
255
|
+
raise Ace::LLM::ProviderError, "Pi model must resolve to provider/model: #{model_string}"
|
|
246
256
|
end
|
|
247
257
|
|
|
248
258
|
# Standard provider/model format
|
|
249
259
|
parts = model_string.split("/", 2)
|
|
250
|
-
|
|
260
|
+
raise Ace::LLM::ProviderError, "Pi model must resolve to provider/model: #{model_string}" unless parts.length == 2 && parts.all? { |part| !part.empty? }
|
|
251
261
|
|
|
252
262
|
[parts[0], parts[1]]
|
|
253
263
|
end
|
|
@@ -276,15 +286,8 @@ module Ace
|
|
|
276
286
|
raise Ace::LLM::ProviderError, "Pi CLI failed: #{error_msg}"
|
|
277
287
|
end
|
|
278
288
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
text, usage = parse_ndjson(stdout)
|
|
282
|
-
response = {"usage" => normalize_usage(usage)}
|
|
283
|
-
else
|
|
284
|
-
# Plain text output
|
|
285
|
-
text = stdout.strip
|
|
286
|
-
response = {}
|
|
287
|
-
end
|
|
289
|
+
text, usage, finish_reason = parse_ndjson(stdout)
|
|
290
|
+
response = {"usage" => normalize_usage(usage), "finish_reason" => finish_reason}
|
|
288
291
|
|
|
289
292
|
metadata = build_metadata(response, text, prompt, options)
|
|
290
293
|
|
|
@@ -320,36 +323,65 @@ module Ace
|
|
|
320
323
|
lines = stdout.split("\n")
|
|
321
324
|
text_parts = []
|
|
322
325
|
usage = nil
|
|
326
|
+
terminal_event = nil
|
|
327
|
+
settled = false
|
|
328
|
+
message_stop_reason = nil
|
|
323
329
|
|
|
324
330
|
lines.each do |line|
|
|
325
331
|
next if line.strip.empty?
|
|
326
332
|
event = JSON.parse(line)
|
|
333
|
+
raise Ace::LLM::ProviderError, "Pi JSON stream contains a non-object event" unless event.is_a?(Hash)
|
|
327
334
|
case event["type"]
|
|
328
335
|
when "message_end"
|
|
329
336
|
# Extract text from content array
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
text_parts
|
|
337
|
+
message = event["message"] || {}
|
|
338
|
+
if message["role"].nil? || message["role"] == "assistant"
|
|
339
|
+
text_parts = extract_message_text(message)
|
|
340
|
+
usage = message["usage"]
|
|
341
|
+
message_stop_reason = message["stopReason"] if message["stopReason"]
|
|
333
342
|
end
|
|
334
|
-
usage = event.dig("message", "usage")
|
|
335
343
|
when "agent_end"
|
|
336
|
-
|
|
344
|
+
settled = false
|
|
345
|
+
if event["willRetry"]
|
|
346
|
+
text_parts.clear
|
|
347
|
+
usage = nil
|
|
348
|
+
terminal_event = nil
|
|
349
|
+
message_stop_reason = nil
|
|
350
|
+
next
|
|
351
|
+
end
|
|
352
|
+
terminal_event = event
|
|
337
353
|
messages = event["messages"] || []
|
|
338
|
-
messages.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
end
|
|
354
|
+
if (last_assistant = messages.reverse.find { |msg| (msg["role"].nil? || msg["role"] == "assistant") && extract_message_text(msg).any? })
|
|
355
|
+
text_parts = extract_message_text(last_assistant)
|
|
356
|
+
usage ||= last_assistant["usage"]
|
|
357
|
+
message_stop_reason ||= last_assistant["stopReason"]
|
|
343
358
|
end
|
|
344
|
-
|
|
359
|
+
when "agent_settled"
|
|
360
|
+
settled = true if terminal_event
|
|
345
361
|
end
|
|
346
362
|
end
|
|
347
363
|
|
|
364
|
+
raise Ace::LLM::ProviderError, "Pi JSON stream ended without agent_end" unless terminal_event
|
|
365
|
+
raise Ace::LLM::ProviderError, "Pi JSON stream ended without agent_settled" unless settled
|
|
366
|
+
|
|
348
367
|
text = text_parts.join("")
|
|
349
|
-
|
|
368
|
+
raise Ace::LLM::ProviderError, "Pi JSON stream produced no final assistant message" if text.strip.empty?
|
|
369
|
+
terminal_reason = terminal_event["stopReason"] || terminal_event.dig("messages", -1, "stopReason")
|
|
370
|
+
successful_reasons = Ace::LLM::SUCCESSFUL_FINISH_REASONS
|
|
371
|
+
finish_reason = if %w[error aborted].include?(terminal_reason.to_s.downcase)
|
|
372
|
+
terminal_reason
|
|
373
|
+
elsif message_stop_reason && !successful_reasons.include?(message_stop_reason.to_s.downcase)
|
|
374
|
+
message_stop_reason
|
|
375
|
+
else
|
|
376
|
+
terminal_reason || message_stop_reason || "success"
|
|
377
|
+
end
|
|
378
|
+
[text, usage || {}, finish_reason]
|
|
350
379
|
rescue JSON::ParserError
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
raise Ace::LLM::ProviderError, "Pi JSON stream contains invalid events"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def extract_message_text(message)
|
|
384
|
+
Array(message["content"]).filter_map { |content| content["text"] if content["type"] == "text" }
|
|
353
385
|
end
|
|
354
386
|
|
|
355
387
|
# Normalize Pi usage field names to our standard format.
|
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.34.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-llm
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.40'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.
|
|
25
|
+
version: '0.40'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rake
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|