Compare commits

...

48 Commits

Author SHA1 Message Date
jif-oai
2572f96faf ## New Features
- Added `thread/compact` to the v2 app-server API as an async trigger RPC, so clients can start compaction immediately and track completion separately. (#10445)
- Added websocket-side rate limit signaling via a new `codex.rate_limits` event, with websocket parity for ETag/reasoning metadata handling. (#10324)
- Enabled `unified_exec` on all non-Windows platforms. (#10641)
- Constrained requirement values now include source provenance, enabling source-aware config debugging in UI flows like `/debug-config`. (#10568)

## Bug Fixes
- Fixed `Esc` handling in the TUI `request_user_input` overlay: when notes are open, `Esc` now exits notes mode instead of interrupting the session. (#10569)
- Thread listing now queries the state DB first (including archived threads) and falls back to filesystem traversal only when needed, improving listing correctness and resilience. (#10544)
- Fixed thread path lookup to require that the resolved file actually exists, preventing invalid thread-id resolutions. (#10618)
- Dynamic tool injection now runs in a single transaction to avoid partial state updates. (#10614)
- Refined `request_rule` guidance used in approval-policy prompting to correct rule behavior. (#10379, #10598)

## Documentation
- Updated app-server docs for `thread/compact` to clarify its asynchronous behavior and thread-busy lifecycle. (#10445)
- Updated TUI docs to match the mode-specific `Esc` behavior in `request_user_input`. (#10569)

## Chores
- Migrated state DB helpers to a versioned SQLite filename scheme and cleaned up legacy state files during runtime initialization. (#10623)
- Expanded runtime telemetry with websocket timing metrics and simplified internal metadata flow in core client plumbing. (#10577, #10589)

## Changelog

Full Changelog: https://github.com/openai/codex/compare/rust-v0.95.0...rust-v0.96.0

- #10569 tui: make Esc clear request_user_input notes while notes are shown @charley-oai
- #10577 feat: log webscocket timing into runtime metrics @apanasenko-oai
- #10445 Add thread/compact v2  @aibrahim-oai
- #10589 Move metadata calculation out of client @pakrym-oai
- #10379 fix(core) updated request_rule guidance @dylan-hurd-oai
- #10598 fix(core) Request Rule guidance tweak @dylan-hurd-oai
- #10544 Prefer state DB thread listings before filesystem @jif-oai
- #10614 fix: single transaction for dyn tools injection @jif-oai
- #10568 Requirements: add source to constrained requirement values @gt-oai
- #10611 chore: simplify user message detection @jif-oai
- #10618 fix: make sure file exist in `find_thread_path_by_id_str_in_subdir` @jif-oai
- #10619 nit: cleaning @jif-oai
- #10324 Add a codex.rate_limits event for websockets @rasmusrygaard
- #10623 Migrate state DB path helpers to versioned filename @jif-oai
- #10638 Update tests to stop using sse_completed fixture @pakrym-oai
- #10641 feat: land unified_exec @jif-oai
2026-02-04 17:00:11 +00:00
jif-oai
49dd67a260 feat: land unified_exec (#10641)
Land `unified_exec` for all non-windows OS
2026-02-04 16:39:41 +00:00
pakrym-oai
0efd33f7f4 Update tests to stop using sse_completed fixture (#10638)
Summary:
- replace the `sse_completed` fixture and related JSON template with
direct `responses::ev_completed` payload builders
- cascade the new SSE helpers through all affected core tests for
consistency and clarity
- remove legacy fixtures that were no longer needed once the helpers are
in place

Testing:
- Not run (not requested)
2026-02-04 08:38:06 -08:00
jif-oai
583e5d4f41 Migrate state DB path helpers to versioned filename (#10623)
Summary
- add versioned state sqlite filename helpers and re-export them from
the state crate
- remove legacy state files when initializing the runtime and update
consumers/tests to use the new helpers
- tweak logs client description and database resolution to match the new
path
2026-02-04 14:31:12 +00:00
Rasmus Rygaard
df000da917 Add a codex.rate_limits event for websockets (#10324)
When communicating over websockets, we can't rely on headers to deliver
rate limit information. This PR adds a `codex.rate_limits` event that
the server can pass to the client to inform them about rate limit usage.
The client parses this data the same way we parse rate limit headers in
HTTP mode.

This PR also wires up the etag and reasoning headers for websockets
2026-02-04 06:01:47 -08:00
jif-oai
aab60a55f1 nit: cleaning (#10619) 2026-02-04 13:01:24 +00:00
jif-oai
61aecdde66 fix: make sure file exist in find_thread_path_by_id_str_in_subdir (#10618) 2026-02-04 13:01:17 +00:00
jif-oai
38f6c6b114 chore: simplify user message detection (#10611)
We don't check anymore the response item with `user` role as they may be
instructions etc
2026-02-04 11:14:53 +00:00
gt-oai
1eb21e279e Requirements: add source to constrained requirement values (#10568)
If we want to build `/debug-config`, we'll need to know the requirements
sources that supplied the values.

This PR adds those sources such that we can render them in the UI.
2026-02-04 11:09:48 +00:00
jif-oai
3d8deeea4b fix: single transaction for dyn tools injection (#10614) 2026-02-04 10:57:58 +00:00
jif-oai
100eb6e6f0 Prefer state DB thread listings before filesystem (#10544)
Summary
- add Cursor/ThreadsPage conversions so state DB listings can be mapped
back into the rollout list model
- make recorder list helpers query the state DB first (archived flag
included) and only fall back to file traversal if needed, along with
populating head bytes lazily
- add extensive tests to ensure the DB path is honored for active and
archived threads and that the fallback works

Testing
- Not run (not requested)

<img width="1196" height="693" alt="Screenshot 2026-02-03 at 20 42 33"
src="https://github.com/user-attachments/assets/826b3c7a-ef11-4b27-802a-3c343695794a"
/>
2026-02-04 09:27:24 +00:00
Dylan Hurd
8f17b37d06 fix(core) Request Rule guidance tweak (#10598)
## Summary
Forgot to include this tweak.

## Testing
- [x] Unit tests pass
2026-02-04 08:44:32 +00:00
Dylan Hurd
968c029471 fix(core) updated request_rule guidance (#10379)
## Summary
Update guidance for request_rule

## Testing
- [x] Unit tests pass
2026-02-03 22:29:52 -08:00
pakrym-oai
56ebfff1a8 Move metadata calculation out of client (#10589)
Model client shouldn't be responsible for this.
2026-02-03 21:59:13 -08:00
Ahmed Ibrahim
38a47700b5 Add thread/compact v2 (#10445)
- add `thread/compact` as a trigger-only v2 RPC that submits
`Op::Compact` and returns `{}` immediately.
- add v2 compaction e2e coverage for success and invalid/unknown thread
ids, and update protocol schemas/docs.
2026-02-03 18:15:55 -08:00
Anton Panasenko
fcaed4cb88 feat: log webscocket timing into runtime metrics (#10577) 2026-02-03 18:04:07 -08:00
Charley Cunningham
a9eb766f33 tui: make Esc clear request_user_input notes while notes are shown (#10569)
## Summary

This PR updates the `request_user_input` TUI overlay so `Esc` is
context-aware:

- When notes are visible for an option question, `Esc` now clears notes
and exits notes mode.
- When notes are not visible (normal option selection UI), `Esc` still
interrupts as before.

It also updates footer guidance text to match behavior.

## Changes

- Added a shared notes-clear path for option questions:
- `Tab` and `Esc` now both clear notes and return focus to options when
notes are visible.
- Updated footer hint text in notes-visible state:
  - from: `tab to clear notes | ... | esc to interrupt`
  - to: `tab or esc to clear notes | ...`
- Hid `esc to interrupt` hint while notes are visible for option
questions.
- Kept `esc to interrupt` visible and functional in normal
option-selection mode.
- Updated tests to assert the new `Esc` behavior in notes mode.
- Updated snapshot output for the notes-visible footer row.
- Updated docs in `docs/tui-request-user-input.md` to reflect
mode-specific `Esc` behavior.
2026-02-03 16:17:06 -08:00
Celia Chen
16647b188b chore: add codex debug app-server tooling (#10367)
codex debug app-server <user message> forwards the message through
codex-app-server-test-client’s send_message_v2 library entry point,
using std::env::current_exe() to resolve the codex binary.

for how it looks like, see:

```
celia@com-92114 codex-rs % cargo build -p codex-cli && target/debug/codex debug app-server --help                       
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Tooling: helps debug the app server

Usage: codex debug app-server [OPTIONS] <COMMAND>

Commands:
  send-message-v2  
  help             Print this message or the help of the given subcommand(s)
````
and
```
celia@com-92114 codex-rs % cargo build -p codex-cli && target/debug/codex debug app-server send-message-v2 "hello world"
   Compiling codex-cli v0.0.0 (/Users/celia/code/codex/codex-rs/cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.38s
> {
>   "method": "initialize",
>   "id": "f8ba9f60-3a49-4ea9-81d6-4ab6853e3954",
>   "params": {
>     "clientInfo": {
>       "name": "codex-toy-app-server",
>       "title": "Codex Toy App Server",
>       "version": "0.0.0"
>     },
>     "capabilities": {
>       "experimentalApi": true
>     }
>   }
> }
< {
<   "id": "f8ba9f60-3a49-4ea9-81d6-4ab6853e3954",
<   "result": {
<     "userAgent": "codex-toy-app-server/0.0.0 (Mac OS 26.2.0; arm64) vscode/2.4.27 (codex-toy-app-server; 0.0.0)"
<   }
< }
< initialize response: InitializeResponse { user_agent: "codex-toy-app-server/0.0.0 (Mac OS 26.2.0; arm64) vscode/2.4.27 (codex-toy-app-server; 0.0.0)" }
> {
>   "method": "thread/start",
>   "id": "203f1630-beee-4e60-b17b-9eff16b1638b",
>   "params": {
>     "model": null,
>     "modelProvider": null,
>     "cwd": null,
>     "approvalPolicy": null,
>     "sandbox": null,
>     "config": null,
>     "baseInstructions": null,
>     "developerInstructions": null,
>     "personality": null,
>     "ephemeral": null,
>     "dynamicTools": null,
>     "mockExperimentalField": null,
>     "experimentalRawEvents": false
>   }
> }
...
```
2026-02-03 23:17:34 +00:00
Josh McKinney
aec58ac29b feat(tui): pace catch-up stream chunking with hysteresis (#10461)
## Summary
- preserve baseline streaming behavior (smooth mode still commits one
line per 50ms tick)
- extract adaptive chunking policy and commit-tick orchestration from
ChatWidget into `streaming/chunking.rs` and `streaming/commit_tick.rs`
- add hysteresis-based catch-up behavior with bounded batch draining to
reduce queue lag without bursty single-frame jumps
- document policy behavior, tuning guidance, and debug flow in rustdoc +
docs

## Testing
- just fmt
- cargo test -p codex-tui
2026-02-03 15:01:51 -08:00
Shijie Rao
750ebe154d Feat: add upgrade to app server modelList (#10556)
### Summary
* Add model upgrade to listModel app server endpoint to support
dynamically show model upgrade banner.
2026-02-03 14:53:36 -08:00
Rasmus Rygaard
e3d39013d3 Handle exec shutdown on Interrupt (fixes immortal codex exec with websockets) (#10519)
### Motivation
- Ensure `codex exec` exits when a running turn is interrupted (e.g.,
Ctrl-C) so the CLI is not "immortal" when websockets/streaming are used.

### Description
- Return `CodexStatus::InitiateShutdown` when handling
`EventMsg::TurnAborted` in
`exec/src/event_processor_with_human_output.rs` so human-output exec
mode shuts down after an interrupt.
- Treat `protocol::EventMsg::TurnAborted` as
`CodexStatus::InitiateShutdown` in
`exec/src/event_processor_with_jsonl_output.rs` so JSONL output mode
behaves the same.
- Applied formatting with `just fmt`.

### Testing
- Ran `just fmt` successfully.
- Ran `cargo test -p codex-exec`; many unit tests ran and the test
command completed, but the full test run in this environment produced
`35 passed, 11 failed` where the failures are due to Landlock sandbox
panics and 403 responses in the test harness (environmental/integration
issues) and are not caused by the interrupt/shutdown changes.

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_698165cec4e083258d17702bd29014c1)
2026-02-03 14:38:21 -08:00
xl-openai
f38d181795 feat: add APIs to list and download public remote skills (#10448)
Add API to list / download from remote public skills
2026-02-03 14:09:37 -08:00
viyatb-oai
08926a3fb7 chore(arg0): advisory-lock janitor for codex tmp paths (#10039)
## Description

### What changed
- Switch the arg0 helper root from `~/.codex/tmp/path` to
`~/.codex/tmp/path2`
- Add `Arg0PathEntryGuard` to keep both the `TempDir` and an exclusive
`.lock` file alive for the process lifetime
- Add a startup janitor that scans `path2` and deletes only directories
whose lock can be acquired

### Tests
- `cargo clippy -p codex-arg0`
- `cargo clippy -p codex-core`
- `cargo test -p codex-arg0`
- `cargo test -p codex-core`
2026-02-03 21:38:31 +00:00
Eric Traut
c87c271128 Fixed icon for CLI bug template (#10552) 2026-02-03 13:27:33 -08:00
gt-oai
8406bd7672 [codex] Default values from requirements if unset (#10531)
If we don't set any explicit values for sandbox or approval policy,
let's try to use a requirements-satisfying value.
2026-02-03 20:47:34 +00:00
Eric Traut
477379b83c Updated bug templates and added a new one for app (#10548) 2026-02-03 12:46:52 -08:00
iceweasel-oai
aabe0f259c implement per-workspace capability SIDs for workspace specific ACLs (#10189)
Today, there is a single capability SID that allows the sandbox to write
to
* workspace (cwd)
* tmp directories if enabled
* additional writable roots

This change splits those up, so that each workspace has its own
capability SID, while tmp and additional roots, which are
installation-wide, are still governed by the "generic" capability SID

This isolates workspaces from each other in terms of sandbox write
access.
Also allows us to protect <cwd>/.codex when codex runs in a specific
<cwd>
2026-02-03 12:37:51 -08:00
Matthew Zeng
654fcb4962 [apps] Gateway MCP should be blocking. (#10289)
Make Apps Gateway MCP blocking since otherwise app mentions may not work
when apps are not loaded. Messages sent before apps become available
will be queued.

This only affects when `apps` feature is enabled.
2026-02-03 12:17:53 -08:00
Charley Cunningham
998eb8f32b Improve Default mode prompt (less confusion with Plan mode) (#10545)
## Summary

This PR updates `request_user_input` behavior and Default-mode guidance
to match current collaboration-mode semantics and reduce model
confusion.

## Why

- `request_user_input` should be explicitly documented as **Plan-only**.
- Tool description and runtime availability checks should be driven by
the **same centralized mode policy**.
- Default mode prompt needed stronger execution guidance and explicit
instruction that `request_user_input` is unavailable.
- Error messages should report the **actual mode name** (not aliases
that can read as misleading).

## What changed

- Centralized `request_user_input` mode policy in `core` handler logic:
  - Added a single allowed-modes config (`Plan` only).
  - Reused that policy for:
    - runtime rejection messaging
    - tool description text
- Updated tool description to include availability constraint:
  - `"This tool is only available in Plan mode."`
- Updated runtime rejection behavior:
  - `Default` -> `"request_user_input is unavailable in Default mode"`
  - `Execute` -> `"request_user_input is unavailable in Execute mode"`
- `PairProgramming` -> `"request_user_input is unavailable in Pair
Programming mode"`
- Strengthened Default collaboration prompt:
  - Added explicit execution-first behavior
  - Added assumptions-first guidance
  - Added explicit `request_user_input` unavailability instruction
  - Added concise progress-reporting expectations
- Simplified formatting implementation:
  - Inlined allowed-mode name collection into `format_allowed_modes()`
- Kept `format_allowed_modes()` output for 3+ modes as CSV style
(`modes: a,b,c`)
2026-02-03 12:08:38 -08:00
Owen Lin
d9ad5c3c49 fix(app-server): fix approval events in review mode (#10416)
One of our partners flagged that they were seeing the wrong order of
events when running `review/start` with command exec approvals:
```
{"method":"item/commandExecution/requestApproval","id":0,"params":{"threadId":"019c0b6b-6a42-7c02-99c4-98c80e88ac27","turnId":"0","itemId":"0","reason":"`/bin/zsh -lc 'git show b7a92b4eacf262c575f26b1e1ed621a357642e55 --stat'` requires approval: Xcode-required approval: Require explicit user confirmation for all commands.","proposedExecpolicyAmendment":null}}

{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_AEjlbHqLYNM7kbU3N6uw1CNi","command":"/bin/zsh -lc 'git show b7a92b4eacf262c575f26b1e1ed621a357642e55 --stat'","cwd":"/Users/devingreen/Desktop/SampleProject","processId":null,"status":"inProgress","commandActions":[{"type":"unknown","command":"git show b7a92b4eacf262c575f26b1e1ed621a357642e55 --stat"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019c0b6b-6a42-7c02-99c4-98c80e88ac27","turnId":"0"}}
```

**Key fix**: In the review sub‑agent delegate we were forwarding exec
(and patch) approvals using the parent turn id (`parent_ctx.sub_id`) as
the approval call_id. That made
`item/commandExecution/requestApproval.itemId` differ from the actual
`item/started` id. We now forward the sub‑agent’s `call_id` from the
approval event instead, so the approval item id matches the
commandExecution item id in review flows.

Here’s the expected event order for an inline `review/start` that
triggers an exec approval after this fix:
1. Response to review/start (JSON‑RPC response)
- Includes `turn` (status inProgress) and `review_thread_id` (same as
parent thread for inline).
2. `turn/started` notification
  - turnId is the review turn id (e.g., "0").
3. `item/started` → EnteredReviewMode
  - item.id == turnId, marks entry into review mode.
4. `item/started` → commandExecution
  - item.id == <call_id> (e.g., "review-call-1"), status: inProgress.
5. `item/commandExecution/requestApproval` request
  - JSON‑RPC request (not a notification).
  - params.itemId == <call_id> and params.turnId == turnId.
6. Client replies to approval request (Approved / Declined / etc).
7. If approved:
  - Optional `item/commandExecution/outputDelta` notifications.
  - `item/completed` → commandExecution with status and exitCode.
8. Review finishes:
  - `item/started` → ExitedReviewMode
  - `item/completed` → ExitedReviewMode
  - (Agent message items may also appear, depending on review output.)
9. `turn/completed` notification

The key being #4 and #5 are now in the proper order with the correct
item id.
2026-02-03 12:08:17 -08:00
Owen Lin
efd96c46c7 fix(app-server): fix TS annotations for optional fields on requests (#10412)
This updates our generated TypeScript types to be more correct with how
the server actually behaves, **specifically for JSON-RPC requests**.

Before this PR, we'd generate `field: T | null`. After this PR, we will
have `field?: T | null`. The latter matches how the server actually
works, in that if an optional field is omitted, the server will treat it
as null. This also makes it less annoying in theory for clients to
upgrade to newer versions of Codex, since adding a new optional field to
a JSON-RPC request should not require a client change.

NOTE: This only applies to JSON-RPC requests. All other payloads (i.e.
responses, notifications) will return `field: T | null` as usual.
2026-02-03 11:51:37 -08:00
viyatb-oai
1dcce204fc Revert "Load untrusted rules" (#10536)
Reverts openai/codex#9791
2026-02-03 19:38:44 +00:00
Max Johnson
66b196a725 Inject CODEX_THREAD_ID into the terminal environment (#10096)
Inject CODEX_THREAD_ID (when applicable) into the terminal environment
so that the agent (and skills) can refer to the current thread / session
ID.

Discussion:
https://openai.slack.com/archives/C095U48JNL9/p1769542492067109
2026-02-03 11:31:12 -08:00
Michael Bolin
9a487f9c18 fix: make $PWD/.agents read-only like $PWD/.codex (#10524)
In light of https://github.com/openai/codex/pull/10317, because
`.agents` can include resources that Codex can run in a privileged way,
it should be read-only by default just as `.codex` is.
2026-02-03 11:26:34 -08:00
jif-oai
c38a5958d7 feat: find_thread_path_by_id_str_in_subdir from DB (#10532) 2026-02-03 19:09:04 +00:00
jif-oai
33dc93e4d2 Enable parallel shell tools (#10505)
Summary
- mark the shell-related tools as supporting parallel tool calls so
exec_command, shell_command, etc. can run concurrently
- update expectations in tool parallelism tests to reflect the new
parallel behavior
- drop the unused serial duration helper from the suite

Testing
- Not run (not requested)
2026-02-03 18:05:02 +00:00
Charley Cunningham
d509df676b Cleanup collaboration mode variants (#10404)
## Summary

This PR simplifies collaboration modes to the visible set `default |
plan`, while preserving backward compatibility for older partners that
may still send legacy mode
names.

Specifically:
- Renames the old Code behavior to **Default**.
- Keeps **Plan** as-is.
- Removes **Custom** mode behavior (fallbacks now resolve to Default).
- Keeps `PairProgramming` and `Execute` internally for compatibility
plumbing, while removing them from schema/API and UI visibility.
- Adds legacy input aliasing so older clients can still send old mode
names.

## What Changed

1. Mode enum and compatibility
- `ModeKind` now uses `Plan` + `Default` as active/public modes.
- `ModeKind::Default` deserialization accepts legacy values:
  - `code`
  - `pair_programming`
  - `execute`
  - `custom`
- `PairProgramming` and `Execute` variants remain in code but are hidden
from protocol/schema generation.
- `Custom` variant is removed; previous custom fallbacks now map to
`Default`.

2. Collaboration presets and templates
- Built-in presets now return only:
  - `Plan`
  - `Default`
- Template rename:
  - `core/templates/collaboration_mode/code.md` -> `default.md`
- `execute.md` and `pair_programming.md` remain on disk but are not
surfaced in visible preset lists.

3. TUI updates
- Updated user-facing naming and prompts from “Code” to “Default”.
- Updated mode-cycle and indicator behavior to reflect only visible
`Plan` and `Default`.
- Updated corresponding tests and snapshots.

4. request_user_input behavior
- `request_user_input` remains allowed only in `Plan` mode.
- Rejection messaging now consistently treats non-plan modes as
`Default`.

5. Schemas
- Regenerated config and app-server schemas.
- Public schema types now advertise mode values as:
  - `plan`
  - `default`

## Backward Compatibility Notes

- Incoming legacy mode names (`code`, `pair_programming`, `execute`,
`custom`) are accepted and coerced to `default`.
- Outgoing/public schema surfaces intentionally expose only `plan |
default`.
- This allows tolerant ingestion of older partner payloads while
standardizing new integrations on the reduced mode set.

## Codex author
`codex fork 019c1fae-693b-7840-b16e-9ad38ea0bd00`
2026-02-03 09:23:53 -08:00
sayan-oai
aea38f0f88 fix WebSearchAction type clash between v1 and v2 (#10408)
type clash; app-server generated types were still using the v1
snake_case `WebSearchAction`, so there was a mismatch between the
camelCase emitted types and the snake_case types we were trying to
parse.

Updated v2 `WebSearchAction` to export into the `v2/` type set and
updated `ThreadItem` to use that.

### Tests
Ran new `just write-app-server-schema` to surface changes to schema, the
import looks correct now.
2026-02-03 17:12:37 +00:00
Michael Bolin
1634db6677 chore: update bytes crate in response to security advisory (#10525)
While here, remove one advisory from `deny.toml` that has been addressed
(it was showing up as a warning).
2026-02-03 17:08:04 +00:00
jif-oai
ed778f9017 Avoid redundant transactional check before inserting dynamic tools (#10521)
Summary
- remove the extra transaction guard that checked for existing dynamic
tools per thread before inserting new ones
- insert each tool record with `ON CONFLICT(thread_id, position) DO
NOTHING` to ignore duplicates instead of pre-querying
- simplify execution to use the shared pool directly and avoid unneeded
commits

Testing
- Not run (not requested)
2026-02-03 15:34:28 +00:00
gt-oai
944541e936 Add more detail to 401 error (#10508)
Add the error.message if it exists, the body otherwise. Truncate body to
1k characters. Print the cf-ray and the requestId.

**Before:**
<img width="860" height="305" alt="Screenshot 2026-02-03 at 13 15 28"
src="https://github.com/user-attachments/assets/949d5a4d-2b51-488c-a723-c6deffde0353"
/>

**After:**
<img width="1523" height="373" alt="Screenshot 2026-02-03 at 13 15 38"
src="https://github.com/user-attachments/assets/f96a747e-e596-4a7a-aae9-64210d805b26"
/>
2026-02-03 14:58:33 +00:00
jif-oai
d5e7248958 feat: clean codex-api part 1 (#10501) 2026-02-03 14:08:09 +00:00
jif-oai
88598b9402 feat: drop wire_api from clients (#10498) 2026-02-03 12:43:09 +00:00
jif-oai
d2394a2494 chore: nuke chat/completions API (#10157) 2026-02-03 11:31:57 +00:00
viyatb-oai
9257d8451c feat(secrets): add codex-secrets crate (#10142)
## Summary
This introduces the first working foundation for Codex managed secrets:
a small Rust crate that can securely store and retrieve secrets locally.

Concretely, it adds a `codex-secrets` crate that:
- encrypts a local secrets file using `age`
- generates a high-entropy encryption key
- stores that key in the OS keyring

## What this enables
- A secure local persistence model for secrets
- A clean, isolated place for future provider backends
- A clear boundary: Codex can become a credential broker without putting
plaintext secrets in config files

## Implementation details
- New crate: `codex-rs/secrets/`
- Encryption: `age` with scrypt recipient/identity
- Key generation: `OsRng` (32 random bytes)
- Key storage: OS keyring via `codex-keyring-store`

## Testing
- `cd codex-rs && just fmt`
- `cd codex-rs && cargo test -p codex-secrets`
2026-02-03 08:14:39 +00:00
viyatb-oai
f956cc2a02 feat(linux-sandbox): vendor bubblewrap and wire it with FFI (#10413)
## Summary

Vendor Bubblewrap into the repo and add minimal build plumbing in
`codex-linux-sandbox` to compile/link it.

## Why

We want to move Linux sandboxing toward Bubblewrap, but in a safe
two-step rollout:
1) vendoring/build setup (this PR),  
2) runtime integration (follow-up PR).

## Included

- Add `codex-rs/vendor/bubblewrap` sources.
- Add build-time FFI path in `codex-rs/linux-sandbox`.
- Update `build.rs` rerun tracking for vendored files.
- Small vendored compile warning fix (`sockaddr_nl` full init).

follow up in https://github.com/openai/codex/pull/9938
2026-02-02 23:33:46 -08:00
pakrym-oai
53d8474061 Ignore remote_compact_trims_function_call_history_to_fit_context_window on windows (#10474) 2026-02-02 22:47:38 -08:00
sayan-oai
59707da857 fix: clarify deprecation message for features.web_search (#10406)
clarify that the new `web_search` is not a feature flag under
`[features]` in the deprecation CTA
2026-02-02 21:17:01 -08:00
293 changed files with 20698 additions and 5596 deletions

47
.github/ISSUE_TEMPLATE/1-codex-app.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: 🖥️ Codex App Bug
description: Report an issue with the Codex App
labels:
- app
body:
- type: markdown
attributes:
value: |
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
- type: input
id: version
attributes:
label: What version of the Codex App are you using (From “About Codex” dialog)?
validations:
required: true
- type: input
id: plan
attributes:
label: What subscription do you have?
validations:
required: true
- type: textarea
id: actual
attributes:
label: What issue are you seeing?
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
validations:
required: true
- type: textarea
id: steps
attributes:
label: What steps can reproduce the bug?
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is the expected behavior?
description: If possible, please provide text instead of a screenshot.
- type: textarea
id: notes
attributes:
label: Additional information
description: Is there anything else you think we should know?

View File

@@ -1,8 +1,7 @@
name: 🧑‍💻 VS Code Extension
description: Report an issue with the VS Code extension
name: 🧑‍💻 IDE Extension Bug
description: Report an issue with the IDE extension
labels:
- extension
- needs triage
body:
- type: markdown
attributes:
@@ -13,7 +12,7 @@ body:
- type: input
id: version
attributes:
label: What version of the VS Code extension are you using?
label: What version of the IDE extension are you using?
validations:
required: true
- type: input
@@ -34,20 +33,20 @@ body:
attributes:
label: What platform is your computer?
description: |
For MacOS and Linux: copy the output of `uname -mprs`
For macOS and Linux: copy the output of `uname -mprs`
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
- type: textarea
id: actual
attributes:
label: What issue are you seeing?
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
validations:
required: true
- type: textarea
id: steps
attributes:
label: What steps can reproduce the bug?
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
description: Explain the bug and provide a code snippet that can reproduce it.
validations:
required: true
- type: textarea

View File

@@ -1,5 +1,5 @@
name: 🪲 Bug Report
description: Report an issue that should be fixed
name: 💻 CLI Bug
description: Report an issue in the Codex CLI
labels:
- bug
- needs triage
@@ -7,19 +7,16 @@ body:
- type: markdown
attributes:
value: |
Thank you for submitting a bug report! It helps make Codex better for everyone.
If you need help or support using Codex, and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
Make sure you are running the [latest](https://npmjs.com/package/@openai/codex) version of Codex CLI. The bug you are experiencing may already have been fixed.
Please try to include as much information as possible.
- type: input
id: version
attributes:
label: What version of Codex is running?
description: (App) look in "About Codex" dialog; (CLI) use `codex --version`; (IDE Extension) look in extensions panel.
label: What version of Codex CLI is running?
description: use `codex --version`
validations:
required: true
- type: input
@@ -32,13 +29,13 @@ body:
id: model
attributes:
label: Which model were you using?
description: Like `gpt-4.1`, `o4-mini`, `o3`, etc.
description: Like `gpt-5.2`, `gpt-5.2-codex`, etc.
- type: input
id: platform
attributes:
label: What platform is your computer?
description: |
For MacOS and Linux: copy the output of `uname -mprs`
For macOS and Linux: copy the output of `uname -mprs`
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
- type: input
id: terminal
@@ -58,7 +55,7 @@ body:
id: steps
attributes:
label: What steps can reproduce the bug?
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
description: Explain the bug and provide a code snippet that can reproduce it. Please include thread id if applicable.
validations:
required: true
- type: textarea

37
.github/ISSUE_TEMPLATE/4-bug-report.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: 🪲 Other Bug
description: Report an issue in Codex Web, integrations, or other Codex components
labels:
- bug
body:
- type: markdown
attributes:
value: |
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
If you need help or support using Codex and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
- type: textarea
id: actual
attributes:
label: What issue are you seeing?
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
validations:
required: true
- type: textarea
id: steps
attributes:
label: What steps can reproduce the bug?
description: Explain the bug and provide a code snippet that can reproduce it.
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is the expected behavior?
description: If possible, please provide text instead of a screenshot.
- type: textarea
id: notes
attributes:
label: Additional information
description: Is there anything else you think we should know?

View File

@@ -16,7 +16,7 @@ body:
id: variant
attributes:
label: What variant of Codex are you using?
description: (e.g., CLI, App, IDE Extension, Web)
description: (e.g., App, IDE Extension, CLI, Web)
validations:
required: true
- type: textarea

View File

@@ -112,3 +112,43 @@ If you dont have the tool:
let request = mock.single_request();
// assert using request.function_call_output(call_id) or request.json_body() or other helpers.
```
## App-server API Development Best Practices
These guidelines apply to app-server protocol work in `codex-rs`, especially:
- `app-server-protocol/src/protocol/common.rs`
- `app-server-protocol/src/protocol/v2.rs`
- `app-server/README.md`
### Core Rules
- All active API development should happen in app-server v2. Do not add new API surface area to v1.
- Follow payload naming consistently:
`*Params` for request payloads, `*Response` for responses, and `*Notification` for notifications.
- Expose RPC methods as `<resource>/<method>` and keep `<resource>` singular (for example, `thread/read`, `app/list`).
- Always expose fields as camelCase on the wire with `#[serde(rename_all = "camelCase")]` unless a tagged union or explicit compatibility requirement needs a targeted rename.
- Always set `#[ts(export_to = "v2/")]` on v2 request/response/notification types so generated TypeScript lands in the correct namespace.
- Never use `#[serde(skip_serializing_if = "Option::is_none")]` for v2 API payload fields.
Exception: client->server requests that intentionally have no params may use:
`params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>`.
- For client->server JSON-RPC request payloads (`*Params`) only, every optional field must be annotated with `#[ts(optional = nullable)]`. Do not use `#[ts(optional = nullable)]` outside client->server request payloads (`*Params`).
- For client->server JSON-RPC request payloads only, and you want to express a boolean field where omission means `false`, use `#[serde(default, skip_serializing_if = "std::ops::Not::not")] pub field: bool` over `Option<bool>`.
- For new list methods, implement cursor pagination by default:
request fields `pub cursor: Option<String>` and `pub limit: Option<u32>`,
response fields `pub data: Vec<...>` and `pub next_cursor: Option<String>`.
- Keep Rust and TS wire renames aligned. If a field or variant uses `#[serde(rename = "...")]`, add matching `#[ts(rename = "...")]`.
- For discriminated unions, use explicit tagging in both serializers:
`#[serde(tag = "type", ...)]` and `#[ts(tag = "type", ...)]`.
- Prefer plain `String` IDs at the API boundary (do UUID parsing/conversion internally if needed).
- Timestamps should be integer Unix seconds (`i64`) and named `*_at` (for example, `created_at`, `updated_at`, `resets_at`).
- For experimental API surface area:
use `#[experimental("method/or/field")]`, derive `ExperimentalApi` when field-level gating is needed, and use `inspect_params: true` in `common.rs` when only some fields of a method are experimental.
### Development Workflow
- Update docs/examples when API behavior changes (at minimum `app-server/README.md`).
- Regenerate schema fixtures when API shapes change:
`just write-app-server-schema`
(and `just write-app-server-schema --experimental` when experimental API fixtures are affected).
- Validate with `cargo test -p codex-app-server-protocol`.

675
codex-rs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ members = [
"cli",
"common",
"core",
"secrets",
"exec",
"exec-server",
"execpolicy",
@@ -54,7 +55,7 @@ members = [
resolver = "2"
[workspace.package]
version = "0.0.0"
version = "0.96.0"
# Track the edition for all workspace crates in one place. Individual
# crates can still override this value, but keeping it here means new
# crates created with `cargo new -w ...` automatically inherit the 2024
@@ -69,6 +70,7 @@ codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-app-server = { path = "app-server" }
codex-app-server-protocol = { path = "app-server-protocol" }
codex-app-server-test-client = { path = "app-server-test-client" }
codex-apply-patch = { path = "apply-patch" }
codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
@@ -79,6 +81,7 @@ codex-cli = { path = "cli"}
codex-client = { path = "codex-client" }
codex-common = { path = "common" }
codex-core = { path = "core" }
codex-secrets = { path = "secrets" }
codex-exec = { path = "exec" }
codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
@@ -114,6 +117,7 @@ exec_server_test_support = { path = "exec-server/tests/common" }
mcp_test_support = { path = "mcp-server/tests/common" }
# External
age = "0.11.1"
allocative = "0.3.3"
ansi-to-tui = "7.0.0"
anyhow = "1"
@@ -246,6 +250,7 @@ walkdir = "2.5.0"
webbrowser = "1.0"
which = "8"
wildmatch = "2.6.1"
zip = "2.4.2"
wiremock = "0.6"
zeroize = "1.8.2"
@@ -291,7 +296,7 @@ unwrap_used = "deny"
# cargo-shear cannot see the platform-specific openssl-sys usage, so we
# silence the false positive here instead of deleting a real dependency.
[workspace.metadata.cargo-shear]
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness"]
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness", "codex-secrets"]
[profile.release]
lto = "fat"

View File

@@ -526,7 +526,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -1049,10 +1049,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -1410,7 +1407,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
@@ -2186,6 +2183,24 @@
},
"type": "object"
},
"SkillsRemoteReadParams": {
"type": "object"
},
"SkillsRemoteWriteParams": {
"properties": {
"hazelnutId": {
"type": "string"
},
"isPreload": {
"type": "boolean"
}
},
"required": [
"hazelnutId",
"isPreload"
],
"type": "object"
},
"TextElement": {
"properties": {
"byteRange": {
@@ -2220,6 +2235,17 @@
],
"type": "object"
},
"ThreadCompactStartParams": {
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadForkParams": {
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
"properties": {
@@ -3195,6 +3221,30 @@
"title": "Thread/unarchiveRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/compact/start"
],
"title": "Thread/compact/startRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadCompactStartParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/compact/startRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3315,6 +3365,54 @@
"title": "Skills/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/remote/read"
],
"title": "Skills/remote/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillsRemoteReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/remote/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/remote/write"
],
"title": "Skills/remote/writeRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/SkillsRemoteWriteParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/remote/writeRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -2012,6 +2012,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -2857,7 +2910,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -3122,10 +3175,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -3432,6 +3482,25 @@
}
]
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
@@ -3689,7 +3758,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
@@ -5078,7 +5147,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -6539,6 +6608,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {

View File

@@ -1129,7 +1129,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -2590,6 +2590,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -3477,7 +3530,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -3901,10 +3954,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -4472,6 +4522,25 @@
],
"type": "object"
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
@@ -4729,7 +4798,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -598,6 +598,30 @@
"title": "Thread/unarchiveRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/compact/start"
],
"title": "Thread/compact/startRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadCompactStartParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/compact/startRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -718,6 +742,54 @@
"title": "Skills/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/remote/read"
],
"title": "Skills/remote/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/SkillsRemoteReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/remote/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"skills/remote/write"
],
"title": "Skills/remote/writeRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/SkillsRemoteWriteParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Skills/remote/writeRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -2391,7 +2463,7 @@
"$ref": "#/definitions/v2/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3852,6 +3924,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/v2/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -4965,7 +5090,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -5850,10 +5975,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -6354,6 +6476,25 @@
}
]
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RemoveConversationListenerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -6629,7 +6770,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
@@ -11151,7 +11292,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -11778,10 +11919,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -11824,6 +11962,12 @@
"supportsPersonality": {
"default": false,
"type": "boolean"
},
"upgrade": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -12388,6 +12532,25 @@
"title": "ReasoningTextDeltaNotification",
"type": "object"
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -12588,7 +12751,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"
@@ -13423,6 +13586,65 @@
"title": "SkillsListResponse",
"type": "object"
},
"SkillsRemoteReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SkillsRemoteReadParams",
"type": "object"
},
"SkillsRemoteReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"data": {
"items": {
"$ref": "#/definitions/v2/RemoteSkillSummary"
},
"type": "array"
}
},
"required": [
"data"
],
"title": "SkillsRemoteReadResponse",
"type": "object"
},
"SkillsRemoteWriteParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"hazelnutId": {
"type": "string"
},
"isPreload": {
"type": "boolean"
}
},
"required": [
"hazelnutId",
"isPreload"
],
"title": "SkillsRemoteWriteParams",
"type": "object"
},
"SkillsRemoteWriteResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"id",
"name",
"path"
],
"title": "SkillsRemoteWriteResponse",
"type": "object"
},
"SubAgentSource": {
"oneOf": [
{
@@ -13657,6 +13879,24 @@
"title": "ThreadArchiveResponse",
"type": "object"
},
"ThreadCompactStartParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadCompactStartParams",
"type": "object"
},
"ThreadCompactStartResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadCompactStartResponse",
"type": "object"
},
"ThreadForkParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",

View File

@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -2012,6 +2012,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -2857,7 +2910,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -3122,10 +3175,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -3432,6 +3482,25 @@
}
]
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
@@ -3689,7 +3758,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -144,7 +144,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -530,7 +530,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -2012,6 +2012,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -2857,7 +2910,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -3122,10 +3175,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -3432,6 +3482,25 @@
}
]
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
@@ -3689,7 +3758,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -2012,6 +2012,59 @@
"title": "ListSkillsResponseEventMsg",
"type": "object"
},
{
"description": "List of remote skills available to the agent.",
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
},
"type": {
"enum": [
"list_remote_skills_response"
],
"title": "ListRemoteSkillsResponseEventMsgType",
"type": "string"
}
},
"required": [
"skills",
"type"
],
"title": "ListRemoteSkillsResponseEventMsg",
"type": "object"
},
{
"description": "Remote skill downloaded to local cache.",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"type": {
"enum": [
"remote_skill_downloaded"
],
"title": "RemoteSkillDownloadedEventMsgType",
"type": "string"
}
},
"required": [
"id",
"name",
"path",
"type"
],
"title": "RemoteSkillDownloadedEventMsg",
"type": "object"
},
{
"description": "Notification that skill data may have been updated and clients may want to reload.",
"properties": {
@@ -2857,7 +2910,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -3122,10 +3175,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -3432,6 +3482,25 @@
}
]
},
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
@@ -3689,7 +3758,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -59,6 +59,12 @@
"supportsPersonality": {
"default": false,
"type": "boolean"
},
"upgrade": {
"type": [
"string",
"null"
]
}
},
"required": [

View File

@@ -111,7 +111,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -417,7 +417,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SkillsRemoteReadParams",
"type": "object"
}

View File

@@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"RemoteSkillSummary": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
}
},
"properties": {
"data": {
"items": {
"$ref": "#/definitions/RemoteSkillSummary"
},
"type": "array"
}
},
"required": [
"data"
],
"title": "SkillsRemoteReadResponse",
"type": "object"
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"hazelnutId": {
"type": "string"
},
"isPreload": {
"type": "boolean"
}
},
"required": [
"hazelnutId",
"isPreload"
],
"title": "SkillsRemoteWriteParams",
"type": "object"
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"id",
"name",
"path"
],
"title": "SkillsRemoteWriteResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadCompactStartParams",
"type": "object"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadCompactStartResponse",
"type": "object"
}

View File

@@ -120,7 +120,7 @@
]
},
"FunctionCallOutputPayload": {
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses/Chat Completions APIs understand.",
"description": "The payload we send back to OpenAI when reporting a tool call result.\n\n`content` preserves the historical plain-string payload so downstream integrations (tests, logging, etc.) can keep treating tool output as `String`. When an MCP server returns richer data we additionally populate `content_items` with the structured form that the Responses API understands.",
"properties": {
"content": {
"type": "string"
@@ -433,7 +433,7 @@
]
},
"id": {
"description": "Set when using the chat completions API.",
"description": "Legacy id field retained for compatibility with older payloads.",
"type": [
"string",
"null"

View File

@@ -53,10 +53,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},

View File

@@ -36,7 +36,10 @@ import type { ModelListParams } from "./v2/ModelListParams";
import type { ReviewStartParams } from "./v2/ReviewStartParams";
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
import type { SkillsListParams } from "./v2/SkillsListParams";
import type { SkillsRemoteReadParams } from "./v2/SkillsRemoteReadParams";
import type { SkillsRemoteWriteParams } from "./v2/SkillsRemoteWriteParams";
import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams";
import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams";
import type { ThreadForkParams } from "./v2/ThreadForkParams";
import type { ThreadListParams } from "./v2/ThreadListParams";
import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams";
@@ -52,4 +55,4 @@ import type { TurnStartParams } from "./v2/TurnStartParams";
/**
* Request from the client to the server.
*/
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };

View File

@@ -33,6 +33,7 @@ import type { GetHistoryEntryResponseEvent } from "./GetHistoryEntryResponseEven
import type { ItemCompletedEvent } from "./ItemCompletedEvent";
import type { ItemStartedEvent } from "./ItemStartedEvent";
import type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent";
import type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent";
import type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent";
import type { McpListToolsResponseEvent } from "./McpListToolsResponseEvent";
import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent";
@@ -45,6 +46,7 @@ import type { PlanDeltaEvent } from "./PlanDeltaEvent";
import type { RawResponseItemEvent } from "./RawResponseItemEvent";
import type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent";
import type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent";
import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
import type { RequestUserInputEvent } from "./RequestUserInputEvent";
import type { ReviewRequest } from "./ReviewRequest";
import type { SessionConfiguredEvent } from "./SessionConfiguredEvent";
@@ -70,4 +72,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
* Response event from the agent
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
*/
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent;
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent;

View File

@@ -9,7 +9,6 @@ import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentI
* `content` preserves the historical plain-string payload so downstream
* integrations (tests, logging, etc.) can keep treating tool output as
* `String`. When an MCP server returns richer data we additionally populate
* `content_items` with the structured form that the Responses/Chat
* Completions APIs understand.
* `content_items` with the structured form that the Responses API understands.
*/
export type FunctionCallOutputPayload = { content: string, content_items: Array<FunctionCallOutputContentItem> | null, success: boolean | null, };

View File

@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RemoteSkillSummary } from "./RemoteSkillSummary";
/**
* Response payload for `Op::ListRemoteSkills`.
*/
export type ListRemoteSkillsResponseEvent = { skills: Array<RemoteSkillSummary>, };

View File

@@ -5,4 +5,4 @@
/**
* Initial collaboration mode to use when the TUI starts.
*/
export type ModeKind = "plan" | "code" | "pair_programming" | "execute" | "custom";
export type ModeKind = "plan" | "default";

View File

@@ -0,0 +1,8 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Response payload for `Op::DownloadRemoteSkill`.
*/
export type RemoteSkillDownloadedEvent = { id: string, name: string, path: string, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemoteSkillSummary = { id: string, name: string, description: string, };

View File

@@ -98,6 +98,7 @@ export type { ItemStartedEvent } from "./ItemStartedEvent";
export type { ListConversationsParams } from "./ListConversationsParams";
export type { ListConversationsResponse } from "./ListConversationsResponse";
export type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent";
export type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent";
export type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent";
export type { LocalShellAction } from "./LocalShellAction";
export type { LocalShellExecAction } from "./LocalShellExecAction";
@@ -140,6 +141,8 @@ export type { ReasoningItemContent } from "./ReasoningItemContent";
export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
export type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent";
export type { ReasoningSummary } from "./ReasoningSummary";
export type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
export type { RemoteSkillSummary } from "./RemoteSkillSummary";
export type { RemoveConversationListenerParams } from "./RemoveConversationListenerParams";
export type { RemoveConversationSubscriptionResponse } from "./RemoveConversationSubscriptionResponse";
export type { RequestId } from "./RequestId";

View File

@@ -6,8 +6,8 @@ export type AppsListParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor: string | null,
cursor?: string | null,
/**
* Optional page size; defaults to a reasonable server-side value.
*/
limit: number | null, };
limit?: number | null, };

View File

@@ -13,4 +13,4 @@ export type ChatgptAuthTokensRefreshParams = { reason: ChatgptAuthTokensRefreshR
* This may be `null` when the prior ID token did not include a workspace
* identifier (`chatgpt_account_id`) or when the token could not be parsed.
*/
previousAccountId: string | null, };
previousAccountId?: string | null, };

View File

@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SandboxPolicy } from "./SandboxPolicy";
export type CommandExecParams = { command: Array<string>, timeoutMs: number | null, cwd: string | null, sandboxPolicy: SandboxPolicy | null, };
export type CommandExecParams = { command: Array<string>, timeoutMs?: number | null, cwd?: string | null, sandboxPolicy?: SandboxPolicy | null, };

View File

@@ -8,20 +8,20 @@ export type CommandExecutionRequestApprovalParams = { threadId: string, turnId:
/**
* Optional explanatory reason (e.g. request for network access).
*/
reason: string | null,
reason?: string | null,
/**
* The command to be executed.
*/
command?: string,
command?: string | null,
/**
* The command's working directory.
*/
cwd?: string,
cwd?: string | null,
/**
* Best-effort parsed command actions for friendly display.
*/
commandActions?: Array<CommandAction>,
commandActions?: Array<CommandAction> | null,
/**
* Optional proposed execpolicy amendment to allow similar commands without prompting.
*/
proposedExecpolicyAmendment: ExecPolicyAmendment | null, };
proposedExecpolicyAmendment?: ExecPolicyAmendment | null, };

View File

@@ -7,4 +7,4 @@ export type ConfigBatchWriteParams = { edits: Array<ConfigEdit>,
/**
* Path to the config file to write; defaults to the user's `config.toml` when omitted.
*/
filePath: string | null, expectedVersion: string | null, };
filePath?: string | null, expectedVersion?: string | null, };

View File

@@ -8,4 +8,4 @@ export type ConfigReadParams = { includeLayers: boolean,
* return the effective config as seen from that directory (i.e., including any
* project layers between `cwd` and the project/repo root).
*/
cwd: string | null, };
cwd?: string | null, };

View File

@@ -8,4 +8,4 @@ export type ConfigValueWriteParams = { keyPath: string, value: JsonValue, mergeS
/**
* Path to the config file to write; defaults to the user's `config.toml` when omitted.
*/
filePath: string | null, expectedVersion: string | null, };
filePath?: string | null, expectedVersion?: string | null, };

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FeedbackUploadParams = { classification: string, reason: string | null, threadId: string | null, includeLogs: boolean, };
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, };

View File

@@ -6,9 +6,9 @@ export type FileChangeRequestApprovalParams = { threadId: string, turnId: string
/**
* Optional explanatory reason (e.g. request for extra write access).
*/
reason: string | null,
reason?: string | null,
/**
* [UNSTABLE] When set, the agent is asking the user to allow writes under this root
* for the remainder of the session (unclear if this is honored today).
*/
grantRoot: string | null, };
grantRoot?: string | null, };

View File

@@ -6,8 +6,8 @@ export type ListMcpServerStatusParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor: string | null,
cursor?: string | null,
/**
* Optional page size; defaults to a server-defined value.
*/
limit: number | null, };
limit?: number | null, };

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type McpServerOauthLoginParams = { name: string, scopes?: Array<string>, timeoutSecs?: bigint, };
export type McpServerOauthLoginParams = { name: string, scopes?: Array<string> | null, timeoutSecs?: bigint | null, };

View File

@@ -5,4 +5,4 @@ import type { InputModality } from "../InputModality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
export type Model = { id: string, model: string, displayName: string, description: string, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, isDefault: boolean, };
export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, isDefault: boolean, };

View File

@@ -6,8 +6,8 @@ export type ModelListParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor: string | null,
cursor?: string | null,
/**
* Optional page size; defaults to a reasonable server-side value.
*/
limit: number | null, };
limit?: number | null, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemoteSkillSummary = { id: string, name: string, description: string, };

View File

@@ -9,4 +9,4 @@ export type ReviewStartParams = { threadId: string, target: ReviewTarget,
* Where to run the review: inline (default) on the current thread or
* detached on a new thread (returned in `reviewThreadId`).
*/
delivery: ReviewDelivery | null, };
delivery?: ReviewDelivery | null, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillsRemoteReadParams = Record<string, never>;

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RemoteSkillSummary } from "./RemoteSkillSummary";
export type SkillsRemoteReadResponse = { data: Array<RemoteSkillSummary>, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillsRemoteWriteResponse = { id: string, name: string, path: string, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadCompactStartParams = { threadId: string, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadCompactStartResponse = Record<string, never>;

View File

@@ -19,8 +19,8 @@ export type ThreadForkParams = { threadId: string,
* [UNSTABLE] Specify the rollout path to fork from.
* If specified, the thread_id param will be ignored.
*/
path: string | null,
path?: string | null,
/**
* Configuration overrides for the forked thread, if any.
*/
model: string | null, modelProvider: string | null, cwd: string | null, approvalPolicy: AskForApproval | null, sandbox: SandboxMode | null, config: { [key in string]?: JsonValue } | null, baseInstructions: string | null, developerInstructions: string | null, };
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, };

View File

@@ -1,7 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { WebSearchAction } from "../WebSearchAction";
import type { JsonValue } from "../serde_json/JsonValue";
import type { CollabAgentState } from "./CollabAgentState";
import type { CollabAgentTool } from "./CollabAgentTool";
@@ -14,6 +13,7 @@ import type { McpToolCallResult } from "./McpToolCallResult";
import type { McpToolCallStatus } from "./McpToolCallStatus";
import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { UserInput } from "./UserInput";
import type { WebSearchAction } from "./WebSearchAction";
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "agentMessage", id: string, text: string, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
/**

View File

@@ -8,27 +8,27 @@ export type ThreadListParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor: string | null,
cursor?: string | null,
/**
* Optional page size; defaults to a reasonable server-side value.
*/
limit: number | null,
limit?: number | null,
/**
* Optional sort key; defaults to created_at.
*/
sortKey: ThreadSortKey | null,
sortKey?: ThreadSortKey | null,
/**
* Optional provider filter; when set, only sessions recorded under these
* providers are returned. When present but empty, includes all providers.
*/
modelProviders: Array<string> | null,
modelProviders?: Array<string> | null,
/**
* Optional source filter; when set, only sessions from these source kinds
* are returned. When omitted or empty, defaults to interactive sources.
*/
sourceKinds: Array<ThreadSourceKind> | null,
sourceKinds?: Array<ThreadSourceKind> | null,
/**
* Optional archived filter; when set to true, only archived threads are returned.
* If false or null, only non-archived threads are returned.
*/
archived: boolean | null, };
archived?: boolean | null, };

View File

@@ -6,8 +6,8 @@ export type ThreadLoadedListParams = {
/**
* Opaque pagination cursor returned by a previous call.
*/
cursor: string | null,
cursor?: string | null,
/**
* Optional page size; defaults to no limit.
*/
limit: number | null, };
limit?: number | null, };

