mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 08:52:46 +00:00
removed model picker
This commit is contained in:
@@ -111,7 +111,6 @@ export interface OpenclawSkillsLimits {
|
||||
|
||||
export interface OpenclawSubagentsSection {
|
||||
allowAgents?: string[];
|
||||
model?: string | { primary?: string; fallbacks?: string[] };
|
||||
thinking?: string;
|
||||
requireAgentId?: boolean;
|
||||
}
|
||||
@@ -172,26 +171,6 @@ export interface AgentBudgetResponse {
|
||||
|
||||
export type AgentBudgetPatch = Partial<Record<AgentBudgetKey, number | null>>;
|
||||
|
||||
// ── Agent model config ──
|
||||
|
||||
export interface AgentModelOption {
|
||||
key: string;
|
||||
alias: string | null;
|
||||
}
|
||||
|
||||
export interface AgentModelConfigResponse {
|
||||
agentId: string;
|
||||
known: boolean;
|
||||
override: string | null;
|
||||
systemDefault: string | null;
|
||||
effective: string | null;
|
||||
available: AgentModelOption[];
|
||||
}
|
||||
|
||||
export interface AgentModelConfigPatch {
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
// ── Agent skills (per-agent allowlist) ──
|
||||
|
||||
export interface AgentSkillSummary {
|
||||
@@ -221,7 +200,6 @@ export type AgentSubagentsThinking = 'minimal' | 'low' | 'medium' | 'high' | 'in
|
||||
|
||||
export interface AgentSubagentsConfig {
|
||||
allowAgents: string[] | null;
|
||||
model: string | null;
|
||||
thinking: string | null;
|
||||
requireAgentId: boolean | null;
|
||||
}
|
||||
@@ -231,12 +209,10 @@ export interface AgentSubagentsResponse {
|
||||
known: boolean;
|
||||
config: AgentSubagentsConfig;
|
||||
availableAgents: { id: string; name: string | null }[];
|
||||
availableModels: AgentModelOption[];
|
||||
}
|
||||
|
||||
export interface AgentSubagentsPatch {
|
||||
allowAgents?: string[] | null;
|
||||
model?: string | null;
|
||||
thinking?: string | null;
|
||||
requireAgentId?: boolean | null;
|
||||
}
|
||||
|
||||
@@ -404,35 +404,6 @@ const updateBudget: RequestHandler = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getModelConfig: RequestHandler = async (req, res, next) => {
|
||||
try {
|
||||
const agentRepo = AppDataSource.getRepository(Agent);
|
||||
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
||||
if (!agent?.openclawAgentId) {
|
||||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
return res.json(ocService.getAgentModelConfig(agent.openclawAgentId));
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateModelConfig: RequestHandler = async (req, res, next) => {
|
||||
try {
|
||||
const agentRepo = AppDataSource.getRepository(Agent);
|
||||
const agent = await agentRepo.findOneBy({ _id: Number(req.params.id) });
|
||||
if (!agent?.openclawAgentId) {
|
||||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
const body = (req.body ?? {}) as { model?: string | null };
|
||||
const result = ocService.setAgentModel(agent.openclawAgentId, body.model ?? null);
|
||||
if (!result.ok) return res.status(400).json(result);
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getSkillsConfig: RequestHandler = async (req, res, next) => {
|
||||
try {
|
||||
const agentRepo = AppDataSource.getRepository(Agent);
|
||||
@@ -522,8 +493,6 @@ export {
|
||||
serveWorkspaceUpload,
|
||||
getBudget,
|
||||
updateBudget,
|
||||
getModelConfig,
|
||||
updateModelConfig,
|
||||
getSkillsConfig,
|
||||
updateSkillsConfig,
|
||||
getSubagentsConfig,
|
||||
|
||||
@@ -397,72 +397,6 @@ paths:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
/agent/{id}/model-config:
|
||||
get:
|
||||
tags:
|
||||
- agent
|
||||
security:
|
||||
- bearerAuth: []
|
||||
operationId: getAgentModelConfig
|
||||
summary: Get per-agent model override
|
||||
description: >
|
||||
Returns the agent's model override (if any), the effective model,
|
||||
the system default, and the list of configured models available
|
||||
to pick from.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id'
|
||||
responses:
|
||||
200:
|
||||
description: Model configuration for this agent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/agentModelConfigResponse'
|
||||
404:
|
||||
description: Agent not found
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
patch:
|
||||
tags:
|
||||
- agent
|
||||
security:
|
||||
- bearerAuth: []
|
||||
operationId: updateAgentModelConfig
|
||||
summary: Set or clear the per-agent model override
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/id'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Model key to pin for this agent. Send null to inherit default.
|
||||
responses:
|
||||
200:
|
||||
description: Updated model configuration
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/agentModelConfigMutationResponse'
|
||||
400:
|
||||
description: Model update failed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/agentModelConfigMutationResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
422:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
/agent/{id}/skills:
|
||||
get:
|
||||
tags:
|
||||
@@ -776,48 +710,6 @@ components:
|
||||
config:
|
||||
$ref: '#/components/schemas/agentBudgetResponse'
|
||||
#
|
||||
agentModelOption:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: Model key (what gets written to config).
|
||||
alias:
|
||||
type: string
|
||||
nullable: true
|
||||
#
|
||||
agentModelConfigResponse:
|
||||
type: object
|
||||
properties:
|
||||
agentId:
|
||||
type: string
|
||||
known:
|
||||
type: boolean
|
||||
override:
|
||||
type: string
|
||||
nullable: true
|
||||
systemDefault:
|
||||
type: string
|
||||
nullable: true
|
||||
effective:
|
||||
type: string
|
||||
nullable: true
|
||||
available:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/agentModelOption'
|
||||
#
|
||||
agentModelConfigMutationResponse:
|
||||
type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
config:
|
||||
$ref: '#/components/schemas/agentModelConfigResponse'
|
||||
#
|
||||
agentSkillSummary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -874,9 +766,6 @@ components:
|
||||
items:
|
||||
type: string
|
||||
description: Explicit child-agent allowlist, or null when unrestricted.
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
thinking:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -908,10 +797,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/agentRef'
|
||||
description: Configured agents (excluding self) that can appear in allowAgents.
|
||||
availableModels:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/agentModelOption'
|
||||
#
|
||||
agentSubagentsPatch:
|
||||
type: object
|
||||
@@ -922,9 +807,6 @@ components:
|
||||
nullable: true
|
||||
items:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
thinking:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
@@ -31,11 +31,6 @@ router
|
||||
.get(auth, validate.id, controller.getSubagentsConfig)
|
||||
.patch(auth, validate.subagentsPatch, controller.updateSubagentsConfig);
|
||||
|
||||
router
|
||||
.route('/agent/:id(\\d+)/model-config')
|
||||
.get(auth, validate.id, controller.getModelConfig)
|
||||
.patch(auth, validate.modelPatch, controller.updateModelConfig);
|
||||
|
||||
router
|
||||
.route('/agent/:id(\\d+)/budget')
|
||||
.get(auth, validate.id, controller.getBudget)
|
||||
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error('Body must be an object.');
|
||||
}
|
||||
const allowed = ['allowAgents', 'model', 'thinking', 'requireAgentId'];
|
||||
const allowed = ['allowAgents', 'thinking', 'requireAgentId'];
|
||||
const unknownKey = Object.keys(value).find((k) => !allowed.includes(k));
|
||||
if (unknownKey) {
|
||||
throw new Error(`Unknown field: ${unknownKey}`);
|
||||
@@ -103,9 +103,6 @@ export default {
|
||||
throw new Error('Every allowed agent id must be a string.');
|
||||
}
|
||||
}
|
||||
if ('model' in v && v.model !== null && typeof v.model !== 'string') {
|
||||
throw new Error('"model" must be a string or null.');
|
||||
}
|
||||
if ('thinking' in v && v.thinking !== null && typeof v.thinking !== 'string') {
|
||||
throw new Error('"thinking" must be a string or null.');
|
||||
}
|
||||
@@ -120,20 +117,6 @@ export default {
|
||||
}),
|
||||
]),
|
||||
|
||||
modelPatch: validate([
|
||||
param('id').isInt().withMessage('Incorrect request url'),
|
||||
body('model').custom((value) => {
|
||||
if (value === null) return true;
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('"model" must be a string or null.');
|
||||
}
|
||||
if (value.length > 200) {
|
||||
throw new Error('"model" is too long.');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
]),
|
||||
|
||||
budgetPatch: validate([
|
||||
param('id').isInt().withMessage('Incorrect request url'),
|
||||
body().custom((value) => {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import {
|
||||
AgentModelConfigResponse,
|
||||
AgentModelOption,
|
||||
OpenclawAgentEntry,
|
||||
OpenclawConfig,
|
||||
} from '../../@types/openclaw';
|
||||
import { ocExec } from '../openclawGateway';
|
||||
import { openclawConfigPath } from './paths';
|
||||
import { execErrText } from '../../utils/errors';
|
||||
|
||||
const CLI_OPTS = {
|
||||
cwd: os.homedir(),
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
timeout: 15000,
|
||||
};
|
||||
|
||||
function readConfig(): OpenclawConfig | null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(openclawConfigPath(), 'utf-8')) as OpenclawConfig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findAgentIndex(config: OpenclawConfig | null, openclawAgentId: string): number {
|
||||
const list = config?.agents?.list ?? [];
|
||||
return list.findIndex((a) => a?.id === openclawAgentId);
|
||||
}
|
||||
|
||||
function extractModel(entry: OpenclawAgentEntry | undefined): string | null {
|
||||
if (!entry?.model) return null;
|
||||
if (typeof entry.model === 'string') return entry.model;
|
||||
if (entry.model && typeof entry.model === 'object') return entry.model.primary || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function listAvailableModels(config: OpenclawConfig | null): AgentModelOption[] {
|
||||
const models = config?.agents?.defaults?.models ?? {};
|
||||
return Object.entries(models).map(([key, val]) => ({
|
||||
key,
|
||||
alias: val && typeof val === 'object' && typeof val.alias === 'string' ? val.alias : null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getAgentModelConfig(openclawAgentId: string): AgentModelConfigResponse {
|
||||
const config = readConfig();
|
||||
const agentIndex = findAgentIndex(config, openclawAgentId);
|
||||
const list = config?.agents?.list ?? [];
|
||||
const entry = agentIndex >= 0 ? list[agentIndex] : undefined;
|
||||
|
||||
const override = extractModel(entry);
|
||||
const systemDefault = config?.agents?.defaults?.model?.primary ?? null;
|
||||
|
||||
return {
|
||||
agentId: openclawAgentId,
|
||||
known: agentIndex >= 0,
|
||||
override,
|
||||
systemDefault,
|
||||
effective: override ?? systemDefault,
|
||||
available: listAvailableModels(config),
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetAgentModelResult {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
config?: AgentModelConfigResponse;
|
||||
}
|
||||
|
||||
export function setAgentModel(
|
||||
openclawAgentId: string,
|
||||
modelKey: string | null
|
||||
): SetAgentModelResult {
|
||||
const config = readConfig();
|
||||
const agentIndex = findAgentIndex(config, openclawAgentId);
|
||||
if (agentIndex < 0) {
|
||||
return { ok: false, error: `Agent "${openclawAgentId}" not found in openclaw config.` };
|
||||
}
|
||||
|
||||
const path = `agents.list[${agentIndex}].model`;
|
||||
|
||||
try {
|
||||
if (modelKey === null || modelKey === '') {
|
||||
try {
|
||||
ocExec(['config', 'unset', path], CLI_OPTS);
|
||||
} catch (err) {
|
||||
const stderr = execErrText(err);
|
||||
if (!/not found|does not exist/i.test(stderr)) throw err;
|
||||
}
|
||||
} else {
|
||||
const available = listAvailableModels(config).map((m) => m.key);
|
||||
if (!available.includes(modelKey)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Model "${modelKey}" is not configured. Configure it via \`openclaw models\` first.`,
|
||||
};
|
||||
}
|
||||
ocExec(['config', 'set', path, JSON.stringify(modelKey), '--strict-json'], CLI_OPTS);
|
||||
}
|
||||
} catch (err) {
|
||||
const stderr = execErrText(err);
|
||||
console.error(`[agent-model] failed to set ${path}:`, stderr);
|
||||
return { ok: false, error: stderr || 'Failed to update agent model.' };
|
||||
}
|
||||
|
||||
return { ok: true, config: getAgentModelConfig(openclawAgentId) };
|
||||
}
|
||||
@@ -2,13 +2,11 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import {
|
||||
AgentModelOption,
|
||||
AgentSubagentsConfig,
|
||||
AgentSubagentsPatch,
|
||||
AgentSubagentsResponse,
|
||||
OpenclawAgentEntry,
|
||||
OpenclawConfig,
|
||||
OpenclawSubagentsSection,
|
||||
} from '../../@types/openclaw';
|
||||
import { ocExec } from '../openclawGateway';
|
||||
import { openclawConfigPath } from './paths';
|
||||
@@ -35,19 +33,10 @@ function findAgentIndex(config: OpenclawConfig | null, openclawAgentId: string):
|
||||
return list.findIndex((a) => a?.id === openclawAgentId);
|
||||
}
|
||||
|
||||
function extractModel(section: OpenclawSubagentsSection | undefined): string | null {
|
||||
const m = section?.model;
|
||||
if (!m) return null;
|
||||
if (typeof m === 'string') return m;
|
||||
if (typeof m === 'object') return m.primary ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeConfig(entry: OpenclawAgentEntry | undefined): AgentSubagentsConfig {
|
||||
const s = entry?.subagents;
|
||||
return {
|
||||
allowAgents: Array.isArray(s?.allowAgents) ? s!.allowAgents.map(String) : null,
|
||||
model: extractModel(s),
|
||||
thinking: typeof s?.thinking === 'string' ? s!.thinking : null,
|
||||
requireAgentId: typeof s?.requireAgentId === 'boolean' ? s!.requireAgentId : null,
|
||||
};
|
||||
@@ -63,14 +52,6 @@ function availableAgents(config: OpenclawConfig | null, selfId: string) {
|
||||
}));
|
||||
}
|
||||
|
||||
function availableModels(config: OpenclawConfig | null): AgentModelOption[] {
|
||||
const models = config?.agents?.defaults?.models ?? {};
|
||||
return Object.entries(models).map(([key, val]) => ({
|
||||
key,
|
||||
alias: val && typeof val === 'object' && typeof val.alias === 'string' ? val.alias : null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getAgentSubagentsConfig(openclawAgentId: string): AgentSubagentsResponse {
|
||||
const config = readConfig();
|
||||
const agentIndex = findAgentIndex(config, openclawAgentId);
|
||||
@@ -81,7 +62,6 @@ export function getAgentSubagentsConfig(openclawAgentId: string): AgentSubagents
|
||||
known: agentIndex >= 0,
|
||||
config: normalizeConfig(entry),
|
||||
availableAgents: availableAgents(config, openclawAgentId),
|
||||
availableModels: availableModels(config),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,11 +83,6 @@ const HANDLERS: Record<keyof AgentSubagentsPatch, PatchHandler> = {
|
||||
const normalized = Array.from(new Set(v.map((x) => String(x).trim()).filter(Boolean)));
|
||||
return { op: 'set', value: JSON.stringify(normalized) };
|
||||
},
|
||||
model: (_b, v) => {
|
||||
if (v === null || v === '') return { op: 'unset' };
|
||||
if (typeof v !== 'string') return { op: 'error', error: '"model" must be a string or null.' };
|
||||
return { op: 'set', value: JSON.stringify(v) };
|
||||
},
|
||||
thinking: (_b, v) => {
|
||||
if (v === null || v === '' || v === 'inherit') return { op: 'unset' };
|
||||
if (typeof v !== 'string' || !ALLOWED_THINKING.has(v)) {
|
||||
@@ -136,12 +111,7 @@ export function setAgentSubagents(
|
||||
return { ok: false, error: `Agent "${openclawAgentId}" not found in openclaw config.` };
|
||||
}
|
||||
|
||||
const keys: (keyof AgentSubagentsPatch)[] = [
|
||||
'allowAgents',
|
||||
'model',
|
||||
'thinking',
|
||||
'requireAgentId',
|
||||
];
|
||||
const keys: (keyof AgentSubagentsPatch)[] = ['allowAgents', 'thinking', 'requireAgentId'];
|
||||
|
||||
const pending = keys.filter((k) => k in patch);
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export {
|
||||
} from './workspace';
|
||||
export { getAgentModel, getAgentModelsForOpenclawIds } from './config';
|
||||
export { getAgentBudget, setAgentBudget, BUDGET_FIELDS } from './budget';
|
||||
export { getAgentModelConfig, setAgentModel } from './agentModel';
|
||||
export { getAgentSkillsConfig, setAgentSkills } from './agentSkills';
|
||||
export { getAgentSubagentsConfig, setAgentSubagents } from './agentSubagents';
|
||||
export { listPlugins, togglePlugin } from './plugins';
|
||||
|
||||
@@ -78,26 +78,6 @@ export interface AgentBudgetMutationResponse {
|
||||
budget?: AgentBudgetResponse;
|
||||
}
|
||||
|
||||
export interface AgentModelOption {
|
||||
key: string;
|
||||
alias: string | null;
|
||||
}
|
||||
|
||||
export interface AgentModelConfigResponse {
|
||||
agentId: string;
|
||||
known: boolean;
|
||||
override: string | null;
|
||||
systemDefault: string | null;
|
||||
effective: string | null;
|
||||
available: AgentModelOption[];
|
||||
}
|
||||
|
||||
export interface AgentModelConfigMutationResponse {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
config?: AgentModelConfigResponse;
|
||||
}
|
||||
|
||||
export interface AgentSkillSummary {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -123,7 +103,6 @@ export interface AgentSkillsMutationResponse {
|
||||
|
||||
export interface AgentSubagentsConfig {
|
||||
allowAgents: string[] | null;
|
||||
model: string | null;
|
||||
thinking: string | null;
|
||||
requireAgentId: boolean | null;
|
||||
}
|
||||
@@ -133,12 +112,10 @@ export interface AgentSubagentsResponse {
|
||||
known: boolean;
|
||||
config: AgentSubagentsConfig;
|
||||
availableAgents: { id: string; name: string | null }[];
|
||||
availableModels: AgentModelOption[];
|
||||
}
|
||||
|
||||
export interface AgentSubagentsPatch {
|
||||
allowAgents?: string[] | null;
|
||||
model?: string | null;
|
||||
thinking?: string | null;
|
||||
requireAgentId?: boolean | null;
|
||||
}
|
||||
@@ -266,24 +243,6 @@ export const agentsApi = baseApi.injectEndpoints({
|
||||
}),
|
||||
invalidatesTags: (_res, _err, { agentId }) => [{ type: 'AgentBudget', id: agentId }],
|
||||
}),
|
||||
getAgentModelConfig: build.query<AgentModelConfigResponse, string>({
|
||||
query: (agentId) => `/agent/${agentId}/model-config`,
|
||||
providesTags: (_res, _err, agentId) => [{ type: 'AgentModelConfig', id: agentId }],
|
||||
}),
|
||||
updateAgentModelConfig: build.mutation<
|
||||
AgentModelConfigMutationResponse,
|
||||
{ agentId: string; model: string | null }
|
||||
>({
|
||||
query: ({ agentId, model }) => ({
|
||||
url: `/agent/${agentId}/model-config`,
|
||||
method: 'PATCH',
|
||||
body: { model },
|
||||
}),
|
||||
invalidatesTags: (_res, _err, { agentId }) => [
|
||||
{ type: 'AgentModelConfig', id: agentId },
|
||||
'Agent',
|
||||
],
|
||||
}),
|
||||
getAgentSkills: build.query<AgentSkillsResponse, string>({
|
||||
query: (agentId) => `/agent/${agentId}/skills`,
|
||||
providesTags: (_res, _err, agentId) => [{ type: 'AgentSkills', id: agentId }],
|
||||
@@ -331,8 +290,6 @@ export const {
|
||||
usePatchSessionSettingsMutation,
|
||||
useGetAgentBudgetQuery,
|
||||
useUpdateAgentBudgetMutation,
|
||||
useGetAgentModelConfigQuery,
|
||||
useUpdateAgentModelConfigMutation,
|
||||
useGetAgentSkillsQuery,
|
||||
useUpdateAgentSkillsMutation,
|
||||
useGetAgentSubagentsQuery,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as AgentModelConfig } from './ui/AgentModelConfig';
|
||||
export { default as AgentModelPicker } from './ui/AgentModelPicker';
|
||||
@@ -1,230 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
useGetAgentModelConfigQuery,
|
||||
useUpdateAgentModelConfigMutation,
|
||||
} from '../../../../entities/agent';
|
||||
|
||||
interface AgentModelConfigProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
const INHERIT_VALUE = '__inherit__';
|
||||
|
||||
export default function AgentModelConfig({ agentId }: AgentModelConfigProps) {
|
||||
const { data, isLoading, isError, refetch } = useGetAgentModelConfigQuery(agentId, {
|
||||
skip: !agentId,
|
||||
});
|
||||
const [update, { isLoading: saving, error: saveError }] = useUpdateAgentModelConfigMutation();
|
||||
|
||||
const [selection, setSelection] = useState<string>(INHERIT_VALUE);
|
||||
const [prevData, setPrevData] = useState(data);
|
||||
if (data !== prevData) {
|
||||
setPrevData(data);
|
||||
if (data) setSelection(data.override ?? INHERIT_VALUE);
|
||||
}
|
||||
|
||||
const dirty = data ? selection !== (data.override ?? INHERIT_VALUE) : false;
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await update({
|
||||
agentId,
|
||||
model: selection === INHERIT_VALUE ? null : selection,
|
||||
}).unwrap();
|
||||
} catch (err) {
|
||||
console.error('Save agent model failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (!data) return;
|
||||
setSelection(data.override ?? INHERIT_VALUE);
|
||||
}
|
||||
|
||||
if (isLoading && !data) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
|
||||
<CircularProgress size={20} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
action={
|
||||
<Button size="small" onClick={() => refetch()}>
|
||||
Retry
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Could not load agent model configuration.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const rpcError =
|
||||
saveError && typeof saveError === 'object' && 'data' in saveError
|
||||
? ((saveError as { data?: { error?: string } }).data?.error ?? 'Failed to save model.')
|
||||
: null;
|
||||
|
||||
const systemDefaultLabel = data.systemDefault ?? 'unset';
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', minWidth: 0 }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={1}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" fontWeight={700}>
|
||||
Model
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Override the primary model for this agent. Leave as "Inherit default" to use the system
|
||||
default.
|
||||
</Typography>
|
||||
</Box>
|
||||
{!data.known && (
|
||||
<Chip
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
label="Agent missing from openclaw config"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{rpcError && (
|
||||
<Alert severity="error" sx={{ mb: 1 }}>
|
||||
{rpcError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
p: 1.5,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.25}>
|
||||
<FormControl fullWidth size="small" disabled={!data.known}>
|
||||
<InputLabel id="agent-model-select-label">Primary model</InputLabel>
|
||||
<Select
|
||||
labelId="agent-model-select-label"
|
||||
label="Primary model"
|
||||
value={selection}
|
||||
onChange={(e) => setSelection(String(e.target.value))}
|
||||
>
|
||||
<MenuItem value={INHERIT_VALUE}>
|
||||
<Box component="span">
|
||||
Inherit default{' '}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.72rem',
|
||||
}}
|
||||
>
|
||||
({systemDefaultLabel})
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
{data.available.length === 0 && (
|
||||
<MenuItem value="" disabled>
|
||||
No models configured — add via <code>openclaw models</code>
|
||||
</MenuItem>
|
||||
)}
|
||||
{data.available.map((m) => (
|
||||
<MenuItem key={m.key} value={m.key}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{m.key}
|
||||
</Box>
|
||||
{m.alias && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontSize: '0.72rem',
|
||||
}}
|
||||
>
|
||||
alias {m.alias}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ color: 'text.primary', fontWeight: 600 }}>
|
||||
effective {data.effective ?? '—'}
|
||||
</Box>
|
||||
<Box component="span" sx={{ opacity: 0.55, mx: 0.75 }}>
|
||||
·
|
||||
</Box>
|
||||
system default {systemDefaultLabel}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={1} sx={{ mt: 1.5 }}>
|
||||
<Button size="small" onClick={handleReset} disabled={!dirty || saving}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || saving || !data.known}
|
||||
>
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ExpandMore, Check } from '@mui/icons-material';
|
||||
import {
|
||||
useGetAgentModelConfigQuery,
|
||||
useUpdateAgentModelConfigMutation,
|
||||
} from '../../../../entities/agent';
|
||||
|
||||
interface AgentModelPickerProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
function shortenModel(key: string | null): string {
|
||||
if (!key) return 'no model';
|
||||
const slash = key.indexOf('/');
|
||||
return slash >= 0 ? key.slice(slash + 1) : key;
|
||||
}
|
||||
|
||||
export default function AgentModelPicker({ agentId }: AgentModelPickerProps) {
|
||||
const { data, isLoading } = useGetAgentModelConfigQuery(agentId, { skip: !agentId });
|
||||
const [update, { isLoading: saving }] = useUpdateAgentModelConfigMutation();
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||
|
||||
if (isLoading && !data) {
|
||||
return <CircularProgress size={12} sx={{ ml: 0.25 }} />;
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
const effective = data.effective;
|
||||
const override = data.override;
|
||||
const label = shortenModel(effective);
|
||||
|
||||
async function handlePick(model: string | null) {
|
||||
setAnchor(null);
|
||||
if (!data?.known) return;
|
||||
if ((override ?? null) === (model ?? null)) return;
|
||||
try {
|
||||
await update({ agentId, model }).unwrap();
|
||||
} catch (err) {
|
||||
console.error('Agent model update failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const canEdit = data.known && !saving;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
data.known
|
||||
? effective
|
||||
? `Model: ${effective}${override ? '' : ' (inherited default)'}`
|
||||
: 'Pick a model'
|
||||
: 'Agent not in openclaw config'
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ display: 'inline-flex', alignItems: 'center', minWidth: 0, maxWidth: '100%' }}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={(e) => canEdit && setAnchor(e.currentTarget)}
|
||||
disabled={!canEdit}
|
||||
focusRipple
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.35,
|
||||
px: 0.6,
|
||||
py: 0.15,
|
||||
ml: -0.6,
|
||||
borderRadius: 0.75,
|
||||
maxWidth: '100%',
|
||||
color: 'text.secondary',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.7rem',
|
||||
lineHeight: 1.3,
|
||||
opacity: data.known ? 1 : 0.6,
|
||||
cursor: canEdit ? 'pointer' : 'default',
|
||||
'&:hover': canEdit ? { bgcolor: 'action.hover', color: 'text.primary' } : undefined,
|
||||
'&:focus-visible': {
|
||||
outline: '2px solid',
|
||||
outlineColor: 'primary.main',
|
||||
outlineOffset: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
{!override && effective && (
|
||||
<Box component="span" sx={{ opacity: 0.55, fontSize: '0.65rem' }}>
|
||||
(default)
|
||||
</Box>
|
||||
)}
|
||||
{saving ? (
|
||||
<CircularProgress size={10} sx={{ ml: 0.25 }} />
|
||||
) : (
|
||||
<ExpandMore sx={{ fontSize: 14, opacity: canEdit ? 0.7 : 0.3 }} />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={Boolean(anchor)}
|
||||
onClose={() => setAnchor(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
slotProps={{ paper: { sx: { minWidth: 260, maxWidth: 360 } } }}
|
||||
>
|
||||
<MenuItem onClick={() => handlePick(null)}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flex: 1 }}>
|
||||
<Box sx={{ width: 16, flexShrink: 0 }}>
|
||||
{override === null && <Check sx={{ fontSize: 16 }} />}
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="body2">Inherit default</Typography>
|
||||
{data.systemDefault && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.68rem',
|
||||
}}
|
||||
>
|
||||
{data.systemDefault}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
{data.available.length === 0 && (
|
||||
<MenuItem disabled>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
No models configured. Run <code>openclaw models</code>.
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
{data.available.map((m) => {
|
||||
const isActive = override === m.key;
|
||||
return (
|
||||
<MenuItem key={m.key} selected={isActive} onClick={() => handlePick(m.key)}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flex: 1 }}>
|
||||
<Box sx={{ width: 16, flexShrink: 0 }}>
|
||||
{isActive && <Check sx={{ fontSize: 16 }} />}
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.82rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{m.key}
|
||||
</Typography>
|
||||
{m.alias && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: 'text.secondary', fontSize: '0.68rem' }}
|
||||
>
|
||||
alias {m.alias}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,10 +26,6 @@ import {
|
||||
type AgentSubagentsPatch,
|
||||
} from '../../../../entities/agent';
|
||||
|
||||
interface AgentSubagentsProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
const THINKING_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'inherit', label: 'Inherit' },
|
||||
{ value: 'minimal', label: 'Minimal' },
|
||||
@@ -38,7 +34,9 @@ const THINKING_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'high', label: 'High' },
|
||||
];
|
||||
|
||||
const INHERIT_MODEL = '__inherit__';
|
||||
interface AgentSubagentsProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
function sameSet(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
@@ -55,7 +53,6 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
|
||||
const [restrict, setRestrict] = useState(false);
|
||||
const [allowed, setAllowed] = useState<Set<string>>(new Set());
|
||||
const [model, setModel] = useState<string>(INHERIT_MODEL);
|
||||
const [thinking, setThinking] = useState<string>('inherit');
|
||||
const [requireAgentId, setRequireAgentId] = useState<boolean | null>(null);
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -71,7 +68,6 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
setRestrict(true);
|
||||
setAllowed(new Set(data.config.allowAgents));
|
||||
}
|
||||
setModel(data.config.model ?? INHERIT_MODEL);
|
||||
setThinking(data.config.thinking ?? 'inherit');
|
||||
setRequireAgentId(data.config.requireAgentId);
|
||||
}
|
||||
@@ -98,10 +94,6 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
: nextAllow === null || !sameSet(currentAllow, nextAllow);
|
||||
if (changedAllow) result.allowAgents = nextAllow;
|
||||
|
||||
const currentModel = data.config.model;
|
||||
const nextModel = model === INHERIT_MODEL ? null : model;
|
||||
if ((currentModel ?? null) !== (nextModel ?? null)) result.model = nextModel;
|
||||
|
||||
const currentThinking = data.config.thinking;
|
||||
const nextThinking = thinking === 'inherit' ? null : thinking;
|
||||
if ((currentThinking ?? null) !== (nextThinking ?? null)) result.thinking = nextThinking;
|
||||
@@ -111,7 +103,7 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
result.requireAgentId = requireAgentId;
|
||||
|
||||
return result;
|
||||
}, [data, restrict, allowed, model, thinking, requireAgentId]);
|
||||
}, [data, restrict, allowed, thinking, requireAgentId]);
|
||||
|
||||
const dirty = Object.keys(patch).length > 0;
|
||||
|
||||
@@ -149,7 +141,6 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
setRestrict(true);
|
||||
setAllowed(new Set(data.config.allowAgents));
|
||||
}
|
||||
setModel(data.config.model ?? INHERIT_MODEL);
|
||||
setThinking(data.config.thinking ?? 'inherit');
|
||||
setRequireAgentId(data.config.requireAgentId);
|
||||
}
|
||||
@@ -368,44 +359,6 @@ export default function AgentSubagents({ agentId }: AgentSubagentsProps) {
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.25} sx={{ mb: 0.5 }}>
|
||||
<FormControl fullWidth size="small" disabled={!data.known || saving}>
|
||||
<InputLabel id="subagent-model-label">Default model for subagents</InputLabel>
|
||||
<Select
|
||||
labelId="subagent-model-label"
|
||||
label="Default model for subagents"
|
||||
value={model}
|
||||
onChange={(e) => setModel(String(e.target.value))}
|
||||
>
|
||||
<MenuItem value={INHERIT_MODEL}>Inherit</MenuItem>
|
||||
{data.availableModels.length === 0 && (
|
||||
<MenuItem value="" disabled>
|
||||
No models configured
|
||||
</MenuItem>
|
||||
)}
|
||||
{data.availableModels.map((m) => (
|
||||
<MenuItem key={m.key} value={m.key}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{m.key}
|
||||
</Box>
|
||||
{m.alias && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: 'text.secondary', fontSize: '0.72rem', ml: 1 }}
|
||||
>
|
||||
alias {m.alias}
|
||||
</Box>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth size="small" disabled={!data.known || saving}>
|
||||
<InputLabel id="subagent-thinking-label">Thinking level</InputLabel>
|
||||
<Select
|
||||
|
||||
@@ -73,13 +73,11 @@ export const baseApi = createApi({
|
||||
'Workspace',
|
||||
'WorkspaceFile',
|
||||
'SessionSettings',
|
||||
'AgentModel',
|
||||
'Channel',
|
||||
'Cron',
|
||||
'Plugin',
|
||||
'Skill',
|
||||
'AgentBudget',
|
||||
'AgentModelConfig',
|
||||
'AgentSkills',
|
||||
'AgentSubagents',
|
||||
],
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Box, TextField, IconButton, Typography, CircularProgress } from '@mui/m
|
||||
import { Edit, Check, Settings, TuneOutlined } from '@mui/icons-material';
|
||||
import { Link } from 'react-router';
|
||||
import { useGetAgentQuery, useUpdateAgentMutation } from '../../../entities/agent';
|
||||
import { AgentModelPicker } from '../../../features/agent/model-config';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
agentId: string;
|
||||
@@ -99,30 +98,19 @@ export default function ChatHeader({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight={600}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0.1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight={600}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{agent.name}
|
||||
</Typography>
|
||||
<AgentModelPicker agentId={agentId} />
|
||||
</Box>
|
||||
{agent.name}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onToggleSessionSettings}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-client",
|
||||
"version": "2.4.4",
|
||||
"version": "2.4.5",
|
||||
"description": "Web-based chat interface for OpenClaw AI agents",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Reference in New Issue
Block a user