Explorar el Código

✨ feat(pricing+endpoints+ui): wire custom endpoint mapping end‑to‑end and overhaul visual JSON editor

Backend (Go)
- Include custom endpoints in each model’s SupportedEndpointTypes by parsing Model.Endpoints (JSON) and appending keys alongside native endpoint types.
- Build a global supportedEndpointMap map[string]EndpointInfo{path, method} by:
  - Seeding with native defaults.
  - Overriding/adding from models.endpoints (accepts string path → default POST, or {path, method}).
- Expose supported_endpoint at the top level of /api/pricing (vendors-like), removing per-model duplication.
- Fix default path for EndpointTypeOpenAIResponse to /v1/responses.
- Keep concurrency/caching for pricing retrieval intact.

Frontend (React)
- Fetch supported_endpoint in useModelPricingData and propagate to PricingPage → ModelDetailSideSheet → ModelEndpoints.
- ModelEndpoints
  - Resolve path+method via endpointMap; replace {model} with actual model name.
  - Fix mobile visibility; always show path and HTTP method.
- JSONEditor
  - Wrap with Form.Slot to inherit form layout; simplify visual styles.
  - Use Tabs for “Visual” / “Manual” modes.
  - Unify editors: key-value editor now supports nested JSON:
    - “+” to convert a primitive into an object and add nested fields.
    - Add “Convert to value” for two‑way toggle back from object.
    - Stable key rename without reordering rows; new rows append at bottom.
    - Use Row/Col grid for clean alignment; region editor uses Form.Slot + grid.
- Editing flows
  - EditModelModal / EditPrefillGroupModal use JSONEditor (editorType='object') for endpoint mappings.
  - PrefillGroupManagement renders endpoint group items by JSON keys.

Data expectations / compatibility
- models.endpoints should be a JSON object mapping endpoint type → string path or {path, method}. Strings default to POST.
- No schema changes; existing TEXT field continues to store JSON.

QA
- /api/pricing now returns custom endpoint types and global supported_endpoint.
- UI shows both native and custom endpoints; paths/methods render on mobile; nested editing works and preserves order.
t0ng7u hace 7 meses
padre
commit
8fba0017c7

+ 32 - 0
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
+}

+ 4 - 3
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) {

+ 100 - 39
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
+}

