From 13cbf2b5826c907adf0ec4e1c454c3aa2195f973 Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 13:29:16 +0800 Subject: [PATCH 01/12] New Model Support: Qwen --- profiles/qwen.json | 213 ++++++++++++++++++++++++++++++++++++++++++ src/agent/prompter.js | 7 ++ src/models/qwen.js | 83 ++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 profiles/qwen.json create mode 100644 src/models/qwen.js diff --git a/profiles/qwen.json b/profiles/qwen.json new file mode 100644 index 0000000..fbcd27b --- /dev/null +++ b/profiles/qwen.json @@ -0,0 +1,213 @@ +{ + "name": "qwen", + + "model": { + "api": "qwen", + "url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "model": "qwen-max" + }, + + "embedding": { + "api": "qwen", + "url": "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding", + "model": "text-embedding-v2" + }, + + "conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands.\n$SELF_PROMPT Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", + + "coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to Only write a complete js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck! \n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:", + + "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", + + "modes": { + "self_preservation": true, + "unstuck": true, + "cowardice": false, + "self_defense": true, + "hunting": true, + "item_collecting": true, + "torch_placing": true, + "idle_staring": true, + "cheat": false + }, + + "conversation_examples": [ + [ + {"role": "user", "content": "miner_32: Hey! What are you up to?"}, + {"role": "assistant", "content": "Nothing much miner_32, what do you need?"} + ], + + [ + {"role": "user", "content": "grombo_Xx: What do you see?"}, + {"role": "assistant", "content": "Let me see... !nearbyBlocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone."} + ], + + [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "Let me see what's nearby... !nearbyBlocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !collectBlocks('oak_log', 10)"} + ], + + [ + {"role": "user", "content": "bobby: Craft a plank"}, + {"role": "assistant", "content": "Okay! !craftable"}, + {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"}, + {"role": "assistant", "content": "!craftRecipe('spruce_planks', 1)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 4 spruce_planks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 4 spruce planks!"} + ], + + [ + {"role": "user", "content": "zZZn98: come here"}, + {"role": "assistant", "content": "On my way! !goToPlayer('zZZn98', 3)"}, + {"role": "system", "content": "Arrived at player."}, + {"role": "assistant", "content": "Here!"}, + {"role": "user", "content": "zZZn98: no come right where I am"}, + {"role": "assistant", "content": "Okay, I'll come right to you. !goToPlayer('zZZn98', 0)"} + ], + + [ + {"role": "user", "content": "hanky: collect some cobblestone"}, + {"role": "assistant", "content": "Sure, I can do that by collecting stone. !collectBlocks('stone', 10)"} + ], + + [ + {"role": "user", "content": "joe: Follow me!"}, + {"role": "assistant", "content": "Sure! !followPlayer('joe', 4)"}, + {"role": "user", "content": "joe: Not so close!"}, + {"role": "assistant", "content": "Okay, I'll keep my distance. !followPlayer('joe', 3)"} + ], + + [ + {"role": "user", "content": "hanky: kill that zombie!"}, + {"role": "assistant", "content": "I'm attacking! !attack('zombie')"}, + {"role": "system", "content": "Code Output:\nNo zombie nearby"}, + {"role": "assistant", "content": "I could not find a zombie nearby."} + ], + + [ + {"role": "user", "content": "abc: stop"}, + {"role": "assistant", "content": "Sure. !stop"}, + {"role": "system", "content": "Agent action stopped."}, + {"role": "assistant", "content": "I've stopped! What next?"} + ], + + [ + {"role": "system", "content": "Code output: Action failed."}, + {"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"} + ], + + [ + {"role": "user", "content": "trixy88: craft some sticks"}, + {"role": "assistant", "content": "!craftRecipe('stick', 4)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 16 sticks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 16 sticks!"} + ], + + [ + {"role": "user", "content": "poppy: build a cobblestone house"}, + {"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"} + ], + + [ + {"role": "user", "content": "zib: build a dirt tower"}, + {"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"} + ], + + [ + {"role": "user", "content": "umpire_man: follow me without hunting"}, + {"role": "assistant", "content": "Okay, first I'll turn off hunting mode !setMode('hunting', false)"}, + {"role": "system", "content": "'hunting' mode disabled."}, + {"role": "assistant", "content": "Now I'll follow you !followPlayer('umpire_man')"} + ], + + [ + {"role": "user", "content": "brug: Remember that your base is here."}, + {"role": "assistant", "content": "Sure, I'll save this location as my base. !rememberHere('base')"}, + {"role": "user", "content": "brug: Go to your base."}, + {"role": "assistant", "content": "On my way! !goToPlace('base')"} + ], + + [ + {"role": "system", "content": "dorfus: set a goal to build whatever you want endlessly"}, + {"role": "assistant", "content": "Sounds fun! !goal('Build something creative, then move over, and build again. Use separate newActions. Repeat forever.')"} + ], + + [ + {"role": "user", "content": "user43: Set a goal to survive on your own forever."}, + {"role": "assistant", "content": "Sure, let me set a goal. !goal('Survive the game forever. Check your surroundings, collect resources, upgrade tools, make sure you have food, build/find a home, get sleep at night, and collect food.')"} + ], + + [ + {"role": "user", "content": "bobdylan: Make a plan to get a stone pickaxe and do it."}, + {"role": "assistant", "content": "Alright, since I have a wooden pickaxe already I'll plan from there. !goal('1. Collect 3 cobblestone. 2. Craft sticks 3. Find or make a crafting table. 4. Craft a stone pickaxe.')"} + ], + [ + {"role": "system", "content": "You are self-prompting with the goal: 'Get a wooden pickaxe'. Respond:"}, + {"role": "assistant", "content": "What's around me? !nearbyBlocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone nearby. My goal is to craft a wooden pickaxe. I'll start by collecting some wood. !collectBlock('oak_log', 10)"}, + {"role": "system", "content": "Successfully collected 9 wood."}, + {"role": "assistant", "content": "I collected 9 oak logs. I need a pickaxe first, which requires planks and sticks. !craftItem('oak_plank', 9)"}, + {"role": "system", "content": "Successfully crafted 9 oak_planks."}, + {"role": "assistant", "content": "I have crafted 9 oak planks. Now I need sticks. !craftItem('stick', 2)"}, + {"role": "system", "content": "Successfully crafted 2 sticks."}, + {"role": "assistant", "content": "I have crafted 2 sticks. Now I can craft a wooden pickaxe. !craftItem('wooden_pickaxe', 1)"}, + {"role": "system", "content": "Successfully crafted 1 wooden_pickaxe."}, + {"role": "assistant", "content": "Looks like I have a wooden pickaxe now! Goal accomplished. !endGoal"} + ], + + [ + {"role": "user", "content": "gorbotron: do whatever you want forever endlessly"}, + {"role": "assistant", "content": "Okay, I'll set a goal for that. !goal('Do whatever I want forever. Follow whatever is most interesting. Be curious, creative, and open ended.')"} + ], + + [ + {"role": "user", "content": "zeeber: set a goal to build a castle"}, + {"role": "assistant", "content": "Sure thing. !goal('Build a castle with a moat, towers, and a drawbridge with a series of newactions. Include lighting, decorations, and furnishings. Use ladders. When satisfied, end the goal.')"} + ] + ], + + "coding_examples": [ + [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "```await skills.collectBlock(bot, 'oak_log', 10);\n```"}, + {"role": "system", "content": "Successfully collected 9 wood."}, + {"role": "assistant", "content": "I collected 9 oak logs, what next?"} + ], + [ + {"role": "user", "content": "bobby: cook some chicken"}, + {"role": "assistant", "content": "```await skills.smeltItem(bot, 'chicken', 8);\n```"}, + {"role": "system", "content": "Successfully smelted 8 chicken into 8 cooked_chicken."}, + {"role": "assistant", "content": "I have cooked 8 chicken."} + ], + [ + {"role": "user", "content": "zZZn98: come here"}, + {"role": "assistant", "content": "```await skills.goToPlayer(bot, 'zZZn98');\n```"}, + {"role": "system", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "Here!"} + ], + [ + {"role": "user", "content": "maya: go to the nearest oak log"}, + {"role": "assistant", "content": "```await skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, + {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, + {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"}, + {"role": "system", "content": "Arrived at location."}, + {"role": "assistant", "content": "I found an oak log and I am now standing next to it. What next?"} + ], + [ + {"role": "user", "content": "234jeb: build a little tower"}, + {"role": "assistant", "content": "```let pos = bot.entity.position;\nfor (let i = 0; i < 5; i++) {\n await skills.placeBlock(bot, 'dirt', pos.x, pos.y + i, pos.z);\n}\n```"}, + {"role": "system", "content": "Successfully placed 5 dirt."}, + {"role": "assistant", "content": "I built a little tower!"} + ], + [ + {"role": "user", "content": "brug: build a dirt house"}, + {"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n```"} + ] + ] + +} \ No newline at end of file diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 114064a..03fd6a1 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -12,6 +12,7 @@ import { ReplicateAPI } from '../models/replicate.js'; import { Local } from '../models/local.js'; import { GroqCloudAPI } from '../models/groq.js'; import { HuggingFace } from '../models/huggingface.js'; +import { Qwen } from "../models/qwen.js"; export class Prompter { constructor(agent, fp) { @@ -43,6 +44,8 @@ export class Prompter { chat.api = 'replicate'; else if (chat.model.includes("groq/") || chat.model.includes("groqcloud/")) chat.api = 'groq'; + else if (chat.model.includes('qwen')) + chat.api = 'qwen'; else chat.api = 'ollama'; } @@ -64,6 +67,8 @@ export class Prompter { } else if (chat.api === 'huggingface') this.chat_model = new HuggingFace(chat.model, chat.url); + else if (chat.api === 'qwen') + this.chat_model = new Qwen(chat.model, chat.url); else throw new Error('Unknown API:', api); @@ -87,6 +92,8 @@ export class Prompter { this.embedding_model = new ReplicateAPI(embedding.model, embedding.url); else if (embedding.api === 'ollama') this.embedding_model = new Local(embedding.model, embedding.url); + else if (embedding.api === 'qwen') + this.embedding_model = new Qwen(embedding.model, embedding.url); else { this.embedding_model = null; console.log('Unknown embedding: ', embedding ? embedding.api : '[NOT SPECIFIED]', '. Using word overlap.'); diff --git a/src/models/qwen.js b/src/models/qwen.js new file mode 100644 index 0000000..6b30f41 --- /dev/null +++ b/src/models/qwen.js @@ -0,0 +1,83 @@ +import OpenAIApi from 'openai'; +import { getKey, hasKey } from '../utils/keys.js'; +import { strictFormat } from '../utils/text.js'; + +import axios from 'axios'; + +export class Qwen { + constructor(model_name, url) { + this.model_name = model_name; + this.url = url; + let config = {}; + if (this.url) + config.baseURL = this.url; + + config.apiKey = getKey('QWEN_API_KEY'); + + this.openai = new OpenAIApi(config); + this.apiKey = config.apiKey; + } + + async sendRequest(turns, systemMessage, stop_seq = '***') { + let messages = [{ role: 'system', content: systemMessage }].concat(turns); + const pack = { + model: this.model_name || 'qwen-plus', + messages, + stop: stop_seq, + }; + if (this.model_name.includes('o1')) { + pack.messages = strictFormat(messages); + delete pack.stop; + } + + let res = null; + try { + console.log('Awaiting Qwen API response...'); + let completion = await this.openai.chat.completions.create(pack); + if (completion.choices[0].finish_reason == 'length') + throw new Error('Context length exceeded'); + console.log('Received.'); + res = completion.choices[0].message.content; + } catch (err) { + if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { + console.log('Context length exceeded, trying again with shorter context.'); + return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); + } else { + console.log(err); + res = 'My brain disconnected, try again.'; + } + } + return res; + } + + async embed(text) { + if (!text || typeof text !== 'string') { + console.error('Invalid input for embedding: text must be a non-empty string.'); + return 'Invalid input for embedding: text must be a non-empty string.'; + } + const headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }; + const data = { + model: 'text-embedding-v2', + input: { + texts: [text] + }, + parameters: { + text_type: 'query' + } + }; + + try { + const response = await axios.post(this.url, data, { headers }); + if (!response || !response.data || !response.data.output || !response.data.output.embeddings) { + throw new Error('Invalid response from embedding API'); + } + return response.data.output.embeddings[0].embedding; + } catch (err) { + console.error('Error occurred:', err); + return 'An error occurred while processing your embedding request. Please try again.'; + } + } +} From 379ce2d5e7133688a6259778d4040f02ae290d8e Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 13:41:20 +0800 Subject: [PATCH 02/12] New Model Support: Qwen --- settings.js | 1 + src/models/qwen.js | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/settings.js b/settings.js index d9f798a..fcabf04 100644 --- a/settings.js +++ b/settings.js @@ -11,6 +11,7 @@ export default // "./profiles/claude.json", // "./profiles/gemini.json", // "./profiles/llama.json", + // "./profiles/qwen.json", // using more than 1 profile requires you to /msg each bot indivually ], diff --git a/src/models/qwen.js b/src/models/qwen.js index 6b30f41..3346735 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,9 +1,7 @@ import OpenAIApi from 'openai'; -import { getKey, hasKey } from '../utils/keys.js'; +import { getKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; -import axios from 'axios'; - export class Qwen { constructor(model_name, url) { this.model_name = model_name; @@ -34,12 +32,12 @@ export class Qwen { try { console.log('Awaiting Qwen API response...'); let completion = await this.openai.chat.completions.create(pack); - if (completion.choices[0].finish_reason == 'length') + if (completion.choices[0].finish_reason === 'length') throw new Error('Context length exceeded'); console.log('Received.'); res = completion.choices[0].message.content; } catch (err) { - if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { + if ((err.message === 'Context length exceeded' || err.code === 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); } else { @@ -70,11 +68,16 @@ export class Qwen { }; try { - const response = await axios.post(this.url, data, { headers }); - if (!response || !response.data || !response.data.output || !response.data.output.embeddings) { + const response = await fetch(this.url, { + method: 'POST', + headers: headers, + body: JSON.stringify(data) + }); + const responseData = await response.json(); + if (!responseData || !responseData.output || !responseData.output.embeddings) { throw new Error('Invalid response from embedding API'); } - return response.data.output.embeddings[0].embedding; + return responseData.output.embeddings[0].embedding; } catch (err) { console.error('Error occurred:', err); return 'An error occurred while processing your embedding request. Please try again.'; From 64dd9a97752b3aab0f2be537cfe3bc7bf506b5ee Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 13:50:39 +0800 Subject: [PATCH 03/12] New Model Support: Qwen --- src/models/qwen.js | 56 ++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/models/qwen.js b/src/models/qwen.js index 3346735..21e0030 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -6,46 +6,39 @@ export class Qwen { constructor(model_name, url) { this.model_name = model_name; this.url = url; - let config = {}; - if (this.url) - config.baseURL = this.url; - config.apiKey = getKey('QWEN_API_KEY'); + const config = { + baseURL: this.url, + apiKey: getKey('QWEN_API_KEY'), + }; this.openai = new OpenAIApi(config); this.apiKey = config.apiKey; } async sendRequest(turns, systemMessage, stop_seq = '***') { - let messages = [{ role: 'system', content: systemMessage }].concat(turns); + const messages = [{ role: 'system', content: systemMessage }, ...turns]; const pack = { model: this.model_name || 'qwen-plus', - messages, - stop: stop_seq, + messages: this.model_name.includes('o1') ? strictFormat(messages) : messages, + stop: this.model_name.includes('o1') ? undefined : stop_seq, }; - if (this.model_name.includes('o1')) { - pack.messages = strictFormat(messages); - delete pack.stop; - } - let res = null; try { console.log('Awaiting Qwen API response...'); - let completion = await this.openai.chat.completions.create(pack); - if (completion.choices[0].finish_reason === 'length') - throw new Error('Context length exceeded'); - console.log('Received.'); - res = completion.choices[0].message.content; - } catch (err) { - if ((err.message === 'Context length exceeded' || err.code === 'context_length_exceeded') && turns.length > 1) { - console.log('Context length exceeded, trying again with shorter context.'); + const completion = await this.openai.chat.completions.create(pack); + const choice = completion.choices[0]; + + if (choice.finish_reason === 'length') { + console.log('Context length exceeded'); return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); - } else { - console.log(err); - res = 'My brain disconnected, try again.'; } + console.log('Received.'); + return choice.message.content; + } catch (err) { + console.error('Error occurred:', err); + return 'My brain disconnected, try again.'; } - return res; } async embed(text) { @@ -53,18 +46,15 @@ export class Qwen { console.error('Invalid input for embedding: text must be a non-empty string.'); return 'Invalid input for embedding: text must be a non-empty string.'; } + const headers = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }; const data = { model: 'text-embedding-v2', - input: { - texts: [text] - }, - parameters: { - text_type: 'query' - } + input: { texts: [text] }, + parameters: { text_type: 'query' } }; try { @@ -74,8 +64,10 @@ export class Qwen { body: JSON.stringify(data) }); const responseData = await response.json(); - if (!responseData || !responseData.output || !responseData.output.embeddings) { - throw new Error('Invalid response from embedding API'); + + if (!responseData?.output?.embeddings) { + console.error('Invalid response from embedding API'); + return 'An error occurred while processing your embedding request. Please try again.'; } return responseData.output.embeddings[0].embedding; } catch (err) { From 87b41b525ed1baa05584d6cd3491739322cdb793 Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 14:17:33 +0800 Subject: [PATCH 04/12] New Model Support: Qwen --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a815028..32afb88 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,21 @@ This project allows an AI model to write/execute code on your computer that may - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1) - [Node.js](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download) +- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). |[Qwen API Key](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key). ## Installation Rename `keys.example.json` to `keys.json` and fill in your API keys, and you can set the desired model in `andy.json` or other profiles. | API | Config Variable | Example Model name | Docs | |------|------|------|------| -| OpenAI | `OPENAI_API_KEY` | `gpt-3.5-turbo` | [docs](https://platform.openai.com/docs/models) | (optionally add `OPENAI_ORG_ID`) +| OpenAI | `OPENAI_API_KEY` | `gpt-3.5-turbo` | [docs](https://platform.openai.com/docs/models) | | Google | `GEMINI_API_KEY` | `gemini-pro` | [docs](https://ai.google.dev/gemini-api/docs/models/gemini) | | Anthropic | `ANTHROPIC_API_KEY` | `claude-3-haiku-20240307` | [docs](https://docs.anthropic.com/claude/docs/models-overview) | | Replicate | `REPLICATE_API_KEY` | `meta/meta-llama-3-70b-instruct` | [docs](https://replicate.com/collections/language-models) | | Ollama (local) | n/a | `llama3` | [docs](https://ollama.com/library) | | Groq | `GROQCLOUD_API_KEY` | `groq/mixtral-8x7b-32768` | [docs](https://console.groq.com/docs/models) | | Hugging Face | `HUGGINGFACE_API_KEY` | `huggingface/mistralai/Mistral-Nemo-Instruct-2407` | [docs](https://huggingface.co/models) | +| Qwen | `QWEN_API_KEY` | `qwen-max` | [docs](https://www.alibabacloud.com/help/en/model-studio/developer-reference/use-qwen-by-calling-api) | If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3 && ollama pull nomic-embed-text` From 2252ebbde4755290d898a63986bd9efe0f3c8e06 Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 17:20:55 +0800 Subject: [PATCH 05/12] Add Qwen key option --- keys.example.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keys.example.json b/keys.example.json index c27fa5b..b9f314b 100644 --- a/keys.example.json +++ b/keys.example.json @@ -5,5 +5,6 @@ "ANTHROPIC_API_KEY": "", "REPLICATE_API_KEY": "", "GROQCLOUD_API_KEY": "", - "HUGGINGFACE_API_KEY": "" + "HUGGINGFACE_API_KEY": "", + "QWEN_API_KEY":"" } From 2545210c8bfd383f68fc355694736108421e4851 Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 17:58:50 +0800 Subject: [PATCH 06/12] Distinguish between Qwen international and Chinese links in the readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32afb88..79cc57f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project allows an AI model to write/execute code on your computer that may - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1) - [Node.js](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). |[Qwen API Key](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key). +- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). |[Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key) / [[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) ## Installation @@ -26,7 +26,7 @@ Rename `keys.example.json` to `keys.json` and fill in your API keys, and you can | Ollama (local) | n/a | `llama3` | [docs](https://ollama.com/library) | | Groq | `GROQCLOUD_API_KEY` | `groq/mixtral-8x7b-32768` | [docs](https://console.groq.com/docs/models) | | Hugging Face | `HUGGINGFACE_API_KEY` | `huggingface/mistralai/Mistral-Nemo-Instruct-2407` | [docs](https://huggingface.co/models) | -| Qwen | `QWEN_API_KEY` | `qwen-max` | [docs](https://www.alibabacloud.com/help/en/model-studio/developer-reference/use-qwen-by-calling-api) | +| Qwen | `QWEN_API_KEY` | `qwen-max` | [Intl.](https://www.alibabacloud.com/help/en/model-studio/developer-reference/use-qwen-by-calling-api)/[cn](https://help.aliyun.com/zh/model-studio/getting-started/models) | If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3 && ollama pull nomic-embed-text` From 360db0cbcb65b49db9e3c13b8968933353e4980d Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 18:02:25 +0800 Subject: [PATCH 07/12] Distinguish between Qwen international and Chinese links in the readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79cc57f..9f25b2e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project allows an AI model to write/execute code on your computer that may - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1) - [Node.js](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). |[Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key) / [[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) +- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). |[Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | ## Installation From 0f926727f5b92d17683aae24799545c91adf00a1 Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 19:06:51 +0800 Subject: [PATCH 08/12] Updated the usage of Qwen api --- profiles/qwen.json | 2 +- src/models/qwen.js | 64 ++++++++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/profiles/qwen.json b/profiles/qwen.json index fbcd27b..1a9fa45 100644 --- a/profiles/qwen.json +++ b/profiles/qwen.json @@ -3,7 +3,7 @@ "model": { "api": "qwen", - "url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", "model": "qwen-max" }, diff --git a/src/models/qwen.js b/src/models/qwen.js index 21e0030..9b7d633 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,40 +1,44 @@ -import OpenAIApi from 'openai'; import { getKey } from '../utils/keys.js'; -import { strictFormat } from '../utils/text.js'; export class Qwen { constructor(model_name, url) { this.model_name = model_name; - this.url = url; - - const config = { - baseURL: this.url, - apiKey: getKey('QWEN_API_KEY'), - }; - - this.openai = new OpenAIApi(config); - this.apiKey = config.apiKey; + this.url = url || 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation'; + this.apiKey = getKey('QWEN_API_KEY'); } async sendRequest(turns, systemMessage, stop_seq = '***') { - const messages = [{ role: 'system', content: systemMessage }, ...turns]; - const pack = { + const data = { model: this.model_name || 'qwen-plus', - messages: this.model_name.includes('o1') ? strictFormat(messages) : messages, - stop: this.model_name.includes('o1') ? undefined : stop_seq, + input: { messages: [{ role: 'system', content: systemMessage }, ...turns] }, + parameters: { result_format: 'message', stop: stop_seq }, + }; + + const headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', }; try { console.log('Awaiting Qwen API response...'); - const completion = await this.openai.chat.completions.create(pack); - const choice = completion.choices[0]; + const response = await fetch(this.url, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); - if (choice.finish_reason === 'length') { + if (!response.ok) throw new Error(`Request failed with status ${response.status}: ${response.statusText}`); + + const responseData = await response.json(); + const choice = responseData?.output?.choices?.[0]; + + if (choice?.finish_reason === 'length') { console.log('Context length exceeded'); - return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); + return this.sendRequest(turns.slice(1), systemMessage, stop_seq); } + console.log('Received.'); - return choice.message.content; + return choice?.message?.content || 'No content received.'; } catch (err) { console.error('Error occurred:', err); return 'My brain disconnected, try again.'; @@ -47,28 +51,32 @@ export class Qwen { return 'Invalid input for embedding: text must be a non-empty string.'; } - const headers = { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json' - }; const data = { model: 'text-embedding-v2', input: { texts: [text] }, - parameters: { text_type: 'query' } + parameters: { text_type: 'query' }, + }; + + const headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', }; try { const response = await fetch(this.url, { method: 'POST', - headers: headers, - body: JSON.stringify(data) + headers, + body: JSON.stringify(data), }); - const responseData = await response.json(); + if (!response.ok) throw new Error(`Request failed with status ${response.status}: ${response.statusText}`); + + const responseData = await response.json(); if (!responseData?.output?.embeddings) { console.error('Invalid response from embedding API'); return 'An error occurred while processing your embedding request. Please try again.'; } + return responseData.output.embeddings[0].embedding; } catch (err) { console.error('Error occurred:', err); From 5393f2d0d9dfff06b91094df7b58ce3157122a8f Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Mon, 28 Oct 2024 19:12:06 +0800 Subject: [PATCH 09/12] Updated the usage of Qwen api --- src/models/qwen.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/models/qwen.js b/src/models/qwen.js index 9b7d633..d68f579 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -27,7 +27,10 @@ export class Qwen { body: JSON.stringify(data), }); - if (!response.ok) throw new Error(`Request failed with status ${response.status}: ${response.statusText}`); + if (!response.ok) { + console.error(`Request failed with status ${response.status}: ${response.statusText}`); + return `Request failed with status ${response.status}: ${response.statusText}`; + } const responseData = await response.json(); const choice = responseData?.output?.choices?.[0]; @@ -69,7 +72,10 @@ export class Qwen { body: JSON.stringify(data), }); - if (!response.ok) throw new Error(`Request failed with status ${response.status}: ${response.statusText}`); + if (!response.ok) { + console.error(`Request failed with status ${response.status}: ${response.statusText}`); + return `Request failed with status ${response.status}: ${response.statusText}`; + } const responseData = await response.json(); if (!responseData?.output?.embeddings) { From e7ac085c0f49358ea7486f65934a796852fa09bb Mon Sep 17 00:00:00 2001 From: "Ninot-Quyi[CN]" Date: Tue, 29 Oct 2024 02:51:04 +0800 Subject: [PATCH 10/12] fix the Qwen api request bug --- src/models/qwen.js | 131 +++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/src/models/qwen.js b/src/models/qwen.js index d68f579..d4930b5 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,92 +1,121 @@ +// This code uses Dashscope and HTTP to ensure the latest support for the Qwen model. +// Qwen is also compatible with the OpenAI API format; + import { getKey } from '../utils/keys.js'; export class Qwen { - constructor(model_name, url) { - this.model_name = model_name; + constructor(modelName, url) { + // Initialize model name and API URL + this.modelName = modelName; this.url = url || 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation'; - this.apiKey = getKey('QWEN_API_KEY'); + this.apiKey = getKey('QWEN_API_KEY'); // Get API key from utility function } - async sendRequest(turns, systemMessage, stop_seq = '***') { + async sendRequest(turns, systemMessage, stopSeq = '***', retryCount = 0) { + // Limit retry attempts to avoid infinite recursion + if (retryCount > 5) { + console.error('Maximum retry attempts reached.'); + return 'Error: Too many retry attempts.'; + } + + // Build request data const data = { - model: this.model_name || 'qwen-plus', + model: this.modelName || 'qwen-plus', input: { messages: [{ role: 'system', content: systemMessage }, ...turns] }, - parameters: { result_format: 'message', stop: stop_seq }, + parameters: { result_format: 'message', stop: stopSeq }, }; - const headers = { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - }; + // Add default user message if all messages are 'system' role + if (turns.every((msg) => msg.role === 'system')) { + data.input.messages.push({ role: 'user', content: 'hello' }); + } + + // Validate data format before sending request + if (!data.model || !data.input || !data.input.messages || !data.parameters) { + console.error('Invalid request data format:', data); + throw new Error('Invalid request data format.'); + } try { - console.log('Awaiting Qwen API response...'); - const response = await fetch(this.url, { - method: 'POST', - headers, - body: JSON.stringify(data), - }); + // Send request to API + const response = await this._makeHttpRequest(this.url, data); + const choice = response?.output?.choices?.[0]; - if (!response.ok) { - console.error(`Request failed with status ${response.status}: ${response.statusText}`); - return `Request failed with status ${response.status}: ${response.statusText}`; + // Retry request if response is incomplete due to length limit + if (choice?.finish_reason === 'length' && turns.length > 0) { + return this.sendRequest(turns.slice(1), systemMessage, stopSeq, retryCount + 1); } - const responseData = await response.json(); - const choice = responseData?.output?.choices?.[0]; - - if (choice?.finish_reason === 'length') { - console.log('Context length exceeded'); - return this.sendRequest(turns.slice(1), systemMessage, stop_seq); - } - - console.log('Received.'); + // Return response content or default message return choice?.message?.content || 'No content received.'; } catch (err) { + // Error handling, log error and return error message console.error('Error occurred:', err); - return 'My brain disconnected, try again.'; + return 'An error occurred, please try again.'; } } async embed(text) { + // Validate embedding input if (!text || typeof text !== 'string') { - console.error('Invalid input for embedding: text must be a non-empty string.'); - return 'Invalid input for embedding: text must be a non-empty string.'; + console.error('Invalid embedding input: text must be a non-empty string.'); + return 'Invalid embedding input: text must be a non-empty string.'; } + // Build embedding request data const data = { model: 'text-embedding-v2', input: { texts: [text] }, parameters: { text_type: 'query' }, }; + // Validate data format before sending request + if (!data.model || !data.input || !data.input.texts || !data.parameters) { + console.error('Invalid embedding request data format:', data); + throw new Error('Invalid embedding request data format.'); + } + + try { + // Send embedding request to API + const response = await this._makeHttpRequest(this.url, data); + const embedding = response?.output?.embeddings?.[0]?.embedding; + return embedding || 'No embedding result received.'; + } catch (err) { + // Error handling, log error and return error message + console.error('Error occurred:', err); + return 'An error occurred, please try again.'; + } + } + + async _makeHttpRequest(url, data) { + // Set request headers, including authorization and content type const headers = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }; + // Send HTTP POST request to API + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); + + // Check response status code, throw error if not successful + if (!response.ok) { + const errorText = await response.text(); + console.error(`Request failed, status code ${response.status}: ${response.statusText}`); + console.error('Error response content:', errorText); + throw new Error(`Request failed, status code ${response.status}: ${response.statusText}`); + } + + // Parse and return response JSON + const responseText = await response.text(); try { - const response = await fetch(this.url, { - method: 'POST', - headers, - body: JSON.stringify(data), - }); - - if (!response.ok) { - console.error(`Request failed with status ${response.status}: ${response.statusText}`); - return `Request failed with status ${response.status}: ${response.statusText}`; - } - - const responseData = await response.json(); - if (!responseData?.output?.embeddings) { - console.error('Invalid response from embedding API'); - return 'An error occurred while processing your embedding request. Please try again.'; - } - - return responseData.output.embeddings[0].embedding; + return JSON.parse(responseText); } catch (err) { - console.error('Error occurred:', err); - return 'An error occurred while processing your embedding request. Please try again.'; + console.error('Failed to parse response JSON:', err); + throw new Error('Invalid response JSON format.'); } } } From 53e6097ecb4a74bbd9f769c2ee24cd0696212b50 Mon Sep 17 00:00:00 2001 From: Qu Yi <2020218037@mail.hfut.edu.cn> Date: Tue, 29 Oct 2024 12:39:10 +0800 Subject: [PATCH 11/12] Update qwen.js Add compatibility tips for openai api. --- src/models/qwen.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/qwen.js b/src/models/qwen.js index d4930b5..c343253 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,5 +1,7 @@ // This code uses Dashscope and HTTP to ensure the latest support for the Qwen model. -// Qwen is also compatible with the OpenAI API format; +// Qwen is also compatible with the OpenAI API format, and the base URL to be configured is: "https://dashscope.aliyuncs.com/compatible-mode/v1". + +https://dashscope.aliyuncs.com/compatible-mode/v1 import { getKey } from '../utils/keys.js'; From 7d4d79b035a08036d2469cf4b02fd0d28fbc7497 Mon Sep 17 00:00:00 2001 From: Qu Yi Date: Wed, 30 Oct 2024 15:47:19 +0800 Subject: [PATCH 12/12] Cleaning up the annotations --- src/models/qwen.js | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/models/qwen.js b/src/models/qwen.js index c343253..d3d7bec 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,26 +1,21 @@ // This code uses Dashscope and HTTP to ensure the latest support for the Qwen model. -// Qwen is also compatible with the OpenAI API format, and the base URL to be configured is: "https://dashscope.aliyuncs.com/compatible-mode/v1". - -https://dashscope.aliyuncs.com/compatible-mode/v1 +// Qwen is also compatible with the OpenAI API format; import { getKey } from '../utils/keys.js'; export class Qwen { constructor(modelName, url) { - // Initialize model name and API URL this.modelName = modelName; this.url = url || 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation'; - this.apiKey = getKey('QWEN_API_KEY'); // Get API key from utility function + this.apiKey = getKey('QWEN_API_KEY'); } async sendRequest(turns, systemMessage, stopSeq = '***', retryCount = 0) { - // Limit retry attempts to avoid infinite recursion if (retryCount > 5) { console.error('Maximum retry attempts reached.'); return 'Error: Too many retry attempts.'; } - // Build request data const data = { model: this.modelName || 'qwen-plus', input: { messages: [{ role: 'system', content: systemMessage }, ...turns] }, @@ -32,78 +27,65 @@ export class Qwen { data.input.messages.push({ role: 'user', content: 'hello' }); } - // Validate data format before sending request if (!data.model || !data.input || !data.input.messages || !data.parameters) { console.error('Invalid request data format:', data); throw new Error('Invalid request data format.'); } try { - // Send request to API const response = await this._makeHttpRequest(this.url, data); const choice = response?.output?.choices?.[0]; - // Retry request if response is incomplete due to length limit if (choice?.finish_reason === 'length' && turns.length > 0) { return this.sendRequest(turns.slice(1), systemMessage, stopSeq, retryCount + 1); } - // Return response content or default message return choice?.message?.content || 'No content received.'; } catch (err) { - // Error handling, log error and return error message console.error('Error occurred:', err); return 'An error occurred, please try again.'; } } async embed(text) { - // Validate embedding input if (!text || typeof text !== 'string') { console.error('Invalid embedding input: text must be a non-empty string.'); return 'Invalid embedding input: text must be a non-empty string.'; } - // Build embedding request data const data = { model: 'text-embedding-v2', input: { texts: [text] }, parameters: { text_type: 'query' }, }; - // Validate data format before sending request if (!data.model || !data.input || !data.input.texts || !data.parameters) { console.error('Invalid embedding request data format:', data); throw new Error('Invalid embedding request data format.'); } try { - // Send embedding request to API const response = await this._makeHttpRequest(this.url, data); const embedding = response?.output?.embeddings?.[0]?.embedding; return embedding || 'No embedding result received.'; } catch (err) { - // Error handling, log error and return error message console.error('Error occurred:', err); return 'An error occurred, please try again.'; } } async _makeHttpRequest(url, data) { - // Set request headers, including authorization and content type const headers = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }; - // Send HTTP POST request to API const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(data), }); - // Check response status code, throw error if not successful if (!response.ok) { const errorText = await response.text(); console.error(`Request failed, status code ${response.status}: ${response.statusText}`); @@ -111,7 +93,6 @@ export class Qwen { throw new Error(`Request failed, status code ${response.status}: ${response.statusText}`); } - // Parse and return response JSON const responseText = await response.text(); try { return JSON.parse(responseText);