Skip to main content

vtcode_core/copilot/
server_client.rs

1use std::path::Path;
2
3use anyhow::{Context, Result, anyhow};
4use serde_json::{Value, json};
5use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader};
6use tokio::process::{ChildStderr, ChildStdout};
7use tokio::time::timeout;
8use vtcode_config::auth::CopilotAuthConfig;
9
10use super::command::{resolve_copilot_command, spawn_copilot_server_process};
11use super::types::CopilotDiscoveredModel;
12
13pub async fn list_available_models(
14    config: &CopilotAuthConfig,
15    workspace_root: &Path,
16) -> Result<Vec<CopilotDiscoveredModel>> {
17    let resolved = resolve_copilot_command(config)?;
18    let mut child = spawn_copilot_server_process(&resolved, workspace_root)?;
19    let stdin = child
20        .stdin
21        .take()
22        .ok_or_else(|| anyhow!("copilot cli server stdin unavailable"))?;
23    let stdout = child
24        .stdout
25        .take()
26        .ok_or_else(|| anyhow!("copilot cli server stdout unavailable"))?;
27    let stderr = child
28        .stderr
29        .take()
30        .ok_or_else(|| anyhow!("copilot cli server stderr unavailable"))?;
31
32    spawn_server_stderr(stderr);
33
34    let result = timeout(resolved.startup_timeout, async move {
35        let mut writer = stdin;
36        let mut reader = BufReader::new(stdout);
37
38        send_request(
39            &mut writer,
40            1,
41            "ping",
42            Some(json!({ "message": "vtcode model discovery" })),
43        )
44        .await
45        .context("copilot cli ping")?;
46        let ping = read_response(&mut reader, 1)
47            .await
48            .context("copilot cli ping")?;
49        let protocol_version = ping
50            .get("protocolVersion")
51            .and_then(Value::as_i64)
52            .unwrap_or(0);
53        if protocol_version <= 0 {
54            return Err(anyhow!(
55                "copilot cli server did not report a protocol version"
56            ));
57        }
58
59        send_request(&mut writer, 2, "models.list", None)
60            .await
61            .context("copilot cli models.list")?;
62        let payload = read_response(&mut reader, 2)
63            .await
64            .context("copilot cli models.list")?;
65        let models = payload
66            .get("models")
67            .and_then(Value::as_array)
68            .ok_or_else(|| anyhow!("copilot cli models.list response missing models"))?;
69
70        let mut discovered = Vec::new();
71        for model in models {
72            let id = model
73                .get("id")
74                .and_then(Value::as_str)
75                .map(str::trim)
76                .filter(|value| !value.is_empty());
77            let name = model
78                .get("name")
79                .and_then(Value::as_str)
80                .map(str::trim)
81                .filter(|value| !value.is_empty());
82            let policy_enabled = model
83                .get("policy")
84                .and_then(Value::as_object)
85                .and_then(|policy| policy.get("state"))
86                .and_then(Value::as_str)
87                .map(|state| state.eq_ignore_ascii_case("enabled"))
88                .unwrap_or(true);
89            if !policy_enabled {
90                continue;
91            }
92            let Some(id) = id else {
93                continue;
94            };
95            discovered.push(CopilotDiscoveredModel {
96                id: id.to_string(),
97                name: name.unwrap_or(id).to_string(),
98            });
99        }
100
101        discovered.sort_by(|left, right| left.id.cmp(&right.id));
102        discovered.dedup_by(|left, right| left.id.eq_ignore_ascii_case(&right.id));
103        Ok::<Vec<CopilotDiscoveredModel>, anyhow::Error>(discovered)
104    })
105    .await
106    .context("copilot cli model discovery timeout")??;
107
108    let _ = child.start_kill();
109    Ok(result)
110}
111
112fn spawn_server_stderr(stderr: ChildStderr) {
113    tokio::spawn(async move {
114        let mut lines = BufReader::new(stderr).lines();
115        while let Ok(Some(line)) = lines.next_line().await {
116            let trimmed = line.trim();
117            if !trimmed.is_empty() {
118                tracing::debug!(target: "copilot.server.stderr", "{}", trimmed);
119            }
120        }
121    });
122}
123
124async fn send_request<W>(writer: &mut W, id: i64, method: &str, params: Option<Value>) -> Result<()>
125where
126    W: AsyncWrite + Unpin,
127{
128    let message = if let Some(params) = params {
129        json!({
130            "jsonrpc": "2.0",
131            "id": id,
132            "method": method,
133            "params": params,
134        })
135    } else {
136        json!({
137            "jsonrpc": "2.0",
138            "id": id,
139            "method": method,
140        })
141    };
142    let payload = serde_json::to_vec(&message).context("copilot cli json serialization failed")?;
143    writer
144        .write_all(format!("Content-Length: {}\r\n\r\n", payload.len()).as_bytes())
145        .await
146        .context("copilot cli write header failed")?;
147    writer
148        .write_all(&payload)
149        .await
150        .context("copilot cli write payload failed")?;
151    writer.flush().await.context("copilot cli flush failed")?;
152    Ok(())
153}
154
155async fn read_response(reader: &mut BufReader<ChildStdout>, expected_id: i64) -> Result<Value> {
156    loop {
157        let message = read_message(reader).await?;
158        let Some(object) = message.as_object() else {
159            continue;
160        };
161
162        if object.get("method").is_some() {
163            continue;
164        }
165
166        if let Some(error) = object.get("error") {
167            let code = error
168                .get("code")
169                .and_then(Value::as_i64)
170                .unwrap_or_default();
171            let detail = error
172                .get("message")
173                .and_then(Value::as_str)
174                .unwrap_or("unknown error");
175            return Err(anyhow!("copilot cli rpc error {code}: {detail}"));
176        }
177
178        if object.get("id").and_then(Value::as_i64) != Some(expected_id) {
179            continue;
180        }
181
182        return object
183            .get("result")
184            .cloned()
185            .ok_or_else(|| anyhow!("copilot cli rpc response missing result"));
186    }
187}
188
189async fn read_message(reader: &mut BufReader<ChildStdout>) -> Result<Value> {
190    let mut content_length = None;
191    loop {
192        let mut line = String::new();
193        let read = reader
194            .read_line(&mut line)
195            .await
196            .context("copilot cli header read failed")?;
197        if read == 0 {
198            return Err(anyhow!("copilot cli server closed the stdio stream"));
199        }
200
201        let trimmed = line.trim_end_matches(['\r', '\n']);
202        if trimmed.is_empty() {
203            break;
204        }
205
206        if let Some(value) = trimmed.strip_prefix("Content-Length:") {
207            content_length = Some(
208                value
209                    .trim()
210                    .parse::<usize>()
211                    .context("invalid copilot cli content length header")?,
212            );
213        }
214    }
215
216    let content_length =
217        content_length.ok_or_else(|| anyhow!("copilot cli response missing Content-Length"))?;
218    let payload = crate::utils::async_utils::read_exact_uninit(reader, content_length)
219        .await
220        .context("copilot cli payload read failed")?;
221    serde_json::from_slice(&payload).context("copilot cli json decode failed")
222}