+ 360 - 336
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 (
       <div className="space-y-1">
         {entries.length === 0 && (
           <div className="text-center py-6 px-4">
-            <div className="text-gray-400 mb-2">
-              <IconCode size={32} />
-            </div>
             <Text type="tertiary" className="text-gray-500 text-sm">
               {t('暂无数据,点击下方按钮添加键值对')}
             </Text>
           </div>
         )}
-        
+
         {entries.map(([key, value], index) => (
-          <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
-            <Row gutter={12} align="middle">
-              <Col span={10}>
-                <div className="space-y-1">
-                  <Text type="tertiary" size="small">{t('键名')}</Text>
-                  <Input
-                    placeholder={t('键名')}
-                    value={key}
-                    onChange={(newKey) => updateKey(key, newKey)}
-                    size="small"
-                  />
-                </div>
-              </Col>
-              <Col span={11}>
-                <div className="space-y-1">
-                  <Text type="tertiary" size="small">{t('值')}</Text>
-                  <Input
-                    placeholder={t('值')}
-                    value={value}
-                    onChange={(newValue) => updateValue(key, newValue)}
-                    size="small"
-                  />
-                </div>
-              </Col>
-              <Col span={3}>
-                <div className="flex justify-center pt-4">
-                  <Button
-                    icon={<IconDelete />}
-                    type="danger"
-                    theme="borderless"
-                    size="small"
-                    onClick={() => removeKeyValue(key)}
-                    className="hover:bg-red-50"
-                  />
-                </div>
-              </Col>
-            </Row>
-          </Card>
+          <Row key={index} gutter={8} align="middle">
+            <Col span={6}>
+              <Input
+                placeholder={t('键名')}
+                value={key}
+                onChange={(newKey) => updateKey(key, newKey)}
+              />
+            </Col>
+            <Col span={16}>
+              {renderValueInput(key, value)}
+            </Col>
+            <Col span={2}>
+              <Button
+                icon={<IconDelete />}
+                type="danger"
+                theme="borderless"
+                onClick={() => removeKeyValue(key)}
+                style={{ width: '100%' }}
+              />
+            </Col>
+          </Row>
         ))}
-        
-        <div className="flex justify-center pt-1">
+
+        <div className="mt-2 flex justify-center">
           <Button
             icon={<IconPlus />}
-            onClick={addKeyValue}
-            size="small"
-            theme="solid"
             type="primary"
-            className="shadow-sm hover:shadow-md transition-shadow px-4"
+            theme="outline"
+            onClick={addKeyValue}
           >
             {t('添加键值对')}
           </Button>
@@ -286,100 +279,61 @@ const JSONEditor = ({
     );
   };
 
-  // 渲染对象编辑器(用于复杂JSON)
-  const renderObjectEditor = () => {
-    const entries = Object.entries(jsonData);
-    
-    return (
-      <div className="space-y-1">
-        {entries.length === 0 && (
-          <div className="text-center py-6 px-4">
-            <div className="text-gray-400 mb-2">
-              <IconSetting size={32} />
-            </div>
-            <Text type="tertiary" className="text-gray-500 text-sm">
-              {t('暂无参数,点击下方按钮添加请求参数')}
-            </Text>
-          </div>
-        )}
-        
-        {entries.map(([key, value], index) => (
-          <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
-            <Row gutter={12} align="middle">
-              <Col span={8}>
-                <div className="space-y-1">
-                  <Text type="tertiary" size="small">{t('参数名')}</Text>
-                  <Input
-                    placeholder={t('参数名')}
-                    value={key}
-                    onChange={(newKey) => updateKey(key, newKey)}
-                    size="small"
-                  />
-                </div>
-              </Col>
-              <Col span={13}>
-                <div className="space-y-1">
-                  <Text type="tertiary" size="small">{t('参数值')} ({typeof value})</Text>
-                  {renderValueInput(key, value)}
-                </div>
-              </Col>
-              <Col span={3}>
-                <div className="flex justify-center pt-4">
-                  <Button
-                    icon={<IconDelete />}
-                    type="danger"
-                    theme="borderless"
-                    size="small"
-                    onClick={() => removeKeyValue(key)}
-                    className="hover:bg-red-50"
-                  />
-                </div>
-              </Col>
-            </Row>
-          </Card>
-        ))}
-        
-        <div className="flex justify-center pt-1">
-          <Button
-            icon={<IconPlus />}
-            onClick={addKeyValue}
-            size="small"
-            theme="solid"
-            type="primary"
-            className="shadow-sm hover:shadow-md transition-shadow px-4"
-          >
-            {t('添加参数')}
-          </Button>
-        </div>
-      </div>
-    );
-  };
+  // 添加嵌套对象
+  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 (
         <div className="flex items-center">
           <Switch
             checked={value}
             onChange={(newValue) => updateValue(key, newValue)}
-            size="small"
           />
-          <Text type="tertiary" size="small" className="ml-2">
+          <Text type="tertiary" className="ml-2">
             {value ? t('true') : t('false')}
           </Text>
         </div>
       );
     }
