mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
* 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>
308 lines
7.6 KiB
TypeScript
308 lines
7.6 KiB
TypeScript
import type {
|
|
BetaToolResultBlockParam,
|
|
BetaToolUseBlock,
|
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
|
import type { AssistantMessage, UserMessage } from '../../types/message.js'
|
|
import type { SystemPrompt } from '../../types/systemPrompt.js'
|
|
import {
|
|
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
|
type GeminiContent,
|
|
type GeminiGenerateContentRequest,
|
|
type GeminiPart,
|
|
} from './types.js'
|
|
|
|
// Simple JSON parse utility (replaces safeParseJSON from main project)
|
|
function safeParseJSON(json: string | null | undefined): unknown {
|
|
if (!json) return null
|
|
try {
|
|
return JSON.parse(json)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function anthropicMessagesToGemini(
|
|
messages: (UserMessage | AssistantMessage)[],
|
|
systemPrompt: SystemPrompt,
|
|
): Pick<GeminiGenerateContentRequest, 'contents' | 'systemInstruction'> {
|
|
const contents: GeminiContent[] = []
|
|
const toolNamesById = new Map<string, string>()
|
|
|
|
for (const msg of messages) {
|
|
if (msg.type === 'assistant') {
|
|
const content = convertInternalAssistantMessage(msg)
|
|
if (content.parts.length > 0) {
|
|
contents.push(content)
|
|
}
|
|
|
|
const assistantContent = msg.message.content
|
|
if (Array.isArray(assistantContent)) {
|
|
for (const block of assistantContent) {
|
|
if (typeof block !== 'string' && block.type === 'tool_use') {
|
|
toolNamesById.set(block.id, block.name)
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (msg.type === 'user') {
|
|
const content = convertInternalUserMessage(msg, toolNamesById)
|
|
if (content.parts.length > 0) {
|
|
contents.push(content)
|
|
}
|
|
}
|
|
}
|
|
|
|
const systemText = systemPromptToText(systemPrompt)
|
|
|
|
return {
|
|
contents,
|
|
...(systemText
|
|
? {
|
|
systemInstruction: {
|
|
parts: [{ text: systemText }],
|
|
},
|
|
}
|
|
: {}),
|
|
}
|
|
}
|
|
|
|
function systemPromptToText(systemPrompt: SystemPrompt): string {
|
|
if (!systemPrompt || systemPrompt.length === 0) return ''
|
|
return systemPrompt.filter(Boolean).join('\n\n')
|
|
}
|
|
|
|
function convertInternalUserMessage(
|
|
msg: UserMessage,
|
|
toolNamesById: ReadonlyMap<string, string>,
|
|
): GeminiContent {
|
|
const content = msg.message.content
|
|
|
|
if (typeof content === 'string') {
|
|
return {
|
|
role: 'user',
|
|
parts: createTextGeminiParts(content),
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(content)) {
|
|
return { role: 'user', parts: [] }
|
|
}
|
|
|
|
return {
|
|
role: 'user',
|
|
parts: content.flatMap(block =>
|
|
convertUserContentBlockToGeminiParts(block as unknown as string | Record<string, unknown>, toolNamesById),
|
|
),
|
|
}
|
|
}
|
|
|
|
function convertUserContentBlockToGeminiParts(
|
|
block: string | Record<string, unknown>,
|
|
toolNamesById: ReadonlyMap<string, string>,
|
|
): GeminiPart[] {
|
|
if (typeof block === 'string') {
|
|
return createTextGeminiParts(block)
|
|
}
|
|
|
|
if (block.type === 'text') {
|
|
return createTextGeminiParts(block.text)
|
|
}
|
|
|
|
if (block.type === 'tool_result') {
|
|
const toolResult = block as unknown as BetaToolResultBlockParam
|
|
return [
|
|
{
|
|
functionResponse: {
|
|
name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
|
|
response: toolResultToResponseObject(toolResult),
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
// Convert Anthropic image blocks to Gemini inlineData
|
|
if (block.type === 'image') {
|
|
const source = block.source as Record<string, unknown> | undefined
|
|
if (source?.type === 'base64' && typeof source.data === 'string') {
|
|
const mediaType = (source.media_type as string) || 'image/png'
|
|
return [
|
|
{
|
|
inlineData: {
|
|
mimeType: mediaType,
|
|
data: source.data,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
// URL images not directly supported by Gemini, convert to text description
|
|
if (source?.type === 'url' && typeof source.url === 'string') {
|
|
return createTextGeminiParts(`[image: ${source.url}]`)
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
|
|
const content = msg.message.content
|
|
|
|
if (typeof content === 'string') {
|
|
return {
|
|
role: 'model',
|
|
parts: createTextGeminiParts(content),
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(content)) {
|
|
return { role: 'model', parts: [] }
|
|
}
|
|
|
|
const parts: GeminiPart[] = []
|
|
for (const block of content) {
|
|
if (typeof block === 'string') {
|
|
parts.push(...createTextGeminiParts(block))
|
|
continue
|
|
}
|
|
|
|
if (block.type === 'text') {
|
|
parts.push(
|
|
...createTextGeminiParts(
|
|
block.text,
|
|
getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
|
),
|
|
)
|
|
continue
|
|
}
|
|
|
|
if (block.type === 'thinking') {
|
|
const thinkingPart = createThinkingGeminiPart(
|
|
block.thinking,
|
|
block.signature,
|
|
)
|
|
if (thinkingPart) {
|
|
parts.push(thinkingPart)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (block.type === 'tool_use') {
|
|
const toolUse = block as unknown as BetaToolUseBlock
|
|
parts.push({
|
|
functionCall: {
|
|
name: toolUse.name,
|
|
args: normalizeToolUseInput(toolUse.input),
|
|
},
|
|
...(getGeminiThoughtSignature(block as unknown as Record<string, unknown>) && {
|
|
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
|
}),
|
|
})
|
|
}
|
|
}
|
|
|
|
return { role: 'model', parts }
|
|
}
|
|
|
|
function createTextGeminiParts(
|
|
value: unknown,
|
|
thoughtSignature?: string,
|
|
): GeminiPart[] {
|
|
if (typeof value !== 'string' || value.length === 0) {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
{
|
|
text: value,
|
|
...(thoughtSignature && { thoughtSignature }),
|
|
},
|
|
]
|
|
}
|
|
|
|
function createThinkingGeminiPart(
|
|
value: unknown,
|
|
thoughtSignature?: string,
|
|
): GeminiPart | undefined {
|
|
if (typeof value !== 'string' || value.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
return {
|
|
text: value,
|
|
thought: true,
|
|
...(thoughtSignature && { thoughtSignature }),
|
|
}
|
|
}
|
|
|
|
function normalizeToolUseInput(input: unknown): Record<string, unknown> {
|
|
if (typeof input === 'string') {
|
|
const parsed = safeParseJSON(input)
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
return parsed as Record<string, unknown>
|
|
}
|
|
return parsed === null ? {} : { value: parsed }
|
|
}
|
|
|
|
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
return input as Record<string, unknown>
|
|
}
|
|
|
|
return input === undefined ? {} : { value: input }
|
|
}
|
|
|
|
function toolResultToResponseObject(
|
|
block: BetaToolResultBlockParam,
|
|
): Record<string, unknown> {
|
|
const result = normalizeToolResultContent(block.content)
|
|
if (
|
|
result &&
|
|
typeof result === 'object' &&
|
|
!Array.isArray(result)
|
|
) {
|
|
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
|
|
}
|
|
|
|
return {
|
|
result,
|
|
...(block.is_error ? { is_error: true } : {}),
|
|
}
|
|
}
|
|
|
|
function normalizeToolResultContent(content: unknown): unknown {
|
|
if (typeof content === 'string') {
|
|
const parsed = safeParseJSON(content)
|
|
return parsed ?? content
|
|
}
|
|
|
|
if (Array.isArray(content)) {
|
|
const text = content
|
|
.map(part => {
|
|
if (typeof part === 'string') return part
|
|
if (
|
|
part &&
|
|
typeof part === 'object' &&
|
|
'text' in part &&
|
|
typeof part.text === 'string'
|
|
) {
|
|
return part.text
|
|
}
|
|
return ''
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
|
|
const parsed = safeParseJSON(text)
|
|
return parsed ?? text
|
|
}
|
|
|
|
return content ?? ''
|
|
}
|
|
|
|
function getGeminiThoughtSignature(block: Record<string, unknown>): string | undefined {
|
|
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
|
|
return typeof signature === 'string' && signature.length > 0
|
|
? signature
|
|
: undefined
|
|
}
|