From 6758d8daf390b26101d85b8bb70b04f443c3cd87 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 10 May 2025 01:53:43 +0200 Subject: [PATCH] ai: add api key advice --- .config/quickshell/services/Ai.qml | 80 +++++++++++++------ .../quickshell/services/KeyringStorage.qml | 2 + 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/.config/quickshell/services/Ai.qml b/.config/quickshell/services/Ai.qml index ff796f267..0cccb740d 100644 --- a/.config/quickshell/services/Ai.qml +++ b/.config/quickshell/services/Ai.qml @@ -18,6 +18,7 @@ Singleton { property string systemPrompt: ConfigOptions.ai.systemPrompt ?? "" property var messages: [] readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {} + readonly property var apiKeysLoaded: KeyringStorage.loaded // Model properties: // - name: Name of the model @@ -27,35 +28,46 @@ Singleton { // - model: Model name of the model // - 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_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: { - "gemini-2.0-flash-gemini-api": { + "gemini-2.0-flash-search": { "name": "Gemini 2.0 Flash", "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", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "model": "gemini-2.0-flash", "requires_key": true, "key_id": "gemini", "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", + "tools": [ + { + "google_search": {} + }, + ] }, "openrouter-llama4-maverick": { - "name": "Llama 4 Maverick (OpenRouter)", + "name": "Llama 4 Maverick", "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", "endpoint": "https://openrouter.ai/api/v1/chat/completions", "model": "meta-llama/llama-4-maverick:free", "requires_key": true, "key_id": "openrouter", "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": { - "name": "DeepSeek R1 (OpenRouter)", + "name": "DeepSeek R1", "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", "endpoint": "https://openrouter.ai/api/v1/chat/completions", "model": "deepseek/deepseek-r1:free", @@ -87,7 +99,8 @@ Singleton { words = words.map((word) => { 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(' '); return result; } @@ -105,7 +118,7 @@ Singleton { root.models[model] = { "name": guessModelName(model), "icon": guessModelLogo(model), - "description": `Local (Ollama) | ${model}`, + "description": `Local Ollama model: ${model}`, "homepage": `https://ollama.com/library/${model}`, "endpoint": "http://localhost:11434/v1/chat/completions", "model": model, @@ -138,12 +151,26 @@ Singleton { 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

For {0}, you can grab one at:\n\n{1}\n\n{2}'), + model.name, model.key_get_link, model.key_get_description ?? qsTr("No further instruction provided")), + Ai.interfaceRole + ); + } + function setModel(model, feedback = true) { if (!model) model = "" model = model.toLowerCase() if (modelList.indexOf(model) !== -1) { currentModel = model 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 { if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n- ") + modelList.join("\n- "), Ai.interfaceRole) } @@ -159,14 +186,11 @@ Singleton { return; } if (!key || key.length === 0) { - 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

For {0}, you can grab one at:\n\n{1}'), - models[currentModel].name, models[currentModel].key_get_link), - Ai.interfaceRole - ); + const model = models[currentModel]; + root.addApiKeyAdvice(model) 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); } @@ -175,7 +199,7 @@ Singleton { if (model.requires_key) { const key = root.apiKeys[model.key_id]; 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 { root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole); } @@ -212,9 +236,7 @@ Singleton { "parts": [{ text: message.content }] })), "tools": [ - { - "google_search": {} - } + ...model.tools, ], "system_instruction": { "parts": [{ text: root.systemPrompt }] @@ -288,11 +310,15 @@ Singleton { } function parseGeminiBuffer() { - const dataJson = JSON.parse(requester.geminiBuffer); - - const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text - requester.message.content += responseContent; - requester.geminiBuffer = ""; + try { + const dataJson = JSON.parse(requester.geminiBuffer); + const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text + requester.message.content += responseContent; + } catch (e) { + requester.message.content += requester.geminiBuffer + } finally { + requester.geminiBuffer = ""; + } } function handleGeminiResponseLine(line) { @@ -352,7 +378,7 @@ Singleton { stdout: SplitParser { onRead: data => { - console.log("RAW DATA: ", data); + // console.log("RAW DATA: ", data); if (data.length === 0) return; // Handle response line @@ -386,6 +412,10 @@ Singleton { } catch (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]); + } } } diff --git a/.config/quickshell/services/KeyringStorage.qml b/.config/quickshell/services/KeyringStorage.qml index 78254ed90..39632c6ca 100644 --- a/.config/quickshell/services/KeyringStorage.qml +++ b/.config/quickshell/services/KeyringStorage.qml @@ -10,6 +10,7 @@ import QtQuick; Singleton { id: root + property bool loaded: false property var keyringData: ({}) // onKeyringDataChanged: { // console.log("[KeyringStorage] Keyring data changed:", JSON.stringify(root.keyringData)); @@ -109,6 +110,7 @@ Singleton { root.keyringData = {}; saveKeyringData() } + root.loaded = true; } }