feat: 重构供应商层次 (#286)

* refactor: 创建 @anthropic-ai/model-provider 包骨架与类型定义

- 新建 workspace 包 packages/@anthropic-ai/model-provider
- 定义 ModelProviderHooks 接口(依赖注入:分析、成本、日志等)
- 定义 ClientFactories 接口(Anthropic/OpenAI/Gemini/Grok 客户端工厂)
- 搬入核心类型:Message 体系、NonNullableUsage、EMPTY_USAGE、SystemPrompt、错误常量
- 主项目 src/types/message.ts 等改为 re-export,保持向后兼容

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 提升 OpenAI 转换器和模型映射到 model-provider 包

- 搬入 OpenAI 消息转换(convertMessages)、工具转换(convertTools)、流适配(streamAdapter)
- 搬入 OpenAI 和 Grok 模型映射(resolveOpenAIModel、resolveGrokModel)
- 主项目文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 搬入 Gemini 兼容层到 model-provider 包

- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射
- 主项目 gemini/ 目录下文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 搬入 errorUtils 并迁移消费者导入到 model-provider

- 搬入 formatAPIError、extractConnectionErrorDetails 等 errorUtils
- 迁移 10 个消费者文件直接从 @anthropic-ai/model-provider 导入
- 更新 emptyUsage、sdkUtilityTypes、systemPromptType 为 re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 添加 agent-loop 绘图

* Revert "feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)"

This reverts commit e458d6391d.

* docs: 添加简化版 agent loop

* fix: 修复 n 快捷键导致关闭的问题

* fix: 修复 node 下 ws 没打包问题

* docs: 修复链接

* test: 添加测试支持

* fix: 修复类型问题(#267) (#271)

* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题

* test: 修复类型校验 (#279)

* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题

* fix(remote-control): harden self-hosted session flows (#278)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>

* docs: update contributors

* build: 新增 vite 构建流程

* feat: 添加环境变量支持以覆盖 max_tokens 设置

* feat(langfuse): LLM generation 记录工具定义

将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 添加对 ACP 协议的支持 (#284)

* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* chore: 1.4.0

* conflict: 解决冲突

* feat: 添加测试覆盖率上报

* style: 改名加移动文件夹位置

* refactor: 移动测试用例及实现

* test: 修复测试用例完成

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cheng Zi Feng <1154238323@qq.com>
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
Co-authored-by: claude-code-best <272536312+claude-code-best@users.noreply.github.com>
This commit is contained in:
claude-code-best
2026-04-17 09:33:14 +08:00
committed by GitHub
parent c8d08d235b
commit bddd146f25
86 changed files with 1661 additions and 1766 deletions

View File

@@ -10,17 +10,10 @@ import type { AgentId } from '../../../types/ids.js'
import type { Tools } from '../../../Tool.js'
import type { Stream } from 'openai/streaming.mjs'
import type {
ChatCompletionChunk,
ChatCompletionCreateParamsStreaming,
} from 'openai/resources/chat/completions/completions.mjs'
import { getOpenAIClient } from './client.js'
import { anthropicMessagesToOpenAI } from './convertMessages.js'
import {
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
} from './convertTools.js'
import { adaptOpenAIStreamToAnthropic } from './streamAdapter.js'
import { resolveOpenAIModel } from './modelMapping.js'
import { anthropicMessagesToOpenAI, resolveOpenAIModel, adaptOpenAIStreamToAnthropic, anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '@ant/model-provider'
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
import { toolToAPISchema } from '../../../utils/api.js'
import {
@@ -30,7 +23,8 @@ import {
import { logForDebugging } from '../../../utils/debug.js'
import { addToTotalSessionCost } from '../../../cost-tracker.js'
import { calculateUSDCost } from '../../../utils/modelCost.js'
import { isEnvTruthy, isEnvDefinedFalsy } from '../../../utils/envUtils.js'
import { isOpenAIThinkingEnabled, resolveOpenAIMaxTokens, buildOpenAIRequestBody } from './requestBody.js'
export { isOpenAIThinkingEnabled, resolveOpenAIMaxTokens, buildOpenAIRequestBody }
import { getModelMaxOutputTokens } from '../../../utils/context.js'
import type { Options } from '../claude.js'
import { randomUUID } from 'crypto'
@@ -48,104 +42,6 @@ import {
TOOL_SEARCH_TOOL_NAME,
} from '@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
/**
* Detect whether DeepSeek-style thinking mode should be enabled.
*
* Enabled when:
* 1. OPENAI_ENABLE_THINKING=1 is set (explicit enable), OR
* 2. Model name contains "deepseek-reasoner" OR "DeepSeek-V3.2" (auto-detect, case-insensitive)
*
* Disabled when:
* - OPENAI_ENABLE_THINKING=0/false/no/off is explicitly set (overrides model detection)
*
* @param model - The resolved OpenAI model name
* @internal Exported for testing purposes only
*/
export function isOpenAIThinkingEnabled(model: string): boolean {
// Explicit disable takes priority (overrides model auto-detect)
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
// Explicit enable
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
// Auto-detect from model name (deepseek-reasoner and DeepSeek-V3.2 support thinking mode)
const modelLower = model.toLowerCase()
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2')
}
/**
* Resolve max output tokens for the OpenAI-compatible path.
*
* Override priority:
* 1. maxOutputTokensOverride (programmatic, from query pipeline)
* 2. OPENAI_MAX_TOKENS env var (OpenAI-specific, useful for local models
* with small context windows, e.g. RTX 3060 12GB running 65536-token models)
* 3. CLAUDE_CODE_MAX_OUTPUT_TOKENS env var (generic override)
* 4. upperLimit default (64000)
*
* @internal Exported for testing purposes only
*/
export function resolveOpenAIMaxTokens(
upperLimit: number,
maxOutputTokensOverride?: number,
): number {
return maxOutputTokensOverride
?? (process.env.OPENAI_MAX_TOKENS ? parseInt(process.env.OPENAI_MAX_TOKENS, 10) || undefined : undefined)
?? (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ? parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10) || undefined : undefined)
?? upperLimit
}
/**
* Build the request body for OpenAI chat.completions.create().
* Extracted for testability — the thinking mode params are injected here.
*
* DeepSeek thinking mode: inject thinking params via request body.
* Two formats are added simultaneously to support different deployments:
* - Official DeepSeek API: `thinking: { type: 'enabled' }`
* - Self-hosted DeepSeek-V3.2: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }`
* OpenAI SDK passes unknown keys through to the HTTP body.
* Each endpoint will use the format it recognizes and ignore the others.
* @internal Exported for testing purposes only
*/
export function buildOpenAIRequestBody(params: {
model: string
messages: any[]
tools: any[]
toolChoice: any
enableThinking: boolean
maxTokens: number
temperatureOverride?: number
}): ChatCompletionCreateParamsStreaming & {
thinking?: { type: string }
enable_thinking?: boolean
chat_template_kwargs?: { thinking: boolean }
} {
const { model, messages, tools, toolChoice, enableThinking, maxTokens, temperatureOverride } = params
return {
model,
messages,
max_tokens: maxTokens,
...(tools.length > 0 && {
tools,
...(toolChoice && { tool_choice: toolChoice }),
}),
stream: true,
stream_options: { include_usage: true },
// DeepSeek thinking mode: enable chain-of-thought output.
// When active, temperature/top_p/presence_penalty/frequency_penalty are ignored by DeepSeek.
...(enableThinking && {
// Official DeepSeek API format
thinking: { type: 'enabled' },
// Self-hosted DeepSeek-V3.2 format
enable_thinking: true,
chat_template_kwargs: { thinking: true },
}),
// Only send temperature when thinking mode is off (DeepSeek ignores it anyway,
// but other providers may respect it)
...(!enableThinking && temperatureOverride !== undefined && {
temperature: temperatureOverride,
}),
}
}
/**
* Assemble the final AssistantMessage (and optional max_tokens error) from
* accumulated stream state. Extracted to avoid duplication between the