Q171: provider policy fields say what they govern and show pending and saved state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c291cf7a7
commit
059970e7e1
4 files changed
+212
-156
No files matched your search
@@ -687,4 +687,12 @@ export const zh: Record<string, string> = {
|
|||||||
"{model} (provider default)": "{model}(Provider 默认)",
|
"{model} (provider default)": "{model}(Provider 默认)",
|
||||||
"Chosen when the session starts": "会话开始时确定",
|
"Chosen when the session starts": "会话开始时确定",
|
||||||
"The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.": "默认模型作用于 {following} 个未单独设置模型的 Agent;{own} 个保留自己的模型。已开始的会话继续使用开始时的模型。",
|
"The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.": "默认模型作用于 {following} 个未单独设置模型的 Agent;{own} 个保留自己的模型。已开始的会话继续使用开始时的模型。",
|
||||||
|
"Scope: “Workspace” refuses every call that is not made for a workspace, including agent sessions on tasks without one.": "范围:选择“工作区”后,凡不是为某个工作区发起的调用都会被拒绝,包括没有工作区的任务上的 Agent 会话。",
|
||||||
|
"Data policy: zero data retention refuses background requests, so it also switches background off.": "数据策略:零数据保留会拒绝后台请求,因此会同时关闭后台执行。",
|
||||||
|
"Background: lets requests from the request console run in the provider's background mode.": "后台:允许请求控制台发出的请求以 Provider 的后台模式运行。",
|
||||||
|
"Monthly limit: counts every model call this company makes through the provider in the calendar month (UTC); at the limit, calls are refused and agent work waits until the month resets.": "月额度:统计本公司在当月(UTC 自然月)通过该 Provider 发起的每次模型调用;达到上限后调用会被拒绝,Agent 工作等待到下月重置。",
|
||||||
|
"Context window: the size new sessions fit prompts, skills, history and tool results into; sessions already running keep theirs.": "上下文窗口:新会话把提示、技能、历史和工具结果放入的容量;已在运行的会话保持原值。",
|
||||||
|
"Zero data retention switched background off.": "零数据保留已关闭后台执行。",
|
||||||
|
"Resets {date} (UTC)": "{date} 重置(UTC)",
|
||||||
|
"Unsaved": "未保存",
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Pill } from "../../shared/ui";
|
||||||
|
import { t } from "../../i18n/i18n";
|
||||||
|
import type { ProviderRateLimitStateRecord, ProviderRecord } from "../../../../shared/domain";
|
||||||
|
import { PROVIDER_DATA_POLICY } from "../../../../shared/domain";
|
||||||
|
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||||
|
|
||||||
|
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 124px 132px";
|
||||||
|
type WorkspaceScope = "organization" | "workspace";
|
||||||
|
|
||||||
|
export function ProviderPolicyTable({
|
||||||
|
orgId,
|
||||||
|
providers,
|
||||||
|
providerRateLimits,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
providers: ProviderRecord[];
|
||||||
|
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||||
|
actions: SnapshotActions;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="section-label" style={{ marginBottom: 6 }}>{t("Runtime policy")}</div>
|
||||||
|
{/* What each column governs, so a change is made knowing what it reaches. */}
|
||||||
|
<ul style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.6, margin: "0 0 10px", paddingLeft: 18 }}>
|
||||||
|
<li>{t("Scope: “Workspace” refuses every call that is not made for a workspace, including agent sessions on tasks without one.")}</li>
|
||||||
|
<li>{t("Data policy: zero data retention refuses background requests, so it also switches background off.")}</li>
|
||||||
|
<li>{t("Background: lets requests from the request console run in the provider's background mode.")}</li>
|
||||||
|
<li>{t("Monthly limit: counts every model call this company makes through the provider in the calendar month (UTC); at the limit, calls are refused and agent work waits until the month resets.")}</li>
|
||||||
|
<li>{t("Context window: the size new sessions fit prompts, skills, history and tool results into; sessions already running keep theirs.")}</li>
|
||||||
|
</ul>
|
||||||
|
<div className="card" style={{ padding: 0, overflowX: "auto", marginBottom: 28 }}>
|
||||||
|
<div
|
||||||
|
className="table-row head"
|
||||||
|
style={{ gridTemplateColumns: POLICY_GRID, minWidth: 940 }}
|
||||||
|
>
|
||||||
|
<div>{t("Provider")}</div><div>{t("Scope")}</div><div>{t("Data policy")}</div><div>{t("Background")}</div><div>{t("Monthly limit")}</div><div>{t("Context window (tokens)")}</div><div>{t("Usage")}</div>
|
||||||
|
</div>
|
||||||
|
{providers.map((provider, index) => (
|
||||||
|
<ProviderPolicyRow
|
||||||
|
key={provider.id}
|
||||||
|
orgId={orgId}
|
||||||
|
provider={provider}
|
||||||
|
rateLimit={requireProviderRateLimit(provider, providerRateLimits)}
|
||||||
|
last={index === providers.length - 1}
|
||||||
|
actions={actions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderPolicyRow({
|
||||||
|
orgId,
|
||||||
|
provider,
|
||||||
|
rateLimit,
|
||||||
|
last,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
provider: ProviderRecord;
|
||||||
|
rateLimit: ProviderRateLimitStateRecord;
|
||||||
|
last: boolean;
|
||||||
|
actions: SnapshotActions;
|
||||||
|
}) {
|
||||||
|
const [dataPolicy, setDataPolicy] = useState(provider.dataPolicy);
|
||||||
|
const [backgroundEnabled, setBackgroundEnabled] = useState(provider.backgroundEnabled);
|
||||||
|
const [monthlyRequestLimit, setMonthlyRequestLimit] = useState(String(provider.monthlyRequestLimit));
|
||||||
|
const [contextWindowTokens, setContextWindowTokens] = useState(String(provider.contextWindowTokens));
|
||||||
|
const [workspaceScope, setWorkspaceScope] = useState<WorkspaceScope>(provider.workspaceScope === "workspace" ? "workspace" : "organization");
|
||||||
|
const [linkedNote, setLinkedNote] = useState<string | null>(null);
|
||||||
|
const [status, setStatus] = useState<{ kind: "saving" | "saved" } | { kind: "error"; message: string } | null>(null);
|
||||||
|
const savedScope = provider.workspaceScope === "workspace" ? "workspace" : "organization";
|
||||||
|
const dirty = dataPolicy !== provider.dataPolicy
|
||||||
|
|| backgroundEnabled !== provider.backgroundEnabled
|
||||||
|
|| Number(monthlyRequestLimit) !== provider.monthlyRequestLimit
|
||||||
|
|| Number(contextWindowTokens) !== provider.contextWindowTokens
|
||||||
|
|| workspaceScope !== savedScope;
|
||||||
|
const edited = <T,>(set: (value: T) => void) => (value: T) => { set(value); setStatus(null); };
|
||||||
|
|
||||||
|
const savePolicy = () => {
|
||||||
|
setStatus({ kind: "saving" });
|
||||||
|
actions.run(() => window.agentCompany.updateProviderPolicy({
|
||||||
|
orgId,
|
||||||
|
providerId: provider.id,
|
||||||
|
dataPolicy,
|
||||||
|
backgroundEnabled,
|
||||||
|
monthlyRequestLimit: Number(monthlyRequestLimit),
|
||||||
|
workspaceScope,
|
||||||
|
contextWindowTokens: Number(contextWindowTokens),
|
||||||
|
}))
|
||||||
|
.then((snapshot) => {
|
||||||
|
const saved = snapshot?.providers.find((item) => item.id === provider.id);
|
||||||
|
if (!saved) return setStatus(null);
|
||||||
|
// The fields show what the backend stored (025 reads back as 25, 26.9 as 26).
|
||||||
|
setMonthlyRequestLimit(String(saved.monthlyRequestLimit));
|
||||||
|
setContextWindowTokens(String(saved.contextWindowTokens));
|
||||||
|
setStatus({ kind: "saved" });
|
||||||
|
setLinkedNote(null);
|
||||||
|
})
|
||||||
|
.catch((failure: unknown) => setStatus({ kind: "error", message: failure instanceof Error ? failure.message : String(failure) }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDataPolicy = (value: ProviderRecord["dataPolicy"]) => {
|
||||||
|
edited(setDataPolicy)(value);
|
||||||
|
if (value === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION && backgroundEnabled) {
|
||||||
|
setBackgroundEnabled(false);
|
||||||
|
setLinkedNote(t("Zero data retention switched background off."));
|
||||||
|
} else setLinkedNote(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="table-row"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: POLICY_GRID,
|
||||||
|
minWidth: 940,
|
||||||
|
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||||
|
{linkedNote && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{linkedNote}</div>}
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
aria-label={t("{label} workspace scope", { label: provider.label })}
|
||||||
|
value={workspaceScope}
|
||||||
|
onChange={(event) => edited(setWorkspaceScope)(event.target.value === "workspace" ? "workspace" : "organization")}
|
||||||
|
>
|
||||||
|
<option value="organization">{t("Organization")}</option>
|
||||||
|
<option value="workspace">{t("Workspace")}</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
aria-label={t("{label} data policy", { label: provider.label })}
|
||||||
|
value={dataPolicy}
|
||||||
|
onChange={(event) => updateDataPolicy(event.target.value as ProviderRecord["dataPolicy"])}
|
||||||
|
>
|
||||||
|
<option value={PROVIDER_DATA_POLICY.STANDARD}>{t("Standard retention")}</option>
|
||||||
|
<option value={PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION}>{t("Zero data retention")}</option>
|
||||||
|
</select>
|
||||||
|
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
||||||
|
<input
|
||||||
|
aria-label={t("{label} background execution", { label: provider.label })}
|
||||||
|
type="checkbox"
|
||||||
|
checked={backgroundEnabled}
|
||||||
|
disabled={dataPolicy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION}
|
||||||
|
onChange={(event) => edited(setBackgroundEnabled)(event.target.checked)}
|
||||||
|
/>
|
||||||
|
{t("Allow")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
aria-label={t("{label} monthly request limit", { label: provider.label })}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={monthlyRequestLimit}
|
||||||
|
onChange={(event) => edited(setMonthlyRequestLimit)(event.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
aria-label={t("{label} context window", { label: provider.label })}
|
||||||
|
title={t("The model's context window. Runs keep prompts, skills, history, and tool results inside it.")}
|
||||||
|
type="number"
|
||||||
|
min={24000}
|
||||||
|
step={1000}
|
||||||
|
value={contextWindowTokens}
|
||||||
|
onChange={(event) => edited(setContextWindowTokens)(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div>
|
||||||
|
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{t("Resets {date} (UTC)", { date: rateLimit.windowEnd.slice(0, 10) })}</div>
|
||||||
|
<div className="row" style={{ gap: 6, marginTop: 4 }}>
|
||||||
|
<button type="button" className="button small" aria-label={t("Save {label} policy", { label: provider.label })} onClick={savePolicy} disabled={!dirty || status?.kind === "saving"}>
|
||||||
|
{t("Save")}
|
||||||
|
</button>
|
||||||
|
{dirty && status?.kind !== "saving" && <Pill tone="amber">{t("Unsaved")}</Pill>}
|
||||||
|
{!dirty && status?.kind === "saved" && <Pill tone="green">{t("Saved")}</Pill>}
|
||||||
|
</div>
|
||||||
|
{status?.kind === "error" && <div className="form-error" role="alert">{status.message}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireProviderRateLimit(
|
||||||
|
provider: ProviderRecord,
|
||||||
|
rateLimits: ProviderRateLimitStateRecord[],
|
||||||
|
): ProviderRateLimitStateRecord {
|
||||||
|
const rateLimit = rateLimits.find((item) => item.providerId === provider.id);
|
||||||
|
if (!rateLimit) throw new Error(`Provider rate limit state missing for ${provider.id}.`);
|
||||||
|
return rateLimit;
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ import type {
|
|||||||
ProviderRateLimitStateRecord,
|
ProviderRateLimitStateRecord,
|
||||||
ProviderRecord,
|
ProviderRecord,
|
||||||
} from "../../../../shared/domain";
|
} from "../../../../shared/domain";
|
||||||
import { PROVIDER_DATA_POLICY } from "../../../../shared/domain";
|
|
||||||
import { ProviderLogo } from "../../shared/ProviderLogo";
|
import { ProviderLogo } from "../../shared/ProviderLogo";
|
||||||
import { ModelPicker, ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog";
|
import { ModelPicker, ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog";
|
||||||
import { composeSecret } from "./provider-secret";
|
import { composeSecret } from "./provider-secret";
|
||||||
|
import { ProviderPolicyTable } from "./ProviderPolicyTable";
|
||||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||||
|
|
||||||
interface ProvidersPanelProps {
|
interface ProvidersPanelProps {
|
||||||
@@ -30,8 +30,6 @@ interface ProvidersPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PRICE_GRID = "minmax(150px, 1.4fr) 110px 110px 110px 92px";
|
const PRICE_GRID = "minmax(150px, 1.4fr) 110px 110px 110px 92px";
|
||||||
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 124px 92px";
|
|
||||||
type WorkspaceScope = "organization" | "workspace";
|
|
||||||
|
|
||||||
export function ProvidersPanel({ orgId, companyName, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
|
export function ProvidersPanel({ orgId, companyName, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
|
||||||
const [active, setActive] = useState<ProviderRecord | null>(null);
|
const [active, setActive] = useState<ProviderRecord | null>(null);
|
||||||
@@ -167,45 +165,6 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProviderPolicyTable({
|
|
||||||
orgId,
|
|
||||||
providers,
|
|
||||||
providerRateLimits,
|
|
||||||
actions,
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
providers: ProviderRecord[];
|
|
||||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
|
||||||
actions: SnapshotActions;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="section-label" style={{ marginBottom: 10 }}>{t("Runtime policy")}</div>
|
|
||||||
<div className="card" style={{ padding: 0, overflowX: "auto", marginBottom: 28 }}>
|
|
||||||
<div
|
|
||||||
className="table-row head"
|
|
||||||
style={{ gridTemplateColumns: POLICY_GRID, minWidth: 900 }}
|
|
||||||
>
|
|
||||||
<div>{t("Provider")}</div><div>{t("Scope")}</div><div>{t("Data policy")}</div><div>{t("Background")}</div><div>{t("Monthly limit")}</div><div>{t("Context window (tokens)")}</div><div>{t("Usage")}</div>
|
|
||||||
</div>
|
|
||||||
{providers.map((provider, index) => {
|
|
||||||
const rateLimit = requireProviderRateLimit(provider, providerRateLimits);
|
|
||||||
return (
|
|
||||||
<ProviderPolicyRow
|
|
||||||
key={provider.id}
|
|
||||||
orgId={orgId}
|
|
||||||
provider={provider}
|
|
||||||
rateLimit={rateLimit}
|
|
||||||
last={index === providers.length - 1}
|
|
||||||
actions={actions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Prices turn recorded token usage into spend, which agent and department budgets are checked against. */
|
/** Prices turn recorded token usage into spend, which agent and department budgets are checked against. */
|
||||||
function ProviderPricesTable({ orgId, providers, actions }: { orgId: string; providers: ProviderRecord[]; actions: SnapshotActions }) {
|
function ProviderPricesTable({ orgId, providers, actions }: { orgId: string; providers: ProviderRecord[]; actions: SnapshotActions }) {
|
||||||
return (
|
return (
|
||||||
@@ -251,110 +210,6 @@ function ProviderPricesRow({ orgId, provider, actions }: { orgId: string; provid
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProviderPolicyRow({
|
|
||||||
orgId,
|
|
||||||
provider,
|
|
||||||
rateLimit,
|
|
||||||
last,
|
|
||||||
actions,
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
provider: ProviderRecord;
|
|
||||||
rateLimit: ProviderRateLimitStateRecord;
|
|
||||||
last: boolean;
|
|
||||||
actions: SnapshotActions;
|
|
||||||
}) {
|
|
||||||
const [dataPolicy, setDataPolicy] = useState(provider.dataPolicy);
|
|
||||||
const [backgroundEnabled, setBackgroundEnabled] = useState(provider.backgroundEnabled);
|
|
||||||
const [monthlyRequestLimit, setMonthlyRequestLimit] = useState(String(provider.monthlyRequestLimit));
|
|
||||||
const [contextWindowTokens, setContextWindowTokens] = useState(String(provider.contextWindowTokens));
|
|
||||||
const [workspaceScope, setWorkspaceScope] = useState<WorkspaceScope>(provider.workspaceScope === "workspace" ? "workspace" : "organization");
|
|
||||||
|
|
||||||
const savePolicy = () => {
|
|
||||||
void actions.run(() => window.agentCompany.updateProviderPolicy({
|
|
||||||
orgId,
|
|
||||||
providerId: provider.id,
|
|
||||||
dataPolicy,
|
|
||||||
backgroundEnabled,
|
|
||||||
monthlyRequestLimit: Number(monthlyRequestLimit),
|
|
||||||
workspaceScope,
|
|
||||||
contextWindowTokens: Number(contextWindowTokens),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDataPolicy = (value: ProviderRecord["dataPolicy"]) => {
|
|
||||||
setDataPolicy(value);
|
|
||||||
if (value === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION) setBackgroundEnabled(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="table-row"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: POLICY_GRID,
|
|
||||||
minWidth: 900,
|
|
||||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ minWidth: 0 }}>
|
|
||||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
className="input"
|
|
||||||
aria-label={t("{label} workspace scope", { label: provider.label })}
|
|
||||||
value={workspaceScope}
|
|
||||||
onChange={(event) => setWorkspaceScope(event.target.value === "workspace" ? "workspace" : "organization")}
|
|
||||||
>
|
|
||||||
<option value="organization">{t("Organization")}</option>
|
|
||||||
<option value="workspace">{t("Workspace")}</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
className="input"
|
|
||||||
aria-label={t("{label} data policy", { label: provider.label })}
|
|
||||||
value={dataPolicy}
|
|
||||||
onChange={(event) => updateDataPolicy(event.target.value as ProviderRecord["dataPolicy"])}
|
|
||||||
>
|
|
||||||
<option value={PROVIDER_DATA_POLICY.STANDARD}>{t("Standard retention")}</option>
|
|
||||||
<option value={PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION}>{t("Zero data retention")}</option>
|
|
||||||
</select>
|
|
||||||
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
|
||||||
<input
|
|
||||||
aria-label={t("{label} background execution", { label: provider.label })}
|
|
||||||
type="checkbox"
|
|
||||||
checked={backgroundEnabled}
|
|
||||||
disabled={dataPolicy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION}
|
|
||||||
onChange={(event) => setBackgroundEnabled(event.target.checked)}
|
|
||||||
/>
|
|
||||||
{t("Allow")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
aria-label={t("{label} monthly request limit", { label: provider.label })}
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={monthlyRequestLimit}
|
|
||||||
onChange={(event) => setMonthlyRequestLimit(event.target.value)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
aria-label={t("{label} context window", { label: provider.label })}
|
|
||||||
title={t("The model's context window. Runs keep prompts, skills, history, and tool results inside it.")}
|
|
||||||
type="number"
|
|
||||||
min={24000}
|
|
||||||
step={1000}
|
|
||||||
value={contextWindowTokens}
|
|
||||||
onChange={(event) => setContextWindowTokens(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div>
|
|
||||||
<button type="button" className="button small" aria-label={t("Save {label} policy", { label: provider.label })} onClick={savePolicy}>
|
|
||||||
{t("Save")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProviderRow({
|
function ProviderRow({
|
||||||
provider,
|
provider,
|
||||||
authProfile,
|
authProfile,
|
||||||
@@ -459,15 +314,6 @@ function readableAuth(authType: ProviderRecord["authType"]): string {
|
|||||||
return authType;
|
return authType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireProviderRateLimit(
|
|
||||||
provider: ProviderRecord,
|
|
||||||
rateLimits: ProviderRateLimitStateRecord[],
|
|
||||||
): ProviderRateLimitStateRecord {
|
|
||||||
const rateLimit = rateLimits.find((item) => item.providerId === provider.id);
|
|
||||||
if (!rateLimit) throw new Error(`Provider rate limit state missing for ${provider.id}.`);
|
|
||||||
return rateLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireCatalogEntry(catalog: ProviderCatalogEntry[], providerKey: string): ProviderCatalogEntry {
|
function requireCatalogEntry(catalog: ProviderCatalogEntry[], providerKey: string): ProviderCatalogEntry {
|
||||||
const entry = catalog.find((candidate) => candidate.key === providerKey);
|
const entry = catalog.find((candidate) => candidate.key === providerKey);
|
||||||
if (!entry) throw new Error(`Provider ${providerKey} is not in the catalog.`);
|
if (!entry) throw new Error(`Provider ${providerKey} is not in the catalog.`);
|
||||||
|
|||||||
@@ -184,7 +184,10 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
|||||||
await expect(page.getByText(label, { exact: true })).toBeVisible();
|
await expect(page.getByText(label, { exact: true })).toBeVisible();
|
||||||
}
|
}
|
||||||
await page.getByLabel("OpenAI background execution").check();
|
await page.getByLabel("OpenAI background execution").check();
|
||||||
|
await expect(page.getByText("Unsaved", { exact: true })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
||||||
|
await expect(page.getByText("Saved", { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "Save OpenAI policy" })).toBeDisabled();
|
||||||
|
|
||||||
// The operator request console lives under Advanced.
|
// The operator request console lives under Advanced.
|
||||||
await page.getByRole("button", { name: "Advanced" }).click();
|
await page.getByRole("button", { name: "Advanced" }).click();
|
||||||
@@ -197,8 +200,11 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
|||||||
await page.getByRole("button", { name: "Providers" }).click();
|
await page.getByRole("button", { name: "Providers" }).click();
|
||||||
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
|
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
|
||||||
await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked();
|
await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked();
|
||||||
await page.getByLabel("OpenAI monthly request limit").fill("25");
|
await expect(page.getByText("Zero data retention switched background off.")).toBeVisible();
|
||||||
|
await page.getByLabel("OpenAI monthly request limit").fill("25.9");
|
||||||
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
||||||
|
await expect(page.getByText("Saved", { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByLabel("OpenAI monthly request limit")).toHaveValue("25");
|
||||||
await expect(page.getByText("0 / 25").first()).toBeVisible();
|
await expect(page.getByText("0 / 25").first()).toBeVisible();
|
||||||
|
|
||||||
// Changing only the default model asks for no key and keeps the sign-in.
|
// Changing only the default model asks for no key and keeps the sign-in.
|
||||||
|
|||||||
Reference in new issue
Block a user