-    
+
     if (valueType === 'number') {
       return (
         <InputNumber
           value={value}
           onChange={(newValue) => 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 (
+        <Card className="!rounded-2xl">
+          {entries.length === 0 && (
+            <Text type="tertiary" className="text-gray-500 text-xs">
+              {t('空对象,点击下方加号添加字段')}
+            </Text>
+          )}
+
+          {entries.map(([nestedKey, nestedValue], index) => (
+            <Row key={index} gutter={4} align="middle" className="mb-1">
+              <Col span={8}>
+                <Input
+                  size="small"
+                  placeholder={t('键名')}
+                  value={nestedKey}
+                  onChange={(newKey) => {
+                    const newData = { ...jsonData };
+                    const oldValue = newData[key][nestedKey];
+                    delete newData[key][nestedKey];
+                    newData[key][newKey] = oldValue;
+                    handleVisualChange(newData);
+                  }}
+                />
+              </Col>
+              <Col span={14}>
+                {typeof nestedValue === 'object' && nestedValue !== null ? (
+                  <TextArea
+                    size="small"
+                    rows={2}
+                    value={JSON.stringify(nestedValue, null, 2)}
+                    onChange={(txt) => {
+                      try {
+                        const obj = txt.trim() ? JSON.parse(txt) : {};
+                        const newData = { ...jsonData };
+                        newData[key][nestedKey] = obj;
+                        handleVisualChange(newData);
+                      } catch {
+                        // ignore parse error
+                      }
+                    }}
+                  />
+                ) : (
+                  <Input
+                    size="small"
+                    placeholder={t('值')}
+                    value={String(nestedValue)}
+                    onChange={(newValue) => {
+                      const newData = { ...jsonData };
+                      let convertedValue = newValue;
+                      if (newValue === 'true') convertedValue = true;
+                      else if (newValue === 'false') convertedValue = false;
+                      else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
+                        convertedValue = Number(newValue);
+                      }
+                      newData[key][nestedKey] = convertedValue;
+                      handleVisualChange(newData);
+                    }}
+                  />
+                )}
+              </Col>
+              <Col span={2}>
+                <Button
+                  size="small"
+                  icon={<IconDelete />}
+                  type="danger"
+                  theme="borderless"
+                  onClick={() => {
+                    const newData = { ...jsonData };
+                    delete newData[key][nestedKey];
+                    handleVisualChange(newData);
+                  }}
+                  style={{ width: '100%' }}
+                />
+              </Col>
+            </Row>
+          ))}
+
+          <div className="flex justify-center mt-1 gap-2">
+            <Button
+              size="small"
+              icon={<IconPlus />}
+              type="tertiary"
+              onClick={() => addNestedObject(key)}
+            >
+              {t('添加字段')}
+            </Button>
+            <Button
+              size="small"
+              icon={<IconRefresh />}
+              type="tertiary"
+              onClick={() => flattenObject(key)}
+            >
+              {t('转换为值')}
+            </Button>
+          </div>
+        </Card>
+      );
+    }
+
+    // 字符串或其他原始类型
     return (
-      <Input
-        placeholder={t('参数值')}
-        value={String(value)}
-        onChange={(newValue) => {
-          // 尝试转换为适当的类型
-          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"
-      />
+      <div className="flex items-center gap-1">
+        <Input
+          placeholder={t('参数值')}
+          value={String(value)}
+          onChange={(newValue) => {
+            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);
+          }}
+        />
+        <Button
+          icon={<IconPlus />}
+          type="tertiary"
+          onClick={() => {
+            // 将当前值转换为对象
+            const newData = { ...jsonData };
+            newData[key] = { '1': value };
+            handleVisualChange(newData);
+          }}
+          title={t('转换为对象')}
+        />
+      </div>
     );
   };
 
@@ -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 (
-      <div className="space-y-1">
+      <div className="space-y-2">
         {/* 默认区域 */}
-        <Card className="!p-2 !border-blue-200 !bg-blue-50">
-          <div className="flex items-center mb-1">
-            <Text strong size="small" className="text-blue-700">{t('默认区域')}</Text>
-          </div>
+        <Form.Slot label={t('默认区域')}>
           <Input
             placeholder={t('默认区域,如: us-central1')}
             value={defaultEntry ? defaultEntry[1] : ''}
             onChange={(value) => updateValue('default', value)}
-            size="small"
           />
-        </Card>
-        
+        </Form.Slot>
+
         {/* 模型专用区域 */}
-        <div className="space-y-1">
-          <Text strong size="small">{t('模型专用区域')}</Text>
-          {modelEntries.map(([modelName, region], index) => (
-            <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
-              <Row gutter={12} align="middle">
+        <Form.Slot label={t('模型专用区域')}>
+          <div>
+            {modelEntries.map(([modelName, region], index) => (
+              <Row key={index} gutter={8} align="middle" className="mb-2">
                 <Col span={10}>
-                  <div className="space-y-1">
-                    <Text type="tertiary" size="small">{t('模型名称')}</Text>
-                    <Input
-                      placeholder={t('模型名称')}
-                      value={modelName}
-                      onChange={(newKey) => updateKey(modelName, newKey)}
-                      size="small"
-                    />
-                  </div>
+                  <Input
+                    placeholder={t('模型名称')}
+                    value={modelName}
+                    onChange={(newKey) => updateKey(modelName, newKey)}
+                  />
                 </Col>
-                <Col span={11}>
-                  <div className="space-y-1">
-                    <Text type="tertiary" size="small">{t('区域')}</Text>
-                    <Input
-                      placeholder={t('区域')}
-                      value={region}
-                      onChange={(newValue) => updateValue(modelName, newValue)}
-                      size="small"
-                    />
-                  </div>
+                <Col span={12}>
+                  <Input
+                    placeholder={t('区域')}
+                    value={region}
+                    onChange={(newValue) => updateValue(modelName, newValue)}
+                  />
                 </Col>
-                <Col span={3}>
-                  <div className="flex justify-center pt-4">
-                    <Button
-                      icon={<IconDelete />}
-                      type="danger"
-                      theme="borderless"
-                      size="small"
-                      onClick={() => removeKeyValue(modelName)}
-                      className="hover:bg-red-50"
-                    />
-                  </div>
+                <Col span={2}>
+                  <Button
+                    icon={<IconDelete />}
+                    type="danger"
+                    theme="borderless"
+                    onClick={() => removeKeyValue(modelName)}
+                    style={{ width: '100%' }}
+                  />
                 </Col>
               </Row>
-            </Card>
-          ))}
-          
-          <div className="flex justify-center pt-1">
-            <Button
-              icon={<IconPlus />}
-              onClick={addKeyValue}
-              size="small"
-              theme="solid"
-              type="primary"
-              className="shadow-sm hover:shadow-md transition-shadow px-4"
-            >
-              {t('添加模型区域')}
-            </Button>
+            ))}
+
+            <div className="mt-2 flex justify-center">
+              <Button
+                icon={<IconPlus />}
+                onClick={addKeyValue}
+                type="primary"
+                theme="outline"
+              >
+                {t('添加模型区域')}
+              </Button>
+            </div>
           </div>
-        </div>
+        </Form.Slot>
       </div>
     );
   };
