ai: add api key advice

This commit is contained in:
end-4
2025-05-10 01:53:43 +02:00
parent c0f5f55c63
commit 6758d8daf3
2 changed files with 57 additions and 25 deletions
+55 -25
View File
@@ -18,6 +18,7 @@ Singleton {
property string systemPrompt: ConfigOptions.ai.systemPrompt ?? "" property string systemPrompt: ConfigOptions.ai.systemPrompt ?? ""
property var messages: [] property var messages: []
readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {} readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}
readonly property var apiKeysLoaded: KeyringStorage.loaded
// Model properties: // Model properties:
// - name: Name of the model // - name: Name of the model
@@ -27,35 +28,46 @@ Singleton {
// - model: Model name of the model // - model: Model name of the model
// - requires_key: Whether the model requires an API key // - requires_key: Whether the model requires an API key
// - key_id: The identifier of the API key. Use the same identifier for models that can be accessed with the same key. // - key_id: The identifier of the API key. Use the same identifier for models that can be accessed with the same key.
// - key_get_link: Link to get the API key // - key_get_link: Link to get an API key
// - key_get_description: Description of pricing and how to get an API key
// - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai".
// - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value.
// - extraParams: Extra parameters to be passed to the model. This is a JSON object.
property var models: { property var models: {
"gemini-2.0-flash-gemini-api": { "gemini-2.0-flash-search": {
"name": "Gemini 2.0 Flash", "name": "Gemini 2.0 Flash",
"icon": "google-gemini-symbolic", "icon": "google-gemini-symbolic",
"description": "Online | Google's model | Has search capabilities, giving you up-to-date information", "description": "Online | Google's model\nGives up-to-date information with search.",
"homepage": "https://aistudio.google.com", "homepage": "https://aistudio.google.com",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
"model": "gemini-2.0-flash", "model": "gemini-2.0-flash",
"requires_key": true, "requires_key": true,
"key_id": "gemini", "key_id": "gemini",
"key_get_link": "https://aistudio.google.com/app/apikey", "key_get_link": "https://aistudio.google.com/app/apikey",
"key_get_description": "Pricing: free. Data used for training.\n\nInstructions: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key",
"api_format": "gemini", "api_format": "gemini",
"tools": [
{
"google_search": {}
},
]
}, },
"openrouter-llama4-maverick": { "openrouter-llama4-maverick": {
"name": "Llama 4 Maverick (OpenRouter)", "name": "Llama 4 Maverick",
"icon": "ollama-symbolic", "icon": "ollama-symbolic",
"description": "Online | OpenRouter | Meta's model", "description": "Online via OpenRouter | Meta's model",
"homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free", "homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free",
"endpoint": "https://openrouter.ai/api/v1/chat/completions", "endpoint": "https://openrouter.ai/api/v1/chat/completions",
"model": "meta-llama/llama-4-maverick:free", "model": "meta-llama/llama-4-maverick:free",
"requires_key": true, "requires_key": true,
"key_id": "openrouter", "key_id": "openrouter",
"key_get_link": "https://openrouter.ai/settings/keys", "key_get_link": "https://openrouter.ai/settings/keys",
"key_get_description": "Pricing: free. Data use policy varies depending on your OpenRouter account settings.\n\nInstructions: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key",
}, },
"openrouter-deepseek-r1": { "openrouter-deepseek-r1": {
"name": "DeepSeek R1 (OpenRouter)", "name": "DeepSeek R1",
"icon": "deepseek-symbolic", "icon": "deepseek-symbolic",
"description": "Online | OpenRouter | DeepSeek's reasoning model", "description": "Online via OpenRouter | DeepSeek's reasoning model",
"homepage": "https://openrouter.ai/deepseek/deepseek-r1:free", "homepage": "https://openrouter.ai/deepseek/deepseek-r1:free",
"endpoint": "https://openrouter.ai/api/v1/chat/completions", "endpoint": "https://openrouter.ai/api/v1/chat/completions",
"model": "deepseek/deepseek-r1:free", "model": "deepseek/deepseek-r1:free",
@@ -87,7 +99,8 @@ Singleton {
words = words.map((word) => { words = words.map((word) => {
return (word.charAt(0).toUpperCase() + word.slice(1)) return (word.charAt(0).toUpperCase() + word.slice(1))
}); });
words[words.length - 1] = `[${words[words.length - 1]}]`; // Surround the last word with square brackets if (words[words.length - 1] === "Latest") words.pop();
else words[words.length - 1] = `(${words[words.length - 1]})`; // Surround the last word with square brackets
const result = words.join(' '); const result = words.join(' ');
return result; return result;
} }
@@ -105,7 +118,7 @@ Singleton {
root.models[model] = { root.models[model] = {
"name": guessModelName(model), "name": guessModelName(model),
"icon": guessModelLogo(model), "icon": guessModelLogo(model),
"description": `Local (Ollama) | ${model}`, "description": `Local Ollama model: ${model}`,
"homepage": `https://ollama.com/library/${model}`, "homepage": `https://ollama.com/library/${model}`,
"endpoint": "http://localhost:11434/v1/chat/completions", "endpoint": "http://localhost:11434/v1/chat/completions",
"model": model, "model": model,
@@ -138,12 +151,26 @@ Singleton {
root.messages = [...root.messages]; root.messages = [...root.messages];
} }
function addApiKeyAdvice(model) {
root.addMessage(
StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command<br/><br/>For {0}, you can grab one at:\n\n{1}\n\n{2}'),
model.name, model.key_get_link, model.key_get_description ?? qsTr("<i>No further instruction provided</i>")),
Ai.interfaceRole
);
}
function setModel(model, feedback = true) { function setModel(model, feedback = true) {
if (!model) model = "" if (!model) model = ""
model = model.toLowerCase() model = model.toLowerCase()
if (modelList.indexOf(model) !== -1) { if (modelList.indexOf(model) !== -1) {
currentModel = model currentModel = model
if (feedback) root.addMessage("Model set to " + models[model].name, Ai.interfaceRole) if (feedback) root.addMessage("Model set to " + models[model].name, Ai.interfaceRole)
if (models[model].requires_key) {
// If key not there show advice
if (root.apiKeysLoaded && (!root.apiKeys[models[model].key_id] || root.apiKeys[models[model].key_id].length === 0)) {
root.addApiKeyAdvice(models[model])
}
}
} else { } else {
if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n- ") + modelList.join("\n- "), Ai.interfaceRole) if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n- ") + modelList.join("\n- "), Ai.interfaceRole)
} }
@@ -159,14 +186,11 @@ Singleton {
return; return;
} }
if (!key || key.length === 0) { if (!key || key.length === 0) {
root.addMessage( const model = models[currentModel];
StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command<br/><br/>For {0}, you can grab one at:\n\n{1}'), root.addApiKeyAdvice(model)
models[currentModel].name, models[currentModel].key_get_link),
Ai.interfaceRole
);
return; return;
} }
KeyringStorage.setNestedField(["apiKeys", model.key_id], key); KeyringStorage.setNestedField(["apiKeys", model.key_id], key.trim());
root.addMessage("API key set for " + model.name, Ai.interfaceRole); root.addMessage("API key set for " + model.name, Ai.interfaceRole);
} }
@@ -175,7 +199,7 @@ Singleton {
if (model.requires_key) { if (model.requires_key) {
const key = root.apiKeys[model.key_id]; const key = root.apiKeys[model.key_id];
if (key) { if (key) {
root.addMessage(StringUtils.format(qsTr("API key:\n\n`{0}`"), key), Ai.interfaceRole); root.addMessage(StringUtils.format(qsTr("API key:\n\n```txt\n{0}\n```"), key), Ai.interfaceRole);
} else { } else {
root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole); root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole);
} }
@@ -212,9 +236,7 @@ Singleton {
"parts": [{ text: message.content }] "parts": [{ text: message.content }]
})), })),
"tools": [ "tools": [
{ ...model.tools,
"google_search": {}
}
], ],
"system_instruction": { "system_instruction": {
"parts": [{ text: root.systemPrompt }] "parts": [{ text: root.systemPrompt }]
@@ -288,11 +310,15 @@ Singleton {
} }
function parseGeminiBuffer() { function parseGeminiBuffer() {
const dataJson = JSON.parse(requester.geminiBuffer); try {
const dataJson = JSON.parse(requester.geminiBuffer);
const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text
requester.message.content += responseContent; requester.message.content += responseContent;
requester.geminiBuffer = ""; } catch (e) {
requester.message.content += requester.geminiBuffer
} finally {
requester.geminiBuffer = "";
}
} }
function handleGeminiResponseLine(line) { function handleGeminiResponseLine(line) {
@@ -352,7 +378,7 @@ Singleton {
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {
console.log("RAW DATA: ", data); // console.log("RAW DATA: ", data);
if (data.length === 0) return; if (data.length === 0) return;
// Handle response line // Handle response line
@@ -386,6 +412,10 @@ Singleton {
} catch (e) { } catch (e) {
// console.log("[AI] Could not parse response on exit: ", e); // console.log("[AI] Could not parse response on exit: ", e);
} }
if (requester.message.content.includes("API key not valid")) {
root.addApiKeyAdvice(models[requester.message.model]);
}
} }
} }
@@ -10,6 +10,7 @@ import QtQuick;
Singleton { Singleton {
id: root id: root
property bool loaded: false
property var keyringData: ({}) property var keyringData: ({})
// onKeyringDataChanged: { // onKeyringDataChanged: {
// console.log("[KeyringStorage] Keyring data changed:", JSON.stringify(root.keyringData)); // console.log("[KeyringStorage] Keyring data changed:", JSON.stringify(root.keyringData));
@@ -109,6 +110,7 @@ Singleton {
root.keyringData = {}; root.keyringData = {};
saveKeyringData() saveKeyringData()
} }
root.loaded = true;
} }
} }