Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/prds/conversations/auto-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Auto planner/worker model routing(aionrs)

> Issue: [#4143](https://github.com/iOfficeAI/AionUi/issues/4143)
> 范围:aionrs 会话模型选择、模型设置页槽位绑定、会话 `extra.auto_model`
> 非目标:ACP 平台、助手 `mode: auto`(「记住上次」)、把 Auto 设成默认模型

---

## 背景与目标

长时间运行(如 7×24)的 aionrs agent 若全程使用高价模型,会在低价值回合上浪费成本。业界常见做法是 **Planner / Worker** 分槽:规划用强模型,执行磨信用便宜模型。

本 PRD 分两阶段:

| 阶段 | 能力 | 用户可感知收益 |
| -------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| **Phase 1(本 PR)** | UI + 配置 + sticky Auto;选 Auto 时用 **worker** 槽解析出具体 `conversation.model`,并写 `extra.auto_model` | 可配置槽位;会话标记为 Auto;为 Phase 2 打基础 |
| **Phase 2** | Core 支持不重建 agent 切换模型;按 phase 在 planner/worker/utility 间切换 | 真正降低长跑成本 |

**名词**:

- **Auto**:产品级「自动路由」入口,与助手权限模式 `mode: auto`(Remember last used)无关。
- **槽位**:`planner` / `worker` / `utility`,可固定绑定某模型,或设为 Automatic(按偏好打分)。
- **Sentinel**:选择器内部 id `__aionui_auto__::auto`,**永不**作为后端 model 上报。

---

## (F-AUTO-01) 选择器出现 Auto 入口 [新增]

**用户故事**:作为 aionrs 用户,我希望在模型列表顶部看到 Auto,而不是只能手选单个模型。

**正常流程**:

1. 打开 aionrs 对话或 Guid 新建页的模型选择器。
2. 列表顶部有 **Auto** 分组 / 选项。
3. 选中后 pill 显示 `Auto · {具体模型名}`。

**验收**:

- [ ] 桌面下拉与移动 action sheet 均有 Auto。
- [ ] 选中后 `conversation.model` 为具体 provider+model,不是 sentinel。
- [ ] `extra.auto_model.enabled === true`,且含 `last_resolved`。

---

## (F-AUTO-02) 设置页绑定槽位与偏好 [新增]

**用户故事**:作为用户,我希望在模型设置里为 planner/worker/utility 指定模型或 Automatic,并选择成本/质量偏好。

**验收**:

- [ ] 设置写入 `configService` 键 `autoModel`。
- [ ] Automatic 时按 preference 对可用模型打分选槽;固定绑定时校验模型仍存在,否则回落 Automatic。
- [ ] 文案说明 Phase 1 实际使用 worker 槽。

---

## (F-AUTO-03) 手动选模型退出 Auto [不变语义]

**验收**:

- [ ] 用户再选具体模型后 `auto_model.enabled` 为 false;pill 不再显示 Auto 前缀。

---

## (F-AUTO-04) 无可用模型时失败提示 [新增]

**验收**:

- [ ] resolve 失败时 Message 提示,不写入无效 model。

---

## Phase 2(后续,不在本 PR)

- Core:同一 agent 实例上切换 model,不 stop/rebuild。
- 按任务 phase 在 planner ↔ worker(及 utility)间切换,并更新 `extra.auto_model.phase` / `last_resolved`。
- 可选:turn 级启发式路由(更接近 Cursor / OpenSquilla)。

---

## 实现备注

- 解析逻辑:`packages/desktop/src/renderer/utils/autoModel/`
- 会话标记:`conversation.extra.auto_model`
- 勿把 Auto 做成全局默认模型;用户显式选择才启用。
14 changes: 14 additions & 0 deletions packages/desktop/src/common/adapter/ipcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,20 @@ export interface ICreateConversationParams {
remote_agent_id?: string;
extra_skill_paths?: string[];
team_id?: string;
/**
* Sticky Auto routing flag (issue #4143).
* Concrete model is always on `model`; this only marks Auto selection.
*/
auto_model?: {
enabled: boolean;
preference?: 'cost' | 'balance' | 'quality';
phase?: 'planner' | 'worker' | 'utility';
last_resolved?: {
provider_id: string;
model: string;
slot: 'planner' | 'worker' | 'utility';
};
};
};
}

