Skip to content

Commit fe3c8b1

Browse files
authored
feat(provider): add NewAPI provider (#1443)
* feat(provider): add NewAPI provider * fix(i18n): translate endpointType for all locales
1 parent dbe0319 commit fe3c8b1

File tree

36 files changed

+1677
-100
lines changed

36 files changed

+1677
-100
lines changed

src/main/presenter/configPresenter/index.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import {
2121
} from '@shared/presenter'
2222
import { ProviderBatchUpdate } from '@shared/provider-operations'
2323
import { SearchEngineTemplate } from '@shared/chat'
24-
import { ModelType } from '@shared/model'
24+
import {
25+
ModelType,
26+
isNewApiEndpointType,
27+
resolveNewApiCapabilityProviderId,
28+
type NewApiEndpointType
29+
} from '@shared/model'
2530
import {
2631
DEFAULT_MODEL_CAPABILITY_FALLBACKS,
2732
resolveModelContextLength,
@@ -531,19 +536,70 @@ export class ConfigPresenter implements IConfigPresenter {
531536
return providerDbLoader.refreshIfNeeded(force)
532537
}
533538

539+
private resolveNewApiCapabilityEndpointType(modelId: string): NewApiEndpointType {
540+
const modelConfig = this.getModelConfig(modelId, 'new-api')
541+
if (isNewApiEndpointType(modelConfig.endpointType)) {
542+
return modelConfig.endpointType
543+
}
544+
545+
const storedModel =
546+
this.getProviderModels('new-api').find((model) => model.id === modelId) ??
547+
this.getCustomModels('new-api').find((model) => model.id === modelId)
548+
549+
if (storedModel) {
550+
if (isNewApiEndpointType(storedModel.endpointType)) {
551+
return storedModel.endpointType
552+
}
553+
554+
const supportedEndpointTypes =
555+
storedModel.supportedEndpointTypes?.filter(isNewApiEndpointType) ?? []
556+
if (
557+
storedModel.type === ModelType.ImageGeneration &&
558+
supportedEndpointTypes.includes('image-generation')
559+
) {
560+
return 'image-generation'
561+
}
562+
if (supportedEndpointTypes.length > 0) {
563+
return supportedEndpointTypes[0]
564+
}
565+
if (storedModel.type === ModelType.ImageGeneration) {
566+
return 'image-generation'
567+
}
568+
}
569+
570+
return 'openai'
571+
}
572+
573+
private resolveCapabilityProviderId(providerId: string, modelId: string): string {
574+
if (providerId.trim().toLowerCase() !== 'new-api') {
575+
return providerId
576+
}
577+
578+
return resolveNewApiCapabilityProviderId(this.resolveNewApiCapabilityEndpointType(modelId))
579+
}
580+
534581
supportsReasoningCapability(providerId: string, modelId: string): boolean {
535-
return modelCapabilities.supportsReasoning(providerId, modelId)
582+
return modelCapabilities.supportsReasoning(
583+
this.resolveCapabilityProviderId(providerId, modelId),
584+
modelId
585+
)
536586
}
537587

538588
getReasoningPortrait(providerId: string, modelId: string): ReasoningPortrait | null {
539-
return modelCapabilities.getReasoningPortrait(providerId, modelId)
589+
return modelCapabilities.getReasoningPortrait(
590+
this.resolveCapabilityProviderId(providerId, modelId),
591+
modelId
592+
)
540593
}
541594

542595
getThinkingBudgetRange(
543596
providerId: string,
544597
modelId: string
545598
): { min?: number; max?: number; default?: number } {
546-
return modelCapabilities.getThinkingBudgetRange(providerId, modelId)
599+
return modelCapabilities.getThinkingBudgetRange(
600+
this.resolveCapabilityProviderId(providerId, modelId),
601+
modelId
602+
)
547603
}
548604

549605
supportsSearchCapability(providerId: string, modelId: string): boolean {
@@ -558,22 +614,34 @@ export class ConfigPresenter implements IConfigPresenter {
558614
}
559615

560616
supportsReasoningEffortCapability(providerId: string, modelId: string): boolean {
561-
return modelCapabilities.supportsReasoningEffort(providerId, modelId)
617+
return modelCapabilities.supportsReasoningEffort(
618+
this.resolveCapabilityProviderId(providerId, modelId),
619+
modelId
620+
)
562621
}
563622

564623
getReasoningEffortDefault(
565624
providerId: string,
566625
modelId: string
567626
): 'minimal' | 'low' | 'medium' | 'high' | undefined {
568-
return modelCapabilities.getReasoningEffortDefault(providerId, modelId)
627+
return modelCapabilities.getReasoningEffortDefault(
628+
this.resolveCapabilityProviderId(providerId, modelId),
629+
modelId
630+
)
569631
}
570632

571633
supportsVerbosityCapability(providerId: string, modelId: string): boolean {
572-
return modelCapabilities.supportsVerbosity(providerId, modelId)
634+
return modelCapabilities.supportsVerbosity(
635+
this.resolveCapabilityProviderId(providerId, modelId),
636+
modelId
637+
)
573638
}
574639

575640
getVerbosityDefault(providerId: string, modelId: string): 'low' | 'medium' | 'high' | undefined {
576-
return modelCapabilities.getVerbosityDefault(providerId, modelId)
641+
return modelCapabilities.getVerbosityDefault(
642+
this.resolveCapabilityProviderId(providerId, modelId),
643+
modelId
644+
)
577645
}
578646

579647
private migrateConfigData(oldVersion: string | undefined): void {

src/main/presenter/configPresenter/modelConfig.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApiEndpointType, ModelType } from '@shared/model'
1+
import { ApiEndpointType, ModelType, isNewApiEndpointType } from '@shared/model'
22
import { IModelConfig, ModelConfig, ModelConfigSource } from '@shared/presenter'
33
import {
44
DEFAULT_MODEL_CAPABILITY_FALLBACKS,
@@ -453,6 +453,7 @@ export class ModelConfigHelper {
453453
temperature: 0.6,
454454
type: ModelType.Chat,
455455
apiEndpoint: ApiEndpointType.Chat,
456+
endpointType: undefined,
456457
thinkingBudget: undefined,
457458
forceInterleavedThinkingCompat: undefined,
458459
reasoningEffort: undefined,
@@ -476,6 +477,9 @@ export class ModelConfigHelper {
476477
maxCompletionTokens: storedConfig.maxCompletionTokens ?? finalConfig.maxCompletionTokens,
477478
conversationId: storedConfig.conversationId ?? finalConfig.conversationId,
478479
apiEndpoint: storedConfig.apiEndpoint ?? finalConfig.apiEndpoint,
480+
endpointType: isNewApiEndpointType(storedConfig.endpointType)
481+
? storedConfig.endpointType
482+
: finalConfig.endpointType,
479483
enableSearch: storedConfig.enableSearch ?? finalConfig.enableSearch,
480484
forcedSearch: storedConfig.forcedSearch ?? finalConfig.forcedSearch,
481485
searchStrategy: storedConfig.searchStrategy ?? finalConfig.searchStrategy,

src/main/presenter/configPresenter/providerModelHelper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export class ProviderModelHelper {
9393
model.reasoning =
9494
model.reasoning !== undefined ? model.reasoning : config.reasoning || false
9595
model.type = model.type !== undefined ? model.type : config.type || ModelType.Chat
96+
model.endpointType = config.endpointType ?? model.endpointType
9697
} else {
9798
model.vision = model.vision || false
9899
model.functionCall = model.functionCall || false
@@ -153,10 +154,12 @@ export class ProviderModelHelper {
153154
const store = this.getProviderModelStore(providerId)
154155
const customModels = (store.get('custom_models') || []) as MODEL_META[]
155156
return customModels.map((model) => {
157+
const config = this.getModelConfig(model.id, providerId)
156158
model.vision = model.vision !== undefined ? model.vision : false
157159
model.functionCall = model.functionCall !== undefined ? model.functionCall : false
158160
model.reasoning = model.reasoning !== undefined ? model.reasoning : false
159161
model.type = model.type || ModelType.Chat
162+
model.endpointType = config?.endpointType ?? model.endpointType
160163
return model
161164
})
162165
}

src/main/presenter/configPresenter/providers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,21 @@ export const DEFAULT_PROVIDERS: LLM_PROVIDER_BASE[] = [
202202
defaultBaseUrl: 'https://open.cherryin.ai/v1'
203203
}
204204
},
205+
{
206+
id: 'new-api',
207+
name: 'New API',
208+
apiType: 'new-api',
209+
apiKey: '',
210+
baseUrl: 'https://www.newapi.ai',
211+
enable: false,
212+
websites: {
213+
official: 'https://www.newapi.ai/',
214+
apiKey: 'https://www.newapi.ai/token',
215+
docs: 'https://www.newapi.ai/zh/docs/api',
216+
models: 'https://www.newapi.ai/zh/docs/api',
217+
defaultBaseUrl: 'https://www.newapi.ai'
218+
}
219+
},
205220
{
206221
id: 'openai',
207222
name: 'OpenAI',

src/main/presenter/llmProviderPresenter/baseProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export abstract class BaseLLMProvider {
7777
return BaseLLMProvider.DEFAULT_MODEL_FETCH_TIMEOUT
7878
}
7979

80+
protected getCapabilityProviderId(): string {
81+
return this.provider.capabilityProviderId || this.provider.id
82+
}
83+
8084
/**
8185
* Load cached model data from configuration
8286
* Called in constructor to avoid needing to re-fetch model lists every time

src/main/presenter/llmProviderPresenter/managers/modelManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ export class ModelManager {
4242
model.functionCall = config.functionCall
4343
model.reasoning = config.reasoning
4444
model.type = config.type
45+
model.endpointType = config.endpointType ?? model.endpointType
4546
} else {
4647
model.vision = model.vision !== undefined ? model.vision : config.vision
4748
model.functionCall =
4849
model.functionCall !== undefined ? model.functionCall : config.functionCall
4950
model.reasoning = model.reasoning !== undefined ? model.reasoning : config.reasoning
5051
model.type = model.type || config.type
52+
model.endpointType = model.endpointType ?? config.endpointType
5153
}
5254

5355
return model

src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { JiekouProvider } from '../providers/jiekouProvider'
3434
import { ZenmuxProvider } from '../providers/zenmuxProvider'
3535
import { O3fanProvider } from '../providers/o3fanProvider'
3636
import { VoiceAIProvider } from '../providers/voiceAIProvider'
37+
import { NewApiProvider } from '../providers/newApiProvider'
3738
import { RateLimitManager } from './rateLimitManager'
3839
import { StreamState } from '../types'
3940
import { AcpSessionPersistence } from '../acp'
@@ -90,6 +91,7 @@ export class ProviderInstanceManager {
9091
['voiceai', VoiceAIProvider],
9192
['openai-responses', OpenAIResponsesProvider],
9293
['cherryin', CherryInProvider],
94+
['new-api', NewApiProvider],
9395
['lmstudio', LMStudioProvider],
9496
['together', TogetherProvider],
9597
['groq', GroqProvider],
@@ -124,6 +126,7 @@ export class ProviderInstanceManager {
124126
['voiceai', VoiceAIProvider],
125127
['openai-compatible', OpenAICompatibleProvider],
126128
['openai-responses', OpenAIResponsesProvider],
129+
['new-api', NewApiProvider],
127130
['lmstudio', LMStudioProvider],
128131
['together', TogetherProvider],
129132
['groq', GroqProvider],

src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,10 @@ export class GeminiProvider extends BaseLLMProvider {
329329
// 判断模型是否支持 thinkingBudget
330330
private supportsThinkingBudget(modelId: string): boolean {
331331
const normalized = modelId.replace(/^models\//i, '')
332-
const range = modelCapabilities.getThinkingBudgetRange(this.provider.id, normalized)
332+
const range = modelCapabilities.getThinkingBudgetRange(
333+
this.getCapabilityProviderId(),
334+
normalized
335+
)
333336
return (
334337
typeof range.default === 'number' ||
335338
typeof range.min === 'number' ||

0 commit comments

Comments
 (0)