fix(ui): restore provider-owned Codex pool inherit state (#585)

* fix(ui): restore provider-owned codex pool inherit state

* docs(pr): add before-after UI evidence

* refactor: remove dead hasProviderDefaults param and harden pool rendering

- Remove unused _hasProviderDefaults parameter from buildDraftRouting
  and routingDraftSignature, clean up all call sites and useMemo deps
- Filter deleted providers from selectedPoolProviderNames to prevent
  ghost entries when a saved pool member no longer exists
- Add symmetric backend test for inherit + non-nil provider defaults

---------

Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
Kai (Tam Nhu) Tranandviettranx authored and GitHub committed 2026-03-30 22:04:23 +07:00
1 parent 3fe0633d35
commit 287946bb9a
15 files changed
+545 -53

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

+467
View File
@@ -0,0 +1,467 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GoClaw Codex Pool UI Before / After</title>
<style>
:root {
--bg: #120905;
--panel: #1c100b;
--panel-2: #24150f;
--text: #f6eee8;
--muted: #c5afa0;
--line: rgba(255, 220, 196, 0.16);
--accent: #ff7b3d;
--bad: #ff5d5d;
--good: #49d18c;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(255, 123, 61, 0.16), transparent 28%),
radial-gradient(circle at top right, rgba(255, 183, 120, 0.08), transparent 22%),
linear-gradient(180deg, #100704 0%, var(--bg) 100%);
}
a {
color: inherit;
}
.wrap {
width: min(1500px, calc(100vw - 48px));
margin: 0 auto;
padding: 32px 0 56px;
}
.hero {
padding: 28px 28px 24px;
border: 1px solid var(--line);
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
box-shadow: var(--shadow);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid rgba(255, 123, 61, 0.28);
border-radius: 999px;
color: #ffd2ba;
background: rgba(255, 123, 61, 0.1);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 14px 0 12px;
font-size: clamp(30px, 5vw, 52px);
line-height: 1.02;
letter-spacing: -0.04em;
}
.hero p {
max-width: 980px;
margin: 0;
color: var(--muted);
font-size: 17px;
line-height: 1.6;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-top: 22px;
}
.meta-card {
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.03);
}
.meta-label {
color: #d9c1b2;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.meta-value {
margin-top: 8px;
font-size: 15px;
line-height: 1.45;
color: var(--text);
word-break: break-word;
}
.section {
margin-top: 28px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0.015));
box-shadow: var(--shadow);
}
.section h2 {
margin: 0;
font-size: 26px;
letter-spacing: -0.03em;
}
.section-intro {
margin: 10px 0 0;
color: var(--muted);
font-size: 15px;
line-height: 1.6;
}
.findings {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
.finding {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: var(--panel-2);
}
.finding strong {
display: block;
margin-bottom: 8px;
font-size: 14px;
letter-spacing: 0.01em;
}
.finding p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
.comparison {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 22px;
}
.shot {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel);
}
.shot-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.shot-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 700;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.tag.before {
color: #ffd3d3;
background: rgba(255, 93, 93, 0.14);
border: 1px solid rgba(255, 93, 93, 0.22);
}
.tag.after {
color: #d9ffea;
background: rgba(73, 209, 140, 0.14);
border: 1px solid rgba(73, 209, 140, 0.22);
}
.shot img {
display: block;
width: 100%;
height: auto;
background: #0b0502;
}
.caption {
padding: 14px 16px 18px;
color: var(--muted);
font-size: 14px;
line-height: 1.65;
}
.caption ul {
margin: 10px 0 0 18px;
padding: 0;
}
.caption li + li {
margin-top: 8px;
}
.foot {
margin-top: 28px;
padding: 18px 20px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
font-size: 14px;
line-height: 1.7;
}
code {
padding: 2px 6px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
color: #ffe8d8;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
font-size: 0.95em;
}
@media (max-width: 1100px) {
.meta-grid,
.comparison,
.findings {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.wrap {
width: min(100vw - 24px, 100%);
padding-top: 18px;
}
.hero,
.section {
padding: 18px;
border-radius: 18px;
}
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="eyebrow">UI Test Visual Report</div>
<h1>Provider-owned Codex pool UI before / after</h1>
<p>
This comparison uses two real local GoClaw dashboards seeded with the same fixture data.
The only difference is the code revision:
<code>main@b38a078e</code> versus <code>fix@faf98c73</code>.
The screenshots below isolate the two regressions fixed by the PR: hidden saved pool
membership on provider pages, and sticky custom routing state on agent pages.
</p>
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">Before Revision</div>
<div class="meta-value"><code>b38a078e</code><br />origin/main</div>
</div>
<div class="meta-card">
<div class="meta-label">After Revision</div>
<div class="meta-value"><code>faf98c73</code><br />PR branch</div>
</div>
<div class="meta-card">
<div class="meta-label">Fixture</div>
<div class="meta-value">
<code>codex-owner</code> owns pool membership<br />
<code>codex-disabled</code> is saved as a disabled member<br />
<code>solo-agent</code> uses <code>codex-solo</code> with no saved pool defaults
</div>
</div>
<div class="meta-card">
<div class="meta-label">Capture Method</div>
<div class="meta-value">
Real local UI via Playwright<br />
SQLite-backed gateways on separate ports
</div>
</div>
</div>
</section>
<section class="section">
<h2>Scenario 1: provider page hides saved non-ready members</h2>
<p class="section-intro">
The provider page should show saved pool membership even when an extra account is disabled
or not ready. Before the fix, the page derived membership only from quota-fetched ready
accounts, so the saved member disappeared entirely.
</p>
<div class="findings">
<div class="finding">
<strong>What was broken on main</strong>
<p>
The pool owner <code>codex-owner</code> had a saved extra member
<code>codex-disabled</code>, but the UI rendered <code>0 in pool</code>, an empty
account list, and no selectable extra account.
</p>
</div>
<div class="finding">
<strong>What the patch changes</strong>
<p>
The page now renders membership from the saved provider settings, while still limiting
quota lookups to ready accounts only. The disabled member remains visible and clearly
labeled as blocked.
</p>
</div>
</div>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Provider Detail</div>
<span class="tag before">Before</span>
</div>
<img src="./before-provider-overview.png" alt="Before fix: provider detail hides the saved disabled pool member." />
<div class="caption">
Main branch behavior:
<ul>
<li><code>Available Extra Accounts</code> is effectively empty.</li>
<li><code>Accounts in Pool</code> shows <code>0 in pool</code>.</li>
<li>The saved disabled member is invisible, so the provider-owned pool looks unsaved.</li>
</ul>
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Provider Detail</div>
<span class="tag after">After</span>
</div>
<img src="./after-provider-overview.png" alt="After fix: provider detail shows the saved disabled pool member and pool count." />
<div class="caption">
Fixed branch behavior:
<ul>
<li><code>Codex Disabled</code> is visible in <code>Available Extra Accounts</code>.</li>
<li><code>Accounts in Pool</code> correctly shows <code>2 in pool</code>.</li>
<li>The disabled member is still shown as saved, with blocked state clearly surfaced.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="section">
<h2>Scenario 2: agent page manufactured a sticky custom override</h2>
<p class="section-intro">
When an agent had no explicit routing override and the provider had no saved pool yet,
the UI should have represented that state as <em>inherit / use provider defaults</em>.
Before the fix, the draft model incorrectly materialized it as a saved custom
<code>primary_first + []</code> state.
</p>
<div class="findings">
<div class="finding">
<strong>What was broken on main</strong>
<p>
The agent page defaulted to <code>Custom For This Agent</code> and framed the agent as
if it had its own routing policy, even though there was no provider-owned pool and no
explicit agent override saved.
</p>
</div>
<div class="finding">
<strong>What the patch changes</strong>
<p>
The draft state now preserves a true inherit mode. The UI shows
<code>Use Provider Defaults</code> and clarifies that the agent stays on its primary
account until provider-owned pool members are later added.
</p>
</div>
</div>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Agent Codex Pool</div>
<span class="tag before">Before</span>
</div>
<img src="./before-agent-codex-pool.png" alt="Before fix: agent page defaults to Custom For This Agent even with no provider-owned pool." />
<div class="caption">
Main branch behavior:
<ul>
<li><code>Custom For This Agent</code> is selected by default.</li>
<li>The copy tells the user to configure provider members first before enabling pooled routing.</li>
<li>This makes the implicit default state look like an explicit agent override.</li>
</ul>
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Agent Codex Pool</div>
<span class="tag after">After</span>
</div>
<img src="./after-agent-codex-pool.png" alt="After fix: agent page defaults to Use Provider Defaults and explains the temporary primary-only behavior." />
<div class="caption">
Fixed branch behavior:
<ul>
<li><code>Use Provider Defaults</code> is selected.</li>
<li>The helper copy explains the temporary primary-only behavior without inventing a custom override.</li>
<li>The agent can stay in inherit mode now and automatically pick up provider pool members later.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="foot">
Generated from a real local browser session using two SQLite-backed GoClaw instances.
The frontends were served from separate Vite dev servers, authenticated with
<code>system</code> + a local owner token, and seeded through the HTTP API with the same
provider / agent records before capture.
</section>
</main>
</body>
</html>
+32
View File
@@ -113,6 +113,38 @@ func TestResolveEffectiveChatGPTOAuthRoutingUsesProviderDefaultsWhenAgentUnset(t
}
}
func TestResolveEffectiveChatGPTOAuthRoutingAllowsInheritWithoutSavedProviderPool(t *testing.T) {
override := &ChatGPTOAuthRoutingConfig{
OverrideMode: ChatGPTOAuthOverrideInherit,
}
got := ResolveEffectiveChatGPTOAuthRouting(nil, override)
if got != nil {
t.Fatalf("ResolveEffectiveChatGPTOAuthRouting() = %#v, want nil", got)
}
}
func TestResolveEffectiveChatGPTOAuthRoutingInheritForwardsProviderDefaults(t *testing.T) {
defaults := &ChatGPTOAuthRoutingConfig{
Strategy: ChatGPTOAuthStrategyRoundRobin,
ExtraProviderNames: []string{"codex-work"},
}
override := &ChatGPTOAuthRoutingConfig{
OverrideMode: ChatGPTOAuthOverrideInherit,
}
got := ResolveEffectiveChatGPTOAuthRouting(defaults, override)
if got == nil {
t.Fatal("ResolveEffectiveChatGPTOAuthRouting() = nil, want config forwarding provider defaults")
}
if got.Strategy != ChatGPTOAuthStrategyRoundRobin {
t.Fatalf("Strategy = %q, want %q", got.Strategy, ChatGPTOAuthStrategyRoundRobin)
}
if !reflect.DeepEqual(got.ExtraProviderNames, []string{"codex-work"}) {
t.Fatalf("ExtraProviderNames = %#v, want provider defaults", got.ExtraProviderNames)
}
}
func TestResolveEffectiveChatGPTOAuthRoutingAllowsCustomSingleAccountToDisableDefaults(t *testing.T) {
defaults := &ChatGPTOAuthRoutingConfig{
Strategy: ChatGPTOAuthStrategyRoundRobin,
+2 -2
View File
@@ -147,7 +147,7 @@
"manageAction": "Open Pool",
"singleAccountHint": "Configure another named OpenAI Codex OAuth provider on the Providers page to create a provider-owned pool.",
"membershipManagedAtProvider": "Pool members are managed on provider {{provider}}. Change membership there, then come back here to tune routing behavior.",
"membershipConfigureProviderFirst": "No provider-owned pool is saved on {{provider}} yet. Configure pool members there before enabling pooled routing for this agent.",
"membershipConfigureProviderFirst": "No provider-owned pool is saved on {{provider}} yet. This agent will stay on the primary account until pool members are added there.",
"readySummary": "{{ready}} of {{total}} extra accounts are ready.",
"preferredNeedsAttention": "The primary account needs attention before this agent can use it reliably.",
"loadingAccounts": "Checking OpenAI Codex OAuth account status...",
@@ -215,7 +215,7 @@
"label": "Agent Routing Mode",
"inherit": "Use Provider Defaults",
"custom": "Custom For This Agent",
"noProviderDefault": "No provider-owned pool is saved yet. Configure reusable pool members on the Providers page first, or keep this agent on its primary account.",
"noProviderDefault": "No provider-owned pool is saved yet. Following provider defaults will keep this agent on its primary account for now and automatically pick up pool members added later.",
"providerDefaultSummary": "{{count}} accounts in provider default",
"summaryInherited": "This agent currently follows the provider default pool.",
"summaryCustom": "This agent currently saves its own routing override for the provider-owned pool."
+2 -2
View File
@@ -147,7 +147,7 @@
"manageAction": "Mở Pool",
"singleAccountHint": "Hãy cấu hình thêm một provider OpenAI Codex OAuth khác trên trang Providers để tạo pool do provider quản lý.",
"membershipManagedAtProvider": "Thành viên pool được quản lý tại provider {{provider}}. Hãy đổi membership ở đó rồi quay lại đây để chỉnh hành vi routing.",
"membershipConfigureProviderFirst": "Provider {{provider}} chưa có provider-owned pool. Hãy cấu hình pool members ở đó trước khi bật pooled routing cho agent này.",
"membershipConfigureProviderFirst": "Provider {{provider}} chưa có provider-owned pool. Agent này sẽ chỉ dùng tài khoản chính cho tới khi pool members được thêm ở đó.",
"readySummary": "{{ready}} / {{total}} tài khoản phụ đã sẵn sàng.",
"preferredNeedsAttention": "Tài khoản chính cần được xử lý trước khi agent dùng ổn định.",
"loadingAccounts": "Đang kiểm tra trạng thái tài khoản OpenAI Codex OAuth...",
@@ -215,7 +215,7 @@
"label": "Chế độ routing của agent",
"inherit": "Dùng mặc định từ provider",
"custom": "Tùy chỉnh riêng cho agent",
"noProviderDefault": "Chưa có provider-owned pool nào được lưu. Hãy cấu hình reusable pool members ở trang Providers trước, hoặc giữ agent này chỉ dùng tài khoản chính.",
"noProviderDefault": "Chưa có provider-owned pool nào được lưu. Chế độ dùng mặc định của provider sẽ giữ agent này ở tài khoản chính lúc này và tự nhận pool members khi provider thêm sau.",
"providerDefaultSummary": "Mặc định từ provider có {{count}} tài khoản",
"summaryInherited": "Agent này hiện đang dùng pool mặc định của provider.",
"summaryCustom": "Agent này hiện đang lưu routing override riêng cho provider-owned pool."
+2 -2
View File
@@ -147,7 +147,7 @@
"manageAction": "打开池管理",
"singleAccountHint": "请在 Providers 页面配置另一个具名 OpenAI Codex OAuth provider,以创建由 provider 管理的账户池。",
"membershipManagedAtProvider": "池成员由 provider {{provider}} 管理。请先在那里修改成员关系,再回到这里调整路由行为。",
"membershipConfigureProviderFirst": "provider {{provider}} 还没有保存 provider-owned pool。请先在那里配置池成员,再为这个 agent 启用池化路由。",
"membershipConfigureProviderFirst": "provider {{provider}} 还没有保存 provider-owned pool。在那里添加池成员之前,这个 agent 会先只使用主账户。",
"readySummary": "{{ready}} / {{total}} 个额外账户已就绪。",
"preferredNeedsAttention": "主账户需要先处理,agent 才能稳定使用它。",
"loadingAccounts": "正在检查 OpenAI Codex OAuth 账户状态...",
@@ -215,7 +215,7 @@
"label": "Agent 路由模式",
"inherit": "使用 Provider 默认值",
"custom": "仅此 Agent 自定义",
"noProviderDefault": "还没有保存 provider-owned pool。请先去 Providers 页面配置可复用的池成员,或者继续让这个 agent 只使用主账户。",
"noProviderDefault": "还没有保存 provider-owned pool。跟随 provider 默认状态时,这个 agent 现在会先停留在主账户,并在以后自动接收新增的池成员。",
"providerDefaultSummary": "Provider 默认池包含 {{count}} 个账户",
"summaryInherited": "这个 agent 当前正在使用 provider 默认账户池。",
"summaryCustom": "这个 agent 当前保存了针对 provider-owned pool 的路由覆盖配置。"
@@ -21,6 +21,7 @@ import {
buildAgentOtherConfigWithChatGPTOAuthRouting,
normalizeChatGPTOAuthRouting,
} from "./agent-display-utils";
import { buildDraftRouting } from "./codex-pool-routing-draft-utils";
interface AgentAdvancedDialogProps {
open: boolean;
@@ -39,17 +40,10 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
const deriveState = (a: AgentData) => {
const otherObj = (a.other_config ?? {}) as Record<string, unknown>;
const routing = normalizeChatGPTOAuthRouting(a.other_config);
const draftRouting = buildDraftRouting(routing);
return {
thinkingLevel: typeof otherObj.thinking_level === "string" ? otherObj.thinking_level : "off",
chatgptRouting: {
override_mode: routing.isExplicit
? routing.overrideMode
: providerDefaults
? "inherit"
: "custom",
strategy: routing.strategy,
extra_provider_names: routing.extraProviderNames,
} as ChatGPTOAuthRoutingConfig,
chatgptRouting: draftRouting,
wsSharing: (otherObj.workspace_sharing ?? {}) as WorkspaceSharingConfig,
comp: a.compaction_config ?? {},
pruneEnabled: a.context_pruning?.mode !== "off",
@@ -87,12 +87,12 @@ export function AgentCodexPoolPage() {
[agent?.provider, currentProvider?.settings, savedRouting],
);
const savedDraftRouting = useMemo(
() => buildDraftRouting(savedRouting, Boolean(providerDefaults)),
[providerDefaults, savedRouting],
() => buildDraftRouting(savedRouting),
[savedRouting],
);
const savedDraftSignature = useMemo(
() => routingDraftSignature(savedDraftRouting, Boolean(providerDefaults)),
[providerDefaults, savedDraftRouting],
() => routingDraftSignature(savedDraftRouting),
[savedDraftRouting],
);
const [routing, setRouting] = useState<ChatGPTOAuthRoutingConfig>(savedDraftRouting);
@@ -101,8 +101,8 @@ export function AgentCodexPoolPage() {
const savedDraftSignatureRef = useRef(savedDraftSignature);
const draftSignature = useMemo(
() => routingDraftSignature(routing, Boolean(providerDefaults)),
[providerDefaults, routing],
() => routingDraftSignature(routing),
[routing],
);
useEffect(() => {
@@ -6,7 +6,6 @@ import {
export function buildDraftRouting(
savedRouting: NormalizedChatGPTOAuthRouting,
hasProviderDefaults: boolean,
): ChatGPTOAuthRoutingConfig {
if (savedRouting.isExplicit) {
return {
@@ -16,16 +15,8 @@ export function buildDraftRouting(
};
}
if (hasProviderDefaults) {
return {
override_mode: "inherit",
strategy: "primary_first",
extra_provider_names: [],
};
}
return {
override_mode: "custom",
override_mode: "inherit",
strategy: "primary_first",
extra_provider_names: [],
};
@@ -33,10 +24,9 @@ export function buildDraftRouting(
export function routingDraftSignature(
routing: ChatGPTOAuthRoutingConfig,
hasProviderDefaults: boolean,
): string {
const normalized = normalizeChatGPTOAuthRoutingInput(routing);
if (normalized.overrideMode === "inherit" && hasProviderDefaults) {
if (normalized.overrideMode === "inherit") {
return JSON.stringify({ override_mode: "inherit" });
}
return JSON.stringify({
@@ -43,7 +43,7 @@ interface MembershipSectionProps {
membershipManagedByLabel?: string;
currentProvider: string;
selectedEntries: SelectedEntry[];
readyExtraProviders: ProviderData[];
selectableExtraProviders: ProviderData[];
selectedExtras: Set<string>;
quotaByName?: Map<string, ChatGPTOAuthProviderQuota>;
canEditMembership: boolean;
@@ -57,7 +57,7 @@ export function MembershipSection({
membershipManagedByLabel,
currentProvider,
selectedEntries,
readyExtraProviders,
selectableExtraProviders,
selectedExtras,
quotaByName,
canEditMembership,
@@ -91,9 +91,9 @@ export function MembershipSection({
<div className="rounded-lg border border-dashed px-3 py-3 text-sm text-muted-foreground">
{t("chatgptOAuthRouting.loadingAccounts")}
</div>
) : readyExtraProviders.length > 0 ? (
) : selectableExtraProviders.length > 0 ? (
<div className="grid gap-2 xl:grid-cols-2">
{readyExtraProviders.map((provider) => {
{selectableExtraProviders.map((provider) => {
const selected = selectedExtras.has(provider.name);
const failureKind = getQuotaFailureKind(quotaByName?.get(provider.name));
return (
@@ -103,13 +103,15 @@ export function ChatGPTOAuthRoutingSection({
if (!currentOAuthProvider) return null;
const allExtraProviders = oauthProviders.filter((p) => p.name !== currentProvider);
const readyExtraProviders = allExtraProviders.filter(
(p) => getAvailabilityFromMap(p, statusByName) === "ready",
const selectedExtras = new Set(value.extra_provider_names ?? []);
const selectableExtraProviders = allExtraProviders.filter(
(p) =>
selectedExtras.has(p.name) ||
getAvailabilityFromMap(p, statusByName) === "ready",
);
const mode = value.override_mode === "inherit" ? "inherit" : "custom";
const providerDefaultsAvailable =
defaultRouting != null && defaultRouting.extraProviderNames.length > 0;
const selectedExtras = new Set(value.extra_provider_names ?? []);
const selectedEntries = entries.map((entry) => ({
...entry,
@@ -214,7 +216,7 @@ export function ChatGPTOAuthRoutingSection({
type="button"
variant={mode === "inherit" ? "default" : "outline"}
onClick={() => setMode("inherit")}
disabled={!canManageProviders || !providerDefaultsAvailable}
disabled={!canManageProviders}
className="h-9 [@media(max-height:760px)]:h-8"
>
{t("chatgptOAuthRouting.mode.inherit")}
@@ -277,7 +279,7 @@ export function ChatGPTOAuthRoutingSection({
membershipManagedByLabel={membershipManagedByLabel}
currentProvider={currentProvider}
selectedEntries={selectedEntries}
readyExtraProviders={readyExtraProviders}
selectableExtraProviders={selectableExtraProviders}
selectedExtras={selectedExtras}
quotaByName={quotaByName}
canEditMembership={canEditMembership}
@@ -171,6 +171,19 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
strategy: initialRouting?.strategy ?? "primary_first",
extra_provider_names: initialRouting?.extraProviderNames ?? [],
});
const selectedPoolProviderNames = useMemo(
() =>
canEditPoolRouting
? Array.from(
new Set(
[provider.name, ...(poolRouting.extra_provider_names ?? [])]
.filter(Boolean)
.filter((name) => name === provider.name || providerByName.has(name)),
),
)
: [provider.name],
[canEditPoolRouting, poolRouting.extra_provider_names, provider.name, providerByName],
);
const initEmb = getEmbeddingSettings(provider.settings);
const [embEnabled, setEmbEnabled] = useState(initEmb?.enabled ?? false);
@@ -253,13 +266,9 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
const quotaProviderNames = useMemo(
() => {
if (!isOAuth) return [];
const candidateNames = [
provider.name,
...(canEditPoolRouting ? poolRouting.extra_provider_names ?? [] : []),
];
return Array.from(
new Set(
candidateNames.filter((providerName) => {
selectedPoolProviderNames.filter((providerName) => {
if (!providerName) return false;
const item = providerByName.get(providerName);
return (
@@ -270,11 +279,9 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
);
},
[
canEditPoolRouting,
isOAuth,
poolRouting.extra_provider_names,
provider.name,
providerByName,
selectedPoolProviderNames,
statusByName,
],
);
@@ -285,7 +292,7 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
} = useChatGPTOAuthProviderQuotas(quotaProviderNames, isOAuth);
const poolEntries = useMemo<CodexPoolEntry[]>(() => {
if (!canEditPoolRouting) return [];
return quotaProviderNames.map((providerName) => {
return selectedPoolProviderNames.map((providerName) => {
const item = providerByName.get(providerName);
return {
name: providerName,
@@ -305,10 +312,10 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
quota: quotaByName.get(providerName),
};
});
}, [canEditPoolRouting, provider.name, providerByName, quotaByName, quotaProviderNames, statusByName]);
}, [canEditPoolRouting, provider.name, providerByName, quotaByName, selectedPoolProviderNames, statusByName]);
// Provider-scoped pool activity (only for pool owners)
const isPoolOwner = canEditPoolRouting && poolEntries.length > 0;
const isPoolOwner = canEditPoolRouting && selectedPoolProviderNames.length > 1;
const {
data: poolActivity,
isFetching: poolActivityFetching,