refactor: extract provider logic into modular registry
Each AI provider now has its own module implementing a ProviderModule interface with createGateway, getAuthHeaders, and fetchModels methods. This replaces the monolithic switch statements in ai-provider.ts and models.post.ts with a clean registry pattern.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { sortByReleaseDate } from '~/utils/sort';
|
||||
import { encryptData, decrypt, uint8ArrayToBase64, base64ToUint8Array } from '~/utils/crypto';
|
||||
import { providerBaseUrls, Providers, type Model } from '~/types/model';
|
||||
import { type Model } from '~/types/model';
|
||||
import ModelItem from './ModelItem.vue';
|
||||
import RowVirtualizerDynamic from '../RowVirtualizerDynamic.vue';
|
||||
|
||||
@@ -31,10 +31,10 @@ const modelSearch = ref('');
|
||||
|
||||
watch(() => props.params, () => {
|
||||
if (props.params === undefined) return;
|
||||
console.log(scrollContainerRef.value);
|
||||
apiKey.value = provider.value?.config.apiKey ?? '';
|
||||
apiProxyUrl.value = provider.value?.config.apiProxyUrl ?? '';
|
||||
modelSearch.value = '';
|
||||
apiKeyVisible.value = false;
|
||||
|
||||
nextTick(() => {
|
||||
if (scrollContainerRef.value) {
|
||||
@@ -339,7 +339,7 @@ const saveCustomModel = async () => {
|
||||
if (editingModel.value) {
|
||||
await updateModel(editingModel.value!.id, { ...previewModelWithoutId });
|
||||
} else {
|
||||
await createModel(previewModelWithoutId);
|
||||
await createModel(previewModelWithoutId as Model);
|
||||
}
|
||||
|
||||
showAddModelPanel.value = false;
|
||||
@@ -403,7 +403,7 @@ defineEmits(['navigate']);
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
<label class="whitespace-nowrap" for="provider-api-key">API Proxy URL</label>
|
||||
<div class="text-sm font-mono flex flex-row rounded-md bg-[var(--bg-container)] items-center gap-1 w-7/10">
|
||||
<input :placeholder="provider.type ? providerBaseUrls[provider.type as typeof Providers[number]] : ''"
|
||||
<input :placeholder="provider.defaultBaseUrl || ''"
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full px-2 py-1 bg-transparent" type="text"
|
||||
id="provider-proxy-url" :value="apiProxyUrl"
|
||||
@input="updateProxyUrl(($event.target! as HTMLInputElement).value)" />
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
import * as schema from '~~/drizzle/schema';
|
||||
|
||||
export const Providers = [
|
||||
'openrouter',
|
||||
'ollama',
|
||||
'vllm',
|
||||
'cerebras',
|
||||
'google',
|
||||
'longcat',
|
||||
'cohere',
|
||||
'inception',
|
||||
'mistral',
|
||||
'closedrouter',
|
||||
] as const;
|
||||
export const SupportedModalities = ['text', 'image', 'audio', 'video', 'pdf'] as const;
|
||||
|
||||
export const providerBaseUrls = {
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
vllm: '',
|
||||
ollama: '',
|
||||
cerebras: 'https://api.cerebras.ai/v1',
|
||||
google: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
longcat: 'https://api.longcat.chat/openai/v1',
|
||||
cohere: 'https://api.cohere.ai/v2',
|
||||
inception: 'https://api.inceptionlabs.ai/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
closedrouter: 'https://router.queef.in/v1',
|
||||
};
|
||||
|
||||
export type Model = typeof schema.models.$inferSelect;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import * as z from 'zod';
|
||||
import { SupportedModalities } from '~/types/model';
|
||||
import { Providers } from '~/types/model';
|
||||
import { GatewayFetchError, getProviderDetails } from '~~/server/utils/ai-provider';
|
||||
import { getProvider } from '~~/server/utils/providers';
|
||||
import { getModelsDevData } from '~~/server/utils/models-dev';
|
||||
import { Err, Ok, type Result } from '~~/types/result';
|
||||
import { db } from '~~/server/lib/db';
|
||||
import { type Provider } from '~/composables/useModels';
|
||||
import { models } from '~~/drizzle/schema';
|
||||
import { and, eq, notInArray } from 'drizzle-orm';
|
||||
|
||||
@@ -34,40 +30,27 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
if (!provider) throw createError({ statusCode: 404, message: 'Provider not found' });
|
||||
|
||||
const [providerModelsRes, modelsDevRes] = await Promise.all([
|
||||
fetchProviderModels(provider, result.data.providerApiKey),
|
||||
getModelsDevData(),
|
||||
]);
|
||||
|
||||
if (!providerModelsRes.ok) {
|
||||
switch (providerModelsRes.error) {
|
||||
case ProviderFetchError.NoProviderApiKey: {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `${provider.type} provider requires an API key`,
|
||||
});
|
||||
}
|
||||
case ProviderFetchError.NoProviderBaseUrl: {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Invalid provider URL',
|
||||
});
|
||||
}
|
||||
}
|
||||
const providerModule = getProvider(provider.type);
|
||||
if (!providerModule) {
|
||||
throw createError({ statusCode: 400, message: `Unknown provider type: ${provider.type}` });
|
||||
}
|
||||
|
||||
const normalizedModels = await normalizeResponse(
|
||||
providerModelsRes.data.data,
|
||||
provider.type as typeof Providers[number],
|
||||
providerModelsRes.data.baseURL,
|
||||
modelsDevRes
|
||||
);
|
||||
let baseURL = undefined;
|
||||
if (provider.config.apiProxyUrl && provider.config.apiProxyUrl.trim() !== '') {
|
||||
baseURL = provider.config.apiProxyUrl;
|
||||
} else {
|
||||
baseURL = providerModule.baseUrl;
|
||||
}
|
||||
baseURL = baseURL.replace(/\/$/, '');
|
||||
|
||||
const normalizedModels = await providerModule.fetchModels({
|
||||
baseURL,
|
||||
apiKey: result.data.providerApiKey,
|
||||
modelsDevData: await getModelsDevData(),
|
||||
})
|
||||
const existingModels = provider.models || [];
|
||||
const apiModelExternalIds = normalizedModels.map((m: any) => m.id);
|
||||
|
||||
console.log({ existingModels, apiModelExternalIds, normalizedModels });
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
if (existingModels.length > 0) {
|
||||
await tx.delete(models)
|
||||
@@ -127,561 +110,3 @@ export default defineEventHandler(async (event) => {
|
||||
models: updatedModels
|
||||
};
|
||||
});
|
||||
|
||||
enum ProviderFetchError {
|
||||
NoProviderApiKey = 0,
|
||||
NoProviderBaseUrl,
|
||||
}
|
||||
|
||||
const fetchProviderModels = async (provider: Provider, providerApiKey: string | undefined): Promise<Result<{ data: Record<string, any>, baseURL: string }, ProviderFetchError>> => {
|
||||
const providerDetails = await getProviderDetails(provider, providerApiKey);
|
||||
if (!providerDetails.ok) {
|
||||
switch (providerDetails.error) {
|
||||
case GatewayFetchError.NoProviderApiKey: {
|
||||
return Err(ProviderFetchError.NoProviderApiKey);
|
||||
}
|
||||
case GatewayFetchError.NoProviderBaseUrl: {
|
||||
return Err(ProviderFetchError.NoProviderBaseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { endpoint: { baseURL, modelsEndpoint, headers } } = providerDetails.data;
|
||||
|
||||
switch (provider.type) {
|
||||
case 'longcat':
|
||||
// longcat doesn't have a model list endpoint so we just hardcode them here,
|
||||
// sry. I talked to Meituan and this is what they said:
|
||||
// 后续如果我们新增了这样的接口会及时同步您。
|
||||
// en (approx): If we add an interface like this in the future, we will keep you in sync
|
||||
return Ok({
|
||||
data: {
|
||||
models: [
|
||||
{
|
||||
id: "LongCat-Flash-Chat",
|
||||
name: "LongCat Flash Chat",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Thinking-2601",
|
||||
name: "LongCat Flash Thinking (2601)",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['reasoning', 'tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Omni-2603",
|
||||
name: "LongCat Flash Omni",
|
||||
attributes: {
|
||||
inputModalities: ['text', 'image', 'audio', 'video'],
|
||||
outputModalities: ['text', 'audio'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Lite",
|
||||
name: "LongCat Flash Lite",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 320_000,
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
baseURL
|
||||
});
|
||||
case 'inception':
|
||||
return Ok({
|
||||
data: {
|
||||
models: [
|
||||
{
|
||||
id: "mercury-2",
|
||||
name: "Mercury 2",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools', 'reasoning'],
|
||||
contextWindow: 128_000,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
baseURL
|
||||
});
|
||||
}
|
||||
|
||||
let res;
|
||||
let data;
|
||||
try {
|
||||
res = await fetch(`${baseURL}${modelsEndpoint}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
data = await res.json();
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch models:', e);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Failed to fetch models ' + e,
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({
|
||||
statusCode: res.status,
|
||||
message: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
return Ok({ data, baseURL });
|
||||
}
|
||||
|
||||
function mergeSets(setA: Set<string>, setB: Set<string>): string[] {
|
||||
return Array.from(new Set([...setA, ...setB]));
|
||||
}
|
||||
|
||||
const getModelData = (modelId: string, providerId: string, modelsDevData: any) => {
|
||||
const modelData = modelsDevData[providerId]?.models[modelId];
|
||||
|
||||
if (modelData === undefined) return {
|
||||
cost: {},
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: [],
|
||||
}
|
||||
};
|
||||
|
||||
const capabilities = new Array<string>();
|
||||
if (modelData.reasoning) {
|
||||
capabilities.push('reasoning');
|
||||
}
|
||||
|
||||
if (modelData.tool_call) {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
|
||||
let inputModalities = modelData.modalities.input.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
let outputModalities = modelData.modalities.output.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
let supportedParameters = [];
|
||||
if (modelData.temperature) {
|
||||
supportedParameters.push('temperature');
|
||||
}
|
||||
|
||||
let contextWindow = modelData.limit?.context || null;
|
||||
let cost: Record<string, string> = {};
|
||||
for (const key in modelData.cost) {
|
||||
switch (key) {
|
||||
case 'input': {
|
||||
cost.prompt = formatMoney(modelData.cost[key].toString());
|
||||
} break;
|
||||
case 'output': {
|
||||
cost.completion = formatMoney(modelData.cost[key].toString());
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
let releasedAt = (new Date(modelData.release_date)).getTime();
|
||||
|
||||
return {
|
||||
name: modelData.name,
|
||||
attributes: {
|
||||
inputModalities,
|
||||
outputModalities,
|
||||
capabilities: capabilities || [],
|
||||
contextWindow,
|
||||
supported_parameters: modelData.supportedParameters,
|
||||
},
|
||||
cost,
|
||||
releasedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// this function multiplies a string in the format of 'D.DD' by 1_000_000
|
||||
// it does this by finding the first digit that is not a zero, and then
|
||||
// left shifting it in decimal by 3 places
|
||||
const lshDecimal = (number: string, shift: number) => {
|
||||
if (number.length === 0) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
let value = '';
|
||||
let isNegative = number[0] === '-';
|
||||
let [integerPart, fractionalPart] = number.substring(isNegative ? 1 : 0).split('.');
|
||||
|
||||
if (!fractionalPart) {
|
||||
fractionalPart = '';
|
||||
}
|
||||
|
||||
if (shift <= fractionalPart.length) {
|
||||
integerPart += fractionalPart.substring(0, shift);
|
||||
fractionalPart = fractionalPart.substring(shift);
|
||||
} else if (shift > fractionalPart.length) {
|
||||
integerPart += fractionalPart;
|
||||
for (let i = 0; i < shift - fractionalPart.length; i++) {
|
||||
integerPart += '0';
|
||||
}
|
||||
fractionalPart = '';
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
value += '-';
|
||||
}
|
||||
|
||||
integerPart = integerPart!.replace(/^0+/, '');
|
||||
if (integerPart.length === 0) {
|
||||
integerPart = '0';
|
||||
}
|
||||
|
||||
value += integerPart;
|
||||
|
||||
if (fractionalPart.length > 0) {
|
||||
if (fractionalPart.length < 2) {
|
||||
fractionalPart += '0';
|
||||
}
|
||||
|
||||
value += '.' + fractionalPart;
|
||||
} else {
|
||||
value += '.00';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const formatMoney = (value: string) => {
|
||||
// ensure that there are two decimal places MINIMUM I WILL STRANGLE YOU SO HELP ME GOD
|
||||
const [integerPart, fractionalPart] = value.split('.');
|
||||
|
||||
if (fractionalPart === undefined) {
|
||||
return `${integerPart}.00`;
|
||||
}
|
||||
|
||||
if (fractionalPart.length === 1) {
|
||||
return `${integerPart}.${fractionalPart}0`;
|
||||
}
|
||||
|
||||
return `${integerPart}.${fractionalPart}`;
|
||||
}
|
||||
|
||||
const normalizeResponse = async (response: Record<string, any>, provider: typeof Providers[number], baseUrl: string, modelsDevData: any): Promise<any[]> => {
|
||||
switch (provider) {
|
||||
case 'cerebras': {
|
||||
console.log("response.data", response.data);
|
||||
for (const model of response.data) {
|
||||
console.log(model.created);
|
||||
}
|
||||
|
||||
return response.data.map((model: any) => ({ ...getModelData(model.id, provider, modelsDevData), id: model.id }));
|
||||
}
|
||||
case 'openrouter': {
|
||||
const models = [];
|
||||
|
||||
for (const model of response.data) {
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of model.supported_parameters) {
|
||||
switch (capability) {
|
||||
case 'reasoning': {
|
||||
capabilities.add('reasoning');
|
||||
} break;
|
||||
case 'tools': {
|
||||
capabilities.add('tools');
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
const pricing = {} as Record<string, string>;
|
||||
for (const key of Object.keys(model.pricing)) {
|
||||
// normalize to /1M tokens since models.dev is already in /1M tokens
|
||||
|
||||
switch (key) {
|
||||
case 'prompt':
|
||||
case 'completion':
|
||||
case 'image':
|
||||
case 'audio':
|
||||
case 'discount':
|
||||
pricing[key] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'request':
|
||||
pricing['request'] = lshDecimal(model.pricing[key], 3);
|
||||
break;
|
||||
case 'image_tokens':
|
||||
pricing['imageTokens'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'image_output':
|
||||
pricing['imageOutput'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'audio_output':
|
||||
pricing['audioOutput'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_audio_cache':
|
||||
pricing['inputAudioCache'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'web_search':
|
||||
pricing['webSearch'] = lshDecimal(model.pricing[key], 3);
|
||||
break;
|
||||
case 'internal_reasoning':
|
||||
pricing['internalReasoning'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_cache_read':
|
||||
pricing['inputCacheRead'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_cache_write':
|
||||
pricing['inputCacheWrite'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: do I really need to pull in data from models.dev for OR models?
|
||||
// const modelData = getModelData(model.id, provider, modelsDevData);
|
||||
|
||||
models.push({
|
||||
id: model.id as string,
|
||||
name: model.name as string,
|
||||
cost: pricing,
|
||||
attributes: {
|
||||
inputModalities: model.architecture.input_modalities.filter((modality: string) => (SupportedModalities as Readonly<string[]>).includes(modality)),
|
||||
outputModalities: model.architecture.output_modalities.filter((modality: string) => (SupportedModalities as Readonly<string[]>).includes(modality)),
|
||||
capabilities: Array.from(capabilities),
|
||||
contextWindow: model.context_length,
|
||||
supported_parameters: model.supported_parameters,
|
||||
},
|
||||
releasedAt: model.created * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
case 'closedrouter': {
|
||||
return response.data.map((model: any) => {
|
||||
const capabilities = new Array<string>();
|
||||
|
||||
if (model.capabilities) {
|
||||
if (model.capabilities.reasoning) {
|
||||
capabilities.push('reasoning');
|
||||
}
|
||||
|
||||
if (model.capabilities.tool_call) {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
attributes: {
|
||||
inputModalities: model.modalities?.input || ['text'],
|
||||
outputModalities: model.modalities?.output || ['text'],
|
||||
capabilities: capabilities,
|
||||
contextWindow: model.context_window,
|
||||
supported_parameters: [],
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'ollama': {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0'
|
||||
}
|
||||
|
||||
if (response.token) {
|
||||
headers['Authorization'] = `Bearer ${response.token}`;
|
||||
}
|
||||
|
||||
const models = new Map<string, any>();
|
||||
const infoPromises = [];
|
||||
|
||||
for (const model of response.models) {
|
||||
infoPromises.push(fetch(`${baseUrl}/api/show`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: model.name,
|
||||
}),
|
||||
}).then((res) => {
|
||||
return res.json()
|
||||
}).then((json) => {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of json.capabilities) {
|
||||
switch (capability) {
|
||||
case 'thinking':
|
||||
capabilities.add('reasoning');
|
||||
break;
|
||||
case 'tools':
|
||||
capabilities.add('tools');
|
||||
break;
|
||||
case 'completion':
|
||||
inputModalities.add('text');
|
||||
break;
|
||||
case 'vision':
|
||||
inputModalities.add('image');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let contextWindow: number | undefined;
|
||||
|
||||
try {
|
||||
for (const key of Object.keys(json.model_info)) {
|
||||
if (key.endsWith('context_length')) {
|
||||
contextWindow = json.model_info[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
let normalizedId = model.name;
|
||||
normalizedId = normalizedId.replace(/:cloud$/, '');
|
||||
normalizedId = normalizedId.replace(/-cloud$/, '');
|
||||
normalizedId = normalizedId.replace(/:latest$/, '');
|
||||
const modelData = getModelData(normalizedId, 'ollama-cloud', modelsDevData);
|
||||
|
||||
models.set(model.name, {
|
||||
...modelData,
|
||||
id: model.name,
|
||||
name: model.name,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow,
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(infoPromises);
|
||||
|
||||
return Array.from(models.values());
|
||||
}
|
||||
case 'vllm': {
|
||||
return response.data.map((model: any) => {
|
||||
return {
|
||||
id: model.id,
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
contextWindow: model.max_model_len,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
case 'google': {
|
||||
return response.models.map((model: any) => ({ ...getModelData(model.name.replace('models/', ''), provider, modelsDevData), id: model.name.replace('models/', ''), name: model.displayName }));
|
||||
}
|
||||
case 'cohere': {
|
||||
const models = [];
|
||||
for (const model of response.models) {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const feature of model.features || []) {
|
||||
switch (feature) {
|
||||
case 'tools': {
|
||||
capabilities.add('tools');
|
||||
} break;
|
||||
case 'vision': {
|
||||
inputModalities.add('image');
|
||||
} break;
|
||||
case 'reasoning': {
|
||||
capabilities.add('reasoning');
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
const modelData = getModelData(model.name, provider, modelsDevData);
|
||||
|
||||
models.push({
|
||||
...modelData,
|
||||
id: model.name,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow: model.context_length || modelData.attributes?.contextWindow,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
case 'mistral': {
|
||||
const models = [];
|
||||
for (const model of response.data) {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of Object.keys(model.capabilities)) {
|
||||
if (model.capabilities[capability] === false) continue;
|
||||
|
||||
switch (capability) {
|
||||
case 'function_calling': {
|
||||
capabilities.add('tools');
|
||||
} break;
|
||||
case 'completion_chat': {
|
||||
inputModalities.add('text');
|
||||
capabilities.add('completion');
|
||||
} break;
|
||||
case 'vision': {
|
||||
inputModalities.add('image');
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
const modelData = getModelData(model.id, provider, modelsDevData);
|
||||
|
||||
models.push({
|
||||
...modelData,
|
||||
id: model.id,
|
||||
name: modelData.name || model.name || model.id,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow: model.max_context_length || modelData.attributes?.contextWindow,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
case 'inception':
|
||||
case 'longcat': {
|
||||
return response.models;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-19
@@ -1,13 +1,16 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Providers } from "~/types/model";
|
||||
import { db } from "~~/server/lib/db";
|
||||
import { providers as providersSchema } from "~~/drizzle/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getAllProviders, getProviderBaseUrl } from "~~/server/utils/providers";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
const userId = event.context.user!.id as string;
|
||||
|
||||
const providers = await db.query.providers.findMany({
|
||||
const providerIds = getAllProviders().map(p => p.id);
|
||||
|
||||
let providers = await db.query.providers.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
@@ -16,24 +19,46 @@ export default defineEventHandler(async (event) => {
|
||||
},
|
||||
});
|
||||
|
||||
Providers.forEach(async p => {
|
||||
const createPromises: Promise<any>[] = [];
|
||||
providerIds.forEach(p => {
|
||||
if (!providers.find(provider => provider.type === p)) {
|
||||
const [created] = await db.insert(providersSchema).values({
|
||||
id: nanoid(),
|
||||
userId,
|
||||
type: p,
|
||||
name: p,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}).returning() as typeof providers;
|
||||
if (created) {
|
||||
created.models = [];
|
||||
providers.push(created);
|
||||
}
|
||||
createPromises.push(
|
||||
db.insert(providersSchema).values({
|
||||
id: nanoid(),
|
||||
userId,
|
||||
type: p,
|
||||
name: p,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}).returning() as any
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return providers;
|
||||
})
|
||||
const createdResults = await Promise.all(createPromises);
|
||||
for (const created of createdResults) {
|
||||
if (created && created[0]) {
|
||||
created[0].models = [];
|
||||
providers.push(created[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const deletePromises: Promise<any>[] = [];
|
||||
providers.forEach(provider => {
|
||||
if (!providerIds.includes(provider.type)) {
|
||||
deletePromises.push(db.delete(providersSchema).where(eq(providersSchema.id, provider.id)));
|
||||
providers = providers.filter(p => p.id !== provider.id);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
const enrichedProviders = providers.map(p => ({
|
||||
...p,
|
||||
defaultBaseUrl: getProviderBaseUrl(p.type),
|
||||
}));
|
||||
|
||||
return enrichedProviders;
|
||||
});
|
||||
|
||||
+32
-159
@@ -1,26 +1,9 @@
|
||||
import { createCerebras, type CerebrasProvider } from "@ai-sdk/cerebras";
|
||||
import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from "@ai-sdk/google";
|
||||
import { createOpenRouter, type OpenRouterProvider } from "@openrouter/ai-sdk-provider";
|
||||
import { createOllama, type OllamaProvider } from "ai-sdk-ollama";
|
||||
import { createOpenAICompatible, type OpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
|
||||
import { createCohere, type CohereProvider } from '@ai-sdk/cohere';
|
||||
import { createMistral, type MistralProvider } from '@ai-sdk/mistral';
|
||||
import { createLongcat, type LongcatProvider } from 'longcat-ai-sdk-provider';
|
||||
import { type StreamTextTransform } from "ai";
|
||||
import { providerBaseUrls, Providers } from "~/types/model";
|
||||
import type { Provider as ProviderDrizzle, Model as ModelDrizzle } from '~/composables/useModels';
|
||||
import { type Result, Err, Ok } from "~~/types/result";
|
||||
// import { createLongcatTransformer } from "./longcat";
|
||||
import { getProvider, getProviderBaseUrl } from './providers';
|
||||
|
||||
export type ModelGateway =
|
||||
OpenRouterProvider
|
||||
| OllamaProvider
|
||||
| CerebrasProvider
|
||||
| GoogleGenerativeAIProvider
|
||||
| OpenAICompatibleProvider
|
||||
| CohereProvider
|
||||
| MistralProvider
|
||||
| LongcatProvider;
|
||||
export type ModelGateway = any;
|
||||
|
||||
export interface Gateway {
|
||||
gateway: ModelGateway;
|
||||
@@ -43,162 +26,52 @@ export enum GatewayFetchError {
|
||||
}
|
||||
|
||||
export async function getProviderDetails(provider: ProviderDrizzle, providerApiKey?: string, model?: ModelDrizzle): Promise<Result<Provider, GatewayFetchError>> {
|
||||
let gateway: Gateway = {} as Gateway;
|
||||
const providerModule = getProvider(provider.type);
|
||||
|
||||
if (!providerModule) {
|
||||
return Err(GatewayFetchError.NoProviderBaseUrl);
|
||||
}
|
||||
|
||||
let baseURL = undefined;
|
||||
let modelsEndpoint = undefined;
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (provider.config.apiProxyUrl && provider.config.apiProxyUrl.trim() !== '') {
|
||||
baseURL = provider.config.apiProxyUrl;
|
||||
} else {
|
||||
baseURL = providerBaseUrls[provider.type as typeof Providers[number]];
|
||||
baseURL = providerModule.baseUrl;
|
||||
}
|
||||
baseURL = baseURL.replace(/\/$/, '');
|
||||
|
||||
switch (provider.type as typeof Providers[number]) {
|
||||
case 'openrouter': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
const gatewayResult = providerModule.createGateway({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
model,
|
||||
});
|
||||
|
||||
gateway.gateway = createOpenRouter({
|
||||
apiKey: providerApiKey,
|
||||
headers: {
|
||||
'HTTP-Referer': 'https://localhost:3000',
|
||||
'X-Title': 'Veridian',
|
||||
},
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = `/models`;
|
||||
} break;
|
||||
case 'closedrouter': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
if (!gatewayResult.ok) {
|
||||
return gatewayResult;
|
||||
}
|
||||
|
||||
gateway.gateway = createOpenAICompatible({
|
||||
name: 'ClosedRouter',
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
includeUsage: true,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = `/models`;
|
||||
} break;
|
||||
case 'ollama': {
|
||||
if (baseURL === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderBaseUrl);
|
||||
}
|
||||
|
||||
if (providerApiKey !== undefined) {
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
}
|
||||
modelsEndpoint = `/api/tags`;
|
||||
|
||||
if (model !== undefined) {
|
||||
const innerGateway = createOllama({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
})
|
||||
|
||||
gateway.gateway = ((modelId: string) => innerGateway(modelId, { think: model.capabilities.includes('reasoning') })) as OllamaProvider;
|
||||
}
|
||||
} break;
|
||||
case 'vllm': {
|
||||
gateway.gateway = createOpenAICompatible({
|
||||
name: 'vLLM',
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
includeUsage: true,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = '/models';
|
||||
} break;
|
||||
case 'cerebras': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createCerebras({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
})
|
||||
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = `/models`;
|
||||
} break;
|
||||
case 'google': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createGoogleGenerativeAI({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
});
|
||||
headers['x-goog-api-key'] = `${providerApiKey}`
|
||||
modelsEndpoint = `/models`;
|
||||
} break;
|
||||
case 'longcat': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createLongcat({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = null;
|
||||
// streamTransformer = createLongcatTransformer() as StreamTextTransform<{}>;
|
||||
} break;
|
||||
case 'cohere': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createCohere({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = `/models`;
|
||||
} break;
|
||||
case 'inception': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createOpenAICompatible({
|
||||
name: 'Inception',
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
includeUsage: true,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = null;
|
||||
}
|
||||
case 'mistral': {
|
||||
if (providerApiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
gateway.gateway = createMistral({
|
||||
apiKey: providerApiKey,
|
||||
baseURL,
|
||||
});
|
||||
headers['Authorization'] = `Bearer ${providerApiKey}`
|
||||
modelsEndpoint = '/models';
|
||||
const rawHeaders = providerModule.getAuthHeaders(providerApiKey);
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(rawHeaders)) {
|
||||
if (value !== undefined) {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway,
|
||||
gateway: gatewayResult.data,
|
||||
endpoint: {
|
||||
baseURL,
|
||||
modelsEndpoint,
|
||||
modelsEndpoint: providerModule.modelsEndpoint,
|
||||
headers,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isRerankingProvider(gateway: any): gateway is { reranking: (modelId: string) => any } {
|
||||
return (
|
||||
gateway !== null &&
|
||||
'reranking' in gateway &&
|
||||
typeof (gateway as any).reranking === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'anthropic',
|
||||
baseUrl: 'https://api.anthropic.com/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createAnthropic({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey
|
||||
? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }
|
||||
: {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-api-key': `${apiKey}`,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => ({
|
||||
...getModelData(model.id, 'anthropic', modelsDevData),
|
||||
id: model.id,
|
||||
name: model.display_name ?? model.id,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createCerebras } from '@ai-sdk/cerebras';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'cerebras',
|
||||
baseUrl: 'https://api.cerebras.ai/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createCerebras({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => ({
|
||||
...getModelData(model.id, 'cerebras', modelsDevData),
|
||||
id: model.id,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
|
||||
export default {
|
||||
id: 'closedrouter',
|
||||
baseUrl: 'https://router.queef.in/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAI({ name: 'ClosedRouter', apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => {
|
||||
const capabilities = new Array<string>();
|
||||
|
||||
if (model.capabilities) {
|
||||
if (model.capabilities.reasoning) {
|
||||
capabilities.push('reasoning');
|
||||
}
|
||||
if (model.capabilities.tool_call) {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
attributes: {
|
||||
inputModalities: model.modalities?.input || ['text'],
|
||||
outputModalities: model.modalities?.output || ['text'],
|
||||
capabilities: capabilities,
|
||||
contextWindow: model.context_window,
|
||||
supported_parameters: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createCohere } from '@ai-sdk/cohere';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData, mergeSets } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'cohere',
|
||||
baseUrl: 'https://api.cohere.ai/v2',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createCohere({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
const models = [];
|
||||
for (const model of data.models) {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const feature of model.features || []) {
|
||||
switch (feature) {
|
||||
case 'tools':
|
||||
capabilities.add('tools');
|
||||
break;
|
||||
case 'vision':
|
||||
inputModalities.add('image');
|
||||
break;
|
||||
case 'reasoning':
|
||||
capabilities.add('reasoning');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const modelData = getModelData(model.name, 'cohere', modelsDevData);
|
||||
|
||||
models.push({
|
||||
...modelData,
|
||||
id: model.name,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow: model.context_length || modelData.attributes?.contextWindow,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'google',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createGoogleGenerativeAI({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { 'x-goog-api-key': apiKey } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { 'x-goog-api-key': `${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.models.map((model: any) => ({
|
||||
...getModelData(model.name.replace('models/', ''), 'google', modelsDevData),
|
||||
id: model.name.replace('models/', ''),
|
||||
name: model.displayName,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,123 @@
|
||||
import { SupportedModalities } from '~/types/model';
|
||||
|
||||
export function mergeSets(setA: Set<string>, setB: Set<string>): string[] {
|
||||
return Array.from(new Set([...setA, ...setB]));
|
||||
}
|
||||
|
||||
export const getModelData = (modelId: string, providerId: string, modelsDevData: any) => {
|
||||
const modelData = modelsDevData[providerId]?.models[modelId];
|
||||
|
||||
if (modelData === undefined) return {
|
||||
cost: {},
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: [],
|
||||
}
|
||||
};
|
||||
|
||||
const capabilities = new Array<string>();
|
||||
if (modelData.reasoning) {
|
||||
capabilities.push('reasoning');
|
||||
}
|
||||
|
||||
if (modelData.tool_call) {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
|
||||
let inputModalities = modelData.modalities.input.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
let outputModalities = modelData.modalities.output.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
let supportedParameters = [];
|
||||
if (modelData.temperature) {
|
||||
supportedParameters.push('temperature');
|
||||
}
|
||||
|
||||
let contextWindow = modelData.limit?.context || null;
|
||||
let cost: Record<string, string> = {};
|
||||
for (const key in modelData.cost) {
|
||||
switch (key) {
|
||||
case 'input': {
|
||||
cost.prompt = formatMoney(modelData.cost[key].toString());
|
||||
} break;
|
||||
case 'output': {
|
||||
cost.completion = formatMoney(modelData.cost[key].toString());
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
let releasedAt = (new Date(modelData.release_date)).getTime();
|
||||
|
||||
return {
|
||||
name: modelData.name,
|
||||
attributes: {
|
||||
inputModalities,
|
||||
outputModalities,
|
||||
capabilities: capabilities || [],
|
||||
contextWindow,
|
||||
supported_parameters: modelData.supportedParameters,
|
||||
},
|
||||
cost,
|
||||
releasedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export const lshDecimal = (number: string, shift: number) => {
|
||||
if (number[0] === '-') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (number.length === 0) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
let value = '';
|
||||
let [integerPart, fractionalPart] = number.split('.');
|
||||
|
||||
if (!fractionalPart) {
|
||||
fractionalPart = '';
|
||||
}
|
||||
|
||||
if (shift <= fractionalPart.length) {
|
||||
integerPart += fractionalPart.substring(0, shift);
|
||||
fractionalPart = fractionalPart.substring(shift);
|
||||
} else if (shift > fractionalPart.length) {
|
||||
integerPart += fractionalPart;
|
||||
for (let i = 0; i < shift - fractionalPart.length; i++) {
|
||||
integerPart += '0';
|
||||
}
|
||||
fractionalPart = '';
|
||||
}
|
||||
|
||||
integerPart = integerPart!.replace(/^0+/, '');
|
||||
if (integerPart.length === 0) {
|
||||
integerPart = '0';
|
||||
}
|
||||
|
||||
value += integerPart;
|
||||
|
||||
if (fractionalPart.length > 0) {
|
||||
if (fractionalPart.length < 2) {
|
||||
fractionalPart += '0';
|
||||
}
|
||||
|
||||
value += '.' + fractionalPart;
|
||||
} else {
|
||||
value += '.00';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const formatMoney = (value: string) => {
|
||||
const [integerPart, fractionalPart] = value.split('.');
|
||||
|
||||
if (fractionalPart === undefined) {
|
||||
return `${integerPart}.00`;
|
||||
}
|
||||
|
||||
if (fractionalPart.length === 1) {
|
||||
return `${integerPart}.${fractionalPart}0`;
|
||||
}
|
||||
|
||||
return `${integerPart}.${fractionalPart}`;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
|
||||
export default {
|
||||
id: 'inception',
|
||||
baseUrl: 'https://api.inceptionlabs.ai/v1',
|
||||
modelsEndpoint: null,
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAI({ name: 'Inception', apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels() {
|
||||
// inception doesn't have a model list endpoint so we hardcode them
|
||||
return [
|
||||
{
|
||||
id: "mercury-2",
|
||||
name: "Mercury 2",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools', 'reasoning'],
|
||||
contextWindow: 128_000,
|
||||
}
|
||||
},
|
||||
];
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,89 @@
|
||||
import openrouter from './openrouter';
|
||||
import ollama from './ollama';
|
||||
import vllm from './vllm';
|
||||
import cerebras from './cerebras';
|
||||
import google from './google';
|
||||
import longcat from './longcat';
|
||||
import cohere from './cohere';
|
||||
import inception from './inception';
|
||||
import mistral from './mistral';
|
||||
import closedrouter from './closedrouter';
|
||||
import nvidia from './nvidia';
|
||||
import xiaomi from './xiaomi';
|
||||
import openai from './openai';
|
||||
import anthropic from './anthropic';
|
||||
import type { Result } from '~~/types/result';
|
||||
import type { Gateway, GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import type { Model as ModelDrizzle } from '~/composables/useModels';
|
||||
|
||||
export interface NormalizedModel {
|
||||
id: string;
|
||||
name?: string;
|
||||
cost?: Record<string, string | undefined>;
|
||||
attributes: {
|
||||
inputModalities: string[];
|
||||
outputModalities: string[];
|
||||
capabilities: string[];
|
||||
contextWindow?: number | null;
|
||||
supported_parameters?: string[];
|
||||
};
|
||||
releasedAt?: number;
|
||||
}
|
||||
|
||||
export interface ProviderModule {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
modelsEndpoint: string | null;
|
||||
|
||||
createGateway(config: {
|
||||
apiKey?: string;
|
||||
baseURL: string;
|
||||
model?: ModelDrizzle;
|
||||
}): Result<Gateway, GatewayFetchError>;
|
||||
|
||||
getAuthHeaders(apiKey?: string): Record<string, string | undefined>;
|
||||
|
||||
fetchModels(config: {
|
||||
baseURL: string;
|
||||
apiKey?: string;
|
||||
modelsDevData: any;
|
||||
}): Promise<NormalizedModel[]>;
|
||||
}
|
||||
|
||||
const providers = [
|
||||
openrouter,
|
||||
ollama,
|
||||
vllm,
|
||||
cerebras,
|
||||
google,
|
||||
longcat,
|
||||
cohere,
|
||||
inception,
|
||||
mistral,
|
||||
closedrouter,
|
||||
nvidia,
|
||||
xiaomi,
|
||||
openai,
|
||||
anthropic,
|
||||
];
|
||||
|
||||
type ProviderId = typeof providers[number]['id'];
|
||||
type Provider = typeof providers[number];
|
||||
|
||||
const registry = new Map<ProviderId, Provider>(providers.map((p) => [p.id, p]));
|
||||
|
||||
export function getProvider<T extends ProviderId>(id: T): Provider | undefined {
|
||||
return registry.get(id);
|
||||
}
|
||||
|
||||
export function getAllProviders() {
|
||||
return Array.from(registry.values());
|
||||
}
|
||||
|
||||
export function getProviderIds() {
|
||||
return Array.from(registry.keys());
|
||||
}
|
||||
|
||||
export function getProviderBaseUrl<T extends ProviderId>(id: T): string {
|
||||
return registry.get(id)?.baseUrl ?? '';
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createLongcat } from 'longcat-ai-sdk-provider';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
|
||||
export default {
|
||||
id: 'longcat',
|
||||
baseUrl: 'https://api.longcat.chat/openai/v1',
|
||||
modelsEndpoint: null,
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createLongcat({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels() {
|
||||
// longcat doesn't have a model list endpoint so we hardcode them
|
||||
return [
|
||||
{
|
||||
id: "LongCat-Flash-Chat",
|
||||
name: "LongCat Flash Chat",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Thinking-2601",
|
||||
name: "LongCat Flash Thinking (2601)",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['reasoning', 'tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Omni-2603",
|
||||
name: "LongCat Flash Omni",
|
||||
attributes: {
|
||||
inputModalities: ['text', 'image', 'audio', 'video'],
|
||||
outputModalities: ['text', 'audio'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 256_000,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "LongCat-Flash-Lite",
|
||||
name: "LongCat Flash Lite",
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
capabilities: ['tools'],
|
||||
contextWindow: 320_000,
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createMistral } from '@ai-sdk/mistral';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData, mergeSets } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'mistral',
|
||||
baseUrl: 'https://api.mistral.ai/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createMistral({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
const models = [];
|
||||
for (const model of data.data) {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of Object.keys(model.capabilities)) {
|
||||
if (model.capabilities[capability] === false) continue;
|
||||
|
||||
switch (capability) {
|
||||
case 'function_calling':
|
||||
capabilities.add('tools');
|
||||
break;
|
||||
case 'completion_chat':
|
||||
inputModalities.add('text');
|
||||
capabilities.add('completion');
|
||||
break;
|
||||
case 'vision':
|
||||
inputModalities.add('image');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const modelData = getModelData(model.id, 'mistral', modelsDevData);
|
||||
|
||||
models.push({
|
||||
...modelData,
|
||||
id: model.id,
|
||||
name: modelData.name || model.name || model.id,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow: model.max_context_length || modelData.attributes?.contextWindow,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'nvidia',
|
||||
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAI({ name: 'Nvidia', apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => ({
|
||||
...getModelData(model.id, 'nvidia', modelsDevData),
|
||||
id: model.id,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createOllama } from 'ai-sdk-ollama';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData, mergeSets } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'ollama',
|
||||
baseUrl: '',
|
||||
modelsEndpoint: '/api/tags',
|
||||
|
||||
createGateway({ apiKey, baseURL, model }) {
|
||||
if (baseURL === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderBaseUrl);
|
||||
}
|
||||
|
||||
if (model !== undefined) {
|
||||
const innerGateway = createOllama({
|
||||
apiKey,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
return Ok({
|
||||
gateway: ((modelId: string) => innerGateway(modelId, { think: model.capabilities.includes('reasoning') })) as any,
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOllama({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0'
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseURL}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
const models = new Map<string, any>();
|
||||
const infoPromises = [];
|
||||
|
||||
for (const model of data.models) {
|
||||
infoPromises.push(fetch(`${baseURL}/api/show`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ model: model.name }),
|
||||
}).then((res) => res.json()).then((json) => {
|
||||
const inputModalities = new Set<string>();
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of json.capabilities) {
|
||||
switch (capability) {
|
||||
case 'thinking':
|
||||
capabilities.add('reasoning');
|
||||
break;
|
||||
case 'tools':
|
||||
capabilities.add('tools');
|
||||
break;
|
||||
case 'completion':
|
||||
inputModalities.add('text');
|
||||
break;
|
||||
case 'vision':
|
||||
inputModalities.add('image');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let contextWindow: number | undefined;
|
||||
try {
|
||||
for (const key of Object.keys(json.model_info)) {
|
||||
if (key.endsWith('context_length')) {
|
||||
contextWindow = json.model_info[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
let normalizedId = model.name;
|
||||
normalizedId = normalizedId.replace(/:cloud$/, '');
|
||||
normalizedId = normalizedId.replace(/-cloud$/, '');
|
||||
normalizedId = normalizedId.replace(/:latest$/, '');
|
||||
const modelData = getModelData(normalizedId, 'ollama-cloud', modelsDevData);
|
||||
|
||||
models.set(model.name, {
|
||||
...modelData,
|
||||
id: model.name,
|
||||
name: model.name,
|
||||
attributes: {
|
||||
...modelData.attributes,
|
||||
inputModalities: mergeSets(
|
||||
inputModalities,
|
||||
new Set(modelData.attributes?.inputModalities || [])
|
||||
),
|
||||
capabilities: mergeSets(
|
||||
capabilities,
|
||||
new Set(modelData.attributes?.capabilities || [])
|
||||
),
|
||||
contextWindow,
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(infoPromises);
|
||||
return Array.from(models.values());
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAI({ apiKey, baseURL }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data
|
||||
.filter((model: any) => model.object === 'model')
|
||||
.map((model: any) => ({
|
||||
...getModelData(model.id, 'openai', modelsDevData),
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import { SupportedModalities } from '~/types/model';
|
||||
import type { ProviderModule } from '.';
|
||||
import { lshDecimal } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'openrouter',
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenRouter({
|
||||
apiKey,
|
||||
headers: {
|
||||
'HTTP-Referer': 'https://localhost:3000',
|
||||
'X-Title': 'Veridian',
|
||||
},
|
||||
}),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
const models = [];
|
||||
|
||||
for (const model of data.data) {
|
||||
const capabilities = new Set<string>();
|
||||
|
||||
for (const capability of model.supported_parameters) {
|
||||
switch (capability) {
|
||||
case 'reasoning':
|
||||
capabilities.add('reasoning');
|
||||
break;
|
||||
case 'tools':
|
||||
capabilities.add('tools');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const pricing = {} as Record<string, string | undefined>;
|
||||
for (const key of Object.keys(model.pricing)) {
|
||||
switch (key) {
|
||||
case 'prompt':
|
||||
case 'completion':
|
||||
case 'image':
|
||||
case 'audio':
|
||||
case 'discount':
|
||||
pricing[key] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'request':
|
||||
pricing['request'] = lshDecimal(model.pricing[key], 3);
|
||||
break;
|
||||
case 'image_tokens':
|
||||
pricing['imageTokens'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'image_output':
|
||||
pricing['imageOutput'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'audio_output':
|
||||
pricing['audioOutput'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_audio_cache':
|
||||
pricing['inputAudioCache'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'web_search':
|
||||
pricing['webSearch'] = lshDecimal(model.pricing[key], 3);
|
||||
break;
|
||||
case 'internal_reasoning':
|
||||
pricing['internalReasoning'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_cache_read':
|
||||
pricing['inputCacheRead'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
case 'input_cache_write':
|
||||
pricing['inputCacheWrite'] = lshDecimal(model.pricing[key], 6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
models.push({
|
||||
id: model.id as string,
|
||||
name: model.name as string,
|
||||
cost: pricing,
|
||||
attributes: {
|
||||
inputModalities: model.architecture.input_modalities.filter((modality: string) => (SupportedModalities as Readonly<string[]>).includes(modality)),
|
||||
outputModalities: model.architecture.output_modalities.filter((modality: string) => (SupportedModalities as Readonly<string[]>).includes(modality)),
|
||||
capabilities: Array.from(capabilities),
|
||||
contextWindow: model.context_length,
|
||||
supported_parameters: model.supported_parameters,
|
||||
},
|
||||
releasedAt: model.created * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
|
||||
export default {
|
||||
id: 'vllm',
|
||||
baseUrl: '',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
return Ok({
|
||||
gateway: createOpenAICompatible({ name: 'vLLM', apiKey, baseURL, includeUsage: true }),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => ({
|
||||
id: model.id,
|
||||
attributes: {
|
||||
inputModalities: ['text'],
|
||||
outputModalities: ['text'],
|
||||
contextWindow: model.max_model_len,
|
||||
}
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { GatewayFetchError } from '~~/server/utils/ai-provider';
|
||||
import { Err, Ok } from '~~/types/result';
|
||||
import type { ProviderModule } from '.';
|
||||
import { getModelData } from './helpers';
|
||||
|
||||
export default {
|
||||
id: 'xiaomi',
|
||||
baseUrl: 'https://api.xiaomimimo.com/v1',
|
||||
modelsEndpoint: '/models',
|
||||
|
||||
createGateway({ apiKey, baseURL }) {
|
||||
if (apiKey === undefined) {
|
||||
return Err(GatewayFetchError.NoProviderApiKey);
|
||||
}
|
||||
|
||||
return Ok({
|
||||
gateway: createOpenAICompatible({
|
||||
name: 'Xiaomi',
|
||||
apiKey,
|
||||
baseURL,
|
||||
includeUsage: true,
|
||||
|
||||
}),
|
||||
streamTransformer: undefined,
|
||||
textTransformer: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
getAuthHeaders(apiKey) {
|
||||
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
||||
},
|
||||
|
||||
async fetchModels({ baseURL, apiKey, modelsDevData }) {
|
||||
const res = await fetch(`${baseURL}/models`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw createError({ statusCode: res.status, message: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => ({
|
||||
...getModelData(model.id, 'xiaomi', modelsDevData),
|
||||
id: model.id,
|
||||
}));
|
||||
},
|
||||
} satisfies ProviderModule;
|
||||
Reference in New Issue
Block a user