zarz 0.3.4-alpha → 0.3.5-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -22
- package/bin/zarz.js +10 -31
- package/package.json +6 -10
- package/scripts/postinstall.js +162 -40
- package/Cargo.lock +0 -2815
- package/Cargo.toml +0 -30
- package/QUICKSTART.md +0 -326
- package/src/cli.rs +0 -201
- package/src/config.rs +0 -249
- package/src/conversation_store.rs +0 -183
- package/src/executor.rs +0 -166
- package/src/fs_ops.rs +0 -117
- package/src/intelligence/context.rs +0 -143
- package/src/intelligence/mod.rs +0 -60
- package/src/intelligence/rust_parser.rs +0 -141
- package/src/intelligence/symbol_search.rs +0 -97
- package/src/main.rs +0 -873
- package/src/mcp/client.rs +0 -316
- package/src/mcp/config.rs +0 -133
- package/src/mcp/manager.rs +0 -186
- package/src/mcp/mod.rs +0 -12
- package/src/mcp/types.rs +0 -170
- package/src/providers/anthropic.rs +0 -242
- package/src/providers/glm.rs +0 -259
- package/src/providers/mod.rs +0 -103
- package/src/providers/openai.rs +0 -247
- package/src/repl.rs +0 -2153
- package/src/session.rs +0 -173
package/src/mcp/types.rs
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
use serde::{Deserialize, Serialize};
|
|
2
|
-
use serde_json::Value;
|
|
3
|
-
use std::collections::HashMap;
|
|
4
|
-
|
|
5
|
-
/// MCP JSON-RPC request
|
|
6
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
7
|
-
pub struct JsonRpcRequest {
|
|
8
|
-
pub jsonrpc: String,
|
|
9
|
-
pub id: u64,
|
|
10
|
-
pub method: String,
|
|
11
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
12
|
-
pub params: Option<Value>,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/// MCP JSON-RPC response
|
|
16
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
17
|
-
pub struct JsonRpcResponse {
|
|
18
|
-
pub jsonrpc: String,
|
|
19
|
-
pub id: u64,
|
|
20
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
21
|
-
pub result: Option<Value>,
|
|
22
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
23
|
-
pub error: Option<JsonRpcError>,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// MCP JSON-RPC error
|
|
27
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
28
|
-
pub struct JsonRpcError {
|
|
29
|
-
pub code: i32,
|
|
30
|
-
pub message: String,
|
|
31
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
32
|
-
pub data: Option<Value>,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/// MCP Tool definition
|
|
36
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
37
|
-
pub struct McpTool {
|
|
38
|
-
pub name: String,
|
|
39
|
-
pub description: Option<String>,
|
|
40
|
-
#[serde(rename = "inputSchema")]
|
|
41
|
-
pub input_schema: Value,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/// MCP Resource definition
|
|
45
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
46
|
-
#[allow(dead_code)]
|
|
47
|
-
pub struct McpResource {
|
|
48
|
-
pub uri: String,
|
|
49
|
-
pub name: String,
|
|
50
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
51
|
-
pub description: Option<String>,
|
|
52
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
53
|
-
pub mime_type: Option<String>,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/// MCP Prompt definition
|
|
57
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
58
|
-
#[allow(dead_code)]
|
|
59
|
-
pub struct McpPrompt {
|
|
60
|
-
pub name: String,
|
|
61
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
62
|
-
pub description: Option<String>,
|
|
63
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
64
|
-
pub arguments: Option<Vec<PromptArgument>>,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
68
|
-
#[allow(dead_code)]
|
|
69
|
-
pub struct PromptArgument {
|
|
70
|
-
pub name: String,
|
|
71
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
72
|
-
pub description: Option<String>,
|
|
73
|
-
pub required: bool,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/// MCP Initialize result
|
|
77
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
78
|
-
pub struct InitializeResult {
|
|
79
|
-
#[serde(rename = "protocolVersion")]
|
|
80
|
-
pub protocol_version: String,
|
|
81
|
-
pub capabilities: ServerCapabilities,
|
|
82
|
-
#[serde(rename = "serverInfo")]
|
|
83
|
-
pub server_info: ServerInfo,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
87
|
-
pub struct ServerCapabilities {
|
|
88
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
89
|
-
pub tools: Option<ToolsCapability>,
|
|
90
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
91
|
-
pub resources: Option<ResourcesCapability>,
|
|
92
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
93
|
-
pub prompts: Option<PromptsCapability>,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
97
|
-
pub struct ToolsCapability {
|
|
98
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
99
|
-
pub list_changed: Option<bool>,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
103
|
-
pub struct ResourcesCapability {
|
|
104
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
105
|
-
pub subscribe: Option<bool>,
|
|
106
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
107
|
-
pub list_changed: Option<bool>,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
111
|
-
pub struct PromptsCapability {
|
|
112
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
113
|
-
pub list_changed: Option<bool>,
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
117
|
-
pub struct ServerInfo {
|
|
118
|
-
pub name: String,
|
|
119
|
-
pub version: String,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/// Tools list response
|
|
123
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
124
|
-
pub struct ToolsListResult {
|
|
125
|
-
pub tools: Vec<McpTool>,
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/// Tool call request
|
|
129
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
130
|
-
#[allow(dead_code)]
|
|
131
|
-
pub struct CallToolParams {
|
|
132
|
-
pub name: String,
|
|
133
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
134
|
-
pub arguments: Option<HashMap<String, Value>>,
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/// Tool call result
|
|
138
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
139
|
-
#[allow(dead_code)]
|
|
140
|
-
pub struct CallToolResult {
|
|
141
|
-
pub content: Vec<ToolContent>,
|
|
142
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
143
|
-
pub is_error: Option<bool>,
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
147
|
-
#[serde(tag = "type")]
|
|
148
|
-
#[allow(dead_code)]
|
|
149
|
-
pub enum ToolContent {
|
|
150
|
-
#[serde(rename = "text")]
|
|
151
|
-
Text { text: String },
|
|
152
|
-
#[serde(rename = "image")]
|
|
153
|
-
Image { data: String, mime_type: String },
|
|
154
|
-
#[serde(rename = "resource")]
|
|
155
|
-
Resource { resource: McpResource },
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/// Resources list response
|
|
159
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
160
|
-
#[allow(dead_code)]
|
|
161
|
-
pub struct ResourcesListResult {
|
|
162
|
-
pub resources: Vec<McpResource>,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/// Prompts list response
|
|
166
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
167
|
-
#[allow(dead_code)]
|
|
168
|
-
pub struct PromptsListResult {
|
|
169
|
-
pub prompts: Vec<McpPrompt>,
|
|
170
|
-
}
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
use anyhow::{Context, Result};
|
|
2
|
-
use bytes::Bytes;
|
|
3
|
-
use futures::stream::StreamExt;
|
|
4
|
-
use reqwest::Client;
|
|
5
|
-
use serde::Deserialize;
|
|
6
|
-
use serde_json::json;
|
|
7
|
-
|
|
8
|
-
use super::{CompletionRequest, CompletionResponse, CompletionStream};
|
|
9
|
-
|
|
10
|
-
const DEFAULT_ENDPOINT: &str = "https://api.anthropic.com/v1/messages";
|
|
11
|
-
const DEFAULT_VERSION: &str = "2023-06-01";
|
|
12
|
-
|
|
13
|
-
pub struct AnthropicClient {
|
|
14
|
-
http: Client,
|
|
15
|
-
endpoint: String,
|
|
16
|
-
api_key: String,
|
|
17
|
-
version: String,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
impl AnthropicClient {
|
|
21
|
-
pub fn from_env(
|
|
22
|
-
api_key_override: Option<String>,
|
|
23
|
-
endpoint_override: Option<String>,
|
|
24
|
-
timeout_override: Option<u64>,
|
|
25
|
-
) -> Result<Self> {
|
|
26
|
-
let api_key = api_key_override
|
|
27
|
-
.or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
|
|
28
|
-
.ok_or_else(|| anyhow::anyhow!("ANTHROPIC_API_KEY is required. Please set it in ~/.zarz/config.toml or as an environment variable"))?;
|
|
29
|
-
let endpoint = endpoint_override
|
|
30
|
-
.or_else(|| std::env::var("ANTHROPIC_API_URL").ok())
|
|
31
|
-
.unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
|
|
32
|
-
let version = std::env::var("ANTHROPIC_API_VERSION")
|
|
33
|
-
.ok()
|
|
34
|
-
.filter(|v| !v.trim().is_empty())
|
|
35
|
-
.unwrap_or_else(|| DEFAULT_VERSION.to_string());
|
|
36
|
-
|
|
37
|
-
let timeout_secs = timeout_override
|
|
38
|
-
.or_else(|| {
|
|
39
|
-
std::env::var("ANTHROPIC_TIMEOUT_SECS")
|
|
40
|
-
.ok()
|
|
41
|
-
.and_then(|raw| raw.parse::<u64>().ok())
|
|
42
|
-
})
|
|
43
|
-
.unwrap_or(120);
|
|
44
|
-
|
|
45
|
-
let http = Client::builder()
|
|
46
|
-
.user_agent("zarz-cli/0.1")
|
|
47
|
-
.timeout(std::time::Duration::from_secs(timeout_secs))
|
|
48
|
-
.build()
|
|
49
|
-
.context("Failed to build HTTP client for Anthropic")?;
|
|
50
|
-
|
|
51
|
-
Ok(Self {
|
|
52
|
-
http,
|
|
53
|
-
endpoint,
|
|
54
|
-
api_key,
|
|
55
|
-
version,
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
pub async fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse> {
|
|
60
|
-
let mut payload = serde_json::Map::new();
|
|
61
|
-
payload.insert("model".to_string(), serde_json::Value::String(request.model.clone()));
|
|
62
|
-
payload.insert(
|
|
63
|
-
"max_tokens".to_string(),
|
|
64
|
-
serde_json::Value::Number(serde_json::Number::from(request.max_output_tokens)),
|
|
65
|
-
);
|
|
66
|
-
payload.insert("temperature".to_string(), json!(request.temperature));
|
|
67
|
-
if let Some(system_prompt) = &request.system_prompt {
|
|
68
|
-
payload.insert(
|
|
69
|
-
"system".to_string(),
|
|
70
|
-
serde_json::Value::String(system_prompt.clone()),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if let Some(tools) = &request.tools {
|
|
75
|
-
payload.insert("tools".to_string(), serde_json::Value::Array(tools.clone()));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if let Some(messages) = &request.messages {
|
|
79
|
-
payload.insert("messages".to_string(), serde_json::Value::Array(messages.clone()));
|
|
80
|
-
} else {
|
|
81
|
-
payload.insert(
|
|
82
|
-
"messages".to_string(),
|
|
83
|
-
json!([{
|
|
84
|
-
"role": "user",
|
|
85
|
-
"content": [{
|
|
86
|
-
"type": "text",
|
|
87
|
-
"text": request.user_prompt
|
|
88
|
-
}]
|
|
89
|
-
}]),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let response = self
|
|
94
|
-
.http
|
|
95
|
-
.post(&self.endpoint)
|
|
96
|
-
.header("x-api-key", &self.api_key)
|
|
97
|
-
.header("anthropic-version", &self.version)
|
|
98
|
-
.json(&payload)
|
|
99
|
-
.send()
|
|
100
|
-
.await
|
|
101
|
-
.context("Anthropic request failed")?;
|
|
102
|
-
|
|
103
|
-
let response = response.error_for_status().context("Anthropic returned an error status")?;
|
|
104
|
-
let parsed: AnthropicResponse = response
|
|
105
|
-
.json()
|
|
106
|
-
.await
|
|
107
|
-
.context("Failed to decode Anthropic response")?;
|
|
108
|
-
|
|
109
|
-
let mut text = String::new();
|
|
110
|
-
let mut tool_calls = Vec::new();
|
|
111
|
-
|
|
112
|
-
for block in parsed.content {
|
|
113
|
-
match block {
|
|
114
|
-
AnthropicResponseBlock::Text { text: t } => {
|
|
115
|
-
text.push_str(&t);
|
|
116
|
-
}
|
|
117
|
-
AnthropicResponseBlock::ToolUse { id, name, input } => {
|
|
118
|
-
tool_calls.push(super::ToolCall { id, name, input });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
Ok(CompletionResponse {
|
|
124
|
-
text,
|
|
125
|
-
tool_calls,
|
|
126
|
-
stop_reason: parsed.stop_reason,
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#[allow(dead_code)]
|
|
131
|
-
pub async fn complete_stream(&self, request: &CompletionRequest) -> Result<CompletionStream> {
|
|
132
|
-
let mut payload = serde_json::Map::new();
|
|
133
|
-
payload.insert("model".to_string(), serde_json::Value::String(request.model.clone()));
|
|
134
|
-
payload.insert(
|
|
135
|
-
"max_tokens".to_string(),
|
|
136
|
-
serde_json::Value::Number(serde_json::Number::from(request.max_output_tokens)),
|
|
137
|
-
);
|
|
138
|
-
payload.insert("temperature".to_string(), json!(request.temperature));
|
|
139
|
-
payload.insert("stream".to_string(), json!(true));
|
|
140
|
-
|
|
141
|
-
if let Some(system_prompt) = &request.system_prompt {
|
|
142
|
-
payload.insert(
|
|
143
|
-
"system".to_string(),
|
|
144
|
-
serde_json::Value::String(system_prompt.clone()),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
payload.insert(
|
|
148
|
-
"messages".to_string(),
|
|
149
|
-
json!([{
|
|
150
|
-
"role": "user",
|
|
151
|
-
"content": [{
|
|
152
|
-
"type": "text",
|
|
153
|
-
"text": request.user_prompt
|
|
154
|
-
}]
|
|
155
|
-
}]),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
let response = self
|
|
159
|
-
.http
|
|
160
|
-
.post(&self.endpoint)
|
|
161
|
-
.header("x-api-key", &self.api_key)
|
|
162
|
-
.header("anthropic-version", &self.version)
|
|
163
|
-
.json(&payload)
|
|
164
|
-
.send()
|
|
165
|
-
.await
|
|
166
|
-
.context("Anthropic streaming request failed")?;
|
|
167
|
-
|
|
168
|
-
let response = response
|
|
169
|
-
.error_for_status()
|
|
170
|
-
.context("Anthropic returned an error status")?;
|
|
171
|
-
|
|
172
|
-
let stream = response.bytes_stream();
|
|
173
|
-
let text_stream = stream.map(|result| {
|
|
174
|
-
let bytes = result?;
|
|
175
|
-
parse_anthropic_sse_chunk(&bytes)
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
Ok(Box::pin(text_stream))
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
#[allow(dead_code)]
|
|
183
|
-
fn parse_anthropic_sse_chunk(bytes: &Bytes) -> Result<String> {
|
|
184
|
-
let text = String::from_utf8_lossy(bytes);
|
|
185
|
-
let mut result = String::new();
|
|
186
|
-
|
|
187
|
-
for line in text.lines() {
|
|
188
|
-
if let Some(data) = line.strip_prefix("data: ") {
|
|
189
|
-
if data == "[DONE]" {
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if let Ok(event) = serde_json::from_str::<StreamEvent>(data) {
|
|
194
|
-
match event.event_type.as_str() {
|
|
195
|
-
"content_block_delta" => {
|
|
196
|
-
if let Some(delta) = event.delta {
|
|
197
|
-
if let Some(text) = delta.text {
|
|
198
|
-
result.push_str(&text);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
_ => {}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
Ok(result)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
#[allow(dead_code)]
|
|
212
|
-
#[derive(Debug, Deserialize)]
|
|
213
|
-
struct StreamEvent {
|
|
214
|
-
#[serde(rename = "type")]
|
|
215
|
-
event_type: String,
|
|
216
|
-
delta: Option<StreamDelta>,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
#[allow(dead_code)]
|
|
220
|
-
#[derive(Debug, Deserialize)]
|
|
221
|
-
struct StreamDelta {
|
|
222
|
-
text: Option<String>,
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
#[derive(Debug, Deserialize)]
|
|
226
|
-
struct AnthropicResponse {
|
|
227
|
-
content: Vec<AnthropicResponseBlock>,
|
|
228
|
-
stop_reason: Option<String>,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
#[derive(Debug, Deserialize, Clone)]
|
|
232
|
-
#[serde(tag = "type")]
|
|
233
|
-
pub enum AnthropicResponseBlock {
|
|
234
|
-
#[serde(rename = "text")]
|
|
235
|
-
Text { text: String },
|
|
236
|
-
#[serde(rename = "tool_use")]
|
|
237
|
-
ToolUse {
|
|
238
|
-
id: String,
|
|
239
|
-
name: String,
|
|
240
|
-
input: serde_json::Value,
|
|
241
|
-
},
|
|
242
|
-
}
|
package/src/providers/glm.rs
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
use anyhow::{anyhow, Context, Result};
|
|
2
|
-
use bytes::Bytes;
|
|
3
|
-
use futures::stream::StreamExt;
|
|
4
|
-
use reqwest::Client;
|
|
5
|
-
use serde::Deserialize;
|
|
6
|
-
use serde_json::json;
|
|
7
|
-
|
|
8
|
-
use super::{CompletionRequest, CompletionResponse, CompletionStream};
|
|
9
|
-
|
|
10
|
-
// GLM Coding Plan endpoint (base URL only, no /chat/completions)
|
|
11
|
-
const DEFAULT_ENDPOINT: &str = "https://api.z.ai/api/coding/paas/v4";
|
|
12
|
-
|
|
13
|
-
pub struct GlmClient {
|
|
14
|
-
http: Client,
|
|
15
|
-
endpoint: String,
|
|
16
|
-
api_key: String,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
impl GlmClient {
|
|
20
|
-
pub fn from_env(
|
|
21
|
-
api_key_override: Option<String>,
|
|
22
|
-
endpoint_override: Option<String>,
|
|
23
|
-
timeout_override: Option<u64>,
|
|
24
|
-
) -> Result<Self> {
|
|
25
|
-
let api_key = api_key_override
|
|
26
|
-
.or_else(|| std::env::var("GLM_API_KEY").ok())
|
|
27
|
-
.ok_or_else(|| anyhow::anyhow!("GLM_API_KEY is required. Please set it in ~/.zarz/config.toml or as an environment variable"))?;
|
|
28
|
-
let endpoint = endpoint_override
|
|
29
|
-
.or_else(|| std::env::var("GLM_API_URL").ok())
|
|
30
|
-
.unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
|
|
31
|
-
|
|
32
|
-
let timeout_secs = timeout_override
|
|
33
|
-
.or_else(|| {
|
|
34
|
-
std::env::var("GLM_TIMEOUT_SECS")
|
|
35
|
-
.ok()
|
|
36
|
-
.and_then(|raw| raw.parse::<u64>().ok())
|
|
37
|
-
})
|
|
38
|
-
.unwrap_or(120);
|
|
39
|
-
|
|
40
|
-
let http = Client::builder()
|
|
41
|
-
.user_agent("zarz-cli/0.1")
|
|
42
|
-
.timeout(std::time::Duration::from_secs(timeout_secs))
|
|
43
|
-
.build()
|
|
44
|
-
.context("Failed to build HTTP client for GLM")?;
|
|
45
|
-
|
|
46
|
-
Ok(Self {
|
|
47
|
-
http,
|
|
48
|
-
endpoint,
|
|
49
|
-
api_key,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
pub async fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse> {
|
|
54
|
-
let messages = if let Some(msgs) = &request.messages {
|
|
55
|
-
msgs.clone()
|
|
56
|
-
} else {
|
|
57
|
-
let mut messages = Vec::new();
|
|
58
|
-
if let Some(system) = &request.system_prompt {
|
|
59
|
-
messages.push(json!({
|
|
60
|
-
"role": "system",
|
|
61
|
-
"content": system,
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
messages.push(json!({
|
|
65
|
-
"role": "user",
|
|
66
|
-
"content": request.user_prompt,
|
|
67
|
-
}));
|
|
68
|
-
messages
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
let mut payload = json!({
|
|
72
|
-
"model": request.model,
|
|
73
|
-
"max_tokens": request.max_output_tokens,
|
|
74
|
-
"messages": messages,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if let Some(tools) = &request.tools {
|
|
78
|
-
let glm_tools: Vec<_> = tools.iter().map(|tool| {
|
|
79
|
-
json!({
|
|
80
|
-
"type": "function",
|
|
81
|
-
"function": {
|
|
82
|
-
"name": tool["name"],
|
|
83
|
-
"description": tool["description"],
|
|
84
|
-
"parameters": tool["input_schema"]
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
}).collect();
|
|
88
|
-
payload["tools"] = json!(glm_tools);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Construct full endpoint URL
|
|
92
|
-
let full_url = format!("{}/chat/completions", self.endpoint);
|
|
93
|
-
|
|
94
|
-
let response = self
|
|
95
|
-
.http
|
|
96
|
-
.post(&full_url)
|
|
97
|
-
.bearer_auth(&self.api_key)
|
|
98
|
-
.json(&payload)
|
|
99
|
-
.send()
|
|
100
|
-
.await
|
|
101
|
-
.context("GLM request failed")?;
|
|
102
|
-
|
|
103
|
-
// Check status and get error details if failed
|
|
104
|
-
let status = response.status();
|
|
105
|
-
if !status.is_success() {
|
|
106
|
-
let error_body = response.text().await.unwrap_or_else(|_| "Unable to read error body".to_string());
|
|
107
|
-
return Err(anyhow!("GLM API error ({}): {}", status, error_body));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let response = response;
|
|
111
|
-
|
|
112
|
-
let parsed: GlmResponse = response
|
|
113
|
-
.json()
|
|
114
|
-
.await
|
|
115
|
-
.context("Failed to decode GLM response")?;
|
|
116
|
-
|
|
117
|
-
let first_choice = parsed.choices.into_iter().next()
|
|
118
|
-
.ok_or_else(|| anyhow!("GLM response did not include any choices"))?;
|
|
119
|
-
|
|
120
|
-
let text = first_choice.message.content.unwrap_or_default();
|
|
121
|
-
let mut tool_calls = Vec::new();
|
|
122
|
-
|
|
123
|
-
if let Some(calls) = first_choice.message.tool_calls {
|
|
124
|
-
for call in calls {
|
|
125
|
-
tool_calls.push(super::ToolCall {
|
|
126
|
-
id: call.id,
|
|
127
|
-
name: call.function.name,
|
|
128
|
-
input: call.function.arguments,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
Ok(CompletionResponse {
|
|
134
|
-
text,
|
|
135
|
-
tool_calls,
|
|
136
|
-
stop_reason: first_choice.finish_reason,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
#[allow(dead_code)]
|
|
141
|
-
pub async fn complete_stream(&self, request: &CompletionRequest) -> Result<CompletionStream> {
|
|
142
|
-
let mut messages = Vec::new();
|
|
143
|
-
if let Some(system) = &request.system_prompt {
|
|
144
|
-
messages.push(json!({
|
|
145
|
-
"role": "system",
|
|
146
|
-
"content": system,
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
|
-
messages.push(json!({
|
|
150
|
-
"role": "user",
|
|
151
|
-
"content": request.user_prompt,
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
let payload = json!({
|
|
155
|
-
"model": request.model,
|
|
156
|
-
"max_tokens": request.max_output_tokens,
|
|
157
|
-
"messages": messages,
|
|
158
|
-
"stream": true,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Construct full endpoint URL
|
|
162
|
-
let full_url = format!("{}/chat/completions", self.endpoint);
|
|
163
|
-
|
|
164
|
-
let response = self
|
|
165
|
-
.http
|
|
166
|
-
.post(&full_url)
|
|
167
|
-
.bearer_auth(&self.api_key)
|
|
168
|
-
.json(&payload)
|
|
169
|
-
.send()
|
|
170
|
-
.await
|
|
171
|
-
.context("GLM streaming request failed")?;
|
|
172
|
-
|
|
173
|
-
let response = response
|
|
174
|
-
.error_for_status()
|
|
175
|
-
.context("GLM returned an error status")?;
|
|
176
|
-
|
|
177
|
-
let stream = response.bytes_stream();
|
|
178
|
-
let text_stream = stream.map(|result| {
|
|
179
|
-
let bytes = result?;
|
|
180
|
-
parse_glm_sse_chunk(&bytes)
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
Ok(Box::pin(text_stream))
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
#[allow(dead_code)]
|
|
188
|
-
fn parse_glm_sse_chunk(bytes: &Bytes) -> Result<String> {
|
|
189
|
-
let text = String::from_utf8_lossy(bytes);
|
|
190
|
-
let mut result = String::new();
|
|
191
|
-
|
|
192
|
-
for line in text.lines() {
|
|
193
|
-
if let Some(data) = line.strip_prefix("data: ") {
|
|
194
|
-
if data == "[DONE]" {
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if let Ok(chunk) = serde_json::from_str::<StreamChunk>(data) {
|
|
199
|
-
if let Some(choice) = chunk.choices.first() {
|
|
200
|
-
if let Some(content) = &choice.delta.content {
|
|
201
|
-
result.push_str(content);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
Ok(result)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
#[allow(dead_code)]
|
|
212
|
-
#[derive(Debug, Deserialize)]
|
|
213
|
-
struct StreamChunk {
|
|
214
|
-
choices: Vec<StreamChoice>,
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
#[allow(dead_code)]
|
|
218
|
-
#[derive(Debug, Deserialize)]
|
|
219
|
-
struct StreamChoice {
|
|
220
|
-
delta: StreamDelta,
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
#[allow(dead_code)]
|
|
224
|
-
#[derive(Debug, Deserialize)]
|
|
225
|
-
struct StreamDelta {
|
|
226
|
-
content: Option<String>,
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#[derive(Debug, Deserialize)]
|
|
230
|
-
struct GlmResponse {
|
|
231
|
-
choices: Vec<GlmChoice>,
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
#[derive(Debug, Deserialize)]
|
|
235
|
-
struct GlmChoice {
|
|
236
|
-
message: GlmMessage,
|
|
237
|
-
finish_reason: Option<String>,
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
#[derive(Debug, Deserialize)]
|
|
241
|
-
struct GlmMessage {
|
|
242
|
-
content: Option<String>,
|
|
243
|
-
tool_calls: Option<Vec<GlmToolCall>>,
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
#[derive(Debug, Deserialize)]
|
|
247
|
-
struct GlmToolCall {
|
|
248
|
-
id: String,
|
|
249
|
-
#[serde(rename = "type")]
|
|
250
|
-
#[allow(dead_code)]
|
|
251
|
-
call_type: String,
|
|
252
|
-
function: GlmFunction,
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
#[derive(Debug, Deserialize)]
|
|
256
|
-
struct GlmFunction {
|
|
257
|
-
name: String,
|
|
258
|
-
arguments: serde_json::Value,
|
|
259
|
-
}
|