1+ import { useState } from 'react' ;
12import type { Translator } from '../model/types' ;
23import {
34 resolveProviderDetailModelOptions ,
@@ -7,13 +8,17 @@ import {
78 type ProviderRemoteModelsState ,
89 type ProviderVerifyState ,
910} from '../model/openAICompatible' ;
11+ import type { RateLimitState , RateLimitStrategyMeta } from '../model/rateLimit' ;
1012import AccountDetailModalFrame from './AccountDetailModalFrame' ;
13+ import RateLimitRulesSection from './RateLimitRulesSection' ;
1114
1215interface OpenAICompatibleDetailModalProps {
1316 t : Translator ;
1417 draft : OpenAICompatibleProviderDraft ;
1518 verifyState : ProviderVerifyState ;
1619 remoteModelsState ?: ProviderRemoteModelsState ;
20+ rateLimitStatus ?: RateLimitState ;
21+ rateLimitStrategies ?: RateLimitStrategyMeta [ ] ;
1722 error : string ;
1823 saving : boolean ;
1924 onClose : ( ) => void ;
@@ -22,6 +27,7 @@ interface OpenAICompatibleDetailModalProps {
2227 onVerify : ( ) => void ;
2328 onFetchModels : ( ) => void ;
2429 onApplyFetchedModels : ( ) => void ;
30+ onRateLimitRulesChanged : ( ) => void ;
2531}
2632
2733function formatLastVerifiedAt ( timestamp : number | null ) {
@@ -36,6 +42,8 @@ export default function OpenAICompatibleDetailModal({
3642 draft,
3743 verifyState,
3844 remoteModelsState,
45+ rateLimitStatus,
46+ rateLimitStrategies,
3947 error,
4048 saving,
4149 onClose,
@@ -44,7 +52,9 @@ export default function OpenAICompatibleDetailModal({
4452 onVerify,
4553 onFetchModels,
4654 onApplyFetchedModels,
55+ onRateLimitRulesChanged,
4756} : OpenAICompatibleDetailModalProps ) {
57+ const [ headersExpanded , setHeadersExpanded ] = useState ( false ) ;
4858 const selectedPreset = resolveOpenAICompatibleProviderPreset ( {
4959 name : draft . name ,
5060 baseUrl : draft . baseUrl ,
@@ -55,6 +65,9 @@ export default function OpenAICompatibleDetailModal({
5565 } ) ;
5666 const suggestedModels : OpenAICompatibleModelRow [ ] = suggestedModelOptions . models ;
5767 const effectiveVerifyModel = draft . verifyModel || suggestedModels [ 0 ] ?. name || '' ;
68+ const rateLimitAccountName = draft . currentName || draft . name ;
69+ const rateLimitAccountKey = `openai-compatible:${ rateLimitAccountName } ` ;
70+ const rateLimitMatchKey = rateLimitStatus ?. matchKey || `provider:${ rateLimitAccountName . trim ( ) . toLowerCase ( ) } ` ;
5871 const modelSourceLabel =
5972 suggestedModelOptions . source === 'remote'
6073 ? t ( 'accounts.openai_provider_models_source_remote' )
@@ -117,8 +130,7 @@ export default function OpenAICompatibleDetailModal({
117130 </ header >
118131
119132 < div className = "min-h-0 flex-1 overflow-auto" >
120- < div className = "grid gap-0 xl:grid-cols-[1.1fr_0.9fr]" >
121- < section className = "min-h-0 px-6 py-6 xl:border-r-2 xl:border-[var(--border-color)]" >
133+ < section className = "min-h-0 px-6 py-6" >
122134 < div className = "space-y-6" >
123135 < label className = "space-y-2" >
124136 < div className = "text-[0.5625rem] font-black uppercase tracking-[0.2em] text-[var(--text-muted)]" >
@@ -146,18 +158,29 @@ export default function OpenAICompatibleDetailModal({
146158 </ label >
147159
148160 < div className = "space-y-2" >
149- < div className = "text-[0.5625rem] font-black uppercase tracking-[0.2em] text-[var(--text-muted)]" >
161+ < button
162+ type = "button"
163+ onClick = { ( ) => setHeadersExpanded ( ( prev ) => ! prev ) }
164+ className = "flex w-full items-center gap-2 text-[0.5625rem] font-black uppercase tracking-[0.2em] text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]"
165+ >
166+ < span className = "inline-grid h-3.5 w-3.5 place-items-center border border-[var(--border-color)] text-[0.5rem] leading-none" >
167+ { draft . headersText || headersExpanded ? '−' : '+' }
168+ </ span >
150169 { t ( 'accounts.openai_provider_headers' ) }
151- </ div >
152- < textarea
153- value = { draft . headersText }
154- onChange = { ( event ) => onChange ( { ...draft , headersText : event . target . value } ) }
155- className = "input-swiss min-h-32 w-full resize-y font-mono !text-[0.6875rem] leading-6"
156- placeholder = { 'Authorization: Bearer sk-...\nHTTP-Referer: https://example.com\nX-Title: GetTokens' }
157- />
158- < div className = "text-[0.5rem] font-black uppercase tracking-[0.14em] text-[var(--text-muted)]" >
159- { t ( 'accounts.openai_provider_headers_hint' ) }
160- </ div >
170+ </ button >
171+ { ( draft . headersText || headersExpanded ) ? (
172+ < >
173+ < textarea
174+ value = { draft . headersText }
175+ onChange = { ( event ) => onChange ( { ...draft , headersText : event . target . value } ) }
176+ className = "input-swiss min-h-32 w-full resize-y font-mono !text-[0.6875rem] leading-6"
177+ placeholder = { 'Authorization: Bearer sk-...\nHTTP-Referer: https://example.com\nX-Title: GetTokens' }
178+ />
179+ < div className = "text-[0.5rem] font-black uppercase tracking-[0.14em] text-[var(--text-muted)]" >
180+ { t ( 'accounts.openai_provider_headers_hint' ) }
181+ </ div >
182+ </ >
183+ ) : null }
161184 </ div >
162185
163186 < div className = "space-y-3" >
@@ -261,7 +284,7 @@ export default function OpenAICompatibleDetailModal({
261284 </ div >
262285 </ section >
263286
264- < section className = "min-h-0 space-y-6 bg-[var(--bg-surface)]/30 px-6 py-6" >
287+ < section className = "min-h-0 space-y-6 border-t-2 border-[var(--border-color)] bg-[var(--bg-surface)]/30 px-6 py-6" >
265288 < div className = "space-y-4" >
266289 < div className = "text-[0.5625rem] font-black uppercase tracking-[0.2em] text-[var(--text-muted)]" >
267290 { t ( 'accounts.openai_provider_test_model' ) }
@@ -311,7 +334,14 @@ export default function OpenAICompatibleDetailModal({
311334 </ div >
312335 </ div >
313336 </ section >
314- </ div >
337+ < RateLimitRulesSection
338+ accountKey = { rateLimitAccountKey }
339+ matchKey = { rateLimitMatchKey }
340+ rateLimitStatus = { rateLimitStatus }
341+ rateLimitStrategies = { rateLimitStrategies }
342+ onRateLimitRulesChanged = { onRateLimitRulesChanged }
343+ t = { t }
344+ />
315345 </ div >
316346
317347 { error ? (
0 commit comments