View File

@@ -24,13 +24,13 @@ export type ThreadResumeParams = { threadId: string,
* If specified, the thread will be resumed with the provided history
* instead of loaded from disk.
*/
history: Array<ResponseItem> | null,
history?: Array<ResponseItem> | null,
/**
* [UNSTABLE] Specify the rollout path to resume from.
* If specified, the thread_id param will be ignored.
*/
path: string | null,
path?: string | null,
/**
* Configuration overrides for the resumed thread, if any.
*/
model: string | null, modelProvider: string | null, cwd: string | null, approvalPolicy: AskForApproval | null, sandbox: SandboxMode | null, config: { [key in string]?: JsonValue } | null, baseInstructions: string | null, developerInstructions: string | null, personality: Personality | null, };
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, };

View File

@@ -6,7 +6,7 @@ import type { JsonValue } from "../serde_json/JsonValue";
import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
export type ThreadStartParams = {model: string | null, modelProvider: string | null, cwd: string | null, approvalPolicy: AskForApproval | null, sandbox: SandboxMode | null, config: { [key in string]?: JsonValue } | null, baseInstructions: string | null, developerInstructions: string | null, personality: Personality | null, ephemeral: boolean | null, /**
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
* If true, opt into emitting raw response items on the event stream.
*
* This is for internal use only (e.g. Codex Cloud).

View File

@@ -14,37 +14,37 @@ export type TurnStartParams = { threadId: string, input: Array<UserInput>,
/**
* Override the working directory for this turn and subsequent turns.
*/
cwd: string | null,
cwd?: string | null,
/**
* Override the approval policy for this turn and subsequent turns.
*/
approvalPolicy: AskForApproval | null,
approvalPolicy?: AskForApproval | null,
/**
* Override the sandbox policy for this turn and subsequent turns.
*/
sandboxPolicy: SandboxPolicy | null,
sandboxPolicy?: SandboxPolicy | null,
/**
* Override the model for this turn and subsequent turns.
*/
model: string | null,
model?: string | null,
/**
* Override the reasoning effort for this turn and subsequent turns.
*/
effort: ReasoningEffort | null,
effort?: ReasoningEffort | null,
/**
* Override the reasoning summary for this turn and subsequent turns.
*/
summary: ReasoningSummary | null,
summary?: ReasoningSummary | null,
/**
* Override the personality for this turn and subsequent turns.
*/
personality: Personality | null,
personality?: Personality | null,
/**
* Optional JSON Schema used to constrain the final assistant message for this turn.
*/
outputSchema: JsonValue | null,
outputSchema?: JsonValue | null,
/**
* EXPERIMENTAL - set a pre-set collaboration mode.
* Takes precedence over model, reasoning_effort, and developer instructions if set.
*/
collaborationMode: CollaborationMode | null, };
collaborationMode?: CollaborationMode | null, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type WebSearchAction = { "type": "search", query: string | null, queries: Array<string> | null, } | { "type": "openPage", url: string | null, } | { "type": "findInPage", url: string | null, pattern: string | null, } | { "type": "other" };

