fix: match yuanjie pricing provider variants

This commit is contained in:
FengLee
2026-05-20 22:25:22 +08:00
parent d2cb017a8c
commit 080f2e2b95
5 changed files with 85 additions and 16 deletions

View File

@@ -133,7 +133,7 @@ All routes in this section require admin unless noted.
| 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 as `待填写` 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/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 `provider = '元界 AI'` image/video system API rows by `model_name`, synchronizing `billing_mode` and the 元界计费同步 `price_note` while preserving administrator-entered numeric prices and non-元界 providers such as mozheAPI. Optional body `{ type: "image"|"video" }` limits the sync. |
| 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. |
| GET/POST/PUT/DELETE | `/api/admin/model-recommendations` | `src/app/api/admin/model-recommendations/route.ts` | Managed model recommendations. All methods require admin bearer auth. |
| GET/DELETE | `/api/admin/generation-jobs` | `src/app/api/admin/generation-jobs/route.ts` | Admin task listing and deletion. |
| GET | `/api/admin/gallery/works` | `src/app/api/admin/gallery/works/route.ts` | Admin public gallery work listing for prompt moderation. |
@@ -177,7 +177,7 @@ Primary SQL tables touched directly in API routes include:
`src/lib/yuanjie-video-model-templates.ts` is the canonical source for built-in 元界 AI video model definitions from the local video docs. It maps each model to a Manifest request body, records whether the model supports 文生视频 and/or 图生视频, and stores aspect ratio, resolution, duration, and quality/mode capability options for the selected system video model. Video templates use the same documented `is_final` plus `state` polling rule.
`src/lib/yuanjie-pricing-sync.ts` is the canonical source for manual 元界 AI pricing metadata sync. It derives billing modes from the built-in image/video templates and local docs: image models default to fixed per-use pricing, duration-sensitive video models sync to `duration`, Seedance token-billed video models sync to `token`, and special variable-cost video models sync to `ratio` with a warning note. The sync is manual from the admin system-default-model page and only updates existing 元界 rows.
`src/lib/yuanjie-pricing-sync.ts` is the canonical source for manual 元界 AI pricing metadata sync. It derives billing modes from the built-in image/video templates and local docs: image models default to fixed per-use pricing, duration-sensitive video models sync to `duration`, Seedance token-billed video models sync to `token`, and special variable-cost video models sync to `ratio` with a warning note. The sync is manual from the admin system-default-model page and only updates existing 元界 rows, including legacy provider spellings such as `元界AI`; update SQL still includes a 元界 provider/model-group guard so mozheAPI rows cannot be touched by the sync.
Yuanjie Manifest references use `$inputImages.urls` for provider-facing JSON fields. For image-to-image, `/api/generate/image` reads the primary `image` plus `extraImages` and sends all references to `src/lib/user-api-manifest-executor.ts`; the executor uploads data URL references into storage before rendering Yuanjie `params.images`, top-level `images`, or `base64Array`. Yuanjie video templates keep documented model-specific fields inside `src/lib/yuanjie-video-model-templates.ts`, including first/last reference fields and mode fields such as `input_reference`, `reference_urls`, `img_url`, `image_tail`, `ratio`, `size`, and `generation_mode`.

View File

@@ -25,7 +25,7 @@ Use this document before changing non-generic provider/platform behavior. If a u
- Yuanjie image and video templates that accept reference media should render provider-facing references with `$inputImages.urls`, not `$inputImages.dataUrls`. The Manifest executor uploads data URL references into storage first, then exposes object/local-storage public URLs as `$inputImages.urls` while keeping raw data URLs available as `$inputImages.dataUrls` for multipart/file-upload manifests.
- Yuanjie GPT Image 2 / GPT Image 2 official-transfer image-to-image must pass all frontend references. `src/components/create/image-to-image.tsx` sends the primary image as `image` and additional references as `extraImages`; `src/app/api/generate/image/route.ts` must normalize them into the Manifest `inputImages` array before calling `src/lib/user-api-manifest-executor.ts`.
- Yuanjie video templates must stay in `src/lib/yuanjie-video-model-templates.ts` and map documented model-specific fields there instead of changing generic mozheAPI/custom-API request builders. Current documented special mappings include `sora-2.params.input_reference`, `wan2.6-cankaosheng.params.reference_urls`, `wan2.6-shouzheng.params.img_url`, `kling-v3-omni-shouweizhen.params.image/image_tail`, `happyhorse-r2v.params.ratio`, `grok-video-3.params.size`, and `veo3.1.params.generation_mode/enhance_prompt/enable_upsample`.
- 元界价格/计费方式同步 is manual only. Admins trigger it from the `系统默认模型` provider view; the route is `/api/admin/system-apis/yuanjie-pricing` and the logic lives in `src/lib/yuanjie-pricing-sync.ts`. It updates only existing `provider = '元界 AI'` image/video rows, synchronizing `billing_mode` and a `元界计费同步` price note from local built-in template metadata. It must not delete, create, or rewrite mozheAPI rows, generic smart-import rows, API keys, Manifest paths, or administrator-entered numeric prices.
- 元界价格/计费方式同步 is manual only. Admins trigger it from the `系统默认模型` provider view; the route is `/api/admin/system-apis/yuanjie-pricing` and the logic lives in `src/lib/yuanjie-pricing-sync.ts`. It updates only existing 元界 image/video rows, accepting provider spellings such as `元界 AI`/`元界AI` and `yuanjie-*` model groups, synchronizing `billing_mode` and a `元界计费同步` price note from local built-in template metadata. It must not delete, create, or rewrite mozheAPI rows, generic smart-import rows, API keys, Manifest paths, or administrator-entered numeric prices.
## mozheAPI

