Files

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] };
}