Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions gui/src/components/provider-workspace/ProviderSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const ADAPTERS = ["openai-responses", "openai-chat", "anthropic", "google", "azu
const EMPTY_MODELS: string[] = [];

type ChoicesStatus = "idle" | "loading" | "ready" | "error";
type PacingRule = { requestsPerMinute?: number; minIntervalMs?: number };
type PacingRule = { requestsPerMinute?: number; minIntervalMs?: number; maxConcurrentRequests?: number };
type PacingStatus = { enabled: boolean; queued: number; nextSlotInMs: number; lastStartedAt?: number; lastModelId?: string };
type CursorHttpVersion = "http2" | "http1.1";

Expand All @@ -50,11 +50,12 @@ function positiveInteger(value: string): number | undefined {
function pacingSignature(value: WorkspaceItem["requestPacing"] | undefined): string {
const models = Object.entries(value?.models ?? {})
.sort(([left], [right]) => left.localeCompare(right))
.map(([model, rule]) => [model, rule.requestsPerMinute ?? null, rule.minIntervalMs ?? null]);
.map(([model, rule]) => [model, rule.requestsPerMinute ?? null, rule.minIntervalMs ?? null, rule.maxConcurrentRequests ?? null]);
return JSON.stringify([
value?.enabled === true,
value?.requestsPerMinute ?? null,
value?.minIntervalMs ?? null,
value?.maxConcurrentRequests ?? null,
models,
]);
}
Expand Down Expand Up @@ -96,10 +97,12 @@ export default function ProviderSettings({
const [pacingEnabled, setPacingEnabled] = useState(item.requestPacing?.enabled === true);
const [pacingRpm, setPacingRpm] = useState(() => numberDraft(item.requestPacing?.requestsPerMinute));
const [pacingDelay, setPacingDelay] = useState(() => numberDraft(item.requestPacing?.minIntervalMs));
const [pacingConcurrency, setPacingConcurrency] = useState(() => numberDraft(item.requestPacing?.maxConcurrentRequests));
const [pacingModels, setPacingModels] = useState<Record<string, PacingRule>>(() => ({ ...(item.requestPacing?.models ?? {}) }));
const [pacingModelId, setPacingModelId] = useState("");
const [pacingModelRpm, setPacingModelRpm] = useState("");
const [pacingModelDelay, setPacingModelDelay] = useState("");
const [pacingModelConcurrency, setPacingModelConcurrency] = useState("");
const [pacingStatus, setPacingStatus] = useState<PacingStatus | null>(null);

/* eslint-disable react-hooks/set-state-in-effect -- intentional form reset when saved provider fields change */
Expand All @@ -116,6 +119,7 @@ export default function ProviderSettings({
setPacingEnabled(item.requestPacing?.enabled === true);
setPacingRpm(numberDraft(item.requestPacing?.requestsPerMinute));
setPacingDelay(numberDraft(item.requestPacing?.minIntervalMs));
setPacingConcurrency(numberDraft(item.requestPacing?.maxConcurrentRequests));
setPacingModels({ ...(item.requestPacing?.models ?? {}) });
setMsg(null);
setModeMsg(null);
Expand Down Expand Up @@ -186,8 +190,9 @@ export default function ProviderSettings({
enabled: pacingEnabled,
...(positiveRpm(pacingRpm) !== undefined ? { requestsPerMinute: positiveRpm(pacingRpm) } : {}),
...(positiveInteger(pacingDelay) !== undefined ? { minIntervalMs: positiveInteger(pacingDelay) } : {}),
...(positiveInteger(pacingConcurrency) !== undefined ? { maxConcurrentRequests: positiveInteger(pacingConcurrency) } : {}),
...(Object.keys(pacingModels).length > 0 ? { models: pacingModels } : {}),
}), [pacingDelay, pacingEnabled, pacingModels, pacingRpm]);
}), [pacingConcurrency, pacingDelay, pacingEnabled, pacingModels, pacingRpm]);

const dirty = adapter.trim() !== item.adapter
|| baseUrl.trim() !== item.baseUrl
Expand Down Expand Up @@ -235,7 +240,10 @@ export default function ProviderSettings({
setSaving(true);
setMsg(null);
try {
if (pacingEnabled && !pacingDraft.requestsPerMinute && !pacingDraft.minIntervalMs && !pacingDraft.models) {
if (pacingConcurrency.trim() !== "" && positiveInteger(pacingConcurrency) === undefined) {
setMsg({ ok: false, text: t("pws.pacingCapInvalid") }); return false;
}
if (pacingEnabled && !pacingDraft.requestsPerMinute && !pacingDraft.minIntervalMs && !pacingDraft.maxConcurrentRequests && !pacingDraft.models) {
setMsg({ ok: false, text: t("pws.pacingRuleRequired") }); return false;
}
const pacingOnly = pacingDirty && !dirty;
Expand Down Expand Up @@ -322,7 +330,9 @@ export default function ProviderSettings({
setNote(item.note ?? ""); setAllowPrivateNetwork(item.allowPrivateNetwork ?? false); setLiveModels(savedLiveModels);
setCursorHttpVersion(savedCursorHttpVersion); setMsg(null);
setPacingEnabled(item.requestPacing?.enabled === true); setPacingRpm(numberDraft(item.requestPacing?.requestsPerMinute));
setPacingDelay(numberDraft(item.requestPacing?.minIntervalMs)); setPacingModels({ ...(item.requestPacing?.models ?? {}) });
setPacingDelay(numberDraft(item.requestPacing?.minIntervalMs));
setPacingConcurrency(numberDraft(item.requestPacing?.maxConcurrentRequests));
setPacingModels({ ...(item.requestPacing?.models ?? {}) });
setEndpointChoice(matchChoiceId(baseUrlChoices, item.baseUrl));
};

Expand All @@ -339,9 +349,21 @@ export default function ProviderSettings({
const modelId = pacingModelId.trim();
const rpm = positiveRpm(pacingModelRpm);
const delay = positiveInteger(pacingModelDelay);
if (!modelId || (rpm === undefined && delay === undefined)) return;
setPacingModels(current => ({ ...current, [modelId]: { ...(rpm !== undefined ? { requestsPerMinute: rpm } : {}), ...(delay !== undefined ? { minIntervalMs: delay } : {}) } }));
setPacingModelId(""); setPacingModelRpm(""); setPacingModelDelay("");
const cap = positiveInteger(pacingModelConcurrency);
if (pacingModelConcurrency.trim() !== "" && cap === undefined) {
setMsg({ ok: false, text: t("pws.pacingCapInvalid") });
return;
}
if (!modelId || (rpm === undefined && delay === undefined && cap === undefined)) return;
setPacingModels(current => ({
...current,
[modelId]: {
...(rpm !== undefined ? { requestsPerMinute: rpm } : {}),
...(delay !== undefined ? { minIntervalMs: delay } : {}),
...(cap !== undefined ? { maxConcurrentRequests: cap } : {}),
},
}));
setPacingModelId(""); setPacingModelRpm(""); setPacingModelDelay(""); setPacingModelConcurrency("");
};

return (
Expand Down Expand Up @@ -500,6 +522,7 @@ export default function ProviderSettings({
<div className="pwi-pacing-grid">
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingRpm")}</span><input className="input" type="number" min="0.016667" step="any" value={pacingRpm} onChange={e => setPacingRpm(e.target.value)} placeholder="38" /></label>
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingDelay")}</span><input className="input" type="number" min="1" step="1" value={pacingDelay} onChange={e => setPacingDelay(e.target.value)} placeholder="1600" /></label>
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingConcurrency")}</span><input className="input" type="number" min="1" step="1" value={pacingConcurrency} onChange={e => setPacingConcurrency(e.target.value)} placeholder="8" /></label>
</div>
<p className="pwi-settings-hint">{t("pws.pacingSlowerWins")}</p>
<div className="pwi-pacing-status" aria-live="polite">
Expand All @@ -512,9 +535,14 @@ export default function ProviderSettings({
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingModel")}</span><input className="input" list={`pacing-models-${item.name}`} value={pacingModelId} onChange={e => setPacingModelId(e.target.value)} /><datalist id={`pacing-models-${item.name}`}>{availableModels.map(model => <option key={model} value={model} />)}</datalist></label>
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingRpm")}</span><input className="input" type="number" min="0.016667" step="any" value={pacingModelRpm} onChange={e => setPacingModelRpm(e.target.value)} /></label>
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingDelay")}</span><input className="input" type="number" min="1" step="1" value={pacingModelDelay} onChange={e => setPacingModelDelay(e.target.value)} /></label>
<label className="pwi-settings-field"><span className="pwi-settings-label">{t("pws.pacingConcurrency")}</span><input className="input" type="number" min="1" step="1" value={pacingModelConcurrency} onChange={e => setPacingModelConcurrency(e.target.value)} /></label>
<button type="button" className="btn btn-ghost btn-sm" onClick={addPacingModel}>{t("pws.pacingAdd")}</button>
</div>
{Object.entries(pacingModels).length > 0 && <div className="pwi-pacing-overrides">{Object.entries(pacingModels).map(([model, rule]) => <div key={model} className="pwi-pacing-row"><code>{model}</code><span>{rule.requestsPerMinute !== undefined ? `${rule.requestsPerMinute} ${t("pws.pacingRpmUnit")}` : ""}{rule.requestsPerMinute !== undefined && rule.minIntervalMs !== undefined ? " · " : ""}{rule.minIntervalMs !== undefined ? `${rule.minIntervalMs} ms` : ""}</span><button type="button" className="btn btn-ghost btn-sm" onClick={() => setPacingModels(current => Object.fromEntries(Object.entries(current).filter(([id]) => id !== model)))} aria-label={t("pws.pacingRemoveModel", { model })}>{t("pws.pacingRemove")}</button></div>)}</div>}
{Object.entries(pacingModels).length > 0 && <div className="pwi-pacing-overrides">{Object.entries(pacingModels).map(([model, rule]) => <div key={model} className="pwi-pacing-row"><code>{model}</code><span>{[
rule.requestsPerMinute !== undefined ? `${rule.requestsPerMinute} ${t("pws.pacingRpmUnit")}` : undefined,
rule.minIntervalMs !== undefined ? `${rule.minIntervalMs} ms` : undefined,
rule.maxConcurrentRequests !== undefined ? `${rule.maxConcurrentRequests} ${t("pws.pacingCapUnit")}` : undefined,
].filter(part => part !== undefined).join(" · ")}</span><button type="button" className="btn btn-ghost btn-sm" onClick={() => setPacingModels(current => Object.fromEntries(Object.entries(current).filter(([id]) => id !== model)))} aria-label={t("pws.pacingRemoveModel", { model })}>{t("pws.pacingRemove")}</button></div>)}</div>}
</section>
{formDirty && (
<div className="pwi-settings-sticky-bar">
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2688,9 +2688,11 @@ export const de: Record<TKey, string> = {
"pws.pacingDesc": "Verteilt ausgehende Anfragestarts für diesen Anbieter gleichmäßig. Streaming-Antworten dürfen sich überlappen.",
"pws.pacingEnabled": "Aktiviert",
"pws.pacingRpm": "Anfragen pro Minute",
"pws.pacingConcurrency": "Maximale gleichzeitige Anfragen",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "Mindestintervall (ms)",
"pws.pacingSlowerWins": "Das langsamere Anbieterlimit gilt. Modellregeln können nur stärker verzögern.",
"pws.pacingSlowerWins": "Das langsamere Anbieterlimit gilt. Modellregeln können nur stärker verzögern oder die Zahl gleichzeitiger Anfragen senken.",
"pws.pacingQueued": "in Warteschlange",
"pws.pacingNextSlot": "bis zum nächsten Slot",
"pws.pacingLastModel": "letztes Modell",
Expand All @@ -2701,6 +2703,7 @@ export const de: Record<TKey, string> = {
"pws.pacingRemove": "Entfernen",
"pws.pacingRemoveModel": "Anfragetaktung für {model} entfernen",
"pws.pacingRuleRequired": "Legen Sie zuerst ein Anbieterlimit oder eine Modellregel fest.",
"pws.pacingCapInvalid": "Die maximale Anzahl gleichzeitiger Anfragen muss eine ganze Zahl größer oder gleich 1 sein.",
"pws.saving": "Wird gespeichert…",
"pws.settingsSaved": "Einstellungen gespeichert.",
"pws.accountModeSaved": "Kontomodus gespeichert.",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,9 +1723,11 @@ export const en = {
"pws.pacingDesc": "Evenly delay outbound request starts for this provider. Streaming responses may overlap.",
"pws.pacingEnabled": "Enabled",
"pws.pacingRpm": "Requests per minute",
"pws.pacingConcurrency": "Max concurrent requests",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "Minimum interval (ms)",
"pws.pacingSlowerWins": "The slower provider limit wins. Model overrides can only add more delay.",
"pws.pacingSlowerWins": "The slower provider limit wins. Model overrides can only add more delay or lower the concurrency cap.",
"pws.pacingQueued": "queued",
"pws.pacingNextSlot": "until next slot",
"pws.pacingLastModel": "last model",
Expand All @@ -1736,6 +1738,7 @@ export const en = {
"pws.pacingRemove": "Remove",
"pws.pacingRemoveModel": "Remove request pacing override for {model}",
"pws.pacingRuleRequired": "Enable request pacing only after setting a provider limit or a model override.",
"pws.pacingCapInvalid": "Max concurrent requests must be a whole number of 1 or more.",
"pws.saving": "Saving…",
"pws.settingsSaved": "Settings saved.",
"pws.accountModeSaved": "Account mode saved.",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,9 +1670,11 @@ export const fr: Record<TKey, string> = {
"pws.pacingDesc": "Répartissez uniformément le démarrage des requêtes sortantes pour ce fournisseur. Les réponses diffusées peuvent se chevaucher.",
"pws.pacingEnabled": "Activé",
"pws.pacingRpm": "Requêtes par minute",
"pws.pacingConcurrency": "Requêtes simultanées max",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "Intervalle minimal (ms)",
"pws.pacingSlowerWins": "La limite la plus lente du fournisseur s’applique. Les remplacements par modèle ne peuvent qu’ajouter un délai.",
"pws.pacingSlowerWins": "La limite la plus lente du fournisseur s’applique. Les remplacements par modèle ne peuvent qu’ajouter un délai ou réduire le plafond de requêtes simultanées.",
"pws.pacingQueued": "en attente",
"pws.pacingNextSlot": "avant le prochain créneau",
"pws.pacingLastModel": "dernier modèle",
Expand All @@ -1683,6 +1685,7 @@ export const fr: Record<TKey, string> = {
"pws.pacingRemove": "Supprimer",
"pws.pacingRemoveModel": "Supprimer le remplacement du cadencement des requêtes pour {model}",
"pws.pacingRuleRequired": "Activez le cadencement des requêtes seulement après avoir défini une limite de fournisseur ou un remplacement par modèle.",
"pws.pacingCapInvalid": "Le nombre maximal de requêtes simultanées doit être un nombre entier supérieur ou égal à 1.",
"pws.providerId": "ID du fournisseur",
"pws.reauth": "Réauthentification requise",
"pws.reauthenticate": "Réauthentifier",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,9 +1599,11 @@ export const ja: Record<TKey, string> = {
"pws.pacingDesc": "このプロバイダーへの送信開始を均等に遅延します。ストリーミング応答は重複できます。",
"pws.pacingEnabled": "有効",
"pws.pacingRpm": "1分あたりのリクエスト数",
"pws.pacingConcurrency": "最大同時リクエスト数",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "最小間隔 (ms)",
"pws.pacingSlowerWins": "より遅いプロバイダー制限が優先され、モデル設定は遅延を増やす場合のみ適用されます。",
"pws.pacingSlowerWins": "より遅いプロバイダー制限が優先され、モデル設定は遅延を増やすか同時リクエスト数の上限を下げる場合のみ適用されます。",
"pws.pacingQueued": "待機中",
"pws.pacingNextSlot": "次のスロットまで",
"pws.pacingLastModel": "最後のモデル",
Expand All @@ -1612,6 +1614,7 @@ export const ja: Record<TKey, string> = {
"pws.pacingRemove": "削除",
"pws.pacingRemoveModel": "{model} のリクエスト間隔設定を削除",
"pws.pacingRuleRequired": "プロバイダー制限またはモデル別設定を追加してから有効にしてください。",
"pws.pacingCapInvalid": "最大同時リクエスト数は 1 以上の整数にしてください。",
"pws.saving": "保存中…",
"pws.settingsSaved": "設定を保存しました。",
"pws.accountModeSaved": "アカウントモードを保存しました。",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2727,9 +2727,11 @@ export const ko: Record<TKey, string> = {
"pws.pacingDesc": "이 프로바이더로 나가는 요청 시작을 일정 간격으로 지연합니다. 스트리밍 응답은 서로 겹칠 수 있습니다.",
"pws.pacingEnabled": "사용",
"pws.pacingRpm": "분당 요청 수",
"pws.pacingConcurrency": "최대 동시 요청 수",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "최소 시작 간격(ms)",
"pws.pacingSlowerWins": "프로바이더의 더 느린 제한이 우선합니다. 모델별 설정은 지연을 더 늘릴 때만 적용됩니다.",
"pws.pacingSlowerWins": "프로바이더의 더 느린 제한이 우선합니다. 모델별 설정은 지연을 더 늘리거나 동시 요청 상한을 낮출 때만 적용됩니다.",
"pws.pacingQueued": "대기 중",
"pws.pacingNextSlot": "다음 슬롯까지",
"pws.pacingLastModel": "마지막 모델",
Expand All @@ -2740,6 +2742,7 @@ export const ko: Record<TKey, string> = {
"pws.pacingRemove": "제거",
"pws.pacingRemoveModel": "{model} 모델의 요청 속도 설정 제거",
"pws.pacingRuleRequired": "프로바이더 제한이나 모델별 제한을 하나 이상 설정한 뒤 요청 속도 조절을 켜세요.",
"pws.pacingCapInvalid": "최대 동시 요청 수는 1 이상의 정수여야 합니다.",
"pws.saving": "저장 중…",
"pws.settingsSaved": "설정이 저장되었습니다.",
"pws.accountModeSaved": "계정 모드가 저장되었습니다.",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1690,9 +1690,11 @@ export const ru: Record<TKey, string> = {
"pws.pacingDesc": "Равномерно задерживает начало исходящих запросов к провайдеру. Потоковые ответы могут пересекаться.",
"pws.pacingEnabled": "Включено",
"pws.pacingRpm": "Запросов в минуту",
"pws.pacingConcurrency": "Макс. одновременных запросов",
"pws.pacingRpmUnit": "RPM",
"pws.pacingCapUnit": "×",
"pws.pacingDelay": "Минимальный интервал (мс)",
"pws.pacingSlowerWins": "Действует более медленный лимит провайдера. Правила моделей могут только увеличить задержку.",
"pws.pacingSlowerWins": "Действует более медленный лимит провайдера. Правила моделей могут только увеличить задержку или снизить лимит одновременных запросов.",
"pws.pacingQueued": "в очереди",
"pws.pacingNextSlot": "до следующего слота",
"pws.pacingLastModel": "последняя модель",
Expand All @@ -1703,6 +1705,7 @@ export const ru: Record<TKey, string> = {
"pws.pacingRemove": "Удалить",
"pws.pacingRemoveModel": "Удалить интервал запросов для {model}",
"pws.pacingRuleRequired": "Сначала задайте лимит провайдера или правило модели.",
"pws.pacingCapInvalid": "Максимальное количество одновременных запросов должно быть целым числом не меньше 1.",
"pws.saving": "Сохранение…",
"pws.settingsSaved": "Настройки сохранены.",
"pws.accountModeSaved": "Режим аккаунта сохранён.",
Expand Down
Loading
Loading