### Summary Added `mcp/servers/list` which is equivalent to `/mcp` slash command in CLI for response. This will be used in VSCE MCP settings to show log in status, available tools etc.
826 lines
27 KiB
Rust
826 lines
27 KiB
Rust
use std::path::Path;
|
|
|
|
use crate::JSONRPCNotification;
|
|
use crate::JSONRPCRequest;
|
|
use crate::RequestId;
|
|
use crate::export::GeneratedSchema;
|
|
use crate::export::write_json_schema;
|
|
use crate::protocol::v1;
|
|
use crate::protocol::v2;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use strum_macros::Display;
|
|
use ts_rs::TS;
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
|
|
#[ts(type = "string")]
|
|
pub struct GitSha(pub String);
|
|
|
|
impl GitSha {
|
|
pub fn new(sha: &str) -> Self {
|
|
Self(sha.to_string())
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum AuthMode {
|
|
ApiKey,
|
|
ChatGPT,
|
|
}
|
|
|
|
/// Generates an `enum ClientRequest` where each variant is a request that the
|
|
/// client can send to the server. Each variant has associated `params` and
|
|
/// `response` types. Also generates a `export_client_responses()` function to
|
|
/// export all response types to TypeScript.
|
|
macro_rules! client_request_definitions {
|
|
(
|
|
$(
|
|
$(#[$variant_meta:meta])*
|
|
$variant:ident $(=> $wire:literal)? {
|
|
params: $(#[$params_meta:meta])* $params:ty,
|
|
response: $response:ty,
|
|
}
|
|
),* $(,)?
|
|
) => {
|
|
/// Request from the client to the server.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(tag = "method", rename_all = "camelCase")]
|
|
pub enum ClientRequest {
|
|
$(
|
|
$(#[$variant_meta])*
|
|
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
|
|
$variant {
|
|
#[serde(rename = "id")]
|
|
request_id: RequestId,
|
|
$(#[$params_meta])*
|
|
params: $params,
|
|
},
|
|
)*
|
|
}
|
|
|
|
pub fn export_client_responses(
|
|
out_dir: &::std::path::Path,
|
|
) -> ::std::result::Result<(), ::ts_rs::ExportError> {
|
|
$(
|
|
<$response as ::ts_rs::TS>::export_all_to(out_dir)?;
|
|
)*
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::vec_init_then_push)]
|
|
pub fn export_client_response_schemas(
|
|
out_dir: &::std::path::Path,
|
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
|
let mut schemas = Vec::new();
|
|
$(
|
|
schemas.push(write_json_schema::<$response>(out_dir, stringify!($response))?);
|
|
)*
|
|
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)
|
|
}
|
|
};
|
|
}
|
|
|
|
client_request_definitions! {
|
|
Initialize {
|
|
params: v1::InitializeParams,
|
|
response: v1::InitializeResponse,
|
|
},
|
|
|
|
/// NEW APIs
|
|
// Thread lifecycle
|
|
ThreadStart => "thread/start" {
|
|
params: v2::ThreadStartParams,
|
|
response: v2::ThreadStartResponse,
|
|
},
|
|
ThreadResume => "thread/resume" {
|
|
params: v2::ThreadResumeParams,
|
|
response: v2::ThreadResumeResponse,
|
|
},
|
|
ThreadArchive => "thread/archive" {
|
|
params: v2::ThreadArchiveParams,
|
|
response: v2::ThreadArchiveResponse,
|
|
},
|
|
ThreadList => "thread/list" {
|
|
params: v2::ThreadListParams,
|
|
response: v2::ThreadListResponse,
|
|
},
|
|
ThreadCompact => "thread/compact" {
|
|
params: v2::ThreadCompactParams,
|
|
response: v2::ThreadCompactResponse,
|
|
},
|
|
TurnStart => "turn/start" {
|
|
params: v2::TurnStartParams,
|
|
response: v2::TurnStartResponse,
|
|
},
|
|
TurnInterrupt => "turn/interrupt" {
|
|
params: v2::TurnInterruptParams,
|
|
response: v2::TurnInterruptResponse,
|
|
},
|
|
ReviewStart => "review/start" {
|
|
params: v2::ReviewStartParams,
|
|
response: v2::ReviewStartResponse,
|
|
},
|
|
|
|
ModelList => "model/list" {
|
|
params: v2::ModelListParams,
|
|
response: v2::ModelListResponse,
|
|
},
|
|
|
|
McpServersList => "mcpServers/list" {
|
|
params: v2::ListMcpServersParams,
|
|
response: v2::ListMcpServersResponse,
|
|
},
|
|
|
|
LoginAccount => "account/login/start" {
|
|
params: v2::LoginAccountParams,
|
|
response: v2::LoginAccountResponse,
|
|
},
|
|
|
|
CancelLoginAccount => "account/login/cancel" {
|
|
params: v2::CancelLoginAccountParams,
|
|
response: v2::CancelLoginAccountResponse,
|
|
},
|
|
|
|
LogoutAccount => "account/logout" {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v2::LogoutAccountResponse,
|
|
},
|
|
|
|
GetAccountRateLimits => "account/rateLimits/read" {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v2::GetAccountRateLimitsResponse,
|
|
},
|
|
|
|
FeedbackUpload => "feedback/upload" {
|
|
params: v2::FeedbackUploadParams,
|
|
response: v2::FeedbackUploadResponse,
|
|
},
|
|
|
|
/// Execute a command (argv vector) under the server's sandbox.
|
|
OneOffCommandExec => "command/exec" {
|
|
params: v2::CommandExecParams,
|
|
response: v2::CommandExecResponse,
|
|
},
|
|
|
|
ConfigRead => "config/read" {
|
|
params: v2::ConfigReadParams,
|
|
response: v2::ConfigReadResponse,
|
|
},
|
|
ConfigValueWrite => "config/value/write" {
|
|
params: v2::ConfigValueWriteParams,
|
|
response: v2::ConfigWriteResponse,
|
|
},
|
|
ConfigBatchWrite => "config/batchWrite" {
|
|
params: v2::ConfigBatchWriteParams,
|
|
response: v2::ConfigWriteResponse,
|
|
},
|
|
|
|
GetAccount => "account/read" {
|
|
params: v2::GetAccountParams,
|
|
response: v2::GetAccountResponse,
|
|
},
|
|
|
|
/// DEPRECATED APIs below
|
|
NewConversation {
|
|
params: v1::NewConversationParams,
|
|
response: v1::NewConversationResponse,
|
|
},
|
|
GetConversationSummary {
|
|
params: v1::GetConversationSummaryParams,
|
|
response: v1::GetConversationSummaryResponse,
|
|
},
|
|
/// List recorded Codex conversations (rollouts) with optional pagination and search.
|
|
ListConversations {
|
|
params: v1::ListConversationsParams,
|
|
response: v1::ListConversationsResponse,
|
|
},
|
|
/// Resume a recorded Codex conversation from a rollout file.
|
|
ResumeConversation {
|
|
params: v1::ResumeConversationParams,
|
|
response: v1::ResumeConversationResponse,
|
|
},
|
|
ArchiveConversation {
|
|
params: v1::ArchiveConversationParams,
|
|
response: v1::ArchiveConversationResponse,
|
|
},
|
|
SendUserMessage {
|
|
params: v1::SendUserMessageParams,
|
|
response: v1::SendUserMessageResponse,
|
|
},
|
|
SendUserTurn {
|
|
params: v1::SendUserTurnParams,
|
|
response: v1::SendUserTurnResponse,
|
|
},
|
|
InterruptConversation {
|
|
params: v1::InterruptConversationParams,
|
|
response: v1::InterruptConversationResponse,
|
|
},
|
|
AddConversationListener {
|
|
params: v1::AddConversationListenerParams,
|
|
response: v1::AddConversationSubscriptionResponse,
|
|
},
|
|
RemoveConversationListener {
|
|
params: v1::RemoveConversationListenerParams,
|
|
response: v1::RemoveConversationSubscriptionResponse,
|
|
},
|
|
GitDiffToRemote {
|
|
params: v1::GitDiffToRemoteParams,
|
|
response: v1::GitDiffToRemoteResponse,
|
|
},
|
|
LoginApiKey {
|
|
params: v1::LoginApiKeyParams,
|
|
response: v1::LoginApiKeyResponse,
|
|
},
|
|
LoginChatGpt {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v1::LoginChatGptResponse,
|
|
},
|
|
// DEPRECATED in favor of CancelLoginAccount
|
|
CancelLoginChatGpt {
|
|
params: v1::CancelLoginChatGptParams,
|
|
response: v1::CancelLoginChatGptResponse,
|
|
},
|
|
LogoutChatGpt {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v1::LogoutChatGptResponse,
|
|
},
|
|
/// DEPRECATED in favor of GetAccount
|
|
GetAuthStatus {
|
|
params: v1::GetAuthStatusParams,
|
|
response: v1::GetAuthStatusResponse,
|
|
},
|
|
GetUserSavedConfig {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v1::GetUserSavedConfigResponse,
|
|
},
|
|
SetDefaultModel {
|
|
params: v1::SetDefaultModelParams,
|
|
response: v1::SetDefaultModelResponse,
|
|
},
|
|
GetUserAgent {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v1::GetUserAgentResponse,
|
|
},
|
|
UserInfo {
|
|
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
|
response: v1::UserInfoResponse,
|
|
},
|
|
FuzzyFileSearch {
|
|
params: FuzzyFileSearchParams,
|
|
response: FuzzyFileSearchResponse,
|
|
},
|
|
/// Execute a command (argv vector) under the server's sandbox.
|
|
ExecOneOffCommand {
|
|
params: v1::ExecOneOffCommandParams,
|
|
response: v1::ExecOneOffCommandResponse,
|
|
},
|
|
}
|
|
|
|
/// Generates an `enum ServerRequest` where each variant is a request that the
|
|
/// server can send to the client along with the corresponding params and
|
|
/// response types. It also generates helper types used by the app/server
|
|
/// infrastructure (payload enum, request constructor, and export helpers).
|
|
macro_rules! server_request_definitions {
|
|
(
|
|
$(
|
|
$(#[$variant_meta:meta])*
|
|
$variant:ident $(=> $wire:literal)? {
|
|
params: $params:ty,
|
|
response: $response:ty,
|
|
}
|
|
),* $(,)?
|
|
) => {
|
|
/// Request initiated from the server and sent to the client.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(tag = "method", rename_all = "camelCase")]
|
|
pub enum ServerRequest {
|
|
$(
|
|
$(#[$variant_meta])*
|
|
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
|
|
$variant {
|
|
#[serde(rename = "id")]
|
|
request_id: RequestId,
|
|
params: $params,
|
|
},
|
|
)*
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, JsonSchema)]
|
|
pub enum ServerRequestPayload {
|
|
$( $variant($params), )*
|
|
}
|
|
|
|
impl ServerRequestPayload {
|
|
pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
|
|
match self {
|
|
$(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn export_server_responses(
|
|
out_dir: &::std::path::Path,
|
|
) -> ::std::result::Result<(), ::ts_rs::ExportError> {
|
|
$(
|
|
<$response as ::ts_rs::TS>::export_all_to(out_dir)?;
|
|
)*
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::vec_init_then_push)]
|
|
pub fn export_server_response_schemas(
|
|
out_dir: &Path,
|
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
|
let mut schemas = Vec::new();
|
|
$(
|
|
schemas.push(crate::export::write_json_schema::<$response>(
|
|
out_dir,
|
|
concat!(stringify!($variant), "Response"),
|
|
)?);
|
|
)*
|
|
Ok(schemas)
|
|
}
|
|
|
|
#[allow(clippy::vec_init_then_push)]
|
|
pub fn export_server_param_schemas(
|
|
out_dir: &Path,
|
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
|
let mut schemas = Vec::new();
|
|
$(
|
|
schemas.push(crate::export::write_json_schema::<$params>(
|
|
out_dir,
|
|
concat!(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, serde_json::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)
|
|
}
|
|
};
|
|
}
|
|
|
|
impl TryFrom<JSONRPCRequest> for ServerRequest {
|
|
type Error = serde_json::Error;
|
|
|
|
fn try_from(value: JSONRPCRequest) -> Result<Self, Self::Error> {
|
|
serde_json::from_value(serde_json::to_value(value)?)
|
|
}
|
|
}
|
|
|
|
server_request_definitions! {
|
|
/// NEW APIs
|
|
/// Sent when approval is requested for a specific command execution.
|
|
/// This request is used for Turns started via turn/start.
|
|
CommandExecutionRequestApproval => "item/commandExecution/requestApproval" {
|
|
params: v2::CommandExecutionRequestApprovalParams,
|
|
response: v2::CommandExecutionRequestApprovalResponse,
|
|
},
|
|
|
|
/// Sent when approval is requested for a specific file change.
|
|
/// This request is used for Turns started via turn/start.
|
|
FileChangeRequestApproval => "item/fileChange/requestApproval" {
|
|
params: v2::FileChangeRequestApprovalParams,
|
|
response: v2::FileChangeRequestApprovalResponse,
|
|
},
|
|
|
|
/// DEPRECATED APIs below
|
|
/// Request to approve a patch.
|
|
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
|
|
ApplyPatchApproval {
|
|
params: v1::ApplyPatchApprovalParams,
|
|
response: v1::ApplyPatchApprovalResponse,
|
|
},
|
|
/// Request to exec a command.
|
|
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
|
|
ExecCommandApproval {
|
|
params: v1::ExecCommandApprovalParams,
|
|
response: v1::ExecCommandApprovalResponse,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase")]
|
|
pub struct FuzzyFileSearchParams {
|
|
pub query: String,
|
|
pub roots: Vec<String>,
|
|
// if provided, will cancel any previous request that used the same value
|
|
pub cancellation_token: Option<String>,
|
|
}
|
|
|
|
/// Superset of [`codex_file_search::FileMatch`]
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
pub struct FuzzyFileSearchResult {
|
|
pub root: String,
|
|
pub path: String,
|
|
pub file_name: String,
|
|
pub score: u32,
|
|
pub indices: Option<Vec<u32>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
pub struct FuzzyFileSearchResponse {
|
|
pub files: Vec<FuzzyFileSearchResult>,
|
|
}
|
|
|
|
server_notification_definitions! {
|
|
/// NEW NOTIFICATIONS
|
|
Error => "error" (v2::ErrorNotification),
|
|
ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
|
|
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
|
|
TurnStarted => "turn/started" (v2::TurnStartedNotification),
|
|
TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
|
|
TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification),
|
|
TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification),
|
|
ItemStarted => "item/started" (v2::ItemStartedNotification),
|
|
ItemCompleted => "item/completed" (v2::ItemCompletedNotification),
|
|
AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification),
|
|
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
|
|
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
|
|
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
|
|
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
|
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
|
|
ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification),
|
|
ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification),
|
|
ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification),
|
|
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
|
|
|
|
/// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.
|
|
WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification),
|
|
|
|
#[serde(rename = "account/login/completed")]
|
|
#[ts(rename = "account/login/completed")]
|
|
#[strum(serialize = "account/login/completed")]
|
|
AccountLoginCompleted(v2::AccountLoginCompletedNotification),
|
|
|
|
/// DEPRECATED NOTIFICATIONS below
|
|
AuthStatusChange(v1::AuthStatusChangeNotification),
|
|
|
|
/// Deprecated: use `account/login/completed` instead.
|
|
LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
|
|
SessionConfigured(v1::SessionConfiguredNotification),
|
|
}
|
|
|
|
client_notification_definitions! {
|
|
Initialized,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use anyhow::Result;
|
|
use codex_protocol::ConversationId;
|
|
use codex_protocol::account::PlanType;
|
|
use codex_protocol::parse_command::ParsedCommand;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn serialize_new_conversation() -> Result<()> {
|
|
let request = ClientRequest::NewConversation {
|
|
request_id: RequestId::Integer(42),
|
|
params: v1::NewConversationParams {
|
|
model: Some("gpt-5.1-codex-max".to_string()),
|
|
model_provider: None,
|
|
profile: None,
|
|
cwd: None,
|
|
approval_policy: Some(AskForApproval::OnRequest),
|
|
sandbox: None,
|
|
config: None,
|
|
base_instructions: None,
|
|
developer_instructions: None,
|
|
compact_prompt: None,
|
|
include_apply_patch_tool: None,
|
|
},
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "newConversation",
|
|
"id": 42,
|
|
"params": {
|
|
"model": "gpt-5.1-codex-max",
|
|
"modelProvider": null,
|
|
"profile": null,
|
|
"cwd": null,
|
|
"approvalPolicy": "on-request",
|
|
"sandbox": null,
|
|
"config": null,
|
|
"baseInstructions": null,
|
|
"includeApplyPatchTool": null
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn conversation_id_serializes_as_plain_string() -> Result<()> {
|
|
let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
|
|
|
assert_eq!(
|
|
json!("67e55044-10b1-426f-9247-bb680e5fe0c8"),
|
|
serde_json::to_value(id)?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn conversation_id_deserializes_from_plain_string() -> Result<()> {
|
|
let id: ConversationId =
|
|
serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?;
|
|
|
|
assert_eq!(
|
|
ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?,
|
|
id,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_client_notification() -> Result<()> {
|
|
let notification = ClientNotification::Initialized;
|
|
// Note there is no "params" field for this notification.
|
|
assert_eq!(
|
|
json!({
|
|
"method": "initialized",
|
|
}),
|
|
serde_json::to_value(¬ification)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_server_request() -> Result<()> {
|
|
let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
|
let params = v1::ExecCommandApprovalParams {
|
|
conversation_id,
|
|
call_id: "call-42".to_string(),
|
|
command: vec!["echo".to_string(), "hello".to_string()],
|
|
cwd: PathBuf::from("/tmp"),
|
|
reason: Some("because tests".to_string()),
|
|
risk: None,
|
|
parsed_cmd: vec![ParsedCommand::Unknown {
|
|
cmd: "echo hello".to_string(),
|
|
}],
|
|
};
|
|
let request = ServerRequest::ExecCommandApproval {
|
|
request_id: RequestId::Integer(7),
|
|
params: params.clone(),
|
|
};
|
|
|
|
assert_eq!(
|
|
json!({
|
|
"method": "execCommandApproval",
|
|
"id": 7,
|
|
"params": {
|
|
"conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
"callId": "call-42",
|
|
"command": ["echo", "hello"],
|
|
"cwd": "/tmp",
|
|
"reason": "because tests",
|
|
"risk": null,
|
|
"parsedCmd": [
|
|
{
|
|
"type": "unknown",
|
|
"cmd": "echo hello"
|
|
}
|
|
]
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
|
|
let payload = ServerRequestPayload::ExecCommandApproval(params);
|
|
assert_eq!(payload.request_with_id(RequestId::Integer(7)), request);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_get_account_rate_limits() -> Result<()> {
|
|
let request = ClientRequest::GetAccountRateLimits {
|
|
request_id: RequestId::Integer(1),
|
|
params: None,
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "account/rateLimits/read",
|
|
"id": 1,
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_account_login_api_key() -> Result<()> {
|
|
let request = ClientRequest::LoginAccount {
|
|
request_id: RequestId::Integer(2),
|
|
params: v2::LoginAccountParams::ApiKey {
|
|
api_key: "secret".to_string(),
|
|
},
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "account/login/start",
|
|
"id": 2,
|
|
"params": {
|
|
"type": "apiKey",
|
|
"apiKey": "secret"
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_account_login_chatgpt() -> Result<()> {
|
|
let request = ClientRequest::LoginAccount {
|
|
request_id: RequestId::Integer(3),
|
|
params: v2::LoginAccountParams::Chatgpt,
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "account/login/start",
|
|
"id": 3,
|
|
"params": {
|
|
"type": "chatgpt"
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_account_logout() -> Result<()> {
|
|
let request = ClientRequest::LogoutAccount {
|
|
request_id: RequestId::Integer(4),
|
|
params: None,
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "account/logout",
|
|
"id": 4,
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_get_account() -> Result<()> {
|
|
let request = ClientRequest::GetAccount {
|
|
request_id: RequestId::Integer(5),
|
|
params: v2::GetAccountParams {
|
|
refresh_token: false,
|
|
},
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "account/read",
|
|
"id": 5,
|
|
"params": {
|
|
"refreshToken": false
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn account_serializes_fields_in_camel_case() -> Result<()> {
|
|
let api_key = v2::Account::ApiKey {};
|
|
assert_eq!(
|
|
json!({
|
|
"type": "apiKey",
|
|
}),
|
|
serde_json::to_value(&api_key)?,
|
|
);
|
|
|
|
let chatgpt = v2::Account::Chatgpt {
|
|
email: "user@example.com".to_string(),
|
|
plan_type: PlanType::Plus,
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"type": "chatgpt",
|
|
"email": "user@example.com",
|
|
"planType": "plus",
|
|
}),
|
|
serde_json::to_value(&chatgpt)?,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_list_models() -> Result<()> {
|
|
let request = ClientRequest::ModelList {
|
|
request_id: RequestId::Integer(6),
|
|
params: v2::ModelListParams::default(),
|
|
};
|
|
assert_eq!(
|
|
json!({
|
|
"method": "model/list",
|
|
"id": 6,
|
|
"params": {
|
|
"limit": null,
|
|
"cursor": null
|
|
}
|
|
}),
|
|
serde_json::to_value(&request)?,
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|