Compare commits

...

12 Commits

18 changed files with 189 additions and 99 deletions

View File

@@ -730,14 +730,6 @@ func DetectChannelUpstreamModelUpdates(c *gin.Context) {
}
settings := channel.GetOtherSettings()
if !settings.UpstreamModelUpdateCheckEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该渠道未开启上游模型更新检测",
})
return
}
modelsChanged, autoAdded, err := checkAndPersistChannelUpstreamModelUpdates(channel, &settings, true, false)
if err != nil {
common.ApiError(c, err)

View File

@@ -27,12 +27,12 @@ type ChannelOtherSettings struct {
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude默认过滤以满足数据驻留合规
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowIncludeObfuscation bool `json:"allow_include_obfuscation, omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude默认过滤以满足数据驻留合规
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新

View File

@@ -59,7 +59,7 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
if !strings.HasPrefix(info.UpstreamModelName, "imagen") {
return nil, errors.New("not supported model for image generation")
return nil, errors.New("not supported model for image generation, only imagen models are supported")
}
// convert size to aspect ratio but allow user to specify aspect ratio

View File

@@ -147,24 +147,22 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
// 如果没有配置价格,检查模型倍率配置
if !success {
// 没有配置费用,返回错误
// 没有配置费用,也要使用默认费用,否则按费率计费模型无法使用
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName]
if !ok {
// 不再使用默认价格,而是返回错误
return types.PriceData{}, fmt.Errorf("模型 %s 价格未配置,请联系管理员设置", info.OriginModelName)
} else {
if ok {
modelPrice = defaultPrice
}
// 没有配置倍率也不接受没配置,那就返回错误
_, ratioSuccess, matchName := ratio_setting.GetModelRatio(info.OriginModelName)
if !ratioSuccess {
} else {
// 没有配置倍率也不接受没配置,那就返回错误
_, ratioSuccess, matchName := ratio_setting.GetModelRatio(info.OriginModelName)
acceptUnsetRatio := false
if info.UserSetting.AcceptUnsetRatioModel {
acceptUnsetRatio = true
}
if !acceptUnsetRatio {
if !ratioSuccess && !acceptUnsetRatio {
return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置请联系管理员设置或开始自用模式Model %s ratio or price not set, please set or start self-use mode", matchName, matchName)
}
// 未配置价格但配置了倍率,使用默认预扣价格
modelPrice = float64(common.PreConsumedQuota) / common.QuotaPerUnit
}
}

View File

@@ -222,13 +222,13 @@ func RecalculateTaskQuota(ctx context.Context, task *model.Task, actualQuota int
}
other := taskBillingOther(task)
other["task_id"] = task.TaskID
other["reason"] = reason
//other["reason"] = reason
other["pre_consumed_quota"] = preConsumedQuota
other["actual_quota"] = actualQuota
model.RecordTaskBillingLog(model.RecordTaskBillingLogParams{
UserId: task.UserId,
LogType: logType,
Content: "",
Content: reason,
ChannelId: task.ChannelId,
ModelName: taskModelName(task),
Quota: logQuota,

8
web/index.html vendored
View File

@@ -7,7 +7,13 @@
<meta name="theme-color" content="#ffffff" />
<meta
name="description"
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure可用于二次分发管理 key仅单可执行文件已打包好 Docker 镜像,一键部署,开箱即用"
lang="zh"
content="统一的 AI 模型聚合与分发网关,支持将各类大语言模型跨格式转换为 OpenAI、Claude、Gemini 兼容接口,为个人与企业提供集中式模型管理与网关服务。"
/>
<meta
name="description"
lang="en"
content="A unified AI model hub for aggregation & distribution. It supports cross-converting various LLMs into OpenAI-compatible, Claude-compatible, or Gemini-compatible formats. A centralized gateway for personal and enterprise model management."
/>
<meta name="generator" content="new-api" />
<title>New API</title>

View File

@@ -23,7 +23,6 @@ import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
import {
Divider,
Button,
Tag,
Row,
Col,
Collapsible,
@@ -46,6 +45,7 @@ import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
* @param {number} collapseHeight 折叠时的高度默认200
* @param {boolean} withCheckbox 是否启用前缀 Checkbox 来控制激活状态
* @param {boolean} loading 是否处于加载状态
* @param {string} variant 颜色变体: 'violet' | 'teal' | 'amber' | 'rose' | 'green',不传则使用默认蓝色
*/
const SelectableButtonGroup = ({
title,
@@ -58,6 +58,7 @@ const SelectableButtonGroup = ({
collapseHeight = 200,
withCheckbox = false,
loading = false,
variant,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [skeletonCount] = useState(12);
@@ -178,9 +179,6 @@ const SelectableButtonGroup = ({
) : (
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
{items.map((item) => {
const isDisabled =
item.disabled ||
(typeof item.tagCount === 'number' && item.tagCount === 0);
const isActive = Array.isArray(activeValue)
? activeValue.includes(item.value)
: activeValue === item.value;
@@ -194,13 +192,11 @@ const SelectableButtonGroup = ({
}}
theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'}
disabled={isDisabled}
className='sbg-button'
icon={
<Checkbox
checked={isActive}
onChange={() => onChange(item.value)}
disabled={isDisabled}
style={{ pointerEvents: 'auto' }}
/>
}
@@ -210,14 +206,9 @@ const SelectableButtonGroup = ({
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
<ConditionalTooltipText text={item.label} />
{item.tagCount !== undefined && shouldShowTags && (
<Tag
className='sbg-tag'
color='white'
shape='circle'
size='small'
>
<span className={`sbg-badge ${isActive ? 'sbg-badge-active' : ''}`}>
{item.tagCount}
</Tag>
</span>
)}
</div>
</Button>
@@ -231,22 +222,16 @@ const SelectableButtonGroup = ({
onClick={() => onChange(item.value)}
theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'}
disabled={isDisabled}
className='sbg-button'
style={{ width: '100%' }}
>
<div className='sbg-content'>
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
<ConditionalTooltipText text={item.label} />
{item.tagCount !== undefined && shouldShowTags && (
<Tag
className='sbg-tag'
color='white'
shape='circle'
size='small'
>
{item.tagCount !== undefined && shouldShowTags && item.tagCount !== '' && (
<span className={`sbg-badge ${isActive ? 'sbg-badge-active' : ''}`}>
{item.tagCount}
</Tag>
</span>
)}
</div>
</Button>
@@ -258,7 +243,7 @@ const SelectableButtonGroup = ({
return (
<div
className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`}
className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}${variant ? ` sbg-variant-${variant}` : ''}`}
ref={containerRef}
>
{title && (

View File

@@ -723,10 +723,6 @@ export const getChannelsColumns = ({
name: t('仅检测上游模型更新'),
type: 'tertiary',
onClick: () => {
if (!upstreamUpdateMeta.enabled) {
showInfo(t('该渠道未开启上游模型更新检测'));
return;
}
detectChannelUpstreamUpdates(record);
},
});

View File

@@ -3291,6 +3291,18 @@ const EditChannelModal = (props) => {
inputs.upstream_model_update_last_check_time,
)}
</div>
<Form.Input
field='upstream_model_update_ignored_models'
label={t('已忽略模型')}
placeholder={t('例如gpt-4.1-nano,gpt-4o-mini')}
onChange={(value) =>
handleInputChange(
'upstream_model_update_ignored_models',
value,
)
}
showClear
/>
</>
)}
@@ -3460,19 +3472,6 @@ const EditChannelModal = (props) => {
)}
/>
<Form.Input
field='upstream_model_update_ignored_models'
label={t('手动忽略模型(逗号分隔)')}
placeholder={t('例如gpt-4.1-nano,gpt-4o-mini')}
onChange={(value) =>
handleInputChange(
'upstream_model_update_ignored_models',
value,
)
}
showClear
/>
<div className='text-xs text-gray-500 mb-3'>
{t('上次检测到可加入模型')}:&nbsp;
{upstreamDetectedModels.length === 0 ? (

View File

@@ -76,7 +76,6 @@ const PricingEndpointTypes = ({
value: 'all',
label: t('全部端点'),
tagCount: getEndpointTypeCount('all'),
disabled: models.length === 0,
},
...availableEndpointTypes.map((endpointType) => {
const count = getEndpointTypeCount(endpointType);
@@ -84,7 +83,6 @@ const PricingEndpointTypes = ({
value: endpointType,
label: getEndpointTypeLabel(endpointType),
tagCount: count,
disabled: count === 0,
};
}),
];
@@ -96,6 +94,7 @@ const PricingEndpointTypes = ({
activeValue={filterEndpointType}
onChange={setFilterEndpointType}
loading={loading}
variant='green'
t={t}
/>
);

View File

@@ -52,20 +52,19 @@ const PricingGroups = ({
.length;
let ratioDisplay = '';
if (g === 'all') {
ratioDisplay = t('全部');
// ratioDisplay = t('全部');
} else {
const ratio = groupRatio[g];
if (ratio !== undefined && ratio !== null) {
ratioDisplay = `x${ratio}`;
ratioDisplay = `${ratio}x`;
} else {
ratioDisplay = 'x1';
ratioDisplay = '1x';
}
}
return {
value: g,
label: g === 'all' ? t('全部分组') : g,
tagCount: ratioDisplay,
disabled: modelCount === 0,
};
});
@@ -76,6 +75,7 @@ const PricingGroups = ({
activeValue={filterGroup}
onChange={setFilterGroup}
loading={loading}
variant='teal'
t={t}
/>
);

View File

@@ -52,6 +52,7 @@ const PricingQuotaTypes = ({
activeValue={filterQuotaType}
onChange={setFilterQuotaType}
loading={loading}
variant='amber'
t={t}
/>
);

View File

@@ -78,7 +78,6 @@ const PricingTags = ({
value: 'all',
label: t('全部标签'),
tagCount: getTagCount('all'),
disabled: models.length === 0,
},
];
@@ -88,7 +87,6 @@ const PricingTags = ({
value: tag,
label: tag,
tagCount: count,
disabled: count === 0,
});
});
@@ -102,6 +100,7 @@ const PricingTags = ({
activeValue={filterTag}
onChange={setFilterTag}
loading={loading}
variant='rose'
t={t}
/>
);

View File

@@ -83,7 +83,6 @@ const PricingVendors = ({
value: 'all',
label: t('全部供应商'),
tagCount: getVendorCount('all'),
disabled: models.length === 0,
},
];
@@ -96,7 +95,6 @@ const PricingVendors = ({
label: vendor,
icon: icon ? getLobeHubIcon(icon, 16) : null,
tagCount: count,
disabled: count === 0,
});
});
@@ -107,7 +105,6 @@ const PricingVendors = ({
value: 'unknown',
label: t('未知供应商'),
tagCount: count,
disabled: count === 0,
});
}
@@ -121,6 +118,7 @@ const PricingVendors = ({
activeValue={filterVendor}
onChange={setFilterVendor}
loading={loading}
variant='violet'
t={t}
/>
);

View File

@@ -113,15 +113,6 @@ const PricingSidebar = ({
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingGroups
filterGroup={filterGroup}
setFilterGroup={handleGroupClick}
@@ -140,6 +131,15 @@ const PricingSidebar = ({
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingEndpointTypes
filterEndpointType={filterEndpointType}
setFilterEndpointType={setFilterEndpointType}

View File

@@ -96,15 +96,6 @@ const FilterModalContent = ({ sidebarProps, t }) => {
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingGroups
filterGroup={filterGroup}
setFilterGroup={setFilterGroup}
@@ -123,6 +114,15 @@ const FilterModalContent = ({ sidebarProps, t }) => {
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingEndpointTypes
filterEndpointType={filterEndpointType}
setFilterEndpointType={setFilterEndpointType}

View File

@@ -21,6 +21,23 @@ import { useRef, useState } from 'react';
import { API, showError, showInfo, showSuccess } from '../../helpers';
import { normalizeModelList } from './upstreamUpdateUtils';
const getManualIgnoredModelCountFromSettings = (settings) => {
let parsed = null;
if (settings && typeof settings === 'object') {
parsed = settings;
} else if (typeof settings === 'string') {
try {
parsed = JSON.parse(settings);
} catch (error) {
parsed = null;
}
}
if (!parsed || typeof parsed !== 'object') {
return 0;
}
return normalizeModelList(parsed.upstream_model_update_ignored_models).length;
};
export const useChannelUpstreamUpdates = ({ t, refresh }) => {
const [showUpstreamUpdateModal, setShowUpstreamUpdateModal] = useState(false);
const [upstreamUpdateChannel, setUpstreamUpdateChannel] = useState(null);
@@ -114,14 +131,18 @@ export const useChannelUpstreamUpdates = ({ t, refresh }) => {
const addedCount = data?.added_models?.length || 0;
const removedCount = data?.removed_models?.length || 0;
const ignoredCount = data?.ignored_models?.length || 0;
const totalIgnoredCount = getManualIgnoredModelCountFromSettings(
data?.settings,
);
const ignoredCount = normalizeModelList(ignoreModels).length;
showSuccess(
t(
'已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,忽略 {{ignored}} 个',
'已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,本次忽略 {{ignored}} 个,当前已忽略模型 {{totalIgnored}} 个',
{
added: addedCount,
removed: removedCount,
ignored: ignoredCount,
totalIgnored: totalIgnoredCount,
},
),
);

96
web/src/index.css vendored
View File

@@ -344,6 +344,102 @@ code {
text-overflow: ellipsis;
}
/* Badge for count/multiplier in filter buttons */
.sbg-badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 18px;
height: 18px;
padding: 0 6px;
border-radius: 9px;
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1;
background-color: var(--semi-color-fill-0);
color: var(--semi-color-text-2);
transition: background-color 0.15s ease, color 0.15s ease;
}
.sbg-badge-active {
background-color: var(--semi-color-primary-light-active);
color: var(--semi-color-primary);
}
/* ---- SelectableButtonGroup color variants ---- */
.sbg-variant-violet {
--semi-color-primary: #6d28d9;
--semi-color-primary-light-default: rgba(124, 58, 237, 0.08);
--semi-color-primary-light-hover: rgba(124, 58, 237, 0.15);
--semi-color-primary-light-active: rgba(124, 58, 237, 0.22);
}
.sbg-variant-teal {
--semi-color-primary: #0f766e;
--semi-color-primary-light-default: rgba(20, 184, 166, 0.08);
--semi-color-primary-light-hover: rgba(20, 184, 166, 0.15);
--semi-color-primary-light-active: rgba(20, 184, 166, 0.22);
}
.sbg-variant-amber {
--semi-color-primary: #b45309;
--semi-color-primary-light-default: rgba(245, 158, 11, 0.08);
--semi-color-primary-light-hover: rgba(245, 158, 11, 0.15);
--semi-color-primary-light-active: rgba(245, 158, 11, 0.22);
}
.sbg-variant-rose {
--semi-color-primary: #be123c;
--semi-color-primary-light-default: rgba(244, 63, 94, 0.08);
--semi-color-primary-light-hover: rgba(244, 63, 94, 0.15);
--semi-color-primary-light-active: rgba(244, 63, 94, 0.22);
}
.sbg-variant-green {
--semi-color-primary: #047857;
--semi-color-primary-light-default: rgba(16, 185, 129, 0.08);
--semi-color-primary-light-hover: rgba(16, 185, 129, 0.15);
--semi-color-primary-light-active: rgba(16, 185, 129, 0.22);
}
/* Dark mode: lighter text, slightly stronger backgrounds */
html.dark .sbg-variant-violet {
--semi-color-primary: #a78bfa;
--semi-color-primary-light-default: rgba(139, 92, 246, 0.14);
--semi-color-primary-light-hover: rgba(139, 92, 246, 0.22);
--semi-color-primary-light-active: rgba(139, 92, 246, 0.3);
}
html.dark .sbg-variant-teal {
--semi-color-primary: #2dd4bf;
--semi-color-primary-light-default: rgba(45, 212, 191, 0.14);
--semi-color-primary-light-hover: rgba(45, 212, 191, 0.22);
--semi-color-primary-light-active: rgba(45, 212, 191, 0.3);
}
html.dark .sbg-variant-amber {
--semi-color-primary: #fbbf24;
--semi-color-primary-light-default: rgba(251, 191, 36, 0.14);
--semi-color-primary-light-hover: rgba(251, 191, 36, 0.22);
--semi-color-primary-light-active: rgba(251, 191, 36, 0.3);
}
html.dark .sbg-variant-rose {
--semi-color-primary: #fb7185;
--semi-color-primary-light-default: rgba(251, 113, 133, 0.14);
--semi-color-primary-light-hover: rgba(251, 113, 133, 0.22);
--semi-color-primary-light-active: rgba(251, 113, 133, 0.3);
}
html.dark .sbg-variant-green {
--semi-color-primary: #34d399;
--semi-color-primary-light-default: rgba(52, 211, 153, 0.14);
--semi-color-primary-light-hover: rgba(52, 211, 153, 0.22);
--semi-color-primary-light-active: rgba(52, 211, 153, 0.3);
}
/* Tabs组件样式 */
.semi-tabs-content {
padding: 0 !important;