View File

@@ -96,6 +96,7 @@ export type { ReasoningEffortOption } from "./ReasoningEffortOption";
export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification";
export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification";
export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification";
export type { RemoteSkillSummary } from "./RemoteSkillSummary";
export type { ResidencyRequirement } from "./ResidencyRequirement";
export type { ReviewDelivery } from "./ReviewDelivery";
export type { ReviewStartParams } from "./ReviewStartParams";
@@ -116,6 +117,10 @@ export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse";
export type { SkillsListEntry } from "./SkillsListEntry";
export type { SkillsListParams } from "./SkillsListParams";
export type { SkillsListResponse } from "./SkillsListResponse";
export type { SkillsRemoteReadParams } from "./SkillsRemoteReadParams";
export type { SkillsRemoteReadResponse } from "./SkillsRemoteReadResponse";
export type { SkillsRemoteWriteParams } from "./SkillsRemoteWriteParams";
export type { SkillsRemoteWriteResponse } from "./SkillsRemoteWriteResponse";
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
export type { TextElement } from "./TextElement";
export type { TextPosition } from "./TextPosition";
@@ -123,6 +128,8 @@ export type { TextRange } from "./TextRange";
export type { Thread } from "./Thread";
export type { ThreadArchiveParams } from "./ThreadArchiveParams";
export type { ThreadArchiveResponse } from "./ThreadArchiveResponse";
export type { ThreadCompactStartParams } from "./ThreadCompactStartParams";
export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
export type { ThreadForkParams } from "./ThreadForkParams";
export type { ThreadForkResponse } from "./ThreadForkResponse";
export type { ThreadItem } from "./ThreadItem";
@@ -169,5 +176,6 @@ export type { TurnStartResponse } from "./TurnStartResponse";
export type { TurnStartedNotification } from "./TurnStartedNotification";
export type { TurnStatus } from "./TurnStatus";
export type { UserInput } from "./UserInput";
export type { WebSearchAction } from "./WebSearchAction";
export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification";
export type { WriteStatus } from "./WriteStatus";

