codexcw 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {codexcw-0.2.2 → codexcw-0.3.0}/Cargo.lock +3 -3
- {codexcw-0.2.2 → codexcw-0.3.0}/Cargo.toml +2 -2
- {codexcw-0.2.2 → codexcw-0.3.0}/PKG-INFO +22 -10
- {codexcw-0.2.2/bindings/python → codexcw-0.3.0}/README.md +20 -8
- {codexcw-0.2.2 → codexcw-0.3.0/bindings/python}/README.md +20 -8
- {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/src/lib.rs +210 -15
- codexcw-0.3.0/bindings/python/tests/fixtures/fake_claude.sh +38 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/tests/test_smoke.py +176 -1
- {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/uv.lock +1 -1
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/Cargo.toml +1 -1
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/README.md +39 -10
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/args.rs +14 -2
- codexcw-0.3.0/crates/codexcw/src/claude.rs +890 -0
- codexcw-0.3.0/crates/codexcw/src/claude_account_usage.rs +184 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/decoder.rs +17 -1
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/error.rs +39 -11
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/event.rs +77 -26
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/group.rs +4 -4
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/lib.rs +23 -10
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/request.rs +41 -9
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/runner.rs +132 -44
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/session.rs +5 -5
- codexcw-0.3.0/crates/codexcw/tests/claude_account_usage_it.rs +135 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/runner_it.rs +115 -1
- {codexcw-0.2.2 → codexcw-0.3.0}/pyproject.toml +2 -2
- {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/__init__.py +94 -12
- {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/_codexcw.pyi +29 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/aio.py +20 -3
- {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/Cargo.toml +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/tests/fixtures/fake_codex.sh +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/examples/run.rs +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/account_usage.rs +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/tail.rs +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/account_usage_it.rs +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/common/mod.rs +0 -0
- {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/py.typed +0 -0
|
@@ -22,7 +22,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
22
22
|
|
|
23
23
|
[[package]]
|
|
24
24
|
name = "codexcw"
|
|
25
|
-
version = "0.
|
|
25
|
+
version = "0.3.0"
|
|
26
26
|
dependencies = [
|
|
27
27
|
"serde",
|
|
28
28
|
"serde_json",
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "codexcw-node"
|
|
38
|
-
version = "0.
|
|
38
|
+
version = "0.3.0"
|
|
39
39
|
dependencies = [
|
|
40
40
|
"codexcw",
|
|
41
41
|
"napi",
|
|
@@ -47,7 +47,7 @@ dependencies = [
|
|
|
47
47
|
|
|
48
48
|
[[package]]
|
|
49
49
|
name = "codexcw-py"
|
|
50
|
-
version = "0.
|
|
50
|
+
version = "0.3.0"
|
|
51
51
|
dependencies = [
|
|
52
52
|
"codexcw",
|
|
53
53
|
"pyo3",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/codexcw", "bindings/python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.3.0"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
rust-version = "1.90"
|
|
9
9
|
license = "CC0-1.0"
|
|
@@ -12,7 +12,7 @@ homepage = "https://github.com/c3-oss/codexcw"
|
|
|
12
12
|
authors = ["c3-oss"]
|
|
13
13
|
|
|
14
14
|
[workspace.dependencies]
|
|
15
|
-
codexcw = { path = "crates/codexcw", version = "0.
|
|
15
|
+
codexcw = { path = "crates/codexcw", version = "0.3.0" }
|
|
16
16
|
tokio = { version = "1", default-features = false, features = [
|
|
17
17
|
"process",
|
|
18
18
|
"io-util",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codexcw
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Operating System :: POSIX
|
|
@@ -9,7 +9,7 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
9
9
|
Requires-Dist: pytest>=8 ; extra == 'dev'
|
|
10
10
|
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
11
11
|
Provides-Extra: dev
|
|
12
|
-
Summary: Run
|
|
12
|
+
Summary: Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
|
|
13
13
|
Keywords: codex,automation,agent,cli
|
|
14
14
|
Author: c3-oss
|
|
15
15
|
License: CC0-1.0
|
|
@@ -20,14 +20,19 @@ Project-URL: Repository, https://github.com/c3-oss/codexcw
|
|
|
20
20
|
|
|
21
21
|
# codexcw (Python)
|
|
22
22
|
|
|
23
|
-
Run
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
|
|
24
|
+
The Codex agent wraps `codex exec --json`; the Claude agent wraps
|
|
25
|
+
`claude -p --output-format stream-json`. Both expose iterables, callbacks,
|
|
26
|
+
results, typed usage, and typed errors through the same API.
|
|
26
27
|
|
|
27
|
-
The
|
|
28
|
-
support `codex exec --json
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
The selected agent executable must be on `PATH` and authenticated. Codex must
|
|
29
|
+
support `codex exec --json`; Claude must support `--output-format stream-json`.
|
|
30
|
+
Defaults are automation-friendly: ephemeral sessions and non-interactive
|
|
31
|
+
execution, with Codex using a read-only sandbox and approval `never`.
|
|
32
|
+
|
|
33
|
+
The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
|
|
34
|
+
events are normalized into the same event model, with model selection via the
|
|
35
|
+
`haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
|
|
31
36
|
|
|
32
37
|
## Install
|
|
33
38
|
|
|
@@ -43,6 +48,8 @@ from codexcw import Runner, Request
|
|
|
43
48
|
runner = Runner()
|
|
44
49
|
result = runner.run(Request(prompt="diga oi"))
|
|
45
50
|
print(result.final_message)
|
|
51
|
+
print(result.usage.total_tokens)
|
|
52
|
+
print(result.usage.total_cost_usd)
|
|
46
53
|
```
|
|
47
54
|
|
|
48
55
|
## Streaming
|
|
@@ -66,9 +73,14 @@ from codexcw import AccountUsageRequest, get_account_usage
|
|
|
66
73
|
usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
|
|
67
74
|
print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
|
|
68
75
|
print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
|
|
76
|
+
|
|
77
|
+
from codexcw import get_claude_account_usage
|
|
78
|
+
|
|
79
|
+
claude_usage = get_claude_account_usage()
|
|
80
|
+
print(claude_usage.windows)
|
|
69
81
|
```
|
|
70
82
|
|
|
71
|
-
## Running many
|
|
83
|
+
## Running many agent instances
|
|
72
84
|
|
|
73
85
|
```python
|
|
74
86
|
from codexcw import Runner, Request
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# codexcw (Python)
|
|
2
2
|
|
|
3
|
-
Run
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
|
|
4
|
+
The Codex agent wraps `codex exec --json`; the Claude agent wraps
|
|
5
|
+
`claude -p --output-format stream-json`. Both expose iterables, callbacks,
|
|
6
|
+
results, typed usage, and typed errors through the same API.
|
|
6
7
|
|
|
7
|
-
The
|
|
8
|
-
support `codex exec --json
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The selected agent executable must be on `PATH` and authenticated. Codex must
|
|
9
|
+
support `codex exec --json`; Claude must support `--output-format stream-json`.
|
|
10
|
+
Defaults are automation-friendly: ephemeral sessions and non-interactive
|
|
11
|
+
execution, with Codex using a read-only sandbox and approval `never`.
|
|
12
|
+
|
|
13
|
+
The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
|
|
14
|
+
events are normalized into the same event model, with model selection via the
|
|
15
|
+
`haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
|
|
11
16
|
|
|
12
17
|
## Install
|
|
13
18
|
|
|
@@ -23,6 +28,8 @@ from codexcw import Runner, Request
|
|
|
23
28
|
runner = Runner()
|
|
24
29
|
result = runner.run(Request(prompt="diga oi"))
|
|
25
30
|
print(result.final_message)
|
|
31
|
+
print(result.usage.total_tokens)
|
|
32
|
+
print(result.usage.total_cost_usd)
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
## Streaming
|
|
@@ -46,9 +53,14 @@ from codexcw import AccountUsageRequest, get_account_usage
|
|
|
46
53
|
usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
|
|
47
54
|
print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
|
|
48
55
|
print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
|
|
56
|
+
|
|
57
|
+
from codexcw import get_claude_account_usage
|
|
58
|
+
|
|
59
|
+
claude_usage = get_claude_account_usage()
|
|
60
|
+
print(claude_usage.windows)
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
## Running many
|
|
63
|
+
## Running many agent instances
|
|
52
64
|
|
|
53
65
|
```python
|
|
54
66
|
from codexcw import Runner, Request
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# codexcw (Python)
|
|
2
2
|
|
|
3
|
-
Run
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
|
|
4
|
+
The Codex agent wraps `codex exec --json`; the Claude agent wraps
|
|
5
|
+
`claude -p --output-format stream-json`. Both expose iterables, callbacks,
|
|
6
|
+
results, typed usage, and typed errors through the same API.
|
|
6
7
|
|
|
7
|
-
The
|
|
8
|
-
support `codex exec --json
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The selected agent executable must be on `PATH` and authenticated. Codex must
|
|
9
|
+
support `codex exec --json`; Claude must support `--output-format stream-json`.
|
|
10
|
+
Defaults are automation-friendly: ephemeral sessions and non-interactive
|
|
11
|
+
execution, with Codex using a read-only sandbox and approval `never`.
|
|
12
|
+
|
|
13
|
+
The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
|
|
14
|
+
events are normalized into the same event model, with model selection via the
|
|
15
|
+
`haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
|
|
11
16
|
|
|
12
17
|
## Install
|
|
13
18
|
|
|
@@ -23,6 +28,8 @@ from codexcw import Runner, Request
|
|
|
23
28
|
runner = Runner()
|
|
24
29
|
result = runner.run(Request(prompt="diga oi"))
|
|
25
30
|
print(result.final_message)
|
|
31
|
+
print(result.usage.total_tokens)
|
|
32
|
+
print(result.usage.total_cost_usd)
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
## Streaming
|
|
@@ -46,9 +53,14 @@ from codexcw import AccountUsageRequest, get_account_usage
|
|
|
46
53
|
usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
|
|
47
54
|
print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
|
|
48
55
|
print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
|
|
56
|
+
|
|
57
|
+
from codexcw import get_claude_account_usage
|
|
58
|
+
|
|
59
|
+
claude_usage = get_claude_account_usage()
|
|
60
|
+
print(claude_usage.windows)
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
## Running many
|
|
63
|
+
## Running many agent instances
|
|
52
64
|
|
|
53
65
|
```python
|
|
54
66
|
from codexcw import Runner, Request
|
|
@@ -15,15 +15,19 @@ use tokio_stream::wrappers::ReceiverStream;
|
|
|
15
15
|
use tokio_stream::StreamExt;
|
|
16
16
|
|
|
17
17
|
use codexcw::{
|
|
18
|
-
get_account_usage as core_get_account_usage,
|
|
19
|
-
|
|
18
|
+
get_account_usage as core_get_account_usage,
|
|
19
|
+
get_claude_account_usage as core_get_claude_account_usage,
|
|
20
|
+
AccountCredits as CoreAccountCredits, AccountRateLimitWindow as CoreAccountRateLimitWindow,
|
|
20
21
|
AccountRateLimits as CoreAccountRateLimits, AccountSpendLimit as CoreAccountSpendLimit,
|
|
21
22
|
AccountTokenUsage as CoreAccountTokenUsage,
|
|
22
23
|
AccountTokenUsageDailyBucket as CoreAccountTokenUsageDailyBucket,
|
|
23
24
|
AccountTokenUsageSummary as CoreAccountTokenUsageSummary, AccountUsage as CoreAccountUsage,
|
|
24
25
|
AccountUsageAccount as CoreAccountUsageAccount, AccountUsageRequest as CoreAccountUsageRequest,
|
|
25
|
-
ApprovalPolicy,
|
|
26
|
-
|
|
26
|
+
ApprovalPolicy, ClaudeAccountUsage as CoreClaudeAccountUsage,
|
|
27
|
+
ClaudeAccountUsageRequest as CoreClaudeAccountUsageRequest,
|
|
28
|
+
ClaudeAccountUsageWindow as CoreClaudeAccountUsageWindow, ConfigOverride, Event, EventPayload,
|
|
29
|
+
Item, ManyOptions, ModelUsage as CoreModelUsage, Request, RunResult, Runner as CoreRunner,
|
|
30
|
+
SandboxMode, Usage,
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
fn runtime() -> &'static Runtime {
|
|
@@ -40,7 +44,29 @@ fn runtime() -> &'static Runtime {
|
|
|
40
44
|
// Output value types.
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
42
46
|
|
|
43
|
-
///
|
|
47
|
+
/// Per-model usage reported by an agent.
|
|
48
|
+
#[pyclass(frozen, skip_from_py_object)]
|
|
49
|
+
#[derive(Clone)]
|
|
50
|
+
pub struct PyModelUsage {
|
|
51
|
+
#[pyo3(get)]
|
|
52
|
+
input_tokens: i64,
|
|
53
|
+
#[pyo3(get)]
|
|
54
|
+
output_tokens: i64,
|
|
55
|
+
#[pyo3(get)]
|
|
56
|
+
cache_read_input_tokens: i64,
|
|
57
|
+
#[pyo3(get)]
|
|
58
|
+
cache_creation_input_tokens: i64,
|
|
59
|
+
#[pyo3(get)]
|
|
60
|
+
web_search_requests: i64,
|
|
61
|
+
#[pyo3(get)]
|
|
62
|
+
cost_usd: f64,
|
|
63
|
+
#[pyo3(get)]
|
|
64
|
+
context_window: i64,
|
|
65
|
+
#[pyo3(get)]
|
|
66
|
+
max_output_tokens: i64,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Token usage reported by an agent.
|
|
44
70
|
#[pyclass(frozen, skip_from_py_object)]
|
|
45
71
|
#[derive(Clone, Default)]
|
|
46
72
|
pub struct PyUsage {
|
|
@@ -49,11 +75,17 @@ pub struct PyUsage {
|
|
|
49
75
|
#[pyo3(get)]
|
|
50
76
|
cached_input_tokens: i64,
|
|
51
77
|
#[pyo3(get)]
|
|
78
|
+
cache_creation_input_tokens: i64,
|
|
79
|
+
#[pyo3(get)]
|
|
52
80
|
output_tokens: i64,
|
|
53
81
|
#[pyo3(get)]
|
|
54
82
|
reasoning_output_tokens: i64,
|
|
55
83
|
#[pyo3(get)]
|
|
56
84
|
total_tokens: i64,
|
|
85
|
+
#[pyo3(get)]
|
|
86
|
+
total_cost_usd: f64,
|
|
87
|
+
#[pyo3(get)]
|
|
88
|
+
model_usage: HashMap<String, PyModelUsage>,
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
/// One file edit inside a `file_change` item.
|
|
@@ -66,7 +98,7 @@ pub struct PyFileChange {
|
|
|
66
98
|
kind: String,
|
|
67
99
|
}
|
|
68
100
|
|
|
69
|
-
/// A typed projection of
|
|
101
|
+
/// A typed projection of an agent item.
|
|
70
102
|
#[pyclass(frozen, skip_from_py_object)]
|
|
71
103
|
#[derive(Clone)]
|
|
72
104
|
pub struct PyItem {
|
|
@@ -92,7 +124,7 @@ pub struct PyItem {
|
|
|
92
124
|
changes: Vec<PyFileChange>,
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
/// One decoded
|
|
127
|
+
/// One decoded agent event.
|
|
96
128
|
#[pyclass(frozen, skip_from_py_object)]
|
|
97
129
|
#[derive(Clone)]
|
|
98
130
|
pub struct PyEvent {
|
|
@@ -300,6 +332,40 @@ pub struct PyAccountUsageOutcome {
|
|
|
300
332
|
error: Option<PyError>,
|
|
301
333
|
}
|
|
302
334
|
|
|
335
|
+
/// One Claude usage window.
|
|
336
|
+
#[pyclass(frozen, skip_from_py_object)]
|
|
337
|
+
#[derive(Clone)]
|
|
338
|
+
pub struct PyClaudeAccountUsageWindow {
|
|
339
|
+
#[pyo3(get)]
|
|
340
|
+
label: String,
|
|
341
|
+
#[pyo3(get)]
|
|
342
|
+
used_percent: f64,
|
|
343
|
+
#[pyo3(get)]
|
|
344
|
+
resets_at: String,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Claude account usage report.
|
|
348
|
+
#[pyclass(frozen, skip_from_py_object)]
|
|
349
|
+
#[derive(Clone)]
|
|
350
|
+
pub struct PyClaudeAccountUsage {
|
|
351
|
+
#[pyo3(get)]
|
|
352
|
+
report: String,
|
|
353
|
+
#[pyo3(get)]
|
|
354
|
+
windows: Vec<PyClaudeAccountUsageWindow>,
|
|
355
|
+
#[pyo3(get)]
|
|
356
|
+
raw: String,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Claude account usage paired with any terminal error.
|
|
360
|
+
#[pyclass(frozen, skip_from_py_object)]
|
|
361
|
+
#[derive(Clone)]
|
|
362
|
+
pub struct PyClaudeAccountUsageOutcome {
|
|
363
|
+
#[pyo3(get)]
|
|
364
|
+
result: Option<PyClaudeAccountUsage>,
|
|
365
|
+
#[pyo3(get)]
|
|
366
|
+
error: Option<PyError>,
|
|
367
|
+
}
|
|
368
|
+
|
|
303
369
|
/// One multiplexed event from `run_many`.
|
|
304
370
|
#[pyclass(frozen, skip_from_py_object)]
|
|
305
371
|
#[derive(Clone)]
|
|
@@ -347,6 +413,9 @@ struct ReqData {
|
|
|
347
413
|
profile: Option<String>,
|
|
348
414
|
sandbox: Option<String>,
|
|
349
415
|
approval: Option<String>,
|
|
416
|
+
permission_mode: Option<String>,
|
|
417
|
+
allowed_tools: Option<Vec<String>>,
|
|
418
|
+
disallowed_tools: Option<Vec<String>>,
|
|
350
419
|
config: Option<Vec<ConfigData>>,
|
|
351
420
|
enable: Option<Vec<String>>,
|
|
352
421
|
disable: Option<Vec<String>>,
|
|
@@ -386,6 +455,9 @@ impl ReqData {
|
|
|
386
455
|
profile: self.profile,
|
|
387
456
|
sandbox,
|
|
388
457
|
approval,
|
|
458
|
+
permission_mode: self.permission_mode,
|
|
459
|
+
allowed_tools: self.allowed_tools.unwrap_or_default(),
|
|
460
|
+
disallowed_tools: self.disallowed_tools.unwrap_or_default(),
|
|
389
461
|
config: self
|
|
390
462
|
.config
|
|
391
463
|
.unwrap_or_default()
|
|
@@ -419,6 +491,29 @@ struct AccountUsageReqData {
|
|
|
419
491
|
timeout: Option<f64>,
|
|
420
492
|
}
|
|
421
493
|
|
|
494
|
+
#[derive(FromPyObject)]
|
|
495
|
+
struct ClaudeAccountUsageReqData {
|
|
496
|
+
executable: Option<String>,
|
|
497
|
+
env: Option<HashMap<String, String>>,
|
|
498
|
+
timeout: Option<f64>,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
impl ClaudeAccountUsageReqData {
|
|
502
|
+
fn into_core(self) -> Result<CoreClaudeAccountUsageRequest, PyError> {
|
|
503
|
+
let timeout = match self.timeout {
|
|
504
|
+
Some(value) => Some(Duration::try_from_secs_f64(value).map_err(|_| {
|
|
505
|
+
invalid_request("timeout must be a finite non-negative number".to_string())
|
|
506
|
+
})?),
|
|
507
|
+
None => None,
|
|
508
|
+
};
|
|
509
|
+
Ok(CoreClaudeAccountUsageRequest {
|
|
510
|
+
executable: self.executable,
|
|
511
|
+
env: self.env.unwrap_or_default().into_iter().collect(),
|
|
512
|
+
timeout,
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
422
517
|
impl AccountUsageReqData {
|
|
423
518
|
fn into_core(self) -> Result<CoreAccountUsageRequest, PyError> {
|
|
424
519
|
Ok(CoreAccountUsageRequest {
|
|
@@ -467,6 +562,14 @@ fn invalid_request(message: String) -> PyError {
|
|
|
467
562
|
}
|
|
468
563
|
}
|
|
469
564
|
|
|
565
|
+
fn parse_agent(value: &str) -> Result<codexcw::Agent, PyError> {
|
|
566
|
+
match value {
|
|
567
|
+
"codex" => Ok(codexcw::Agent::Codex),
|
|
568
|
+
"claude" => Ok(codexcw::Agent::Claude),
|
|
569
|
+
other => Err(invalid_request(format!("unknown agent: {other}"))),
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
470
573
|
fn parse_account_usage_timeout(timeout: f64) -> Result<Duration, PyError> {
|
|
471
574
|
Duration::try_from_secs_f64(timeout).map_err(|_| {
|
|
472
575
|
invalid_request(
|
|
@@ -480,9 +583,29 @@ fn to_py_usage(usage: &Usage) -> PyUsage {
|
|
|
480
583
|
PyUsage {
|
|
481
584
|
input_tokens: usage.input_tokens,
|
|
482
585
|
cached_input_tokens: usage.cached_input_tokens,
|
|
586
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens,
|
|
483
587
|
output_tokens: usage.output_tokens,
|
|
484
588
|
reasoning_output_tokens: usage.reasoning_output_tokens,
|
|
485
589
|
total_tokens: usage.total_tokens,
|
|
590
|
+
total_cost_usd: usage.total_cost_usd,
|
|
591
|
+
model_usage: usage
|
|
592
|
+
.model_usage
|
|
593
|
+
.iter()
|
|
594
|
+
.map(|(model, usage)| (model.clone(), to_py_model_usage(usage)))
|
|
595
|
+
.collect(),
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
fn to_py_model_usage(usage: &CoreModelUsage) -> PyModelUsage {
|
|
600
|
+
PyModelUsage {
|
|
601
|
+
input_tokens: usage.input_tokens,
|
|
602
|
+
output_tokens: usage.output_tokens,
|
|
603
|
+
cache_read_input_tokens: usage.cache_read_input_tokens,
|
|
604
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens,
|
|
605
|
+
web_search_requests: usage.web_search_requests,
|
|
606
|
+
cost_usd: usage.cost_usd,
|
|
607
|
+
context_window: usage.context_window,
|
|
608
|
+
max_output_tokens: usage.max_output_tokens,
|
|
486
609
|
}
|
|
487
610
|
}
|
|
488
611
|
|
|
@@ -515,7 +638,10 @@ fn to_py_event(event: &Event) -> PyEvent {
|
|
|
515
638
|
match &event.payload {
|
|
516
639
|
EventPayload::ItemStarted(i) | EventPayload::ItemCompleted(i) => item = Some(to_py_item(i)),
|
|
517
640
|
EventPayload::TurnCompleted { usage: u } => usage = Some(to_py_usage(u)),
|
|
518
|
-
EventPayload::TurnFailed { error: e } =>
|
|
641
|
+
EventPayload::TurnFailed { error: e, usage: u } => {
|
|
642
|
+
error = Some(e.message.clone());
|
|
643
|
+
usage = Some(to_py_usage(u));
|
|
644
|
+
}
|
|
519
645
|
EventPayload::Error(e) => error = Some(e.message.clone()),
|
|
520
646
|
_ => {}
|
|
521
647
|
}
|
|
@@ -575,6 +701,13 @@ fn to_py_error(error: &codexcw::Error) -> PyError {
|
|
|
575
701
|
stderr: None,
|
|
576
702
|
line: None,
|
|
577
703
|
},
|
|
704
|
+
Claude { .. } => PyError {
|
|
705
|
+
kind: "claude".to_string(),
|
|
706
|
+
message: error.to_string(),
|
|
707
|
+
code: None,
|
|
708
|
+
stderr: None,
|
|
709
|
+
line: None,
|
|
710
|
+
},
|
|
578
711
|
Handler(_) => PyError {
|
|
579
712
|
kind: "handler".to_string(),
|
|
580
713
|
message: error.to_string(),
|
|
@@ -697,6 +830,28 @@ fn to_py_account_usage(usage: &CoreAccountUsage) -> PyAccountUsage {
|
|
|
697
830
|
}
|
|
698
831
|
}
|
|
699
832
|
|
|
833
|
+
fn to_py_claude_account_usage_window(
|
|
834
|
+
window: &CoreClaudeAccountUsageWindow,
|
|
835
|
+
) -> PyClaudeAccountUsageWindow {
|
|
836
|
+
PyClaudeAccountUsageWindow {
|
|
837
|
+
label: window.label.clone(),
|
|
838
|
+
used_percent: window.used_percent,
|
|
839
|
+
resets_at: window.resets_at.clone(),
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
fn to_py_claude_account_usage(usage: &CoreClaudeAccountUsage) -> PyClaudeAccountUsage {
|
|
844
|
+
PyClaudeAccountUsage {
|
|
845
|
+
report: usage.report.clone(),
|
|
846
|
+
windows: usage
|
|
847
|
+
.windows
|
|
848
|
+
.iter()
|
|
849
|
+
.map(to_py_claude_account_usage_window)
|
|
850
|
+
.collect(),
|
|
851
|
+
raw: usage.raw.clone(),
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
700
855
|
#[pyfunction]
|
|
701
856
|
#[pyo3(signature = (req=None))]
|
|
702
857
|
fn get_account_usage(py: Python<'_>, req: Option<AccountUsageReqData>) -> PyAccountUsageOutcome {
|
|
@@ -722,6 +877,34 @@ fn get_account_usage(py: Python<'_>, req: Option<AccountUsageReqData>) -> PyAcco
|
|
|
722
877
|
}
|
|
723
878
|
}
|
|
724
879
|
|
|
880
|
+
#[pyfunction]
|
|
881
|
+
#[pyo3(signature = (req=None))]
|
|
882
|
+
fn get_claude_account_usage(
|
|
883
|
+
py: Python<'_>,
|
|
884
|
+
req: Option<ClaudeAccountUsageReqData>,
|
|
885
|
+
) -> PyClaudeAccountUsageOutcome {
|
|
886
|
+
let core_req = match req.map(ClaudeAccountUsageReqData::into_core).transpose() {
|
|
887
|
+
Ok(req) => req.unwrap_or_default(),
|
|
888
|
+
Err(error) => {
|
|
889
|
+
return PyClaudeAccountUsageOutcome {
|
|
890
|
+
result: None,
|
|
891
|
+
error: Some(error),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
let result = py.detach(|| runtime().block_on(core_get_claude_account_usage(core_req)));
|
|
896
|
+
match result {
|
|
897
|
+
Ok(usage) => PyClaudeAccountUsageOutcome {
|
|
898
|
+
result: Some(to_py_claude_account_usage(&usage)),
|
|
899
|
+
error: None,
|
|
900
|
+
},
|
|
901
|
+
Err(error) => PyClaudeAccountUsageOutcome {
|
|
902
|
+
result: None,
|
|
903
|
+
error: Some(to_py_error(&error)),
|
|
904
|
+
},
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
725
908
|
// ---------------------------------------------------------------------------
|
|
726
909
|
// Session.
|
|
727
910
|
// ---------------------------------------------------------------------------
|
|
@@ -736,7 +919,7 @@ enum SessionInner {
|
|
|
736
919
|
Failed(PyError),
|
|
737
920
|
}
|
|
738
921
|
|
|
739
|
-
/// A running
|
|
922
|
+
/// A running selected-agent process. Iterate it for events; call `wait()` for the
|
|
740
923
|
/// final outcome.
|
|
741
924
|
#[pyclass]
|
|
742
925
|
pub struct Session {
|
|
@@ -812,7 +995,7 @@ impl Session {
|
|
|
812
995
|
}
|
|
813
996
|
}
|
|
814
997
|
|
|
815
|
-
/// The
|
|
998
|
+
/// The agent thread or session id once known.
|
|
816
999
|
fn thread_id(&self) -> String {
|
|
817
1000
|
match &self.inner {
|
|
818
1001
|
SessionInner::Failed(_) => String::new(),
|
|
@@ -840,7 +1023,7 @@ struct LiveGroup {
|
|
|
840
1023
|
total: usize,
|
|
841
1024
|
}
|
|
842
1025
|
|
|
843
|
-
/// A batch of running
|
|
1026
|
+
/// A batch of running selected-agent processes.
|
|
844
1027
|
#[pyclass]
|
|
845
1028
|
pub struct Group {
|
|
846
1029
|
inner: LiveGroup,
|
|
@@ -929,7 +1112,7 @@ impl Group {
|
|
|
929
1112
|
// Runner.
|
|
930
1113
|
// ---------------------------------------------------------------------------
|
|
931
1114
|
|
|
932
|
-
/// Starts
|
|
1115
|
+
/// Starts selected-agent processes with safe automation defaults.
|
|
933
1116
|
#[pyclass]
|
|
934
1117
|
pub struct Runner {
|
|
935
1118
|
core: CoreRunner,
|
|
@@ -946,6 +1129,7 @@ impl Runner {
|
|
|
946
1129
|
scan_max_bytes=None,
|
|
947
1130
|
default_sandbox=None,
|
|
948
1131
|
default_approval=None,
|
|
1132
|
+
agent=None,
|
|
949
1133
|
))]
|
|
950
1134
|
#[allow(clippy::too_many_arguments)]
|
|
951
1135
|
fn new(
|
|
@@ -956,8 +1140,14 @@ impl Runner {
|
|
|
956
1140
|
scan_max_bytes: Option<u32>,
|
|
957
1141
|
default_sandbox: Option<String>,
|
|
958
1142
|
default_approval: Option<String>,
|
|
959
|
-
|
|
1143
|
+
agent: Option<String>,
|
|
1144
|
+
) -> PyResult<Self> {
|
|
960
1145
|
let mut builder = CoreRunner::builder();
|
|
1146
|
+
if let Some(value) = agent {
|
|
1147
|
+
let agent = parse_agent(&value)
|
|
1148
|
+
.map_err(|error| pyo3::exceptions::PyValueError::new_err(error.message))?;
|
|
1149
|
+
builder = builder.agent(agent);
|
|
1150
|
+
}
|
|
961
1151
|
if let Some(executable) = executable {
|
|
962
1152
|
builder = builder.executable(executable);
|
|
963
1153
|
}
|
|
@@ -981,9 +1171,9 @@ impl Runner {
|
|
|
981
1171
|
builder =
|
|
982
1172
|
builder.default_approval(parse_approval(&value).unwrap_or(ApprovalPolicy::Never));
|
|
983
1173
|
}
|
|
984
|
-
Runner {
|
|
1174
|
+
Ok(Runner {
|
|
985
1175
|
core: builder.build(),
|
|
986
|
-
}
|
|
1176
|
+
})
|
|
987
1177
|
}
|
|
988
1178
|
|
|
989
1179
|
/// Runs one process to completion and returns its outcome.
|
|
@@ -1073,6 +1263,7 @@ fn _codexcw(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
1073
1263
|
m.add_class::<Group>()?;
|
|
1074
1264
|
m.add_class::<PyEvent>()?;
|
|
1075
1265
|
m.add_class::<PyItem>()?;
|
|
1266
|
+
m.add_class::<PyModelUsage>()?;
|
|
1076
1267
|
m.add_class::<PyUsage>()?;
|
|
1077
1268
|
m.add_class::<PyFileChange>()?;
|
|
1078
1269
|
m.add_class::<PyRunResult>()?;
|
|
@@ -1088,8 +1279,12 @@ fn _codexcw(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
1088
1279
|
m.add_class::<PyAccountTokenUsageSummary>()?;
|
|
1089
1280
|
m.add_class::<PyAccountTokenUsageDailyBucket>()?;
|
|
1090
1281
|
m.add_class::<PyAccountUsageOutcome>()?;
|
|
1282
|
+
m.add_class::<PyClaudeAccountUsage>()?;
|
|
1283
|
+
m.add_class::<PyClaudeAccountUsageWindow>()?;
|
|
1284
|
+
m.add_class::<PyClaudeAccountUsageOutcome>()?;
|
|
1091
1285
|
m.add_class::<PyRunEvent>()?;
|
|
1092
1286
|
m.add_class::<PyGroupResult>()?;
|
|
1093
1287
|
m.add_function(wrap_pyfunction!(get_account_usage, m)?)?;
|
|
1288
|
+
m.add_function(wrap_pyfunction!(get_claude_account_usage, m)?)?;
|
|
1094
1289
|
Ok(())
|
|
1095
1290
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Fake `claude` for tests: records argv and stdin, then emits a fixed
|
|
3
|
+
# stream-json event stream. Mirrors the claude cases in the Go/Rust suites.
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
if [ "${CODEXCW_ARGS_FILE:-}" != "" ]; then
|
|
7
|
+
: >"$CODEXCW_ARGS_FILE"
|
|
8
|
+
for arg in "$@"; do
|
|
9
|
+
printf '%s\n' "$arg" >>"$CODEXCW_ARGS_FILE"
|
|
10
|
+
done
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
prompt=""
|
|
14
|
+
if [ "${CODEXCW_STDIN_FILE:-}" != "" ]; then
|
|
15
|
+
cat >"$CODEXCW_STDIN_FILE"
|
|
16
|
+
prompt=$(cat "$CODEXCW_STDIN_FILE")
|
|
17
|
+
else
|
|
18
|
+
prompt=$(cat)
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ "$prompt" = "/usage" ]; then
|
|
22
|
+
printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"You are currently using your subscription\n\nCurrent session: 13% used · resets Jul 16 at 3:50pm (America/Sao_Paulo)\nCurrent week (all models): 5% used · resets Jul 18 at 9am (America/Sao_Paulo)"}'
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [ "${CODEXCW_CLAUDE_ERROR:-}" != "" ]; then
|
|
27
|
+
printf '%s\n' '{"type":"system","subtype":"init","cwd":"/work","session_id":"sess-error"}'
|
|
28
|
+
printf '%s\n' '{"type":"result","subtype":"success","is_error":true,"result":"Claude fixture failure","session_id":"sess-error","usage":{"input_tokens":1,"cache_creation_input_tokens":2,"cache_read_input_tokens":3,"output_tokens":4}}'
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
printf '%s\n' '{"type":"system","subtype":"init","cwd":"/work","session_id":"sess-1"}'
|
|
33
|
+
printf '%s\n' '{"type":"assistant","message":{"id":"msg_1","content":[{"type":"thinking","thinking":"I will create the file."}]},"session_id":"sess-1"}'
|
|
34
|
+
printf '%s\n' '{"type":"assistant","message":{"id":"msg_1","content":[{"type":"tool_use","id":"toolu_1","name":"Write","input":{"file_path":"/work/hello.txt","content":"hello"}}]},"session_id":"sess-1"}'
|
|
35
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":"File created successfully"}]},"session_id":"sess-1","tool_use_result":{"type":"create"}}'
|
|
36
|
+
printf '%s\n' '{"type":"assistant","message":{"id":"msg_2","content":[{"type":"thinking","thinking":"The file is ready."}]},"session_id":"sess-1"}'
|
|
37
|
+
printf '%s\n' '{"type":"assistant","message":{"id":"msg_2","content":[{"type":"text","text":"Done."}]},"session_id":"sess-1"}'
|
|
38
|
+
printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"Done.","session_id":"sess-1","total_cost_usd":0.013562,"usage":{"input_tokens":18,"cache_creation_input_tokens":3944,"cache_read_input_tokens":45921,"output_tokens":380},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":18,"outputTokens":380,"cacheReadInputTokens":45921,"cacheCreationInputTokens":3944,"webSearchRequests":0,"costUSD":0.013562,"contextWindow":200000,"maxOutputTokens":32000}}}'
|