Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 156 additions & 2 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
name: rust-ci
permissions:
contents: write
actions: read
on:
pull_request: {}
push:
Expand Down Expand Up @@ -61,7 +64,27 @@ jobs:
with:
components: rustfmt
- name: cargo fmt
run: cargo fmt -- --config imports_granularity=Item --check
shell: bash
run: |
set -euo pipefail
if ! cargo fmt -- --config imports_granularity=Item --check; then
while IFS= read -r -d '' entry; do
file="${entry:3}"
echo "::error file=${file}::cargo fmt would rewrite ${file}"
done < <(git status --porcelain -z)
{
echo "## cargo fmt diff";
echo;
echo "\`\`\`";
git status --short
echo "\`\`\`";
echo;
echo "\`\`\`diff";
git diff
echo "\`\`\`";
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: Verify codegen for mcp-types
run: ./mcp-types/check_lib_rs.py

Expand Down Expand Up @@ -469,10 +492,135 @@ jobs:
echo "Tests failed. See logs for details."
exit 1

build_binaries:
name: Binary artifacts — ${{ matrix.artifact_name }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
needs: changed
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
defaults:
run:
working-directory: codex-rs
strategy:
fail-fast: false
matrix:
include:
- runner: macos-14
target: aarch64-apple-darwin
artifact_name: codex-macos-aarch64
- runner: macos-14
target: x86_64-apple-darwin
artifact_name: codex-macos-x86_64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
artifact_name: codex-linux-x86_64-gnu
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
artifact_name: codex-linux-x86_64-musl
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
artifact_name: codex-linux-aarch64-gnu
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
artifact_name: codex-linux-aarch64-musl
- runner: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: codex-windows-x86_64
- runner: windows-11-arm
target: aarch64-pc-windows-msvc
artifact_name: codex-windows-arm64
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.90
with:
targets: ${{ matrix.target }}
- name: Install musl build tools
if: contains(matrix.target, 'musl') && startsWith(matrix.runner, 'ubuntu')
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y musl-tools pkg-config
- name: cargo build (release)
run: cargo build --target ${{ matrix.target }} --release --bin codex
- name: Stage binary (Linux)
if: ${{ !startsWith(matrix.runner, 'windows') }}
shell: bash
run: |
set -euo pipefail
dest="artifacts/${{ matrix.artifact_name }}"
mkdir -p "$dest"
cp "target/${{ matrix.target }}/release/codex" "$dest/codex"
- name: Stage binary (Windows)
if: ${{ startsWith(matrix.runner, 'windows') }}
shell: pwsh
run: |
$dest = "artifacts/${{ matrix.artifact_name }}"
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Copy-Item "target/${{ matrix.target }}/release/codex.exe" "$dest/codex.exe"
- name: Upload binary artifact
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.artifact_name }}
path: codex-rs/artifacts/${{ matrix.artifact_name }}

