fix: handle missing v8 config nodes and normalize alias transaction views

- Default absent optional config nodes to empty values without hiding other errors
- Normalize empty parent mappings to prevent alias save and rollback conflicts
- Add regression tests for missing config, first-time alias creation, and rollback
This commit is contained in:
lzt404 committed 2026-09-27 20:03:33 +08:00
1 parent 02d5a2d469
commit c52039dfed
21 files changed
+1271 -193

No files matched your search

+73 -9
View File
@@ -21,6 +21,16 @@ const PROVIDER_SECTIONS = [
'claude-api-key',
'openai-compatibility',
] as const;
const V8_PROVIDER_BY_SECTION = {
'gemini-api-key': 'gemini',
'codex-api-key': 'codex',
'claude-api-key': 'claude',
'openai-compatibility': 'openai-compatibility',
} as const;
const SHARED_PROVIDER_FIELDS = new Set([
'priority', 'prefix', 'proxy-url', 'headers', 'models', 'excluded-models',
'disable-cooling', 'request-retry', 'request-scoped-errors',
]);
const clone = <Value,>(value: Value): Value => structuredClone(value);
const sleep = (delayMs: number) => delayMs > 0
@@ -640,8 +650,53 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
const query = asObject(request.query);
const body = request.body;
const v8Provider = Object.entries(V8_PROVIDER_BY_SECTION).find(
([, provider]) => path === `/config/api-keys/${provider}`,
);
const groupRecords = (section: (typeof PROVIDER_SECTIONS)[number], records: JsonObject[]) => records.map((record, index) => {
if (section === 'openai-compatibility') {
const group: JsonObject = { ...record, keys: asArray(record['api-key-entries']).map((item) => clone(asObject(item))) };
delete group['api-key-entries'];
return group;
}
const group: JsonObject = { name: `${V8_PROVIDER_BY_SECTION[section]}-${index + 1}` };
const key: JsonObject = {};
Object.entries(record).forEach(([field, value]) => {
if (field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)) group[field] = value;
else key[field] = value;
});
group.keys = [key];
return group;
});
const flattenGroups = (section: (typeof PROVIDER_SECTIONS)[number], groups: unknown) => asArray(groups).flatMap((item) => {
const group = asObject(item);
const keys = asArray(group.keys).map(asObject);
if (section === 'openai-compatibility') {
const record: JsonObject = { ...group };
delete record.keys;
if (keys.length > 0) record['api-key-entries'] = keys;
return [record];
}
const shared = Object.fromEntries(Object.entries(group).filter(([field]) => field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)));
return keys.map((key) => ({ ...shared, ...Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null)) }));
});
if (method === 'GET' && path === '/config') {
return clone(state.providerConfig);
return {
'config-version': 8,
'api-keys': Object.fromEntries(PROVIDER_SECTIONS.map((section) => [
V8_PROVIDER_BY_SECTION[section],
groupRecords(section, state.providerConfig[section]),
])),
};
}
if (v8Provider) {
const section = v8Provider[0] as (typeof PROVIDER_SECTIONS)[number];
if (method === 'GET') return groupRecords(section, state.providerConfig[section]);
if (method === 'PUT') {
state.providerConfig[section] = flattenGroups(section, body).map((item) => clone(asObject(item)));
return { status: 'ok', 'config-version': 8 };
}
}
if (PROVIDER_SECTIONS.some((section) => path === `/${section}`)) {
const section = path.slice(1) as (typeof PROVIDER_SECTIONS)[number];
@@ -658,15 +713,24 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
return { [section]: clone(state.providerConfig[section]) };
}
}
if (method === 'GET' && path === '/auth-files') {
if (method === 'GET' && path === '/credentials') {
return { files: clone(state.authFiles), observed_at: new Date().toISOString() };
}
if (method === 'GET' && path === '/auth-files/download') {
if (method === 'GET' && path === '/credentials/download') {
const file = findAuthFile(state, readString(query.name));
return clone(file ?? { name: query.name, excluded_models: [] });
}
if (method === 'GET' && path === '/auth-files/models') return mockModelDefinitions();
if (method === 'GET' && path.startsWith('/model-definitions/')) return mockModelDefinitions();
if (method === 'GET' && path === '/credentials/models') return mockModelDefinitions();
if (method === 'GET' && path.startsWith('/routing/model-definitions/')) return mockModelDefinitions();
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];
return { status: 'ok', 'config-version': 8 };
}
if (method === 'GET' && path === '/oauth-excluded-models') {
return { 'oauth-excluded-models': clone(state.oauthExcludedModels) };
}
@@ -679,19 +743,19 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
delete state.oauthExcludedModels[readString(query.provider)];
return null;
}
if (method === 'PATCH' && (path === '/auth-files/fields' || path === '/auth-files/status')) {
if (method === 'PATCH' && (path === '/credentials/fields' || path === '/credentials/status')) {
const patch = asObject(body);
const file = findAuthFile(state, readString(patch.name));
if (file) Object.assign(file, patch);
return clone(file ?? null);
}
if (method === 'DELETE' && path === '/auth-files') {
if (method === 'DELETE' && path === '/credentials') {
const index = state.authFiles.findIndex((file) => file.name === query.name);
if (index >= 0) state.authFiles.splice(index, 1);
return null;
}
if (method === 'POST' && path === '/api-call') return mockApiCall(asObject(body));
if (method === 'DELETE' && path === '/oauth-session') return null;
if (method === 'POST' && path === '/requests/api-call') return mockApiCall(asObject(body));
if (method === 'DELETE' && path === '/oauth/session') return null;
return {};
}
+183 -3
View File
@@ -9,6 +9,108 @@ 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];
async function optionalConfigValue(
path: string, fallback: ManagementJson, options: ManagementRequestOptions,
): Promise<unknown> {
try {
return await invoke<unknown>('management_request', {
request: { method: 'GET', path, query: normalizeQuery(options.query), timeoutMs: options.timeoutMs },
});
} catch (error) {
// Only an absent v8 config node is optional, not an unavailable endpoint or server.
const message = error instanceof Error ? error.message : error;
if (message === 'Management API error (404): not_found') return fallback;
throw error;
}
}
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;
if (keys.length > 0) record['api-key-entries'] = keys.map((key) => ({ ...key }));
return [record];
}
const shared = Object.fromEntries(
Object.entries(group).filter(([key]) => key === 'base-url' || SHARED_PROVIDER_FIELDS.has(key)),
);
return keys.map((key) => {
const overrides = Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null));
return { ...shared, ...overrides };
});
});
}
export function groupLegacyProviderRecords(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).map((record, index) => {
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'];
return group;
}
const group: Record<string, unknown> = { name: `${provider}-${index + 1}` };
const key: Record<string, unknown> = {};
Object.entries(record).forEach(([field, value]) => {
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 => {
@@ -29,15 +131,93 @@ async function request<T = ManagementJson>(
path: string,
options: ManagementRequestOptions = {},
): Promise<T> {
return invoke<T>('management_request', {
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', {
request: {
method,
path,
path: apiPath,
query: normalizeQuery(options.query),
body: options.body,
body,
timeoutMs: options.timeoutMs,
},
});
if (method === 'GET' && path === '/config') {
return legacyManagementConfigView(payload) as T;
}
return payload as T;
}
export const managementApi = {