View File

@@ -78,7 +78,7 @@ Use this document to jump directly to code before broad searching.
| Custom API transport | `src/lib/custom-api-fetch.ts`, `src/lib/custom-image-fallback.ts` | Headers, one retry for 502/503/504 gateway failures, progress JSON parsing, upstream error parsing, stream-to-sync fallback policy for system image APIs. |
| Server API resolution | `src/lib/server-api-config.ts` | Resolves user custom API and admin system API IDs into decrypted credentials, enforces system API default visibility plus membership-tier allowlists before generation, and builds default-model polling candidates by media type plus admin display name (`system_api_configs.name`). The upstream `model_name` remains the per-provider request model only. |
| User API smart import | `src/components/profile/api-key-manager.tsx`, `src/app/api/user-api-keys/smart-import/route.ts`, `src/lib/user-api-manifest.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/model-capabilities.ts`, `src/lib/model-display.ts` | The profile API settings page has an `智能配置 API` button next to `添加 API 密钥`. It opens a wide viewport-capped Manifest editor, can copy the LLM prompt, shows guidance under the prompt button explaining the copy-to-chat-AI and paste-and-import flow, can paste clipboard JSON without importing, and can paste-and-import in one action. The prompt instructs the LLM to stop and ask the user for the relay API Base URL when the docs do not contain it. Imports create each profile/model as an independent `user_api_keys` row plus a separate `user-api-manifests/<userId>/<keyId>.json` file and reject incomplete configs without a resolvable request URL. Imported rows should store a human-readable provider name in the editable provider/supplier fields and resolve the visible API request URL from `profile.baseUrl + submit.path` for synchronous endpoints. Generic placeholder notes such as `导入的 API Key` must not be used as model labels; creation/profile UI should prefer a real note plus model, or provider plus model. Optional `profile.capabilities` filters or hides create-page aspect ratio, resolution, image format, and quality controls for the selected model. Polling Manifest query values can include `{task_id}` so task IDs are sent as real query parameters rather than being embedded into pathname strings. Generation routes must use the selected model key's `manifest_path`; do not merge different request configs under one user-level file. |
| Admin system API smart import | `src/components/admin/api-management-tab.tsx`, `src/app/api/admin/system-apis/smart-import/route.ts`, `src/app/api/admin/system-apis/route.ts`, `src/lib/server-api-config.ts`, `src/lib/user-api-manifest.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/model-capabilities.ts` | The console API management page has a separate `智能配置 API` section for admins, but this section is generic Manifest import only. It supports copy-to-chat-AI and paste-and-import Manifest flow, then creates one independent system API row and `system-api-manifests/<systemApiId>.json` file per imported profile/model. Imported rows resolve the visible API request URL from the Manifest profile/provider before save, and optional `profile.capabilities` can constrain or hide create-page image/video parameter choices for the selected system model. Provider-specific built-in template management, including 元界 AI, belongs in the `系统默认模型` management flow and should not be exposed in the smart import UI. 元界价格/计费方式手动同步 uses `src/app/api/admin/system-apis/yuanjie-pricing/route.ts` and `src/lib/yuanjie-pricing-sync.ts`; it updates only existing `provider = '元界 AI'` image/video rows and leaves mozheAPI/global smart-import configs untouched. |
| Admin system API smart import | `src/components/admin/api-management-tab.tsx`, `src/app/api/admin/system-apis/smart-import/route.ts`, `src/app/api/admin/system-apis/route.ts`, `src/lib/server-api-config.ts`, `src/lib/user-api-manifest.ts`, `src/lib/user-api-manifest-executor.ts`, `src/lib/model-capabilities.ts` | The console API management page has a separate `智能配置 API` section for admins, but this section is generic Manifest import only. It supports copy-to-chat-AI and paste-and-import Manifest flow, then creates one independent system API row and `system-api-manifests/<systemApiId>.json` file per imported profile/model. Imported rows resolve the visible API request URL from the Manifest profile/provider before save, and optional `profile.capabilities` can constrain or hide create-page image/video parameter choices for the selected system model. Provider-specific built-in template management, including 元界 AI, belongs in the `系统默认模型` management flow and should not be exposed in the smart import UI. 元界价格/计费方式手动同步 uses `src/app/api/admin/system-apis/yuanjie-pricing/route.ts` and `src/lib/yuanjie-pricing-sync.ts`; it updates only existing 元界 image/video rows, tolerates provider spellings such as `元界AI`, and leaves mozheAPI/global smart-import configs untouched. |
| Admin console active page persistence | `src/modules/console/pages/console-dashboard-page.tsx` | The console active view is stored in `sessionStorage`, so browser refresh keeps the current admin page/tab. Logout clears the value, and closing/reopening the console starts from the dashboard because `sessionStorage` is tab-scoped. |
| Manifest input image URLs | `src/lib/user-api-manifest-executor.ts`, `src/app/api/generate/image/route.ts`, `src/app/api/generate/video/route.ts` | Manifest templates can use `$inputImages.dataUrls` for raw uploaded data and `$inputImages.urls` for provider-facing public references. The executor converts data URL input images into storage-backed URLs before rendering templates. Image-to-image generation normalizes the primary `image` plus `extraImages` into Manifest `inputImages`, so multi-reference providers such as Yuanjie GPT Image 2 receive all references. |
@@ -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/lib/server-api-config.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. 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/lib/server-api-config.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. 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. |
| 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

