Add native OpenAI spine CLI

This commit is contained in:
Meta Vens 2026-05-13 05:28:13 +01:00
commit 71eca3889e
7 changed files with 198 additions and 2 deletions

View file

@ -19,3 +19,17 @@ codex-model-provider-info = { path = "../vendor/codex/codex-rs/model-provider-in
codex-protocol = { path = "../vendor/codex/codex-rs/protocol" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
futures = "0.3"
serde = "1"
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[[bin]]
name = "epiphany-openai-spine"
path = "src/bin/epiphany-openai-spine.rs"
[patch.crates-io]
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" }
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
[patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"]
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }

View file

@ -0,0 +1,154 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_login::AuthCredentialsStoreMode;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use epiphany_openai_adapter::EpiphanyOpenAiInputItem;
use epiphany_openai_adapter::EpiphanyOpenAiModelRequest;
use epiphany_openai_codex_spine::EpiphanyCodexOpenAiTransport;
use epiphany_openai_codex_spine::responses_request_from_epiphany;
use epiphany_openai_codex_spine::status_from_auth_manager;
use epiphany_openai_codex_spine::status_from_codex_auth;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
let mut args = env::args().skip(1);
let command = args.next().unwrap_or_else(|| "usage".to_string());
match command.as_str() {
"status" => {
let options = parse_common_options(args.collect())?;
let auth_manager = auth_manager(options.codex_home);
let status = status_from_auth_manager(
&auth_manager,
options.default_model,
/*supports_websockets*/ true,
)
.await;
print_json(&status)?;
}
"model-turn" => {
let options = parse_model_turn_options(args.collect())?;
let request_text = fs::read_to_string(&options.request_path)
.with_context(|| format!("failed to read {}", options.request_path.display()))?;
let request: EpiphanyOpenAiModelRequest = serde_json::from_str(&request_text)
.with_context(|| format!("failed to parse {}", options.request_path.display()))?;
let transport = EpiphanyCodexOpenAiTransport::openai(auth_manager(options.codex_home));
let events = transport.collect_model_events(request).await?;
print_json(&events)?;
}
"smoke" => {
let auth = CodexAuth::from_api_key("test-key");
let status = status_from_codex_auth(Some(&auth), Some("gpt-5.4".to_string()), true);
let mut request = EpiphanyOpenAiModelRequest::new(
"smoke-request",
"smoke-conversation",
"gpt-5.4",
"Answer with one sentence.",
);
request.input.push(EpiphanyOpenAiInputItem::UserText {
text: "smoke".to_string(),
});
let mapped = responses_request_from_epiphany(request)?;
print_json(&json!({
"status": status,
"mappedModel": mapped.model,
"mappedInputItems": mapped.input.len(),
"transport": "not-opened"
}))?;
}
_ => {
return Err(anyhow!(
"usage: epiphany-openai-spine <status|model-turn|smoke> [--codex-home path] [--default-model model] [--request path]"
));
}
}
Ok(())
}
struct CommonOptions {
codex_home: PathBuf,
default_model: Option<String>,
}
struct ModelTurnOptions {
codex_home: PathBuf,
request_path: PathBuf,
}
fn parse_common_options(args: Vec<String>) -> Result<CommonOptions> {
let mut codex_home = default_codex_home()?;
let mut default_model = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--codex-home" => {
codex_home = PathBuf::from(next_value(&mut iter, "--codex-home")?);
}
"--default-model" => {
default_model = Some(next_value(&mut iter, "--default-model")?);
}
other => return Err(anyhow!("unknown status argument: {other}")),
}
}
Ok(CommonOptions {
codex_home,
default_model,
})
}
fn parse_model_turn_options(args: Vec<String>) -> Result<ModelTurnOptions> {
let mut codex_home = default_codex_home()?;
let mut request_path = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--codex-home" => {
codex_home = PathBuf::from(next_value(&mut iter, "--codex-home")?);
}
"--request" => {
request_path = Some(PathBuf::from(next_value(&mut iter, "--request")?));
}
other => return Err(anyhow!("unknown model-turn argument: {other}")),
}
}
Ok(ModelTurnOptions {
codex_home,
request_path: request_path.context("model-turn requires --request")?,
})
}
fn next_value(iter: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
iter.next()
.ok_or_else(|| anyhow!("{name} requires a value"))
}
fn default_codex_home() -> Result<PathBuf> {
if let Ok(path) = env::var("CODEX_HOME") {
return Ok(PathBuf::from(path));
}
let home = env::var("USERPROFILE")
.or_else(|_| env::var("HOME"))
.context("CODEX_HOME is unset and no home directory environment variable exists")?;
Ok(PathBuf::from(home).join(".codex"))
}
fn auth_manager(codex_home: PathBuf) -> Arc<AuthManager> {
AuthManager::shared(
codex_home,
/*enable_codex_api_key_env*/ true,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
)
}
fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}

