Merge branch 'main' into main

This commit is contained in:
Onuzulike Anthony Ifechukwu 2026-06-02 17:09:17 +01:00 committed by GitHub
commit 079986d09b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 456 additions and 15 deletions

View File

@ -10,12 +10,11 @@
> Which Claude do you like? The open source one is the best.
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 完整复原的工程化项目。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 并在此基础上扩展了更多好玩的特性。
> 我们将会在五一期间进行整个代码仓库的 lint 规范化, 这个期间提交的 PR 可能会有非常多的冲突, 所以大的功能请尽量在这之前提交哈
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/uApuzJWGKX)
[Peri Code](https://github.com/KonghaYao/peri)Claude Code 兼容的 Rust Agent多年大模型经验匠心制作国内大模型DeepSeek/GLM精调CPU/内存极致优化,在开发版/树莓派上也能跑 CC 一样的体验。
[文档在这里](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组,群主在线答疑](https://discord.gg/uApuzJWGKX)
| 特性 | 说明 | 文档 |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
@ -158,9 +157,13 @@ bun run build
- **Ollama 深度集成**: 可直接在 CLI 中查看已安装模型,或输入模型名(如 `llama3.1`一键拉取Pull。支持模型列表交互切换和硬件状态自动检测。
- 自动检测本地运行状态和默认端口。
#### /login 字段说明 (云端模式)
> 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
| 📌 字段 | 📝 说明 | 💡 示例 |
| ------------ | ------------- | ---------------------------- |
| Base URL | API 服务地址 | `https://api.example.com/v1` |
| API Key | 认证密钥 | `sk-xxx` |
| Haiku Model | 快速模型 ID | `claude-haiku-4-5-20251001` |
| Sonnet Model | 均衡模型 ID | `claude-sonnet-4-6` |
| Opus Model | 高性能模型 ID | `claude-opus-4-6` |
### 🩺 系统诊断 /doctor

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -25,6 +25,7 @@
import { getOauthConfig } from '../constants/oauth.js'
import { isEnvTruthy } from './envUtils.js'
import { isEssentialTrafficOnly } from './privacyLevel.js'
let fired = false
@ -32,6 +33,10 @@ export function preconnectAnthropicApi(): void {
if (fired) return
fired = true
// Also skip when non-essential traffic is disabled via
// CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC / DISABLE_TELEMETRY / proxy env.
if (isEssentialTrafficOnly()) return
// Skip if using a cloud provider — different endpoint + auth
if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||

View File

@ -120,6 +120,19 @@ export function getBestModel(): ModelName {
return getDefaultOpusModel()
}
/**
* Resolve the provider's primary model from its env var (e.g. OPENAI_MODEL).
* Returns undefined for providers that don't have a primary-model env var
* (Bedrock, Vertex, Foundry, firstParty).
*/
function getProviderPrimaryModel(): ModelName | undefined {
const provider = getAPIProvider()
if (provider === 'openai') return process.env.OPENAI_MODEL
if (provider === 'gemini') return process.env.GEMINI_MODEL
if (provider === 'grok') return process.env.GROK_MODEL
return undefined
}
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
export function getDefaultOpusModel(): ModelName {
const provider = getAPIProvider()
@ -138,10 +151,12 @@ export function getDefaultOpusModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
// 3P providers (Bedrock, Vertex, Foundry) all publish Opus 4.7 in sync
// with firstParty as of 2026-04-17 (AWS Bedrock, Google Vertex AI, and
// Microsoft Foundry announcements and model catalogs all confirm). The
// branch is kept as a structural hook in case a future launch lags on 3P.
// 3P providers: if user set a primary model (e.g. OPENAI_MODEL=glm-5.1),
// fall back to it instead of a hardcoded Anthropic model. This prevents
// sideQuery / background tasks from sending requests to Anthropic's API
// when the user configured a third-party provider.
const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
if (provider !== 'firstParty') {
return getModelStrings().opus47
}
@ -166,7 +181,11 @@ export function getDefaultSonnetModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
// 3P providers: fall back to user's primary model instead of a hardcoded
// Anthropic model name. Prevents background API calls from being routed to
// Anthropic when the user configured a third-party endpoint.
const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
if (provider !== 'firstParty') {
return getModelStrings().sonnet45
}
@ -191,6 +210,10 @@ export function getDefaultHaikuModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
// 3P providers: fall back to user's primary model instead of a hardcoded
// Anthropic model name.
const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex)
return getModelStrings().haiku45

View File

@ -33,6 +33,19 @@ import { errorMessage } from './errors.js'
import { computeFingerprint } from './fingerprint.js'
import { getAPIProvider } from './model/providers.js'
import { normalizeModelStringForAPI } from './model/model.js'
import { getOpenAIClient } from '../services/api/openai/client.js'
import { getGrokClient } from '../services/api/grok/client.js'
import {
anthropicMessagesToOpenAI,
resolveOpenAIModel,
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
resolveGrokModel,
resolveGeminiModel,
anthropicToolsToGemini,
anthropicToolChoiceToGemini,
} from '@ant/model-provider'
import type { SystemPrompt } from './systemPromptType.js'
type MessageParam = Anthropic.MessageParam
type TextBlockParam = Anthropic.TextBlockParam
@ -99,6 +112,46 @@ function extractFirstUserMessageText(messages: MessageParam[]): string {
return textBlock?.type === 'text' ? textBlock.text : ''
}
/**
* Extract system prompt text from the `system` option.
*/
function extractSystemText(system?: string | TextBlockParam[]): string {
if (!system) return ''
if (typeof system === 'string') return system
return system
.filter((b): b is { type: 'text'; text: string } => 'text' in b && !!b.text)
.map(b => b.text)
.join('\n\n')
}
/**
* Convert Anthropic MessageParam[] to a list of {role, content} objects
* suitable for OpenAI-compatible chat.completions APIs.
*/
function messageParamsToOpenAIRoleContent(
messages: MessageParam[],
): Array<{ role: 'user' | 'assistant'; content: string }> {
const result: Array<{ role: 'user' | 'assistant'; content: string }> = []
for (const m of messages) {
if (m.role !== 'user' && m.role !== 'assistant') continue
const text =
typeof m.content === 'string'
? m.content
: Array.isArray(m.content)
? m.content
.filter(
(b): b is { type: 'text'; text: string } => b.type === 'text',
)
.map(b => b.text)
.join('\n')
: ''
if (text) {
result.push({ role: m.role as 'user' | 'assistant', content: text })
}
}
return result
}
/**
* Lightweight API wrapper for "side queries" outside the main conversation loop.
*
@ -112,6 +165,7 @@ function extractFirstUserMessageText(messages: MessageParam[]): string {
* - Proper betas for the model
* - API metadata
* - Model string normalization (strips [1m] suffix for API)
* - Third-party provider routing (OpenAI, Grok, Gemini)
*
* @example
* // Permission explainer
@ -142,6 +196,14 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
stop_sequences,
} = opts
const provider = getAPIProvider()
if (provider === 'openai' || provider === 'grok') {
return sideQueryViaOpenAICompatible(opts)
}
if (provider === 'gemini') {
return sideQueryViaGemini(opts)
}
const client = await getAnthropicClient({
maxRetries,
model,
@ -198,7 +260,6 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
}
const normalizedModel = normalizeModelStringForAPI(model)
const provider = getAPIProvider()
const start = Date.now()
const traceName = `side-query:${opts.querySource}`
@ -328,3 +389,352 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
return response
}
/**
* OpenAI-compatible side query for OpenAI and Grok providers.
* Both use the OpenAI SDK with different base URLs.
*
* Converts Anthropic-format params to OpenAI Chat Completions, sends a
* non-streaming request, and wraps the response back into a BetaMessage
* shape so callers remain provider-agnostic.
*
* Supports tools and tool_choice for structured output (e.g. yoloClassifier,
* permissionExplainer).
*/
async function sideQueryViaOpenAICompatible(
opts: SideQueryOptions,
): Promise<BetaMessage> {
const {
model,
system,
messages,
tools,
tool_choice,
max_tokens = 1024,
temperature,
signal,
} = opts
const provider = getAPIProvider()
const normalizedModel = normalizeModelStringForAPI(model)
// Resolve model name and client per provider
let openaiModel: string
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
let client: import('openai').default
if (provider === 'grok') {
openaiModel = resolveGrokModel(normalizedModel)
client = getGrokClient({ maxRetries: opts.maxRetries ?? 2 })
} else {
openaiModel = resolveOpenAIModel(normalizedModel)
client = getOpenAIClient({ maxRetries: opts.maxRetries ?? 2 })
}
// Build system prompt text
const systemText = extractSystemText(system)
// Build OpenAI messages: system first, then user/assistant
const openaiMessages: Array<{
role: 'system' | 'user' | 'assistant'
content: string
}> = []
if (systemText) {
openaiMessages.push({ role: 'system', content: systemText })
}
openaiMessages.push(...messageParamsToOpenAIRoleContent(messages))
// Convert tools and tool_choice if provided
const openaiTools =
tools && tools.length > 0
? anthropicToolsToOpenAI(tools as BetaToolUnion[])
: undefined
const openaiToolChoice = tool_choice
? anthropicToolChoiceToOpenAI(tool_choice)
: undefined
const start = Date.now()
const requestParams: Record<string, unknown> = {
model: openaiModel,
messages: openaiMessages,
max_tokens,
}
if (temperature !== undefined) requestParams.temperature = temperature
if (openaiTools && openaiTools.length > 0) {
requestParams.tools = openaiTools
if (openaiToolChoice) requestParams.tool_choice = openaiToolChoice
}
const response = await client.chat.completions.create(
requestParams as unknown as import('openai/resources/chat/completions/completions.mjs').ChatCompletionCreateParamsNonStreaming,
{ signal },
)
const choice = response.choices[0]
const message = choice?.message
// Build content blocks for BetaMessage
const contentBlocks: Array<
| { type: 'text'; text: string }
| { type: 'tool_use'; id: string; name: string; input: unknown }
> = []
if (message?.content) {
contentBlocks.push({ type: 'text', text: message.content })
}
if (message?.tool_calls) {
for (const tc of message.tool_calls) {
// ChatCompletionMessageToolCall is a union — only function-type has .function
if (tc.type === 'function' && 'function' in tc) {
const fn = (tc as { function: { name: string; arguments: string } })
.function
contentBlocks.push({
type: 'tool_use',
id: tc.id ?? `toolu_${Date.now()}`,
name: fn.name,
input: JSON.parse(fn.arguments || '{}'),
})
}
}
}
const now = Date.now()
const requestId = response.id
const lastCompletion = getLastApiCompletionTimestamp()
logEvent('tengu_api_success', {
requestId:
requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
querySource:
opts.querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
model:
openaiModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
inputTokens: response.usage?.prompt_tokens ?? 0,
outputTokens: response.usage?.completion_tokens ?? 0,
cachedInputTokens: 0,
uncachedInputTokens: response.usage?.prompt_tokens ?? 0,
durationMsIncludingRetries: now - start,
timeSinceLastApiCallMs:
lastCompletion !== null ? now - lastCompletion : undefined,
})
setLastApiCompletionTimestamp(now)
const stopReason =
choice?.finish_reason === 'tool_calls'
? 'tool_use'
: choice?.finish_reason === 'length'
? 'max_tokens'
: 'end_turn'
return {
id: response.id,
type: 'message',
role: 'assistant',
content: contentBlocks as BetaMessage['content'],
model: openaiModel,
stop_reason: stopReason as BetaMessage['stop_reason'],
stop_sequence: null,
usage: {
input_tokens: response.usage?.prompt_tokens ?? 0,
output_tokens: response.usage?.completion_tokens ?? 0,
},
} as BetaMessage
}
/**
* Gemini side query. Converts Anthropic-format params to Gemini
* generateContent format, sends a non-streaming request via fetch,
* and wraps the response back into a BetaMessage shape.
*/
async function sideQueryViaGemini(
opts: SideQueryOptions,
): Promise<BetaMessage> {
const {
model,
system,
messages,
tools,
tool_choice,
max_tokens = 1024,
temperature,
signal,
} = opts
const normalizedModel = normalizeModelStringForAPI(model)
const geminiModel = resolveGeminiModel(normalizedModel)
// Build Gemini contents from Anthropic MessageParam[]
const contents: Array<{
role: 'user' | 'model'
parts: Array<{ text: string }>
}> = []
for (const m of messages) {
if (m.role !== 'user' && m.role !== 'assistant') continue
const text =
typeof m.content === 'string'
? m.content
: Array.isArray(m.content)
? m.content
.filter(
(b): b is { type: 'text'; text: string } => b.type === 'text',
)
.map(b => b.text)
.join('\n')
: ''
if (text) {
contents.push({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text }],
})
}
}
// Build system instruction
const systemText = extractSystemText(system)
const systemInstruction = systemText
? { parts: [{ text: systemText }] }
: undefined
// Convert tools and tool_choice
const geminiTools =
tools && tools.length > 0
? anthropicToolsToGemini(tools as BetaToolUnion[])
: undefined
const geminiToolConfig = tool_choice
? anthropicToolChoiceToGemini(tool_choice)
: undefined
const baseUrl = (
process.env.GEMINI_BASE_URL ||
'https://generativelanguage.googleapis.com/v1beta'
).replace(/\/+$/, '')
const modelPath = geminiModel.startsWith('models/')
? geminiModel
: `models/${geminiModel}`
const url = `${baseUrl}/${modelPath}:generateContent`
const body: Record<string, unknown> = {
contents,
...(systemInstruction && { systemInstruction }),
...(geminiTools && geminiTools.length > 0 && { tools: geminiTools }),
...(geminiToolConfig && {
toolConfig: { functionCallingConfig: geminiToolConfig },
}),
...(temperature !== undefined && {
generationConfig: { temperature },
}),
...(max_tokens !== undefined && {
generationConfig: {
...(temperature !== undefined && { temperature }),
maxOutputTokens: max_tokens,
},
}),
}
// Merge generationConfig if both temperature and max_tokens are set
if (temperature !== undefined && max_tokens !== undefined) {
body.generationConfig = { temperature, maxOutputTokens: max_tokens }
}
const start = Date.now()
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': process.env.GEMINI_API_KEY || '',
},
body: JSON.stringify(body),
signal,
})
if (!res.ok) {
const errorBody = await res.text()
throw new Error(
`Gemini API request failed (${res.status} ${res.statusText}): ${errorBody || 'empty response body'}`,
)
}
const geminiResponse = (await res.json()) as {
candidates?: Array<{
content?: {
role?: string
parts?: Array<{
text?: string
functionCall?: { name?: string; args?: Record<string, unknown> }
}>
}
finishReason?: string
}>
usageMetadata?: {
promptTokenCount?: number
candidatesTokenCount?: number
totalTokenCount?: number
}
id?: string
}
// Build content blocks from Gemini response
const contentBlocks: Array<
| { type: 'text'; text: string }
| { type: 'tool_use'; id: string; name: string; input: unknown }
> = []
const candidate = geminiResponse.candidates?.[0]
const parts = candidate?.content?.parts
if (parts) {
for (const part of parts) {
if (part.text) {
contentBlocks.push({ type: 'text', text: part.text })
}
if (part.functionCall) {
contentBlocks.push({
type: 'tool_use',
id: `toolu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: part.functionCall.name ?? '',
input: part.functionCall.args ?? {},
})
}
}
}
const now = Date.now()
const lastCompletion = getLastApiCompletionTimestamp()
logEvent('tengu_api_success', {
requestId: (geminiResponse.id ??
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
querySource:
opts.querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
model:
geminiModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
inputTokens: geminiResponse.usageMetadata?.promptTokenCount ?? 0,
outputTokens: geminiResponse.usageMetadata?.candidatesTokenCount ?? 0,
cachedInputTokens: 0,
uncachedInputTokens: geminiResponse.usageMetadata?.promptTokenCount ?? 0,
durationMsIncludingRetries: now - start,
timeSinceLastApiCallMs:
lastCompletion !== null ? now - lastCompletion : undefined,
})
setLastApiCompletionTimestamp(now)
const stopReason =
candidate?.finishReason === 'STOP'
? 'end_turn'
: candidate?.finishReason === 'MAX_TOKENS'
? 'max_tokens'
: 'end_turn'
return {
id: geminiResponse.id ?? `gemini_${Date.now()}`,
type: 'message',
role: 'assistant',
content: contentBlocks as BetaMessage['content'],
model: geminiModel,
stop_reason: stopReason as BetaMessage['stop_reason'],
stop_sequence: null,
usage: {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount ?? 0,
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount ?? 0,
},
} as BetaMessage
}