A local context bridge for Codex CLI and Chat Completions backends.
Context-D is the project name. ktxd is its working handle — used across the crate, executable, config files, logs, and Codex provider key. Same project, just the name it travels under.
Context-D lets Codex CLI use an upstream Chat Completions model through the OpenAI Responses API shape that Codex expects. It translates /v1/responses requests into upstream Chat Completions requests, normalizes responses back into Responses objects or Server-Sent Events (SSE), and keeps completed responses in memory so previous_response_id can work across turns.
The first supported target is DeepSeek-V4-Pro on Azure AI Foundry.
| Project | Context-D |
| Command and package | ktxd |
| Primary client | Codex CLI |
| Client-facing API | OpenAI Responses API |
| Upstream compatibility | Chat Completions |
| First-class target | DeepSeek-V4-Pro on Azure AI Foundry |
| Runtime | Local Rust proxy |
Codex CLI
│ Responses API: /v1/responses
▼
Context-D (`ktxd`)
│ request translation, response normalization,
│ SSE conversion, and in-memory continuation state
▼
Chat Completions backend
- Why this exists
- Current status
- Prerequisites
- Quick start
- Configure Codex CLI
- Function/tool-call smoke test
- Configuration reference
- Troubleshooting
- Development
- Project layout
- Security notes
Codex CLI expects a Responses-compatible provider. Many useful hosted models, including Azure AI Foundry serverless models, expose Chat Completions-compatible endpoints instead. Context-D bridges that gap while preserving the Responses-facing contract Codex uses.
Use it when you want to:
- Run Codex CLI against
DeepSeek-V4-Proor another Chat Completions backend. - Keep Codex on the
/v1/responseswire API while adapting the upstream request format locally. - Test Responses-style SSE output, function/tool calls, and
previous_response_idbehavior before wiring a provider directly into Codex. - Provide Codex model metadata through
model_catalog_jsonso Codex does not fall back to degraded unknown-model defaults.
Implemented today:
GET /healthzGET /v1/modelsPOST /v1/responsesGET /v1/responses/{response_id}- Non-streaming Responses output
- Responses-style SSE output for streamed requests
- In-memory response/session storage
previous_response_idcontinuation for completed responses- Function tool definitions and
function_call_outputfollow-up turns - Azure-style auth headers and Azure v1 Chat Completions endpoints
Known limitations:
- Storage is in-memory only. Restarting the proxy forgets response IDs.
- Only function tools are supported. Tools such as
web_searchare rejected intentionally. - Streamed upstream responses are fully buffered before their chunks are normalized and emitted as Responses SSE events; this is not low-latency pass-through streaming.
- The first-class target is currently
DeepSeek-V4-Pro; other models may need config and metadata tuning. upstream_familyandupstream_deploymentare currently required configuration fields but do not select an adapter or construct an upstream URL. The current implementation always uses the Chat Completions client configured bychat_completions_url.
- Rust with edition 2024 support.
- A reachable Chat Completions endpoint. The examples below use Azure AI Foundry.
- An upstream credential, such as an API key or bearer token.
curlandjqfor the quick smoke tests.- Codex CLI if you want to use the proxy from Codex.
Copy the example config and edit it for your upstream endpoint:
cd ktxd
cp config.example.toml config.toml
$EDITOR config.tomlFor Azure AI Foundry DeepSeek-V4-Pro, the most important fields are:
[server]
bind = "127.0.0.1:3000"
[models.DeepSeek-V4-Pro]
public_model = "DeepSeek-V4-Pro"
upstream_family = "chat_completions"
upstream_deployment = "DeepSeek-V4-Pro"
upstream_model = "DeepSeek-V4-Pro"
# Azure AI Foundry v1 Chat Completions endpoint.
# Replace <resource> and <region> with your Foundry resource host.
chat_completions_url = "https://<resource>.<region>.services.ai.azure.com/openai/v1/chat/completions"
auth_header = "api-key"
auth_env_var = "AZURE_AI_FOUNDRY_API_KEY"
# Azure v1 endpoints commonly require the model/deployment in the JSON body.
send_model_in_body = true
include_stream_usage = true
retry_without_stream_options_on_4xx = true
instruction_role = "system"
context_window = 1000000Set your upstream key in the environment. The variable name must match auth_env_var:
export AZURE_AI_FOUNDRY_API_KEY="<your-api-key>"export KTXD_CONFIG="$PWD/config.toml"
RUST_LOG=ktxd=info,tower_http=info cargo runYou should see a log line like:
ktxd listening bind=127.0.0.1:3000
Check health:
curl -sS http://127.0.0.1:3000/healthz | jq .List models:
curl -sS http://127.0.0.1:3000/v1/models | jq .Run a non-streaming Responses request:
curl -sS http://127.0.0.1:3000/v1/responses \
-H 'content-type: application/json' \
-d '{
"model": "DeepSeek-V4-Pro",
"instructions": "Be concise.",
"input": "Reply with exactly: pong",
"stream": false
}' | jq .Expected shape:
{
"object": "response",
"model": "DeepSeek-V4-Pro",
"status": "completed",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "pong"
}
]
}
]
}Run the same request as streaming SSE:
curl -N http://127.0.0.1:3000/v1/responses \
-H 'content-type: application/json' \
-d '{
"model": "DeepSeek-V4-Pro",
"instructions": "Be concise.",
"input": "Reply with exactly: pong",
"stream": true
}'You should see events such as response.created, response.output_item.added, response.output_text.delta, response.output_item.done, and response.completed. The proxy buffers the upstream stream first, so these events are emitted after the upstream response has been collected rather than incrementally as upstream chunks arrive.
Store the response ID from a completed response:
FIRST=$(curl -sS http://127.0.0.1:3000/v1/responses \
-H 'content-type: application/json' \
-d '{
"model": "DeepSeek-V4-Pro",
"instructions": "Be concise.",
"input": "Remember this favorite flower: ORCHID. Reply exactly: remembered",
"stream": false
}')
RESP_ID=$(echo "$FIRST" | jq -r .id)
echo "$RESP_ID"Retrieve it:
curl -sS "http://127.0.0.1:3000/v1/responses/$RESP_ID" | jq .Continue from it:
curl -sS http://127.0.0.1:3000/v1/responses \
-H 'content-type: application/json' \
-d "$(jq -n --arg previous_response_id "$RESP_ID" '{
model: "DeepSeek-V4-Pro",
previous_response_id: $previous_response_id,
instructions: "Be concise.",
input: "What is my favorite flower? Reply with only the flower name.",
stream: false
}')" | jq .Codex needs two things:
- A provider entry that points Codex at
ktxd. - Model metadata for
DeepSeek-V4-Pro, otherwise Codex prints a warning like:
⚠ Model metadata for `DeepSeek-V4-Pro` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.
The metadata is supplied through model_catalog_json. The included catalog advertises DeepSeek-V4-Pro's 1M context window while intentionally capping any single tool/function output at 50K tokens through truncation_policy; that keeps runaway command output from crowding out the rest of the conversation.
Copy the example catalog into your Codex home:
mkdir -p ~/.codex/model-catalogs
cp examples/codex/model-catalogs/ktxd.json ~/.codex/model-catalogs/ktxd.jsonUse an absolute path when referencing this file from Codex config. For example:
/Users/alice/.codex/model-catalogs/ktxd.json
Add this provider block to your user-level Codex config:
[model_providers.ktxd]
name = "Context-D (ktxd) proxy"
base_url = "http://127.0.0.1:3000/v1"
wire_api = "responses"
requires_openai_auth = false
request_max_retries = 0
stream_max_retries = 0
stream_idle_timeout_ms = 300000Keep provider configuration in user-level ~/.codex/config.toml. Project-scoped .codex/config.toml files are useful for project behavior, but provider definitions are machine-local settings.
Codex profile loading changed across releases, so check codex --version and choose the layout that matches your installed version. If a profile does not load or the metadata warning remains, try the other layout and restart Codex.
Create ~/.codex/ktxd.config.toml:
model = "DeepSeek-V4-Pro"
model_provider = "ktxd"
model_catalog_json = "/Users/alice/.codex/model-catalogs/ktxd.json"
# DeepSeek-V4-Pro is exposed here without Codex reasoning metadata.
model_reasoning_effort = "none"
model_reasoning_summary = "none"
web_search = "disabled"Then run:
codex --profile ktxdFor non-interactive smoke testing:
codex exec --profile ktxd \
--skip-git-repo-check \
--sandbox read-only \
"what is your model identity?"A healthy response should identify as Codex running on DeepSeek-V4-Pro via ktxd, and the metadata warning should be gone.
Some Codex builds read profile tables from ~/.codex/config.toml. If the standalone profile file does not load, put this in ~/.codex/config.toml instead:
[profiles.ktxd]
model = "DeepSeek-V4-Pro"
model_provider = "ktxd"
model_catalog_json = "/Users/alice/.codex/model-catalogs/ktxd.json"
# DeepSeek-V4-Pro is exposed here without Codex reasoning metadata.
model_reasoning_effort = "none"
model_reasoning_summary = "none"
web_search = "disabled"Then run the same command:
codex --profile ktxdIf you want to test without touching your real ~/.codex, create a local Codex home and launch Codex with CODEX_HOME.
For Codex releases that use standalone profile files, keep the provider in .codex/config.toml and put the profile in .codex/ktxd.config.toml:
mkdir -p .codex/model-catalogs
cp examples/codex/model-catalogs/ktxd.json .codex/model-catalogs/ktxd.json
CATALOG_PATH="$PWD/.codex/model-catalogs/ktxd.json"
cat > .codex/config.toml <<'TOML'
[model_providers.ktxd]
name = "Context-D (ktxd) proxy"
base_url = "http://127.0.0.1:3000/v1"
wire_api = "responses"
requires_openai_auth = false
request_max_retries = 0
stream_max_retries = 0
stream_idle_timeout_ms = 300000
TOML
cat > .codex/ktxd.config.toml <<TOML
model = "DeepSeek-V4-Pro"
model_provider = "ktxd"
model_catalog_json = "$CATALOG_PATH"
model_reasoning_effort = "none"
model_reasoning_summary = "none"
web_search = "disabled"
TOML
CODEX_HOME="$PWD/.codex" codex exec --profile ktxd \
--skip-git-repo-check \
--sandbox read-only \
"what is your model identity?"For Codex releases that use profile tables, put the profile table in .codex/config.toml instead:
[profiles.ktxd]
model = "DeepSeek-V4-Pro"
model_provider = "ktxd"
model_catalog_json = "/absolute/path/to/.codex/model-catalogs/ktxd.json"
model_reasoning_effort = "none"
model_reasoning_summary = "none"
web_search = "disabled"ktxd supports Responses function tools and converts them to Chat Completions tool calls.
curl -sS http://127.0.0.1:3000/v1/responses \
-H 'content-type: application/json' \
-d '{
"model": "DeepSeek-V4-Pro",
"input": "Use the tool to look up the status for ticket ABC-123.",
"tools": [
{
"type": "function",
"name": "lookup_ticket",
"description": "Look up a ticket by ID.",
"parameters": {
"type": "object",
"properties": {
"ticket_id": { "type": "string" }
},
"required": ["ticket_id"],
"additionalProperties": false
}
}
],
"tool_choice": "auto",
"stream": false
}' | jq .If the model calls the tool, send the result back using previous_response_id and an input item of type function_call_output:
{
"model": "DeepSeek-V4-Pro",
"previous_response_id": "resp_...",
"input": [
{
"type": "function_call_output",
"call_id": "call_...",
"output": "Ticket ABC-123 is open and assigned to Support."
}
]
}KTXD_CONFIG points the binary at a TOML config file. If it is not set, the binary uses built-in defaults suitable for local startup and configuration validation, but not for real upstream calls.
When neither config environment variable is set, the binary checks the current directory for config.toml and then config.local.toml.
export KTXD_CONFIG="$PWD/config.toml"Supported model config fields:
| Field | Purpose |
|---|---|
public_model |
Model name accepted from Codex and returned by /v1/models. |
display_name |
Human-readable model name returned by /v1/models. |
description |
Model description returned by /v1/models. |
upstream_family |
Required compatibility field. Only chat_completions is implemented; this field does not currently select an adapter. |
upstream_deployment |
Required compatibility field retained for deployment metadata; it is not currently used to construct the URL or request. |
upstream_model |
Model value sent upstream when send_model_in_body = true. |
chat_completions_url |
Full upstream Chat Completions URL. |
auth_header |
api-key or authorization_bearer. |
auth_env_var |
Environment variable that stores the upstream secret. |
send_model_in_body |
Include model in the upstream JSON body. Useful for Azure v1 endpoints. |
include_stream_usage |
Request upstream stream usage when supported. |
retry_without_stream_options_on_4xx |
Retry streaming without stream_options if the upstream rejects that field. |
instruction_role |
Lower Responses instructions as system or developer. |
context_window |
Metadata returned from /v1/models. |
| Variable | Purpose |
|---|---|
KTXD_CONFIG |
Path to proxy TOML config. |
AZURE_AI_FOUNDRY_API_KEY |
Example upstream API key variable. Rename via auth_env_var if needed. |
RUST_LOG |
Enables Rust/tracing logs, for example ktxd=debug,tower_http=info. |
CODEX_HOME |
Optional Codex config directory for isolated testing. |
Check these items:
model_catalog_jsonis set in the active profile or top-level Codex config.- The path is absolute.
- The file exists and contains a model with
"slug": "DeepSeek-V4-Pro". - Your installed Codex version is using the profile layout you edited: either standalone
~/.codex/ktxd.config.tomlor[profiles.ktxd]in~/.codex/config.toml. - Restart Codex after changing config.
Use the Azure AI Foundry v1 Chat Completions endpoint shape:
chat_completions_url = "https://<resource>.<region>.services.ai.azure.com/openai/v1/chat/completions"
send_model_in_body = trueDo not append an unsupported api-version query string to the v1 endpoint.
The proxy stores sessions in memory. The response ID must come from a completed response created by the currently running proxy process. If you restart the proxy, old IDs are gone.
Only function tools are currently supported. Disable Codex web search in the ktxd profile:
web_search = "disabled"Set the environment variable named by auth_env_var before starting the proxy:
export AZURE_AI_FOUNDRY_API_KEY="<your-api-key>"Change the bind address in config.toml:
[server]
bind = "127.0.0.1:3001"Then update the Codex provider base_url accordingly:
base_url = "http://127.0.0.1:3001/v1"Run the repository test suite:
cargo +stable test --all-features --lockedRun the pre-PR checks before opening a pull request:
cargo +1.86.0 fmt --all -- --check
cargo +stable clippy --all-targets --all-features --locked -- -D warnings
cargo +stable test --all-features --locked
cargo +stable package --lockedThe GitHub Actions workflow runs these checks on every pull request targeting
main, and also verifies that the crate still builds with its declared Rust
MSRV of 1.86.
Run with debug logs:
RUST_LOG=ktxd=debug,tower_http=debug cargo runsrc/
├── capability
│ └── mod.rs Supported upstream capability types
├── domain
│ ├── hash.rs Canonical serialization and content hashes
│ ├── items.rs Tagged messages, tool calls, and provenance
│ ├── mod.rs Canonical domain model exports
│ └── session.rs Sessions, transcripts, turns, and usage
├── driver
│ ├── mod.rs Turn driver exports
│ └── turn_driver.rs Turn orchestration and response persistence
├── policy
│ └── mod.rs Static route policy placeholder
├── responses
│ ├── events.rs Responses objects and SSE event construction
│ ├── handlers.rs HTTP routes and endpoint handling
│ └── mod.rs Responses API module exports
├── session
│ └── mod.rs In-memory session and response store
├── stream
│ └── mod.rs Stream translation exports
├── substrate
│ └── mod.rs Node sink and seed resolver interfaces
├── translator
│ ├── chat_compiler.rs Responses-to-Chat-Completions compiler
│ ├── chat_stream.rs Chat-Completions-to-Responses translation
│ ├── mod.rs Translation module exports
│ └── responses_normalizer.rs Responses request normalization
├── upstream
│ └── mod.rs Reqwest Chat Completions client
├── wire
│ ├── chat.rs Chat Completions request/response schemas
│ ├── mod.rs Wire schema module exports
│ └── responses.rs Responses API request/response schemas
├── app_state.rs Shared application state
├── config.rs Proxy and model configuration
├── error.rs Error types and HTTP error responses
├── ids.rs Typed response, turn, item, and tenant IDs
├── lib.rs Library module exports
└── main.rs Binary entry point and server startup
- Bind to
127.0.0.1for local Codex use. Exposing the proxy on a network interface should be done only behind appropriate authentication and network controls. - The proxy forwards prompts and tool outputs to your configured upstream provider. Review your provider's data handling terms before sending sensitive code or secrets.