# --- Gatherer job that you mark as the ONLY required status -----------------
publish_release_assets:
name: Publish nightly release assets
runs-on: ubuntu-24.04
needs: build_binaries
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.build_binaries.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Download binary artifacts
uses: actions/download-artifact@v4
with:
pattern: codex-*
path: release-artifacts
merge-multiple: false
- name: Prepare release assets
shell: bash
run: |
set -euo pipefail
mkdir -p release-assets
shopt -s nullglob
count=0
for dir in release-artifacts/*; do
name="$(basename "$dir")"
if [[ -f "$dir/codex.exe" ]]; then
cp "$dir/codex.exe" "release-assets/${name}.exe"
elif [[ -f "$dir/codex" ]]; then
cp "$dir/codex" "release-assets/${name}"
chmod +x "release-assets/${name}"
else
echo "::error::No codex binary found in $dir"
exit 1
fi
count=$((count + 1))
done
if [[ $count -eq 0 ]]; then
echo "::error::No artifacts available to publish."
exit 1
fi
- name: Ensure nightly release exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if ! gh release view nightly >/dev/null 2>&1; then
gh release create nightly --prerelease --title "Nightly builds" --notes "Latest successful CI binaries."
fi
- name: Upload release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release upload nightly release-assets/* --clobber

results:
name: CI results (required)
needs: [changed, general, cargo_shear, lint_build, tests]
needs: [changed, general, cargo_shear, lint_build, tests, build_binaries, publish_release_assets]
if: always()
runs-on: ubuntu-24.04
steps:
Expand All @@ -483,6 +631,8 @@ jobs:
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
echo "tests : ${{ needs.tests.result }}"
echo "bins : ${{ needs.build_binaries.result }}"
echo "release: ${{ needs.publish_release_assets.result }}"

# If nothing relevant changed (PR touching only root README, etc.),
# declare success regardless of other jobs.
Expand All @@ -496,6 +646,10 @@ jobs:
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
[[ '${{ needs.build_binaries.result }}' == 'success' ]] || { echo 'build_binaries failed'; exit 1; }
if [[ '${{ github.event_name }}' == 'push' || '${{ github.event_name }}' == 'workflow_dispatch' ]]; then
[[ '${{ needs.publish_release_assets.result }}' == 'success' ]] || { echo 'publish_release_assets failed'; exit 1; }
fi

- name: sccache summary note
if: always()
Expand Down
67 changes: 50 additions & 17 deletions codex-rs/core/src/chat_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use bytes::Bytes;
use codex_otel::otel_event_manager::OtelEventManager;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::LocalShellAction;
use codex_protocol::models::LocalShellExecAction;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
Expand Down Expand Up @@ -137,7 +139,9 @@ pub(crate) async fn stream_chat_completions(
// Otherwise, attach to immediate next assistant anchor (tool-calls or assistant message)
if !attached && idx + 1 < input.len() {
match &input[idx + 1] {
ResponseItem::FunctionCall { .. } | ResponseItem::LocalShellCall { .. } => {
ResponseItem::FunctionCall { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::CustomToolCall { .. } => {
reasoning_by_anchor_index
.entry(idx + 1)
.and_modify(|v| v.push_str(&text))
Expand Down Expand Up @@ -240,19 +244,25 @@ pub(crate) async fn stream_chat_completions(
}
ResponseItem::LocalShellCall {
id,
call_id: _,
status,
call_id,
action,
..
} => {
// Confirm with API team.
let tool_call_id = call_id.clone().or_else(|| id.clone()).unwrap_or_default();
let arguments = match action {
LocalShellAction::Exec(exec) => local_shell_arguments(exec),
};

let mut msg = json!({
"role": "assistant",
"content": null,
"tool_calls": [{
"id": id.clone().unwrap_or_else(|| "".to_string()),
"type": "local_shell_call",
"status": status,
"action": action,
"id": tool_call_id,
"type": "function",
"function": {
"name": "local_shell",
"arguments": arguments.to_string(),
}
}]
});
if let Some(reasoning) = reasoning_by_anchor_index.get(&idx)
Expand Down Expand Up @@ -289,24 +299,29 @@ pub(crate) async fn stream_chat_completions(
}));
}
ResponseItem::CustomToolCall {
id,
call_id: _,
call_id,
name,
input,
status: _,
..
} => {
messages.push(json!({
let mut msg = json!({
"role": "assistant",
"content": null,
"tool_calls": [{
"id": id,
"type": "custom",
"custom": {
"id": call_id,
"type": "function",
"function": {
"name": name,
"input": input,
"arguments": json!({ "input": input }).to_string(),
}
}]
}));
});
if 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::CustomToolCallOutput { call_id, output } => {
messages.push(json!({
Expand Down Expand Up @@ -431,6 +446,24 @@ pub(crate) async fn stream_chat_completions(
}
}

fn local_shell_arguments(action: &LocalShellExecAction) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert("command".to_string(), json!(action.command));
if let Some(workdir) = &action.working_directory {
map.insert("workdir".to_string(), json!(workdir));
}
if let Some(timeout) = action.timeout_ms {
map.insert("timeout_ms".to_string(), json!(timeout));
}
if let Some(env) = &action.env {
map.insert("env".to_string(), json!(env));
}
if let Some(user) = &action.user {
map.insert("user".to_string(), json!(user));
}
serde_json::Value::Object(map)
}

async fn append_assistant_text(
tx_event: &mpsc::Sender<Result<ResponseEvent>>,
assistant_item: &mut Option<ResponseItem>,
Expand Down
Loading
Loading