Merge branch 'main' into feat/azure-openai-codex-series

This commit is contained in:
YanZhiwei 2026-04-03 09:21:05 +08:00 committed by GitHub
commit 25d25f8b40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 985 additions and 56 deletions

View File

@ -1,11 +1,51 @@
ANTHROPIC_AUTH_TOKEN=your_token_here
ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_MODEL=MiniMax-M2.7-highspeed
API_TIMEOUT_MS=3000000
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
# ============================================================
# MiniMax直连 Anthropic 兼容接口)
# ============================================================
# ANTHROPIC_AUTH_TOKEN=your_token_here
# ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
# ANTHROPIC_MODEL=MiniMax-M2.7-highspeed
# ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M2.7-highspeed
# ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M2.7-highspeed
# ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M2.7-highspeed
# API_TIMEOUT_MS=3000000
# ============================================================
# OpenAI通过 LiteLLM 代理)
# 先启动: litellm --config litellm_config.yaml --port 4000
# ============================================================
# ANTHROPIC_AUTH_TOKEN=sk-anything
# ANTHROPIC_BASE_URL=http://localhost:4000
# ANTHROPIC_MODEL=gpt-4o
# ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-4o
# ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-4o
# ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-4o
# API_TIMEOUT_MS=3000000
# ============================================================
# DeepSeek通过 LiteLLM 代理)
# 先启动: litellm --config litellm_config.yaml --port 4000
# ============================================================
# ANTHROPIC_AUTH_TOKEN=sk-anything
# ANTHROPIC_BASE_URL=http://localhost:4000
# ANTHROPIC_MODEL=deepseek-chat
# ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-chat
# ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-chat
# ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-chat
# API_TIMEOUT_MS=3000000
# ============================================================
# OpenRouter直连 Anthropic 兼容接口)
# ============================================================
# ANTHROPIC_AUTH_TOKEN=sk-or-v1-xxx
# ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
# ANTHROPIC_MODEL=openai/gpt-4o
# ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-4o
# ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-4o-mini
# ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-4o
# ============================================================
# 通用设置(建议始终开启)
# ============================================================
DISABLE_TELEMETRY=1
# Azure OpenAI (Codex)
@ -14,3 +54,4 @@ AZURE_OPENAI_BASE_URL=https://your-resource.cognitiveservices.azure.com
AZURE_OPENAI_API_VERSION=2025-04-01-preview
AZURE_OPENAI_API_KEY=your_azure_openai_key
AZURE_OPENAI_CODEX_DEPLOYMENT=your_codex_deployment
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

View File