@@ -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 (
-    <div className="space-y-1">
-      {/* Label统一显示在上方 */}
-      {label && (
-        <div className="flex items-center">
-          <Text className="text-sm font-medium text-gray-900">{label}</Text>
-        </div>
-      )}
-      
-      {/* 编辑模式切换 */}
-      <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
-        <div className="flex items-center gap-2">
-          {editMode === 'visual' && (
-            <Text type="tertiary" size="small" className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">
-              {t('可视化模式')}
-            </Text>
-          )}
-          {editMode === 'manual' && (
-            <Text type="tertiary" size="small" className="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs">
-              {t('手动编辑模式')}
-            </Text>
-          )}
-        </div>
-        <div className="flex items-center gap-2">
-          {template && templateLabel && (
-            <Button
-              size="small"
-              type="tertiary"
-              onClick={fillTemplate}
-              className="!text-semi-color-primary hover:bg-blue-50 text-xs"
-            >
-              {templateLabel}
-            </Button>
-          )}
-          <Space size="tight">
-            <Button
-              size="small"
-              type={editMode === 'visual' ? 'primary' : 'tertiary'}
-              icon={<IconEdit />}
-              onClick={toggleEditMode}
-              disabled={editMode === 'manual' && hasJsonError}
-              className={editMode === 'visual' ? 'shadow-sm' : ''}
-            >
-              {t('可视化')}
-            </Button>
-            <Button
-              size="small"
-              type={editMode === 'manual' ? 'primary' : 'tertiary'}
-              icon={<IconCode />}
-              onClick={toggleEditMode}
-              className={editMode === 'manual' ? 'shadow-sm' : ''}
+    <Form.Slot label={label}>
+      <Card
+        header={
+          <div className="flex justify-between items-center">
+            <Tabs
+              type="slash"
+              activeKey={editMode}
+              onChange={(key) => {
+                if (key === 'manual' && editMode === 'visual') {
+                  setEditMode('manual');
+                } else if (key === 'visual' && editMode === 'manual') {
+                  toggleEditMode();
+                }
+              }}
             >
-              {t('手动编辑')}
-            </Button>
-          </Space>
-        </div>
-      </div>
+              <TabPane tab={t('可视化')} itemKey="visual" />
+              <TabPane tab={t('手动编辑')} itemKey="manual" />
+            </Tabs>
 
-      {/* JSON错误提示 */}
-      {hasJsonError && (
-        <Banner
-          type="danger"
-          description={`JSON 格式错误: ${jsonError}`}
-          className="!rounded-md text-sm"
-        />
-      )}
+            {template && templateLabel && (
+              <Button
+                type="tertiary"
+                onClick={fillTemplate}
+                size="small"
+              >
+                {templateLabel}
+              </Button>
+            )}
+          </div>
+        }
+        headerStyle={{ padding: '12px 16px' }}
+        bodyStyle={{ padding: '16px' }}
+        className="!rounded-2xl"
+      >
+        {/* JSON错误提示 */}
+        {hasJsonError && (
+          <Banner
+            type="danger"
+            description={`JSON 格式错误: ${jsonError}`}
+            className="mb-3"
+          />
+        )}
 
-      {/* 编辑器内容 */}
-      {editMode === 'visual' ? (
-        <div>
-          <Card className="!p-3 !border-gray-200 !shadow-sm !rounded-md bg-white">
+        {/* 编辑器内容 */}
+        {editMode === 'visual' ? (
+          <div>
             {renderVisualEditor()}
-          </Card>
-          {/* 可视化模式下的额外文本显示在下方 */}
-          {extraText && (
-            <div className="text-xs text-gray-600 mt-0.5">
-              {extraText}
-            </div>
-          )}
-          {/* 隐藏的Form字段用于验证和数据绑定 */}
-          <Form.Input
-            field={field}
-            value={value}
-            rules={rules}
-            style={{ display: 'none' }}
-            noLabel={true}
-            {...props}
-          />
-        </div>
-      ) : (
-        <Form.TextArea
-          field={field}
-          placeholder={placeholder}
-          value={value}
-          onChange={handleManualChange}
-          showClear={showClear}
-          rows={Math.max(8, value ? value.split('\n').length : 8)}
-          rules={rules}
-          noLabel={true}
-          {...props}
-        />
-      )}
+            {/* 隐藏的Form字段用于验证和数据绑定 */}
+            <Form.Input
+              field={field}
+              value={value}
+              rules={rules}
+              style={{ display: 'none' }}
+              noLabel={true}
+              {...props}
+            />
+          </div>
+        ) : (
+          <div>
+            <TextArea
+              placeholder={placeholder}
+              value={value}
+              onChange={handleManualChange}
+              showClear={showClear}
+              rows={Math.max(8, value ? value.split('\n').length : 8)}
+            />
+            {/* 隐藏的Form字段用于验证和数据绑定 */}
+            <Form.Input
+              field={field}
+              value={value}
+              rules={rules}
+              style={{ display: 'none' }}
+              noLabel={true}
+              {...props}
+            />
+          </div>
+        )}
 
-      {/* 额外文本在手动编辑模式下显示 */}
-      {extraText && editMode === 'manual' && (
-        <div className="text-xs text-gray-600">
-          {extraText}
-        </div>
-      )}
-    </div>
+        {/* 额外文本显示在卡片底部 */}
+        {extraText && (
+          <div className="text-gray-600 mt-3 pt-3">
+            {extraText}
+          </div>
+        )}
+      </Card>
+    </Form.Slot>
   );
 };
 

+ 1 - 0
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}
       />
     </div>

+ 2 - 1
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 && (
           <>
             <ModelBasicInfo modelData={modelData} vendorsMap={vendorsMap} t={t} />
-            <ModelEndpoints modelData={modelData} t={t} />
+            <ModelEndpoints modelData={modelData} endpointMap={endpointMap} t={t} />
             <ModelPricingTable
               modelData={modelData}
               selectedGroup={selectedGroup}

+ 36 - 22
web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx

@@ -23,31 +23,45 @@ import { IconLink } from '@douyinfe/semi-icons';
 
 const { Text } = Typography;
 
-const ModelEndpoints = ({ modelData, t }) => {
+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) => (
-      <div
-        key={index}
-        className="flex justify-between border-b border-dashed last:border-0 py-2 last:pb-0"
-        style={{ borderColor: 'var(--semi-color-border)' }}
-      >
-        <span className="flex items-center pr-5">
-          <Badge dot type="success" className="mr-2" />
-          {endpoint.name}:
-          <span className="text-gray-500 hidden md:inline">https://api.newapi.pro</span>
-          /v1/chat/completions
-        </span>
-        <span className="text-gray-500 text-xs hidden md:inline">POST</span>
-      </div>
-    ));
+    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 (
+        <div
+          key={type}
+          className="flex justify-between border-b border-dashed last:border-0 py-2 last:pb-0"
+          style={{ borderColor: 'var(--semi-color-border)' }}
+        >
+          <span className="flex items-center pr-5">
+            <Badge dot type="success" className="mr-2" />
+            {type}{path && ':'}
+            {path && (
+              <span className="text-gray-500 md:ml-1 break-all">
+                {path}
+              </span>
+            )}
+          </span>
+          {path && (
+            <span className="text-gray-500 text-xs md:ml-1">
+              {method}
+            </span>
+          )}
+        </div>
+      );
+    });
   };
 
   return (

+ 14 - 40
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) => {
                     />
                   </Col>
                   <Col span={24}>
-                    <Form.TagInput
+                    <JSONEditor
                       field='endpoints'
-                      label={t('支持端点')}
-                      placeholder={t('输入端点名称,按回车添加')}
-                      addOnBlur
-                      showClear
-                      style={{ width: '100%' }}
-                      {...(endpointGroups.length > 0 && {
-                        extraText: (
-                          <Space wrap>
-                            {endpointGroups.map(group => (
-                              <Button
-                                key={group.id}
-                                size='small'
-                                type='primary'
-                                onClick={() => {
-                                  if (formApiRef.current) {
-                                    const currentEndpoints = formApiRef.current.getValue('endpoints') || [];
-                                    const newEndpoints = [...currentEndpoints, ...(group.items || [])];
-                                    const uniqueEndpoints = [...new Set(newEndpoints)];
-                                    formApiRef.current.setValue('endpoints', uniqueEndpoints);
-                                  }
-                                }}
-                              >
-                                {group.name}
-                              </Button>
-                            ))}
-                          </Space>
-                        )
-                      })}
+                      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}')}
                     />
                   </Col>
                   <Col span={24}>

