Skip to content

Commit 9acea35

Browse files
committed
v1.6.0: 历史消息管理 + Gemini 工具调用
新功能: - 历史消息管理: 4 种策略处理对话长度限制 - 自动截断: 发送前按数量/字符数截断 - 智能摘要: 用 AI 生成早期对话摘要 - 错误重试: 遇到长度错误时自动截断重试 (默认) - 预估检测: 预估 token 数量,超限预先截断 - WebUI 设置页面: 可配置历史消息管理策略 - Gemini 完整工具调用支持: - functionDeclarations 工具定义 - functionCall 响应处理 - functionResponse 工具结果 - toolConfig.mode 支持 改进: - 三种协议 (Anthropic/OpenAI/Gemini) 统一集成历史消息管理 - Gemini handler 增强: Token 刷新、配额管理、错误重试 - 错误检测优化: 更准确识别内容长度超限错误 - 文档更新: 工具调用支持说明
1 parent 34bb55c commit 9acea35

11 files changed

Lines changed: 1217 additions & 138 deletions

File tree

README.md

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,27 @@
2020

2121
> **⚠️ 测试说明**
2222
>
23-
> 本项目主要针对 **Claude Code (VSCode 插件版)** 进行测试,工具调用功能已验证可用。
24-
>
25-
> 其他客户端(Codex CLI、Gemini CLI、Claude Code CLI 等)理论上兼容,但未经充分测试,如遇问题欢迎反馈。
23+
> 本项目支持 **Claude Code****Codex CLI****Gemini CLI** 三种客户端,工具调用功能已全面支持。
2624
2725
## 功能特性
2826

2927
### 核心功能
3028
- **多协议支持** - OpenAI / Anthropic / Gemini 三种协议兼容
31-
- **工具调用支持** - 支持 Claude Code 的工具调用功能
29+
- **完整工具调用** - 三种协议的工具调用功能全面支持
3230
- **多账号轮询** - 支持添加多个 Kiro 账号,自动负载均衡
3331
- **会话粘性** - 同一会话 60 秒内使用同一账号,保持上下文
34-
- **Web UI** - 简洁的管理界面,支持对话测试、监控、日志查看
35-
36-
### v1.5.0 新功能
32+
- **Web UI** - 简洁的管理界面,支持监控、日志、设置
33+
34+
### v1.6.0 新功能
35+
- **历史消息管理** - 4 种策略处理对话长度限制,可自由组合
36+
- 自动截断:发送前按数量/字符数截断
37+
- 智能摘要:用 AI 生成早期对话摘要,保留关键信息
38+
- 错误重试:遇到长度错误时自动截断重试(默认启用)
39+
- 预估检测:预估 token 数量,超限预先截断
40+
- **Gemini 工具调用** - 完整支持 functionDeclarations/functionCall/functionResponse
41+
- **设置页面** - WebUI 新增设置标签页,可配置历史消息管理策略
42+
43+
### v1.5.0 功能
3744
- **用量查询** - 查询账号配额使用情况,显示已用/余额/使用率
3845
- **多登录方式** - 支持 Google / GitHub / AWS Builder ID 三种登录方式
3946
- **流量监控** - 完整的 LLM 请求监控,支持搜索、过滤、导出
@@ -46,12 +53,16 @@
4653
- **请求统计增强** - 按账号/模型统计,24 小时趋势
4754
- **请求重试机制** - 网络错误/5xx 自动重试,指数退避
4855

49-
### v1.3.0 功能
50-
- **Token 自动刷新** - 检测过期自动刷新,支持 Social 认证
51-
- **动态 Machine ID** - 每个账号独立指纹,基于凭证 + 时间因子生成
52-
- **配额管理** - 429 自动检测、冷却 (300s)、自动恢复
53-
- **自动账号切换** - 配额超限时自动切换到下一个可用账号
54-
- **配置持久化** - 账号配置保存到 `~/.kiro-proxy/config.json`,重启不丢失
56+
## 工具调用支持
57+
58+
| 功能 | Anthropic (Claude Code) | OpenAI (Codex CLI) | Gemini |
59+
|------|------------------------|-------------------|--------|
60+
| 工具定义 |`tools` |`tools.function` |`functionDeclarations` |
61+
| 工具调用响应 |`tool_use` |`tool_calls` |`functionCall` |
62+
| 工具结果 |`tool_result` |`tool` 角色消息 |`functionResponse` |
63+
| 强制工具调用 |`tool_choice` |`tool_choice` |`toolConfig.mode` |
64+
| 工具数量限制 | ✅ 50 个 | ✅ 50 个 | ✅ 50 个 |
65+
| 历史消息修复 ||||
5566