View file

@ -141,5 +141,20 @@ contracts in its hello/schema catalog. This is the contract bridge the native
runtime should consume next. Do not add another JSON-RPC model endpoint and call
it progress.
The first native operator/debug edge now exists as the
`epiphany-openai-spine` binary in `epiphany-openai-codex-spine`. It can report
typed adapter status and consume a serialized `EpiphanyOpenAiModelRequest`
document for a model turn without going through Codex app-server JSON-RPC. This
is not the final CultNet daemon. It is a buildable extracted edge that proves
the spine can be called from Epiphany-owned code and gives the next cut a place
to route through while the old `thread/epiphany/*` model path is starved.
The bad news is useful: the current spine still drags in `codex-api` and its
large transport dependency stack. That is tolerable as a sealed compatibility
reliquary, but not as the final shape. The next purification should shrink the
surviving Codex dependency surface to credential loading/refresh, provider
auth, model selection, and the smallest OpenAI Responses HTTP call that keeps
Codex subscription compatibility.
The point is ownership: Epiphany calls a model adapter; it does not live inside
the Codex host brain.

View file

@ -567,6 +567,16 @@ plus the coordinator-owned model request contract. The native runtime should
consume that contract next; do not make a new JSON-RPC model endpoint and
pretend the whale got lighter.
The `epiphany-openai-spine` binary is now the first native edge for that
wrapper. It can print typed adapter status and consume a serialized
`EpiphanyOpenAiModelRequest` document for a model turn outside Codex
app-server. Treat its JSON as CLI/file-edge serialization of typed documents,
not internal data cargo. The current wrapper still pulls `codex-api` and a
large dependency stack, so the next real cut is not celebration; it is routing
native runtime/CultNet model calls through this spine and then shrinking the
surviving Codex dependency surface to auth, provider/model routing, and the
smallest OpenAI Responses call needed for subscription compatibility.
Also: MCP itself is allowed to be JSON. The target is not "replace MCP JSON";
the target is an Epiphany-owned boundary that speaks typed Epiphany
intent/result/receipt documents internally and normal MCP JSON-RPC externally.

Binary file not shown.

View file

@ -279,5 +279,5 @@ rejected_paths:
reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet.
current_status:
summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the pure native `epiphany-openai-adapter` document boundary, and a workspace-verified Codex auth/model wrapper in `epiphany-openai-codex-spine`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,071 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader. `epiphany-openai-adapter` defines typed request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. `epiphany-runtime-spine` now advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the next caller can be native instead of JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON.
next_action: Continue Codex starvation by making an Epiphany-native runtime surface consume the advertised OpenAI adapter CultNet contract and call the typed spine directly, then starve the old model-turn path out of `thread/epiphany/*` JSON-RPC and `codex_message_processor.rs`, without adding Codex app-server, apps, skills, marketplace, or Epiphany JSON-RPC dependencies. Do not resume app/skill/marketplace expansion.
summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the pure native `epiphany-openai-adapter` document boundary, and a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,071 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader. `epiphany-openai-adapter` defines typed request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. The `epiphany-openai-spine` binary is the first native edge: it prints typed status and can consume serialized typed request documents outside Codex app-server JSON-RPC. Treat that JSON as CLI/file-edge serialization, not internal authority. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the next caller can be native instead of JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after native routing exists.
next_action: Continue Codex starvation by routing an Epiphany-native runtime/CultNet model-call surface through the typed OpenAI spine, then starve the old model-turn path out of `thread/epiphany/*` JSON-RPC and `codex_message_processor.rs`. After routing is real, shrink the surviving Codex dependency surface to credential refresh, provider/model routing, and the smallest OpenAI Responses call required for Codex subscription compatibility. Do not add Codex app-server, apps, skills, marketplace, or new Epiphany JSON-RPC dependencies.

3
vendor/codex/codex-rs/Cargo.lock generated vendored
View file

@ -5275,6 +5275,9 @@ dependencies = [
"codex-protocol",
"epiphany-openai-adapter",
"futures",
"serde",
"serde_json",
"tokio",
]
[[package]]