From c252ed0b9ffabf39c2e6bfce7bf419b527981b9c Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Tue, 3 Jun 2025 15:25:39 +0800 Subject: [PATCH] support grok3 & support model update --- CHANGELOG.md | 6 +- package.json | 2 +- .../main-panel/chat/chat-box/chat.ts | 13 +---- .../main-panel/chat/core/handle-tool-calls.ts | 57 +++++++++++++++++-- .../main-panel/chat/core/task-loop.ts | 29 +++++----- renderer/src/views/about/index.vue | 2 +- renderer/src/views/setting/api.vue | 43 +++++++++++++- renderer/src/views/setting/connect-test.vue | 21 +++++++ renderer/src/views/setting/llm.ts | 19 ++++++- service/src/hook/axios-fetch.ts | 2 +- service/src/hook/llm.ts | 24 ++++++++ service/src/llm/llm.controller.ts | 22 ++++++- service/src/llm/llm.service.ts | 19 +++---- 13 files changed, 210 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f569a..1d9ce59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Change Log ## [main] 0.1.4 -- 支持 Google Gemini 模型。 -- 支持 Grok3 的 tool call 流式传输。 +- 重新实现 openai 协议的底层网络实现,从而支持 Google Gemini 全系列模型。 +- 实现 index 适配器,从而支持 Grok3 全系列模型。 +- 解决 issue#23 插件创建连接时报错“Cannot read properties of undefined (reading 'name')” +- 在填写 apikey 和 baseurl 的情况下,现在可以一键刷新模型列表,避免用户手动输入模型列表。 ## [main] 0.1.3 - 解决 issue#21 点击按钮后的发送文本后不会清空当前的输入框。 diff --git a/package.json b/package.json index c1cabd4..82cd970 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openmcp", "displayName": "OpenMCP", "description": "An all in one MCP Client/TestTool", - "version": "0.1.3", + "version": "0.1.4", "publisher": "kirigaya", "author": { "name": "kirigaya", diff --git a/renderer/src/components/main-panel/chat/chat-box/chat.ts b/renderer/src/components/main-panel/chat/chat-box/chat.ts index 5af36dd..93eba02 100644 --- a/renderer/src/components/main-panel/chat/chat-box/chat.ts +++ b/renderer/src/components/main-panel/chat/chat-box/chat.ts @@ -14,7 +14,8 @@ export enum MessageState { ToolCall = 'tool call failed', None = 'none', Success = 'success', - ParseJsonError = 'parse json error' + ParseJsonError = 'parse json error', + NoToolFunction = 'no tool function', } export interface IExtraInfo { @@ -69,15 +70,7 @@ export interface ChatStorage { settings: ChatSetting } -export interface ToolCall { - id?: string; - index?: number; - type: string; - function: { - name: string; - arguments: string; - } -} +export type ToolCall = OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta.ToolCall; interface PromptTextItem { type: 'prompt' diff --git a/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts b/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts index 05d2b8f..34e3583 100644 --- a/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts +++ b/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts @@ -1,16 +1,32 @@ import type { ToolCallContent, ToolCallResponse } from "@/hook/type"; import { MessageState, type ToolCall } from "../chat-box/chat"; import { mcpClientAdapter } from "@/views/connect/core"; +import type { BasicLlmDescription } from "@/views/setting/llm"; +import { redLog } from "@/views/setting/util"; export interface ToolCallResult { state: MessageState; content: ToolCallContent[]; } +export type IToolCallIndex = number; + export async function handleToolCalls(toolCall: ToolCall): Promise { + + if (!toolCall.function) { + return { + content: [{ + type: 'error', + text: 'no tool function' + }], + state: MessageState.NoToolFunction + } + } + // 反序列化 streaming 来的参数字符串 - const toolName = toolCall.function.name; - const argsResult = deserializeToolCallResponse(toolCall.function.arguments); + // TODO: check as string + const toolName = toolCall.function.name as string; + const argsResult = deserializeToolCallResponse(toolCall.function.arguments as string); if (argsResult.error) { return { @@ -47,8 +63,7 @@ function deserializeToolCallResponse(toolArgs: string) { function handleToolResponse(toolResponse: ToolCallResponse) { if (typeof toolResponse === 'string') { // 如果是 string,说明是错误信息 - console.log(toolResponse); - + redLog('error happen' + JSON.stringify(toolResponse)); return { content: [{ @@ -83,4 +98,38 @@ function parseErrorObject(error: any): string { } else { return error.toString(); } +} + +function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map): IToolCallIndex { + // grok 采用 id 作为 index,需要将 id 映射到 zero-based 的 index + if (!toolCall.id) { + return 0; + } + if (!callId2Index.has(toolCall.id)) { + callId2Index.set(toolCall.id, callId2Index.size); + } + return callId2Index.get(toolCall.id)!; +} + +function geminiIndexAdapter(toolCall: ToolCall): IToolCallIndex { + // TODO: 等待后续支持 + return 0; +} + +function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex { + return toolCall.index || 0; +} + +export function getToolCallIndexAdapter(llm: BasicLlmDescription) { + + if (llm.userModel.startsWith('gemini')) { + return geminiIndexAdapter; + } + + if (llm.userModel.startsWith('grok')) { + const callId2Index = new Map(); + return (toolCall: ToolCall) => grokIndexAdapter(toolCall, callId2Index); + } + + return defaultIndexAdapter; } \ No newline at end of file diff --git a/renderer/src/components/main-panel/chat/core/task-loop.ts b/renderer/src/components/main-panel/chat/core/task-loop.ts index 93106bd..3a55f56 100644 --- a/renderer/src/components/main-panel/chat/core/task-loop.ts +++ b/renderer/src/components/main-panel/chat/core/task-loop.ts @@ -3,10 +3,10 @@ import { ref, type Ref } from "vue"; import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat"; import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge"; import type { OpenAI } from 'openai'; -import { llmManager, llms } from "@/views/setting/llm"; +import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm"; import { pinkLog, redLog } from "@/views/setting/util"; import { ElMessage } from "element-plus"; -import { handleToolCalls, type ToolCallResult } from "./handle-tool-calls"; +import { getToolCallIndexAdapter, handleToolCalls, type IToolCallIndex, type ToolCallResult } from "./handle-tool-calls"; import { getPlatform } from "@/api/platform"; import { getSystemPrompt } from "../chat-box/options/system-prompt"; import { mcpSetting } from "@/hook/mcp"; @@ -45,7 +45,7 @@ export class TaskLoop { private onToolCalled: (toolCallResult: ToolCallResult) => ToolCallResult = toolCallResult => toolCallResult; private onEpoch: () => void = () => {}; private completionUsage: ChatCompletionChunk['usage'] | undefined; - private llmConfig: any; + private llmConfig?: BasicLlmDescription; constructor( private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20, maxJsonParseRetry: 3, adapter: undefined }, @@ -76,7 +76,7 @@ export class TaskLoop { } } - private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk) { + private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) { const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; if (toolCall) { @@ -84,7 +84,8 @@ export class TaskLoop { console.warn('tool_call.index is undefined or null'); } - const index = toolCall.index || 0; + + const index = toolcallIndexAdapter(toolCall); const currentCall = this.streamingToolCalls.value[index]; if (currentCall === undefined) { @@ -105,10 +106,10 @@ export class TaskLoop { currentCall.id = toolCall.id; } if (toolCall.function?.name) { - currentCall.function.name = toolCall.function.name; + currentCall.function!.name = toolCall.function.name; } if (toolCall.function?.arguments) { - currentCall.function.arguments += toolCall.function.arguments; + currentCall.function!.arguments += toolCall.function.arguments; } } } @@ -123,7 +124,7 @@ export class TaskLoop { } } - private doConversation(chatData: ChatCompletionCreateParamsBase) { + private doConversation(chatData: ChatCompletionCreateParamsBase, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) { return new Promise((resolve, reject) => { const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { @@ -134,7 +135,7 @@ export class TaskLoop { // 处理增量的 content 和 tool_calls this.handleChunkDeltaContent(chunk); - this.handleChunkDeltaToolCalls(chunk); + this.handleChunkDeltaToolCalls(chunk, toolcallIndexAdapter); this.handleChunkUsage(chunk); this.onChunk(chunk); @@ -352,9 +353,11 @@ export class TaskLoop { } this.currentChatId = chatData.id!; + const llm = this.getLlmConfig(); + const toolcallIndexAdapter = getToolCallIndexAdapter(llm); // 发送请求 - const doConverationResult = await this.doConversation(chatData); + const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter); console.log('[doConverationResult] Response'); console.log(doConverationResult); @@ -405,7 +408,7 @@ export class TaskLoop { } else if (toolCallResult.state === MessageState.Success) { tabStorage.messages.push({ role: 'tool', - index: toolCall.index || 0, + index: toolcallIndexAdapter(toolCall), tool_call_id: toolCall.id || '', content: toolCallResult.content, extraInfo: { @@ -419,8 +422,8 @@ export class TaskLoop { tabStorage.messages.push({ role: 'tool', - index: toolCall.index || 0, - tool_call_id: toolCall.id || toolCall.function.name, + index: toolcallIndexAdapter(toolCall), + tool_call_id: toolCall.id || toolCall.function!.name, content: toolCallResult.content, extraInfo: { created: Date.now(), diff --git a/renderer/src/views/about/index.vue b/renderer/src/views/about/index.vue index 539ab67..1dd0a08 100644 --- a/renderer/src/views/about/index.vue +++ b/renderer/src/views/about/index.vue @@ -6,7 +6,7 @@

- OpenMCP Client 0.1.3 由 OpenMCP@锦恢 开发 + OpenMCP Client 0.1.4 由 OpenMCP@锦恢 开发

diff --git a/renderer/src/views/setting/api.vue b/renderer/src/views/setting/api.vue index a9ea614..6af6ca4 100644 --- a/renderer/src/views/setting/api.vue +++ b/renderer/src/views/setting/api.vue @@ -51,10 +51,21 @@

+ type="success" + @click="addNewServer" + > {{ t("add-new-server") }} + + {{ "更新模型列表" }} + + item.object === 'model') + .map(item => item.id); + + llm.models = models; + saveLlmSetting(); + } else { + ElMessage.error('模型列表更新失败' + msg); + } + updateModelLoading.value = false; +} + function updateProvider() { if (editingIndex.value < 0) { return; diff --git a/renderer/src/views/setting/connect-test.vue b/renderer/src/views/setting/connect-test.vue index b2e11f0..f25e594 100644 --- a/renderer/src/views/setting/connect-test.vue +++ b/renderer/src/views/setting/connect-test.vue @@ -1,4 +1,7 @@