Expand Down
14 changes: 14 additions & 0 deletions packages/desktop/src/common/config/configKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ export type ConfigKeyMap = {
'pet.size': number | undefined;
'pet.dnd': boolean | undefined;
'pet.confirmEnabled': boolean | undefined;
/**
* Auto planner/worker routing preferences for long-running aionrs agents.
* See docs/prds/conversations/auto-model.md and issue #4143.
*/
autoModel:
| {
preference: 'cost' | 'balance' | 'quality';
slots: {
planner: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
worker: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
utility: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
};
}
| undefined;
// Removed: 'system.autoPreviewOfficeFiles'. It gated "auto-open a preview tab
// when an Office file appears in the workspace", a behaviour that was dropped
// along with its hook — leaving the toggle would have been a switch the user
Expand Down
27 changes: 27 additions & 0 deletions packages/desktop/src/common/config/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ export interface IConfigStorageRefer {
// Desktop Pet: whether tool-call confirmations are routed to the pet's bubble
// (true) or remain in the main chat window (false). Default true.
'pet.confirmEnabled'?: boolean;
/**
* Auto planner/worker routing preferences (long-running aionrs agents).
* Mirrored in ConfigKeyMap.autoModel for configService.
*/
autoModel?: {
preference: 'cost' | 'balance' | 'quality';
slots: {
planner: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
worker: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
utility: { mode: 'automatic' } | { mode: 'fixed'; provider_id: string; model: string };
};
};
}

/**
Expand Down Expand Up @@ -478,6 +490,21 @@ export type TChatConversation =
last_token_usage?: TokenUsageData;
/** Cron job ID that spawned this conversation */
cron_job_id?: string;
/**
* Sticky Auto routing flag for this conversation (issue #4143).
* Concrete model always lives on conversation.model; this only marks Auto.
* Phase 2 will use `phase` for planner/worker switches without rebuild.
*/
auto_model?: {
enabled: boolean;
preference?: 'cost' | 'balance' | 'quality';
phase?: 'planner' | 'worker' | 'utility';
last_resolved?: {
provider_id: string;
model: string;
slot: 'planner' | 'worker' | 'utility';
};
};
}
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@
import { ipcBridge } from '@/common';
import type { IProvider } from '@/common/config/storage';
import { supportsOpenAiApiMode } from '@/common/utils/modelCapabilities';
import { Button, Divider, Message, Popconfirm, Collapse, Tag, Switch, Tooltip } from '@arco-design/web-react';
import {
Button,
Divider,
Message,
Popconfirm,
Collapse,
Tag,
Switch,
Tooltip,
Select,
Typography,
} from '@arco-design/web-react';
import {
DeleteFour,
Heartbeat,
Expand All @@ -19,7 +30,7 @@ import {
SettingTwo,
Write,
} from '@icon-park/react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { formatDateTime } from '@/renderer/services/i18n/format';
import AddModelModal from '@/renderer/pages/settings/components/AddModelModal';
Expand All @@ -28,7 +39,9 @@ import { isNewApiPlatform, NEW_API_PROTOCOL_OPTIONS } from '@/renderer/utils/mod
import EditModeModal from '@/renderer/pages/settings/components/EditModeModal';
import AionScrollArea from '@/renderer/components/base/AionScrollArea';
import TalkToButlerButton from '@/renderer/components/base/TalkToButlerButton';
import { useProvidersQuery } from '@/renderer/hooks/agent/useModelProviderList';
import { useModelProviderList, useProvidersQuery } from '@/renderer/hooks/agent/useModelProviderList';
import { listSlotModelOptions, useAutoModelSettings } from '@/renderer/hooks/agent/useAutoModelSettings';
import type { AutoModelSlotBinding, AutoModelSlots } from '@/renderer/utils/autoModel';
import { useSettingsViewMode } from '../settingsViewContext';
import SettingsPageHeader from '@/renderer/pages/settings/components/SettingsPageHeader';
import { consumePendingDeepLink } from '@/renderer/hooks/system/useDeepLink';
Expand Down Expand Up @@ -106,6 +119,73 @@ const isModelEnabled = (platform: IProvider, model: string): boolean => {
return platform.model_enabled[model] !== false;
};

const AUTO_SLOT_KEYS: Array<keyof AutoModelSlots> = ['planner', 'worker', 'utility'];

const bindingToSelectValue = (binding: AutoModelSlotBinding): string => {
if (binding.mode === 'automatic') return 'automatic';
return `${binding.provider_id}::${binding.model}`;
};

const parseSelectValue = (value: string): AutoModelSlotBinding => {
if (value === 'automatic') return { mode: 'automatic' };
const [provider_id, ...rest] = value.split('::');
return { mode: 'fixed', provider_id, model: rest.join('::') };
};

/** Phase-1 Auto planner/worker slot bindings (issue #4143). */
const AutoModelSettingsBlock: React.FC = () => {
const { t } = useTranslation();
const { settings, setPreference, setSlot } = useAutoModelSettings();
const { providers, getAvailableModels } = useModelProviderList();
const modelOptions = useMemo(
() => listSlotModelOptions(providers, getAvailableModels),
[getAvailableModels, providers]
);

return (
<div className='mb-16px rounded-12px border border-solid border-[var(--color-border-2)] bg-fill-1 px-14px py-12px'>
<Typography.Title heading={6} className='!mt-0 !mb-4px'>
{t('settings.autoModel.title')}
</Typography.Title>
<Typography.Paragraph className='!mb-12px text-12px text-t-secondary'>
{t('settings.autoModel.description')}
</Typography.Paragraph>
<div className='mb-12px'>
<div className='mb-6px text-12px text-t-secondary'>{t('settings.autoModel.preference')}</div>
<Select
value={settings.preference}
onChange={(value) => void setPreference(value)}
options={[
{ label: t('settings.autoModel.preferenceCost'), value: 'cost' },
{ label: t('settings.autoModel.preferenceBalance'), value: 'balance' },
{ label: t('settings.autoModel.preferenceQuality'), value: 'quality' },
]}
/>
</div>
{AUTO_SLOT_KEYS.map((slot) => (
<div key={slot} className='mb-10px last:mb-0'>
<div className='mb-6px text-12px text-t-secondary'>{t(`settings.autoModel.slot.${slot}`)}</div>
<Select
value={bindingToSelectValue(settings.slots[slot])}
onChange={(value) => void setSlot(slot, parseSelectValue(String(value)))}
showSearch
options={[
{ label: t('settings.autoModel.automatic'), value: 'automatic' },
...modelOptions.map((option) => ({
label: `${option.provider.name} / ${option.modelName}`,
value: option.value,
})),
]}
/>
</div>
))}
<Typography.Paragraph className='!mb-0 !mt-10px text-12px text-t-secondary'>
{t('settings.autoModel.phaseNote')}
</Typography.Paragraph>
</div>
);
};

const ModelModalContent: React.FC = () => {
const { t, i18n } = useTranslation();
const viewMode = useSettingsViewMode();
Expand Down Expand Up @@ -402,26 +482,27 @@ const ModelModalContent: React.FC = () => {

{/* Content Area */}
<AionScrollArea className='flex-1 min-h-0' disableOverflow={isPageMode}>
{!data || data.length === 0 ? (
<div className='flex flex-col items-center justify-center py-40px'>
<Info theme='outline' size='48' className='text-t-secondary mb-16px' />
<h3 className='text-16px font-500 text-t-primary mb-8px'>{t('settings.noConfiguredModels')}</h3>
<p className='text-14px text-t-secondary text-center max-w-400px'>
{t('settings.needHelpConfigGuide')}
<a
href='https://github.com/iOfficeAI/AionUi/wiki/LLM-Configuration'
target='_blank'
rel='noopener noreferrer'
className='text-[rgb(var(--primary-6))] hover:text-[rgb(var(--primary-5))] underline ms-4px'
>
{t('settings.configGuide')}
</a>
{t('settings.configGuideSuffix')}
</p>
</div>
) : (
<div className='space-y-16px'>
{(data || []).map((platform: IProvider) => {
<div className='space-y-16px'>
<AutoModelSettingsBlock />
{!data || data.length === 0 ? (
<div className='flex flex-col items-center justify-center py-40px'>
<Info theme='outline' size='48' className='text-t-secondary mb-16px' />
<h3 className='text-16px font-500 text-t-primary mb-8px'>{t('settings.noConfiguredModels')}</h3>
<p className='text-14px text-t-secondary text-center max-w-400px'>
{t('settings.needHelpConfigGuide')}
<a
href='https://github.com/iOfficeAI/AionUi/wiki/LLM-Configuration'
target='_blank'
rel='noopener noreferrer'
className='text-[rgb(var(--primary-6))] hover:text-[rgb(var(--primary-5))] underline ms-4px'
>
{t('settings.configGuide')}
</a>
{t('settings.configGuideSuffix')}
</p>
</div>
) : (
(data || []).map((platform: IProvider) => {
const key = platform.id;
const isExpanded = collapseKey[platform.id] ?? false;
return (
Expand Down Expand Up @@ -682,9 +763,9 @@ const ModelModalContent: React.FC = () => {
</Collapse.Item>
</Collapse>
);
})}
</div>
)}
})
)}
</div>
</AionScrollArea>
</div>
);
Expand Down
Loading
Loading