[app-server] feat: export.rs supports a v2 namespace, initial v2 notifications (#6212)
**Typescript and JSON schema exports** While working on Thread/Turn/Items type definitions, I realize we will run into name conflicts between v1 and v2 APIs (e.g. `RateLimitWindow` which won't be reusable since v1 uses `RateLimitWindow` from `protocol/` which uses snake_case, but we want to expose camelCase everywhere, so we'll define a V2 version of that struct that serializes as camelCase). To set us up for a clean and isolated v2 API, generate types into a `v2/` namespace for both typescript and JSON schema. - TypeScript: v2 types emit under `out_dir/v2/*.ts`, and root index.ts now re-exports them via `export * as v2 from "./v2"`;. - JSON Schemas: v2 definitions bundle under `#/definitions/v2/*` rather than the root. The location for the original types (v1 and types pulled from `protocol/` and other core crates) haven't changed and are still at the root. This is for backwards compatibility: no breaking changes to existing usages of v1 APIs and types. **Notifications** While working on export.rs, I: - refactored server/client notifications with macros (like we already do for methods) so they also get exported (I noticed they weren't being exported at all). - removed the hardcoded list of types to export as JSON schema by leveraging the existing macros instead - and took a stab at API V2 notifications. These aren't wired up yet, and I expect to iterate on these this week.
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -871,6 +871,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
|
"mcp-types",
|
||||||
"paste",
|
"paste",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"schemars 0.8.22",
|
"schemars 0.8.22",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ workspace = true
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
|
mcp-types = { workspace = true }
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -2,20 +2,27 @@ use crate::ClientNotification;
|
|||||||
use crate::ClientRequest;
|
use crate::ClientRequest;
|
||||||
use crate::ServerNotification;
|
use crate::ServerNotification;
|
||||||
use crate::ServerRequest;
|
use crate::ServerRequest;
|
||||||
|
use crate::export_client_notification_schemas;
|
||||||
|
use crate::export_client_param_schemas;
|
||||||
use crate::export_client_response_schemas;
|
use crate::export_client_response_schemas;
|
||||||
use crate::export_client_responses;
|
use crate::export_client_responses;
|
||||||
|
use crate::export_server_notification_schemas;
|
||||||
|
use crate::export_server_param_schemas;
|
||||||
use crate::export_server_response_schemas;
|
use crate::export_server_response_schemas;
|
||||||
use crate::export_server_responses;
|
use crate::export_server_responses;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use codex_protocol::parse_command::ParsedCommand;
|
||||||
|
use codex_protocol::protocol::EventMsg;
|
||||||
|
use codex_protocol::protocol::FileChange;
|
||||||
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use schemars::schema::RootSchema;
|
|
||||||
use schemars::schema_for;
|
use schemars::schema_for;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Map;
|
use serde_json::Map;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -28,84 +35,29 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
|
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
|
||||||
|
|
||||||
macro_rules! for_each_schema_type {
|
#[derive(Clone)]
|
||||||
($macro:ident) => {
|
pub struct GeneratedSchema {
|
||||||
$macro!(crate::RequestId);
|
namespace: Option<String>,
|
||||||
$macro!(crate::JSONRPCMessage);
|
logical_name: String,
|
||||||
$macro!(crate::JSONRPCRequest);
|
value: Value,
|
||||||
$macro!(crate::JSONRPCNotification);
|
in_v1_dir: bool,
|
||||||
$macro!(crate::JSONRPCResponse);
|
|
||||||
$macro!(crate::JSONRPCError);
|
|
||||||
$macro!(crate::JSONRPCErrorError);
|
|
||||||
$macro!(crate::AddConversationListenerParams);
|
|
||||||
$macro!(crate::AddConversationSubscriptionResponse);
|
|
||||||
$macro!(crate::ApplyPatchApprovalParams);
|
|
||||||
$macro!(crate::ApplyPatchApprovalResponse);
|
|
||||||
$macro!(crate::ArchiveConversationParams);
|
|
||||||
$macro!(crate::ArchiveConversationResponse);
|
|
||||||
$macro!(crate::AuthMode);
|
|
||||||
$macro!(crate::AccountUpdatedNotification);
|
|
||||||
$macro!(crate::AuthStatusChangeNotification);
|
|
||||||
$macro!(crate::CancelLoginChatGptParams);
|
|
||||||
$macro!(crate::CancelLoginChatGptResponse);
|
|
||||||
$macro!(crate::ClientInfo);
|
|
||||||
$macro!(crate::ClientNotification);
|
|
||||||
$macro!(crate::ClientRequest);
|
|
||||||
$macro!(crate::ConversationSummary);
|
|
||||||
$macro!(crate::ExecCommandApprovalParams);
|
|
||||||
$macro!(crate::ExecCommandApprovalResponse);
|
|
||||||
$macro!(crate::ExecOneOffCommandParams);
|
|
||||||
$macro!(crate::ExecOneOffCommandResponse);
|
|
||||||
$macro!(crate::FuzzyFileSearchParams);
|
|
||||||
$macro!(crate::FuzzyFileSearchResponse);
|
|
||||||
$macro!(crate::FuzzyFileSearchResult);
|
|
||||||
$macro!(crate::GetAuthStatusParams);
|
|
||||||
$macro!(crate::GetAuthStatusResponse);
|
|
||||||
$macro!(crate::GetUserAgentResponse);
|
|
||||||
$macro!(crate::GetUserSavedConfigResponse);
|
|
||||||
$macro!(crate::GitDiffToRemoteParams);
|
|
||||||
$macro!(crate::GitDiffToRemoteResponse);
|
|
||||||
$macro!(crate::GitSha);
|
|
||||||
$macro!(crate::InitializeParams);
|
|
||||||
$macro!(crate::InitializeResponse);
|
|
||||||
$macro!(crate::InputItem);
|
|
||||||
$macro!(crate::InterruptConversationParams);
|
|
||||||
$macro!(crate::InterruptConversationResponse);
|
|
||||||
$macro!(crate::ListConversationsParams);
|
|
||||||
$macro!(crate::ListConversationsResponse);
|
|
||||||
$macro!(crate::LoginApiKeyParams);
|
|
||||||
$macro!(crate::LoginApiKeyResponse);
|
|
||||||
$macro!(crate::LoginChatGptCompleteNotification);
|
|
||||||
$macro!(crate::LoginChatGptResponse);
|
|
||||||
$macro!(crate::LogoutChatGptParams);
|
|
||||||
$macro!(crate::LogoutChatGptResponse);
|
|
||||||
$macro!(crate::NewConversationParams);
|
|
||||||
$macro!(crate::NewConversationResponse);
|
|
||||||
$macro!(crate::Profile);
|
|
||||||
$macro!(crate::RemoveConversationListenerParams);
|
|
||||||
$macro!(crate::RemoveConversationSubscriptionResponse);
|
|
||||||
$macro!(crate::ResumeConversationParams);
|
|
||||||
$macro!(crate::ResumeConversationResponse);
|
|
||||||
$macro!(crate::SandboxSettings);
|
|
||||||
$macro!(crate::SendUserMessageParams);
|
|
||||||
$macro!(crate::SendUserMessageResponse);
|
|
||||||
$macro!(crate::SendUserTurnParams);
|
|
||||||
$macro!(crate::SendUserTurnResponse);
|
|
||||||
$macro!(crate::ServerNotification);
|
|
||||||
$macro!(crate::ServerRequest);
|
|
||||||
$macro!(crate::SessionConfiguredNotification);
|
|
||||||
$macro!(crate::SetDefaultModelParams);
|
|
||||||
$macro!(crate::SetDefaultModelResponse);
|
|
||||||
$macro!(crate::Tools);
|
|
||||||
$macro!(crate::UserInfoResponse);
|
|
||||||
$macro!(crate::UserSavedConfig);
|
|
||||||
$macro!(codex_protocol::protocol::EventMsg);
|
|
||||||
$macro!(codex_protocol::protocol::FileChange);
|
|
||||||
$macro!(codex_protocol::parse_command::ParsedCommand);
|
|
||||||
$macro!(codex_protocol::protocol::SandboxPolicy);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GeneratedSchema {
|
||||||
|
fn namespace(&self) -> Option<&str> {
|
||||||
|
self.namespace.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logical_name(&self) -> &str {
|
||||||
|
&self.logical_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(&self) -> &Value {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonSchemaEmitter = fn(&Path) -> Result<GeneratedSchema>;
|
||||||
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
||||||
generate_ts(out_dir, prettier)?;
|
generate_ts(out_dir, prettier)?;
|
||||||
generate_json(out_dir)?;
|
generate_json(out_dir)?;
|
||||||
@@ -113,7 +65,9 @@ pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
||||||
|
let v2_out_dir = out_dir.join("v2");
|
||||||
ensure_dir(out_dir)?;
|
ensure_dir(out_dir)?;
|
||||||
|
ensure_dir(&v2_out_dir)?;
|
||||||
|
|
||||||
ClientRequest::export_all_to(out_dir)?;
|
ClientRequest::export_all_to(out_dir)?;
|
||||||
export_client_responses(out_dir)?;
|
export_client_responses(out_dir)?;
|
||||||
@@ -124,12 +78,15 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
|||||||
ServerNotification::export_all_to(out_dir)?;
|
ServerNotification::export_all_to(out_dir)?;
|
||||||
|
|
||||||
generate_index_ts(out_dir)?;
|
generate_index_ts(out_dir)?;
|
||||||
|
generate_index_ts(&v2_out_dir)?;
|
||||||
|
|
||||||
let ts_files = ts_files_in(out_dir)?;
|
// Ensure our header is present on all TS files (root + subdirs like v2/).
|
||||||
|
let ts_files = ts_files_in_recursive(out_dir)?;
|
||||||
for file in &ts_files {
|
for file in &ts_files {
|
||||||
prepend_header_if_missing(file)?;
|
prepend_header_if_missing(file)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionally run Prettier on all generated TS files.
|
||||||
if let Some(prettier_bin) = prettier
|
if let Some(prettier_bin) = prettier
|
||||||
&& !ts_files.is_empty()
|
&& !ts_files.is_empty()
|
||||||
{
|
{
|
||||||
@@ -148,23 +105,47 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
|||||||
|
|
||||||
pub fn generate_json(out_dir: &Path) -> Result<()> {
|
pub fn generate_json(out_dir: &Path) -> Result<()> {
|
||||||
ensure_dir(out_dir)?;
|
ensure_dir(out_dir)?;
|
||||||
let mut bundle: BTreeMap<String, RootSchema> = BTreeMap::new();
|
let envelope_emitters: &[JsonSchemaEmitter] = &[
|
||||||
|
|d| write_json_schema_with_return::<crate::RequestId>(d, "RequestId"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCMessage>(d, "JSONRPCMessage"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCRequest>(d, "JSONRPCRequest"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCNotification>(d, "JSONRPCNotification"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCResponse>(d, "JSONRPCResponse"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCError>(d, "JSONRPCError"),
|
||||||
|
|d| write_json_schema_with_return::<crate::JSONRPCErrorError>(d, "JSONRPCErrorError"),
|
||||||
|
|d| write_json_schema_with_return::<crate::ClientRequest>(d, "ClientRequest"),
|
||||||
|
|d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
|
||||||
|
|d| write_json_schema_with_return::<crate::ClientNotification>(d, "ClientNotification"),
|
||||||
|
|d| write_json_schema_with_return::<crate::ServerNotification>(d, "ServerNotification"),
|
||||||
|
|d| write_json_schema_with_return::<EventMsg>(d, "EventMsg"),
|
||||||
|
|d| write_json_schema_with_return::<FileChange>(d, "FileChange"),
|
||||||
|
|d| write_json_schema_with_return::<crate::protocol::v1::InputItem>(d, "InputItem"),
|
||||||
|
|d| write_json_schema_with_return::<ParsedCommand>(d, "ParsedCommand"),
|
||||||
|
|d| write_json_schema_with_return::<SandboxPolicy>(d, "SandboxPolicy"),
|
||||||
|
];
|
||||||
|
|
||||||
macro_rules! add_schema {
|
let mut schemas: Vec<GeneratedSchema> = Vec::new();
|
||||||
($ty:path) => {{
|
for emit in envelope_emitters {
|
||||||
let name = type_basename(stringify!($ty));
|
schemas.push(emit(out_dir)?);
|
||||||
let schema = write_json_schema_with_return::<$ty>(out_dir, &name)?;
|
|
||||||
bundle.insert(name, schema);
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for_each_schema_type!(add_schema);
|
schemas.extend(export_client_param_schemas(out_dir)?);
|
||||||
|
schemas.extend(export_client_response_schemas(out_dir)?);
|
||||||
|
schemas.extend(export_server_param_schemas(out_dir)?);
|
||||||
|
schemas.extend(export_server_response_schemas(out_dir)?);
|
||||||
|
schemas.extend(export_client_notification_schemas(out_dir)?);
|
||||||
|
schemas.extend(export_server_notification_schemas(out_dir)?);
|
||||||
|
|
||||||
export_client_response_schemas(out_dir)?;
|
let bundle = build_schema_bundle(schemas)?;
|
||||||
export_server_response_schemas(out_dir)?;
|
write_pretty_json(
|
||||||
|
out_dir.join("codex_app_server_protocol.schemas.json"),
|
||||||
|
&bundle,
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut definitions = Map::new();
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_schema_bundle(schemas: Vec<GeneratedSchema>) -> Result<Value> {
|
||||||
const SPECIAL_DEFINITIONS: &[&str] = &[
|
const SPECIAL_DEFINITIONS: &[&str] = &[
|
||||||
"ClientNotification",
|
"ClientNotification",
|
||||||
"ClientRequest",
|
"ClientRequest",
|
||||||
@@ -177,22 +158,62 @@ pub fn generate_json(out_dir: &Path) -> Result<()> {
|
|||||||
"ServerRequest",
|
"ServerRequest",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (name, schema) in bundle {
|
let namespaced_types = collect_namespaced_types(&schemas);
|
||||||
let mut schema_value = serde_json::to_value(schema)?;
|
let mut definitions = Map::new();
|
||||||
annotate_schema(&mut schema_value, Some(name.as_str()));
|
|
||||||
|
|
||||||
if let Value::Object(ref mut obj) = schema_value
|
for schema in schemas {
|
||||||
|
let GeneratedSchema {
|
||||||
|
namespace,
|
||||||
|
logical_name,
|
||||||
|
mut value,
|
||||||
|
in_v1_dir,
|
||||||
|
} = schema;
|
||||||
|
|
||||||
|
if let Some(ref ns) = namespace {
|
||||||
|
rewrite_refs_to_namespace(&mut value, ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut forced_namespace_refs: Vec<(String, String)> = Vec::new();
|
||||||
|
if let Value::Object(ref mut obj) = value
|
||||||
&& let Some(defs) = obj.remove("definitions")
|
&& let Some(defs) = obj.remove("definitions")
|
||||||
&& let Value::Object(defs_obj) = defs
|
&& let Value::Object(defs_obj) = defs
|
||||||
{
|
{
|
||||||
for (def_name, mut def_schema) in defs_obj {
|
for (def_name, mut def_schema) in defs_obj {
|
||||||
if !SPECIAL_DEFINITIONS.contains(&def_name.as_str()) {
|
if SPECIAL_DEFINITIONS.contains(&def_name.as_str()) {
|
||||||
annotate_schema(&mut def_schema, Some(def_name.as_str()));
|
continue;
|
||||||
|
}
|
||||||
|
annotate_schema(&mut def_schema, Some(def_name.as_str()));
|
||||||
|
let target_namespace = match namespace {
|
||||||
|
Some(ref ns) => Some(ns.clone()),
|
||||||
|
None => namespace_for_definition(&def_name, &namespaced_types)
|
||||||
|
.cloned()
|
||||||
|
.filter(|_| !in_v1_dir),
|
||||||
|
};
|
||||||
|
if let Some(ref ns) = target_namespace {
|
||||||
|
if namespace.as_deref() == Some(ns.as_str()) {
|
||||||
|
rewrite_refs_to_namespace(&mut def_schema, ns);
|
||||||
|
insert_into_namespace(&mut definitions, ns, def_name.clone(), def_schema)?;
|
||||||
|
} else if !forced_namespace_refs
|
||||||
|
.iter()
|
||||||
|
.any(|(name, existing_ns)| name == &def_name && existing_ns == ns)
|
||||||
|
{
|
||||||
|
forced_namespace_refs.push((def_name.clone(), ns.clone()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
definitions.insert(def_name, def_schema);
|
definitions.insert(def_name, def_schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
definitions.insert(name, schema_value);
|
|
||||||
|
for (name, ns) in forced_namespace_refs {
|
||||||
|
rewrite_named_ref_to_namespace(&mut value, &ns, &name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref ns) = namespace {
|
||||||
|
insert_into_namespace(&mut definitions, ns, logical_name.clone(), value)?;
|
||||||
|
} else {
|
||||||
|
definitions.insert(logical_name, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut root = Map::new();
|
let mut root = Map::new();
|
||||||
@@ -207,15 +228,28 @@ pub fn generate_json(out_dir: &Path) -> Result<()> {
|
|||||||
root.insert("type".to_string(), Value::String("object".into()));
|
root.insert("type".to_string(), Value::String("object".into()));
|
||||||
root.insert("definitions".to_string(), Value::Object(definitions));
|
root.insert("definitions".to_string(), Value::Object(definitions));
|
||||||
|
|
||||||
write_pretty_json(
|
Ok(Value::Object(root))
|
||||||
out_dir.join("codex_app_server_protocol.schemas.json"),
|
|
||||||
&Value::Object(root),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_json_schema_with_return<T>(out_dir: &Path, name: &str) -> Result<RootSchema>
|
fn insert_into_namespace(
|
||||||
|
definitions: &mut Map<String, Value>,
|
||||||
|
namespace: &str,
|
||||||
|
name: String,
|
||||||
|
schema: Value,
|
||||||
|
) -> Result<()> {
|
||||||
|
let entry = definitions
|
||||||
|
.entry(namespace.to_string())
|
||||||
|
.or_insert_with(|| Value::Object(Map::new()));
|
||||||
|
match entry {
|
||||||
|
Value::Object(map) => {
|
||||||
|
map.insert(name, schema);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("expected namespace {namespace} to be an object")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_json_schema_with_return<T>(out_dir: &Path, name: &str) -> Result<GeneratedSchema>
|
||||||
where
|
where
|
||||||
T: JsonSchema,
|
T: JsonSchema,
|
||||||
{
|
{
|
||||||
@@ -223,17 +257,37 @@ where
|
|||||||
let schema = schema_for!(T);
|
let schema = schema_for!(T);
|
||||||
let mut schema_value = serde_json::to_value(schema)?;
|
let mut schema_value = serde_json::to_value(schema)?;
|
||||||
annotate_schema(&mut schema_value, Some(file_stem));
|
annotate_schema(&mut schema_value, Some(file_stem));
|
||||||
write_pretty_json(out_dir.join(format!("{file_stem}.json")), &schema_value)
|
// If the name looks like a namespaced path (e.g., "v2::Type"), mirror
|
||||||
|
// the TypeScript layout and write to out_dir/v2/Type.json. Otherwise
|
||||||
|
// write alongside the legacy files.
|
||||||
|
let (raw_namespace, logical_name) = split_namespace(file_stem);
|
||||||
|
let out_path = if let Some(ns) = raw_namespace {
|
||||||
|
let dir = out_dir.join(ns);
|
||||||
|
ensure_dir(&dir)?;
|
||||||
|
dir.join(format!("{logical_name}.json"))
|
||||||
|
} else {
|
||||||
|
out_dir.join(format!("{file_stem}.json"))
|
||||||
|
};
|
||||||
|
|
||||||
|
write_pretty_json(out_path, &schema_value)
|
||||||
.with_context(|| format!("Failed to write JSON schema for {file_stem}"))?;
|
.with_context(|| format!("Failed to write JSON schema for {file_stem}"))?;
|
||||||
let annotated_schema = serde_json::from_value(schema_value)?;
|
let namespace = match raw_namespace {
|
||||||
Ok(annotated_schema)
|
Some("v1") | None => None,
|
||||||
|
Some(ns) => Some(ns.to_string()),
|
||||||
|
};
|
||||||
|
Ok(GeneratedSchema {
|
||||||
|
in_v1_dir: raw_namespace == Some("v1"),
|
||||||
|
namespace,
|
||||||
|
logical_name: logical_name.to_string(),
|
||||||
|
value: schema_value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write_json_schema<T>(out_dir: &Path, name: &str) -> Result<()>
|
pub(crate) fn write_json_schema<T>(out_dir: &Path, name: &str) -> Result<GeneratedSchema>
|
||||||
where
|
where
|
||||||
T: JsonSchema,
|
T: JsonSchema,
|
||||||
{
|
{
|
||||||
write_json_schema_with_return::<T>(out_dir, name).map(|_| ())
|
write_json_schema_with_return::<T>(out_dir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_pretty_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
|
fn write_pretty_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
|
||||||
@@ -242,13 +296,73 @@ fn write_pretty_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
|
|||||||
fs::write(&path, json).with_context(|| format!("Failed to write {}", path.display()))?;
|
fs::write(&path, json).with_context(|| format!("Failed to write {}", path.display()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn type_basename(type_path: &str) -> String {
|
|
||||||
type_path
|
/// Split a fully-qualified type name like "v2::Type" into its namespace and logical name.
|
||||||
.rsplit_once("::")
|
fn split_namespace(name: &str) -> (Option<&str>, &str) {
|
||||||
.map(|(_, name)| name)
|
name.split_once("::")
|
||||||
.unwrap_or(type_path)
|
.map_or((None, name), |(ns, rest)| (Some(ns), rest))
|
||||||
.trim()
|
}
|
||||||
.to_string()
|
|
||||||
|
/// Recursively rewrite $ref values that point at "#/definitions/..." so that
|
||||||
|
/// they point to a namespaced location under the bundle.
|
||||||
|
fn rewrite_refs_to_namespace(value: &mut Value, ns: &str) {
|
||||||
|
match value {
|
||||||
|
Value::Object(obj) => {
|
||||||
|
if let Some(Value::String(r)) = obj.get_mut("$ref")
|
||||||
|
&& let Some(suffix) = r.strip_prefix("#/definitions/")
|
||||||
|
{
|
||||||
|
let prefix = format!("{ns}/");
|
||||||
|
if !suffix.starts_with(&prefix) {
|
||||||
|
*r = format!("#/definitions/{ns}/{suffix}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for v in obj.values_mut() {
|
||||||
|
rewrite_refs_to_namespace(v, ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(items) => {
|
||||||
|
for v in items.iter_mut() {
|
||||||
|
rewrite_refs_to_namespace(v, ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_namespaced_types(schemas: &[GeneratedSchema]) -> HashMap<String, String> {
|
||||||
|
let mut types = HashMap::new();
|
||||||
|
for schema in schemas {
|
||||||
|
if let Some(ns) = schema.namespace() {
|
||||||
|
types
|
||||||
|
.entry(schema.logical_name().to_string())
|
||||||
|
.or_insert_with(|| ns.to_string());
|
||||||
|
if let Some(Value::Object(defs)) = schema.value().get("definitions") {
|
||||||
|
for key in defs.keys() {
|
||||||
|
types.entry(key.clone()).or_insert_with(|| ns.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(Value::Object(defs)) = schema.value().get("$defs") {
|
||||||
|
for key in defs.keys() {
|
||||||
|
types.entry(key.clone()).or_insert_with(|| ns.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
types
|
||||||
|
}
|
||||||
|
|
||||||
|
fn namespace_for_definition<'a>(
|
||||||
|
name: &str,
|
||||||
|
types: &'a HashMap<String, String>,
|
||||||
|
) -> Option<&'a String> {
|
||||||
|
if let Some(ns) = types.get(name) {
|
||||||
|
return Some(ns);
|
||||||
|
}
|
||||||
|
let trimmed = name.trim_end_matches(|c: char| c.is_ascii_digit());
|
||||||
|
if trimmed != name {
|
||||||
|
return types.get(trimmed);
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn variant_definition_name(base: &str, variant: &Value) -> Option<String> {
|
fn variant_definition_name(base: &str, variant: &Value) -> Option<String> {
|
||||||
@@ -468,6 +582,33 @@ fn ensure_dir(dir: &Path) -> Result<()> {
|
|||||||
.with_context(|| format!("Failed to create output directory {}", dir.display()))
|
.with_context(|| format!("Failed to create output directory {}", dir.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rewrite_named_ref_to_namespace(value: &mut Value, ns: &str, name: &str) {
|
||||||
|
let direct = format!("#/definitions/{name}");
|
||||||
|
let prefixed = format!("{direct}/");
|
||||||
|
let replacement = format!("#/definitions/{ns}/{name}");
|
||||||
|
let replacement_prefixed = format!("{replacement}/");
|
||||||
|
match value {
|
||||||
|
Value::Object(obj) => {
|
||||||
|
if let Some(Value::String(reference)) = obj.get_mut("$ref") {
|
||||||
|
if reference == &direct {
|
||||||
|
*reference = replacement;
|
||||||
|
} else if let Some(rest) = reference.strip_prefix(&prefixed) {
|
||||||
|
*reference = format!("{replacement_prefixed}{rest}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for child in obj.values_mut() {
|
||||||
|
rewrite_named_ref_to_namespace(child, ns, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(items) => {
|
||||||
|
for child in items {
|
||||||
|
rewrite_named_ref_to_namespace(child, ns, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prepend_header_if_missing(path: &Path) -> Result<()> {
|
fn prepend_header_if_missing(path: &Path) -> Result<()> {
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
{
|
{
|
||||||
@@ -505,6 +646,26 @@ fn ts_files_in(dir: &Path) -> Result<Vec<PathBuf>> {
|
|||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ts_files_in_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut stack = vec![dir.to_path_buf()];
|
||||||
|
while let Some(d) = stack.pop() {
|
||||||
|
for entry in
|
||||||
|
fs::read_dir(&d).with_context(|| format!("Failed to read dir {}", d.display()))?
|
||||||
|
{
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
} else if path.is_file() && path.extension() == Some(OsStr::new("ts")) {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
|
fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
|
||||||
let mut entries: Vec<String> = Vec::new();
|
let mut entries: Vec<String> = Vec::new();
|
||||||
let mut stems: Vec<String> = ts_files_in(out_dir)?
|
let mut stems: Vec<String> = ts_files_in(out_dir)?
|
||||||
@@ -521,6 +682,14 @@ fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {
|
|||||||
entries.push(format!("export type {{ {name} }} from \"./{name}\";\n"));
|
entries.push(format!("export type {{ {name} }} from \"./{name}\";\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is the root out_dir and a ./v2 folder exists with TS files,
|
||||||
|
// expose it as a namespace to avoid symbol collisions at the root.
|
||||||
|
let v2_dir = out_dir.join("v2");
|
||||||
|
let has_v2_ts = ts_files_in(&v2_dir).map(|v| !v.is_empty()).unwrap_or(false);
|
||||||
|
if has_v2_ts {
|
||||||
|
entries.push("export * as v2 from \"./v2\";\n".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let mut content =
|
let mut content =
|
||||||
String::with_capacity(HEADER.len() + entries.iter().map(String::len).sum::<usize>());
|
String::with_capacity(HEADER.len() + entries.iter().map(String::len).sum::<usize>());
|
||||||
content.push_str(HEADER);
|
content.push_str(HEADER);
|
||||||
@@ -547,6 +716,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_ts_has_no_optional_nullable_fields() -> Result<()> {
|
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.
|
||||||
let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7()));
|
let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7()));
|
||||||
fs::create_dir(&output_dir)?;
|
fs::create_dir(&output_dir)?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ use std::path::PathBuf;
|
|||||||
use crate::JSONRPCNotification;
|
use crate::JSONRPCNotification;
|
||||||
use crate::JSONRPCRequest;
|
use crate::JSONRPCRequest;
|
||||||
use crate::RequestId;
|
use crate::RequestId;
|
||||||
|
use crate::export::GeneratedSchema;
|
||||||
|
use crate::export::write_json_schema;
|
||||||
use crate::protocol::v1;
|
use crate::protocol::v1;
|
||||||
use crate::protocol::v2;
|
use crate::protocol::v2;
|
||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
use codex_protocol::parse_command::ParsedCommand;
|
use codex_protocol::parse_command::ParsedCommand;
|
||||||
use codex_protocol::protocol::FileChange;
|
use codex_protocol::protocol::FileChange;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
|
||||||
use codex_protocol::protocol::ReviewDecision;
|
use codex_protocol::protocol::ReviewDecision;
|
||||||
use codex_protocol::protocol::SandboxCommandAssessment;
|
use codex_protocol::protocol::SandboxCommandAssessment;
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
@@ -74,13 +75,26 @@ macro_rules! client_request_definitions {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_init_then_push)]
|
||||||
pub fn export_client_response_schemas(
|
pub fn export_client_response_schemas(
|
||||||
out_dir: &::std::path::Path,
|
out_dir: &::std::path::Path,
|
||||||
) -> ::anyhow::Result<()> {
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let mut schemas = Vec::new();
|
||||||
$(
|
$(
|
||||||
crate::export::write_json_schema::<$response>(out_dir, stringify!($response))?;
|
schemas.push(write_json_schema::<$response>(out_dir, stringify!($response))?);
|
||||||
)*
|
)*
|
||||||
Ok(())
|
Ok(schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_init_then_push)]
|
||||||
|
pub fn export_client_param_schemas(
|
||||||
|
out_dir: &::std::path::Path,
|
||||||
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let mut schemas = Vec::new();
|
||||||
|
$(
|
||||||
|
schemas.push(write_json_schema::<$params>(out_dir, stringify!($params))?);
|
||||||
|
)*
|
||||||
|
Ok(schemas)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -276,13 +290,101 @@ macro_rules! server_request_definitions {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_init_then_push)]
|
||||||
pub fn export_server_response_schemas(
|
pub fn export_server_response_schemas(
|
||||||
out_dir: &::std::path::Path,
|
out_dir: &::std::path::Path,
|
||||||
) -> ::anyhow::Result<()> {
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let mut schemas = Vec::new();
|
||||||
paste! {
|
paste! {
|
||||||
$(crate::export::write_json_schema::<[<$variant Response>]>(out_dir, stringify!([<$variant Response>]))?;)*
|
$(schemas.push(crate::export::write_json_schema::<[<$variant Response>]>(out_dir, stringify!([<$variant Response>]))?);)*
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_init_then_push)]
|
||||||
|
pub fn export_server_param_schemas(
|
||||||
|
out_dir: &::std::path::Path,
|
||||||
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let mut schemas = Vec::new();
|
||||||
|
paste! {
|
||||||
|
$(schemas.push(crate::export::write_json_schema::<[<$variant Params>]>(out_dir, stringify!([<$variant Params>]))?);)*
|
||||||
|
}
|
||||||
|
Ok(schemas)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates `ServerNotification` enum and helpers, including a JSON Schema
|
||||||
|
/// exporter for each notification.
|
||||||
|
macro_rules! server_notification_definitions {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta:meta])*
|
||||||
|
$variant:ident $(=> $wire:literal)? ( $payload:ty )
|
||||||
|
),* $(,)?
|
||||||
|
) => {
|
||||||
|
/// Notification sent from the server to the client.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
|
||||||
|
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum ServerNotification {
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta])*
|
||||||
|
$(#[serde(rename = $wire)] #[ts(rename = $wire)] #[strum(serialize = $wire)])?
|
||||||
|
$variant($payload),
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerNotification {
|
||||||
|
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
|
||||||
|
match self {
|
||||||
|
$(Self::$variant(params) => serde_json::to_value(params),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<JSONRPCNotification> for ServerNotification {
|
||||||
|
type Error = serde_json::Error;
|
||||||
|
|
||||||
|
fn try_from(value: JSONRPCNotification) -> Result<Self, Self::Error> {
|
||||||
|
serde_json::from_value(serde_json::to_value(value)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_init_then_push)]
|
||||||
|
pub fn export_server_notification_schemas(
|
||||||
|
out_dir: &::std::path::Path,
|
||||||
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let mut schemas = Vec::new();
|
||||||
|
$(schemas.push(crate::export::write_json_schema::<$payload>(out_dir, stringify!($payload))?);)*
|
||||||
|
Ok(schemas)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/// Notifications sent from the client to the server.
|
||||||
|
macro_rules! client_notification_definitions {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta:meta])*
|
||||||
|
$variant:ident $( ( $payload:ty ) )?
|
||||||
|
),* $(,)?
|
||||||
|
) => {
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
|
||||||
|
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum ClientNotification {
|
||||||
|
$(
|
||||||
|
$(#[$variant_meta])*
|
||||||
|
$variant $( ( $payload ) )?,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_client_notification_schemas(
|
||||||
|
_out_dir: &::std::path::Path,
|
||||||
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
|
let schemas = Vec::new();
|
||||||
|
$( $(schemas.push(crate::export::write_json_schema::<$payload>(_out_dir, stringify!($payload))?);)? )*
|
||||||
|
Ok(schemas)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -366,58 +468,26 @@ pub struct FuzzyFileSearchResponse {
|
|||||||
pub files: Vec<FuzzyFileSearchResult>,
|
pub files: Vec<FuzzyFileSearchResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notification sent from the server to the client.
|
server_notification_definitions! {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
|
|
||||||
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
|
||||||
#[strum(serialize_all = "camelCase")]
|
|
||||||
pub enum ServerNotification {
|
|
||||||
/// NEW NOTIFICATIONS
|
/// NEW NOTIFICATIONS
|
||||||
#[serde(rename = "account/updated")]
|
ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
|
||||||
#[ts(rename = "account/updated")]
|
TurnStarted => "turn/started" (v2::TurnStartedNotification),
|
||||||
#[strum(serialize = "account/updated")]
|
TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
|
||||||
AccountUpdated(v2::AccountUpdatedNotification),
|
ItemStarted => "item/started" (v2::ItemStartedNotification),
|
||||||
|
ItemCompleted => "item/completed" (v2::ItemCompletedNotification),
|
||||||
#[serde(rename = "account/rateLimits/updated")]
|
AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification),
|
||||||
#[ts(rename = "account/rateLimits/updated")]
|
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
|
||||||
#[strum(serialize = "account/rateLimits/updated")]
|
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
|
||||||
AccountRateLimitsUpdated(RateLimitSnapshot),
|
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
||||||
|
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
|
||||||
|
|
||||||
/// DEPRECATED NOTIFICATIONS below
|
/// DEPRECATED NOTIFICATIONS below
|
||||||
/// Authentication status changed
|
|
||||||
AuthStatusChange(v1::AuthStatusChangeNotification),
|
AuthStatusChange(v1::AuthStatusChangeNotification),
|
||||||
|
|
||||||
/// ChatGPT login flow completed
|
|
||||||
LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
|
LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
|
||||||
|
|
||||||
/// The special session configured event for a new or resumed conversation.
|
|
||||||
SessionConfigured(v1::SessionConfiguredNotification),
|
SessionConfigured(v1::SessionConfiguredNotification),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerNotification {
|
client_notification_definitions! {
|
||||||
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
|
|
||||||
match self {
|
|
||||||
ServerNotification::AccountUpdated(params) => serde_json::to_value(params),
|
|
||||||
ServerNotification::AccountRateLimitsUpdated(params) => serde_json::to_value(params),
|
|
||||||
ServerNotification::AuthStatusChange(params) => serde_json::to_value(params),
|
|
||||||
ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params),
|
|
||||||
ServerNotification::SessionConfigured(params) => serde_json::to_value(params),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<JSONRPCNotification> for ServerNotification {
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn try_from(value: JSONRPCNotification) -> Result<Self, Self::Error> {
|
|
||||||
serde_json::from_value(serde_json::to_value(value)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notification sent from the client to the server.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
|
|
||||||
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
|
||||||
#[strum(serialize_all = "camelCase")]
|
|
||||||
pub enum ClientNotification {
|
|
||||||
Initialized,
|
Initialized,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ use crate::protocol::common::AuthMode;
|
|||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
use codex_protocol::account::PlanType;
|
use codex_protocol::account::PlanType;
|
||||||
use codex_protocol::config_types::ReasoningEffort;
|
use codex_protocol::config_types::ReasoningEffort;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||||
|
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
|
||||||
|
use mcp_types::ContentBlock as McpContentBlock;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::path::PathBuf;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
#[ts(tag = "type")]
|
#[ts(tag = "type")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub enum Account {
|
pub enum Account {
|
||||||
#[serde(rename = "apiKey", rename_all = "camelCase")]
|
#[serde(rename = "apiKey", rename_all = "camelCase")]
|
||||||
#[ts(rename = "apiKey", rename_all = "camelCase")]
|
#[ts(rename = "apiKey", rename_all = "camelCase")]
|
||||||
@@ -28,6 +33,7 @@ pub enum Account {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
#[ts(tag = "type")]
|
#[ts(tag = "type")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub enum LoginAccountParams {
|
pub enum LoginAccountParams {
|
||||||
#[serde(rename = "apiKey")]
|
#[serde(rename = "apiKey")]
|
||||||
#[ts(rename = "apiKey")]
|
#[ts(rename = "apiKey")]
|
||||||
@@ -43,6 +49,7 @@ pub enum LoginAccountParams {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct LoginAccountResponse {
|
pub struct LoginAccountResponse {
|
||||||
/// Only set if the login method is ChatGPT.
|
/// Only set if the login method is ChatGPT.
|
||||||
#[schemars(with = "String")]
|
#[schemars(with = "String")]
|
||||||
@@ -55,22 +62,26 @@ pub struct LoginAccountResponse {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct LogoutAccountResponse {}
|
pub struct LogoutAccountResponse {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct GetAccountRateLimitsResponse {
|
pub struct GetAccountRateLimitsResponse {
|
||||||
pub rate_limits: RateLimitSnapshot,
|
pub rate_limits: RateLimitSnapshot,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct GetAccountResponse {
|
pub struct GetAccountResponse {
|
||||||
pub account: Account,
|
pub account: Account,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ListModelsParams {
|
pub struct ListModelsParams {
|
||||||
/// Optional page size; defaults to a reasonable server-side value.
|
/// Optional page size; defaults to a reasonable server-side value.
|
||||||
pub page_size: Option<usize>,
|
pub page_size: Option<usize>,
|
||||||
@@ -80,6 +91,7 @@ pub struct ListModelsParams {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
@@ -93,6 +105,7 @@ pub struct Model {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ReasoningEffortOption {
|
pub struct ReasoningEffortOption {
|
||||||
pub reasoning_effort: ReasoningEffort,
|
pub reasoning_effort: ReasoningEffort,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@@ -100,6 +113,7 @@ pub struct ReasoningEffortOption {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ListModelsResponse {
|
pub struct ListModelsResponse {
|
||||||
pub items: Vec<Model>,
|
pub items: Vec<Model>,
|
||||||
/// Opaque cursor to pass to the next call to continue after the last item.
|
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||||
@@ -109,6 +123,7 @@ pub struct ListModelsResponse {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct UploadFeedbackParams {
|
pub struct UploadFeedbackParams {
|
||||||
pub classification: String,
|
pub classification: String,
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
@@ -118,12 +133,295 @@ pub struct UploadFeedbackParams {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct UploadFeedbackResponse {
|
pub struct UploadFeedbackResponse {
|
||||||
pub thread_id: String,
|
pub thread_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
pub struct AccountUpdatedNotification {
|
pub struct AccountUpdatedNotification {
|
||||||
pub auth_method: Option<AuthMode>,
|
pub auth_method: Option<AuthMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct Thread {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct Turn {
|
||||||
|
pub id: String,
|
||||||
|
pub items: Vec<ThreadItem>,
|
||||||
|
pub status: TurnStatus,
|
||||||
|
pub error: Option<TurnError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum TurnStatus {
|
||||||
|
Completed,
|
||||||
|
Interrupted,
|
||||||
|
Failed,
|
||||||
|
InProgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct TurnError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum UserInput {
|
||||||
|
Text { text: String },
|
||||||
|
Image { url: String },
|
||||||
|
LocalImage { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum ThreadItem {
|
||||||
|
UserMessage {
|
||||||
|
id: String,
|
||||||
|
content: Vec<UserInput>,
|
||||||
|
},
|
||||||
|
AgentMessage {
|
||||||
|
id: String,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Reasoning {
|
||||||
|
id: String,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
CommandExecution {
|
||||||
|
id: String,
|
||||||
|
command: String,
|
||||||
|
aggregated_output: String,
|
||||||
|
exit_code: Option<i32>,
|
||||||
|
status: CommandExecutionStatus,
|
||||||
|
duration_ms: Option<i64>,
|
||||||
|
},
|
||||||
|
FileChange {
|
||||||
|
id: String,
|
||||||
|
changes: Vec<FileUpdateChange>,
|
||||||
|
status: PatchApplyStatus,
|
||||||
|
},
|
||||||
|
McpToolCall {
|
||||||
|
id: String,
|
||||||
|
server: String,
|
||||||
|
tool: String,
|
||||||
|
status: McpToolCallStatus,
|
||||||
|
arguments: JsonValue,
|
||||||
|
result: Option<McpToolCallResult>,
|
||||||
|
error: Option<McpToolCallError>,
|
||||||
|
},
|
||||||
|
WebSearch {
|
||||||
|
id: String,
|
||||||
|
query: String,
|
||||||
|
},
|
||||||
|
TodoList {
|
||||||
|
id: String,
|
||||||
|
items: Vec<TodoItem>,
|
||||||
|
},
|
||||||
|
ImageView {
|
||||||
|
id: String,
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
CodeReview {
|
||||||
|
id: String,
|
||||||
|
review: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum CommandExecutionStatus {
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct FileUpdateChange {
|
||||||
|
pub path: String,
|
||||||
|
pub kind: PatchChangeKind,
|
||||||
|
pub diff: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum PatchChangeKind {
|
||||||
|
Add,
|
||||||
|
Delete,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum PatchApplyStatus {
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum McpToolCallStatus {
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct McpToolCallResult {
|
||||||
|
pub content: Vec<McpContentBlock>,
|
||||||
|
pub structured_content: JsonValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct McpToolCallError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct TodoItem {
|
||||||
|
pub id: String,
|
||||||
|
pub text: String,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Server Notifications ===
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadStartedNotification {
|
||||||
|
pub thread: Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct TurnStartedNotification {
|
||||||
|
pub turn: Turn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct Usage {
|
||||||
|
pub input_tokens: i32,
|
||||||
|
pub cached_input_tokens: i32,
|
||||||
|
pub output_tokens: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct TurnCompletedNotification {
|
||||||
|
pub turn: Turn,
|
||||||
|
pub usage: Usage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ItemStartedNotification {
|
||||||
|
pub item: ThreadItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ItemCompletedNotification {
|
||||||
|
pub item: ThreadItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct AgentMessageDeltaNotification {
|
||||||
|
pub item_id: String,
|
||||||
|
pub delta: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct CommandExecutionOutputDeltaNotification {
|
||||||
|
pub item_id: String,
|
||||||
|
pub delta: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct McpToolCallProgressNotification {
|
||||||
|
pub item_id: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct AccountRateLimitsUpdatedNotification {
|
||||||
|
pub rate_limits: RateLimitSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct RateLimitSnapshot {
|
||||||
|
pub primary: Option<RateLimitWindow>,
|
||||||
|
pub secondary: Option<RateLimitWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
|
||||||
|
fn from(value: CoreRateLimitSnapshot) -> Self {
|
||||||
|
Self {
|
||||||
|
primary: value.primary.map(RateLimitWindow::from),
|
||||||
|
secondary: value.secondary.map(RateLimitWindow::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct RateLimitWindow {
|
||||||
|
pub used_percent: i32,
|
||||||
|
pub window_duration_mins: Option<i64>,
|
||||||
|
pub resets_at: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CoreRateLimitWindow> for RateLimitWindow {
|
||||||
|
fn from(value: CoreRateLimitWindow) -> Self {
|
||||||
|
Self {
|
||||||
|
used_percent: value.used_percent.round() as i32,
|
||||||
|
window_duration_mins: value.window_minutes,
|
||||||
|
resets_at: value.resets_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::fuzzy_file_search::run_fuzzy_file_search;
|
|||||||
use crate::models::supported_models;
|
use crate::models::supported_models;
|
||||||
use crate::outgoing_message::OutgoingMessageSender;
|
use crate::outgoing_message::OutgoingMessageSender;
|
||||||
use crate::outgoing_message::OutgoingNotification;
|
use crate::outgoing_message::OutgoingNotification;
|
||||||
|
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||||
use codex_app_server_protocol::AddConversationListenerParams;
|
use codex_app_server_protocol::AddConversationListenerParams;
|
||||||
use codex_app_server_protocol::AddConversationSubscriptionResponse;
|
use codex_app_server_protocol::AddConversationSubscriptionResponse;
|
||||||
@@ -625,7 +626,9 @@ impl CodexMessageProcessor {
|
|||||||
async fn get_account_rate_limits(&self, request_id: RequestId) {
|
async fn get_account_rate_limits(&self, request_id: RequestId) {
|
||||||
match self.fetch_account_rate_limits().await {
|
match self.fetch_account_rate_limits().await {
|
||||||
Ok(rate_limits) => {
|
Ok(rate_limits) => {
|
||||||
let response = GetAccountRateLimitsResponse { rate_limits };
|
let response = GetAccountRateLimitsResponse {
|
||||||
|
rate_limits: rate_limits.into(),
|
||||||
|
};
|
||||||
self.outgoing.send_response(request_id, response).await;
|
self.outgoing.send_response(request_id, response).await;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -1766,7 +1769,9 @@ async fn apply_bespoke_event_handling(
|
|||||||
if let Some(rate_limits) = token_count_event.rate_limits {
|
if let Some(rate_limits) = token_count_event.rate_limits {
|
||||||
outgoing
|
outgoing
|
||||||
.send_server_notification(ServerNotification::AccountRateLimitsUpdated(
|
.send_server_notification(ServerNotification::AccountRateLimitsUpdated(
|
||||||
rate_limits,
|
AccountRateLimitsUpdatedNotification {
|
||||||
|
rate_limits: rate_limits.into(),
|
||||||
|
},
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,11 +141,12 @@ pub(crate) struct OutgoingError {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||||
use codex_app_server_protocol::AuthMode;
|
use codex_app_server_protocol::AuthMode;
|
||||||
use codex_app_server_protocol::LoginChatGptCompleteNotification;
|
use codex_app_server_protocol::LoginChatGptCompleteNotification;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
use codex_app_server_protocol::RateLimitSnapshot;
|
||||||
use codex_protocol::protocol::RateLimitWindow;
|
use codex_app_server_protocol::RateLimitWindow;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -179,26 +180,31 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_account_rate_limits_notification_serialization() {
|
fn verify_account_rate_limits_notification_serialization() {
|
||||||
let notification = ServerNotification::AccountRateLimitsUpdated(RateLimitSnapshot {
|
let notification =
|
||||||
primary: Some(RateLimitWindow {
|
ServerNotification::AccountRateLimitsUpdated(AccountRateLimitsUpdatedNotification {
|
||||||
used_percent: 25.0,
|
rate_limits: RateLimitSnapshot {
|
||||||
window_minutes: Some(15),
|
primary: Some(RateLimitWindow {
|
||||||
resets_at: Some(123),
|
used_percent: 25,
|
||||||
}),
|
window_duration_mins: Some(15),
|
||||||
secondary: None,
|
resets_at: Some(123),
|
||||||
});
|
}),
|
||||||
|
secondary: None,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
json!({
|
json!({
|
||||||
"method": "account/rateLimits/updated",
|
"method": "account/rateLimits/updated",
|
||||||
"params": {
|
"params": {
|
||||||
"primary": {
|
"rateLimits": {
|
||||||
"used_percent": 25.0,
|
"primary": {
|
||||||
"window_minutes": 15,
|
"usedPercent": 25,
|
||||||
"resets_at": 123,
|
"windowDurationMins": 15,
|
||||||
},
|
"resetsAt": 123
|
||||||
"secondary": null,
|
},
|
||||||
|
"secondary": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
serde_json::to_value(jsonrpc_notification)
|
serde_json::to_value(jsonrpc_notification)
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
|||||||
use codex_app_server_protocol::JSONRPCError;
|
use codex_app_server_protocol::JSONRPCError;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
use codex_app_server_protocol::LoginApiKeyParams;
|
use codex_app_server_protocol::LoginApiKeyParams;
|
||||||
|
use codex_app_server_protocol::RateLimitSnapshot;
|
||||||
|
use codex_app_server_protocol::RateLimitWindow;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_core::auth::AuthCredentialsStoreMode;
|
use codex_core::auth::AuthCredentialsStoreMode;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
|
||||||
use codex_protocol::protocol::RateLimitWindow;
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -143,13 +143,13 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
|||||||
let expected = GetAccountRateLimitsResponse {
|
let expected = GetAccountRateLimitsResponse {
|
||||||
rate_limits: RateLimitSnapshot {
|
rate_limits: RateLimitSnapshot {
|
||||||
primary: Some(RateLimitWindow {
|
primary: Some(RateLimitWindow {
|
||||||
used_percent: 42.0,
|
used_percent: 42,
|
||||||
window_minutes: Some(60),
|
window_duration_mins: Some(60),
|
||||||
resets_at: Some(primary_reset_timestamp),
|
resets_at: Some(primary_reset_timestamp),
|
||||||
}),
|
}),
|
||||||
secondary: Some(RateLimitWindow {
|
secondary: Some(RateLimitWindow {
|
||||||
used_percent: 5.0,
|
used_percent: 5,
|
||||||
window_minutes: Some(1440),
|
window_duration_mins: Some(1440),
|
||||||
resets_at: Some(secondary_reset_timestamp),
|
resets_at: Some(secondary_reset_timestamp),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user