mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +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>
244 lines
6.4 KiB
TypeScript
244 lines
6.4 KiB
TypeScript
import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
|
import { randomUUID } from 'crypto'
|
|
import type { GeminiPart, GeminiStreamChunk } from './types.js'
|
|
|
|
export async function* adaptGeminiStreamToAnthropic(
|
|
stream: AsyncIterable<GeminiStreamChunk>,
|
|
model: string,
|
|
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
|
|
const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
|
let started = false
|
|
let stopped = false
|
|
let nextContentIndex = 0
|
|
let openTextLikeBlock:
|
|
| { index: number; type: 'text' | 'thinking' }
|
|
| null = null
|
|
let sawToolUse = false
|
|
let finishReason: string | undefined
|
|
let inputTokens = 0
|
|
let outputTokens = 0
|
|
|
|
for await (const chunk of stream) {
|
|
const usage = chunk.usageMetadata
|
|
if (usage) {
|
|
inputTokens = usage.promptTokenCount ?? inputTokens
|
|
outputTokens =
|
|
(usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0)
|
|
}
|
|
|
|
if (!started) {
|
|
started = true
|
|
yield {
|
|
type: 'message_start',
|
|
message: {
|
|
id: messageId,
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [],
|
|
model,
|
|
stop_reason: null,
|
|
stop_sequence: null,
|
|
usage: {
|
|
input_tokens: inputTokens,
|
|
output_tokens: 0,
|
|
cache_creation_input_tokens: 0,
|
|
cache_read_input_tokens: 0,
|
|
},
|
|
},
|
|
} as unknown as BetaRawMessageStreamEvent
|
|
}
|
|
const candidate = chunk.candidates?.[0]
|
|
const parts = candidate?.content?.parts ?? []
|
|
|
|
for (const part of parts) {
|
|
if (part.functionCall) {
|
|
if (openTextLikeBlock) {
|
|
yield {
|
|
type: 'content_block_stop',
|
|
index: openTextLikeBlock.index,
|
|
} as BetaRawMessageStreamEvent
|
|
openTextLikeBlock = null
|
|
}
|
|
|
|
sawToolUse = true
|
|
const toolIndex = nextContentIndex++
|
|
const toolId = `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
|
yield {
|
|
type: 'content_block_start',
|
|
index: toolIndex,
|
|
content_block: {
|
|
type: 'tool_use',
|
|
id: toolId,
|
|
name: part.functionCall.name || '',
|
|
input: {},
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
|
|
if (part.thoughtSignature) {
|
|
yield {
|
|
type: 'content_block_delta',
|
|
index: toolIndex,
|
|
delta: {
|
|
type: 'signature_delta',
|
|
signature: part.thoughtSignature,
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) {
|
|
yield {
|
|
type: 'content_block_delta',
|
|
index: toolIndex,
|
|
delta: {
|
|
type: 'input_json_delta',
|
|
partial_json: JSON.stringify(part.functionCall.args),
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
yield {
|
|
type: 'content_block_stop',
|
|
index: toolIndex,
|
|
} as BetaRawMessageStreamEvent
|
|
continue
|
|
}
|
|
|
|
const textLikeType = getTextLikeBlockType(part)
|
|
if (textLikeType) {
|
|
if (!openTextLikeBlock || openTextLikeBlock.type !== textLikeType) {
|
|
if (openTextLikeBlock) {
|
|
yield {
|
|
type: 'content_block_stop',
|
|
index: openTextLikeBlock.index,
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
openTextLikeBlock = {
|
|
index: nextContentIndex++,
|
|
type: textLikeType,
|
|
}
|
|
|
|
yield {
|
|
type: 'content_block_start',
|
|
index: openTextLikeBlock.index,
|
|
content_block:
|
|
textLikeType === 'thinking'
|
|
? {
|
|
type: 'thinking',
|
|
thinking: '',
|
|
signature: '',
|
|
}
|
|
: {
|
|
type: 'text',
|
|
text: '',
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
if (part.text) {
|
|
yield {
|
|
type: 'content_block_delta',
|
|
index: openTextLikeBlock.index,
|
|
delta:
|
|
textLikeType === 'thinking'
|
|
? {
|
|
type: 'thinking_delta',
|
|
thinking: part.text,
|
|
}
|
|
: {
|
|
type: 'text_delta',
|
|
text: part.text,
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
if (part.thoughtSignature) {
|
|
yield {
|
|
type: 'content_block_delta',
|
|
index: openTextLikeBlock.index,
|
|
delta: {
|
|
type: 'signature_delta',
|
|
signature: part.thoughtSignature,
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if (part.thoughtSignature && openTextLikeBlock) {
|
|
yield {
|
|
type: 'content_block_delta',
|
|
index: openTextLikeBlock.index,
|
|
delta: {
|
|
type: 'signature_delta',
|
|
signature: part.thoughtSignature,
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
}
|
|
|
|
if (candidate?.finishReason) {
|
|
finishReason = candidate.finishReason
|
|
}
|
|
}
|
|
|
|
if (!started) {
|
|
return
|
|
}
|
|
|
|
if (openTextLikeBlock) {
|
|
yield {
|
|
type: 'content_block_stop',
|
|
index: openTextLikeBlock.index,
|
|
} as BetaRawMessageStreamEvent
|
|
}
|
|
|
|
if (!stopped) {
|
|
yield {
|
|
type: 'message_delta',
|
|
delta: {
|
|
stop_reason: mapGeminiFinishReason(finishReason, sawToolUse),
|
|
stop_sequence: null,
|
|
},
|
|
usage: {
|
|
output_tokens: outputTokens,
|
|
},
|
|
} as BetaRawMessageStreamEvent
|
|
|
|
yield {
|
|
type: 'message_stop',
|
|
} as BetaRawMessageStreamEvent
|
|
stopped = true
|
|
}
|
|
}
|
|
|
|
function getTextLikeBlockType(
|
|
part: GeminiPart,
|
|
): 'text' | 'thinking' | null {
|
|
if (typeof part.text !== 'string') {
|
|
return null
|
|
}
|
|
return part.thought ? 'thinking' : 'text'
|
|
}
|
|
|
|
function mapGeminiFinishReason(
|
|
reason: string | undefined,
|
|
sawToolUse: boolean,
|
|
): string {
|
|
switch (reason) {
|
|
case 'MAX_TOKENS':
|
|
return 'max_tokens'
|
|
case 'STOP':
|
|
case 'FINISH_REASON_UNSPECIFIED':
|
|
case 'SAFETY':
|
|
case 'RECITATION':
|
|
case 'BLOCKLIST':
|
|
case 'PROHIBITED_CONTENT':
|
|
case 'SPII':
|
|
case 'MALFORMED_FUNCTION_CALL':
|
|
default:
|
|
return sawToolUse ? 'tool_use' : 'end_turn'
|
|
}
|
|
}
|