mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
@@ -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
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
@@ -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
@@ -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>>;
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
//! CodeWhisperer/Kiro API 数据模型
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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(¶ms)
|
||||
.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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user