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>
286 lines
7.8 KiB
TypeScript
286 lines
7.8 KiB
TypeScript
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
|
import type {
|
|
GeminiFunctionCallingConfig,
|
|
GeminiTool,
|
|
} from './types.js'
|
|
|
|
const GEMINI_JSON_SCHEMA_TYPES = new Set([
|
|
'string',
|
|
'number',
|
|
'integer',
|
|
'boolean',
|
|
'object',
|
|
'array',
|
|
'null',
|
|
])
|
|
|
|
function normalizeGeminiJsonSchemaType(
|
|
value: unknown,
|
|
): string | string[] | undefined {
|
|
if (typeof value === 'string') {
|
|
return GEMINI_JSON_SCHEMA_TYPES.has(value) ? value : undefined
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
const normalized = value.filter(
|
|
(item): item is string =>
|
|
typeof item === 'string' && GEMINI_JSON_SCHEMA_TYPES.has(item),
|
|
)
|
|
const unique = Array.from(new Set(normalized))
|
|
if (unique.length === 0) return undefined
|
|
return unique.length === 1 ? unique[0] : unique
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined {
|
|
if (value === null) return 'null'
|
|
if (Array.isArray(value)) return 'array'
|
|
if (typeof value === 'string') return 'string'
|
|
if (typeof value === 'boolean') return 'boolean'
|
|
if (typeof value === 'number') {
|
|
return Number.isInteger(value) ? 'integer' : 'number'
|
|
}
|
|
if (typeof value === 'object') return 'object'
|
|
return undefined
|
|
}
|
|
|
|
function inferGeminiJsonSchemaTypeFromEnum(
|
|
values: unknown[],
|
|
): string | string[] | undefined {
|
|
const inferred = values
|
|
.map(inferGeminiJsonSchemaTypeFromValue)
|
|
.filter((value): value is string => value !== undefined)
|
|
const unique = Array.from(new Set(inferred))
|
|
if (unique.length === 0) return undefined
|
|
return unique.length === 1 ? unique[0] : unique
|
|
}
|
|
|
|
function addNullToGeminiJsonSchemaType(
|
|
value: string | string[] | undefined,
|
|
): string | string[] | undefined {
|
|
if (value === undefined) return ['null']
|
|
if (Array.isArray(value)) {
|
|
return value.includes('null') ? value : [...value, 'null']
|
|
}
|
|
return value === 'null' ? value : [value, 'null']
|
|
}
|
|
|
|
function sanitizeGeminiJsonSchemaProperties(
|
|
value: unknown,
|
|
): Record<string, Record<string, unknown>> | undefined {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return undefined
|
|
}
|
|
|
|
const sanitizedEntries = Object.entries(value as Record<string, unknown>)
|
|
.map(([key, schema]) => [key, sanitizeGeminiJsonSchema(schema)] as const)
|
|
.filter(([, schema]) => Object.keys(schema).length > 0)
|
|
|
|
if (sanitizedEntries.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
return Object.fromEntries(sanitizedEntries)
|
|
}
|
|
|
|
function sanitizeGeminiJsonSchemaArray(
|
|
value: unknown,
|
|
): Record<string, unknown>[] | undefined {
|
|
if (!Array.isArray(value)) return undefined
|
|
|
|
const sanitized = value
|
|
.map(item => sanitizeGeminiJsonSchema(item))
|
|
.filter(item => Object.keys(item).length > 0)
|
|
|
|
return sanitized.length > 0 ? sanitized : undefined
|
|
}
|
|
|
|
function sanitizeGeminiJsonSchema(
|
|
schema: unknown,
|
|
): Record<string, unknown> {
|
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
return {}
|
|
}
|
|
|
|
const source = schema as Record<string, unknown>
|
|
const result: Record<string, unknown> = {}
|
|
|
|
let type = normalizeGeminiJsonSchemaType(source.type)
|
|
|
|
if (source.const !== undefined) {
|
|
result.enum = [source.const]
|
|
type = type ?? inferGeminiJsonSchemaTypeFromValue(source.const)
|
|
} else if (Array.isArray(source.enum) && source.enum.length > 0) {
|
|
result.enum = source.enum
|
|
type = type ?? inferGeminiJsonSchemaTypeFromEnum(source.enum)
|
|
}
|
|
|
|
if (!type) {
|
|
if (source.properties && typeof source.properties === 'object') {
|
|
type = 'object'
|
|
} else if (source.items !== undefined || source.prefixItems !== undefined) {
|
|
type = 'array'
|
|
}
|
|
}
|
|
|
|
if (source.nullable === true) {
|
|
type = addNullToGeminiJsonSchemaType(type)
|
|
}
|
|
|
|
if (type) {
|
|
result.type = type
|
|
}
|
|
|
|
if (typeof source.title === 'string') {
|
|
result.title = source.title
|
|
}
|
|
if (typeof source.description === 'string') {
|
|
result.description = source.description
|
|
}
|
|
if (typeof source.format === 'string') {
|
|
result.format = source.format
|
|
}
|
|
if (typeof source.pattern === 'string') {
|
|
result.pattern = source.pattern
|
|
}
|
|
if (typeof source.minimum === 'number') {
|
|
result.minimum = source.minimum
|
|
} else if (typeof source.exclusiveMinimum === 'number') {
|
|
result.minimum = source.exclusiveMinimum
|
|
}
|
|
if (typeof source.maximum === 'number') {
|
|
result.maximum = source.maximum
|
|
} else if (typeof source.exclusiveMaximum === 'number') {
|
|
result.maximum = source.exclusiveMaximum
|
|
}
|
|
if (typeof source.minItems === 'number') {
|
|
result.minItems = source.minItems
|
|
}
|
|
if (typeof source.maxItems === 'number') {
|
|
result.maxItems = source.maxItems
|
|
}
|
|
if (typeof source.minLength === 'number') {
|
|
result.minLength = source.minLength
|
|
}
|
|
if (typeof source.maxLength === 'number') {
|
|
result.maxLength = source.maxLength
|
|
}
|
|
if (typeof source.minProperties === 'number') {
|
|
result.minProperties = source.minProperties
|
|
}
|
|
if (typeof source.maxProperties === 'number') {
|
|
result.maxProperties = source.maxProperties
|
|
}
|
|
|
|
const properties = sanitizeGeminiJsonSchemaProperties(source.properties)
|
|
if (properties) {
|
|
result.properties = properties
|
|
result.propertyOrdering = Object.keys(properties)
|
|
}
|
|
|
|
if (Array.isArray(source.required)) {
|
|
const required = source.required.filter(
|
|
(item): item is string => typeof item === 'string',
|
|
)
|
|
if (required.length > 0) {
|
|
result.required = required
|
|
}
|
|
}
|
|
|
|
if (typeof source.additionalProperties === 'boolean') {
|
|
result.additionalProperties = source.additionalProperties
|
|
} else {
|
|
const additionalProperties = sanitizeGeminiJsonSchema(
|
|
source.additionalProperties,
|
|
)
|
|
if (Object.keys(additionalProperties).length > 0) {
|
|
result.additionalProperties = additionalProperties
|
|
}
|
|
}
|
|
|
|
const items = sanitizeGeminiJsonSchema(source.items)
|
|
if (Object.keys(items).length > 0) {
|
|
result.items = items
|
|
}
|
|
|
|
const prefixItems = sanitizeGeminiJsonSchemaArray(source.prefixItems)
|
|
if (prefixItems) {
|
|
result.prefixItems = prefixItems
|
|
}
|
|
|
|
const anyOf = sanitizeGeminiJsonSchemaArray(source.anyOf ?? source.oneOf)
|
|
if (anyOf) {
|
|
result.anyOf = anyOf
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function sanitizeGeminiFunctionParameters(
|
|
schema: unknown,
|
|
): Record<string, unknown> {
|
|
const sanitized = sanitizeGeminiJsonSchema(schema)
|
|
if (Object.keys(sanitized).length > 0) {
|
|
return sanitized
|
|
}
|
|
|
|
return {
|
|
type: 'object',
|
|
properties: {},
|
|
}
|
|
}
|
|
|
|
export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
|
|
const functionDeclarations = tools
|
|
.filter(tool => {
|
|
const toolType = (tool as unknown as { type?: string }).type
|
|
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
|
})
|
|
.map(tool => {
|
|
const anyTool = tool as unknown as Record<string, unknown>
|
|
const name = (anyTool.name as string) || ''
|
|
const description = (anyTool.description as string) || ''
|
|
const inputSchema =
|
|
(anyTool.input_schema as Record<string, unknown> | undefined) ?? {
|
|
type: 'object',
|
|
properties: {},
|
|
}
|
|
|
|
return {
|
|
name,
|
|
description,
|
|
parametersJsonSchema: sanitizeGeminiFunctionParameters(inputSchema),
|
|
}
|
|
})
|
|
|
|
return functionDeclarations.length > 0
|
|
? [{ functionDeclarations }]
|
|
: []
|
|
}
|
|
|
|
export function anthropicToolChoiceToGemini(
|
|
toolChoice: unknown,
|
|
): GeminiFunctionCallingConfig | undefined {
|
|
if (!toolChoice || typeof toolChoice !== 'object') return undefined
|
|
|
|
const tc = toolChoice as Record<string, unknown>
|
|
const type = tc.type as string
|
|
|
|
switch (type) {
|
|
case 'auto':
|
|
return { mode: 'AUTO' }
|
|
case 'any':
|
|
return { mode: 'ANY' }
|
|
case 'tool':
|
|
return {
|
|
mode: 'ANY',
|
|
allowedFunctionNames:
|
|
typeof tc.name === 'string' ? [tc.name] : undefined,
|
|
}
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|