fix: resolve all clippy warnings and add CI workflow

- Add Default trait implementations for all providers
- Use inline format string variables (clippy::uninlined_format_args)
- Use Range::contains for status code checks
- Fix needless_range_loop in openai_to_cw converter
- Run cargo fmt to fix formatting
- Add GitHub Actions CI workflow with:
  - Rust clippy lint check
  - Rust format check
  - Frontend TypeScript check
  - Build verification
This commit is contained in:
coso
2025-12-14 10:47:36 +08:00
parent c9b4b78432
commit d25ca669db
18 changed files with 953 additions and 418 deletions
+102
View File
@@ -0,0 +1,102 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
shared-key: "rust-cache-lint"
- name: Install system dependencies (Linux)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
- name: Check formatting
working-directory: src-tauri
run: cargo fmt --all -- --check
- name: Run Clippy
working-directory: src-tauri
run: cargo clippy --all-targets --all-features -- -D warnings
frontend-lint:
name: Frontend Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: TypeScript check
run: npx tsc --noEmit
build-check:
name: Build Check
runs-on: ubuntu-latest
needs: [lint, frontend-lint]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
shared-key: "rust-cache-build"
- name: Install system dependencies (Linux)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
- name: Install frontend dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Check Rust build
working-directory: src-tauri
run: cargo check --all-targets
+60 -46
View File
@@ -1,12 +1,12 @@
//! Anthropic 格式转换为 OpenAI 格式 (支持 Claude Code)
use crate::models::openai::*;
use crate::models::anthropic::*;
use crate::models::openai::*;
use uuid::Uuid;
/// 将 Anthropic MessagesRequest 转换为 OpenAI ChatCompletionRequest
pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCompletionRequest {
let mut openai_messages: Vec<ChatMessage> = Vec::new();
// 处理 system prompt
if let Some(system) = &request.system {
let system_text = extract_system_text(system);
@@ -19,27 +19,28 @@ pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCo
});
}
}
// 转换消息
for msg in &request.messages {
let converted = convert_anthropic_message(msg);
openai_messages.extend(converted);
}
// 转换 tools
let tools = request.tools.as_ref().map(|tools| {
tools.iter().map(|t| {
Tool {
tools
.iter()
.map(|t| Tool {
tool_type: "function".to_string(),
function: FunctionDef {
name: t.name.clone(),
description: t.description.clone(),
parameters: t.input_schema.clone(),
},
}
}).collect()
})
.collect()
});
ChatCompletionRequest {
model: request.model.clone(),
messages: openai_messages,
@@ -54,25 +55,26 @@ pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCo
fn extract_system_text(system: &serde_json::Value) -> String {
match system {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(arr) => {
arr.iter()
.filter_map(|item| {
if item.get("type") == Some(&serde_json::Value::String("text".to_string())) {
item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n")
}
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|item| {
if item.get("type") == Some(&serde_json::Value::String("text".to_string())) {
item.get("text")
.and_then(|t| t.as_str())
.map(|s| s.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
let mut result: Vec<ChatMessage> = Vec::new();
match &msg.content {
serde_json::Value::String(s) => {
result.push(ChatMessage {
@@ -86,10 +88,10 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
let mut text_parts: Vec<String> = Vec::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
let mut tool_results: Vec<(String, String)> = Vec::new(); // (tool_use_id, content)
for part in parts {
let part_type = part.get("type").and_then(|t| t.as_str()).unwrap_or("");
match part_type {
"text" => {
if let Some(text) = part.get("text").and_then(|t| t.as_str()) {
@@ -98,11 +100,13 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
}
"tool_use" => {
let default_id = format!("call_{}", &Uuid::new_v4().to_string()[..8]);
let id = part.get("id").and_then(|i| i.as_str())
let id = part
.get("id")
.and_then(|i| i.as_str())
.unwrap_or(&default_id);
let name = part.get("name").and_then(|n| n.as_str()).unwrap_or("");
let input = part.get("input").cloned().unwrap_or(serde_json::json!({}));
tool_calls.push(ToolCall {
id: id.to_string(),
call_type: "function".to_string(),
@@ -113,21 +117,30 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
});
}
"tool_result" => {
let tool_use_id = part.get("tool_use_id").and_then(|i| i.as_str()).unwrap_or("");
let tool_use_id = part
.get("tool_use_id")
.and_then(|i| i.as_str())
.unwrap_or("");
let content = extract_tool_result_content(part.get("content"));
tool_results.push((tool_use_id.to_string(), content));
}
_ => {}
}
}
// 处理 assistant 消息
if msg.role == "assistant" {
let content = if text_parts.is_empty() { None } else {
Some(MessageContent::Text(text_parts.join("")))
let content = if text_parts.is_empty() {
None
} else {
Some(MessageContent::Text(text_parts.join("")))
};
let tc = if tool_calls.is_empty() { None } else { Some(tool_calls) };
let tc = if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
};
result.push(ChatMessage {
role: "assistant".to_string(),
content,
@@ -146,7 +159,7 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
tool_call_id: Some(tool_use_id),
});
}
// 添加文本内容
if !text_parts.is_empty() {
result.push(ChatMessage {
@@ -160,25 +173,26 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec<ChatMessage> {
}
_ => {}
}
result
}
fn extract_tool_result_content(content: Option<&serde_json::Value>) -> String {
match content {
Some(serde_json::Value::String(s)) => s.clone(),
Some(serde_json::Value::Array(arr)) => {
arr.iter()
.filter_map(|item| {
if item.get("type") == Some(&serde_json::Value::String("text".to_string())) {
item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n")
}
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|item| {
if item.get("type") == Some(&serde_json::Value::String("text".to_string())) {
item.get("text")
.and_then(|t| t.as_str())
.map(|s| s.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
+21 -10
View File
@@ -1,6 +1,8 @@
//! CodeWhisperer 响应转换为 OpenAI 格式
use crate::models::openai::*;
#![allow(dead_code)]
use crate::models::codewhisperer::*;
use crate::models::openai::*;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
@@ -14,7 +16,7 @@ pub fn convert_cw_event_to_openai_chunk(
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some(resp_event) = &event.assistant_response_event {
// 文本内容
if let Some(content) = &resp_event.content {
@@ -34,7 +36,7 @@ pub fn convert_cw_event_to_openai_chunk(
}],
});
}
// Tool use
if let Some(tool_use) = &resp_event.tool_use {
return Some(ChatCompletionChunk {
@@ -52,7 +54,8 @@ pub fn convert_cw_event_to_openai_chunk(
call_type: "function".to_string(),
function: FunctionCall {
name: tool_use.name.clone(),
arguments: serde_json::to_string(&tool_use.input).unwrap_or_default(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
}]),
},
@@ -61,7 +64,7 @@ pub fn convert_cw_event_to_openai_chunk(
});
}
}
None
}
@@ -77,9 +80,13 @@ pub fn create_openai_response(
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let finish_reason = if tool_calls.is_some() { "tool_calls" } else { "stop" };
let finish_reason = if tool_calls.is_some() {
"tool_calls"
} else {
"stop"
};
ChatCompletionResponse {
id: format!("chatcmpl-{}", Uuid::new_v4()),
object: "chat.completion".to_string(),
@@ -89,7 +96,11 @@ pub fn create_openai_response(
index: 0,
message: ResponseMessage {
role: "assistant".to_string(),
content: if content.is_empty() { None } else { Some(content.to_string()) },
content: if content.is_empty() {
None
} else {
Some(content.to_string())
},
tool_calls,
},
finish_reason: finish_reason.to_string(),
@@ -108,7 +119,7 @@ pub fn create_stream_end_chunk(model: &str, response_id: &str) -> ChatCompletion
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
ChatCompletionChunk {
id: response_id.to_string(),
object: "chat.completion.chunk".to_string(),
+5 -2
View File
@@ -1,7 +1,10 @@
pub mod openai_to_cw;
pub mod anthropic_to_openai;
pub mod cw_to_openai;
pub mod openai_to_cw;
pub use openai_to_cw::*;
#[allow(unused_imports)]
pub use anthropic_to_openai::*;
#[allow(unused_imports)]
pub use cw_to_openai::*;
#[allow(unused_imports)]
pub use openai_to_cw::*;
+116 -63
View File
@@ -1,8 +1,8 @@
//! OpenAI 格式转换为 CodeWhisperer 格式
use crate::models::openai::*;
use crate::models::codewhisperer::*;
use uuid::Uuid;
use crate::models::openai::*;
use std::collections::HashMap;
use uuid::Uuid;
/// 模型映射表
pub fn get_model_map() -> HashMap<&'static str, &'static str> {
@@ -10,11 +10,23 @@ pub fn get_model_map() -> HashMap<&'static str, &'static str> {
map.insert("claude-opus-4-5", "claude-opus-4.5");
map.insert("claude-haiku-4-5", "claude-haiku-4.5");
map.insert("claude-sonnet-4-5", "CLAUDE_SONNET_4_5_20250929_V1_0");
map.insert("claude-sonnet-4-5-20250929", "CLAUDE_SONNET_4_5_20250929_V1_0");
map.insert(
"claude-sonnet-4-5-20250929",
"CLAUDE_SONNET_4_5_20250929_V1_0",
);
map.insert("claude-sonnet-4-20250514", "CLAUDE_SONNET_4_20250514_V1_0");
map.insert("claude-3-7-sonnet-20250219", "CLAUDE_3_7_SONNET_20250219_V1_0");
map.insert("claude-3-5-sonnet-20241022", "CLAUDE_3_7_SONNET_20250219_V1_0");
map.insert("claude-3-5-sonnet-latest", "CLAUDE_3_7_SONNET_20250219_V1_0");
map.insert(
"claude-3-7-sonnet-20250219",
"CLAUDE_3_7_SONNET_20250219_V1_0",
);
map.insert(
"claude-3-5-sonnet-20241022",
"CLAUDE_3_7_SONNET_20250219_V1_0",
);
map.insert(
"claude-3-5-sonnet-latest",
"CLAUDE_3_7_SONNET_20250219_V1_0",
);
map
}
@@ -26,16 +38,17 @@ pub fn convert_openai_to_codewhisperer(
profile_arn: Option<String>,
) -> CodeWhispererRequest {
let model_map = get_model_map();
let cw_model = model_map.get(request.model.as_str())
let cw_model = model_map
.get(request.model.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| DEFAULT_MODEL.to_string());
let conversation_id = Uuid::new_v4().to_string();
// 提取 system prompt 和消息
let mut system_prompt = String::new();
let mut messages: Vec<&ChatMessage> = Vec::new();
for msg in &request.messages {
if msg.role == "system" {
system_prompt = msg.get_content_text();
@@ -43,15 +56,15 @@ pub fn convert_openai_to_codewhisperer(
messages.push(msg);
}
}
// 构建历史记录
let mut history: Vec<HistoryItem> = Vec::new();
let mut start_idx = 0;
// 处理 system prompt - 合并到第一条用户消息
if !system_prompt.is_empty() && !messages.is_empty() && messages[0].role == "user" {
let first_content = messages[0].get_content_text();
let combined = format!("{}\n\n{}", system_prompt, first_content);
let combined = format!("{system_prompt}\n\n{first_content}");
history.push(HistoryItem::User(UserHistoryItem {
user_input_message: UserInputMessage {
content: combined,
@@ -63,33 +76,41 @@ pub fn convert_openai_to_codewhisperer(
}));
start_idx = 1;
}
// 处理历史消息(除最后一条)
for i in start_idx..messages.len().saturating_sub(1) {
let msg = messages[i];
for msg in messages
.iter()
.take(messages.len().saturating_sub(1))
.skip(start_idx)
{
match msg.role.as_str() {
"user" => {
let content = msg.get_content_text();
let tool_results = extract_tool_results(msg);
let mut user_msg = UserInputMessage {
content: if content.is_empty() {
if tool_results.is_some() { "Tool results provided.".to_string() }
else { "Continue".to_string() }
} else { content },
content: if content.is_empty() {
if tool_results.is_some() {
"Tool results provided.".to_string()
} else {
"Continue".to_string()
}
} else {
content
},
model_id: cw_model.clone(),
origin: "AI_EDITOR".to_string(),
images: None,
user_input_message_context: None,
};
if tool_results.is_some() {
user_msg.user_input_message_context = Some(UserInputMessageContext {
tools: None,
tool_results,
});
}
history.push(HistoryItem::User(UserHistoryItem {
user_input_message: user_msg,
}));
@@ -97,10 +118,14 @@ pub fn convert_openai_to_codewhisperer(
"assistant" => {
let content = msg.get_content_text();
let tool_uses = extract_tool_uses(msg);
history.push(HistoryItem::Assistant(AssistantHistoryItem {
assistant_response_message: AssistantResponseMessage {
content: if content.is_empty() { "I understand.".to_string() } else { content },
content: if content.is_empty() {
"I understand.".to_string()
} else {
content
},
tool_uses,
},
}));
@@ -108,10 +133,13 @@ pub fn convert_openai_to_codewhisperer(
"tool" => {
let tool_content = msg.get_content_text();
let tool_id = msg.tool_call_id.clone().unwrap_or_default();
history.push(HistoryItem::User(UserHistoryItem {
user_input_message: UserInputMessage {
content: format!("Tool result: {}", &tool_content[..tool_content.len().min(200)]),
content: format!(
"Tool result: {}",
&tool_content[..tool_content.len().min(200)]
),
model_id: cw_model.clone(),
origin: "AI_EDITOR".to_string(),
images: None,
@@ -129,10 +157,10 @@ pub fn convert_openai_to_codewhisperer(
_ => {}
}
}
// 修复历史记录交替顺序
let history = fix_history_alternation(history, &cw_model);
// 构建当前消息
let current_content = if messages.is_empty() {
"Continue".to_string()
@@ -142,36 +170,54 @@ pub fn convert_openai_to_codewhisperer(
"Continue".to_string()
} else {
let content = last_msg.get_content_text();
if content.is_empty() { "Continue".to_string() } else { content }
if content.is_empty() {
"Continue".to_string()
} else {
content
}
}
};
// 构建 tools
let tools = request.tools.as_ref().map(|tools| {
tools.iter().take(50).map(|t| {
let params = t.function.parameters.clone()
.unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
let desc = t.function.description.clone()
.unwrap_or_else(|| format!("Tool: {}", t.function.name));
CWTool {
tool_specification: ToolSpecification {
name: t.function.name.clone(),
description: if desc.len() > 500 { format!("{}...", &desc[..497]) } else { desc },
input_schema: InputSchema { json: params },
},
}
}).collect()
tools
.iter()
.take(50)
.map(|t| {
let params = t
.function
.parameters
.clone()
.unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
let desc = t
.function
.description
.clone()
.unwrap_or_else(|| format!("Tool: {}", t.function.name));
CWTool {
tool_specification: ToolSpecification {
name: t.function.name.clone(),
description: if desc.len() > 500 {
format!("{}...", &desc[..497])
} else {
desc
},
input_schema: InputSchema { json: params },
},
}
})
.collect()
});
let current_tool_results = if !messages.is_empty() {
let last_msg = messages.last().unwrap();
extract_tool_results(last_msg)
} else {
None
};
let user_input_message_context = if tools.is_some() || current_tool_results.is_some() {
Some(UserInputMessageContext {
tools,
@@ -180,7 +226,7 @@ pub fn convert_openai_to_codewhisperer(
} else {
None
};
CodeWhispererRequest {
conversation_state: ConversationState {
chat_trigger_type: "MANUAL".to_string(),
@@ -194,7 +240,11 @@ pub fn convert_openai_to_codewhisperer(
user_input_message_context,
},
},
history: if history.is_empty() { None } else { Some(history) },
history: if history.is_empty() {
None
} else {
Some(history)
},
},
profile_arn,
}
@@ -215,15 +265,18 @@ fn extract_tool_results(msg: &ChatMessage) -> Option<Vec<CWToolResult>> {
fn extract_tool_uses(msg: &ChatMessage) -> Option<Vec<CWToolUse>> {
msg.tool_calls.as_ref().map(|calls| {
calls.iter().map(|tc| {
let input: serde_json::Value = serde_json::from_str(&tc.function.arguments)
.unwrap_or(serde_json::json!({}));
CWToolUse {
input,
name: tc.function.name.clone(),
tool_use_id: tc.id.clone(),
}
}).collect()
calls
.iter()
.map(|tc| {
let input: serde_json::Value =
serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::json!({}));
CWToolUse {
input,
name: tc.function.name.clone(),
tool_use_id: tc.id.clone(),
}
})
.collect()
})
}
@@ -232,9 +285,9 @@ fn fix_history_alternation(history: Vec<HistoryItem>, model_id: &str) -> Vec<His
if history.is_empty() {
return history;
}
let mut fixed: Vec<HistoryItem> = Vec::new();
for item in history {
match &item {
HistoryItem::User(_) => {
@@ -278,7 +331,7 @@ fn fix_history_alternation(history: Vec<HistoryItem>, model_id: &str) -> Vec<His
}
}
}
// 确保以 assistant 结尾
if let Some(HistoryItem::User(_)) = fixed.last() {
fixed.push(HistoryItem::Assistant(AssistantHistoryItem {
@@ -288,6 +341,6 @@ fn fix_history_alternation(history: Vec<HistoryItem>, model_id: &str) -> Vec<His
},
}));
}
fixed
}
+257 -89
View File
@@ -1,9 +1,9 @@
mod config;
mod server;
mod providers;
mod models;
mod converter;
mod logger;
mod models;
mod providers;
mod server;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -12,16 +12,30 @@ pub type AppState = Arc<RwLock<server::ServerState>>;
pub type LogState = Arc<RwLock<logger::LogStore>>;
#[tauri::command]
async fn start_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn start_server(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "Starting server...");
s.start(logs.inner().clone()).await.map_err(|e| e.to_string())?;
logs.write().await.add("info", &format!("Server started on {}:{}", s.config.server.host, s.config.server.port));
s.start(logs.inner().clone())
.await
.map_err(|e| e.to_string())?;
logs.write().await.add(
"info",
&format!(
"Server started on {}:{}",
s.config.server.host, s.config.server.port
),
);
Ok("Server started".to_string())
}
#[tauri::command]
async fn stop_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn stop_server(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
s.stop().await;
logs.write().await.add("info", "Server stopped");
@@ -29,7 +43,9 @@ async fn stop_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, L
}
#[tauri::command]
async fn get_server_status(state: tauri::State<'_, AppState>) -> Result<server::ServerStatus, String> {
async fn get_server_status(
state: tauri::State<'_, AppState>,
) -> Result<server::ServerStatus, String> {
let s = state.read().await;
Ok(s.status())
}
@@ -41,7 +57,10 @@ async fn get_config(state: tauri::State<'_, AppState>) -> Result<config::Config,
}
#[tauri::command]
async fn save_config(state: tauri::State<'_, AppState>, config: config::Config) -> Result<(), String> {
async fn save_config(
state: tauri::State<'_, AppState>,
config: config::Config,
) -> Result<(), String> {
let mut s = state.write().await;
s.config = config.clone();
config::save_config(&config).map_err(|e| e.to_string())
@@ -61,33 +80,53 @@ async fn set_default_provider(
) -> Result<String, String> {
let valid_providers = ["kiro", "gemini", "qwen", "openai", "claude"];
if !valid_providers.contains(&provider.as_str()) {
return Err(format!("Invalid provider: {}", provider));
return Err(format!("Invalid provider: {provider}"));
}
let mut s = state.write().await;
s.config.default_provider = provider.clone();
config::save_config(&s.config).map_err(|e| e.to_string())?;
logs.write().await.add("info", &format!("默认 Provider 已切换为: {}", provider));
logs.write()
.await
.add("info", &format!("默认 Provider 已切换为: {provider}"));
Ok(provider)
}
#[tauri::command]
async fn refresh_kiro_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn refresh_kiro_token(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "Refreshing Kiro token...");
let result = s.kiro_provider.refresh_token().await.map_err(|e| e.to_string());
let result = s
.kiro_provider
.refresh_token()
.await
.map_err(|e| e.to_string());
match &result {
Ok(_) => logs.write().await.add("info", "Token refreshed successfully"),
Err(e) => logs.write().await.add("error", &format!("Token refresh failed: {}", e)),
Ok(_) => logs
.write()
.await
.add("info", "Token refreshed successfully"),
Err(e) => logs
.write()
.await
.add("error", &format!("Token refresh failed: {e}")),
}
result
}
#[tauri::command]
async fn reload_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn reload_credentials(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "Reloading credentials...");
s.kiro_provider.load_credentials().map_err(|e| e.to_string())?;
s.kiro_provider
.load_credentials()
.map_err(|e| e.to_string())?;
logs.write().await.add("info", "Credentials reloaded");
Ok("Credentials reloaded".to_string())
}
@@ -104,11 +143,13 @@ struct KiroCredentialStatus {
}
#[tauri::command]
async fn get_kiro_credentials(state: tauri::State<'_, AppState>) -> Result<KiroCredentialStatus, String> {
async fn get_kiro_credentials(
state: tauri::State<'_, AppState>,
) -> Result<KiroCredentialStatus, String> {
let s = state.read().await;
let creds = &s.kiro_provider.credentials;
let path = providers::kiro::KiroProvider::default_creds_path();
Ok(KiroCredentialStatus {
loaded: creds.access_token.is_some() || creds.refresh_token.is_some(),
has_access_token: creds.access_token.is_some(),
@@ -132,7 +173,7 @@ async fn get_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvV
let s = state.read().await;
let creds = &s.kiro_provider.credentials;
let mut vars = Vec::new();
if let Some(token) = &creds.access_token {
vars.push(EnvVariable {
key: "KIRO_ACCESS_TOKEN".to_string(),
@@ -182,7 +223,7 @@ async fn get_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvV
masked: method.clone(),
});
}
Ok(vars)
}
@@ -190,7 +231,7 @@ fn mask_token(token: &str) -> String {
if token.len() <= 12 {
"****".to_string()
} else {
format!("{}****{}", &token[..6], &token[token.len()-4..])
format!("{}****{}", &token[..6], &token[token.len() - 4..])
}
}
@@ -200,7 +241,7 @@ async fn get_token_file_hash() -> Result<String, String> {
if !path.exists() {
return Ok("".to_string());
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let hash = format!("{:x}", md5::compute(&content));
Ok(hash)
@@ -214,7 +255,7 @@ async fn check_and_reload_credentials(
last_hash: String,
) -> Result<CheckResult, String> {
let path = providers::kiro::KiroProvider::default_creds_path();
if !path.exists() {
return Ok(CheckResult {
changed: false,
@@ -222,17 +263,21 @@ async fn check_and_reload_credentials(
reloaded: false,
});
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let new_hash = format!("{:x}", md5::compute(&content));
if !last_hash.is_empty() && new_hash != last_hash {
logs.write().await.add("info", "[自动检测] 凭证文件已变化,正在重新加载...");
logs.write()
.await
.add("info", "[自动检测] 凭证文件已变化,正在重新加载...");
let mut s = state.write().await;
match s.kiro_provider.load_credentials() {
Ok(_) => {
logs.write().await.add("info", "[自动检测] 凭证重新加载成功");
logs.write()
.await
.add("info", "[自动检测] 凭证重新加载成功");
Ok(CheckResult {
changed: true,
new_hash,
@@ -240,7 +285,9 @@ async fn check_and_reload_credentials(
})
}
Err(e) => {
logs.write().await.add("error", &format!("[自动检测] 凭证重新加载失败: {}", e));
logs.write()
.await
.add("error", &format!("[自动检测] 凭证重新加载失败: {e}"));
Ok(CheckResult {
changed: true,
new_hash,
@@ -277,11 +324,13 @@ struct GeminiCredentialStatus {
}
#[tauri::command]
async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result<GeminiCredentialStatus, String> {
async fn get_gemini_credentials(
state: tauri::State<'_, AppState>,
) -> Result<GeminiCredentialStatus, String> {
let s = state.read().await;
let creds = &s.gemini_provider.credentials;
let path = providers::gemini::GeminiProvider::default_creds_path();
Ok(GeminiCredentialStatus {
loaded: creds.access_token.is_some() || creds.refresh_token.is_some(),
has_access_token: creds.access_token.is_some(),
@@ -293,32 +342,49 @@ async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result<Gem
}
#[tauri::command]
async fn reload_gemini_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn reload_gemini_credentials(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "[Gemini] 正在加载凭证...");
s.gemini_provider.load_credentials().map_err(|e| e.to_string())?;
s.gemini_provider
.load_credentials()
.map_err(|e| e.to_string())?;
logs.write().await.add("info", "[Gemini] 凭证加载成功");
Ok("Gemini credentials reloaded".to_string())
}
#[tauri::command]
async fn refresh_gemini_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn refresh_gemini_token(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "[Gemini] 正在刷新 Token...");
let result = s.gemini_provider.refresh_token().await.map_err(|e| e.to_string());
let result = s
.gemini_provider
.refresh_token()
.await
.map_err(|e| e.to_string());
match &result {
Ok(_) => logs.write().await.add("info", "[Gemini] Token 刷新成功"),
Err(e) => logs.write().await.add("error", &format!("[Gemini] Token 刷新失败: {}", e)),
Err(e) => logs
.write()
.await
.add("error", &format!("[Gemini] Token 刷新失败: {e}")),
}
result
}
#[tauri::command]
async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvVariable>, String> {
async fn get_gemini_env_variables(
state: tauri::State<'_, AppState>,
) -> Result<Vec<EnvVariable>, String> {
let s = state.read().await;
let creds = &s.gemini_provider.credentials;
let mut vars = Vec::new();
if let Some(token) = &creds.access_token {
vars.push(EnvVariable {
key: "GEMINI_ACCESS_TOKEN".to_string(),
@@ -341,7 +407,7 @@ async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result<V
masked: expiry_str,
});
}
Ok(vars)
}
@@ -351,7 +417,7 @@ async fn get_gemini_token_file_hash() -> Result<String, String> {
if !path.exists() {
return Ok("".to_string());
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let hash = format!("{:x}", md5::compute(&content));
Ok(hash)
@@ -364,7 +430,7 @@ async fn check_and_reload_gemini_credentials(
last_hash: String,
) -> Result<CheckResult, String> {
let path = providers::gemini::GeminiProvider::default_creds_path();
if !path.exists() {
return Ok(CheckResult {
changed: false,
@@ -372,17 +438,21 @@ async fn check_and_reload_gemini_credentials(
reloaded: false,
});
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let new_hash = format!("{:x}", md5::compute(&content));
if !last_hash.is_empty() && new_hash != last_hash {
logs.write().await.add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载...");
logs.write()
.await
.add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载...");
let mut s = state.write().await;
match s.gemini_provider.load_credentials() {
Ok(_) => {
logs.write().await.add("info", "[Gemini][自动检测] 凭证重新加载成功");
logs.write()
.await
.add("info", "[Gemini][自动检测] 凭证重新加载成功");
Ok(CheckResult {
changed: true,
new_hash,
@@ -390,7 +460,10 @@ async fn check_and_reload_gemini_credentials(
})
}
Err(e) => {
logs.write().await.add("error", &format!("[Gemini][自动检测] 凭证重新加载失败: {}", e));
logs.write().await.add(
"error",
&format!("[Gemini][自动检测] 凭证重新加载失败: {e}"),
);
Ok(CheckResult {
changed: true,
new_hash,
@@ -420,11 +493,13 @@ struct QwenCredentialStatus {
}
#[tauri::command]
async fn get_qwen_credentials(state: tauri::State<'_, AppState>) -> Result<QwenCredentialStatus, String> {
async fn get_qwen_credentials(
state: tauri::State<'_, AppState>,
) -> Result<QwenCredentialStatus, String> {
let s = state.read().await;
let creds = &s.qwen_provider.credentials;
let path = providers::qwen::QwenProvider::default_creds_path();
Ok(QwenCredentialStatus {
loaded: creds.access_token.is_some() || creds.refresh_token.is_some(),
has_access_token: creds.access_token.is_some(),
@@ -436,32 +511,49 @@ async fn get_qwen_credentials(state: tauri::State<'_, AppState>) -> Result<QwenC
}
#[tauri::command]
async fn reload_qwen_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn reload_qwen_credentials(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "[Qwen] 正在加载凭证...");
s.qwen_provider.load_credentials().map_err(|e| e.to_string())?;
s.qwen_provider
.load_credentials()
.map_err(|e| e.to_string())?;
logs.write().await.add("info", "[Qwen] 凭证加载成功");
Ok("Qwen credentials reloaded".to_string())
}
#[tauri::command]
async fn refresh_qwen_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
async fn refresh_qwen_token(
state: tauri::State<'_, AppState>,
logs: tauri::State<'_, LogState>,
) -> Result<String, String> {
let mut s = state.write().await;
logs.write().await.add("info", "[Qwen] 正在刷新 Token...");
let result = s.qwen_provider.refresh_token().await.map_err(|e| e.to_string());
let result = s
.qwen_provider
.refresh_token()
.await
.map_err(|e| e.to_string());
match &result {
Ok(_) => logs.write().await.add("info", "[Qwen] Token 刷新成功"),
Err(e) => logs.write().await.add("error", &format!("[Qwen] Token 刷新失败: {}", e)),
Err(e) => logs
.write()
.await
.add("error", &format!("[Qwen] Token 刷新失败: {e}")),
}
result
}
#[tauri::command]
async fn get_qwen_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvVariable>, String> {
async fn get_qwen_env_variables(
state: tauri::State<'_, AppState>,
) -> Result<Vec<EnvVariable>, String> {
let s = state.read().await;
let creds = &s.qwen_provider.credentials;
let mut vars = Vec::new();
if let Some(token) = &creds.access_token {
vars.push(EnvVariable {
key: "QWEN_ACCESS_TOKEN".to_string(),
@@ -491,7 +583,7 @@ async fn get_qwen_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec
masked: expiry_str,
});
}
Ok(vars)
}
@@ -501,7 +593,7 @@ async fn get_qwen_token_file_hash() -> Result<String, String> {
if !path.exists() {
return Ok("".to_string());
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let hash = format!("{:x}", md5::compute(&content));
Ok(hash)
@@ -514,7 +606,7 @@ async fn check_and_reload_qwen_credentials(
last_hash: String,
) -> Result<CheckResult, String> {
let path = providers::qwen::QwenProvider::default_creds_path();
if !path.exists() {
return Ok(CheckResult {
changed: false,
@@ -522,17 +614,21 @@ async fn check_and_reload_qwen_credentials(
reloaded: false,
});
}
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
let new_hash = format!("{:x}", md5::compute(&content));
if !last_hash.is_empty() && new_hash != last_hash {
logs.write().await.add("info", "[Qwen][自动检测] 凭证文件已变化,正在重新加载...");
logs.write()
.await
.add("info", "[Qwen][自动检测] 凭证文件已变化,正在重新加载...");
let mut s = state.write().await;
match s.qwen_provider.load_credentials() {
Ok(_) => {
logs.write().await.add("info", "[Qwen][自动检测] 凭证重新加载成功");
logs.write()
.await
.add("info", "[Qwen][自动检测] 凭证重新加载成功");
Ok(CheckResult {
changed: true,
new_hash,
@@ -540,7 +636,9 @@ async fn check_and_reload_qwen_credentials(
})
}
Err(e) => {
logs.write().await.add("error", &format!("[Qwen][自动检测] 凭证重新加载失败: {}", e));
logs.write()
.await
.add("error", &format!("[Qwen][自动检测] 凭证重新加载失败: {e}"));
Ok(CheckResult {
changed: true,
new_hash,
@@ -567,7 +665,9 @@ struct OpenAICustomStatus {
}
#[tauri::command]
async fn get_openai_custom_status(state: tauri::State<'_, AppState>) -> Result<OpenAICustomStatus, String> {
async fn get_openai_custom_status(
state: tauri::State<'_, AppState>,
) -> Result<OpenAICustomStatus, String> {
let s = state.read().await;
let config = &s.openai_custom_provider.config;
Ok(OpenAICustomStatus {
@@ -589,7 +689,10 @@ async fn set_openai_custom_config(
s.openai_custom_provider.config.api_key = api_key;
s.openai_custom_provider.config.base_url = base_url;
s.openai_custom_provider.config.enabled = enabled;
logs.write().await.add("info", &format!("[OpenAI Custom] 配置已更新, enabled={}", enabled));
logs.write().await.add(
"info",
&format!("[OpenAI Custom] 配置已更新, enabled={enabled}"),
);
Ok("OpenAI Custom config updated".to_string())
}
@@ -603,7 +706,9 @@ struct ClaudeCustomStatus {
}
#[tauri::command]
async fn get_claude_custom_status(state: tauri::State<'_, AppState>) -> Result<ClaudeCustomStatus, String> {
async fn get_claude_custom_status(
state: tauri::State<'_, AppState>,
) -> Result<ClaudeCustomStatus, String> {
let s = state.read().await;
let config = &s.claude_custom_provider.config;
Ok(ClaudeCustomStatus {
@@ -625,7 +730,10 @@ async fn set_claude_custom_config(
s.claude_custom_provider.config.api_key = api_key;
s.claude_custom_provider.config.base_url = base_url;
s.claude_custom_provider.config.enabled = enabled;
logs.write().await.add("info", &format!("[Claude Custom] 配置已更新, enabled={}", enabled));
logs.write().await.add(
"info",
&format!("[Claude Custom] 配置已更新, enabled={enabled}"),
);
Ok("Claude Custom config updated".to_string())
}
@@ -659,22 +767,78 @@ struct ModelInfo {
async fn get_available_models() -> Result<Vec<ModelInfo>, String> {
Ok(vec![
// Kiro/Claude models
ModelInfo { id: "claude-sonnet-4-5".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-sonnet-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-sonnet-4-5-20250929".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-3-7-sonnet-20250219".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-3-5-sonnet-latest".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-opus-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo { id: "claude-haiku-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() },
ModelInfo {
id: "claude-sonnet-4-5".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-sonnet-4-5-20250514".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-sonnet-4-5-20250929".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-3-7-sonnet-20250219".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-3-5-sonnet-latest".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-opus-4-5-20250514".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
ModelInfo {
id: "claude-haiku-4-5-20250514".to_string(),
object: "model".to_string(),
owned_by: "anthropic".to_string(),
},
// Gemini models
ModelInfo { id: "gemini-2.5-flash".to_string(), object: "model".to_string(), owned_by: "google".to_string() },
ModelInfo { id: "gemini-2.5-flash-lite".to_string(), object: "model".to_string(), owned_by: "google".to_string() },
ModelInfo { id: "gemini-2.5-pro".to_string(), object: "model".to_string(), owned_by: "google".to_string() },
ModelInfo { id: "gemini-2.5-pro-preview-06-05".to_string(), object: "model".to_string(), owned_by: "google".to_string() },
ModelInfo { id: "gemini-3-pro-preview".to_string(), object: "model".to_string(), owned_by: "google".to_string() },
ModelInfo {
id: "gemini-2.5-flash".to_string(),
object: "model".to_string(),
owned_by: "google".to_string(),
},
ModelInfo {
id: "gemini-2.5-flash-lite".to_string(),
object: "model".to_string(),
owned_by: "google".to_string(),
},
ModelInfo {
id: "gemini-2.5-pro".to_string(),
object: "model".to_string(),
owned_by: "google".to_string(),
},
ModelInfo {
id: "gemini-2.5-pro-preview-06-05".to_string(),
object: "model".to_string(),
owned_by: "google".to_string(),
},
ModelInfo {
id: "gemini-3-pro-preview".to_string(),
object: "model".to_string(),
owned_by: "google".to_string(),
},
// Qwen models
ModelInfo { id: "qwen3-coder-plus".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() },
ModelInfo { id: "qwen3-coder-flash".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() },
ModelInfo {
id: "qwen3-coder-plus".to_string(),
object: "model".to_string(),
owned_by: "alibaba".to_string(),
},
ModelInfo {
id: "qwen3-coder-flash".to_string(),
object: "model".to_string(),
owned_by: "alibaba".to_string(),
},
])
}
@@ -696,7 +860,7 @@ async fn test_api(
.build()
.map_err(|e| e.to_string())?;
let url = format!("{}{}", base_url, path);
let url = format!("{base_url}{path}");
tracing::info!("Testing API: {} {}", method, url);
@@ -711,7 +875,7 @@ async fn test_api(
req = req.header("Content-Type", "application/json");
if auth {
req = req.header("Authorization", format!("Bearer {}", api_key));
req = req.header("Authorization", format!("Bearer {api_key}"));
}
if let Some(b) = body {
@@ -724,10 +888,14 @@ async fn test_api(
let body = resp.text().await.unwrap_or_default();
let time_ms = start.elapsed().as_millis() as u64;
tracing::info!("API test result: status={}, body_len={}", status, body.len());
tracing::info!(
"API test result: status={}, body_len={}",
status,
body.len()
);
Ok(TestResult {
success: status >= 200 && status < 300,
success: (200..300).contains(&status),
status,
body,
time_ms,
+12 -5
View File
@@ -1,8 +1,8 @@
//! 日志管理模块
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use chrono::Utc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
@@ -16,13 +16,19 @@ pub struct LogStore {
max_logs: usize,
}
impl LogStore {
pub fn new() -> Self {
impl Default for LogStore {
fn default() -> Self {
Self {
logs: Vec::new(),
max_logs: 1000,
}
}
}
impl LogStore {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, level: &str, message: &str) {
let entry = LogEntry {
@@ -30,9 +36,9 @@ impl LogStore {
level: level.to_string(),
message: message.to_string(),
};
self.logs.push(entry);
// 保持日志数量在限制内
if self.logs.len() > self.max_logs {
self.logs.remove(0);
@@ -48,4 +54,5 @@ impl LogStore {
}
}
#[allow(dead_code)]
pub type SharedLogStore = Arc<RwLock<LogStore>>;
+9 -5
View File
@@ -18,9 +18,7 @@ pub enum AnthropicContentBlock {
content: serde_json::Value,
},
#[serde(rename = "image")]
Image {
source: ImageSource,
},
Image { source: ImageSource },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -89,13 +87,19 @@ pub enum AnthropicStreamEvent {
#[serde(rename = "message_start")]
MessageStart { message: AnthropicMessageStart },
#[serde(rename = "content_block_start")]
ContentBlockStart { index: u32, content_block: AnthropicContentBlock },
ContentBlockStart {
index: u32,
content_block: AnthropicContentBlock,
},
#[serde(rename = "content_block_delta")]
ContentBlockDelta { index: u32, delta: AnthropicDelta },
#[serde(rename = "content_block_stop")]
ContentBlockStop { index: u32 },
#[serde(rename = "message_delta")]
MessageDelta { delta: AnthropicMessageDelta, usage: AnthropicUsage },
MessageDelta {
delta: AnthropicMessageDelta,
usage: AnthropicUsage,
},
#[serde(rename = "message_stop")]
MessageStop,
}
-1
View File
@@ -1,6 +1,5 @@
//! CodeWhisperer/Kiro API 数据模型
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
+5 -2
View File
@@ -1,7 +1,10 @@
pub mod openai;
pub mod anthropic;
pub mod codewhisperer;
pub mod openai;
pub use openai::*;
#[allow(unused_imports)]
pub use anthropic::*;
#[allow(unused_imports)]
pub use codewhisperer::*;
#[allow(unused_imports)]
pub use openai::*;
+6 -5
View File
@@ -1,6 +1,5 @@
//! OpenAI API 数据模型
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
@@ -54,15 +53,17 @@ impl ChatMessage {
pub fn get_content_text(&self) -> String {
match &self.content {
Some(MessageContent::Text(s)) => s.clone(),
Some(MessageContent::Parts(parts)) => {
parts.iter().filter_map(|p| {
Some(MessageContent::Parts(parts)) => parts
.iter()
.filter_map(|p| {
if let ContentPart::Text { text } = p {
Some(text.clone())
} else {
None
}
}).collect::<Vec<_>>().join("")
}
})
.collect::<Vec<_>>()
.join(""),
None => String::new(),
}
}
+26 -11
View File
@@ -1,7 +1,7 @@
//! Claude Custom Provider (自定义 Claude API)
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::error::Error;
use reqwest::Client;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClaudeCustomConfig {
@@ -15,16 +15,23 @@ pub struct ClaudeCustomProvider {
pub client: Client,
}
impl ClaudeCustomProvider {
pub fn new() -> Self {
impl Default for ClaudeCustomProvider {
fn default() -> Self {
Self {
config: ClaudeCustomConfig::default(),
client: Client::new(),
}
}
}
impl ClaudeCustomProvider {
pub fn new() -> Self {
Self::default()
}
pub fn get_base_url(&self) -> String {
self.config.base_url
self.config
.base_url
.clone()
.unwrap_or_else(|| "https://api.anthropic.com".to_string())
}
@@ -37,13 +44,17 @@ impl ClaudeCustomProvider {
&self,
request: &serde_json::Value,
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
let api_key = self.config.api_key.as_ref()
let api_key = self
.config
.api_key
.as_ref()
.ok_or("Claude API key not configured")?;
let base_url = self.get_base_url();
let url = format!("{}/v1/messages", base_url);
let url = format!("{base_url}/v1/messages");
let resp = self.client
let resp = self
.client
.post(&url)
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
@@ -59,13 +70,17 @@ impl ClaudeCustomProvider {
&self,
request: &serde_json::Value,
) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
let api_key = self.config.api_key.as_ref()
let api_key = self
.config
.api_key
.as_ref()
.ok_or("Claude API key not configured")?;
let base_url = self.get_base_url();
let url = format!("{}/v1/messages/count_tokens", base_url);
let url = format!("{base_url}/v1/messages/count_tokens");
let resp = self.client
let resp = self
.client
.post(&url)
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
@@ -77,7 +92,7 @@ impl ClaudeCustomProvider {
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Failed to count tokens: {} - {}", status, body).into());
return Err(format!("Failed to count tokens: {status} - {body}").into());
}
let data: serde_json::Value = resp.json().await?;
+40 -27
View File
@@ -1,8 +1,8 @@
//! Gemini CLI OAuth Provider
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;
use reqwest::Client;
// Constants
const CODE_ASSIST_ENDPOINT: &str = "https://cloudcode-pa.googleapis.com";
@@ -21,9 +21,10 @@ fn get_oauth_client_secret() -> Option<String> {
std::env::var("GEMINI_OAUTH_CLIENT_SECRET").ok()
}
#[allow(dead_code)]
pub const GEMINI_MODELS: &[&str] = &[
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.5-flash-lite",
"gemini-2.5-pro",
"gemini-2.5-pro-preview-06-05",
"gemini-2.5-flash-preview-09-2025",
@@ -122,14 +123,20 @@ pub struct GeminiProvider {
pub client: Client,
}
impl GeminiProvider {
pub fn new() -> Self {
impl Default for GeminiProvider {
fn default() -> Self {
Self {
credentials: GeminiCredentials::default(),
project_id: None,
client: Client::new(),
}
}
}
impl GeminiProvider {
pub fn new() -> Self {
Self::default()
}
pub fn default_creds_path() -> PathBuf {
dirs::home_dir()
@@ -140,13 +147,13 @@ impl GeminiProvider {
pub fn load_credentials(&mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
let path = Self::default_creds_path();
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let creds: GeminiCredentials = serde_json::from_str(&content)?;
self.credentials = creds;
}
Ok(())
}
@@ -173,14 +180,16 @@ impl GeminiProvider {
}
pub async fn refresh_token(&mut self) -> Result<String, Box<dyn Error + Send + Sync>> {
let refresh_token = self.credentials.refresh_token.as_ref()
let refresh_token = self
.credentials
.refresh_token
.as_ref()
.ok_or("No refresh token available")?;
let client_id = get_oauth_client_id()
.ok_or("GEMINI_OAUTH_CLIENT_ID not set")?;
let client_secret = get_oauth_client_secret()
.ok_or("GEMINI_OAUTH_CLIENT_SECRET not set")?;
let client_id = get_oauth_client_id().ok_or("GEMINI_OAUTH_CLIENT_ID not set")?;
let client_secret =
get_oauth_client_secret().ok_or("GEMINI_OAUTH_CLIENT_SECRET not set")?;
let params = [
("client_id", client_id.as_str()),
("client_secret", client_secret.as_str()),
@@ -188,7 +197,8 @@ impl GeminiProvider {
("grant_type", "refresh_token"),
];
let resp = self.client
let resp = self
.client
.post("https://oauth2.googleapis.com/token")
.form(&params)
.send()
@@ -197,21 +207,20 @@ impl GeminiProvider {
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Token refresh failed: {} - {}", status, body).into());
return Err(format!("Token refresh failed: {status} - {body}").into());
}
let data: serde_json::Value = resp.json().await?;
let new_token = data["access_token"]
.as_str()
.ok_or("No access token in response")?;
self.credentials.access_token = Some(new_token.to_string());
if let Some(expires_in) = data["expires_in"].as_i64() {
self.credentials.expiry_date = Some(
chrono::Utc::now().timestamp_millis() + expires_in * 1000
);
self.credentials.expiry_date =
Some(chrono::Utc::now().timestamp_millis() + expires_in * 1000);
}
// Save refreshed credentials
@@ -221,7 +230,7 @@ impl GeminiProvider {
}
pub fn get_api_url(&self, action: &str) -> String {
format!("{}/{}:{}", CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION, action)
format!("{CODE_ASSIST_ENDPOINT}/{CODE_ASSIST_API_VERSION}:{action}")
}
pub async fn call_api(
@@ -229,14 +238,18 @@ impl GeminiProvider {
action: &str,
body: &serde_json::Value,
) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
let token = self.credentials.access_token.as_ref()
let token = self
.credentials
.access_token
.as_ref()
.ok_or("No access token")?;
let url = self.get_api_url(action);
let resp = self.client
let resp = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.json(body)
.send()
@@ -245,7 +258,7 @@ impl GeminiProvider {
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("API call failed: {} - {}", status, body).into());
return Err(format!("API call failed: {status} - {body}").into());
}
let data: serde_json::Value = resp.json().await?;
@@ -268,7 +281,7 @@ impl GeminiProvider {
});
let resp = self.call_api("loadCodeAssist", &body).await?;
if let Some(project) = resp["cloudaicompanionProject"].as_str() {
if !project.is_empty() {
self.project_id = Some(project.to_string());
@@ -289,7 +302,7 @@ impl GeminiProvider {
});
let mut lro_resp = self.call_api("onboardUser", &onboard_body).await?;
// Poll until done
for _ in 0..30 {
if lro_resp["done"].as_bool().unwrap_or(false) {
+101 -42
View File
@@ -1,11 +1,10 @@
//! Kiro/CodeWhisperer Provider
use crate::converter::openai_to_cw::convert_openai_to_codewhisperer;
use crate::models::openai::*;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;
use reqwest::Client;
use crate::models::openai::*;
use crate::models::codewhisperer::*;
use crate::converter::openai_to_cw::convert_openai_to_codewhisperer;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -40,14 +39,20 @@ pub struct KiroProvider {
pub client: Client,
}
impl KiroProvider {
pub fn new() -> Self {
impl Default for KiroProvider {
fn default() -> Self {
Self {
credentials: KiroCredentials::default(),
client: Client::new(),
}
}
}
impl KiroProvider {
pub fn new() -> Self {
Self::default()
}
pub fn default_creds_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
@@ -56,27 +61,27 @@ impl KiroProvider {
.join("cache")
.join("kiro-auth-token.json")
}
pub fn load_credentials(&mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
let path = Self::default_creds_path();
let dir = path.parent().unwrap();
let mut merged = KiroCredentials::default();
// 读取主凭证文件
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let creds: KiroCredentials = serde_json::from_str(&content)?;
merge_credentials(&mut merged, &creds);
}
// 读取目录中其他 JSON 文件
if dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let file_path = entry.path();
if file_path.extension().map(|e| e == "json").unwrap_or(false)
&& file_path != path {
if file_path.extension().map(|e| e == "json").unwrap_or(false) && file_path != path
{
if let Ok(content) = std::fs::read_to_string(&file_path) {
if let Ok(creds) = serde_json::from_str::<KiroCredentials>(&content) {
merge_credentials(&mut merged, &creds);
@@ -85,34 +90,47 @@ impl KiroProvider {
}
}
}
self.credentials = merged;
Ok(())
}
pub fn get_base_url(&self) -> String {
let region = self.credentials.region.as_deref().unwrap_or("us-east-1");
format!("https://codewhisperer.{}.amazonaws.com/generateAssistantResponse", region)
format!("https://codewhisperer.{region}.amazonaws.com/generateAssistantResponse")
}
pub fn get_refresh_url(&self) -> String {
let region = self.credentials.region.as_deref().unwrap_or("us-east-1");
let auth_method = self.credentials.auth_method.as_deref().unwrap_or("social");
let auth_method = self
.credentials
.auth_method
.as_deref()
.unwrap_or("social")
.to_lowercase();
if auth_method == "idc" {
format!("https://oidc.{}.amazonaws.com/token", region)
format!("https://oidc.{region}.amazonaws.com/token")
} else {
format!("https://prod.{}.auth.desktop.kiro.dev/refreshToken", region)
format!("https://prod.{region}.auth.desktop.kiro.dev/refreshToken")
}
}
pub async fn refresh_token(&mut self) -> Result<String, Box<dyn Error + Send + Sync>> {
let refresh_token = self.credentials.refresh_token.as_ref()
let refresh_token = self
.credentials
.refresh_token
.as_ref()
.ok_or("No refresh token")?;
let auth_method = self.credentials.auth_method.as_deref().unwrap_or("social");
let auth_method = self
.credentials
.auth_method
.as_deref()
.unwrap_or("social")
.to_lowercase();
let refresh_url = self.get_refresh_url();
let body = if auth_method == "idc" {
serde_json::json!({
"refreshToken": refresh_token,
@@ -123,60 +141,101 @@ impl KiroProvider {
} else {
serde_json::json!({ "refreshToken": refresh_token })
};
let resp = self.client
let resp = self
.client
.post(&refresh_url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
return Err(format!("Refresh failed: {}", resp.status()).into());
let status = resp.status();
let body_text = resp.text().await.unwrap_or_default();
return Err(format!("Refresh failed: {status} {body_text}").into());
}
let data: serde_json::Value = resp.json().await?;
let new_token = data["accessToken"]
.as_str()
.ok_or("No access token in response")?;
self.credentials.access_token = Some(new_token.to_string());
if let Some(rt) = data["refreshToken"].as_str() {
self.credentials.refresh_token = Some(rt.to_string());
}
if let Some(arn) = data["profileArn"].as_str() {
self.credentials.profile_arn = Some(arn.to_string());
}
// 保存更新后的凭证到文件
self.save_credentials()?;
Ok(new_token.to_string())
}
pub fn save_credentials(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
let path = Self::default_creds_path();
// 读取现有文件内容
let mut existing: serde_json::Value = if path.exists() {
let content = std::fs::read_to_string(&path)?;
serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
} else {
serde_json::json!({})
};
// 更新字段
if let Some(token) = &self.credentials.access_token {
existing["accessToken"] = serde_json::json!(token);
}
if let Some(token) = &self.credentials.refresh_token {
existing["refreshToken"] = serde_json::json!(token);
}
if let Some(arn) = &self.credentials.profile_arn {
existing["profileArn"] = serde_json::json!(arn);
}
// 写回文件
let content = serde_json::to_string_pretty(&existing)?;
std::fs::write(&path, content)?;
Ok(())
}
pub async fn call_api(
&self,
request: &ChatCompletionRequest,
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
let token = self.credentials.access_token.as_ref()
let token = self
.credentials
.access_token
.as_ref()
.ok_or("No access token")?;
let profile_arn = if self.credentials.auth_method.as_deref() == Some("social") {
self.credentials.profile_arn.clone()
} else {
None
};
let cw_request = convert_openai_to_codewhisperer(request, profile_arn);
let url = self.get_base_url();
let resp = self.client
let resp = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("amz-sdk-invocation-id", uuid::Uuid::new_v4().to_string())
.json(&cw_request)
.send()
.await?;
Ok(resp)
}
}
+13 -8
View File
@@ -1,11 +1,16 @@
pub mod kiro;
pub mod gemini;
pub mod qwen;
pub mod openai_custom;
pub mod claude_custom;
pub mod gemini;
pub mod kiro;
pub mod openai_custom;
pub mod qwen;
pub use kiro::KiroProvider;
pub use gemini::GeminiProvider;
pub use qwen::QwenProvider;
pub use openai_custom::OpenAICustomProvider;
#[allow(unused_imports)]
pub use claude_custom::ClaudeCustomProvider;
#[allow(unused_imports)]
pub use gemini::GeminiProvider;
#[allow(unused_imports)]
pub use kiro::KiroProvider;
#[allow(unused_imports)]
pub use openai_custom::OpenAICustomProvider;
#[allow(unused_imports)]
pub use qwen::QwenProvider;
+28 -13
View File
@@ -1,7 +1,7 @@
//! OpenAI Custom Provider (自定义 OpenAI 兼容 API)
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::error::Error;
use reqwest::Client;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OpenAICustomConfig {
@@ -15,16 +15,23 @@ pub struct OpenAICustomProvider {
pub client: Client,
}
impl OpenAICustomProvider {
pub fn new() -> Self {
impl Default for OpenAICustomProvider {
fn default() -> Self {
Self {
config: OpenAICustomConfig::default(),
client: Client::new(),
}
}
}
impl OpenAICustomProvider {
pub fn new() -> Self {
Self::default()
}
pub fn get_base_url(&self) -> String {
self.config.base_url
self.config
.base_url
.clone()
.unwrap_or_else(|| "https://api.openai.com/v1".to_string())
}
@@ -37,15 +44,19 @@ impl OpenAICustomProvider {
&self,
request: &serde_json::Value,
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
let api_key = self.config.api_key.as_ref()
let api_key = self
.config
.api_key
.as_ref()
.ok_or("OpenAI API key not configured")?;
let base_url = self.get_base_url();
let url = format!("{}/chat/completions", base_url);
let url = format!("{base_url}/chat/completions");
let resp = self.client
let resp = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Authorization", format!("Bearer {api_key}"))
.header("Content-Type", "application/json")
.json(request)
.send()
@@ -55,22 +66,26 @@ impl OpenAICustomProvider {
}
pub async fn list_models(&self) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
let api_key = self.config.api_key.as_ref()
let api_key = self
.config
.api_key
.as_ref()
.ok_or("OpenAI API key not configured")?;
let base_url = self.get_base_url();
let url = format!("{}/models", base_url);
let url = format!("{base_url}/models");
let resp = self.client
let resp = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Authorization", format!("Bearer {api_key}"))
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Failed to list models: {} - {}", status, body).into());
return Err(format!("Failed to list models: {status} - {body}").into());
}
let data: serde_json::Value = resp.json().await?;
+38 -27
View File
@@ -1,18 +1,15 @@
//! Qwen (通义千问) OAuth Provider
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;
use reqwest::Client;
// Constants
const QWEN_DIR: &str = ".qwen";
const CREDENTIALS_FILE: &str = "oauth_creds.json";
const QWEN_BASE_URL: &str = "https://portal.qwen.ai/v1";
pub const QWEN_MODELS: &[&str] = &[
"qwen3-coder-plus",
"qwen3-coder-flash",
];
pub const QWEN_MODELS: &[&str] = &["qwen3-coder-plus", "qwen3-coder-flash"];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QwenCredentials {
@@ -40,13 +37,19 @@ pub struct QwenProvider {
pub client: Client,
}
impl QwenProvider {
pub fn new() -> Self {
impl Default for QwenProvider {
fn default() -> Self {
Self {
credentials: QwenCredentials::default(),
client: Client::new(),
}
}
}
impl QwenProvider {
pub fn new() -> Self {
Self::default()
}
pub fn default_creds_path() -> PathBuf {
dirs::home_dir()
@@ -57,13 +60,13 @@ impl QwenProvider {
pub fn load_credentials(&mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
let path = Self::default_creds_path();
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let creds: QwenCredentials = serde_json::from_str(&content)?;
self.credentials = creds;
}
Ok(())
}
@@ -90,25 +93,29 @@ impl QwenProvider {
}
pub fn get_base_url(&self) -> String {
self.credentials.resource_url
self.credentials
.resource_url
.as_ref()
.map(|url| {
let normalized = if url.starts_with("http") {
url.clone()
} else {
format!("https://{}", url)
format!("https://{url}")
};
if normalized.ends_with("/v1") {
normalized
} else {
format!("{}/v1", normalized)
format!("{normalized}/v1")
}
})
.unwrap_or_else(|| QWEN_BASE_URL.to_string())
}
pub async fn refresh_token(&mut self) -> Result<String, Box<dyn Error + Send + Sync>> {
let refresh_token = self.credentials.refresh_token.as_ref()
let refresh_token = self
.credentials
.refresh_token
.as_ref()
.ok_or("No refresh token available")?;
let client_id = std::env::var("QWEN_OAUTH_CLIENT_ID")
@@ -122,7 +129,8 @@ impl QwenProvider {
"client_id": client_id
});
let resp = self.client
let resp = self
.client
.post("https://chat.qwen.ai/api/v1/oauth2/token")
.header("Content-Type", "application/json")
.json(&body)
@@ -132,29 +140,28 @@ impl QwenProvider {
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Token refresh failed: {} - {}", status, body).into());
return Err(format!("Token refresh failed: {status} - {body}").into());
}
let data: serde_json::Value = resp.json().await?;
let new_token = data["access_token"]
.as_str()
.ok_or("No access token in response")?;
self.credentials.access_token = Some(new_token.to_string());
if let Some(rt) = data["refresh_token"].as_str() {
self.credentials.refresh_token = Some(rt.to_string());
}
if let Some(resource_url) = data["resource_url"].as_str() {
self.credentials.resource_url = Some(resource_url.to_string());
}
if let Some(expires_in) = data["expires_in"].as_i64() {
self.credentials.expiry_date = Some(
chrono::Utc::now().timestamp_millis() + expires_in * 1000
);
self.credentials.expiry_date =
Some(chrono::Utc::now().timestamp_millis() + expires_in * 1000);
}
// Save refreshed credentials
@@ -167,11 +174,14 @@ impl QwenProvider {
&self,
request: &serde_json::Value,
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
let token = self.credentials.access_token.as_ref()
let token = self
.credentials
.access_token
.as_ref()
.ok_or("No access token")?;
let base_url = self.get_base_url();
let url = format!("{}/chat/completions", base_url);
let url = format!("{base_url}/chat/completions");
// Ensure model is valid
let mut req_body = request.clone();
@@ -181,9 +191,10 @@ impl QwenProvider {
}
}
let resp = self.client
let resp = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.header("X-DashScope-AuthType", "qwen-oauth")
.json(&req_body)
+114 -62
View File
@@ -1,24 +1,24 @@
//! HTTP API 服务器
use crate::config::Config;
use crate::models::openai::*;
use crate::models::anthropic::*;
use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
use crate::providers::kiro::KiroProvider;
use crate::providers::gemini::GeminiProvider;
use crate::providers::qwen::QwenProvider;
use crate::providers::openai_custom::OpenAICustomProvider;
use crate::providers::claude_custom::ClaudeCustomProvider;
use crate::logger::LogStore;
use crate::models::anthropic::*;
use crate::models::openai::*;
use crate::providers::claude_custom::ClaudeCustomProvider;
use crate::providers::gemini::GeminiProvider;
use crate::providers::kiro::KiroProvider;
use crate::providers::openai_custom::OpenAICustomProvider;
use crate::providers::qwen::QwenProvider;
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{oneshot, RwLock};
use axum::{
routing::{get, post},
Router, Json,
extract::State,
http::{StatusCode, HeaderMap},
response::{IntoResponse, Response},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerStatus {
@@ -46,16 +46,16 @@ impl ServerState {
pub fn new(config: Config) -> Self {
let mut kiro = KiroProvider::new();
let _ = kiro.load_credentials();
let mut gemini = GeminiProvider::new();
let _ = gemini.load_credentials();
let mut qwen = QwenProvider::new();
let _ = qwen.load_credentials();
let openai_custom = OpenAICustomProvider::new();
let claude_custom = ClaudeCustomProvider::new();
Self {
config,
running: false,
@@ -80,7 +80,10 @@ impl ServerState {
}
}
pub async fn start(&mut self, logs: Arc<RwLock<LogStore>>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
pub async fn start(
&mut self,
logs: Arc<RwLock<LogStore>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if self.running {
return Ok(());
}
@@ -91,7 +94,7 @@ impl ServerState {
let host = self.config.server.host.clone();
let port = self.config.server.port;
let api_key = self.config.server.api_key.clone();
// 重新加载凭证
let _ = self.kiro_provider.load_credentials();
let kiro = self.kiro_provider.clone();
@@ -154,9 +157,9 @@ async fn run_server(
.route("/v1/messages/count_tokens", post(count_tokens))
.with_state(state);
let addr: std::net::SocketAddr = format!("{}:{}", host, port).parse()?;
let addr: std::net::SocketAddr = format!("{host}:{port}").parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("Server listening on {}", addr);
axum::serve(listener, app)
@@ -197,27 +200,33 @@ async fn models() -> impl IntoResponse {
}))
}
async fn verify_api_key(headers: &HeaderMap, expected_key: &str) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
let auth = headers.get("authorization")
async fn verify_api_key(
headers: &HeaderMap,
expected_key: &str,
) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
let auth = headers
.get("authorization")
.or_else(|| headers.get("x-api-key"))
.and_then(|v| v.to_str().ok());
let key = match auth {
Some(s) if s.starts_with("Bearer ") => &s[7..],
Some(s) => s,
None => return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": {"message": "No API key provided"}}))
)),
None => {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": {"message": "No API key provided"}})),
))
}
};
if key != expected_key {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": {"message": "Invalid API key"}}))
Json(serde_json::json!({"error": {"message": "Invalid API key"}})),
));
}
Ok(())
}
@@ -227,21 +236,32 @@ async fn chat_completions(
Json(request): Json<ChatCompletionRequest>,
) -> Response {
if let Err(e) = verify_api_key(&headers, &state.api_key).await {
state.logs.write().await.add("warn", "Unauthorized request to /v1/chat/completions");
state
.logs
.write()
.await
.add("warn", "Unauthorized request to /v1/chat/completions");
return e.into_response();
}
state.logs.write().await.add("info", &format!("POST /v1/chat/completions model={}", request.model));
state.logs.write().await.add(
"info",
&format!("POST /v1/chat/completions model={}", request.model),
);
let kiro = state.kiro.read().await;
match kiro.call_api(&request).await {
Ok(resp) => {
if resp.status().is_success() {
// 解析 CodeWhisperer 响应并转换
match resp.text().await {
Ok(body) => {
state.logs.write().await.add("info", "Request completed successfully");
state
.logs
.write()
.await
.add("info", "Request completed successfully");
let response = serde_json::json!({
"id": format!("chatcmpl-{}", uuid::Uuid::new_v4()),
"object": "chat.completion",
@@ -268,8 +288,9 @@ async fn chat_completions(
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": {"message": e.to_string()}}))
).into_response()
Json(serde_json::json!({"error": {"message": e.to_string()}})),
)
.into_response(),
}
} else {
let status = resp.status();
@@ -282,8 +303,9 @@ async fn chat_completions(
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": {"message": e.to_string()}}))
).into_response()
Json(serde_json::json!({"error": {"message": e.to_string()}})),
)
.into_response(),
}
}
@@ -293,23 +315,34 @@ async fn anthropic_messages(
Json(request): Json<AnthropicMessagesRequest>,
) -> Response {
if let Err(e) = verify_api_key(&headers, &state.api_key).await {
state.logs.write().await.add("warn", "Unauthorized request to /v1/messages");
state
.logs
.write()
.await
.add("warn", "Unauthorized request to /v1/messages");
return e.into_response();
}
state.logs.write().await.add("info", &format!("POST /v1/messages (Anthropic) model={}", request.model));
state.logs.write().await.add(
"info",
&format!("POST /v1/messages (Anthropic) model={}", request.model),
);
// 转换为 OpenAI 格式
let openai_request = convert_anthropic_to_openai(&request);
let kiro = state.kiro.read().await;
match kiro.call_api(&openai_request).await {
Ok(resp) => {
if resp.status().is_success() {
match resp.text().await {
Ok(body) => {
let content = extract_content_from_cw_response(&body);
state.logs.write().await.add("info", "Anthropic request completed successfully");
state
.logs
.write()
.await
.add("info", "Anthropic request completed successfully");
// 返回 Anthropic 格式响应
let response = serde_json::json!({
"id": format!("msg_{}", uuid::Uuid::new_v4()),
@@ -326,17 +359,29 @@ async fn anthropic_messages(
Json(response).into_response()
}
Err(e) => {
state.logs.write().await.add("error", &format!("Response parse error: {}", e));
state
.logs
.write()
.await
.add("error", &format!("Response parse error: {e}"));
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": {"message": e.to_string()}}))
).into_response()
Json(serde_json::json!({"error": {"message": e.to_string()}})),
)
.into_response()
}
}
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
state.logs.write().await.add("error", &format!("Upstream error {}: {}", status, &body[..body.len().min(200)]));
state.logs.write().await.add(
"error",
&format!(
"Upstream error {}: {}",
status,
&body[..body.len().min(200)]
),
);
(
StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
Json(serde_json::json!({"error": {"message": format!("Upstream error: {}", body)}}))
@@ -344,11 +389,16 @@ async fn anthropic_messages(
}
}
Err(e) => {
state.logs.write().await.add("error", &format!("API call failed: {}", e));
state
.logs
.write()
.await
.add("error", &format!("API call failed: {e}"));
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": {"message": e.to_string()}}))
).into_response()
Json(serde_json::json!({"error": {"message": e.to_string()}})),
)
.into_response()
}
}
}
@@ -361,26 +411,28 @@ async fn count_tokens(
if let Err(e) = verify_api_key(&headers, &state.api_key).await {
return e.into_response();
}
// Claude Code 需要这个端点,返回估算值
Json(serde_json::json!({
"input_tokens": 100
})).into_response()
}))
.into_response()
}
fn extract_content_from_cw_response(body: &str) -> String {
// CodeWhisperer 返回 AWS Event Stream 格式
// 使用正则提取 JSON 内容
let mut content = String::new();
// 查找所有 {"content":"..."} 模式
let re = regex::Regex::new(r#"\{"content":"([^"\\]*(\\.[^"\\]*)*)"\}"#).ok();
if let Some(re) = re {
for cap in re.captures_iter(body) {
if let Some(text) = cap.get(1) {
// 处理转义字符
let unescaped = text.as_str()
let unescaped = text
.as_str()
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\\"", "\"")
@@ -389,7 +441,7 @@ fn extract_content_from_cw_response(body: &str) -> String {
}
}
}
if content.is_empty() {
// 备用方案:查找 assistantResponseEvent
if let Some(start) = body.find(r#""content":""#) {
@@ -399,7 +451,7 @@ fn extract_content_from_cw_response(body: &str) -> String {
}
}
}
if content.is_empty() {
"Response received but could not parse content".to_string()
} else {