Merge branch 'main' into v2
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import { reduxService } from '@main/services/ReduxService'
|
||||
import { isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
|
||||
import type { ApiModel, Model, Provider } from '@types'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerUtils')
|
||||
@@ -287,6 +288,8 @@ export const getProviderAnthropicModelChecker = (providerId: string): ((m: Model
|
||||
return (m: Model) => m.endpoint_type === 'anthropic'
|
||||
case 'aihubmix':
|
||||
return (m: Model) => m.id.includes('claude')
|
||||
case 'silicon':
|
||||
return (m: Model) => isSiliconAnthropicCompatibleModel(m.id)
|
||||
default:
|
||||
// allow all models when checker not configured
|
||||
return () => true
|
||||
|
||||
@@ -50,7 +50,40 @@ export default class ModernAiProvider {
|
||||
private model?: Model
|
||||
private localProvider: Awaited<AiSdkProvider> | null = null
|
||||
|
||||
// 构造函数重载签名
|
||||
/**
|
||||
* Constructor for ModernAiProvider
|
||||
*
|
||||
* @param modelOrProvider - Model or Provider object
|
||||
* @param provider - Optional Provider object (only used when first param is Model)
|
||||
*
|
||||
* @remarks
|
||||
* **Important behavior notes**:
|
||||
*
|
||||
* 1. When called with `(model)`:
|
||||
* - Calls `getActualProvider(model)` to retrieve and format the provider
|
||||
* - URL will be automatically formatted via `formatProviderApiHost`, adding version suffixes like `/v1`
|
||||
*
|
||||
* 2. When called with `(model, provider)`:
|
||||
* - **Directly uses the provided provider WITHOUT going through `getActualProvider`**
|
||||
* - **URL will NOT be automatically formatted, `/v1` suffix will NOT be added**
|
||||
* - This is legacy behavior kept for backward compatibility
|
||||
*
|
||||
* 3. When called with `(provider)`:
|
||||
* - Directly uses the provider without requiring a model
|
||||
* - Used for operations that don't need a model (e.g., fetchModels)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Recommended: Auto-format URL
|
||||
* const ai = new ModernAiProvider(model)
|
||||
*
|
||||
* // Not recommended: Skip URL formatting (only for special cases)
|
||||
* const ai = new ModernAiProvider(model, customProvider)
|
||||
*
|
||||
* // For operations that don't need a model
|
||||
* const ai = new ModernAiProvider(provider)
|
||||
* ```
|
||||
*/
|
||||
constructor(model: Model, provider?: Provider)
|
||||
constructor(provider: Provider)
|
||||
constructor(modelOrProvider: Model | Provider, provider?: Provider)
|
||||
@@ -156,7 +189,7 @@ export default class ModernAiProvider {
|
||||
config: ModernAiProviderConfig
|
||||
): Promise<CompletionsResult> {
|
||||
// ai-gateway不是image/generation 端点,所以就先不走legacy了
|
||||
if (config.isImageGenerationEndpoint && config.provider!.id !== SystemProviderIds['ai-gateway']) {
|
||||
if (config.isImageGenerationEndpoint && this.getActualProvider().id !== SystemProviderIds['ai-gateway']) {
|
||||
// 使用 legacy 实现处理图像生成(支持图片编辑等高级功能)
|
||||
if (!config.uiMessages) {
|
||||
throw new Error('uiMessages is required for image generation endpoint')
|
||||
@@ -322,10 +355,10 @@ export default class ModernAiProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用现代化 AI SDK 的图像生成实现,支持流式输出
|
||||
* @deprecated 已改为使用 legacy 实现以支持图片编辑等高级功能
|
||||
*/
|
||||
// /**
|
||||
// * 使用现代化 AI SDK 的图像生成实现,支持流式输出
|
||||
// * @deprecated 已改为使用 legacy 实现以支持图片编辑等高级功能
|
||||
// */
|
||||
/*
|
||||
private async modernImageGeneration(
|
||||
model: ImageModel,
|
||||
|
||||
@@ -11,10 +11,8 @@ import {
|
||||
findTokenLimit,
|
||||
GEMINI_FLASH_MODEL_REGEX,
|
||||
getThinkModelType,
|
||||
isClaudeReasoningModel,
|
||||
isDeepSeekHybridInferenceModel,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGeminiReasoningModel,
|
||||
isGPT5SeriesModel,
|
||||
isGrokReasoningModel,
|
||||
isNotSupportSystemMessageModel,
|
||||
@@ -651,7 +649,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
logger.warn('No user message. Some providers may not support.')
|
||||
}
|
||||
|
||||
// poe 需要通过用户消息传递 reasoningEffort
|
||||
const reasoningEffort = this.getReasoningEffort(assistant, model)
|
||||
|
||||
const lastUserMsg = userMessages.findLast((m) => m.role === 'user')
|
||||
@@ -662,22 +659,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
|
||||
lastUserMsg.content = processPostsuffixQwen3Model(currentContent, qwenThinkModeEnabled)
|
||||
}
|
||||
if (this.provider.id === SystemProviderIds.poe) {
|
||||
// 如果以后 poe 支持 reasoning_effort 参数了,可以删掉这部分
|
||||
let suffix = ''
|
||||
if (isGPT5SeriesModel(model) && reasoningEffort.reasoning_effort) {
|
||||
suffix = ` --reasoning_effort ${reasoningEffort.reasoning_effort}`
|
||||
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
|
||||
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
|
||||
}
|
||||
// FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题
|
||||
// 临时解决方案是强制poe用string content,但是其实poe部分支持array
|
||||
if (typeof lastUserMsg.content === 'string') {
|
||||
lastUserMsg.content += suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 最终请求消息
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
|
||||
import { isGemini3Model, isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
|
||||
import type { MCPTool } from '@renderer/types'
|
||||
import { type Assistant, type Message, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
@@ -9,11 +9,13 @@ import type { LanguageModelMiddleware } from 'ai'
|
||||
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { getAiSdkProviderId } from '../provider/factory'
|
||||
import { isOpenRouterGeminiGenerateImageModel } from '../utils/image'
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware'
|
||||
import { openrouterReasoningMiddleware } from './openrouterReasoningMiddleware'
|
||||
import { qwenThinkingMiddleware } from './qwenThinkingMiddleware'
|
||||
import { skipGeminiThoughtSignatureMiddleware } from './skipGeminiThoughtSignatureMiddleware'
|
||||
import { toolChoiceMiddleware } from './toolChoiceMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
@@ -257,6 +259,15 @@ function addModelSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: Ai
|
||||
middleware: openrouterGenerateImageMiddleware()
|
||||
})
|
||||
}
|
||||
|
||||
if (isGemini3Model(config.model)) {
|
||||
const aiSdkId = getAiSdkProviderId(config.provider)
|
||||
builder.add({
|
||||
name: 'skip-gemini3-thought-signature',
|
||||
middleware: skipGeminiThoughtSignatureMiddleware(aiSdkId)
|
||||
})
|
||||
logger.debug('Added skip Gemini3 thought signature middleware')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
/**
|
||||
* skip Gemini Thought Signature Middleware
|
||||
* 由于多模型客户端请求的复杂性(可以中途切换其他模型),这里选择通过中间件方式添加跳过所有 Gemini3 思考签名
|
||||
* Due to the complexity of multi-model client requests (which can switch to other models mid-process),
|
||||
* it was decided to add a skip for all Gemini3 thinking signatures via middleware.
|
||||
* @param aiSdkId AI SDK Provider ID
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function skipGeminiThoughtSignatureMiddleware(aiSdkId: string): LanguageModelMiddleware {
|
||||
const MAGIC_STRING = 'skip_thought_signature_validator'
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
if (typeof message.content !== 'string') {
|
||||
for (const part of message.content) {
|
||||
const googleOptions = part?.providerOptions?.[aiSdkId]
|
||||
if (googleOptions?.thoughtSignature) {
|
||||
googleOptions.thoughtSignature = MAGIC_STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,10 @@ describe('messageConverter', () => {
|
||||
const result = await convertMessagesToSdkMessages([initialUser, assistant, finalUser], model)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Start editing' }]
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Here is the current preview' }]
|
||||
@@ -217,6 +221,7 @@ describe('messageConverter', () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'system', content: 'fileid://reference' },
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Use this document as inspiration' }] },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Generated previews ready' }]
|
||||
|
||||
@@ -194,20 +194,20 @@ async function convertMessageToAssistantModelMessage(
|
||||
* This function processes messages and transforms them into the format required by the SDK.
|
||||
* It handles special cases for vision models and image enhancement models.
|
||||
*
|
||||
* @param messages - Array of messages to convert. Must contain at least 2 messages when using image enhancement models.
|
||||
* @param messages - Array of messages to convert. Must contain at least 3 messages when using image enhancement models for special handling.
|
||||
* @param model - The model configuration that determines conversion behavior
|
||||
*
|
||||
* @returns A promise that resolves to an array of SDK-compatible model messages
|
||||
*
|
||||
* @remarks
|
||||
* For image enhancement models with 2+ messages:
|
||||
* - Expects the second-to-last message (index length-2) to be an assistant message containing image blocks
|
||||
* - Expects the last message (index length-1) to be a user message
|
||||
* - Extracts images from the assistant message and appends them to the user message content
|
||||
* - Returns only the last two processed messages [assistantSdkMessage, userSdkMessage]
|
||||
* For image enhancement models with 3+ messages:
|
||||
* - Examines the last 2 messages to find an assistant message containing image blocks
|
||||
* - If found, extracts images from the assistant message and appends them to the last user message content
|
||||
* - Returns all converted messages (not just the last two) with the images merged into the user message
|
||||
* - Typical pattern: [system?, assistant(image), user] -> [system?, assistant, user(image)]
|
||||
*
|
||||
* For other models:
|
||||
* - Returns all converted messages in order
|
||||
* - Returns all converted messages in order without special image handling
|
||||
*
|
||||
* The function automatically detects vision model capabilities and adjusts conversion accordingly.
|
||||
*/
|
||||
@@ -220,29 +220,25 @@ export async function convertMessagesToSdkMessages(messages: Message[], model: M
|
||||
sdkMessages.push(...(Array.isArray(sdkMessage) ? sdkMessage : [sdkMessage]))
|
||||
}
|
||||
// Special handling for image enhancement models
|
||||
// Only keep the last two messages and merge images into the user message
|
||||
// [system?, user, assistant, user]
|
||||
// Only merge images into the user message
|
||||
// [system?, assistant(image), user] -> [system?, assistant, user(image)]
|
||||
if (isImageEnhancementModel(model) && messages.length >= 3) {
|
||||
const needUpdatedMessages = messages.slice(-2)
|
||||
const needUpdatedSdkMessages = sdkMessages.slice(-2)
|
||||
const assistantMessage = needUpdatedMessages.filter((m) => m.role === 'assistant')[0]
|
||||
const assistantSdkMessage = needUpdatedSdkMessages.filter((m) => m.role === 'assistant')[0]
|
||||
const userSdkMessage = needUpdatedSdkMessages.filter((m) => m.role === 'user')[0]
|
||||
const systemSdkMessages = sdkMessages.filter((m) => m.role === 'system')
|
||||
const imageBlocks = findImageBlocks(assistantMessage)
|
||||
const imageParts = await convertImageBlockToImagePart(imageBlocks)
|
||||
const parts: Array<TextPart | ImagePart | FilePart> = []
|
||||
if (typeof userSdkMessage.content === 'string') {
|
||||
parts.push({ type: 'text', text: userSdkMessage.content })
|
||||
parts.push(...imageParts)
|
||||
userSdkMessage.content = parts
|
||||
} else {
|
||||
userSdkMessage.content.push(...imageParts)
|
||||
const assistantMessage = needUpdatedMessages.find((m) => m.role === 'assistant')
|
||||
const userSdkMessage = sdkMessages[sdkMessages.length - 1]
|
||||
|
||||
if (assistantMessage && userSdkMessage?.role === 'user') {
|
||||
const imageBlocks = findImageBlocks(assistantMessage)
|
||||
const imageParts = await convertImageBlockToImagePart(imageBlocks)
|
||||
|
||||
if (imageParts.length > 0) {
|
||||
if (typeof userSdkMessage.content === 'string') {
|
||||
userSdkMessage.content = [{ type: 'text', text: userSdkMessage.content }, ...imageParts]
|
||||
} else if (Array.isArray(userSdkMessage.content)) {
|
||||
userSdkMessage.content.push(...imageParts)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (systemSdkMessages.length > 0) {
|
||||
return [systemSdkMessages[0], assistantSdkMessage, userSdkMessage]
|
||||
}
|
||||
return [assistantSdkMessage, userSdkMessage]
|
||||
}
|
||||
|
||||
return sdkMessages
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* 处理温度、TopP、超时等基础参数的获取逻辑
|
||||
*/
|
||||
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import {
|
||||
isClaude45ReasoningModel,
|
||||
isClaudeReasoningModel,
|
||||
@@ -73,11 +72,19 @@ export function getTimeout(model: Model): number {
|
||||
|
||||
export function getMaxTokens(assistant: Assistant, model: Model): number | undefined {
|
||||
// NOTE: ai-sdk会把maxToken和budgetToken加起来
|
||||
let { maxTokens = DEFAULT_MAX_TOKENS } = getAssistantSettings(assistant)
|
||||
const assistantSettings = getAssistantSettings(assistant)
|
||||
const enabledMaxTokens = assistantSettings.enableMaxTokens ?? false
|
||||
let maxTokens = assistantSettings.maxTokens
|
||||
|
||||
// If user hasn't enabled enableMaxTokens, return undefined to let the API use its default value.
|
||||
// Note: Anthropic API requires max_tokens, but that's handled by the Anthropic client with a fallback.
|
||||
if (!enabledMaxTokens || maxTokens === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const provider = getProviderByModel(model)
|
||||
if (isSupportedThinkingTokenClaudeModel(model) && ['anthropic', 'aws-bedrock'].includes(provider.type)) {
|
||||
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
||||
const { reasoning_effort: reasoningEffort } = assistantSettings
|
||||
const budget = getAnthropicThinkingBudget(maxTokens, reasoningEffort, model.id)
|
||||
if (budget) {
|
||||
maxTokens -= budget
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function buildStreamTextParams(
|
||||
searchWithTime: store.getState().websearch.searchWithTime
|
||||
}
|
||||
|
||||
const providerOptions = buildProviderOptions(assistant, model, provider, {
|
||||
const { providerOptions, standardParams } = buildProviderOptions(assistant, model, provider, {
|
||||
enableReasoning,
|
||||
enableWebSearch,
|
||||
enableGenerateImage
|
||||
@@ -181,11 +181,16 @@ export async function buildStreamTextParams(
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
// Note: standardParams (topK, frequencyPenalty, presencePenalty, stopSequences, seed)
|
||||
// are extracted from custom parameters and passed directly to streamText()
|
||||
// instead of being placed in providerOptions
|
||||
const params: StreamTextParams = {
|
||||
messages: sdkMessages,
|
||||
maxOutputTokens: getMaxTokens(assistant, model),
|
||||
temperature: getTemperature(assistant, model),
|
||||
topP: getTopP(assistant, model),
|
||||
// Include AI SDK standard params extracted from custom parameters
|
||||
...standardParams,
|
||||
abortSignal: options.requestOptions?.signal,
|
||||
headers,
|
||||
providerOptions,
|
||||
|
||||
@@ -60,8 +60,12 @@ function tryResolveProviderId(identifier: string): ProviderId | null {
|
||||
export function getAiSdkProviderId(provider: Provider): string {
|
||||
// 1. 尝试解析provider.id
|
||||
const resolvedFromId = tryResolveProviderId(provider.id)
|
||||
if (isAzureOpenAIProvider(provider) && isAzureResponsesEndpoint(provider)) {
|
||||
return 'azure-responses'
|
||||
if (isAzureOpenAIProvider(provider)) {
|
||||
if (isAzureResponsesEndpoint(provider)) {
|
||||
return 'azure-responses'
|
||||
} else {
|
||||
return 'azure'
|
||||
}
|
||||
}
|
||||
if (resolvedFromId) {
|
||||
return resolvedFromId
|
||||
|
||||
@@ -91,6 +91,7 @@ function formatProviderApiHost(provider: Provider): Provider {
|
||||
|
||||
if (isAnthropicProvider(provider)) {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
// AI SDK needs /v1 in baseURL, Anthropic SDK will strip it in getSdkClient
|
||||
formatted.apiHost = formatApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
/**
|
||||
* extractAiSdkStandardParams Unit Tests
|
||||
* Tests for extracting AI SDK standard parameters from custom parameters
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { extractAiSdkStandardParams } from '../options'
|
||||
|
||||
// Mock logger to prevent errors
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock settings store
|
||||
vi.mock('@renderer/store/settings', () => ({
|
||||
default: (state = { settings: {} }) => state
|
||||
}))
|
||||
|
||||
// Mock hooks to prevent uuid errors
|
||||
vi.mock('@renderer/hooks/useSettings', () => ({
|
||||
getStoreSetting: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
// Mock uuid to prevent errors
|
||||
vi.mock('uuid', () => ({
|
||||
v4: vi.fn(() => 'test-uuid')
|
||||
}))
|
||||
|
||||
// Mock AssistantService to prevent uuid errors
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getDefaultAssistant: vi.fn(() => ({
|
||||
id: 'test-assistant',
|
||||
name: 'Test Assistant',
|
||||
settings: {}
|
||||
})),
|
||||
getDefaultTopic: vi.fn(() => ({
|
||||
id: 'test-topic',
|
||||
assistantId: 'test-assistant',
|
||||
createdAt: new Date().toISOString()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock provider service
|
||||
vi.mock('@renderer/services/ProviderService', () => ({
|
||||
getProviderById: vi.fn(() => ({
|
||||
id: 'test-provider',
|
||||
name: 'Test Provider'
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock config modules
|
||||
vi.mock('@renderer/config/models', () => ({
|
||||
isOpenAIModel: vi.fn(() => false),
|
||||
isQwenMTModel: vi.fn(() => false),
|
||||
isSupportFlexServiceTierModel: vi.fn(() => false),
|
||||
isSupportVerbosityModel: vi.fn(() => false),
|
||||
getModelSupportedVerbosity: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/config/translate', () => ({
|
||||
mapLanguageToQwenMTModel: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/provider', () => ({
|
||||
isSupportServiceTierProvider: vi.fn(() => false),
|
||||
isSupportVerbosityProvider: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
describe('extractAiSdkStandardParams', () => {
|
||||
describe('Positive cases - Standard parameters extraction', () => {
|
||||
it('should extract all AI SDK standard parameters', () => {
|
||||
const customParams = {
|
||||
maxOutputTokens: 1000,
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.3,
|
||||
stopSequences: ['STOP', 'END'],
|
||||
seed: 42
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
maxOutputTokens: 1000,
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.3,
|
||||
stopSequences: ['STOP', 'END'],
|
||||
seed: 42
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract single standard parameter', () => {
|
||||
const customParams = {
|
||||
temperature: 0.8
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.8
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract topK parameter', () => {
|
||||
const customParams = {
|
||||
topK: 50
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
topK: 50
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract frequencyPenalty parameter', () => {
|
||||
const customParams = {
|
||||
frequencyPenalty: 0.6
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
frequencyPenalty: 0.6
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract presencePenalty parameter', () => {
|
||||
const customParams = {
|
||||
presencePenalty: 0.4
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
presencePenalty: 0.4
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract stopSequences parameter', () => {
|
||||
const customParams = {
|
||||
stopSequences: ['HALT', 'TERMINATE']
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
stopSequences: ['HALT', 'TERMINATE']
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract seed parameter', () => {
|
||||
const customParams = {
|
||||
seed: 12345
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
seed: 12345
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract maxOutputTokens parameter', () => {
|
||||
const customParams = {
|
||||
maxOutputTokens: 2048
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
maxOutputTokens: 2048
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should extract topP parameter', () => {
|
||||
const customParams = {
|
||||
topP: 0.95
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
topP: 0.95
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Negative cases - Provider-specific parameters', () => {
|
||||
it('should place all non-standard parameters in providerParams', () => {
|
||||
const customParams = {
|
||||
customParam: 'value',
|
||||
anotherParam: 123,
|
||||
thirdParam: true
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customParam: 'value',
|
||||
anotherParam: 123,
|
||||
thirdParam: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should place single provider-specific parameter in providerParams', () => {
|
||||
const customParams = {
|
||||
reasoningEffort: 'high'
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
reasoningEffort: 'high'
|
||||
})
|
||||
})
|
||||
|
||||
it('should place model-specific parameter in providerParams', () => {
|
||||
const customParams = {
|
||||
thinking: { type: 'enabled', budgetTokens: 5000 }
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
thinking: { type: 'enabled', budgetTokens: 5000 }
|
||||
})
|
||||
})
|
||||
|
||||
it('should place serviceTier in providerParams', () => {
|
||||
const customParams = {
|
||||
serviceTier: 'auto'
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
serviceTier: 'auto'
|
||||
})
|
||||
})
|
||||
|
||||
it('should place textVerbosity in providerParams', () => {
|
||||
const customParams = {
|
||||
textVerbosity: 'high'
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
textVerbosity: 'high'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mixed parameters', () => {
|
||||
it('should correctly separate mixed standard and provider-specific parameters', () => {
|
||||
const customParams = {
|
||||
temperature: 0.7,
|
||||
topK: 40,
|
||||
customParam: 'custom_value',
|
||||
reasoningEffort: 'medium',
|
||||
frequencyPenalty: 0.5,
|
||||
seed: 999
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.7,
|
||||
topK: 40,
|
||||
frequencyPenalty: 0.5,
|
||||
seed: 999
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customParam: 'custom_value',
|
||||
reasoningEffort: 'medium'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle complex mixed parameters with nested objects', () => {
|
||||
const customParams = {
|
||||
topP: 0.9,
|
||||
presencePenalty: 0.3,
|
||||
thinking: { type: 'enabled', budgetTokens: 5000 },
|
||||
stopSequences: ['STOP'],
|
||||
serviceTier: 'auto',
|
||||
maxOutputTokens: 4096
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
topP: 0.9,
|
||||
presencePenalty: 0.3,
|
||||
stopSequences: ['STOP'],
|
||||
maxOutputTokens: 4096
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
thinking: { type: 'enabled', budgetTokens: 5000 },
|
||||
serviceTier: 'auto'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle all standard params with some provider params', () => {
|
||||
const customParams = {
|
||||
maxOutputTokens: 2000,
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
topK: 50,
|
||||
presencePenalty: 0.6,
|
||||
frequencyPenalty: 0.4,
|
||||
stopSequences: ['END', 'DONE'],
|
||||
seed: 777,
|
||||
customApiParam: 'value',
|
||||
anotherCustomParam: 123
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
maxOutputTokens: 2000,
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
topK: 50,
|
||||
presencePenalty: 0.6,
|
||||
frequencyPenalty: 0.4,
|
||||
stopSequences: ['END', 'DONE'],
|
||||
seed: 777
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customApiParam: 'value',
|
||||
anotherCustomParam: 123
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty object', () => {
|
||||
const customParams = {}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should handle zero values for numeric parameters', () => {
|
||||
const customParams = {
|
||||
temperature: 0,
|
||||
topK: 0,
|
||||
seed: 0
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0,
|
||||
topK: 0,
|
||||
seed: 0
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should handle negative values for numeric parameters', () => {
|
||||
const customParams = {
|
||||
presencePenalty: -0.5,
|
||||
frequencyPenalty: -0.3,
|
||||
seed: -1
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
presencePenalty: -0.5,
|
||||
frequencyPenalty: -0.3,
|
||||
seed: -1
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should handle empty arrays for stopSequences', () => {
|
||||
const customParams = {
|
||||
stopSequences: []
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
stopSequences: []
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should handle null values in mixed parameters', () => {
|
||||
const customParams = {
|
||||
temperature: 0.7,
|
||||
customNull: null,
|
||||
topK: 40
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.7,
|
||||
topK: 40
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customNull: null
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle undefined values in mixed parameters', () => {
|
||||
const customParams = {
|
||||
temperature: 0.7,
|
||||
customUndefined: undefined,
|
||||
topK: 40
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.7,
|
||||
topK: 40
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customUndefined: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle boolean values for standard parameters', () => {
|
||||
const customParams = {
|
||||
temperature: 0.7,
|
||||
customBoolean: false,
|
||||
topK: 40
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.7,
|
||||
topK: 40
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
customBoolean: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle very large numeric values', () => {
|
||||
const customParams = {
|
||||
maxOutputTokens: 999999,
|
||||
seed: 2147483647,
|
||||
topK: 10000
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
maxOutputTokens: 999999,
|
||||
seed: 2147483647,
|
||||
topK: 10000
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('should handle decimal values with high precision', () => {
|
||||
const customParams = {
|
||||
temperature: 0.123456789,
|
||||
topP: 0.987654321,
|
||||
presencePenalty: 0.111111111
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.123456789,
|
||||
topP: 0.987654321,
|
||||
presencePenalty: 0.111111111
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Case sensitivity', () => {
|
||||
it('should NOT extract parameters with incorrect case - uppercase first letter', () => {
|
||||
const customParams = {
|
||||
Temperature: 0.7,
|
||||
TopK: 40,
|
||||
FrequencyPenalty: 0.5
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
Temperature: 0.7,
|
||||
TopK: 40,
|
||||
FrequencyPenalty: 0.5
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT extract parameters with incorrect case - all uppercase', () => {
|
||||
const customParams = {
|
||||
TEMPERATURE: 0.7,
|
||||
TOPK: 40,
|
||||
SEED: 42
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
TEMPERATURE: 0.7,
|
||||
TOPK: 40,
|
||||
SEED: 42
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT extract parameters with incorrect case - all lowercase', () => {
|
||||
const customParams = {
|
||||
maxoutputtokens: 1000,
|
||||
frequencypenalty: 0.5,
|
||||
stopsequences: ['STOP']
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
maxoutputtokens: 1000,
|
||||
frequencypenalty: 0.5,
|
||||
stopsequences: ['STOP']
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly extract exact case match while rejecting incorrect case', () => {
|
||||
const customParams = {
|
||||
temperature: 0.7,
|
||||
Temperature: 0.8,
|
||||
TEMPERATURE: 0.9,
|
||||
topK: 40,
|
||||
TopK: 50
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
temperature: 0.7,
|
||||
topK: 40
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
Temperature: 0.8,
|
||||
TEMPERATURE: 0.9,
|
||||
TopK: 50
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parameter name variations', () => {
|
||||
it('should NOT extract similar but incorrect parameter names', () => {
|
||||
const customParams = {
|
||||
temp: 0.7, // should not match temperature
|
||||
top_k: 40, // should not match topK
|
||||
max_tokens: 1000, // should not match maxOutputTokens
|
||||
freq_penalty: 0.5 // should not match frequencyPenalty
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
temp: 0.7,
|
||||
top_k: 40,
|
||||
max_tokens: 1000,
|
||||
freq_penalty: 0.5
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT extract snake_case versions of standard parameters', () => {
|
||||
const customParams = {
|
||||
top_k: 40,
|
||||
top_p: 0.9,
|
||||
presence_penalty: 0.5,
|
||||
frequency_penalty: 0.3,
|
||||
stop_sequences: ['STOP'],
|
||||
max_output_tokens: 1000
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
top_k: 40,
|
||||
top_p: 0.9,
|
||||
presence_penalty: 0.5,
|
||||
frequency_penalty: 0.3,
|
||||
stop_sequences: ['STOP'],
|
||||
max_output_tokens: 1000
|
||||
})
|
||||
})
|
||||
|
||||
it('should extract exact camelCase parameters only', () => {
|
||||
const customParams = {
|
||||
topK: 40, // correct
|
||||
top_k: 50, // incorrect
|
||||
topP: 0.9, // correct
|
||||
top_p: 0.8, // incorrect
|
||||
frequencyPenalty: 0.5, // correct
|
||||
frequency_penalty: 0.4 // incorrect
|
||||
}
|
||||
|
||||
const result = extractAiSdkStandardParams(customParams)
|
||||
|
||||
expect(result.standardParams).toStrictEqual({
|
||||
topK: 40,
|
||||
topP: 0.9,
|
||||
frequencyPenalty: 0.5
|
||||
})
|
||||
expect(result.providerParams).toStrictEqual({
|
||||
top_k: 50,
|
||||
top_p: 0.8,
|
||||
frequency_penalty: 0.4
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -128,7 +128,20 @@ vi.mock('../reasoning', () => ({
|
||||
reasoningConfig: { type: 'enabled', budgetTokens: 5000 }
|
||||
})),
|
||||
getReasoningEffort: vi.fn(() => ({ reasoningEffort: 'medium' })),
|
||||
getCustomParameters: vi.fn(() => ({}))
|
||||
getCustomParameters: vi.fn(() => ({})),
|
||||
extractAiSdkStandardParams: vi.fn((customParams: Record<string, any>) => {
|
||||
const AI_SDK_STANDARD_PARAMS = ['topK', 'frequencyPenalty', 'presencePenalty', 'stopSequences', 'seed']
|
||||
const standardParams: Record<string, any> = {}
|
||||
const providerParams: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(customParams)) {
|
||||
if (AI_SDK_STANDARD_PARAMS.includes(key)) {
|
||||
standardParams[key] = value
|
||||
} else {
|
||||
providerParams[key] = value
|
||||
}
|
||||
}
|
||||
return { standardParams, providerParams }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../image', () => ({
|
||||
@@ -184,8 +197,9 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('openai')
|
||||
expect(result.openai).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('openai')
|
||||
expect(result.providerOptions.openai).toBeDefined()
|
||||
expect(result.standardParams).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include reasoning parameters when enabled', () => {
|
||||
@@ -195,8 +209,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.openai).toHaveProperty('reasoningEffort')
|
||||
expect(result.openai.reasoningEffort).toBe('medium')
|
||||
expect(result.providerOptions.openai).toHaveProperty('reasoningEffort')
|
||||
expect(result.providerOptions.openai.reasoningEffort).toBe('medium')
|
||||
})
|
||||
|
||||
it('should include service tier when supported', () => {
|
||||
@@ -211,8 +225,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.openai).toHaveProperty('serviceTier')
|
||||
expect(result.openai.serviceTier).toBe(OpenAIServiceTiers.auto)
|
||||
expect(result.providerOptions.openai).toHaveProperty('serviceTier')
|
||||
expect(result.providerOptions.openai.serviceTier).toBe(OpenAIServiceTiers.auto)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,8 +253,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('anthropic')
|
||||
expect(result.anthropic).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('anthropic')
|
||||
expect(result.providerOptions.anthropic).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include reasoning parameters when enabled', () => {
|
||||
@@ -250,8 +264,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.anthropic).toHaveProperty('thinking')
|
||||
expect(result.anthropic.thinking).toEqual({
|
||||
expect(result.providerOptions.anthropic).toHaveProperty('thinking')
|
||||
expect(result.providerOptions.anthropic.thinking).toEqual({
|
||||
type: 'enabled',
|
||||
budgetTokens: 5000
|
||||
})
|
||||
@@ -282,8 +296,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('google')
|
||||
expect(result.google).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('google')
|
||||
expect(result.providerOptions.google).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include reasoning parameters when enabled', () => {
|
||||
@@ -293,8 +307,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.google).toHaveProperty('thinkingConfig')
|
||||
expect(result.google.thinkingConfig).toEqual({
|
||||
expect(result.providerOptions.google).toHaveProperty('thinkingConfig')
|
||||
expect(result.providerOptions.google.thinkingConfig).toEqual({
|
||||
include_thoughts: true
|
||||
})
|
||||
})
|
||||
@@ -306,8 +320,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: true
|
||||
})
|
||||
|
||||
expect(result.google).toHaveProperty('responseModalities')
|
||||
expect(result.google.responseModalities).toEqual(['TEXT', 'IMAGE'])
|
||||
expect(result.providerOptions.google).toHaveProperty('responseModalities')
|
||||
expect(result.providerOptions.google.responseModalities).toEqual(['TEXT', 'IMAGE'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -335,8 +349,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('xai')
|
||||
expect(result.xai).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('xai')
|
||||
expect(result.providerOptions.xai).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include reasoning parameters when enabled', () => {
|
||||
@@ -346,8 +360,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.xai).toHaveProperty('reasoningEffort')
|
||||
expect(result.xai.reasoningEffort).toBe('high')
|
||||
expect(result.providerOptions.xai).toHaveProperty('reasoningEffort')
|
||||
expect(result.providerOptions.xai.reasoningEffort).toBe('high')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -374,8 +388,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('deepseek')
|
||||
expect(result.deepseek).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('deepseek')
|
||||
expect(result.providerOptions.deepseek).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -402,8 +416,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('openrouter')
|
||||
expect(result.openrouter).toBeDefined()
|
||||
expect(result.providerOptions).toHaveProperty('openrouter')
|
||||
expect(result.providerOptions.openrouter).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include web search parameters when enabled', () => {
|
||||
@@ -413,12 +427,12 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result.openrouter).toHaveProperty('enable_search')
|
||||
expect(result.providerOptions.openrouter).toHaveProperty('enable_search')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom parameters', () => {
|
||||
it('should merge custom parameters', async () => {
|
||||
it('should merge custom provider-specific parameters', async () => {
|
||||
const { getCustomParameters } = await import('../reasoning')
|
||||
|
||||
vi.mocked(getCustomParameters).mockReturnValue({
|
||||
@@ -443,10 +457,88 @@ describe('options utils', () => {
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.openai).toHaveProperty('custom_param')
|
||||
expect(result.openai.custom_param).toBe('custom_value')
|
||||
expect(result.openai).toHaveProperty('another_param')
|
||||
expect(result.openai.another_param).toBe(123)
|
||||
expect(result.providerOptions.openai).toHaveProperty('custom_param')
|
||||
expect(result.providerOptions.openai.custom_param).toBe('custom_value')
|
||||
expect(result.providerOptions.openai).toHaveProperty('another_param')
|
||||
expect(result.providerOptions.openai.another_param).toBe(123)
|
||||
})
|
||||
|
||||
it('should extract AI SDK standard params from custom parameters', async () => {
|
||||
const { getCustomParameters } = await import('../reasoning')
|
||||
|
||||
vi.mocked(getCustomParameters).mockReturnValue({
|
||||
topK: 5,
|
||||
frequencyPenalty: 0.5,
|
||||
presencePenalty: 0.3,
|
||||
seed: 42,
|
||||
custom_param: 'custom_value'
|
||||
})
|
||||
|
||||
const result = buildProviderOptions(
|
||||
mockAssistant,
|
||||
mockModel,
|
||||
{
|
||||
id: SystemProviderIds.gemini,
|
||||
name: 'Google',
|
||||
type: 'gemini',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://generativelanguage.googleapis.com'
|
||||
} as Provider,
|
||||
{
|
||||
enableReasoning: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
}
|
||||
)
|
||||
|
||||
// Standard params should be extracted and returned separately
|
||||
expect(result.standardParams).toEqual({
|
||||
topK: 5,
|
||||
frequencyPenalty: 0.5,
|
||||
presencePenalty: 0.3,
|
||||
seed: 42
|
||||
})
|
||||
|
||||
// Provider-specific params should still be in providerOptions
|
||||
expect(result.providerOptions.google).toHaveProperty('custom_param')
|
||||
expect(result.providerOptions.google.custom_param).toBe('custom_value')
|
||||
|
||||
// Standard params should NOT be in providerOptions
|
||||
expect(result.providerOptions.google).not.toHaveProperty('topK')
|
||||
expect(result.providerOptions.google).not.toHaveProperty('frequencyPenalty')
|
||||
expect(result.providerOptions.google).not.toHaveProperty('presencePenalty')
|
||||
expect(result.providerOptions.google).not.toHaveProperty('seed')
|
||||
})
|
||||
|
||||
it('should handle stopSequences in custom parameters', async () => {
|
||||
const { getCustomParameters } = await import('../reasoning')
|
||||
|
||||
vi.mocked(getCustomParameters).mockReturnValue({
|
||||
stopSequences: ['STOP', 'END'],
|
||||
custom_param: 'value'
|
||||
})
|
||||
|
||||
const result = buildProviderOptions(
|
||||
mockAssistant,
|
||||
mockModel,
|
||||
{
|
||||
id: SystemProviderIds.gemini,
|
||||
name: 'Google',
|
||||
type: 'gemini',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://generativelanguage.googleapis.com'
|
||||
} as Provider,
|
||||
{
|
||||
enableReasoning: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.standardParams).toEqual({
|
||||
stopSequences: ['STOP', 'END']
|
||||
})
|
||||
expect(result.providerOptions.google).not.toHaveProperty('stopSequences')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -474,8 +566,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: true
|
||||
})
|
||||
|
||||
expect(result.google).toHaveProperty('thinkingConfig')
|
||||
expect(result.google).toHaveProperty('responseModalities')
|
||||
expect(result.providerOptions.google).toHaveProperty('thinkingConfig')
|
||||
expect(result.providerOptions.google).toHaveProperty('responseModalities')
|
||||
})
|
||||
|
||||
it('should handle all capabilities enabled', () => {
|
||||
@@ -485,8 +577,8 @@ describe('options utils', () => {
|
||||
enableGenerateImage: true
|
||||
})
|
||||
|
||||
expect(result.google).toBeDefined()
|
||||
expect(Object.keys(result.google).length).toBeGreaterThan(0)
|
||||
expect(result.providerOptions.google).toBeDefined()
|
||||
expect(Object.keys(result.providerOptions.google).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -513,7 +605,7 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('google')
|
||||
expect(result.providerOptions).toHaveProperty('google')
|
||||
})
|
||||
|
||||
it('should map google-vertex-anthropic to anthropic', () => {
|
||||
@@ -538,7 +630,7 @@ describe('options utils', () => {
|
||||
enableGenerateImage: false
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('anthropic')
|
||||
expect(result.providerOptions).toHaveProperty('anthropic')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import type { Assistant, Model, ReasoningEffortOption } from '@renderer/types'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getReasoningEffort } from '../reasoning'
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store/settings', () => ({
|
||||
default: {},
|
||||
settingsSlice: {
|
||||
name: 'settings',
|
||||
reducer: vi.fn(),
|
||||
actions: {}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store/assistants', () => {
|
||||
const mockAssistantsSlice = {
|
||||
name: 'assistants',
|
||||
reducer: vi.fn((state = { entities: {}, ids: [] }) => state),
|
||||
actions: {
|
||||
updateTopicUpdatedAt: vi.fn(() => ({ type: 'UPDATE_TOPIC_UPDATED_AT' }))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
default: mockAssistantsSlice.reducer,
|
||||
updateTopicUpdatedAt: vi.fn(() => ({ type: 'UPDATE_TOPIC_UPDATED_AT' })),
|
||||
assistantsSlice: mockAssistantsSlice
|
||||
}
|
||||
})
|
||||
|
||||
// Mock provider service
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: (model: Model) => ({
|
||||
id: model.provider,
|
||||
name: 'Poe',
|
||||
type: 'openai'
|
||||
}),
|
||||
getAssistantSettings: (assistant: Assistant) => assistant.settings || {}
|
||||
}))
|
||||
|
||||
describe('Poe Provider Reasoning Support', () => {
|
||||
const createPoeModel = (id: string): Model => ({
|
||||
id,
|
||||
name: id,
|
||||
provider: SystemProviderIds.poe,
|
||||
group: 'poe'
|
||||
})
|
||||
|
||||
const createAssistant = (reasoning_effort?: ReasoningEffortOption, maxTokens?: number): Assistant => ({
|
||||
id: 'test-assistant',
|
||||
name: 'Test Assistant',
|
||||
emoji: '🤖',
|
||||
prompt: '',
|
||||
topics: [],
|
||||
messages: [],
|
||||
type: 'assistant',
|
||||
regularPhrases: [],
|
||||
settings: {
|
||||
reasoning_effort,
|
||||
maxTokens
|
||||
}
|
||||
})
|
||||
|
||||
describe('GPT-5 Series Models', () => {
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with low effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('low')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'low'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with medium effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with high effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('high')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'high'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should convert auto to medium for GPT-5 model in extra_body', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5.1 model', () => {
|
||||
const model = createPoeModel('gpt-5.1')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Claude Models', () => {
|
||||
it('should return thinking_budget in extra_body for Claude 3.7 Sonnet', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('medium', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should return thinking_budget in extra_body for Claude Sonnet 4', () => {
|
||||
const model = createPoeModel('claude-sonnet-4')
|
||||
const assistant = createAssistant('high', 8192)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
|
||||
it('should calculate thinking_budget based on effort ratio and maxTokens', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('low', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gemini Models', () => {
|
||||
it('should return thinking_budget in extra_body for Gemini 2.5 Flash', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
|
||||
it('should return thinking_budget in extra_body for Gemini 2.5 Pro', () => {
|
||||
const model = createPoeModel('gemini-2.5-pro')
|
||||
const assistant = createAssistant('high')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
})
|
||||
|
||||
it('should use -1 for auto effort', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBe(-1)
|
||||
})
|
||||
|
||||
it('should calculate thinking_budget for non-auto effort', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('low')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
})
|
||||
|
||||
describe('No Reasoning Effort', () => {
|
||||
it('should return empty object when reasoning_effort is not set', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant(undefined)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should return empty object when reasoning_effort is "none"', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('none')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Non-Reasoning Models', () => {
|
||||
it('should return empty object for non-reasoning models', () => {
|
||||
const model = createPoeModel('gpt-4')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases: Models Without Token Limit Configuration', () => {
|
||||
it('should return empty object for Claude models without token limit configuration', () => {
|
||||
const model = createPoeModel('claude-unknown-variant')
|
||||
const assistant = createAssistant('medium', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// Should return empty object when token limit is not found
|
||||
expect(result).toEqual({})
|
||||
expect(result.extra_body?.thinking_budget).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return empty object for unmatched Poe reasoning models', () => {
|
||||
// A hypothetical reasoning model that doesn't match GPT-5, Claude, or Gemini
|
||||
const model = createPoeModel('some-reasoning-model')
|
||||
// Make it appear as a reasoning model by giving it a name that won't match known categories
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// Should return empty object for unmatched models
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should fallback to -1 for Gemini models without token limit', () => {
|
||||
// Use a Gemini model variant that won't match any token limit pattern
|
||||
// The current regex patterns cover gemini-.*-flash.*$ and gemini-.*-pro.*$
|
||||
// so we need a model that matches isSupportedThinkingTokenGeminiModel but not THINKING_TOKEN_MAP
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// For 'auto' effort, should use -1
|
||||
expect(result.extra_body?.thinking_budget).toBe(-1)
|
||||
})
|
||||
|
||||
it('should enforce minimum 1024 token floor for Claude models', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
// Use very small maxTokens to test the minimum floor
|
||||
const assistant = createAssistant('low', 100)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
|
||||
it('should handle undefined maxTokens for Claude models', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('medium', undefined)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@renderer/config/models'
|
||||
import { mapLanguageToQwenMTModel } from '@renderer/config/translate'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
import {
|
||||
type Assistant,
|
||||
type GroqServiceTier,
|
||||
@@ -30,8 +31,8 @@ import {
|
||||
type Provider,
|
||||
type ServiceTier
|
||||
} from '@renderer/types'
|
||||
import type { OpenAIVerbosity } from '@renderer/types/aiCoreTypes'
|
||||
import { isSupportServiceTierProvider } from '@renderer/utils/provider'
|
||||
import { type AiSdkParam, isAiSdkParam, type OpenAIVerbosity } from '@renderer/types/aiCoreTypes'
|
||||
import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@renderer/utils/provider'
|
||||
import type { JSONValue } from 'ai'
|
||||
import { t } from 'i18next'
|
||||
|
||||
@@ -90,15 +91,56 @@ function getServiceTier<T extends Provider>(model: Model, provider: T): OpenAISe
|
||||
}
|
||||
}
|
||||
|
||||
function getVerbosity(): OpenAIVerbosity {
|
||||
function getVerbosity(model: Model): OpenAIVerbosity {
|
||||
if (!isSupportVerbosityModel(model) || !isSupportVerbosityProvider(getProviderById(model.provider)!)) {
|
||||
return undefined
|
||||
}
|
||||
const openAI = getStoreSetting('openAI')
|
||||
return openAI.verbosity
|
||||
|
||||
const userVerbosity = openAI.verbosity
|
||||
|
||||
if (userVerbosity) {
|
||||
const supportedVerbosity = getModelSupportedVerbosity(model)
|
||||
// Use user's verbosity if supported, otherwise use the first supported option
|
||||
const verbosity = supportedVerbosity.includes(userVerbosity) ? userVerbosity : supportedVerbosity[0]
|
||||
return verbosity
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract AI SDK standard parameters from custom parameters
|
||||
* These parameters should be passed directly to streamText() instead of providerOptions
|
||||
*/
|
||||
export function extractAiSdkStandardParams(customParams: Record<string, any>): {
|
||||
standardParams: Partial<Record<AiSdkParam, any>>
|
||||
providerParams: Record<string, any>
|
||||
} {
|
||||
const standardParams: Partial<Record<AiSdkParam, any>> = {}
|
||||
const providerParams: Record<string, any> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(customParams)) {
|
||||
if (isAiSdkParam(key)) {
|
||||
standardParams[key] = value
|
||||
} else {
|
||||
providerParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return { standardParams, providerParams }
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 AI SDK 的 providerOptions
|
||||
* 按 provider 类型分离,保持类型安全
|
||||
* 返回格式:{ 'providerId': providerOptions }
|
||||
* 返回格式:{
|
||||
* providerOptions: { 'providerId': providerOptions },
|
||||
* standardParams: { topK, frequencyPenalty, presencePenalty, stopSequences, seed }
|
||||
* }
|
||||
*
|
||||
* Custom parameters are split into two categories:
|
||||
* 1. AI SDK standard parameters (topK, frequencyPenalty, etc.) - returned separately to be passed to streamText()
|
||||
* 2. Provider-specific parameters - merged into providerOptions
|
||||
*/
|
||||
export function buildProviderOptions(
|
||||
assistant: Assistant,
|
||||
@@ -109,13 +151,16 @@ export function buildProviderOptions(
|
||||
enableWebSearch: boolean
|
||||
enableGenerateImage: boolean
|
||||
}
|
||||
): Record<string, Record<string, JSONValue>> {
|
||||
): {
|
||||
providerOptions: Record<string, Record<string, JSONValue>>
|
||||
standardParams: Partial<Record<AiSdkParam, any>>
|
||||
} {
|
||||
logger.debug('buildProviderOptions', { assistant, model, actualProvider, capabilities })
|
||||
const rawProviderId = getAiSdkProviderId(actualProvider)
|
||||
// 构建 provider 特定的选项
|
||||
let providerSpecificOptions: Record<string, any> = {}
|
||||
const serviceTier = getServiceTier(model, actualProvider)
|
||||
const textVerbosity = getVerbosity()
|
||||
const textVerbosity = getVerbosity(model)
|
||||
// 根据 provider 类型分离构建逻辑
|
||||
const { data: baseProviderId, success } = baseProviderIdSchema.safeParse(rawProviderId)
|
||||
if (success) {
|
||||
@@ -130,7 +175,8 @@ export function buildProviderOptions(
|
||||
assistant,
|
||||
model,
|
||||
capabilities,
|
||||
serviceTier
|
||||
serviceTier,
|
||||
textVerbosity
|
||||
)
|
||||
providerSpecificOptions = options
|
||||
}
|
||||
@@ -163,7 +209,8 @@ export function buildProviderOptions(
|
||||
model,
|
||||
capabilities,
|
||||
actualProvider,
|
||||
serviceTier
|
||||
serviceTier,
|
||||
textVerbosity
|
||||
)
|
||||
break
|
||||
default:
|
||||
@@ -201,10 +248,14 @@ export function buildProviderOptions(
|
||||
}
|
||||
}
|
||||
|
||||
// 合并自定义参数到 provider 特定的选项中
|
||||
// 获取自定义参数并分离标准参数和 provider 特定参数
|
||||
const customParams = getCustomParameters(assistant)
|
||||
const { standardParams, providerParams } = extractAiSdkStandardParams(customParams)
|
||||
|
||||
// 合并 provider 特定的自定义参数到 providerSpecificOptions
|
||||
providerSpecificOptions = {
|
||||
...providerSpecificOptions,
|
||||
...getCustomParameters(assistant)
|
||||
...providerParams
|
||||
}
|
||||
|
||||
let rawProviderKey =
|
||||
@@ -212,16 +263,21 @@ export function buildProviderOptions(
|
||||
'google-vertex': 'google',
|
||||
'google-vertex-anthropic': 'anthropic',
|
||||
'azure-anthropic': 'anthropic',
|
||||
'ai-gateway': 'gateway'
|
||||
'ai-gateway': 'gateway',
|
||||
azure: 'openai',
|
||||
'azure-responses': 'openai'
|
||||
}[rawProviderId] || rawProviderId
|
||||
|
||||
if (rawProviderKey === 'cherryin') {
|
||||
rawProviderKey = { gemini: 'google' }[actualProvider.type] || actualProvider.type
|
||||
rawProviderKey = { gemini: 'google', ['openai-response']: 'openai' }[actualProvider.type] || actualProvider.type
|
||||
}
|
||||
|
||||
// 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions }
|
||||
// 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions } 以及提取的标准参数
|
||||
return {
|
||||
[rawProviderKey]: providerSpecificOptions
|
||||
providerOptions: {
|
||||
[rawProviderKey]: providerSpecificOptions
|
||||
},
|
||||
standardParams
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +292,8 @@ function buildOpenAIProviderOptions(
|
||||
enableWebSearch: boolean
|
||||
enableGenerateImage: boolean
|
||||
},
|
||||
serviceTier: OpenAIServiceTier
|
||||
serviceTier: OpenAIServiceTier,
|
||||
textVerbosity?: OpenAIVerbosity
|
||||
): OpenAIResponsesProviderOptions {
|
||||
const { enableReasoning } = capabilities
|
||||
let providerOptions: OpenAIResponsesProviderOptions = {}
|
||||
@@ -248,8 +305,13 @@ function buildOpenAIProviderOptions(
|
||||
...reasoningParams
|
||||
}
|
||||
}
|
||||
const provider = getProviderById(model.provider)
|
||||
|
||||
if (isSupportVerbosityModel(model)) {
|
||||
if (!provider) {
|
||||
throw new Error(`Provider ${model.provider} not found`)
|
||||
}
|
||||
|
||||
if (isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider)) {
|
||||
const openAI = getStoreSetting<'openAI'>('openAI')
|
||||
const userVerbosity = openAI?.verbosity
|
||||
|
||||
@@ -267,7 +329,8 @@ function buildOpenAIProviderOptions(
|
||||
|
||||
providerOptions = {
|
||||
...providerOptions,
|
||||
serviceTier
|
||||
serviceTier,
|
||||
textVerbosity
|
||||
}
|
||||
|
||||
return providerOptions
|
||||
@@ -366,11 +429,13 @@ function buildCherryInProviderOptions(
|
||||
enableGenerateImage: boolean
|
||||
},
|
||||
actualProvider: Provider,
|
||||
serviceTier: OpenAIServiceTier
|
||||
serviceTier: OpenAIServiceTier,
|
||||
textVerbosity: OpenAIVerbosity
|
||||
): OpenAIResponsesProviderOptions | AnthropicProviderOptions | GoogleGenerativeAIProviderOptions {
|
||||
switch (actualProvider.type) {
|
||||
case 'openai':
|
||||
return buildOpenAIProviderOptions(assistant, model, capabilities, serviceTier)
|
||||
case 'openai-response':
|
||||
return buildOpenAIProviderOptions(assistant, model, capabilities, serviceTier, textVerbosity)
|
||||
|
||||
case 'anthropic':
|
||||
return buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
isDeepSeekHybridInferenceModel,
|
||||
isDoubaoSeedAfter251015,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGemini3Model,
|
||||
isGemini3ThinkingTokenModel,
|
||||
isGPT5SeriesModel,
|
||||
isGPT51SeriesModel,
|
||||
isGrok4FastReasoningModel,
|
||||
isGrokReasoningModel,
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
} from '@renderer/config/models'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import { getAssistantSettings, getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Assistant, Model, ReasoningEffortOption } from '@renderer/types'
|
||||
import type { Assistant, Model } from '@renderer/types'
|
||||
import { EFFORT_RATIO, isSystemProvider, SystemProviderIds } from '@renderer/types'
|
||||
import type { OpenAISummaryText } from '@renderer/types/aiCoreTypes'
|
||||
import type { ReasoningEffortOptionalParams } from '@renderer/types/sdk'
|
||||
@@ -142,6 +143,69 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
}
|
||||
|
||||
// reasoningEffort有效的情况
|
||||
// https://creator.poe.com/docs/external-applications/openai-compatible-api#additional-considerations
|
||||
// Poe provider - supports custom bot parameters via extra_body
|
||||
if (provider.id === SystemProviderIds.poe) {
|
||||
// GPT-5 series models use reasoning_effort parameter in extra_body
|
||||
if (isGPT5SeriesModel(model) || isGPT51SeriesModel(model)) {
|
||||
return {
|
||||
extra_body: {
|
||||
reasoning_effort: reasoningEffort === 'auto' ? 'medium' : reasoningEffort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Claude models use thinking_budget parameter in extra_body
|
||||
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
const tokenLimit = findTokenLimit(model.id)
|
||||
const maxTokens = assistant.settings?.maxTokens
|
||||
|
||||
if (!tokenLimit) {
|
||||
logger.warn(
|
||||
`No token limit configuration found for Claude model "${model.id}" on Poe provider. ` +
|
||||
`Reasoning effort setting "${reasoningEffort}" will not be applied.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
let budgetTokens = Math.floor((tokenLimit.max - tokenLimit.min) * effortRatio + tokenLimit.min)
|
||||
budgetTokens = Math.floor(Math.max(1024, Math.min(budgetTokens, (maxTokens || DEFAULT_MAX_TOKENS) * effortRatio)))
|
||||
|
||||
return {
|
||||
extra_body: {
|
||||
thinking_budget: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini models use thinking_budget parameter in extra_body
|
||||
if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
const tokenLimit = findTokenLimit(model.id)
|
||||
let budgetTokens: number | undefined
|
||||
if (tokenLimit && reasoningEffort !== 'auto') {
|
||||
budgetTokens = Math.floor((tokenLimit.max - tokenLimit.min) * effortRatio + tokenLimit.min)
|
||||
} else if (!tokenLimit && reasoningEffort !== 'auto') {
|
||||
logger.warn(
|
||||
`No token limit configuration found for Gemini model "${model.id}" on Poe provider. ` +
|
||||
`Using auto (-1) instead of requested effort "${reasoningEffort}".`
|
||||
)
|
||||
}
|
||||
return {
|
||||
extra_body: {
|
||||
thinking_budget: budgetTokens ?? -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poe reasoning model not in known categories (GPT-5, Claude, Gemini)
|
||||
logger.warn(
|
||||
`Poe provider reasoning model "${model.id}" does not match known categories ` +
|
||||
`(GPT-5, Claude, Gemini). Reasoning effort setting "${reasoningEffort}" will not be applied.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
// OpenRouter models
|
||||
if (model.provider === SystemProviderIds.openrouter) {
|
||||
@@ -281,7 +345,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
// gemini series, openai compatible api
|
||||
if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||
// https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#openai_compatibility
|
||||
if (isGemini3Model(model)) {
|
||||
if (isGemini3ThinkingTokenModel(model)) {
|
||||
return {
|
||||
reasoning_effort: reasoningEffort
|
||||
}
|
||||
@@ -465,20 +529,20 @@ export function getAnthropicReasoningParams(
|
||||
return {}
|
||||
}
|
||||
|
||||
type GoogelThinkingLevel = NonNullable<GoogleGenerativeAIProviderOptions['thinkingConfig']>['thinkingLevel']
|
||||
// type GoogleThinkingLevel = NonNullable<GoogleGenerativeAIProviderOptions['thinkingConfig']>['thinkingLevel']
|
||||
|
||||
function mapToGeminiThinkingLevel(reasoningEffort: ReasoningEffortOption): GoogelThinkingLevel {
|
||||
switch (reasoningEffort) {
|
||||
case 'low':
|
||||
return 'low'
|
||||
case 'medium':
|
||||
return 'medium'
|
||||
case 'high':
|
||||
return 'high'
|
||||
default:
|
||||
return 'medium'
|
||||
}
|
||||
}
|
||||
// function mapToGeminiThinkingLevel(reasoningEffort: ReasoningEffortOption): GoogelThinkingLevel {
|
||||
// switch (reasoningEffort) {
|
||||
// case 'low':
|
||||
// return 'low'
|
||||
// case 'medium':
|
||||
// return 'medium'
|
||||
// case 'high':
|
||||
// return 'high'
|
||||
// default:
|
||||
// return 'medium'
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取 Gemini 推理参数
|
||||
@@ -507,14 +571,15 @@ export function getGeminiReasoningParams(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 很多中转还不支持
|
||||
// https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#new_api_features_in_gemini_3
|
||||
if (isGemini3Model(model)) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
thinkingLevel: mapToGeminiThinkingLevel(reasoningEffort)
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (isGemini3ThinkingTokenModel(model)) {
|
||||
// return {
|
||||
// thinkingConfig: {
|
||||
// thinkingLevel: mapToGeminiThinkingLevel(reasoningEffort)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
interface SelectorOption<V = string | number | undefined | null> {
|
||||
interface SelectorOption<V = string | number> {
|
||||
label: string | ReactNode
|
||||
value: V
|
||||
type?: 'group'
|
||||
@@ -14,7 +14,7 @@ interface SelectorOption<V = string | number | undefined | null> {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface BaseSelectorProps<V = string | number | undefined | null> {
|
||||
interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
@@ -39,7 +39,7 @@ interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
|
||||
|
||||
export type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
|
||||
|
||||
const Selector = <V extends string | number | undefined | null>({
|
||||
const Selector = <V extends string | number>({
|
||||
options,
|
||||
value,
|
||||
onChange = () => {},
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
MODEL_SUPPORTED_OPTIONS,
|
||||
MODEL_SUPPORTED_REASONING_EFFORT
|
||||
} from '../reasoning'
|
||||
import { isGemini3ThinkingTokenModel } from '../utils'
|
||||
import { isTextToImageModel } from '../vision'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
@@ -799,20 +800,6 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Token limit lookup', () => {
|
||||
it.each([
|
||||
['gemini-2.5-flash-lite-latest', { min: 512, max: 24576 }],
|
||||
['qwen-plus-2025-07-14', { min: 0, max: 38912 }],
|
||||
['claude-haiku-4', { min: 1024, max: 64000 }]
|
||||
])('returns configured min/max pairs for %s', (id, expected) => {
|
||||
expect(findTokenLimit(id)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('returns undefined when regex misses', () => {
|
||||
expect(findTokenLimit('unknown-model')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gemini Models', () => {
|
||||
describe('isSupportedThinkingTokenGeminiModel', () => {
|
||||
it('should return true for gemini 2.5 models', () => {
|
||||
@@ -955,7 +942,7 @@ describe('Gemini Models', () => {
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3.0-flash-image-preview',
|
||||
@@ -963,7 +950,7 @@ describe('Gemini Models', () => {
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isSupportedThinkingTokenGeminiModel({
|
||||
id: 'gemini-3.5-pro-image-preview',
|
||||
@@ -971,7 +958,7 @@ describe('Gemini Models', () => {
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for gemini-2.x image models', () => {
|
||||
@@ -1163,7 +1150,7 @@ describe('Gemini Models', () => {
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGeminiReasoningModel({
|
||||
id: 'gemini-3.5-flash-image-preview',
|
||||
@@ -1171,7 +1158,7 @@ describe('Gemini Models', () => {
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for older gemini models without thinking', () => {
|
||||
@@ -1200,6 +1187,19 @@ describe('Gemini Models', () => {
|
||||
})
|
||||
|
||||
describe('findTokenLimit', () => {
|
||||
describe('General token limit lookup', () => {
|
||||
it.each([
|
||||
['gemini-2.5-flash-lite-latest', { min: 512, max: 24576 }],
|
||||
['qwen-plus-2025-07-14', { min: 0, max: 38912 }]
|
||||
])('returns configured min/max pairs for %s', (id, expected) => {
|
||||
expect(findTokenLimit(id)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('returns undefined when regex misses', () => {
|
||||
expect(findTokenLimit('unknown-model')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
const cases: Array<{ modelId: string; expected: { min: number; max: number } }> = [
|
||||
{ modelId: 'gemini-2.5-flash-lite-exp', expected: { min: 512, max: 24_576 } },
|
||||
{ modelId: 'gemini-1.5-flash', expected: { min: 0, max: 24_576 } },
|
||||
@@ -1215,11 +1215,7 @@ describe('findTokenLimit', () => {
|
||||
{ modelId: 'qwen-plus-ultra', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen-turbo-pro', expected: { min: 0, max: 38_912 } },
|
||||
{ modelId: 'qwen-flash-lite', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } },
|
||||
{ modelId: 'claude-3.7-sonnet-extended', expected: { min: 1_024, max: 64_000 } },
|
||||
{ modelId: 'claude-sonnet-4.1', expected: { min: 1_024, max: 64_000 } },
|
||||
{ modelId: 'claude-sonnet-4-5-20250929', expected: { min: 1_024, max: 64_000 } },
|
||||
{ modelId: 'claude-opus-4-1-extended', expected: { min: 1_024, max: 32_000 } }
|
||||
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } }
|
||||
]
|
||||
|
||||
it.each(cases)('returns correct limits for $modelId', ({ modelId, expected }) => {
|
||||
@@ -1229,4 +1225,355 @@ describe('findTokenLimit', () => {
|
||||
it('returns undefined for unknown models', () => {
|
||||
expect(findTokenLimit('unknown-model')).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('Claude models', () => {
|
||||
describe('Claude 3.7 Sonnet models', () => {
|
||||
it.each([
|
||||
'claude-3.7-sonnet',
|
||||
'claude-3-7-sonnet',
|
||||
'claude-3.7-sonnet-latest',
|
||||
'claude-3-7-sonnet-latest',
|
||||
'claude-3.7-sonnet-20250201',
|
||||
'claude-3-7-sonnet-20250201',
|
||||
// Official Claude API IDs
|
||||
'claude-3-7-sonnet-20250219',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-3-7-sonnet-20250219-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-3-7-sonnet@20250219'
|
||||
])('should return { min: 1024, max: 64000 } for %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it.each(['CLAUDE-3.7-SONNET', 'Claude-3-7-Sonnet-Latest'])('should be case insensitive for %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Claude 4.0 series models', () => {
|
||||
it.each([
|
||||
'claude-sonnet-4',
|
||||
'claude-sonnet-4.0',
|
||||
'claude-sonnet-4-0',
|
||||
'claude-sonnet-4-preview',
|
||||
'claude-sonnet-4.0-preview',
|
||||
'claude-sonnet-4-20250101',
|
||||
// Official Claude API IDs
|
||||
'claude-sonnet-4-20250514',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-sonnet-4@20250514'
|
||||
])('should return { min: 1024, max: 64000 } for Sonnet variant %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'claude-opus-4',
|
||||
'claude-opus-4.0',
|
||||
'claude-opus-4-0',
|
||||
'claude-opus-4-preview',
|
||||
'claude-opus-4.0-preview',
|
||||
'claude-opus-4-20250101',
|
||||
// Official Claude API IDs
|
||||
'claude-opus-4-20250514',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-opus-4-20250514-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-opus-4@20250514'
|
||||
])('should return { min: 1024, max: 32000 } for Opus variant %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
|
||||
})
|
||||
|
||||
it.each(['CLAUDE-SONNET-4', 'Claude-Opus-4-Preview'])('should be case insensitive for %s', (modelId) => {
|
||||
const expectedSonnet = { min: 1024, max: 64_000 }
|
||||
const expectedOpus = { min: 1024, max: 32_000 }
|
||||
const result = findTokenLimit(modelId)
|
||||
expect(result).toBeDefined()
|
||||
expect([expectedSonnet, expectedOpus]).toContainEqual(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Claude Opus 4.1 models', () => {
|
||||
it.each([
|
||||
'claude-opus-4.1',
|
||||
'claude-opus-4-1',
|
||||
'claude-opus-4.1-preview',
|
||||
'claude-opus-4-1-preview',
|
||||
'claude-opus-4.1-20250120',
|
||||
'claude-opus-4-1-20250120',
|
||||
// Official Claude API IDs
|
||||
'claude-opus-4-1-20250805',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-opus-4-1@20250805'
|
||||
])('should return { min: 1024, max: 32000 } for %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
|
||||
})
|
||||
|
||||
it.each(['CLAUDE-OPUS-4.1', 'Claude-Opus-4-1-Preview'])('should be case insensitive for %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 32_000 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Claude 4.5 series models (Haiku, Sonnet, Opus)', () => {
|
||||
it.each([
|
||||
'claude-haiku-4.5',
|
||||
'claude-haiku-4-5',
|
||||
'claude-haiku-4.5-preview',
|
||||
'claude-haiku-4-5-preview',
|
||||
'claude-haiku-4.5-20250929',
|
||||
'claude-haiku-4-5-20250929',
|
||||
// Official Claude API IDs
|
||||
'claude-haiku-4-5-20251001',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-haiku-4-5-20251001-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-haiku-4-5@20251001'
|
||||
])('should return { min: 1024, max: 64000 } for Haiku variant %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'claude-sonnet-4.5',
|
||||
'claude-sonnet-4-5',
|
||||
'claude-sonnet-4.5-preview',
|
||||
'claude-sonnet-4-5-preview',
|
||||
'claude-sonnet-4.5-20250929',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
// Official Claude API IDs
|
||||
'claude-sonnet-4-5-20250929',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-sonnet-4-5@20250929'
|
||||
])('should return { min: 1024, max: 64000 } for Sonnet variant %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'claude-opus-4.5',
|
||||
'claude-opus-4-5',
|
||||
'claude-opus-4.5-preview',
|
||||
'claude-opus-4-5-preview',
|
||||
'claude-opus-4.5-20250929',
|
||||
'claude-opus-4-5-20250929',
|
||||
// Official Claude API IDs
|
||||
'claude-opus-4-5-20251101',
|
||||
// AWS Bedrock format
|
||||
'anthropic.claude-opus-4-5-20251101-v1:0',
|
||||
// GCP Vertex AI format
|
||||
'claude-opus-4-5@20251101'
|
||||
])('should return { min: 1024, max: 64000 } for Opus variant %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it.each(['CLAUDE-HAIKU-4.5', 'Claude-Sonnet-4-5-Preview', 'CLAUDE-OPUS-4.5-20250929'])(
|
||||
'should be case insensitive for %s',
|
||||
(modelId) => {
|
||||
expect(findTokenLimit(modelId)).toEqual({ min: 1024, max: 64_000 })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('Claude models that should NOT match', () => {
|
||||
it.each([
|
||||
'claude-3-opus',
|
||||
'claude-3-sonnet',
|
||||
'claude-3-haiku',
|
||||
'claude-3.5-sonnet',
|
||||
'claude-3-5-sonnet',
|
||||
'claude-2.1',
|
||||
'claude-instant',
|
||||
'claude-haiku-4',
|
||||
'claude-haiku-4.0',
|
||||
'claude-haiku-4-0',
|
||||
'claude-opus-4.2',
|
||||
'claude-opus-4-2',
|
||||
'claude-sonnet-4.2',
|
||||
'claude-sonnet-4-2',
|
||||
// Old Haiku models (no Extended thinking support)
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-5-haiku-latest',
|
||||
'anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
'claude-3-5-haiku@20241022',
|
||||
'claude-3-haiku-20240307',
|
||||
'anthropic.claude-3-haiku-20240307-v1:0',
|
||||
'claude-3-haiku@20240307'
|
||||
])('should return undefined for older/unsupported model %s', (modelId) => {
|
||||
expect(findTokenLimit(modelId)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle models with custom suffixes', () => {
|
||||
expect(findTokenLimit('claude-3.7-sonnet-custom-variant')).toEqual({ min: 1024, max: 64_000 })
|
||||
expect(findTokenLimit('claude-opus-4.1-custom')).toEqual({ min: 1024, max: 32_000 })
|
||||
expect(findTokenLimit('claude-sonnet-4.5-custom-variant')).toEqual({ min: 1024, max: 64_000 })
|
||||
})
|
||||
|
||||
it('should NOT match non-existent Claude 4.1 variants (only Opus 4.1 exists)', () => {
|
||||
// Claude Sonnet 4.1 and Haiku 4.1 do not exist
|
||||
expect(findTokenLimit('claude-sonnet-4.1')).toBeUndefined()
|
||||
expect(findTokenLimit('claude-haiku-4.1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not match partial model names', () => {
|
||||
expect(findTokenLimit('claude-3.7')).toBeUndefined()
|
||||
expect(findTokenLimit('claude-opus')).toBeUndefined()
|
||||
expect(findTokenLimit('claude-4.5')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGemini3ThinkingTokenModel', () => {
|
||||
it('should return true for Gemini 3 non-image models', () => {
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3-flash',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3-pro',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3-pro-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'google/gemini-3-flash',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3.0-flash',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3.5-pro-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for Gemini 3 image models', () => {
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3-flash-image',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3-pro-image-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3.0-flash-image-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-3.5-pro-image-preview',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for non-Gemini 3 models', () => {
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-2.5-flash',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gemini-1.5-pro',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'gpt-4',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'claude-3-opus',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle case insensitivity', () => {
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'Gemini-3-Flash',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'GEMINI-3-PRO',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isGemini3ThinkingTokenModel({
|
||||
id: 'Gemini-3-Pro-Image',
|
||||
name: '',
|
||||
provider: '',
|
||||
group: ''
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
isGemmaModel,
|
||||
isGenerateImageModels,
|
||||
isMaxTemperatureOneModel,
|
||||
isNotSupportedTextDelta,
|
||||
isNotSupportSystemMessageModel,
|
||||
isNotSupportTemperatureAndTopP,
|
||||
isNotSupportTextDeltaModel,
|
||||
isSupportedFlexServiceTier,
|
||||
isSupportedModel,
|
||||
isSupportFlexServiceTierModel,
|
||||
@@ -215,12 +215,51 @@ describe('model utils', () => {
|
||||
|
||||
it('aggregates boolean helpers based on regex rules', () => {
|
||||
expect(isAnthropicModel(createModel({ id: 'claude-3.5' }))).toBe(true)
|
||||
expect(isQwenMTModel(createModel({ id: 'qwen-mt-large' }))).toBe(true)
|
||||
expect(isNotSupportedTextDelta(createModel({ id: 'qwen-mt-large' }))).toBe(true)
|
||||
expect(isQwenMTModel(createModel({ id: 'qwen-mt-plus' }))).toBe(true)
|
||||
expect(isNotSupportSystemMessageModel(createModel({ id: 'gemma-moe' }))).toBe(true)
|
||||
expect(isOpenAIOpenWeightModel(createModel({ id: 'gpt-oss-free' }))).toBe(true)
|
||||
})
|
||||
|
||||
describe('isNotSupportedTextDelta', () => {
|
||||
it('returns true for qwen-mt-turbo and qwen-mt-plus models', () => {
|
||||
// qwen-mt series that don't support text delta
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-mt-turbo' }))).toBe(true)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-mt-plus' }))).toBe(true)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'Qwen-MT-Turbo' }))).toBe(true)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'QWEN-MT-PLUS' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for qwen-mt-flash and other models', () => {
|
||||
// qwen-mt-flash supports text delta
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-mt-flash' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'Qwen-MT-Flash' }))).toBe(false)
|
||||
|
||||
// Legacy qwen models without mt prefix (support text delta)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-turbo' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-plus' }))).toBe(false)
|
||||
|
||||
// Other qwen models
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-max' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen2.5-72b' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-vl-plus' }))).toBe(false)
|
||||
|
||||
// Non-qwen models
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'gpt-4o' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'claude-3.5' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'glm-4-plus' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('handles models with version suffixes', () => {
|
||||
// qwen-mt models with version suffixes
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-mt-turbo-1201' }))).toBe(true)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-mt-plus-0828' }))).toBe(true)
|
||||
|
||||
// Legacy qwen models with version suffixes (support text delta)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-turbo-0828' }))).toBe(false)
|
||||
expect(isNotSupportTextDeltaModel(createModel({ id: 'qwen-plus-latest' }))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('evaluates GPT-5 family helpers', () => {
|
||||
expect(isGPT5SeriesModel(createModel({ id: 'gpt-5-preview' }))).toBe(true)
|
||||
expect(isGPT5SeriesModel(createModel({ id: 'gpt-5.1-preview' }))).toBe(false)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
isOpenAIReasoningModel,
|
||||
isSupportedReasoningEffortOpenAIModel
|
||||
} from './openai'
|
||||
import { GEMINI_FLASH_MODEL_REGEX, isGemini3Model } from './utils'
|
||||
import { GEMINI_FLASH_MODEL_REGEX, isGemini3ThinkingTokenModel } from './utils'
|
||||
import { isTextToImageModel } from './vision'
|
||||
|
||||
// Reasoning models
|
||||
@@ -115,7 +115,7 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
|
||||
} else {
|
||||
thinkingModelType = 'gemini_pro'
|
||||
}
|
||||
if (isGemini3Model(model)) {
|
||||
if (isGemini3ThinkingTokenModel(model)) {
|
||||
thinkingModelType = 'gemini3'
|
||||
}
|
||||
} else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok'
|
||||
@@ -201,7 +201,7 @@ export function isSupportedReasoningEffortGrokModel(model?: Model): boolean {
|
||||
}
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
const providerId = model.provider.toLowerCase()
|
||||
const providerId = model?.provider?.toLowerCase()
|
||||
if (modelId.includes('grok-3-mini')) {
|
||||
return true
|
||||
}
|
||||
@@ -271,14 +271,6 @@ export const GEMINI_THINKING_MODEL_REGEX =
|
||||
export const isSupportedThinkingTokenGeminiModel = (model: Model): boolean => {
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
if (GEMINI_THINKING_MODEL_REGEX.test(modelId)) {
|
||||
// gemini-3.x 的 image 模型支持思考模式
|
||||
if (isGemini3Model(model)) {
|
||||
if (modelId.includes('tts')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// gemini-2.x 的 image/tts 模型不支持
|
||||
if (modelId.includes('image') || modelId.includes('tts')) {
|
||||
return false
|
||||
}
|
||||
@@ -555,7 +547,7 @@ export function isReasoningModel(model?: Model): boolean {
|
||||
return REASONING_REGEX.test(modelId) || false
|
||||
}
|
||||
|
||||
export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
// Gemini models
|
||||
'gemini-2\\.5-flash-lite.*$': { min: 512, max: 24576 },
|
||||
'gemini-.*-flash.*$': { min: 0, max: 24576 },
|
||||
@@ -576,10 +568,18 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
||||
'qwen-flash.*$': { min: 0, max: 81_920 },
|
||||
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
||||
|
||||
// Claude models
|
||||
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64_000 },
|
||||
'claude-(:?haiku|sonnet)-4.*$': { min: 1024, max: 64_000 },
|
||||
'claude-opus-4-1.*$': { min: 1024, max: 32_000 }
|
||||
// Claude models (supports AWS Bedrock 'anthropic.' prefix, GCP Vertex AI '@' separator, and '-v1:0' suffix)
|
||||
'(?:anthropic\\.)?claude-3[.-]7.*sonnet.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
|
||||
'(?:anthropic\\.)?claude-(:?haiku|sonnet|opus)-4[.-]5.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
|
||||
'(?:anthropic\\.)?claude-opus-4[.-]1.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 32_000 },
|
||||
'(?:anthropic\\.)?claude-sonnet-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
|
||||
min: 1024,
|
||||
max: 64_000
|
||||
},
|
||||
'(?:anthropic\\.)?claude-opus-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
|
||||
min: 1024,
|
||||
max: 32_000
|
||||
}
|
||||
}
|
||||
|
||||
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
|
||||
|
||||
@@ -43,7 +43,8 @@ const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
||||
'gpt-5-chat(?:-[\\w-]+)?',
|
||||
'glm-4\\.5v',
|
||||
'gemini-2.5-flash-image(?:-[\\w-]+)?',
|
||||
'gemini-2.0-flash-preview-image-generation'
|
||||
'gemini-2.0-flash-preview-image-generation',
|
||||
'gemini-3(?:\\.\\d+)?-pro-image(?:-[\\w-]+)?'
|
||||
]
|
||||
|
||||
export const FUNCTION_CALLING_REGEX = new RegExp(
|
||||
|
||||
@@ -19,6 +19,7 @@ export function isSupportFlexServiceTierModel(model: Model): boolean {
|
||||
(modelId.includes('o3') && !modelId.includes('o3-mini')) || modelId.includes('o4-mini') || modelId.includes('gpt-5')
|
||||
)
|
||||
}
|
||||
|
||||
export function isSupportedFlexServiceTier(model: Model): boolean {
|
||||
return isSupportFlexServiceTierModel(model)
|
||||
}
|
||||
@@ -111,8 +112,11 @@ export const isAnthropicModel = (model?: Model): boolean => {
|
||||
return modelId.startsWith('claude')
|
||||
}
|
||||
|
||||
export const isNotSupportedTextDelta = (model: Model): boolean => {
|
||||
return isQwenMTModel(model)
|
||||
const NOT_SUPPORT_TEXT_DELTA_MODEL_REGEX = new RegExp('qwen-mt-(?:turbo|plus)')
|
||||
|
||||
export const isNotSupportTextDeltaModel = (model: Model): boolean => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return NOT_SUPPORT_TEXT_DELTA_MODEL_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
export const isNotSupportSystemMessageModel = (model: Model): boolean => {
|
||||
@@ -160,3 +164,8 @@ export const isGemini3Model = (model: Model) => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return modelId.includes('gemini-3')
|
||||
}
|
||||
|
||||
export const isGemini3ThinkingTokenModel = (model: Model) => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return isGemini3Model(model) && !modelId.includes('image')
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
anthropicApiHost: 'https://api.siliconflow.cn',
|
||||
models: SYSTEM_MODELS.silicon,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@@ -168,6 +169,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://www.dmxapi.cn',
|
||||
anthropicApiHost: 'https://www.dmxapi.cn',
|
||||
models: SYSTEM_MODELS.dmxapi,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
||||
"go_to_settings": "Go to settings",
|
||||
"i_know": "I know",
|
||||
"ignore": "Ignore",
|
||||
"inspect": "Inspect",
|
||||
"invalid_value": "Invalid Value",
|
||||
"knowledge_base": "Knowledge Base",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Enter additional information or context for this knowledge base...",
|
||||
"provider_not_found": "Provider not found",
|
||||
"quota": "{{name}} Left Quota: {{quota}}",
|
||||
"quota_empty": "Today's {{name}} quota exhausted, please apply on the official website",
|
||||
"quota_infinity": "{{name}} Quota: Unlimited",
|
||||
"rename": "Rename",
|
||||
"search": "Search knowledge base",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "View WebDAV settings"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq Settings"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "Disabling hardware acceleration requires restarting the app to take effect. Do you want to restart now?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "Does the provider support the stream_options parameter?",
|
||||
"label": "Support stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Whether the provider supports the verbosity parameter",
|
||||
"label": "Support verbosity"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "已进入全屏模式,按 F11 退出",
|
||||
"go_to_settings": "前往设置",
|
||||
"i_know": "我知道了",
|
||||
"ignore": "忽略",
|
||||
"inspect": "检查",
|
||||
"invalid_value": "无效值",
|
||||
"knowledge_base": "知识库",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "输入此知识库的附加信息或上下文...",
|
||||
"provider_not_found": "未找到服务商",
|
||||
"quota": "{{name}} 剩余额度:{{quota}}",
|
||||
"quota_empty": "今日{{name}}额度不足,请前往官网申请",
|
||||
"quota_infinity": "{{name}} 剩余额度:无限制",
|
||||
"rename": "重命名",
|
||||
"search": "搜索知识库",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "查看 WebDAV 设置"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq 设置"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "禁用硬件加速需要重启应用才能生效,是否现在重启?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "该提供商是否支持 stream_options 参数",
|
||||
"label": "支持 stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "该提供商是否支持 verbosity 参数",
|
||||
"label": "支持 verbosity"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "已進入全螢幕模式,按 F11 結束",
|
||||
"go_to_settings": "前往設定",
|
||||
"i_know": "我知道了",
|
||||
"ignore": "忽略",
|
||||
"inspect": "檢查",
|
||||
"invalid_value": "無效值",
|
||||
"knowledge_base": "知識庫",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "輸入此知識庫的附加資訊或上下文...",
|
||||
"provider_not_found": "未找到服務商",
|
||||
"quota": "{{name}} 剩餘配額:{{quota}}",
|
||||
"quota_empty": "今日{{name}}額度不足,請前往官網申請",
|
||||
"quota_infinity": "{{name}} 配額:無限制",
|
||||
"rename": "重新命名",
|
||||
"search": "搜尋知識庫",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "檢視 WebDAV 設定"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq 設定"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "禁用硬件加速需要重新啟動應用程序才能生效。是否立即重新啟動?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "該提供商是否支援 stream_options 參數",
|
||||
"label": "支援 stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "提供者是否支援詳細程度參數",
|
||||
"label": "支援詳細資訊"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Vollbildmodus aktiviert, F11 zum Beenden",
|
||||
"go_to_settings": "Zu Einstellungen",
|
||||
"i_know": "Verstanden",
|
||||
"ignore": "Ignorieren",
|
||||
"inspect": "Prüfen",
|
||||
"invalid_value": "Ungültiger Wert",
|
||||
"knowledge_base": "Wissensdatenbank",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Zusätzliche Informationen oder Kontext für diese Wissensdatenbank eingeben...",
|
||||
"provider_not_found": "Anbieter nicht gefunden",
|
||||
"quota": "{{name}} verbleibendes Kontingent: {{quota}}",
|
||||
"quota_empty": "Das heutige {{name}}-Kontingent ist erschöpft. Bitte beantragen Sie es auf der offiziellen Website",
|
||||
"quota_infinity": "{{name}} verbleibendes Kontingent: unbegrenzt",
|
||||
"rename": "Umbenennen",
|
||||
"search": "Wissensdatenbank durchsuchen",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "WebDAV-Einstellungen anzeigen"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq Einstellungen"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "Deaktivierung der Hardwarebeschleunigung erfordert Neustart. Jetzt neu starten?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "Unterstützt stream_options",
|
||||
"label": "Unterstützt stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Ob der Anbieter den Ausführlichkeitsparameter unterstützt",
|
||||
"label": "Unterstützung der Ausführlichkeit"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Εισήχθη σε πλήρη οθόνη, πατήστε F11 για να έξω",
|
||||
"go_to_settings": "Πηγαίνετε στις ρυθμίσεις",
|
||||
"i_know": "Το έχω καταλάβει",
|
||||
"ignore": "Αγνόησε",
|
||||
"inspect": "Επιθεώρηση",
|
||||
"invalid_value": "Μη έγκυρη τιμή",
|
||||
"knowledge_base": "Βάση Γνώσεων",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Εισάγετε πρόσθετες πληροφορίες ή πληροφορίες προσδιορισμού για αυτή τη βάση γνώσεων...",
|
||||
"provider_not_found": "Η παροχή υπηρεσιών μοντέλου βάσης γνώσεων χαθηκε, αυτή η βάση γνώσεων δεν θα υποστηρίζεται πλέον, παρακαλείστε να δημιουργήσετε ξανά μια βάση γνώσεων",
|
||||
"quota": "Διαθέσιμο όριο για {{name}}: {{quota}}",
|
||||
"quota_empty": "Το σημερινό όριο {{name}} εξαντλήθηκε, παρακαλούμε υποβάλετε αίτηση στην επίσημη ιστοσελίδα",
|
||||
"quota_infinity": "Διαθέσιμο όριο για {{name}}: Απεριόριστο",
|
||||
"rename": "Μετονομασία",
|
||||
"search": "Αναζήτηση βάσης γνώσεων",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "Προβολή ρυθμίσεων WebDAV"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Ρυθμίσεις Groq"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "Η απενεργοποίηση της υλικοποιημένης επιτάχυνσης απαιτεί επανεκκίνηση της εφαρμογής για να τεθεί σε ισχύ. Θέλετε να επανεκκινήσετε τώρα;",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "Υποστηρίζει ο πάροχος την παράμετρο stream_options;",
|
||||
"label": "Υποστήριξη stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Αν ο πάροχος υποστηρίζει την παράμετρο αναλυτικότητας",
|
||||
"label": "Υποστήριξη πολυλογίας"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "En modo pantalla completa, presione F11 para salir",
|
||||
"go_to_settings": "Ir a la configuración",
|
||||
"i_know": "Entendido",
|
||||
"ignore": "Ignorar",
|
||||
"inspect": "Inspeccionar",
|
||||
"invalid_value": "Valor inválido",
|
||||
"knowledge_base": "Base de conocimiento",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Ingrese información adicional o contexto para esta base de conocimientos...",
|
||||
"provider_not_found": "El proveedor del modelo de la base de conocimientos ha sido perdido, esta base de conocimientos ya no es compatible, por favor cree una nueva base de conocimientos",
|
||||
"quota": "Cupo restante de {{name}}: {{quota}}",
|
||||
"quota_empty": "La cuota de {{name}} de hoy está agotada, por favor solicítela en el sitio web oficial",
|
||||
"quota_infinity": "Cupo restante de {{name}}: ilimitado",
|
||||
"rename": "Renombrar",
|
||||
"search": "Buscar en la base de conocimientos",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "Ver configuración WebDAV"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configuración de Groq"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "La desactivación de la aceleración por hardware requiere reiniciar la aplicación para que surta efecto, ¿desea reiniciar ahora?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "¿Admite el proveedor el parámetro stream_options?",
|
||||
"label": "Admite stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Si el proveedor admite el parámetro de verbosidad",
|
||||
"label": "Soporte de verbosidad"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Mode plein écran, appuyez sur F11 pour quitter",
|
||||
"go_to_settings": "Aller aux paramètres",
|
||||
"i_know": "J'ai compris",
|
||||
"ignore": "Ignorer",
|
||||
"inspect": "Vérifier",
|
||||
"invalid_value": "valeur invalide",
|
||||
"knowledge_base": "Base de connaissances",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Entrez des informations supplémentaires ou un contexte pour cette base de connaissances...",
|
||||
"provider_not_found": "Le fournisseur du modèle de la base de connaissances a été perdu, cette base de connaissances ne sera plus supportée, veuillez en créer une nouvelle",
|
||||
"quota": "Quota restant pour {{name}} : {{quota}}",
|
||||
"quota_empty": "Le quota {{name}} d'aujourd'hui est épuisé, veuillez en faire la demande sur le site officiel",
|
||||
"quota_infinity": "Quota restant pour {{name}} : illimité",
|
||||
"rename": "Renommer",
|
||||
"search": "Rechercher dans la base de connaissances",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "Voir les paramètres WebDAV"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Paramètres Groq"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "La désactivation de l'accélération matérielle nécessite un redémarrage de l'application pour prendre effet. Voulez-vous redémarrer maintenant ?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "Le fournisseur prend-il en charge le paramètre stream_options ?",
|
||||
"label": "Prise en charge des options de flux"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Si le fournisseur prend en charge le paramètre de verbosité",
|
||||
"label": "Prend en charge la verbosité"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -893,7 +893,7 @@
|
||||
"title": "コード実行"
|
||||
},
|
||||
"code_fancy_block": {
|
||||
"label": "<translate_input>\n装飾的なコードブロック\n</translate_input>",
|
||||
"label": "装飾的なコードブロック",
|
||||
"tip": "より見栄えの良いコードブロックスタイルを使用する、例えばHTMLカード"
|
||||
},
|
||||
"code_image_tools": {
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "全画面モードに入りました。F11キーで終了します",
|
||||
"go_to_settings": "設定に移動",
|
||||
"i_know": "わかりました",
|
||||
"ignore": "無視",
|
||||
"inspect": "検査",
|
||||
"invalid_value": "無効な値",
|
||||
"knowledge_base": "ナレッジベース",
|
||||
@@ -1296,7 +1297,7 @@
|
||||
"statusCode": "ステータスコード",
|
||||
"statusText": "状態テキスト",
|
||||
"text": "テキスト",
|
||||
"toolInput": "<translate_input>\nツール入力\n</translate_input>",
|
||||
"toolInput": "ツール入力",
|
||||
"toolName": "ツール名",
|
||||
"unknown": "不明なエラー",
|
||||
"usage": "用量",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "このナレッジベースの追加情報やコンテキストを入力...",
|
||||
"provider_not_found": "プロバイダーが見つかりません",
|
||||
"quota": "{{name}} 残りクォータ: {{quota}}",
|
||||
"quota_empty": "本日の{{name}}クォータが不足しています。公式サイトで申請してください",
|
||||
"quota_infinity": "{{name}} クォータ: 無制限",
|
||||
"rename": "名前を変更",
|
||||
"search": "ナレッジベースを検索",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "WebDAV設定を表示"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq設定"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "ハードウェアアクセラレーションを無効にするには、アプリを再起動する必要があります。再起動しますか?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "このプロバイダーは stream_options パラメータをサポートしていますか",
|
||||
"label": "stream_options をサポート"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "プロバイダーが冗長度パラメータをサポートしているかどうか",
|
||||
"label": "冗長性のサポート"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Entrou no modo de tela cheia, pressione F11 para sair",
|
||||
"go_to_settings": "Ir para configurações",
|
||||
"i_know": "Entendi",
|
||||
"ignore": "Pular",
|
||||
"inspect": "Verificar",
|
||||
"invalid_value": "Valor inválido",
|
||||
"knowledge_base": "Base de Conhecimento",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Digite informações adicionais ou contexto para este repositório de conhecimento...",
|
||||
"provider_not_found": "O provedor do modelo do repositório de conhecimento foi perdido, este repositório de conhecimento não será mais suportado, por favor, crie um novo repositório de conhecimento",
|
||||
"quota": "Cota restante de {{name}}: {{quota}}",
|
||||
"quota_empty": "A cota de {{name}} de hoje está esgotada, por favor solicite no site oficial",
|
||||
"quota_infinity": "Cota restante de {{name}}: ilimitada",
|
||||
"rename": "Renomear",
|
||||
"search": "Pesquisar repositório de conhecimento",
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "Ver configurações WebDAV"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configurações do Groq"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "A desativação da aceleração de hardware requer a reinicialização do aplicativo para entrar em vigor. Deseja reiniciar agora?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "O fornecedor suporta o parâmetro stream_options?",
|
||||
"label": "suporta stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Se o provedor suporta o parâmetro de verbosidade",
|
||||
"label": "Suportar verbosidade"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода",
|
||||
"go_to_settings": "Перейти в настройки",
|
||||
"i_know": "Я понял",
|
||||
"ignore": "Игнорировать",
|
||||
"inspect": "Осмотреть",
|
||||
"invalid_value": "недопустимое значение",
|
||||
"knowledge_base": "База знаний",
|
||||
@@ -1504,6 +1505,7 @@
|
||||
"notes_placeholder": "Введите дополнительную информацию или контекст для этой базы знаний...",
|
||||
"provider_not_found": "Поставщик не найден",
|
||||
"quota": "{{name}} Остаток квоты: {{quota}}",
|
||||
"quota_empty": "Сегодняшняя квота {{name}} исчерпана, пожалуйста, подайте заявку на официальном сайте",
|
||||
"quota_infinity": "{{name}} Квота: Не ограничена",
|
||||
"rename": "Переименовать",
|
||||
"search": "Поиск в базе знаний",
|
||||
@@ -1513,7 +1515,7 @@
|
||||
"preprocessing_tooltip": "Предварительная обработка документов",
|
||||
"title": "Настройки базы знаний"
|
||||
},
|
||||
"sitemap_added": "添加成功",
|
||||
"sitemap_added": "Карта сайта добавлена",
|
||||
"sitemap_placeholder": "Введите URL карты сайта",
|
||||
"sitemaps": "Сайты",
|
||||
"source": "Источник",
|
||||
@@ -2173,12 +2175,12 @@
|
||||
"font_size": "Размер шрифта",
|
||||
"font_size_description": "Отрегулируйте размер шрифта для лучшего чтения (10–30 пикселей)",
|
||||
"font_size_large": "Большой",
|
||||
"font_size_medium": "中",
|
||||
"font_size_small": "<translate_input>\nмаленький\n</translate_input>",
|
||||
"font_size_medium": "Средний",
|
||||
"font_size_small": "маленький",
|
||||
"font_title": "Настройки шрифта",
|
||||
"serif_font": "Serif Font",
|
||||
"show_table_of_contents": "Показать оглавление",
|
||||
"show_table_of_contents_description": "显示目录大纲侧边栏,方便文档内导航",
|
||||
"show_table_of_contents_description": "Показать боковую панель оглавления для удобной навигации по документу",
|
||||
"title": "показывать"
|
||||
},
|
||||
"editor": {
|
||||
@@ -3771,6 +3773,9 @@
|
||||
},
|
||||
"view_webdav_settings": "Просмотр настроек WebDAV"
|
||||
},
|
||||
"groq": {
|
||||
"title": "Настройки Groq"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"confirm": {
|
||||
"content": "Отключение аппаратного ускорения требует перезапуска приложения для вступления в силу. Перезапустить приложение?",
|
||||
@@ -4357,6 +4362,10 @@
|
||||
"stream_options": {
|
||||
"help": "Поддерживает ли этот провайдер параметр stream_options",
|
||||
"label": "Поддержка stream_options"
|
||||
},
|
||||
"verbosity": {
|
||||
"help": "Поддерживает ли провайдер параметр verbosity",
|
||||
"label": "Поддержка многословности"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
@@ -4767,7 +4776,7 @@
|
||||
}
|
||||
},
|
||||
"prompt": "Следуйте системному запросу",
|
||||
"title": "翻译设置"
|
||||
"title": "Настройки перевода"
|
||||
},
|
||||
"tray": {
|
||||
"onclose": "Свернуть в трей при закрытии",
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { EndpointType, Model } from '@renderer/types'
|
||||
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
|
||||
import type { TerminalConfig } from '@shared/config/constant'
|
||||
import { codeTools, terminalApps } from '@shared/config/constant'
|
||||
import { isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
|
||||
import { Alert, Checkbox, Input, Popover, Select, Space } from 'antd'
|
||||
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
@@ -82,6 +83,10 @@ const CodeToolsPage: FC = () => {
|
||||
if (m.supported_endpoint_types) {
|
||||
return m.supported_endpoint_types.includes('anthropic')
|
||||
}
|
||||
// Special handling for silicon provider: only specific models support Anthropic API
|
||||
if (m.provider === 'silicon') {
|
||||
return isSiliconAnthropicCompatibleModel(m.id)
|
||||
}
|
||||
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EndpointType, Model, Provider } from '@renderer/types'
|
||||
import { type EndpointType, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
|
||||
export interface LaunchValidationResult {
|
||||
@@ -25,7 +25,17 @@ export const CLI_TOOLS = [
|
||||
]
|
||||
|
||||
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', 'cherryin']
|
||||
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope']
|
||||
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = [
|
||||
'deepseek',
|
||||
'moonshot',
|
||||
'zhipu',
|
||||
'dashscope',
|
||||
'modelscope',
|
||||
'minimax',
|
||||
'longcat',
|
||||
SystemProviderIds.qiniu,
|
||||
SystemProviderIds.silicon
|
||||
]
|
||||
export const CLAUDE_SUPPORTED_PROVIDERS = [
|
||||
'aihubmix',
|
||||
'dmxapi',
|
||||
@@ -79,6 +89,11 @@ export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
|
||||
anthropic: {
|
||||
api_base_url: 'https://api-inference.modelscope.cn'
|
||||
}
|
||||
},
|
||||
minimax: {
|
||||
anthropic: {
|
||||
api_base_url: 'https://api.minimaxi.com/anthropic'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +140,8 @@ export const generateToolEnvironment = ({
|
||||
|
||||
switch (tool) {
|
||||
case codeTools.claudeCode:
|
||||
env.ANTHROPIC_BASE_URL = getCodeToolsApiBaseUrl(model, 'anthropic') || modelProvider.apiHost
|
||||
env.ANTHROPIC_BASE_URL =
|
||||
getCodeToolsApiBaseUrl(model, 'anthropic') || modelProvider.anthropicApiHost || modelProvider.apiHost
|
||||
env.ANTHROPIC_MODEL = model.id
|
||||
if (modelProvider.type === 'anthropic') {
|
||||
env.ANTHROPIC_API_KEY = apiKey
|
||||
|
||||
+22
-23
@@ -4,6 +4,7 @@ import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from
|
||||
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||
import { QuickPanelReservedSymbol } from '@renderer/components/QuickPanel'
|
||||
import {
|
||||
isFunctionCallingModel,
|
||||
isGeminiModel,
|
||||
isGPT5SeriesReasoningModel,
|
||||
isOpenAIWebSearchModel,
|
||||
@@ -18,6 +19,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import type { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
|
||||
import { hasObjectKey } from '@renderer/utils'
|
||||
import { isToolUseModeFunction } from '@renderer/utils/assistant'
|
||||
import { isPromptToolUse } from '@renderer/utils/mcp-tools'
|
||||
import { isGeminiWebSearchProvider } from '@renderer/utils/provider'
|
||||
import { Globe } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
@@ -126,20 +128,25 @@ export const useWebSearchPanelController = (assistantId: string, quickPanelContr
|
||||
|
||||
const providerItems = useMemo<QuickPanelListItem[]>(() => {
|
||||
const isWebSearchModelEnabled = assistant.model && isWebSearchModel(assistant.model)
|
||||
const items: QuickPanelListItem[] = providers
|
||||
.map((p) => ({
|
||||
label: p.name,
|
||||
description: WebSearchService.isWebSearchEnabled(p.id)
|
||||
? hasObjectKey(p, 'apiKey')
|
||||
? t('settings.tool.websearch.apikey')
|
||||
: t('settings.tool.websearch.free')
|
||||
: t('chat.input.web_search.enable_content'),
|
||||
icon: <WebSearchProviderIcon size={13} pid={p.id} />,
|
||||
isSelected: p.id === assistant?.webSearchProviderId,
|
||||
disabled: !WebSearchService.isWebSearchEnabled(p.id),
|
||||
action: () => updateQuickPanelItem(p.id)
|
||||
}))
|
||||
.filter((item) => !item.disabled)
|
||||
const items: QuickPanelListItem[] = []
|
||||
if (isFunctionCallingModel(assistant.model) || isPromptToolUse(assistant)) {
|
||||
items.push(
|
||||
...providers
|
||||
.map((p) => ({
|
||||
label: p.name,
|
||||
description: WebSearchService.isWebSearchEnabled(p.id)
|
||||
? hasObjectKey(p, 'apiKey')
|
||||
? t('settings.tool.websearch.apikey')
|
||||
: t('settings.tool.websearch.free')
|
||||
: t('chat.input.web_search.enable_content'),
|
||||
icon: <WebSearchProviderIcon size={13} pid={p.id} />,
|
||||
isSelected: p.id === assistant?.webSearchProviderId,
|
||||
disabled: !WebSearchService.isWebSearchEnabled(p.id),
|
||||
action: () => updateQuickPanelItem(p.id)
|
||||
}))
|
||||
.filter((item) => !item.disabled)
|
||||
)
|
||||
}
|
||||
|
||||
if (isWebSearchModelEnabled) {
|
||||
items.unshift({
|
||||
@@ -155,15 +162,7 @@ export const useWebSearchPanelController = (assistantId: string, quickPanelContr
|
||||
}
|
||||
|
||||
return items
|
||||
}, [
|
||||
assistant.enableWebSearch,
|
||||
assistant.model,
|
||||
assistant?.webSearchProviderId,
|
||||
providers,
|
||||
t,
|
||||
updateQuickPanelItem,
|
||||
updateToModelBuiltinWebSearch
|
||||
])
|
||||
}, [assistant, providers, t, updateQuickPanelItem, updateToModelBuiltinWebSearch])
|
||||
|
||||
const openQuickPanel = useCallback(() => {
|
||||
quickPanelController.open({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isMandatoryWebSearchModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { isMandatoryWebSearchModel } from '@renderer/config/models'
|
||||
import { defineTool, registerTool, TopicType } from '@renderer/pages/home/Inputbar/types'
|
||||
|
||||
import WebSearchButton from './components/WebSearchButton'
|
||||
@@ -15,7 +15,7 @@ const webSearchTool = defineTool({
|
||||
label: (t) => t('chat.input.web_search.label'),
|
||||
|
||||
visibleInScopes: [TopicType.Chat],
|
||||
condition: ({ model }) => isWebSearchModel(model) && !isMandatoryWebSearchModel(model),
|
||||
condition: ({ model }) => !isMandatoryWebSearchModel(model),
|
||||
|
||||
render: function WebSearchToolRender(context) {
|
||||
const { assistant, quickPanelController } = context
|
||||
|
||||
@@ -31,8 +31,10 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types'
|
||||
import { isGroqSystemProvider } from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
|
||||
import { isSupportServiceTierProvider } from '@renderer/utils/provider'
|
||||
import type { MultiModelMessageStyle, SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Col, InputNumber, Row, Slider } from 'antd'
|
||||
@@ -42,6 +44,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GroqSettingsGroup from './components/GroqSettingsGroup'
|
||||
import OpenAISettingsGroup from './components/OpenAISettingsGroup'
|
||||
|
||||
// Type definition for select items
|
||||
@@ -239,7 +242,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
|
||||
const model = assistant.model || getDefaultModel()
|
||||
|
||||
const isOpenAI = isOpenAIModel(model)
|
||||
const showOpenAiSettings = isOpenAIModel(model) || isSupportServiceTierProvider(provider)
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
@@ -390,7 +393,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</SettingGroup>
|
||||
</CollapsibleSettingGroup>
|
||||
)}
|
||||
{isOpenAI && (
|
||||
{showOpenAiSettings && (
|
||||
<OpenAISettingsGroup
|
||||
model={model}
|
||||
providerId={provider.id}
|
||||
@@ -398,6 +401,9 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
SettingRowTitleSmall={SettingRowTitleSmall}
|
||||
/>
|
||||
)}
|
||||
{isGroqSystemProvider(provider) && (
|
||||
<GroqSettingsGroup SettingGroup={SettingGroup} SettingRowTitleSmall={SettingRowTitleSmall} />
|
||||
)}
|
||||
<CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
|
||||
<SettingGroup>
|
||||
<SettingRow>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { SettingDivider, SettingRow } from '@renderer/pages/settings'
|
||||
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
|
||||
import type { GroqServiceTier, ServiceTier } from '@renderer/types'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import { toOptionValue, toRealValue } from '@renderer/utils/select'
|
||||
import { Tooltip } from 'antd'
|
||||
import { CircleHelp } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ServiceTierOptions = { value: NonNullable<GroqServiceTier> | 'undefined'; label: string }
|
||||
|
||||
interface Props {
|
||||
SettingGroup: FC<{ children: React.ReactNode }>
|
||||
SettingRowTitleSmall: FC<{ children: React.ReactNode }>
|
||||
}
|
||||
|
||||
const GroqSettingsGroup: FC<Props> = ({ SettingGroup, SettingRowTitleSmall }) => {
|
||||
const { t } = useTranslation()
|
||||
const { provider, updateProvider } = useProvider(SystemProviderIds.groq)
|
||||
const serviceTierMode = provider.serviceTier
|
||||
|
||||
const setServiceTierMode = useCallback(
|
||||
(value: ServiceTier) => {
|
||||
updateProvider({ serviceTier: value })
|
||||
},
|
||||
[updateProvider]
|
||||
)
|
||||
|
||||
const serviceTierOptions = useMemo(() => {
|
||||
const options = [
|
||||
{
|
||||
value: 'undefined',
|
||||
label: t('common.ignore')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.openai.service_tier.auto')
|
||||
},
|
||||
{
|
||||
value: 'on_demand',
|
||||
label: t('settings.openai.service_tier.on_demand')
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
label: t('settings.openai.service_tier.flex')
|
||||
}
|
||||
] as const satisfies ServiceTierOptions[]
|
||||
return options
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<CollapsibleSettingGroup title={t('settings.groq.title')} defaultExpanded={true}>
|
||||
<SettingGroup>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>
|
||||
{t('settings.openai.service_tier.title')}{' '}
|
||||
<Tooltip title={t('settings.openai.service_tier.tip')}>
|
||||
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
|
||||
</Tooltip>
|
||||
</SettingRowTitleSmall>
|
||||
<Selector
|
||||
value={toOptionValue(serviceTierMode)}
|
||||
onChange={(value) => {
|
||||
setServiceTierMode(toRealValue(value))
|
||||
}}
|
||||
options={serviceTierOptions}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingDivider />
|
||||
</CollapsibleSettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroqSettingsGroup
|
||||
@@ -12,29 +12,27 @@ import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setOpenAISummaryText, setOpenAIVerbosity } from '@renderer/store/settings'
|
||||
import type { GroqServiceTier, Model, OpenAIServiceTier, ServiceTier } from '@renderer/types'
|
||||
import { GroqServiceTiers, OpenAIServiceTiers, SystemProviderIds } from '@renderer/types'
|
||||
import type { Model, OpenAIServiceTier, ServiceTier } from '@renderer/types'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import type { OpenAISummaryText, OpenAIVerbosity } from '@renderer/types/aiCoreTypes'
|
||||
import { isSupportServiceTierProvider } from '@renderer/utils/provider'
|
||||
import { isSupportServiceTierProvider, isSupportVerbosityProvider } from '@renderer/utils/provider'
|
||||
import { toOptionValue, toRealValue } from '@renderer/utils/select'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
type VerbosityOption = {
|
||||
value: OpenAIVerbosity
|
||||
value: NonNullable<OpenAIVerbosity> | 'undefined'
|
||||
label: string
|
||||
}
|
||||
|
||||
type SummaryTextOption = {
|
||||
value: OpenAISummaryText
|
||||
value: NonNullable<OpenAISummaryText> | 'undefined'
|
||||
label: string
|
||||
}
|
||||
|
||||
type OpenAIServiceTierOption = { value: OpenAIServiceTier; label: string }
|
||||
type GroqServiceTierOption = { value: GroqServiceTier; label: string }
|
||||
|
||||
type ServiceTierOptions = OpenAIServiceTierOption[] | GroqServiceTierOption[]
|
||||
type OpenAIServiceTierOption = { value: NonNullable<OpenAIServiceTier> | 'null' | 'undefined'; label: string }
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
@@ -51,13 +49,14 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
const serviceTierMode = provider.serviceTier
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isOpenAIReasoning =
|
||||
const showSummarySetting =
|
||||
isSupportedReasoningEffortOpenAIModel(model) &&
|
||||
!model.id.includes('o1-pro') &&
|
||||
(provider.type === 'openai-response' || provider.id === 'aihubmix')
|
||||
const isSupportVerbosity = isSupportVerbosityModel(model)
|
||||
(provider.type === 'openai-response' || model.endpoint_type === 'openai-response' || provider.id === 'aihubmix')
|
||||
const showVerbositySetting = isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider)
|
||||
const isSupportFlexServiceTier = isSupportFlexServiceTierModel(model)
|
||||
const isSupportServiceTier = isSupportServiceTierProvider(provider)
|
||||
const isSupportedFlexServiceTier = isSupportFlexServiceTierModel(model)
|
||||
const showServiceTierSetting = isSupportServiceTier && providerId !== SystemProviderIds.groq
|
||||
|
||||
const setSummaryText = useCallback(
|
||||
(value: OpenAISummaryText) => {
|
||||
@@ -82,8 +81,8 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
|
||||
const summaryTextOptions = [
|
||||
{
|
||||
value: undefined,
|
||||
label: t('common.default')
|
||||
value: 'undefined',
|
||||
label: t('common.ignore')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
@@ -102,8 +101,8 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
const verbosityOptions = useMemo(() => {
|
||||
const allOptions = [
|
||||
{
|
||||
value: undefined,
|
||||
label: t('common.default')
|
||||
value: 'undefined',
|
||||
label: t('common.ignore')
|
||||
},
|
||||
{
|
||||
value: 'low',
|
||||
@@ -118,73 +117,44 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
label: t('settings.openai.verbosity.high')
|
||||
}
|
||||
] as const satisfies VerbosityOption[]
|
||||
const supportedVerbosityLevels = getModelSupportedVerbosity(model)
|
||||
const supportedVerbosityLevels = getModelSupportedVerbosity(model).map((v) => toOptionValue(v))
|
||||
return allOptions.filter((option) => supportedVerbosityLevels.includes(option.value))
|
||||
}, [model, t])
|
||||
|
||||
const serviceTierOptions = useMemo(() => {
|
||||
let options: ServiceTierOptions
|
||||
if (provider.id === SystemProviderIds.groq) {
|
||||
options = [
|
||||
{
|
||||
value: null,
|
||||
label: t('common.off')
|
||||
},
|
||||
{
|
||||
value: undefined,
|
||||
label: t('common.default')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.openai.service_tier.auto')
|
||||
},
|
||||
{
|
||||
value: 'on_demand',
|
||||
label: t('settings.openai.service_tier.on_demand')
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
label: t('settings.openai.service_tier.flex')
|
||||
}
|
||||
] as const satisfies GroqServiceTierOption[]
|
||||
} else {
|
||||
// 其他情况默认是和 OpenAI 相同
|
||||
options = [
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.openai.service_tier.auto')
|
||||
},
|
||||
{
|
||||
value: 'default',
|
||||
label: t('settings.openai.service_tier.default')
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
label: t('settings.openai.service_tier.flex')
|
||||
},
|
||||
{
|
||||
value: 'priority',
|
||||
label: t('settings.openai.service_tier.priority')
|
||||
}
|
||||
] as const satisfies OpenAIServiceTierOption[]
|
||||
}
|
||||
const options = [
|
||||
{
|
||||
value: 'undefined',
|
||||
label: t('common.ignore')
|
||||
},
|
||||
{
|
||||
value: 'null',
|
||||
label: t('common.off')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.openai.service_tier.auto')
|
||||
},
|
||||
{
|
||||
value: 'default',
|
||||
label: t('settings.openai.service_tier.default')
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
label: t('settings.openai.service_tier.flex')
|
||||
},
|
||||
{
|
||||
value: 'priority',
|
||||
label: t('settings.openai.service_tier.priority')
|
||||
}
|
||||
] as const satisfies OpenAIServiceTierOption[]
|
||||
return options.filter((option) => {
|
||||
if (option.value === 'flex') {
|
||||
return isSupportedFlexServiceTier
|
||||
return isSupportFlexServiceTier
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [isSupportedFlexServiceTier, provider.id, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (serviceTierMode && !serviceTierOptions.some((option) => option.value === serviceTierMode)) {
|
||||
if (provider.id === SystemProviderIds.groq) {
|
||||
setServiceTierMode(GroqServiceTiers.on_demand)
|
||||
} else {
|
||||
setServiceTierMode(OpenAIServiceTiers.auto)
|
||||
}
|
||||
}
|
||||
}, [provider.id, serviceTierMode, serviceTierOptions, setServiceTierMode])
|
||||
}, [isSupportFlexServiceTier, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (verbosity && !verbosityOptions.some((option) => option.value === verbosity)) {
|
||||
@@ -195,14 +165,14 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
}
|
||||
}, [model, verbosity, verbosityOptions, setVerbosity])
|
||||
|
||||
if (!isOpenAIReasoning && !isSupportServiceTier && !isSupportVerbosity) {
|
||||
if (!showSummarySetting && !showServiceTierSetting && !showVerbositySetting) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsibleSettingGroup title={t('settings.openai.title')} defaultExpanded={true}>
|
||||
<SettingGroup>
|
||||
{isSupportServiceTier && (
|
||||
{showServiceTierSetting && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>
|
||||
@@ -210,18 +180,17 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
<HelpTooltip content={t('settings.openai.service_tier.tip')} />
|
||||
</SettingRowTitleSmall>
|
||||
<Selector
|
||||
value={serviceTierMode}
|
||||
value={toOptionValue(serviceTierMode)}
|
||||
onChange={(value) => {
|
||||
setServiceTierMode(value as OpenAIServiceTier)
|
||||
setServiceTierMode(toRealValue(value))
|
||||
}}
|
||||
options={serviceTierOptions}
|
||||
placeholder={t('settings.openai.service_tier.auto')}
|
||||
/>
|
||||
</SettingRow>
|
||||
{(isOpenAIReasoning || isSupportVerbosity) && <SettingDivider />}
|
||||
{(showSummarySetting || showVerbositySetting) && <SettingDivider />}
|
||||
</>
|
||||
)}
|
||||
{isOpenAIReasoning && (
|
||||
{showSummarySetting && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>
|
||||
@@ -236,10 +205,10 @@ const OpenAISettingsGroup: FC<Props> = ({ model, providerId, SettingGroup, Setti
|
||||
options={summaryTextOptions}
|
||||
/>
|
||||
</SettingRow>
|
||||
{isSupportVerbosity && <SettingDivider />}
|
||||
{showVerbositySetting && <SettingDivider />}
|
||||
</>
|
||||
)}
|
||||
{isSupportVerbosity && (
|
||||
{showVerbositySetting && (
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>
|
||||
{t('settings.openai.verbosity.title')} <HelpTooltip content={t('settings.openai.verbosity.tip')} />
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('QuotaTag')
|
||||
|
||||
const QUOTA_UNLIMITED = -9999
|
||||
|
||||
const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quota?: number }> = ({
|
||||
base,
|
||||
providerId,
|
||||
@@ -24,8 +26,8 @@ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quot
|
||||
if (provider.id !== 'mineru') return
|
||||
// 使用用户的key时quota为无限
|
||||
if (provider.apiKey) {
|
||||
setQuota(-9999)
|
||||
updateProvider({ quota: -9999 })
|
||||
setQuota(QUOTA_UNLIMITED)
|
||||
updateProvider({ quota: QUOTA_UNLIMITED })
|
||||
return
|
||||
}
|
||||
if (quota === undefined) {
|
||||
@@ -43,28 +45,37 @@ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quot
|
||||
}
|
||||
}
|
||||
if (_quota !== undefined) {
|
||||
setQuota(_quota)
|
||||
updateProvider({ quota: _quota })
|
||||
return
|
||||
}
|
||||
checkQuota()
|
||||
}, [_quota, base, provider.id, provider.apiKey, provider, quota, updateProvider])
|
||||
|
||||
return (
|
||||
<>
|
||||
{quota && (
|
||||
const getQuotaDisplay = () => {
|
||||
if (quota === undefined) return null
|
||||
if (quota === QUOTA_UNLIMITED) {
|
||||
return (
|
||||
<Tag color="orange" style={{ borderRadius: 20, margin: 0 }}>
|
||||
{quota === -9999
|
||||
? t('knowledge.quota_infinity', {
|
||||
name: provider.name
|
||||
})
|
||||
: t('knowledge.quota', {
|
||||
name: provider.name,
|
||||
quota: quota
|
||||
})}
|
||||
{t('knowledge.quota_infinity', { name: provider.name })}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
if (quota === 0) {
|
||||
return (
|
||||
<Tag color="red" style={{ borderRadius: 20, margin: 0 }}>
|
||||
{t('knowledge.quota_empty', { name: provider.name })}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tag color="orange" style={{ borderRadius: 20, margin: 0 }}>
|
||||
{t('knowledge.quota', { name: provider.name, quota: quota })}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{getQuotaDisplay()}</>
|
||||
}
|
||||
|
||||
export default QuotaTag
|
||||
|
||||
@@ -89,7 +89,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const getNewPainting = useCallback(() => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
model: mode === 'aihubmix_image_generate' ? 'gpt-image-1' : 'V_3',
|
||||
model: mode === 'aihubmix_image_generate' ? 'gemini-3-pro-image-preview' : 'V_3',
|
||||
id: uuid()
|
||||
}
|
||||
}, [mode])
|
||||
@@ -197,6 +197,74 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
|
||||
}
|
||||
return
|
||||
} else if (painting.model === 'gemini-3-pro-image-preview') {
|
||||
const geminiUrl = `${aihubmixProvider.apiHost}/gemini/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent`
|
||||
const geminiHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-goog-api-key': aihubmixProvider.apiKey
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
contents: [
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
text: prompt
|
||||
}
|
||||
],
|
||||
role: 'user'
|
||||
}
|
||||
],
|
||||
generationConfig: {
|
||||
responseModalities: ['TEXT', 'IMAGE'],
|
||||
imageConfig: {
|
||||
aspectRatio: painting.aspectRatio?.replace('ASPECT_', '').replace('_', ':') || '1:1',
|
||||
imageSize: painting.imageSize || '1k'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.silly(`Gemini Request: ${JSON.stringify(requestBody)}`)
|
||||
|
||||
const response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: geminiHeaders,
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
logger.error('Gemini API Error:', errorData)
|
||||
throw new Error(errorData.error?.message || '生成图像失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
logger.silly(`Gemini API Response: ${JSON.stringify(data)}`)
|
||||
|
||||
// Handle array response (stream) or single object
|
||||
const responseItems = Array.isArray(data) ? data : [data]
|
||||
const base64s: string[] = []
|
||||
|
||||
responseItems.forEach((item) => {
|
||||
item.candidates?.forEach((candidate: any) => {
|
||||
candidate.content?.parts?.forEach((part: any) => {
|
||||
if (part.inlineData?.data) {
|
||||
base64s.push(part.inlineData.data)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (base64s.length > 0) {
|
||||
const validFiles = await Promise.all(
|
||||
base64s.map(async (base64: string) => {
|
||||
return await window.api.file.saveBase64Image(base64)
|
||||
})
|
||||
)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
|
||||
}
|
||||
return
|
||||
} else if (painting.model === 'V_3') {
|
||||
// V3 API uses different endpoint and parameters format
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -72,6 +72,7 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
label: 'Gemini',
|
||||
title: 'Gemini',
|
||||
options: [
|
||||
{ label: 'Nano Banana Pro', value: 'gemini-3-pro-image-preview' },
|
||||
{ label: 'imagen-4.0-preview', value: 'imagen-4.0-generate-preview-06-06' },
|
||||
{ label: 'imagen-4.0-ultra', value: 'imagen-4.0-ultra-generate-preview-06-06' }
|
||||
]
|
||||
@@ -224,7 +225,20 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
{ label: '16:9', value: 'ASPECT_16_9' }
|
||||
],
|
||||
initialValue: 'ASPECT_1_1',
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('imagen-'))
|
||||
condition: (painting) =>
|
||||
Boolean(painting.model?.startsWith('imagen-') || painting.model === 'gemini-3-pro-image-preview')
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'imageSize',
|
||||
title: 'paintings.image.size',
|
||||
options: [
|
||||
{ label: '1K', value: '1K' },
|
||||
{ label: '2K', value: '2K' },
|
||||
{ label: '4K', value: '4K' }
|
||||
],
|
||||
initialValue: '1K',
|
||||
condition: (painting) => painting.model === 'gemini-3-pro-image-preview'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
@@ -398,7 +412,7 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
// 几种默认的绘画配置
|
||||
export const DEFAULT_PAINTING: PaintingAction = {
|
||||
id: 'aihubmix_1',
|
||||
model: 'gpt-image-1',
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
aspectRatio: 'ASPECT_1_1',
|
||||
numImages: 1,
|
||||
styleType: 'AUTO',
|
||||
@@ -420,5 +434,6 @@ export const DEFAULT_PAINTING: PaintingAction = {
|
||||
moderation: 'auto',
|
||||
n: 1,
|
||||
numberOfImages: 4,
|
||||
safetyTolerance: 6
|
||||
safetyTolerance: 6,
|
||||
imageSize: '1K'
|
||||
}
|
||||
|
||||
+11
@@ -76,6 +76,17 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
|
||||
})
|
||||
},
|
||||
checked: !provider.apiOptions?.isNotSupportEnableThinking
|
||||
},
|
||||
{
|
||||
key: 'openai_verbosity',
|
||||
label: t('settings.provider.api.options.verbosity.label'),
|
||||
tip: t('settings.provider.api.options.verbosity.help'),
|
||||
onChange: (checked: boolean) => {
|
||||
updateProviderTransition({
|
||||
apiOptions: { ...provider.apiOptions, isNotSupportVerbosity: !checked }
|
||||
})
|
||||
},
|
||||
checked: !provider.apiOptions?.isNotSupportVerbosity
|
||||
}
|
||||
],
|
||||
[t, provider, updateProviderTransition]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex } from '@cherrystudio/ui'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { isNotSupportedTextDelta } from '@renderer/config/models'
|
||||
import { isNotSupportTextDeltaModel } from '@renderer/config/models'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName } from '@renderer/utils'
|
||||
@@ -60,7 +60,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
group: values.group ?? getDefaultGroupName(id)
|
||||
}
|
||||
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) })
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportTextDeltaModel(model) })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
groupQwenModels,
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
isNotSupportedTextDelta,
|
||||
isNotSupportTextDeltaModel,
|
||||
isReasoningModel,
|
||||
isRerankModel,
|
||||
isVisionModel,
|
||||
@@ -136,13 +136,13 @@ const PopupContainer: React.FC<Props> = ({ providerId, resolve }) => {
|
||||
addModel({
|
||||
...model,
|
||||
endpoint_type: endpointTypes.includes('image-generation') ? 'image-generation' : endpointTypes[0],
|
||||
supported_text_delta: !isNotSupportedTextDelta(model)
|
||||
supported_text_delta: !isNotSupportTextDeltaModel(model)
|
||||
})
|
||||
} else {
|
||||
NewApiAddModelPopup.show({ title: t('settings.models.add.add_model'), provider, model })
|
||||
}
|
||||
} else {
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) })
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportTextDeltaModel(model) })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex } from '@cherrystudio/ui'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import { isNotSupportedTextDelta } from '@renderer/config/models'
|
||||
import { isNotSupportTextDeltaModel } from '@renderer/config/models'
|
||||
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import type { EndpointType, Model, Provider } from '@renderer/types'
|
||||
@@ -67,7 +67,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model, endp
|
||||
endpoint_type: isNewApiProvider(provider) ? values.endpointType : undefined
|
||||
}
|
||||
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) })
|
||||
addModel({ ...model, supported_text_delta: !isNotSupportTextDeltaModel(model) })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Flex } from '@cherrystudio/ui'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import { isNotSupportedTextDelta } from '@renderer/config/models'
|
||||
import { isNotSupportTextDeltaModel } from '@renderer/config/models'
|
||||
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import type { EndpointType, Model, Provider } from '@renderer/types'
|
||||
@@ -50,7 +50,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, batchModels
|
||||
addModel({
|
||||
...model,
|
||||
endpoint_type: values.endpointType,
|
||||
supported_text_delta: !isNotSupportedTextDelta(model)
|
||||
supported_text_delta: !isNotSupportTextDeltaModel(model)
|
||||
})
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -82,7 +82,10 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
|
||||
SystemProviderIds.grok,
|
||||
SystemProviderIds.cherryin,
|
||||
SystemProviderIds.longcat,
|
||||
SystemProviderIds.minimax
|
||||
SystemProviderIds.minimax,
|
||||
SystemProviderIds.silicon,
|
||||
SystemProviderIds.qiniu,
|
||||
SystemProviderIds.dmxapi
|
||||
] as const
|
||||
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
|
||||
|
||||
@@ -295,7 +298,10 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}
|
||||
|
||||
if (isAnthropicProvider(provider)) {
|
||||
return formatApiHost(apiHost) + '/messages'
|
||||
// AI SDK uses the baseURL with /v1, then appends /messages
|
||||
// formatApiHost adds /v1 automatically if not present
|
||||
const normalizedHost = formatApiHost(apiHost)
|
||||
return normalizedHost + '/messages'
|
||||
}
|
||||
|
||||
if (isGeminiProvider(provider)) {
|
||||
@@ -349,6 +355,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
|
||||
const anthropicHostPreview = useMemo(() => {
|
||||
const rawHost = anthropicApiHost ?? provider.anthropicApiHost
|
||||
// AI SDK uses the baseURL with /v1, then appends /messages
|
||||
const normalizedHost = formatApiHost(rawHost)
|
||||
|
||||
return `${normalizedHost}/messages`
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
*/
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import type { CompletionsParams } from '@renderer/aiCore/legacy/middleware/schemas'
|
||||
import type { AiSdkMiddlewareConfig } from '@renderer/aiCore/middleware/AiSdkMiddlewareBuilder'
|
||||
import { buildStreamTextParams } from '@renderer/aiCore/prepareParams'
|
||||
import { isDedicatedImageGenerationModel, isEmbeddingModel, isFunctionCallingModel } from '@renderer/config/models'
|
||||
@@ -463,76 +461,55 @@ export function checkApiProvider(provider: Provider): void {
|
||||
export async function checkApi(provider: Provider, model: Model, timeout = 15000): Promise<void> {
|
||||
checkApiProvider(provider)
|
||||
|
||||
// Don't pass in provider parameter. We need auto-format URL
|
||||
const ai = new AiProviderNew(model)
|
||||
|
||||
const assistant = getDefaultAssistant()
|
||||
assistant.model = model
|
||||
assistant.prompt = 'test' // 避免部分 provider 空系统提示词会报错
|
||||
try {
|
||||
if (isEmbeddingModel(model)) {
|
||||
// race 超时 15s
|
||||
logger.silly("it's a embedding model")
|
||||
const timerPromise = new Promise((_, reject) => setTimeout(() => reject('Timeout'), timeout))
|
||||
await Promise.race([ai.getEmbeddingDimensions(model), timerPromise])
|
||||
} else {
|
||||
const abortId = uuid()
|
||||
const signal = readyToAbort(abortId)
|
||||
let chunkError
|
||||
const params: StreamTextParams = {
|
||||
system: assistant.prompt,
|
||||
prompt: 'hi',
|
||||
abortSignal: signal
|
||||
}
|
||||
const config: ModernAiProviderConfig = {
|
||||
streamOutput: true,
|
||||
enableReasoning: false,
|
||||
isSupportedToolUse: false,
|
||||
isImageGenerationEndpoint: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false,
|
||||
isPromptToolUse: false,
|
||||
enableUrlContext: false,
|
||||
assistant,
|
||||
callType: 'check',
|
||||
onChunk: (chunk: Chunk) => {
|
||||
if (chunk.type === ChunkType.ERROR) {
|
||||
chunkError = chunk.error
|
||||
} else {
|
||||
abortCompletion(abortId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try streaming check first
|
||||
try {
|
||||
await ai.completions(model.id, params, config)
|
||||
} catch (e) {
|
||||
if (!isAbortError(e) && !isAbortError(chunkError)) {
|
||||
throw e
|
||||
if (isEmbeddingModel(model)) {
|
||||
// race 超时 15s
|
||||
logger.silly("it's a embedding model")
|
||||
const timerPromise = new Promise((_, reject) => setTimeout(() => reject('Timeout'), timeout))
|
||||
await Promise.race([ai.getEmbeddingDimensions(model), timerPromise])
|
||||
} else {
|
||||
const abortId = uuid()
|
||||
const signal = readyToAbort(abortId)
|
||||
let chunkError
|
||||
const params: StreamTextParams = {
|
||||
system: assistant.prompt,
|
||||
prompt: 'hi',
|
||||
abortSignal: signal
|
||||
}
|
||||
const config: ModernAiProviderConfig = {
|
||||
streamOutput: true,
|
||||
enableReasoning: false,
|
||||
isSupportedToolUse: false,
|
||||
isImageGenerationEndpoint: false,
|
||||
enableWebSearch: false,
|
||||
enableGenerateImage: false,
|
||||
isPromptToolUse: false,
|
||||
enableUrlContext: false,
|
||||
assistant,
|
||||
callType: 'check',
|
||||
onChunk: (chunk: Chunk) => {
|
||||
if (chunk.type === ChunkType.ERROR) {
|
||||
chunkError = chunk.error
|
||||
} else {
|
||||
abortCompletion(abortId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 失败回退legacy
|
||||
const legacyAi = new AiProvider(provider)
|
||||
if (error.message.includes('stream')) {
|
||||
const params: CompletionsParams = {
|
||||
callType: 'check',
|
||||
messages: 'hi',
|
||||
assistant,
|
||||
streamOutput: false,
|
||||
shouldThrow: true
|
||||
|
||||
// Try streaming check
|
||||
try {
|
||||
await ai.completions(model.id, params, config)
|
||||
} catch (e) {
|
||||
if (!isAbortError(e) && !isAbortError(chunkError)) {
|
||||
throw e
|
||||
}
|
||||
const result = await legacyAi.completions(params)
|
||||
if (!result.getText()) {
|
||||
throw new Error('No response received')
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
// } finally {
|
||||
// removeAbortController(taskId, abortFn)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,11 +202,10 @@ const assistantsSlice = createSlice({
|
||||
},
|
||||
updateAssistantPreset: (state, action: PayloadAction<AssistantPreset>) => {
|
||||
const preset = action.payload
|
||||
state.presets.forEach((a) => {
|
||||
if (a.id === preset.id) {
|
||||
a = preset
|
||||
}
|
||||
})
|
||||
const index = state.presets.findIndex((a) => a.id === preset.id)
|
||||
if (index !== -1) {
|
||||
state.presets[index] = preset
|
||||
}
|
||||
},
|
||||
updateAssistantPresetSettings: (
|
||||
state,
|
||||
|
||||
@@ -71,7 +71,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 177,
|
||||
version: 179,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import {
|
||||
glm45FlashModel,
|
||||
isFunctionCallingModel,
|
||||
isNotSupportedTextDelta,
|
||||
isNotSupportTextDeltaModel,
|
||||
SYSTEM_MODELS
|
||||
} from '@renderer/config/models'
|
||||
import { BUILTIN_OCR_PROVIDERS, BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
||||
@@ -1993,7 +1993,7 @@ const migrateConfig = {
|
||||
const updateModelTextDelta = (model?: Model) => {
|
||||
if (model) {
|
||||
model.supported_text_delta = true
|
||||
if (isNotSupportedTextDelta(model)) {
|
||||
if (isNotSupportTextDeltaModel(model)) {
|
||||
model.supported_text_delta = false
|
||||
}
|
||||
}
|
||||
@@ -2876,6 +2876,41 @@ const migrateConfig = {
|
||||
logger.error('migrate 177 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'178': (state: RootState) => {
|
||||
try {
|
||||
const groq = state.llm.providers.find((p) => p.id === SystemProviderIds.groq)
|
||||
if (groq) {
|
||||
groq.verbosity = undefined
|
||||
}
|
||||
logger.info('migrate 178 success')
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 178 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'179': (state: RootState) => {
|
||||
try {
|
||||
state.llm.providers.forEach((provider) => {
|
||||
switch (provider.id) {
|
||||
case SystemProviderIds.silicon:
|
||||
provider.anthropicApiHost = 'https://api.siliconflow.cn'
|
||||
break
|
||||
case SystemProviderIds.qiniu:
|
||||
provider.anthropicApiHost = 'https://api.qnaigc.com'
|
||||
break
|
||||
case SystemProviderIds.dmxapi:
|
||||
provider.anthropicApiHost = provider.apiHost
|
||||
break
|
||||
}
|
||||
})
|
||||
logger.info('migrate 179 success')
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 179 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ export const initialState: SettingsState = {
|
||||
openAI: {
|
||||
summaryText: 'auto',
|
||||
serviceTier: 'auto',
|
||||
verbosity: 'medium'
|
||||
verbosity: undefined
|
||||
},
|
||||
notification: {
|
||||
assistant: false,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type OpenAI from '@cherrystudio/openai'
|
||||
import type { NotNull, NotUndefined } from '@types'
|
||||
import type { ImageModel, LanguageModel } from 'ai'
|
||||
import type { generateObject, generateText, ModelMessage, streamObject, streamText } from 'ai'
|
||||
import * as z from 'zod'
|
||||
|
||||
export type StreamTextParams = Omit<Parameters<typeof streamText>[0], 'model' | 'messages'> &
|
||||
(
|
||||
@@ -42,3 +43,20 @@ export type OpenAIReasoningEffort = OpenAI.ReasoningEffort
|
||||
// I pick undefined as the unique falsy type since they seem like share the same meaning according to OpenAI API docs.
|
||||
// Parameter would not be passed into request if it's undefined.
|
||||
export type OpenAISummaryText = NotNull<OpenAI.Reasoning['summary']>
|
||||
|
||||
const AiSdkParamsSchema = z.enum([
|
||||
'maxOutputTokens',
|
||||
'temperature',
|
||||
'topP',
|
||||
'topK',
|
||||
'presencePenalty',
|
||||
'frequencyPenalty',
|
||||
'stopSequences',
|
||||
'seed'
|
||||
])
|
||||
|
||||
export type AiSdkParam = z.infer<typeof AiSdkParamsSchema>
|
||||
|
||||
export const isAiSdkParam = (param: string): param is AiSdkParam => {
|
||||
return AiSdkParamsSchema.safeParse(param).success
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ export interface GeneratePainting extends PaintingParams {
|
||||
safetyTolerance?: number
|
||||
width?: number
|
||||
height?: number
|
||||
imageSize?: string
|
||||
}
|
||||
|
||||
export interface EditPainting extends PaintingParams {
|
||||
|
||||
@@ -20,28 +20,30 @@ export const ProviderTypeSchema = z.enum([
|
||||
|
||||
export type ProviderType = z.infer<typeof ProviderTypeSchema>
|
||||
|
||||
// undefined 视为支持,默认支持
|
||||
// undefined is treated as supported, enabled by default
|
||||
export type ProviderApiOptions = {
|
||||
/** 是否不支持 message 的 content 为数组类型 */
|
||||
/** Whether message content of array type is not supported */
|
||||
isNotSupportArrayContent?: boolean
|
||||
/** 是否不支持 stream_options 参数 */
|
||||
/** Whether the stream_options parameter is not supported */
|
||||
isNotSupportStreamOptions?: boolean
|
||||
/**
|
||||
* @deprecated
|
||||
* 是否不支持 message 的 role 为 developer */
|
||||
* Whether message role 'developer' is not supported */
|
||||
isNotSupportDeveloperRole?: boolean
|
||||
/* 是否支持 message 的 role 为 developer */
|
||||
/* Whether message role 'developer' is supported */
|
||||
isSupportDeveloperRole?: boolean
|
||||
/**
|
||||
* @deprecated
|
||||
* 是否不支持 service_tier 参数. Only for OpenAI Models. */
|
||||
* Whether the service_tier parameter is not supported. Only for OpenAI Models. */
|
||||
isNotSupportServiceTier?: boolean
|
||||
/* 是否支持 service_tier 参数. Only for OpenAI Models. */
|
||||
/* Whether the service_tier parameter is supported. Only for OpenAI Models. */
|
||||
isSupportServiceTier?: boolean
|
||||
/** 是否不支持 enable_thinking 参数 */
|
||||
/** Whether the enable_thinking parameter is not supported */
|
||||
isNotSupportEnableThinking?: boolean
|
||||
/** 是否不支持 APIVersion */
|
||||
/** Whether APIVersion is not supported */
|
||||
isNotSupportAPIVersion?: boolean
|
||||
/** Whether verbosity is not supported. For OpenAI API (completions & responses). */
|
||||
isNotSupportVerbosity?: boolean
|
||||
}
|
||||
|
||||
// scale is not well supported now. It even lacks of docs
|
||||
@@ -61,6 +63,7 @@ export function isOpenAIServiceTier(tier: string | null | undefined): tier is Op
|
||||
}
|
||||
|
||||
// https://console.groq.com/docs/api-reference#responses
|
||||
// null is not used.
|
||||
export type GroqServiceTier = 'auto' | 'on_demand' | 'flex' | undefined | null
|
||||
|
||||
export const GroqServiceTiers = {
|
||||
|
||||
@@ -96,6 +96,8 @@ export type ReasoningEffortOptionalParams = {
|
||||
include_thoughts?: boolean
|
||||
}
|
||||
}
|
||||
thinking_budget?: number
|
||||
reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams['reasoning_effort'] | 'auto'
|
||||
}
|
||||
disable_reasoning?: boolean
|
||||
// Add any other potential reasoning-related keys here if they exist
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
isSupportEnableThinkingProvider,
|
||||
isSupportServiceTierProvider,
|
||||
isSupportStreamOptionsProvider,
|
||||
isSupportUrlContextProvider
|
||||
isSupportUrlContextProvider,
|
||||
isSupportVerbosityProvider
|
||||
} from '../provider'
|
||||
|
||||
vi.mock('@renderer/store/settings', () => ({
|
||||
@@ -104,6 +105,35 @@ describe('provider utils', () => {
|
||||
expect(isSupportServiceTierProvider(createSystemProvider({ id: SystemProviderIds.github }))).toBe(false)
|
||||
})
|
||||
|
||||
it('determines verbosity support', () => {
|
||||
// Custom providers with explicit flag
|
||||
expect(isSupportVerbosityProvider(createProvider({ apiOptions: { isNotSupportVerbosity: false } }))).toBe(true)
|
||||
expect(isSupportVerbosityProvider(createProvider({ apiOptions: { isNotSupportVerbosity: true } }))).toBe(false)
|
||||
|
||||
// Custom providers without apiOptions (should support by default)
|
||||
expect(isSupportVerbosityProvider(createProvider())).toBe(true)
|
||||
expect(isSupportVerbosityProvider(createProvider({ apiOptions: {} }))).toBe(true)
|
||||
|
||||
// System providers that support verbosity (default behavior)
|
||||
expect(isSupportVerbosityProvider(createSystemProvider())).toBe(true)
|
||||
expect(isSupportVerbosityProvider(createSystemProvider({ id: SystemProviderIds.openai }))).toBe(true)
|
||||
|
||||
// System providers in the NOT_SUPPORT_VERBOSITY_PROVIDERS list (cannot be overridden by apiOptions)
|
||||
expect(isSupportVerbosityProvider(createSystemProvider({ id: SystemProviderIds.groq }))).toBe(false)
|
||||
expect(
|
||||
isSupportVerbosityProvider(
|
||||
createSystemProvider({ id: SystemProviderIds.groq, apiOptions: { isNotSupportVerbosity: false } })
|
||||
)
|
||||
).toBe(false)
|
||||
|
||||
// apiOptions can disable verbosity for any provider
|
||||
expect(
|
||||
isSupportVerbosityProvider(
|
||||
createSystemProvider({ id: SystemProviderIds.openai, apiOptions: { isNotSupportVerbosity: true } })
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('detects URL context capable providers', () => {
|
||||
expect(isSupportUrlContextProvider(createProvider({ type: 'gemini' }))).toBe(true)
|
||||
expect(
|
||||
|
||||
@@ -83,6 +83,21 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_VERBOSITY_PROVIDERS = ['groq'] as const satisfies SystemProviderId[]
|
||||
|
||||
/**
|
||||
* Determines whether the provider supports the verbosity option.
|
||||
* Only applies to system providers that are not in the exclusion list.
|
||||
* @param provider - The provider to check
|
||||
* @returns true if the provider supports verbosity, false otherwise
|
||||
*/
|
||||
export const isSupportVerbosityProvider = (provider: Provider) => {
|
||||
return (
|
||||
provider.apiOptions?.isNotSupportVerbosity !== true &&
|
||||
!NOT_SUPPORT_VERBOSITY_PROVIDERS.some((pid) => pid === provider.id)
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Convert a value (string | undefined | null) into an option-compatible string.
|
||||
* - `undefined` becomes the literal string `'undefined'`
|
||||
* - `null` becomes the literal string `'null'`
|
||||
* - Any other string is returned as-is
|
||||
*
|
||||
* @param v - The value to convert
|
||||
* @returns The string representation safe for option usage
|
||||
*/
|
||||
export function toOptionValue<T extends undefined | Exclude<string, null>>(v: T): NonNullable<T> | 'undefined'
|
||||
export function toOptionValue<T extends null | Exclude<string, undefined>>(v: T): NonNullable<T> | 'null'
|
||||
export function toOptionValue<T extends string | undefined | null>(v: T): NonNullable<T> | 'undefined' | 'null'
|
||||
export function toOptionValue<T extends Exclude<string, null | undefined>>(v: T): T
|
||||
export function toOptionValue(v: string | undefined | null) {
|
||||
if (v === undefined) return 'undefined'
|
||||
if (v === null) return 'null'
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an option string back to its original value.
|
||||
* - The literal string `'undefined'` becomes `undefined`
|
||||
* - The literal string `'null'` becomes `null`
|
||||
* - Any other string is returned as-is
|
||||
*
|
||||
* @param v - The option string to convert
|
||||
* @returns The real value (`undefined`, `null`, or the original string)
|
||||
*/
|
||||
export function toRealValue<T extends 'undefined'>(v: T): undefined
|
||||
export function toRealValue<T extends 'null'>(v: T): null
|
||||
export function toRealValue<T extends string>(v: T): Exclude<T, 'undefined' | 'null'>
|
||||
export function toRealValue(v: string) {
|
||||
if (v === 'undefined') return undefined
|
||||
if (v === 'null') return null
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user