Files
claude-code/packages/@ant/model-provider/src/providers/gemini/convertTools.ts
claude-code-best bddd146f25 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>
2026-04-17 09:33:14 +08:00

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
}
}