{"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 # 续上次会话\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 # 续上次会话\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 # 续上次会话\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 # 续上次会话\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>> {\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,\n api_key: impl Into,\n base_url: impl Into,\n default_model: impl Into,\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) -> 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> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec = 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 = Vec::new();\n let mut tool_calls: Vec = 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::>().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 = 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>> {\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,\n api_key: impl Into,\n base_url: impl Into,\n default_model: impl Into,\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) -> 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> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec = 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 = Vec::new();\n let mut tool_calls: Vec = 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::>().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 = 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>> {\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,\n api_key: impl Into,\n base_url: impl Into,\n default_model: impl Into,\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) -> 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> + Unpin + Send>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec = 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::>().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,\n api_key: impl Into,\n base_url: impl Into,\n default_model: impl Into,\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) -> 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>> {\n let url = format!(\"{}/chat/completions\", self.base_url.trim_end_matches('/'));\n\n let mut openai_messages: Vec = 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::>().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"}}}}