feat: add model switching functionality for AI agent (#11839)

This commit is contained in:
CityFun
2026-02-09 09:51:03 +00:00
committed by GitHub
parent 8a7be481b0
commit 46b8d3bd80
23 changed files with 443 additions and 17 deletions
+20
View File
@@ -71,6 +71,26 @@ func (b *BaseApi) DeleteAgent(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Update Agent model config
// @Accept json
// @Param request body dto.AgentModelConfigUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/model/update [post]
func (b *BaseApi) UpdateAgentModelConfig(c *gin.Context) {
var req dto.AgentModelConfigUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateModelConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Providers
// @Success 200 {object} []dto.ProviderInfo
+7
View File
@@ -38,6 +38,7 @@ type AgentItem struct {
Status string `json:"status"`
Message string `json:"message"`
AppInstallID uint `json:"appInstallId"`
AccountID uint `json:"accountId"`
AppVersion string `json:"appVersion"`
Container string `json:"containerName"`
WebUIPort int `json:"webUIPort"`
@@ -54,6 +55,12 @@ type AgentDeleteReq struct {
ForceDelete bool `json:"forceDelete"`
}
type AgentModelConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"`
}
type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"`
+188 -11
View File
@@ -32,6 +32,7 @@ type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
Delete(req dto.AgentDeleteReq) error
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
GetProviders() ([]dto.ProviderInfo, error)
CreateAccount(req dto.AgentAccountCreateReq) error
UpdateAccount(req dto.AgentAccountUpdateReq) error
@@ -209,6 +210,64 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error {
return nil
}
func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error {
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
if err != nil {
return err
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil {
return err
}
if !account.Verified {
return buserr.New("ErrAgentAccountNotVerified")
}
provider := strings.ToLower(strings.TrimSpace(account.Provider))
if !isSupportedAgentProvider(provider) {
return buserr.New("ErrAgentProviderNotSupported")
}
modelName := strings.TrimSpace(req.Model)
if modelName == "" {
return buserr.New("ErrAgentProviderMismatch")
}
if !strings.HasPrefix(modelName, provider+"/") {
return buserr.New("ErrAgentProviderMismatch")
}
baseURL := strings.TrimSpace(account.BaseURL)
if provider != "ollama" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
baseURL = defaultURL
}
}
if provider == "ollama" && baseURL == "" {
return buserr.New("ErrAgentBaseURLRequired")
}
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
return buserr.New("ErrAgentApiKeyRequired")
}
confDir := ""
if agent.ConfigPath != "" {
confDir = path.Dir(agent.ConfigPath)
} else if agent.AppInstallID > 0 {
install, errGet := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
if errGet == nil {
confDir = path.Join(install.GetPath(), "data", "conf")
}
}
if confDir == "" {
return buserr.New("ErrRecordNotFound")
}
if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, account.APIKey, agent.Token); err != nil {
return err
}
agent.Provider = provider
agent.Model = modelName
agent.BaseURL = baseURL
agent.APIKey = account.APIKey
agent.AccountID = account.ID
return agentRepo.Save(agent)
}
func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
definitions := providerDefinitions()
providers := make([]dto.ProviderInfo, 0, len(definitions))
@@ -612,6 +671,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
Status: agent.Status,
Message: agent.Message,
AppInstallID: agent.AppInstallID,
AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
}
@@ -768,12 +828,20 @@ type modelProvider struct {
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
ID string `json:"id"`
Name string `json:"name"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Cost modelCost `json:"cost"`
}
type modelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cacheRead"`
CacheWrite float64 `json:"cacheWrite"`
}
func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token string) error {
@@ -814,16 +882,28 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
}
provider = strings.ToLower(strings.TrimSpace(provider))
modelID := modelName
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
modelID = parts[1]
}
configProvider := provider
primaryModel := modelName
if provider == "kimi" {
configProvider = "moonshot"
primaryModel = "moonshot/" + modelID
}
if provider == "deepseek" {
cfg.Agents.Defaults.Model.Primary = modelName
base := baseURL
if base == "" {
base = "https://api.deepseek.com/v1"
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"deepseek": {
ApiKey: "${DEEPSEEK_API_KEY}",
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
@@ -834,16 +914,44 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
Input: []string{"text"},
ContextWindow: 128000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "moonshot" || provider == "kimi" {
cfg.Agents.Defaults.Model.Primary = primaryModel
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
configProvider: {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: modelID,
Name: modelID,
Reasoning: strings.Contains(modelID, "thinking"),
Input: []string{"text"},
ContextWindow: 256000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "ollama" {
modelID := modelName
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
modelID = parts[1]
}
cfg.Agents.Defaults.Model.Primary = modelName
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
@@ -859,6 +967,37 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
Input: []string{"text"},
ContextWindow: 160000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "kimi-coding" {
cfg.Agents.Defaults.Model.Primary = modelName
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"kimi-coding": {
ApiKey: plainKey,
BaseUrl: base,
Api: "anthropic-messages",
Models: []modelEntry{
{
ID: modelID,
Name: modelID,
Reasoning: true,
Input: []string{"text"},
ContextWindow: 200000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
@@ -896,6 +1035,12 @@ func providerEnvKey(provider string) string {
return "MINIMAX_API_KEY"
case "deepseek":
return "DEEPSEEK_API_KEY"
case "moonshot":
return "MOONSHOT_API_KEY"
case "kimi":
return "KIMI_API_KEY"
case "kimi-coding":
return "KIMI_API_KEY"
case "qwen":
return "QWEN_API_KEY"
case "ollama":
@@ -969,6 +1114,31 @@ func providerDefinitions() map[string]providerDefinition {
{ID: "minimax/Minimax-M2.1", Name: "Minimax M2.1"},
},
},
"moonshot": {
Sort: 7,
BaseURL: "https://api.moonshot.ai/v1",
Models: []dto.ProviderModelInfo{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
},
},
"kimi": {
Sort: 8,
BaseURL: "https://api.moonshot.cn/v1",
Models: []dto.ProviderModelInfo{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
},
},
"kimi-coding": {
Sort: 9,
BaseURL: "https://api.moonshot.cn/anthropic/v1",
Models: []dto.ProviderModelInfo{
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
},
},
}
}
@@ -999,6 +1169,13 @@ func buildVerifyRequest(provider, baseURL, apiKey string) (string, map[string]st
return base + "/models", headers
}
return base + "/v1/models", headers
case "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
return base + "/models", headers
}
return base + "/v1/models", headers
case "gemini":
if strings.Contains(base, "/v1beta") {
return fmt.Sprintf("%s/models?key=%s", base, apiKey), headers
+6
View File
@@ -948,6 +948,12 @@ func defaultBaseURL(provider string) (string, bool) {
return "https://api.minimax.chat/v1", true
case "deepseek":
return "https://api.deepseek.com/v1", true
case "moonshot":
return "https://api.moonshot.ai/v1", true
case "kimi":
return "https://api.moonshot.cn/v1", true
case "kimi-coding":
return "https://api.moonshot.cn/anthropic/v1", true
case "qwen":
return "https://dashscope.aliyuncs.com/compatible-mode/v1", true
default:
+1
View File
@@ -43,6 +43,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents", baseApi.CreateAgent)
aiToolsRouter.POST("/agents/search", baseApi.PageAgents)
aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent)
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
+7 -1
View File
@@ -272,6 +272,7 @@ export namespace AI {
status: string;
message: string;
appInstallId: number;
accountId: number;
appVersion: string;
containerName: string;
webUIPort: number;
@@ -282,13 +283,18 @@ export namespace AI {
createdAt: string;
}
export interface AgentDeleteReq {
id: number;
taskID: string;
forceDelete: boolean;
}
export interface AgentModelConfigUpdateReq {
agentId: number;
accountId: number;
model: string;
}
export interface ProviderModelInfo {
id: string;
name: string;
+4
View File
@@ -104,6 +104,10 @@ export const deleteAgent = (req: AI.AgentDeleteReq) => {
return http.post(`/ai/agents/delete`, req);
};
export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => {
return http.post(`/ai/agents/model/update`, req);
};
export const getAgentProviders = () => {
return http.get<AI.ProviderInfo[]>(`/ai/agents/providers`);
};
+1
View File
@@ -696,6 +696,7 @@ const message = {
manualModel: 'Manual input',
verified: 'Verified',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -692,6 +692,7 @@ const message = {
manualModel: 'Entrada manual de modelo',
verified: 'Verificado',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -681,6 +681,7 @@ const message = {
manualModel: '手動入力',
verified: '検証済み',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -678,6 +678,7 @@ const message = {
manualModel: '수동 입력',
verified: '검증됨',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -693,6 +693,7 @@ const message = {
manualModel: 'Input manual',
verified: 'Disahkan',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -690,6 +690,7 @@ const message = {
manualModel: 'Entrada manual',
verified: 'Verificado',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -686,6 +686,7 @@ const message = {
manualModel: 'Ручной ввод',
verified: 'Проверено',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -700,6 +700,7 @@ const message = {
manualModel: 'Manuel giriş',
verified: 'Doğrulandı',
configTitle: 'Configuration',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
feishu: 'Feishu',
dmPolicy: 'DM Policy',
+1
View File
@@ -668,6 +668,7 @@ const message = {
manualModel: '手動輸入模型',
verified: '驗證狀態',
configTitle: '配置',
switchModelSuccess: '模型切換成功',
channelsTab: '聊天渠道',
feishu: '飛書',
dmPolicy: '私聊策略',
+1
View File
@@ -670,6 +670,7 @@ const message = {
manualModel: '手动输入模型',
verified: '验证状态',
configTitle: '配置',
switchModelSuccess: '模型切换成功',
channelsTab: '聊天渠道',
feishu: '飞书',
dmPolicy: '私聊策略',
@@ -174,6 +174,9 @@ const providerLabelMap: Record<string, string> = {
openai: 'OpenAI',
ollama: 'Ollama',
minimax: 'MiniMax',
moonshot: 'Moonshot',
kimi: 'Kimi',
'kimi-coding': 'Kimi Coding',
qwen: 'Qwen',
deepseek: 'DeepSeek',
anthropic: 'Anthropic',
@@ -1,6 +1,9 @@
<template>
<DrawerPro v-model="open" :header="header" size="large" @close="handleClose">
<el-tabs v-model="activeTab" tab-position="left" class="config-tabs" @tab-click="handleTabClick">
<el-tab-pane :label="t('aiTools.model.model')" name="model">
<ModelTab ref="modelRef" @updated="handleModelUpdated" />
</el-tab-pane>
<el-tab-pane :label="t('aiTools.agents.channelsTab')" name="channels">
<ChannelsTab ref="channelsRef" />
</el-tab-pane>
@@ -14,13 +17,25 @@ import type { TabsPaneContext } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import ChannelsTab from './tabs/channels.vue';
import ModelTab from './tabs/model.vue';
const { t } = useI18n();
const emit = defineEmits(['updated']);
const open = ref(false);
const activeTab = ref('channels');
const activeTab = ref('model');
const header = ref('');
const agentId = ref(0);
const currentAgent = ref<AI.AgentItem>();
const channelsRef = ref();
const modelRef = ref();
const loadModel = async () => {
if (!currentAgent.value) {
return;
}
await nextTick();
await modelRef.value?.load(currentAgent.value);
};
const loadChannels = async () => {
if (agentId.value <= 0) {
@@ -31,21 +46,29 @@ const loadChannels = async () => {
};
const handleClose = () => {
activeTab.value = 'channels';
activeTab.value = 'model';
};
const handleTabClick = async (pane: TabsPaneContext) => {
if (pane.paneName === 'model' && currentAgent.value) {
await loadModel();
}
if (pane.paneName === 'channels' && agentId.value > 0) {
await loadChannels();
}
};
const handleModelUpdated = () => {
emit('updated');
};
const openDrawer = async (agent: AI.AgentItem) => {
agentId.value = agent.id;
currentAgent.value = agent;
header.value = `${agent.name} - ${t('aiTools.agents.configTitle')}`;
activeTab.value = 'channels';
activeTab.value = 'model';
open.value = true;
await loadChannels();
await loadModel();
};
defineExpose({
@@ -0,0 +1,160 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
<el-form-item :label="t('aiTools.agents.account')" prop="accountId">
<el-select v-model="form.accountId" @change="handleAccountChange">
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.manualModel')">
<el-switch v-model="form.manualModel" @change="handleManualModelChange" />
</el-form-item>
<el-form-item :label="t('aiTools.model.model')" prop="model">
<el-input v-if="form.manualModel" v-model="form.model" />
<el-select v-else v-model="form.model" filterable>
<el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveModel">
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
const emit = defineEmits(['updated']);
const { t } = useI18n();
const loading = ref(false);
const saving = ref(false);
const formRef = ref<FormInstance>();
const agentId = ref(0);
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
const accountOptions = ref<AI.AgentAccountItem[]>([]);
const modelOptions = ref<AI.ProviderModelInfo[]>([]);
const form = reactive({
accountId: undefined as unknown as number,
manualModel: false,
model: '',
});
const rules = reactive({
accountId: [Rules.requiredSelect],
model: [Rules.requiredInput],
});
const loadProviders = async () => {
if (Object.keys(providerModels.value).length > 0) {
return;
}
const res = await getAgentProviders();
const data = res.data || [];
providerModels.value = data.reduce((acc, item) => {
acc[item.provider] = item.models || [];
return acc;
}, {} as Record<string, AI.ProviderModelInfo[]>);
};
const loadAccounts = async () => {
const res = await pageAgentAccounts({
page: 1,
pageSize: 200,
provider: '',
name: '',
});
accountOptions.value = res.data.items || [];
};
const setModelsByProvider = (provider: string) => {
modelOptions.value = providerModels.value[provider] || [];
};
const handleAccountChange = () => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (!selected) {
modelOptions.value = [];
form.model = '';
return;
}
setModelsByProvider(selected.provider);
if (!form.manualModel && (!form.model || !form.model.startsWith(`${selected.provider}/`))) {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
};
const handleManualModelChange = (val: boolean) => {
if (val) {
return;
}
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (!selected) {
form.model = '';
return;
}
if (!form.model || !form.model.startsWith(`${selected.provider}/`)) {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
};
const load = async (agent: AI.AgentItem) => {
loading.value = true;
try {
agentId.value = agent.id;
await loadProviders();
await loadAccounts();
if (accountOptions.value.length === 0) {
form.accountId = undefined as unknown as number;
form.model = '';
modelOptions.value = [];
return;
}
const currentAccount =
accountOptions.value.find((item) => item.id === agent.accountId) || accountOptions.value[0];
form.accountId = currentAccount.id;
setModelsByProvider(currentAccount.provider);
const inProviderModels = modelOptions.value.some((item) => item.id === agent.model);
form.manualModel = !inProviderModels;
if (agent.model && (form.manualModel || agent.model.startsWith(`${currentAccount.provider}/`))) {
form.model = agent.model;
} else {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
} finally {
loading.value = false;
}
};
const saveModel = async () => {
if (!agentId.value || !formRef.value) {
return;
}
await formRef.value.validate();
saving.value = true;
try {
await updateAgentModelConfig({
agentId: agentId.value,
accountId: form.accountId,
model: form.model,
});
MsgSuccess(t('aiTools.agents.switchModelSuccess'));
emit('updated');
} finally {
saving.value = false;
}
};
defineExpose({
load,
});
</script>
+4 -1
View File
@@ -81,7 +81,7 @@
<AddDialog ref="addRef" @search="search" @task="openTaskLog" />
<TaskLog ref="taskLogRef" @close="search" />
<DeleteDialog ref="deleteRef" @close="search" />
<ConfigDrawer ref="configRef" />
<ConfigDrawer ref="configRef" @updated="search" />
<AppUpgrade ref="upgradeRef" @close="search" />
<ComposeLogs ref="composeLogRef" />
<TerminalDialog ref="dialogTerminalRef" />
@@ -127,6 +127,9 @@ const providerLabelMap: Record<string, string> = {
openai: 'OpenAI',
ollama: 'Ollama',
minimax: 'MiniMax',
moonshot: 'Moonshot',
kimi: 'Kimi',
'kimi-coding': 'Kimi Coding',
qwen: 'Qwen',
deepseek: 'DeepSeek',
anthropic: 'Anthropic',
@@ -58,6 +58,9 @@ const providerLabelMap: Record<string, string> = {
openai: 'OpenAI',
ollama: 'Ollama',
minimax: 'MiniMax',
moonshot: 'Moonshot',
kimi: 'Kimi',
'kimi-coding': 'Kimi Coding',
qwen: 'Qwen',
anthropic: 'Anthropic',
gemini: 'Gemini',
@@ -61,6 +61,9 @@ const providerLabelMap: Record<string, string> = {
openai: 'OpenAI',
ollama: 'Ollama',
minimax: 'MiniMax',
moonshot: 'Moonshot',
kimi: 'Kimi',
'kimi-coding': 'Kimi Coding',
qwen: 'Qwen',
deepseek: 'DeepSeek',
anthropic: 'Anthropic',