mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 00:46:42 +00:00
feat(task): add model redirection, per-call billing, and multipart retry fix for async tasks
1. Async task model redirection (aligned with sync tasks):
- Integrate ModelMappedHelper in RelayTaskSubmit after model name
determination, populating OriginModelName / UpstreamModelName on RelayInfo.
- All task adaptors now send UpstreamModelName to upstream providers:
- Gemini & Vertex: BuildRequestURL uses UpstreamModelName.
- Doubao & Ali: BuildRequestBody conditionally overwrites body.Model.
- Vidu, Kling, Hailuo, Jimeng: convertToRequestPayload accepts RelayInfo
and unconditionally uses info.UpstreamModelName.
- Sora: BuildRequestBody parses JSON and multipart bodies to replace
the "model" field with UpstreamModelName.
- Frontend log visibility: LogTaskConsumption and taskBillingOther now
emit is_model_mapped / upstream_model_name in the "other" JSON field.
- Billing safety: RecalculateTaskQuotaByTokens reads model name from
BillingContext.OriginModelName (via taskModelName) instead of
task.Data["model"], preventing billing leaks from upstream model names.
2. Per-call billing (TaskPricePatches lifecycle):
- Rename TaskBillingContext.ModelName → OriginModelName; add PerCallBilling
bool field, populated from TaskPricePatches at submission time.
- settleTaskBillingOnComplete short-circuits when PerCallBilling is true,
skipping both adaptor adjustments and token-based recalculation.
- Remove ModelName from TaskSubmitResult; use relayInfo.OriginModelName
consistently in controller/relay.go for billing context and logging.
3. Multipart retry boundary mismatch fix:
- Root cause: after Sora (or OpenAI audio) rebuilds a multipart body with a
new boundary and overwrites c.Request.Header["Content-Type"], subsequent
calls to ParseMultipartFormReusable on retry would parse the cached
original body with the wrong boundary, causing "NextPart: EOF".
- Fix: ParseMultipartFormReusable now caches the original Content-Type in
gin context key "_original_multipart_ct" on first call and reuses it for
all subsequent parses, making multipart parsing retry-safe globally.
- Sora adaptor reverted to the standard pattern (direct header set/get),
which is now safe thanks to the root fix.
4. Tests:
- task_billing_test.go: update makeTask to use OriginModelName; add
PerCallBilling settlement tests (skip adaptor adjust, skip token recalc);
add non-per-call adaptor adjustment test with refund verification.
This commit is contained in:
@@ -84,8 +84,8 @@ function renderDuration(submit_time, finishTime) {
|
||||
|
||||
// 返回带有样式的颜色标签
|
||||
return (
|
||||
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{durationSec} 秒
|
||||
<Tag color={color} shape='circle'>
|
||||
{durationSec} s
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -149,7 +149,7 @@ const renderPlatform = (platform, t) => {
|
||||
);
|
||||
if (option) {
|
||||
return (
|
||||
<Tag color={option.color} shape='circle' prefixIcon={<Video size={14} />}>
|
||||
<Tag color={option.color} shape='circle'>
|
||||
{option.label}
|
||||
</Tag>
|
||||
);
|
||||
@@ -157,13 +157,13 @@ const renderPlatform = (platform, t) => {
|
||||
switch (platform) {
|
||||
case 'suno':
|
||||
return (
|
||||
<Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
|
||||
<Tag color='green' shape='circle'>
|
||||
Suno
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
<Tag color='white' shape='circle'>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -240,7 +240,6 @@ export const getTaskLogsColumns = ({
|
||||
openContentModal,
|
||||
isAdminUser,
|
||||
openVideoModal,
|
||||
showUserInfoFunc,
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
@@ -278,7 +277,6 @@ export const getTaskLogsColumns = ({
|
||||
color={colors[parseInt(text) % colors.length]}
|
||||
size='large'
|
||||
shape='circle'
|
||||
prefixIcon={<Hash size={14} />}
|
||||
onClick={() => {
|
||||
copyText(text);
|
||||
}}
|
||||
@@ -294,7 +292,7 @@ export const getTaskLogsColumns = ({
|
||||
{
|
||||
key: COLUMN_KEYS.USERNAME,
|
||||
title: t('用户'),
|
||||
dataIndex: 'user_id',
|
||||
dataIndex: 'username',
|
||||
render: (userId, record, index) => {
|
||||
if (!isAdminUser) {
|
||||
return <></>;
|
||||
@@ -302,22 +300,14 @@ export const getTaskLogsColumns = ({
|
||||
const displayText = String(record.username || userId || '?');
|
||||
return (
|
||||
<Space>
|
||||
<Tooltip content={displayText}>
|
||||
<Avatar
|
||||
size='extra-small'
|
||||
color={stringToColor(displayText)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => showUserInfoFunc && showUserInfoFunc(userId)}
|
||||
>
|
||||
{displayText.slice(0, 1)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
style={{ cursor: 'pointer', color: 'var(--semi-color-primary)' }}
|
||||
onClick={() => showUserInfoFunc && showUserInfoFunc(userId)}
|
||||
<Avatar
|
||||
size='extra-small'
|
||||
color={stringToColor(displayText)}
|
||||
>
|
||||
{userId}
|
||||
{displayText.slice(0, 1)}
|
||||
</Avatar>
|
||||
<Typography.Text>
|
||||
{displayText}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,6 @@ import TaskLogsActions from './TaskLogsActions';
|
||||
import TaskLogsFilters from './TaskLogsFilters';
|
||||
import ColumnSelectorModal from './modals/ColumnSelectorModal';
|
||||
import ContentModal from './modals/ContentModal';
|
||||
import UserInfoModal from '../usage-logs/modals/UserInfoModal';
|
||||
import { useTaskLogsData } from '../../../hooks/task-logs/useTaskLogsData';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { createCardProPagination } from '../../../helpers/utils';
|
||||
@@ -46,7 +45,6 @@ const TaskLogsPage = () => {
|
||||
modalContent={taskLogsData.videoUrl}
|
||||
isVideo={true}
|
||||
/>
|
||||
<UserInfoModal {...taskLogsData} />
|
||||
|
||||
<Layout>
|
||||
<CardPro
|
||||
|
||||
Reference in New Issue
Block a user