@ -10,12 +10,26 @@ A **locally runnable version** repaired from the leaked Claude Code source, with
<img src="docs/00runtime.png" alt="Runtime screenshot" width="800">
</p>
## Table of Contents
- [Features](#features)
- [Architecture Overview](#architecture-overview)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Fallback Mode](#fallback-mode)
- [FAQ](#faq)
- [Fixes Compared with the Original Leaked Source](#fixes-compared-with-the-original-leaked-source)
- [Project Structure](#project-structure)
- [Tech Stack](#tech-stack)
---
## Features
- Full Ink TUI experience (matching the official Claude Code interface)
- `--print` headless mode for scripts and CI
- MCP server, plugin, and Skills support
- Custom API endpoint and model support
- Custom API endpoint and model support ([Third-Party Models Guide](docs/third-party-models.en.md))
- Fallback Recovery CLI mode
---
@ -87,7 +101,7 @@ Copy the example file and fill in your API key:
cp .env.example .env
```
Edit `.env`:
Edit `.env` (the example below uses [MiniMax](https://platform.minimaxi.com/subscribe/token-plan?code=1TG2Cseab2&source=link) as the API provider — you can replace it with any compatible service):
```env
# API authentication (choose one)
@ -111,6 +125,20 @@ DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
> **Tip**: You can also configure environment variables via the `env` field in `~/.claude/settings.json`. This is consistent with the official Claude Code configuration:
>
> ```json
> {
> "env": {
> "ANTHROPIC_AUTH_TOKEN": "sk-xxx",
> "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic",
> "ANTHROPIC_MODEL": "MiniMax-M2.7-highspeed"
> }
> }
> ```
>
> Priority: Environment variables > `.env` file > `~/.claude/settings.json`
### 4. Start
#### macOS / Linux
@ -288,6 +316,42 @@ src/
---
## FAQ
### Q: `undefined is not an object (evaluating 'usage.input_tokens')`
**Cause**: `ANTHROPIC_BASE_URL` is misconfigured. The API endpoint is returning HTML or another non-JSON format instead of a valid Anthropic protocol response.
This project uses the **Anthropic Messages API protocol**. `ANTHROPIC_BASE_URL` must point to an endpoint compatible with Anthropic's `/v1/messages` interface. The Anthropic SDK automatically appends `/v1/messages` to the base URL, so:
- MiniMax: `ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic`
- OpenRouter: `ANTHROPIC_BASE_URL=https://openrouter.ai/api`
- OpenRouter (wrong): `ANTHROPIC_BASE_URL=https://openrouter.ai/anthropic` ❌ (returns HTML)
If your model provider only supports the OpenAI protocol, you need a proxy like LiteLLM for protocol translation. See the [Third-Party Models Guide](docs/third-party-models.en.md).
### Q: `Cannot find package 'bundle'`
```
error: Cannot find package 'bundle' from '.../claude-code-haha/src/entrypoints/cli.tsx'
```
**Cause**: Your Bun version is too old and doesn't support the required `bun:bundle` built-in module.
**Fix**: Upgrade Bun to the latest version:
```bash
bun upgrade
```
### Q: How to use OpenAI / DeepSeek / Ollama or other non-Anthropic models?
This project only supports the Anthropic protocol. If your model provider doesn't natively support the Anthropic protocol, you need a proxy like [LiteLLM](https://github.com/BerriAI/litellm) for protocol translation (OpenAI → Anthropic).
See the [Third-Party Models Guide](docs/third-party-models.en.md) for detailed setup instructions.
---
## Disclaimer
This repository is based on the Claude Code source leaked from the Anthropic npm registry on 2026-03-31. All original source code copyrights belong to [Anthropic](https://www.anthropic.com). It is provided for learning and research purposes only.

View File

@ -10,12 +10,26 @@
<img src="docs/00runtime.png" alt="运行截图" width="800">
</p>
## 目录
- [功能](#功能)
- [架构概览](#架构概览)
- [快速开始](#快速开始)
- [环境变量说明](#环境变量说明)
- [降级模式](#降级模式)
- [常见问题](#常见问题)
- [相对于原始泄露源码的修复](#相对于原始泄露源码的修复)
- [项目结构](#项目结构)
- [技术栈](#技术栈)
---
## 功能
- 完整的 Ink TUI 交互界面(与官方 Claude Code 一致)
- `--print` 无头模式(脚本/CI 场景)
- 支持 MCP 服务器、插件、Skills
- 支持自定义 API 端点和模型
- 支持自定义 API 端点和模型[第三方模型使用指南](docs/third-party-models.md)
- 降级 Recovery CLI 模式
---
@ -87,7 +101,7 @@ bun install
cp .env.example .env
```
编辑 `.env`
编辑 `.env`(以下示例使用 [MiniMax](https://platform.minimaxi.com/subscribe/token-plan?code=1TG2Cseab2&source=link) 作为 API 提供商,也可替换为其他兼容服务)
```env
# API 认证(二选一)
@ -111,6 +125,20 @@ DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
> **提示**:除了 `.env` 文件,你也可以通过 `~/.claude/settings.json``env` 字段配置环境变量。这与官方 Claude Code 的配置方式一致:
>
> ```json
> {
> "env": {
> "ANTHROPIC_AUTH_TOKEN": "sk-xxx",
> "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic",
> "ANTHROPIC_MODEL": "MiniMax-M2.7-highspeed"
> }
> }
> ```
>
> 配置优先级:环境变量 > `.env` 文件 > `~/.claude/settings.json`
### 4. 启动
#### macOS / Linux
@ -271,6 +299,42 @@ src/
---
## 常见问题
### Q: `undefined is not an object (evaluating 'usage.input_tokens')`
**原因**`ANTHROPIC_BASE_URL` 配置不正确API 端点返回的不是 Anthropic 协议格式的 JSON而是 HTML 页面或其他格式。
本项目使用 **Anthropic Messages API 协议**`ANTHROPIC_BASE_URL` 必须指向一个兼容 Anthropic `/v1/messages` 接口的端点。Anthropic SDK 会自动在 base URL 后面拼接 `/v1/messages`,所以:
- MiniMax`ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic`
- OpenRouter`ANTHROPIC_BASE_URL=https://openrouter.ai/api`
- OpenRouter 错误写法:`ANTHROPIC_BASE_URL=https://openrouter.ai/anthropic` ❌(返回 HTML
如果你的模型供应商只支持 OpenAI 协议,需要通过 LiteLLM 等代理做协议转换,详见 [第三方模型使用指南](docs/third-party-models.md)。
### Q: `Cannot find package 'bundle'`
```
error: Cannot find package 'bundle' from '.../claude-code-haha/src/entrypoints/cli.tsx'
```
**原因**Bun 版本过低,不支持项目所需的 `bun:bundle` 等内置模块。
**解决**:升级 Bun 到最新版本:
```bash
bun upgrade
```
### Q: 怎么接入 OpenAI / DeepSeek / Ollama 等非 Anthropic 模型?
本项目只支持 Anthropic 协议。如果模型供应商不直接支持 Anthropic 协议,需要用 [LiteLLM](https://github.com/BerriAI/litellm) 等代理做协议转换OpenAI → Anthropic
详细配置步骤请参考:[第三方模型使用指南](docs/third-party-models.md)
---
## Disclaimer
本仓库基于 2026-03-31 从 Anthropic npm registry 泄露的 Claude Code 源码。所有原始源码版权归 [Anthropic](https://www.anthropic.com) 所有。仅供学习和研究用途。

View File

@ -0,0 +1,254 @@
# Using Third-Party Models (OpenAI / DeepSeek / Local Models)
This project communicates with LLMs via the Anthropic protocol. By using a protocol translation proxy, you can use any model including OpenAI, DeepSeek, Ollama, etc.
## How It Works
```
claude-code-haha ──Anthropic protocol──▶ LiteLLM Proxy ──OpenAI protocol──▶ Target Model API
(translation)
```
This project sends Anthropic Messages API requests. The LiteLLM proxy automatically translates them to OpenAI Chat Completions API format and forwards them to the target model.
---
## Option 1: LiteLLM Proxy (Recommended)
[LiteLLM](https://github.com/BerriAI/litellm) is a unified proxy gateway supporting 100+ LLMs (41k+ GitHub Stars), with native support for receiving Anthropic protocol requests.
### 1. Install LiteLLM
```bash
pip install 'litellm[proxy]'
```
### 2. Create Configuration File
Create `litellm_config.yaml`:
#### Using OpenAI Models
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
drop_params: true # Drop Anthropic-specific params (thinking, etc.)
```
#### Using DeepSeek Models
```yaml
model_list:
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
api_base: https://api.deepseek.com
litellm_settings:
drop_params: true
```
#### Using Ollama Local Models
```yaml
model_list:
- model_name: llama3
litellm_params:
model: ollama/llama3
api_base: http://localhost:11434
litellm_settings:
drop_params: true
```
#### Using Multiple Models (switchable after startup)
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
api_base: https://api.deepseek.com
- model_name: llama3
litellm_params:
model: ollama/llama3
api_base: http://localhost:11434
litellm_settings:
drop_params: true
```
### 3. Start the Proxy
```bash
# Set your target model's API key
export OPENAI_API_KEY=sk-xxx
# or
export DEEPSEEK_API_KEY=sk-xxx
# Start the proxy
litellm --config litellm_config.yaml --port 4000
```
The proxy will listen on `http://localhost:4000` and expose an Anthropic-compatible `/v1/messages` endpoint.
### 4. Configure This Project
Choose one of two configuration methods:
#### Method A: Via `.env` File
```env
ANTHROPIC_AUTH_TOKEN=sk-anything
ANTHROPIC_BASE_URL=http://localhost:4000
ANTHROPIC_MODEL=gpt-4o
ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-4o
ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-4o
ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-4o
API_TIMEOUT_MS=3000000
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
#### Method B: Via `~/.claude/settings.json`
```json
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-anything",
"ANTHROPIC_BASE_URL": "http://localhost:4000",
"ANTHROPIC_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-4o",
"API_TIMEOUT_MS": "3000000",
"DISABLE_TELEMETRY": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
```
> **Note**: The `ANTHROPIC_AUTH_TOKEN` value can be any string when using the LiteLLM proxy (LiteLLM uses its own configured key for forwarding), unless you've set a `master_key` on the LiteLLM side.
### 5. Start and Verify
```bash
./bin/claude-haha
```
If everything is configured correctly, you should see the normal chat interface, with your configured target model handling the requests.
---
## Option 2: Direct Connection to Anthropic-Compatible Services
Some third-party services directly support the Anthropic Messages API, no proxy needed:
### OpenRouter
```env
ANTHROPIC_AUTH_TOKEN=sk-or-v1-xxx
ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
ANTHROPIC_MODEL=openai/gpt-4o
ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-4o
ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-4o-mini
ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-4o
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
### MiniMax (pre-configured in .env.example)
```env
ANTHROPIC_AUTH_TOKEN=your_token_here
ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
ANTHROPIC_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M2.7-highspeed
API_TIMEOUT_MS=3000000
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
---
## Option 3: Other Proxy Tools
The community has built several proxy tools specifically for Claude Code:
| Tool | Description | Link |
|------|-------------|------|
| **a2o** | Anthropic → OpenAI single binary, zero dependencies | [Twitter](https://x.com/mantou543/status/2018846154855940200) |
| **Empero Proxy** | Full Anthropic Messages API to OpenAI translation | [Twitter](https://x.com/EmperoAI/status/2036840854065762551) |
| **Alma** | Client with built-in OpenAI → Anthropic proxy | [Twitter](https://x.com/yetone/status/2003508782127833332) |
| **Chutes** | Docker container supporting 60+ open-source models | [Twitter](https://x.com/chutes_ai/status/2027039742915662232) |
---
## Known Limitations
### 1. `drop_params: true` Is Essential
This project sends Anthropic-specific parameters (e.g., `thinking`, `cache_control`) that don't exist in the OpenAI API. You must set `drop_params: true` in the LiteLLM config, otherwise requests will fail.
### 2. Extended Thinking Unavailable
Anthropic's Extended Thinking is a proprietary feature not supported by other models. It is automatically disabled when using third-party models.
### 3. Prompt Caching Unavailable
`cache_control` is an Anthropic-specific feature. Prompt caching won't work with third-party models (but won't cause errors — it's silently ignored by `drop_params`).
### 4. Tool Calling Compatibility
This project heavily uses tool calling (tool_use). LiteLLM automatically translates Anthropic's tool_use format to OpenAI's function_calling format. This works in most cases, but some complex tool calls may have compatibility issues. If you encounter problems, try using a more capable model (e.g., GPT-4o).
### 5. Telemetry and Non-Essential Requests
Configure these environment variables to avoid unnecessary network requests:
```
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
---
## FAQ
### Q: LiteLLM proxy returns `/v1/responses` not found?
Some OpenAI-compatible services only support `/v1/chat/completions`. Add this to your LiteLLM config:
```yaml
litellm_settings:
use_chat_completions_url_for_anthropic_messages: true
```
### Q: What's the difference between `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN`?
- `ANTHROPIC_API_KEY` → Sent via `x-api-key` header
- `ANTHROPIC_AUTH_TOKEN` → Sent via `Authorization: Bearer` header
LiteLLM proxy accepts Bearer Token format by default, so `ANTHROPIC_AUTH_TOKEN` is recommended.
### Q: Can I configure multiple models?
Yes. Define multiple `model_name` entries in `litellm_config.yaml`, then switch by changing the `ANTHROPIC_MODEL` value.
### Q: Local Ollama models don't work well?
This project's system prompts and tool calls require strong model capabilities. Use larger models (e.g., Llama 3 70B+, Qwen 72B+). Smaller models may fail to handle tool calling correctly.

254
docs/third-party-models.md Normal file
View File

@ -0,0 +1,254 @@
# 使用第三方模型OpenAI / DeepSeek / 本地模型)
本项目基于 Anthropic 协议与 LLM 通信。通过协议转换代理,可以使用 OpenAI、DeepSeek、Ollama 等任意模型。
## 原理
```
claude-code-haha ──Anthropic协议──▶ LiteLLM Proxy ──OpenAI协议──▶ 目标模型 API
(协议转换)
```
本项目发出 Anthropic Messages API 请求LiteLLM 代理将其自动转换为 OpenAI Chat Completions API 格式并转发给目标模型。
---
## 方式一LiteLLM 代理(推荐)
[LiteLLM](https://github.com/BerriAI/litellm) 是一个支持 100+ LLM 的统一代理网关41k+ GitHub Stars原生支持接收 Anthropic 协议请求。
### 1. 安装 LiteLLM
```bash
pip install 'litellm[proxy]'
```
### 2. 创建配置文件
新建 `litellm_config.yaml`
#### 使用 OpenAI 模型
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
litellm_settings:
drop_params: true # 丢弃 Anthropic 专有参数thinking 等)
```
#### 使用 DeepSeek 模型
```yaml
model_list:
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
api_base: https://api.deepseek.com
litellm_settings:
drop_params: true
```
#### 使用 Ollama 本地模型
```yaml
model_list:
- model_name: llama3
litellm_params:
model: ollama/llama3
api_base: http://localhost:11434
litellm_settings:
drop_params: true
```
#### 使用多个模型(可在启动后切换)
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
api_base: https://api.deepseek.com
- model_name: llama3
litellm_params:
model: ollama/llama3
api_base: http://localhost:11434
litellm_settings:
drop_params: true
```
### 3. 启动代理
```bash
# 设置目标模型的 API Key
export OPENAI_API_KEY=sk-xxx
# 或
export DEEPSEEK_API_KEY=sk-xxx
# 启动代理
litellm --config litellm_config.yaml --port 4000
```
代理启动后会在 `http://localhost:4000` 监听,并暴露 Anthropic 兼容的 `/v1/messages` 端点。
### 4. 配置本项目
有两种配置方式,任选其一:
#### 方式 A通过 `.env` 文件
```env
ANTHROPIC_AUTH_TOKEN=sk-anything
ANTHROPIC_BASE_URL=http://localhost:4000
ANTHROPIC_MODEL=gpt-4o
ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-4o
ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-4o
ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-4o
API_TIMEOUT_MS=3000000
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
#### 方式 B通过 `~/.claude/settings.json`
```json
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-anything",
"ANTHROPIC_BASE_URL": "http://localhost:4000",
"ANTHROPIC_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-4o",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-4o",
"API_TIMEOUT_MS": "3000000",
"DISABLE_TELEMETRY": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
```
> **说明**`ANTHROPIC_AUTH_TOKEN` 的值在使用 LiteLLM 代理时可以是任意字符串LiteLLM 会用自己配置的 key 转发),除非你在 LiteLLM 端设置了 `master_key` 校验。
### 5. 启动并验证
```bash
./bin/claude-haha
```
如果一切正常,你应该能看到正常的对话界面,实际调用的是你配置的目标模型。
---
## 方式二:直连兼容 Anthropic 协议的第三方服务
部分第三方服务直接兼容 Anthropic Messages API无需额外代理
### OpenRouter
```env
ANTHROPIC_AUTH_TOKEN=sk-or-v1-xxx
ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
ANTHROPIC_MODEL=openai/gpt-4o
ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-4o
ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-4o-mini
ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-4o
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
### MiniMax已在 .env.example 中配置)
```env
ANTHROPIC_AUTH_TOKEN=your_token_here
ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
ANTHROPIC_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M2.7-highspeed
ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M2.7-highspeed
API_TIMEOUT_MS=3000000
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
---
## 方式三:其他代理工具
社区还有一些专门为 Claude Code 做的代理工具:
| 工具 | 说明 | 链接 |
|------|------|------|
| **a2o** | Anthropic → OpenAI 单二进制文件,零依赖 | [Twitter](https://x.com/mantou543/status/2018846154855940200) |
| **Empero Proxy** | 完整的 Anthropic Messages API 转 OpenAI 代理 | [Twitter](https://x.com/EmperoAI/status/2036840854065762551) |
| **Alma** | 内置 OpenAI → Anthropic 转换代理的客户端 | [Twitter](https://x.com/yetone/status/2003508782127833332) |
| **Chutes** | Docker 容器,支持 60+ 开源模型 | [Twitter](https://x.com/chutes_ai/status/2027039742915662232) |
---
## 注意事项与已知限制
### 1. `drop_params: true` 很重要
本项目会发送 Anthropic 专有参数(如 `thinking``cache_control`),这些参数在 OpenAI API 中不存在。LiteLLM 配置中必须设置 `drop_params: true`,否则请求会报错。
### 2. Extended Thinking 不可用
Anthropic 的 Extended Thinking 功能是专有特性,其他模型不支持。使用第三方模型时此功能自动失效。
### 3. Prompt Caching 不可用
`cache_control` 是 Anthropic 专有功能。使用第三方模型时prompt caching 不会生效(但不会导致报错,会被 `drop_params` 忽略)。
### 4. 工具调用兼容性
本项目大量使用工具调用tool_useLiteLLM 会自动转换 Anthropic tool_use 格式到 OpenAI function_calling 格式。大部分情况下可以正常工作,但某些复杂工具调用可能存在兼容性问题。如遇问题,建议使用能力较强的模型(如 GPT-4o
### 5. 遥测和非必要网络请求
建议配置以下环境变量以避免不必要的网络请求:
```
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
---
## FAQ
### Q: LiteLLM 代理报错 `/v1/responses` 找不到?
部分 OpenAI 兼容服务只支持 `/v1/chat/completions`。在 LiteLLM 配置中添加:
```yaml
litellm_settings:
use_chat_completions_url_for_anthropic_messages: true
```
### Q: `ANTHROPIC_API_KEY``ANTHROPIC_AUTH_TOKEN` 有什么区别?
- `ANTHROPIC_API_KEY` → 通过 `x-api-key` 请求头发送
- `ANTHROPIC_AUTH_TOKEN` → 通过 `Authorization: Bearer` 请求头发送
LiteLLM 代理默认接受 Bearer Token 格式,建议使用 `ANTHROPIC_AUTH_TOKEN`
### Q: 可以同时配置多个模型吗?
可以。在 `litellm_config.yaml` 中配置多个 `model_name`,然后通过修改 `ANTHROPIC_MODEL` 切换。
### Q: 本地 Ollama 模型效果不好怎么办?
本项目的系统提示和工具调用对模型能力要求较高。建议使用参数量较大的模型(如 Llama 3 70B+, Qwen 72B+),小模型可能无法正确处理工具调用。

View File

@ -1,5 +1,4 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures';
import React, { useEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
@ -165,7 +164,6 @@ function spriteColWidth(nameWidth: number): number {
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
// (above input in fullscreen, below in scrollback), so no reservation.
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
if (!feature('BUDDY')) return 0;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return 0;
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
@ -212,7 +210,6 @@ export function CompanionSprite(): React.ReactNode {
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
}, [reaction, setAppState]);
if (!feature('BUDDY')) return null;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null;
const color = RARITY_COLORS[companion.rarity];
@ -337,7 +334,7 @@ export function CompanionFloatingBubble() {
t3 = $[4];
}
useEffect(t2, t3);
if (!feature("BUDDY") || !reaction) {
if (!reaction) {
return null;
}
const companion = getCompanion();

67
src/buddy/observer.ts Normal file
View File

@ -0,0 +1,67 @@
import type { Message } from '../types/message.js'
import { getCompanion } from './companion.js'
import { getGlobalConfig } from '../utils/config.js'
// Simple companion observer: picks a reaction based on the last assistant message.
// This is a lightweight placeholder that generates fun reactions without an LLM call.
const DEBUGGING_QUIPS = [
'Found it!',
'Interesting...',
'Have you tried rubber duck debugging?',
'Stack trace time!',
'I see what happened.',
]
const GENERAL_QUIPS = [
'Looking good!',
'Keep it up!',
'Nice work!',
'I believe in you!',
'You got this!',
]
const CODE_QUIPS = [
'Fancy!',
'Clean code!',
'Elegant solution!',
'Ship it!',
]
function pickQuip(messages: Message[]): string | undefined {
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant')
if (!lastAssistant) return undefined
const content = Array.isArray(lastAssistant.content)
? lastAssistant.content.map(c => (typeof c === 'string' ? c : c.type === 'text' ? c.text : '')).join('')
: typeof lastAssistant.content === 'string'
? lastAssistant.content
: ''
if (!content) return undefined
// Only react occasionally (1 in 5 turns)
if (Math.random() > 0.2) return undefined
const lower = content.toLowerCase()
if (lower.includes('error') || lower.includes('bug') || lower.includes('fix') || lower.includes('debug')) {
return DEBUGGING_QUIPS[Math.floor(Math.random() * DEBUGGING_QUIPS.length)]
}
if (lower.includes('function') || lower.includes('class') || lower.includes('const') || lower.includes('```')) {
return CODE_QUIPS[Math.floor(Math.random() * CODE_QUIPS.length)]
}
return GENERAL_QUIPS[Math.floor(Math.random() * GENERAL_QUIPS.length)]
}
export async function fireCompanionObserver(
messages: Message[],
onReaction: (reaction: string) => void,
): Promise<void> {
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return
const quip = pickQuip(messages)
if (quip) {
onReaction(quip)
}
}

View File

@ -1,4 +1,3 @@
import { feature } from 'bun:bundle'
import type { Message } from '../types/message.js'
import type { Attachment } from '../utils/attachments.js'
import { getGlobalConfig } from '../utils/config.js'
@ -15,7 +14,6 @@ When the user addresses ${name} directly (by name), its bubble will answer. Your
export function getCompanionIntroAttachment(
messages: Message[] | undefined,
): Attachment[] {
if (!feature('BUDDY')) return []
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return []

View File

@ -1,5 +1,4 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js';
import { Text } from '../ink.js';
@ -50,9 +49,6 @@ export function useBuddyNotification() {
let t1;
if ($[0] !== addNotification || $[1] !== removeNotification) {
t0 = () => {
if (!feature("BUDDY")) {
return;
}
const config = getGlobalConfig();
if (config.companion || !isBuddyTeaserWindow()) {
return;
@ -80,7 +76,6 @@ export function findBuddyTriggerPositions(text: string): Array<{
start: number;
end: number;
}> {
if (!feature('BUDDY')) return [];
const triggers: Array<{
start: number;
end: number;

View File

@ -115,11 +115,9 @@ const forkCmd = feature('FORK_SUBAGENT')
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
).default
: null
const buddy = feature('BUDDY')
? (
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default
: null
const buddy = (
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default
/* eslint-enable @typescript-eslint/no-require-imports */
import thinkback from './commands/thinkback/index.js'
import thinkbackPlay from './commands/thinkback-play/index.js'
@ -319,7 +317,7 @@ const COMMANDS = memoize((): Command[] => [
vim,
...(webCmd ? [webCmd] : []),
...(forkCmd ? [forkCmd] : []),
...(buddy ? [buddy] : []),
buddy,
...(proactive ? [proactive] : []),
...(briefCommand ? [briefCommand] : []),
...(assistantCommand ? [assistantCommand] : []),

View File

@ -0,0 +1,198 @@
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
import {
getCompanion,
roll,
companionUserId,
} from '../../buddy/companion.js'
import { renderSprite } from '../../buddy/sprites.js'
import {
RARITY_COLORS,
RARITY_STARS,
STAT_NAMES,
type StoredCompanion,
} from '../../buddy/types.js'
import { saveGlobalConfig } from '../../utils/config.js'
function CompanionCard({
onDone,
args,
setAppState,
}: {
onDone: (result?: string, options?: { display?: string }) => void
args: string
setAppState: (updater: (prev: any) => any) => void
}) {
const trimmed = args.trim().toLowerCase()
const companion = getCompanion()
// Handle keyboard input to dismiss
const handleKeyDown = (e: any) => {
if (e.key === 'q' || e.key === 'Enter') {
e.preventDefault()
onDone()
}
}
// Handle subcommands
React.useEffect(() => {
if (trimmed === 'mute') {
saveGlobalConfig(c => ({ ...c, companionMuted: true }))
onDone(`${companion?.name ?? 'Companion'} is now muted.`, {
display: 'system',
})
return
}
if (trimmed === 'unmute') {
saveGlobalConfig(c => ({ ...c, companionMuted: false }))
onDone(`${companion?.name ?? 'Companion'} says hello!`, {
display: 'system',
})
return
}
if (trimmed === 'pet') {
if (!companion) {
onDone('You need to hatch a companion first! Use /buddy hatch', {
display: 'system',
})
return
}
setAppState((prev: any) => ({ ...prev, companionPetAt: Date.now() }))
onDone(`You pet ${companion.name}! ♥`, { display: 'system' })
return
}
if (trimmed === 'hatch') {
if (companion) {
onDone(
`You already have ${companion.name}! Use /buddy info to see them.`,
{ display: 'system' },
)
return
}
// Hatch a new companion with a generated name
const { bones } = roll(companionUserId())
const adjectives = [
'Bright', 'Cozy', 'Swift', 'Calm', 'Wise', 'Bold',
'Fuzzy', 'Lucky', 'Snappy', 'Quirky',
]
const nouns = [
'Spark', 'Pixel', 'Ember', 'Glitch', 'Byte',
'Flux', 'Drift', 'Blip', 'Quip', 'Zap',
]
const adj = adjectives[Math.floor(Math.random() * adjectives.length)]!
const noun = nouns[Math.floor(Math.random() * nouns.length)]!
const name = `${adj} ${noun}`
const soul: StoredCompanion = {
name,
personality: `A ${bones.rarity} ${bones.species} who loves debugging and hanging out.`,
hatchedAt: Date.now(),
}
saveGlobalConfig(c => ({ ...c, companion: soul }))
onDone(
`✨ You hatched ${name} the ${bones.rarity} ${bones.species}! Say hello!`,
{ display: 'system' },
)
return
}
if (trimmed === 'release') {
if (!companion) {
onDone('No companion to release.', { display: 'system' })
return
}
const name = companion.name
saveGlobalConfig(c => {
const next = { ...c }
delete next.companion
return next
})
onDone(`Goodbye, ${name}! You'll be missed.`, { display: 'system' })
return
}
}, [])
// Render companion info
if (!companion) {
const { bones } = roll(companionUserId())
const preview = renderSprite(bones, 0)
const color = RARITY_COLORS[bones.rarity]
return (
<Box flexDirection="column" paddingX={1} paddingY={1} autoFocus={true} onKeyDown={handleKeyDown} tabIndex={0}>
<Text bold>You haven't hatched a companion yet!</Text>
<Text dimColor>Here's a preview of yours:</Text>
<Box flexDirection="column" marginY={1}>
{preview.map((line, i) => (
<Text key={i} color={color}>
{line}
</Text>
))}
<Text italic dimColor>
A {bones.rarity} {bones.species} {RARITY_STARS[bones.rarity]}
</Text>
</Box>
<Text>Run <Text bold>/buddy hatch</Text> to bring them to life!</Text>
<Text dimColor>Or type <Text bold>q</Text> to dismiss.</Text>
</Box>
)
}
const sprite = renderSprite(companion, 0)
const color = RARITY_COLORS[companion.rarity]
return (
<Box flexDirection="column" paddingX={1} paddingY={1} autoFocus={true} onKeyDown={handleKeyDown} tabIndex={0}>
<Box flexDirection="row" gap={2}>
<Box flexDirection="column">
{sprite.map((line, i) => (
<Text key={i} color={color}>
{line}
</Text>
))}
<Text italic bold color={color}>
{companion.name}
</Text>
</Box>
<Box flexDirection="column" justifyContent="center">
<Text>
<Text bold>Species:</Text>{' '}
<Text color={color}>{companion.species}</Text>
</Text>
<Text>
<Text bold>Rarity:</Text>{' '}
<Text color={color}>
{companion.rarity} {RARITY_STARS[companion.rarity]}
</Text>
</Text>
{companion.shiny && <Text color="warning"> Shiny!</Text>}
<Text dimColor>{'─'.repeat(20)}</Text>
<Text bold>Stats:</Text>
{STAT_NAMES.map(stat => (
<Text key={stat}>
<Text dimColor>{stat}:</Text>{' '}
<Text color={color}>{companion.stats[stat]}</Text>
</Text>
))}
</Box>
</Box>
<Text dimColor>{'─'.repeat(40)}</Text>
<Text dimColor>
/buddy pet · /buddy mute · /buddy unmute · /buddy release
</Text>
<Text dimColor>Press q or Enter to dismiss</Text>
</Box>
)
}
export const call: LocalJSXCommandCall = async (onDone, context, args = '') => {
return (
<CompanionCard
onDone={onDone}
args={args}
setAppState={context.setAppState}
/>
)
}

View File

@ -0,0 +1,11 @@
import type { Command } from '../../commands.js'
const buddyCommand = {
type: 'local-jsx',
name: 'buddy',
description: 'Meet your companion',
argumentHint: '[hatch|pet|mute|unmute|info]',
load: () => import('./buddy.js'),
} satisfies Command
export default buddyCommand

View File

@ -309,10 +309,7 @@ function PromptInput({
const {
companion: _companion,
companionMuted
} = feature('BUDDY') ? getGlobalConfig() : {
companion: undefined,
companionMuted: undefined
};
} = getGlobalConfig();
const companionFooterVisible = !!_companion && !companionMuted;
// Brief mode: BriefSpinner/BriefIdleStatus own the 2-row footprint above
// the input. Dropping marginTop here lets the spinner sit flush against
@ -1786,10 +1783,8 @@ function PromptInput({
}
switch (footerItemSelected) {
case 'companion':
if (feature('BUDDY')) {
selectFooterItem(null);
void onSubmit('/buddy');
}
selectFooterItem(null);
void onSubmit('/buddy');
break;
case 'tasks':
if (isTeammateMode) {
@ -1981,9 +1976,9 @@ function PromptInput({
});
}, [effortNotificationText, addNotification, removeNotification]);
useBuddyNotification();
const companionSpeaking = feature('BUDDY') ?
const companionSpeaking =
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s => s.companionReaction !== undefined) : false;
useAppState(s => s.companionReaction !== undefined);
const {
columns,
rows

View File

@ -274,6 +274,7 @@ const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/We
import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';
import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js';
import { fireCompanionObserver } from '../buddy/observer.js';
import { DevBar } from '../components/DevBar.js';
// Session manager removed - using AppState now
import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js';
@ -1299,12 +1300,10 @@ export function REPL({
// Dismiss the companion bubble on scroll — it's absolute-positioned
// at bottom-right and covers transcript content. Scrolling = user is
// trying to read something under it.
if (feature('BUDDY')) {
setAppState(prev => prev.companionReaction === undefined ? prev : {
setAppState(prev => prev.companionReaction === undefined ? prev : {
...prev,
companionReaction: undefined
});
}
}
}, [onRepin, onScrollAway, maybeLoadOlder, setAppState]);
// Deferred SessionStart hook messages — REPL renders immediately and
@ -2801,12 +2800,10 @@ export function REPL({
})) {
onQueryEvent(event);
}
if (feature('BUDDY')) {
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
...prev,
companionReaction: reaction
}));
}
queryCheckpoint('query_end');
// Capture ant-only API metrics before resetLoadingState clears the ref.
@ -4562,7 +4559,7 @@ export function REPL({
{feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
<MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
setCursor(null);
jumpToNew(scrollRef.current);
}} scrollable={<>
@ -4587,8 +4584,8 @@ export function REPL({
{showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
{!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
{isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
</>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
{feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
</>} bottom={<Box flexDirection={companionNarrow ? 'column' : 'row'} width="100%" alignItems={companionNarrow ? undefined : 'flex-end'}>
{companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
<Box flexDirection="column" flexGrow={1}>
{permissionStickyFooter}
{/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
@ -4992,7 +4989,7 @@ export function REPL({
}} />}
{"external" === 'ant' && <DevBar />}
</Box>
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
{!(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
</Box>} />
</MCPConnectionManager>
</KeybindingSetup>;

View File

@ -861,13 +861,9 @@ export async function getAttachments(
),
),
),
...(feature('BUDDY')
? [
maybe('companion_intro', () =>
Promise.resolve(getCompanionIntroAttachment(messages)),
),
]
: []),
maybe('companion_intro', () =>
Promise.resolve(getCompanionIntroAttachment(messages)),
),
maybe('changed_files', () => getChangedFiles(context)),
maybe('nested_memory', () => getNestedMemoryAttachments(context)),
// relevant_memories moved to async prefetch (startRelevantMemoryPrefetch)