feat(providers): add Anthropic provider with comprehensive debug logging
This commit introduces dedicated Anthropic provider support and enhanced debugging capabilities for LLM provider operations. Changes: - Add dedicated AnthropicProvider implementation for Claude models - Enhance HTTP provider with detailed debug logging throughout request/response lifecycle - Add comprehensive logging for provider creation, API calls, and error conditions - Include debugging documentation for LLM and skills troubleshooting Benefits: - Better debugging experience when troubleshooting LLM API issues - Native Anthropic API support with proper request/response handling - Detailed logs for API key validation, request bodies, and response status - Documentation to guide users through common debugging scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ddd6fca1be
commit
c9db46afd0
4 changed files with 1058 additions and 1 deletions
300
docs/llm-debug-guide.md
Normal file
300
docs/llm-debug-guide.md
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
# LLM Debug 日志指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档说明如何使用新增的 LLM 调试日志来排查 API 调用问题,特别是 `404 page not found` 错误。
|
||||||
|
|
||||||
|
## 新增的调试日志
|
||||||
|
|
||||||
|
### 1. Provider 创建日志 (http_provider.go)
|
||||||
|
|
||||||
|
**位置**: `CreateProvider` 函数
|
||||||
|
|
||||||
|
**日志级别**:
|
||||||
|
- `DebugCF`: 创建 provider 时的详细信息
|
||||||
|
- `InfoCF`: Provider 创建成功
|
||||||
|
- `ErrorCF`: 配置错误
|
||||||
|
|
||||||
|
**记录信息**:
|
||||||
|
```go
|
||||||
|
// 创建时的调试信息
|
||||||
|
logger.DebugCF("llm", "Creating LLM provider", map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"lower_model": lowerModel,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建成功
|
||||||
|
logger.InfoCF("llm", "Provider created successfully", map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"api_base": apiBase,
|
||||||
|
"has_api_key": apiKey != "",
|
||||||
|
"api_key_len": len(apiKey),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. HTTP 请求日志
|
||||||
|
|
||||||
|
**位置**: `HTTPProvider.Chat` 函数
|
||||||
|
|
||||||
|
**发送请求前的日志**:
|
||||||
|
```go
|
||||||
|
logger.DebugCF("llm", "Sending LLM request", map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"model": model,
|
||||||
|
"api_base": p.apiBase,
|
||||||
|
"has_api_key": p.apiKey != "",
|
||||||
|
"api_key_len": len(p.apiKey),
|
||||||
|
"message_count": len(messages),
|
||||||
|
"tools_count": len(tools),
|
||||||
|
"request_body": string(jsonData), // 完整请求体
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**发送请求时的日志**:
|
||||||
|
```go
|
||||||
|
logger.DebugCF("llm", "Sending HTTP request", map[string]interface{}{
|
||||||
|
"method": "POST",
|
||||||
|
"url": fullURL,
|
||||||
|
"headers": req.Header, // 包括 Authorization 等 header
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. HTTP 响应日志
|
||||||
|
|
||||||
|
**成功接收响应**:
|
||||||
|
```go
|
||||||
|
logger.DebugCF("llm", "Received LLM response", map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"content_length": len(body),
|
||||||
|
"response_body": string(body), // 完整响应体
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应 (非 200 状态码)**:
|
||||||
|
```go
|
||||||
|
logger.ErrorCF("llm", "LLM API returned non-OK status", map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"url": fullURL,
|
||||||
|
"response_body": string(body),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 如何启用调试日志
|
||||||
|
|
||||||
|
### 方法 1: 通过配置文件
|
||||||
|
|
||||||
|
修改 `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logging:
|
||||||
|
level: debug # 设置为 debug 级别以查看所有日志
|
||||||
|
category_levels:
|
||||||
|
llm: debug # 只启用 llm 相关的 debug 日志
|
||||||
|
agent: info # 其他类别保持 info 级别
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2: 通过环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PICOCLAW_LOG_LEVEL=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
或者只针对 llm 分类:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PICOCLAW_LOG_CATEGORY_LLM=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## 排查 404 错误的步骤
|
||||||
|
|
||||||
|
当遇到 `Error processing message: LLM call failed: API error: 404 page not found` 错误时:
|
||||||
|
|
||||||
|
### 步骤 1: 检查 Provider 创建日志
|
||||||
|
|
||||||
|
查找日志中的 "Provider created successfully" 消息:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO] [llm] Provider created successfully
|
||||||
|
model: gpt-4
|
||||||
|
api_base: https://api.openai.com/v1
|
||||||
|
has_api_key: true
|
||||||
|
api_key_len: 51
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查点**:
|
||||||
|
- ✅ `api_base` 是否正确?(常见错误:末尾多了 `/chat/completions`)
|
||||||
|
- ✅ `has_api_key` 是否为 true?
|
||||||
|
- ✅ `model` 名称是否正确?
|
||||||
|
|
||||||
|
### 步骤 2: 检查请求 URL
|
||||||
|
|
||||||
|
查找 "Sending LLM request" 日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
[DEBUG] [llm] Sending LLM request
|
||||||
|
url: https://api.openai.com/v1/chat/completions
|
||||||
|
model: gpt-4
|
||||||
|
api_base: https://api.openai.com/v1
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查点**:
|
||||||
|
- ✅ 完整的 `url` 是否正确?
|
||||||
|
- ✅ 路径是否为 `/chat/completions`?
|
||||||
|
- ✅ 是否有重复的路径(如 `/v1/v1/chat/completions`)?
|
||||||
|
|
||||||
|
### 步骤 3: 检查响应详情
|
||||||
|
|
||||||
|
查找 "LLM API returned non-OK status" 错误日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] [llm] LLM API returned non-OK status
|
||||||
|
status_code: 404
|
||||||
|
status: 404 Not Found
|
||||||
|
url: https://wrong-url.com/v1/chat/completions
|
||||||
|
response_body: 404 page not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查点**:
|
||||||
|
- ✅ `status_code` 为 404 表示 URL 路径错误
|
||||||
|
- ✅ `response_body` 可能包含更详细的错误信息
|
||||||
|
- ✅ 对比 `url` 和正确的 API endpoint
|
||||||
|
|
||||||
|
## 常见的 404 错误原因
|
||||||
|
|
||||||
|
### 1. API Base 配置错误
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
openai:
|
||||||
|
api_base: "https://api.openai.com/v1/chat/completions" # ❌ 错误:包含了完整路径
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确配置**:
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
openai:
|
||||||
|
api_base: "https://api.openai.com/v1" # ✅ 正确:只包含 base URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 自定义代理或中转服务配置错误
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
openai:
|
||||||
|
api_base: "https://my-proxy.com" # ❌ 缺少 /v1 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确配置**:
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
openai:
|
||||||
|
api_base: "https://my-proxy.com/v1" # ✅ 包含正确的路径
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. vLLM 或本地模型服务配置
|
||||||
|
|
||||||
|
**正确示例**:
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
vllm:
|
||||||
|
api_base: "http://localhost:8000/v1" # ✅ vLLM 通常也使用 /v1 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试命令
|
||||||
|
|
||||||
|
### 查看完整的调试日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 picoclaw 并查看所有 debug 日志
|
||||||
|
PICOCLAW_LOG_LEVEL=debug ./picoclaw
|
||||||
|
|
||||||
|
# 只看 llm 相关的日志
|
||||||
|
PICOCLAW_LOG_LEVEL=debug ./picoclaw | grep '\[llm\]'
|
||||||
|
|
||||||
|
# 保存日志到文件以便分析
|
||||||
|
PICOCLAW_LOG_LEVEL=debug ./picoclaw 2>&1 | tee debug.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 jq 格式化 JSON 日志(如果日志是 JSON 格式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PICOCLAW_LOG_LEVEL=debug ./picoclaw 2>&1 | jq -r 'select(.category == "llm")'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例:完整的调试流程
|
||||||
|
|
||||||
|
假设遇到 404 错误,以下是完整的调试输出示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
[2026-02-12 10:30:00] [DEBUG] [llm] Creating LLM provider
|
||||||
|
model: gpt-4
|
||||||
|
lower_model: gpt-4
|
||||||
|
|
||||||
|
[2026-02-12 10:30:00] [INFO] [llm] Provider created successfully
|
||||||
|
model: gpt-4
|
||||||
|
api_base: https://api.openai.com/wrong-path # ⚠️ 错误的路径
|
||||||
|
has_api_key: true
|
||||||
|
api_key_len: 51
|
||||||
|
|
||||||
|
[2026-02-12 10:30:01] [DEBUG] [llm] Sending LLM request
|
||||||
|
url: https://api.openai.com/wrong-path/chat/completions # ⚠️ 最终的 URL 错误
|
||||||
|
model: gpt-4
|
||||||
|
message_count: 1
|
||||||
|
request_body: {"model":"gpt-4","messages":[...]}
|
||||||
|
|
||||||
|
[2026-02-12 10:30:01] [DEBUG] [llm] Sending HTTP request
|
||||||
|
method: POST
|
||||||
|
url: https://api.openai.com/wrong-path/chat/completions
|
||||||
|
headers: {Content-Type: application/json, Authorization: Bearer sk-...}
|
||||||
|
|
||||||
|
[2026-02-12 10:30:02] [DEBUG] [llm] Received LLM response
|
||||||
|
status_code: 404
|
||||||
|
status: 404 Not Found
|
||||||
|
response_body: 404 page not found
|
||||||
|
|
||||||
|
[2026-02-12 10:30:02] [ERROR] [llm] LLM API returned non-OK status
|
||||||
|
status_code: 404
|
||||||
|
url: https://api.openai.com/wrong-path/chat/completions
|
||||||
|
response_body: 404 page not found
|
||||||
|
|
||||||
|
[2026-02-12 10:30:02] [ERROR] [agent] LLM call failed
|
||||||
|
iteration: 1
|
||||||
|
error: API error: 404 page not found
|
||||||
|
```
|
||||||
|
|
||||||
|
从这个日志可以清楚地看到:
|
||||||
|
1. Provider 创建时使用了错误的 `api_base`
|
||||||
|
2. 最终的请求 URL 拼接错误
|
||||||
|
3. 服务器返回 404 错误
|
||||||
|
|
||||||
|
**解决方案**: 修改配置文件中的 `api_base` 为 `https://api.openai.com/v1`
|
||||||
|
|
||||||
|
## 敏感信息保护
|
||||||
|
|
||||||
|
注意到在日志中:
|
||||||
|
- ✅ API Key 只显示长度和前缀,不会完整输出
|
||||||
|
- ✅ Authorization header 会被隐藏
|
||||||
|
- ⚠️ 完整的请求体和响应体会被记录(debug 级别)
|
||||||
|
|
||||||
|
**生产环境建议**:
|
||||||
|
- 使用 `info` 或更高级别,避免泄露敏感信息
|
||||||
|
- 只在本地开发或受控环境中使用 `debug` 级别
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `pkg/providers/http_provider.go` - HTTP Provider 实现和调试日志
|
||||||
|
- `pkg/agent/loop.go` - Agent 循环和错误处理
|
||||||
|
- `pkg/logger/logger.go` - 日志系统实现
|
||||||
|
|
||||||
|
## 获取帮助
|
||||||
|
|
||||||
|
如果调试日志无法解决问题,请在 GitHub Issue 中提供:
|
||||||
|
1. 完整的配置文件(隐藏 API Key)
|
||||||
|
2. 相关的调试日志输出
|
||||||
|
3. 使用的模型和 provider
|
||||||
|
4. 错误发生的上下文
|
||||||
316
docs/skills-debugging-guide.md
Normal file
316
docs/skills-debugging-guide.md
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
# PicoClaw Skills 调试指南
|
||||||
|
|
||||||
|
## 问题排查和解决方案
|
||||||
|
|
||||||
|
### 问题1: Skills 无法被识别和使用
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- Agent 有 21 个 skills,但在处理请求时不使用它们
|
||||||
|
- 即使明确指定 skill 名称,也提示 skill 不存在
|
||||||
|
|
||||||
|
**根本原因**:
|
||||||
|
Skills 文件不在正确的位置。代码从以下位置加载 skills:
|
||||||
|
1. `~/.picoclaw/workspace/skills/` (workspace skills - 项目级别)
|
||||||
|
2. `~/.picoclaw/skills/` (全局 skills)
|
||||||
|
3. 内置 skills 目录
|
||||||
|
|
||||||
|
但用户的 skills 实际存放在 `~/.claude/skills/`。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
将需要使用的 skills 复制到 workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制单个 skill
|
||||||
|
cp -r ~/.claude/skills/sentinel-search ~/.picoclaw/workspace/skills/
|
||||||
|
|
||||||
|
# 或者批量复制所有 skills
|
||||||
|
cp -r ~/.claude/skills/* ~/.picoclaw/workspace/skills/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题2: Anthropic API 404 错误
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
Error: LLM call failed: API error: 404 page not found
|
||||||
|
url=https://meta.nevis.sina.com.cn/wecode/anthropic/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**:
|
||||||
|
- 原来的代码只有一个通用的 `HTTPProvider`
|
||||||
|
- 它使用 OpenAI 风格的 API 端点:`/chat/completions`
|
||||||
|
- 但 Anthropic API 使用不同的端点:`/v1/messages`
|
||||||
|
- 请求/响应格式也完全不同
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
创建了专门的 `AnthropicProvider` (pkg/providers/anthropic_provider.go):
|
||||||
|
- ✅ 使用正确的端点:`/v1/messages`
|
||||||
|
- ✅ 使用正确的请求头:`x-api-key` 而不是 `Authorization: Bearer`
|
||||||
|
- ✅ 转换消息格式(system 单独参数、content blocks 等)
|
||||||
|
- ✅ 正确处理 tool_use 和 tool_result
|
||||||
|
- ✅ 处理孤立的 tool_result(跳过没有对应 tool_use 的结果)
|
||||||
|
|
||||||
|
### 问题3: Tool call 参数传递错误
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
Error: messages.5.content.1.tool_use.name: String should have at least 1 character
|
||||||
|
Error: messages.5.content.1.tool_use.input: Input should be a valid dictionary
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**:
|
||||||
|
Agent 在构建 assistant 消息时,将 tool call 信息存储在:
|
||||||
|
- `tc.Function.Name` - 工具名称
|
||||||
|
- `tc.Function.Arguments` - JSON 字符串格式的参数
|
||||||
|
|
||||||
|
但 AnthropicProvider 最初只读取:
|
||||||
|
- `tc.Name` - 为空
|
||||||
|
- `tc.Arguments` - 为空的 map
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
在 AnthropicProvider 中添加了兼容逻辑:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 提取 name
|
||||||
|
name := tc.Name
|
||||||
|
if name == "" && tc.Function != nil {
|
||||||
|
name = tc.Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 arguments
|
||||||
|
var input map[string]interface{}
|
||||||
|
if len(tc.Arguments) > 0 {
|
||||||
|
input = tc.Arguments
|
||||||
|
} else if tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
// 从 JSON 字符串解析
|
||||||
|
json.Unmarshal([]byte(tc.Function.Arguments), &input)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题4: 孤立的 tool_result 导致 API 错误
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
Error: unexpected tool_use_id found in tool_result blocks: toolu_xxx.
|
||||||
|
Each tool_result block must have a corresponding tool_use block in the previous message.
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**:
|
||||||
|
- 会话历史可能被截断或清理
|
||||||
|
- tool_result 保留了,但对应的 tool_use 被删除了
|
||||||
|
- Anthropic API 严格要求 tool_result 必须紧跟在包含对应 tool_use 的 assistant 消息后
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
添加了 tool_use ID 跟踪机制:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 跟踪当前有效的 tool_use IDs
|
||||||
|
validToolUseIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
// 在 assistant 消息中记录所有 tool_use IDs
|
||||||
|
for _, tc := range msg.ToolCalls {
|
||||||
|
validToolUseIDs[tc.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 tool_result 是否有对应的 tool_use
|
||||||
|
if msg.Role == "tool" && !validToolUseIDs[msg.ToolCallID] {
|
||||||
|
// 跳过孤立的 tool_result
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skills 使用流程
|
||||||
|
|
||||||
|
### 1. Skills 加载
|
||||||
|
|
||||||
|
Skills 从以下位置按优先级加载:
|
||||||
|
1. **Workspace skills** (`~/.picoclaw/workspace/skills/`) - 最高优先级,项目专用
|
||||||
|
2. **Global skills** (`~/.picoclaw/skills/`) - 中等优先级,用户全局
|
||||||
|
3. **Builtin skills** - 最低优先级,系统内置
|
||||||
|
|
||||||
|
### 2. Skills 在系统提示中的展示
|
||||||
|
|
||||||
|
Skills 只显示摘要信息:
|
||||||
|
```xml
|
||||||
|
<skills>
|
||||||
|
<skill>
|
||||||
|
<name>sentinel-search</name>
|
||||||
|
<description>Sentinel 安全平台 - 网络资产、服务和 Web 应用搜索工具</description>
|
||||||
|
<location>/path/to/SKILL.md</location>
|
||||||
|
<source>workspace</source>
|
||||||
|
</skill>
|
||||||
|
</skills>
|
||||||
|
```
|
||||||
|
|
||||||
|
提示 AI:"To use a skill, read its SKILL.md file using the read_file tool"
|
||||||
|
|
||||||
|
### 3. Skills 使用流程
|
||||||
|
|
||||||
|
当用户请求使用某个 skill 时:
|
||||||
|
1. AI 使用 `read_file` 工具读取 `SKILL.md`
|
||||||
|
2. 理解 skill 的 API 文档和使用方法
|
||||||
|
3. 使用 `exec` 工具调用相应的命令/API
|
||||||
|
4. 处理结果并返回给用户
|
||||||
|
|
||||||
|
## 测试 Skills
|
||||||
|
|
||||||
|
### 测试 sentinel-search skill
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 确保 skill 在正确位置
|
||||||
|
ls ~/.picoclaw/workspace/skills/sentinel-search/SKILL.md
|
||||||
|
|
||||||
|
# 2. 明确指定使用 sentinel 平台
|
||||||
|
./picoclaw agent -m "使用 sentinel 平台搜索 nginx 服务,查询语句是 fp.nmap.product=nginx"
|
||||||
|
|
||||||
|
# 3. 观察日志,应该看到:
|
||||||
|
# - list_dir: 列出 skills 目录
|
||||||
|
# - read_file: 读取 SKILL.md
|
||||||
|
# - exec: 执行 curl 命令调用 API
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志分析
|
||||||
|
|
||||||
|
成功的 skill 使用日志:
|
||||||
|
```
|
||||||
|
[INFO] agent: LLM requested tool calls {tools=[list_dir], count=1, iteration=1}
|
||||||
|
[INFO] agent: Tool call: list_dir({"path":"/Users/xingyue/.picoclaw/workspace/skills"})
|
||||||
|
[INFO] agent: LLM requested tool calls {tools=[read_file], count=1, iteration=2}
|
||||||
|
[INFO] agent: Tool call: read_file({"path":"...sentinel-search/SKILL.md"})
|
||||||
|
[INFO] agent: LLM requested tool calls {tools=[exec], count=1, iteration=3}
|
||||||
|
[INFO] agent: Tool call: exec({"command":"curl -X POST http://..."})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 启用 Debug 日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启用所有 debug 日志
|
||||||
|
export PICOCLAW_LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# 只启用 llm 分类的 debug 日志
|
||||||
|
export PICOCLAW_LOG_CATEGORY_LLM=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查 Skills 是否加载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 agent 初始化日志
|
||||||
|
./picoclaw agent -m "test" 2>&1 | grep "Agent initialized"
|
||||||
|
# 应该看到: skills_total=22, skills_available=22
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证 API 连接
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 手动测试 Sentinel API
|
||||||
|
curl -X POST http://172.16.10.239:31223/api/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query":"fp.nmap.product=nginx","page":1,"page_size":5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 检查网络连接
|
||||||
|
|
||||||
|
如果 skill 涉及网络请求,确保:
|
||||||
|
- ✅ VPN 已连接(如果需要)
|
||||||
|
- ✅ 防火墙允许访问
|
||||||
|
- ✅ 目标服务正常运行
|
||||||
|
|
||||||
|
## 创建自定义 Skills
|
||||||
|
|
||||||
|
### Skill 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.picoclaw/workspace/skills/my-skill/
|
||||||
|
├── SKILL.md # Skill 文档(必需)
|
||||||
|
└── .claude/ # 可选的元数据
|
||||||
|
└── config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### SKILL.md 格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-skill
|
||||||
|
description: 简短描述(会显示在 skills 列表中)
|
||||||
|
version: 1.0.0
|
||||||
|
author: your-name
|
||||||
|
tags:
|
||||||
|
- tag1
|
||||||
|
- tag2
|
||||||
|
triggers:
|
||||||
|
- 触发词1
|
||||||
|
- 触发词2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill 名称
|
||||||
|
|
||||||
|
详细的使用说明和 API 文档...
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
\```bash
|
||||||
|
# 示例命令
|
||||||
|
curl ...
|
||||||
|
\```
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **清晰的文档** - 提供完整的 API 文档和示例
|
||||||
|
2. **错误处理** - 说明常见错误和解决方法
|
||||||
|
3. **网络要求** - 明确说明网络依赖和访问限制
|
||||||
|
4. **参数说明** - 详细描述所有参数和选项
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `pkg/skills/loader.go` - Skills 加载器
|
||||||
|
- `pkg/agent/context.go` - Skills 在上下文中的集成
|
||||||
|
- `pkg/providers/anthropic_provider.go` - Anthropic API 适配器
|
||||||
|
- `docs/llm-debug-guide.md` - LLM 调试指南
|
||||||
|
|
||||||
|
## 未来改进
|
||||||
|
|
||||||
|
### 1. Skills 自动同步
|
||||||
|
|
||||||
|
考虑添加配置选项,自动从 `~/.claude/skills/` 同步到 workspace:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
skills:
|
||||||
|
auto_sync: true
|
||||||
|
source: ~/.claude/skills
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Skills 作为工具
|
||||||
|
|
||||||
|
考虑将常用 skills 直接注册为工具,而不需要每次都 read_file:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 将 skill 转换为 tool definition
|
||||||
|
func (s *Skill) AsToolDefinition() providers.ToolDefinition {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Skills 模板
|
||||||
|
|
||||||
|
提供 skill 创建模板:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw skill new my-skill --template api-wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
经过调试,现在 PicoClaw 的 skills 系统可以正常工作:
|
||||||
|
|
||||||
|
1. ✅ Skills 从正确的位置加载
|
||||||
|
2. ✅ Anthropic API 正确处理消息和工具调用
|
||||||
|
3. ✅ Tool calls 的 name 和 arguments 正确提取
|
||||||
|
4. ✅ 孤立的 tool_result 被过滤
|
||||||
|
5. ✅ 详细的调试日志帮助排查问题
|
||||||
|
|
||||||
|
Skills 使用流程清晰简单:
|
||||||
|
1. 将 skill 放到 workspace/skills/
|
||||||
|
2. 明确告诉 AI 使用哪个 skill
|
||||||
|
3. AI 读取 SKILL.md 并执行相应操作
|
||||||
342
pkg/providers/anthropic_provider.go
Normal file
342
pkg/providers/anthropic_provider.go
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
// PicoClaw - Ultra-lightweight personal AI agent
|
||||||
|
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
||||||
|
// License: MIT
|
||||||
|
//
|
||||||
|
// Copyright (c) 2026 PicoClaw contributors
|
||||||
|
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnthropicProvider implements the Anthropic Messages API
|
||||||
|
type AnthropicProvider struct {
|
||||||
|
apiKey string
|
||||||
|
apiBase string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnthropicProvider(apiKey, apiBase string) *AnthropicProvider {
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase = "https://api.anthropic.com/v1"
|
||||||
|
}
|
||||||
|
return &AnthropicProvider{
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiBase: apiBase,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AnthropicProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
||||||
|
if p.apiBase == "" {
|
||||||
|
return nil, fmt.Errorf("API base not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert messages to Anthropic format
|
||||||
|
anthropicMessages := make([]map[string]interface{}, 0, len(messages))
|
||||||
|
var systemPrompt string
|
||||||
|
|
||||||
|
// Track tool_use IDs from the previous assistant message
|
||||||
|
validToolUseIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
for i, msg := range messages {
|
||||||
|
if msg.Role == "system" {
|
||||||
|
// Anthropic uses separate system parameter
|
||||||
|
systemPrompt = msg.Content
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
anthMsg := map[string]interface{}{
|
||||||
|
"role": msg.Role,
|
||||||
|
"content": msg.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tool results - only include if there's a corresponding tool_use
|
||||||
|
if msg.Role == "tool" || msg.ToolCallID != "" {
|
||||||
|
// Check if this tool_call_id was in the previous assistant message
|
||||||
|
if !validToolUseIDs[msg.ToolCallID] {
|
||||||
|
logger.WarnCF("llm", "Skipping tool result without corresponding tool_use",
|
||||||
|
map[string]interface{}{
|
||||||
|
"tool_call_id": msg.ToolCallID,
|
||||||
|
"message_idx": i,
|
||||||
|
})
|
||||||
|
continue // Skip orphaned tool results
|
||||||
|
}
|
||||||
|
|
||||||
|
anthMsg["role"] = "user"
|
||||||
|
anthMsg["content"] = []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": msg.ToolCallID,
|
||||||
|
"content": msg.Content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle assistant messages with tool calls
|
||||||
|
if msg.Role == "assistant" && len(msg.ToolCalls) > 0 {
|
||||||
|
// Clear previous valid IDs and record new ones
|
||||||
|
validToolUseIDs = make(map[string]bool)
|
||||||
|
|
||||||
|
contentBlocks := make([]map[string]interface{}, 0, len(msg.ToolCalls)+1)
|
||||||
|
|
||||||
|
// Add text content if present
|
||||||
|
if msg.Content != "" {
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "text",
|
||||||
|
"text": msg.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tool use blocks
|
||||||
|
for _, tc := range msg.ToolCalls {
|
||||||
|
// Extract name from either tc.Name or tc.Function.Name
|
||||||
|
name := tc.Name
|
||||||
|
if name == "" && tc.Function != nil {
|
||||||
|
name = tc.Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract arguments - they might be in tc.Arguments (map) or tc.Function.Arguments (JSON string)
|
||||||
|
var input map[string]interface{}
|
||||||
|
if len(tc.Arguments) > 0 {
|
||||||
|
input = tc.Arguments
|
||||||
|
} else if tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
// Parse JSON string to map
|
||||||
|
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
|
||||||
|
logger.ErrorCF("llm", "Failed to parse tool arguments",
|
||||||
|
map[string]interface{}{
|
||||||
|
"id": tc.ID,
|
||||||
|
"name": name,
|
||||||
|
"error": err.Error(),
|
||||||
|
"raw": tc.Function.Arguments,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.DebugCF("llm", "Converting tool call to Anthropic format",
|
||||||
|
map[string]interface{}{
|
||||||
|
"id": tc.ID,
|
||||||
|
"name": name,
|
||||||
|
"type": tc.Type,
|
||||||
|
"input": input,
|
||||||
|
})
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
logger.ErrorCF("llm", "Tool call has no name",
|
||||||
|
map[string]interface{}{
|
||||||
|
"id": tc.ID,
|
||||||
|
"type": tc.Type,
|
||||||
|
"has_function": tc.Function != nil,
|
||||||
|
"tc_name": tc.Name,
|
||||||
|
})
|
||||||
|
continue // Skip this tool call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record this as a valid tool_use ID
|
||||||
|
validToolUseIDs[tc.ID] = true
|
||||||
|
|
||||||
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||||
|
"type": "tool_use",
|
||||||
|
"id": tc.ID,
|
||||||
|
"name": name,
|
||||||
|
"input": input,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
anthMsg["content"] = contentBlocks
|
||||||
|
} else if msg.Role == "assistant" {
|
||||||
|
// Assistant message without tool calls - clear valid IDs
|
||||||
|
validToolUseIDs = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
anthropicMessages = append(anthropicMessages, anthMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request body
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"messages": anthropicMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
if systemPrompt != "" {
|
||||||
|
requestBody["system"] = systemPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tools if present
|
||||||
|
if len(tools) > 0 {
|
||||||
|
anthropicTools := make([]map[string]interface{}, 0, len(tools))
|
||||||
|
for _, tool := range tools {
|
||||||
|
anthropicTools = append(anthropicTools, map[string]interface{}{
|
||||||
|
"name": tool.Function.Name,
|
||||||
|
"description": tool.Function.Description,
|
||||||
|
"input_schema": tool.Function.Parameters,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
requestBody["tools"] = anthropicTools
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add max_tokens (required by Anthropic)
|
||||||
|
if maxTokens, ok := options["max_tokens"].(int); ok {
|
||||||
|
requestBody["max_tokens"] = maxTokens
|
||||||
|
} else {
|
||||||
|
requestBody["max_tokens"] = 8192 // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add temperature if specified
|
||||||
|
if temperature, ok := options["temperature"].(float64); ok {
|
||||||
|
requestBody["temperature"] = temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL := p.apiBase + "/messages"
|
||||||
|
|
||||||
|
// Debug log: Log request details
|
||||||
|
logger.DebugCF("llm", "Sending Anthropic API request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"model": model,
|
||||||
|
"api_base": p.apiBase,
|
||||||
|
"has_api_key": p.apiKey != "",
|
||||||
|
"api_key_len": len(p.apiKey),
|
||||||
|
"message_count": len(anthropicMessages),
|
||||||
|
"tools_count": len(tools),
|
||||||
|
"request_body": string(jsonData),
|
||||||
|
})
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", fullURL, bytes.NewReader(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "Failed to create HTTP request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Anthropic-specific headers
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("x-api-key", p.apiKey)
|
||||||
|
req.Header.Set("anthropic-version", "2023-06-01")
|
||||||
|
|
||||||
|
logger.DebugCF("llm", "Sending HTTP request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"method": "POST",
|
||||||
|
"url": fullURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := p.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "HTTP request failed",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "Failed to read response body",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug log: Log response details
|
||||||
|
logger.DebugCF("llm", "Received Anthropic API response",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"content_length": len(body),
|
||||||
|
"response_body": string(body),
|
||||||
|
})
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
logger.ErrorCF("llm", "Anthropic API returned non-OK status",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"url": fullURL,
|
||||||
|
"response_body": string(body),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("API error: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.parseResponse(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AnthropicProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
|
var apiResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Input map[string]interface{} `json:"input,omitempty"`
|
||||||
|
} `json:"content"`
|
||||||
|
StopReason string `json:"stop_reason"`
|
||||||
|
Usage struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
} `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text content and tool calls
|
||||||
|
var textContent string
|
||||||
|
toolCalls := make([]ToolCall, 0)
|
||||||
|
|
||||||
|
for _, content := range apiResponse.Content {
|
||||||
|
switch content.Type {
|
||||||
|
case "text":
|
||||||
|
textContent += content.Text
|
||||||
|
case "tool_use":
|
||||||
|
toolCalls = append(toolCalls, ToolCall{
|
||||||
|
ID: content.ID,
|
||||||
|
Name: content.Name,
|
||||||
|
Arguments: content.Input,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LLMResponse{
|
||||||
|
Content: textContent,
|
||||||
|
ToolCalls: toolCalls,
|
||||||
|
FinishReason: apiResponse.StopReason,
|
||||||
|
Usage: &UsageInfo{
|
||||||
|
PromptTokens: apiResponse.Usage.InputTokens,
|
||||||
|
CompletionTokens: apiResponse.Usage.OutputTokens,
|
||||||
|
TotalTokens: apiResponse.Usage.InputTokens + apiResponse.Usage.OutputTokens,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AnthropicProvider) GetDefaultModel() string {
|
||||||
|
return "claude-sonnet-4-5"
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPProvider struct {
|
type HTTPProvider struct {
|
||||||
|
|
@ -24,6 +25,14 @@ type HTTPProvider struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get minimum of two integers
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func NewHTTPProvider(apiKey, apiBase string) *HTTPProvider {
|
func NewHTTPProvider(apiKey, apiBase string) *HTTPProvider {
|
||||||
return &HTTPProvider{
|
return &HTTPProvider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
|
@ -67,8 +76,28 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/chat/completions", bytes.NewReader(jsonData))
|
fullURL := p.apiBase + "/chat/completions"
|
||||||
|
|
||||||
|
// Debug log: Log request details
|
||||||
|
logger.DebugCF("llm", "Sending LLM request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"model": model,
|
||||||
|
"api_base": p.apiBase,
|
||||||
|
"has_api_key": p.apiKey != "",
|
||||||
|
"api_key_len": len(p.apiKey),
|
||||||
|
"message_count": len(messages),
|
||||||
|
"tools_count": len(tools),
|
||||||
|
"request_body": string(jsonData),
|
||||||
|
})
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", fullURL, bytes.NewReader(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "Failed to create HTTP request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,20 +105,57 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
if p.apiKey != "" {
|
if p.apiKey != "" {
|
||||||
authHeader := "Bearer " + p.apiKey
|
authHeader := "Bearer " + p.apiKey
|
||||||
req.Header.Set("Authorization", authHeader)
|
req.Header.Set("Authorization", authHeader)
|
||||||
|
logger.DebugCF("llm", "Authorization header set",
|
||||||
|
map[string]interface{}{
|
||||||
|
"key_prefix": p.apiKey[:min(10, len(p.apiKey))] + "...",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.DebugCF("llm", "Sending HTTP request",
|
||||||
|
map[string]interface{}{
|
||||||
|
"method": "POST",
|
||||||
|
"url": fullURL,
|
||||||
|
"headers": req.Header,
|
||||||
|
})
|
||||||
|
|
||||||
resp, err := p.httpClient.Do(req)
|
resp, err := p.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "HTTP request failed",
|
||||||
|
map[string]interface{}{
|
||||||
|
"url": fullURL,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.ErrorCF("llm", "Failed to read response body",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug log: Log response details
|
||||||
|
logger.DebugCF("llm", "Received LLM response",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"content_length": len(body),
|
||||||
|
"response_body": string(body),
|
||||||
|
})
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
logger.ErrorCF("llm", "LLM API returned non-OK status",
|
||||||
|
map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"url": fullURL,
|
||||||
|
"response_body": string(body),
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("API error: %s", string(body))
|
return nil, fmt.Errorf("API error: %s", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,6 +243,12 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
||||||
lowerModel := strings.ToLower(model)
|
lowerModel := strings.ToLower(model)
|
||||||
|
|
||||||
|
logger.DebugCF("llm", "Creating LLM provider",
|
||||||
|
map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"lower_model": lowerModel,
|
||||||
|
})
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || strings.HasPrefix(model, "openai/") || strings.HasPrefix(model, "meta-llama/") || strings.HasPrefix(model, "deepseek/") || strings.HasPrefix(model, "google/"):
|
case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || strings.HasPrefix(model, "openai/") || strings.HasPrefix(model, "meta-llama/") || strings.HasPrefix(model, "deepseek/") || strings.HasPrefix(model, "google/"):
|
||||||
apiKey = cfg.Providers.OpenRouter.APIKey
|
apiKey = cfg.Providers.OpenRouter.APIKey
|
||||||
|
|
@ -187,12 +259,23 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && cfg.Providers.Anthropic.APIKey != "":
|
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && cfg.Providers.Anthropic.APIKey != "":
|
||||||
|
// Use dedicated Anthropic provider
|
||||||
apiKey = cfg.Providers.Anthropic.APIKey
|
apiKey = cfg.Providers.Anthropic.APIKey
|
||||||
apiBase = cfg.Providers.Anthropic.APIBase
|
apiBase = cfg.Providers.Anthropic.APIBase
|
||||||
if apiBase == "" {
|
if apiBase == "" {
|
||||||
apiBase = "https://api.anthropic.com/v1"
|
apiBase = "https://api.anthropic.com/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.InfoCF("llm", "Anthropic provider created successfully",
|
||||||
|
map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"api_base": apiBase,
|
||||||
|
"has_api_key": apiKey != "",
|
||||||
|
"api_key_len": len(apiKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
return NewAnthropicProvider(apiKey, apiBase), nil
|
||||||
|
|
||||||
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && cfg.Providers.OpenAI.APIKey != "":
|
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && cfg.Providers.OpenAI.APIKey != "":
|
||||||
apiKey = cfg.Providers.OpenAI.APIKey
|
apiKey = cfg.Providers.OpenAI.APIKey
|
||||||
apiBase = cfg.Providers.OpenAI.APIBase
|
apiBase = cfg.Providers.OpenAI.APIBase
|
||||||
|
|
@ -239,12 +322,28 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiKey == "" && !strings.HasPrefix(model, "bedrock/") {
|
if apiKey == "" && !strings.HasPrefix(model, "bedrock/") {
|
||||||
|
logger.ErrorCF("llm", "No API key configured",
|
||||||
|
map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("no API key configured for provider (model: %s)", model)
|
return nil, fmt.Errorf("no API key configured for provider (model: %s)", model)
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiBase == "" {
|
if apiBase == "" {
|
||||||
|
logger.ErrorCF("llm", "No API base configured",
|
||||||
|
map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
})
|
||||||
return nil, fmt.Errorf("no API base configured for provider (model: %s)", model)
|
return nil, fmt.Errorf("no API base configured for provider (model: %s)", model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.InfoCF("llm", "Provider created successfully",
|
||||||
|
map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"api_base": apiBase,
|
||||||
|
"has_api_key": apiKey != "",
|
||||||
|
"api_key_len": len(apiKey),
|
||||||
|
})
|
||||||
|
|
||||||
return NewHTTPProvider(apiKey, apiBase), nil
|
return NewHTTPProvider(apiKey, apiBase), nil
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue