- Add structured editors for request, OAuth, plugin, and concurrency settings - Expand advanced configuration for all eight provider families and credentials - Preserve YAML comments, secrets, unknown fields, and explicit overrides - Add validation, conflict detection, default restoration, and restart notices - Improve responsive layouts and localize configuration controls - Fix provider health checks and extend mocks and integration tests
129 lines
8.5 KiB
TypeScript
129 lines
8.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { buildProviderGroupRecord, providerDraftFromRecord, type ProviderSection } from '../src/pages/ApiAccessPage';
|
|
import { appendNativeProviderGroup, validateProviderTemplateRecord, validateProviderGroupKeys, providerKeyDraft } from '../src/services/providerGroups';
|
|
import { modelEndpointCandidates, modelsFromRecord } from '../src/services/modelService';
|
|
import { buildProviderHealthProbe } from '../src/services/providerHealthCheck';
|
|
|
|
const sections: ProviderSection[] = ['gemini-api-key', 'interactions-api-key', 'vertex-api-key', 'codex-api-key', 'claude-api-key', 'xai-api-key', 'meta-api-key', 'openai-compatibility'];
|
|
describe('v8 provider template fields', () => {
|
|
for (const section of sections) it(`${section} preserves unknown data, explicit defaults and inheritance`, () => {
|
|
const group = {
|
|
name: 'production', priority: 0, headers: {}, 'request-retry': -1, 'request-scoped-errors': [],
|
|
extension: { future: false }, models: null,
|
|
keys: [{ 'api-key': 'first', weight: -2, 'request-scoped-errors': null, models: [], headers: null, 'disable-cooling': false },
|
|
{ 'api-key': 'second', weight: 0, 'proxy-url': '', extension: { opaque: 0 } }],
|
|
};
|
|
const draft = providerDraftFromRecord(section, group);
|
|
draft.name = 'renamed';
|
|
expect(buildProviderGroupRecord(section, draft, group)).toEqual({ ...group, name: 'renamed' });
|
|
});
|
|
|
|
it('edits group error rules separately from key overrides without merging lists', () => {
|
|
const group = { name: 'gateway', 'request-retry': 5,
|
|
'request-scoped-errors': [{ status: 400, match: ['old'], action: 'continue', opaque: { keep: true } }],
|
|
keys: [{ 'api-key': 'inherited', 'request-scoped-errors': null }, { 'api-key': 'cleared', 'request-scoped-errors': [] }] };
|
|
const draft = providerDraftFromRecord('codex-api-key', group);
|
|
draft.templateFields!['request-retry'] = 0;
|
|
(draft.templateFields!['request-scoped-errors'] as any[])[0].match = [];
|
|
const saved = buildProviderGroupRecord('codex-api-key', draft, group);
|
|
expect(saved['request-retry']).toBe(0);
|
|
expect(saved['request-scoped-errors']).toEqual([{ ...group['request-scoped-errors'][0], match: [] }]);
|
|
expect(saved.keys).toEqual(group.keys);
|
|
delete draft.templateFields!['request-retry'];
|
|
expect(buildProviderGroupRecord('codex-api-key', draft, group)).not.toHaveProperty('request-retry');
|
|
});
|
|
|
|
it('supports explicit model capabilities and removal without dropping future metadata or pooled aliases', () => {
|
|
const models = [
|
|
{ name: 'model-a', alias: 'pooled', 'display-name': 'A', 'max-context-length': 32768, 'is-compat': true,
|
|
thinking: { levels: ['low', 'high'], 'zero-allowed': false, vendor: 1 }, custom: { preserve: [] } },
|
|
{ name: 'model-b', alias: 'pooled', image: false, 'input-modalities': ['text', 'image'], 'output-modalities': ['text'] },
|
|
];
|
|
const group = { name: 'gateway', models, keys: [{ 'api-key': 'key' }] };
|
|
const draft = providerDraftFromRecord('openai-compatibility', group);
|
|
const first = draft.models[0];
|
|
first.config = { ...first.config, 'is-compat': false, 'max-context-length': 0, 'display-name': '' };
|
|
delete first.config.thinking;
|
|
delete first.thinking;
|
|
const second = draft.models[1];
|
|
second.config = { ...second.config, 'input-modalities': [], 'output-modalities': [], 'use-max-completion-tokens': false };
|
|
const saved = buildProviderGroupRecord('openai-compatibility', draft, group);
|
|
expect(saved.models).toEqual([
|
|
{ name: 'model-a', alias: 'pooled', 'display-name': '', 'max-context-length': 0, 'is-compat': false, custom: { preserve: [] } },
|
|
{ ...models[1], 'input-modalities': [], 'output-modalities': [], 'use-max-completion-tokens': false },
|
|
]);
|
|
});
|
|
|
|
it('retains per-key provider options when rotating the credential', () => {
|
|
for (const options of [
|
|
{ 'disable-codex-cloaking': false, 'alpha-search': true, websockets: false },
|
|
{ 'rebuild-mid-system-message': false, 'fingerprint-profile': '', 'experimental-cch-signing': false,
|
|
cloak: { mode: 'never', 'strict-mode': false, 'sensitive-words': [], 'cache-user-id': false, custom: {} } },
|
|
]) {
|
|
const group = { name: 'gateway', keys: [{ 'api-key': 'old', ...options }] };
|
|
const draft = providerDraftFromRecord('claude-api-key', group);
|
|
draft.groupKeys![0].value['api-key'] = 'new';
|
|
expect(buildProviderGroupRecord('claude-api-key', draft, group).keys).toEqual([{ 'api-key': 'new', ...options }]);
|
|
}
|
|
});
|
|
|
|
it('rejects invalid retry/error/model values and accepts negative weights that disable weighted routing', () => {
|
|
for (const value of [
|
|
{ 'request-retry': 1.5 }, { 'request-scoped-errors': [{ status: 600, action: 'stop' }] },
|
|
{ 'request-scoped-errors': [{ status: 400, action: 'retry' }] },
|
|
{ models: [{ name: 'm', 'max-context-length': -1 }] },
|
|
{ models: [{ name: 'm', thinking: { min: 500, max: 200 } }] },
|
|
{ keys: [{ models: [{ name: 'm', thinking: { levels: [2] } }] }] },
|
|
]) expect(validateProviderTemplateRecord(value)).not.toBeNull();
|
|
expect(validateProviderTemplateRecord({ 'request-retry': -1, keys: [{ weight: -1 }] })).toBeNull();
|
|
expect(validateProviderGroupKeys([providerKeyDraft({ 'api-key': 'k', weight: -1 })])).toBeNull();
|
|
});
|
|
|
|
it('EasyMode appends one native group without flattening or losing existing overrides', () => {
|
|
const existing = [{ name: 'codex', opaque: { keep: null }, models: [{ name: 'm', thinking: { levels: [] } }], keys: [
|
|
{ 'api-key': 'a', weight: 5, headers: null, models: null },
|
|
{ 'api-key': 'b', weight: 0, headers: {}, models: [], 'request-retry': 0 },
|
|
] }, { name: 'codex (2)', keys: [] }];
|
|
const snapshot = structuredClone(existing);
|
|
const next = appendNativeProviderGroup(existing, { name: 'codex', keys: [{ 'api-key': 'c' }] });
|
|
expect(next.slice(0, 2)).toEqual(snapshot);
|
|
expect(next[2]).toEqual({ name: 'codex (3)', keys: [{ 'api-key': 'c' }] });
|
|
expect(existing).toEqual(snapshot);
|
|
});
|
|
|
|
it('retains full model metadata only when requested for the configuration editor', () => {
|
|
const record = { name: 'm', 'support-configuration-update': false, thinking: { zero_allowed: false, levels: [] }, unknown: [] };
|
|
expect(modelsFromRecord([record], true)[0].config).toEqual({ ...record, thinking: { 'zero-allowed': false, levels: [] } });
|
|
expect(modelsFromRecord([record])[0]).not.toHaveProperty('config');
|
|
});
|
|
});
|
|
|
|
describe('new native provider connections', () => {
|
|
it('uses native model discovery defaults', () => {
|
|
expect(modelEndpointCandidates('interactions', '')).toEqual(['https://generativelanguage.googleapis.com/v1beta/models']);
|
|
expect(modelEndpointCandidates('vertex', '')[0]).toBe('https://aiplatform.googleapis.com/v1/publishers/google/models');
|
|
expect(modelEndpointCandidates('xai', '')).toEqual(['https://api.x.ai/v1/models']);
|
|
expect(modelEndpointCandidates('meta', '')).toEqual(['https://api.meta.ai/v1/models']);
|
|
});
|
|
it('probes Interactions and Vertex with Google keys and their native endpoints', () => {
|
|
const interactions = buildProviderHealthProbe('interactions', '', 'models/gemini-3-pro-preview', 'google-key');
|
|
expect(interactions.url).toBe('https://generativelanguage.googleapis.com/v1beta/interactions');
|
|
expect(interactions.protocol).toBe('interactions');
|
|
expect(interactions.header['x-goog-api-key']).toBe('google-key');
|
|
expect(JSON.parse(interactions.data)).toEqual({ model: 'gemini-3-pro-preview', input: 'hi', stream: true });
|
|
const vertex = buildProviderHealthProbe('vertex', '', 'gemini-3-pro-preview', 'vertex-key');
|
|
expect(vertex.url).toBe('https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3-pro-preview:streamGenerateContent?alt=sse');
|
|
expect(vertex.header['x-goog-api-key']).toBe('vertex-key');
|
|
});
|
|
it('uses native Responses defaults for xAI and Meta', () => {
|
|
for (const [provider, host] of [['xai', 'api.x.ai'], ['meta', 'api.meta.ai']] as const) {
|
|
const probe = buildProviderHealthProbe(provider, '', 'native-model', 'key');
|
|
expect(probe.url).toBe(`https://${host}/v1/responses`);
|
|
expect(probe.protocol).toBe('openai-responses');
|
|
expect(probe.header.Authorization).toBe('Bearer key');
|
|
expect(buildProviderHealthProbe(provider, 'https://gateway.test/api', 'native-model', 'key').url).toBe('https://gateway.test/api/responses');
|
|
expect(modelEndpointCandidates(provider, 'https://gateway.test/api')).toEqual(['https://gateway.test/api/models']);
|
|
}
|
|
});
|
|
});
|