support grok3 & support model update

This commit is contained in:
锦恢 2025-06-03 15:25:39 +08:00
parent f8447cadb6
commit c252ed0b9f
13 changed files with 210 additions and 49 deletions

View File

@ -1,8 +1,10 @@
# Change Log # Change Log
## [main] 0.1.4 ## [main] 0.1.4
- 支持 Google Gemini 模型。 - 重新实现 openai 协议的底层网络实现,从而支持 Google Gemini 全系列模型。
- 支持 Grok3 的 tool call 流式传输。 - 实现 index 适配器,从而支持 Grok3 全系列模型。
- 解决 issue#23 插件创建连接时报错“Cannot read properties of undefined (reading 'name')”
- 在填写 apikey 和 baseurl 的情况下,现在可以一键刷新模型列表,避免用户手动输入模型列表。
## [main] 0.1.3 ## [main] 0.1.3
- 解决 issue#21 点击按钮后的发送文本后不会清空当前的输入框。 - 解决 issue#21 点击按钮后的发送文本后不会清空当前的输入框。

View File

@ -2,7 +2,7 @@
"name": "openmcp", "name": "openmcp",
"displayName": "OpenMCP", "displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool", "description": "An all in one MCP Client/TestTool",
"version": "0.1.3", "version": "0.1.4",
"publisher": "kirigaya", "publisher": "kirigaya",
"author": { "author": {
"name": "kirigaya", "name": "kirigaya",

View File

@ -14,7 +14,8 @@ export enum MessageState {
ToolCall = 'tool call failed', ToolCall = 'tool call failed',
None = 'none', None = 'none',
Success = 'success', Success = 'success',
ParseJsonError = 'parse json error' ParseJsonError = 'parse json error',
NoToolFunction = 'no tool function',
} }
export interface IExtraInfo { export interface IExtraInfo {
@ -69,15 +70,7 @@ export interface ChatStorage {
settings: ChatSetting settings: ChatSetting
} }
export interface ToolCall { export type ToolCall = OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta.ToolCall;
id?: string;
index?: number;
type: string;
function: {
name: string;
arguments: string;
}
}
interface PromptTextItem { interface PromptTextItem {
type: 'prompt' type: 'prompt'

View File

@ -1,16 +1,32 @@
import type { ToolCallContent, ToolCallResponse } from "@/hook/type"; import type { ToolCallContent, ToolCallResponse } from "@/hook/type";
import { MessageState, type ToolCall } from "../chat-box/chat"; import { MessageState, type ToolCall } from "../chat-box/chat";
import { mcpClientAdapter } from "@/views/connect/core"; import { mcpClientAdapter } from "@/views/connect/core";
import type { BasicLlmDescription } from "@/views/setting/llm";
import { redLog } from "@/views/setting/util";
export interface ToolCallResult { export interface ToolCallResult {
state: MessageState; state: MessageState;
content: ToolCallContent[]; content: ToolCallContent[];
} }
export type IToolCallIndex = number;
export async function handleToolCalls(toolCall: ToolCall): Promise<ToolCallResult> { export async function handleToolCalls(toolCall: ToolCall): Promise<ToolCallResult> {
if (!toolCall.function) {
return {
content: [{
type: 'error',
text: 'no tool function'
}],
state: MessageState.NoToolFunction
}
}
// 反序列化 streaming 来的参数字符串 // 反序列化 streaming 来的参数字符串
const toolName = toolCall.function.name; // TODO: check as string
const argsResult = deserializeToolCallResponse(toolCall.function.arguments); const toolName = toolCall.function.name as string;
const argsResult = deserializeToolCallResponse(toolCall.function.arguments as string);
if (argsResult.error) { if (argsResult.error) {
return { return {
@ -47,8 +63,7 @@ function deserializeToolCallResponse(toolArgs: string) {
function handleToolResponse(toolResponse: ToolCallResponse) { function handleToolResponse(toolResponse: ToolCallResponse) {
if (typeof toolResponse === 'string') { if (typeof toolResponse === 'string') {
// 如果是 string说明是错误信息 // 如果是 string说明是错误信息
console.log(toolResponse); redLog('error happen' + JSON.stringify(toolResponse));
return { return {
content: [{ content: [{
@ -83,4 +98,38 @@ function parseErrorObject(error: any): string {
} else { } else {
return error.toString(); return error.toString();
} }
}
function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map<string, number>): 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<string, number>();
return (toolCall: ToolCall) => grokIndexAdapter(toolCall, callId2Index);
}
return defaultIndexAdapter;
} }

View File

@ -3,10 +3,10 @@ import { ref, type Ref } from "vue";
import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat"; import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat";
import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge"; import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge";
import type { OpenAI } from 'openai'; 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 { pinkLog, redLog } from "@/views/setting/util";
import { ElMessage } from "element-plus"; 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 { getPlatform } from "@/api/platform";
import { getSystemPrompt } from "../chat-box/options/system-prompt"; import { getSystemPrompt } from "../chat-box/options/system-prompt";
import { mcpSetting } from "@/hook/mcp"; import { mcpSetting } from "@/hook/mcp";
@ -45,7 +45,7 @@ export class TaskLoop {
private onToolCalled: (toolCallResult: ToolCallResult) => ToolCallResult = toolCallResult => toolCallResult; private onToolCalled: (toolCallResult: ToolCallResult) => ToolCallResult = toolCallResult => toolCallResult;
private onEpoch: () => void = () => {}; private onEpoch: () => void = () => {};
private completionUsage: ChatCompletionChunk['usage'] | undefined; private completionUsage: ChatCompletionChunk['usage'] | undefined;
private llmConfig: any; private llmConfig?: BasicLlmDescription;
constructor( constructor(
private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20, maxJsonParseRetry: 3, adapter: undefined }, 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]; const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
if (toolCall) { if (toolCall) {
@ -84,7 +84,8 @@ export class TaskLoop {
console.warn('tool_call.index is undefined or null'); console.warn('tool_call.index is undefined or null');
} }
const index = toolCall.index || 0;
const index = toolcallIndexAdapter(toolCall);
const currentCall = this.streamingToolCalls.value[index]; const currentCall = this.streamingToolCalls.value[index];
if (currentCall === undefined) { if (currentCall === undefined) {
@ -105,10 +106,10 @@ export class TaskLoop {
currentCall.id = toolCall.id; currentCall.id = toolCall.id;
} }
if (toolCall.function?.name) { if (toolCall.function?.name) {
currentCall.function.name = toolCall.function.name; currentCall.function!.name = toolCall.function.name;
} }
if (toolCall.function?.arguments) { 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<IDoConversationResult>((resolve, reject) => { return new Promise<IDoConversationResult>((resolve, reject) => {
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
@ -134,7 +135,7 @@ export class TaskLoop {
// 处理增量的 content 和 tool_calls // 处理增量的 content 和 tool_calls
this.handleChunkDeltaContent(chunk); this.handleChunkDeltaContent(chunk);
this.handleChunkDeltaToolCalls(chunk); this.handleChunkDeltaToolCalls(chunk, toolcallIndexAdapter);
this.handleChunkUsage(chunk); this.handleChunkUsage(chunk);
this.onChunk(chunk); this.onChunk(chunk);
@ -352,9 +353,11 @@ export class TaskLoop {
} }
this.currentChatId = chatData.id!; 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] Response');
console.log(doConverationResult); console.log(doConverationResult);
@ -405,7 +408,7 @@ export class TaskLoop {
} else if (toolCallResult.state === MessageState.Success) { } else if (toolCallResult.state === MessageState.Success) {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'tool', role: 'tool',
index: toolCall.index || 0, index: toolcallIndexAdapter(toolCall),
tool_call_id: toolCall.id || '', tool_call_id: toolCall.id || '',
content: toolCallResult.content, content: toolCallResult.content,
extraInfo: { extraInfo: {
@ -419,8 +422,8 @@ export class TaskLoop {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'tool', role: 'tool',
index: toolCall.index || 0, index: toolcallIndexAdapter(toolCall),
tool_call_id: toolCall.id || toolCall.function.name, tool_call_id: toolCall.id || toolCall.function!.name,
content: toolCallResult.content, content: toolCallResult.content,
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),

View File

@ -6,7 +6,7 @@
</span> </span>
<p> <p>
OpenMCP Client 0.1.3 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发 OpenMCP Client 0.1.4 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
</p> </p>
<p> <p>

View File

@ -51,10 +51,21 @@
<div class="setting-save-container"> <div class="setting-save-container">
<el-button <el-button
id="add-new-server-button" id="add-new-server-button"
type="success" @click="addNewServer"> type="success"
@click="addNewServer"
>
{{ t("add-new-server") }} {{ t("add-new-server") }}
</el-button> </el-button>
<el-button
id="add-new-server-button"
type="success"
@click="updateModels"
:loading="updateModelLoading"
>
{{ "更新模型列表" }}
</el-button>
<el-button <el-button
type="primary" type="primary"
id="test-llm-button" id="test-llm-button"
@ -120,6 +131,7 @@ import { pinkLog } from './util';
import ConnectInterfaceOpenai from './connect-interface-openai.vue'; import ConnectInterfaceOpenai from './connect-interface-openai.vue';
import ConnectTest from './connect-test.vue'; import ConnectTest from './connect-test.vue';
import { llmSettingRef, makeSimpleTalk, simpleTestResult } from './api'; import { llmSettingRef, makeSimpleTalk, simpleTestResult } from './api';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'api' }); defineComponent({ name: 'api' });
const { t } = useI18n(); const { t } = useI18n();
@ -212,6 +224,35 @@ function addNewProvider() {
}; };
} }
const updateModelLoading = ref(false);
async function updateModels() {
updateModelLoading.value = true;
const llm = llms[llmManager.currentModelIndex];
const apiKey = llm.userToken;
const baseURL = llm.baseUrl;
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('llm/models', {
apiKey,
baseURL
});
if (code === 200 && Array.isArray(msg)) {
const models = msg
.filter(item => item.object === 'model')
.map(item => item.id);
llm.models = models;
saveLlmSetting();
} else {
ElMessage.error('模型列表更新失败' + msg);
}
updateModelLoading.value = false;
}
function updateProvider() { function updateProvider() {
if (editingIndex.value < 0) { if (editingIndex.value < 0) {
return; return;

View File

@ -1,4 +1,7 @@
<template> <template>
<div class="extra-info warning" v-if="isGoogle">
当前模型组协议兼容性较差特别是 gemini-2.0-flash 模型的函数调用能力不稳定如果想要稳定使用 gemini 的服务请尽可能使用最新的模型或者使用 newApi 进行协议转接
</div>
<div class="connect-test" v-if="simpleTestResult.done || simpleTestResult.error"> <div class="connect-test" v-if="simpleTestResult.done || simpleTestResult.error">
<div class="test-result"> <div class="test-result">
<div class="result-item" v-if="simpleTestResult.done"> <div class="result-item" v-if="simpleTestResult.done">
@ -18,9 +21,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { simpleTestResult } from './api'; import { simpleTestResult } from './api';
import { llmManager, llms } from './llm';
import { computed } from '@vue/reactivity';
const { t } = useI18n(); const { t } = useI18n();
const isGoogle = computed(() => {
const model = llms[llmManager.currentModelIndex];
return model.userModel.startsWith('gemini') || model.baseUrl.includes('googleapis');
});
console.log(llms[llmManager.currentModelIndex]);
</script> </script>
<style scoped> <style scoped>
@ -61,4 +74,12 @@ const { t } = useI18n();
.result-item .iconfont { .result-item .iconfont {
font-size: 16px; font-size: 16px;
} }
.extra-info.warning {
background-color: rgba(230, 162, 60, 0.5);
padding: 10px;
border-radius: 4px;
margin-top: 15px;
margin-bottom: 10px;
}
</style> </style>

View File

@ -6,12 +6,25 @@ import type { ToolCall } from '@/components/main-panel/chat/chat-box/chat';
import I18n from '@/i18n'; import I18n from '@/i18n';
const { t } = I18n.global; const { t } = I18n.global;
export const llms = reactive<any[]>([]); export const llms = reactive<BasicLlmDescription[]>([]);
export const llmManager = reactive({ export const llmManager = reactive({
currentModelIndex: 0, currentModelIndex: 0,
}); });
export interface BasicLlmDescription {
id: string,
name: string,
baseUrl: string,
models: string[],
isOpenAICompatible: boolean,
description: string,
website: string,
userToken: string,
userModel: string,
[key: string]: any
}
export function createTest(call: ToolCall) { export function createTest(call: ToolCall) {
const tab = createTab('tool', 0); const tab = createTab('tool', 0);
tab.componentIndex = 2; tab.componentIndex = 2;
@ -21,8 +34,8 @@ export function createTest(call: ToolCall) {
const storage: ToolStorage = { const storage: ToolStorage = {
activeNames: [0], activeNames: [0],
currentToolName: call.function.name, currentToolName: call.function!.name!,
formData: JSON.parse(call.function.arguments) formData: JSON.parse(call.function!.arguments!)
}; };
tab.storage = storage; tab.storage = storage;

View File

@ -1,4 +1,4 @@
import axios, { AxiosProxyConfig } from "axios"; import axios from "axios";
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
interface FetchOptions { interface FetchOptions {

View File

@ -47,6 +47,30 @@ export const llms = [
userToken: '', userToken: '',
userModel: 'doubao-1.5-pro-32k' userModel: 'doubao-1.5-pro-32k'
}, },
{
id: 'gemini',
name: 'Gemini',
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
models: ['gemini-2.0-flash', 'gemini-2.5-flash-preview-05-20', 'gemini-2.5-pro-preview-05-06'],
provider: 'google',
isOpenAICompatible: true,
description: 'Google Gemini',
website: 'https://ai.google.dev/gemini-api/docs/models?hl=zh-cn%2F%2Fgemini-2.5-pro-preview-05-06#gemini-2.5-pro-preview-05-06',
userToken: '',
userModel: 'gemini-2.0-flash'
},
{
id: 'grok',
name: 'Grok',
baseUrl: 'https://api.x.ai/v1',
models: ['grok-3', 'grok-3-fast', 'grok-3-mini', 'grok-3-mini-fast'],
provider: 'xai',
isOpenAICompatible: true,
description: 'xAI Grok',
website: 'https://docs.x.ai/docs/models',
userToken: '',
userModel: 'grok-3-mini'
},
{ {
id: 'mistral', id: 'mistral',
name: 'Mistral', name: 'Mistral',

View File

@ -1,7 +1,7 @@
import { Controller, RequestClientType } from "../common"; import { OpenAI } from "openai";
import { Controller } from "../common";
import { RequestData } from "../common/index.dto"; import { RequestData } from "../common/index.dto";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";
import { getClient } from "../mcp/connect.service";
import { abortMessageService, streamingChatCompletion } from "./llm.service"; import { abortMessageService, streamingChatCompletion } from "./llm.service";
export class LlmController { export class LlmController {
@ -10,7 +10,7 @@ export class LlmController {
async chatCompletion(data: RequestData, webview: PostMessageble) { async chatCompletion(data: RequestData, webview: PostMessageble) {
try { try {
await streamingChatCompletion(data, webview); await streamingChatCompletion(data, webview);
} catch (error) { } catch (error) {
console.log('error' + error); console.log('error' + error);
@ -34,4 +34,20 @@ export class LlmController {
return abortMessageService(data, webview); return abortMessageService(data, webview);
} }
@Controller('llm/models')
async getModels(data: RequestData, webview: PostMessageble) {
const {
baseURL,
apiKey,
} = data;
const client = new OpenAI({ apiKey, baseURL });
const models = await client.models.list();
return {
code: 200,
msg: models.data
}
}
} }

View File

@ -31,19 +31,15 @@ export async function streamingChatCompletion(
console.log('openai fetch begin, proxyServer:', proxyServer); console.log('openai fetch begin, proxyServer:', proxyServer);
if (model.startsWith('gemini')) { if (model.startsWith('gemini') && init) {
// 该死的 google // 该死的 google
if (init) { init.headers = {
init.headers = { 'Content-Type': 'application/json',
'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`
'Authorization': `Bearer ${apiKey}`
}
} }
return await axiosFetch(input, init, { proxyServer });
} else {
return await fetch(input, init);
} }
return await axiosFetch(input, init, { proxyServer });
} }
}); });
@ -52,6 +48,9 @@ export async function streamingChatCompletion(
undefined: model.startsWith('gemini') ? undefined : parallelToolCalls; undefined: model.startsWith('gemini') ? undefined : parallelToolCalls;
await postProcessMessages(messages); await postProcessMessages(messages);
console.log('seriableTools', seriableTools);
console.log('seriableParallelToolCalls', seriableParallelToolCalls);
const stream = await client.chat.completions.create({ const stream = await client.chat.completions.create({
model, model,