feat: add huggingface provider (#10966)

* Refactor code structure for improved readability and maintainability

* fix(i18n): Auto update translations for PR #10966

* fix: add empty array for huggingface models in SYSTEM_MODELS

* feat: integrate HuggingFace provider and enhance reasoning options

* fix: remove debug console logs from provider options functions

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
SuYao 2025-10-27 13:30:23 +08:00 committed by GitHub
parent 44e01e5ad4
commit 82132d479a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 266 additions and 24 deletions

View File

@ -0,0 +1,131 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index b3f018730a93639aad7c203f15fb1aeb766c73f4..ade2a43d66e9184799d072153df61ef7be4ea110 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -296,7 +296,14 @@ var HuggingFaceResponsesLanguageModel = class {
metadata: huggingfaceOptions == null ? void 0 : huggingfaceOptions.metadata,
instructions: huggingfaceOptions == null ? void 0 : huggingfaceOptions.instructions,
...preparedTools && { tools: preparedTools },
- ...preparedToolChoice && { tool_choice: preparedToolChoice }
+ ...preparedToolChoice && { tool_choice: preparedToolChoice },
+ ...(huggingfaceOptions?.reasoningEffort != null && {
+ reasoning: {
+ ...(huggingfaceOptions?.reasoningEffort != null && {
+ effort: huggingfaceOptions.reasoningEffort,
+ }),
+ },
+ }),
};
return { args: baseArgs, warnings };
}
@@ -365,6 +372,20 @@ var HuggingFaceResponsesLanguageModel = class {
}
break;
}
+ case 'reasoning': {
+ for (const contentPart of part.content) {
+ content.push({
+ type: 'reasoning',
+ text: contentPart.text,
+ providerMetadata: {
+ huggingface: {
+ itemId: part.id,
+ },
+ },
+ });
+ }
+ break;
+ }
case "mcp_call": {
content.push({
type: "tool-call",
@@ -519,6 +540,11 @@ var HuggingFaceResponsesLanguageModel = class {
id: value.item.call_id,
toolName: value.item.name
});
+ } else if (value.item.type === 'reasoning') {
+ controller.enqueue({
+ type: 'reasoning-start',
+ id: value.item.id,
+ });
}
return;
}
@@ -570,6 +596,22 @@ var HuggingFaceResponsesLanguageModel = class {
});
return;
}
+ if (isReasoningDeltaChunk(value)) {
+ controller.enqueue({
+ type: 'reasoning-delta',
+ id: value.item_id,
+ delta: value.delta,
+ });
+ return;
+ }
+
+ if (isReasoningEndChunk(value)) {
+ controller.enqueue({
+ type: 'reasoning-end',
+ id: value.item_id,
+ });
+ return;
+ }
},
flush(controller) {
controller.enqueue({
@@ -593,7 +635,8 @@ var HuggingFaceResponsesLanguageModel = class {
var huggingfaceResponsesProviderOptionsSchema = z2.object({
metadata: z2.record(z2.string(), z2.string()).optional(),
instructions: z2.string().optional(),
- strictJsonSchema: z2.boolean().optional()
+ strictJsonSchema: z2.boolean().optional(),
+ reasoningEffort: z2.string().optional(),
});
var huggingfaceResponsesResponseSchema = z2.object({
id: z2.string(),
@@ -727,12 +770,31 @@ var responseCreatedChunkSchema = z2.object({
model: z2.string()
})
});
+var reasoningTextDeltaChunkSchema = z2.object({
+ type: z2.literal('response.reasoning_text.delta'),
+ item_id: z2.string(),
+ output_index: z2.number(),
+ content_index: z2.number(),
+ delta: z2.string(),
+ sequence_number: z2.number(),
+});
+
+var reasoningTextEndChunkSchema = z2.object({
+ type: z2.literal('response.reasoning_text.done'),
+ item_id: z2.string(),
+ output_index: z2.number(),
+ content_index: z2.number(),
+ text: z2.string(),
+ sequence_number: z2.number(),
+});
var huggingfaceResponsesChunkSchema = z2.union([
responseOutputItemAddedSchema,
responseOutputItemDoneSchema,
textDeltaChunkSchema,
responseCompletedChunkSchema,
responseCreatedChunkSchema,
+ reasoningTextDeltaChunkSchema,
+ reasoningTextEndChunkSchema,
z2.object({ type: z2.string() }).loose()
// fallback for unknown chunks
]);
@@ -751,6 +813,12 @@ function isResponseCompletedChunk(chunk) {
function isResponseCreatedChunk(chunk) {
return chunk.type === "response.created";
}
+function isReasoningDeltaChunk(chunk) {
+ return chunk.type === 'response.reasoning_text.delta';
+}
+function isReasoningEndChunk(chunk) {
+ return chunk.type === 'response.reasoning_text.done';
+}
// src/huggingface-provider.ts
function createHuggingFace(options = {}) {

View File

@ -103,6 +103,7 @@
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.35",
"@ai-sdk/google-vertex": "^3.0.40",
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
"@ai-sdk/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13",
"@ant-design/v5-patch-for-react-19": "^1.0.3",

View File

@ -7,6 +7,7 @@ import { createAzure } from '@ai-sdk/azure'
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
import { createDeepSeek } from '@ai-sdk/deepseek'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
import { createHuggingFace } from '@ai-sdk/huggingface'
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import { LanguageModelV2 } from '@ai-sdk/provider'
@ -28,7 +29,8 @@ export const baseProviderIds = [
'azure',
'azure-responses',
'deepseek',
'openrouter'
'openrouter',
'huggingface'
] as const
/**
@ -132,6 +134,12 @@ export const baseProviders = [
name: 'OpenRouter',
creator: createOpenRouter,
supportsImageGeneration: true
},
{
id: 'huggingface',
name: 'HuggingFace',
creator: createHuggingFace,
supportsImageGeneration: true
}
] as const satisfies BaseProvider[]

View File

@ -49,7 +49,7 @@ class AdapterTracer {
this.cachedParentContext = undefined
}
logger.info('AdapterTracer created with parent context info', {
logger.debug('AdapterTracer created with parent context info', {
topicId,
modelName,
parentTraceId: this.parentSpanContext?.traceId,
@ -62,7 +62,7 @@ class AdapterTracer {
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, fn: F): ReturnType<F>
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, context: any, fn: F): ReturnType<F>
startActiveSpan<F extends (span: Span) => any>(name: string, arg2?: any, arg3?: any, arg4?: any): ReturnType<F> {
logger.info('AdapterTracer.startActiveSpan called', {
logger.debug('AdapterTracer.startActiveSpan called', {
spanName: name,
topicId: this.topicId,
modelName: this.modelName,
@ -88,7 +88,7 @@ class AdapterTracer {
// 包装span的end方法
const originalEnd = span.end.bind(span)
span.end = (endTime?: any) => {
logger.info('AI SDK span.end() called in startActiveSpan - about to convert span', {
logger.debug('AI SDK span.end() called in startActiveSpan - about to convert span', {
spanName: name,
spanId: span.spanContext().spanId,
traceId: span.spanContext().traceId,
@ -101,14 +101,14 @@ class AdapterTracer {
// 转换并保存 span 数据
try {
logger.info('Converting AI SDK span to SpanEntity (from startActiveSpan)', {
logger.debug('Converting AI SDK span to SpanEntity (from startActiveSpan)', {
spanName: name,
spanId: span.spanContext().spanId,
traceId: span.spanContext().traceId,
topicId: this.topicId,
modelName: this.modelName
})
logger.info('span', span)
logger.silly('span', span)
const spanEntity = AiSdkSpanAdapter.convertToSpanEntity({
span,
topicId: this.topicId,
@ -118,7 +118,7 @@ class AdapterTracer {
// 保存转换后的数据
window.api.trace.saveEntity(spanEntity)
logger.info('AI SDK span converted and saved successfully (from startActiveSpan)', {
logger.debug('AI SDK span converted and saved successfully (from startActiveSpan)', {
spanName: name,
spanId: span.spanContext().spanId,
traceId: span.spanContext().traceId,
@ -151,7 +151,7 @@ class AdapterTracer {
if (this.parentSpanContext) {
try {
const ctx = trace.setSpanContext(otelContext.active(), this.parentSpanContext)
logger.info('Created active context with parent SpanContext for startActiveSpan', {
logger.debug('Created active context with parent SpanContext for startActiveSpan', {
spanName: name,
parentTraceId: this.parentSpanContext.traceId,
parentSpanId: this.parentSpanContext.spanId,
@ -218,7 +218,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
if (effectiveTopicId) {
try {
// 从 SpanManagerService 获取当前的 span
logger.info('Attempting to find parent span', {
logger.debug('Attempting to find parent span', {
topicId: effectiveTopicId,
requestId: context.requestId,
modelName: modelName,
@ -230,7 +230,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
if (parentSpan) {
// 直接使用父 span 的 SpanContext避免手动拼装字段遗漏
parentSpanContext = parentSpan.spanContext()
logger.info('Found active parent span for AI SDK', {
logger.debug('Found active parent span for AI SDK', {
parentSpanId: parentSpanContext.spanId,
parentTraceId: parentSpanContext.traceId,
topicId: effectiveTopicId,
@ -302,7 +302,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
logger.debug('Updated active context with parent span')
})
logger.info('Set parent context for AI SDK spans', {
logger.debug('Set parent context for AI SDK spans', {
parentSpanId: parentSpanContext?.spanId,
parentTraceId: parentSpanContext?.traceId,
hasActiveContext: !!activeContext,
@ -313,7 +313,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
}
}
logger.info('Injecting AI SDK telemetry config with adapter', {
logger.debug('Injecting AI SDK telemetry config with adapter', {
requestId: context.requestId,
topicId: effectiveTopicId,
modelId: context.modelId,

View File

@ -63,6 +63,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
creatorFunctionName: 'createMistral',
supportsImageGeneration: false,
aliases: ['mistral']
},
{
id: 'huggingface',
name: 'HuggingFace',
import: () => import('@ai-sdk/huggingface'),
creatorFunctionName: 'createHuggingFace',
supportsImageGeneration: true,
aliases: ['hf', 'hugging-face']
}
] as const

View File

@ -90,7 +90,9 @@ export function buildProviderOptions(
serviceTier: serviceTierSetting
}
break
case 'huggingface':
providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities)
break
case 'anthropic':
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
break

View File

@ -10,6 +10,7 @@ import {
isGrok4FastReasoningModel,
isGrokReasoningModel,
isOpenAIDeepResearchModel,
isOpenAIModel,
isOpenAIReasoningModel,
isQwenAlwaysThinkModel,
isQwenReasoningModel,
@ -319,6 +320,20 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re
if (!isReasoningModel(model)) {
return {}
}
let reasoningEffort = assistant?.settings?.reasoning_effort
if (!reasoningEffort) {
return {}
}
// 非OpenAI模型但是Provider类型是responses/azure openai的情况
if (!isOpenAIModel(model)) {
return {
reasoningEffort
}
}
const openAI = getStoreSetting('openAI') as SettingsState['openAI']
const summaryText = openAI?.summaryText || 'off'
@ -330,16 +345,10 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re
reasoningSummary = summaryText
}
let reasoningEffort = assistant?.settings?.reasoning_effort
if (isOpenAIDeepResearchModel(model)) {
reasoningEffort = 'medium'
}
if (!reasoningEffort) {
return {}
}
// OpenAI 推理参数
if (isSupportedReasoningEffortOpenAIModel(model)) {
return {

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1837,5 +1837,6 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
provider: 'longcat',
group: 'LongCat'
}
]
],
huggingface: []
}

View File

@ -22,6 +22,7 @@ import GoogleProviderLogo from '@renderer/assets/images/providers/google.png'
import GPUStackProviderLogo from '@renderer/assets/images/providers/gpustack.svg'
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import HuggingfaceProviderLogo from '@renderer/assets/images/providers/huggingface.webp'
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
import IntelOvmsLogo from '@renderer/assets/images/providers/intel.png'
@ -653,6 +654,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
models: SYSTEM_MODELS.longcat,
isSystem: true,
enabled: false
},
huggingface: {
id: 'huggingface',
name: 'Hugging Face',
type: 'openai-response',
apiKey: '',
apiHost: 'https://router.huggingface.co/v1/',
models: [],
isSystem: true,
enabled: false
}
} as const
@ -717,7 +728,8 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
'aws-bedrock': AwsProviderLogo,
poe: 'poe', // use svg icon component
aionly: AiOnlyProviderLogo,
longcat: LongCatProviderLogo
longcat: LongCatProviderLogo,
huggingface: HuggingfaceProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@ -1344,6 +1356,17 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
docs: 'https://longcat.chat/platform/docs/zh/',
models: 'https://longcat.chat/platform/docs/zh/APIDocs.html'
}
},
huggingface: {
api: {
url: 'https://router.huggingface.co/v1/'
},
websites: {
official: 'https://huggingface.co/',
apiKey: 'https://huggingface.co/settings/tokens',
docs: 'https://huggingface.co/docs',
models: 'https://huggingface.co/models'
}
}
}

View File

@ -88,7 +88,9 @@ const providerKeyMap = {
zhinao: 'provider.zhinao',
zhipu: 'provider.zhipu',
poe: 'provider.poe',
aionly: 'provider.aionly'
aionly: 'provider.aionly',
longcat: 'provider.longcat',
huggingface: 'provider.huggingface'
} as const
/**

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hunyuan",
"hyperbolic": "Hyperbolic",
"infini": "Infini",
"jina": "Jina",
"lanyun": "LANYUN",
"lmstudio": "LM Studio",
"longcat": "LongCat AI",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "腾讯混元",
"hyperbolic": "Hyperbolic",
"infini": "无问芯穹",
"jina": "Jina",
"lanyun": "蓝耘科技",
"lmstudio": "LM Studio",
"longcat": "龙猫",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope 魔搭",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "騰訊混元",
"hyperbolic": "Hyperbolic",
"infini": "無問芯穹",
"jina": "Jina",
"lanyun": "藍耘",
"lmstudio": "LM Studio",
"longcat": "龍貓",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope 魔搭",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hunyuan",
"hyperbolic": "Hyperbolic",
"infini": "Infini-AI",
"jina": "Jina",
"lanyun": "Lanyun Technologie",
"lmstudio": "LM Studio",
"longcat": "Meißner Riesenhamster",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hunyuan",
"hyperbolic": "Υπερβολικός",
"infini": "Χωρίς Ερώτημα Xin Qiong",
"jina": "Jina",
"lanyun": "Λανιούν Τεχνολογία",
"lmstudio": "LM Studio",
"longcat": "Τσίρο",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope Magpie",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hùnyuán",
"hyperbolic": "Hiperbólico",
"infini": "Infini",
"jina": "Jina",
"lanyun": "Tecnología Lanyun",
"lmstudio": "Estudio LM",
"longcat": "Totoro",
"minimax": "Minimax",
"mistral": "Mistral",
"modelscope": "ModelScope Módulo",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent HunYuan",
"hyperbolic": "Hyperbolique",
"infini": "Sans Frontières Céleste",
"jina": "Jina",
"lanyun": "Technologie Lan Yun",
"lmstudio": "Studio LM",
"longcat": "Mon voisin Totoro",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope MoDa",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "ハギングフェイス",
"hunyuan": "腾讯混元",
"hyperbolic": "Hyperbolic",
"infini": "Infini",
"jina": "Jina",
"lanyun": "LANYUN",
"lmstudio": "LM Studio",
"longcat": "トトロ",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Compreender",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hún Yuán",
"hyperbolic": "Hiperbólico",
"infini": "Infinito",
"jina": "Jina",
"lanyun": "Lanyun Tecnologia",
"lmstudio": "Estúdio LM",
"longcat": "Totoro",
"minimax": "Minimax",
"mistral": "Mistral",
"modelscope": "ModelScope MôDá",

View File

@ -2345,12 +2345,14 @@
"gpustack": "GPUStack",
"grok": "Grok",
"groq": "Groq",
"huggingface": "Hugging Face",
"hunyuan": "Tencent Hunyuan",
"hyperbolic": "Hyperbolic",
"infini": "Infini",
"jina": "Jina",
"lanyun": "LANYUN",
"lmstudio": "LM Studio",
"longcat": "Тоторо",
"minimax": "MiniMax",
"mistral": "Mistral",
"modelscope": "ModelScope",

View File

@ -65,7 +65,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 166,
version: 167,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@ -2720,6 +2720,15 @@ const migrateConfig = {
logger.error('migrate 166 error', error as Error)
return state
}
},
'167': (state: RootState) => {
try {
addProvider(state, 'huggingface')
return state
} catch (error) {
logger.error('migrate 167 error', error as Error)
return state
}
}
}

View File

@ -162,7 +162,8 @@ export const SystemProviderIds = {
'aws-bedrock': 'aws-bedrock',
poe: 'poe',
aionly: 'aionly',
longcat: 'longcat'
longcat: 'longcat',
huggingface: 'huggingface'
} as const
export type SystemProviderId = keyof typeof SystemProviderIds

View File

@ -180,6 +180,32 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/huggingface@npm:0.0.4":
version: 0.0.4
resolution: "@ai-sdk/huggingface@npm:0.0.4"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.22"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4
checksum: 10c0/756b8f820b89bf9550c9281dfe2a1a813477dec82be5557e236e8b5eaaf0204b65a65925ad486b7576c687f33c709f6d99fd4fc87a46b1add210435b08834986
languageName: node
linkType: hard
"@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch":
version: 0.0.4
resolution: "@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch::version=0.0.4&hash=ceb48e"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.22"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4
checksum: 10c0/4726a10de7a6fd554b58d62f79cd6514c2cc5166052e035ba1517e224a310ddb355a5d2922ee8507fb8d928d6d5b2b102d3d221af5a44b181e436e6b64382087
languageName: node
linkType: hard
"@ai-sdk/mistral@npm:^2.0.19":
version: 2.0.19
resolution: "@ai-sdk/mistral@npm:2.0.19"
@ -13853,6 +13879,7 @@ __metadata:
"@agentic/tavily": "npm:^7.3.3"
"@ai-sdk/amazon-bedrock": "npm:^3.0.35"
"@ai-sdk/google-vertex": "npm:^3.0.40"
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch"
"@ai-sdk/mistral": "npm:^2.0.19"
"@ai-sdk/perplexity": "npm:^2.0.13"
"@ant-design/v5-patch-for-react-19": "npm:^1.0.3"