Skip to content

server: improve Responses API compliance and Codex CLI compatibility#21174

Open
krystophny wants to merge 24 commits into
ggml-org:masterfrom
krystophny:responses-api-codex-compat
Open

server: improve Responses API compliance and Codex CLI compatibility#21174
krystophny wants to merge 24 commits into
ggml-org:masterfrom
krystophny:responses-api-codex-compat

Conversation

@krystophny

@krystophny krystophny commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Improve Responses API (/v1/responses) compliance and Codex CLI compatibility.

Response object (non-streaming and streaming):

  • Add 24 missing fields per OpenAI Responses API spec (tools, truncation, temperature, top_p, metadata, store, service_tier, etc.)
  • Fix function_call id/call_id field mapping (id gets unique fc_ ID, call_id gets the model's tool_call.id)
  • Add output_text top-level convenience field
  • Add output_tokens_details with reasoning_tokens to usage
  • Restore refusal content type handling (was broken in upstream, unreachable code after throw)

Streaming events:

  • Add sequence_number to ALL streaming events (created, in_progress, added, delta, done, completed)
  • Add output_index to all events referencing output items
  • Add content_index to content-related events
  • Populate full response object in response.created and response.in_progress (was only {id, object, status})
  • Function call item IDs consistent across output_item.added, function_call_arguments.delta, and output_item.done
  • Counter state persisted across streaming chunks via task_result_state

Codex CLI compatibility:

  • Skip non-function tool types (web_search, code_interpreter) instead of rejecting with 400
  • Merge developer/system role messages into first system message for templates requiring system at position 0 (e.g. Qwen)
  • Strip Responses-only request keys (store, include, prompt_cache_key, web_search, text, truncation, metadata)
  • Accept input_text type alongside output_text for EasyInputMessage / AssistantMessageItemParam

Prior art: #19720 by @riskywindow (stale, 500+ commits behind). This PR incorporates applicable ideas adapted to the current codebase.

Fixes #19138. Related: #20156, #20733, #20607.

Verification

Post-merge local verification (2026-06-18)

Focused failures fixed during the merge:

test-llama-archs: ggml/src/ggml-backend-meta.cpp:590: split_states_equal(src_ss[0], src_ss[1])
test-thread-safety: CUDA error: unspecified launch failure during CUDA stream teardown

Passing checks after the fixes:

$ cmake -S . -B build -G Ninja -DLLAMA_BUILD_TESTS=ON && cmake --build build -j$(nproc)
[6/6] Linking CXX executable bin/llama
$ ctest --test-dir build --output-on-failure -R '^test-llama-archs$'
100% tests passed, 0 tests failed out of 1
$ ctest --test-dir build --output-on-failure -R '^test-thread-safety$'
100% tests passed, 0 tests failed out of 2
$ ctest --test-dir build --output-on-failure -E '^(test-backend-ops|test-quant-type-selection)$'
100% tests passed, 0 tests failed out of 53
Total Test time (real) = 151.27 sec
$ cd tools/server/tests
$ PORT=18080 AIP_HTTP_PORT=18080 LLAMA_SERVER_BIN_PATH=../../../build/bin/llama-server LLAMA_CACHE=$HOME/.cache/llama.cpp uv run --with-requirements requirements.txt ./tests.sh
314 passed, 3 skipped, 199 deselected in 818.91s (0:13:38)
$ cd tools/ui
$ npm run test:ui -- --run --testTimeout=60000
Test Files 11 passed (11)
Tests 35 passed (35)
$ cd tools/ui
$ npm run test:client -- --run
Test Files 2 passed (2)
Tests 9 passed (9)
$ cd tools/ui
$ npm run test:unit -- --run
Test Files 15 passed (15)
Tests 211 passed (211)
$ cd tools/ui
$ npm run test:e2e
6 passed (1.4m)

Local all-CTest without exclusions was not the final gate: test-backend-ops is a long backend matrix, and test-quant-type-selection failed because live model metadata no longer matched the upstream snapshots. The quant snapshot files are unchanged against upstream/master.

pytest (14 tests, tinyllama2)

$ LLAMA_SERVER_BIN_PATH=./build/bin/llama-server pytest unit/test_compat_oai_responses.py -v
test_responses_with_openai_library PASSED
test_responses_stream_with_openai_library PASSED
test_responses_schema_fields PASSED
test_responses_stream_schema_fields PASSED
test_responses_non_function_tool_skipped PASSED
test_responses_only_non_function_tools_same_as_no_tools PASSED
test_responses_extra_keys_stripped PASSED
test_responses_developer_role_merging PASSED
test_responses_input_text_type_multi_turn PASSED
test_responses_output_text_matches_content PASSED
test_responses_stream_output_text_consistency PASSED
test_responses_stream_created_event_has_full_response PASSED
test_responses_stream_all_events_have_sequence_number PASSED
test_responses_stream_delta_events_have_indices PASSED
14 passed

E2E with async OpenAI SDK + Qwen3.5-9B Q4_K_M

$ python3 e2e_test.py  # uses AsyncOpenAI against localhost:8080
Test 1: Non-streaming basic          OK: output_text='4', fields=36
Test 2: Streaming basic              OK: 205 events, gathered text matches
Test 3: Non-function tools skipped   OK: status=completed
Test 4: Developer role merging       OK
Test 5: Multi-turn with input_text   OK
Test 7: Streaming seq_nums           OK: 105 events, strictly increasing
Test 6: Concurrent stress (5 req)    OK: 5/5 completed, 0 failures
ALL E2E TESTS PASSED

Codex CLI E2E

$ codex exec -p local "Say hello in one word"
Hello
tokens used: 6,119

$ codex exec -p local "Run 'echo hello world' and show the output"
exec: /bin/zsh -lc 'echo hello world' succeeded
hello world
tokens used: 1,054

Test plan

  • 14 pytest tests covering all code paths (schema, streaming, tools, roles, keys)
  • Async OpenAI SDK: non-streaming, streaming, concurrent stress
  • Streaming events: sequence_number strictly increasing, output_index/content_index present
  • response.created has full response object with all fields
  • Non-function tools silently skipped (200 not 400)
  • Developer/system messages merged correctly
  • Codex CLI connects and works (text + tool calling)
  • Function call IDs consistent between added/done streaming events

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - AI was used to help draft code, tests, and PR text. The submitter reviewed and is responsible for the final changes.

If reviewers prefer, this can be split into smaller PRs for faster review. Let me know.

@krystophny krystophny requested a review from a team as a code owner March 30, 2026 07:54
@ngxson

ngxson commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

please add proper testings for it

@github-actions github-actions Bot added the python python script changes label Mar 30, 2026
@krystophny krystophny force-pushed the responses-api-codex-compat branch 3 times, most recently from 530e13c to 467266b Compare March 30, 2026 13:04
@krystophny

Copy link
Copy Markdown
Contributor Author

please add proper testings for it

Done!

@fumlig

fumlig commented Mar 30, 2026

Copy link
Copy Markdown

According to the responses API reference for streaming events, the response.created and response.in_progress streaming events should also contain created_at, model etc.

It seems like the current implementation just returns a minimal response object in those events. This causes issues with certain spec-compliant client libraries like async-openai. Would it be possible to add the missing streaming event fields here as well?

krystophny added a commit to krystophny/llama.cpp that referenced this pull request Mar 30, 2026
- Add sequence_number to ALL streaming events (created, in_progress,
  output_item.added, content_part.added, all delta events)
- Add output_index to all events referencing output items
- Add content_index to content-related events
- Populate full response object in response.created and
  response.in_progress events (was only {id, object, status})
- Add id field to function_call output_item.added events
- Add status: completed to reasoning output_item.done events
- Counter state persisted across streaming chunks via task_result_state

Fixes: spec-compliant client libraries (async-openai) that require
these fields can now parse all streaming events without error.

Refs: ggml-org#21174 (fumlig review comment)
krystophny added a commit to krystophny/llama.cpp that referenced this pull request Mar 30, 2026
Code fixes:
- build_oai_resp_metadata accepts status param; completed_at is null
  when status is in_progress (was always set to timestamp)
- response.created/in_progress events use zeroed usage (was passing
  actual prompt tokens before response was logically started)
- Function call item IDs are now generated once per tool call in
  update() and reused consistently across output_item.added,
  function_call_arguments.delta, and output_item.done events
  (was generating independent random IDs in each path)
- Clean up commented-out status checks in server-common.cpp

Test fixes:
- Assert sequence_number on every event unconditionally (was using
  weak "if present" guard)
- Check actual values not just key presence in streaming created
  event test (completed_at is None, usage tokens are 0, etc.)

Refs: ggml-org#21174 (patrick review)
@krystophny

Copy link
Copy Markdown
Contributor Author

@fumlig Thanks for the feedback. The streaming events now include the full response object in response.created and response.in_progress events (with all 24+ required fields, status: "in_progress", completed_at: null, zeroed usage). All partial events (output_item.added, content_part.added, all deltas) now have sequence_number, output_index, and content_index per spec.

Tested with the async OpenAI Python SDK (which validates event schemas similarly to async-openai on the Rust side).

@ngxson Tests added: 14 pytest tests covering schema fields, streaming compliance, tool type skipping, developer role merging, key stripping, multi-turn input, and output_text consistency. Plus E2E tests with async OpenAI SDK against Qwen3.5-9B.

If you'd prefer this split into smaller PRs for faster review, happy to do so.

@krystophny

Copy link
Copy Markdown
Contributor Author

Additional E2E testing (async OpenAI SDK, Codex CLI, concurrent stress tests, multiple Qwen3.5 models) is documented in the companion meta-repo: https://github.com/krystophny/llama.cpp-dev

The meta-repo includes a Nix flake for reproducible test environments and scripted test harnesses under scripts/.

krystophny added a commit to krystophny/llama.cpp that referenced this pull request Mar 30, 2026
Codex CLI compatibility:
- Skip non-function tool types (web_search, code_interpreter)
- Merge developer/system messages into position 0 for Qwen templates
- Strip Responses-only request keys (store, include, prompt_cache_key)
- Restore refusal content type handling

Responses API compliance (ideas from ggml-org#19720 by riskywindow, adapted):
- Add 24 missing Response object fields per OpenAI spec
- Fix function_call id/call_id field mapping
- Add sequence_number, output_index, content_index to ALL streaming events
- Full response object in response.created/in_progress events
- Accept input_text type and EasyInputMessage for multi-turn input
- output_text convenience field, output_tokens_details

14 pytest tests, E2E tested with async OpenAI SDK and Codex CLI.

Refs: ggml-org#19138, ggml-org#19720, ggml-org#21174
@ngxson

ngxson commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

I'm ok with the current PR, but could you let us know when you are finally ready for review? I have been re-running the CI each time you pushed a new PR, and without CI passed, I cannot merge it

@krystophny krystophny marked this pull request as draft March 30, 2026 18:04
@krystophny

Copy link
Copy Markdown
Contributor Author

I'm ok with the current PR, but could you let us know when you are finally ready for review? I have been re-running the CI each time you pushed a new PR, and without CI passed, I cannot merge it

@ngxson thanks! I marked it as draft and iterate a bit and test locally more with https://github.com/lazy-fortran/fortbench during the coming days and see if any problems pop up on the codex path compared to opencode. Then I'll let you know.

@krystophny krystophny force-pushed the responses-api-codex-compat branch from adef64c to 7229135 Compare April 3, 2026 07:02
krystophny added a commit to krystophny/llama.cpp that referenced this pull request Apr 3, 2026
- Add sequence_number to ALL streaming events (created, in_progress,
  output_item.added, content_part.added, all delta events)
- Add output_index to all events referencing output items
- Add content_index to content-related events
- Populate full response object in response.created and
  response.in_progress events (was only {id, object, status})
- Add id field to function_call output_item.added events
- Add status: completed to reasoning output_item.done events
- Counter state persisted across streaming chunks via task_result_state

Fixes: spec-compliant client libraries (async-openai) that require
these fields can now parse all streaming events without error.

Refs: ggml-org#21174 (fumlig review comment)
krystophny added a commit to krystophny/llama.cpp that referenced this pull request Apr 3, 2026
Code fixes:
- build_oai_resp_metadata accepts status param; completed_at is null
  when status is in_progress (was always set to timestamp)
- response.created/in_progress events use zeroed usage (was passing
  actual prompt tokens before response was logically started)
- Function call item IDs are now generated once per tool call in
  update() and reused consistently across output_item.added,
  function_call_arguments.delta, and output_item.done events
  (was generating independent random IDs in each path)
- Clean up commented-out status checks in server-common.cpp

Test fixes:
- Assert sequence_number on every event unconditionally (was using
  weak "if present" guard)
- Check actual values not just key presence in streaming created
  event test (completed_at is None, usage tokens are 0, etc.)

Refs: ggml-org#21174 (patrick review)
@krystophny krystophny marked this pull request as ready for review April 3, 2026 07:02
@krystophny

Copy link
Copy Markdown
Contributor Author

I'm ok with the current PR, but could you let us know when you are finally ready for review? I have been re-running the CI each time you pushed a new PR, and without CI passed, I cannot merge it

@ngxson I saw no more issues in practical usage with Codex CLI and the smaller Qwen 3.5 models with the responses API. Rebased once more and set ready to review.

@Macmee

Macmee commented Apr 4, 2026

Copy link
Copy Markdown

Thank you for this!!! I ran into this problem today when I was trying codex with llama.cpp and this change makes it work :)

@michaelw9999

Copy link
Copy Markdown
Contributor

Nice work, thank you! I tried using it with the Codex IDE extension and almost everything works, but still had a few issues causing errors that would freeze up the entire thread, making it unusable to continue from where it was left off. I'll share some small suggested changes soon.

Comment thread tools/server/server-common.cpp Outdated
Comment thread tools/server/server-common.cpp Outdated
Comment thread tools/server/server-common.cpp Outdated
@krystophny

Copy link
Copy Markdown
Contributor Author

Fixed a small issue that caused tests to fail. Please approve the workflows once more and provide some final review approval if everything is fine @michaelw9999 .

@michaelw9999

Copy link
Copy Markdown
Contributor

Fixed a small issue that caused tests to fail. Please approve the workflows once more and provide some final review approval if everything is fine @michaelw9999 .

@krystophny I am actually using the PR and this is great (plus I've made some other tricks too!) but but I am just a user and contributor, not an admin, so I cannot approve anything...

HosheaLi added a commit to HosheaLi/dsv4-cc-proxy that referenced this pull request Jun 17, 2026
根因分析(对比 ai-adapter + llama.cpp PR #21174):

1. sequence_number 缺失 — 非 delta 事件(response.created/in_progress/
   output_item.added/content_part.added/output_text.done/content_part.done/
   output_item.done/completed)均缺少 sequence_number 字段。Codex CLI
   兼容性要求所有流式事件包含此字段。

2. created → created_at — response 对象中使用 Chat Completions API
   的 created 字段名,而 Responses API 规范要求 created_at。

3. content_part.added 多余 item_id — 该事件不应包含 item_id,
   应通过 output_index 关联到所属 output item。

附加修复:
- 添加 SSE keepalive 心跳(3s 间隔)防止 Codex CLI 超时断开
- function_call_arguments.done 字段 delta → arguments 对齐规范

参考: dyrnq/ai-adapter, ggml-org/llama.cpp#21174, OmniRoute#2599
@krystophny krystophny requested a review from pwilkin as a code owner June 17, 2026 22:19
@github-actions github-actions Bot added the testing Everything test related label Jun 17, 2026
@krystophny

Copy link
Copy Markdown
Contributor Author

OK sorry for the mess. Now CI checks should pass.

@krystophny

Copy link
Copy Markdown
Contributor Author

Update once more from upstream/master . This should fix last remaining real CI failure and re-run of checks hopefully one flaky download.

…ex-compat

# Conflicts:
#	tools/server/server-common.cpp
#	tools/server/server-task.cpp
@ngxson

ngxson commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

If reviewers prefer, this can be split into smaller PRs for faster review. Let me know.

yes, I don't think any of the maintainers can review this PR as-is

also, there seems to be some unrelated changes included here. please review your PR again before requesting a maintainer's review

The synchronize-on-teardown fix (src/llama-context.cpp) and the phi3
meta split-state fix (src/llama-model.cpp) entered this branch via an
upstream merge and are unrelated to the Responses API work. They are
now filed independently as ggml-org#24935 and ggml-org#24936. Remove them here so this
PR is limited to the server Responses changes.
@krystophny

Copy link
Copy Markdown
Contributor Author

If reviewers prefer, this can be split into smaller PRs for faster review. Let me know.

yes, I don't think any of the maintainers can review this PR as-is

also, there seems to be some unrelated changes included here. please review your PR again before requesting a maintainer's review

Moved two unrelated changes to #24935 and #24936 . Once those land, I will continue slicing the PR to smaller pieces.

@pwilkin pwilkin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't approve the PR as it is because it has a lot of content that I find questionable. While I have no problems with adding fields that are needed for Responses API compliance, I don't understand the behavior here with reporting a lot of native tools, then providing implementations that assume a specific Unix shell environment? I'm not even sure how this is supposed to work because it's completely uncommented, but that's certainly not something that we can merge.

@krystophny

Copy link
Copy Markdown
Contributor Author

I can't approve the PR as it is because it has a lot of content that I find questionable. While I have no problems with adding fields that are needed for Responses API compliance, I don't understand the behavior here with reporting a lot of native tools, then providing implementations that assume a specific Unix shell environment? I'm not even sure how this is supposed to work because it's completely uncommented, but that's certainly not something that we can merge.

Thank you for taking a look into this. This was indeed nonsense. Sorry about it. I remove it now.

The Responses-to-chat-completions path synthesized local_shell exec
commands (a ripgrep invocation for file_search, a wrapper CLI for
web_search) and parsed them back, gated on the X-Llama-Responses-*-Wrapper
request headers. This assumed a specific client shell environment and had
no test coverage.

Remove the bridge end to end: the two wrapper request headers and their
internal __responses_*_wrapper fields, the local_shell command builders,
the inbound local_shell_call to web/file search parsing, and the
shell-output scraping. A local_shell_call is now relayed verbatim for the
client to execute.

Hosted web_search, file_search and image_generation have no server-side
backend, so they are declared to the model only when tool_choice
explicitly selects them, and skipped otherwise.
Comment thread tools/server/server-chat.cpp Outdated
static std::string responses_custom_tool_description(const json & resp_tool, const std::string & name) {
std::string description = json_value(resp_tool, "description", std::string("Freeform Codex tool input."));

if (name == "apply_patch") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still see some hardcoded stuff that I'm not sure belongs here. Serving tool descriptions should always be the client's responsibility, not the server's (unless they're server-side tools).

Comment thread tools/server/server-chat.cpp Outdated
name = sanitize_tool_name(json_value(resp_tool, "name", std::string("local_shell")), "local_shell");
fn = responses_function_tool(
name,
"Run a local shell command.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ones as well, doesn't look like something a server should be handling at all.

Comment thread tools/server/server-chat.cpp Outdated
name = sanitize_tool_name(json_value(resp_tool, "name", std::string("update_plan")), "update_plan");
fn = responses_function_tool(
name,
json_value(resp_tool, "description", std::string("Updates the task plan.")),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment thread tools/server/server-chat.cpp Outdated
name = sanitize_tool_name(json_value(resp_tool, "name", std::string("tool_search")), "tool_search");
fn = responses_function_tool(
name,
json_value(resp_tool, "description", std::string("Search for tools available to this Codex session.")),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment thread tools/server/server-chat.cpp Outdated
}
fn = responses_function_tool(
name,
"Search files in the current workspace. Use mode='files' to find paths by name, or mode='content' to search file contents. Prefer this over shell commands such as find.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment thread tools/server/server-chat.cpp Outdated
}
fn = responses_function_tool(
name,
"Generate an image from a prompt.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

item.at("call_id").get<std::string>(),
parse_arguments_best_effort(item.at("arguments"))));
} else if (exists_and_is_string(item, "type") &&
(item.at("type") == "function_call" ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it those are actually separate calls, but we should treat them just like all function_call instances, i.e. pass them on without modifications.

Comment thread tools/server/server-task.cpp Outdated
return current_input.substr(previous_input.size());
}

static json build_local_shell_action(const json & args, const std::string & raw_arguments) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has got to go.

Drop the server-side fabrication of native tool types (local_shell,
custom/apply_patch, web_search, file_search, image_generation,
tool_search, task_list): their hardcoded descriptions and JSON schemas,
the bash -lc shell synthesis, apply_patch body normalization, and the
per-type reconstruction of tool-call items. Non-function Responses tool
types are now skipped instead of fabricated; inbound and outbound tool
calls relay as plain function_call. Removes the responses_tool_metadata
plumbing, an orphaned header helper, and the stale README block on the
removed search wrappers.
Stop injecting "[responses recovery: ...]" placeholder text into the
converted prompt, and drop the per-item warning log, when an input
content part is malformed or of an unknown type. A diagnostic in the
prompt only confuses the model, wastes context, and (with Codex
replaying full history) gets re-logged every turn. Such parts are now
dropped quietly and the request still completes; the tests assert the
converted prompt is unchanged.
Drop the "[unsupported Responses item: ...]" and "[unsupported Responses
input item: ...]" placeholders injected for unknown or non-object
top-level input items; they are skipped quietly, like the content parts.
Folds the now-redundant ghost_snapshot special case into the default
skip.
@krystophny

Copy link
Copy Markdown
Contributor Author

@pwilkin thanks for the remarks. I removed the tool-related code you flagged:

  1. The hardcoded tool descriptions and parameter schemas for the non-function tool types (local_shell, apply_patch, web_search, file_search, image_generation, tool_search, task_list). The server no longer serves any of these — non-function tools are simply skipped instead of translated.
  2. The shell-command synthesis (build_local_shell_action and the bash -lc wrapping) is gone, so nothing assumes a Unix shell anymore.
  3. The per-type reconstruction of tool calls is gone: *_call items are relayed as plain function_call on both the input and output side, instead of being rebuilt per tool type.

So the converter only relays function tools and their calls now. It's in the latest three commits.

@pwilkin

pwilkin commented Jun 24, 2026

Copy link
Copy Markdown
Member

Yeah, this is starting to resemble something merge'able, let's see the test results :)

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples python python script changes server testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support OpenAI Responses API (/v1/responses) in llama.cpp server