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 ( ...@@ -18,6 +18,46 @@ import (
) )
func GetCodexChannelUsage(c *gin.Context) { 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")) channelId, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
common.ApiError(c, fmt.Errorf("invalid channel id: %w", err)) common.ApiError(c, fmt.Errorf("invalid channel id: %w", err))
...@@ -68,10 +108,10 @@ func GetCodexChannelUsage(c *gin.Context) { ...@@ -68,10 +108,10 @@ func GetCodexChannelUsage(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel() 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 { if err != nil {
common.SysError("failed to fetch codex usage: " + err.Error()) common.SysError(logPrefix + ": " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取用量信息失败,请稍后重试"}) c.JSON(http.StatusOK, gin.H{"success": false, "message": userMessage})
return return
} }
...@@ -98,10 +138,10 @@ func GetCodexChannelUsage(c *gin.Context) { ...@@ -98,10 +138,10 @@ func GetCodexChannelUsage(c *gin.Context) {
ctx2, cancel2 := context.WithTimeout(c.Request.Context(), 15*time.Second) ctx2, cancel2 := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel2() 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 { if err != nil {
common.SysError("failed to fetch codex usage after refresh: " + err.Error()) common.SysError(logPrefix + " after refresh: " + err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "获取用量信息失败,请稍后重试"}) c.JSON(http.StatusOK, gin.H{"success": false, "message": userMessage})
return return
} }
} }
......
...@@ -251,6 +251,8 @@ func SetApiRouter(router *gin.Engine) { ...@@ -251,6 +251,8 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels) channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels)
channelRoute.POST("/:id/codex/refresh", controller.RefreshCodexChannelCredential) channelRoute.POST("/:id/codex/refresh", controller.RefreshCodexChannelCredential)
channelRoute.GET("/:id/codex/usage", controller.GetCodexChannelUsage) 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", controller.OllamaPullModel)
channelRoute.POST("/ollama/pull/stream", controller.OllamaPullModelStream) channelRoute.POST("/ollama/pull/stream", controller.OllamaPullModelStream)
channelRoute.DELETE("/ollama/delete", controller.OllamaDeleteModel) channelRoute.DELETE("/ollama/delete", controller.OllamaDeleteModel)
......
package service package service
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
"github.com/google/uuid"
) )
func FetchCodexWhamUsage( func FetchCodexWhamUsage(
...@@ -35,13 +39,50 @@ func FetchCodexWhamUsage( ...@@ -35,13 +39,50 @@ func FetchCodexWhamUsage(
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
req.Header.Set("Authorization", "Bearer "+at) setCodexWhamRequestHeaders(req, at, aid)
req.Header.Set("chatgpt-account-id", aid)
req.Header.Set("Accept", "application/json") resp, err := client.Do(req)
if req.Header.Get("originator") == "" { if err != nil {
req.Header.Set("originator", "codex_cli_rs") 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) resp, err := client.Do(req)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
...@@ -54,3 +95,67 @@ func FetchCodexWhamUsage( ...@@ -54,3 +95,67 @@ func FetchCodexWhamUsage(
} }
return resp.StatusCode, body, nil 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 { ...@@ -25,7 +25,6 @@ import {
Typography, Typography,
Spin, Spin,
Tag, Tag,
Descriptions,
Collapse, Collapse,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { API, showError } from '../../../../helpers'; import { API, showError } from '../../../../helpers';
...@@ -33,6 +32,21 @@ import { MOBILE_BREAKPOINT } from '../../../../hooks/common/useIsMobile'; ...@@ -33,6 +32,21 @@ import { MOBILE_BREAKPOINT } from '../../../../hooks/common/useIsMobile';
const { Text } = Typography; 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 clampPercent = (value) => {
const v = Number(value); const v = Number(value);
if (!Number.isFinite(v)) return 0; if (!Number.isFinite(v)) return 0;
...@@ -146,12 +160,12 @@ const getCodexUsageModalLayout = () => { ...@@ -146,12 +160,12 @@ const getCodexUsageModalLayout = () => {
return { return {
width: 'calc(100vw - 16px)', width: 'calc(100vw - 16px)',
style: { style: {
top: 8, top: 0,
maxWidth: 'calc(100vw - 16px)', maxWidth: 'calc(100vw - 16px)',
margin: '0 auto', margin: '8px auto',
}, },
bodyStyle: { bodyStyle: {
maxHeight: 'calc(100vh - 148px)', maxHeight: 'calc(100vh - 164px)',
overflowY: 'auto', overflowY: 'auto',
padding: '16px 16px 12px', padding: '16px 16px 12px',
}, },
...@@ -161,11 +175,12 @@ const getCodexUsageModalLayout = () => { ...@@ -161,11 +175,12 @@ const getCodexUsageModalLayout = () => {
return { return {
width: 900, width: 900,
style: { style: {
top: 24, top: 0,
margin: '16px auto',
maxWidth: 'min(900px, 92vw)', maxWidth: 'min(900px, 92vw)',
}, },
bodyStyle: { bodyStyle: {
maxHeight: 'calc(100vh - 172px)', maxHeight: 'calc(100vh - 188px)',
overflowY: 'auto', overflowY: 'auto',
padding: '20px 24px 16px', padding: '20px 24px 16px',
}, },
...@@ -220,34 +235,70 @@ const resolveUsageStatusTag = (t, rateLimit) => { ...@@ -220,34 +235,70 @@ const resolveUsageStatusTag = (t, rateLimit) => {
return <Tag color='red'>{tt('受限')}</Tag>; 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 tt = typeof t === 'function' ? t : (v) => v;
const text = getDisplayText(value); const text = getDisplayText(value);
const hasValue = text !== ''; const hasValue = text !== '';
return ( return (
<div className='flex min-w-0 items-start justify-between gap-2'>
<div <div
className={`min-w-0 flex-1 break-all text-xs leading-5 text-semi-color-text-1 ${ 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}`}
monospace ? 'font-mono' : '' >
<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 : '-'} {hasValue ? text : '-'}
</div> </div>
{onCopy ? (
<Button <Button
size='small' size='small'
type='tertiary' type='tertiary'
theme='borderless' theme='borderless'
className='shrink-0 px-1 text-xs' className='h-6 shrink-0 px-1 text-xs'
disabled={!hasValue} disabled={!hasValue}
onClick={() => onCopy?.(text)} onClick={() => onCopy(text)}
> >
{tt('复制')} {tt('复制')}
</Button> </Button>
) : null}
</div>
</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 RateLimitWindowCard = ({ t, title, windowData }) => {
const tt = typeof t === 'function' ? t : (v) => v; const tt = typeof t === 'function' ? t : (v) => v;
const hasWindowData = const hasWindowData =
...@@ -260,13 +311,30 @@ const RateLimitWindowCard = ({ t, title, windowData }) => { ...@@ -260,13 +311,30 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
const limitWindowSeconds = windowData?.limit_window_seconds; const limitWindowSeconds = windowData?.limit_window_seconds;
return ( return (
<div className='rounded-lg border border-semi-color-border bg-semi-color-bg-0 p-3'> <div className='rounded-lg bg-semi-color-bg-0 p-3 shadow-[0_1px_2px_rgba(0,0,0,0.04)]'>
<div className='flex flex-wrap items-start justify-between gap-x-3 gap-y-1'> <div className='flex items-start justify-between gap-3'>
<div className='font-medium'>{title}</div> <div className='min-w-0'>
<Text type='tertiary' size='small'> <div className='text-sm font-semibold text-semi-color-text-0'>
{tt('重置时间:')} {title}
{formatUnixSeconds(resetAt)} </div>
</Text> <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> </div>
{hasWindowData ? ( {hasWindowData ? (
...@@ -274,25 +342,25 @@ const RateLimitWindowCard = ({ t, title, windowData }) => { ...@@ -274,25 +342,25 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
<Progress <Progress
percent={percent} percent={percent}
stroke={pickStrokeColor(percent)} stroke={pickStrokeColor(percent)}
showInfo={true} showInfo={false}
/> />
</div> </div>
) : ( ) : (
<div className='mt-3 text-sm text-semi-color-text-2'>-</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 className='mt-3 grid grid-cols-1 gap-2 text-xs text-semi-color-text-2 sm:grid-cols-2'>
<div> <div className='min-w-0'>
{tt('已使用:')} <div className='text-[11px]'>{tt('重置时间')}</div>
{hasWindowData ? `${percent}%` : '-'} <div className='break-all text-semi-color-text-0 [font-variant-numeric:tabular-nums]'>
{hasWindowData ? formatUnixSeconds(resetAt) : '-'}
</div> </div>
<div> </div>
{tt('距离重置:')} <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) : '-'} {hasWindowData ? formatDurationSeconds(resetAfterSeconds, tt) : '-'}
</div> </div>
<div>
{tt('窗口:')}
{hasWindowData ? formatDurationSeconds(limitWindowSeconds, tt) : '-'}
</div> </div>
</div> </div>
</div> </div>
...@@ -332,20 +400,12 @@ const RateLimitGroupSection = ({ ...@@ -332,20 +400,12 @@ const RateLimitGroupSection = ({
const featureText = getDisplayText(meteredFeature); const featureText = getDisplayText(meteredFeature);
return ( return (
<section className='space-y-3'> <section className='space-y-3 rounded-lg bg-semi-color-fill-0 p-3'>
<div className='flex flex-wrap items-start justify-between gap-3'> <SectionHeading title={title} description={description}>
<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>
{statusTag} {statusTag}
</div> </SectionHeading>
{(description || featureText) && (
<div className='flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
{description ? <span>{description}</span> : null}
{featureText ? ( {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'> <span className='text-[11px] text-semi-color-text-2'>
metered_feature metered_feature
</span> </span>
...@@ -354,10 +414,6 @@ const RateLimitGroupSection = ({ ...@@ -354,10 +414,6 @@ const RateLimitGroupSection = ({
</span> </span>
</div> </div>
) : null} ) : null}
</div>
)}
</div>
</div>
<RateLimitWindowGrid <RateLimitWindowGrid
t={tt} t={tt}
...@@ -386,7 +442,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -386,7 +442,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
const statusTag = resolveUsageStatusTag(tt, rateLimit); const statusTag = resolveUsageStatusTag(tt, rateLimit);
const userId = data?.user_id; const userId = data?.user_id;
const email = data?.email; 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 = const errorMessage =
payload?.success === false payload?.success === false
? getDisplayText(payload?.message) || tt('获取用量失败') ? getDisplayText(payload?.message) || tt('获取用量失败')
...@@ -398,16 +455,27 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -398,16 +455,27 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{errorMessage && ( {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} {errorMessage}
</div> </div>
)} )}
<div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-3'> <div className='rounded-lg bg-semi-color-fill-0 p-4'>
<div className='flex flex-wrap items-start justify-between gap-2'> <div className='flex items-start justify-between gap-3'>
<div className='min-w-0'> <div className='min-w-0'>
<div className='text-xs font-medium text-semi-color-text-2'> <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>
<div className='mt-2 flex flex-wrap items-center gap-2'> <div className='mt-2 flex flex-wrap items-center gap-2'>
<Tag <Tag
...@@ -421,86 +489,59 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -421,86 +489,59 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
</Tag> </Tag>
{statusTag} {statusTag}
<Tag color='grey' type='light' shape='circle'> <Tag color='grey' type='light' shape='circle'>
{tt('上游状态码:')} HTTP {upstreamStatus ?? '-'}
{upstreamStatus ?? '-'}
</Tag> </Tag>
</div> <Tag
</div> color={Number(resetCredits) > 0 ? 'blue' : 'grey'}
<Button type='light'
size='small' shape='circle'
type='tertiary'
theme='outline'
onClick={onRefresh}
> >
{tt('刷新')} {tt('重置次数:')}
</Button> {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>
<div className='mt-2 rounded-lg bg-semi-color-fill-0 px-3 py-2'> <div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
<Descriptions> <InfoField t={tt} label={tt('邮箱')} value={email} onCopy={onCopy} />
<Descriptions.Item itemKey='User ID'> <InfoField t={tt} label={tt('渠道')} value={channelLabel} />
<AccountInfoValue <InfoField
t={tt} t={tt}
label='User ID'
value={userId} value={userId}
onCopy={onCopy} onCopy={onCopy}
monospace={true} 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>
<div> <div className='space-y-3'>
<div className='mb-2'> <SectionHeading
<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}
title={tt('基础额度')} title={tt('基础额度')}
description={tt('当前帐号的基础额度窗口')} description={tt('当前帐号的基础额度窗口')}
rateLimitSource={data} >
statusTag={statusTag} {statusTag}
/> </SectionHeading>
<RateLimitWindowGrid t={tt} {...resolveRateLimitWindows(data)} />
{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>
</div> </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) => { {additionalRateLimits.map((item, index) => {
const limitName = const limitName =
getDisplayText(item?.limit_name) || getDisplayText(item?.limit_name) ||
...@@ -508,13 +549,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -508,13 +549,8 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
`${tt('附加额度')} ${index + 1}`; `${tt('附加额度')} ${index + 1}`;
return ( return (
<div
key={`${limitName}-${getDisplayText(item?.metered_feature)}-${index}`}
className={
index > 0 ? 'border-t border-semi-color-border pt-4' : ''
}
>
<RateLimitGroupSection <RateLimitGroupSection
key={`${limitName}-${getDisplayText(item?.metered_feature)}-${index}`}
t={tt} t={tt}
title={limitName} title={limitName}
description={tt('附加计费能力')} description={tt('附加计费能力')}
...@@ -522,13 +558,11 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -522,13 +558,11 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
statusTag={resolveUsageStatusTag(tt, item?.rate_limit)} statusTag={resolveUsageStatusTag(tt, item?.rate_limit)}
meteredFeature={item?.metered_feature} meteredFeature={item?.metered_feature}
/> />
</div>
); );
})} })}
</div> </div>
</div> </div>
) : null} ) : null}
</div>
<Collapse <Collapse
activeKey={showRawJson ? ['raw-json'] : []} activeKey={showRawJson ? ['raw-json'] : []}
...@@ -536,6 +570,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -536,6 +570,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
const keys = Array.isArray(activeKey) ? activeKey : [activeKey]; const keys = Array.isArray(activeKey) ? activeKey : [activeKey];
setShowRawJson(keys.includes('raw-json')); 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'> <Collapse.Panel header={tt('原始 JSON')} itemKey='raw-json'>
<div className='mb-2 flex justify-end'> <div className='mb-2 flex justify-end'>
...@@ -549,7 +584,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { ...@@ -549,7 +584,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
{tt('复制')} {tt('复制')}
</Button> </Button>
</div> </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} {rawText}
</pre> </pre>
</Collapse.Panel> </Collapse.Panel>
...@@ -609,14 +644,19 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => { ...@@ -609,14 +644,19 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
if (loading) { if (loading) {
return ( return (
<>
<CodexUsageModalStyles />
<div className='flex items-center justify-center py-10'> <div className='flex items-center justify-center py-10'>
<Spin spinning={true} size='large' tip={tt('加载中...')} /> <Spin spinning={true} size='large' tip={tt('加载中...')} />
</div> </div>
</>
); );
} }
if (!payload) { if (!payload) {
return ( return (
<>
<CodexUsageModalStyles />
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<Text type='danger'>{tt('获取用量失败')}</Text> <Text type='danger'>{tt('获取用量失败')}</Text>
<div className='flex justify-end'> <div className='flex justify-end'>
...@@ -630,10 +670,13 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => { ...@@ -630,10 +670,13 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
</Button> </Button>
</div> </div>
</div> </div>
</>
); );
} }
return ( return (
<>
<CodexUsageModalStyles />
<CodexUsageView <CodexUsageView
t={tt} t={tt}
record={record} record={record}
...@@ -641,6 +684,7 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => { ...@@ -641,6 +684,7 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
onCopy={onCopy} onCopy={onCopy}
onRefresh={fetchUsage} onRefresh={fetchUsage}
/> />
</>
); );
}; };
...@@ -650,6 +694,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => { ...@@ -650,6 +694,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
Modal.info({ Modal.info({
title: tt('Codex 帐号与用量'), title: tt('Codex 帐号与用量'),
className: CODEX_USAGE_MODAL_CLASS_NAME,
centered: false, centered: false,
width: layout.width, width: layout.width,
style: layout.style, style: layout.style,
......
...@@ -53,6 +53,10 @@ export type CodexUsageResponse = { ...@@ -53,6 +53,10 @@ export type CodexUsageResponse = {
data?: Record<string, unknown> data?: Record<string, unknown>
} }
export type CodexResetCreditsResponse = CodexUsageResponse
export type CodexUsageResetResponse = CodexUsageResponse
export type CodexCredentialRefreshResponse = { export type CodexCredentialRefreshResponse = {
success: boolean success: boolean
message?: string message?: string
...@@ -287,6 +291,27 @@ export async function getCodexUsage( ...@@ -287,6 +291,27 @@ export async function getCodexUsage(
return res.data 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 // Multi-Key Management
// ============================================================================ // ============================================================================
......
...@@ -16,25 +16,53 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,25 +16,53 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useMemo, useState } from 'react' import { type ReactNode, useCallback, useMemo, useState } from 'react'
import { import {
Copy, Copy,
Check, Check,
RefreshCw, RefreshCw,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
User, RotateCcw,
Mail, AlertTriangle,
Hash,
} from 'lucide-react' } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import dayjs from '@/lib/dayjs' 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 { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' 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 { Progress } from '@/components/ui/progress'
import { ScrollArea } from '@/components/ui/scroll-area' 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 { Dialog } from '@/components/dialog'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import {
getCodexResetCredits,
resetCodexUsage,
type CodexResetCreditsResponse,
} from '../../api'
type CodexRateLimitWindow = { type CodexRateLimitWindow = {
used_percent?: number used_percent?: number
...@@ -60,13 +88,41 @@ type CodexAdditionalRateLimit = { ...@@ -60,13 +88,41 @@ type CodexAdditionalRateLimit = {
plan_type?: string 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 = { type CodexUsagePayload = {
plan_type?: string plan_type?: string
user_id?: string user_id?: string
email?: string email?: string
account_id?: string
rate_limit?: CodexRateLimit rate_limit?: CodexRateLimit
additional_rate_limits?: CodexAdditionalRateLimit[] additional_rate_limits?: CodexAdditionalRateLimit[]
rate_limit_reset_credits?: {
available_count?: number
}
credits?: {
overage_limit_reached?: boolean
}
spend_control?: {
reached?: boolean
}
} }
export type CodexUsageDialogData = { export type CodexUsageDialogData = {
...@@ -82,7 +138,7 @@ type CodexUsageDialogProps = { ...@@ -82,7 +138,7 @@ type CodexUsageDialogProps = {
channelName?: string channelName?: string
channelId?: number channelId?: number
response: CodexUsageDialogData | null response: CodexUsageDialogData | null
onRefresh?: () => void onRefresh?: () => void | Promise<void>
isRefreshing?: boolean isRefreshing?: boolean
} }
...@@ -93,12 +149,14 @@ function clampPercent(value: unknown): number { ...@@ -93,12 +149,14 @@ function clampPercent(value: unknown): number {
function formatUnixSeconds(unixSeconds: unknown): string { function formatUnixSeconds(unixSeconds: unknown): string {
const v = Number(unixSeconds) const v = Number(unixSeconds)
if (!Number.isFinite(v) || v <= 0) return '-' return Number.isFinite(v) && v > 0 ? formatTimestampToDate(v) : '-'
try { }
return dayjs(v * 1000).format('YYYY-MM-DD HH:mm:ss')
} catch { function formatIsoTimestamp(value: unknown): string {
return String(unixSeconds) if (typeof value !== 'string' || value.trim() === '') return '-'
} const d = dayjs(value)
if (!d.isValid()) return value
return formatDateTimeStr(d.toDate())
} }
function formatDurationSeconds( function formatDurationSeconds(
...@@ -118,11 +176,64 @@ function formatDurationSeconds( ...@@ -118,11 +176,64 @@ function formatDurationSeconds(
return `${secs}${t('s')}` 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 { function normalizePlanType(value: unknown): string {
if (value == null) return '' if (value == null) return ''
return String(value).trim().toLowerCase() 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( function classifyWindowByDuration(
windowData?: CodexRateLimitWindow | null windowData?: CodexRateLimitWindow | null
): 'weekly' | 'fiveHour' | null { ): 'weekly' | 'fiveHour' | null {
...@@ -190,6 +301,15 @@ const PLAN_TYPE_BADGE: Record< ...@@ -190,6 +301,15 @@ const PLAN_TYPE_BADGE: Record<
free: { label: 'Free', variant: 'warning' }, 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( function getAccountTypeBadge(
value: unknown, value: unknown,
t: (key: string) => string t: (key: string) => string
...@@ -203,6 +323,19 @@ function getAccountTypeBadge( ...@@ -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) { function windowLabel(windowData?: CodexRateLimitWindow | null) {
const percent = clampPercent(windowData?.used_percent) const percent = clampPercent(windowData?.used_percent)
const variant: StatusBadgeProps['variant'] = const variant: StatusBadgeProps['variant'] =
...@@ -210,6 +343,54 @@ function windowLabel(windowData?: CodexRateLimitWindow | null) { ...@@ -210,6 +343,54 @@ function windowLabel(windowData?: CodexRateLimitWindow | null) {
return { percent, variant } 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 = { type RateLimitWindowProps = {
title: string title: string
window?: CodexRateLimitWindow | null window?: CodexRateLimitWindow | null
...@@ -224,34 +405,107 @@ function RateLimitWindow(props: RateLimitWindowProps) { ...@@ -224,34 +405,107 @@ function RateLimitWindow(props: RateLimitWindowProps) {
const { percent, variant } = windowLabel(props.window) const { percent, variant } = windowLabel(props.window)
return ( return (
<div className='rounded-lg border p-4'> <Card size='sm' className='gap-0 py-0'>
<div className='flex items-center justify-between gap-2'> <CardHeader className='p-3 pb-2'>
<div className='text-sm font-medium'>{props.title}</div> <div className='flex items-start justify-between gap-3'>
<StatusBadge label={`${percent}%`} variant={variant} copyable={false} /> <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>
<div className='mt-3'> </div>
</CardHeader>
<CardContent className='p-3 pt-0'>
{hasData ? (
<Progress <Progress
value={percent} value={percent}
aria-label={`${props.title} usage: ${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> </div>
{hasData ? ( <div className='break-all tabular-nums'>
<div className='text-muted-foreground mt-2 space-y-1 text-xs'> {hasData ? formatUnixSeconds(props.window?.reset_at) : '-'}
<div>
{t('Reset at:')} {formatUnixSeconds(props.window?.reset_at)}
</div> </div>
<div>
{t('Resets in:')}{' '}
{formatDurationSeconds(props.window?.reset_after_seconds, t)}
</div> </div>
<div> <div className='min-w-0 sm:text-right'>
{t('Window:')}{' '} <div className='text-muted-foreground text-[11px]'>
{formatDurationSeconds(props.window?.limit_window_seconds, t)} {t('Resets in:')}
</div> </div>
<div className='tabular-nums'>
{hasData
? formatDurationSeconds(props.window?.reset_after_seconds, t)
: '-'}
</div> </div>
) : ( </div>
<div className='text-muted-foreground mt-2 text-xs'>-</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> </div>
) )
} }
...@@ -266,72 +520,310 @@ type RateLimitGroupSectionProps = { ...@@ -266,72 +520,310 @@ type RateLimitGroupSectionProps = {
function RateLimitGroupSection(props: RateLimitGroupSectionProps) { function RateLimitGroupSection(props: RateLimitGroupSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(props.source) const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(props.source)
const statusBadge = getUsageStatusBadge(props.source?.rate_limit, t)
return ( return (
<section className='space-y-3'> <section className='bg-muted/40 flex flex-col gap-3 rounded-xl p-3'>
<div className='space-y-1'> <SectionHeading title={props.title} description={props.description}>
<div className='text-sm font-semibold'>{props.title}</div> {statusBadge}
{(props.description || props.meteredFeature) && ( </SectionHeading>
<div className='text-muted-foreground flex flex-wrap items-center gap-2 text-xs'> {props.meteredFeature ? (
{props.description && <span>{props.description}</span>} <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'>
{props.meteredFeature && ( <span className='text-muted-foreground text-[11px]'>
<span className='bg-muted/60 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-0.5'> metered_feature
<span className='text-[11px]'>metered_feature</span>
<span className='min-w-0 font-mono text-xs break-all'>
{props.meteredFeature}
</span> </span>
<span className='min-w-0 font-mono break-all'>
{props.meteredFeature}
</span> </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> </div>
) : null}
<RateLimitWindowGrid
fiveHourWindow={fiveHourWindow}
weeklyWindow={weeklyWindow}
/>
</section> </section>
) )
} }
function CopyableField(props: { function InfoField(props: {
icon: React.ReactNode
label: string label: string
value?: string | null value?: string | null
mono?: boolean mono?: boolean
copyable?: boolean
className?: string
}) { }) {
const { t } = useTranslation()
const { copyToClipboard, copiedText } = useCopyToClipboard({ notify: false }) const { copyToClipboard, copiedText } = useCopyToClipboard({ notify: false })
const text = props.value?.trim() || '' const text = props.value?.trim() || ''
const hasCopied = copiedText === text const hasCopied = copiedText === text
return ( return (
<div className='flex items-center justify-between gap-2 py-1'> <div
<div className='flex min-w-0 items-center gap-2'> className={cn(
<span className='text-muted-foreground flex-shrink-0'> 'bg-background ring-border/60 min-w-0 rounded-lg p-3 ring-1',
{props.icon} props.className
</span> )}
<span className='text-muted-foreground flex-shrink-0 text-xs'> >
<div className='text-muted-foreground text-[11px] font-medium'>
{props.label} {props.label}
</span> </div>
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
<span <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 || '-'} {text || '-'}
</span> </span>
</div> {props.copyable !== false && text ? (
{text && (
<Button <Button
type='button' type='button'
variant='ghost' variant='ghost'
size='sm' size='icon-xs'
className='h-6 w-6 flex-shrink-0 p-0' aria-label={t('Copy')}
onClick={() => copyToClipboard(text)} onClick={() => copyToClipboard(text)}
> >
{hasCopied ? ( {hasCopied ? <Check className='text-success' /> : <Copy />}
<Check className='h-3 w-3 text-green-600' /> </Button>
) : ( ) : null}
<Copy className='h-3 w-3' /> </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> </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> </div>
) )
...@@ -349,6 +841,15 @@ export function CodexUsageDialog({ ...@@ -349,6 +841,15 @@ export function CodexUsageDialog({
const { t } = useTranslation() const { t } = useTranslation()
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false }) const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
const [showRawJson, setShowRawJson] = useState(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 payload: CodexUsagePayload | null = useMemo(() => {
const raw = response?.data const raw = response?.data
...@@ -356,37 +857,122 @@ export function CodexUsageDialog({ ...@@ -356,37 +857,122 @@ export function CodexUsageDialog({
return raw as CodexUsagePayload return raw as CodexUsagePayload
}, [response?.data]) }, [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 rateLimit = payload?.rate_limit
const accountType = payload?.plan_type ?? rateLimit?.plan_type const accountType = payload?.plan_type ?? rateLimit?.plan_type
const accountBadge = getAccountTypeBadge(accountType, t) const accountBadge = getAccountTypeBadge(accountType, t)
const additionalRateLimits = (payload?.additional_rate_limits ?? []).filter( const additionalRateLimits = (payload?.additional_rate_limits ?? []).filter(
(item) => item && Object.keys(item).length > 0 (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 = (() => { const errorMessage =
if (!rateLimit || Object.keys(rateLimit).length === 0) { response?.success === false
return ( ? response?.message?.trim() || t('Failed to fetch usage')
<StatusBadge label={t('Pending')} variant='neutral' copyable={false} /> : ''
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) { setResetCreditsResponse(res)
return ( } catch (error) {
<StatusBadge setResetCreditsError(
label={t('Available')} error instanceof Error
variant='success' ? error.message
copyable={false} : 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 = const handleResetCreditsOpenChange = (nextOpen: boolean) => {
response?.success === false setShowResetCredits(nextOpen)
? response?.message?.trim() || t('Failed to fetch usage') 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(() => { const rawJsonText = useMemo(() => {
if (!response) return '' if (!response) return ''
...@@ -409,153 +995,214 @@ export function CodexUsageDialog({ ...@@ -409,153 +995,214 @@ export function CodexUsageDialog({
return ( return (
<Dialog <Dialog
open={open} open={open}
onOpenChange={onOpenChange} onOpenChange={handleDialogOpenChange}
title={t('Codex Account & Usage')} title={t('Codex Account & Usage')}
description={ contentClassName='sm:max-w-[900px]'
<>
{t('Channel:')}
<strong>{channelName || '-'}</strong>{' '}
{channelId ? `(#${channelId})` : ''}
</>
}
contentClassName='sm:max-w-3xl'
titleClassName='flex items-center gap-2' titleClassName='flex items-center gap-2'
contentHeight='auto' contentHeight='auto'
bodyClassName='space-y-4' bodyClassName='flex flex-col gap-4'
footer={ footer={
<> <>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
onClick={() => onOpenChange(false)} onClick={() => handleDialogOpenChange(false)}
> >
{t('Close')} {t('Close')}
</Button> </Button>
</> </>
} }
> >
<div className='space-y-4'> <div className='flex flex-col gap-4'>
{errorMessage && ( {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'> <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} {errorMessage}
</div> </div>
)} )}
{/* Account summary */} <Card size='sm' className='bg-muted/30 gap-0 py-0'>
<div className='rounded-lg border p-4'> <CardHeader className='p-4 pb-2'>
<div className='flex flex-wrap items-center justify-between gap-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'> <div className='flex flex-wrap items-center gap-2'>
<StatusBadge <StatusBadge
label={accountBadge.label} label={accountBadge.label}
variant={accountBadge.variant} variant={accountBadge.variant}
copyable={false} copyable={false}
/> />
{statusBadge} {getUsageStatusBadge(rateLimit, t)}
{typeof response?.upstream_status === 'number' && (
<StatusBadge <StatusBadge
label={`${t('Status:')} ${response.upstream_status}`} label={`HTTP ${response?.upstream_status ?? '-'}`}
variant='neutral' variant='neutral'
copyable={false} copyable={false}
/> />
)} <StatusBadge
</div> label={formatLabelValue(t('Reset count:'), resetCreditsText)}
{onRefresh && ( variant={Number(resetCredits) > 0 ? 'blue' : 'neutral'}
<Button copyable={false}
type='button' />
variant='outline' {payload?.credits?.overage_limit_reached ? (
size='sm' <StatusBadge
onClick={onRefresh} label={t('Overage limited')}
disabled={Boolean(isRefreshing)} variant='danger'
> copyable={false}
<RefreshCw className='mr-1.5 h-3.5 w-3.5' /> />
{t('Refresh')} ) : null}
</Button> {payload?.spend_control?.reached ? (
)} <StatusBadge
label={t('Spend limited')}
variant='danger'
copyable={false}
/>
) : null}
</div> </div>
<div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
{/* Account identity info */} <InfoField
<div className='bg-muted/30 mt-3 rounded-md px-3 py-2'> label={t('Email')}
<CopyableField value={payload?.email}
icon={<User className='h-3.5 w-3.5' />} copyable={true}
/>
<InfoField
label={t('Channel')}
value={channelLabel}
copyable={false}
/>
<InfoField
label='User ID' label='User ID'
value={payload?.user_id} value={payload?.user_id}
mono mono
className='md:col-span-2'
/> />
<CopyableField </div>
icon={<Mail className='h-3.5 w-3.5' />} </CardContent>
label={t('Email')} </Card>
value={payload?.email}
<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' <div className='min-w-0'>
value={payload?.account_id} <div className='flex flex-wrap items-center gap-2'>
mono <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>
<div className='text-muted-foreground mt-1 text-xs leading-5'>
{t('View issued reset credits, grant dates, and expiration.')}
</div> </div>
{/* Rate limit windows */}
<div className='space-y-5'>
<div>
<div className='mb-1 text-sm font-medium'>
{t('Rate Limit Windows')}
</div> </div>
<p className='text-muted-foreground mb-3 text-xs'> {showResetCredits ? (
{t( <ChevronUp className='text-muted-foreground mt-0.5 shrink-0' />
'Tracks current account base limits and additional metered usage on Codex upstream.' ) : (
<ChevronDown className='text-muted-foreground mt-0.5 shrink-0' />
)} )}
</p> </CollapsibleTrigger>
<RateLimitGroupSection <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')} title={t('Base Limits')}
description={t('Base rate limit windows for this account.')} description={t('Base rate limit windows for this account.')}
source={payload} >
{getUsageStatusBadge(rateLimit, t)}
</SectionHeading>
<RateLimitWindowGrid
fiveHourWindow={fiveHourWindow}
weeklyWindow={weeklyWindow}
/> />
</div> </div>
{additionalRateLimits.length > 0 && ( {additionalRateLimits.length > 0 ? (
<div className='space-y-4 border-t pt-4'> <div className='flex flex-col gap-3'>
<div> <SectionHeading
<div className='text-sm font-medium'> title={t('Additional Limits')}
{t('Additional Limits')} description={t(
</div>
<p className='text-muted-foreground text-xs'>
{t(
'Per-feature metered windows split by model or capability.' 'Per-feature metered windows split by model or capability.'
)} )}
</p> />
</div> <div className='flex flex-col gap-3'>
<div className='space-y-4'>
{additionalRateLimits.map((item, index) => { {additionalRateLimits.map((item, index) => {
const limitName = const limitName =
item.limit_name || item.limit_name ||
item.metered_feature || item.metered_feature ||
`${t('Additional Limit')} ${index + 1}` `${t('Additional Limit')} ${index + 1}`
return ( return (
<div
key={`${limitName}-${item.metered_feature ?? ''}-${index}`}
className={index > 0 ? 'border-t pt-4' : ''}
>
<RateLimitGroupSection <RateLimitGroupSection
key={`${limitName}-${item.metered_feature ?? ''}-${index}`}
title={limitName} title={limitName}
description={t('Additional metered capability')} description={t('Additional metered capability')}
source={item} source={item}
meteredFeature={item.metered_feature} meteredFeature={item.metered_feature}
/> />
</div>
) )
})} })}
</div> </div>
</div> </div>
)} ) : null}
</div>
{/* Raw JSON collapsible */} <Collapsible
<div className='rounded-lg border'> open={showRawJson}
onOpenChange={setShowRawJson}
className='rounded-lg border'
>
<CollapsibleTrigger
render={
<button <button
type='button' type='button'
className='hover:bg-muted/40 flex w-full items-center justify-between gap-2 p-3 transition-colors' 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> <div className='text-sm font-medium'>{t('Raw JSON')}</div>
{showRawJson ? ( {showRawJson ? (
...@@ -563,8 +1210,8 @@ export function CodexUsageDialog({ ...@@ -563,8 +1210,8 @@ export function CodexUsageDialog({
) : ( ) : (
<ChevronDown className='text-muted-foreground h-4 w-4' /> <ChevronDown className='text-muted-foreground h-4 w-4' />
)} )}
</button> </CollapsibleTrigger>
{showRawJson && ( <CollapsibleContent>
<> <>
<div className='flex justify-end border-t px-3 py-2'> <div className='flex justify-end border-t px-3 py-2'>
<Button <Button
...@@ -575,9 +1222,9 @@ export function CodexUsageDialog({ ...@@ -575,9 +1222,9 @@ export function CodexUsageDialog({
disabled={!rawJsonText} disabled={!rawJsonText}
> >
{copiedText === 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')} {t('Copy')}
</Button> </Button>
...@@ -588,9 +1235,38 @@ export function CodexUsageDialog({ ...@@ -588,9 +1235,38 @@ export function CodexUsageDialog({
</pre> </pre>
</ScrollArea> </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> </div>
<p className='text-destructive'>
{t('Used reset credits cannot be restored.')}
</p>
</div> </div>
}
destructive
disabled={!canResetCodexUsage}
isLoading={isResetting}
cancelBtnText={t('Cancel')}
confirmText={isResetting ? t('Resetting...') : t('Apply reset')}
handleConfirm={handleConfirmReset}
/>
</Dialog> </Dialog>
) )
} }
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "Advanced Settings", "Advanced Settings": "Advanced Settings",
"Advanced text editing": "Advanced text editing", "Advanced text editing": "Advanced text editing",
"Aesthetic style": "Aesthetic style", "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 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 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?", "After enabling, the plan will be shown to users. Continue?": "After enabling, the plan will be shown to users. Continue?",
...@@ -394,6 +395,7 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "Apply Filters", "Apply Filters": "Apply Filters",
"Apply IP Filter to Resolved Domains": "Apply IP Filter to Resolved Domains", "Apply IP Filter to Resolved Domains": "Apply IP Filter to Resolved Domains",
"Apply Overwrite": "Apply Overwrite", "Apply Overwrite": "Apply Overwrite",
"Apply reset": "Apply reset",
"Apply Sync": "Apply Sync", "Apply Sync": "Apply Sync",
"Applying...": "Applying...", "Applying...": "Applying...",
"Approx.": "Approx.", "Approx.": "Approx.",
...@@ -485,8 +487,10 @@ ...@@ -485,8 +487,10 @@
"Automatically sync model list when upstream changes are detected": "Automatically sync model list when upstream changes are detected", "Automatically sync model list when upstream changes are detected": "Automatically sync model list when upstream changes are detected",
"Availability (last 24h)": "Availability (last 24h)", "Availability (last 24h)": "Availability (last 24h)",
"Available": "Available", "Available": "Available",
"Available credits are ordered by soonest expiration.": "Available credits are ordered by soonest expiration.",
"Available disk space": "Available disk space", "Available disk space": "Available disk space",
"Available Models": "Available Models", "Available Models": "Available Models",
"Available reset credits": "Available reset credits",
"Available Rewards": "Available Rewards", "Available Rewards": "Available Rewards",
"Average latency": "Average latency", "Average latency": "Average latency",
"Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group", "Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group",
...@@ -686,6 +690,7 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "Channel enabled successfully", "Channel enabled successfully": "Channel enabled successfully",
"Channel Extra Settings": "Channel Extra Settings", "Channel Extra Settings": "Channel Extra Settings",
"Channel ID": "Channel ID", "Channel ID": "Channel ID",
"Channel ID is required": "Channel ID is required",
"Channel key": "Channel key", "Channel key": "Channel key",
"Channel key unlocked": "Channel key unlocked", "Channel key unlocked": "Channel key unlocked",
"Channel models": "Channel models", "Channel models": "Channel models",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "Codes copied!", "Codes copied!": "Codes copied!",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Codex Account & Usage", "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 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 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", "Codex CLI Header Passthrough": "Codex CLI Header Passthrough",
...@@ -928,6 +934,7 @@ ...@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Confirm settings and finish setup", "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 that I bear legal responsibility arising from deployment": "confirm that I bear legal responsibility arising from deployment",
"Confirm Unbind": "Confirm Unbind", "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 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.", "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}}", "Confirmed at {{time}} by user #{{userId}}": "Confirmed at {{time}} by user #{{userId}}",
...@@ -1625,6 +1632,8 @@ ...@@ -1625,6 +1632,8 @@
"Expired at": "Expired at", "Expired at": "Expired at",
"Expired time cannot be earlier than current time": "Expired time cannot be earlier than current time", "Expired time cannot be earlier than current time": "Expired time cannot be earlier than current time",
"Expires": "Expires", "Expires": "Expires",
"Expires at": "Expires at",
"Expires in": "Expires in",
"Expose ratio API": "Expose ratio API", "Expose ratio API": "Expose ratio API",
"Exposes the pricing/models catalog in the top navigation.": "Exposes the pricing/models catalog in the top navigation.", "Exposes the pricing/models catalog in the top navigation.": "Exposes the pricing/models catalog in the top navigation.",
"Expression": "Expression", "Expression": "Expression",
...@@ -1699,6 +1708,7 @@ ...@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Failed to fetch deployment details", "Failed to fetch deployment details": "Failed to fetch deployment details",
"Failed to fetch models": "Failed to fetch models", "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 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 prices": "Failed to fetch upstream prices",
"Failed to fetch upstream ratios": "Failed to fetch upstream ratios", "Failed to fetch upstream ratios": "Failed to fetch upstream ratios",
"Failed to fetch usage": "Failed to fetch usage", "Failed to fetch usage": "Failed to fetch usage",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Failed to reset 2FA", "Failed to reset 2FA": "Failed to reset 2FA",
"Failed to reset model ratios": "Failed to reset model ratios", "Failed to reset model ratios": "Failed to reset model ratios",
"Failed to reset Passkey": "Failed to reset Passkey", "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": "Failed to save",
"Failed to save announcements": "Failed to save announcements", "Failed to save announcements": "Failed to save announcements",
"Failed to save API info": "Failed to save API info", "Failed to save API info": "Failed to save API info",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.", "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.",
"GPU count": "GPU count", "GPU count": "GPU count",
"Granted at": "Granted at",
"Greater than": "Greater than", "Greater than": "Greater than",
"Greater Than": "Greater Than", "Greater Than": "Greater Than",
"Greater than or equal": "Greater than or equal", "Greater than or equal": "Greater than or equal",
...@@ -2726,6 +2738,8 @@ ...@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "No related models available for this channel type", "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 release notes provided.": "No release notes provided.",
"No Reset": "No Reset", "No Reset": "No Reset",
"No reset credits": "No reset credits",
"No reset credits available": "No reset credits available",
"No restriction": "No restriction", "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 for \"{{query}}\". Try adjusting your search or filters.": "No results for \"{{query}}\". Try adjusting your search or filters.",
"No results found": "No results found", "No results found": "No results found",
...@@ -2915,6 +2929,7 @@ ...@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Output token price for generated tokens.", "Output token price for generated tokens.": "Output token price for generated tokens.",
"Output tokens": "Output tokens", "Output tokens": "Output tokens",
"Output Tokens": "Output Tokens", "Output Tokens": "Output Tokens",
"Overage limited": "Overage limited",
"overall": "overall", "overall": "overall",
"Overnight range": "Overnight range", "Overnight range": "Overnight range",
"override": "override", "override": "override",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "Recursive", "Recursive": "Recursive",
"Redeem": "Redeem", "Redeem": "Redeem",
"Redeem codes": "Redeem codes", "Redeem codes": "Redeem codes",
"Redeemed": "Redeemed",
"Redeemed at": "Redeemed at",
"Redeemed By": "Redeemed By", "Redeemed By": "Redeemed By",
"Redeemed:": "Redeemed:", "Redeemed:": "Redeemed:",
"redemption code": "redemption code", "redemption code": "redemption code",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "Refresh", "Refresh": "Refresh",
"Refresh Cache": "Refresh Cache", "Refresh Cache": "Refresh Cache",
"Refresh credential": "Refresh credential", "Refresh credential": "Refresh credential",
"Refresh details": "Refresh details",
"Refresh failed": "Refresh failed", "Refresh failed": "Refresh failed",
"Refresh interval (minutes)": "Refresh interval (minutes)", "Refresh interval (minutes)": "Refresh interval (minutes)",
"Refresh Stats": "Refresh Stats", "Refresh Stats": "Refresh Stats",
...@@ -3491,6 +3509,11 @@ ...@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Reset all settings to default values", "Reset all settings to default values": "Reset all settings to default values",
"Reset at:": "Reset at:", "Reset at:": "Reset at:",
"Reset balance and used quota": "Reset balance and used quota", "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 Cycle": "Reset Cycle",
"Reset email sent, please check your inbox": "Reset email sent, please check your inbox", "Reset email sent, please check your inbox": "Reset email sent, please check your inbox",
"Reset failed": "Reset failed", "Reset failed": "Reset failed",
...@@ -3506,7 +3529,9 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "Reset to Default", "Reset to Default": "Reset to Default",
"Reset to default configuration": "Reset to default configuration", "Reset to default configuration": "Reset to default configuration",
"Reset Two-Factor Authentication": "Reset Two-Factor Authentication", "Reset Two-Factor Authentication": "Reset Two-Factor Authentication",
"Reset usage window": "Reset usage window",
"Resets in:": "Resets in:", "Resets in:": "Resets in:",
"Resetting...": "Resetting...",
"Resolve Conflicts": "Resolve Conflicts", "Resolve Conflicts": "Resolve Conflicts",
"Resource Configuration": "Resource Configuration", "Resource Configuration": "Resource Configuration",
"Response": "Response", "Response": "Response",
...@@ -3872,6 +3897,7 @@ ...@@ -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 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": "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.", "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.", "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", "SSRF Protection": "SSRF Protection",
"Standard": "Standard", "Standard": "Standard",
...@@ -4088,6 +4114,7 @@ ...@@ -4088,6 +4114,7 @@
"The name displayed across the application": "The name displayed across the application", "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 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 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 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 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:", "The slug is appended to the URL:": "The slug is appended to the URL:",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "Upload photo", "Upload photo": "Upload photo",
"Upscale": "Upscale", "Upscale": "Upscale",
"Upstream": "Upstream", "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 Detection Settings": "Upstream Model Detection Settings",
"Upstream Model Update Check": "Upstream Model Update Check", "Upstream Model Update Check": "Upstream Model Update Check",
"Upstream Model Updates": "Upstream Model Updates", "Upstream Model Updates": "Upstream Model Updates",
...@@ -4447,6 +4475,8 @@ ...@@ -4447,6 +4475,8 @@
"Use backup code": "Use backup code", "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 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 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 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 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.", "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 @@ ...@@ -4464,6 +4494,7 @@
"Used for load balancing. Higher weight = more requests": "Used for load balancing. Higher weight = more requests", "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 in URLs and API routes": "Used in URLs and API routes",
"Used Quota": "Used Quota", "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 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 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.", "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 @@ ...@@ -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 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 details": "View details",
"View document": "View document", "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 logs": "View logs",
"View mode": "View mode", "View mode": "View mode",
"View model statistics and charts": "View model statistics and charts", "View model statistics and charts": "View model statistics and charts",
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "Paramètres avancés", "Advanced Settings": "Paramètres avancés",
"Advanced text editing": "Édition de texte avancée", "Advanced text editing": "Édition de texte avancée",
"Aesthetic style": "Style esthétique", "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 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 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 ?", "After enabling, the plan will be shown to users. Continue?": "Après activation, le plan sera affiché aux utilisateurs. Continuer ?",
...@@ -394,6 +395,7 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "Appliquer les filtres", "Apply Filters": "Appliquer les filtres",
"Apply IP Filter to Resolved Domains": "Appliquer le filtre IP aux domaines résolus", "Apply IP Filter to Resolved Domains": "Appliquer le filtre IP aux domaines résolus",
"Apply Overwrite": "Appliquer l'écrasement", "Apply Overwrite": "Appliquer l'écrasement",
"Apply reset": "Appliquer la réinitialisation",
"Apply Sync": "Appliquer la synchronisation", "Apply Sync": "Appliquer la synchronisation",
"Applying...": "Application en cours...", "Applying...": "Application en cours...",
"Approx.": "Environ.", "Approx.": "Environ.",
...@@ -485,8 +487,10 @@ ...@@ -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", "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)", "Availability (last 24h)": "Disponibilité (dernières 24 h)",
"Available": "Disponible", "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 disk space": "Espace disque disponible",
"Available Models": "Modèles disponibles", "Available Models": "Modèles disponibles",
"Available reset credits": "Crédits de réinitialisation disponibles",
"Available Rewards": "Récompenses disponibles", "Available Rewards": "Récompenses disponibles",
"Average latency": "Latence moyenne", "Average latency": "Latence moyenne",
"Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe", "Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe",
...@@ -686,6 +690,7 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "Canal activé avec succès", "Channel enabled successfully": "Canal activé avec succès",
"Channel Extra Settings": "Paramètres supplémentaires du canal", "Channel Extra Settings": "Paramètres supplémentaires du canal",
"Channel ID": "ID du Canal", "Channel ID": "ID du Canal",
"Channel ID is required": "L'ID du canal est requis",
"Channel key": "Clé du canal", "Channel key": "Clé du canal",
"Channel key unlocked": "Clé de canal déverrouillée", "Channel key unlocked": "Clé de canal déverrouillée",
"Channel models": "Modèles de canaux", "Channel models": "Modèles de canaux",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "Codes copiés !", "Codes copied!": "Codes copiés !",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Compte et utilisation 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 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 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", "Codex CLI Header Passthrough": "Passthrough en-tête Codex CLI",
...@@ -928,6 +934,7 @@ ...@@ -928,6 +934,7 @@
"Confirm settings and finish setup": "Confirmez les paramètres et terminez la configuration", "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 that I bear legal responsibility arising from deployment": "confirme assumer la responsabilité juridique découlant du déploiement",
"Confirm Unbind": "Confirmer la dissociation", "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 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.", "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}}", "Confirmed at {{time}} by user #{{userId}}": "Confirmé le {{time}} par l’utilisateur #{{userId}}",
...@@ -1625,6 +1632,8 @@ ...@@ -1625,6 +1632,8 @@
"Expired at": "Expiré le", "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", "Expired time cannot be earlier than current time": "L'heure d'expiration ne peut pas être antérieure à l'heure actuelle",
"Expires": "Expire", "Expires": "Expire",
"Expires at": "Expire le",
"Expires in": "Expire dans",
"Expose ratio API": "Exposer l'API de ratio", "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.", "Exposes the pricing/models catalog in the top navigation.": "Expose le catalogue des prix/modèles dans la navigation supérieure.",
"Expression": "Expression", "Expression": "Expression",
...@@ -1699,6 +1708,7 @@ ...@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Impossible de récupérer les détails du déploiement", "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 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 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 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 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", "Failed to fetch usage": "Échec de la récupération de l'utilisation",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Échec de la réinitialisation de la 2FA", "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 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 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": "Échec de la sauvegarde",
"Failed to save announcements": "Échec de la sauvegarde des annonces", "Failed to save announcements": "Échec de la sauvegarde des annonces",
"Failed to save API info": "Échec de l'enregistrement des informations API", "Failed to save API info": "Échec de l'enregistrement des informations API",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.", "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, etc.",
"GPU count": "Nombre de GPU", "GPU count": "Nombre de GPU",
"Granted at": "Accordé le",
"Greater than": "Supérieur à", "Greater than": "Supérieur à",
"Greater Than": "Supérieur à", "Greater Than": "Supérieur à",
"Greater than or equal": "Supérieur ou égal", "Greater than or equal": "Supérieur ou égal",
...@@ -2726,6 +2738,8 @@ ...@@ -2726,6 +2738,8 @@
"No related models available for this channel type": "Aucun modèle associé disponible pour ce type de canal", "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 release notes provided.": "Aucune note de version fournie.",
"No Reset": "Pas de réinitialisation", "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 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 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é", "No results found": "Aucun résultat trouvé",
...@@ -2915,6 +2929,7 @@ ...@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Prix des tokens de sortie générés.", "Output token price for generated tokens.": "Prix des tokens de sortie générés.",
"Output tokens": "Jetons de sortie", "Output tokens": "Jetons de sortie",
"Output Tokens": "Tokens de sortie", "Output Tokens": "Tokens de sortie",
"Overage limited": "Dépassement limité",
"overall": "global", "overall": "global",
"Overnight range": "Plage nocturne", "Overnight range": "Plage nocturne",
"override": "remplacer", "override": "remplacer",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "Récursif", "Recursive": "Récursif",
"Redeem": "Utiliser", "Redeem": "Utiliser",
"Redeem codes": "Échanger des codes", "Redeem codes": "Échanger des codes",
"Redeemed": "Utilisé",
"Redeemed at": "Utilisé le",
"Redeemed By": "Utilisé par", "Redeemed By": "Utilisé par",
"Redeemed:": "Utilisé :", "Redeemed:": "Utilisé :",
"redemption code": "code d'échange", "redemption code": "code d'échange",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "Actualiser", "Refresh": "Actualiser",
"Refresh Cache": "Actualiser le cache", "Refresh Cache": "Actualiser le cache",
"Refresh credential": "Actualiser l'identifiant", "Refresh credential": "Actualiser l'identifiant",
"Refresh details": "Actualiser les détails",
"Refresh failed": "Échec de l'actualisation", "Refresh failed": "Échec de l'actualisation",
"Refresh interval (minutes)": "Intervalle d'actualisation (minutes)", "Refresh interval (minutes)": "Intervalle d'actualisation (minutes)",
"Refresh Stats": "Actualiser les statistiques", "Refresh Stats": "Actualiser les statistiques",
...@@ -3491,6 +3509,11 @@ ...@@ -3491,6 +3509,11 @@
"Reset all settings to default values": "Réinitialiser tous les paramètres aux valeurs par défaut", "Reset all settings to default values": "Réinitialiser tous les paramètres aux valeurs par défaut",
"Reset at:": "Réinitialisation à :", "Reset at:": "Réinitialisation à :",
"Reset balance and used quota": "Réinitialiser le solde et le quota utilisé", "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 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 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", "Reset failed": "Échec de la réinitialisation",
...@@ -3506,7 +3529,9 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "Réinitialiser par défaut", "Reset to Default": "Réinitialiser par défaut",
"Reset to default configuration": "Réinitialisé à la configuration par défaut", "Reset to default configuration": "Réinitialisé à la configuration par défaut",
"Reset Two-Factor Authentication": "Réinitialiser 2FA", "Reset Two-Factor Authentication": "Réinitialiser 2FA",
"Reset usage window": "Réinitialiser la fenêtre d’utilisation",
"Resets in:": "Réinitialise dans :", "Resets in:": "Réinitialise dans :",
"Resetting...": "Réinitialisation...",
"Resolve Conflicts": "Résoudre les conflits", "Resolve Conflicts": "Résoudre les conflits",
"Resource Configuration": "Configuration des ressources", "Resource Configuration": "Configuration des ressources",
"Response": "Réponse", "Response": "Réponse",
...@@ -3872,6 +3897,7 @@ ...@@ -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 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": "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.", "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.", "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", "SSRF Protection": "Protection SSRF",
"Standard": "Standard", "Standard": "Standard",
...@@ -4088,6 +4114,7 @@ ...@@ -4088,6 +4114,7 @@
"The name displayed across the application": "Le nom affiché dans l'application", "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 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 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 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 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 :", "The slug is appended to the URL:": "Le slug est ajouté à l'URL :",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "Téléverser une photo", "Upload photo": "Téléverser une photo",
"Upscale": "Agrandir", "Upscale": "Agrandir",
"Upstream": "Amont", "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 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 Update Check": "Vérification des mises à jour des modèles en amont",
"Upstream Model Updates": "Mises à jour des modèles en amont", "Upstream Model Updates": "Mises à jour des modèles en amont",
...@@ -4447,6 +4475,8 @@ ...@@ -4447,6 +4475,8 @@
"Use backup code": "Utiliser un code de secours", "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 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 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 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 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.", "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 @@ ...@@ -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 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 in URLs and API routes": "Utilisé dans les URLs et les routes API",
"Used Quota": "Quota utilisé", "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 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 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.", "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 @@ ...@@ -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 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 details": "Voir les détails",
"View document": "Afficher le document", "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 logs": "Voir les logs",
"View mode": "Mode d'affichage", "View mode": "Mode d'affichage",
"View model statistics and charts": "Afficher les statistiques et graphiques des modèles", "View model statistics and charts": "Afficher les statistiques et graphiques des modèles",
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "詳細設定", "Advanced Settings": "詳細設定",
"Advanced text editing": "高度なテキスト編集", "Advanced text editing": "高度なテキスト編集",
"Aesthetic style": "スタイル", "Aesthetic style": "スタイル",
"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 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "フィルターを適用", "Apply Filters": "フィルターを適用",
"Apply IP Filter to Resolved Domains": "解決されたドメインにIPフィルターを適用", "Apply IP Filter to Resolved Domains": "解決されたドメインにIPフィルターを適用",
"Apply Overwrite": "上書き適用", "Apply Overwrite": "上書き適用",
"Apply reset": "リセットを実行",
"Apply Sync": "同期を適用", "Apply Sync": "同期を適用",
"Applying...": "適用中...", "Applying...": "適用中...",
"Approx.": "約", "Approx.": "約",
...@@ -485,8 +487,10 @@ ...@@ -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)": "可用性(過去 24 時間)", "Availability (last 24h)": "可用性(過去 24 時間)",
"Available": "空き", "Available": "空き",
"Available credits are ordered by soonest expiration.": "利用可能なクレジットは有効期限の近い順に表示されます。",
"Available disk space": "利用可能なディスク容量", "Available disk space": "利用可能なディスク容量",
"Available Models": "利用可能なモデル", "Available Models": "利用可能なモデル",
"Available reset credits": "利用可能なリセット回数",
"Available Rewards": "利用可能な報酬", "Available Rewards": "利用可能な報酬",
"Average latency": "平均レイテンシ", "Average latency": "平均レイテンシ",
"Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率", "Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率",
...@@ -686,6 +690,7 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "チャネルが正常に有効化されました", "Channel enabled successfully": "チャネルが正常に有効化されました",
"Channel Extra Settings": "チャネル詳細設定", "Channel Extra Settings": "チャネル詳細設定",
"Channel ID": "チャネルID", "Channel ID": "チャネルID",
"Channel ID is required": "チャネル ID が必要です",
"Channel key": "チャネルキー", "Channel key": "チャネルキー",
"Channel key unlocked": "チャネルキーが解除されました", "Channel key unlocked": "チャネルキーが解除されました",
"Channel models": "チャネルモデル", "Channel models": "チャネルモデル",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "コードをコピーしました!", "Codes copied!": "コードをコピーしました!",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Codex アカウントと使用量", "Codex Account & Usage": "Codex アカウントと使用量",
"Codex Account Status": "Codex アカウント状態",
"Codex channels do not support batch creation": "Codex チャネルは一括作成をサポートしていません", "Codex channels do not support batch creation": "Codex チャネルは一括作成をサポートしていません",
"Codex channels use an OAuth JSON credential as the key.": "CodexチャネルはOAuth JSON認証情報をキーとして使用します。", "Codex channels use an OAuth JSON credential as the key.": "CodexチャネルはOAuth JSON認証情報をキーとして使用します。",
"Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー", "Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー",
...@@ -928,6 +934,7 @@ ...@@ -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 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}}": "ユーザー #{{userId}} が {{time}} に確認しました", "Confirmed at {{time}} by user #{{userId}}": "ユーザー #{{userId}} が {{time}} に確認しました",
...@@ -1625,6 +1632,8 @@ ...@@ -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 in": "期限まで",
"Expose ratio API": "倍率APIを公開", "Expose ratio API": "倍率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 @@ ...@@ -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": "OIDC構成の取得に失敗しました。URLとネットワーク状態を確認してください", "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 prices": "上流価格の取得に失敗しました",
"Failed to fetch upstream ratios": "アップストリーム比率の取得に失敗しました", "Failed to fetch upstream ratios": "アップストリーム比率の取得に失敗しました",
"Failed to fetch usage": "利用状況の取得に失敗しました", "Failed to fetch usage": "利用状況の取得に失敗しました",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "2FAのリセットに失敗しました", "Failed to reset 2FA": "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 save": "保存に失敗", "Failed to save": "保存に失敗",
"Failed to save announcements": "お知らせの保存に失敗しました", "Failed to save announcements": "お知らせの保存に失敗しました",
"Failed to save API info": "API情報の保存に失敗しました", "Failed to save API info": "API情報の保存に失敗しました",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4、claude-3-opus など", "gpt-4, claude-3-opus, etc.": "gpt-4、claude-3-opus など",
"GPU count": "GPU 数", "GPU count": "GPU 数",
"Granted at": "付与日時",
"Greater than": "より大きい", "Greater than": "より大きい",
"Greater Than": "より大きい", "Greater Than": "より大きい",
"Greater than or equal": "以上", "Greater than or equal": "以上",
...@@ -2726,6 +2738,8 @@ ...@@ -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 available": "利用可能なリセット回数がありません",
"No restriction": "制限なし", "No restriction": "制限なし",
"No results for \"{{query}}\". Try adjusting your search or filters.": "\"{{query}}\" の結果が見つかりません。検索条件やフィルターを調整してみてください。", "No results for \"{{query}}\". Try adjusting your search or filters.": "\"{{query}}\" の結果が見つかりません。検索条件やフィルターを調整してみてください。",
"No results found": "検索結果がありません", "No results found": "検索結果がありません",
...@@ -2915,6 +2929,7 @@ ...@@ -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": "超過利用制限中",
"overall": "全体", "overall": "全体",
"Overnight range": "日跨ぎ範囲", "Overnight range": "日跨ぎ範囲",
"override": "上書き", "override": "上書き",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "再帰", "Recursive": "再帰",
"Redeem": "引き換え", "Redeem": "引き換え",
"Redeem codes": "コードを交換", "Redeem codes": "コードを交換",
"Redeemed": "使用済み",
"Redeemed at": "使用日時",
"Redeemed By": "引き換え元", "Redeemed By": "引き換え元",
"Redeemed:": "引き換え済み:", "Redeemed:": "引き換え済み:",
"redemption code": "引き換えコード", "redemption code": "引き換えコード",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "更新", "Refresh": "更新",
"Refresh Cache": "キャッシュ更新", "Refresh Cache": "キャッシュ更新",
"Refresh credential": "認証情報を更新", "Refresh credential": "認証情報を更新",
"Refresh details": "詳細を更新",
"Refresh failed": "更新に失敗しました", "Refresh failed": "更新に失敗しました",
"Refresh interval (minutes)": "更新間隔 (分)", "Refresh interval (minutes)": "更新間隔 (分)",
"Refresh Stats": "統計を更新", "Refresh Stats": "統計を更新",
...@@ -3491,6 +3509,11 @@ ...@@ -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. Latest usage has been refreshed.": "リセットが完了しました。最新の使用状況を更新しました。",
"Reset count:": "リセット回数:",
"Reset Credit": "リセット回数",
"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 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "デフォルトにリセット", "Reset to Default": "デフォルトにリセット",
"Reset to default configuration": "デフォルト設定にリセットしました", "Reset to default configuration": "デフォルト設定にリセットしました",
"Reset Two-Factor Authentication": "2要素認証のリセット", "Reset Two-Factor Authentication": "2要素認証のリセット",
"Reset usage window": "使用量ウィンドウをリセット",
"Resets in:": "リセットまで:", "Resets in:": "リセットまで:",
"Resetting...": "リセット中...",
"Resolve Conflicts": "競合を解決", "Resolve Conflicts": "競合を解決",
"Resource Configuration": "リソース設定", "Resource Configuration": "リソース設定",
"Response": "レスポンス", "Response": "レスポンス",
...@@ -3872,6 +3897,7 @@ ...@@ -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": "支出制限中",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite はすべてのデータを単一ファイルに保存します。コンテナで実行する場合は、ファイルが永続化されていることを確認してください。", "SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite はすべてのデータを単一ファイルに保存します。コンテナで実行する場合は、ファイルが永続化されていることを確認してください。",
"SSRF Protection": "SSRF保護", "SSRF Protection": "SSRF保護",
"Standard": "標準", "Standard": "標準",
...@@ -4088,6 +4114,7 @@ ...@@ -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": "OAuthコールバック、Webhook、その他の外部統合に使用されるサーバーの公開URL", "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 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 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:": "スラッグがURLに追加されます:", "The slug is appended to the URL:": "スラッグがURLに追加されます:",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "写真をアップロード", "Upload photo": "写真をアップロード",
"Upscale": "アップスケール", "Upscale": "アップスケール",
"Upstream": "アップストリーム", "Upstream": "アップストリーム",
"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 @@ ...@@ -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.": "このチャンネルで利用可能なリセット回数を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 our unified OpenAI-compatible endpoint in your applications": "アプリケーションでOpenAI互換の統一エンドポイントを使用",
"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 @@ ...@@ -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": "URLとAPIルートで使用されます", "Used in URLs and API routes": "URLとAPIルートで使用されます",
"Used Quota": "使用済みクォータ", "Used Quota": "使用済みクォータ",
"Used reset credits cannot be restored.": "使用済みのリセット回数は復元できません。",
"Used to authenticate with io.net deployment API": "io.net デプロイ API の認証に使用します", "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 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 パラメーターとして送信されます。", "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 @@ ...@@ -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 logs": "ログを表示", "View logs": "ログを表示",
"View mode": "表示モード", "View mode": "表示モード",
"View model statistics and charts": "モデルの統計とグラフを表示", "View model statistics and charts": "モデルの統計とグラフを表示",
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "Расширенные настройки", "Advanced Settings": "Расширенные настройки",
"Advanced text editing": "Расширенное редактирование текста", "Advanced text editing": "Расширенное редактирование текста",
"Aesthetic style": "Стиль", "Aesthetic style": "Стиль",
"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 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "Применить фильтры", "Apply Filters": "Применить фильтры",
"Apply IP Filter to Resolved Domains": "Применить IP-фильтр к разрешенным доменам", "Apply IP Filter to Resolved Domains": "Применить IP-фильтр к разрешенным доменам",
"Apply Overwrite": "Применить перезапись", "Apply Overwrite": "Применить перезапись",
"Apply reset": "Выполнить сброс",
"Apply Sync": "Применить синхронизацию", "Apply Sync": "Применить синхронизацию",
"Applying...": "Применение...", "Applying...": "Применение...",
"Approx.": "Примерно.", "Approx.": "Примерно.",
...@@ -485,8 +487,10 @@ ...@@ -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)": "Доступность (последние 24 ч)", "Availability (last 24h)": "Доступность (последние 24 ч)",
"Available": "Доступно", "Available": "Доступно",
"Available credits are ordered by soonest expiration.": "Доступные сбросы отсортированы по ближайшему истечению.",
"Available disk space": "Доступное дисковое пространство", "Available disk space": "Доступное дисковое пространство",
"Available Models": "Доступные модели", "Available Models": "Доступные модели",
"Available reset credits": "Доступные сбросы лимита",
"Available Rewards": "Доступные награды", "Available Rewards": "Доступные награды",
"Average latency": "Средняя задержка", "Average latency": "Средняя задержка",
"Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам", "Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам",
...@@ -686,6 +690,7 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "Канал успешно включён", "Channel enabled successfully": "Канал успешно включён",
"Channel Extra Settings": "Дополнительные настройки канала", "Channel Extra Settings": "Дополнительные настройки канала",
"Channel ID": "ID канала", "Channel ID": "ID канала",
"Channel ID is required": "Требуется ID канала",
"Channel key": "Ключ канала", "Channel key": "Ключ канала",
"Channel key unlocked": "Ключ канала разблокирован", "Channel key unlocked": "Ключ канала разблокирован",
"Channel models": "Модели каналов", "Channel models": "Модели каналов",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "Коды скопированы!", "Codes copied!": "Коды скопированы!",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Аккаунт и использование Codex", "Codex Account & Usage": "Аккаунт и использование Codex",
"Codex Account Status": "Статус аккаунта Codex",
"Codex channels do not support batch creation": "Каналы Codex не поддерживают пакетное создание", "Codex channels do not support batch creation": "Каналы Codex не поддерживают пакетное создание",
"Codex channels use an OAuth JSON credential as the key.": "Каналы Codex используют учётные данные OAuth в формате JSON в качестве ключа.", "Codex channels use an OAuth JSON credential as the key.": "Каналы Codex используют учётные данные OAuth в формате JSON в качестве ключа.",
"Codex CLI Header Passthrough": "Проброс заголовков Codex CLI", "Codex CLI Header Passthrough": "Проброс заголовков Codex CLI",
...@@ -928,6 +934,7 @@ ...@@ -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 your identity before removing this Passkey from your account.": "Подтвердите свою личность перед удалением этой Passkey из вашей учётной записи.", "Confirm your identity before removing this Passkey from your account.": "Подтвердите свою личность перед удалением этой Passkey из вашей учётной записи.",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "Подтвердите свою личность с помощью двухфакторной аутентификации перед регистрацией Passkey.", "Confirm your identity with Two-factor Authentication before registering a Passkey.": "Подтвердите свою личность с помощью двухфакторной аутентификации перед регистрацией Passkey.",
"Confirmed at {{time}} by user #{{userId}}": "Подтверждено {{time}} пользователем #{{userId}}", "Confirmed at {{time}} by user #{{userId}}": "Подтверждено {{time}} пользователем #{{userId}}",
...@@ -1625,6 +1632,8 @@ ...@@ -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 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 @@ ...@@ -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": "Не удалось получить конфигурацию OIDC. Проверьте URL и состояние сети", "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 prices": "Не удалось получить цены провайдера",
"Failed to fetch upstream ratios": "Не удалось получить коэффициенты upstream", "Failed to fetch upstream ratios": "Не удалось получить коэффициенты upstream",
"Failed to fetch usage": "Не удалось получить данные об использовании", "Failed to fetch usage": "Не удалось получить данные об использовании",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Не удалось сбросить 2FA", "Failed to reset 2FA": "Не удалось сбросить 2FA",
"Failed to reset model ratios": "Не удалось сбросить коэффициенты модели", "Failed to reset model ratios": "Не удалось сбросить коэффициенты модели",
"Failed to reset Passkey": "Не удалось сбросить Passkey", "Failed to reset Passkey": "Не удалось сбросить Passkey",
"Failed to reset usage": "Не удалось сбросить использование",
"Failed to save": "Не удалось сохранить", "Failed to save": "Не удалось сохранить",
"Failed to save announcements": "Не удалось сохранить объявления", "Failed to save announcements": "Не удалось сохранить объявления",
"Failed to save API info": "Не удалось сохранить информацию API", "Failed to save API info": "Не удалось сохранить информацию API",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus и т. д.", "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus и т. д.",
"GPU count": "Количество GPU", "GPU count": "Количество GPU",
"Granted at": "Выдано",
"Greater than": "Больше", "Greater than": "Больше",
"Greater Than": "Больше", "Greater Than": "Больше",
"Greater than or equal": "Больше или равно", "Greater than or equal": "Больше или равно",
...@@ -2726,6 +2738,8 @@ ...@@ -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 available": "Нет доступных сбросов",
"No restriction": "Без ограничений", "No restriction": "Без ограничений",
"No results for \"{{query}}\". Try adjusting your search or filters.": "Нет результатов для \"{{query}}\". Попробуйте изменить поиск или фильтры.", "No results for \"{{query}}\". Try adjusting your search or filters.": "Нет результатов для \"{{query}}\". Попробуйте изменить поиск или фильтры.",
"No results found": "Поиск не дал результатов", "No results found": "Поиск не дал результатов",
...@@ -2915,6 +2929,7 @@ ...@@ -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": "Ограничение перерасхода",
"overall": "всего", "overall": "всего",
"Overnight range": "Диапазон через полночь", "Overnight range": "Диапазон через полночь",
"override": "переопределить", "override": "переопределить",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "Рекурсивно", "Recursive": "Рекурсивно",
"Redeem": "Обменять квоту", "Redeem": "Обменять квоту",
"Redeem codes": "Активировать коды", "Redeem codes": "Активировать коды",
"Redeemed": "Использовано",
"Redeemed at": "Использовано в",
"Redeemed By": "Активировано", "Redeemed By": "Активировано",
"Redeemed:": "Активировано:", "Redeemed:": "Активировано:",
"redemption code": "код активации", "redemption code": "код активации",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "Обновить", "Refresh": "Обновить",
"Refresh Cache": "Обновить кэш", "Refresh Cache": "Обновить кэш",
"Refresh credential": "Обновить учётные данные", "Refresh credential": "Обновить учётные данные",
"Refresh details": "Обновить сведения",
"Refresh failed": "Ошибка обновления", "Refresh failed": "Ошибка обновления",
"Refresh interval (minutes)": "Интервал обновления (минуты)", "Refresh interval (minutes)": "Интервал обновления (минуты)",
"Refresh Stats": "Обновить статистику", "Refresh Stats": "Обновить статистику",
...@@ -3491,6 +3509,11 @@ ...@@ -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. Latest usage has been refreshed.": "Сброс завершён. Последние данные использования обновлены.",
"Reset count:": "Сбросов:",
"Reset Credit": "Сброс лимита",
"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 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "Сбросить по умолчанию", "Reset to Default": "Сбросить по умолчанию",
"Reset to default configuration": "Сброшено к конфигурации по умолчанию", "Reset to default configuration": "Сброшено к конфигурации по умолчанию",
"Reset Two-Factor Authentication": "Сброс 2FA", "Reset Two-Factor Authentication": "Сброс 2FA",
"Reset usage window": "Сбросить окно использования",
"Resets in:": "Сброс через:", "Resets in:": "Сброс через:",
"Resetting...": "Сброс...",
"Resolve Conflicts": "Разрешить конфликты", "Resolve Conflicts": "Разрешить конфликты",
"Resource Configuration": "Конфигурация ресурсов", "Resource Configuration": "Конфигурация ресурсов",
"Response": "Ответ", "Response": "Ответ",
...@@ -3872,6 +3897,7 @@ ...@@ -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": "Ограничение расходов",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite хранит все данные в одном файле. Убедитесь, что файл сохраняется при работе в контейнерах.", "SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite хранит все данные в одном файле. Убедитесь, что файл сохраняется при работе в контейнерах.",
"SSRF Protection": "Защита от SSRF", "SSRF Protection": "Защита от SSRF",
"Standard": "Стандартный", "Standard": "Стандартный",
...@@ -4088,6 +4114,7 @@ ...@@ -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": "Публичный URL вашего сервера, используемый для OAuth-перенаправлений, вебхуков и других внешних интеграций", "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 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 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:": "Слаг добавляется к URL:", "The slug is appended to the URL:": "Слаг добавляется к URL:",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "Загрузить фото", "Upload photo": "Загрузить фото",
"Upscale": "Увеличение", "Upscale": "Увеличение",
"Upstream": "Источник", "Upstream": "Источник",
"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 @@ ...@@ -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 to refresh the current Codex usage windows.": "Использует один доступный сброс, чтобы обновить текущие окна использования Codex.",
"Use our unified OpenAI-compatible endpoint in your applications": "Используйте наш единый OpenAI-совместимый эндпоинт в ваших приложениях", "Use our unified OpenAI-compatible endpoint in your applications": "Используйте наш единый OpenAI-совместимый эндпоинт в ваших приложениях",
"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.": "Используйте пресеты или обнаружение upstream, чтобы быстрее заполнить список моделей.", "Use presets or upstream discovery to populate the model list faster.": "Используйте пресеты или обнаружение upstream, чтобы быстрее заполнить список моделей.",
...@@ -4464,6 +4494,7 @@ ...@@ -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": "Используется в URL и маршрутах API", "Used in URLs and API routes": "Используется в URL и маршрутах API",
"Used Quota": "Лимит потребления", "Used Quota": "Лимит потребления",
"Used reset credits cannot be restored.": "Использованные сбросы нельзя восстановить.",
"Used to authenticate with io.net deployment API": "Используется для аутентификации в API развертывания io.net", "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 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.", "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 @@ ...@@ -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 logs": "Просмотреть логи", "View logs": "Просмотреть логи",
"View mode": "Режим отображения", "View mode": "Режим отображения",
"View model statistics and charts": "Просмотр статистики и графиков моделей", "View model statistics and charts": "Просмотр статистики и графиков моделей",
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "Cài đặt nâng cao", "Advanced Settings": "Cài đặt nâng cao",
"Advanced text editing": "Chỉnh sửa văn bản nâng cao", "Advanced text editing": "Chỉnh sửa văn bản nâng cao",
"Aesthetic style": "Phong cách", "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 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 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?", "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 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "Áp dụng bộ lọc", "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 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 Overwrite": "Áp dụng Ghi đè",
"Apply reset": "Thực hiện đặt lại",
"Apply Sync": "Áp dụng đồng bộ", "Apply Sync": "Áp dụng đồng bộ",
"Applying...": "Đang áp dụng...", "Applying...": "Đang áp dụng...",
"Approx.": "Xấp xỉ.", "Approx.": "Xấp xỉ.",
...@@ -485,8 +487,10 @@ ...@@ -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", "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)", "Availability (last 24h)": "Khả dụng (24 giờ qua)",
"Available": "Khả dụng", "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 disk space": "Dung lượng đĩa khả dụng",
"Available Models": "Mô hình 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ó", "Available Rewards": "Phần thưởng hiện có",
"Average latency": "Độ trễ trung bình", "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", "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 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "Kênh đã được bật thành công", "Channel enabled successfully": "Kênh đã được bật thành công",
"Channel Extra Settings": "Cài đặt thêm kênh", "Channel Extra Settings": "Cài đặt thêm kênh",
"Channel ID": "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": "Khóa kênh",
"Channel key unlocked": "Khóa kênh đã được mở khóa", "Channel key unlocked": "Khóa kênh đã được mở khóa",
"Channel models": "Mô hình kênh", "Channel models": "Mô hình kênh",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "Đã sao chép mã!", "Codes copied!": "Đã sao chép mã!",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Tài khoản và sử dụng 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 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 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", "Codex CLI Header Passthrough": "Chuyển tiếp header Codex CLI",
...@@ -928,6 +934,7 @@ ...@@ -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 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 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 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 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.", "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}}", "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 @@ ...@@ -1625,6 +1632,8 @@
"Expired at": "Hết hạn lúc", "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", "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": "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ệ", "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.", "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", "Expression": "Biểu thức",
...@@ -1699,6 +1708,7 @@ ...@@ -1699,6 +1708,7 @@
"Failed to fetch deployment details": "Không thể lấy chi tiết triển khai", "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 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 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 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 upstream ratios": "Không thể lấy tỷ lệ upstream",
"Failed to fetch usage": "Không lấy được mức sử dụng", "Failed to fetch usage": "Không lấy được mức sử dụng",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "Không thể đặt lại 2FA", "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 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 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": "Lưu thất bại",
"Failed to save announcements": "Không thể lưu thông báo", "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", "Failed to save API info": "Không thể lưu thông tin API",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, v.v.", "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, v.v.",
"GPU count": "Số lượng GPU", "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": "Lớn hơn", "Greater Than": "Lớn hơn",
"Greater than or equal": "Lớn hơn hoặc bằng", "Greater than or equal": "Lớn hơn hoặc bằng",
...@@ -2726,6 +2738,8 @@ ...@@ -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 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 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": "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 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 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", "No results found": "Không tìm thấy kết quả nào",
...@@ -2915,6 +2929,7 @@ ...@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "Giá token đầu ra cho nội dung được tạo.", "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",
"Output Tokens": "Token đầu ra", "Output Tokens": "Token đầu ra",
"Overage limited": "Đã giới hạn vượt mức",
"overall": "tổng", "overall": "tổng",
"Overnight range": "Khoảng qua nửa đêm", "Overnight range": "Khoảng qua nửa đêm",
"override": "ghi đè", "override": "ghi đè",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "Đệ quy", "Recursive": "Đệ quy",
"Redeem": "Đổi", "Redeem": "Đổi",
"Redeem codes": "Đổi mã", "Redeem codes": "Đổi mã",
"Redeemed": "Đã dùng",
"Redeemed at": "Đã dùng lúc",
"Redeemed By": "Được chuộc bởi", "Redeemed By": "Được chuộc bởi",
"Redeemed:": "Đã đổi:", "Redeemed:": "Đã đổi:",
"redemption code": "mã đổi thưởng", "redemption code": "mã đổi thưởng",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "Làm mới", "Refresh": "Làm mới",
"Refresh Cache": "Làm mới bộ nhớ đệm", "Refresh Cache": "Làm mới bộ nhớ đệm",
"Refresh credential": "Làm mới thông tin xác thực", "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 failed": "Làm mới thất bại",
"Refresh interval (minutes)": "Khoảng thời gian làm mới (phút)", "Refresh interval (minutes)": "Khoảng thời gian làm mới (phút)",
"Refresh Stats": "Làm mới thống kê", "Refresh Stats": "Làm mới thống kê",
...@@ -3491,6 +3509,11 @@ ...@@ -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 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 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 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 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 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", "Reset failed": "Đặt lại thất bại",
...@@ -3506,7 +3529,9 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "Đặt lại mặc định", "Reset to Default": "Đặt lại mặc định",
"Reset to default configuration": "Đã đặt lại cấu hình 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 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:", "Resets in:": "Đặt lại sau:",
"Resetting...": "Đang đặt lại...",
"Resolve Conflicts": "Giải quyết Xung đột", "Resolve Conflicts": "Giải quyết Xung đột",
"Resource Configuration": "Cấu hình tài nguyên", "Resource Configuration": "Cấu hình tài nguyên",
"Response": "Phản hồi", "Response": "Phản hồi",
...@@ -3872,6 +3897,7 @@ ...@@ -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 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": "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ể.", "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.", "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", "SSRF Protection": "Bảo vệ SSRF",
"Standard": "Tiêu chuẩn", "Standard": "Tiêu chuẩn",
...@@ -4088,6 +4114,7 @@ ...@@ -4088,6 +4114,7 @@
"The name displayed across the application": "Tên hiển thị trên ứng dụng", "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 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 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 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 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:", "The slug is appended to the URL:": "Slug được gắn vào URL:",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "Tải ảnh lên", "Upload photo": "Tải ảnh lên",
"Upscale": "Phóng to", "Upscale": "Phóng to",
"Upstream": "Thượng nguồn", "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 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 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", "Upstream Model Updates": "Cập nhật mô hình upstream",
...@@ -4447,6 +4475,8 @@ ...@@ -4447,6 +4475,8 @@
"Use backup code": "Sử dụng mã dự phòng", "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 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 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 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 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.", "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 @@ ...@@ -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 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 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 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 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 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.", "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 @@ ...@@ -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 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 details": "Xem chi tiết",
"View document": "Xem tài liệu", "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 logs": "Xem nhật ký",
"View mode": "Chế độ xem", "View mode": "Chế độ xem",
"View model statistics and charts": "Xem thống kê và biểu đồ mô hình", "View model statistics and charts": "Xem thống kê và biểu đồ mô hình",
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
"Advanced Settings": "高级设置", "Advanced Settings": "高级设置",
"Advanced text editing": "高级文本编辑", "Advanced text editing": "高级文本编辑",
"Aesthetic style": "画风", "Aesthetic style": "画风",
"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 @@ ...@@ -394,6 +395,7 @@
"Apply Filters": "应用筛选器", "Apply Filters": "应用筛选器",
"Apply IP Filter to Resolved Domains": "对已解析的域应用 IP 筛选器", "Apply IP Filter to Resolved Domains": "对已解析的域应用 IP 筛选器",
"Apply Overwrite": "应用覆盖", "Apply Overwrite": "应用覆盖",
"Apply reset": "执行重置",
"Apply Sync": "应用同步", "Apply Sync": "应用同步",
"Applying...": "正在应用...", "Applying...": "正在应用...",
"Approx.": "约", "Approx.": "约",
...@@ -485,8 +487,10 @@ ...@@ -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)": "可用率(最近 24 小时)", "Availability (last 24h)": "可用率(最近 24 小时)",
"Available": "可用", "Available": "可用",
"Available credits are ordered by soonest expiration.": "可用次数按最早到期排序。",
"Available disk space": "可用磁盘空间", "Available disk space": "可用磁盘空间",
"Available Models": "可用模型", "Available Models": "可用模型",
"Available reset credits": "可用重置次数",
"Available Rewards": "可用奖励", "Available Rewards": "可用奖励",
"Average latency": "平均延迟", "Average latency": "平均延迟",
"Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率", "Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率",
...@@ -686,6 +690,7 @@ ...@@ -686,6 +690,7 @@
"Channel enabled successfully": "渠道启用成功", "Channel enabled successfully": "渠道启用成功",
"Channel Extra Settings": "渠道额外设置", "Channel Extra Settings": "渠道额外设置",
"Channel ID": "渠道 ID", "Channel ID": "渠道 ID",
"Channel ID is required": "缺少渠道 ID",
"Channel key": "渠道密钥", "Channel key": "渠道密钥",
"Channel key unlocked": "渠道密钥已解锁", "Channel key unlocked": "渠道密钥已解锁",
"Channel models": "渠道模型", "Channel models": "渠道模型",
...@@ -811,6 +816,7 @@ ...@@ -811,6 +816,7 @@
"Codes copied!": "代码已复制!", "Codes copied!": "代码已复制!",
"Codex": "Codex", "Codex": "Codex",
"Codex Account & Usage": "Codex 账户和用量", "Codex Account & Usage": "Codex 账户和用量",
"Codex Account Status": "Codex 账号状态",
"Codex channels do not support batch creation": "Codex 渠道不支持批量创建", "Codex channels do not support batch creation": "Codex 渠道不支持批量创建",
"Codex channels use an OAuth JSON credential as the key.": "Codex 渠道使用 OAuth JSON 凭据作为密钥。", "Codex channels use an OAuth JSON credential as the key.": "Codex 渠道使用 OAuth JSON 凭据作为密钥。",
"Codex CLI Header Passthrough": "Codex CLI 请求头透传", "Codex CLI Header Passthrough": "Codex CLI 请求头透传",
...@@ -928,6 +934,7 @@ ...@@ -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 your identity before removing this Passkey from your account.": "在解绑当前账号的 Passkey 前请确认你的身份。", "Confirm your identity before removing this Passkey from your account.": "在解绑当前账号的 Passkey 前请确认你的身份。",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "在注册 Passkey 前请使用两步验证确认你的身份。", "Confirm your identity with Two-factor Authentication before registering a Passkey.": "在注册 Passkey 前请使用两步验证确认你的身份。",
"Confirmed at {{time}} by user #{{userId}}": "用户 #{{userId}} 于 {{time}} 确认", "Confirmed at {{time}} by user #{{userId}}": "用户 #{{userId}} 于 {{time}} 确认",
...@@ -1625,6 +1632,8 @@ ...@@ -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 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 @@ ...@@ -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": "获取 OIDC 配置失败。请检查 URL 和网络状态", "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 prices": "获取上游价格失败",
"Failed to fetch upstream ratios": "获取上游比率失败", "Failed to fetch upstream ratios": "获取上游比率失败",
"Failed to fetch usage": "获取用量失败", "Failed to fetch usage": "获取用量失败",
...@@ -1732,6 +1742,7 @@ ...@@ -1732,6 +1742,7 @@
"Failed to reset 2FA": "重置 2FA 失败", "Failed to reset 2FA": "重置 2FA 失败",
"Failed to reset model ratios": "重置模型比率失败", "Failed to reset model ratios": "重置模型比率失败",
"Failed to reset Passkey": "重置 Passkey 失败", "Failed to reset Passkey": "重置 Passkey 失败",
"Failed to reset usage": "重置用量失败",
"Failed to save": "保存失败", "Failed to save": "保存失败",
"Failed to save announcements": "保存公告失败", "Failed to save announcements": "保存公告失败",
"Failed to save API info": "保存 API 信息失败", "Failed to save API info": "保存 API 信息失败",
...@@ -1962,6 +1973,7 @@ ...@@ -1962,6 +1973,7 @@
"gpt-4": "gpt-4", "gpt-4": "gpt-4",
"gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, 等", "gpt-4, claude-3-opus, etc.": "gpt-4, claude-3-opus, 等",
"GPU count": "GPU 数量", "GPU count": "GPU 数量",
"Granted at": "发放时间",
"Greater than": "大于", "Greater than": "大于",
"Greater Than": "大于", "Greater Than": "大于",
"Greater than or equal": "大于等于", "Greater than or equal": "大于等于",
...@@ -2726,6 +2738,8 @@ ...@@ -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 available": "没有可用重置次数",
"No restriction": "无限制", "No restriction": "无限制",
"No results for \"{{query}}\". Try adjusting your search or filters.": "未找到 \"{{query}}\" 的结果。请尝试调整搜索或筛选条件。", "No results for \"{{query}}\". Try adjusting your search or filters.": "未找到 \"{{query}}\" 的结果。请尝试调整搜索或筛选条件。",
"No results found": "搜索无结果", "No results found": "搜索无结果",
...@@ -2915,6 +2929,7 @@ ...@@ -2915,6 +2929,7 @@
"Output token price for generated tokens.": "生成内容的输出 token 价格。", "Output token price for generated tokens.": "生成内容的输出 token 价格。",
"Output tokens": "输出 token", "Output tokens": "输出 token",
"Output Tokens": "输出 Token", "Output Tokens": "输出 Token",
"Overage limited": "超额受限",
"overall": "总体", "overall": "总体",
"Overnight range": "跨日范围", "Overnight range": "跨日范围",
"override": "覆盖", "override": "覆盖",
...@@ -3346,6 +3361,8 @@ ...@@ -3346,6 +3361,8 @@
"Recursive": "递归", "Recursive": "递归",
"Redeem": "兑换额度", "Redeem": "兑换额度",
"Redeem codes": "兑换码", "Redeem codes": "兑换码",
"Redeemed": "已使用",
"Redeemed at": "使用时间",
"Redeemed By": "兑换人", "Redeemed By": "兑换人",
"Redeemed:": "已兑换:", "Redeemed:": "已兑换:",
"redemption code": "兑换码", "redemption code": "兑换码",
...@@ -3373,6 +3390,7 @@ ...@@ -3373,6 +3390,7 @@
"Refresh": "刷新", "Refresh": "刷新",
"Refresh Cache": "刷新缓存", "Refresh Cache": "刷新缓存",
"Refresh credential": "刷新凭据", "Refresh credential": "刷新凭据",
"Refresh details": "刷新详情",
"Refresh failed": "刷新失败", "Refresh failed": "刷新失败",
"Refresh interval (minutes)": "刷新间隔 (分钟)", "Refresh interval (minutes)": "刷新间隔 (分钟)",
"Refresh Stats": "刷新统计", "Refresh Stats": "刷新统计",
...@@ -3491,6 +3509,11 @@ ...@@ -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. Latest usage has been refreshed.": "重置完成,已刷新最新用量。",
"Reset count:": "重置次数:",
"Reset Credit": "重置次数",
"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 @@ ...@@ -3506,7 +3529,9 @@
"Reset to Default": "重置为默认", "Reset to Default": "重置为默认",
"Reset to default configuration": "已重置为默认配置", "Reset to default configuration": "已重置为默认配置",
"Reset Two-Factor Authentication": "重置 2FA", "Reset Two-Factor Authentication": "重置 2FA",
"Reset usage window": "重置用量窗口",
"Resets in:": "将于以下时间重置:", "Resets in:": "将于以下时间重置:",
"Resetting...": "重置中...",
"Resolve Conflicts": "解决冲突", "Resolve Conflicts": "解决冲突",
"Resource Configuration": "资源配置", "Resource Configuration": "资源配置",
"Response": "响应", "Response": "响应",
...@@ -3872,6 +3897,7 @@ ...@@ -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": "消费受限",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite 将所有数据存储在单个文件中。在容器中运行时请确保该文件已持久化。", "SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite 将所有数据存储在单个文件中。在容器中运行时请确保该文件已持久化。",
"SSRF Protection": "SSRF 保护", "SSRF Protection": "SSRF 保护",
"Standard": "标准", "Standard": "标准",
...@@ -4088,6 +4114,7 @@ ...@@ -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": "服务器的公开URL,用于OAuth回调、Webhook和其他外部集成", "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 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 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:": "别名将附加到 URL:", "The slug is appended to the URL:": "别名将附加到 URL:",
...@@ -4402,6 +4429,7 @@ ...@@ -4402,6 +4429,7 @@
"Upload photo": "上传照片", "Upload photo": "上传照片",
"Upscale": "放大", "Upscale": "放大",
"Upstream": "上游", "Upstream": "上游",
"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 @@ ...@@ -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.": "将为当前渠道使用 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 our unified OpenAI-compatible endpoint in your applications": "在应用中使用我们兼容 OpenAI 的统一接口",
"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 @@ ...@@ -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": "用于URL和API路由", "Used in URLs and API routes": "用于URL和API路由",
"Used Quota": "消耗额度", "Used Quota": "消耗额度",
"Used reset credits cannot be restored.": "已使用的重置次数无法恢复。",
"Used to authenticate with io.net deployment API": "用于 io.net 部署 API 鉴权", "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 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 参数提交。", "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 @@ ...@@ -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 logs": "查看日志", "View logs": "查看日志",
"View mode": "视图模式", "View mode": "视图模式",
"View model statistics and charts": "查看模型统计和图表", "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