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.
This commit is contained in:
t0ng7u
2025-08-08 02:34:15 +08:00
parent 7cfeb6e87c
commit 8fba0017c7
11 changed files with 614 additions and 458 deletions

View File

@@ -80,6 +80,7 @@ const PricingPage = () => {
displayPrice={pricingData.displayPrice}
showRatio={allProps.showRatio}
vendorsMap={pricingData.vendorsMap}
endpointMap={pricingData.endpointMap}
t={pricingData.t}
/>
</div>

View File

@@ -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}

View File

@@ -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 (