fix: mark Agnes system models as free
This commit is contained in:
@@ -132,7 +132,7 @@ All routes in this section require admin unless noted.
|
||||
| GET/POST/PUT/DELETE | `/api/admin/system-apis` | `src/app/api/admin/system-apis/route.ts` | System API config CRUD with encrypted keys, pricing metadata, platform-default visibility, allowed membership tiers, default-model polling fields `pollingMode`/`pollingOrder`, and video entry usage modes `videoUsageModes`. Successful system-default image/video generation jobs charge user credits from this selected row's pricing through `src/lib/generation-credit-service.ts`; queued/running system-default jobs are counted during new-job balance preflight, and failed jobs do not write consume transactions. |
|
||||
| POST | `/api/admin/system-apis/smart-import` | `src/app/api/admin/system-apis/smart-import/route.ts` | Admin-only intelligent Manifest import. Creates one global `system_api_configs` row per imported profile/model, resolves the visible API request URL from the Manifest profile/provider, rejects configs without a resolvable relay API request URL, writes `system-api-manifests/<systemApiId>.json`, and leaves API Key empty for admin review. Optional `profile.capabilities` is returned through system model config for selected-model image option filtering. Imported rows also carry platform-default visibility, membership-tier allowlist, and default polling fields. |
|
||||
| GET | `/api/admin/system-apis/agnes-capabilities` | `src/app/api/admin/system-apis/agnes-capabilities/route.ts` | Admin-only Agnes AI built-in template preview for the system-default-model flow. Returns `capabilitiesText`, image templates from `src/lib/agnes-model-templates.ts`, video templates, and text templates. It covers Agnes Image 2.1 Flash, Agnes Image 2.0 Flash, Agnes Video V2.0, Agnes 2.0 Flash, and Agnes 1.5 Flash without calling upstream. |
|
||||
| POST | `/api/admin/system-apis/agnes-capabilities` | `src/app/api/admin/system-apis/agnes-capabilities/route.ts` | Admin-only Agnes AI built-in installer. `{ syncImageModels, syncVideoModels, syncTextModels }` resets only matching `provider = 'Agnes AI'` rows by media type. Image/video rows are installed as inactive, 0-credit system default templates with empty API Key and independent `system-api-manifests/<systemApiId>.json` files. Text rows use OpenAI-compatible `chat/completions` directly and do not need a Manifest. Admins must edit rows to fill the API Key and enable them before users can generate. |
|
||||
| POST | `/api/admin/system-apis/agnes-capabilities` | `src/app/api/admin/system-apis/agnes-capabilities/route.ts` | Admin-only Agnes AI built-in installer. `{ syncImageModels, syncVideoModels, syncTextModels }` resets only matching `provider = 'Agnes AI'` rows by media type. Rows are installed as inactive free system default templates (`billing_mode = free`, 0 credits) with empty API Key. Image/video rows also get independent `system-api-manifests/<systemApiId>.json` files. Text rows use OpenAI-compatible `chat/completions` directly and do not need a Manifest. Admins must edit rows to fill the API Key and enable them before users can generate. |
|
||||
| GET | `/api/admin/system-apis/yuanjie-capabilities` | `src/app/api/admin/system-apis/yuanjie-capabilities/route.ts` | Admin-only 元界 AI built-in image/video template preview retained for the system-default-model template path, not for the `智能配置 API` UI. Returns `capabilitiesText`, image templates from `src/lib/yuanjie-image-model-templates.ts`, and video templates from `src/lib/yuanjie-video-model-templates.ts`; it does not call 元界 `/v1/skills` or `/v1/skills/guide`. |
|
||||
| POST | `/api/admin/system-apis/yuanjie-capabilities` | `src/app/api/admin/system-apis/yuanjie-capabilities/route.ts` | Admin-only 元界 AI built-in installer retained for system-default-model template management, not for the generic smart import UI. `{ syncModels: true }` resets only `provider = '元界 AI' AND type = 'image'` rows and installs 17 inactive image rows. `{ syncVideoModels: true }` resets only `provider = '元界 AI' AND type = 'video'` rows and installs inactive video rows with `videoUsageModes`. Rows have no API Key by default; admins must edit each model to set Key, pricing, visibility/member scope, polling, usage mode, and enable it before users can generate. |
|
||||
| GET/POST | `/api/admin/system-apis/yuanjie-pricing` | `src/app/api/admin/system-apis/yuanjie-pricing/route.ts` | Admin-only manual 元界 AI pricing sync. GET previews built-in pricing targets from `src/lib/yuanjie-pricing-sync.ts`. POST updates only existing 元界 image/video system API rows by `model_name`, matching compatible provider spellings such as `元界 AI`/`元界AI` plus `yuanjie-*` model groups, and synchronizes `billing_mode` plus the 元界计费同步 `price_note` while preserving administrator-entered numeric prices and non-元界 providers such as mozheAPI. Optional body `{ type: "image"|"video" }` limits the sync. |
|
||||
|
||||
@@ -90,7 +90,7 @@ Use this document to jump directly to code before broad searching.
|
||||
| Public model config API | `src/app/api/model-config/route.ts`, `src/app/api/style-presets/route.ts` | Returns model/provider config plus DB-backed image style presets for clients. |
|
||||
| User custom API keys | `src/lib/custom-api-store.ts`, `src/app/api/user-api-keys/route.ts`, `src/components/profile/api-key-manager.tsx` | User-owned encrypted API credentials. |
|
||||
| Admin provider presets | `src/app/api/admin/providers/route.ts`, `src/components/admin/api-management-tab.tsx` | Provider registry, default API URL/model/type. Reads and mutations require admin bearer auth; the admin tab must send `Authorization` for the initial list fetch too. |
|
||||
| Admin system API configs | `src/components/admin/api-management-tab.tsx`, `src/app/api/admin/system-apis/route.ts`, `src/app/api/admin/system-apis/agnes-capabilities/route.ts`, `src/lib/server-api-config.ts`, `src/lib/yuanjie-system-manifest.ts`, `src/lib/agnes-model-templates.ts`, `src/lib/agnes-template-installer.ts` | Encrypted shared system API credentials, pricing metadata, platform-default visibility, per-model membership-tier allowlists, and default-model polling fields (`polling_mode`, `polling_order`). The admin list browses system models by provider, then model type, then individual model rows for editing. When browsing 元界 AI, admins can manually click `同步元界价格` to call `/api/admin/system-apis/yuanjie-pricing`, which syncs billing mode and a 元界 pricing note from built-in template metadata without overwriting manually entered numeric prices; the sync matches 元界 provider variants such as `元界AI` and keeps a provider/model-group guard to avoid mozheAPI. The same system-default page can install Agnes AI free templates through `/api/admin/system-apis/agnes-capabilities`; image/video rows are inactive 0-credit templates with isolated `system-api-manifests/<systemApiId>.json` files, while Agnes text rows use `chat/completions` directly and wait for admin-entered API Keys before activation. For legacy 元界 rows without Manifest, built-in capabilities still drive frontend options and generation resolution can write the missing Manifest. Video models can be priced by per-use count (`fixed`), per-second duration (`duration` using `duration_price_per_second`), or token mode. Token billing input/output prices are configured as credits per 1M tokens in the console UI; the `input_price_per_1k`/`output_price_per_1k` DB/API field names are legacy-compatible storage names only. |
|
||||
| Admin system API configs | `src/components/admin/api-management-tab.tsx`, `src/app/api/admin/system-apis/route.ts`, `src/app/api/admin/system-apis/agnes-capabilities/route.ts`, `src/lib/server-api-config.ts`, `src/lib/yuanjie-system-manifest.ts`, `src/lib/agnes-model-templates.ts`, `src/lib/agnes-template-installer.ts` | Encrypted shared system API credentials, pricing metadata, platform-default visibility, per-model membership-tier allowlists, and default-model polling fields (`polling_mode`, `polling_order`). The admin list browses system models by provider, then model type, then individual model rows for editing. When browsing 元界 AI, admins can manually click `同步元界价格` to call `/api/admin/system-apis/yuanjie-pricing`, which syncs billing mode and a 元界 pricing note from built-in template metadata without overwriting manually entered numeric prices; the sync matches 元界 provider variants such as `元界AI` and keeps a provider/model-group guard to avoid mozheAPI. The same system-default page can install Agnes AI free templates through `/api/admin/system-apis/agnes-capabilities`; Agnes rows are inactive `free` billing templates with 0 credits, image/video rows use isolated `system-api-manifests/<systemApiId>.json` files, and text rows use `chat/completions` directly while waiting for admin-entered API Keys before activation. For legacy 元界 rows without Manifest, built-in capabilities still drive frontend options and generation resolution can write the missing Manifest. Models can be free (`free`), priced by per-use count (`fixed`), per-second duration (`duration` using `duration_price_per_second`), ratio, or token mode. Token billing input/output prices are configured as credits per 1M tokens in the console UI; the `input_price_per_1k`/`output_price_per_1k` DB/API field names are legacy-compatible storage names only. |
|
||||
| Model recommendations | `src/app/api/admin/model-recommendations/route.ts`, `src/components/admin/api-management-tab.tsx` | Admin-controlled displayed/recommended model lists. Reads and mutations require admin bearer auth. |
|
||||
|
||||
## Profile, Credits, Orders
|
||||
|
||||
@@ -99,7 +99,7 @@ function createFakeClient() {
|
||||
api_key_preview: '',
|
||||
type: 'image',
|
||||
credits_per_use: 0,
|
||||
billing_mode: 'fixed',
|
||||
billing_mode: 'free',
|
||||
fixed_price: 0,
|
||||
duration_price_per_second: 0,
|
||||
input_price_per_1k: 0,
|
||||
@@ -241,11 +241,12 @@ await runTest('Agnes installer creates free inactive rows with empty API key and
|
||||
assert.ok(rows.every(row => row.apiKeyEncrypted === ''));
|
||||
assert.ok(rows.every(row => row.apiKeyPreview === ''));
|
||||
assert.ok(rows.every(row => row.creditsPerUse === 0));
|
||||
assert.ok(rows.every(row => row.billingMode === 'free'));
|
||||
assert.ok(rows.every(row => row.fixedPrice === 0));
|
||||
assert.ok(rows.every(row => row.isDefault === true));
|
||||
assert.ok(rows.every(row => row.isActive === false));
|
||||
assert.ok(rows.some(row => row.type === 'image' && row.modelGroup === AGNES_IMAGE_MODEL_GROUP && row.apiUrl === `${AGNES_BASE_URL}/v1/images/generations`));
|
||||
assert.ok(rows.some(row => row.type === 'video' && row.modelGroup === AGNES_VIDEO_MODEL_GROUP && row.apiUrl === AGNES_BASE_URL && row.billingMode === 'duration' && row.durationPricePerSecond === 0));
|
||||
assert.ok(rows.some(row => row.type === 'video' && row.modelGroup === AGNES_VIDEO_MODEL_GROUP && row.apiUrl === AGNES_BASE_URL && row.durationPricePerSecond === 0));
|
||||
assert.ok(rows.some(row => row.type === 'text' && row.modelGroup === AGNES_TEXT_MODEL_GROUP && row.apiUrl === `${AGNES_BASE_URL}/v1/chat/completions` && /免费/.test(row.priceNote)));
|
||||
|
||||
const updateCalls = client.calls.filter(call => call.sql.includes('UPDATE system_api_configs'));
|
||||
@@ -265,6 +266,7 @@ await runTest('admin UI and docs expose Agnes as system-default built-in templat
|
||||
|
||||
assert.match(adminTab, /agnes-capabilities/);
|
||||
assert.match(adminTab, /安装 Agnes 免费模型/);
|
||||
assert.match(adminTab, /免费模型/);
|
||||
assert.match(apiReference, /\/api\/admin\/system-apis\/agnes-capabilities/);
|
||||
assert.match(customIntegrations, /Agnes AI/);
|
||||
assert.match(featureIndex, /agnes-model-templates/);
|
||||
|
||||
@@ -20,8 +20,8 @@ function normalizeType(value: unknown): 'image' | 'video' | 'text' {
|
||||
return value === 'video' || value === 'text' ? value : 'image';
|
||||
}
|
||||
|
||||
function normalizeBillingMode(value: unknown): 'fixed' | 'ratio' | 'token' | 'duration' {
|
||||
return value === 'ratio' || value === 'token' || value === 'duration' ? value : 'fixed';
|
||||
function normalizeBillingMode(value: unknown): 'free' | 'fixed' | 'ratio' | 'token' | 'duration' {
|
||||
return value === 'free' || value === 'ratio' || value === 'token' || value === 'duration' ? value : 'fixed';
|
||||
}
|
||||
|
||||
function numberOrDefault(value: unknown, fallback: number): number {
|
||||
@@ -46,6 +46,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const secret = encryptApiKeyForStorage(String(body.apiKey || ''));
|
||||
const billingMode = normalizeBillingMode(body.billingMode);
|
||||
const client = await getDbClient();
|
||||
try {
|
||||
await ensureSystemApiSchema(client);
|
||||
@@ -76,12 +77,12 @@ export async function POST(request: NextRequest) {
|
||||
secret.encrypted,
|
||||
secret.preview,
|
||||
normalizeType(body.type),
|
||||
numberOrDefault(body.creditsPerUse, 10),
|
||||
normalizeBillingMode(body.billingMode),
|
||||
numberOrDefault(body.fixedPrice, 0),
|
||||
numberOrDefault(body.durationPricePerSecond, 0),
|
||||
numberOrDefault(body.inputPricePer1K, 0),
|
||||
numberOrDefault(body.outputPricePer1K, 0),
|
||||
billingMode === 'free' ? 0 : numberOrDefault(body.creditsPerUse, 10),
|
||||
billingMode,
|
||||
billingMode === 'free' ? 0 : numberOrDefault(body.fixedPrice, 0),
|
||||
billingMode === 'free' ? 0 : numberOrDefault(body.durationPricePerSecond, 0),
|
||||
billingMode === 'free' ? 0 : numberOrDefault(body.inputPricePer1K, 0),
|
||||
billingMode === 'free' ? 0 : numberOrDefault(body.outputPricePer1K, 0),
|
||||
numberOrDefault(body.modelRatio, 1),
|
||||
numberOrDefault(body.completionRatio, 1),
|
||||
numberOrDefault(body.groupRatio, 1),
|
||||
@@ -113,6 +114,7 @@ export async function PUT(request: NextRequest) {
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
const billingMode = normalizeBillingMode(body.billingMode);
|
||||
const add = (column: string, value: unknown) => {
|
||||
updates.push(`${column} = $${idx++}`);
|
||||
params.push(value);
|
||||
@@ -125,12 +127,12 @@ export async function PUT(request: NextRequest) {
|
||||
add('model_group', String(body.modelGroup || 'default').trim() || 'default');
|
||||
add('note', String(body.note || '').trim());
|
||||
add('type', normalizeType(body.type));
|
||||
add('credits_per_use', numberOrDefault(body.creditsPerUse, 10));
|
||||
add('billing_mode', normalizeBillingMode(body.billingMode));
|
||||
add('fixed_price', numberOrDefault(body.fixedPrice, 0));
|
||||
add('duration_price_per_second', numberOrDefault(body.durationPricePerSecond, 0));
|
||||
add('input_price_per_1k', numberOrDefault(body.inputPricePer1K, 0));
|
||||
add('output_price_per_1k', numberOrDefault(body.outputPricePer1K, 0));
|
||||
add('credits_per_use', billingMode === 'free' ? 0 : numberOrDefault(body.creditsPerUse, 10));
|
||||
add('billing_mode', billingMode);
|
||||
add('fixed_price', billingMode === 'free' ? 0 : numberOrDefault(body.fixedPrice, 0));
|
||||
add('duration_price_per_second', billingMode === 'free' ? 0 : numberOrDefault(body.durationPricePerSecond, 0));
|
||||
add('input_price_per_1k', billingMode === 'free' ? 0 : numberOrDefault(body.inputPricePer1K, 0));
|
||||
add('output_price_per_1k', billingMode === 'free' ? 0 : numberOrDefault(body.outputPricePer1K, 0));
|
||||
add('model_ratio', numberOrDefault(body.modelRatio, 1));
|
||||
add('completion_ratio', numberOrDefault(body.completionRatio, 1));
|
||||
add('group_ratio', numberOrDefault(body.groupRatio, 1));
|
||||
|
||||
@@ -24,7 +24,10 @@ const MODEL_TYPE_LABELS: Record<ManagedModelType, string> = {
|
||||
text: '\u6587\u672c\u6a21\u578b',
|
||||
};
|
||||
|
||||
const BILLING_MODE_LABELS: Record<'fixed' | 'ratio' | 'token' | 'duration', string> = {
|
||||
type SystemApiBillingMode = 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
|
||||
const BILLING_MODE_LABELS: Record<SystemApiBillingMode, string> = {
|
||||
free: '免费模型',
|
||||
fixed: '按次计费',
|
||||
ratio: '倍率计费',
|
||||
token: 'Token 计费',
|
||||
@@ -48,6 +51,9 @@ const SYSTEM_API_PROVIDER_OPTIONS = ['mozheAPI', 'New API', '元界 AI', 'Agnes
|
||||
|
||||
function formatSystemApiPricing(api: SystemApiConfig): string {
|
||||
const billingMode = api.billingMode || 'fixed';
|
||||
if (billingMode === 'free') {
|
||||
return '免费使用,不消耗积分';
|
||||
}
|
||||
if (billingMode === 'token') {
|
||||
return `输入 ${api.inputPricePer1K ?? 0} / 1M tokens,输出 ${api.outputPricePer1K ?? 0} / 1M tokens`;
|
||||
}
|
||||
@@ -232,7 +238,7 @@ export default function ApiManagementTab() {
|
||||
const [formApiKey, setFormApiKey] = useState('');
|
||||
const [formType, setFormType] = useState<'image' | 'video' | 'text'>('image');
|
||||
const [formCredits, setFormCredits] = useState('10');
|
||||
const [formBillingMode, setFormBillingMode] = useState<'fixed' | 'ratio' | 'token' | 'duration'>('fixed');
|
||||
const [formBillingMode, setFormBillingMode] = useState<SystemApiBillingMode>('fixed');
|
||||
const [formFixedPrice, setFormFixedPrice] = useState('0');
|
||||
const [formDurationPricePerSecond, setFormDurationPricePerSecond] = useState('0');
|
||||
const [formInputPricePer1K, setFormInputPricePer1K] = useState('0');
|
||||
@@ -657,12 +663,12 @@ export default function ApiManagementTab() {
|
||||
note: formNote,
|
||||
apiKey: formApiKey,
|
||||
type: formType,
|
||||
creditsPerUse: Number(formCredits) || 10,
|
||||
creditsPerUse: formBillingMode === 'free' ? 0 : Number(formCredits) || 10,
|
||||
billingMode: formBillingMode,
|
||||
fixedPrice: Number(formFixedPrice) || 0,
|
||||
durationPricePerSecond: Number(formDurationPricePerSecond) || 0,
|
||||
inputPricePer1K: Number(formInputPricePer1K) || 0,
|
||||
outputPricePer1K: Number(formOutputPricePer1K) || 0,
|
||||
fixedPrice: formBillingMode === 'free' ? 0 : Number(formFixedPrice) || 0,
|
||||
durationPricePerSecond: formBillingMode === 'free' ? 0 : Number(formDurationPricePerSecond) || 0,
|
||||
inputPricePer1K: formBillingMode === 'free' ? 0 : Number(formInputPricePer1K) || 0,
|
||||
outputPricePer1K: formBillingMode === 'free' ? 0 : Number(formOutputPricePer1K) || 0,
|
||||
modelRatio: Number(formModelRatio) || 1,
|
||||
completionRatio: Number(formCompletionRatio) || 1,
|
||||
groupRatio: Number(formGroupRatio) || 1,
|
||||
@@ -830,9 +836,10 @@ export default function ApiManagementTab() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>计费模式</Label>
|
||||
<Select value={formBillingMode} onValueChange={v => setFormBillingMode(v as 'fixed' | 'ratio' | 'token' | 'duration')}>
|
||||
<Select value={formBillingMode} onValueChange={v => setFormBillingMode(v as SystemApiBillingMode)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="free">免费模型</SelectItem>
|
||||
<SelectItem value="fixed">按次计费</SelectItem>
|
||||
{formType === 'video' && <SelectItem value="duration">按秒计费</SelectItem>}
|
||||
<SelectItem value="ratio">倍率计费</SelectItem>
|
||||
@@ -840,10 +847,12 @@ export default function ApiManagementTab() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>每次积分</Label>
|
||||
<Input type="number" step="0.0001" value={formFixedPrice} onChange={e => setFormFixedPrice(e.target.value)} />
|
||||
</div>
|
||||
{formBillingMode !== 'free' && (
|
||||
<div className="space-y-2">
|
||||
<Label>每次积分</Label>
|
||||
<Input type="number" step="0.0001" value={formFixedPrice} onChange={e => setFormFixedPrice(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{formType === 'video' && formBillingMode === 'duration' && (
|
||||
<div className="space-y-2">
|
||||
@@ -852,30 +861,37 @@ export default function ApiManagementTab() {
|
||||
<p className="text-xs text-muted-foreground">按秒计费会按前端用户选择的视频时长计算:视频秒数 x 每秒积分。</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label>模型倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formModelRatio} onChange={e => setFormModelRatio(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>补全倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formCompletionRatio} onChange={e => setFormCompletionRatio(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>分组倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formGroupRatio} onChange={e => setFormGroupRatio(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>输入积分 / 1M tokens</Label>
|
||||
<Input type="number" step="0.000001" value={formInputPricePer1K} onChange={e => setFormInputPricePer1K(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出积分 / 1M tokens</Label>
|
||||
<Input type="number" step="0.000001" value={formOutputPricePer1K} onChange={e => setFormOutputPricePer1K(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{formBillingMode !== 'free' && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label>模型倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formModelRatio} onChange={e => setFormModelRatio(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>补全倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formCompletionRatio} onChange={e => setFormCompletionRatio(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>分组倍率</Label>
|
||||
<Input type="number" step="0.000001" value={formGroupRatio} onChange={e => setFormGroupRatio(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>输入积分 / 1M tokens</Label>
|
||||
<Input type="number" step="0.000001" value={formInputPricePer1K} onChange={e => setFormInputPricePer1K(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出积分 / 1M tokens</Label>
|
||||
<Input type="number" step="0.000001" value={formOutputPricePer1K} onChange={e => setFormOutputPricePer1K(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{formBillingMode === 'free' && (
|
||||
<p className="text-xs text-muted-foreground">免费模型不会预占或扣除用户积分,适合 Agnes 这类上游免费额度模型。</p>
|
||||
)}
|
||||
{formBillingMode === 'token' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Token 计费按每 1,000,000 tokens 填写积分价格;内部兼容旧字段名,实际含义以 1M tokens 为准。
|
||||
@@ -1343,7 +1359,10 @@ export default function ApiManagementTab() {
|
||||
<span className="font-medium">{api.modelName || api.name}</span>
|
||||
<Badge variant={api.isActive ? 'default' : 'secondary'} className="text-xs">{api.isActive ? '已启用' : '未启用'}</Badge>
|
||||
{api.name && api.name !== api.modelName && <span className="text-xs text-muted-foreground">{api.name}</span>}
|
||||
<span className="text-xs text-muted-foreground"><Coins className="mr-1 inline h-3 w-3" />{api.creditsPerUse} 积分/次</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Coins className="mr-1 inline h-3 w-3" />
|
||||
{(api.billingMode || 'fixed') === 'free' ? '免费' : `${api.creditsPerUse} 积分/次`}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs">{BILLING_MODE_LABELS[api.billingMode || 'fixed']}</Badge>
|
||||
<Badge variant={api.isDefault !== false ? 'outline' : 'secondary'} className="text-xs">{api.isDefault !== false ? '平台默认' : '不展示'}</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SystemApiConfig {
|
||||
apiKeyPreview: string; // e.g. "sk-...abc"
|
||||
type: 'image' | 'video' | 'text'; // What this API generates
|
||||
creditsPerUse: number; // Credits consumed per generation
|
||||
billingMode?: 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
billingMode?: 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
fixedPrice?: number;
|
||||
durationPricePerSecond?: number;
|
||||
inputPricePer1K?: number;
|
||||
|
||||
@@ -58,7 +58,7 @@ async function insertAgnesSystemApi(
|
||||
modelGroup: string;
|
||||
note: string;
|
||||
type: 'image' | 'video' | 'text';
|
||||
billingMode: 'fixed' | 'duration';
|
||||
billingMode: 'free' | 'fixed' | 'duration';
|
||||
priceNote: string;
|
||||
isDefault: boolean;
|
||||
allowedMembershipTiersJson: string;
|
||||
@@ -168,7 +168,7 @@ export async function installAgnesTemplatesWithClient(client: DbClient, input: A
|
||||
modelGroup: AGNES_IMAGE_MODEL_GROUP,
|
||||
note: `${template.displayName}(Agnes AI 内置免费图片模型)`,
|
||||
type: 'image',
|
||||
billingMode: 'fixed',
|
||||
billingMode: 'free',
|
||||
priceNote: agnesImagePriceNote(template),
|
||||
isDefault,
|
||||
allowedMembershipTiersJson,
|
||||
@@ -194,7 +194,7 @@ export async function installAgnesTemplatesWithClient(client: DbClient, input: A
|
||||
modelGroup: AGNES_VIDEO_MODEL_GROUP,
|
||||
note: `${template.displayName}(Agnes AI 内置免费视频模型)`,
|
||||
type: 'video',
|
||||
billingMode: 'duration',
|
||||
billingMode: 'free',
|
||||
priceNote: agnesVideoPriceNote(template),
|
||||
isDefault,
|
||||
allowedMembershipTiersJson,
|
||||
@@ -217,7 +217,7 @@ export async function installAgnesTemplatesWithClient(client: DbClient, input: A
|
||||
modelGroup: AGNES_TEXT_MODEL_GROUP,
|
||||
note: template.note,
|
||||
type: 'text',
|
||||
billingMode: 'fixed',
|
||||
billingMode: 'free',
|
||||
priceNote: `Agnes 免费模型;文档价格 $0。参数来自 ${template.sourceDoc}`,
|
||||
isDefault,
|
||||
allowedMembershipTiersJson,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PoolClient } from 'pg';
|
||||
import { isUuid } from '@/lib/server-api-config';
|
||||
|
||||
type BillingMode = 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
type BillingMode = 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
|
||||
export interface GenerationCreditCharge {
|
||||
creditsCost: number;
|
||||
@@ -29,6 +29,7 @@ function resolveImageResultCount(payload: Record<string, unknown>, result: Recor
|
||||
|
||||
function resolvePerUseCredits(row: Record<string, unknown>): number {
|
||||
const billingMode = String(row.billing_mode || 'fixed') as BillingMode;
|
||||
if (billingMode === 'free') return 0;
|
||||
const creditsPerUse = Number(row.credits_per_use || 0);
|
||||
const fixedPrice = Number(row.fixed_price || 0);
|
||||
if (billingMode === 'fixed') return Math.ceil(fixedPrice || creditsPerUse || 0);
|
||||
|
||||
@@ -60,7 +60,7 @@ export interface ManagedSystemApi {
|
||||
apiKeyPreview: string;
|
||||
type: ManagedModelType;
|
||||
creditsPerUse: number;
|
||||
billingMode: 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
billingMode: 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
fixedPrice: number;
|
||||
durationPricePerSecond?: number;
|
||||
inputPricePer1K: number;
|
||||
|
||||
@@ -329,7 +329,7 @@ export function calcVideoCredits(
|
||||
modelId?: string,
|
||||
systemPricing?: number | {
|
||||
creditsPerUse?: number;
|
||||
billingMode?: 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
billingMode?: 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
fixedPrice?: number;
|
||||
durationPricePerSecond?: number;
|
||||
},
|
||||
@@ -337,6 +337,7 @@ export function calcVideoCredits(
|
||||
if (modelId && isCustomModel(modelId)) return 0;
|
||||
if (modelId && isSystemModel(modelId) && systemPricing !== undefined) {
|
||||
if (typeof systemPricing === 'number') return systemPricing;
|
||||
if (systemPricing.billingMode === 'free') return 0;
|
||||
if (systemPricing.billingMode === 'duration') {
|
||||
const seconds = Math.max(0, Number(duration) || 0);
|
||||
return Math.ceil(seconds * Number(systemPricing.durationPricePerSecond || 0));
|
||||
|
||||
@@ -29,7 +29,7 @@ export type ServerManagedApiConfig = {
|
||||
apiKeyPreview: string;
|
||||
type: 'image' | 'video' | 'text';
|
||||
creditsPerUse: number;
|
||||
billingMode: 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
billingMode: 'free' | 'fixed' | 'ratio' | 'token' | 'duration';
|
||||
fixedPrice: number;
|
||||
durationPricePerSecond: number;
|
||||
inputPricePer1K: number;
|
||||
@@ -199,7 +199,7 @@ export async function ensureSystemApiSchema(client: DbClient): Promise<void> {
|
||||
}
|
||||
|
||||
function normalizeBillingMode(value: unknown): ServerManagedApiConfig['billingMode'] {
|
||||
return value === 'ratio' || value === 'token' || value === 'duration' ? value : 'fixed';
|
||||
return value === 'free' || value === 'ratio' || value === 'token' || value === 'duration' ? value : 'fixed';
|
||||
}
|
||||
|
||||
export function normalizeSystemApiPollingMode(value: unknown): SystemApiPollingMode {
|
||||
|
||||
Reference in New Issue
Block a user