checking models capabilities

This commit is contained in:
2026-02-12 14:22:43 +03:00
parent 77d0ca2f38
commit 2026c673f5
12 changed files with 216 additions and 86 deletions
+19
View File
@@ -2,6 +2,8 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {googleAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class GeminiGetModel extends Command { export class GeminiGetModel extends Command {
title = "/geminiGetModel"; title = "/geminiGetModel";
@@ -10,4 +12,21 @@ export class GeminiGetModel extends Command {
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError);
} }
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await googleAi.models.get({model: Environment.GEMINI_MODEL});
console.log(info);
return {
vision: {supported: true},
ocr: null,
thinking: {supported: info.thinking},
tools: null
};
} catch (e) {
logError(e);
return null;
}
}
} }
+12 -58
View File
@@ -1,11 +1,10 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {callbackCommands, commands} from "../index"; import {callbackCommands, commands} from "../index";
import {AiProvider, Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {boolToEmoji, logError, replyToMessage} from "../util/utils"; import {boolToEmoji, getCurrentModel, getCurrentModelCapabilities, logError, replyToMessage} from "../util/utils";
import {OllamaGetModel} from "./ollama-get-model"; import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {AiProvider} from "../model/ai-provider";
type AiCapabilityInfo = { supported?: boolean, external?: boolean, model?: string };
export class Info extends ChatCommand { export class Info extends ChatCommand {
command = ["info", "v"]; command = ["info", "v"];
@@ -15,73 +14,28 @@ export class Info extends ChatCommand {
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
const aiProvider = Environment.DEFAULT_AI_PROVIDER; const aiProvider = Environment.DEFAULT_AI_PROVIDER;
let aiModel: string; const aiModel = getCurrentModel();
let aiVisionSupported: AiCapabilityInfo = {}; let aiModelCapabilities: AiModelCapabilities = {};
let aiThinkingSupported: AiCapabilityInfo = {};
let aiToolsSupported: AiCapabilityInfo = {};
try { try {
switch (aiProvider) { aiModelCapabilities = await getCurrentModelCapabilities();
case AiProvider.OLLAMA: {
const ollamaGetModel = commands.find(c => c instanceof OllamaGetModel);
aiModel = Environment.OLLAMA_MODEL;
aiVisionSupported = {
supported: (await ollamaGetModel.loadImageModelInfo()).capabilities.includes("vision"),
external: Environment.OLLAMA_IMAGE_MODEL !== Environment.OLLAMA_MODEL,
model: Environment.OLLAMA_IMAGE_MODEL
};
aiThinkingSupported = {
supported: (await ollamaGetModel.loadThinkModelInfo()).capabilities.includes("thinking"),
external: Environment.OLLAMA_THINK_MODEL !== Environment.OLLAMA_MODEL,
model: Environment.OLLAMA_THINK_MODEL
};
aiToolsSupported = {
supported: (await ollamaGetModel.loadModelInfo()).capabilities.includes("tools"),
external: false,
model: Environment.OLLAMA_MODEL
};
break;
}
case AiProvider.GEMINI:
aiModel = Environment.GEMINI_MODEL;
aiVisionSupported = {supported: true};
aiThinkingSupported = {};
aiToolsSupported = {};
break;
case AiProvider.MISTRAL:
aiModel = Environment.MISTRAL_MODEL;
aiVisionSupported = {supported: true};
aiThinkingSupported = {};
aiToolsSupported = {};
break;
case AiProvider.OPENAI:
aiModel = Environment.OPENAI_MODEL;
aiVisionSupported = {};
aiThinkingSupported = {};
aiToolsSupported = {};
break;
}
} catch (e) { } catch (e) {
logError(e); logError(e);
await replyToMessage({message: msg, text: `Произошла ошибка: ${e}`}).catch(logError); await replyToMessage({message: msg, text: `Произошла ошибка: ${e}`}).catch(logError);
return; return;
} }
const aiInfo = "```" + const aiInfo = "```" +
"AI\n" + "AI\n" +
`supported providers: ${Object.keys(AiProvider).filter(key => isNaN(Number(key))).length}\n\n` + `supported providers: ${Object.keys(AiProvider).filter(key => isNaN(Number(key))).length}\n\n` +
`provider: ${aiProvider.toLowerCase()}\n` + `provider: ${aiProvider.toLowerCase()}\n` +
`model: ${aiModel}\n\n` + `model: ${aiModel}\n\n` +
`vision${aiVisionSupported.external ? "(ext)" : ""}: ${boolToEmoji(aiVisionSupported.supported)}\n` + `vision${aiModelCapabilities.vision?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.vision?.supported)}\n` +
`thinking${aiThinkingSupported.external ? "(ext)" : ""}: ${boolToEmoji(aiThinkingSupported.supported)}\n` + `ocr${aiModelCapabilities.ocr?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.ocr?.supported)}\n` +
`tools: ${boolToEmoji(aiToolsSupported.supported)}` + `thinking${aiModelCapabilities.thinking?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.thinking?.supported)}\n` +
`tools${aiModelCapabilities.tools?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.tools?.supported)}` +
"```"; "```";
const cmds = commands.filter(c => !(c instanceof ChatCommand)); const cmds = commands.filter(c => !(c instanceof ChatCommand));
+20 -1
View File
@@ -6,12 +6,14 @@ import {
escapeMarkdownV2Text, escapeMarkdownV2Text,
logError, logError,
oldReplyToMessage, oldReplyToMessage,
replyToMessage,
startIntervalEditor startIntervalEditor
} from "../util/utils"; } from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {bot, mistralAi} from "../index"; import {bot, commands, mistralAi} from "../index";
import {MessageStore} from "../common/message-store"; import {MessageStore} from "../common/message-store";
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {MistralGetModel} from "./mistral-get-model";
export class MistralChat extends ChatCommand { export class MistralChat extends ChatCommand {
command = "mistral"; command = "mistral";
@@ -67,6 +69,23 @@ export class MistralChat extends ChatCommand {
return total + (curr.content.filter(c => c.type === "image_url")?.length ?? 0); return total + (curr.content.filter(c => c.type === "image_url")?.length ?? 0);
}, 0); }, 0);
if (imagesCount) {
try {
const modelInfo = await commands.find(c => c instanceof MistralGetModel).getModelCapabilities();
if (modelInfo) {
if (!modelInfo.vision?.supported) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
waitMessage = await bot.sendMessage({ waitMessage = await bot.sendMessage({
chat_id: chatId, chat_id: chatId,
text: imagesCount ? text: imagesCount ?
+19
View File
@@ -4,6 +4,8 @@ import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {mistralAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class MistralGetModel extends Command { export class MistralGetModel extends Command {
title = "/mistralGetModel"; title = "/mistralGetModel";
@@ -14,4 +16,21 @@ export class MistralGetModel extends Command {
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.MISTRAL_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: `Текущая модель: "${Environment.MISTRAL_MODEL}"`}).catch(logError);
} }
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await mistralAi.models.retrieve({modelId: Environment.MISTRAL_MODEL});
console.log(info);
return {
vision: {supported: info.capabilities.vision},
ocr: {supported: info.capabilities.ocr},
thinking: null,
tools: {supported: info.capabilities.functionCalling}
};
} catch (e) {
logError(e);
return null;
}
}
} }
+2 -4
View File
@@ -59,8 +59,7 @@ export class OllamaChat extends ChatCommand {
try { try {
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadImageModelInfo(); const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
if (modelInfo) { if (modelInfo) {
const caps = modelInfo.capabilities || []; if (!modelInfo.vision?.supported) {
if (!caps.includes("vision")) {
await replyToMessage({ await replyToMessage({
message: msg, message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹" text: "Моя текущая модель не умеет анализировать изображения 🥹"
@@ -77,8 +76,7 @@ export class OllamaChat extends ChatCommand {
try { try {
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo(); const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
if (modelInfo) { if (modelInfo) {
const caps = modelInfo.capabilities || []; if (!modelInfo.thinking?.supported) {
if (!caps.includes("thinking")) {
await replyToMessage({ await replyToMessage({
message: msg, message: msg,
text: "Моя текущая модель не умеет размышлять 🥹" text: "Моя текущая модель не умеет размышлять 🥹"
+41 -14
View File
@@ -3,7 +3,7 @@ import {Message} from "typescript-telegram-bot-api";
import {boolToEmoji, logError, replyToMessage} from "../util/utils"; import {boolToEmoji, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {ollama} from "../index"; import {ollama} from "../index";
import {ShowResponse} from "ollama"; import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class OllamaGetModel extends Command { export class OllamaGetModel extends Command {
title = "/ollamaGetModel"; title = "/ollamaGetModel";
@@ -15,7 +15,7 @@ export class OllamaGetModel extends Command {
const imageModel = Environment.OLLAMA_IMAGE_MODEL; const imageModel = Environment.OLLAMA_IMAGE_MODEL;
const thinkModel = Environment.OLLAMA_THINK_MODEL; const thinkModel = Environment.OLLAMA_THINK_MODEL;
const promises: (Promise<ShowResponse | null> | null)[] = [this.loadModelInfo()]; const promises: (Promise<AiModelCapabilities | null> | null)[] = [this.getModelCapabilities()];
if (imageModel && imageModel !== model) { if (imageModel && imageModel !== model) {
promises.push(this.loadImageModelInfo()); promises.push(this.loadImageModelInfo());
@@ -62,24 +62,51 @@ export class OllamaGetModel extends Command {
} }
} }
private getModelText(model: string, info: ShowResponse): string { private getModelText(model: string, info: AiModelCapabilities): string {
const caps = info.capabilities;
return `model: ${model}\n\n` + return `model: ${model}\n\n` +
`vision: ${boolToEmoji(caps.includes("vision"))}\n` + `vision: ${boolToEmoji(info.vision?.supported)}\n` +
`thinking: ${boolToEmoji(caps.includes("thinking"))}\n` + `thinking: ${boolToEmoji(info.thinking?.supported)}\n` +
`tools: ${boolToEmoji(caps.includes("tools"))}`; `tools: ${boolToEmoji(info.tools?.supported)}`;
} }
async loadModelInfo(): Promise<ShowResponse | null> { async getModelCapabilities(model: string = Environment.OLLAMA_MODEL): Promise<AiModelCapabilities | null> {
return ollama.show({model: Environment.OLLAMA_MODEL}); try {
const info = await ollama.show({model: model});
console.log(info);
return {
vision: {
supported: info.capabilities.includes("vision"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
ocr: {
supported: info.capabilities.includes("ocr"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
thinking: {
supported: info.capabilities.includes("thinking"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
tools: {
supported: info.capabilities.includes("tools"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
};
} catch (e) {
logError(e);
return null;
}
} }
async loadImageModelInfo(): Promise<ShowResponse | null> { async loadImageModelInfo(): Promise<AiModelCapabilities | null> {
return ollama.show({model: Environment.OLLAMA_IMAGE_MODEL}); return this.getModelCapabilities(Environment.OLLAMA_IMAGE_MODEL);
} }
async loadThinkModelInfo(): Promise<ShowResponse | null> { async loadThinkModelInfo(): Promise<AiModelCapabilities | null> {
return ollama.show({model: Environment.OLLAMA_THINK_MODEL}); return this.getModelCapabilities(Environment.OLLAMA_THINK_MODEL);
} }
} }
+16
View File
@@ -2,6 +2,7 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class OpenAIGetModel extends Command { export class OpenAIGetModel extends Command {
title = "/openAIGetModel"; title = "/openAIGetModel";
@@ -10,4 +11,19 @@ export class OpenAIGetModel extends Command {
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.OPENAI_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: `Текущая модель: "${Environment.OPENAI_MODEL}"`}).catch(logError);
} }
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
// TODO: 12/02/2026, Danil Nikolaev: find solution
try {
return {
vision: {supported: true},
ocr: null,
thinking: {supported: true},
tools: {supported: true},
};
} catch (e) {
logError(e);
return null;
}
}
} }
+2 -8
View File
@@ -2,13 +2,7 @@ import path from "node:path";
import {saveData} from "../db/database"; import {saveData} from "../db/database";
import {Answers} from "../model/answers"; import {Answers} from "../model/answers";
import {ifTrue} from "../util/utils"; import {ifTrue} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
export enum AiProvider {
OLLAMA = "OLLAMA",
GEMINI = "GEMINI",
MISTRAL = "MISTRAL",
OPENAI = "OPENAI",
}
export class Environment { export class Environment {
static BOT_TOKEN: string; static BOT_TOKEN: string;
@@ -94,7 +88,7 @@ export class Environment {
Environment.GEMINI_IMAGE_MODEL = process.env.GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image"; Environment.GEMINI_IMAGE_MODEL = process.env.GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image";
Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY; Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-small-latest"; Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-tiny-latest";
Environment.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL; Environment.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
Environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY; Environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+5
View File
@@ -0,0 +1,5 @@
export type AiCapabilityInfo = {
supported?: boolean,
external?: boolean,
model?: string
};
+8
View File
@@ -0,0 +1,8 @@
import {AiCapabilityInfo} from "./ai-capability-info";
export class AiModelCapabilities {
vision?: AiCapabilityInfo;
ocr?: AiCapabilityInfo;
thinking?: AiCapabilityInfo;
tools?: AiCapabilityInfo;
}
+6
View File
@@ -0,0 +1,6 @@
export enum AiProvider {
OLLAMA = "OLLAMA",
GEMINI = "GEMINI",
MISTRAL = "MISTRAL",
OPENAI = "OPENAI",
}
+66 -1
View File
@@ -13,7 +13,7 @@ import {
PhotoSize, PhotoSize,
User User
} from "typescript-telegram-bot-api"; } from "typescript-telegram-bot-api";
import {AiProvider, Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors"; import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
import {bot, botUser, callbackCommands, commands, messageDao, ollama} from "../index"; import {bot, botUser, callbackCommands, commands, messageDao, ollama} from "../index";
import os from "os"; import os from "os";
@@ -37,6 +37,12 @@ import {WebSearchResponse} from "../model/web-search-response";
import {GeminiChat} from "../commands/gemini-chat"; import {GeminiChat} from "../commands/gemini-chat";
import {MistralChat} from "../commands/mistral-chat"; import {MistralChat} from "../commands/mistral-chat";
import {OpenAIChat} from "../commands/openai-chat"; import {OpenAIChat} from "../commands/openai-chat";
import {AiProvider} from "../model/ai-provider";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {OllamaGetModel} from "../commands/ollama-get-model";
import {GeminiGetModel} from "../commands/gemini-get-model";
import {MistralGetModel} from "../commands/mistral-get-model";
import {OpenAIGetModel} from "../commands/openai-get-model";
export const ignore = () => { export const ignore = () => {
}; };
@@ -1060,6 +1066,65 @@ export function photoPathByUniqueId(uniqueId: string): string {
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg"); return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
} }
export function getCurrentModel(): string {
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA:
return Environment.OLLAMA_MODEL;
case AiProvider.GEMINI:
return Environment.GEMINI_MODEL;
case AiProvider.MISTRAL:
return Environment.MISTRAL_MODEL;
case AiProvider.OPENAI:
return Environment.OPENAI_MODEL;
}
}
export async function getCurrentModelCapabilities(): Promise<AiModelCapabilities | null> {
let promise: Promise<AiModelCapabilities | null> = null;
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA: {
const ollamaGetModel = commands.find(c => c instanceof OllamaGetModel);
// eslint-disable-next-line no-async-promise-executor
promise = new Promise(async (resolve, reject) => {
try {
const result = {
vision: (await ollamaGetModel.loadImageModelInfo()).vision,
ocr: null,
thinking: (await ollamaGetModel.loadThinkModelInfo()).thinking,
tools: (await ollamaGetModel.getModelCapabilities()).tools
};
resolve(result);
} catch (e) {
reject(e);
}
});
break;
}
case AiProvider.GEMINI: {
promise = commands.find(c => c instanceof GeminiGetModel).getModelCapabilities();
break;
}
case AiProvider.MISTRAL: {
promise = commands.find(c => c instanceof MistralGetModel).getModelCapabilities();
break;
}
case AiProvider.OPENAI: {
promise = commands.find(c => c instanceof OpenAIGetModel).getModelCapabilities();
break;
}
}
if (!promise) return null;
try {
return await promise;
} catch (e) {
logError(e);
return null;
}
}
export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> { export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
console.log("my_chat_member", u); console.log("my_chat_member", u);
} }