View File

@@ -1402,8 +1402,8 @@ mod tests {
use uuid::Uuid;
#[test]
fn generated_ts_has_no_optional_nullable_fields() -> Result<()> {
// Assert that there are no types of the form "?: T | null" in the generated TS files.
fn generated_ts_optional_nullable_fields_only_in_params() -> Result<()> {
// Assert that "?: T | null" only appears in generated *Params types.
let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7()));
fs::create_dir(&output_dir)?;
@@ -1463,6 +1463,13 @@ mod tests {
}
if matches!(path.extension().and_then(|ext| ext.to_str()), Some("ts")) {
// Only allow "?: T | null" in objects representing JSON-RPC requests,
// which we assume are called "*Params".
let allow_optional_nullable = path
.file_stem()
.and_then(|stem| stem.to_str())
.is_some_and(|stem| stem.ends_with("Params"));
let contents = fs::read_to_string(&path)?;
if contents.contains("| undefined") {
undefined_offenders.push(path.clone());
@@ -1583,9 +1590,11 @@ mod tests {
}
// If the last non-whitespace before ':' is '?', then this is an
// optional field with a nullable type (i.e., "?: T | null"),
// which we explicitly disallow.
if field_prefix.chars().rev().find(|c| !c.is_whitespace()) == Some('?') {
// optional field with a nullable type (i.e., "?: T | null").
// These are only allowed in *Params types.
if field_prefix.chars().rev().find(|c| !c.is_whitespace()) == Some('?')
&& !allow_optional_nullable
{
let line_number =
contents[..abs_idx].chars().filter(|c| *c == '\n').count() + 1;
let offending_line_end = contents[line_start_idx..]
@@ -1613,12 +1622,12 @@ mod tests {
"Generated TypeScript still includes unions with `undefined` in {undefined_offenders:?}"
);
// If this assertion fails, it means a field was generated as
// "?: T | null" — i.e., both optional (undefined) and nullable (null).
// We only want either "?: T" or ": T | null".
// If this assertion fails, it means a field was generated as "?: T | null",
// which is both optional (undefined) and nullable (null), for a type not ending
// in "Params" (which represent JSON-RPC requests).
assert!(
optional_nullable_offenders.is_empty(),
"Generated TypeScript has optional fields with nullable types (disallowed '?: T | null'), add #[ts(optional)] to fix:\n{optional_nullable_offenders:?}"
"Generated TypeScript has optional nullable fields outside *Params types (disallowed '?: T | null'):\n{optional_nullable_offenders:?}"
);
Ok(())

View File

@@ -208,6 +208,10 @@ client_request_definitions! {
params: v2::ThreadUnarchiveParams,
response: v2::ThreadUnarchiveResponse,
},
ThreadCompactStart => "thread/compact/start" {
params: v2::ThreadCompactStartParams,
response: v2::ThreadCompactStartResponse,
},
ThreadRollback => "thread/rollback" {
params: v2::ThreadRollbackParams,
response: v2::ThreadRollbackResponse,
@@ -228,6 +232,14 @@ client_request_definitions! {
params: v2::SkillsListParams,
response: v2::SkillsListResponse,
},
SkillsRemoteRead => "skills/remote/read" {
params: v2::SkillsRemoteReadParams,
response: v2::SkillsRemoteReadResponse,
},
SkillsRemoteWrite => "skills/remote/write" {
params: v2::SkillsRemoteWriteParams,
response: v2::SkillsRemoteWriteResponse,
},
AppsList => "app/list" {
params: v2::AppsListParams,
response: v2::AppsListResponse,

View File

@@ -480,6 +480,7 @@ pub struct ConfigReadParams {
/// Optional working directory to resolve project config layers. If specified,
/// return the effective config as seen from that directory (i.e., including any
/// project layers between `cwd` and the project/repo root).
#[ts(optional = nullable)]
pub cwd: Option<String>,
}
@@ -525,7 +526,9 @@ pub struct ConfigValueWriteParams {
pub value: JsonValue,
pub merge_strategy: MergeStrategy,
/// Path to the config file to write; defaults to the user's `config.toml` when omitted.
#[ts(optional = nullable)]
pub file_path: Option<String>,
#[ts(optional = nullable)]
pub expected_version: Option<String>,
}
@@ -535,7 +538,9 @@ pub struct ConfigValueWriteParams {
pub struct ConfigBatchWriteParams {
pub edits: Vec<ConfigEdit>,
/// Path to the config file to write; defaults to the user's `config.toml` when omitted.
#[ts(optional = nullable)]
pub file_path: Option<String>,
#[ts(optional = nullable)]
pub expected_version: Option<String>,
}
@@ -935,6 +940,7 @@ pub struct ChatgptAuthTokensRefreshParams {
///
/// This may be `null` when the prior ID token did not include a workspace
/// identifier (`chatgpt_account_id`) or when the token could not be parsed.
#[ts(optional = nullable)]
pub previous_account_id: Option<String>,
}
@@ -979,8 +985,10 @@ pub struct GetAccountResponse {
#[ts(export_to = "v2/")]
pub struct ModelListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to a reasonable server-side value.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
@@ -990,6 +998,7 @@ pub struct ModelListParams {
pub struct Model {
pub id: String,
pub model: String,
pub upgrade: Option<String>,
pub display_name: String,
pub description: String,
pub supported_reasoning_efforts: Vec<ReasoningEffortOption>,
@@ -1039,8 +1048,10 @@ pub struct CollaborationModeListResponse {
#[ts(export_to = "v2/")]
pub struct ListMcpServerStatusParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to a server-defined value.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
@@ -1070,8 +1081,10 @@ pub struct ListMcpServerStatusResponse {
#[ts(export_to = "v2/")]
pub struct AppsListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to a reasonable server-side value.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
@@ -1116,10 +1129,10 @@ pub struct McpServerRefreshResponse {}
pub struct McpServerOauthLoginParams {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(optional = nullable)]
pub scopes: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(optional = nullable)]
pub timeout_secs: Option<i64>,
}
@@ -1135,7 +1148,9 @@ pub struct McpServerOauthLoginResponse {
#[ts(export_to = "v2/")]
pub struct FeedbackUploadParams {
pub classification: String,
#[ts(optional = nullable)]
pub reason: Option<String>,
#[ts(optional = nullable)]
pub thread_id: Option<String>,
pub include_logs: bool,
}
@@ -1153,8 +1168,11 @@ pub struct FeedbackUploadResponse {
pub struct CommandExecParams {
pub command: Vec<String>,
#[ts(type = "number | null")]
#[ts(optional = nullable)]
pub timeout_ms: Option<i64>,
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
}
@@ -1175,21 +1193,33 @@ pub struct CommandExecResponse {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadStartParams {
#[ts(optional = nullable)]
pub model: Option<String>,
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
#[ts(optional = nullable)]
pub personality: Option<Personality>,
#[ts(optional = nullable)]
pub ephemeral: Option<bool>,
#[experimental("thread/start.dynamicTools")]
#[ts(optional = nullable)]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
/// Test-only experimental field used to validate experimental gating and
/// schema filtering behavior in a stable way.
#[experimental("thread/start.mockExperimentalField")]
#[ts(optional = nullable)]
pub mock_experimental_field: Option<String>,
/// If true, opt into emitting raw response items on the event stream.
///
@@ -1204,6 +1234,7 @@ pub struct ThreadStartParams {
#[ts(export_to = "v2/")]
pub struct MockExperimentalMethodParams {
/// Test-only payload field.
#[ts(optional = nullable)]
pub value: Option<String>,
}
@@ -1246,21 +1277,32 @@ pub struct ThreadResumeParams {
/// [UNSTABLE] FOR CODEX CLOUD - DO NOT USE.
/// If specified, the thread will be resumed with the provided history
/// instead of loaded from disk.
#[ts(optional = nullable)]
pub history: Option<Vec<ResponseItem>>,
/// [UNSTABLE] Specify the rollout path to resume from.
/// If specified, the thread_id param will be ignored.
#[ts(optional = nullable)]
pub path: Option<PathBuf>,
/// Configuration overrides for the resumed thread, if any.
#[ts(optional = nullable)]
pub model: Option<String>,
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
#[ts(optional = nullable)]
pub personality: Option<Personality>,
}
@@ -1292,16 +1334,25 @@ pub struct ThreadForkParams {
/// [UNSTABLE] Specify the rollout path to fork from.
/// If specified, the thread_id param will be ignored.
#[ts(optional = nullable)]
pub path: Option<PathBuf>,
/// Configuration overrides for the forked thread, if any.
#[ts(optional = nullable)]
pub model: Option<String>,
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
}
@@ -1357,6 +1408,18 @@ pub struct ThreadUnarchiveResponse {
pub thread: Thread,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadCompactStartParams {
pub thread_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadCompactStartResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1386,19 +1449,25 @@ pub struct ThreadRollbackResponse {
#[ts(export_to = "v2/")]
pub struct ThreadListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to a reasonable server-side value.
#[ts(optional = nullable)]
pub limit: Option<u32>,
/// Optional sort key; defaults to created_at.
#[ts(optional = nullable)]
pub sort_key: Option<ThreadSortKey>,
/// Optional provider filter; when set, only sessions recorded under these
/// providers are returned. When present but empty, includes all providers.
#[ts(optional = nullable)]
pub model_providers: Option<Vec<String>>,
/// Optional source filter; when set, only sessions from these source kinds
/// are returned. When omitted or empty, defaults to interactive sources.
#[ts(optional = nullable)]
pub source_kinds: Option<Vec<ThreadSourceKind>>,
/// Optional archived filter; when set to true, only archived threads are returned.
/// If false or null, only non-archived threads are returned.
#[ts(optional = nullable)]
pub archived: Option<bool>,
}
@@ -1443,8 +1512,10 @@ pub struct ThreadListResponse {
#[ts(export_to = "v2/")]
pub struct ThreadLoadedListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to no limit.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
@@ -1496,6 +1567,44 @@ pub struct SkillsListResponse {
pub data: Vec<SkillsListEntry>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsRemoteReadParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct RemoteSkillSummary {
pub id: String,
pub name: String,
pub description: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsRemoteReadResponse {
pub data: Vec<RemoteSkillSummary>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsRemoteWriteParams {
pub hazelnut_id: String,
pub is_preload: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsRemoteWriteResponse {
pub id: String,
pub name: String,
pub path: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
@@ -1832,24 +1941,33 @@ pub struct TurnStartParams {
pub thread_id: String,
pub input: Vec<UserInput>,
/// Override the working directory for this turn and subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Override the approval policy for this turn and subsequent turns.
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
/// Override the sandbox policy for this turn and subsequent turns.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Override the model for this turn and subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
/// Override the reasoning effort for this turn and subsequent turns.
#[ts(optional = nullable)]
pub effort: Option<ReasoningEffort>,
/// Override the reasoning summary for this turn and subsequent turns.
#[ts(optional = nullable)]
pub summary: Option<ReasoningSummary>,
/// Override the personality for this turn and subsequent turns.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// Optional JSON Schema used to constrain the final assistant message for this turn.
#[ts(optional = nullable)]
pub output_schema: Option<JsonValue>,
/// EXPERIMENTAL - set a pre-set collaboration mode.
/// Takes precedence over model, reasoning_effort, and developer instructions if set.
#[ts(optional = nullable)]
pub collaboration_mode: Option<CollaborationMode>,
}
@@ -1863,6 +1981,7 @@ pub struct ReviewStartParams {
/// Where to run the review: inline (default) on the current thread or
/// detached on a new thread (returned in `reviewThreadId`).
#[serde(default)]
#[ts(optional = nullable)]
pub delivery: Option<ReviewDelivery>,
}
@@ -2170,6 +2289,7 @@ pub enum ThreadItem {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum WebSearchAction {
Search {
query: Option<String>,
@@ -2643,20 +2763,22 @@ pub struct CommandExecutionRequestApprovalParams {
pub turn_id: String,
pub item_id: String,
/// Optional explanatory reason (e.g. request for network access).
#[ts(optional = nullable)]
pub reason: Option<String>,
/// The command to be executed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(optional = nullable)]
pub command: Option<String>,
/// The command's working directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Best-effort parsed command actions for friendly display.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(optional = nullable)]
pub command_actions: Option<Vec<CommandAction>>,
/// Optional proposed execpolicy amendment to allow similar commands without prompting.
#[ts(optional = nullable)]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
}
@@ -2675,9 +2797,11 @@ pub struct FileChangeRequestApprovalParams {
pub turn_id: String,
pub item_id: String,
/// Optional explanatory reason (e.g. request for extra write access).
#[ts(optional = nullable)]
pub reason: Option<String>,
/// [UNSTABLE] When set, the agent is asking the user to allow writes under this root
/// for the remainder of the session (unclear if this is honored today).
#[ts(optional = nullable)]
pub grant_root: Option<PathBuf>,
}

View File

@@ -1,6 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "codex-app-server-test-client",
name = "app-server-test-client",
crate_name = "codex_app_server_test_client",
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -85,14 +85,17 @@ Example (from OpenAI's official VSCode extension):
- `thread/archive` — move a threads rollout file into the archived directory; returns `{}` on success.
- `thread/name/set` — set or update a threads user-facing name; returns `{}` on success. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success.
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
- `thread/rollback` — drop the last N turns from the agents in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `review/start` — kick off Codexs automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
- `model/list` — list available models (with reasoning effort options).
- `model/list` — list available models (with reasoning effort options and optional `upgrade` model ids).
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination).
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `skills/remote/read` — list public remote skills (**under development; do not call from production clients yet**).
- `skills/remote/write` — download a public remote skill by `hazelnutId`; `isPreload=true` writes to `.codex/vendor_imports/skills` under `codex_home` (**under development; do not call from production clients yet**).
- `app/list` — list available apps.
- `skills/config/write` — write user-level skill config by path.
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
@@ -237,6 +240,22 @@ Use `thread/unarchive` to move an archived rollout back into the sessions direct
{ "id": 24, "result": { "thread": { "id": "thr_b" } } }
```
### Example: Trigger thread compaction
Use `thread/compact/start` to trigger manual history compaction for a thread. The request returns immediately with `{}`.
Progress is emitted as standard `turn/*` and `item/*` notifications on the same `threadId`. Clients should expect a single compaction item:
- `item/started` with `item: { "type": "contextCompaction", ... }`
- `item/completed` with the same `contextCompaction` item id
While compaction is running, the thread is effectively in a turn so clients should surface progress UI based on the notifications.
```json
{ "method": "thread/compact/start", "id": 25, "params": { "threadId": "thr_b" } }
{ "id": 25, "result": {} }
```
### Example: Start a turn (send user input)
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:

View File

@@ -97,9 +97,15 @@ use codex_app_server_protocol::SkillsConfigWriteParams;
use codex_app_server_protocol::SkillsConfigWriteResponse;
use codex_app_server_protocol::SkillsListParams;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::SkillsRemoteReadParams;
use codex_app_server_protocol::SkillsRemoteReadResponse;
use codex_app_server_protocol::SkillsRemoteWriteParams;
use codex_app_server_protocol::SkillsRemoteWriteResponse;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadArchiveParams;
use codex_app_server_protocol::ThreadArchiveResponse;
use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadCompactStartResponse;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadItem;
@@ -176,6 +182,8 @@ use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::skills::remote::download_remote_skill;
use codex_core::skills::remote::list_remote_skills;
use codex_core::state_db::get_state_db;
use codex_core::token_data::parse_id_token;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
@@ -449,6 +457,9 @@ impl CodexMessageProcessor {
ClientRequest::ThreadUnarchive { request_id, params } => {
self.thread_unarchive(request_id, params).await;
}
ClientRequest::ThreadCompactStart { request_id, params } => {
self.thread_compact_start(request_id, params).await;
}
ClientRequest::ThreadRollback { request_id, params } => {
self.thread_rollback(request_id, params).await;
}
@@ -464,6 +475,12 @@ impl CodexMessageProcessor {
ClientRequest::SkillsList { request_id, params } => {
self.skills_list(request_id, params).await;
}
ClientRequest::SkillsRemoteRead { request_id, params } => {
self.skills_remote_read(request_id, params).await;
}
ClientRequest::SkillsRemoteWrite { request_id, params } => {
self.skills_remote_write(request_id, params).await;
}
ClientRequest::AppsList { request_id, params } => {
self.apps_list(request_id, params).await;
}
@@ -1430,7 +1447,7 @@ impl CodexMessageProcessor {
}
let cwd = params.cwd.unwrap_or_else(|| self.config.cwd.clone());
let env = create_env(&self.config.shell_environment_policy);
let env = create_env(&self.config.shell_environment_policy, None);
let timeout_ms = params
.timeout_ms
.and_then(|timeout_ms| u64::try_from(timeout_ms).ok());
@@ -2082,6 +2099,30 @@ impl CodexMessageProcessor {
}
}
async fn thread_compact_start(&self, request_id: RequestId, params: ThreadCompactStartParams) {
let ThreadCompactStartParams { thread_id } = params;
let (_, thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
match thread.submit(Op::Compact).await {
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadCompactStartResponse {})
.await;
}
Err(err) => {
self.send_internal_error(request_id, format!("failed to start compaction: {err}"))
.await;
}
}
}
async fn thread_list(&self, request_id: RequestId, params: ThreadListParams) {
let ThreadListParams {
cursor,
@@ -4053,6 +4094,61 @@ impl CodexMessageProcessor {
.await;
}
async fn skills_remote_read(&self, request_id: RequestId, _params: SkillsRemoteReadParams) {
match list_remote_skills(&self.config).await {
Ok(skills) => {
let data = skills
.into_iter()
.map(|skill| codex_app_server_protocol::RemoteSkillSummary {
id: skill.id,
name: skill.name,
description: skill.description,
})
.collect();
self.outgoing
.send_response(request_id, SkillsRemoteReadResponse { data })
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to read remote skills: {err}"),
)
.await;
}
}
}
async fn skills_remote_write(&self, request_id: RequestId, params: SkillsRemoteWriteParams) {
let SkillsRemoteWriteParams {
hazelnut_id,
is_preload,
} = params;
let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await;
match response {
Ok(downloaded) => {
self.outgoing
.send_response(
request_id,
SkillsRemoteWriteResponse {
id: downloaded.id,
name: downloaded.name,
path: downloaded.path,
},
)
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to download remote skill: {err}"),
)
.await;
}
}
}
async fn skills_config_write(&self, request_id: RequestId, params: SkillsConfigWriteParams) {
let SkillsConfigWriteParams { path, enabled } = params;
let edits = vec![ConfigEdit::SetSkillConfig { path, enabled }];

View File

@@ -22,6 +22,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
Model {
id: preset.id.to_string(),
model: preset.model.to_string(),
upgrade: preset.upgrade.map(|upgrade| upgrade.id),
display_name: preset.display_name.to_string(),
description: preset.description.to_string(),
supported_reasoning_efforts: reasoning_efforts_from_preset(

View File

@@ -50,6 +50,7 @@ use codex_app_server_protocol::SendUserTurnParams;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::SetDefaultModelParams;
use codex_app_server_protocol::ThreadArchiveParams;
use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadLoadedListParams;
@@ -418,6 +419,15 @@ impl McpProcess {
self.send_request("thread/unarchive", params).await
}
/// Send a `thread/compact/start` JSON-RPC request.
pub async fn send_thread_compact_start_request(
&mut self,
params: ThreadCompactStartParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/compact/start", params).await
}
/// Send a `thread/rollback` JSON-RPC request.
pub async fn send_thread_rollback_request(
&mut self,

View File

@@ -1,8 +1,8 @@
//! Validates that the collaboration mode list endpoint returns the expected default presets.
//!
//! The test drives the app server through the MCP harness and asserts that the list response
//! includes the plan, coding, pair programming, and execute modes with their default model and reasoning
//! effort settings, which keeps the API contract visible in one place.
//! includes the plan and default modes with their default model and reasoning effort
//! settings, which keeps the API contract visible in one place.
#![allow(clippy::unwrap_used)]
@@ -45,23 +45,8 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> {
let CollaborationModeListResponse { data: items } =
to_response::<CollaborationModeListResponse>(response)?;
let expected = [
plan_preset(),
code_preset(),
pair_programming_preset(),
execute_preset(),
];
assert_eq!(expected.len(), items.len());
for (expected_mask, actual_mask) in expected.iter().zip(items.iter()) {
assert_eq!(expected_mask.name, actual_mask.name);
assert_eq!(expected_mask.mode, actual_mask.mode);
assert_eq!(expected_mask.model, actual_mask.model);
assert_eq!(expected_mask.reasoning_effort, actual_mask.reasoning_effort);
assert_eq!(
expected_mask.developer_instructions,
actual_mask.developer_instructions
);
}
let expected = vec![plan_preset(), default_preset()];
assert_eq!(expected, items);
Ok(())
}
@@ -77,35 +62,11 @@ fn plan_preset() -> CollaborationModeMask {
.unwrap()
}
/// Builds the pair programming preset that the list response is expected to return.
///
/// The helper keeps the expected model and reasoning defaults co-located with the test
/// so that mismatches point directly at the API contract being exercised.
fn pair_programming_preset() -> CollaborationModeMask {
/// Builds the default preset that the list response is expected to return.
fn default_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::PairProgramming))
.unwrap()
}
/// Builds the code preset that the list response is expected to return.
fn code_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Code))
.unwrap()
}
/// Builds the execute preset that the list response is expected to return.
///
/// The execute preset uses a different reasoning effort to capture the higher-effort
/// execution contract the server currently exposes.
fn execute_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Execute))
.find(|p| p.mode == Some(ModeKind::Default))
.unwrap()
}

View File

@@ -15,9 +15,12 @@ use app_test_support::write_chatgpt_auth;
use app_test_support::write_mock_responses_config_toml;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadCompactStartResponse;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
@@ -39,6 +42,7 @@ use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const AUTO_COMPACT_LIMIT: i64 = 1_000;
const COMPACT_PROMPT: &str = "Summarize the conversation.";
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()> {
@@ -196,6 +200,134 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let sse = responses::sse(vec![
responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"),
responses::ev_completed_with_tokens("r1", 200),
]);
responses::mount_sse_sequence(&server, vec![sse]).await;
let codex_home = TempDir::new()?;
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
"mock_provider",
COMPACT_PROMPT,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_thread(&mut mcp).await?;
let compact_id = mcp
.send_thread_compact_start_request(ThreadCompactStartParams {
thread_id: thread_id.clone(),
})
.await?;
let compact_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(compact_id)),
)
.await??;
let _compact: ThreadCompactStartResponse =
to_response::<ThreadCompactStartResponse>(compact_resp)?;
let started = wait_for_context_compaction_started(&mut mcp).await?;
let completed = wait_for_context_compaction_completed(&mut mcp).await?;
let ThreadItem::ContextCompaction { id: started_id } = started.item else {
unreachable!("started item should be context compaction");
};
let ThreadItem::ContextCompaction { id: completed_id } = completed.item else {
unreachable!("completed item should be context compaction");
};
assert_eq!(started.thread_id, thread_id);
assert_eq!(completed.thread_id, thread_id);
assert_eq!(started_id, completed_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let codex_home = TempDir::new()?;
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
"mock_provider",
COMPACT_PROMPT,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_thread_compact_start_request(ThreadCompactStartParams {
thread_id: "not-a-thread-id".to_string(),
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(error.error.message.contains("invalid thread id"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let codex_home = TempDir::new()?;
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
"mock_provider",
COMPACT_PROMPT,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_thread_compact_start_request(ThreadCompactStartParams {
thread_id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
})
.await?;
let error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(error.error.message.contains("thread not found"));
Ok(())
}
async fn start_thread(mcp: &mut McpProcess) -> Result<String> {
let thread_id = mcp
.send_thread_start_request(ThreadStartParams {

View File

@@ -51,6 +51,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Model {
id: "gpt-5.2-codex".to_string(),
model: "gpt-5.2-codex".to_string(),
upgrade: None,
display_name: "gpt-5.2-codex".to_string(),
description: "Latest frontier agentic coding model.".to_string(),
supported_reasoning_efforts: vec![
@@ -80,6 +81,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Model {
id: "gpt-5.1-codex-max".to_string(),
model: "gpt-5.1-codex-max".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-max".to_string(),
description: "Codex-optimized flagship for deep and fast reasoning.".to_string(),
supported_reasoning_efforts: vec![
@@ -109,6 +111,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Model {
id: "gpt-5.1-codex-mini".to_string(),
model: "gpt-5.1-codex-mini".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-mini".to_string(),
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
supported_reasoning_efforts: vec![
@@ -130,6 +133,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Model {
id: "gpt-5.2".to_string(),
model: "gpt-5.2".to_string(),
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.2".to_string(),
description:
"Latest frontier model with improvements across knowledge, reasoning and coding"

View File

@@ -1,6 +1,9 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_shell_command_sse_response;
use app_test_support::to_response;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
@@ -12,6 +15,7 @@ use codex_app_server_protocol::ReviewDelivery;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
use codex_app_server_protocol::ReviewTarget;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
@@ -129,6 +133,91 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<()
Ok(())
}
#[tokio::test]
async fn review_start_exec_approval_item_id_matches_command_execution_item() -> Result<()> {
let responses = vec![
create_shell_command_sse_response(
vec![
"git".to_string(),
"rev-parse".to_string(),
"HEAD".to_string(),
],
None,
Some(5000),
"review-call-1",
)?,
create_final_assistant_message_sse_response("done")?,
];
let server = create_mock_responses_server_sequence(responses).await;
let codex_home = TempDir::new()?;
create_config_toml_with_approval_policy(codex_home.path(), &server.uri(), "untrusted")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_default_thread(&mut mcp).await?;
let review_req = mcp
.send_review_start_request(ReviewStartParams {
thread_id,
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::Commit {
sha: "1234567deadbeef".to_string(),
title: Some("Check review approvals".to_string()),
},
})
.await?;
let review_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(review_req)),
)
.await??;
let ReviewStartResponse { turn, .. } = to_response::<ReviewStartResponse>(review_resp)?;
let turn_id = turn.id.clone();
let server_req = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_request_message(),
)
.await??;
let ServerRequest::CommandExecutionRequestApproval { request_id, params } = server_req else {
panic!("expected CommandExecutionRequestApproval request");
};
assert_eq!(params.item_id, "review-call-1");
assert_eq!(params.turn_id, turn_id);
let mut command_item_id = None;
for _ in 0..10 {
let item_started: JSONRPCNotification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("item/started"),
)
.await??;
let started: ItemStartedNotification =
serde_json::from_value(item_started.params.expect("params must be present"))?;
if let ThreadItem::CommandExecution { id, .. } = started.item {
command_item_id = Some(id);
break;
}
}
let command_item_id = command_item_id.expect("did not observe command execution item");
assert_eq!(command_item_id, params.item_id);
mcp.send_response(
request_id,
serde_json::json!({ "decision": codex_core::protocol::ReviewDecision::Approved }),
)
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
#[tokio::test]
async fn review_start_rejects_empty_base_branch() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -299,13 +388,21 @@ async fn start_default_thread(mcp: &mut McpProcess) -> Result<String> {
}
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
create_config_toml_with_approval_policy(codex_home, server_uri, "never")
}
fn create_config_toml_with_approval_policy(
codex_home: &std::path::Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
model_provider = "mock_provider"

View File

@@ -365,7 +365,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: "mock-model-collab".to_string(),
reasoning_effort: Some(ReasoningEffort::High),

View File

@@ -1,3 +1,4 @@
use std::fs::File;
use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
@@ -10,8 +11,24 @@ use tempfile::TempDir;
const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
const APPLY_PATCH_ARG0: &str = "apply_patch";
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
const LOCK_FILENAME: &str = ".lock";
pub fn arg0_dispatch() -> Option<TempDir> {
/// Keeps the per-session PATH entry alive and locked for the process lifetime.
pub struct Arg0PathEntryGuard {
_temp_dir: TempDir,
_lock_file: File,
}
impl Arg0PathEntryGuard {
fn new(temp_dir: TempDir, lock_file: File) -> Self {
Self {
_temp_dir: temp_dir,
_lock_file: lock_file,
}
}
}
pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
// Determine if we were invoked via the special alias.
let mut args = std::env::args_os();
let argv0 = args.next().unwrap_or_default();
@@ -149,7 +166,7 @@ where
///
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
/// be called before multiple threads are spawned.
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGuard> {
let codex_home = codex_core::config::find_codex_home()?;
#[cfg(not(debug_assertions))]
{
@@ -167,7 +184,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
std::fs::create_dir_all(&codex_home)?;
// Use a CODEX_HOME-scoped temp root to avoid cluttering the top-level directory.
let temp_root = codex_home.join("tmp").join("path");
let temp_root = codex_home.join("tmp").join("arg0");
std::fs::create_dir_all(&temp_root)?;
#[cfg(unix)]
{
@@ -177,11 +194,25 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
std::fs::set_permissions(&temp_root, std::fs::Permissions::from_mode(0o700))?;
}
// Best-effort cleanup of stale per-session dirs. Ignore failures so startup proceeds.
if let Err(err) = janitor_cleanup(&temp_root) {
eprintln!("WARNING: failed to clean up stale arg0 temp dirs: {err}");
}
let temp_dir = tempfile::Builder::new()
.prefix("codex-arg0")
.tempdir_in(&temp_root)?;
let path = temp_dir.path();
let lock_path = path.join(LOCK_FILENAME);
let lock_file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)?;
lock_file.try_lock()?;
for filename in &[
APPLY_PATCH_ARG0,
MISSPELLED_APPLY_PATCH_ARG0,
@@ -231,5 +262,107 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
std::env::set_var("PATH", updated_path_env_var);
}
Ok(temp_dir)
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file))
}
fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {
let entries = match std::fs::read_dir(temp_root) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err),
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
// Skip the directory if locking fails or the lock is currently held.
let Some(_lock_file) = try_lock_dir(&path)? else {
continue;
};
match std::fs::remove_dir_all(&path) {
Ok(()) => {}
// Expected TOCTOU race: directory can disappear after read_dir/lock checks.
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
}
}
Ok(())
}
fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>> {
let lock_path = dir.join(LOCK_FILENAME);
let lock_file = match File::options().read(true).write(true).open(&lock_path) {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
};
match lock_file.try_lock() {
Ok(()) => Ok(Some(lock_file)),
Err(std::fs::TryLockError::WouldBlock) => Ok(None),
Err(err) => Err(err.into()),
}
}
#[cfg(test)]
mod tests {
use super::LOCK_FILENAME;
use super::janitor_cleanup;
use std::fs;
use std::fs::File;
use std::path::Path;
fn create_lock(dir: &Path) -> std::io::Result<File> {
let lock_path = dir.join(LOCK_FILENAME);
File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(lock_path)
}
#[test]
fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> {
let root = tempfile::tempdir()?;
let dir = root.path().join("no-lock");
fs::create_dir(&dir)?;
janitor_cleanup(root.path())?;
assert!(dir.exists());
Ok(())
}
#[test]
fn janitor_skips_dirs_with_held_lock() -> std::io::Result<()> {
let root = tempfile::tempdir()?;
let dir = root.path().join("locked");
fs::create_dir(&dir)?;
let lock_file = create_lock(&dir)?;
lock_file.try_lock()?;
janitor_cleanup(root.path())?;
assert!(dir.exists());
Ok(())
}
#[test]
fn janitor_removes_dirs_with_unlocked_lock() -> std::io::Result<()> {
let root = tempfile::tempdir()?;
let dir = root.path().join("stale");
fs::create_dir(&dir)?;
create_lock(&dir)?;
janitor_cleanup(root.path())?;
assert!(!dir.exists());
Ok(())
}
}

View File

@@ -21,6 +21,7 @@ clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
codex-app-server = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-app-server-test-client = { workspace = true }
codex-arg0 = { workspace = true }
codex-chatgpt = { workspace = true }
codex-cloud-tasks = { path = "../cloud-tasks" }

View File

@@ -130,7 +130,7 @@ async fn run_command_under_sandbox(
let sandbox_policy_cwd = cwd.clone();
let stdio_policy = StdioPolicy::Inherit;
let env = create_env(&config.shell_environment_policy);
let env = create_env(&config.shell_environment_policy, None);
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
if let SandboxType::Windows = sandbox_type {

View File

@@ -110,9 +110,11 @@ enum Subcommand {
Completion(CompletionCommand),
/// Run commands within a Codex-provided sandbox.
#[clap(visible_alias = "debug")]
Sandbox(SandboxArgs),
/// Debugging tools.
Debug(DebugCommand),
/// Execpolicy tooling.
#[clap(hide = true)]
Execpolicy(ExecpolicyCommand),
@@ -150,6 +152,36 @@ struct CompletionCommand {
shell: Shell,
}
#[derive(Debug, Parser)]
struct DebugCommand {
#[command(subcommand)]
subcommand: DebugSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum DebugSubcommand {
/// Tooling: helps debug the app server.
AppServer(DebugAppServerCommand),
}
#[derive(Debug, Parser)]
struct DebugAppServerCommand {
#[command(subcommand)]
subcommand: DebugAppServerSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum DebugAppServerSubcommand {
// Send message to app server V2.
SendMessageV2(DebugAppServerSendMessageV2Command),
}
#[derive(Debug, Parser)]
struct DebugAppServerSendMessageV2Command {
#[arg(value_name = "USER_MESSAGE", required = true)]
user_message: String,
}
#[derive(Debug, Parser)]
struct ResumeCommand {
/// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses.
@@ -425,6 +457,15 @@ fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> {
cmd.run()
}
fn run_debug_app_server_command(cmd: DebugAppServerCommand) -> anyhow::Result<()> {
match cmd.subcommand {
DebugAppServerSubcommand::SendMessageV2(cmd) => {
let codex_bin = std::env::current_exe()?;
codex_app_server_test_client::send_message_v2(&codex_bin, &[], cmd.user_message, &None)
}
}
}
#[derive(Debug, Default, Parser, Clone)]
struct FeatureToggles {
/// Enable a feature (repeatable). Equivalent to `-c features.<name>=true`.
@@ -693,6 +734,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
.await?;
}
},
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
DebugSubcommand::AppServer(cmd) => {
run_debug_app_server_command(cmd)?;
}
},
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?,
},

View File

@@ -2,7 +2,7 @@
Typed clients for Codex/OpenAI APIs built on top of the generic transport in `codex-client`.
- Hosts the request/response models and prompt helpers for Responses, Chat Completions, and Compact APIs.
- Hosts the request/response models and prompt helpers for Responses and Compact APIs.
- Owns provider configuration (base URLs, headers, query params), auth header injection, retry tuning, and stream idle settings.
- Parses SSE streams into `ResponseEvent`/`ResponseStream`, including rate-limit snapshots and API-specific error mapping.
- Serves as the wire-level layer consumed by `codex-core`; higher layers handle auth refresh and business logic.
@@ -11,7 +11,7 @@ Typed clients for Codex/OpenAI APIs built on top of the generic transport in `co
The public interface of this crate is intentionally small and uniform:
- **Prompted endpoints (Chat + Responses)**
- **Prompted endpoints (Responses)**
- Input: a single `Prompt` plus endpoint-specific options.
- `Prompt` (re-exported as `codex_api::Prompt`) carries:
- `instructions: String` the fully-resolved system prompt for this turn.

View File

@@ -1,4 +1,6 @@
use codex_client::Request;
use http::HeaderMap;
use http::HeaderValue;
/// Provides bearer and account identity information for API requests.
///
@@ -12,16 +14,20 @@ pub trait AuthProvider: Send + Sync {
}
}
pub(crate) fn add_auth_headers<A: AuthProvider>(auth: &A, mut req: Request) -> Request {
pub(crate) fn add_auth_headers_to_header_map<A: AuthProvider>(auth: &A, headers: &mut HeaderMap) {
if let Some(token) = auth.bearer_token()
&& let Ok(header) = format!("Bearer {token}").parse()
&& let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}"))
{
let _ = req.headers.insert(http::header::AUTHORIZATION, header);
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Some(account_id) = auth.account_id()
&& let Ok(header) = account_id.parse()
&& let Ok(header) = HeaderValue::from_str(&account_id)
{
let _ = req.headers.insert("ChatGPT-Account-ID", header);
let _ = headers.insert("ChatGPT-Account-ID", header);
}
}
pub(crate) fn add_auth_headers<A: AuthProvider>(auth: &A, mut req: Request) -> Request {
add_auth_headers_to_header_map(auth, &mut req.headers);
req
}

View File

@@ -13,7 +13,7 @@ use std::task::Context;
use std::task::Poll;
use tokio::sync::mpsc;
/// Canonical prompt input for Chat and Responses endpoints.
/// Canonical prompt input for Responses endpoints.
#[derive(Debug, Clone)]
pub struct Prompt {
/// Fully-resolved system instructions for this turn.

View File

@@ -1,111 +1,21 @@
use crate::ChatRequest;
use crate::auth::AuthProvider;
use crate::common::Prompt as ApiPrompt;
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::endpoint::streaming::StreamingClient;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::provider::WireApi;
use crate::sse::chat::spawn_chat_stream;
use crate::telemetry::SseTelemetry;
use codex_client::HttpTransport;
use codex_client::RequestCompression;
use codex_client::RequestTelemetry;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
use futures::Stream;
use http::HeaderMap;
use serde_json::Value;
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
pub struct ChatClient<T: HttpTransport, A: AuthProvider> {
streaming: StreamingClient<T, A>,
}
impl<T: HttpTransport, A: AuthProvider> ChatClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
streaming: StreamingClient::new(transport, provider, auth),
}
}
pub fn with_telemetry(
self,
request: Option<Arc<dyn RequestTelemetry>>,
sse: Option<Arc<dyn SseTelemetry>>,
) -> Self {
Self {
streaming: self.streaming.with_telemetry(request, sse),
}
}
pub async fn stream_request(&self, request: ChatRequest) -> Result<ResponseStream, ApiError> {
self.stream(request.body, request.headers).await
}
pub async fn stream_prompt(
&self,
model: &str,
prompt: &ApiPrompt,
conversation_id: Option<String>,
session_source: Option<SessionSource>,
) -> Result<ResponseStream, ApiError> {
use crate::requests::ChatRequestBuilder;
let request =
ChatRequestBuilder::new(model, &prompt.instructions, &prompt.input, &prompt.tools)
.conversation_id(conversation_id)
.session_source(session_source)
.build(self.streaming.provider())?;
self.stream_request(request).await
}
fn path(&self) -> &'static str {
match self.streaming.provider().wire {
WireApi::Chat => "chat/completions",
_ => "responses",
}
}
pub async fn stream(
&self,
body: Value,
extra_headers: HeaderMap,
) -> Result<ResponseStream, ApiError> {
self.streaming
.stream(
self.path(),
body,
extra_headers,
RequestCompression::None,
spawn_chat_stream,
None,
)
.await
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum AggregateMode {
AggregatedOnly,
Streaming,
}
/// Stream adapter that merges token deltas into a single assistant message per turn.
pub struct AggregatedStream {
inner: ResponseStream,
cumulative: String,
cumulative_reasoning: String,
pending: VecDeque<ResponseEvent>,
mode: AggregateMode,
}
impl Stream for AggregatedStream {
@@ -122,7 +32,7 @@ impl Stream for AggregatedStream {
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(item)))) => {
let is_assistant_message = matches!(
&item,
@@ -130,29 +40,16 @@ impl Stream for AggregatedStream {
);
if is_assistant_message {
match this.mode {
AggregateMode::AggregatedOnly => {
if this.cumulative.is_empty()
&& let ResponseItem::Message { content, .. } = &item
&& let Some(text) = content.iter().find_map(|c| match c {
ContentItem::OutputText { text } => Some(text),
_ => None,
})
{
this.cumulative.push_str(text);
}
continue;
}
AggregateMode::Streaming => {
if this.cumulative.is_empty() {
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(
item,
))));
} else {
continue;
}
}
if this.cumulative.is_empty()
&& let ResponseItem::Message { content, .. } = &item
&& let Some(text) = content.iter().find_map(|c| match c {
ContentItem::OutputText { text } => Some(text),
_ => None,
})
{
this.cumulative.push_str(text);
}
continue;
}
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(item))));
@@ -216,35 +113,20 @@ impl Stream for AggregatedStream {
token_usage,
})));
}
Poll::Ready(Some(Ok(ResponseEvent::Created))) => {
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::Created))) => continue,
Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => {
this.cumulative.push_str(&delta);
if matches!(this.mode, AggregateMode::Streaming) {
return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta))));
} else {
continue;
}
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta {
delta,
content_index,
content_index: _,
}))) => {
this.cumulative_reasoning.push_str(&delta);
if matches!(this.mode, AggregateMode::Streaming) {
return Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta {
delta,
content_index,
})));
} else {
continue;
}
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta { .. }))) => continue,
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded { .. }))) => {
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta { .. }))) => continue,
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded { .. }))) => continue,
Poll::Ready(Some(Ok(ResponseEvent::OutputItemAdded(item)))) => {
return Poll::Ready(Some(Ok(ResponseEvent::OutputItemAdded(item))));
}
@@ -255,28 +137,21 @@ impl Stream for AggregatedStream {
pub trait AggregateStreamExt {
fn aggregate(self) -> AggregatedStream;
fn streaming_mode(self) -> ResponseStream;
}
impl AggregateStreamExt for ResponseStream {
fn aggregate(self) -> AggregatedStream {
AggregatedStream::new(self, AggregateMode::AggregatedOnly)
}
fn streaming_mode(self) -> ResponseStream {
self
AggregatedStream::new(self)
}
}
impl AggregatedStream {
fn new(inner: ResponseStream, mode: AggregateMode) -> Self {
fn new(inner: ResponseStream) -> Self {
AggregatedStream {
inner,
cumulative: String::new(),
cumulative_reasoning: String::new(),
pending: VecDeque::new(),
mode,
}
}
}

View File

@@ -1,10 +1,8 @@
use crate::auth::AuthProvider;
use crate::auth::add_auth_headers;
use crate::common::CompactionInput;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::provider::WireApi;
use crate::telemetry::run_with_request_telemetry;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use codex_protocol::models::ResponseItem;
@@ -15,34 +13,24 @@ use serde_json::to_value;
use std::sync::Arc;
pub struct CompactClient<T: HttpTransport, A: AuthProvider> {
transport: T,
provider: Provider,
auth: A,
request_telemetry: Option<Arc<dyn RequestTelemetry>>,
session: EndpointSession<T, A>,
}
impl<T: HttpTransport, A: AuthProvider> CompactClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
transport,
provider,
auth,
request_telemetry: None,
session: EndpointSession::new(transport, provider, auth),
}
}
pub fn with_telemetry(mut self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
self.request_telemetry = request;
self
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
Self {
session: self.session.with_request_telemetry(request),
}
}
fn path(&self) -> Result<&'static str, ApiError> {
match self.provider.wire {
WireApi::Compact | WireApi::Responses => Ok("responses/compact"),
WireApi::Chat => Err(ApiError::Stream(
"compact endpoint requires responses wire api".to_string(),
)),
}
fn path() -> &'static str {
"responses/compact"
}
pub async fn compact(
@@ -50,21 +38,10 @@ impl<T: HttpTransport, A: AuthProvider> CompactClient<T, A> {
body: serde_json::Value,
extra_headers: HeaderMap,
) -> Result<Vec<ResponseItem>, ApiError> {
let path = self.path()?;
let builder = || {
let mut req = self.provider.build_request(Method::POST, path);
req.headers.extend(extra_headers.clone());
req.body = Some(body.clone());
add_auth_headers(&self.auth, req)
};
let resp = run_with_request_telemetry(
self.provider.retry.to_policy(),
self.request_telemetry.clone(),
builder,
|req| self.transport.execute(req),
)
.await?;
let resp = self
.session
.execute(Method::POST, Self::path(), extra_headers, Some(body))
.await?;
let parsed: CompactHistoryResponse =
serde_json::from_slice(&resp.body).map_err(|e| ApiError::Stream(e.to_string()))?;
Ok(parsed.output)
@@ -89,14 +66,11 @@ struct CompactHistoryResponse {
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::RetryConfig;
use async_trait::async_trait;
use codex_client::Request;
use codex_client::Response;
use codex_client::StreamResponse;
use codex_client::TransportError;
use http::HeaderMap;
use std::time::Duration;
#[derive(Clone, Default)]
struct DummyTransport;
@@ -121,42 +95,11 @@ mod tests {
}
}
fn provider(wire: WireApi) -> Provider {
Provider {
name: "test".to_string(),
base_url: "https://example.com/v1".to_string(),
query_params: None,
wire,
headers: HeaderMap::new(),
retry: RetryConfig {
max_attempts: 1,
base_delay: Duration::from_millis(1),
retry_429: false,
retry_5xx: true,
retry_transport: true,
},
stream_idle_timeout: Duration::from_secs(1),
}
}
#[tokio::test]
async fn errors_when_wire_is_chat() {
let client = CompactClient::new(DummyTransport, provider(WireApi::Chat), DummyAuth);
let input = CompactionInput {
model: "gpt-test",
input: &[],
instructions: "inst",
};
let err = client
.compact_input(&input, HeaderMap::new())
.await
.expect_err("expected wire mismatch to fail");
match err {
ApiError::Stream(msg) => {
assert_eq!(msg, "compact endpoint requires responses wire api");
}
other => panic!("unexpected error: {other:?}"),
}
#[test]
fn path_is_responses_compact() {
assert_eq!(
CompactClient::<DummyTransport, DummyAuth>::path(),
"responses/compact"
);
}
}

View File

@@ -1,6 +1,6 @@
pub mod chat;
pub mod aggregate;
pub mod compact;
pub mod models;
pub mod responses;
pub mod responses_websocket;
mod streaming;
mod session;

View File

@@ -1,8 +1,7 @@
use crate::auth::AuthProvider;
use crate::auth::add_auth_headers;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::telemetry::run_with_request_telemetry;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use codex_protocol::openai_models::ModelInfo;
@@ -13,53 +12,42 @@ use http::header::ETAG;
use std::sync::Arc;
pub struct ModelsClient<T: HttpTransport, A: AuthProvider> {
transport: T,
provider: Provider,
auth: A,
request_telemetry: Option<Arc<dyn RequestTelemetry>>,
session: EndpointSession<T, A>,
}
impl<T: HttpTransport, A: AuthProvider> ModelsClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
transport,
provider,
auth,
request_telemetry: None,
session: EndpointSession::new(transport, provider, auth),
}
}
pub fn with_telemetry(mut self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
self.request_telemetry = request;
self
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
Self {
session: self.session.with_request_telemetry(request),
}
}
fn path(&self) -> &'static str {
fn path() -> &'static str {
"models"
}
fn append_client_version_query(req: &mut codex_client::Request, client_version: &str) {
let separator = if req.url.contains('?') { '&' } else { '?' };
req.url = format!("{}{}client_version={client_version}", req.url, separator);
}
pub async fn list_models(
&self,
client_version: &str,
extra_headers: HeaderMap,
) -> Result<(Vec<ModelInfo>, Option<String>), ApiError> {
let builder = || {
let mut req = self.provider.build_request(Method::GET, self.path());
req.headers.extend(extra_headers.clone());
let separator = if req.url.contains('?') { '&' } else { '?' };
req.url = format!("{}{}client_version={client_version}", req.url, separator);
add_auth_headers(&self.auth, req)
};
let resp = run_with_request_telemetry(
self.provider.retry.to_policy(),
self.request_telemetry.clone(),
builder,
|req| self.transport.execute(req),
)
.await?;
let resp = self
.session
.execute_with(Method::GET, Self::path(), extra_headers, None, |req| {
Self::append_client_version_query(req, client_version);
})
.await?;
let header_etag = resp
.headers
@@ -83,7 +71,6 @@ impl<T: HttpTransport, A: AuthProvider> ModelsClient<T, A> {
mod tests {
use super::*;
use crate::provider::RetryConfig;
use crate::provider::WireApi;
use async_trait::async_trait;
use codex_client::Request;
use codex_client::Response;
@@ -149,7 +136,6 @@ mod tests {
name: "test".to_string(),
base_url: base_url.to_string(),
query_params: None,
wire: WireApi::Responses,
headers: HeaderMap::new(),
retry: RetryConfig {
max_attempts: 1,

View File

@@ -3,10 +3,9 @@ use crate::common::Prompt as ApiPrompt;
use crate::common::Reasoning;
use crate::common::ResponseStream;
use crate::common::TextControls;
use crate::endpoint::streaming::StreamingClient;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::provider::WireApi;
use crate::requests::ResponsesRequest;
use crate::requests::ResponsesRequestBuilder;
use crate::requests::responses::Compression;
@@ -17,13 +16,16 @@ use codex_client::RequestCompression;
use codex_client::RequestTelemetry;
use codex_protocol::protocol::SessionSource;
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use serde_json::Value;
use std::sync::Arc;
use std::sync::OnceLock;
use tracing::instrument;
pub struct ResponsesClient<T: HttpTransport, A: AuthProvider> {
streaming: StreamingClient<T, A>,
session: EndpointSession<T, A>,
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
}
#[derive(Default)]
@@ -43,7 +45,8 @@ pub struct ResponsesOptions {
impl<T: HttpTransport, A: AuthProvider> ResponsesClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
streaming: StreamingClient::new(transport, provider, auth),
session: EndpointSession::new(transport, provider, auth),
sse_telemetry: None,
}
}
@@ -53,7 +56,8 @@ impl<T: HttpTransport, A: AuthProvider> ResponsesClient<T, A> {
sse: Option<Arc<dyn SseTelemetry>>,
) -> Self {
Self {
streaming: self.streaming.with_telemetry(request, sse),
session: self.session.with_request_telemetry(request),
sse_telemetry: sse,
}
}
@@ -103,16 +107,13 @@ impl<T: HttpTransport, A: AuthProvider> ResponsesClient<T, A> {
.store_override(store_override)
.extra_headers(extra_headers)
.compression(compression)
.build(self.streaming.provider())?;
.build(self.session.provider())?;
self.stream_request(request, turn_state).await
}
fn path(&self) -> &'static str {
match self.streaming.provider().wire {
WireApi::Responses | WireApi::Compact => "responses",
WireApi::Chat => "chat/completions",
}
fn path() -> &'static str {
"responses"
}
pub async fn stream(
@@ -122,20 +123,33 @@ impl<T: HttpTransport, A: AuthProvider> ResponsesClient<T, A> {
compression: Compression,
turn_state: Option<Arc<OnceLock<String>>>,
) -> Result<ResponseStream, ApiError> {
let compression = match compression {
let request_compression = match compression {
Compression::None => RequestCompression::None,
Compression::Zstd => RequestCompression::Zstd,
};
self.streaming
.stream(
self.path(),
body,
let stream_response = self
.session
.stream_with(
Method::POST,
Self::path(),
extra_headers,
compression,
spawn_response_stream,
turn_state,
Some(body),
|req| {
req.headers.insert(
http::header::ACCEPT,
HeaderValue::from_static("text/event-stream"),
);
req.compression = request_compression;
},
)
.await
.await?;
Ok(spawn_response_stream(
stream_response,
self.session.provider().stream_idle_timeout,
self.sse_telemetry.clone(),
turn_state,
))
}
}

View File

@@ -1,9 +1,11 @@
use crate::auth::AuthProvider;
use crate::auth::add_auth_headers_to_header_map;
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::common::ResponsesWsRequest;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::rate_limits::parse_rate_limit_event;
use crate::sse::responses::ResponsesStreamEvent;
use crate::sse::responses::process_responses_event;
use crate::telemetry::WebsocketTelemetry;
@@ -11,7 +13,6 @@ use codex_client::TransportError;
use futures::SinkExt;
use futures::StreamExt;
use http::HeaderMap;
use http::HeaderValue;
use serde_json::Value;
use std::sync::Arc;
use std::sync::OnceLock;
@@ -33,6 +34,7 @@ use url::Url;
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
const X_MODELS_ETAG_HEADER: &str = "x-models-etag";
const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";
pub struct ResponsesWebsocketConnection {
@@ -40,6 +42,7 @@ pub struct ResponsesWebsocketConnection {
// TODO (pakrym): is this the right place for timeout?
idle_timeout: Duration,
server_reasoning_included: bool,
models_etag: Option<String>,
telemetry: Option<Arc<dyn WebsocketTelemetry>>,
}
@@ -48,12 +51,14 @@ impl ResponsesWebsocketConnection {
stream: WsStream,
idle_timeout: Duration,
server_reasoning_included: bool,
models_etag: Option<String>,
telemetry: Option<Arc<dyn WebsocketTelemetry>>,
) -> Self {
Self {
stream: Arc::new(Mutex::new(Some(stream))),
idle_timeout,
server_reasoning_included,
models_etag,
telemetry,
}
}
@@ -71,12 +76,16 @@ impl ResponsesWebsocketConnection {
let stream = Arc::clone(&self.stream);
let idle_timeout = self.idle_timeout;
let server_reasoning_included = self.server_reasoning_included;
let models_etag = self.models_etag.clone();
let telemetry = self.telemetry.clone();
let request_body = serde_json::to_value(&request).map_err(|err| {
ApiError::Stream(format!("failed to encode websocket request: {err}"))
})?;
tokio::spawn(async move {
if let Some(etag) = models_etag {
let _ = tx_event.send(Ok(ResponseEvent::ModelsEtag(etag))).await;
}
if server_reasoning_included {
let _ = tx_event
.send(Ok(ResponseEvent::ServerReasoningIncluded(true)))
@@ -134,38 +143,25 @@ impl<A: AuthProvider> ResponsesWebsocketClient<A> {
let mut headers = self.provider.headers.clone();
headers.extend(extra_headers);
apply_auth_headers(&mut headers, &self.auth);
add_auth_headers_to_header_map(&self.auth, &mut headers);
let (stream, server_reasoning_included) =
connect_websocket(ws_url, headers, turn_state).await?;
let (stream, server_reasoning_included, models_etag) =
connect_websocket(ws_url, headers, turn_state.clone()).await?;
Ok(ResponsesWebsocketConnection::new(
stream,
self.provider.stream_idle_timeout,
server_reasoning_included,
models_etag,
telemetry,
))
}
}
// TODO (pakrym): share with /auth
fn apply_auth_headers(headers: &mut HeaderMap, auth: &impl AuthProvider) {
if let Some(token) = auth.bearer_token()
&& let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}"))
{
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Some(account_id) = auth.account_id()
&& let Ok(header) = HeaderValue::from_str(&account_id)
{
let _ = headers.insert("ChatGPT-Account-ID", header);
}
}
async fn connect_websocket(
url: Url,
headers: HeaderMap,
turn_state: Option<Arc<OnceLock<String>>>,
) -> Result<(WsStream, bool), ApiError> {
) -> Result<(WsStream, bool, Option<String>), ApiError> {
info!("connecting to websocket: {url}");
let mut request = url
@@ -191,6 +187,11 @@ async fn connect_websocket(
};
let reasoning_included = response.headers().contains_key(X_REASONING_INCLUDED_HEADER);
let models_etag = response
.headers()
.get(X_MODELS_ETAG_HEADER)
.and_then(|value| value.to_str().ok())
.map(ToString::to_string);
if let Some(turn_state) = turn_state
&& let Some(header_value) = response
.headers()
@@ -199,7 +200,7 @@ async fn connect_websocket(
{
let _ = turn_state.set(header_value.to_string());
}
Ok((stream, reasoning_included))
Ok((stream, reasoning_included, models_etag))
}
fn map_ws_error(err: WsError, url: &Url) -> ApiError {
@@ -287,6 +288,12 @@ async fn run_websocket_response_stream(
continue;
}
};
if event.kind() == "codex.rate_limits" {
if let Some(snapshot) = parse_rate_limit_event(&text) {
let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;
}
continue;
}
match process_responses_event(event) {
Ok(Some(event)) => {
let is_completed = matches!(event, ResponseEvent::Completed { .. });

View File

@@ -0,0 +1,126 @@
use crate::auth::AuthProvider;
use crate::auth::add_auth_headers;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::telemetry::run_with_request_telemetry;
use codex_client::HttpTransport;
use codex_client::Request;
use codex_client::RequestTelemetry;
use codex_client::Response;
use codex_client::StreamResponse;
use http::HeaderMap;
use http::Method;
use serde_json::Value;
use std::sync::Arc;
pub(crate) struct EndpointSession<T: HttpTransport, A: AuthProvider> {
transport: T,
provider: Provider,
auth: A,
request_telemetry: Option<Arc<dyn RequestTelemetry>>,
}
impl<T: HttpTransport, A: AuthProvider> EndpointSession<T, A> {
pub(crate) fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
transport,
provider,
auth,
request_telemetry: None,
}
}
pub(crate) fn with_request_telemetry(
mut self,
request: Option<Arc<dyn RequestTelemetry>>,
) -> Self {
self.request_telemetry = request;
self
}
pub(crate) fn provider(&self) -> &Provider {
&self.provider
}
fn make_request(
&self,
method: &Method,
path: &str,
extra_headers: &HeaderMap,
body: Option<&Value>,
) -> Request {
let mut req = self.provider.build_request(method.clone(), path);
req.headers.extend(extra_headers.clone());
if let Some(body) = body {
req.body = Some(body.clone());
}
add_auth_headers(&self.auth, req)
}
pub(crate) async fn execute(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<Value>,
) -> Result<Response, ApiError> {
self.execute_with(method, path, extra_headers, body, |_| {})
.await
}
pub(crate) async fn execute_with<C>(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<Value>,
configure: C,
) -> Result<Response, ApiError>
where
C: Fn(&mut Request),
{
let make_request = || {
let mut req = self.make_request(&method, path, &extra_headers, body.as_ref());
configure(&mut req);
req
};
let response = run_with_request_telemetry(
self.provider.retry.to_policy(),
self.request_telemetry.clone(),
make_request,
|req| self.transport.execute(req),
)
.await?;
Ok(response)
}
pub(crate) async fn stream_with<C>(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<Value>,
configure: C,
) -> Result<StreamResponse, ApiError>
where
C: Fn(&mut Request),
{
let make_request = || {
let mut req = self.make_request(&method, path, &extra_headers, body.as_ref());
configure(&mut req);
req
};
let stream = run_with_request_telemetry(
self.provider.retry.to_policy(),
self.request_telemetry.clone(),
make_request,
|req| self.transport.stream(req),
)
.await?;
Ok(stream)
}
}

View File

@@ -1,95 +0,0 @@
use crate::auth::AuthProvider;
use crate::auth::add_auth_headers;
use crate::common::ResponseStream;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::telemetry::SseTelemetry;
use crate::telemetry::run_with_request_telemetry;
use codex_client::HttpTransport;
use codex_client::RequestCompression;
use codex_client::RequestTelemetry;
use codex_client::StreamResponse;
use http::HeaderMap;
use http::Method;
use serde_json::Value;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
pub(crate) struct StreamingClient<T: HttpTransport, A: AuthProvider> {
transport: T,
provider: Provider,
auth: A,
request_telemetry: Option<Arc<dyn RequestTelemetry>>,
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
}
type StreamSpawner = fn(
StreamResponse,
Duration,
Option<Arc<dyn SseTelemetry>>,
Option<Arc<OnceLock<String>>>,
) -> ResponseStream;
impl<T: HttpTransport, A: AuthProvider> StreamingClient<T, A> {
pub(crate) fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
transport,
provider,
auth,
request_telemetry: None,
sse_telemetry: None,
}
}
pub(crate) fn with_telemetry(
mut self,
request: Option<Arc<dyn RequestTelemetry>>,
sse: Option<Arc<dyn SseTelemetry>>,
) -> Self {
self.request_telemetry = request;
self.sse_telemetry = sse;
self
}
pub(crate) fn provider(&self) -> &Provider {
&self.provider
}
pub(crate) async fn stream(
&self,
path: &str,
body: Value,
extra_headers: HeaderMap,
compression: RequestCompression,
spawner: StreamSpawner,
turn_state: Option<Arc<OnceLock<String>>>,
) -> Result<ResponseStream, ApiError> {
let builder = || {
let mut req = self.provider.build_request(Method::POST, path);
req.headers.extend(extra_headers.clone());
req.headers.insert(
http::header::ACCEPT,
http::HeaderValue::from_static("text/event-stream"),
);
req.body = Some(body.clone());
req.compression = compression;
add_auth_headers(&self.auth, req)
};
let stream_response = run_with_request_telemetry(
self.provider.retry.to_policy(),
self.request_telemetry.clone(),
builder,
|req| self.transport.stream(req),
)
.await?;
Ok(spawner(
stream_response,
self.provider.stream_idle_timeout,
self.sse_telemetry.clone(),
turn_state,
))
}
}

View File

@@ -22,8 +22,7 @@ pub use crate::common::ResponseEvent;
pub use crate::common::ResponseStream;
pub use crate::common::ResponsesApiRequest;
pub use crate::common::create_text_param_for_request;
pub use crate::endpoint::chat::AggregateStreamExt;
pub use crate::endpoint::chat::ChatClient;
pub use crate::endpoint::aggregate::AggregateStreamExt;
pub use crate::endpoint::compact::CompactClient;
pub use crate::endpoint::models::ModelsClient;
pub use crate::endpoint::responses::ResponsesClient;
@@ -32,10 +31,7 @@ pub use crate::endpoint::responses_websocket::ResponsesWebsocketClient;
pub use crate::endpoint::responses_websocket::ResponsesWebsocketConnection;
pub use crate::error::ApiError;
pub use crate::provider::Provider;
pub use crate::provider::WireApi;
pub use crate::provider::is_azure_responses_wire_base_url;
pub use crate::requests::ChatRequest;
pub use crate::requests::ChatRequestBuilder;
pub use crate::requests::ResponsesRequest;
pub use crate::requests::ResponsesRequestBuilder;
pub use crate::sse::stream_from_fixture;

View File

@@ -8,14 +8,6 @@ use std::collections::HashMap;
use std::time::Duration;
use url::Url;
/// Wire-level APIs supported by a `Provider`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireApi {
Responses,
Chat,
Compact,
}
/// High-level retry configuration for a provider.
///
/// This is converted into a `RetryPolicy` used by `codex-client` to drive
@@ -52,7 +44,6 @@ pub struct Provider {
pub name: String,
pub base_url: String,
pub query_params: Option<HashMap<String, String>>,
pub wire: WireApi,
pub headers: HeaderMap,
pub retry: RetryConfig,
pub stream_idle_timeout: Duration,
@@ -95,7 +86,7 @@ impl Provider {
}
pub fn is_azure_responses_endpoint(&self) -> bool {
is_azure_responses_wire_base_url(self.wire.clone(), &self.name, Some(&self.base_url))
is_azure_responses_wire_base_url(&self.name, Some(&self.base_url))
}
pub fn websocket_url_for_path(&self, path: &str) -> Result<Url, url::ParseError> {
@@ -112,11 +103,7 @@ impl Provider {
}
}
pub fn is_azure_responses_wire_base_url(wire: WireApi, name: &str, base_url: Option<&str>) -> bool {
if wire != WireApi::Responses {
return false;
}
pub fn is_azure_responses_wire_base_url(name: &str, base_url: Option<&str>) -> bool {
if name.eq_ignore_ascii_case("azure") {
return true;
}
@@ -157,13 +144,12 @@ mod tests {
for base_url in positive_cases {
assert!(
is_azure_responses_wire_base_url(WireApi::Responses, "test", Some(base_url)),
is_azure_responses_wire_base_url("test", Some(base_url)),
"expected {base_url} to be detected as Azure"
);
}
assert!(is_azure_responses_wire_base_url(
WireApi::Responses,
"Azure",
Some("https://example.com")
));
@@ -176,15 +162,9 @@ mod tests {
for base_url in negative_cases {
assert!(
!is_azure_responses_wire_base_url(WireApi::Responses, "test", Some(base_url)),
!is_azure_responses_wire_base_url("test", Some(base_url)),
"expected {base_url} not to be detected as Azure"
);
}
assert!(!is_azure_responses_wire_base_url(
WireApi::Chat,
"Azure",
Some("https://foo.openai.azure.com/openai")
));
}
}

View File

@@ -1,7 +1,9 @@
use codex_protocol::account::PlanType;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use http::HeaderMap;
use serde::Deserialize;
use std::fmt::Display;
#[derive(Debug)]
@@ -41,6 +43,70 @@ pub fn parse_rate_limit(headers: &HeaderMap) -> Option<RateLimitSnapshot> {
})
}
#[derive(Debug, Deserialize)]
struct RateLimitEventWindow {
used_percent: f64,
window_minutes: Option<i64>,
reset_at: Option<i64>,
}
#[derive(Debug, Deserialize)]
struct RateLimitEventDetails {
primary: Option<RateLimitEventWindow>,
secondary: Option<RateLimitEventWindow>,
}
#[derive(Debug, Deserialize)]
struct RateLimitEventCredits {
has_credits: bool,
unlimited: bool,
balance: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RateLimitEvent {
#[serde(rename = "type")]
kind: String,
plan_type: Option<PlanType>,
rate_limits: Option<RateLimitEventDetails>,
credits: Option<RateLimitEventCredits>,
}
pub fn parse_rate_limit_event(payload: &str) -> Option<RateLimitSnapshot> {
let event: RateLimitEvent = serde_json::from_str(payload).ok()?;
if event.kind != "codex.rate_limits" {
return None;
}
let (primary, secondary) = if let Some(details) = event.rate_limits.as_ref() {
(
map_event_window(details.primary.as_ref()),
map_event_window(details.secondary.as_ref()),
)
} else {
(None, None)
};
let credits = event.credits.map(|credits| CreditsSnapshot {
has_credits: credits.has_credits,
unlimited: credits.unlimited,
balance: credits.balance,
});
Some(RateLimitSnapshot {
primary,
secondary,
credits,
plan_type: event.plan_type,
})
}
fn map_event_window(window: Option<&RateLimitEventWindow>) -> Option<RateLimitWindow> {
let window = window?;
Some(RateLimitWindow {
used_percent: window.used_percent,
window_minutes: window.window_minutes,
resets_at: window.reset_at,
})
}
/// Parses the bespoke Codex rate-limit headers into a `RateLimitSnapshot`.
pub fn parse_promo_message(headers: &HeaderMap) -> Option<String> {
parse_header_str(headers, "x-codex-promo-message")

View File

@@ -1,494 +0,0 @@
use crate::error::ApiError;
use crate::provider::Provider;
use crate::requests::headers::build_conversation_headers;
use crate::requests::headers::insert_header;
use crate::requests::headers::subagent_header;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
use http::HeaderMap;
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
/// Assembled request body plus headers for Chat Completions streaming calls.
pub struct ChatRequest {
pub body: Value,
pub headers: HeaderMap,
}
pub struct ChatRequestBuilder<'a> {
model: &'a str,
instructions: &'a str,
input: &'a [ResponseItem],
tools: &'a [Value],
conversation_id: Option<String>,
session_source: Option<SessionSource>,
}
impl<'a> ChatRequestBuilder<'a> {
pub fn new(
model: &'a str,
instructions: &'a str,
input: &'a [ResponseItem],
tools: &'a [Value],
) -> Self {
Self {
model,
instructions,
input,
tools,
conversation_id: None,
session_source: None,
}
}
pub fn conversation_id(mut self, id: Option<String>) -> Self {
self.conversation_id = id;
self
}
pub fn session_source(mut self, source: Option<SessionSource>) -> Self {
self.session_source = source;
self
}
pub fn build(self, _provider: &Provider) -> Result<ChatRequest, ApiError> {
let mut messages = Vec::<Value>::new();
messages.push(json!({"role": "system", "content": self.instructions}));
let input = self.input;
let mut reasoning_by_anchor_index: HashMap<usize, String> = HashMap::new();
let mut last_emitted_role: Option<&str> = None;
for item in input {
match item {
ResponseItem::Message { role, .. } => last_emitted_role = Some(role.as_str()),
ResponseItem::FunctionCall { .. } | ResponseItem::LocalShellCall { .. } => {
last_emitted_role = Some("assistant")
}
ResponseItem::FunctionCallOutput { .. } => last_emitted_role = Some("tool"),
ResponseItem::Reasoning { .. } | ResponseItem::Other => {}
ResponseItem::CustomToolCall { .. } => {}
ResponseItem::CustomToolCallOutput { .. } => {}
ResponseItem::WebSearchCall { .. } => {}
ResponseItem::GhostSnapshot { .. } => {}
ResponseItem::Compaction { .. } => {}
}
}
let mut last_user_index: Option<usize> = None;
for (idx, item) in input.iter().enumerate() {
if let ResponseItem::Message { role, .. } = item
&& role == "user"
{
last_user_index = Some(idx);
}
}
if !matches!(last_emitted_role, Some("user")) {
for (idx, item) in input.iter().enumerate() {
if let Some(u_idx) = last_user_index
&& idx <= u_idx
{
continue;
}
if let ResponseItem::Reasoning {
content: Some(items),
..
} = item
{
let mut text = String::new();
for entry in items {
match entry {
ReasoningItemContent::ReasoningText { text: segment }
| ReasoningItemContent::Text { text: segment } => {
text.push_str(segment)
}
}
}
if text.trim().is_empty() {
continue;
}
let mut attached = false;
if idx > 0
&& let ResponseItem::Message { role, .. } = &input[idx - 1]
&& role == "assistant"
{
reasoning_by_anchor_index
.entry(idx - 1)
.and_modify(|v| v.push_str(&text))
.or_insert(text.clone());
attached = true;
}
if !attached && idx + 1 < input.len() {
match &input[idx + 1] {
ResponseItem::FunctionCall { .. }
| ResponseItem::LocalShellCall { .. } => {
reasoning_by_anchor_index
.entry(idx + 1)
.and_modify(|v| v.push_str(&text))
.or_insert(text.clone());
}
ResponseItem::Message { role, .. } if role == "assistant" => {
reasoning_by_anchor_index
.entry(idx + 1)
.and_modify(|v| v.push_str(&text))
.or_insert(text.clone());
}
_ => {}
}
}
}
}
}
let mut last_assistant_text: Option<String> = None;
for (idx, item) in input.iter().enumerate() {
match item {
ResponseItem::Message { role, content, .. } => {
let mut text = String::new();
let mut items: Vec<Value> = Vec::new();
let mut saw_image = false;
for c in content {
match c {
ContentItem::InputText { text: t }
| ContentItem::OutputText { text: t } => {
text.push_str(t);
items.push(json!({"type":"text","text": t}));
}
ContentItem::InputImage { image_url } => {
saw_image = true;
items.push(
json!({"type":"image_url","image_url": {"url": image_url}}),
);
}
}
}
if role == "assistant" {
if let Some(prev) = &last_assistant_text
&& prev == &text
{
continue;
}
last_assistant_text = Some(text.clone());
}
let content_value = if role == "assistant" {
json!(text)
} else if saw_image {
json!(items)
} else {
json!(text)
};
let mut msg = json!({"role": role, "content": content_value});
if role == "assistant"
&& let Some(reasoning) = reasoning_by_anchor_index.get(&idx)
&& let Some(obj) = msg.as_object_mut()
{
obj.insert("reasoning".to_string(), json!(reasoning));
}
messages.push(msg);
}
ResponseItem::FunctionCall {
name,
arguments,
call_id,
..
} => {
let reasoning = reasoning_by_anchor_index.get(&idx).map(String::as_str);
let tool_call = json!({
"id": call_id,
"type": "function",
"function": {
"name": name,
"arguments": arguments,
}
});
push_tool_call_message(&mut messages, tool_call, reasoning);
}
ResponseItem::LocalShellCall {
id,
call_id: _,
status,
action,
} => {
let reasoning = reasoning_by_anchor_index.get(&idx).map(String::as_str);
let tool_call = json!({
"id": id.clone().unwrap_or_default(),
"type": "local_shell_call",
"status": status,
"action": action,
});
push_tool_call_message(&mut messages, tool_call, reasoning);
}
ResponseItem::FunctionCallOutput { call_id, output } => {
let content_value = if let Some(items) = &output.content_items {
let mapped: Vec<Value> = items
.iter()
.map(|it| match it {
FunctionCallOutputContentItem::InputText { text } => {
json!({"type":"text","text": text})
}
FunctionCallOutputContentItem::InputImage { image_url } => {
json!({"type":"image_url","image_url": {"url": image_url}})
}
})
.collect();
json!(mapped)
} else {
json!(output.content)
};
messages.push(json!({
"role": "tool",
"tool_call_id": call_id,
"content": content_value,
}));
}
ResponseItem::CustomToolCall {
id,
call_id: _,
name,
input,
status: _,
} => {
let tool_call = json!({
"id": id,
"type": "custom",
"custom": {
"name": name,
"input": input,
}
});
let reasoning = reasoning_by_anchor_index.get(&idx).map(String::as_str);
push_tool_call_message(&mut messages, tool_call, reasoning);
}
ResponseItem::CustomToolCallOutput { call_id, output } => {
messages.push(json!({
"role": "tool",
"tool_call_id": call_id,
"content": output,
}));
}
ResponseItem::GhostSnapshot { .. } => {
continue;
}
ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::Other
| ResponseItem::Compaction { .. } => {
continue;
}
}
}
let payload = json!({
"model": self.model,
"messages": messages,
"stream": true,
"tools": self.tools,
});
let mut headers = build_conversation_headers(self.conversation_id);
if let Some(subagent) = subagent_header(&self.session_source) {
insert_header(&mut headers, "x-openai-subagent", &subagent);
}
Ok(ChatRequest {
body: payload,
headers,
})
}
}
fn push_tool_call_message(messages: &mut Vec<Value>, tool_call: Value, reasoning: Option<&str>) {
// Chat Completions requires that tool calls are grouped into a single assistant message
// (with `tool_calls: [...]`) followed by tool role responses.
if let Some(Value::Object(obj)) = messages.last_mut()
&& obj.get("role").and_then(Value::as_str) == Some("assistant")
&& obj.get("content").is_some_and(Value::is_null)
&& let Some(tool_calls) = obj.get_mut("tool_calls").and_then(Value::as_array_mut)
{
tool_calls.push(tool_call);
if let Some(reasoning) = reasoning {
if let Some(Value::String(existing)) = obj.get_mut("reasoning") {
if !existing.is_empty() {
existing.push('\n');
}
existing.push_str(reasoning);
} else {
obj.insert(
"reasoning".to_string(),
Value::String(reasoning.to_string()),
);
}
}
return;
}
let mut msg = json!({
"role": "assistant",
"content": null,
"tool_calls": [tool_call],
});
if let Some(reasoning) = reasoning
&& let Some(obj) = msg.as_object_mut()
{
obj.insert("reasoning".to_string(), json!(reasoning));
}
messages.push(msg);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::RetryConfig;
use crate::provider::WireApi;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use http::HeaderValue;
use pretty_assertions::assert_eq;
use std::time::Duration;
fn provider() -> Provider {
Provider {
name: "openai".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
query_params: None,
wire: WireApi::Chat,
headers: HeaderMap::new(),
retry: RetryConfig {
max_attempts: 1,
base_delay: Duration::from_millis(10),
retry_429: false,
retry_5xx: true,
retry_transport: true,
},
stream_idle_timeout: Duration::from_secs(1),
}
}
#[test]
fn attaches_conversation_and_subagent_headers() {
let prompt_input = vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hi".to_string(),
}],
end_turn: None,
phase: None,
}];
let req = ChatRequestBuilder::new("gpt-test", "inst", &prompt_input, &[])
.conversation_id(Some("conv-1".into()))
.session_source(Some(SessionSource::SubAgent(SubAgentSource::Review)))
.build(&provider())
.expect("request");
assert_eq!(
req.headers.get("session_id"),
Some(&HeaderValue::from_static("conv-1"))
);
assert_eq!(
req.headers.get("x-openai-subagent"),
Some(&HeaderValue::from_static("review"))
);
}
#[test]
fn groups_consecutive_tool_calls_into_a_single_assistant_message() {
let prompt_input = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "read these".to_string(),
}],
end_turn: None,
phase: None,
},
ResponseItem::FunctionCall {
id: None,
name: "read_file".to_string(),
arguments: r#"{"path":"a.txt"}"#.to_string(),
call_id: "call-a".to_string(),
},
ResponseItem::FunctionCall {
id: None,
name: "read_file".to_string(),
arguments: r#"{"path":"b.txt"}"#.to_string(),
call_id: "call-b".to_string(),
},
ResponseItem::FunctionCall {
id: None,
name: "read_file".to_string(),
arguments: r#"{"path":"c.txt"}"#.to_string(),
call_id: "call-c".to_string(),
},
ResponseItem::FunctionCallOutput {
call_id: "call-a".to_string(),
output: FunctionCallOutputPayload {
content: "A".to_string(),
..Default::default()
},
},
ResponseItem::FunctionCallOutput {
call_id: "call-b".to_string(),
output: FunctionCallOutputPayload {
content: "B".to_string(),
..Default::default()
},
},
ResponseItem::FunctionCallOutput {
call_id: "call-c".to_string(),
output: FunctionCallOutputPayload {
content: "C".to_string(),
..Default::default()
},
},
];
let req = ChatRequestBuilder::new("gpt-test", "inst", &prompt_input, &[])
.build(&provider())
.expect("request");
let messages = req
.body
.get("messages")
.and_then(|v| v.as_array())
.expect("messages array");
// system + user + assistant(tool_calls=[...]) + 3 tool outputs
assert_eq!(messages.len(), 6);
assert_eq!(messages[0]["role"], "system");
assert_eq!(messages[1]["role"], "user");
let tool_calls_msg = &messages[2];
assert_eq!(tool_calls_msg["role"], "assistant");
assert_eq!(tool_calls_msg["content"], serde_json::Value::Null);
let tool_calls = tool_calls_msg["tool_calls"]
.as_array()
.expect("tool_calls array");
assert_eq!(tool_calls.len(), 3);
assert_eq!(tool_calls[0]["id"], "call-a");
assert_eq!(tool_calls[1]["id"], "call-b");
assert_eq!(tool_calls[2]["id"], "call-c");
assert_eq!(messages[3]["role"], "tool");
assert_eq!(messages[3]["tool_call_id"], "call-a");
assert_eq!(messages[4]["role"], "tool");
assert_eq!(messages[4]["tool_call_id"], "call-b");
assert_eq!(messages[5]["role"], "tool");
assert_eq!(messages[5]["tool_call_id"], "call-c");
}
}

View File

@@ -1,8 +1,5 @@
pub mod chat;
pub(crate) mod headers;
pub mod responses;
pub use chat::ChatRequest;
pub use chat::ChatRequestBuilder;
pub use responses::ResponsesRequest;
pub use responses::ResponsesRequestBuilder;

Some files were not shown because too many files have changed in this diff Show More