fix(api): align management requests and configuration with v8

- Use native v8 routes for credentials, models, OAuth, and API calls
- Remove implicit legacy endpoint and response transformations
- Serialize OAuth exclusion updates while preserving other providers
- Send YAML as raw text and preserve explicit null request bodies
- Update mocks and regression tests, including migrated Claude aliases
This commit is contained in:
lzt404 committed 2026-10-04 17:31:52 +08:00
1 parent 245319be2d
commit 0e7437493d
32 files changed
+383 -455

No files matched your search

+84 -1
View File
@@ -61,11 +61,20 @@ pub(crate) struct ManagementRequest {
method: String,
path: String,
query: Option<HashMap<String, String>>,
#[serde(default, deserialize_with = "deserialize_management_body")]
body: Option<serde_json::Value>,
#[serde(rename = "timeoutMs")]
timeout_ms: Option<u64>,
}
fn deserialize_management_body<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
// An explicit v8 null is a JSON value, not an omitted request body.
serde_json::Value::deserialize(deserializer).map(Some)
}
#[tauri::command]
pub(crate) async fn management_request(
gui_config_state: tauri::State<'_, GuiConfigState>,
@@ -96,7 +105,7 @@ pub(crate) async fn management_request(
builder = builder.query(&query);
}
if let Some(body) = request.body {
builder = builder.json(&body);
builder = management_request_body(builder, path, body)?;
}
let response = builder
@@ -106,6 +115,21 @@ pub(crate) async fn management_request(
read_management_value(response).await
}
fn management_request_body(
builder: reqwest::RequestBuilder,
path: &str,
body: serde_json::Value,
) -> Result<reqwest::RequestBuilder, String> {
if path.trim_start_matches('/') == "config.yaml" {
let yaml = body.as_str().ok_or("YAML configuration must be text")?;
Ok(builder
.header("Content-Type", "application/yaml")
.body(yaml.to_owned()))
} else {
Ok(builder.json(&body))
}
}
#[tauri::command]
pub(crate) async fn upload_auth_file(
gui_config_state: tauri::State<'_, GuiConfigState>,
@@ -507,6 +531,65 @@ mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn v8_yaml_body_is_raw_text_and_config_values_remain_json() {
let client = reqwest::Client::new();
let yaml = "config-version: 8\naccess: {api-keys: [test-client]}\n";
let request = management_request_body(
client.put("http://127.0.0.1/v8/management/config.yaml"),
"/config.yaml",
serde_json::json!(yaml),
)
.unwrap()
.build()
.unwrap();
assert_eq!(request.headers()["Content-Type"], "application/yaml");
assert_eq!(request.body().unwrap().as_bytes().unwrap(), yaml.as_bytes());
for value in [
serde_json::Value::Null,
serde_json::json!(false),
serde_json::json!(0),
serde_json::json!([]),
serde_json::json!({}),
serde_json::json!("direct"),
] {
let request = management_request_body(
client.put("http://127.0.0.1/v8/management/config/requests/proxy-url"),
"/config/requests/proxy-url",
value.clone(),
)
.unwrap()
.build()
.unwrap();
assert_eq!(request.headers()["Content-Type"], "application/json");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(request.body().unwrap().as_bytes().unwrap()).unwrap(),
value,
);
}
assert!(management_request_body(
client.put("http://127.0.0.1/v8/management/config.yaml"),
"/config.yaml",
serde_json::json!({}),
)
.is_err());
}
#[test]
fn v8_requests_distinguish_explicit_null_from_an_absent_body() {
let absent: ManagementRequest = serde_json::from_value(serde_json::json!({
"method": "GET", "path": "/config",
}))
.unwrap();
let explicit: ManagementRequest = serde_json::from_value(serde_json::json!({
"method": "PUT", "path": "/config/requests/payload", "body": null,
}))
.unwrap();
assert!(absent.body.is_none());
assert_eq!(explicit.body, Some(serde_json::Value::Null));
}
#[test]
fn plugin_oauth_preserves_literal_provider_ids_without_builtin_aliases() {
for provider in [
+1 -1
View File
@@ -25,7 +25,7 @@ export function AuthFileModelsDialog({ name, onClose }: AuthFileModelsDialogProp
setLoading(true);
setModels([]);
setError('');
void managementApi.get('/auth-files/models', { name })
void managementApi.get('/credentials/models', { name })
.then((payload) => { if (!cancelled) setModels(oauthModelsFromPayload(payload)); })
.catch((requestError: unknown) => { if (!cancelled) setError(String(requestError)); })
.finally(() => { if (!cancelled) setLoading(false); });
+1 -1
View File
@@ -56,7 +56,7 @@ export function AuthFileSettingsDialog({ name, onClose, onSaved }: {
}).catch((reason: unknown) => {
if (active) setError(reason instanceof Error ? reason.message : String(reason));
}).finally(() => { if (active) setLoading(false); });
void managementApi.get('/auth-files/models', { name }).then((payload) => {
void managementApi.get('/credentials/models', { name }).then((payload) => {
if (active) setModels(oauthModelsFromPayload(payload));
}).catch((reason: unknown) => {
if (active) setCatalogError(reason instanceof Error ? reason.message : String(reason));
+2 -45
View File
@@ -81,20 +81,6 @@ function groupProviderRecords(section: ProviderSection, records: JsonObject[]):
});
}
function flattenProviderGroups(section: ProviderSection, groups: JsonObject[]): JsonObject[] {
return groups.flatMap((group) => {
const keys = asArray(group.keys).map(asObject);
if (section === 'openai-compatibility') {
const record: JsonObject = clone(group);
delete record.keys;
if (keys.length > 0) record['api-key-entries'] = clone(keys);
return [record];
}
const shared = Object.fromEntries(Object.entries(group).filter(([field]) => field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)));
return keys.map((key) => clone({ ...shared, ...Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null)) }));
});
}
const isoHoursAgo = (hours: number) => new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
const localHourKey = (date: Date) => [
date.getFullYear(),
@@ -738,21 +724,6 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
return { status: 'ok', 'config-version': 8 };
}
}
if (PROVIDER_SECTIONS.some((section) => path === `/${section}`)) {
const section = path.slice(1) as ProviderSection;
if (method === 'GET') return { [section]: flattenProviderGroups(section, state.providerConfig[section]) };
if (method === 'PUT') {
state.providerConfig[section] = groupProviderRecords(section, asArray(body).map(asObject));
return { [section]: flattenProviderGroups(section, state.providerConfig[section]) };
}
if (method === 'PATCH' && section === 'openai-compatibility') {
const patch = asObject(body);
const index = readNumber(patch.index, -1);
const current = flattenProviderGroups(section, state.providerConfig[section])[index];
if (current) state.providerConfig[section][index] = groupProviderRecords(section, [{ ...current, ...asObject(patch.value) }])[0];
return { [section]: flattenProviderGroups(section, state.providerConfig[section]) };
}
}
if (method === 'GET' && path === '/credentials') {
return { files: clone(state.authFiles), observed_at: new Date().toISOString() };
}
@@ -765,24 +736,10 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
if (method === 'GET' && path === '/config/oauth/excluded-models') {
return clone(state.oauthExcludedModels);
}
if (path.startsWith('/config/oauth/excluded-models/')) {
const provider = decodeURIComponent(path.slice('/config/oauth/excluded-models/'.length));
if (method === 'PUT') state.oauthExcludedModels[provider] = asArray(body).map(String);
if (method === 'DELETE') delete state.oauthExcludedModels[provider];
if (method === 'PUT' && path === '/config/oauth/excluded-models') {
state.oauthExcludedModels = clone(asObject(body)) as Record<string, string[]>;
return { status: 'ok', 'config-version': 8 };
}
if (method === 'GET' && path === '/oauth-excluded-models') {
return { 'oauth-excluded-models': clone(state.oauthExcludedModels) };
}
if (method === 'PATCH' && path === '/oauth-excluded-models') {
const patch = asObject(body);
state.oauthExcludedModels[readString(patch.provider)] = asArray(patch.models).map(String);
return { 'oauth-excluded-models': clone(state.oauthExcludedModels) };
}
if (method === 'DELETE' && path === '/oauth-excluded-models') {
delete state.oauthExcludedModels[readString(query.provider)];
return null;
}
if (method === 'PATCH' && (path === '/credentials/fields' || path === '/credentials/status')) {
const patch = asObject(body);
const file = findAuthFile(state, readString(patch.name));
+2 -2
View File
@@ -172,7 +172,7 @@ export function AuthFileManagementPage() {
if (showLoading) setLoading(true);
setError('');
try {
const payload = await managementApi.get('/auth-files');
const payload = await managementApi.get('/credentials');
if (!mountedRef.current || requestId !== fileRequestRef.current) return;
const nextFiles = dedupeAuthFiles(responseList(payload, 'files'));
setFileSnapshot({ files: nextFiles, receivedAtMs: Date.now(), observedAt: readString(payload, 'observed_at') });
@@ -414,7 +414,7 @@ export function AuthFileManagementPage() {
setBusy(true);
setError('');
try {
await managementApi.delete('/auth-files', { query: { name } });
await managementApi.delete('/credentials', { query: { name } });
showNotice({ key: 'authFiles.deleted' });
await loadFiles();
} catch (requestError) {
+1 -1
View File
@@ -206,7 +206,7 @@ export function EasyModePage({
const refreshSourceStatus = useCallback(async () => {
try {
const authFilesPayload = await managementApi.get("/auth-files");
const authFilesPayload = await managementApi.get("/credentials");
const files = responseList(authFilesPayload, "files");
setAuthFiles(files);
+4 -4
View File
@@ -262,7 +262,7 @@ export function OAuthLoginPage() {
);
const captureCredentialSnapshot = useCallback(async (provider: OAuthProviderId) => {
const payload = await managementApi.get('/auth-files');
const payload = await managementApi.get('/credentials');
credentialSnapshots.current[provider] = snapshotAuthFiles(responseList(payload, 'files'));
}, []);
@@ -271,9 +271,9 @@ export function OAuthLoginPage() {
delete credentialSnapshots.current[provider];
if (!before) return;
const payload = await managementApi.get('/auth-files');
const payload = await managementApi.get('/credentials');
const names = changedOAuthAuthFileNames(before, responseList(payload, 'files'), provider);
await Promise.all(names.map((name) => managementApi.patch('/auth-files/fields', {
await Promise.all(names.map((name) => managementApi.patch('/credentials/fields', {
name,
priority: 0,
})));
@@ -417,7 +417,7 @@ export function OAuthLoginPage() {
updateProviderState(provider, { refreshing: true });
try {
if (currentState) {
await managementApi.delete('/oauth-session', { query: { state: currentState } });
await managementApi.delete('/oauth/session', { query: { state: currentState } });
}
} catch (error) {
if (provider === 'devin') {
+1 -1
View File
@@ -63,7 +63,7 @@ export function QuotaPage() {
setLoading(true);
setError('');
try {
const payload = await managementApi.get('/auth-files');
const payload = await managementApi.get('/credentials');
const allFiles = dedupeAuthFiles(responseList(payload, 'files'));
const nextFiles = allFiles.filter((file) => !readBoolean(file, 'disabled') && providerForFile(file));
setFiles(nextFiles);
+4 -28
View File
@@ -91,6 +91,7 @@
}
.usage-request-log .usage-events-table {
min-width: 100%;
font-size: 12px;
font-variant-numeric: tabular-nums;
}
@@ -119,8 +120,8 @@
}
.usage-request-log .usage-events-table td {
height: 76px;
padding: 12px 8px;
height: 68px;
padding: 9px 8px;
color: var(--usage-log-text);
line-height: 1.45;
}
@@ -161,38 +162,13 @@
}
.usage-request-log .usage-event-source {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.usage-request-log .usage-event-source > span:last-child {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.usage-request-log .usage-event-source-icon {
display: inline-flex;
width: 25px;
height: 25px;
flex: 0 0 25px;
align-items: center;
justify-content: center;
border: 3px solid var(--usage-log-surface);
border-radius: 50%;
background: linear-gradient(145deg, #a5a1ff, #6557f4);
color: #fff;
box-shadow: 0 0 0 1px var(--usage-log-line);
}
.usage-request-log .usage-event-source-icon svg {
width: 13px;
height: 13px;
}
.usage-request-log .usage-event-metrics {
display: flex;
align-items: center;
+5 -50
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3Cog, Database, DatabaseZap, Download, RotateCcw, Terminal, TriangleAlert, X } from 'lucide-react';
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3Cog, Database, DatabaseZap, Download, RotateCcw, TriangleAlert, X } from 'lucide-react';
import { getCurrentLocale, useI18n } from '../i18n';
import type { MessageKey } from '../i18n/resources';
import { formatCacheReadRate, formatGenerationSpeed } from '../services/usageMetrics';
@@ -62,16 +62,11 @@ type EventColumnKey =
| 'provider'
| 'source'
| 'key'
| 'input'
| 'output'
| 'reasoning'
| 'cache'
| 'total'
| 'result'
| 'latency'
| 'ttft'
| 'speed'
| 'cacheRate'
| 'effort'
| 'request';
@@ -96,17 +91,12 @@ const EVENT_COLUMNS: readonly EventColumnDef[] = [
{ key: 'total', labelKey: 'usage.column.tokens', defaultWidth: 120, minWidth: 112, align: 'left' },
{ key: 'cache', labelKey: 'usage.column.cache', defaultWidth: 104, minWidth: 88, align: 'left' },
{ key: 'provider', labelKey: 'usage.column.provider', defaultWidth: 108, minWidth: 88, align: 'left' },
{ key: 'input', labelKey: 'usage.column.input', defaultWidth: 76, minWidth: 60, align: 'left' },
{ key: 'output', labelKey: 'usage.column.output', defaultWidth: 76, minWidth: 60, align: 'left' },
{ key: 'reasoning', labelKey: 'usage.column.reasoning', defaultWidth: 76, minWidth: 60, align: 'left' },
{ key: 'cacheRate', labelKey: 'usage.column.cacheRate', defaultWidth: 80, minWidth: 68, align: 'left' },
{ key: 'ttft', labelKey: 'usage.column.ttft', defaultWidth: 92, minWidth: 76, align: 'left' },
] as const;
const DEFAULT_EVENT_VISIBLE_COLUMNS: readonly EventColumnKey[] = [
'time', 'key', 'source', 'model', 'effort', 'result', 'request', 'latency', 'speed', 'total', 'cache', 'provider',
];
const LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS: readonly EventColumnKey[] = [
const LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS: readonly string[] = [
'time', 'model', 'input', 'output', 'cache', 'cacheRate', 'total', 'speed', 'ttft', 'latency', 'result', 'provider', 'source',
];
@@ -115,7 +105,6 @@ const LEGACY_EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v1'
const PREVIOUS_EVENT_COLUMN_WIDTHS: Record<EventColumnKey, number> = {
time: 105, key: 150, source: 205, model: 150, effort: 100, result: 95,
request: 145, latency: 125, speed: 110, total: 145, cache: 135, provider: 135,
input: 90, output: 90, reasoning: 90, cacheRate: 95, ttft: 110,
};
const EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v3';
const LEGACY_EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v2';
@@ -140,8 +129,8 @@ const getInitialVisibleColumns = (): EventColumnKey[] => {
});
if (savedKeys.length > 0) {
const wasLegacyDefault = currentRaw === null
&& savedKeys.length === LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.length
&& LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => savedKeys.includes(key));
&& parsed.length === LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.length
&& LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => parsed.includes(key));
return wasLegacyDefault ? [...DEFAULT_EVENT_VISIBLE_COLUMNS] : savedKeys;
}
}
@@ -330,10 +319,7 @@ function UsageEventCell({
case 'source':
return (
<td className="usage-td-source align-left" title={record.source_display || record.source || undefined}>
<div className="usage-event-source">
<span className="usage-event-source-icon" aria-hidden="true"><Terminal size={13} /></span>
<span>{record.source_display || record.source || '—'}</span>
</div>
<span className="usage-event-source">{record.source_display || record.source || '—'}</span>
</td>
);
case 'key':
@@ -343,24 +329,6 @@ function UsageEventCell({
<small title={record.api_key_remark}>{record.api_key_remark || noRemarkLabel}</small>
</td>
);
case 'input':
return (
<td className="usage-td-token align-left" title={`${record.tokens.input_tokens.toLocaleString()} tokens`}>
{compactNumber(record.tokens.input_tokens)}
</td>
);
case 'output':
return (
<td className="usage-td-token align-left" title={`${record.tokens.output_tokens.toLocaleString()} tokens`}>
{compactNumber(record.tokens.output_tokens)}
</td>
);
case 'reasoning':
return (
<td className="usage-td-token align-left" title={`${record.tokens.reasoning_tokens.toLocaleString()} tokens`}>
{compactNumber(record.tokens.reasoning_tokens)}
</td>
);
case 'cache':
return (
<td
@@ -376,13 +344,6 @@ function UsageEventCell({
<div className="usage-event-metrics"><span className="tone-cache-write" title={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens.toLocaleString()}`} aria-label={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens}`}><DatabaseZap size={11} aria-hidden="true" />{compactNumber(record.tokens.cache_creation_tokens)}</span></div>
</td>
);
case 'cacheRate': {
const value = formatCacheReadRate({
inputTokens: record.tokens.input_tokens,
cacheReadTokens: record.tokens.cache_read_tokens,
});
return <td className="usage-td-cache-rate align-left" title={value === '—' ? undefined : value}>{value}</td>;
}
case 'total':
return (
<td className="usage-td-token usage-td-total align-left" title={`${record.tokens.total_tokens.toLocaleString()} tokens`}>
@@ -403,12 +364,6 @@ function UsageEventCell({
<small title={record.ttft_ms == null ? undefined : `${record.ttft_ms} ms`}>{t('usage.column.ttft')} {record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)}</small>
</td>
);
case 'ttft':
return (
<td className="usage-td-ttft align-left" title={record.ttft_ms == null ? undefined : `${record.ttft_ms} ms`}>
{record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)}
</td>
);
case 'speed': {
const value = formatGenerationSpeed({
outputTokens: record.tokens.output_tokens,
+2 -2
View File
@@ -134,7 +134,7 @@ type SettingsApi = {
};
export const loadAuthFileSettings = async (name: string, api: SettingsApi = managementApi) =>
authFileSettingsFromPayload(await api.get('/auth-files/download', { name }));
authFileSettingsFromPayload(await api.get('/credentials/download', { name }));
export const saveAuthFileSettings = async (
name: string, original: AuthFileSettingsDraft, draft: AuthFileSettingsDraft,
@@ -142,6 +142,6 @@ export const saveAuthFileSettings = async (
) => {
const patch = buildAuthFileSettingsPatch(original, draft);
if (!Object.keys(patch).length) return false;
await api.patch('/auth-files/fields', { name, ...patch });
await api.patch('/credentials/fields', { name, ...patch });
return true;
};
+1 -1
View File
@@ -57,7 +57,7 @@ export const setOAuthCredentialFileDisabled = async (
if (!isOAuthCredentialFile(file)) {
throw new Error(translate(getCurrentLocale(), 'authFiles.fileOnly'));
}
await api.patch('/auth-files/status', { name: readString(file, 'name'), disabled });
await api.patch('/credentials/status', { name: readString(file, 'name'), disabled });
};
export const parseAuthFilePriority = (value: unknown): number | undefined => {
+1 -1
View File
@@ -130,7 +130,7 @@ export async function loadHomeOverview(coreReady: boolean, nowMs = Date.now()):
const [usage, credentials, providerKeys, models] = await Promise.allSettled([
// Usage is stored locally and remains meaningful while the core is stopped.
invoke<unknown>('get_usage_overview', { query }).then(summarizeHomeUsage),
coreReady ? managementApi.get('/auth-files').then(summarizeHomeCredentials) : Promise.resolve(null),
coreReady ? managementApi.get('/credentials').then(summarizeHomeCredentials) : Promise.resolve(null),
coreReady ? loadProviderKeys() : Promise.resolve(null),
coreReady ? invoke<unknown>('get_core_models').then(normalizeHomeModels) : Promise.resolve(null),
]);
+4 -179
View File
@@ -1,6 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import { getCurrentLocale, translate } from '../i18n';
import { normalizeProviderModels } from './providerModels';
export type ManagementJson = Record<string, unknown> | unknown[] | string | number | boolean | null;
@@ -10,33 +9,6 @@ type ManagementRequestOptions = {
timeoutMs?: number;
};
const PROVIDER_PATHS = {
'/gemini-api-key': { provider: 'gemini', legacy: 'gemini-api-key' },
'/interactions-api-key': { provider: 'interactions', legacy: 'interactions-api-key' },
'/vertex-api-key': { provider: 'vertex', legacy: 'vertex-api-key' },
'/codex-api-key': { provider: 'codex', legacy: 'codex-api-key' },
'/claude-api-key': { provider: 'claude', legacy: 'claude-api-key' },
'/xai-api-key': { provider: 'xai', legacy: 'xai-api-key' },
'/meta-api-key': { provider: 'meta', legacy: 'meta-api-key' },
'/openai-compatibility': { provider: 'openai-compatibility', legacy: 'openai-compatibility' },
} as const;
type ProviderPath = keyof typeof PROVIDER_PATHS;
const SHARED_PROVIDER_FIELDS = new Set([
'priority',
'prefix',
'proxy-url',
'headers',
'models',
'excluded-models',
'disable-cooling',
'request-retry',
'request-scoped-errors',
]);
const providerDefinition = (path: string) => PROVIDER_PATHS[path as ProviderPath];
// Native v8 groups: callers keep keys and overrides intact through every edit.
export const providerGroupsApi = {
get: async (section: string): Promise<Record<string, unknown>[]> => {
@@ -49,7 +21,7 @@ export const providerGroupsApi = {
};
async function optionalConfigValue(
path: string, fallback: ManagementJson, options: ManagementRequestOptions,
path: string, fallback: ManagementJson, options: ManagementRequestOptions = {},
): Promise<unknown> {
try {
return await invoke<unknown>('management_request', {
@@ -63,75 +35,6 @@ async function optionalConfigValue(
}
}
export function flattenV8ProviderGroups(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).flatMap((group) => {
const keys = Array.isArray(group.keys) ? group.keys.filter(isRecord) : [];
if (provider === 'openai-compatibility') {
const record: Record<string, unknown> = { ...group };
delete record.keys;
delete record['test-model'];
delete record.testModel;
if (keys.length > 0) record['api-key-entries'] = keys.map((key) => ({ ...key }));
return [normalizeProviderModels(record)];
}
const shared = Object.fromEntries(
Object.entries(group).filter(([key]) => key === 'base-url' || SHARED_PROVIDER_FIELDS.has(key)),
);
const name = readString(group, 'name');
const generatedName = name.startsWith(`${provider}-`) && /^[0-9]+$/.test(name.slice(provider.length + 1));
if (name && !generatedName) shared.name = name;
return keys.map((key) => {
const overrides = Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null));
return normalizeProviderModels({ ...shared, ...overrides });
});
});
}
export function groupLegacyProviderRecords(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).map((input, index) => {
const record = normalizeProviderModels(input);
if (provider === 'openai-compatibility') {
const group: Record<string, unknown> = {
...record,
keys: Array.isArray(record['api-key-entries'])
? record['api-key-entries'].filter(isRecord).map((key) => ({ ...key }))
: [],
};
delete group['api-key-entries'];
delete group['test-model'];
delete group.testModel;
return group;
}
const group: Record<string, unknown> = { name: readString(record, 'name') || `${provider}-${index + 1}` };
const key: Record<string, unknown> = {};
Object.entries(record).forEach(([field, value]) => {
if (field === 'name') return;
if (field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)) group[field] = value;
else key[field] = value;
});
group.keys = [key];
return group;
});
}
export function legacyManagementConfigView(payload: unknown): unknown {
if (!isRecord(payload)) return payload;
const legacy: Record<string, unknown> = { ...payload };
const upstream = isRecord(payload['api-keys']) ? payload['api-keys'] : {};
Object.values(PROVIDER_PATHS).forEach(({ provider, legacy: legacyKey }) => {
legacy[legacyKey] = flattenV8ProviderGroups(provider, upstream[provider]);
});
const access = isRecord(payload.access) ? payload.access : null;
if (access && Array.isArray(access['api-keys'])) legacy['api-keys'] = access['api-keys'];
const oauth = isRecord(payload.oauth) ? payload.oauth : null;
if (oauth && oauth['model-alias'] !== undefined) legacy['oauth-model-alias'] = oauth['model-alias'];
const requests = isRecord(payload.requests) ? payload.requests : null;
if (requests && requests.payload !== undefined) legacy.payload = requests.payload;
return legacy;
}
const normalizeQuery = (
query?: Record<string, string | number | boolean | undefined>,
): Record<string, string> | undefined => {
@@ -152,93 +55,15 @@ async function request<T = ManagementJson>(
path: string,
options: ManagementRequestOptions = {},
): Promise<T> {
if (path === '/oauth-excluded-models') {
if (method === 'PATCH' && isRecord(options.body)) {
const provider = readString(options.body, 'provider');
if (!provider || !Array.isArray(options.body.models)) {
throw new Error('Invalid OAuth model exclusion update');
}
return invoke<T>('management_request', {
request: {
method: 'PUT',
path: `/config/oauth/excluded-models/${encodeURIComponent(provider)}`,
body: options.body.models,
},
});
}
if (method === 'DELETE') {
const provider = options.query?.provider;
if (typeof provider !== 'string' || !provider.trim()) {
throw new Error('Invalid OAuth model exclusion delete');
}
return invoke<T>('management_request', {
request: {
method: 'DELETE',
path: `/config/oauth/excluded-models/${encodeURIComponent(provider.trim())}`,
},
});
}
if (method === 'GET') {
const exclusions = await optionalConfigValue('/config/oauth/excluded-models', {}, options);
return { 'oauth-excluded-models': exclusions } as T;
}
}
const definition = providerDefinition(path);
if (method === 'GET' && definition) {
const groups = await optionalConfigValue(`/config/api-keys/${definition.provider}`, [], options);
return { [definition.legacy]: flattenV8ProviderGroups(definition.provider, groups) } as T;
}
if (method === 'PATCH' && path === '/openai-compatibility' && isRecord(options.body)) {
const index = Number(options.body.index);
const value = options.body.value;
if (!Number.isInteger(index) || index < 0 || !isRecord(value)) {
throw new Error('Invalid OpenAI compatibility update');
}
const groups = await invoke<unknown>('management_request', {
request: { method: 'GET', path: '/config/api-keys/openai-compatibility' },
});
const records = flattenV8ProviderGroups('openai-compatibility', groups);
if (!records[index]) throw new Error('OpenAI compatibility entry no longer exists');
records[index] = { ...records[index], ...value };
return invoke<T>('management_request', {
request: {
method: 'PUT',
path: '/config/api-keys/openai-compatibility',
body: groupLegacyProviderRecords('openai-compatibility', records),
},
});
}
let apiPath = path;
let body = options.body;
if (definition) {
apiPath = `/config/api-keys/${definition.provider}`;
if (method === 'PUT' || method === 'PATCH') {
body = groupLegacyProviderRecords(definition.provider, body);
}
} else if (path === '/api-call') {
apiPath = '/requests/api-call';
} else if (path === '/oauth-session') {
apiPath = '/oauth/session';
} else if (path.startsWith('/auth-files')) {
apiPath = `/credentials${path.slice('/auth-files'.length)}`;
} else if (path.startsWith('/model-definitions/')) {
apiPath = `/routing${path}`;
}
const payload = await invoke<unknown>('management_request', {
return invoke<T>('management_request', {
request: {
method,
path: apiPath,
path,
query: normalizeQuery(options.query),
body,
body: options.body,
timeoutMs: options.timeoutMs,
},
});
if (method === 'GET' && path === '/config') {
return legacyManagementConfigView(payload) as T;
}
return payload as T;
}
export const managementApi = {
+3 -3
View File
@@ -220,7 +220,7 @@ export async function fetchModels(
for (let page = 0; page < (['gemini', 'interactions', 'vertex'].includes(provider) ? 20 : 1); page += 1) {
const pageUrl = new URL(url);
if (pageToken) pageUrl.searchParams.set('pageToken', pageToken);
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
const response = await managementApi.post<Record<string, unknown>>('/requests/api-call', {
authIndex: authIndex?.trim() || undefined,
method: 'GET',
url: pageUrl.toString(),
@@ -257,7 +257,7 @@ export async function fetchModels(
if (collected.length) return collected;
if (provider === 'openai' && Object.keys(headers).length > 0) {
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
const response = await managementApi.post<Record<string, unknown>>('/requests/api-call', {
method: 'GET',
url,
}, { timeoutMs });
@@ -271,7 +271,7 @@ export async function fetchModels(
lastError = String(error);
if (provider === 'openai' && Object.keys(headers).length > 0) {
try {
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
const response = await managementApi.post<Record<string, unknown>>('/requests/api-call', {
method: 'GET',
url,
}, { timeoutMs });
+43 -11
View File
@@ -23,9 +23,44 @@ export type OAuthModelSettings = {
type OAuthModelSettingsApi = {
get: (path: string, query?: Record<string, string>) => Promise<unknown>;
patch: (path: string, body: Record<string, unknown>) => Promise<unknown>;
delete: (path: string, options?: { query?: Record<string, string> }) => Promise<unknown>;
put: (path: string, body: Record<string, unknown>) => Promise<unknown>;
};
const exclusionsPath = '/config/oauth/excluded-models';
const exclusionWrites = new WeakMap<OAuthModelSettingsApi, Promise<void>>();
async function readExclusions(api: OAuthModelSettingsApi): Promise<Record<string, unknown>> {
try {
const value = await api.get(exclusionsPath);
if (!isRecord(value)) throw new Error('Invalid OAuth exclusion configuration');
return value;
} catch (error) {
if ((error instanceof Error ? error.message : error) === 'Management API error (404): not_found') return {};
throw error;
}
}
// v8 accepts the provider map directly. Serialize read/modify/write operations
// so simultaneous edits to different providers preserve each other's changes.
export function saveOAuthProviderExclusions(
provider: string, models: string[] | undefined, api: OAuthModelSettingsApi = managementApi,
): Promise<void> {
const key = provider.trim().toLowerCase();
if (!key) return Promise.reject(new Error('Invalid OAuth provider'));
const pending = (exclusionWrites.get(api) ?? Promise.resolve()).then(async () => {
const current = await readExclusions(api);
const next = Object.fromEntries(Object.entries(current).filter(([name]) => name.trim().toLowerCase() !== key));
if (models !== undefined) Object.defineProperty(next, key, { value: models, enumerable: true });
await api.put(exclusionsPath, next);
});
const settled = pending.catch(() => {});
exclusionWrites.set(api, settled);
void settled.then(() => {
if (exclusionWrites.get(api) === settled) exclusionWrites.delete(api);
});
return pending;
}
export const authFileExcludedRulesFromPayload = (payload: unknown): string[] => {
let metadata = payload;
if (typeof metadata === 'string') {
@@ -54,13 +89,13 @@ export const loadOAuthModelSettings = async (
): Promise<OAuthModelSettings> => {
const [catalog, payload] = await Promise.all([
(target.scope === 'credential'
? api.get('/auth-files/models', { name: target.name })
: api.get(`/model-definitions/${encodeURIComponent(target.provider)}`))
? api.get('/credentials/models', { name: target.name })
: api.get(`/routing/model-definitions/${encodeURIComponent(target.provider)}`))
.then((definitions) => ({ models: oauthModelsFromPayload(definitions), error: '' }))
.catch((error: unknown) => ({ models: [] as OAuthModelDefinition[], error: String(error) })),
target.scope === 'credential'
? api.get('/auth-files/download', { name: target.name })
: api.get('/oauth-excluded-models'),
? api.get('/credentials/download', { name: target.name })
: readExclusions(api),
]);
const excludedRules = target.scope === 'credential'
? authFileExcludedRulesFromPayload(payload)
@@ -82,16 +117,13 @@ export const saveOAuthModelSettings = async (
if (excludedModels.length === settings.excludedRules.length
&& excludedModels.every((rule) => settings.excludedRules.includes(rule))) return;
if (settings.target.scope === 'credential') {
await api.patch('/auth-files/fields', {
await api.patch('/credentials/fields', {
name: settings.target.name,
excluded_models: excludedModels,
});
} else if (excludedModels.length > 0) {
await api.patch('/oauth-excluded-models', {
provider: settings.target.provider,
models: excludedModels,
});
await saveOAuthProviderExclusions(settings.target.provider, excludedModels, api);
} else if (settings.excludedRules.length > 0) {
await api.delete('/oauth-excluded-models', { query: { provider: settings.target.provider } });
await saveOAuthProviderExclusions(settings.target.provider, undefined, api);
}
};
+4 -9
View File
@@ -21,15 +21,10 @@ export const oauthModelsFromPayload = (payload: unknown): OAuthModelDefinition[]
export const oauthExcludedRulesFromPayload = (payload: unknown, provider: string): string[] => {
if (!isRecord(payload)) return [];
const source = isRecord(payload['oauth-excluded-models'])
? payload['oauth-excluded-models']
: payload;
const value = source[provider.trim().toLowerCase()];
if (!Array.isArray(value)) return [];
return value
.map(String)
.map((rule) => rule.trim().toLowerCase())
.filter((rule, index, rules) => rule && rules.indexOf(rule) === index);
const key = provider.trim().toLowerCase();
return normalizeOAuthExcludedRules(Object.entries(payload).flatMap(([name, value]) =>
name.trim().toLowerCase() === key && Array.isArray(value) ? value.map(String) : [],
));
};
const wildcardPattern = (rule: string) => new RegExp(
+2 -2
View File
@@ -579,7 +579,7 @@ const resolveProjectId = async (file: AuthFile): Promise<string> => {
const direct = antigravityProjectFor(file);
if (direct) return direct;
try {
const payload = parseBody(await managementApi.get('/auth-files/download', { name: fileName(file) }));
const payload = parseBody(await managementApi.get('/credentials/download', { name: fileName(file) }));
return isRecord(payload) ? antigravityProjectFor(payload) : '';
} catch {
return '';
@@ -615,7 +615,7 @@ const requestQuotaPayload = async (
timeoutMs?: number,
responseClock?: { serverTimeOffsetMs?: number },
) => {
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
const response = await managementApi.post<Record<string, unknown>>('/requests/api-call', {
authIndex,
method,
url,
+1 -3
View File
@@ -9891,9 +9891,7 @@ body.table-col-resizing * {
.usage-events-table td.usage-td-token,
.usage-events-table td.usage-td-latency,
.usage-events-table td.usage-td-ttft,
.usage-events-table td.usage-td-speed,
.usage-events-table td.usage-td-cache-rate {
.usage-events-table td.usage-td-speed {
font-variant-numeric: tabular-nums;
}
+2 -2
View File
@@ -15,7 +15,7 @@ describe('credential settings', () => {
const writes: unknown[] = [];
const api = {
async get(path: string, query: Record<string, string>) {
expect([path, query]).toEqual(['/auth-files/download', { name: 'test.json' }]);
expect([path, query]).toEqual(['/credentials/download', { name: 'test.json' }]);
return { ...file };
},
async patch(path: string, body: Record<string, unknown>) {
@@ -28,7 +28,7 @@ describe('credential settings', () => {
file.access_token = 'refreshed';
file.priority = 9;
expect(await saveAuthFileSettings('test.json', original, { ...original, note: 'new' }, api)).toBe(true);
expect(writes).toEqual([{ path: '/auth-files/fields', body: { name: 'test.json', note: 'new' } }]);
expect(writes).toEqual([{ path: '/credentials/fields', body: { name: 'test.json', note: 'new' } }]);
expect(file).toEqual({ access_token: 'refreshed', priority: 9, note: 'new', auth_index: 'private-index' });
expect(await saveAuthFileSettings('test.json', original, original, api)).toBe(false);
expect(writes).toHaveLength(1);
+2 -2
View File
@@ -143,8 +143,8 @@ describe('OAuth credential file boundaries', () => {
await setOAuthCredentialFileDisabled(oauthFile, true, api);
await setOAuthCredentialFileDisabled({ ...oauthFile, disabled: true }, false, api);
expect(writes).toEqual([
{ path: '/auth-files/status', body: { name: 'codex-user.json', disabled: true } },
{ path: '/auth-files/status', body: { name: 'codex-user.json', disabled: false } },
{ path: '/credentials/status', body: { name: 'codex-user.json', disabled: true } },
{ path: '/credentials/status', body: { name: 'codex-user.json', disabled: false } },
]);
});
+9 -22
View File
@@ -71,15 +71,13 @@ describe('browser mock runtime', () => {
await runtime.invoke('management_request', {
request: {
method: 'PUT',
path: '/gemini-api-key',
body: [{ 'api-key': 'AIza-test', models: [{ name: 'gemini-test' }] }],
path: '/config/api-keys/gemini',
body: [{ name: 'test', keys: [{ 'api-key': 'AIza-test' }], models: [{ name: 'gemini-test' }] }],
},
});
expect(await runtime.invoke('management_request', {
request: { method: 'GET', path: '/gemini-api-key' },
})).toEqual({
'gemini-api-key': [{ 'api-key': 'AIza-test', models: [{ name: 'gemini-test' }] }],
});
request: { method: 'GET', path: '/config/api-keys/gemini' },
})).toEqual([{ name: 'test', keys: [{ 'api-key': 'AIza-test' }], models: [{ name: 'gemini-test' }] }]);
});
test('uses normalized ratios for percentage-based usage metrics', async () => {
@@ -143,7 +141,7 @@ describe('browser mock native provider groups', () => {
});
}
test('derives effective legacy key records without changing native inheritance', async () => {
test('returns native groups without flattening inherited and explicit empty values', async () => {
const runtime = createBrowserMockRuntime('running');
const groups = [{
name: 'preserve this group name',
@@ -157,16 +155,10 @@ describe('browser mock native provider groups', () => {
],
}];
await request(runtime, 'PUT', '/config/api-keys/interactions', groups);
expect(await request(runtime, 'GET', '/interactions-api-key')).toEqual({
'interactions-api-key': [
{ 'api-key': 'first', 'base-url': 'https://provider.example', priority: 0, headers: { 'X-Team': 'shared' }, models: [] },
{ 'api-key': 'second', 'base-url': 'https://provider.example', priority: 5, headers: {}, models: [{ name: 'shared-model' }] },
],
});
expect(await request(runtime, 'GET', '/config/api-keys/interactions')).toEqual(groups);
});
test('adapts legacy OpenAI mutations while preserving native group metadata and keys', async () => {
test('updates native OpenAI groups while preserving metadata and keys', async () => {
const runtime = createBrowserMockRuntime('running');
const group = {
name: 'custom group', 'base-url': 'https://provider.example/v1', disabled: false,
@@ -174,14 +166,9 @@ describe('browser mock native provider groups', () => {
keys: [{ 'api-key': 'first', weight: 0, 'proxy-url': '' }, { 'api-key': 'second', weight: 3 }],
};
await request(runtime, 'PUT', '/config/api-keys/openai-compatibility', [group]);
await request(runtime, 'PATCH', '/openai-compatibility', { index: 0, value: { disabled: true } });
await request(runtime, 'PUT', '/config/api-keys/openai-compatibility', [{ ...group, disabled: true }]);
expect(await request(runtime, 'GET', '/config/api-keys/openai-compatibility')).toEqual([{ ...group, disabled: true }]);
const { keys, ...shared } = group;
expect(await request(runtime, 'GET', '/openai-compatibility')).toEqual({
'openai-compatibility': [{ ...shared, disabled: true, 'api-key-entries': keys }],
});
await request(runtime, 'PUT', '/openai-compatibility', [{ ...shared, 'api-key-entries': [keys[1]] }]);
expect(await request(runtime, 'GET', '/config/api-keys/openai-compatibility')).toEqual([{ ...shared, keys: [keys[1]] }]);
await request(runtime, 'PUT', '/config/api-keys/openai-compatibility', [{ ...group, keys: [group.keys[1]] }]);
expect(await request(runtime, 'GET', '/config/api-keys/openai-compatibility')).toEqual([{ ...group, keys: [group.keys[1]] }]);
});
});
+14 -14
View File
@@ -8,7 +8,7 @@ import { join, resolve, sep } from 'node:path';
import { managementApi, responseList } from '../src/services/managementApi';
import { loadAuthFileSettings, saveAuthFileSettings } from '../src/services/authFileSettings';
import { setOAuthCredentialFileDisabled } from '../src/services/authFiles';
import { loadOAuthModelSettings, saveOAuthModelSettings } from '../src/services/oauthModelSettings';
import { loadOAuthModelSettings, saveOAuthModelSettings, saveOAuthProviderExclusions } from '../src/services/oauthModelSettings';
import { fetchModels } from '../src/services/modelService';
const executable = process.env.CPA_V8_TEST_CORE;
@@ -90,7 +90,7 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
})], name, { type: 'application/json' }));
let file: Record<string, unknown> | undefined;
for (let attempt = 0; attempt < 50; attempt++) {
file = responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name);
file = responseList(await managementApi.get('/credentials'), 'files').find((item) => item.name === name);
if (file) break;
await Bun.sleep(100);
}
@@ -118,15 +118,15 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
expect(cleared).toMatchObject({ prefix: '', proxy_url: '', priority: '0', weight: '',
disable_cooling: '', websockets: '', excluded_models: '', note: '' });
expect(JSON.parse(cleared.headers)).toEqual({ 'X-Test': 'updated' });
const metadata = await managementApi.get<Record<string, unknown>>('/auth-files/download', { name });
const metadata = await managementApi.get<Record<string, unknown>>('/credentials/download', { name });
expect(metadata.custom_metadata).toEqual({ preserve: true });
expect(metadata.access_token).toBe('isolated-access-token');
await setOAuthCredentialFileDisabled(file!, true);
expect(responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name)?.disabled).toBe(true);
expect(responseList(await managementApi.get('/credentials'), 'files').find((item) => item.name === name)?.disabled).toBe(true);
await setOAuthCredentialFileDisabled(file!, false);
expect(responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name)?.disabled).toBe(false);
await managementApi.delete('/auth-files', { query: { name } });
expect(responseList(await managementApi.get('/auth-files'), 'files').some((item) => item.name === name)).toBe(false);
expect(responseList(await managementApi.get('/credentials'), 'files').find((item) => item.name === name)?.disabled).toBe(false);
await managementApi.delete('/credentials', { query: { name } });
expect(responseList(await managementApi.get('/credentials'), 'files').some((item) => item.name === name)).toBe(false);
});
it('edits provider model exclusions without changing siblings and clears the last rule', async () => {
@@ -136,14 +136,14 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
expect(original.catalogError).toBe('');
expect(original.models.length).toBeGreaterThan(0);
expect(original.excludedRules).toEqual([]);
await managementApi.patch('/oauth-excluded-models', { provider: 'claude', models: ['keep-*'] });
await saveOAuthProviderExclusions('claude', ['keep-*']);
await saveOAuthModelSettings(original, ['hide-*']);
const edited = await loadOAuthModelSettings(target);
expect(edited.excludedRules).toEqual(['hide-*']);
await saveOAuthModelSettings(edited, []);
expect((await loadOAuthModelSettings(target)).excludedRules).toEqual([]);
expect(await managementApi.get('/oauth-excluded-models')).toEqual({ 'oauth-excluded-models': { claude: ['keep-*'] } });
await managementApi.delete('/oauth-excluded-models', { query: { provider: 'claude' } });
expect(await managementApi.get('/config/oauth/excluded-models')).toEqual({ claude: ['keep-*'] });
await saveOAuthProviderExclusions('claude', undefined);
});
it('round-trips template credential aliases and Claude settings without replacing token metadata', async () => {
@@ -158,13 +158,13 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
await saveAuthFileSettings(fileName, original, { ...original, advanced });
const loaded = await loadAuthFileSettings(fileName);
expect(loaded.advanced).toEqual(advanced);
const metadata = await managementApi.get<Record<string, unknown>>('/auth-files/download', { name: fileName });
const metadata = await managementApi.get<Record<string, unknown>>('/credentials/download', { name: fileName });
expect(metadata.access_token).toBe('isolated-template-token');
expect(metadata.custom_metadata).toEqual({ keep: true });
await saveAuthFileSettings(fileName, loaded, { ...loaded, advanced: {} });
const cleared = await loadAuthFileSettings(fileName);
expect(Object.values(cleared.advanced).every(value => value === null)).toBe(true);
} finally { await managementApi.delete('/auth-files', { query: { name: fileName } }); }
} finally { await managementApi.delete('/credentials', { query: { name: fileName } }); }
});
it('discovers models and forwards API-call requests to an isolated local upstream', async () => {
@@ -175,7 +175,7 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
try {
const models = await fetchModels('openai', `http://127.0.0.1:${upstream.port}/v1`, 'local-key');
expect(models.map((model) => model.name)).toEqual(['local-model']);
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
const response = await managementApi.post<Record<string, unknown>>('/requests/api-call', {
method: 'GET', url: `http://127.0.0.1:${upstream.port}/usage`, header: { Authorization: 'Bearer local-key' },
});
expect(response.status_code).toBe(200);
@@ -189,7 +189,7 @@ oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
expect(typeof status.status).toBe('string');
await expect(call('GET', '/oauth/auth-url', { provider: 'unsupported-test-provider' })).rejects.toThrow('(404): provider_not_found');
await expect(call('POST', '/oauth/callback', undefined, { provider: 'codex', redirect_url: 'http://localhost/callback' })).rejects.toThrow('(400)');
await managementApi.delete('/oauth-session', { query: { state: 'isolated-unknown-state' } });
await managementApi.delete('/oauth/session', { query: { state: 'isolated-unknown-state' } });
});
it('accepts new template controls and retains explicit false, zero and empty collections', async () => {
+5 -5
View File
@@ -67,7 +67,7 @@ describe('Devin live quota', () => {
expect(get).not.toHaveBeenCalled();
expect(post).toHaveBeenCalledTimes(1);
const [path, body] = post.mock.calls[0];
expect(path).toBe('/api-call');
expect(path).toBe('/requests/api-call');
expect(body).toMatchObject({ authIndex: 'devin-index', method: 'POST', url: 'https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus', header: { 'Content-Type': 'application/json', 'Connect-Protocol-Version': '1' } });
expect(JSON.parse((body as { data: string }).data)).toEqual({ metadata: { ideName: 'chisel', ideVersion: '3000.10.21', apiKey: '$TOKEN$', locale: 'en', os: 'darwin', extensionVersion: '3000.10.21', clientName: 'chisel' } });
await loadQuota(file);
@@ -95,15 +95,15 @@ it('loads and saves Devin model exclusions under its own provider channel', asyn
const api = {
get: async (path: string) => {
calls.push(path);
return path === '/model-definitions/devin'
return path === '/routing/model-definitions/devin'
? { models: [{ id: 'devin/swe-2', display_name: 'SWE-2' }] }
: { 'oauth-excluded-models': { devin: ['devin/old'] } };
: { devin: ['devin/old'] };
},
patch: async (path: string, body: Record<string, unknown>) => { calls.push([path, body]); },
delete: async () => {},
put: async (path: string, body: Record<string, unknown>) => { calls.push([path, body]); },
};
const settings = await loadOAuthModelSettings({ scope: 'provider', provider: 'devin', label: 'Devin' }, api);
expect(settings.models.map((model) => model.id)).toEqual(['devin/old', 'devin/swe-2']);
await saveOAuthModelSettings(settings, ['devin/swe-2'], api);
expect(calls).toContainEqual(['/oauth-excluded-models', { provider: 'devin', models: ['devin/swe-2'] }]);
expect(calls).toContainEqual(['/config/oauth/excluded-models', { devin: ['devin/swe-2'] }]);
});
+69
View File
@@ -0,0 +1,69 @@
// Legacy fixtures used to verify migration into native v8 provider groups.
import { normalizeProviderModels } from '../../src/services/providerModels';
import { isRecord, readString } from '../../src/services/managementApi';
const SHARED_PROVIDER_FIELDS = new Set([
'priority',
'prefix',
'proxy-url',
'headers',
'models',
'excluded-models',
'disable-cooling',
'request-retry',
'request-scoped-errors',
]);
export function flattenV8ProviderGroups(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).flatMap((group) => {
const keys = Array.isArray(group.keys) ? group.keys.filter(isRecord) : [];
if (provider === 'openai-compatibility') {
const record: Record<string, unknown> = { ...group };
delete record.keys;
delete record['test-model'];
delete record.testModel;
if (keys.length > 0) record['api-key-entries'] = keys.map((key) => ({ ...key }));
return [normalizeProviderModels(record)];
}
const shared = Object.fromEntries(
Object.entries(group).filter(([key]) => key === 'base-url' || SHARED_PROVIDER_FIELDS.has(key)),
);
const name = readString(group, 'name');
const generatedName = name.startsWith(`${provider}-`) && /^[0-9]+$/.test(name.slice(provider.length + 1));
if (name && !generatedName) shared.name = name;
return keys.map((key) => {
const overrides = Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null));
return normalizeProviderModels({ ...shared, ...overrides });
});
});
}
export function groupLegacyProviderRecords(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).map((input, index) => {
const record = normalizeProviderModels(input);
if (provider === 'openai-compatibility') {
const group: Record<string, unknown> = {
...record,
keys: Array.isArray(record['api-key-entries'])
? record['api-key-entries'].filter(isRecord).map((key) => ({ ...key }))
: [],
};
delete group['api-key-entries'];
delete group['test-model'];
delete group.testModel;
return group;
}
const group: Record<string, unknown> = { name: readString(record, 'name') || `${provider}-${index + 1}` };
const key: Record<string, unknown> = {};
Object.entries(record).forEach(([field, value]) => {
if (field === 'name') return;
if (field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)) group[field] = value;
else key[field] = value;
});
group.keys = [key];
return group;
});
}
+44 -34
View File
@@ -1,10 +1,9 @@
import { flattenV8ProviderGroups, groupLegacyProviderRecords } from './fixtures/legacyProviderRecords';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
import {
apiCallErrorMessage,
flattenV8ProviderGroups,
groupLegacyProviderRecords,
legacyManagementConfigView,
providerGroupsApi,
managementApi,
} from '../src/services/managementApi';
import {
@@ -27,21 +26,50 @@ describe('v8 Management API requests', () => {
else Reflect.deleteProperty(globalThis, 'window');
});
it('returns the native config tree without flattening groups or replacing access keys', async () => {
const config = {
'config-version': 8,
access: { 'api-keys': ['client-test'] },
'api-keys': { claude: [{ name: 'team', models: [{ name: 'upstream', alias: 'public' }],
keys: [{ 'api-key': 'one', models: null }, { 'api-key': 'two', models: [] }] }] },
oauth: { 'model-alias': { claude: [{ name: 'original', alias: 'desktop' }] } },
requests: { payload: { override: [] } },
};
mockIPC((_command, args) => {
expect(args?.request).toMatchObject({ method: 'GET', path: '/config' });
return config;
});
expect(await managementApi.get('/config')).toEqual(config);
});
it('preserves native request bodies, query defaults and timeouts', async () => {
const calls: unknown[] = [];
mockIPC((_command, args) => { calls.push(args?.request); return { status: 'ok' }; });
await managementApi.get('/credentials', { all: false, offset: 0, omitted: undefined });
await managementApi.put('/config/access/api-keys', []);
await managementApi.put('/config/routing/session-affinity', false);
await managementApi.post('/requests/api-call', { method: 'GET', url: 'https://example.invalid', auth_index: '0' }, { timeoutMs: 12345 });
await managementApi.delete('/credentials', { body: { names: ['test.json'] } });
expect(calls).toMatchObject([
{ method: 'GET', path: '/credentials', query: { all: 'false', offset: '0' } },
{ method: 'PUT', path: '/config/access/api-keys', body: [] },
{ method: 'PUT', path: '/config/routing/session-affinity', body: false },
{ method: 'POST', path: '/requests/api-call', body: { method: 'GET', url: 'https://example.invalid', auth_index: '0' }, timeoutMs: 12345 },
{ method: 'DELETE', path: '/credentials', body: { names: ['test.json'] } },
]);
});
it('defaults absent exclusions and provider groups without swallowing other errors', async () => {
for (const [path, key, empty] of [
['/oauth-excluded-models', 'oauth-excluded-models', {}],
['/codex-api-key', 'codex-api-key', []],
['/openai-compatibility', 'openai-compatibility', []],
] as const) {
for (const section of ['codex', 'openai-compatibility']) {
mockIPC(() => { throw 'Management API error (404): not_found'; });
expect(await managementApi.get(path)).toEqual({ [key]: empty });
expect(await providerGroupsApi.get(section)).toEqual([]);
for (const message of [
'Management API error (401): unauthorized',
'Management API error (404): 404 page not found',
'Management API request failed: connection refused',
]) {
mockIPC(() => { throw new Error(message); });
await expect(managementApi.get(path)).rejects.toThrow(message);
await expect(providerGroupsApi.get(section)).rejects.toThrow(message);
}
}
});
@@ -74,17 +102,17 @@ describe('v8 Management API requests', () => {
baseUrl: 'https://codex.example.test/v1', models: [{ name: 'gpt-test' }],
});
for (const record of [original, providerRecordWithDisabledState('codex-api-key', original, true)]) {
await managementApi.put('/codex-api-key', [regular, record]);
const loaded = await managementApi.get<{ 'codex-api-key': Record<string, unknown>[] }>('/codex-api-key');
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [regular, record]));
const loaded = { 'codex-api-key': flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex')) };
expect(loaded['codex-api-key']).toEqual([regular, record]);
expect(providerCategoryMatchesRecord('deepseek', loaded['codex-api-key'][1])).toBe(true);
expect(apiAccessRemarkLocatorFromRecord('codex-api-key', loaded['codex-api-key'][1]))
.toEqual(apiAccessRemarkLocatorFromRecord('codex-api-key', original));
await managementApi.put('/codex-api-key', [record, regular]);
expect((await managementApi.get('/codex-api-key'))).toEqual({ 'codex-api-key': [record, regular] });
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [record, regular]));
expect(flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex'))).toEqual([record, regular]);
}
await managementApi.put('/codex-api-key', [regular]);
expect(await managementApi.get('/codex-api-key')).toEqual({ 'codex-api-key': [regular] });
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [regular]));
expect(flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex'))).toEqual([regular]);
}
});
});
@@ -163,22 +191,4 @@ describe('v8 Management API compatibility view', () => {
expect(records[1]).not.toHaveProperty('name');
});
it('projects the v8 config tree into the legacy semantic view used by the UI', () => {
const view = legacyManagementConfigView({
access: { 'api-keys': ['client-key'] },
'api-keys': {
codex: [{ name: 'codex-1', keys: [{ 'api-key': 'upstream-key' }] }],
'openai-compatibility': [{ name: 'openrouter', keys: [{ 'api-key': 'openrouter-key' }] }],
},
oauth: { 'model-alias': { codex: [{ name: 'gpt', alias: 'custom' }] } },
requests: { payload: { override: [] } },
}) as Record<string, unknown>;
expect(view['api-keys']).toEqual(['client-key']);
expect(view['codex-api-key']).toEqual([{ 'api-key': 'upstream-key' }]);
expect(view['openai-compatibility']).toEqual([{
name: 'openrouter', 'api-key-entries': [{ 'api-key': 'openrouter-key' }],
}]);
expect(view['oauth-model-alias']).toEqual({ codex: [{ name: 'gpt', alias: 'custom' }] });
expect(view.payload).toEqual({ override: [] });
});
});
+1 -1
View File
@@ -8,7 +8,7 @@ import {
applyProviderPreset, buildProviderRecord, buildProviderGroupRecord, createProviderDraft, providerDraftFromRecord,
type ProviderSection,
} from '../src/pages/ApiAccessPage';
import { flattenV8ProviderGroups, groupLegacyProviderRecords } from '../src/services/managementApi';
import { flattenV8ProviderGroups, groupLegacyProviderRecords } from './fixtures/legacyProviderRecords';
const executable = process.env.CPA_V8_TEST_CORE;
describe.skipIf(!executable)('real v8 provider configuration', () => {
+59 -26
View File
@@ -3,6 +3,7 @@ import {
authFileExcludedRulesFromPayload,
loadOAuthModelSettings,
saveOAuthModelSettings,
saveOAuthProviderExclusions,
type OAuthModelTarget,
} from '../src/services/oauthModelSettings';
import { openOAuthModelNames, setOAuthModelsExcluded } from '../src/services/oauthModels';
@@ -26,11 +27,11 @@ function createApi() {
const api = {
async get(path: string, query?: Record<string, string>): Promise<unknown> {
reads.push({ path, query });
if (path.startsWith('/model-definitions/')) return { models };
if (path === '/oauth-excluded-models') return { 'oauth-excluded-models': structuredClone(globalRules) };
if (path.startsWith('/routing/model-definitions/')) return { models };
if (path === '/config/oauth/excluded-models') return structuredClone(globalRules);
const file = query?.name ? files[query.name] : undefined;
if (path === '/auth-files/download' && file) return structuredClone(file);
if (path === '/auth-files/models' && file) {
if (path === '/credentials/download' && file) return structuredClone(file);
if (path === '/credentials/models' && file) {
const allowed = openOAuthModelNames(models, [...authFileExcludedRulesFromPayload(file), ...globalRules.codex ?? []]);
return { models: models.filter((model) => allowed.has(model.id)) };
}
@@ -38,18 +39,17 @@ function createApi() {
},
async patch(path: string, body: Record<string, unknown>): Promise<unknown> {
writes.push({ path, body });
if (path === '/auth-files/fields') {
if (path === '/credentials/fields') {
const { name, ...fields } = body;
Object.assign(files[String(name)], fields);
} else if (path === '/oauth-excluded-models') {
globalRules[String(body.provider)] = body.models as string[];
} else throw new Error('unexpected write');
return { status: 'ok' };
},
async delete(path: string, options?: { query?: Record<string, string> }): Promise<unknown> {
if (path !== '/oauth-excluded-models' || !options?.query?.provider) throw new Error('unexpected delete');
deletes.push({ path, query: options.query });
delete globalRules[options.query.provider];
async put(path: string, body: Record<string, unknown>): Promise<unknown> {
if (path !== '/config/oauth/excluded-models') throw new Error('unexpected put');
writes.push({ path, body });
for (const key of Object.keys(globalRules)) delete globalRules[key];
Object.assign(globalRules, structuredClone(body));
return { status: 'ok' };
},
};
@@ -57,6 +57,36 @@ function createApi() {
}
describe('OAuth model settings scopes', () => {
it('serializes simultaneous provider edits and preserves unrelated values verbatim', async () => {
const { api, globalRules } = createApi();
globalRules[' CODEX '] = ['old'];
globalRules['Future-Plugin'] = ['CaseSensitive-*'];
await Promise.all([
saveOAuthProviderExclusions(' Codex ', ['next'], api),
saveOAuthProviderExclusions('meta', [], api),
saveOAuthProviderExclusions('claude', undefined, api),
]);
expect(globalRules).toEqual({ codex: ['next'], meta: [], 'Future-Plugin': ['CaseSensitive-*'] });
});
it('only defaults missing config nodes and releases the write queue after errors', async () => {
const { api, writes } = createApi();
const get = api.get;
for (const error of ['Management API error (401): unauthorized', 'Management API error (404): 404 page not found']) {
api.get = async () => { throw new Error(error); };
await expect(saveOAuthProviderExclusions('codex', ['next'], api)).rejects.toThrow(error);
}
api.get = async () => [];
await expect(saveOAuthProviderExclusions('codex', ['next'], api)).rejects.toThrow('Invalid OAuth exclusion configuration');
expect(writes).toEqual([]);
api.get = async () => { throw 'Management API error (404): not_found'; };
await saveOAuthProviderExclusions('codex', ['next'], api);
expect(writes).toEqual([{ path: '/config/oauth/excluded-models', body: { codex: ['next'] } }]);
api.get = get;
await saveOAuthProviderExclusions('claude', ['next'], api);
expect(writes.at(-1)?.body).toEqual({ codex: ['next'], claude: ['next'] });
});
it('saves A exclusions without changing B, global rules, or refreshed account fields', async () => {
const { api, files, globalRules, writes, reads } = createApi();
const settings = await loadOAuthModelSettings(account('a.json'), api);
@@ -66,9 +96,9 @@ describe('OAuth model settings scopes', () => {
expect(files['a.json']).toMatchObject({ excluded_models: [modelId], access_token: 'refreshed-during-edit', priority: 9 });
expect((await loadOAuthModelSettings(account('b.json'), api)).excludedRules).toEqual([]);
expect(globalRules).toEqual({ codex: [], claude: ['claude-old-*'] });
expect(writes).toEqual([{ path: '/auth-files/fields', body: { name: 'a.json', excluded_models: [modelId] } }]);
expect(reads).toContainEqual({ path: '/auth-files/models', query: { name: 'a.json' } });
expect(reads.some(({ path }) => path === '/oauth-excluded-models' || path.startsWith('/model-definitions/'))).toBe(false);
expect(writes).toEqual([{ path: '/credentials/fields', body: { name: 'a.json', excluded_models: [modelId] } }]);
expect(reads).toContainEqual({ path: '/credentials/models', query: { name: 'a.json' } });
expect(reads.some(({ path }) => path === '/config/oauth/excluded-models' || path.startsWith('/routing/model-definitions/'))).toBe(false);
});
it('loads exact account exclusions as checked candidates without copying inherited bans', async () => {
@@ -82,7 +112,7 @@ describe('OAuth model settings scopes', () => {
expect(files['a.json'].excluded_models).toEqual(['gpt-5.4', modelId]);
expect(globalRules.codex).toEqual(['gpt-image-*']);
globalRules.codex = [];
expect((await api.get('/auth-files/models', { name: 'a.json' }) as { models: typeof models }).models)
expect((await api.get('/credentials/models', { name: 'a.json' }) as { models: typeof models }).models)
.toEqual([{ id: 'gpt-image-2' }]);
});
@@ -93,7 +123,7 @@ describe('OAuth model settings scopes', () => {
await saveOAuthModelSettings(settings, ['gpt-image-*'], api);
globalRules.codex = [];
expect(files['a.json'].excluded_models).toEqual(['gpt-image-*']);
expect((await api.get('/auth-files/models', { name: 'a.json' }) as { models: typeof models }).models)
expect((await api.get('/credentials/models', { name: 'a.json' }) as { models: typeof models }).models)
.not.toContainEqual({ id: 'gpt-image-2' });
});
@@ -112,7 +142,7 @@ describe('OAuth model settings scopes', () => {
const { api, files } = createApi();
const originalGet = api.get;
api.get = async (path, query) => {
if (path === '/auth-files/models') throw new Error('catalog unavailable');
if (path === '/credentials/models') throw new Error('catalog unavailable');
return originalGet(path, query);
};
const settings = await loadOAuthModelSettings(account('a.json'), api);
@@ -125,14 +155,15 @@ describe('OAuth model settings scopes', () => {
const { api, files, globalRules, writes, deletes, reads } = createApi();
files['a.json'].excluded_models = [modelId];
await saveOAuthModelSettings(await loadOAuthModelSettings(provider, api), ['gpt-image-*'], api);
expect(writes).toEqual([{ path: '/oauth-excluded-models', body: { provider: 'codex', models: ['gpt-image-*'] } }]);
expect(writes).toEqual([{ path: '/config/oauth/excluded-models', body: { codex: ['gpt-image-*'], claude: ['claude-old-*'] } }]);
await saveOAuthModelSettings(await loadOAuthModelSettings(provider, api), [], api);
expect(deletes).toEqual([{ path: '/oauth-excluded-models', query: { provider: 'codex' } }]);
expect(deletes).toEqual([]);
expect(writes.at(-1)).toEqual({ path: '/config/oauth/excluded-models', body: { claude: ['claude-old-*'] } });
expect(globalRules).toEqual({ claude: ['claude-old-*'] });
expect(files['a.json'].excluded_models).toEqual([modelId]);
expect(reads.some(({ path }) => path.startsWith('/auth-files/'))).toBe(false);
expect(reads.some(({ path }) => path.startsWith('/credentials/'))).toBe(false);
await saveOAuthModelSettings(await loadOAuthModelSettings(provider, api), [], api);
expect(deletes).toHaveLength(1);
expect(writes).toHaveLength(2);
});
it('uses only the OAuth exclusion endpoint even when excluding every model for a provider', async () => {
@@ -140,17 +171,19 @@ describe('OAuth model settings scopes', () => {
const beforeFiles = structuredClone(files);
await saveOAuthModelSettings(await loadOAuthModelSettings(provider, api), ['*'], api);
expect(reads).toEqual([
{ path: '/model-definitions/codex', query: undefined },
{ path: '/oauth-excluded-models', query: undefined },
{ path: '/routing/model-definitions/codex', query: undefined },
{ path: '/config/oauth/excluded-models', query: undefined },
{ path: '/config/oauth/excluded-models', query: undefined },
]);
expect(writes).toEqual([
{ path: '/oauth-excluded-models', body: { provider: 'codex', models: ['*'] } },
{ path: '/config/oauth/excluded-models', body: { codex: ['*'], claude: ['claude-old-*'] } },
]);
expect(globalRules).toEqual({ codex: ['*'], claude: ['claude-old-*'] });
expect(files).toEqual(beforeFiles);
await saveOAuthModelSettings(await loadOAuthModelSettings(provider, api), [], api);
expect(deletes).toEqual([{ path: '/oauth-excluded-models', query: { provider: 'codex' } }]);
expect(writes).toHaveLength(1);
expect(deletes).toEqual([]);
expect(writes.at(-1)).toEqual({ path: '/config/oauth/excluded-models', body: { claude: ['claude-old-*'] } });
expect(writes).toHaveLength(2);
expect(files).toEqual(beforeFiles);
});
+1 -1
View File
@@ -22,7 +22,7 @@ describe('OAuth model exclusion rules', () => {
{ id: 'GPT-5.4' },
] })).toEqual([{ id: 'gpt-5.4', displayName: 'GPT 5.4' }]);
expect(oauthExcludedRulesFromPayload({
'oauth-excluded-models': { codex: [' GPT-IMAGE-* ', 'gpt-image-*'] },
codex: [' GPT-IMAGE-* ', 'gpt-image-*'],
}, 'codex')).toEqual(['gpt-image-*']);
expect(normalizeOAuthExcludedRules([' GPT-* ', '', 'gpt-*', 'future-model']))
.toEqual(['gpt-*', 'future-model']);
+1 -1
View File
@@ -23,7 +23,7 @@ import {
stripResponseFields,
} from '../src/pages/ApiAccessPage';
import { modelsFromRecord } from '../src/services/modelService';
import { flattenV8ProviderGroups } from '../src/services/managementApi';
import { flattenV8ProviderGroups } from './fixtures/legacyProviderRecords';
describe('shared-credential provider entries (#276)', () => {
const first = {
+2 -2
View File
@@ -19,7 +19,7 @@ beforeEach(() => {
calls = [];
handler = () => { throw new Error('Unexpected API request'); };
post = spyOn(managementApi, 'post').mockImplementation(async (path, body) => {
expect(path).toBe('/api-call');
expect(path).toBe('/requests/api-call');
const request = body as unknown as Request;
calls.push(request);
return await handler(request) as never;
@@ -110,7 +110,7 @@ describe('quota API compatibility', () => {
groups: [{ buckets: [{ remainingFraction: 1 }] }],
});
expect((await loadQuota({ name: 'anti-download.json', provider: 'antigravity', auth_index: 'a' })).status).toBe('success');
expect(get).toHaveBeenCalledWith('/auth-files/download', { name: 'anti-download.json' });
expect(get).toHaveBeenCalledWith('/credentials/download', { name: 'anti-download.json' });
expect(calls.find((request) => request.url.endsWith(':retrieveUserQuotaSummary'))?.data).toBe('{"project":"downloaded-project"}');
});
+8
View File
@@ -159,6 +159,11 @@ function parseCsv(text) {
}
await page.setViewportSize({ width: 1100, height: 700 });
// Existing installations may have explicitly enabled every old metric column.
await page.evaluate(() => localStorage.setItem('cpa-gui.usage-events-visible-cols.v3', JSON.stringify([
'time', 'key', 'source', 'model', 'effort', 'result', 'request', 'latency', 'speed', 'total', 'cache', 'provider',
'input', 'output', 'reasoning', 'cacheRate', 'ttft',
])));
await page.goto(`${base}/tests/fixtures/usage-layout.html?tab=events&locale=en`, { waitUntil: 'domcontentloaded' });
await page.locator('.usage-table-top-scrollbar:not(.is-hidden)').waitFor();
await page.locator('.usage-page-size-select').selectOption('200');
@@ -166,6 +171,8 @@ function parseCsv(text) {
assert.equal(await page.getByRole('heading', { name: 'Request Event Log' }).count(), 0, 'The redundant request log heading is removed');
const firstRow = page.locator('.usage-events-table tbody tr').first();
assert.equal(await page.locator('.usage-events-table th').count(), 12, 'Saved settings cannot restore removed duplicate columns');
assert.equal(await firstRow.locator('.usage-td-source svg, .usage-td-source img').count(), 0, 'Source is plain text without a logo');
assert.equal(await firstRow.locator('.usage-td-total').getAttribute('title'), '1,200 tokens', 'The displayed total uses the recorded total instead of adding cache and reasoning again');
assert.equal(await firstRow.locator('.tone-input').getAttribute('aria-label'), 'Input: 1000');
assert.equal(await firstRow.locator('.tone-output').getAttribute('aria-label'), 'Output: 200');
@@ -284,6 +291,7 @@ function parseCsv(text) {
await page.locator('.usage-col-settings-btn').click();
const checkboxes = page.locator('.usage-column-option input');
assert.equal(await checkboxes.count(), 12, 'Column settings contain only the remaining columns');
for (let index = 2; index < await checkboxes.count(); index += 1) await checkboxes.nth(index).uncheck();
await page.locator('.usage-column-dialog-actions .primary-button').click();
await page.waitForFunction(() => document.querySelector('.usage-table-top-scrollbar').classList.contains('is-hidden'));