Commit 3fcd741c by Seefs Committed by GitHub

refactor: codex usage ui (#5595)

* refactor: codex usage ui

* feat: show Codex reset credit details

* feat: add Codex usage reset flow
parent 6bd69f3e
......@@ -18,6 +18,46 @@ import (
)
func GetCodexChannelUsage(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.FetchCodexWhamUsage,
"failed to fetch codex usage",
"获取用量信息失败,请稍后重试",
)
}
func GetCodexChannelRateLimitResetCredits(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.FetchCodexWhamRateLimitResetCredits,
"failed to fetch codex reset credits",
"获取重置次数详情失败,请稍后重试",
)
}
func ResetCodexChannelUsage(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.ConsumeCodexWhamRateLimitResetCredit,
"failed to reset codex usage",
"重置用量失败,请稍后重试",
)
}
type codexWhamFetchFunc func(
ctx context.Context,
client *http.Client,
baseURL string,
accessToken string,
accountID string,
) (statusCode int, body []byte, err error)
func fetchCodexChannelWhamData(
c *gin.Context,
fetch codexWhamFetchFunc,
logPrefix string,
userMessage string,
) {
channelId, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, fmt.Errorf("invalid channel id: %w", err))
......@@ -68,10 +108,10 @@ func GetCodexChannelUsage(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
statusCode, body, err := service.FetchCodexWhamUsage(ctx, client, ch.GetBaseURL(), accessToken, accountID)
statusCode, body, err := fetch(ctx, client, ch.GetBaseURL(), accessToken, accountID)
if err != nil {
common.SysError("failed to fetch codex usage: " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取用量信息失败,请稍后重试"})
common.SysError(logPrefix + ": " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": userMessage})
return
}
......@@ -98,10 +138,10 @@ func GetCodexChannelUsage(c *gin.Context) {
ctx2, cancel2 := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel2()
statusCode, body, err = service.FetchCodexWhamUsage(ctx2, client, ch.GetBaseURL(), oauthKey.AccessToken, accountID)
statusCode, body, err = fetch(ctx2, client, ch.GetBaseURL(), oauthKey.AccessToken, accountID)
if err != nil {
common.SysError("failed to fetch codex usage after refresh: " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取用量信息失败,请稍后重试"})
common.SysError(logPrefix + " after refresh: " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": userMessage})
return
}
}
......
......@@ -251,6 +251,8 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels)
channelRoute.POST("/:id/codex/refresh", controller.RefreshCodexChannelCredential)
channelRoute.GET("/:id/codex/usage", controller.GetCodexChannelUsage)
channelRoute.GET("/:id/codex/usage/reset-credits", controller.GetCodexChannelRateLimitResetCredits)
channelRoute.POST("/:id/codex/usage/reset", controller.ResetCodexChannelUsage)
channelRoute.POST("/ollama/pull", controller.OllamaPullModel)
channelRoute.POST("/ollama/pull/stream", controller.OllamaPullModelStream)
channelRoute.DELETE("/ollama/delete", controller.OllamaDeleteModel)
......
package service
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/google/uuid"
)
func FetchCodexWhamUsage(
......@@ -35,13 +39,50 @@ func FetchCodexWhamUsage(
if err != nil {
return 0, nil, err
}
req.Header.Set("Authorization", "Bearer "+at)
req.Header.Set("chatgpt-account-id", aid)
req.Header.Set("Accept", "application/json")
if req.Header.Get("originator") == "" {
req.Header.Set("originator", "codex_cli_rs")
setCodexWhamRequestHeaders(req, at, aid)
resp, err := client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, nil, err
}
return resp.StatusCode, body, nil
}
func FetchCodexWhamRateLimitResetCredits(
ctx context.Context,
client *http.Client,
baseURL string,
accessToken string,
accountID string,
) (statusCode int, body []byte, err error) {
if client == nil {
return 0, nil, fmt.Errorf("nil http client")
}
bu := strings.TrimRight(strings.TrimSpace(baseURL), "/")
if bu == "" {
return 0, nil, fmt.Errorf("empty baseURL")
}
at := strings.TrimSpace(accessToken)
aid := strings.TrimSpace(accountID)
if at == "" {
return 0, nil, fmt.Errorf("empty accessToken")
}
if aid == "" {
return 0, nil, fmt.Errorf("empty accountID")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bu+"/backend-api/wham/rate-limit-reset-credits", nil)
if err != nil {
return 0, nil, err
}
setCodexWhamRequestHeaders(req, at, aid)
resp, err := client.Do(req)
if err != nil {
return 0, nil, err
......@@ -54,3 +95,67 @@ func FetchCodexWhamUsage(
}
return resp.StatusCode, body, nil
}
func ConsumeCodexWhamRateLimitResetCredit(
ctx context.Context,
client *http.Client,
baseURL string,
accessToken string,
accountID string,
) (statusCode int, body []byte, err error) {
if client == nil {
return 0, nil, fmt.Errorf("nil http client")
}
bu := strings.TrimRight(strings.TrimSpace(baseURL), "/")
if bu == "" {
return 0, nil, fmt.Errorf("empty baseURL")
}
at := strings.TrimSpace(accessToken)
aid := strings.TrimSpace(accountID)
if at == "" {
return 0, nil, fmt.Errorf("empty accessToken")
}
if aid == "" {
return 0, nil, fmt.Errorf("empty accountID")
}
requestBody, err := common.Marshal(map[string]string{
"redeem_request_id": uuid.NewString(),
})
if err != nil {
return 0, nil, err
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
bu+"/backend-api/wham/rate-limit-reset-credits/consume",
bytes.NewReader(requestBody),
)
if err != nil {
return 0, nil, err
}
setCodexWhamRequestHeaders(req, at, aid)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, nil, err
}
return resp.StatusCode, body, nil
}
func setCodexWhamRequestHeaders(req *http.Request, accessToken string, accountID string) {
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("chatgpt-account-id", accountID)
req.Header.Set("Accept", "application/json")
if req.Header.Get("originator") == "" {
req.Header.Set("originator", "codex_cli_rs")
}
}
......@@ -25,7 +25,6 @@ import {
Typography,
Spin,
Tag,
Descriptions,
Collapse,
} from '@douyinfe/semi-ui';
import { API, showError } from '../../../../helpers';
......@@ -33,6 +32,21 @@ import { MOBILE_BREAKPOINT } from '../../../../hooks/common/useIsMobile';
const { Text } = Typography;
const CODEX_USAGE_MODAL_CLASS_NAME = 'codex-usage-modal';
const CodexUsageModalStyles = () => (
<style>
{`
@media (max-width: ${MOBILE_BREAKPOINT - 1}px) {
.${CODEX_USAGE_MODAL_CLASS_NAME} .semi-modal-body.semi-modal-withIcon {
margin-left: 0 !important;
width: auto !important;
}
}
`}
</style>
);
const clampPercent = (value) => {
const v = Number(value);
if (!Number.isFinite(v)) return 0;
......@@ -146,12 +160,12 @@ const getCodexUsageModalLayout = () => {
return {
width: 'calc(100vw - 16px)',
style: {
top: 8,
top: 0,
maxWidth: 'calc(100vw - 16px)',
margin: '0 auto',
margin: '8px auto',
},
bodyStyle: {
maxHeight: 'calc(100vh - 148px)',
maxHeight: 'calc(100vh - 164px)',
overflowY: 'auto',
padding: '16px 16px 12px',
},
......@@ -161,11 +175,12 @@ const getCodexUsageModalLayout = () => {
return {
width: 900,
style: {
top: 24,
top: 0,
margin: '16px auto',
maxWidth: 'min(900px, 92vw)',
},
bodyStyle: {
maxHeight: 'calc(100vh - 172px)',
maxHeight: 'calc(100vh - 188px)',
overflowY: 'auto',
padding: '20px 24px 16px',
},
......@@ -220,34 +235,70 @@ const resolveUsageStatusTag = (t, rateLimit) => {
return <Tag color='red'>{tt('受限')}</Tag>;
};
const AccountInfoValue = ({ t, value, onCopy, monospace = false }) => {
const InfoField = ({
t,
label,
value,
onCopy,
monospace = false,
className = '',
}) => {
const tt = typeof t === 'function' ? t : (v) => v;
const text = getDisplayText(value);
const hasValue = text !== '';
return (
<div className='flex min-w-0 items-start justify-between gap-2'>
<div
className={`min-w-0 flex-1 break-all text-xs leading-5 text-semi-color-text-1 ${
monospace ? 'font-mono' : ''
className={`min-w-0 rounded-lg bg-semi-color-bg-0 px-3 py-2 shadow-[0_1px_2px_rgba(0,0,0,0.04)] ${className}`}
>
<div className='text-[11px] font-medium text-semi-color-text-2'>
{label}
</div>
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
<div
className={`min-w-0 flex-1 break-all text-xs leading-5 text-semi-color-text-0 ${
monospace ? 'font-mono [font-variant-numeric:tabular-nums]' : ''
}`}
>
{hasValue ? text : '-'}
</div>
{onCopy ? (
<Button
size='small'
type='tertiary'
theme='borderless'
className='shrink-0 px-1 text-xs'
className='h-6 shrink-0 px-1 text-xs'
disabled={!hasValue}
onClick={() => onCopy?.(text)}
onClick={() => onCopy(text)}
>
{tt('复制')}
</Button>
) : null}
</div>
</div>
);
};
const SectionHeading = ({ title, description, children }) => (
<div className='flex flex-wrap items-start justify-between gap-3'>
<div className='min-w-0'>
<div className='text-sm font-semibold text-semi-color-text-0'>
{title}
</div>
{description ? (
<div className='mt-1 text-xs leading-5 text-semi-color-text-2'>
{description}
</div>
) : null}
</div>
{children ? (
<div className='flex shrink-0 flex-wrap items-center gap-2'>
{children}
</div>
) : null}
</div>
);
const RateLimitWindowCard = ({ t, title, windowData }) => {
const tt = typeof t === 'function' ? t : (v) => v;
const hasWindowData =
......@@ -260,13 +311,30 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
const limitWindowSeconds = windowData?.limit_window_seconds;
return (
<div className='rounded-lg border border-semi-color-border bg-semi-color-bg-0 p-3'>
<div className='flex flex-wrap items-start justify-between gap-x-3 gap-y-1'>
<div className='font-medium'>{title}</div>
<Text type='tertiary' size='small'>
{tt('重置时间:')}
{formatUnixSeconds(resetAt)}
</Text>
<div className='rounded-lg bg-semi-color-bg-0 p-3 shadow-[0_1px_2px_rgba(0,0,0,0.04)]'>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0'>
<div className='text-sm font-semibold text-semi-color-text-0'>
{title}
</div>
<div className='mt-1 text-xs text-semi-color-text-2'>
{tt('窗口:')}
{hasWindowData
? formatDurationSeconds(limitWindowSeconds, tt)
: '-'}
</div>
</div>
<div className='shrink-0 text-right'>
<div
className='text-xl font-semibold leading-none [font-variant-numeric:tabular-nums]'
style={{ color: pickStrokeColor(percent) }}
>
{hasWindowData ? `${percent}%` : '-'}
</div>
<div className='mt-1 text-[11px] text-semi-color-text-2'>
{tt('已使用')}
</div>
</div>
</div>
{hasWindowData ? (
......@@ -274,25 +342,25 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
<Progress
percent={percent}
stroke={pickStrokeColor(percent)}
showInfo={true}
showInfo={false}
/>
</div>
) : (
<div className='mt-3 text-sm text-semi-color-text-2'>-</div>
)}
<div className='mt-1 flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
<div>
{tt('已使用:')}
{hasWindowData ? `${percent}%` : '-'}
<div className='mt-3 grid grid-cols-1 gap-2 text-xs text-semi-color-text-2 sm:grid-cols-2'>
<div className='min-w-0'>
<div className='text-[11px]'>{tt('重置时间')}</div>
<div className='break-all text-semi-color-text-0 [font-variant-numeric:tabular-nums]'>
{hasWindowData ? formatUnixSeconds(resetAt) : '-'}
</div>
<div>
{tt('距离重置:')}
</div>
<div className='min-w-0 sm:text-right'>
<div className='text-[11px]'>{tt('距离重置')}</div>
<div className='text-semi-color-text-0 [font-variant-numeric:tabular-nums]'>
{hasWindowData ? formatDurationSeconds(resetAfterSeconds, tt) : '-'}
</div>
<div>
{tt('窗口:')}
{hasWindowData ? formatDurationSeconds(limitWindowSeconds, tt) : '-'}
</div>
</div>
</div>
......@@ -332,20 +400,12 @@ const RateLimitGroupSection = ({
const featureText = getDisplayText(meteredFeature);
return (
<section className='space-y-3'>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div className='min-w-0 space-y-2'>
<div className='flex flex-wrap items-center gap-2'>
<div className='text-sm font-semibold text-semi-color-text-0'>
{title}
</div>
<section className='space-y-3 rounded-lg bg-semi-color-fill-0 p-3'>
<SectionHeading title={title} description={description}>
{statusTag}
</div>
{(description || featureText) && (
<div className='flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
{description ? <span>{description}</span> : null}
</SectionHeading>
{featureText ? (
<div className='inline-flex max-w-full items-center gap-2 rounded-full bg-semi-color-fill-0 px-2 py-1'>
<div className='inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg bg-semi-color-bg-0 px-2 py-1 shadow-[0_1px_2px_rgba(0,0,0,0.04)]'>
<span className='text-[11px] text-semi-color-text-2'>
metered_feature
</span>
......@@ -354,10 +414,6 @@ const RateLimitGroupSection = ({
</span>
</div>
) : null}
</div>
)}
</div>
</div>
<RateLimitWindowGrid
t={tt}
......@@ -386,7 +442,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
const statusTag = resolveUsageStatusTag(tt, rateLimit);
const userId = data?.user_id;
const email = data?.email;
const accountId = data?.account_id;
const channelLabel = `${record?.name || '-'} (#${record?.id || '-'})`;
const resetCredits = data?.rate_limit_reset_credits?.available_count;
const errorMessage =
payload?.success === false
? getDisplayText(payload?.message) || tt('获取用量失败')
......@@ -398,16 +455,27 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
return (
<div className='flex flex-col gap-4'>
{errorMessage && (
<div className='rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'>
<div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'>
{errorMessage}
</div>
)}
<div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-3'>
<div className='flex flex-wrap items-start justify-between gap-2'>
<div className='rounded-lg bg-semi-color-fill-0 p-4'>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0'>
<div className='text-xs font-medium text-semi-color-text-2'>
{tt('Codex 帐号')}
{tt('Codex 帐号状态')}
</div>
</div>
<Button
size='small'
type='tertiary'
theme='outline'
onClick={onRefresh}
className='shrink-0'
>
{tt('刷新')}
</Button>
</div>
<div className='mt-2 flex flex-wrap items-center gap-2'>
<Tag
......@@ -421,86 +489,59 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
</Tag>
{statusTag}
<Tag color='grey' type='light' shape='circle'>
{tt('上游状态码:')}
{upstreamStatus ?? '-'}
HTTP {upstreamStatus ?? '-'}
</Tag>
</div>
</div>
<Button
size='small'
type='tertiary'
theme='outline'
onClick={onRefresh}
<Tag
color={Number(resetCredits) > 0 ? 'blue' : 'grey'}
type='light'
shape='circle'
>
{tt('刷新')}
</Button>
{tt('重置次数:')}
{Number.isFinite(Number(resetCredits)) ? String(resetCredits) : '-'}
</Tag>
{data?.credits?.overage_limit_reached ? (
<Tag color='red' type='light' shape='circle'>
{tt('超额受限')}
</Tag>
) : null}
{data?.spend_control?.reached ? (
<Tag color='red' type='light' shape='circle'>
{tt('消费受限')}
</Tag>
) : null}
</div>
<div className='mt-2 rounded-lg bg-semi-color-fill-0 px-3 py-2'>
<Descriptions>
<Descriptions.Item itemKey='User ID'>
<AccountInfoValue
<div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
<InfoField t={tt} label={tt('邮箱')} value={email} onCopy={onCopy} />
<InfoField t={tt} label={tt('渠道')} value={channelLabel} />
<InfoField
t={tt}
label='User ID'
value={userId}
onCopy={onCopy}
monospace={true}
className='md:col-span-2'
/>
</Descriptions.Item>
<Descriptions.Item itemKey={tt('邮箱')}>
<AccountInfoValue t={tt} value={email} onCopy={onCopy} />
</Descriptions.Item>
<Descriptions.Item itemKey='Account ID'>
<AccountInfoValue
t={tt}
value={accountId}
onCopy={onCopy}
monospace={true}
/>
</Descriptions.Item>
</Descriptions>
</div>
<div className='mt-2 text-xs text-semi-color-text-2'>
{tt('渠道:')}
{record?.name || '-'} ({tt('编号:')}
{record?.id || '-'})
</div>
</div>
<div>
<div className='mb-2'>
<div className='text-sm font-semibold text-semi-color-text-0'>
{tt('额度窗口')}
</div>
<Text type='tertiary' size='small'>
{tt(
'用于观察当前帐号在 Codex 上游的基础限额与附加计费能力使用情况',
)}
</Text>
</div>
</div>
<div className='space-y-5'>
<RateLimitGroupSection
t={tt}
<div className='space-y-3'>
<SectionHeading
title={tt('基础额度')}
description={tt('当前帐号的基础额度窗口')}
rateLimitSource={data}
statusTag={statusTag}
/>
{additionalRateLimits.length > 0 ? (
<div className='space-y-4 border-t border-semi-color-border pt-4'>
<div>
<div className='text-sm font-semibold text-semi-color-text-0'>
{tt('附加额度')}
</div>
<Text type='tertiary' size='small'>
{tt('按模型或能力拆分的附加计费能力窗口')}
</Text>
>
{statusTag}
</SectionHeading>
<RateLimitWindowGrid t={tt} {...resolveRateLimitWindows(data)} />
</div>
<div className='space-y-4'>
{additionalRateLimits.length > 0 ? (
<div className='space-y-3'>
<SectionHeading
title={tt('附加额度')}
description={tt('按模型或能力拆分的附加计费能力窗口')}
/>
<div className='space-y-3'>
{additionalRateLimits.map((item, index) => {
const limitName =
getDisplayText(item?.limit_name) ||
......@@ -508,13 +549,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
`${tt('附加额度')} ${index + 1}`;
return (
<div
key={`${limitName}-${getDisplayText(item?.metered_feature)}-${index}`}
className={
index > 0 ? 'border-t border-semi-color-border pt-4' : ''
}
>
<RateLimitGroupSection
key={`${limitName}-${getDisplayText(item?.metered_feature)}-${index}`}
t={tt}
title={limitName}
description={tt('附加计费能力')}
......@@ -522,13 +558,11 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
statusTag={resolveUsageStatusTag(tt, item?.rate_limit)}
meteredFeature={item?.metered_feature}
/>
</div>
);
})}
</div>
</div>
) : null}
</div>
<Collapse
activeKey={showRawJson ? ['raw-json'] : []}
......@@ -536,6 +570,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
const keys = Array.isArray(activeKey) ? activeKey : [activeKey];
setShowRawJson(keys.includes('raw-json'));
}}
className='rounded-lg border border-semi-color-border bg-semi-color-bg-0'
>
<Collapse.Panel header={tt('原始 JSON')} itemKey='raw-json'>
<div className='mb-2 flex justify-end'>
......@@ -549,7 +584,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
{tt('复制')}
</Button>
</div>
<pre className='max-h-[50vh] overflow-y-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
<pre className='max-h-[44vh] overflow-y-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0 [font-variant-numeric:tabular-nums]'>
{rawText}
</pre>
</Collapse.Panel>
......@@ -609,14 +644,19 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
if (loading) {
return (
<>
<CodexUsageModalStyles />
<div className='flex items-center justify-center py-10'>
<Spin spinning={true} size='large' tip={tt('加载中...')} />
</div>
</>
);
}
if (!payload) {
return (
<>
<CodexUsageModalStyles />
<div className='flex flex-col gap-3'>
<Text type='danger'>{tt('获取用量失败')}</Text>
<div className='flex justify-end'>
......@@ -630,10 +670,13 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
</Button>
</div>
</div>
</>
);
}
return (
<>
<CodexUsageModalStyles />
<CodexUsageView
t={tt}
record={record}
......@@ -641,6 +684,7 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
onCopy={onCopy}
onRefresh={fetchUsage}
/>
</>
);
};
......@@ -650,6 +694,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
Modal.info({
title: tt('Codex 帐号与用量'),
className: CODEX_USAGE_MODAL_CLASS_NAME,
centered: false,
width: layout.width,
style: layout.style,
......
......@@ -53,6 +53,10 @@ export type CodexUsageResponse = {
data?: Record<string, unknown>
}
export type CodexResetCreditsResponse = CodexUsageResponse
export type CodexUsageResetResponse = CodexUsageResponse
export type CodexCredentialRefreshResponse = {
success: boolean
message?: string
......@@ -287,6 +291,27 @@ export async function getCodexUsage(
return res.data
}
export async function getCodexResetCredits(
channelId: number
): Promise<CodexResetCreditsResponse> {
const res = await api.get(
`/api/channel/${channelId}/codex/usage/reset-credits`,
channelActionConfig({ disableDuplicate: true })
)
return res.data
}
export async function resetCodexUsage(
channelId: number
): Promise<CodexUsageResetResponse> {
const res = await api.post(
`/api/channel/${channelId}/codex/usage/reset`,
{},
channelActionConfig({ disableDuplicate: true })
)
return res.data
}
// ============================================================================
// Multi-Key Management
// ============================================================================
......
......@@ -16,25 +16,53 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useMemo, useState } from 'react'
import { type ReactNode, useCallback, useMemo, useState } from 'react'
import {
Copy,
Check,
RefreshCw,
ChevronDown,
ChevronUp,
User,
Mail,
Hash,
RotateCcw,
AlertTriangle,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import dayjs from '@/lib/dayjs'
import { formatDateTimeStr, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty'
import { Progress } from '@/components/ui/progress'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { Dialog } from '@/components/dialog'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import {
getCodexResetCredits,
resetCodexUsage,
type CodexResetCreditsResponse,
} from '../../api'
type CodexRateLimitWindow = {
used_percent?: number
......@@ -60,13 +88,41 @@ type CodexAdditionalRateLimit = {
plan_type?: string
}
type CodexResetCredit = {
id?: string
reset_type?: string
status?: string
granted_at?: string | null
expires_at?: string | null
redeem_started_at?: string | null
redeemed_at?: string | null
profile_image_url?: string
profile_user_id?: string
title?: string
description?: string
}
type CodexResetCreditsPayload = {
credits?: CodexResetCredit[]
available_count?: number
total_earned_count?: number
}
type CodexUsagePayload = {
plan_type?: string
user_id?: string
email?: string
account_id?: string
rate_limit?: CodexRateLimit
additional_rate_limits?: CodexAdditionalRateLimit[]
rate_limit_reset_credits?: {
available_count?: number
}
credits?: {
overage_limit_reached?: boolean
}
spend_control?: {
reached?: boolean
}
}
export type CodexUsageDialogData = {
......@@ -82,7 +138,7 @@ type CodexUsageDialogProps = {
channelName?: string
channelId?: number
response: CodexUsageDialogData | null
onRefresh?: () => void
onRefresh?: () => void | Promise<void>
isRefreshing?: boolean
}
......@@ -93,12 +149,14 @@ function clampPercent(value: unknown): number {
function formatUnixSeconds(unixSeconds: unknown): string {
const v = Number(unixSeconds)
if (!Number.isFinite(v) || v <= 0) return '-'
try {
return dayjs(v * 1000).format('YYYY-MM-DD HH:mm:ss')
} catch {
return String(unixSeconds)
}
return Number.isFinite(v) && v > 0 ? formatTimestampToDate(v) : '-'
}
function formatIsoTimestamp(value: unknown): string {
if (typeof value !== 'string' || value.trim() === '') return '-'
const d = dayjs(value)
if (!d.isValid()) return value
return formatDateTimeStr(d.toDate())
}
function formatDurationSeconds(
......@@ -118,11 +176,64 @@ function formatDurationSeconds(
return `${secs}${t('s')}`
}
function formatTimeLeftUntil(
value: unknown,
t: (key: string) => string
): string {
if (typeof value !== 'string' || value.trim() === '') return '-'
const expiresAt = dayjs(value)
if (!expiresAt.isValid()) return '-'
const secondsLeft = expiresAt.diff(dayjs(), 'second')
if (secondsLeft <= 0) return t('Expired')
const days = Math.floor(secondsLeft / (24 * 60 * 60))
const remainingSeconds = secondsLeft % (24 * 60 * 60)
if (days > 0) {
const hours = Math.floor(remainingSeconds / 3600)
return `${days} ${t('days')} ${hours}${t('h')}`
}
return formatDurationSeconds(secondsLeft, t)
}
function normalizePlanType(value: unknown): string {
if (value == null) return ''
return String(value).trim().toLowerCase()
}
function parseTimeValue(value: unknown): number {
if (typeof value !== 'string' || value.trim() === '') {
return Number.POSITIVE_INFINITY
}
const d = dayjs(value)
return d.isValid() ? d.valueOf() : Number.POSITIVE_INFINITY
}
function normalizeResetCreditStatus(value: unknown): string {
return String(value || '')
.trim()
.toLowerCase()
}
function sortResetCredits(credits: CodexResetCredit[]): CodexResetCredit[] {
return [...credits].sort((a, b) => {
const aAvailable = normalizeResetCreditStatus(a.status) === 'available'
const bAvailable = normalizeResetCreditStatus(b.status) === 'available'
if (aAvailable !== bAvailable) return aAvailable ? -1 : 1
const expiresDiff =
parseTimeValue(a.expires_at) - parseTimeValue(b.expires_at)
if (expiresDiff !== 0) return expiresDiff
const grantedDiff =
parseTimeValue(a.granted_at) - parseTimeValue(b.granted_at)
if (grantedDiff !== 0) return grantedDiff
return String(a.id || '').localeCompare(String(b.id || ''))
})
}
function classifyWindowByDuration(
windowData?: CodexRateLimitWindow | null
): 'weekly' | 'fiveHour' | null {
......@@ -190,6 +301,15 @@ const PLAN_TYPE_BADGE: Record<
free: { label: 'Free', variant: 'warning' },
}
const RESET_CREDIT_STATUS_BADGE: Record<
string,
{ label: string; variant: StatusBadgeProps['variant'] }
> = {
available: { label: 'Available', variant: 'success' },
redeemed: { label: 'Redeemed', variant: 'neutral' },
expired: { label: 'Expired', variant: 'warning' },
}
function getAccountTypeBadge(
value: unknown,
t: (key: string) => string
......@@ -203,6 +323,19 @@ function getAccountTypeBadge(
)
}
function getResetCreditStatusBadge(
value: unknown,
t: (key: string) => string
): { label: string; variant: StatusBadgeProps['variant'] } {
const normalized = normalizeResetCreditStatus(value)
return (
RESET_CREDIT_STATUS_BADGE[normalized] ?? {
label: String(value || '') || t('Unknown'),
variant: 'neutral' as const,
}
)
}
function windowLabel(windowData?: CodexRateLimitWindow | null) {
const percent = clampPercent(windowData?.used_percent)
const variant: StatusBadgeProps['variant'] =
......@@ -210,6 +343,54 @@ function windowLabel(windowData?: CodexRateLimitWindow | null) {
return { percent, variant }
}
function getUsageStatusBadge(
rateLimit: CodexRateLimit | undefined,
t: (key: string) => string
) {
if (!rateLimit || Object.keys(rateLimit).length === 0) {
return (
<StatusBadge label={t('Pending')} variant='neutral' copyable={false} />
)
}
if (rateLimit.allowed && !rateLimit.limit_reached) {
return (
<StatusBadge label={t('Available')} variant='success' copyable={false} />
)
}
return <StatusBadge label={t('Limited')} variant='danger' copyable={false} />
}
function formatLabelValue(label: string, value: string) {
return label.endsWith(':') ? `${label}${value}` : `${label} ${value}`
}
const percentTextClassName: Record<
NonNullable<StatusBadgeProps['variant']>,
string
> = {
success: 'text-success',
warning: 'text-warning',
danger: 'text-destructive',
info: 'text-info',
neutral: 'text-muted-foreground',
purple: 'text-chart-4',
amber: 'text-warning',
blue: 'text-chart-1',
cyan: 'text-chart-2',
green: 'text-success',
grey: 'text-muted-foreground',
indigo: 'text-chart-1',
'light-blue': 'text-info',
'light-green': 'text-success',
lime: 'text-chart-3',
orange: 'text-warning',
pink: 'text-chart-5',
red: 'text-destructive',
teal: 'text-chart-2',
violet: 'text-chart-4',
yellow: 'text-warning',
}
type RateLimitWindowProps = {
title: string
window?: CodexRateLimitWindow | null
......@@ -224,34 +405,107 @@ function RateLimitWindow(props: RateLimitWindowProps) {
const { percent, variant } = windowLabel(props.window)
return (
<div className='rounded-lg border p-4'>
<div className='flex items-center justify-between gap-2'>
<div className='text-sm font-medium'>{props.title}</div>
<StatusBadge label={`${percent}%`} variant={variant} copyable={false} />
<Card size='sm' className='gap-0 py-0'>
<CardHeader className='p-3 pb-2'>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0'>
<CardTitle className='text-sm font-semibold'>
{props.title}
</CardTitle>
<CardDescription className='mt-1 text-xs'>
{t('Window:')}{' '}
{hasData
? formatDurationSeconds(props.window?.limit_window_seconds, t)
: '-'}
</CardDescription>
</div>
<div className='shrink-0 text-right'>
<div
className={cn(
'text-xl leading-none font-semibold tabular-nums',
percentTextClassName[variant ?? 'neutral']
)}
>
{hasData ? `${percent}%` : '-'}
</div>
<div className='text-muted-foreground mt-1 text-[11px]'>
{t('Used')}
</div>
</div>
<div className='mt-3'>
</div>
</CardHeader>
<CardContent className='p-3 pt-0'>
{hasData ? (
<Progress
value={percent}
aria-label={`${props.title} usage: ${percent}%`}
className='mt-1'
/>
) : (
<div className='text-muted-foreground mt-1 text-sm'>-</div>
)}
<div className='mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-2'>
<div className='min-w-0'>
<div className='text-muted-foreground text-[11px]'>
{t('Reset at:')}
</div>
{hasData ? (
<div className='text-muted-foreground mt-2 space-y-1 text-xs'>
<div>
{t('Reset at:')} {formatUnixSeconds(props.window?.reset_at)}
<div className='break-all tabular-nums'>
{hasData ? formatUnixSeconds(props.window?.reset_at) : '-'}
</div>
<div>
{t('Resets in:')}{' '}
{formatDurationSeconds(props.window?.reset_after_seconds, t)}
</div>
<div>
{t('Window:')}{' '}
{formatDurationSeconds(props.window?.limit_window_seconds, t)}
<div className='min-w-0 sm:text-right'>
<div className='text-muted-foreground text-[11px]'>
{t('Resets in:')}
</div>
<div className='tabular-nums'>
{hasData
? formatDurationSeconds(props.window?.reset_after_seconds, t)
: '-'}
</div>
) : (
<div className='text-muted-foreground mt-2 text-xs'>-</div>
)}
</div>
</div>
</CardContent>
</Card>
)
}
function RateLimitWindowGrid(props: {
fiveHourWindow?: CodexRateLimitWindow | null
weeklyWindow?: CodexRateLimitWindow | null
}) {
const { t } = useTranslation()
return (
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
<RateLimitWindow
title={t('5-Hour Window')}
window={props.fiveHourWindow}
/>
<RateLimitWindow title={t('Weekly Window')} window={props.weeklyWindow} />
</div>
)
}
function SectionHeading(props: {
title: string
description?: string
children?: ReactNode
}) {
return (
<div className='flex flex-wrap items-start justify-between gap-3'>
<div className='min-w-0'>
<div className='text-sm font-semibold'>{props.title}</div>
{props.description ? (
<div className='text-muted-foreground mt-1 text-xs leading-5'>
{props.description}
</div>
) : null}
</div>
{props.children ? (
<div className='flex shrink-0 flex-wrap items-center gap-2'>
{props.children}
</div>
) : null}
</div>
)
}
......@@ -266,72 +520,310 @@ type RateLimitGroupSectionProps = {
function RateLimitGroupSection(props: RateLimitGroupSectionProps) {
const { t } = useTranslation()
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(props.source)
const statusBadge = getUsageStatusBadge(props.source?.rate_limit, t)
return (
<section className='space-y-3'>
<div className='space-y-1'>
<div className='text-sm font-semibold'>{props.title}</div>
{(props.description || props.meteredFeature) && (
<div className='text-muted-foreground flex flex-wrap items-center gap-2 text-xs'>
{props.description && <span>{props.description}</span>}
{props.meteredFeature && (
<span className='bg-muted/60 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-0.5'>
<span className='text-[11px]'>metered_feature</span>
<span className='min-w-0 font-mono text-xs break-all'>
{props.meteredFeature}
<section className='bg-muted/40 flex flex-col gap-3 rounded-xl p-3'>
<SectionHeading title={props.title} description={props.description}>
{statusBadge}
</SectionHeading>
{props.meteredFeature ? (
<div className='bg-background ring-border/60 inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg px-2 py-1 text-xs ring-1'>
<span className='text-muted-foreground text-[11px]'>
metered_feature
</span>
<span className='min-w-0 font-mono break-all'>
{props.meteredFeature}
</span>
)}
</div>
)}
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<RateLimitWindow title={t('5-Hour Window')} window={fiveHourWindow} />
<RateLimitWindow title={t('Weekly Window')} window={weeklyWindow} />
</div>
) : null}
<RateLimitWindowGrid
fiveHourWindow={fiveHourWindow}
weeklyWindow={weeklyWindow}
/>
</section>
)
}
function CopyableField(props: {
icon: React.ReactNode
function InfoField(props: {
label: string
value?: string | null
mono?: boolean
copyable?: boolean
className?: string
}) {
const { t } = useTranslation()
const { copyToClipboard, copiedText } = useCopyToClipboard({ notify: false })
const text = props.value?.trim() || ''
const hasCopied = copiedText === text
return (
<div className='flex items-center justify-between gap-2 py-1'>
<div className='flex min-w-0 items-center gap-2'>
<span className='text-muted-foreground flex-shrink-0'>
{props.icon}
</span>
<span className='text-muted-foreground flex-shrink-0 text-xs'>
<div
className={cn(
'bg-background ring-border/60 min-w-0 rounded-lg p-3 ring-1',
props.className
)}
>
<div className='text-muted-foreground text-[11px] font-medium'>
{props.label}
</span>
</div>
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
<span
className={`min-w-0 truncate text-xs ${props.mono ? 'font-mono' : ''}`}
className={cn(
'min-w-0 flex-1 text-xs leading-5 break-all',
props.mono && 'font-mono tabular-nums'
)}
>
{text || '-'}
</span>
</div>
{text && (
{props.copyable !== false && text ? (
<Button
type='button'
variant='ghost'
size='sm'
className='h-6 w-6 flex-shrink-0 p-0'
size='icon-xs'
aria-label={t('Copy')}
onClick={() => copyToClipboard(text)}
>
{hasCopied ? (
<Check className='h-3 w-3 text-green-600' />
) : (
<Copy className='h-3 w-3' />
{hasCopied ? <Check className='text-success' /> : <Copy />}
</Button>
) : null}
</div>
</div>
)
}
function ResetCreditTimeField(props: {
label: string
value: string
emphasis?: boolean
}) {
return (
<div className='min-w-0'>
<div className='text-muted-foreground text-[11px] font-medium'>
{props.label}
</div>
<div
className={cn(
'mt-1 text-xs leading-5 tabular-nums',
props.emphasis ? 'font-semibold' : 'text-foreground'
)}
>
{props.value}
</div>
</div>
)
}
function ResetCreditItem(props: { credit: CodexResetCredit; index: number }) {
const { t } = useTranslation()
const statusBadge = getResetCreditStatusBadge(props.credit.status, t)
const title =
props.credit.title?.trim() || `${t('Reset Credit')} ${props.index + 1}`
const expiresIn = formatTimeLeftUntil(props.credit.expires_at, t)
const isAvailable =
normalizeResetCreditStatus(props.credit.status) === 'available'
return (
<div className='bg-background rounded-lg border p-3'>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div className='min-w-0'>
<div className='flex flex-wrap items-center gap-2'>
<div className='min-w-0 text-sm font-medium break-words'>
{title}
</div>
<StatusBadge
label={t(statusBadge.label)}
variant={statusBadge.variant}
copyable={false}
/>
</div>
{props.credit.description ? (
<div className='text-muted-foreground mt-1 text-xs leading-5'>
{props.credit.description}
</div>
) : null}
{props.credit.id ? (
<div className='text-muted-foreground mt-1 font-mono text-[11px] break-all'>
{props.credit.id}
</div>
) : null}
</div>
<div className='shrink-0 text-right'>
<div className='text-muted-foreground text-[11px] font-medium'>
{t('Expires in')}
</div>
<div
className={cn(
'mt-1 text-sm font-semibold tabular-nums',
isAvailable ? 'text-success' : 'text-muted-foreground'
)}
>
{expiresIn}
</div>
</div>
</div>
<div className='mt-3 grid grid-cols-1 gap-3 sm:grid-cols-3'>
<ResetCreditTimeField
label={t('Granted at')}
value={formatIsoTimestamp(props.credit.granted_at)}
/>
<ResetCreditTimeField
label={t('Expires at')}
value={formatIsoTimestamp(props.credit.expires_at)}
/>
<ResetCreditTimeField
label={t('Redeemed at')}
value={formatIsoTimestamp(props.credit.redeemed_at)}
emphasis={Boolean(props.credit.redeemed_at)}
/>
</div>
</div>
)
}
function ResetCreditsPanel(props: {
payload: CodexResetCreditsPayload | null
response: CodexResetCreditsResponse | null
usageAvailableCount: string
isLoading: boolean
isResetting: boolean
errorMessage: string
resetErrorMessage: string
resetSuccessMessage: string
onRefresh: () => void
onRequestReset: () => void
}) {
const { t } = useTranslation()
const credits = useMemo(
() => sortResetCredits(props.payload?.credits ?? []),
[props.payload?.credits]
)
const detailAvailableCount = props.payload?.available_count
const availableCount = Number.isFinite(Number(detailAvailableCount))
? String(detailAvailableCount)
: props.usageAvailableCount
const totalEarnedCount = Number.isFinite(
Number(props.payload?.total_earned_count)
)
? String(props.payload?.total_earned_count)
: '-'
const canReset = Number(availableCount) > 0
return (
<div className='flex flex-col gap-3 p-3'>
<div className='grid grid-cols-1 gap-3 sm:grid-cols-3'>
<InfoField
label={t('Available reset credits')}
value={availableCount}
mono
copyable={false}
/>
<InfoField
label={t('Total earned')}
value={totalEarnedCount}
mono
copyable={false}
/>
<InfoField
label='HTTP'
value={String(props.response?.upstream_status ?? '-')}
mono
copyable={false}
/>
</div>
<div className='flex flex-wrap items-center justify-between gap-2'>
<div className='text-muted-foreground text-xs leading-5'>
{t('Available credits are ordered by soonest expiration.')}
</div>
<Button
type='button'
variant='outline'
size='sm'
onClick={props.onRefresh}
disabled={props.isLoading}
>
<RefreshCw data-icon='inline-start' />
{t('Refresh details')}
</Button>
</div>
<div className='bg-muted/30 flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between'>
<div className='min-w-0'>
<div className='text-sm font-semibold'>{t('Reset usage window')}</div>
<div className='text-muted-foreground mt-1 text-xs leading-5'>
{t(
'Use one available reset credit to refresh the current Codex usage windows.'
)}
</div>
</div>
<Button
type='button'
variant={canReset ? 'destructive' : 'outline'}
size='sm'
onClick={props.onRequestReset}
disabled={!canReset || props.isLoading || props.isResetting}
className='shrink-0'
>
<RotateCcw data-icon='inline-start' />
{props.isResetting ? t('Resetting...') : t('Apply reset')}
</Button>
</div>
{!canReset ? (
<Alert>
<AlertTriangle />
<AlertTitle>{t('No reset credits available')}</AlertTitle>
<AlertDescription>
{t('The reset request stays disabled until a credit is available.')}
</AlertDescription>
</Alert>
) : null}
{props.resetSuccessMessage ? (
<Alert className='border-success/40 bg-success/10 text-success'>
<Check />
<AlertTitle>{t('Reset completed')}</AlertTitle>
<AlertDescription>{props.resetSuccessMessage}</AlertDescription>
</Alert>
) : null}
{props.resetErrorMessage ? (
<Alert variant='destructive'>
<AlertTriangle />
<AlertTitle>{t('Reset failed')}</AlertTitle>
<AlertDescription>{props.resetErrorMessage}</AlertDescription>
</Alert>
) : null}
{props.errorMessage ? (
<div className='border-destructive/40 bg-destructive/10 text-destructive rounded-lg border px-3 py-2 text-sm'>
{props.errorMessage}
</div>
) : props.isLoading ? (
<div className='flex flex-col gap-2'>
<Skeleton className='h-24 w-full' />
<Skeleton className='h-24 w-full' />
</div>
) : credits.length > 0 ? (
<div className='flex flex-col gap-2'>
{credits.map((credit, index) => (
<ResetCreditItem
key={`${credit.id ?? credit.expires_at ?? 'credit'}-${index}`}
credit={credit}
index={index}
/>
))}
</div>
) : (
<Empty className='min-h-32 border'>
<EmptyHeader>
<EmptyTitle>{t('No reset credits')}</EmptyTitle>
<EmptyDescription>
{t('Upstream did not return reset credit details.')}
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</div>
)
......@@ -349,6 +841,15 @@ export function CodexUsageDialog({
const { t } = useTranslation()
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
const [showRawJson, setShowRawJson] = useState(false)
const [showResetCredits, setShowResetCredits] = useState(false)
const [resetCreditsResponse, setResetCreditsResponse] =
useState<CodexResetCreditsResponse | null>(null)
const [isLoadingResetCredits, setIsLoadingResetCredits] = useState(false)
const [resetCreditsError, setResetCreditsError] = useState('')
const [resetConfirmOpen, setResetConfirmOpen] = useState(false)
const [isResetting, setIsResetting] = useState(false)
const [resetActionError, setResetActionError] = useState('')
const [resetActionMessage, setResetActionMessage] = useState('')
const payload: CodexUsagePayload | null = useMemo(() => {
const raw = response?.data
......@@ -356,37 +857,122 @@ export function CodexUsageDialog({
return raw as CodexUsagePayload
}, [response?.data])
const resetCreditsPayload: CodexResetCreditsPayload | null = useMemo(() => {
const raw = resetCreditsResponse?.data
if (!raw || typeof raw !== 'object') return null
return raw as CodexResetCreditsPayload
}, [resetCreditsResponse?.data])
const rateLimit = payload?.rate_limit
const accountType = payload?.plan_type ?? rateLimit?.plan_type
const accountBadge = getAccountTypeBadge(accountType, t)
const additionalRateLimits = (payload?.additional_rate_limits ?? []).filter(
(item) => item && Object.keys(item).length > 0
)
const resetCredits =
resetCreditsPayload?.available_count ??
payload?.rate_limit_reset_credits?.available_count
const resetCreditsText = Number.isFinite(Number(resetCredits))
? String(resetCredits)
: '-'
const canResetCodexUsage = Number(resetCredits) > 0
const channelLabel = `${channelName || '-'}${
channelId ? ` (#${channelId})` : ''
}`
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(payload)
const statusBadge = (() => {
if (!rateLimit || Object.keys(rateLimit).length === 0) {
return (
<StatusBadge label={t('Pending')} variant='neutral' copyable={false} />
const errorMessage =
response?.success === false
? response?.message?.trim() || t('Failed to fetch usage')
: ''
const loadResetCredits = useCallback(
async (force = false) => {
if (!channelId) {
setResetCreditsError(t('Channel ID is required'))
return
}
if (isLoadingResetCredits || (!force && resetCreditsResponse)) return
setIsLoadingResetCredits(true)
setResetCreditsError('')
try {
const res = await getCodexResetCredits(channelId)
if (!res.success) {
throw new Error(
res.message || t('Failed to fetch reset credit details')
)
}
if (rateLimit.allowed && !rateLimit.limit_reached) {
return (
<StatusBadge
label={t('Available')}
variant='success'
copyable={false}
/>
setResetCreditsResponse(res)
} catch (error) {
setResetCreditsError(
error instanceof Error
? error.message
: t('Failed to fetch reset credit details')
)
} finally {
setIsLoadingResetCredits(false)
}
return (
<StatusBadge label={t('Limited')} variant='danger' copyable={false} />
},
[channelId, isLoadingResetCredits, resetCreditsResponse, t]
)
})()
const errorMessage =
response?.success === false
? response?.message?.trim() || t('Failed to fetch usage')
: ''
const handleResetCreditsOpenChange = (nextOpen: boolean) => {
setShowResetCredits(nextOpen)
if (nextOpen) {
void loadResetCredits(false)
}
}
const handleDialogOpenChange = (nextOpen: boolean) => {
if (!nextOpen) {
setShowRawJson(false)
setShowResetCredits(false)
setResetCreditsResponse(null)
setResetCreditsError('')
setIsLoadingResetCredits(false)
setResetConfirmOpen(false)
setIsResetting(false)
setResetActionError('')
setResetActionMessage('')
}
onOpenChange(nextOpen)
}
const handleConfirmReset = async () => {
if (!channelId || isResetting || !canResetCodexUsage) return
setIsResetting(true)
setResetActionError('')
setResetActionMessage('')
try {
const res = await resetCodexUsage(channelId)
if (!res.success) {
throw new Error(res.message || t('Failed to reset usage'))
}
const resetPayload = res.data as
| { windows_reset?: number; code?: string }
| undefined
const windowsReset = Number(resetPayload?.windows_reset)
setResetActionMessage(
Number.isFinite(windowsReset)
? `${t('Reset completed. Latest usage has been refreshed.')} ${t(
'Affected windows:'
)} ${windowsReset}`
: t('Reset completed. Latest usage has been refreshed.')
)
setResetConfirmOpen(false)
await Promise.resolve(onRefresh?.())
await loadResetCredits(true)
} catch (error) {
setResetActionError(
error instanceof Error ? error.message : t('Failed to reset usage')
)
} finally {
setIsResetting(false)
}
}
const rawJsonText = useMemo(() => {
if (!response) return ''
......@@ -409,153 +995,214 @@ export function CodexUsageDialog({
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
onOpenChange={handleDialogOpenChange}
title={t('Codex Account & Usage')}
description={
<>
{t('Channel:')}
<strong>{channelName || '-'}</strong>{' '}
{channelId ? `(#${channelId})` : ''}
</>
}
contentClassName='sm:max-w-3xl'
contentClassName='sm:max-w-[900px]'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
bodyClassName='flex flex-col gap-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
onClick={() => handleDialogOpenChange(false)}
>
{t('Close')}
</Button>
</>
}
>
<div className='space-y-4'>
<div className='flex flex-col gap-4'>
{errorMessage && (
<div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400'>
{errorMessage}
</div>
)}
{/* Account summary */}
<div className='rounded-lg border p-4'>
<div className='flex flex-wrap items-center justify-between gap-2'>
<Card size='sm' className='bg-muted/30 gap-0 py-0'>
<CardHeader className='p-4 pb-2'>
<CardTitle className='text-muted-foreground text-xs font-medium'>
{t('Codex Account Status')}
</CardTitle>
{onRefresh ? (
<CardAction>
<Button
type='button'
variant='outline'
size='sm'
onClick={onRefresh}
disabled={Boolean(isRefreshing)}
>
<RefreshCw data-icon='inline-start' />
{t('Refresh')}
</Button>
</CardAction>
) : null}
</CardHeader>
<CardContent className='p-4 pt-0'>
<div className='flex flex-wrap items-center gap-2'>
<StatusBadge
label={accountBadge.label}
variant={accountBadge.variant}
copyable={false}
/>
{statusBadge}
{typeof response?.upstream_status === 'number' && (
{getUsageStatusBadge(rateLimit, t)}
<StatusBadge
label={`${t('Status:')} ${response.upstream_status}`}
label={`HTTP ${response?.upstream_status ?? '-'}`}
variant='neutral'
copyable={false}
/>
)}
</div>
{onRefresh && (
<Button
type='button'
variant='outline'
size='sm'
onClick={onRefresh}
disabled={Boolean(isRefreshing)}
>
<RefreshCw className='mr-1.5 h-3.5 w-3.5' />
{t('Refresh')}
</Button>
)}
<StatusBadge
label={formatLabelValue(t('Reset count:'), resetCreditsText)}
variant={Number(resetCredits) > 0 ? 'blue' : 'neutral'}
copyable={false}
/>
{payload?.credits?.overage_limit_reached ? (
<StatusBadge
label={t('Overage limited')}
variant='danger'
copyable={false}
/>
) : null}
{payload?.spend_control?.reached ? (
<StatusBadge
label={t('Spend limited')}
variant='danger'
copyable={false}
/>
) : null}
</div>
{/* Account identity info */}
<div className='bg-muted/30 mt-3 rounded-md px-3 py-2'>
<CopyableField
icon={<User className='h-3.5 w-3.5' />}
<div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
<InfoField
label={t('Email')}
value={payload?.email}
copyable={true}
/>
<InfoField
label={t('Channel')}
value={channelLabel}
copyable={false}
/>
<InfoField
label='User ID'
value={payload?.user_id}
mono
className='md:col-span-2'
/>
<CopyableField
icon={<Mail className='h-3.5 w-3.5' />}
label={t('Email')}
value={payload?.email}
</div>
</CardContent>
</Card>
<Collapsible
open={showResetCredits}
onOpenChange={handleResetCreditsOpenChange}
className='rounded-lg border'
>
<CollapsibleTrigger
render={
<button
type='button'
className='hover:bg-muted/40 flex w-full items-start justify-between gap-3 p-3 text-left transition-colors'
aria-expanded={showResetCredits}
/>
<CopyableField
icon={<Hash className='h-3.5 w-3.5' />}
label='Account ID'
value={payload?.account_id}
mono
}
>
<div className='min-w-0'>
<div className='flex flex-wrap items-center gap-2'>
<div className='text-sm font-semibold'>
{t('Reset Credits')}
</div>
<StatusBadge
label={`${t('Available')} ${resetCreditsText}`}
variant={Number(resetCredits) > 0 ? 'blue' : 'neutral'}
copyable={false}
/>
</div>
<div className='text-muted-foreground mt-1 text-xs leading-5'>
{t('View issued reset credits, grant dates, and expiration.')}
</div>
{/* Rate limit windows */}
<div className='space-y-5'>
<div>
<div className='mb-1 text-sm font-medium'>
{t('Rate Limit Windows')}
</div>
<p className='text-muted-foreground mb-3 text-xs'>
{t(
'Tracks current account base limits and additional metered usage on Codex upstream.'
{showResetCredits ? (
<ChevronUp className='text-muted-foreground mt-0.5 shrink-0' />
) : (
<ChevronDown className='text-muted-foreground mt-0.5 shrink-0' />
)}
</p>
<RateLimitGroupSection
</CollapsibleTrigger>
<CollapsibleContent className='border-t'>
<ResetCreditsPanel
payload={resetCreditsPayload}
response={resetCreditsResponse}
usageAvailableCount={resetCreditsText}
isLoading={isLoadingResetCredits}
isResetting={isResetting}
errorMessage={resetCreditsError}
resetErrorMessage={resetActionError}
resetSuccessMessage={resetActionMessage}
onRefresh={() => void loadResetCredits(true)}
onRequestReset={() => {
setResetActionError('')
setResetActionMessage('')
setResetConfirmOpen(true)
}}
/>
</CollapsibleContent>
</Collapsible>
<div className='flex flex-col gap-3'>
<SectionHeading
title={t('Base Limits')}
description={t('Base rate limit windows for this account.')}
source={payload}
>
{getUsageStatusBadge(rateLimit, t)}
</SectionHeading>
<RateLimitWindowGrid
fiveHourWindow={fiveHourWindow}
weeklyWindow={weeklyWindow}
/>
</div>
{additionalRateLimits.length > 0 && (
<div className='space-y-4 border-t pt-4'>
<div>
<div className='text-sm font-medium'>
{t('Additional Limits')}
</div>
<p className='text-muted-foreground text-xs'>
{t(
{additionalRateLimits.length > 0 ? (
<div className='flex flex-col gap-3'>
<SectionHeading
title={t('Additional Limits')}
description={t(
'Per-feature metered windows split by model or capability.'
)}
</p>
</div>
<div className='space-y-4'>
/>
<div className='flex flex-col gap-3'>
{additionalRateLimits.map((item, index) => {
const limitName =
item.limit_name ||
item.metered_feature ||
`${t('Additional Limit')} ${index + 1}`
return (
<div
key={`${limitName}-${item.metered_feature ?? ''}-${index}`}
className={index > 0 ? 'border-t pt-4' : ''}
>
<RateLimitGroupSection
key={`${limitName}-${item.metered_feature ?? ''}-${index}`}
title={limitName}
description={t('Additional metered capability')}
source={item}
meteredFeature={item.metered_feature}
/>
</div>
)
})}
</div>
</div>
)}
</div>
) : null}
{/* Raw JSON collapsible */}
<div className='rounded-lg border'>
<Collapsible
open={showRawJson}
onOpenChange={setShowRawJson}
className='rounded-lg border'
>
<CollapsibleTrigger
render={
<button
type='button'
className='hover:bg-muted/40 flex w-full items-center justify-between gap-2 p-3 transition-colors'
onClick={() => setShowRawJson((v) => !v)}
aria-expanded={showRawJson}
/>
}
>
<div className='text-sm font-medium'>{t('Raw JSON')}</div>
{showRawJson ? (
......@@ -563,8 +1210,8 @@ export function CodexUsageDialog({
) : (
<ChevronDown className='text-muted-foreground h-4 w-4' />
)}
</button>
{showRawJson && (
</CollapsibleTrigger>
<CollapsibleContent>
<>
<div className='flex justify-end border-t px-3 py-2'>
<Button
......@@ -575,9 +1222,9 @@ export function CodexUsageDialog({
disabled={!rawJsonText}
>
{copiedText === rawJsonText ? (
<Check className='mr-1.5 h-3.5 w-3.5 text-green-600' />
<Check data-icon='inline-start' className='text-success' />
) : (
<Copy className='mr-1.5 h-3.5 w-3.5' />
<Copy data-icon='inline-start' />
)}
{t('Copy')}
</Button>
......@@ -588,9 +1235,38 @@ export function CodexUsageDialog({
</pre>
</ScrollArea>
</>
</CollapsibleContent>
</Collapsible>
</div>
<ConfirmDialog
open={resetConfirmOpen}
onOpenChange={setResetConfirmOpen}
title={t('Confirm usage reset')}
desc={
<div className='flex flex-col gap-2'>
<p>
{t(
'Use one available reset credit for this channel. The reset request is sent only after confirmation.'
)}
</p>
<div className='bg-muted/50 rounded-lg border px-3 py-2 text-xs'>
<div className='font-medium'>{channelLabel}</div>
<div className='text-muted-foreground mt-1'>
{t('Available reset credits')}: {resetCreditsText}
</div>
</div>
<p className='text-destructive'>
{t('Used reset credits cannot be restored.')}
</p>
</div>
}
destructive
disabled={!canResetCodexUsage}
isLoading={isResetting}
cancelBtnText={t('Cancel')}
confirmText={isResetting ? t('Resetting...') : t('Apply reset')}
handleConfirm={handleConfirmReset}
/>
</Dialog>
)
}
......@@ -234,6 +234,7 @@
"Advanced Settings": "Advanced Settings",
"Advanced text editing": "Advanced text editing",
"Aesthetic style": "Aesthetic style",
"Affected windows:": "Affected windows:",
"After clicking the button, you'll be asked to authorize the bot": "After clicking the button, you'll be asked to authorize the bot",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?",
"After enabling, the plan will be shown to users. Continue?": "After enabling, the plan will be shown to users. Continue?",
......@@ -394,6 +395,7 @@
"Apply Filters": "Apply Filters",
"Apply IP Filter to Resolved Domains": "Apply IP Filter to Resolved Domains",
"Apply Overwrite": "Apply Overwrite",
"Apply reset": "Apply reset",
"Apply Sync": "Apply Sync",
"Applying...": "Applying...",
"Approx.": "Approx.",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "Automatically sync model list when upstream changes are detected",
"Availability (last 24h)": "Availability (last 24h)",
"Available": "Available",
"Available credits are ordered by soonest expiration.": "Available credits are ordered by soonest expiration.",
"Available disk space": "Available disk space",
"Available Models": "Available Models",
"Available reset credits": "Available reset credits",
"Available Rewards": "Available Rewards",
"Average latency": "Average latency",
"Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "Channel enabled successfully",
"Channel Extra Settings": "Channel Extra Settings",
"Channel ID": "Channel ID",
"Channel ID is required": "Channel ID is required",
"Channel key": "Channel key",
"Channel key unlocked": "Channel key unlocked",
"Channel models": "Channel models",
......@@ -811,6 +816,7 @@
"Codes copied!": "Codes copied!",
"Codex": "Codex",
"Codex Account & Usage": "Codex Account & Usage",
"Codex Account Status": "Codex Account Status",
"Codex channels do not support batch creation": "Codex channels do not support batch creation",
"Codex channels use an OAuth JSON credential as the key.": "Codex channels use an OAuth JSON credential as the key.",
"Codex CLI Header Passthrough": "Codex CLI Header Passthrough",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Confirm settings and finish setup",
"confirm that I bear legal responsibility arising from deployment": "confirm that I bear legal responsibility arising from deployment",
"Confirm Unbind": "Confirm Unbind",
"Confirm usage reset": "Confirm usage reset",
"Confirm your identity before removing this Passkey from your account.": "Confirm your identity before removing this Passkey from your account.",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "Confirm your identity with Two-factor Authentication before registering a Passkey.",
"Confirmed at {{time}} by user #{{userId}}": "Confirmed at {{time}} by user #{{userId}}",
......@@ -1625,6 +1632,8 @@
"Expired at": "Expired at",
"Expired time cannot be earlier than current time": "Expired time cannot be earlier than current time",
"Expires": "Expires",
"Expires at": "Expires at",
"Expires in": "Expires in",
"Expose ratio API": "Expose ratio API",
"Exposes the pricing/models catalog in the top navigation.": "Exposes the pricing/models catalog in the top navigation.",
"Expression": "Expression",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Failed to fetch deployment details",
"Failed to fetch models": "Failed to fetch models",
"Failed to fetch OIDC configuration. Please check the URL and network status": "Failed to fetch OIDC configuration. Please check the URL and network status",
"Failed to fetch reset credit details": "Failed to fetch reset credit details",
"Failed to fetch upstream prices": "Failed to fetch upstream prices",
"Failed to fetch upstream ratios": "Failed to fetch upstream ratios",
"Failed to fetch usage": "Failed to fetch usage",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Failed to reset 2FA",
"Failed to reset model ratios": "Failed to reset model ratios",
"Failed to reset Passkey": "Failed to reset Passkey",
"Failed to reset usage": "Failed to reset usage",
"Failed to save": "Failed to save",
"Failed to save announcements": "Failed to save announcements",
"Failed to save API info": "Failed to save API info",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.",
"GPU count": "GPU count",
"Granted at": "Granted at",
"Greater than": "Greater than",
"Greater Than": "Greater Than",
"Greater than or equal": "Greater than or equal",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "No related models available for this channel type",
"No release notes provided.": "No release notes provided.",
"No Reset": "No Reset",
"No reset credits": "No reset credits",
"No reset credits available": "No reset credits available",
"No restriction": "No restriction",
"No results for \"{{query}}\". Try adjusting your search or filters.": "No results for \"{{query}}\". Try adjusting your search or filters.",
"No results found": "No results found",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Output token price for generated tokens.",
"Output tokens": "Output tokens",
"Output Tokens": "Output Tokens",
"Overage limited": "Overage limited",
"overall": "overall",
"Overnight range": "Overnight range",
"override": "override",
......@@ -3346,6 +3361,8 @@
"Recursive": "Recursive",
"Redeem": "Redeem",
"Redeem codes": "Redeem codes",
"Redeemed": "Redeemed",
"Redeemed at": "Redeemed at",
"Redeemed By": "Redeemed By",
"Redeemed:": "Redeemed:",
"redemption code": "redemption code",
......@@ -3373,6 +3390,7 @@
"Refresh": "Refresh",
"Refresh Cache": "Refresh Cache",
"Refresh credential": "Refresh credential",
"Refresh details": "Refresh details",
"Refresh failed": "Refresh failed",
"Refresh interval (minutes)": "Refresh interval (minutes)",
"Refresh Stats": "Refresh Stats",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Reset all settings to default values",
"Reset at:": "Reset at:",
"Reset balance and used quota": "Reset balance and used quota",
"Reset completed": "Reset completed",
"Reset completed. Latest usage has been refreshed.": "Reset completed. Latest usage has been refreshed.",
"Reset count:": "Reset count:",
"Reset Credit": "Reset Credit",
"Reset Credits": "Reset Credits",
"Reset Cycle": "Reset Cycle",
"Reset email sent, please check your inbox": "Reset email sent, please check your inbox",
"Reset failed": "Reset failed",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "Reset to Default",
"Reset to default configuration": "Reset to default configuration",
"Reset Two-Factor Authentication": "Reset Two-Factor Authentication",
"Reset usage window": "Reset usage window",
"Resets in:": "Resets in:",
"Resetting...": "Resetting...",
"Resolve Conflicts": "Resolve Conflicts",
"Resource Configuration": "Resource Configuration",
"Response": "Response",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "Special ratios override the token group ratio for specific user group and token group combinations.",
"Special usable group rules": "Special usable group rules",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Special usable group rules can add, remove, or append selectable token groups for a specific user group.",
"Spend limited": "Spend limited",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite stores all data in a single file. Make sure that file is persisted when running in containers.",
"SSRF Protection": "SSRF Protection",
"Standard": "Standard",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "The name displayed across the application",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations",
"The requested chat preset does not exist or has been removed.": "The requested chat preset does not exist or has been removed.",
"The reset request stays disabled until a credit is available.": "The reset request stays disabled until a credit is available.",
"The setup wizard will use this database during initialization.": "The setup wizard will use this database during initialization.",
"The site is not available at the moment.": "The site is not available at the moment.",
"The slug is appended to the URL:": "The slug is appended to the URL:",
......@@ -4402,6 +4429,7 @@
"Upload photo": "Upload photo",
"Upscale": "Upscale",
"Upstream": "Upstream",
"Upstream did not return reset credit details.": "Upstream did not return reset credit details.",
"Upstream Model Detection Settings": "Upstream Model Detection Settings",
"Upstream Model Update Check": "Upstream Model Update Check",
"Upstream Model Updates": "Upstream Model Updates",
......@@ -4447,6 +4475,8 @@
"Use backup code": "Use backup code",
"Use disk cache when request body exceeds this size": "Use disk cache when request body exceeds this size",
"Use external tools to extend capabilities": "Use external tools to extend capabilities",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Use one available reset credit for this channel. The reset request is sent only after confirmation.",
"Use one available reset credit to refresh the current Codex usage windows.": "Use one available reset credit to refresh the current Codex usage windows.",
"Use our unified OpenAI-compatible endpoint in your applications": "Use our unified OpenAI-compatible endpoint in your applications",
"Use Passkey to sign in without entering your password.": "Use Passkey to sign in without entering your password.",
"Use presets or upstream discovery to populate the model list faster.": "Use presets or upstream discovery to populate the model list faster.",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "Used for load balancing. Higher weight = more requests",
"Used in URLs and API routes": "Used in URLs and API routes",
"Used Quota": "Used Quota",
"Used reset credits cannot be restored.": "Used reset credits cannot be restored.",
"Used to authenticate with io.net deployment API": "Used to authenticate with io.net deployment API",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Used to authenticate with the worker. Leave blank to keep the existing secret.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "View detailed information about this user including balance, usage statistics, and invitation details.",
"View details": "View details",
"View document": "View document",
"View issued reset credits, grant dates, and expiration.": "View issued reset credits, grant dates, and expiration.",
"View logs": "View logs",
"View mode": "View mode",
"View model statistics and charts": "View model statistics and charts",
......
......@@ -234,6 +234,7 @@
"Advanced Settings": "Paramètres avancés",
"Advanced text editing": "Édition de texte avancée",
"Aesthetic style": "Style esthétique",
"Affected windows:": "Fenêtres affectées :",
"After clicking the button, you'll be asked to authorize the bot": "Après avoir cliqué sur le bouton, il vous sera demandé d'autoriser le bot",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "Après désactivation, il ne sera plus affiché aux utilisateurs, mais les commandes historiques ne sont pas affectées. Continuer ?",
"After enabling, the plan will be shown to users. Continue?": "Après activation, le plan sera affiché aux utilisateurs. Continuer ?",
......@@ -394,6 +395,7 @@
"Apply Filters": "Appliquer les filtres",
"Apply IP Filter to Resolved Domains": "Appliquer le filtre IP aux domaines résolus",
"Apply Overwrite": "Appliquer l'écrasement",
"Apply reset": "Appliquer la réinitialisation",
"Apply Sync": "Appliquer la synchronisation",
"Applying...": "Application en cours...",
"Approx.": "Environ.",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "Synchroniser automatiquement la liste des modèles lorsque des changements en amont sont détectés",
"Availability (last 24h)": "Disponibilité (dernières 24 h)",
"Available": "Disponible",
"Available credits are ordered by soonest expiration.": "Les crédits disponibles sont triés par expiration la plus proche.",
"Available disk space": "Espace disque disponible",
"Available Models": "Modèles disponibles",
"Available reset credits": "Crédits de réinitialisation disponibles",
"Available Rewards": "Récompenses disponibles",
"Average latency": "Latence moyenne",
"Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "Canal activé avec succès",
"Channel Extra Settings": "Paramètres supplémentaires du canal",
"Channel ID": "ID du Canal",
"Channel ID is required": "L'ID du canal est requis",
"Channel key": "Clé du canal",
"Channel key unlocked": "Clé de canal déverrouillée",
"Channel models": "Modèles de canaux",
......@@ -811,6 +816,7 @@
"Codes copied!": "Codes copiés !",
"Codex": "Codex",
"Codex Account & Usage": "Compte et utilisation Codex",
"Codex Account Status": "Statut du compte Codex",
"Codex channels do not support batch creation": "Les canaux Codex ne prennent pas en charge la création par lot",
"Codex channels use an OAuth JSON credential as the key.": "Les canaux Codex utilisent un identifiant OAuth JSON comme clé.",
"Codex CLI Header Passthrough": "Passthrough en-tête Codex CLI",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Confirmez les paramètres et terminez la configuration",
"confirm that I bear legal responsibility arising from deployment": "confirme assumer la responsabilité juridique découlant du déploiement",
"Confirm Unbind": "Confirmer la dissociation",
"Confirm usage reset": "Confirmer la réinitialisation de l’utilisation",
"Confirm your identity before removing this Passkey from your account.": "Confirmez votre identité avant de supprimer cette Passkey de votre compte.",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "Confirmez votre identité avec l’authentification à deux facteurs avant d’enregistrer une Passkey.",
"Confirmed at {{time}} by user #{{userId}}": "Confirmé le {{time}} par l’utilisateur #{{userId}}",
......@@ -1625,6 +1632,8 @@
"Expired at": "Expiré le",
"Expired time cannot be earlier than current time": "L'heure d'expiration ne peut pas être antérieure à l'heure actuelle",
"Expires": "Expire",
"Expires at": "Expire le",
"Expires in": "Expire dans",
"Expose ratio API": "Exposer l'API de ratio",
"Exposes the pricing/models catalog in the top navigation.": "Expose le catalogue des prix/modèles dans la navigation supérieure.",
"Expression": "Expression",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Impossible de récupérer les détails du déploiement",
"Failed to fetch models": "Échec de la récupération des modèles",
"Failed to fetch OIDC configuration. Please check the URL and network status": "Échec de la récupération de la configuration OIDC. Veuillez vérifier l'URL et le statut du réseau",
"Failed to fetch reset credit details": "Échec de la récupération des détails des crédits de réinitialisation",
"Failed to fetch upstream prices": "Échec de la récupération des prix amont",
"Failed to fetch upstream ratios": "Échec de la récupération des ratios en amont",
"Failed to fetch usage": "Échec de la récupération de l'utilisation",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Échec de la réinitialisation de la 2FA",
"Failed to reset model ratios": "Échec de la réinitialisation des ratios du modèle",
"Failed to reset Passkey": "Échec de la réinitialisation de la Passkey",
"Failed to reset usage": "Échec de la réinitialisation de l’utilisation",
"Failed to save": "Échec de la sauvegarde",
"Failed to save announcements": "Échec de la sauvegarde des annonces",
"Failed to save API info": "Échec de l'enregistrement des informations API",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.",
"GPU count": "Nombre de GPU",
"Granted at": "Accordé le",
"Greater than": "Supérieur à",
"Greater Than": "Supérieur à",
"Greater than or equal": "Supérieur ou égal",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "Aucun modèle associé disponible pour ce type de canal",
"No release notes provided.": "Aucune note de version fournie.",
"No Reset": "Pas de réinitialisation",
"No reset credits": "Aucun crédit de réinitialisation",
"No reset credits available": "Aucun crédit de réinitialisation disponible",
"No restriction": "Aucune restriction",
"No results for \"{{query}}\". Try adjusting your search or filters.": "Aucun résultat pour \"{{query}}\". Essayez d'ajuster votre recherche ou vos filtres.",
"No results found": "Aucun résultat trouvé",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Prix des tokens de sortie générés.",
"Output tokens": "Jetons de sortie",
"Output Tokens": "Tokens de sortie",
"Overage limited": "Dépassement limité",
"overall": "global",
"Overnight range": "Plage nocturne",
"override": "remplacer",
......@@ -3346,6 +3361,8 @@
"Recursive": "Récursif",
"Redeem": "Utiliser",
"Redeem codes": "Échanger des codes",
"Redeemed": "Utilisé",
"Redeemed at": "Utilisé le",
"Redeemed By": "Utilisé par",
"Redeemed:": "Utilisé :",
"redemption code": "code d'échange",
......@@ -3373,6 +3390,7 @@
"Refresh": "Actualiser",
"Refresh Cache": "Actualiser le cache",
"Refresh credential": "Actualiser l'identifiant",
"Refresh details": "Actualiser les détails",
"Refresh failed": "Échec de l'actualisation",
"Refresh interval (minutes)": "Intervalle d'actualisation (minutes)",
"Refresh Stats": "Actualiser les statistiques",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Réinitialiser tous les paramètres aux valeurs par défaut",
"Reset at:": "Réinitialisation à :",
"Reset balance and used quota": "Réinitialiser le solde et le quota utilisé",
"Reset completed": "Réinitialisation terminée",
"Reset completed. Latest usage has been refreshed.": "Réinitialisation terminée. La dernière utilisation a été actualisée.",
"Reset count:": "Nombre de réinitialisations :",
"Reset Credit": "Crédit de réinitialisation",
"Reset Credits": "Crédits de réinitialisation",
"Reset Cycle": "Cycle de réinitialisation",
"Reset email sent, please check your inbox": "Email de réinitialisation envoyé, veuillez vérifier votre boîte de réception",
"Reset failed": "Échec de la réinitialisation",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "Réinitialiser par défaut",
"Reset to default configuration": "Réinitialisé à la configuration par défaut",
"Reset Two-Factor Authentication": "Réinitialiser 2FA",
"Reset usage window": "Réinitialiser la fenêtre d’utilisation",
"Resets in:": "Réinitialise dans :",
"Resetting...": "Réinitialisation...",
"Resolve Conflicts": "Résoudre les conflits",
"Resource Configuration": "Configuration des ressources",
"Response": "Réponse",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "Les ratios spéciaux remplacent le ratio du groupe de jeton pour des combinaisons précises de groupe utilisateur et de groupe de jeton.",
"Special usable group rules": "Règles spéciales de groupes utilisables",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Les règles de groupes utilisables spéciaux peuvent ajouter, supprimer ou annexer des groupes de jetons sélectionnables pour un groupe utilisateur précis.",
"Spend limited": "Dépenses limitées",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite stocke toutes les données dans un seul fichier. Assurez-vous que ce fichier est persisté lors de l'exécution dans des conteneurs.",
"SSRF Protection": "Protection SSRF",
"Standard": "Standard",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "Le nom affiché dans l'application",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "L'URL publique de votre serveur, utilisée pour les rappels OAuth, les webhooks et autres intégrations externes",
"The requested chat preset does not exist or has been removed.": "Le préréglage de discussion demandé n'existe pas ou a été supprimé.",
"The reset request stays disabled until a credit is available.": "La demande de réinitialisation reste désactivée tant qu’aucun crédit n’est disponible.",
"The setup wizard will use this database during initialization.": "L'assistant de configuration utilisera cette base de données lors de l'initialisation.",
"The site is not available at the moment.": "Le site n'est pas disponible pour le moment.",
"The slug is appended to the URL:": "Le slug est ajouté à l'URL :",
......@@ -4402,6 +4429,7 @@
"Upload photo": "Téléverser une photo",
"Upscale": "Agrandir",
"Upstream": "Amont",
"Upstream did not return reset credit details.": "L'amont n'a renvoyé aucun détail de crédit de réinitialisation.",
"Upstream Model Detection Settings": "Paramètres de détection des modèles en amont",
"Upstream Model Update Check": "Vérification des mises à jour des modèles en amont",
"Upstream Model Updates": "Mises à jour des modèles en amont",
......@@ -4447,6 +4475,8 @@
"Use backup code": "Utiliser un code de secours",
"Use disk cache when request body exceeds this size": "Utiliser le cache disque quand le corps de requête dépasse cette taille",
"Use external tools to extend capabilities": "Utiliser des outils externes pour étendre les capacités",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Utilise un crédit de réinitialisation disponible pour ce canal. La demande n’est envoyée qu’après confirmation.",
"Use one available reset credit to refresh the current Codex usage windows.": "Utilise un crédit de réinitialisation disponible pour actualiser les fenêtres d’utilisation Codex actuelles.",
"Use our unified OpenAI-compatible endpoint in your applications": "Utilisez notre point de terminaison unifié compatible OpenAI dans vos applications",
"Use Passkey to sign in without entering your password.": "Utilisez une clé d'accès (Passkey) pour vous connecter sans saisir votre mot de passe.",
"Use presets or upstream discovery to populate the model list faster.": "Utilisez des préréglages ou la découverte en amont pour remplir plus vite la liste des modèles.",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "Utilisé pour l'équilibrage de charge. Poids plus élevé = plus de requêtes",
"Used in URLs and API routes": "Utilisé dans les URLs et les routes API",
"Used Quota": "Quota utilisé",
"Used reset credits cannot be restored.": "Les crédits de réinitialisation utilisés ne peuvent pas être restaurés.",
"Used to authenticate with io.net deployment API": "Utilisée pour l'authentification auprès de l'API de déploiement io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Utilisé pour l'authentification auprès du worker. Laissez vide pour conserver le secret existant.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Détermine le flux de paiement utilisé au clic. Les clés intégrées incluent stripe pour Stripe et waffo_pancake pour Waffo Pancake ; les autres valeurs sont envoyées à Epay comme paramètre type.",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "Afficher des informations détaillées sur cet utilisateur, y compris le solde, les statistiques d'utilisation et les détails d'invitation.",
"View details": "Voir les détails",
"View document": "Afficher le document",
"View issued reset credits, grant dates, and expiration.": "Voir les crédits de réinitialisation émis, les dates d'attribution et d'expiration.",
"View logs": "Voir les logs",
"View mode": "Mode d'affichage",
"View model statistics and charts": "Afficher les statistiques et graphiques des modèles",
......
......@@ -234,6 +234,7 @@
"Advanced Settings": "詳細設定",
"Advanced text editing": "高度なテキスト編集",
"Aesthetic style": "スタイル",
"Affected windows:": "対象ウィンドウ:",
"After clicking the button, you'll be asked to authorize the bot": "ボタンをクリックすると、ボットの認証を求められます",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "無効化するとユーザーに表示されなくなりますが、過去の注文には影響しません。続行しますか?",
"After enabling, the plan will be shown to users. Continue?": "有効化するとユーザーに表示されます。続行しますか?",
......@@ -394,6 +395,7 @@
"Apply Filters": "フィルターを適用",
"Apply IP Filter to Resolved Domains": "解決されたドメインにIPフィルターを適用",
"Apply Overwrite": "上書き適用",
"Apply reset": "リセットを実行",
"Apply Sync": "同期を適用",
"Applying...": "適用中...",
"Approx.": "約",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "アップストリームの変更が検出されたときにモデルリストを自動的に同期",
"Availability (last 24h)": "可用性(過去 24 時間)",
"Available": "空き",
"Available credits are ordered by soonest expiration.": "利用可能なクレジットは有効期限の近い順に表示されます。",
"Available disk space": "利用可能なディスク容量",
"Available Models": "利用可能なモデル",
"Available reset credits": "利用可能なリセット回数",
"Available Rewards": "利用可能な報酬",
"Average latency": "平均レイテンシ",
"Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "チャネルが正常に有効化されました",
"Channel Extra Settings": "チャネル詳細設定",
"Channel ID": "チャネルID",
"Channel ID is required": "チャネル ID が必要です",
"Channel key": "チャネルキー",
"Channel key unlocked": "チャネルキーが解除されました",
"Channel models": "チャネルモデル",
......@@ -811,6 +816,7 @@
"Codes copied!": "コードをコピーしました!",
"Codex": "Codex",
"Codex Account & Usage": "Codex アカウントと使用量",
"Codex Account Status": "Codex アカウント状態",
"Codex channels do not support batch creation": "Codex チャネルは一括作成をサポートしていません",
"Codex channels use an OAuth JSON credential as the key.": "CodexチャネルはOAuth JSON認証情報をキーとして使用します。",
"Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "設定を確認してセットアップを完了",
"confirm that I bear legal responsibility arising from deployment": "デプロイ",
"Confirm Unbind": "連携解除を確認",
"Confirm usage reset": "使用量リセットの確認",
"Confirm your identity before removing this Passkey from your account.": "このパスキーをアカウントから削除する前に、本人確認を行ってください。",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "パスキーを登録する前に、二要素認証で本人確認を行ってください。",
"Confirmed at {{time}} by user #{{userId}}": "ユーザー #{{userId}} が {{time}} に確認しました",
......@@ -1625,6 +1632,8 @@
"Expired at": "有効期限",
"Expired time cannot be earlier than current time": "有効期限は現在時刻より早く設定できません",
"Expires": "有効期限",
"Expires at": "有効期限",
"Expires in": "期限まで",
"Expose ratio API": "倍率APIを公開",
"Exposes the pricing/models catalog in the top navigation.": "価格/モデルカタログをトップナビゲーションに表示します。",
"Expression": "式",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "デプロイメント詳細の取得に失敗しました",
"Failed to fetch models": "モデルの取得に失敗しました",
"Failed to fetch OIDC configuration. Please check the URL and network status": "OIDC構成の取得に失敗しました。URLとネットワーク状態を確認してください",
"Failed to fetch reset credit details": "リセット回数の詳細取得に失敗しました",
"Failed to fetch upstream prices": "上流価格の取得に失敗しました",
"Failed to fetch upstream ratios": "アップストリーム比率の取得に失敗しました",
"Failed to fetch usage": "利用状況の取得に失敗しました",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "2FAのリセットに失敗しました",
"Failed to reset model ratios": "モデル比率のリセットに失敗しました",
"Failed to reset Passkey": "パスキーのリセットに失敗しました",
"Failed to reset usage": "使用量のリセットに失敗しました",
"Failed to save": "保存に失敗",
"Failed to save announcements": "お知らせの保存に失敗しました",
"Failed to save API info": "API情報の保存に失敗しました",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4、claude-3-opus など",
"GPU count": "GPU 数",
"Granted at": "付与日時",
"Greater than": "より大きい",
"Greater Than": "より大きい",
"Greater than or equal": "以上",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "このチャネルタイプに関連するモデルが利用できません",
"No release notes provided.": "リリースノートは提供されていません。",
"No Reset": "リセットなし",
"No reset credits": "リセット回数はありません",
"No reset credits available": "利用可能なリセット回数がありません",
"No restriction": "制限なし",
"No results for \"{{query}}\". Try adjusting your search or filters.": "\"{{query}}\" の結果が見つかりません。検索条件やフィルターを調整してみてください。",
"No results found": "検索結果がありません",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "生成された出力トークンの価格。",
"Output tokens": "出力トークン",
"Output Tokens": "出力トークン",
"Overage limited": "超過利用制限中",
"overall": "全体",
"Overnight range": "日跨ぎ範囲",
"override": "上書き",
......@@ -3346,6 +3361,8 @@
"Recursive": "再帰",
"Redeem": "引き換え",
"Redeem codes": "コードを交換",
"Redeemed": "使用済み",
"Redeemed at": "使用日時",
"Redeemed By": "引き換え元",
"Redeemed:": "引き換え済み:",
"redemption code": "引き換えコード",
......@@ -3373,6 +3390,7 @@
"Refresh": "更新",
"Refresh Cache": "キャッシュ更新",
"Refresh credential": "認証情報を更新",
"Refresh details": "詳細を更新",
"Refresh failed": "更新に失敗しました",
"Refresh interval (minutes)": "更新間隔 (分)",
"Refresh Stats": "統計を更新",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "すべての設定をデフォルト値にリセット",
"Reset at:": "リセット日時:",
"Reset balance and used quota": "残高と使用済みクォータをリセット",
"Reset completed": "リセット完了",
"Reset completed. Latest usage has been refreshed.": "リセットが完了しました。最新の使用状況を更新しました。",
"Reset count:": "リセット回数:",
"Reset Credit": "リセット回数",
"Reset Credits": "リセット回数",
"Reset Cycle": "リセット周期",
"Reset email sent, please check your inbox": "リセットメールを送信しました、受信箱を確認してください",
"Reset failed": "リセット失敗",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "デフォルトにリセット",
"Reset to default configuration": "デフォルト設定にリセットしました",
"Reset Two-Factor Authentication": "2要素認証のリセット",
"Reset usage window": "使用量ウィンドウをリセット",
"Resets in:": "リセットまで:",
"Resetting...": "リセット中...",
"Resolve Conflicts": "競合を解決",
"Resource Configuration": "リソース設定",
"Response": "レスポンス",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "特殊倍率は、特定のユーザーグループとトークングループの組み合わせでトークングループ倍率を上書きします。",
"Special usable group rules": "特別な使用可能グループルール",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "特殊利用可能グループルールでは、特定ユーザーグループ向けに選択可能なトークングループを追加、削除、追記できます。",
"Spend limited": "支出制限中",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite はすべてのデータを単一ファイルに保存します。コンテナで実行する場合は、ファイルが永続化されていることを確認してください。",
"SSRF Protection": "SSRF保護",
"Standard": "標準",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "アプリケーション全体に表示される名前",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "OAuthコールバック、Webhook、その他の外部統合に使用されるサーバーの公開URL",
"The requested chat preset does not exist or has been removed.": "要求されたチャットプリセットは存在しないか、削除されました。",
"The reset request stays disabled until a credit is available.": "リセット回数が利用可能になるまで、リセット要求は無効です。",
"The setup wizard will use this database during initialization.": "セットアップウィザードは初期化時にこのデータベースを使用します。",
"The site is not available at the moment.": "現在、このサイトは利用できません。",
"The slug is appended to the URL:": "スラッグがURLに追加されます:",
......@@ -4402,6 +4429,7 @@
"Upload photo": "写真をアップロード",
"Upscale": "アップスケール",
"Upstream": "アップストリーム",
"Upstream did not return reset credit details.": "上流からリセット回数の詳細が返されませんでした。",
"Upstream Model Detection Settings": "アップストリームモデル検出設定",
"Upstream Model Update Check": "アップストリームモデル更新チェック",
"Upstream Model Updates": "上流モデルの更新",
......@@ -4447,6 +4475,8 @@
"Use backup code": "バックアップコードを使用",
"Use disk cache when request body exceeds this size": "リクエストボディがこのサイズを超えた場合にディスクキャッシュを使用",
"Use external tools to extend capabilities": "外部ツールを利用して機能を拡張",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "このチャンネルで利用可能なリセット回数を1回使用します。確認後にのみリセット要求を送信します。",
"Use one available reset credit to refresh the current Codex usage windows.": "利用可能なリセット回数を1回使用して、現在の Codex 使用量ウィンドウを更新します。",
"Use our unified OpenAI-compatible endpoint in your applications": "アプリケーションでOpenAI互換の統一エンドポイントを使用",
"Use Passkey to sign in without entering your password.": "パスワードを入力せずにサインインするには、パスキーを使用してください。",
"Use presets or upstream discovery to populate the model list faster.": "プリセットまたは上流検出を使ってモデルリストをすばやく入力します。",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "ロードバランシングに使用されます。重みが高いほどリクエスト数が増えます",
"Used in URLs and API routes": "URLとAPIルートで使用されます",
"Used Quota": "使用済みクォータ",
"Used reset credits cannot be restored.": "使用済みのリセット回数は復元できません。",
"Used to authenticate with io.net deployment API": "io.net デプロイ API の認証に使用します",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "ワーカーの認証に使用されます。既存のシークレットを保持するには、空欄のままにしてください。",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "クリック後に使用する決済フローを決定します。組み込みキーは Stripe 用の stripe、Waffo Pancake 用の waffo_pancake です。それ以外の値は Epay の type パラメーターとして送信されます。",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "残高、使用統計、招待の詳細など、このユーザーに関する詳細情報を表示します。",
"View details": "詳細を表示",
"View document": "ドキュメントを表示",
"View issued reset credits, grant dates, and expiration.": "発行済みリセット回数、付与日時、有効期限を表示します。",
"View logs": "ログを表示",
"View mode": "表示モード",
"View model statistics and charts": "モデルの統計とグラフを表示",
......
......@@ -234,6 +234,7 @@
"Advanced Settings": "Расширенные настройки",
"Advanced text editing": "Расширенное редактирование текста",
"Aesthetic style": "Стиль",
"Affected windows:": "Затронутые окна:",
"After clicking the button, you'll be asked to authorize the bot": "После нажатия кнопки вам будет предложено авторизовать бота",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "После отключения план не будет отображаться пользователям, но исторические заказы не затронуты. Продолжить?",
"After enabling, the plan will be shown to users. Continue?": "После включения план будет отображаться пользователям. Продолжить?",
......@@ -394,6 +395,7 @@
"Apply Filters": "Применить фильтры",
"Apply IP Filter to Resolved Domains": "Применить IP-фильтр к разрешенным доменам",
"Apply Overwrite": "Применить перезапись",
"Apply reset": "Выполнить сброс",
"Apply Sync": "Применить синхронизацию",
"Applying...": "Применение...",
"Approx.": "Примерно.",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "Автоматически синхронизировать список моделей при обнаружении изменений у провайдера",
"Availability (last 24h)": "Доступность (последние 24 ч)",
"Available": "Доступно",
"Available credits are ordered by soonest expiration.": "Доступные сбросы отсортированы по ближайшему истечению.",
"Available disk space": "Доступное дисковое пространство",
"Available Models": "Доступные модели",
"Available reset credits": "Доступные сбросы лимита",
"Available Rewards": "Доступные награды",
"Average latency": "Средняя задержка",
"Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "Канал успешно включён",
"Channel Extra Settings": "Дополнительные настройки канала",
"Channel ID": "ID канала",
"Channel ID is required": "Требуется ID канала",
"Channel key": "Ключ канала",
"Channel key unlocked": "Ключ канала разблокирован",
"Channel models": "Модели каналов",
......@@ -811,6 +816,7 @@
"Codes copied!": "Коды скопированы!",
"Codex": "Codex",
"Codex Account & Usage": "Аккаунт и использование Codex",
"Codex Account Status": "Статус аккаунта Codex",
"Codex channels do not support batch creation": "Каналы Codex не поддерживают пакетное создание",
"Codex channels use an OAuth JSON credential as the key.": "Каналы Codex используют учётные данные OAuth в формате JSON в качестве ключа.",
"Codex CLI Header Passthrough": "Проброс заголовков Codex CLI",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Подтвердите настройки и завершите установку",
"confirm that I bear legal responsibility arising from deployment": "подтверждаю, что несу юридическую ответственность, возникающую из развертывания",
"Confirm Unbind": "Подтвердить отвязку",
"Confirm usage reset": "Подтвердить сброс использования",
"Confirm your identity before removing this Passkey from your account.": "Подтвердите свою личность перед удалением этой Passkey из вашей учётной записи.",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "Подтвердите свою личность с помощью двухфакторной аутентификации перед регистрацией Passkey.",
"Confirmed at {{time}} by user #{{userId}}": "Подтверждено {{time}} пользователем #{{userId}}",
......@@ -1625,6 +1632,8 @@
"Expired at": "Истекает",
"Expired time cannot be earlier than current time": "Время истечения срока действия не может быть раньше текущего времени",
"Expires": "Истекает",
"Expires at": "Истекает",
"Expires in": "До истечения",
"Expose ratio API": "Интерфейс экспонирования коэффициента",
"Exposes the pricing/models catalog in the top navigation.": "Отображает каталог цен/моделей в верхней навигации.",
"Expression": "Выражение",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Не удалось получить сведения о развертывании",
"Failed to fetch models": "Не удалось получить модели",
"Failed to fetch OIDC configuration. Please check the URL and network status": "Не удалось получить конфигурацию OIDC. Проверьте URL и состояние сети",
"Failed to fetch reset credit details": "Не удалось получить сведения о сбросах лимита",
"Failed to fetch upstream prices": "Не удалось получить цены провайдера",
"Failed to fetch upstream ratios": "Не удалось получить коэффициенты upstream",
"Failed to fetch usage": "Не удалось получить данные об использовании",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Не удалось сбросить 2FA",
"Failed to reset model ratios": "Не удалось сбросить коэффициенты модели",
"Failed to reset Passkey": "Не удалось сбросить Passkey",
"Failed to reset usage": "Не удалось сбросить использование",
"Failed to save": "Не удалось сохранить",
"Failed to save announcements": "Не удалось сохранить объявления",
"Failed to save API info": "Не удалось сохранить информацию API",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus и т. д.",
"GPU count": "Количество GPU",
"Granted at": "Выдано",
"Greater than": "Больше",
"Greater Than": "Больше",
"Greater than or equal": "Больше или равно",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "Для этого типа канала нет доступных связанных моделей",
"No release notes provided.": "Примечания к выпуску не предоставлены.",
"No Reset": "Без сброса",
"No reset credits": "Нет сбросов лимита",
"No reset credits available": "Нет доступных сбросов",
"No restriction": "Без ограничений",
"No results for \"{{query}}\". Try adjusting your search or filters.": "Нет результатов для \"{{query}}\". Попробуйте изменить поиск или фильтры.",
"No results found": "Поиск не дал результатов",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Цена выходных токенов для сгенерированного текста.",
"Output tokens": "Выходные токены",
"Output Tokens": "Выходные токены",
"Overage limited": "Ограничение перерасхода",
"overall": "всего",
"Overnight range": "Диапазон через полночь",
"override": "переопределить",
......@@ -3346,6 +3361,8 @@
"Recursive": "Рекурсивно",
"Redeem": "Обменять квоту",
"Redeem codes": "Активировать коды",
"Redeemed": "Использовано",
"Redeemed at": "Использовано в",
"Redeemed By": "Активировано",
"Redeemed:": "Активировано:",
"redemption code": "код активации",
......@@ -3373,6 +3390,7 @@
"Refresh": "Обновить",
"Refresh Cache": "Обновить кэш",
"Refresh credential": "Обновить учётные данные",
"Refresh details": "Обновить сведения",
"Refresh failed": "Ошибка обновления",
"Refresh interval (minutes)": "Интервал обновления (минуты)",
"Refresh Stats": "Обновить статистику",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Сбросить все настройки до значений по умолчанию",
"Reset at:": "Сброс в:",
"Reset balance and used quota": "Сбросить баланс и использованную квоту",
"Reset completed": "Сброс завершён",
"Reset completed. Latest usage has been refreshed.": "Сброс завершён. Последние данные использования обновлены.",
"Reset count:": "Сбросов:",
"Reset Credit": "Сброс лимита",
"Reset Credits": "Сбросы лимита",
"Reset Cycle": "Цикл сброса",
"Reset email sent, please check your inbox": "Письмо для сброса отправлено, проверьте входящие",
"Reset failed": "Ошибка сброса",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "Сбросить по умолчанию",
"Reset to default configuration": "Сброшено к конфигурации по умолчанию",
"Reset Two-Factor Authentication": "Сброс 2FA",
"Reset usage window": "Сбросить окно использования",
"Resets in:": "Сброс через:",
"Resetting...": "Сброс...",
"Resolve Conflicts": "Разрешить конфликты",
"Resource Configuration": "Конфигурация ресурсов",
"Response": "Ответ",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "Специальные коэффициенты переопределяют коэффициент группы токена для конкретных сочетаний группы пользователя и группы токена.",
"Special usable group rules": "Специальные правила для групп с доступом",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Правила специальных доступных групп могут добавлять, удалять или дополнять выбираемые группы токенов для конкретной группы пользователей.",
"Spend limited": "Ограничение расходов",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite хранит все данные в одном файле. Убедитесь, что файл сохраняется при работе в контейнерах.",
"SSRF Protection": "Защита от SSRF",
"Standard": "Стандартный",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "Имя, отображаемое в приложении",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "Публичный URL вашего сервера, используемый для OAuth-перенаправлений, вебхуков и других внешних интеграций",
"The requested chat preset does not exist or has been removed.": "Запрошенный предустановленный чат не существует или был удален.",
"The reset request stays disabled until a credit is available.": "Запрос сброса недоступен, пока нет доступного сброса.",
"The setup wizard will use this database during initialization.": "Мастер настройки будет использовать эту базу данных при инициализации.",
"The site is not available at the moment.": "Сайт в данный момент недоступен.",
"The slug is appended to the URL:": "Слаг добавляется к URL:",
......@@ -4402,6 +4429,7 @@
"Upload photo": "Загрузить фото",
"Upscale": "Увеличение",
"Upstream": "Источник",
"Upstream did not return reset credit details.": "Вышестоящий сервис не вернул сведения о сбросах лимита.",
"Upstream Model Detection Settings": "Настройки обнаружения моделей провайдера",
"Upstream Model Update Check": "Проверка обновлений моделей провайдера",
"Upstream Model Updates": "Обновления моделей источника",
......@@ -4447,6 +4475,8 @@
"Use backup code": "Использовать резервный код",
"Use disk cache when request body exceeds this size": "Использовать дисковый кэш, когда тело запроса превышает этот размер",
"Use external tools to extend capabilities": "Использовать внешние инструменты для расширения возможностей",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Для этого канала будет использован один доступный сброс. Запрос отправляется только после подтверждения.",
"Use one available reset credit to refresh the current Codex usage windows.": "Использует один доступный сброс, чтобы обновить текущие окна использования Codex.",
"Use our unified OpenAI-compatible endpoint in your applications": "Используйте наш единый OpenAI-совместимый эндпоинт в ваших приложениях",
"Use Passkey to sign in without entering your password.": "Используйте ключ доступа для входа без ввода пароля.",
"Use presets or upstream discovery to populate the model list faster.": "Используйте пресеты или обнаружение upstream, чтобы быстрее заполнить список моделей.",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "Используется для балансировки нагрузки. Больший вес = больше запросов",
"Used in URLs and API routes": "Используется в URL и маршрутах API",
"Used Quota": "Лимит потребления",
"Used reset credits cannot be restored.": "Использованные сбросы нельзя восстановить.",
"Used to authenticate with io.net deployment API": "Используется для аутентификации в API развертывания io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Используется для аутентификации с воркером. Оставьте пустым, чтобы сохранить существующий секрет.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Определяет платежный сценарий после нажатия. Встроенные ключи: stripe для Stripe и waffo_pancake для Waffo Pancake; остальные значения отправляются в Epay как параметр type.",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "Просмотр подробной информации об этом пользователе, включая баланс, статистику использования и данные приглашения.",
"View details": "Просмотреть детали",
"View document": "Просмотреть документ",
"View issued reset credits, grant dates, and expiration.": "Показать выданные сбросы лимита, даты выдачи и истечения.",
"View logs": "Просмотреть логи",
"View mode": "Режим отображения",
"View model statistics and charts": "Просмотр статистики и графиков моделей",
......
......@@ -234,6 +234,7 @@
"Advanced Settings": "Cài đặt nâng cao",
"Advanced text editing": "Chỉnh sửa văn bản nâng cao",
"Aesthetic style": "Phong cách",
"Affected windows:": "Cửa sổ bị ảnh hưởng:",
"After clicking the button, you'll be asked to authorize the bot": "Sau khi nhấp vào nút, bạn sẽ được yêu cầu ủy quyền cho bot",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "Sau khi vô hiệu hóa, sẽ không hiển thị cho người dùng nữa, nhưng đơn hàng lịch sử không bị ảnh hưởng. Tiếp tục?",
"After enabling, the plan will be shown to users. Continue?": "Sau khi kích hoạt, gói sẽ được hiển thị cho người dùng. Tiếp tục?",
......@@ -394,6 +395,7 @@
"Apply Filters": "Áp dụng bộ lọc",
"Apply IP Filter to Resolved Domains": "Áp dụng Bộ lọc IP cho Tên miền đã phân giải",
"Apply Overwrite": "Áp dụng Ghi đè",
"Apply reset": "Thực hiện đặt lại",
"Apply Sync": "Áp dụng đồng bộ",
"Applying...": "Đang áp dụng...",
"Approx.": "Xấp xỉ.",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "Tự động đồng bộ danh sách mô hình khi phát hiện thay đổi từ nguồn",
"Availability (last 24h)": "Khả dụng (24 giờ qua)",
"Available": "Khả dụng",
"Available credits are ordered by soonest expiration.": "Các lượt khả dụng được sắp xếp theo thời điểm hết hạn gần nhất.",
"Available disk space": "Dung lượng đĩa khả dụng",
"Available Models": "Mô hình khả dụng",
"Available reset credits": "Lượt đặt lại khả dụng",
"Available Rewards": "Phần thưởng hiện có",
"Average latency": "Độ trễ trung bình",
"Average latency, TTFT, and success rate by group": "Độ trễ trung bình, TTFT và tỷ lệ thành công theo nhóm",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "Kênh đã được bật thành công",
"Channel Extra Settings": "Cài đặt thêm kênh",
"Channel ID": "Mã kênh",
"Channel ID is required": "Cần có ID kênh",
"Channel key": "Khóa kênh",
"Channel key unlocked": "Khóa kênh đã được mở khóa",
"Channel models": "Mô hình kênh",
......@@ -811,6 +816,7 @@
"Codes copied!": "Đã sao chép mã!",
"Codex": "Codex",
"Codex Account & Usage": "Tài khoản và sử dụng Codex",
"Codex Account Status": "Trạng thái tài khoản Codex",
"Codex channels do not support batch creation": "Kênh Codex không hỗ trợ tạo hàng loạt",
"Codex channels use an OAuth JSON credential as the key.": "Kênh Codex dùng thông tin xác thực OAuth JSON làm khóa.",
"Codex CLI Header Passthrough": "Chuyển tiếp header Codex CLI",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Xác nhận cài đặt và hoàn tất thiết lập",
"confirm that I bear legal responsibility arising from deployment": "xác nhận rằng tôi chịu trách nhiệm pháp lý phát sinh từ việc triển khai",
"Confirm Unbind": "Xác nhận hủy liên kết",
"Confirm usage reset": "Xác nhận đặt lại mức dùng",
"Confirm your identity before removing this Passkey from your account.": "Hãy xác minh danh tính trước khi gỡ Passkey khỏi tài khoản.",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "Hãy xác minh danh tính bằng Xác thực hai yếu tố trước khi đăng ký Passkey.",
"Confirmed at {{time}} by user #{{userId}}": "Được xác nhận lúc {{time}} bởi người dùng #{{userId}}",
......@@ -1625,6 +1632,8 @@
"Expired at": "Hết hạn lúc",
"Expired time cannot be earlier than current time": "Thời gian hết hạn không thể sớm hơn thời gian hiện tại",
"Expires": "Hết hạn",
"Expires at": "Hết hạn lúc",
"Expires in": "Còn lại",
"Expose ratio API": "Cung cấp API tỷ lệ",
"Exposes the pricing/models catalog in the top navigation.": "Hiển thị danh mục giá/mô hình trên thanh điều hướng đầu trang.",
"Expression": "Biểu thức",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Không thể lấy chi tiết triển khai",
"Failed to fetch models": "Không thể lấy các mô hình",
"Failed to fetch OIDC configuration. Please check the URL and network status": "Không thể lấy cấu hình OIDC. Vui lòng kiểm tra URL và trạng thái mạng",
"Failed to fetch reset credit details": "Không lấy được chi tiết lượt đặt lại",
"Failed to fetch upstream prices": "Không thể lấy giá từ upstream",
"Failed to fetch upstream ratios": "Không thể lấy tỷ lệ upstream",
"Failed to fetch usage": "Không lấy được mức sử dụng",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Không thể đặt lại 2FA",
"Failed to reset model ratios": "Không thể đặt lại tỷ lệ mô hình",
"Failed to reset Passkey": "Không thể đặt lại Passkey",
"Failed to reset usage": "Không thể đặt lại mức dùng",
"Failed to save": "Lưu thất bại",
"Failed to save announcements": "Không thể lưu thông báo",
"Failed to save API info": "Không thể lưu thông tin API",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, v.v.",
"GPU count": "Số lượng GPU",
"Granted at": "Được cấp lúc",
"Greater than": "Lớn hơn",
"Greater Than": "Lớn hơn",
"Greater than or equal": "Lớn hơn hoặc bằng",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "Không có mô hình liên quan nào cho loại kênh này",
"No release notes provided.": "Không có ghi chú phát hành nào được cung cấp.",
"No Reset": "Không đặt lại",
"No reset credits": "Không có lượt đặt lại",
"No reset credits available": "Không có lượt đặt lại khả dụng",
"No restriction": "Không giới hạn",
"No results for \"{{query}}\". Try adjusting your search or filters.": "Không tìm thấy kết quả cho \"{{query}}\". Hãy thử điều chỉnh tìm kiếm hoặc bộ lọc.",
"No results found": "Không tìm thấy kết quả nào",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Giá token đầu ra cho nội dung được tạo.",
"Output tokens": "Token đầu ra",
"Output Tokens": "Token đầu ra",
"Overage limited": "Đã giới hạn vượt mức",
"overall": "tổng",
"Overnight range": "Khoảng qua nửa đêm",
"override": "ghi đè",
......@@ -3346,6 +3361,8 @@
"Recursive": "Đệ quy",
"Redeem": "Đổi",
"Redeem codes": "Đổi mã",
"Redeemed": "Đã dùng",
"Redeemed at": "Đã dùng lúc",
"Redeemed By": "Được chuộc bởi",
"Redeemed:": "Đã đổi:",
"redemption code": "mã đổi thưởng",
......@@ -3373,6 +3390,7 @@
"Refresh": "Làm mới",
"Refresh Cache": "Làm mới bộ nhớ đệm",
"Refresh credential": "Làm mới thông tin xác thực",
"Refresh details": "Làm mới chi tiết",
"Refresh failed": "Làm mới thất bại",
"Refresh interval (minutes)": "Khoảng thời gian làm mới (phút)",
"Refresh Stats": "Làm mới thống kê",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Đặt lại tất cả cài đặt về giá trị mặc định",
"Reset at:": "Đặt lại lúc:",
"Reset balance and used quota": "Đặt lại số dư và hạn mức đã sử dụng",
"Reset completed": "Đặt lại hoàn tất",
"Reset completed. Latest usage has been refreshed.": "Đặt lại hoàn tất. Dữ liệu sử dụng mới nhất đã được làm mới.",
"Reset count:": "Số lần đặt lại:",
"Reset Credit": "Lượt đặt lại",
"Reset Credits": "Lượt đặt lại",
"Reset Cycle": "Chu kỳ đặt lại",
"Reset email sent, please check your inbox": "Email đặt lại đã được gửi, vui lòng kiểm tra hộp thư đến",
"Reset failed": "Đặt lại thất bại",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "Đặt lại mặc định",
"Reset to default configuration": "Đã đặt lại cấu hình mặc định",
"Reset Two-Factor Authentication": "Đặt lại Xác thực hai yếu tố",
"Reset usage window": "Đặt lại cửa sổ mức dùng",
"Resets in:": "Đặt lại sau:",
"Resetting...": "Đang đặt lại...",
"Resolve Conflicts": "Giải quyết Xung đột",
"Resource Configuration": "Cấu hình tài nguyên",
"Response": "Phản hồi",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "Tỷ lệ đặc biệt ghi đè tỷ lệ nhóm token cho các tổ hợp nhóm người dùng và nhóm token cụ thể.",
"Special usable group rules": "Quy tắc nhóm sử dụng đặc biệt",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Quy tắc nhóm khả dụng đặc biệt có thể thêm, xóa hoặc nối nhóm token có thể chọn cho một nhóm người dùng cụ thể.",
"Spend limited": "Đã giới hạn chi tiêu",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite lưu trữ tất cả dữ liệu trong một tệp duy nhất. Đảm bảo tệp được lưu trữ lâu dài khi chạy trong container.",
"SSRF Protection": "Bảo vệ SSRF",
"Standard": "Tiêu chuẩn",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "Tên hiển thị trên ứng dụng",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "URL công khai của máy chủ, dùng cho callback OAuth, webhook và các tích hợp bên ngoài khác",
"The requested chat preset does not exist or has been removed.": "Cài đặt sẵn cuộc trò chuyện được yêu cầu không tồn tại hoặc đã bị xóa.",
"The reset request stays disabled until a credit is available.": "Yêu cầu đặt lại sẽ bị tắt cho đến khi có lượt khả dụng.",
"The setup wizard will use this database during initialization.": "Trình hướng dẫn thiết lập sẽ sử dụng cơ sở dữ liệu này trong quá trình khởi tạo.",
"The site is not available at the moment.": "Trang web hiện không khả dụng.",
"The slug is appended to the URL:": "Slug được gắn vào URL:",
......@@ -4402,6 +4429,7 @@
"Upload photo": "Tải ảnh lên",
"Upscale": "Phóng to",
"Upstream": "Thượng nguồn",
"Upstream did not return reset credit details.": "Upstream không trả về chi tiết lượt đặt lại.",
"Upstream Model Detection Settings": "Cài đặt phát hiện mô hình nguồn",
"Upstream Model Update Check": "Kiểm tra cập nhật mô hình nguồn",
"Upstream Model Updates": "Cập nhật mô hình upstream",
......@@ -4447,6 +4475,8 @@
"Use backup code": "Sử dụng mã dự phòng",
"Use disk cache when request body exceeds this size": "Sử dụng bộ nhớ đệm đĩa khi nội dung yêu cầu vượt quá kích thước này",
"Use external tools to extend capabilities": "Sử dụng công cụ ngoài để mở rộng khả năng",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Sử dụng một lượt đặt lại khả dụng cho kênh này. Yêu cầu chỉ được gửi sau khi xác nhận.",
"Use one available reset credit to refresh the current Codex usage windows.": "Sử dụng một lượt đặt lại khả dụng để làm mới các cửa sổ mức dùng Codex hiện tại.",
"Use our unified OpenAI-compatible endpoint in your applications": "Sử dụng endpoint thống nhất tương thích OpenAI trong ứng dụng của bạn",
"Use Passkey to sign in without entering your password.": "Sử dụng Khóa truy cập để đăng nhập mà không cần nhập mật khẩu của bạn.",
"Use presets or upstream discovery to populate the model list faster.": "Dùng mẫu đặt sẵn hoặc phát hiện từ upstream để điền danh sách mô hình nhanh hơn.",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "Được sử dụng để cân bằng tải. Trọng số càng cao = càng nhiều yêu cầu",
"Used in URLs and API routes": "Sử dụng trong URL và các tuyến API",
"Used Quota": "Hạn mức đã sử dụng",
"Used reset credits cannot be restored.": "Các lượt đặt lại đã dùng không thể khôi phục.",
"Used to authenticate with io.net deployment API": "Dùng để xác thực với API triển khai io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Dùng để xác thực với worker. Để trống để giữ nguyên secret hiện có.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Dùng để quyết định luồng thanh toán khi người dùng nhấp. Các khóa có sẵn gồm stripe cho Stripe và waffo_pancake cho Waffo Pancake; các giá trị khác được gửi tới Epay dưới dạng tham số type.",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "Xem thông tin chi tiết về người dùng này bao gồm số dư, thống kê sử dụng và chi tiết lời mời.",
"View details": "Xem chi tiết",
"View document": "Xem tài liệu",
"View issued reset credits, grant dates, and expiration.": "Xem các lượt đặt lại đã cấp, ngày cấp và thời điểm hết hạn.",
"View logs": "Xem nhật ký",
"View mode": "Chế độ xem",
"View model statistics and charts": "Xem thống kê và biểu đồ mô hình",
......
......@@ -234,6 +234,7 @@
"Advanced Settings": "高级设置",
"Advanced text editing": "高级文本编辑",
"Aesthetic style": "画风",
"Affected windows:": "影响窗口:",
"After clicking the button, you'll be asked to authorize the bot": "点击按钮后,您将被要求授权机器人",
"After disabling, it will no longer be shown to users, but historical orders are not affected. Continue?": "禁用后用户端不再展示,但历史订单不受影响。是否继续?",
"After enabling, the plan will be shown to users. Continue?": "启用后套餐将在用户端展示。是否继续?",
......@@ -394,6 +395,7 @@
"Apply Filters": "应用筛选器",
"Apply IP Filter to Resolved Domains": "对已解析的域应用 IP 筛选器",
"Apply Overwrite": "应用覆盖",
"Apply reset": "执行重置",
"Apply Sync": "应用同步",
"Applying...": "正在应用...",
"Approx.": "约",
......@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "检测到上游模型变更时自动同步模型列表",
"Availability (last 24h)": "可用率(最近 24 小时)",
"Available": "可用",
"Available credits are ordered by soonest expiration.": "可用次数按最早到期排序。",
"Available disk space": "可用磁盘空间",
"Available Models": "可用模型",
"Available reset credits": "可用重置次数",
"Available Rewards": "可用奖励",
"Average latency": "平均延迟",
"Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率",
......@@ -686,6 +690,7 @@
"Channel enabled successfully": "渠道启用成功",
"Channel Extra Settings": "渠道额外设置",
"Channel ID": "渠道 ID",
"Channel ID is required": "缺少渠道 ID",
"Channel key": "渠道密钥",
"Channel key unlocked": "渠道密钥已解锁",
"Channel models": "渠道模型",
......@@ -811,6 +816,7 @@
"Codes copied!": "代码已复制!",
"Codex": "Codex",
"Codex Account & Usage": "Codex 账户和用量",
"Codex Account Status": "Codex 账号状态",
"Codex channels do not support batch creation": "Codex 渠道不支持批量创建",
"Codex channels use an OAuth JSON credential as the key.": "Codex 渠道使用 OAuth JSON 凭据作为密钥。",
"Codex CLI Header Passthrough": "Codex CLI 请求头透传",
......@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "确认设置并完成安装",
"confirm that I bear legal responsibility arising from deployment": "确认承担因部署",
"Confirm Unbind": "确认解绑",
"Confirm usage reset": "确认重置用量",
"Confirm your identity before removing this Passkey from your account.": "在解绑当前账号的 Passkey 前请确认你的身份。",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "在注册 Passkey 前请使用两步验证确认你的身份。",
"Confirmed at {{time}} by user #{{userId}}": "用户 #{{userId}} 于 {{time}} 确认",
......@@ -1625,6 +1632,8 @@
"Expired at": "过期于",
"Expired time cannot be earlier than current time": "过期时间不能早于当前时间",
"Expires": "过期",
"Expires at": "到期时间",
"Expires in": "剩余到期时间",
"Expose ratio API": "暴露倍率接口",
"Exposes the pricing/models catalog in the top navigation.": "在顶部导航中显示定价/模型目录。",
"Expression": "表达式",
......@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "获取部署详情失败",
"Failed to fetch models": "获取模型失败",
"Failed to fetch OIDC configuration. Please check the URL and network status": "获取 OIDC 配置失败。请检查 URL 和网络状态",
"Failed to fetch reset credit details": "获取重置次数详情失败",
"Failed to fetch upstream prices": "获取上游价格失败",
"Failed to fetch upstream ratios": "获取上游比率失败",
"Failed to fetch usage": "获取用量失败",
......@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "重置 2FA 失败",
"Failed to reset model ratios": "重置模型比率失败",
"Failed to reset Passkey": "重置 Passkey 失败",
"Failed to reset usage": "重置用量失败",
"Failed to save": "保存失败",
"Failed to save announcements": "保存公告失败",
"Failed to save API info": "保存 API 信息失败",
......@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, 等",
"GPU count": "GPU 数量",
"Granted at": "发放时间",
"Greater than": "大于",
"Greater Than": "大于",
"Greater than or equal": "大于等于",
......@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "此渠道类型没有相关模型可用",
"No release notes provided.": "未提供发布说明。",
"No Reset": "不重置",
"No reset credits": "暂无重置次数",
"No reset credits available": "没有可用重置次数",
"No restriction": "无限制",
"No results for \"{{query}}\". Try adjusting your search or filters.": "未找到 \"{{query}}\" 的结果。请尝试调整搜索或筛选条件。",
"No results found": "搜索无结果",
......@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "生成内容的输出 token 价格。",
"Output tokens": "输出 token",
"Output Tokens": "输出 Token",
"Overage limited": "超额受限",
"overall": "总体",
"Overnight range": "跨日范围",
"override": "覆盖",
......@@ -3346,6 +3361,8 @@
"Recursive": "递归",
"Redeem": "兑换额度",
"Redeem codes": "兑换码",
"Redeemed": "已使用",
"Redeemed at": "使用时间",
"Redeemed By": "兑换人",
"Redeemed:": "已兑换:",
"redemption code": "兑换码",
......@@ -3373,6 +3390,7 @@
"Refresh": "刷新",
"Refresh Cache": "刷新缓存",
"Refresh credential": "刷新凭据",
"Refresh details": "刷新详情",
"Refresh failed": "刷新失败",
"Refresh interval (minutes)": "刷新间隔 (分钟)",
"Refresh Stats": "刷新统计",
......@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "将所有设置重置为默认值",
"Reset at:": "重置于:",
"Reset balance and used quota": "重置余额和已用配额",
"Reset completed": "重置完成",
"Reset completed. Latest usage has been refreshed.": "重置完成,已刷新最新用量。",
"Reset count:": "重置次数:",
"Reset Credit": "重置次数",
"Reset Credits": "重置次数",
"Reset Cycle": "重置周期",
"Reset email sent, please check your inbox": "重置邮件已发送,请检查您的收件箱",
"Reset failed": "重置失败",
......@@ -3506,7 +3529,9 @@
"Reset to Default": "重置为默认",
"Reset to default configuration": "已重置为默认配置",
"Reset Two-Factor Authentication": "重置 2FA",
"Reset usage window": "重置用量窗口",
"Resets in:": "将于以下时间重置:",
"Resetting...": "重置中...",
"Resolve Conflicts": "解决冲突",
"Resource Configuration": "资源配置",
"Response": "响应",
......@@ -3872,6 +3897,7 @@
"Special ratios override the token group ratio for specific user group and token group combinations.": "特殊倍率会针对特定用户分组和令牌分组组合覆盖令牌分组倍率。",
"Special usable group rules": "特殊可用分组规则",
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "特殊可用分组规则可以为特定用户分组添加、移除或追加可选令牌分组。",
"Spend limited": "消费受限",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite 将所有数据存储在单个文件中。在容器中运行时请确保该文件已持久化。",
"SSRF Protection": "SSRF 保护",
"Standard": "标准",
......@@ -4088,6 +4114,7 @@
"The name displayed across the application": "在整个应用程序中显示的名称",
"The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "服务器的公开URL,用于OAuth回调、Webhook和其他外部集成",
"The requested chat preset does not exist or has been removed.": "请求的聊天预设不存在或已被删除。",
"The reset request stays disabled until a credit is available.": "没有可用次数时,重置请求会保持禁用。",
"The setup wizard will use this database during initialization.": "设置向导将在初始化过程中使用此数据库。",
"The site is not available at the moment.": "该站点目前不可用。",
"The slug is appended to the URL:": "别名将附加到 URL:",
......@@ -4402,6 +4429,7 @@
"Upload photo": "上传照片",
"Upscale": "放大",
"Upstream": "上游",
"Upstream did not return reset credit details.": "上游未返回重置次数详情。",
"Upstream Model Detection Settings": "检测上游模型设置",
"Upstream Model Update Check": "上游模型更新检查",
"Upstream Model Updates": "上游模型更新",
......@@ -4447,6 +4475,8 @@
"Use backup code": "使用备用代码",
"Use disk cache when request body exceeds this size": "请求体超过此大小时使用磁盘缓存",
"Use external tools to extend capabilities": "通过外部工具扩展能力",
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "将为当前渠道使用 1 次可用重置次数。只有确认后才会发送重置请求。",
"Use one available reset credit to refresh the current Codex usage windows.": "使用 1 次可用重置次数,刷新当前 Codex 用量窗口。",
"Use our unified OpenAI-compatible endpoint in your applications": "在应用中使用我们兼容 OpenAI 的统一接口",
"Use Passkey to sign in without entering your password.": "使用通行密钥登录,无需输入密码。",
"Use presets or upstream discovery to populate the model list faster.": "使用预设或上游发现来更快填充模型列表。",
......@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "用于负载均衡。权重越高 = 请求越多",
"Used in URLs and API routes": "用于URL和API路由",
"Used Quota": "消耗额度",
"Used reset credits cannot be restored.": "已使用的重置次数无法恢复。",
"Used to authenticate with io.net deployment API": "用于 io.net 部署 API 鉴权",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "用于与 Worker 进行身份验证。留空以保留现有密钥。",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "用于决定点击后走哪个支付流程。stripe 走 Stripe,waffo_pancake 走 Waffo Pancake;其他值会作为 Epay 的 type 参数提交。",
......@@ -4555,6 +4586,7 @@
"View detailed information about this user including balance, usage statistics, and invitation details.": "查看此用户的详细信息,包括余额、使用统计和邀请详情。",
"View details": "查看详情",
"View document": "查看文档",
"View issued reset credits, grant dates, and expiration.": "查看已发放的重置次数、发放日期和到期时间。",
"View logs": "查看日志",
"View mode": "视图模式",
"View model statistics and charts": "查看模型统计和图表",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment