diff --git a/common/endpoint_defaults.go b/common/endpoint_defaults.go
new file mode 100644
index 00000000..1dfe1dc9
--- /dev/null
+++ b/common/endpoint_defaults.go
@@ -0,0 +1,32 @@
+package common
+
+import "one-api/constant"
+
+// EndpointInfo 描述单个端点的默认请求信息
+// path: 上游路径
+// method: HTTP 请求方式,例如 POST/GET
+// 目前均为 POST,后续可扩展
+//
+// json 标签用于直接序列化到 API 输出
+// 例如:{"path":"/v1/chat/completions","method":"POST"}
+
+type EndpointInfo struct {
+ Path string `json:"path"`
+ Method string `json:"method"`
+}
+
+// defaultEndpointInfoMap 保存内置端点的默认 Path 与 Method
+var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
+ constant.EndpointTypeOpenAI: {Path: "/v1/chat/completions", Method: "POST"},
+ constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
+ constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
+ constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
+ constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
+ constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
+}
+
+// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
+func GetDefaultEndpointInfo(et constant.EndpointType) (EndpointInfo, bool) {
+ info, ok := defaultEndpointInfoMap[et]
+ return info, ok
+}
diff --git a/controller/pricing.go b/controller/pricing.go
index 7205cb03..e1719cf3 100644
--- a/controller/pricing.go
+++ b/controller/pricing.go
@@ -42,9 +42,10 @@ func GetPricing(c *gin.Context) {
"success": true,
"data": pricing,
"vendors": model.GetVendors(),
- "group_ratio": groupRatio,
- "usable_group": usableGroup,
- })
+ "group_ratio": groupRatio,
+ "usable_group": usableGroup,
+ "supported_endpoint": model.GetSupportedEndpointMap(),
+ })
}
func ResetModelRatio(c *gin.Context) {
diff --git a/model/pricing.go b/model/pricing.go
index 1eaf8c16..2b3920ba 100644
--- a/model/pricing.go
+++ b/model/pricing.go
@@ -1,28 +1,30 @@
package model
import (
- "fmt"
- "strings"
- "one-api/common"
- "one-api/constant"
- "one-api/setting/ratio_setting"
- "one-api/types"
- "sync"
- "time"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "one-api/common"
+ "one-api/constant"
+ "one-api/setting/ratio_setting"
+ "one-api/types"
+ "sync"
+ "time"
)
type Pricing struct {
- ModelName string `json:"model_name"`
- Description string `json:"description,omitempty"`
- Tags string `json:"tags,omitempty"`
- VendorID int `json:"vendor_id,omitempty"`
- QuotaType int `json:"quota_type"`
- ModelRatio float64 `json:"model_ratio"`
- ModelPrice float64 `json:"model_price"`
- OwnerBy string `json:"owner_by"`
- CompletionRatio float64 `json:"completion_ratio"`
- EnableGroup []string `json:"enable_groups"`
- SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
+ ModelName string `json:"model_name"`
+ Description string `json:"description,omitempty"`
+ Tags string `json:"tags,omitempty"`
+ VendorID int `json:"vendor_id,omitempty"`
+ QuotaType int `json:"quota_type"`
+ ModelRatio float64 `json:"model_ratio"`
+ ModelPrice float64 `json:"model_price"`
+ OwnerBy string `json:"owner_by"`
+ CompletionRatio float64 `json:"completion_ratio"`
+ EnableGroup []string `json:"enable_groups"`
+ SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
}
type PricingVendor struct {
@@ -33,10 +35,11 @@ type PricingVendor struct {
}
var (
- pricingMap []Pricing
- vendorsList []PricingVendor
- lastGetPricingTime time.Time
- updatePricingLock sync.Mutex
+ pricingMap []Pricing
+ vendorsList []PricingVendor
+ supportedEndpointMap map[string]common.EndpointInfo
+ lastGetPricingTime time.Time
+ updatePricingLock sync.Mutex
// 缓存映射:模型名 -> 启用分组 / 计费类型
modelEnableGroups = make(map[string][]string)
@@ -176,20 +179,34 @@ func updatePricing() {
//这里使用切片而不是Set,因为一个模型可能支持多个端点类型,并且第一个端点是优先使用端点
modelSupportEndpointsStr := make(map[string][]string)
- for _, ability := range enableAbilities {
- endpoints, ok := modelSupportEndpointsStr[ability.Model]
- if !ok {
- endpoints = make([]string, 0)
- modelSupportEndpointsStr[ability.Model] = endpoints
- }
- channelTypes := common.GetEndpointTypesByChannelType(ability.ChannelType, ability.Model)
- for _, channelType := range channelTypes {
- if !common.StringsContains(endpoints, string(channelType)) {
- endpoints = append(endpoints, string(channelType))
- }
- }
- modelSupportEndpointsStr[ability.Model] = endpoints
- }
+ // 先根据已有能力填充原生端点
+ for _, ability := range enableAbilities {
+ endpoints := modelSupportEndpointsStr[ability.Model]
+ channelTypes := common.GetEndpointTypesByChannelType(ability.ChannelType, ability.Model)
+ for _, channelType := range channelTypes {
+ if !common.StringsContains(endpoints, string(channelType)) {
+ endpoints = append(endpoints, string(channelType))
+ }
+ }
+ modelSupportEndpointsStr[ability.Model] = endpoints
+ }
+
+ // 再补充模型自定义端点
+ for modelName, meta := range metaMap {
+ if strings.TrimSpace(meta.Endpoints) == "" {
+ continue
+ }
+ var raw map[string]interface{}
+ if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
+ endpoints := modelSupportEndpointsStr[modelName]
+ for k := range raw {
+ if !common.StringsContains(endpoints, k) {
+ endpoints = append(endpoints, k)
+ }
+ }
+ modelSupportEndpointsStr[modelName] = endpoints
+ }
+ }
modelSupportEndpointTypes = make(map[string][]constant.EndpointType)
for model, endpoints := range modelSupportEndpointsStr {
@@ -199,9 +216,48 @@ func updatePricing() {
supportedEndpoints = append(supportedEndpoints, endpointType)
}
modelSupportEndpointTypes[model] = supportedEndpoints
- }
+ }
- pricingMap = make([]Pricing, 0)
+ // 构建全局 supportedEndpointMap(默认 + 自定义覆盖)
+ supportedEndpointMap = make(map[string]common.EndpointInfo)
+ // 1. 默认端点
+ for _, endpoints := range modelSupportEndpointTypes {
+ for _, et := range endpoints {
+ if info, ok := common.GetDefaultEndpointInfo(et); ok {
+ if _, exists := supportedEndpointMap[string(et)]; !exists {
+ supportedEndpointMap[string(et)] = info
+ }
+ }
+ }
+ }
+ // 2. 自定义端点(models 表)覆盖默认
+ for _, meta := range metaMap {
+ if strings.TrimSpace(meta.Endpoints) == "" {
+ continue
+ }
+ var raw map[string]interface{}
+ if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
+ for k, v := range raw {
+ switch val := v.(type) {
+ case string:
+ supportedEndpointMap[k] = common.EndpointInfo{Path: val, Method: "POST"}
+ case map[string]interface{}:
+ ep := common.EndpointInfo{Method: "POST"}
+ if p, ok := val["path"].(string); ok {
+ ep.Path = p
+ }
+ if m, ok := val["method"].(string); ok {
+ ep.Method = strings.ToUpper(m)
+ }
+ supportedEndpointMap[k] = ep
+ default:
+ // ignore unsupported types
+ }
+ }
+ }
+ }
+
+ pricingMap = make([]Pricing, 0)
for model, groups := range modelGroupsMap {
pricing := Pricing{
ModelName: model,
@@ -244,3 +300,8 @@ func updatePricing() {
lastGetPricingTime = time.Now()
}
+
+// GetSupportedEndpointMap 返回全局端点到路径的映射
+func GetSupportedEndpointMap() map[string]common.EndpointInfo {
+ return supportedEndpointMap
+}
diff --git a/web/src/components/common/ui/JSONEditor.js b/web/src/components/common/ui/JSONEditor.js
index 649d5a58..fd4064dd 100644
--- a/web/src/components/common/ui/JSONEditor.js
+++ b/web/src/components/common/ui/JSONEditor.js
@@ -1,25 +1,25 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
- Space,
Button,
Form,
- Card,
Typography,
Banner,
- Row,
- Col,
+ Tabs,
+ TabPane,
+ Card,
+ Input,
InputNumber,
Switch,
- Select,
- Input,
+ TextArea,
+ Row,
+ Col,
} from '@douyinfe/semi-ui';
import {
IconCode,
- IconEdit,
IconPlus,
IconDelete,
- IconSetting,
+ IconRefresh,
} from '@douyinfe/semi-icons';
const { Text } = Typography;
@@ -34,18 +34,17 @@ const JSONEditor = ({
showClear = true,
template,
templateLabel,
- editorType = 'keyValue', // keyValue, object, region
- autosize = true,
+ editorType = 'keyValue',
rules = [],
formApi = null,
...props
}) => {
const { t } = useTranslation();
-
+
// 初始化JSON数据
const [jsonData, setJsonData] = useState(() => {
// 初始化时解析JSON数据
- if (value && value.trim()) {
+ if (typeof value === 'string' && value.trim()) {
try {
const parsed = JSON.parse(value);
return parsed;
@@ -53,13 +52,16 @@ const JSONEditor = ({
return {};
}
}
+ if (typeof value === 'object' && value !== null) {
+ return value;
+ }
return {};
});
-
+
// 根据键数量决定默认编辑模式
const [editMode, setEditMode] = useState(() => {
// 如果初始JSON数据的键数量大于10个,则默认使用手动模式
- if (value && value.trim()) {
+ if (typeof value === 'string' && value.trim()) {
try {
const parsed = JSON.parse(value);
const keyCount = Object.keys(parsed).length;
@@ -76,7 +78,12 @@ const JSONEditor = ({
// 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
useEffect(() => {
try {
- const parsed = value && value.trim() ? JSON.parse(value) : {};
+ let parsed = {};
+ if (typeof value === 'string' && value.trim()) {
+ parsed = JSON.parse(value);
+ } else if (typeof value === 'object' && value !== null) {
+ parsed = value;
+ }
setJsonData(parsed);
setJsonError('');
} catch (error) {
@@ -86,18 +93,17 @@ const JSONEditor = ({
}
}, [value]);
-
// 处理可视化编辑的数据变化
const handleVisualChange = useCallback((newData) => {
setJsonData(newData);
setJsonError('');
const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
-
+
// 通过formApi设置值(如果提供的话)
if (formApi && field) {
formApi.setValue(field, jsonString);
}
-
+
onChange?.(jsonString);
}, [onChange, formApi, field]);
@@ -127,7 +133,12 @@ const JSONEditor = ({
} else {
// 从手动模式切换到可视化模式,需要验证JSON
try {
- const parsed = value && value.trim() ? JSON.parse(value) : {};
+ let parsed = {};
+ if (typeof value === 'string' && value.trim()) {
+ parsed = JSON.parse(value);
+ } else if (typeof value === 'object' && value !== null) {
+ parsed = value;
+ }
setJsonData(parsed);
setJsonError('');
setEditMode('visual');
@@ -143,11 +154,11 @@ const JSONEditor = ({
const addKeyValue = useCallback(() => {
const newData = { ...jsonData };
const keys = Object.keys(newData);
- let newKey = 'key';
let counter = 1;
+ let newKey = `field_${counter}`;
while (newData.hasOwnProperty(newKey)) {
- newKey = `key${counter}`;
- counter++;
+ counter += 1;
+ newKey = `field_${counter}`;
}
newData[newKey] = '';
handleVisualChange(newData);
@@ -162,11 +173,15 @@ const JSONEditor = ({
// 更新键名
const updateKey = useCallback((oldKey, newKey) => {
- if (oldKey === newKey) return;
- const newData = { ...jsonData };
- const value = newData[oldKey];
- delete newData[oldKey];
- newData[newKey] = value;
+ if (oldKey === newKey || !newKey) return;
+ const newData = {};
+ Object.entries(jsonData).forEach(([k, v]) => {
+ if (k === oldKey) {
+ newData[newKey] = v;
+ } else {
+ newData[k] = v;
+ }
+ });
handleVisualChange(newData);
}, [jsonData, handleVisualChange]);
@@ -181,20 +196,20 @@ const JSONEditor = ({
const fillTemplate = useCallback(() => {
if (template) {
const templateString = JSON.stringify(template, null, 2);
-
+
// 通过formApi设置值(如果提供的话)
if (formApi && field) {
formApi.setValue(field, templateString);
}
-
+
// 无论哪种模式都要更新值
onChange?.(templateString);
-
+
// 如果是可视化模式,同时更新jsonData
if (editMode === 'visual') {
setJsonData(template);
}
-
+
// 清除错误状态
setJsonError('');
}
@@ -215,69 +230,47 @@ const JSONEditor = ({
);
}
const entries = Object.entries(jsonData);
-
+
return (
{entries.length === 0 && (
-
-
-
{t('暂无数据,点击下方按钮添加键值对')}
)}
-
+
{entries.map(([key, value], index) => (
-
-
-
-
- {t('键名')}
- updateKey(key, newKey)}
- size="small"
- />
-
-
-
-
- {t('值')}
- updateValue(key, newValue)}
- size="small"
- />
-
-
-
-
- }
- type="danger"
- theme="borderless"
- size="small"
- onClick={() => removeKeyValue(key)}
- className="hover:bg-red-50"
- />
-
-
-
-
+
+
+ updateKey(key, newKey)}
+ />
+
+
+ {renderValueInput(key, value)}
+
+
+ }
+ type="danger"
+ theme="borderless"
+ onClick={() => removeKeyValue(key)}
+ style={{ width: '100%' }}
+ />
+
+
))}
-
-
+
+
}
- onClick={addKeyValue}
- size="small"
- theme="solid"
type="primary"
- className="shadow-sm hover:shadow-md transition-shadow px-4"
+ theme="outline"
+ onClick={addKeyValue}
>
{t('添加键值对')}
@@ -286,100 +279,61 @@ const JSONEditor = ({
);
};
- // 渲染对象编辑器(用于复杂JSON)
- const renderObjectEditor = () => {
- const entries = Object.entries(jsonData);
-
- return (
-
- {entries.length === 0 && (
-
-
-
-
-
- {t('暂无参数,点击下方按钮添加请求参数')}
-
-
- )}
-
- {entries.map(([key, value], index) => (
-
-
-
-
- {t('参数名')}
- updateKey(key, newKey)}
- size="small"
- />
-
-
-
-
- {t('参数值')} ({typeof value})
- {renderValueInput(key, value)}
-
-
-
-
- }
- type="danger"
- theme="borderless"
- size="small"
- onClick={() => removeKeyValue(key)}
- className="hover:bg-red-50"
- />
-
-
-
-
- ))}
-
-
- }
- onClick={addKeyValue}
- size="small"
- theme="solid"
- type="primary"
- className="shadow-sm hover:shadow-md transition-shadow px-4"
- >
- {t('添加参数')}
-
-
-
- );
- };
+ // 添加嵌套对象
+ const flattenObject = useCallback((parentKey) => {
+ const newData = { ...jsonData };
+ let primitive = '';
+ const obj = newData[parentKey];
+ if (obj && typeof obj === 'object') {
+ const firstKey = Object.keys(obj)[0];
+ if (firstKey !== undefined) {
+ const firstVal = obj[firstKey];
+ if (typeof firstVal !== 'object') primitive = firstVal;
+ }
+ }
+ newData[parentKey] = primitive;
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
- // 渲染参数值输入控件
+ const addNestedObject = useCallback((parentKey) => {
+ const newData = { ...jsonData };
+ if (typeof newData[parentKey] !== 'object' || newData[parentKey] === null) {
+ newData[parentKey] = {};
+ }
+ const existingKeys = Object.keys(newData[parentKey]);
+ let counter = 1;
+ let newKey = `field_${counter}`;
+ while (newData[parentKey].hasOwnProperty(newKey)) {
+ counter += 1;
+ newKey = `field_${counter}`;
+ }
+ newData[parentKey][newKey] = '';
+ handleVisualChange(newData);
+ }, [jsonData, handleVisualChange]);
+
+ // 渲染参数值输入控件(支持嵌套)
const renderValueInput = (key, value) => {
const valueType = typeof value;
-
+
if (valueType === 'boolean') {
return (
updateValue(key, newValue)}
- size="small"
/>
-
+
{value ? t('true') : t('false')}
);
}
-
+
if (valueType === 'number') {
return (
updateValue(key, newValue)}
- size="small"
style={{ width: '100%' }}
step={key === 'temperature' ? 0.1 : 1}
precision={key === 'temperature' ? 2 : 0}
@@ -387,25 +341,137 @@ const JSONEditor = ({
/>
);
}
-
- // 字符串类型或其他类型
+
+ if (valueType === 'object' && value !== null) {
+ // 渲染嵌套对象
+ const entries = Object.entries(value);
+ return (
+
+ {entries.length === 0 && (
+
+ {t('空对象,点击下方加号添加字段')}
+
+ )}
+
+ {entries.map(([nestedKey, nestedValue], index) => (
+
+
+ {
+ const newData = { ...jsonData };
+ const oldValue = newData[key][nestedKey];
+ delete newData[key][nestedKey];
+ newData[key][newKey] = oldValue;
+ handleVisualChange(newData);
+ }}
+ />
+
+
+ {typeof nestedValue === 'object' && nestedValue !== null ? (
+
+ ))}
+
+
+ }
+ type="tertiary"
+ onClick={() => addNestedObject(key)}
+ >
+ {t('添加字段')}
+
+ }
+ type="tertiary"
+ onClick={() => flattenObject(key)}
+ >
+ {t('转换为值')}
+
+
+
+ );
+ }
+
+ // 字符串或其他原始类型
return (
- {
- // 尝试转换为适当的类型
- let convertedValue = newValue;
- if (newValue === 'true') convertedValue = true;
- else if (newValue === 'false') convertedValue = false;
- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
- convertedValue = Number(newValue);
- }
-
- updateValue(key, convertedValue);
- }}
- size="small"
- />
+
+ {
+ let convertedValue = newValue;
+ if (newValue === 'true') convertedValue = true;
+ else if (newValue === 'false') convertedValue = false;
+ else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
+ convertedValue = Number(newValue);
+ }
+ updateValue(key, convertedValue);
+ }}
+ />
+ }
+ type="tertiary"
+ onClick={() => {
+ // 将当前值转换为对象
+ const newData = { ...jsonData };
+ newData[key] = { '1': value };
+ handleVisualChange(newData);
+ }}
+ title={t('转换为对象')}
+ />
+
);
};
@@ -414,79 +480,61 @@ const JSONEditor = ({
const entries = Object.entries(jsonData);
const defaultEntry = entries.find(([key]) => key === 'default');
const modelEntries = entries.filter(([key]) => key !== 'default');
-
+
return (
-
+
{/* 默认区域 */}
-
-
- {t('默认区域')}
-
+
updateValue('default', value)}
- size="small"
/>
-
-
+
+
{/* 模型专用区域 */}
-
-
{t('模型专用区域')}
- {modelEntries.map(([modelName, region], index) => (
-
-
+
+
+
);
};
@@ -497,7 +545,6 @@ const JSONEditor = ({
case 'region':
return renderRegionEditor();
case 'object':
- return renderObjectEditor();
case 'keyValue':
default:
return renderKeyValueEditor();
@@ -507,115 +554,92 @@ const JSONEditor = ({
const hasJsonError = jsonError && jsonError.trim() !== '';
return (
-
- {/* Label统一显示在上方 */}
- {label && (
-
- {label}
-
- )}
-
- {/* 编辑模式切换 */}
-
-
- {editMode === 'visual' && (
-
- {t('可视化模式')}
-
- )}
- {editMode === 'manual' && (
-
- {t('手动编辑模式')}
-
- )}
-
-
- {template && templateLabel && (
-
- )}
-
- }
- onClick={toggleEditMode}
- disabled={editMode === 'manual' && hasJsonError}
- className={editMode === 'visual' ? 'shadow-sm' : ''}
- >
- {t('可视化')}
-
- }
- onClick={toggleEditMode}
- className={editMode === 'manual' ? 'shadow-sm' : ''}
- >
- {t('手动编辑')}
-
-
-
-
+
+
+
- {/* JSON错误提示 */}
- {hasJsonError && (
-
- )}
-
- {/* 编辑器内容 */}
- {editMode === 'visual' ? (
-
-
- {renderVisualEditor()}
-
- {/* 可视化模式下的额外文本显示在下方 */}
- {extraText && (
-
- {extraText}
-
- )}
- {/* 隐藏的Form字段用于验证和数据绑定 */}
-
+ {templateLabel}
+
+ )}
+
+ }
+ headerStyle={{ padding: '12px 16px' }}
+ bodyStyle={{ padding: '16px' }}
+ className="!rounded-2xl"
+ >
+ {/* JSON错误提示 */}
+ {hasJsonError && (
+
-
- ) : (
-
- )}
+ )}
- {/* 额外文本在手动编辑模式下显示 */}
- {extraText && editMode === 'manual' && (
-
- {extraText}
-
- )}
-
+ {/* 编辑器内容 */}
+ {editMode === 'visual' ? (
+
+ {renderVisualEditor()}
+ {/* 隐藏的Form字段用于验证和数据绑定 */}
+
+
+ ) : (
+
+
+ {/* 隐藏的Form字段用于验证和数据绑定 */}
+
+
+ )}
+
+ {/* 额外文本显示在卡片底部 */}
+ {extraText && (
+
+ {extraText}
+
+ )}
+
+
);
};
diff --git a/web/src/components/table/model-pricing/layout/PricingPage.jsx b/web/src/components/table/model-pricing/layout/PricingPage.jsx
index 74f47dc0..69ac2336 100644
--- a/web/src/components/table/model-pricing/layout/PricingPage.jsx
+++ b/web/src/components/table/model-pricing/layout/PricingPage.jsx
@@ -80,6 +80,7 @@ const PricingPage = () => {
displayPrice={pricingData.displayPrice}
showRatio={allProps.showRatio}
vendorsMap={pricingData.vendorsMap}
+ endpointMap={pricingData.endpointMap}
t={pricingData.t}
/>
diff --git a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
index 372401c0..44a3607c 100644
--- a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
+++ b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
@@ -47,6 +47,7 @@ const ModelDetailSideSheet = ({
showRatio,
usableGroup,
vendorsMap,
+ endpointMap,
t,
}) => {
const isMobile = useIsMobile();
@@ -82,7 +83,7 @@ const ModelDetailSideSheet = ({
{modelData && (
<>
-
+
{
+const ModelEndpoints = ({ modelData, endpointMap = {}, t }) => {
const renderAPIEndpoints = () => {
- const endpoints = [];
+ if (!modelData) return null;
- if (modelData?.supported_endpoint_types) {
- modelData.supported_endpoint_types.forEach(endpoint => {
- endpoints.push({ name: endpoint, type: endpoint });
- });
- }
+ const mapping = endpointMap;
+ const types = modelData.supported_endpoint_types || [];
- return endpoints.map((endpoint, index) => (
-
-
-
- {endpoint.name}:
- https://api.newapi.pro
- /v1/chat/completions
-
- POST
-
- ));
+ return types.map(type => {
+ const info = mapping[type] || {};
+ let path = info.path || '';
+ // 如果路径中包含 {model} 占位符,替换为真实模型名称
+ if (path.includes('{model}')) {
+ const modelName = modelData.model_name || modelData.modelName || '';
+ path = path.replaceAll('{model}', modelName);
+ }
+ const method = info.method || 'POST';
+ return (
+
+
+
+ {type}{path && ':'}
+ {path && (
+
+ {path}
+
+ )}
+
+ {path && (
+
+ {method}
+
+ )}
+
+ );
+ });
};
return (
diff --git a/web/src/components/table/models/modals/EditModelModal.jsx b/web/src/components/table/models/modals/EditModelModal.jsx
index 13b8d6b8..8cfc1306 100644
--- a/web/src/components/table/models/modals/EditModelModal.jsx
+++ b/web/src/components/table/models/modals/EditModelModal.jsx
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useRef, useMemo } from 'react';
+import JSONEditor from '../../../common/ui/JSONEditor';
import {
SideSheet,
Form,
@@ -109,7 +110,7 @@ const EditModelModal = (props) => {
vendor_id: undefined,
vendor: '',
vendor_icon: '',
- endpoints: [],
+ endpoints: '',
name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配
status: true,
});
@@ -132,15 +133,9 @@ const EditModelModal = (props) => {
} else {
data.tags = [];
}
- // 处理endpoints
- if (data.endpoints) {
- try {
- data.endpoints = JSON.parse(data.endpoints);
- } catch (e) {
- data.endpoints = [];
- }
- } else {
- data.endpoints = [];
+ // endpoints 保持原始 JSON 字符串,若为空设为空串
+ if (!data.endpoints) {
+ data.endpoints = '';
}
// 处理status,将数字转为布尔值
data.status = data.status === 1;
@@ -188,7 +183,7 @@ const EditModelModal = (props) => {
const submitData = {
...values,
tags: Array.isArray(values.tags) ? values.tags.join(',') : values.tags,
- endpoints: JSON.stringify(values.endpoints || []),
+ endpoints: values.endpoints || '',
status: values.status ? 1 : 0,
};
@@ -382,36 +377,15 @@ const EditModelModal = (props) => {
/>
- 0 && {
- extraText: (
-
- {endpointGroups.map(group => (
-
- ))}
-
- )
- })}
+ label={t('端点映射')}
+ placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'}
+ value={values.endpoints}
+ onChange={(val) => formApiRef.current?.setValue('endpoints', val)}
+ formApi={formApiRef.current}
+ editorType='object'
+ extraText={t('留空则使用默认端点;支持 {path, method}')}
/>
diff --git a/web/src/components/table/models/modals/EditPrefillGroupModal.jsx b/web/src/components/table/models/modals/EditPrefillGroupModal.jsx
index 146aae89..6e3a6f20 100644
--- a/web/src/components/table/models/modals/EditPrefillGroupModal.jsx
+++ b/web/src/components/table/models/modals/EditPrefillGroupModal.jsx
@@ -17,7 +17,8 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
+import JSONEditor from '../../../common/ui/JSONEditor';
import {
SideSheet,
Button,
@@ -49,6 +50,13 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
const formRef = useRef(null);
const isEdit = editingGroup && editingGroup.id !== undefined;
+ const [selectedType, setSelectedType] = useState(editingGroup?.type || 'tag');
+
+ // 当外部传入的编辑组类型变化时同步 selectedType
+ useEffect(() => {
+ setSelectedType(editingGroup?.type || 'tag');
+ }, [editingGroup?.type]);
+
const typeOptions = [
{ label: t('模型组'), value: 'model' },
{ label: t('标签组'), value: 'tag' },
@@ -61,8 +69,12 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
try {
const submitData = {
...values,
- items: Array.isArray(values.items) ? values.items : [],
};
+ if (values.type === 'endpoint') {
+ submitData.items = values.items || '';
+ } else {
+ submitData.items = Array.isArray(values.items) ? values.items : [];
+ }
if (editingGroup.id) {
submitData.id = editingGroup.id;
@@ -146,11 +158,17 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
description: editingGroup?.description || '',
items: (() => {
try {
- return typeof editingGroup?.items === 'string'
- ? JSON.parse(editingGroup.items)
- : editingGroup?.items || [];
+ if (editingGroup?.type === 'endpoint') {
+ // 保持原始字符串
+ return typeof editingGroup?.items === 'string'
+ ? editingGroup.items
+ : JSON.stringify(editingGroup.items || {}, null, 2);
+ }
+ return Array.isArray(editingGroup?.items)
+ ? editingGroup.items
+ : [];
} catch {
- return [];
+ return editingGroup?.type === 'endpoint' ? '' : [];
}
})(),
}}
@@ -186,6 +204,7 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
optionList={typeOptions}
rules={[{ required: true, message: t('请选择组类型') }]}
style={{ width: '100%' }}
+ onChange={(val) => setSelectedType(val)}
/>
@@ -213,14 +232,26 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
-
+ {selectedType === 'endpoint' ? (
+ formRef.current?.setValue('items', val)}
+ editorType='object'
+ placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'}
+ extraText={t('键为端点类型,值为路径和方法对象')}
+ />
+ ) : (
+
+ )}
diff --git a/web/src/components/table/models/modals/PrefillGroupManagement.jsx b/web/src/components/table/models/modals/PrefillGroupManagement.jsx
index 1ce51b9e..45300138 100644
--- a/web/src/components/table/models/modals/PrefillGroupManagement.jsx
+++ b/web/src/components/table/models/modals/PrefillGroupManagement.jsx
@@ -137,8 +137,22 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
title: t('项目内容'),
dataIndex: 'items',
key: 'items',
- render: (items) => {
+ render: (items, record) => {
try {
+ if (record.type === 'endpoint') {
+ const obj = typeof items === 'string' ? JSON.parse(items || '{}') : (items || {});
+ const keys = Object.keys(obj);
+ if (keys.length === 0) return
{t('暂无项目')};
+ return renderLimitedItems({
+ items: keys,
+ renderItem: (key, idx) => (
+
+ {key}
+
+ ),
+ maxDisplay: 3,
+ });
+ }
const itemsArray = typeof items === 'string' ? JSON.parse(items) : items;
if (!Array.isArray(itemsArray) || itemsArray.length === 0) {
return
{t('暂无项目')};
diff --git a/web/src/hooks/model-pricing/useModelPricingData.js b/web/src/hooks/model-pricing/useModelPricingData.js
index 1a8fb719..966e346b 100644
--- a/web/src/hooks/model-pricing/useModelPricingData.js
+++ b/web/src/hooks/model-pricing/useModelPricingData.js
@@ -48,6 +48,7 @@ export const useModelPricingData = () => {
const [loading, setLoading] = useState(true);
const [groupRatio, setGroupRatio] = useState({});
const [usableGroup, setUsableGroup] = useState({});
+ const [endpointMap, setEndpointMap] = useState({});
const [statusState] = useContext(StatusContext);
const [userState] = useContext(UserContext);
@@ -159,7 +160,7 @@ export const useModelPricingData = () => {
setLoading(true);
let url = '/api/pricing';
const res = await API.get(url);
- const { success, message, data, vendors, group_ratio, usable_group } = res.data;
+ const { success, message, data, vendors, group_ratio, usable_group, supported_endpoint } = res.data;
if (success) {
setGroupRatio(group_ratio);
setUsableGroup(usable_group);
@@ -172,6 +173,7 @@ export const useModelPricingData = () => {
});
}
setVendorsMap(vendorMap);
+ setEndpointMap(supported_endpoint || {});
setModelsFormat(data, group_ratio, vendorMap);
} else {
showError(message);
@@ -279,6 +281,7 @@ export const useModelPricingData = () => {
loading,
groupRatio,
usableGroup,
+ endpointMap,
// 计算属性
priceRate,