View File

@@ -36,6 +36,13 @@ function createFakeClient(rows) {
const text = String(sql);
calls.push({ sql: text, params });
if (text.includes('FROM system_api_configs')) {
if (text.includes('provider = $1')) {
const provider = params[0];
const types = Array.isArray(params[1]) ? params[1] : [];
return {
rows: rows.filter(row => row.provider === provider && types.includes(row.type)),
};
}
return { rows };
}
if (text.includes('UPDATE system_api_configs')) {
@@ -110,15 +117,38 @@ await runTest('sync updates only Yuanjie system API rows and leaves mozheAPI unt
const selectCall = client.calls.find(call => call.sql.includes('FROM system_api_configs'));
assert.ok(selectCall, 'expected a system_api_configs select');
assert.match(selectCall.sql, /provider\s*=\s*\$1/);
assert.equal(selectCall.params[0], YUANJIE_PROVIDER_NAME);
assert.match(selectCall.sql, /type\s*=\s*ANY\(\$1::text\[\]\)/);
assert.match(selectCall.sql, /replace\(lower\(provider\)/);
assert.match(selectCall.sql, /model_group/);
assert.deepEqual(selectCall.params[0], ['image', 'video']);
assert.ok(Array.isArray(selectCall.params[1]));
assert.ok(selectCall.params[1].includes(String(YUANJIE_PROVIDER_NAME).replace(/\s+/g, '').toLowerCase()));
const updateCalls = client.calls.filter(call => call.sql.includes('UPDATE system_api_configs'));
assert.equal(updateCalls.length, 1);
assert.match(updateCalls[0].sql, /provider\s*=\s*\$/);
assert.match(updateCalls[0].sql, /replace\(lower\(provider\)/);
assert.match(updateCalls[0].sql, /model_group/);
assert.equal(updateCalls[0].params.includes('22222222-2222-2222-2222-222222222222'), false);
});
await runTest('sync matches Yuanjie provider name variants used by existing image configs', async () => {
const client = createFakeClient([
{
id: '33333333-3333-3333-3333-333333333333',
provider: '元界AI',
model_group: 'default',
model_name: 'gpt-image-2',
type: 'image',
price_note: '',
},
]);
const result = await syncYuanjiePricingMetadata(client, { type: 'image' });
assert.equal(result.matched, 1);
assert.equal(result.updated, 1);
assert.deepEqual(result.unmatched, []);
});
await runTest('admin page exposes a manual Yuanjie pricing sync button', () => {
const source = read('src/components/admin/api-management-tab.tsx');
assert.match(source, /syncYuanjiePricing/);

View File

@@ -1,10 +1,12 @@
import type { ManagedModelType } from '@/lib/model-config-types';
import {
YUANJIE_MODEL_GROUP,
YUANJIE_IMAGE_MODEL_TEMPLATES,
YUANJIE_PROVIDER_NAME,
type YuanjieImageModelTemplate,
} from '@/lib/yuanjie-image-model-templates';
import {
YUANJIE_VIDEO_MODEL_GROUP,
YUANJIE_VIDEO_MODEL_TEMPLATES,
type YuanjieVideoModelTemplate,
} from '@/lib/yuanjie-video-model-templates';
@@ -51,14 +53,40 @@ const SPECIAL_COST_NOTE_BY_MODEL = new Map<string, string>([
]);
const SYNC_NOTE_PREFIX = '元界计费同步:';
const YUANJIE_PROVIDER_ALIASES = [
YUANJIE_PROVIDER_NAME,
'\u5143\u754c AI',
'\u5143\u754cAI',
'\u934f\u51aa\u666b AI',
'\u934f\u51aa\u666bAI',
];
const YUANJIE_MODEL_GROUPS = [YUANJIE_MODEL_GROUP, YUANJIE_VIDEO_MODEL_GROUP];
function normalizeProvider(value: unknown): string {
return String(value || '').replace(/\s+/g, '').toLowerCase();
}
function getNormalizedProviderAliases(): string[] {
return Array.from(new Set(YUANJIE_PROVIDER_ALIASES.map(normalizeProvider).filter(Boolean)));
}
function isYuanjieProvider(value: unknown): boolean {
const normalized = normalizeProvider(value);
return normalized === normalizeProvider(YUANJIE_PROVIDER_NAME) || normalized.includes('yuanjie') || normalized.includes('元界');
return (
getNormalizedProviderAliases().includes(normalized) ||
normalized.includes('yuanjie') ||
normalized.includes('\u5143\u754c') ||
normalized.includes('\u934f\u51aa\u666b')
);
}
function isYuanjieModelGroup(value: unknown): boolean {
const normalized = String(value || '').trim().toLowerCase();
return normalized === YUANJIE_MODEL_GROUP || normalized === YUANJIE_VIDEO_MODEL_GROUP || normalized.startsWith('yuanjie-');
}
function isYuanjieSystemApiRow(row: Record<string, unknown>): boolean {
return isYuanjieProvider(row.provider) || isYuanjieModelGroup(row.model_group);
}
function buildImagePricingTarget(template: YuanjieImageModelTemplate): YuanjiePricingSyncTarget {
@@ -144,12 +172,18 @@ export async function syncYuanjiePricingMetadata(
): Promise<YuanjiePricingSyncResult> {
const targets = getYuanjiePricingSyncTargets(options.type);
const targetByKey = new Map(targets.map(target => [`${target.type}:${target.modelName}`, target]));
const providerAliases = getNormalizedProviderAliases();
const result = await client.query(
`SELECT id, provider, model_name, type, price_note
`SELECT id, provider, model_group, model_name, type, price_note
FROM system_api_configs
WHERE provider = $1
AND type = ANY($2::text[])`,
[YUANJIE_PROVIDER_NAME, options.type ? [options.type] : ['image', 'video']],
WHERE type = ANY($1::text[])
AND (
replace(lower(provider), ' ', '') = ANY($2::text[])
OR provider ILIKE $3
OR COALESCE(model_group, '') = ANY($4::text[])
OR COALESCE(model_group, '') LIKE 'yuanjie-%'
)`,
[options.type ? [options.type] : ['image', 'video'], providerAliases, '%yuanjie%', YUANJIE_MODEL_GROUPS],
);
let updated = 0;
@@ -157,7 +191,7 @@ export async function syncYuanjiePricingMetadata(
const unmatched: string[] = [];
for (const row of result.rows) {
if (!isYuanjieProvider(row.provider)) {
if (!isYuanjieSystemApiRow(row)) {
continue;
}
const type = row.type === 'video' ? 'video' : row.type === 'image' ? 'image' : null;
@@ -174,8 +208,13 @@ export async function syncYuanjiePricingMetadata(
price_note = $2,
updated_at = NOW()
WHERE id = $3
AND provider = $4`,
[target.billingMode, nextNote, row.id, YUANJIE_PROVIDER_NAME],
AND type = $4
AND (
replace(lower(provider), ' ', '') = ANY($5::text[])
OR COALESCE(model_group, '') = ANY($6::text[])
OR COALESCE(model_group, '') LIKE 'yuanjie-%'
)`,
[target.billingMode, nextNote, row.id, type, providerAliases, YUANJIE_MODEL_GROUPS],
);
updated += updateResult.rowCount || 0;
}