+ 45 - 14
web/src/components/table/models/modals/EditPrefillGroupModal.jsx

@@ -17,7 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 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)}
                   />
                 </Col>
                 <Col span={24}>
@@ -213,14 +232,26 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
               </div>
               <Row gutter={12}>
                 <Col span={24}>
-                  <Form.TagInput
-                    field="items"
-                    label={t('项目')}
-                    placeholder={t('输入项目名称,按回车添加')}
-                    addOnBlur
-                    showClear
-                    style={{ width: '100%' }}
-                  />
+                  {selectedType === 'endpoint' ? (
+                    <JSONEditor
+                      field="items"
+                      label={t('端点映射')}
+                      value={formRef.current?.getValue('items') ?? (typeof editingGroup?.items === 'string' ? editingGroup.items : JSON.stringify(editingGroup.items || {}, null, 2))}
+                      onChange={(val) => formRef.current?.setValue('items', val)}
+                      editorType='object'
+                      placeholder={'{\n  "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'}
+                      extraText={t('键为端点类型,值为路径和方法对象')}
+                    />
+                  ) : (
+                    <Form.TagInput
+                      field="items"
+                      label={t('项目')}
+                      placeholder={t('输入项目名称,按回车添加')}
+                      addOnBlur
+                      showClear
+                      style={{ width: '100%' }}
+                    />
+                  )}
                 </Col>
               </Row>
             </Card>

+ 15 - 1
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 <Text type="tertiary">{t('暂无项目')}</Text>;
+            return renderLimitedItems({
+              items: keys,
+              renderItem: (key, idx) => (
+                <Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
+                  {key}
+                </Tag>
+              ),
+              maxDisplay: 3,
+            });
+          }
           const itemsArray = typeof items === 'string' ? JSON.parse(items) : items;
           if (!Array.isArray(itemsArray) || itemsArray.length === 0) {
             return <Text type="tertiary">{t('暂无项目')}</Text>;

+ 4 - 1
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,