Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/anxiong2025_code-iris.json
T

1 line
100 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "anxiong2025/code-iris", "files": {"README.md": {"509a7b32dfb46d043c8c24c9bab713525fc0dbc4": {"t": 1785674101.3220875, "n": 7723, "h": "7884b058ee2a4c16", "c": "# Code Iris\n\n> Rust-powered AI coding agent — single binary, 17+ LLM providers, typed multi-agent pipeline\n\n[![Crates.io](https://img.shields.io/crates/v/iris-cli.svg)](https://crates.io/crates/iris-cli)\n[![CI](https://github.com/anxiong2025/code-iris/actions/workflows/ci.yml/badge.svg)](https://github.com/anxiong2025/code-iris/actions)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n[![Changelog](https://img.shields.io/badge/changelog-更新日志-orange)](./CHANGELOG.md)\n\n## 30 秒上手\n\n```bash\n# 安装(需要 Rust 1.85+)\ncargo install iris-cli # CLI\ncargo install code-iris # TUI(Claude Code 风格终端界面)\n\n# 配置 API key(任选其一)\nexport ANTHROPIC_API_KEY=sk-ant-... # Claude(推荐)\nexport DEEPSEEK_API_KEY=sk-... # DeepSeek(便宜)\nexport DASHSCOPE_API_KEY=sk-... # 通义千问(国内)\n\n# 开始\ncode-iris # 打开 TUI\niris chat # CLI 交互模式\niris plan \"给 REST API 加 JWT 认证\" # 三步自动规划\niris doc-sync --since HEAD~1 # 检测文档与代码是否一致\n```\n\n---\n\n## 为什么选 Code Iris\n\nAI 编程工具要么太重(Node.js/Python 运行时),要么功能单薄(只有单 Agent 聊天)。Code Iris 用 Rust 重建,有两个独有能力:\n\n### ① 串行 Pipeline Multi-Agent\n\n```\niris plan \"需求\"\n → ProductAgent (explorer) — 分析需求、约束、用户\n ↓ 结构化输出注入下一步 system context\n → ArchAgent (reviewer) — 组件设计、接口、风险\n ↓ 结构化输出注入下一步 system context\n → CodeAgent (worker) — 生成可用代码 + 测试\n```\n\n每一步的输出作为下一步的 system context **结构化注入**,不是自由文本拼接。Claude Code、gstack、LangChain 都没有这个。\n\n### ② 文档漂移检测\n\n```bash\niris doc-sync --since HEAD~1\n# → 自动 diff 代码变更 → 扫描 .md 文件 → 找出过时段落\n```\n\n### ③ Hooks 系统(类 Claude Code)\n\n```toml\n# .iris/hooks.toml\n[[hooks]]\nevent = \"PreToolUse\"\nmatcher = \"bash\"\ncommand = \"jq -e '.tool_input.command | test(\\\"rm -rf\\\") | not' > /dev/null\"\n\n[[hooks]]\nevent = \"PostToolUse\"\nmatcher = \"file_write\"\ncommand = \"iris doc-sync --since HEAD~1\"\n```\n\n### ④ 分层 Instructions(类 CLAUDE.md)\n\n```markdown\n# .iris/instructions.md\n你在 code-iris Rust workspace 工作。\n- 改 .rs 文件后必须 cargo check\n- 错误处理用 anyhow::Result\n```\n\n---\n\n## 对比\n\n> 数据基于公开文档和源码,如有出入欢迎提 issue。\n\n| | Code Iris | Claude Code | OpenAI Codex CLI |\n|---|---|---|---|\n| 语言 | **Rust** | TypeScript | TypeScript |\n| 分发 | **单二进制** | npm 包 | npm 包 |\n| 启动时间 | **~1ms** | ~300ms | ~300ms |\n| 内存 | **~15MB** | ~100MB+ | ~100MB+ |\n| 后端 | 本地直连 LLM | Anthropic API | **云沙箱**(需联网) |\n| 串行 Pipeline | **✅** | ❌ | ❌ |\n| 文档漂移检测 | **✅** | ❌ | ❌ |\n| Hooks 系统 | **✅** | ✅ settings.json | ❌ |\n| 分层 Instructions | **✅** | ✅ CLAUDE.md | ✅ AGENTS.md |\n| LSP 集成 | **✅** | ❌ | ❌ |\n| 持久 bash session | **✅**(进程内) | 依赖 MCP shell | ✅(云沙箱) |\n| 多 Provider | **17+** | Anthropic only(可走 proxy) | OpenAI only |\n| Claude OAuth | **✅** | ✅ | ❌ |\n| MCP Client | **✅** | ✅ | ❌ |\n| 子 Agent / 并行 | **✅** | ✅ | ✅ |\n| 开源 | **✅** | ❌ | **✅** |\n\n---\n\n## TUI — Claude Code 风格终端界面\n\n```bash\ncode-iris\n```\n\n- Markdown 渲染 + **语法高亮**(syntect,base16-ocean 主题)\n- 光标移动(Left/Right/Ctrl+A/E)、历史记录(Up/Down)\n- Shift+Enter 多行输入\n- Ctrl+C 中断当前 turn,Ctrl+D 退出\n- 实时 spinner + 工具调用显示\n- `/plan <需求>` `/agents` `/commit` `/memory` `/cd` `/worktree`\n\n---\n\n## 核心功能\n\n### `iris chat` / `code-iris`\n\n```bash\niris chat # 自动检测 API key\niris chat --model claude-opus-4-6 # 指定模型\niris chat --plan # 只读 Plan Mode\niris chat --resume <session-id> # 续上次会话\n```\n\n### `iris plan` — 三步自动规划\n\n```bash\niris plan \"实现用户登录模块\"\niris plan \"重构数据库层\" --arch-only # 只输出架构,不生成代码\n```\n\n### `iris doc-sync` — 文档漂移检测\n\n```bash\niris doc-sync # 检测 HEAD~1 以来\niris doc-sync --since HEAD~5\niris doc-sync --since v1.0.0\n```\n\n### `iris run` — 多 Agent 任务\n\n```bash\n# 并行(fan-out + synthesis)\niris run \"分析这个 PR\" \\\n --sub \"security@reviewer:找安全漏洞\" \\\n --sub \"quality@reviewer:评估代码质量\" \\\n --sub \"tests@explorer:检查测试覆盖率\"\n\n# 串行 Pipeline\niris run \"重构认证模块\" --pipeline \\\n --sub \"explore@explorer:分析现有代码\" \\\n --sub \"review@reviewer:找设计缺陷\" \\\n --sub \"implement@worker:执行重构\"\n```\n\n---\n\n## Agent 类型系统\n\n| 类型 | 权限 | 默认模型 | 用途 |\n|------|------|---------|------|\n| `explorer` | 只读 | claude-haiku | 代码探索、结构分析 |\n| `reviewer` | 只读 | 主模型 | 代码审查、风险评估 |\n| `worker` | 全权限 | 主模型 | 实现、文件修改 |\n\n**自定义 agent**(`.iris/agents/my-agent.toml`):\n\n```toml\nname = \"security-auditor\"\ndescription = \"专注安全漏洞的审查 agent\"\nsandbox_mode = \"read-only\"\nmodel = \"claude-opus-4-6\"\ninstructions = \"\"\"\n你是安全审计专家,专注 OWASP Top 10、注入攻击、权限提升。\n\"\"\"\n```\n\n---\n\n## 工具列表(14 个)\n\n| 工具 | 说明 |\n|------|------|\n| `bash` | 持久 bash session(cd/export 跨调用生效) |\n| `file_read` / `file_write` / `file_edit` | 文件 I/O |\n| `grep` / `glob` | 代码搜索 |\n| `lsp` | LSP 代码智能(hover/definition/references/diagnostics) |\n| `web_fetch` / `web_search` | 网络访问 |\n| `task_create/update/list/get` | 任务管理 |\n| `agent_tool` | 召唤子 agent |\n| `send_message` | Agent 间通信 |\n\n---\n\n## 架构\n\n```\n┌──────────────────────────────────────────────────────┐\n│ Code Iris │\n├──────────────────────┬───────────────────────────────┤\n│ code-iris (TUI) │ iris (CLI) │\n│ Ratatui 终端界面 │ CLI / CI 脚本友好 │\n└───────────┬──────────┴──────────────┬────────────────┘\n ▼ ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-core (SDK) │\n│ Agent Loop │ 14 Tools │ Coordinator │ AgentDef │\n│ Hooks │ Instructions│ Pipeline │ Permissions│\n│ Context compression (80% autocompact) │\n└───────────────────────────┬──────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-llm │\n│ Anthropic │ OpenAI-compat │ Google │ 17+ providers │\n│ OAuth │ SSE streaming │ Retry │ MCP Client │\n└──────────────────────────────────────────────────────┘\n```\n\n---\n\n## 安装\n\n```bash\n# crates.io(推荐)\ncargo install iris-cli # iris 命令\ncargo install code-iris # code-iris TUI\n\n# 从源码\ngit clone https://github.com/anxiong2025/code-iris\ncd code-iris\ncargo install --path crates/iris-cli\ncargo install --path crates/iris-tui\n```\n\n**系统要求:** Rust 1.85+,无其他运行时依赖。\n\n---\n\n## 支持的 LLM 提供商\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| Anthropic | `ANTHROPIC_API_KEY` | claude-sonnet-4-6 |\n| OpenAI | `OPENAI_API_KEY` | gpt-4o |\n| Google Gemini | `GOOGLE_API_KEY` | gemini-2.0-flash |\n| DeepSeek | `DEEPSEEK_API_KEY` | deepseek-chat |\n| 通义千问 | `DASHSCOPE_API_KEY` | qwen-plus |\n| 月之暗面 | `MOONSHOT_API_KEY` | moonshot-v1-8k |\n| 智谱 AI | `ZHIPU_API_KEY` | glm-4-flash |\n| 硅基流动 | `SILICONFLOW_API_KEY` | deepseek-ai/DeepSeek-V3 |\n| Groq | `GROQ_API_KEY` | llama-3.3-70b-versatile |\n| OpenRouter | `OPENROUTER_API_KEY` | anthropic/claude-sonnet-4 |\n\n自动检测:OAuth → `ANTHROPIC_API_KEY` → 其他环境变量(先检测到哪个用哪个)。\n\n---\n\n## 安全\n\n- **零 npm 依赖** — 无供应链攻击面\n- **rustls-tls** — 纯 Rust TLS,不依赖 OpenSSL\n- **secrecy** — API key 内存保护\n- **静态链接** — 单二进制,无运行时\n- **权限分级** — Default / Plan / Auto / Custom\n- **max_depth = 1** — 防止 agent 递归爆炸\n- **max_threads = 6** — 并发上限\n\n---\n\n## 开发\n\n```bash\ncargo check --workspace\ncargo test --workspace # 90 个测试\ncargo fmt\ncargo clippy\n```\n\n---\n\n## 路线图\n\n- [x] Phase 1 — 多提供商、流式输出、代码扫描、OAuth\n- [x] Phase 2 — Agent 循环、14 工具、权限模型、TUI、多 Agent\n- [x] Phase 3a — Pipeline Multi-Agent、Agent 类型系统、doc-sync\n- [x] Phase 3b — LSP Tool、持久 bash session、Hooks、Instructions、TUI 语法高亮\n- [ ] Phase 4 — crates.io 发布、SDK 文档、Show HN\n\n---\n\n## 文档\n\n- [架构设计](docs/architecture.md) / [中文](docs/architecture-zh.md)\n- [Pipeline Handoff 协议](docs/handoff-protocol.md) / [中文](docs/handoff-protocol-zh.md)\n- [技术博客(英文)](docs/blog/2026-04-01-multi-agent-pipeline.md)\n- [技术博客(中文)](docs/blog/2026-04-01-multi-agent-pipeline-zh.md)\n\n---\n\n## 致谢\n\nCode Iris 从零原创构建。Agent 架构设计参考 Claude Code(Anthropic)公开的设计理念,与 Anthropic 官方无任何关联,不含任何非公开代码。\n\n## 许可证\n\n[MIT](./LICENSE)\n"}, "83e665e962bc095c0a167f0ea108457ef8883eab": {"t": 1785757346.103053, "n": 7723, "h": "7884b058ee2a4c16", "c": "# Code Iris\n\n> Rust-powered AI coding agent — single binary, 17+ LLM providers, typed multi-agent pipeline\n\n[![Crates.io](https://img.shields.io/crates/v/iris-cli.svg)](https://crates.io/crates/iris-cli)\n[![CI](https://github.com/anxiong2025/code-iris/actions/workflows/ci.yml/badge.svg)](https://github.com/anxiong2025/code-iris/actions)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n[![Changelog](https://img.shields.io/badge/changelog-更新日志-orange)](./CHANGELOG.md)\n\n## 30 秒上手\n\n```bash\n# 安装(需要 Rust 1.85+)\ncargo install iris-cli # CLI\ncargo install code-iris # TUI(Claude Code 风格终端界面)\n\n# 配置 API key(任选其一)\nexport ANTHROPIC_API_KEY=sk-ant-... # Claude(推荐)\nexport DEEPSEEK_API_KEY=sk-... # DeepSeek(便宜)\nexport DASHSCOPE_API_KEY=sk-... # 通义千问(国内)\n\n# 开始\ncode-iris # 打开 TUI\niris chat # CLI 交互模式\niris plan \"给 REST API 加 JWT 认证\" # 三步自动规划\niris doc-sync --since HEAD~1 # 检测文档与代码是否一致\n```\n\n---\n\n## 为什么选 Code Iris\n\nAI 编程工具要么太重(Node.js/Python 运行时),要么功能单薄(只有单 Agent 聊天)。Code Iris 用 Rust 重建,有两个独有能力:\n\n### ① 串行 Pipeline Multi-Agent\n\n```\niris plan \"需求\"\n → ProductAgent (explorer) — 分析需求、约束、用户\n ↓ 结构化输出注入下一步 system context\n → ArchAgent (reviewer) — 组件设计、接口、风险\n ↓ 结构化输出注入下一步 system context\n → CodeAgent (worker) — 生成可用代码 + 测试\n```\n\n每一步的输出作为下一步的 system context **结构化注入**,不是自由文本拼接。Claude Code、gstack、LangChain 都没有这个。\n\n### ② 文档漂移检测\n\n```bash\niris doc-sync --since HEAD~1\n# → 自动 diff 代码变更 → 扫描 .md 文件 → 找出过时段落\n```\n\n### ③ Hooks 系统(类 Claude Code)\n\n```toml\n# .iris/hooks.toml\n[[hooks]]\nevent = \"PreToolUse\"\nmatcher = \"bash\"\ncommand = \"jq -e '.tool_input.command | test(\\\"rm -rf\\\") | not' > /dev/null\"\n\n[[hooks]]\nevent = \"PostToolUse\"\nmatcher = \"file_write\"\ncommand = \"iris doc-sync --since HEAD~1\"\n```\n\n### ④ 分层 Instructions(类 CLAUDE.md)\n\n```markdown\n# .iris/instructions.md\n你在 code-iris Rust workspace 工作。\n- 改 .rs 文件后必须 cargo check\n- 错误处理用 anyhow::Result\n```\n\n---\n\n## 对比\n\n> 数据基于公开文档和源码,如有出入欢迎提 issue。\n\n| | Code Iris | Claude Code | OpenAI Codex CLI |\n|---|---|---|---|\n| 语言 | **Rust** | TypeScript | TypeScript |\n| 分发 | **单二进制** | npm 包 | npm 包 |\n| 启动时间 | **~1ms** | ~300ms | ~300ms |\n| 内存 | **~15MB** | ~100MB+ | ~100MB+ |\n| 后端 | 本地直连 LLM | Anthropic API | **云沙箱**(需联网) |\n| 串行 Pipeline | **✅** | ❌ | ❌ |\n| 文档漂移检测 | **✅** | ❌ | ❌ |\n| Hooks 系统 | **✅** | ✅ settings.json | ❌ |\n| 分层 Instructions | **✅** | ✅ CLAUDE.md | ✅ AGENTS.md |\n| LSP 集成 | **✅** | ❌ | ❌ |\n| 持久 bash session | **✅**(进程内) | 依赖 MCP shell | ✅(云沙箱) |\n| 多 Provider | **17+** | Anthropic only(可走 proxy) | OpenAI only |\n| Claude OAuth | **✅** | ✅ | ❌ |\n| MCP Client | **✅** | ✅ | ❌ |\n| 子 Agent / 并行 | **✅** | ✅ | ✅ |\n| 开源 | **✅** | ❌ | **✅** |\n\n---\n\n## TUI — Claude Code 风格终端界面\n\n```bash\ncode-iris\n```\n\n- Markdown 渲染 + **语法高亮**(syntect,base16-ocean 主题)\n- 光标移动(Left/Right/Ctrl+A/E)、历史记录(Up/Down)\n- Shift+Enter 多行输入\n- Ctrl+C 中断当前 turn,Ctrl+D 退出\n- 实时 spinner + 工具调用显示\n- `/plan <需求>` `/agents` `/commit` `/memory` `/cd` `/worktree`\n\n---\n\n## 核心功能\n\n### `iris chat` / `code-iris`\n\n```bash\niris chat # 自动检测 API key\niris chat --model claude-opus-4-6 # 指定模型\niris chat --plan # 只读 Plan Mode\niris chat --resume <session-id> # 续上次会话\n```\n\n### `iris plan` — 三步自动规划\n\n```bash\niris plan \"实现用户登录模块\"\niris plan \"重构数据库层\" --arch-only # 只输出架构,不生成代码\n```\n\n### `iris doc-sync` — 文档漂移检测\n\n```bash\niris doc-sync # 检测 HEAD~1 以来\niris doc-sync --since HEAD~5\niris doc-sync --since v1.0.0\n```\n\n### `iris run` — 多 Agent 任务\n\n```bash\n# 并行(fan-out + synthesis)\niris run \"分析这个 PR\" \\\n --sub \"security@reviewer:找安全漏洞\" \\\n --sub \"quality@reviewer:评估代码质量\" \\\n --sub \"tests@explorer:检查测试覆盖率\"\n\n# 串行 Pipeline\niris run \"重构认证模块\" --pipeline \\\n --sub \"explore@explorer:分析现有代码\" \\\n --sub \"review@reviewer:找设计缺陷\" \\\n --sub \"implement@worker:执行重构\"\n```\n\n---\n\n## Agent 类型系统\n\n| 类型 | 权限 | 默认模型 | 用途 |\n|------|------|---------|------|\n| `explorer` | 只读 | claude-haiku | 代码探索、结构分析 |\n| `reviewer` | 只读 | 主模型 | 代码审查、风险评估 |\n| `worker` | 全权限 | 主模型 | 实现、文件修改 |\n\n**自定义 agent**(`.iris/agents/my-agent.toml`):\n\n```toml\nname = \"security-auditor\"\ndescription = \"专注安全漏洞的审查 agent\"\nsandbox_mode = \"read-only\"\nmodel = \"claude-opus-4-6\"\ninstructions = \"\"\"\n你是安全审计专家,专注 OWASP Top 10、注入攻击、权限提升。\n\"\"\"\n```\n\n---\n\n## 工具列表(14 个)\n\n| 工具 | 说明 |\n|------|------|\n| `bash` | 持久 bash session(cd/export 跨调用生效) |\n| `file_read` / `file_write` / `file_edit` | 文件 I/O |\n| `grep` / `glob` | 代码搜索 |\n| `lsp` | LSP 代码智能(hover/definition/references/diagnostics) |\n| `web_fetch` / `web_search` | 网络访问 |\n| `task_create/update/list/get` | 任务管理 |\n| `agent_tool` | 召唤子 agent |\n| `send_message` | Agent 间通信 |\n\n---\n\n## 架构\n\n```\n┌──────────────────────────────────────────────────────┐\n│ Code Iris │\n├──────────────────────┬───────────────────────────────┤\n│ code-iris (TUI) │ iris (CLI) │\n│ Ratatui 终端界面 │ CLI / CI 脚本友好 │\n└───────────┬──────────┴──────────────┬────────────────┘\n ▼ ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-core (SDK) │\n│ Agent Loop │ 14 Tools │ Coordinator │ AgentDef │\n│ Hooks │ Instructions│ Pipeline │ Permissions│\n│ Context compression (80% autocompact) │\n└───────────────────────────┬──────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-llm │\n│ Anthropic │ OpenAI-compat │ Google │ 17+ providers │\n│ OAuth │ SSE streaming │ Retry │ MCP Client │\n└──────────────────────────────────────────────────────┘\n```\n\n---\n\n## 安装\n\n```bash\n# crates.io(推荐)\ncargo install iris-cli # iris 命令\ncargo install code-iris # code-iris TUI\n\n# 从源码\ngit clone https://github.com/anxiong2025/code-iris\ncd code-iris\ncargo install --path crates/iris-cli\ncargo install --path crates/iris-tui\n```\n\n**系统要求:** Rust 1.85+,无其他运行时依赖。\n\n---\n\n## 支持的 LLM 提供商\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| Anthropic | `ANTHROPIC_API_KEY` | claude-sonnet-4-6 |\n| OpenAI | `OPENAI_API_KEY` | gpt-4o |\n| Google Gemini | `GOOGLE_API_KEY` | gemini-2.0-flash |\n| DeepSeek | `DEEPSEEK_API_KEY` | deepseek-chat |\n| 通义千问 | `DASHSCOPE_API_KEY` | qwen-plus |\n| 月之暗面 | `MOONSHOT_API_KEY` | moonshot-v1-8k |\n| 智谱 AI | `ZHIPU_API_KEY` | glm-4-flash |\n| 硅基流动 | `SILICONFLOW_API_KEY` | deepseek-ai/DeepSeek-V3 |\n| Groq | `GROQ_API_KEY` | llama-3.3-70b-versatile |\n| OpenRouter | `OPENROUTER_API_KEY` | anthropic/claude-sonnet-4 |\n\n自动检测:OAuth → `ANTHROPIC_API_KEY` → 其他环境变量(先检测到哪个用哪个)。\n\n---\n\n## 安全\n\n- **零 npm 依赖** — 无供应链攻击面\n- **rustls-tls** — 纯 Rust TLS,不依赖 OpenSSL\n- **secrecy** — API key 内存保护\n- **静态链接** — 单二进制,无运行时\n- **权限分级** — Default / Plan / Auto / Custom\n- **max_depth = 1** — 防止 agent 递归爆炸\n- **max_threads = 6** — 并发上限\n\n---\n\n## 开发\n\n```bash\ncargo check --workspace\ncargo test --workspace # 90 个测试\ncargo fmt\ncargo clippy\n```\n\n---\n\n## 路线图\n\n- [x] Phase 1 — 多提供商、流式输出、代码扫描、OAuth\n- [x] Phase 2 — Agent 循环、14 工具、权限模型、TUI、多 Agent\n- [x] Phase 3a — Pipeline Multi-Agent、Agent 类型系统、doc-sync\n- [x] Phase 3b — LSP Tool、持久 bash session、Hooks、Instructions、TUI 语法高亮\n- [ ] Phase 4 — crates.io 发布、SDK 文档、Show HN\n\n---\n\n## 文档\n\n- [架构设计](docs/architecture.md) / [中文](docs/architecture-zh.md)\n- [Pipeline Handoff 协议](docs/handoff-protocol.md) / [中文](docs/handoff-protocol-zh.md)\n- [技术博客(英文)](docs/blog/2026-04-01-multi-agent-pipeline.md)\n- [技术博客(中文)](docs/blog/2026-04-01-multi-agent-pipeline-zh.md)\n\n---\n\n## 致谢\n\nCode Iris 从零原创构建。Agent 架构设计参考 Claude Code(Anthropic)公开的设计理念,与 Anthropic 官方无任何关联,不含任何非公开代码。\n\n## 许可证\n\n[MIT](./LICENSE)\n"}, "2b61613f2a960531eec6ee8349848cf193c94cfa": {"t": 1785757347.9997022, "n": 7640, "h": "549a1c993b69980f", "c": "# Code Iris\n\n> Rust-powered AI coding agent — single binary, 17+ LLM providers, typed multi-agent pipeline\n\n[![Crates.io](https://img.shields.io/crates/v/iris-cli.svg)](https://crates.io/crates/iris-cli)\n[![CI](https://github.com/anxiong2025/code-iris/actions/workflows/ci.yml/badge.svg)](https://github.com/anxiong2025/code-iris/actions)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n\n## 30 秒上手\n\n```bash\n# 安装(需要 Rust 1.85+)\ncargo install iris-cli # CLI\ncargo install code-iris # TUI(Claude Code 风格终端界面)\n\n# 配置 API key(任选其一)\nexport ANTHROPIC_API_KEY=sk-ant-... # Claude(推荐)\nexport DEEPSEEK_API_KEY=sk-... # DeepSeek(便宜)\nexport DASHSCOPE_API_KEY=sk-... # 通义千问(国内)\n\n# 开始\ncode-iris # 打开 TUI\niris chat # CLI 交互模式\niris plan \"给 REST API 加 JWT 认证\" # 三步自动规划\niris doc-sync --since HEAD~1 # 检测文档与代码是否一致\n```\n\n---\n\n## 为什么选 Code Iris\n\nAI 编程工具要么太重(Node.js/Python 运行时),要么功能单薄(只有单 Agent 聊天)。Code Iris 用 Rust 重建,有两个独有能力:\n\n### ① 串行 Pipeline Multi-Agent\n\n```\niris plan \"需求\"\n → ProductAgent (explorer) — 分析需求、约束、用户\n ↓ 结构化输出注入下一步 system context\n → ArchAgent (reviewer) — 组件设计、接口、风险\n ↓ 结构化输出注入下一步 system context\n → CodeAgent (worker) — 生成可用代码 + 测试\n```\n\n每一步的输出作为下一步的 system context **结构化注入**,不是自由文本拼接。Claude Code、gstack、LangChain 都没有这个。\n\n### ② 文档漂移检测\n\n```bash\niris doc-sync --since HEAD~1\n# → 自动 diff 代码变更 → 扫描 .md 文件 → 找出过时段落\n```\n\n### ③ Hooks 系统(类 Claude Code)\n\n```toml\n# .iris/hooks.toml\n[[hooks]]\nevent = \"PreToolUse\"\nmatcher = \"bash\"\ncommand = \"jq -e '.tool_input.command | test(\\\"rm -rf\\\") | not' > /dev/null\"\n\n[[hooks]]\nevent = \"PostToolUse\"\nmatcher = \"file_write\"\ncommand = \"iris doc-sync --since HEAD~1\"\n```\n\n### ④ 分层 Instructions(类 CLAUDE.md)\n\n```markdown\n# .iris/instructions.md\n你在 code-iris Rust workspace 工作。\n- 改 .rs 文件后必须 cargo check\n- 错误处理用 anyhow::Result\n```\n\n---\n\n## 对比\n\n> 数据基于公开文档和源码,如有出入欢迎提 issue。\n\n| | Code Iris | Claude Code | OpenAI Codex CLI |\n|---|---|---|---|\n| 语言 | **Rust** | TypeScript | TypeScript |\n| 分发 | **单二进制** | npm 包 | npm 包 |\n| 启动时间 | **~1ms** | ~300ms | ~300ms |\n| 内存 | **~15MB** | ~100MB+ | ~100MB+ |\n| 后端 | 本地直连 LLM | Anthropic API | **云沙箱**(需联网) |\n| 串行 Pipeline | **✅** | ❌ | ❌ |\n| 文档漂移检测 | **✅** | ❌ | ❌ |\n| Hooks 系统 | **✅** | ✅ settings.json | ❌ |\n| 分层 Instructions | **✅** | ✅ CLAUDE.md | ✅ AGENTS.md |\n| LSP 集成 | **✅** | ❌ | ❌ |\n| 持久 bash session | **✅**(进程内) | 依赖 MCP shell | ✅(云沙箱) |\n| 多 Provider | **17+** | Anthropic only(可走 proxy) | OpenAI only |\n| Claude OAuth | **✅** | ✅ | ❌ |\n| MCP Client | **✅** | ✅ | ❌ |\n| 子 Agent / 并行 | **✅** | ✅ | ✅ |\n| 开源 | **✅** | ❌ | **✅** |\n\n---\n\n## TUI — Claude Code 风格终端界面\n\n```bash\ncode-iris\n```\n\n- Markdown 渲染 + **语法高亮**(syntect,base16-ocean 主题)\n- 光标移动(Left/Right/Ctrl+A/E)、历史记录(Up/Down)\n- Shift+Enter 多行输入\n- Ctrl+C 中断当前 turn,Ctrl+D 退出\n- 实时 spinner + 工具调用显示\n- `/plan <需求>` `/agents` `/commit` `/memory` `/cd` `/worktree`\n\n---\n\n## 核心功能\n\n### `iris chat` / `code-iris`\n\n```bash\niris chat # 自动检测 API key\niris chat --model claude-opus-4-6 # 指定模型\niris chat --plan # 只读 Plan Mode\niris chat --resume <session-id> # 续上次会话\n```\n\n### `iris plan` — 三步自动规划\n\n```bash\niris plan \"实现用户登录模块\"\niris plan \"重构数据库层\" --arch-only # 只输出架构,不生成代码\n```\n\n### `iris doc-sync` — 文档漂移检测\n\n```bash\niris doc-sync # 检测 HEAD~1 以来\niris doc-sync --since HEAD~5\niris doc-sync --since v1.0.0\n```\n\n### `iris run` — 多 Agent 任务\n\n```bash\n# 并行(fan-out + synthesis)\niris run \"分析这个 PR\" \\\n --sub \"security@reviewer:找安全漏洞\" \\\n --sub \"quality@reviewer:评估代码质量\" \\\n --sub \"tests@explorer:检查测试覆盖率\"\n\n# 串行 Pipeline\niris run \"重构认证模块\" --pipeline \\\n --sub \"explore@explorer:分析现有代码\" \\\n --sub \"review@reviewer:找设计缺陷\" \\\n --sub \"implement@worker:执行重构\"\n```\n\n---\n\n## Agent 类型系统\n\n| 类型 | 权限 | 默认模型 | 用途 |\n|------|------|---------|------|\n| `explorer` | 只读 | claude-haiku | 代码探索、结构分析 |\n| `reviewer` | 只读 | 主模型 | 代码审查、风险评估 |\n| `worker` | 全权限 | 主模型 | 实现、文件修改 |\n\n**自定义 agent**(`.iris/agents/my-agent.toml`):\n\n```toml\nname = \"security-auditor\"\ndescription = \"专注安全漏洞的审查 agent\"\nsandbox_mode = \"read-only\"\nmodel = \"claude-opus-4-6\"\ninstructions = \"\"\"\n你是安全审计专家,专注 OWASP Top 10、注入攻击、权限提升。\n\"\"\"\n```\n\n---\n\n## 工具列表(14 个)\n\n| 工具 | 说明 |\n|------|------|\n| `bash` | 持久 bash session(cd/export 跨调用生效) |\n| `file_read` / `file_write` / `file_edit` | 文件 I/O |\n| `grep` / `glob` | 代码搜索 |\n| `lsp` | LSP 代码智能(hover/definition/references/diagnostics) |\n| `web_fetch` / `web_search` | 网络访问 |\n| `task_create/update/list/get` | 任务管理 |\n| `agent_tool` | 召唤子 agent |\n| `send_message` | Agent 间通信 |\n\n---\n\n## 架构\n\n```\n┌──────────────────────────────────────────────────────┐\n│ Code Iris │\n├──────────────────────┬───────────────────────────────┤\n│ code-iris (TUI) │ iris (CLI) │\n│ Ratatui 终端界面 │ CLI / CI 脚本友好 │\n└───────────┬──────────┴──────────────┬────────────────┘\n ▼ ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-core (SDK) │\n│ Agent Loop │ 14 Tools │ Coordinator │ AgentDef │\n│ Hooks │ Instructions│ Pipeline │ Permissions│\n│ Context compression (80% autocompact) │\n└───────────────────────────┬──────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-llm │\n│ Anthropic │ OpenAI-compat │ Google │ 17+ providers │\n│ OAuth │ SSE streaming │ Retry │ MCP Client │\n└──────────────────────────────────────────────────────┘\n```\n\n---\n\n## 安装\n\n```bash\n# crates.io(推荐)\ncargo install iris-cli # iris 命令\ncargo install code-iris # code-iris TUI\n\n# 从源码\ngit clone https://github.com/anxiong2025/code-iris\ncd code-iris\ncargo install --path crates/iris-cli\ncargo install --path crates/iris-tui\n```\n\n**系统要求:** Rust 1.85+,无其他运行时依赖。\n\n---\n\n## 支持的 LLM 提供商\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| Anthropic | `ANTHROPIC_API_KEY` | claude-sonnet-4-6 |\n| OpenAI | `OPENAI_API_KEY` | gpt-4o |\n| Google Gemini | `GOOGLE_API_KEY` | gemini-2.0-flash |\n| DeepSeek | `DEEPSEEK_API_KEY` | deepseek-chat |\n| 通义千问 | `DASHSCOPE_API_KEY` | qwen-plus |\n| 月之暗面 | `MOONSHOT_API_KEY` | moonshot-v1-8k |\n| 智谱 AI | `ZHIPU_API_KEY` | glm-4-flash |\n| 硅基流动 | `SILICONFLOW_API_KEY` | deepseek-ai/DeepSeek-V3 |\n| Groq | `GROQ_API_KEY` | llama-3.3-70b-versatile |\n| OpenRouter | `OPENROUTER_API_KEY` | anthropic/claude-sonnet-4 |\n\n自动检测:OAuth → `ANTHROPIC_API_KEY` → 其他环境变量(先检测到哪个用哪个)。\n\n---\n\n## 安全\n\n- **零 npm 依赖** — 无供应链攻击面\n- **rustls-tls** — 纯 Rust TLS,不依赖 OpenSSL\n- **secrecy** — API key 内存保护\n- **静态链接** — 单二进制,无运行时\n- **权限分级** — Default / Plan / Auto / Custom\n- **max_depth = 1** — 防止 agent 递归爆炸\n- **max_threads = 6** — 并发上限\n\n---\n\n## 开发\n\n```bash\ncargo check --workspace\ncargo test --workspace # 90 个测试\ncargo fmt\ncargo clippy\n```\n\n---\n\n## 路线图\n\n- [x] Phase 1 — 多提供商、流式输出、代码扫描、OAuth\n- [x] Phase 2 — Agent 循环、14 工具、权限模型、TUI、多 Agent\n- [x] Phase 3a — Pipeline Multi-Agent、Agent 类型系统、doc-sync\n- [x] Phase 3b — LSP Tool、持久 bash session、Hooks、Instructions、TUI 语法高亮\n- [ ] Phase 4 — crates.io 发布、SDK 文档、Show HN\n\n---\n\n## 文档\n\n- [架构设计](docs/architecture.md) / [中文](docs/architecture-zh.md)\n- [Pipeline Handoff 协议](docs/handoff-protocol.md) / [中文](docs/handoff-protocol-zh.md)\n- [技术博客(英文)](docs/blog/2026-04-01-multi-agent-pipeline.md)\n- [技术博客(中文)](docs/blog/2026-04-01-multi-agent-pipeline-zh.md)\n\n---\n\n## 致谢\n\nCode Iris 从零原创构建。Agent 架构设计参考 Claude Code(Anthropic)公开的设计理念,与 Anthropic 官方无任何关联,不含任何非公开代码。\n\n## 许可证\n\n[MIT](./LICENSE)\n"}, "e366b73d489d54afcf186b8495163ea33fdc50f2": {"t": 1785757349.7091582, "n": 7529, "h": "a2395603d281e49a", "c": "# Code Iris\n\n> Rust-powered AI coding agent — single binary, 17+ LLM providers, typed multi-agent pipeline\n\n[![Crates.io](https://img.shields.io/crates/v/iris-cli.svg)](https://crates.io/crates/iris-cli)\n[![CI](https://github.com/anxiong2025/code-iris/actions/workflows/ci.yml/badge.svg)](https://github.com/anxiong2025/code-iris/actions)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n\n## 30 秒上手\n\n```bash\n# 安装(需要 Rust 1.85+)\ncargo install iris-cli # CLI\ncargo install code-iris # TUI(Claude Code 风格终端界面)\n\n# 配置 API key(任选其一)\nexport ANTHROPIC_API_KEY=sk-ant-... # Claude(推荐)\nexport DEEPSEEK_API_KEY=sk-... # DeepSeek(便宜)\nexport DASHSCOPE_API_KEY=sk-... # 通义千问(国内)\n\n# 开始\ncode-iris # 打开 TUI\niris chat # CLI 交互模式\niris plan \"给 REST API 加 JWT 认证\" # 三步自动规划\niris doc-sync --since HEAD~1 # 检测文档与代码是否一致\n```\n\n---\n\n## 为什么选 Code Iris\n\nAI 编程工具要么太重(Node.js/Python 运行时),要么功能单薄(只有单 Agent 聊天)。Code Iris 用 Rust 重建,有两个独有能力:\n\n### ① 串行 Pipeline Multi-Agent\n\n```\niris plan \"需求\"\n → ProductAgent (explorer) — 分析需求、约束、用户\n ↓ 结构化输出注入下一步 system context\n → ArchAgent (reviewer) — 组件设计、接口、风险\n ↓ 结构化输出注入下一步 system context\n → CodeAgent (worker) — 生成可用代码 + 测试\n```\n\n每一步的输出作为下一步的 system context **结构化注入**,不是自由文本拼接。Claude Code、gstack、LangChain 都没有这个。\n\n### ② 文档漂移检测\n\n```bash\niris doc-sync --since HEAD~1\n# → 自动 diff 代码变更 → 扫描 .md 文件 → 找出过时段落\n```\n\n### ③ Hooks 系统(类 Claude Code)\n\n```toml\n# .iris/hooks.toml\n[[hooks]]\nevent = \"PreToolUse\"\nmatcher = \"bash\"\ncommand = \"jq -e '.tool_input.command | test(\\\"rm -rf\\\") | not' > /dev/null\"\n\n[[hooks]]\nevent = \"PostToolUse\"\nmatcher = \"file_write\"\ncommand = \"iris doc-sync --since HEAD~1\"\n```\n\n### ④ 分层 Instructions(类 CLAUDE.md)\n\n```markdown\n# .iris/instructions.md\n你在 code-iris Rust workspace 工作。\n- 改 .rs 文件后必须 cargo check\n- 错误处理用 anyhow::Result\n```\n\n---\n\n## 对比\n\n| | Code Iris | Claude Code | gstack | claw-code |\n|---|---|---|---|---|\n| 语言 | **Rust** | TypeScript | Shell | Python |\n| 分发 | **单二进制** | npm 包 | git clone | pip |\n| 启动时间 | **~1ms** | ~300ms | — | — |\n| 内存 | **~15MB** | ~100MB+ | — | — |\n| 串行 Pipeline | **✅** | ❌ | ❌ | ❌ |\n| 文档漂移检测 | **✅** | ❌ | ❌ | ❌ |\n| Hooks 系统 | **✅** | ✅ | ❌ | ❌ |\n| 分层 Instructions | **✅** | ✅ CLAUDE.md | ❌ | ❌ |\n| LSP 集成 | **✅** | ❌ | ❌ | ❌ |\n| 持久 bash session | **✅** | ✅ | ❌ | ❌ |\n| 多 Provider | **17+** | Anthropic only | Anthropic only | 2 |\n| Claude OAuth | **✅** | ✅ | — | — |\n| MCP Client | **✅** | ✅ | — | — |\n| 开源 | **✅** | ❌ | ❌ | ❌ |\n\n---\n\n## TUI — Claude Code 风格终端界面\n\n```bash\ncode-iris\n```\n\n- Markdown 渲染 + **语法高亮**(syntect,base16-ocean 主题)\n- 光标移动(Left/Right/Ctrl+A/E)、历史记录(Up/Down)\n- Shift+Enter 多行输入\n- Ctrl+C 中断当前 turn,Ctrl+D 退出\n- 实时 spinner + 工具调用显示\n- `/plan <需求>` `/agents` `/commit` `/memory` `/cd` `/worktree`\n\n---\n\n## 核心功能\n\n### `iris chat` / `code-iris`\n\n```bash\niris chat # 自动检测 API key\niris chat --model claude-opus-4-6 # 指定模型\niris chat --plan # 只读 Plan Mode\niris chat --resume <session-id> # 续上次会话\n```\n\n### `iris plan` — 三步自动规划\n\n```bash\niris plan \"实现用户登录模块\"\niris plan \"重构数据库层\" --arch-only # 只输出架构,不生成代码\n```\n\n### `iris doc-sync` — 文档漂移检测\n\n```bash\niris doc-sync # 检测 HEAD~1 以来\niris doc-sync --since HEAD~5\niris doc-sync --since v1.0.0\n```\n\n### `iris run` — 多 Agent 任务\n\n```bash\n# 并行(fan-out + synthesis)\niris run \"分析这个 PR\" \\\n --sub \"security@reviewer:找安全漏洞\" \\\n --sub \"quality@reviewer:评估代码质量\" \\\n --sub \"tests@explorer:检查测试覆盖率\"\n\n# 串行 Pipeline\niris run \"重构认证模块\" --pipeline \\\n --sub \"explore@explorer:分析现有代码\" \\\n --sub \"review@reviewer:找设计缺陷\" \\\n --sub \"implement@worker:执行重构\"\n```\n\n---\n\n## Agent 类型系统\n\n| 类型 | 权限 | 默认模型 | 用途 |\n|------|------|---------|------|\n| `explorer` | 只读 | claude-haiku | 代码探索、结构分析 |\n| `reviewer` | 只读 | 主模型 | 代码审查、风险评估 |\n| `worker` | 全权限 | 主模型 | 实现、文件修改 |\n\n**自定义 agent**(`.iris/agents/my-agent.toml`):\n\n```toml\nname = \"security-auditor\"\ndescription = \"专注安全漏洞的审查 agent\"\nsandbox_mode = \"read-only\"\nmodel = \"claude-opus-4-6\"\ninstructions = \"\"\"\n你是安全审计专家,专注 OWASP Top 10、注入攻击、权限提升。\n\"\"\"\n```\n\n---\n\n## 工具列表(14 个)\n\n| 工具 | 说明 |\n|------|------|\n| `bash` | 持久 bash session(cd/export 跨调用生效) |\n| `file_read` / `file_write` / `file_edit` | 文件 I/O |\n| `grep` / `glob` | 代码搜索 |\n| `lsp` | LSP 代码智能(hover/definition/references/diagnostics) |\n| `web_fetch` / `web_search` | 网络访问 |\n| `task_create/update/list/get` | 任务管理 |\n| `agent_tool` | 召唤子 agent |\n| `send_message` | Agent 间通信 |\n\n---\n\n## 架构\n\n```\n┌──────────────────────────────────────────────────────┐\n│ Code Iris │\n├──────────────────────┬───────────────────────────────┤\n│ code-iris (TUI) │ iris (CLI) │\n│ Ratatui 终端界面 │ CLI / CI 脚本友好 │\n└───────────┬──────────┴──────────────┬────────────────┘\n ▼ ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-core (SDK) │\n│ Agent Loop │ 14 Tools │ Coordinator │ AgentDef │\n│ Hooks │ Instructions│ Pipeline │ Permissions│\n│ Context compression (80% autocompact) │\n└───────────────────────────┬──────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────────┐\n│ iris-llm │\n│ Anthropic │ OpenAI-compat │ Google │ 17+ providers │\n│ OAuth │ SSE streaming │ Retry │ MCP Client │\n└──────────────────────────────────────────────────────┘\n```\n\n---\n\n## 安装\n\n```bash\n# crates.io(推荐)\ncargo install iris-cli # iris 命令\ncargo install code-iris # code-iris TUI\n\n# 从源码\ngit clone https://github.com/anxiong2025/code-iris\ncd code-iris\ncargo install --path crates/iris-cli\ncargo install --path crates/iris-tui\n```\n\n**系统要求:** Rust 1.85+,无其他运行时依赖。\n\n---\n\n## 支持的 LLM 提供商\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| Anthropic | `ANTHROPIC_API_KEY` | claude-sonnet-4-6 |\n| OpenAI | `OPENAI_API_KEY` | gpt-4o |\n| Google Gemini | `GOOGLE_API_KEY` | gemini-2.0-flash |\n| DeepSeek | `DEEPSEEK_API_KEY` | deepseek-chat |\n| 通义千问 | `DASHSCOPE_API_KEY` | qwen-plus |\n| 月之暗面 | `MOONSHOT_API_KEY` | moonshot-v1-8k |\n| 智谱 AI | `ZHIPU_API_KEY` | glm-4-flash |\n| 硅基流动 | `SILICONFLOW_API_KEY` | deepseek-ai/DeepSeek-V3 |\n| Groq | `GROQ_API_KEY` | llama-3.3-70b-versatile |\n| OpenRouter | `OPENROUTER_API_KEY` | anthropic/claude-sonnet-4 |\n\n自动检测:OAuth → `ANTHROPIC_API_KEY` → 其他环境变量(先检测到哪个用哪个)。\n\n---\n\n## 安全\n\n- **零 npm 依赖** — 无供应链攻击面\n- **rustls-tls** — 纯 Rust TLS,不依赖 OpenSSL\n- **secrecy** — API key 内存保护\n- **静态链接** — 单二进制,无运行时\n- **权限分级** — Default / Plan / Auto / Custom\n- **max_depth = 1** — 防止 agent 递归爆炸\n- **max_threads = 6** — 并发上限\n\n---\n\n## 开发\n\n```bash\ncargo check --workspace\ncargo test --workspace # 90 个测试\ncargo fmt\ncargo clippy\n```\n\n---\n\n## 路线图\n\n- [x] Phase 1 — 多提供商、流式输出、代码扫描、OAuth\n- [x] Phase 2 — Agent 循环、14 工具、权限模型、TUI、多 Agent\n- [x] Phase 3a — Pipeline Multi-Agent、Agent 类型系统、doc-sync\n- [x] Phase 3b — LSP Tool、持久 bash session、Hooks、Instructions、TUI 语法高亮\n- [ ] Phase 4 — crates.io 发布、SDK 文档、Show HN\n\n---\n\n## 文档\n\n- [架构设计](docs/architecture.md) / [中文](docs/architecture-zh.md)\n- [Pipeline Handoff 协议](docs/handoff-protocol.md) / [中文](docs/handoff-protocol-zh.md)\n- [技术博客(英文)](docs/blog/2026-04-01-multi-agent-pipeline.md)\n- [技术博客(中文)](docs/blog/2026-04-01-multi-agent-pipeline-zh.md)\n\n---\n\n## 致谢\n\nCode Iris 从零原创构建。Agent 架构设计参考 Claude Code(Anthropic)公开的设计理念,与 Anthropic 官方无任何关联,不含任何非公开代码。\n\n## 许可证\n\n[MIT](./LICENSE)\n"}, "040f67de205f988b8c9f8a7ad00c87681741975e": {"t": 1785757351.36888, "n": 7697, "h": "8fa9a43ff5c105db", "c": "# Code Iris\n\n> See through your code — Rust-powered architecture analyzer & AI agent\n\nAI 编程助手的时代已经到来,但现有工具普遍存在两个问题:**启动慢、吃内存、依赖复杂**,以及**只会聊代码,看不透架构**。\n\nCode Iris 的出发点很简单:用 Rust 重新思考 AI 编程助手应该长什么样。\n\n设计上,我们深入研究了 [Claude Code](https://github.com/anthropics/claude-code) 公开的架构思想 —— tool-call 驱动的 Agent 循环、流式响应、分级权限模型、多 Agent 协作 —— 将这些经过工程验证的模式用 Rust 重新实现,并在此基础上做了三件事:\n\n- **极致轻量**:单二进制分发,冷启动 ~1ms,内存占用 ~15MB,零 npm 依赖\n- **架构透视**:不只是聊天,而是真正读懂项目结构、模块依赖、代码演进\n- **开放生态**:支持 Anthropic、OpenAI、Google 及 15+ 国内主流提供商,不绑定任何平台\n\n## 为什么选 Rust\n\n| 维度 | Code Iris (Rust) | 传统方案 (Node/Python) |\n|------|-----------------|----------------------|\n| 启动速度 | ~1ms | ~300-500ms |\n| 内存占用 | ~10-20MB | ~50-100MB |\n| 分发方式 | 单二进制,零依赖 | 需要运行时环境 |\n| 供应链安全 | 零 npm,无 postinstall | npm 生态投毒风险 |\n| TLS | rustls (纯 Rust) | 依赖系统 OpenSSL |\n\n## 核心模块\n\n| # | 模块 | 实现文件 | 功能 |\n|---|------|---------|------|\n| 1 | **Entry Layer** | `iris-cli` · `iris-tui` | CLI 流式输出 + Ratatui 终端界面,slash 命令,异步 Agent Worker |\n| 2 | **Agent Loop** | `iris-core/agent.rs` · `context.rs` | tool-call 驱动循环(MAX 20 轮)、4 级上下文压缩、LLM autocompact |\n| 3 | **Tools System** | `iris-core/tools/` | 13 个工具:Bash · FileRead/Write/Edit · Grep · Glob · WebFetch · WebSearch · Task×4 · AgentTool · SendMessage |\n| 4 | **Commands System** | `iris-cli/main.rs` · `iris-tui/main.rs` | `/help` `/model` `/compact` `/clear` `/session` 及 scan · arch · deps · stats 子命令 |\n| 5 | **Permissions System** | `iris-core/permissions.rs` | Default / Plan / Auto / Custom 四种模式,危险工具交互确认,`format_preview()` |\n| 6 | **Multi-agent** | `iris-core/coordinator.rs` · `tools/send_message.rs` | Coordinator 并发分发子任务 + synthesis 聚合,MessageBus 跨 Agent 广播通信 |\n\n## 架构\n\n### 总览\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Code Iris │\n│ Rust-powered AI Agent & Code Analyzer │\n├────────────────────────┬────────────────────────────────┤\n│ iris-tui │ iris-cli │\n│ 终端交互界面 (TUI) │ 纯命令行 / 脚本友好 │\n└────────────┬───────────┴──────────────┬─────────────────┘\n │ │\n ▼ ▼\n┌─────────────────────────────────────────────────────────┐\n│ iris-core │\n│ 核心引擎 (UI 无关) │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │\n│ │ Agent │ │ Tools │ │ Scanner │ │Context │ │\n│ │ Loop │ │ Registry │ │ (AST分析)│ │ Mgmt │ │\n│ └────┬─────┘ └────┬─────┘ └──────────┘ └────────┘ │\n│ │ │ │\n│ ┌────▼─────┐ ┌─────▼────┐ ┌──────────┐ ┌────────┐ │\n│ │Permissions│ │Coordinator│ │ Storage │ │Config │ │\n│ │ System │ │(多 Agent) │ │(Sessions)│ │ │ │\n│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │\n└─────────────────────────┬───────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────┐\n│ iris-llm │\n│ LLM 提供商适配层 │\n│ │\n│ Anthropic │ OpenAI-compat │ Google │ 15+ 国内提供商 │\n└─────────────────────────────────────────────────────────┘\n```\n\n### 分层详解\n\n**① 入口层 (iris-tui / iris-cli)**\n```\n用户输入\n │\n ├─ iris-tui ──▶ tokio task (agent_worker)\n │ mpsc channel ──▶ AgentEvent (TextChunk / ToolCall / Done)\n │ ratatui 渲染 (异步事件循环)\n │\n └─ iris-cli ──▶ clap 命令解析\n streaming callback (on_text)\n slash 命令 (/help /model /compact …)\n```\n\n**② Agent 循环 (iris-core/agent.rs)**\n```\nchat(user_input)\n └── loop (max 20 turns)\n ├── [1] 上下文压缩 截断 → 丢弃旧轮次 → 折叠 → LLM 摘要\n ├── [2] callModel() SSE 流式 → TextDelta / ToolUse\n ├── [3] 权限检查 串行 (Default 交互确认 / Auto 直通)\n ├── [4] 并行工具执行 tokio::JoinSet → 保序追加结果\n └── [5] needsFollowUp? → continue / return\n```\n\n**③ 工具系统 (iris-core/tools/)**\n```\nToolRegistry\n ├── Bash Shell 命令执行\n ├── FileRead/Write/Edit 文件操作三件套\n ├── Grep / Glob 代码搜索\n ├── WebFetch 网页抓取 + HTML 转纯文本\n ├── WebSearch DuckDuckGo (无需 API key)\n ├── Task ×4 任务管理 (Create/Update/List/Get)\n ├── AgentTool 启动子 Agent (minimal_registry 防递归)\n └── SendMessage Agent 间消息总线 (broadcast channel)\n```\n\n**④ 多 Agent 协调 (iris-core/coordinator.rs)**\n```\nCoordinator::run(tasks)\n ├── spawn sub-agent-0 ──┐\n ├── spawn sub-agent-1 ──┼── tokio::JoinSet 并发\n ├── spawn sub-agent-N ──┘\n │ ↕ MessageBus (broadcast)\n └── synthesis agent ←── 汇总所有子结果 → 最终输出\n```\n\n### 数据流\n\n```\n用户输入 ──▶ Agent Loop ──▶ LLM (流式)\n │ │\n │ TextDelta ──▶ 实时渲染\n │\n └──▶ ToolUse ──▶ 权限检查 ──▶ 并行执行\n │\n ToolResult ◀─────────┘\n │\n 下一轮 LLM 调用\n```\n\n详见 [ARCHITECTURE.md](./ARCHITECTURE.md)。\n\n## 安装\n\n```bash\n# 从源码构建\ngit clone https://github.com/anxiong2025/code-iris.git\ncd code-iris\ncargo build --release\n\n# 安装到系统 (TUI 版)\ncargo install --path crates/iris-tui\n\n# 安装到系统 (CLI 版)\ncargo install --path crates/iris-cli\n```\n\n## 使用\n\n### CLI 模式 (脚本友好)\n\n```bash\n# 扫描项目结构\niris scan .\n\n# 生成架构报告\niris arch . --output report.md\n\n# 分析依赖关系\niris deps .\n\n# 查看统计信息\niris stats .\n\n# 配置 API keys\niris configure\n\n# 列出可用模型\niris models\n```\n\n### TUI 模式 (交互式)\n\n```bash\n# 启动终端界面\ncode-iris\n\n# 指定提供商和模型\ncode-iris --provider anthropic --model claude-sonnet-4-6-20250514\n```\n\n## 支持的 LLM 提供商\n\n### 国际\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| Anthropic (Claude) | `ANTHROPIC_API_KEY` | claude-sonnet-4-6-20250514 |\n| OpenAI (GPT) | `OPENAI_API_KEY` | gpt-4o |\n| Google (Gemini) | `GOOGLE_API_KEY` | gemini-2.0-flash |\n| Groq | `GROQ_API_KEY` | llama-3.3-70b-versatile |\n| OpenRouter | `OPENROUTER_API_KEY` | anthropic/claude-sonnet-4 |\n\n### 中国\n\n| 提供商 | 环境变量 | 默认模型 |\n|--------|---------|---------|\n| DeepSeek | `DEEPSEEK_API_KEY` | deepseek-chat |\n| 智谱 AI (GLM) | `ZHIPU_API_KEY` | glm-4-flash |\n| 通义千问 (Qwen) | `DASHSCOPE_API_KEY` | qwen-plus |\n| 月之暗面 (Kimi) | `MOONSHOT_API_KEY` | moonshot-v1-8k |\n| 百川智能 | `BAICHUAN_API_KEY` | Baichuan4-Air |\n| MiniMax (稀宇) | `MINIMAX_API_KEY` | MiniMax-Text-01 |\n| 零一万物 (Yi) | `YI_API_KEY` | yi-lightning |\n| 硅基流动 | `SILICONFLOW_API_KEY` | deepseek-ai/DeepSeek-V3 |\n| 阶跃星辰 | `STEPFUN_API_KEY` | step-2-16k |\n| 讯飞星火 | `SPARK_API_KEY` | generalv3.5 |\n| Ollama (本地) | — | 自动检测 |\n\n## 安全\n\n- **零 npm 依赖** — 完全消除 npm 供应链攻击面\n- **rustls-tls** — 纯 Rust TLS 实现,不依赖 OpenSSL\n- **cargo audit** — RustSec 已知漏洞扫描\n- **cargo vet** — Mozilla 出品的依赖审查系统\n- **cargo deny** — 许可证 + 依赖来源检查\n- **secrecy** — API key 内存保护,释放时自动清零\n- **静态链接** — 单二进制分发,无运行时依赖\n\n## 开发\n\n```bash\n# 检查编译\ncargo check\n\n# 运行测试\ncargo test\n\n# 安全审计\ncargo audit\ncargo deny check\n\n# 格式化\ncargo fmt\n\n# 代码检查\ncargo clippy\n```\n\n## 路线图\n\n- [x] **Phase 1** — Core + LLM + CLI:多提供商适配、流式输出、代码扫描\n- [x] **Phase 2** — Agent Loop + TUI:tool-call 驱动循环、权限系统、4级上下文压缩、多 Agent 协调、ratatui 终端界面\n- [ ] **Phase 3** — 深度代码理解:多语言 AST 分析、依赖图可视化、MCP 协议集成\n- [ ] **Phase 4** — 桌面端:Tauri GUI,跨平台原生体验\n\n## 致谢\n\n本项目的 Agent 架构设计深受 [Claude Code](https://github.com/anthropics/claude-code)(Anthropic)公开架构思想的启发,特此致谢。Code Iris 是基于这些公开设计理念的独立 Rust 实现,与 Anthropic 官方无任何关联。\n\n## 协议与免责声明\n\n本项目采用 [MIT License](./LICENSE) 开源。\n\n> **免责声明**\n>\n> - 本项目仅供**个人学习、技术研究与非商业用途**,不得用于任何商业目的\n> - 项目中涉及的架构设计思想参考自 Claude Code 等公开资料,相关知识产权归其原始权利人所有\n> - 使用本软件所产生的任何法律责任由使用者自行承担,作者不承担连带责任\n> - 本项目与 Anthropic 公司无任何隶属、授权或合作关系\n"}}, "crates/iris-llm/src/openai.rs": {"509a7b32dfb46d043c8c24c9bab713525fc0dbc4": {"t": 1785745860.80376, "n": 13098, "h": "739d0aa4c7d3bedf", "c": "use anyhow::{bail, Result};\nuse async_stream::stream;\nuse futures::Stream;\nuse reqwest::Response;\nuse secrecy::{ExposeSecret, SecretString};\nuse serde_json::json;\n\nuse crate::sse::parse_openai_sse;\nuse crate::types::{Message, ModelConfig, StreamEvent, TokenUsage, ToolDefinition};\n\n/// Parse a non-streaming OpenAI-format JSON response into a single-item Stream.\nasync fn parse_openai_json(response: Response) -> Result<impl Stream<Item = Result<StreamEvent>>> {\n let text = response.text().await?;\n tracing::debug!(\"Non-streaming JSON body: {text}\");\n let v: serde_json::Value = serde_json::from_str(&text)?;\n\n let content = v\n .pointer(\"/choices/0/message/content\")\n .and_then(|c| c.as_str())\n .unwrap_or(\"\")\n .to_string();\n\n let usage = v.get(\"usage\").and_then(|u| {\n Some(TokenUsage {\n input_tokens: u.get(\"prompt_tokens\")?.as_u64()? as u32,\n output_tokens: u.get(\"completion_tokens\")?.as_u64()? as u32,\n })\n });\n\n Ok(stream! {\n if !content.is_empty() {\n yield Ok(StreamEvent::TextDelta { text: content });\n }\n if let Some(u) = usage {\n yield Ok(StreamEvent::Usage(u));\n }\n yield Ok(StreamEvent::MessageStop);\n })\n}\n\n/// Metadata about a single LLM provider.\n#[derive(Debug, Clone)]\npub struct ProviderInfo {\n /// Short identifier, e.g. \"deepseek\"\n pub name: &'static str,\n /// Environment variable holding the API key, e.g. \"DEEPSEEK_API_KEY\"\n pub env_key: &'static str,\n /// OpenAI-compatible base URL (empty string → Anthropic native provider)\n pub base_url: &'static str,\n /// Default model to use when none is specified\n pub default_model: &'static str,\n /// Human-readable label (bilingual for Chinese providers)\n pub label: &'static str,\n}\n\n/// All 15 supported providers — ported from code-robin providers.py.\npub static PROVIDERS: &[ProviderInfo] = &[\n // --- International ---\n ProviderInfo {\n name: \"anthropic\",\n env_key: \"ANTHROPIC_API_KEY\",\n base_url: \"\",\n default_model: \"claude-sonnet-4-6-20250514\",\n label: \"Anthropic (Claude)\",\n },\n ProviderInfo {\n name: \"openai\",\n env_key: \"OPENAI_API_KEY\",\n base_url: \"https://api.openai.com/v1\",\n default_model: \"gpt-4o\",\n label: \"OpenAI (GPT)\",\n },\n ProviderInfo {\n name: \"google\",\n env_key: \"GOOGLE_API_KEY\",\n base_url: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n default_model: \"gemini-2.0-flash\",\n label: \"Google (Gemini)\",\n },\n ProviderInfo {\n name: \"groq\",\n env_key: \"GROQ_API_KEY\",\n base_url: \"https://api.groq.com/openai/v1\",\n default_model: \"llama-3.3-70b-versatile\",\n label: \"Groq\",\n },\n ProviderInfo {\n name: \"openrouter\",\n env_key: \"OPENROUTER_API_KEY\",\n base_url: \"https://openrouter.ai/api/v1\",\n default_model: \"anthropic/claude-sonnet-4\",\n label: \"OpenRouter (多模型聚合)\",\n },\n // --- China ---\n ProviderInfo {\n name: \"deepseek\",\n env_key: \"DEEPSEEK_API_KEY\",\n base_url: \"https://api.deepseek.com/v1\",\n default_model: \"deepseek-chat\",\n label: \"DeepSeek (深度求索)\",\n },\n ProviderInfo {\n name: \"zhipu\",\n env_key: \"ZHIPU_API_KEY\",\n base_url: \"https://open.bigmodel.cn/api/paas/v4\",\n default_model: \"glm-4-flash\",\n label: \"智谱 AI (GLM)\",\n },\n ProviderInfo {\n name: \"qwen\",\n env_key: \"DASHSCOPE_API_KEY\",\n base_url: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n default_model: \"qwen-plus\",\n label: \"通义千问 (Qwen/百炼)\",\n },\n ProviderInfo {\n name: \"moonshot\",\n env_key: \"MOONSHOT_API_KEY\",\n base_url: \"https://api.moonshot.cn/v1\",\n default_model: \"moonshot-v1-8k\",\n label: \"月之暗面 (Kimi)\",\n },\n ProviderInfo {\n name: \"baichuan\",\n env_key: \"BAICHUAN_API_KEY\",\n base_url: \"https://api.baichuan-ai.com/v1\",\n default_model: \"Baichuan4-Air\",\n label: \"百川智能\",\n },\n ProviderInfo {\n name: \"minimax\",\n env_key: \"MINIMAX_API_KEY\",\n base_url: \"https://api.minimax.chat/v1\",\n default_model: \"MiniMax-Text-01\",\n label: \"MiniMax (稀宇)\",\n },\n ProviderInfo {\n name: \"yi\",\n env_key: \"YI_API_KEY\",\n base_url: \"https://api.lingyiwanwu.com/v1\",\n default_model: \"yi-lightning\",\n label: \"零一万物 (Yi)\",\n },\n ProviderInfo {\n name: \"siliconflow\",\n env_key: \"SILICONFLOW_API_KEY\",\n base_url: \"https://api.siliconflow.cn/v1\",\n default_model: \"deepseek-ai/DeepSeek-V3\",\n label: \"硅基流动 (SiliconFlow)\",\n },\n ProviderInfo {\n name: \"stepfun\",\n env_key: \"STEPFUN_API_KEY\",\n base_url: \"https://api.stepfun.com/v1\",\n default_model: \"step-2-16k\",\n label: \"阶跃星辰 (StepFun)\",\n },\n ProviderInfo {\n name: \"spark\",\n env_key: \"SPARK_API_KEY\",\n base_url: \"https://spark-api-open.xf-yun.com/v1\",\n default_model: \"generalv3.5\",\n label: \"讯飞星火 (Spark)\",\n },\n];\n\n/// Return the first provider that has an API key set in the environment.\npub fn detect_provider() -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| {\n std::env::var(p.env_key).map(|v| !v.is_empty()).unwrap_or(false)\n })\n}\n\n/// Look up a provider by name.\npub fn get_provider(name: &str) -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| p.name == name)\n}\n\n/// OpenAI-compatible streaming provider.\n///\n/// Covers: OpenAI, DeepSeek, Groq, OpenRouter, all Chinese providers.\npub struct OpenAiCompatProvider {\n pub name: String,\n pub api_key: SecretString,\n pub client: reqwest::Client,\n pub base_url: String,\n pub default_model: String,\n}\n\nimpl OpenAiCompatProvider {\n pub fn new(\n name: impl Into<String>,\n api_key: impl Into<String>,\n base_url: impl Into<String>,\n default_model: impl Into<String>,\n ) -> Self {\n let client = reqwest::Client::builder()\n .use_rustls_tls()\n .build()\n .expect(\"Failed to build reqwest client with rustls\");\n\n Self {\n name: name.into(),\n api_key: SecretString::new(api_key.into().into()),\n client,\n base_url: base_url.into(),\n default_model: default_model.into(),\n }\n }\n\n /// Build from a ProviderInfo + API key.\n pub fn from_info(info: &ProviderInfo, api_key: impl Into<String>) -> Self {\n Self::new(info.name, api_key, info.base_url, info.default_model)\n }\n\n /// Stream a chat completion.\n pub async fn chat_stream(\n &self,\n messages: &[Message],\n tools: &[ToolDefinition],\n config: &ModelConfig,\n ) -> Result<Box<dyn Stream<Item = Result<StreamEvent>> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec<serde_json::Value> = Vec::new();\n if let Some(ref sys) = config.system_prompt {\n openai_messages.push(json!({\"role\": \"system\", \"content\": sys}));\n }\n for msg in messages {\n match msg.role {\n crate::types::Role::User => {\n // User messages may contain text or tool_results.\n // Tool results must be sent as separate \"tool\" role messages.\n for block in &msg.content {\n match block {\n crate::types::ContentBlock::Text { text } => {\n openai_messages.push(json!({\"role\": \"user\", \"content\": text}));\n }\n crate::types::ContentBlock::ToolResult { tool_use_id, content, is_error } => {\n let mut m = json!({\n \"role\": \"tool\",\n \"tool_call_id\": tool_use_id,\n \"content\": content,\n });\n if *is_error == Some(true) {\n // Some providers support error indication\n m[\"name\"] = json!(\"error\");\n }\n openai_messages.push(m);\n }\n _ => {}\n }\n }\n }\n crate::types::Role::Assistant => {\n // Assistant messages may contain text + tool_use blocks.\n let mut text_parts: Vec<String> = Vec::new();\n let mut tool_calls: Vec<serde_json::Value> = Vec::new();\n for block in &msg.content {\n match block {\n crate::types::ContentBlock::Text { text } => {\n text_parts.push(text.clone());\n }\n crate::types::ContentBlock::ToolUse { id, name, input } => {\n tool_calls.push(json!({\n \"id\": id,\n \"type\": \"function\",\n \"function\": {\n \"name\": name,\n \"arguments\": input.to_string(),\n }\n }));\n }\n _ => {}\n }\n }\n let mut m = json!({\"role\": \"assistant\"});\n let content_str = text_parts.join(\"\\n\");\n if !content_str.is_empty() {\n m[\"content\"] = json!(content_str);\n }\n if !tool_calls.is_empty() {\n m[\"tool_calls\"] = json!(tool_calls);\n }\n openai_messages.push(m);\n }\n crate::types::Role::Tool => {\n // Legacy path — shouldn't normally be reached\n let content: String = msg.content.iter().filter_map(|b| match b {\n crate::types::ContentBlock::Text { text } => Some(text.clone()),\n crate::types::ContentBlock::ToolResult { content, .. } => Some(content.clone()),\n _ => None,\n }).collect::<Vec<_>>().join(\"\\n\");\n openai_messages.push(json!({\"role\": \"assistant\", \"content\": content}));\n }\n }\n }\n\n // Convert tool definitions to OpenAI function calling format.\n let openai_tools: Vec<serde_json::Value> = tools.iter().map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n }).collect();\n\n let mut body = json!({\n \"model\": config.model,\n \"max_tokens\": config.max_tokens,\n \"messages\": openai_messages,\n \"stream\": true,\n });\n if !openai_tools.is_empty() {\n body[\"tools\"] = json!(openai_tools);\n body[\"tool_choice\"] = json!(\"auto\");\n }\n\n let response = self\n .client\n .post(&url)\n .header(\"Authorization\", format!(\"Bearer {}\", self.api_key.expose_secret()))\n .header(\"Content-Type\", \"application/json\")\n .json(&body)\n .send()\n .await?;\n\n let status = response.status();\n let content_type = response\n .headers()\n .get(\"content-type\")\n .and_then(|v| v.to_str().ok())\n .unwrap_or(\"\")\n .to_string();\n tracing::debug!(provider = self.name, %status, %content_type, \"LLM response status\");\n if !status.is_success() {\n let body = response.text().await.unwrap_or_default();\n bail!(\"Provider {} error {status}: {body}\", self.name);\n }\n\n // Debug mode: dump raw body instead of streaming (set IRIS_DEBUG_BODY=1)\n if std::env::var(\"IRIS_DEBUG_BODY\").as_deref() == Ok(\"1\") {\n let raw = response.text().await.unwrap_or_default();\n eprintln!(\"[DEBUG BODY] provider={} content-type={}\\n{}\", self.name, content_type, raw);\n bail!(\"Debug body dump complete — unset IRIS_DEBUG_BODY to resume normal operation\");\n }\n\n // If content-type is JSON (not event-stream), parse as non-streaming response.\n if content_type.contains(\"application/json\") && !content_type.contains(\"event-stream\") {\n return Ok(Box::new(Box::pin(parse_openai_json(response).await?)));\n }\n\n Ok(Box::new(Box::pin(parse_openai_sse(response))))\n }\n}\n"}, "5ca88cd7dc0a90cfc5afe3f9c13910449194f32b": {"t": 1785757345.3947449, "n": 13098, "h": "739d0aa4c7d3bedf", "c": "use anyhow::{bail, Result};\nuse async_stream::stream;\nuse futures::Stream;\nuse reqwest::Response;\nuse secrecy::{ExposeSecret, SecretString};\nuse serde_json::json;\n\nuse crate::sse::parse_openai_sse;\nuse crate::types::{Message, ModelConfig, StreamEvent, TokenUsage, ToolDefinition};\n\n/// Parse a non-streaming OpenAI-format JSON response into a single-item Stream.\nasync fn parse_openai_json(response: Response) -> Result<impl Stream<Item = Result<StreamEvent>>> {\n let text = response.text().await?;\n tracing::debug!(\"Non-streaming JSON body: {text}\");\n let v: serde_json::Value = serde_json::from_str(&text)?;\n\n let content = v\n .pointer(\"/choices/0/message/content\")\n .and_then(|c| c.as_str())\n .unwrap_or(\"\")\n .to_string();\n\n let usage = v.get(\"usage\").and_then(|u| {\n Some(TokenUsage {\n input_tokens: u.get(\"prompt_tokens\")?.as_u64()? as u32,\n output_tokens: u.get(\"completion_tokens\")?.as_u64()? as u32,\n })\n });\n\n Ok(stream! {\n if !content.is_empty() {\n yield Ok(StreamEvent::TextDelta { text: content });\n }\n if let Some(u) = usage {\n yield Ok(StreamEvent::Usage(u));\n }\n yield Ok(StreamEvent::MessageStop);\n })\n}\n\n/// Metadata about a single LLM provider.\n#[derive(Debug, Clone)]\npub struct ProviderInfo {\n /// Short identifier, e.g. \"deepseek\"\n pub name: &'static str,\n /// Environment variable holding the API key, e.g. \"DEEPSEEK_API_KEY\"\n pub env_key: &'static str,\n /// OpenAI-compatible base URL (empty string → Anthropic native provider)\n pub base_url: &'static str,\n /// Default model to use when none is specified\n pub default_model: &'static str,\n /// Human-readable label (bilingual for Chinese providers)\n pub label: &'static str,\n}\n\n/// All 15 supported providers — ported from code-robin providers.py.\npub static PROVIDERS: &[ProviderInfo] = &[\n // --- International ---\n ProviderInfo {\n name: \"anthropic\",\n env_key: \"ANTHROPIC_API_KEY\",\n base_url: \"\",\n default_model: \"claude-sonnet-4-6-20250514\",\n label: \"Anthropic (Claude)\",\n },\n ProviderInfo {\n name: \"openai\",\n env_key: \"OPENAI_API_KEY\",\n base_url: \"https://api.openai.com/v1\",\n default_model: \"gpt-4o\",\n label: \"OpenAI (GPT)\",\n },\n ProviderInfo {\n name: \"google\",\n env_key: \"GOOGLE_API_KEY\",\n base_url: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n default_model: \"gemini-2.0-flash\",\n label: \"Google (Gemini)\",\n },\n ProviderInfo {\n name: \"groq\",\n env_key: \"GROQ_API_KEY\",\n base_url: \"https://api.groq.com/openai/v1\",\n default_model: \"llama-3.3-70b-versatile\",\n label: \"Groq\",\n },\n ProviderInfo {\n name: \"openrouter\",\n env_key: \"OPENROUTER_API_KEY\",\n base_url: \"https://openrouter.ai/api/v1\",\n default_model: \"anthropic/claude-sonnet-4\",\n label: \"OpenRouter (多模型聚合)\",\n },\n // --- China ---\n ProviderInfo {\n name: \"deepseek\",\n env_key: \"DEEPSEEK_API_KEY\",\n base_url: \"https://api.deepseek.com/v1\",\n default_model: \"deepseek-chat\",\n label: \"DeepSeek (深度求索)\",\n },\n ProviderInfo {\n name: \"zhipu\",\n env_key: \"ZHIPU_API_KEY\",\n base_url: \"https://open.bigmodel.cn/api/paas/v4\",\n default_model: \"glm-4-flash\",\n label: \"智谱 AI (GLM)\",\n },\n ProviderInfo {\n name: \"qwen\",\n env_key: \"DASHSCOPE_API_KEY\",\n base_url: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n default_model: \"qwen-plus\",\n label: \"通义千问 (Qwen/百炼)\",\n },\n ProviderInfo {\n name: \"moonshot\",\n env_key: \"MOONSHOT_API_KEY\",\n base_url: \"https://api.moonshot.cn/v1\",\n default_model: \"moonshot-v1-8k\",\n label: \"月之暗面 (Kimi)\",\n },\n ProviderInfo {\n name: \"baichuan\",\n env_key: \"BAICHUAN_API_KEY\",\n base_url: \"https://api.baichuan-ai.com/v1\",\n default_model: \"Baichuan4-Air\",\n label: \"百川智能\",\n },\n ProviderInfo {\n name: \"minimax\",\n env_key: \"MINIMAX_API_KEY\",\n base_url: \"https://api.minimax.chat/v1\",\n default_model: \"MiniMax-Text-01\",\n label: \"MiniMax (稀宇)\",\n },\n ProviderInfo {\n name: \"yi\",\n env_key: \"YI_API_KEY\",\n base_url: \"https://api.lingyiwanwu.com/v1\",\n default_model: \"yi-lightning\",\n label: \"零一万物 (Yi)\",\n },\n ProviderInfo {\n name: \"siliconflow\",\n env_key: \"SILICONFLOW_API_KEY\",\n base_url: \"https://api.siliconflow.cn/v1\",\n default_model: \"deepseek-ai/DeepSeek-V3\",\n label: \"硅基流动 (SiliconFlow)\",\n },\n ProviderInfo {\n name: \"stepfun\",\n env_key: \"STEPFUN_API_KEY\",\n base_url: \"https://api.stepfun.com/v1\",\n default_model: \"step-2-16k\",\n label: \"阶跃星辰 (StepFun)\",\n },\n ProviderInfo {\n name: \"spark\",\n env_key: \"SPARK_API_KEY\",\n base_url: \"https://spark-api-open.xf-yun.com/v1\",\n default_model: \"generalv3.5\",\n label: \"讯飞星火 (Spark)\",\n },\n];\n\n/// Return the first provider that has an API key set in the environment.\npub fn detect_provider() -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| {\n std::env::var(p.env_key).map(|v| !v.is_empty()).unwrap_or(false)\n })\n}\n\n/// Look up a provider by name.\npub fn get_provider(name: &str) -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| p.name == name)\n}\n\n/// OpenAI-compatible streaming provider.\n///\n/// Covers: OpenAI, DeepSeek, Groq, OpenRouter, all Chinese providers.\npub struct OpenAiCompatProvider {\n pub name: String,\n pub api_key: SecretString,\n pub client: reqwest::Client,\n pub base_url: String,\n pub default_model: String,\n}\n\nimpl OpenAiCompatProvider {\n pub fn new(\n name: impl Into<String>,\n api_key: impl Into<String>,\n base_url: impl Into<String>,\n default_model: impl Into<String>,\n ) -> Self {\n let client = reqwest::Client::builder()\n .use_rustls_tls()\n .build()\n .expect(\"Failed to build reqwest client with rustls\");\n\n Self {\n name: name.into(),\n api_key: SecretString::new(api_key.into().into()),\n client,\n base_url: base_url.into(),\n default_model: default_model.into(),\n }\n }\n\n /// Build from a ProviderInfo + API key.\n pub fn from_info(info: &ProviderInfo, api_key: impl Into<String>) -> Self {\n Self::new(info.name, api_key, info.base_url, info.default_model)\n }\n\n /// Stream a chat completion.\n pub async fn chat_stream(\n &self,\n messages: &[Message],\n tools: &[ToolDefinition],\n config: &ModelConfig,\n ) -> Result<Box<dyn Stream<Item = Result<StreamEvent>> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec<serde_json::Value> = Vec::new();\n if let Some(ref sys) = config.system_prompt {\n openai_messages.push(json!({\"role\": \"system\", \"content\": sys}));\n }\n for msg in messages {\n match msg.role {\n crate::types::Role::User => {\n // User messages may contain text or tool_results.\n // Tool results must be sent as separate \"tool\" role messages.\n for block in &msg.content {\n match block {\n crate::types::ContentBlock::Text { text } => {\n openai_messages.push(json!({\"role\": \"user\", \"content\": text}));\n }\n crate::types::ContentBlock::ToolResult { tool_use_id, content, is_error } => {\n let mut m = json!({\n \"role\": \"tool\",\n \"tool_call_id\": tool_use_id,\n \"content\": content,\n });\n if *is_error == Some(true) {\n // Some providers support error indication\n m[\"name\"] = json!(\"error\");\n }\n openai_messages.push(m);\n }\n _ => {}\n }\n }\n }\n crate::types::Role::Assistant => {\n // Assistant messages may contain text + tool_use blocks.\n let mut text_parts: Vec<String> = Vec::new();\n let mut tool_calls: Vec<serde_json::Value> = Vec::new();\n for block in &msg.content {\n match block {\n crate::types::ContentBlock::Text { text } => {\n text_parts.push(text.clone());\n }\n crate::types::ContentBlock::ToolUse { id, name, input } => {\n tool_calls.push(json!({\n \"id\": id,\n \"type\": \"function\",\n \"function\": {\n \"name\": name,\n \"arguments\": input.to_string(),\n }\n }));\n }\n _ => {}\n }\n }\n let mut m = json!({\"role\": \"assistant\"});\n let content_str = text_parts.join(\"\\n\");\n if !content_str.is_empty() {\n m[\"content\"] = json!(content_str);\n }\n if !tool_calls.is_empty() {\n m[\"tool_calls\"] = json!(tool_calls);\n }\n openai_messages.push(m);\n }\n crate::types::Role::Tool => {\n // Legacy path — shouldn't normally be reached\n let content: String = msg.content.iter().filter_map(|b| match b {\n crate::types::ContentBlock::Text { text } => Some(text.clone()),\n crate::types::ContentBlock::ToolResult { content, .. } => Some(content.clone()),\n _ => None,\n }).collect::<Vec<_>>().join(\"\\n\");\n openai_messages.push(json!({\"role\": \"assistant\", \"content\": content}));\n }\n }\n }\n\n // Convert tool definitions to OpenAI function calling format.\n let openai_tools: Vec<serde_json::Value> = tools.iter().map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n }).collect();\n\n let mut body = json!({\n \"model\": config.model,\n \"max_tokens\": config.max_tokens,\n \"messages\": openai_messages,\n \"stream\": true,\n });\n if !openai_tools.is_empty() {\n body[\"tools\"] = json!(openai_tools);\n body[\"tool_choice\"] = json!(\"auto\");\n }\n\n let response = self\n .client\n .post(&url)\n .header(\"Authorization\", format!(\"Bearer {}\", self.api_key.expose_secret()))\n .header(\"Content-Type\", \"application/json\")\n .json(&body)\n .send()\n .await?;\n\n let status = response.status();\n let content_type = response\n .headers()\n .get(\"content-type\")\n .and_then(|v| v.to_str().ok())\n .unwrap_or(\"\")\n .to_string();\n tracing::debug!(provider = self.name, %status, %content_type, \"LLM response status\");\n if !status.is_success() {\n let body = response.text().await.unwrap_or_default();\n bail!(\"Provider {} error {status}: {body}\", self.name);\n }\n\n // Debug mode: dump raw body instead of streaming (set IRIS_DEBUG_BODY=1)\n if std::env::var(\"IRIS_DEBUG_BODY\").as_deref() == Ok(\"1\") {\n let raw = response.text().await.unwrap_or_default();\n eprintln!(\"[DEBUG BODY] provider={} content-type={}\\n{}\", self.name, content_type, raw);\n bail!(\"Debug body dump complete — unset IRIS_DEBUG_BODY to resume normal operation\");\n }\n\n // If content-type is JSON (not event-stream), parse as non-streaming response.\n if content_type.contains(\"application/json\") && !content_type.contains(\"event-stream\") {\n return Ok(Box::new(Box::pin(parse_openai_json(response).await?)));\n }\n\n Ok(Box::new(Box::pin(parse_openai_sse(response))))\n }\n}\n"}, "2b61613f2a960531eec6ee8349848cf193c94cfa": {"t": 1785757347.0508473, "n": 9726, "h": "1fd2fb07200604b7", "c": "use anyhow::{bail, Result};\nuse async_stream::stream;\nuse futures::Stream;\nuse reqwest::Response;\nuse secrecy::{ExposeSecret, SecretString};\nuse serde_json::json;\n\nuse crate::sse::parse_openai_sse;\nuse crate::types::{Message, ModelConfig, StreamEvent, TokenUsage, ToolDefinition};\n\n/// Parse a non-streaming OpenAI-format JSON response into a single-item Stream.\nasync fn parse_openai_json(response: Response) -> Result<impl Stream<Item = Result<StreamEvent>>> {\n let text = response.text().await?;\n tracing::debug!(\"Non-streaming JSON body: {text}\");\n let v: serde_json::Value = serde_json::from_str(&text)?;\n\n let content = v\n .pointer(\"/choices/0/message/content\")\n .and_then(|c| c.as_str())\n .unwrap_or(\"\")\n .to_string();\n\n let usage = v.get(\"usage\").and_then(|u| {\n Some(TokenUsage {\n input_tokens: u.get(\"prompt_tokens\")?.as_u64()? as u32,\n output_tokens: u.get(\"completion_tokens\")?.as_u64()? as u32,\n })\n });\n\n Ok(stream! {\n if !content.is_empty() {\n yield Ok(StreamEvent::TextDelta { text: content });\n }\n if let Some(u) = usage {\n yield Ok(StreamEvent::Usage(u));\n }\n yield Ok(StreamEvent::MessageStop);\n })\n}\n\n/// Metadata about a single LLM provider.\n#[derive(Debug, Clone)]\npub struct ProviderInfo {\n /// Short identifier, e.g. \"deepseek\"\n pub name: &'static str,\n /// Environment variable holding the API key, e.g. \"DEEPSEEK_API_KEY\"\n pub env_key: &'static str,\n /// OpenAI-compatible base URL (empty string → Anthropic native provider)\n pub base_url: &'static str,\n /// Default model to use when none is specified\n pub default_model: &'static str,\n /// Human-readable label (bilingual for Chinese providers)\n pub label: &'static str,\n}\n\n/// All 15 supported providers — ported from code-robin providers.py.\npub static PROVIDERS: &[ProviderInfo] = &[\n // --- International ---\n ProviderInfo {\n name: \"anthropic\",\n env_key: \"ANTHROPIC_API_KEY\",\n base_url: \"\",\n default_model: \"claude-sonnet-4-6-20250514\",\n label: \"Anthropic (Claude)\",\n },\n ProviderInfo {\n name: \"openai\",\n env_key: \"OPENAI_API_KEY\",\n base_url: \"https://api.openai.com/v1\",\n default_model: \"gpt-4o\",\n label: \"OpenAI (GPT)\",\n },\n ProviderInfo {\n name: \"google\",\n env_key: \"GOOGLE_API_KEY\",\n base_url: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n default_model: \"gemini-2.0-flash\",\n label: \"Google (Gemini)\",\n },\n ProviderInfo {\n name: \"groq\",\n env_key: \"GROQ_API_KEY\",\n base_url: \"https://api.groq.com/openai/v1\",\n default_model: \"llama-3.3-70b-versatile\",\n label: \"Groq\",\n },\n ProviderInfo {\n name: \"openrouter\",\n env_key: \"OPENROUTER_API_KEY\",\n base_url: \"https://openrouter.ai/api/v1\",\n default_model: \"anthropic/claude-sonnet-4\",\n label: \"OpenRouter (多模型聚合)\",\n },\n // --- China ---\n ProviderInfo {\n name: \"deepseek\",\n env_key: \"DEEPSEEK_API_KEY\",\n base_url: \"https://api.deepseek.com/v1\",\n default_model: \"deepseek-chat\",\n label: \"DeepSeek (深度求索)\",\n },\n ProviderInfo {\n name: \"zhipu\",\n env_key: \"ZHIPU_API_KEY\",\n base_url: \"https://open.bigmodel.cn/api/paas/v4\",\n default_model: \"glm-4-flash\",\n label: \"智谱 AI (GLM)\",\n },\n ProviderInfo {\n name: \"qwen\",\n env_key: \"DASHSCOPE_API_KEY\",\n base_url: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n default_model: \"qwen-plus\",\n label: \"通义千问 (Qwen/百炼)\",\n },\n ProviderInfo {\n name: \"moonshot\",\n env_key: \"MOONSHOT_API_KEY\",\n base_url: \"https://api.moonshot.cn/v1\",\n default_model: \"moonshot-v1-8k\",\n label: \"月之暗面 (Kimi)\",\n },\n ProviderInfo {\n name: \"baichuan\",\n env_key: \"BAICHUAN_API_KEY\",\n base_url: \"https://api.baichuan-ai.com/v1\",\n default_model: \"Baichuan4-Air\",\n label: \"百川智能\",\n },\n ProviderInfo {\n name: \"minimax\",\n env_key: \"MINIMAX_API_KEY\",\n base_url: \"https://api.minimax.chat/v1\",\n default_model: \"MiniMax-Text-01\",\n label: \"MiniMax (稀宇)\",\n },\n ProviderInfo {\n name: \"yi\",\n env_key: \"YI_API_KEY\",\n base_url: \"https://api.lingyiwanwu.com/v1\",\n default_model: \"yi-lightning\",\n label: \"零一万物 (Yi)\",\n },\n ProviderInfo {\n name: \"siliconflow\",\n env_key: \"SILICONFLOW_API_KEY\",\n base_url: \"https://api.siliconflow.cn/v1\",\n default_model: \"deepseek-ai/DeepSeek-V3\",\n label: \"硅基流动 (SiliconFlow)\",\n },\n ProviderInfo {\n name: \"stepfun\",\n env_key: \"STEPFUN_API_KEY\",\n base_url: \"https://api.stepfun.com/v1\",\n default_model: \"step-2-16k\",\n label: \"阶跃星辰 (StepFun)\",\n },\n ProviderInfo {\n name: \"spark\",\n env_key: \"SPARK_API_KEY\",\n base_url: \"https://spark-api-open.xf-yun.com/v1\",\n default_model: \"generalv3.5\",\n label: \"讯飞星火 (Spark)\",\n },\n];\n\n/// Return the first provider that has an API key set in the environment.\npub fn detect_provider() -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| {\n std::env::var(p.env_key).map(|v| !v.is_empty()).unwrap_or(false)\n })\n}\n\n/// Look up a provider by name.\npub fn get_provider(name: &str) -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| p.name == name)\n}\n\n/// OpenAI-compatible streaming provider.\n///\n/// Covers: OpenAI, DeepSeek, Groq, OpenRouter, all Chinese providers.\npub struct OpenAiCompatProvider {\n pub name: String,\n pub api_key: SecretString,\n pub client: reqwest::Client,\n pub base_url: String,\n pub default_model: String,\n}\n\nimpl OpenAiCompatProvider {\n pub fn new(\n name: impl Into<String>,\n api_key: impl Into<String>,\n base_url: impl Into<String>,\n default_model: impl Into<String>,\n ) -> Self {\n let client = reqwest::Client::builder()\n .use_rustls_tls()\n .build()\n .expect(\"Failed to build reqwest client with rustls\");\n\n Self {\n name: name.into(),\n api_key: SecretString::new(api_key.into().into()),\n client,\n base_url: base_url.into(),\n default_model: default_model.into(),\n }\n }\n\n /// Build from a ProviderInfo + API key.\n pub fn from_info(info: &ProviderInfo, api_key: impl Into<String>) -> Self {\n Self::new(info.name, api_key, info.base_url, info.default_model)\n }\n\n /// Stream a chat completion.\n pub async fn chat_stream(\n &self,\n messages: &[Message],\n _tools: &[ToolDefinition],\n config: &ModelConfig,\n ) -> Result<Box<dyn Stream<Item = Result<StreamEvent>> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec<serde_json::Value> = Vec::new();\n if let Some(ref sys) = config.system_prompt {\n openai_messages.push(json!({\"role\": \"system\", \"content\": sys}));\n }\n for msg in messages {\n let role = match msg.role {\n crate::types::Role::User => \"user\",\n crate::types::Role::Assistant => \"assistant\",\n crate::types::Role::Tool => \"tool\",\n };\n // Flatten content blocks to a single string for OpenAI compat\n let content: String = msg.content.iter().filter_map(|b| match b {\n crate::types::ContentBlock::Text { text } => Some(text.clone()),\n crate::types::ContentBlock::ToolResult { content, .. } => Some(content.clone()),\n _ => None,\n }).collect::<Vec<_>>().join(\"\\n\");\n openai_messages.push(json!({\"role\": role, \"content\": content}));\n }\n\n let body = json!({\n \"model\": config.model,\n \"max_tokens\": config.max_tokens,\n \"messages\": openai_messages,\n \"stream\": true,\n });\n\n let response = self\n .client\n .post(&url)\n .header(\"Authorization\", format!(\"Bearer {}\", self.api_key.expose_secret()))\n .header(\"Content-Type\", \"application/json\")\n .json(&body)\n .send()\n .await?;\n\n let status = response.status();\n let content_type = response\n .headers()\n .get(\"content-type\")\n .and_then(|v| v.to_str().ok())\n .unwrap_or(\"\")\n .to_string();\n tracing::debug!(provider = self.name, %status, %content_type, \"LLM response status\");\n if !status.is_success() {\n let body = response.text().await.unwrap_or_default();\n bail!(\"Provider {} error {status}: {body}\", self.name);\n }\n\n // Debug mode: dump raw body instead of streaming (set IRIS_DEBUG_BODY=1)\n if std::env::var(\"IRIS_DEBUG_BODY\").as_deref() == Ok(\"1\") {\n let raw = response.text().await.unwrap_or_default();\n eprintln!(\"[DEBUG BODY] provider={} content-type={}\\n{}\", self.name, content_type, raw);\n bail!(\"Debug body dump complete — unset IRIS_DEBUG_BODY to resume normal operation\");\n }\n\n // If content-type is JSON (not event-stream), parse as non-streaming response.\n if content_type.contains(\"application/json\") && !content_type.contains(\"event-stream\") {\n return Ok(Box::new(Box::pin(parse_openai_json(response).await?)));\n }\n\n Ok(Box::new(Box::pin(parse_openai_sse(response))))\n }\n}\n"}, "040f67de205f988b8c9f8a7ad00c87681741975e": {"t": 1785757348.5820987, "n": 7675, "h": "17c0c6e3a375684d", "c": "use anyhow::{bail, Result};\nuse futures::Stream;\nuse secrecy::{ExposeSecret, SecretString};\nuse serde_json::json;\n\nuse crate::sse::parse_openai_sse;\nuse crate::types::{Message, ModelConfig, StreamEvent, ToolDefinition};\n\n/// Metadata about a single LLM provider.\n#[derive(Debug, Clone)]\npub struct ProviderInfo {\n /// Short identifier, e.g. \"deepseek\"\n pub name: &'static str,\n /// Environment variable holding the API key, e.g. \"DEEPSEEK_API_KEY\"\n pub env_key: &'static str,\n /// OpenAI-compatible base URL (empty string → Anthropic native provider)\n pub base_url: &'static str,\n /// Default model to use when none is specified\n pub default_model: &'static str,\n /// Human-readable label (bilingual for Chinese providers)\n pub label: &'static str,\n}\n\n/// All 15 supported providers — ported from code-robin providers.py.\npub static PROVIDERS: &[ProviderInfo] = &[\n // --- International ---\n ProviderInfo {\n name: \"anthropic\",\n env_key: \"ANTHROPIC_API_KEY\",\n base_url: \"\",\n default_model: \"claude-sonnet-4-6-20250514\",\n label: \"Anthropic (Claude)\",\n },\n ProviderInfo {\n name: \"openai\",\n env_key: \"OPENAI_API_KEY\",\n base_url: \"https://api.openai.com/v1\",\n default_model: \"gpt-4o\",\n label: \"OpenAI (GPT)\",\n },\n ProviderInfo {\n name: \"google\",\n env_key: \"GOOGLE_API_KEY\",\n base_url: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n default_model: \"gemini-2.0-flash\",\n label: \"Google (Gemini)\",\n },\n ProviderInfo {\n name: \"groq\",\n env_key: \"GROQ_API_KEY\",\n base_url: \"https://api.groq.com/openai/v1\",\n default_model: \"llama-3.3-70b-versatile\",\n label: \"Groq\",\n },\n ProviderInfo {\n name: \"openrouter\",\n env_key: \"OPENROUTER_API_KEY\",\n base_url: \"https://openrouter.ai/api/v1\",\n default_model: \"anthropic/claude-sonnet-4\",\n label: \"OpenRouter (多模型聚合)\",\n },\n // --- China ---\n ProviderInfo {\n name: \"deepseek\",\n env_key: \"DEEPSEEK_API_KEY\",\n base_url: \"https://api.deepseek.com/v1\",\n default_model: \"deepseek-chat\",\n label: \"DeepSeek (深度求索)\",\n },\n ProviderInfo {\n name: \"zhipu\",\n env_key: \"ZHIPU_API_KEY\",\n base_url: \"https://open.bigmodel.cn/api/paas/v4\",\n default_model: \"glm-4-flash\",\n label: \"智谱 AI (GLM)\",\n },\n ProviderInfo {\n name: \"qwen\",\n env_key: \"DASHSCOPE_API_KEY\",\n base_url: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n default_model: \"qwen-plus\",\n label: \"通义千问 (Qwen/百炼)\",\n },\n ProviderInfo {\n name: \"moonshot\",\n env_key: \"MOONSHOT_API_KEY\",\n base_url: \"https://api.moonshot.cn/v1\",\n default_model: \"moonshot-v1-8k\",\n label: \"月之暗面 (Kimi)\",\n },\n ProviderInfo {\n name: \"baichuan\",\n env_key: \"BAICHUAN_API_KEY\",\n base_url: \"https://api.baichuan-ai.com/v1\",\n default_model: \"Baichuan4-Air\",\n label: \"百川智能\",\n },\n ProviderInfo {\n name: \"minimax\",\n env_key: \"MINIMAX_API_KEY\",\n base_url: \"https://api.minimax.chat/v1\",\n default_model: \"MiniMax-Text-01\",\n label: \"MiniMax (稀宇)\",\n },\n ProviderInfo {\n name: \"yi\",\n env_key: \"YI_API_KEY\",\n base_url: \"https://api.lingyiwanwu.com/v1\",\n default_model: \"yi-lightning\",\n label: \"零一万物 (Yi)\",\n },\n ProviderInfo {\n name: \"siliconflow\",\n env_key: \"SILICONFLOW_API_KEY\",\n base_url: \"https://api.siliconflow.cn/v1\",\n default_model: \"deepseek-ai/DeepSeek-V3\",\n label: \"硅基流动 (SiliconFlow)\",\n },\n ProviderInfo {\n name: \"stepfun\",\n env_key: \"STEPFUN_API_KEY\",\n base_url: \"https://api.stepfun.com/v1\",\n default_model: \"step-2-16k\",\n label: \"阶跃星辰 (StepFun)\",\n },\n ProviderInfo {\n name: \"spark\",\n env_key: \"SPARK_API_KEY\",\n base_url: \"https://spark-api-open.xf-yun.com/v1\",\n default_model: \"generalv3.5\",\n label: \"讯飞星火 (Spark)\",\n },\n];\n\n/// Return the first provider that has an API key set in the environment.\npub fn detect_provider() -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| {\n std::env::var(p.env_key).map(|v| !v.is_empty()).unwrap_or(false)\n })\n}\n\n/// Look up a provider by name.\npub fn get_provider(name: &str) -> Option<&'static ProviderInfo> {\n PROVIDERS.iter().find(|p| p.name == name)\n}\n\n/// OpenAI-compatible streaming provider.\n///\n/// Covers: OpenAI, DeepSeek, Groq, OpenRouter, all Chinese providers.\npub struct OpenAiCompatProvider {\n pub name: String,\n pub api_key: SecretString,\n pub client: reqwest::Client,\n pub base_url: String,\n pub default_model: String,\n}\n\nimpl OpenAiCompatProvider {\n pub fn new(\n name: impl Into<String>,\n api_key: impl Into<String>,\n base_url: impl Into<String>,\n default_model: impl Into<String>,\n ) -> Self {\n let client = reqwest::Client::builder()\n .use_rustls_tls()\n .build()\n .expect(\"Failed to build reqwest client with rustls\");\n\n Self {\n name: name.into(),\n api_key: SecretString::new(api_key.into().into()),\n client,\n base_url: base_url.into(),\n default_model: default_model.into(),\n }\n }\n\n /// Build from a ProviderInfo + API key.\n pub fn from_info(info: &ProviderInfo, api_key: impl Into<String>) -> Self {\n Self::new(info.name, api_key, info.base_url, info.default_model)\n }\n\n /// Stream a chat completion.\n pub async fn chat_stream(\n &self,\n messages: &[Message],\n _tools: &[ToolDefinition],\n config: &ModelConfig,\n ) -> Result<impl Stream<Item = Result<StreamEvent>>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec<serde_json::Value> = Vec::new();\n if let Some(ref sys) = config.system_prompt {\n openai_messages.push(json!({\"role\": \"system\", \"content\": sys}));\n }\n for msg in messages {\n let role = match msg.role {\n crate::types::Role::User => \"user\",\n crate::types::Role::Assistant => \"assistant\",\n crate::types::Role::Tool => \"tool\",\n };\n // Flatten content blocks to a single string for OpenAI compat\n let content: String = msg.content.iter().filter_map(|b| match b {\n crate::types::ContentBlock::Text { text } => Some(text.clone()),\n crate::types::ContentBlock::ToolResult { content, .. } => Some(content.clone()),\n _ => None,\n }).collect::<Vec<_>>().join(\"\\n\");\n openai_messages.push(json!({\"role\": role, \"content\": content}));\n }\n\n let body = json!({\n \"model\": config.model,\n \"max_tokens\": config.max_tokens,\n \"messages\": openai_messages,\n \"stream\": true,\n });\n\n let response = self\n .client\n .post(&url)\n .header(\"Authorization\", format!(\"Bearer {}\", self.api_key.expose_secret()))\n .header(\"Content-Type\", \"application/json\")\n .json(&body)\n .send()\n .await?;\n\n if !response.status().is_success() {\n let status = response.status();\n let body = response.text().await.unwrap_or_default();\n bail!(\"Provider {} error {status}: {body}\", self.name);\n }\n\n Ok(parse_openai_sse(response))\n }\n}\n"}}}}