5667
## 已知限制
5768

@@ -63,13 +74,19 @@ Kiro API 有输入长度限制。当对话历史过长时,会返回错误:
6374
Input is too long. (CONTENT_LENGTH_EXCEEDS_THRESHOLD)
6475
```
6576

66-
**这是 Kiro 服务端的限制,无法绕过。**
77+
#### 自动处理(v1.6.0+)
78+
79+
代理内置了历史消息管理功能,可在「设置」页面配置:
80+
81+
- **错误重试**(默认):遇到长度错误时自动截断并重试
82+
- **智能摘要**:用 AI 生成早期对话摘要,保留关键信息
83+
- **自动截断**:每次请求前按数量/字符数截断
84+
- **预估检测**:预估 token 数量,超限预先截断
6785

68-
#### 解决方案
86+
#### 手动处理
6987

70-
1. **清空对话历史** - 在 Claude Code 中输入 `/clear` 清空当前会话
71-
2. **恢复工作进度** - 清空后,告诉 Claude 你之前在做什么,它会读取代码文件恢复上下文
72-
3. **预防措施** - 复杂任务分阶段完成,每个阶段结束后 `/clear` 开始新会话
88+
1. 在 Claude Code 中输入 `/clear` 清空对话历史
89+
2. 告诉 AI 你之前在做什么,它会读取代码文件恢复上下文
7390

7491
## 快速开始
7592

kiro_proxy/converters.py

Lines changed: 242 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -665,41 +665,267 @@ def convert_kiro_response_to_openai(result: dict, model: str, msg_id: str) -> di
665665

666666
# ==================== Gemini 转换 ====================
667667

668-
def convert_gemini_contents_to_kiro(contents: List[dict], system_instruction: dict, model: str) -> Tuple[str, List[dict]]:
669-
"""将 Gemini 消息格式转换为 Kiro 格式"""
668+
def convert_gemini_tools_to_kiro(tools: List[dict]) -> List[dict]:
669+
"""将 Gemini 工具格式转换为 Kiro 格式
670+
671+
Gemini 工具格式:
672+
{
673+
"functionDeclarations": [
674+
{
675+
"name": "get_weather",
676+
"description": "Get weather info",
677+
"parameters": {...}
678+
}
679+
]
680+
}
681+
"""
682+
kiro_tools = []
683+
function_count = 0
684+
685+
for tool in tools:
686+
# Gemini 的工具定义在 functionDeclarations 中
687+
declarations = tool.get("functionDeclarations", [])
688+
689+
for func in declarations:
690+
# 限制工具数量
691+
if function_count >= MAX_TOOLS:
692+
break
693+
function_count += 1
694+
695+
name = func.get("name", "")
696+
description = func.get("description", f"Tool: {name}")
697+
description = truncate_description(description)
698+
parameters = func.get("parameters", {"type": "object", "properties": {}})
699+
700+
kiro_tools.append({
701+
"toolSpecification": {
702+
"name": name,
703+
"description": description,
704+
"inputSchema": {
705+
"json": parameters
706+
}
707+
}
708+
})
709+
710+
return kiro_tools
711+
712+
713+
def convert_gemini_contents_to_kiro(
714+
contents: List[dict],
715+
system_instruction: dict,
716+
model: str,
717+
tools: List[dict] = None,
718+
tool_config: dict = None
719+
) -> Tuple[str, List[dict], List[dict], List[dict]]:
720+
"""将 Gemini 消息格式转换为 Kiro 格式
721+
722+
增强:
723+
- 支持 functionCall 和 functionResponse
724+
- 支持 tool_config
725+
726+
Returns:
727+
(user_content, history, tool_results, kiro_tools)
728+
"""
670729
history = []
671730
user_content = ""
731+
current_tool_results = []
732+
pending_tool_results = []
672733

734+
# 处理 system instruction
673735
system_text = ""
674736
if system_instruction:
675737
parts = system_instruction.get("parts", [])
676738
system_text = " ".join(p.get("text", "") for p in parts if "text" in p)
677739

678-
for content in contents:
740+
# 处理 tool_config(类似 tool_choice)
741+
tool_instruction = ""
742+
if tool_config:
743+
mode = tool_config.get("functionCallingConfig", {}).get("mode", "")
744+
if mode in ("ANY", "REQUIRED"):
745+
tool_instruction = "\n\n[CRITICAL INSTRUCTION] You MUST use one of the provided tools to respond. Do NOT respond with plain text."
746+
747+
for i, content in enumerate(contents):
679748
role = content.get("role", "user")
680749
parts = content.get("parts", [])
681-
text = " ".join(p.get("text", "") for p in parts if "text" in p)
750+
is_last = (i == len(contents) - 1)
751+
752+
# 提取文本和工具调用
753+
text_parts = []
754+
tool_calls = []
755+
tool_responses = []
756+
757+
for part in parts:
758+
if "text" in part:
759+
text_parts.append(part["text"])
760+
elif "functionCall" in part:
761+
# Gemini 的工具调用
762+
fc = part["functionCall"]
763+
tool_calls.append({
764+
"toolUseId": fc.get("name", "") + "_" + str(i), # Gemini 没有 ID,生成一个
765+
"name": fc.get("name", ""),
766+
"input": fc.get("args", {})
767+
})
768+
elif "functionResponse" in part:
769+
# Gemini 的工具响应
770+
fr = part["functionResponse"]
771+
response_content = fr.get("response", {})
772+
if isinstance(response_content, dict):
773+
response_text = json.dumps(response_content)
774+
else:
775+
response_text = str(response_content)
776+
777+
tool_responses.append({
778+
"content": [{"text": response_text}],
779+
"status": "success",
780+
"toolUseId": fr.get("name", "") + "_" + str(i - 1) # 匹配上一个调用
781+
})
782+
783+
text = " ".join(text_parts)
682784

683785
if role == "user":
786+
# 处理待处理的 tool responses
787+
if pending_tool_results:
788+
seen_ids = set()
789+
unique_results = []
790+
for tr in pending_tool_results:
791+
if tr["toolUseId"] not in seen_ids:
792+
seen_ids.add(tr["toolUseId"])
793+
unique_results.append(tr)
794+
795+
history.append({
796+
"userInputMessage": {
797+
"content": "Tool results provided.",
798+
"modelId": model,
799+
"origin": "AI_EDITOR",
800+
"userInputMessageContext": {
801+
"toolResults": unique_results
802+
}
803+
}
804+
})
805+
pending_tool_results = []
806+
807+
# 处理 functionResponse(用户消息中的工具响应)
808+
if tool_responses:
809+
pending_tool_results.extend(tool_responses)
810+
811+
# 合并 system prompt
684812
if system_text and not history:
685-
text = f"{system_text}\n\n{text}"
686-
history.append({
687-
"userInputMessage": {
688-
"content": text,
689-
"modelId": model,
690-
"origin": "AI_EDITOR"
691-
}
692-
})
693-
user_content = text
813+
text = f"{system_text}{tool_instruction}\n\n{text}"
814+
815+
if is_last:
816+
user_content = text
817+
if pending_tool_results:
818+
current_tool_results = pending_tool_results
819+
pending_tool_results = []
820+
else:
821+
if text:
822+
history.append({
823+
"userInputMessage": {
824+
"content": text,
825+
"modelId": model,
826+
"origin": "AI_EDITOR"
827+
}
828+
})
829+
694830
elif role == "model":
831+
# 处理待处理的 tool responses
832+
if pending_tool_results:
833+
seen_ids = set()
834+
unique_results = []
835+
for tr in pending_tool_results:
836+
if tr["toolUseId"] not in seen_ids:
837+
seen_ids.add(tr["toolUseId"])
838+
unique_results.append(tr)
839+
840+
history.append({
841+
"userInputMessage": {
842+
"content": "Tool results provided.",
843+
"modelId": model,
844+
"origin": "AI_EDITOR",
845+
"userInputMessageContext": {
846+
"toolResults": unique_results
847+
}
848+
}
849+
})
850+
pending_tool_results = []
851+
852+
assistant_text = text if text else "I understand."
853+
695854
history.append({
696855
"assistantResponseMessage": {
697-
"content": text if text else "I understand.",
698-
"toolUses": []
856+
"content": assistant_text,
857+
"toolUses": tool_calls
699858
}
700859
})
701860

861+
# 处理末尾的 tool results
862+
if pending_tool_results:
863+
current_tool_results = pending_tool_results
864+
if not user_content:
865+
user_content = "Tool results provided."
866+
867+
# 如果没有用户消息
868+
if not user_content:
869+
if contents:
870+
last_parts = contents[-1].get("parts", [])
871+
user_content = " ".join(p.get("text", "") for p in last_parts if "text" in p)
872+
if not user_content:
873+
user_content = "Continue"
874+
702875
# 修复历史交替
703876
history = fix_history_alternation(history, model)
704877

705-
return user_content, history[:-1] if history else []
878+
# 移除最后一条(当前用户消息)
879+
if history and "userInputMessage" in history[-1]:
880+
history = history[:-1]
881+
882+
# 转换工具
883+
kiro_tools = convert_gemini_tools_to_kiro(tools) if tools else []
884+
885+
return user_content, history, current_tool_results, kiro_tools
886+
887+
888+
def convert_kiro_response_to_gemini(result: dict, model: str) -> dict:
889+
"""将 Kiro 响应转换为 Gemini 格式"""
890+
text = "".join(result.get("content", []))
891+
tool_uses = result.get("tool_uses", [])
892+
893+
parts = []
894+
895+
# 添加文本部分
896+
if text:
897+
parts.append({"text": text})
898+
899+
# 添加工具调用
900+
for tool_use in tool_uses:
901+
if tool_use.get("type") == "tool_use":
902+
parts.append({
903+
"functionCall": {
904+
"name": tool_use.get("name", ""),
905+
"args": tool_use.get("input", {})
906+
}
907+
})
908+
909+
# 映射 stop_reason
910+
stop_reason = result.get("stop_reason", "STOP")
911+
finish_reason = "STOP"
912+
if tool_uses:
913+
finish_reason = "TOOL_CALLS"
914+
elif stop_reason == "max_tokens":
915+
finish_reason = "MAX_TOKENS"
916+
917+
return {
918+
"candidates": [{
919+
"content": {
920+
"parts": parts,
921+
"role": "model"
922+
},
923+
"finishReason": finish_reason,
924+
"index": 0
925+
}],
926+
"usageMetadata": {
927+
"promptTokenCount": 100,
928+
"candidatesTokenCount": 100,
929+
"totalTokenCount": 200
930+
}
931+
}

kiro_proxy/core/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from .browser import detect_browsers, open_url, get_browsers_info
99
from .flow_monitor import flow_monitor, FlowMonitor, LLMFlow, FlowState, TokenUsage
1010
from .usage import get_usage_limits, get_account_usage, UsageInfo
11+
from .history_manager import (
12+
HistoryManager, HistoryConfig, TruncateStrategy,
13+
get_history_config, set_history_config, update_history_config,
14+
is_content_length_error
15+
)
1116

1217
__all__ = [
1318
"state", "ProxyState", "RequestLog", "Account",
@@ -16,5 +21,8 @@
1621
"scheduler", "stats_manager",
1722
"detect_browsers", "open_url", "get_browsers_info",
1823
"flow_monitor", "FlowMonitor", "LLMFlow", "FlowState", "TokenUsage",
19-
"get_usage_limits", "get_account_usage", "UsageInfo"
24+
"get_usage_limits", "get_account_usage", "UsageInfo",
25+
"HistoryManager", "HistoryConfig", "TruncateStrategy",
26+
"get_history_config", "set_history_config", "update_history_config",
27+
"is_content_length_error"
2028
]

0 commit comments

Comments
 (0)