79 lines
5.1 KiB
JavaScript
79 lines
5.1 KiB
JavaScript
// Provider configuration and transport declarations owned by the proxy module.
|
|
export const interfaceVersion = 1;
|
|
export const ZAI_URL = 'https://api.z.ai/api/anthropic';
|
|
export const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/;
|
|
const MODEL = MODEL_ID;
|
|
const MAX_MODELS = 10000;
|
|
const family = url => url === ZAI_URL ? 'zai' : 'other';
|
|
const harnesses = upstream => upstream === 'claude' ? ['claude-code', 'pi'] : ['pi'];
|
|
const slug = (upstream, model) => `${upstream}-${model}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/g, '');
|
|
|
|
export async function catalogue({ listAccounts, listModels, listGroups, now }) {
|
|
try {
|
|
const [accounts, groups] = await Promise.all([listAccounts(), listGroups()]);
|
|
if (!Array.isArray(accounts?.files) || !Array.isArray(groups) || accounts.files.length > MAX_MODELS || groups.length > MAX_MODELS) throw Error();
|
|
const models = [], notices = [], keys = new Map();
|
|
const add = (upstream, model) => {
|
|
if (typeof model !== 'string' || !MODEL.test(model)) throw Error();
|
|
const key = slug(upstream, model);
|
|
if (key.length > 64 || !/^[a-z][a-z0-9-]*$/.test(key)) throw Error();
|
|
const identity = JSON.stringify([upstream, model]);
|
|
if (keys.has(key) && keys.get(key) !== identity) throw Object.assign(Error(), { code: 'model-key-collision' });
|
|
if (keys.has(key)) return;
|
|
keys.set(key, identity);
|
|
models.push({ key, module: 'cliproxy', upstream, provider: upstream === 'claude' ? 'anthropic' : upstream, model, label: `${upstream} / ${model}`, available: true,
|
|
reason: upstream === 'other' || upstream === 'codex' ? 'unverified' : null, harnesses: harnesses(upstream) });
|
|
if (models.length > MAX_MODELS) throw Error();
|
|
};
|
|
for (const row of accounts.files) {
|
|
if (!['claude', 'codex'].includes(row?.provider) || row.disabled === true || row.unavailable === true || !['ready', 'active'].includes(row.status)) continue;
|
|
if (typeof row.name !== 'string' || !row.name || row.name.length > 512) throw Error();
|
|
const response = await listModels(row.name);
|
|
if (!Array.isArray(response?.models) || response.models.length > MAX_MODELS) throw Error();
|
|
for (const model of response.models) add(row.provider, model?.id);
|
|
}
|
|
for (const group of groups) {
|
|
if (!Array.isArray(group?.keys)) throw Error();
|
|
if (group.disabled === true || !group.keys.some(key => key && key.disabled !== true)) continue;
|
|
const upstream = family(group['base-url']);
|
|
if (group.models === undefined || (Array.isArray(group.models) && !group.models.length)) {
|
|
if (upstream === 'zai') notices.push('z.ai account has no models configured. Add them on Model proxy.');
|
|
continue;
|
|
}
|
|
if (!Array.isArray(group.models) || group.models.length > MAX_MODELS) throw Error();
|
|
for (const model of group.models) add(upstream, model?.alias || model?.name);
|
|
}
|
|
return { status: 'ready', observedAt: now(), models, notices: [...new Set(notices)] };
|
|
} catch (error) {
|
|
return { status: 'unavailable', observedAt: null, models: [], notices: [], ...(error.code === 'model-key-collision' ? { reason: error.code } : {}) };
|
|
}
|
|
}
|
|
export function preflight({ selection, harness, catalogue: c }) {
|
|
if (c?.status !== 'ready') return { allow: false, code: 'model-catalogue-unavailable' };
|
|
const model = c.models.find(m => m.upstream === selection?.upstream && m.model === selection?.model);
|
|
if (!model?.available) return { allow: false, code: 'model-provider-unavailable' };
|
|
if (!model.harnesses.includes(harness)) return { allow: false, code: 'model-harness-incompatible' };
|
|
return { allow: true };
|
|
}
|
|
|
|
// Declarative transport only: credentials and endpoints are supplied by the host.
|
|
const ROUTES = Object.freeze({
|
|
claude: { routeId: 'anthropic', endpoint: 'root', api: 'anthropic-messages', piProvider: 'anthropic', compat: null },
|
|
zai: { routeId: 'glm', endpoint: 'v1', api: 'openai-completions', piProvider: 'mosaic-proxy-glm', compat: { thinkingFormat: 'zai', zaiToolStream: true } },
|
|
codex: { routeId: 'codex', endpoint: 'v1', api: 'openai-responses', piProvider: 'mosaic-proxy-codex', compat: null },
|
|
other: { routeId: 'anthropic', endpoint: 'root', api: 'anthropic-messages', piProvider: 'anthropic', compat: null },
|
|
});
|
|
export function route({ selection, harness }) {
|
|
let upstream = selection?.upstream;
|
|
if (!upstream && selection?.provider === 'proxy') {
|
|
if (!selection.route) return { code: 'proxy-route-missing' };
|
|
upstream = ({ anthropic: 'claude', glm: 'zai', codex: 'codex' })[selection.route];
|
|
if (!upstream) return { code: 'proxy-route-unknown' };
|
|
}
|
|
if (!upstream) upstream = ({ anthropic: 'claude', zai: 'zai' })[selection?.provider];
|
|
if (!Object.hasOwn(ROUTES, upstream ?? '')) return { code: 'proxy-provider-unsupported' };
|
|
if (harness && !harnesses(upstream).includes(harness)) return { code: 'model-harness-incompatible' };
|
|
if (typeof selection.model !== 'string' || !MODEL.test(selection.model)) return { code: 'model-provider-unavailable' };
|
|
return { provider: selection.provider ?? upstream, model: selection.model, upstream, ...ROUTES[upstream] };
|
|
}
|