From 9233518b11a8ad9bd2d5abf7a810f3a76a5ed0f1 Mon Sep 17 00:00:00 2001 From: DevinZeng Date: Fri, 3 Apr 2026 00:30:56 +0800 Subject: [PATCH 1/4] Enable and fix /buddy command The /buddy command was completely disabled by bun:bundle feature('BUDDY') flag which evaluates to false at runtime. Removed all feature('BUDDY') checks across the codebase to register the command, and added keyboard event handling (q/Enter to dismiss) which was missing from the UI. --- src/buddy/CompanionSprite.tsx | 5 +- src/buddy/observer.ts | 67 +++++++ src/buddy/prompt.ts | 2 - src/buddy/useBuddyNotification.tsx | 5 - src/commands.ts | 10 +- src/commands/buddy/buddy.tsx | 198 +++++++++++++++++++++ src/commands/buddy/index.ts | 11 ++ src/components/PromptInput/PromptInput.tsx | 15 +- src/screens/REPL.tsx | 17 +- src/utils/attachments.ts | 10 +- 10 files changed, 296 insertions(+), 44 deletions(-) create mode 100644 src/buddy/observer.ts create mode 100644 src/commands/buddy/buddy.tsx create mode 100644 src/commands/buddy/index.ts diff --git a/src/buddy/CompanionSprite.tsx b/src/buddy/CompanionSprite.tsx index f7f1f720..e60afa68 100644 --- a/src/buddy/CompanionSprite.tsx +++ b/src/buddy/CompanionSprite.tsx @@ -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(); diff --git a/src/buddy/observer.ts b/src/buddy/observer.ts new file mode 100644 index 00000000..ede25145 --- /dev/null +++ b/src/buddy/observer.ts @@ -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 { + const companion = getCompanion() + if (!companion || getGlobalConfig().companionMuted) return + + const quip = pickQuip(messages) + if (quip) { + onReaction(quip) + } +} diff --git a/src/buddy/prompt.ts b/src/buddy/prompt.ts index c5782c0c..302b8b9f 100644 --- a/src/buddy/prompt.ts +++ b/src/buddy/prompt.ts @@ -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 [] diff --git a/src/buddy/useBuddyNotification.tsx b/src/buddy/useBuddyNotification.tsx index d6eed22b..fddcb704 100644 --- a/src/buddy/useBuddyNotification.tsx +++ b/src/buddy/useBuddyNotification.tsx @@ -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; diff --git a/src/commands.ts b/src/commands.ts index 10f03b22..0d8ae711 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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] : []), diff --git a/src/commands/buddy/buddy.tsx b/src/commands/buddy/buddy.tsx new file mode 100644 index 00000000..56d7e9c9 --- /dev/null +++ b/src/commands/buddy/buddy.tsx @@ -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 ( + + You haven't hatched a companion yet! + Here's a preview of yours: + + {preview.map((line, i) => ( + + {line} + + ))} + + A {bones.rarity} {bones.species} {RARITY_STARS[bones.rarity]} + + + Run /buddy hatch to bring them to life! + Or type q to dismiss. + + ) + } + + const sprite = renderSprite(companion, 0) + const color = RARITY_COLORS[companion.rarity] + + return ( + + + + {sprite.map((line, i) => ( + + {line} + + ))} + + {companion.name} + + + + + Species:{' '} + {companion.species} + + + Rarity:{' '} + + {companion.rarity} {RARITY_STARS[companion.rarity]} + + + {companion.shiny && ✦ Shiny!} + {'─'.repeat(20)} + Stats: + {STAT_NAMES.map(stat => ( + + {stat}:{' '} + {companion.stats[stat]} + + ))} + + + {'─'.repeat(40)} + + /buddy pet · /buddy mute · /buddy unmute · /buddy release + + Press q or Enter to dismiss + + ) +} + +export const call: LocalJSXCommandCall = async (onDone, context, args = '') => { + return ( + + ) +} diff --git a/src/commands/buddy/index.ts b/src/commands/buddy/index.ts new file mode 100644 index 00000000..0975fef6 --- /dev/null +++ b/src/commands/buddy/index.ts @@ -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 diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 128e73cb..0f7263db 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -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 diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 11cc4d84..56b2665a 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -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 ? : null} - : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => { + : 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 && 0} leaderIsIdle={!isLoading} />} {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && } {isFullscreenEnvEnabled() && } - } bottom={ - {feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? : null} + } bottom={ + {companionNarrow && isFullscreenEnvEnabled() && companionVisible ? : null} {permissionStickyFooter} {/* Immediate local-jsx commands (/btw, /sandbox, /assistant, @@ -4992,7 +4989,7 @@ export function REPL({ }} />} {"external" === 'ant' && } - {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? : null} + {!(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? : null} } /> ; diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index 8a1612a4..556defeb 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -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) From 3c705f491b62419731151cc05cb2e1dfb67a7892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Apr 2026 00:45:06 +0800 Subject: [PATCH 2/4] docs: add third-party model integration guide (OpenAI/DeepSeek/Ollama via LiteLLM) Add comprehensive documentation for using non-Anthropic models through protocol translation proxies like LiteLLM. Also document ~/.claude/settings.json env configuration as an alternative to .env files. Co-Authored-By: Claude Opus 4.6 --- .env.example | 57 ++++++-- README.en.md | 18 ++- README.md | 18 ++- docs/third-party-models.en.md | 254 ++++++++++++++++++++++++++++++++++ docs/third-party-models.md | 254 ++++++++++++++++++++++++++++++++++ 5 files changed, 589 insertions(+), 12 deletions(-) create mode 100644 docs/third-party-models.en.md create mode 100644 docs/third-party-models.md diff --git a/.env.example b/.env.example index dd9e7136..99f367cc 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,50 @@ -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 +CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 diff --git a/README.en.md b/README.en.md index f34407b2..dea188e2 100644 --- a/README.en.md +++ b/README.en.md @@ -15,7 +15,7 @@ A **locally runnable version** repaired from the leaked Claude Code source, with - 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 +87,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 +111,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 diff --git a/README.md b/README.md index 4deb6eba..9fd492f7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - 完整的 Ink TUI 交互界面(与官方 Claude Code 一致) - `--print` 无头模式(脚本/CI 场景) - 支持 MCP 服务器、插件、Skills -- 支持自定义 API 端点和模型 +- 支持自定义 API 端点和模型([第三方模型使用指南](docs/third-party-models.md)) - 降级 Recovery CLI 模式 --- @@ -87,7 +87,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 +111,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 diff --git a/docs/third-party-models.en.md b/docs/third-party-models.en.md new file mode 100644 index 00000000..66416c50 --- /dev/null +++ b/docs/third-party-models.en.md @@ -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. diff --git a/docs/third-party-models.md b/docs/third-party-models.md new file mode 100644 index 00000000..1bc746a6 --- /dev/null +++ b/docs/third-party-models.md @@ -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_use),LiteLLM 会自动转换 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+),小模型可能无法正确处理工具调用。 From 62bb0169bb7a14c2fae791bab362a8fbffc36e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Apr 2026 01:27:27 +0800 Subject: [PATCH 3/4] docs: add FAQ section for common issues Cover three frequently reported problems: - usage.input_tokens error from misconfigured ANTHROPIC_BASE_URL - Cannot find package 'bundle' from outdated Bun version - How to use non-Anthropic models (OpenAI/DeepSeek/Ollama) Co-Authored-By: Claude Opus 4.6 --- README.en.md | 36 ++++++++++++++++++++++++++++++++++++ README.md | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/README.en.md b/README.en.md index dea188e2..07ecd161 100644 --- a/README.en.md +++ b/README.en.md @@ -252,6 +252,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. diff --git a/README.md b/README.md index 9fd492f7..319faa73 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,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) 所有。仅供学习和研究用途。 From 7d40dbf48ca61dee77e0d1eeb78fc6fdbbba0656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Apr 2026 01:30:02 +0800 Subject: [PATCH 4/4] docs: add table of contents with anchor links to READMEs Co-Authored-By: Claude Opus 4.6 --- README.en.md | 14 ++++++++++++++ README.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.en.md b/README.en.md index 07ecd161..afb19c87 100644 --- a/README.en.md +++ b/README.en.md @@ -10,6 +10,20 @@ A **locally runnable version** repaired from the leaked Claude Code source, with Runtime screenshot

+## 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) diff --git a/README.md b/README.md index 319faa73..c715228d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,20 @@ 运行截图

+## 目录 + +- [功能](#功能) +- [架构概览](#架构概览) +- [快速开始](#快速开始) +- [环境变量说明](#环境变量说明) +- [降级模式](#降级模式) +- [常见问题](#常见问题) +- [相对于原始泄露源码的修复](#相对于原始泄露源码的修复) +- [项目结构](#项目结构) +- [技术栈](#技术栈) + +--- + ## 功能 - 完整的 Ink TUI 交互界面(与官方 Claude Code 一致)