fix: support max reasoning and custom model selection for 1.0.0

This commit is contained in:
chenmingxuan committed 2026-09-23 00:24:35 +08:00
1 parent e1185a5104
commit 2ec563c079
12 files changed
+119 -16

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ cd Artemis-Eapil
3. 输入任务目标,选择 Flash 或 Pro 后执行;在工作区查看步骤、截图和结果。 3. 输入任务目标,选择 Flash 或 Pro 后执行;在工作区查看步骤、截图和结果。
4. 通过“报告与批量”导出报告或导入 Excel 用例;任务队列支持历史搜索。 4. 通过“报告与批量”导出报告或导入 Excel 用例;任务队列支持历史搜索。
预设模型为 `gpt-5.6-luna`、`mimo-v2.6-flash`,也可填写自定义名称。默认模型是 `mimo-v2.6-flash`,思考强度 `high`,单次请求超时 **300 秒**,失败后最多重试 **5 次**,间隔 **10 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。 预设模型为 `gpt-5.6-luna`、`mimo-v2.6-flash`,也可填写自定义名称。默认模型是 `mimo-v2.6-flash`,思考强度默认 `high`(最高可选 `max`),单次请求超时 **300 秒**,失败后最多重试 **5 次**,间隔 **10 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。
也可在项目 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL` 和 `OPENAI_MODEL`,其他参数见 [.env.example](.env.example)。直接编辑环境文件后需重启相应服务。不要提交自己的密钥。 也可在项目 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL` 和 `OPENAI_MODEL`,其他参数见 [.env.example](.env.example)。直接编辑环境文件后需重启相应服务。不要提交自己的密钥。
@@ -22,9 +22,9 @@ class ModelSettingsUpdate(BaseModel):
timeout_seconds: float = Field(default=300, gt=0, le=3600) timeout_seconds: float = Field(default=300, gt=0, le=3600)
max_retries: int = Field(default=5, ge=0, le=10) max_retries: int = Field(default=5, ge=0, le=10)
retry_interval_seconds: float = Field(default=10, ge=0, le=300) retry_interval_seconds: float = Field(default=10, ge=0, le=300)
reasoning_effort: Literal["default", "none", "minimal", "low", "medium", "high", "xhigh"] = ( reasoning_effort: Literal[
"high" "default", "none", "minimal", "low", "medium", "high", "xhigh", "max"
) ] = "high"
@field_validator("base_url") @field_validator("base_url")
@classmethod @classmethod
@@ -36,6 +36,47 @@ describe('持久化模型与功能设置', () => {
expect(component.message()).toContain('保存'); expect(component.message()).toContain('保存');
}); });
it('预设按钮填入名称,自定义模型与 max 可以保存并重新加载', async () => {
const fixture = TestBed.createComponent(ModelSettingsComponent);
fixture.detectChanges();
http.expectOne('/api/system/model-settings').flush(config);
fixture.detectChanges();
await fixture.whenStable();
const root: HTMLElement = fixture.nativeElement;
const presets = root.querySelectorAll<HTMLButtonElement>('.model-presets button');
expect(presets.length).toBe(2);
presets[0].click();
fixture.detectChanges();
await fixture.whenStable();
const input = root.querySelector<HTMLInputElement>('#model-name')!;
expect(input.value).toBe('gpt-5.6-luna');
expect(presets[0].getAttribute('aria-pressed')).toBe('true');
expect(input.getAttribute('list')).toBeNull();
expect(input.autocomplete).toBe('off');
input.value = 'custom/model-v2';
input.dispatchEvent(new Event('input'));
const reasoning = root.querySelector<HTMLSelectElement>('#model-reasoning')!;
reasoning.value = 'max';
reasoning.dispatchEvent(new Event('change'));
fixture.detectChanges();
await fixture.whenStable();
expect(presets[0].getAttribute('aria-pressed')).toBe('false');
root.querySelector<HTMLFormElement>('form')!.dispatchEvent(new Event('submit', { cancelable: true }));
const request = http.expectOne('/api/system/model-settings');
expect(request.request.body.model).toBe('custom/model-v2');
expect(request.request.body.reasoning_effort).toBe('max');
const saved = { ...config, model: 'custom/model-v2', reasoning_effort: 'max' };
request.flush(saved);
fixture.destroy();
const reopened = TestBed.createComponent(ModelSettingsComponent);
reopened.detectChanges();
http.expectOne('/api/system/model-settings').flush(saved);
reopened.detectChanges();
await reopened.whenStable();
expect(reopened.nativeElement.querySelector('#model-name').value).toBe('custom/model-v2');
expect(reopened.nativeElement.querySelector('#model-reasoning').value).toBe('max');
});
it('连接测试使用当前输入而不写入保存接口', () => { it('连接测试使用当前输入而不写入保存接口', () => {
const fixture = TestBed.createComponent(ModelSettingsComponent); const fixture = TestBed.createComponent(ModelSettingsComponent);
fixture.detectChanges(); fixture.detectChanges();
@@ -27,14 +27,22 @@ interface ModelSettings {
placeholder="https://api.openai.com/v1" [disabled]="busy() || loading()" /> placeholder="https://api.openai.com/v1" [disabled]="busy() || loading()" />
<small>填写接口根地址,通常包含 /v1,无需添加 /responses。</small> <small>填写接口根地址,通常包含 /v1,无需添加 /responses。</small>
<label for="model-name">模型名称</label> <label for="model-name">模型名称</label>
<input id="model-name" name="model" list="model-presets" [(ngModel)]="model" required placeholder="输入接口支持的模型名" <div class="model-presets" role="group" aria-label="预设模型">
@for (preset of modelPresets; track preset) {
<button type="button" [attr.aria-pressed]="model === preset" (click)="model = preset"
[disabled]="busy() || loading()">{{ preset }}</button>
}
</div>
<input id="model-name" name="llmModelName" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
[(ngModel)]="model" required placeholder="输入接口支持的模型名" aria-describedby="model-name-help"
[disabled]="busy() || loading()" /> [disabled]="busy() || loading()" />
<datalist id="model-presets"><option value="gpt-5.6-luna"></option><option value="mimo-v2.6-flash"></option></datalist> <small id="model-name-help">点击预设填入,或直接输入自定义模型名称;保存后可继续使用。</small>
<label for="model-reasoning">思考强度</label> <label for="model-reasoning">思考强度</label>
<select id="model-reasoning" name="reasoning" [(ngModel)]="reasoning" [disabled]="busy() || loading()"> <select id="model-reasoning" name="reasoning" [(ngModel)]="reasoning" [disabled]="busy() || loading()">
<option value="default">由模型默认</option><option value="none">关闭(none)</option> <option value="default">由模型默认</option><option value="none">关闭(none)</option>
<option value="minimal">最低(minimal)</option><option value="low">低(low)</option> <option value="minimal">最低(minimal)</option><option value="low">低(low)</option>
<option value="medium">中(medium)</option><option value="high">高(high)</option><option value="xhigh">最高(xhigh)</option> <option value="medium">中(medium)</option><option value="high">高(high)</option>
<option value="xhigh">极高(xhigh)</option><option value="max">最高(max)</option>
</select> </select>
<small>接口需支持所选强度;不支持时会明确报错,可手动选择“由模型默认”。</small> <small>接口需支持所选强度;不支持时会明确报错,可手动选择“由模型默认”。</small>
<label for="model-key">API Key <span>{{ keyConfigured() ? '(已配置,留空保留)' : '(尚未配置)' }}</span></label> <label for="model-key">API Key <span>{{ keyConfigured() ? '(已配置,留空保留)' : '(尚未配置)' }}</span></label>
@@ -71,12 +79,18 @@ interface ModelSettings {
.actions { display: flex; gap: 12px; margin-top: 8px; } .actions { display: flex; gap: 12px; margin-top: 8px; }
button { padding: 11px 20px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; cursor: pointer; font: inherit; } button { padding: 11px 20px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; cursor: pointer; font: inherit; }
button:disabled { opacity: .5; cursor: default; } .success { color: #047857; } .error { color: #be123c; } button:disabled { opacity: .5; cursor: default; } .success { color: #047857; } .error { color: #be123c; }
.model-presets { display: flex; flex-wrap: wrap; gap: 8px; }
.model-presets button { padding: 8px 12px; background: #f8fafc; color: #475569; border-color: #cbd5e1; font-size: 14px; }
.model-presets button:hover:not(:disabled) { background: #e0f2fe; border-color: #38bdf8; }
.model-presets button[aria-pressed="true"] { background: #e0f2fe; border-color: #0284c7; color: #075985; }
button:focus-visible { outline: 2px solid #0284c7; outline-offset: 2px; }
small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } } small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }
`] `]
}) })
export class ModelSettingsComponent implements OnInit { export class ModelSettingsComponent implements OnInit {
private http = inject(HttpClient); private http = inject(HttpClient);
saved = output<void>(); saved = output<void>();
readonly modelPresets = ['gpt-5.6-luna', 'mimo-v2.6-flash'];
baseUrl = ''; model = ''; apiKey = ''; timeout = 300; retries = 5; retryInterval = 10; reasoning = 'high'; baseUrl = ''; model = ''; apiKey = ''; timeout = 300; retries = 5; retryInterval = 10; reasoning = 'high';
busy = signal<'save' | 'test' | null>(null); busy = signal<'save' | 'test' | null>(null);
loading = signal(true); keyConfigured = signal(false); envPath = signal(''); loading = signal(true); keyConfigured = signal(false); envPath = signal('');
+1 -1
View File
@@ -67,7 +67,7 @@ class LLM(BaseModel):
max_tokens: int | None = Field(default=None, gt=0) max_tokens: int | None = Field(default=None, gt=0)
temperature: float | None = None temperature: float | None = None
reasoning_effort: ( reasoning_effort: (
Literal["default", "none", "minimal", "low", "medium", "high", "xhigh"] | None Literal["default", "none", "minimal", "low", "medium", "high", "xhigh", "max"] | None
) = Field(default_factory=lambda: settings.OPENAI_REASONING_EFFORT) ) = Field(default_factory=lambda: settings.OPENAI_REASONING_EFFORT)
enable_grounding: bool | None = None enable_grounding: bool | None = None
+1 -1
View File
@@ -42,6 +42,6 @@ MCP 自动刷新只处理已存在且指向当前项目的标准 Python 模块
报告导出到 `artifacts/mcp_exports/`,包含本地路径及服务下载链接。运行中导出是快照;执行完成不等于验证通过,报告会保留未验证、失败、跳过与中断状态。 报告导出到 `artifacts/mcp_exports/`,包含本地路径及服务下载链接。运行中导出是快照;执行完成不等于验证通过,报告会保留未验证、失败、跳过与中断状态。
模型默认使用 `mimo-v2.6-flash`,也预设 `gpt-5.6-luna` 并支持自定义。思考强度默认 `high`,选“由模型默认”不发送该参数;接口不支持时明确报错。单次请求 300 秒,首次失败后最多再试 5 次,每次等待 10 秒,SDK 不叠加重试。耗尽后沿用任务暂停/恢复机制,鉴权或非法参数立即失败。视频分析与录屏默认关闭。 模型默认使用 `mimo-v2.6-flash`,也预设 `gpt-5.6-luna` 并支持自定义。思考强度最高支持 `max`,默认 `high`,选“由模型默认”不发送该参数;接口不支持时明确报错。单次请求 300 秒,首次失败后最多再试 5 次,每次等待 10 秒,SDK 不叠加重试。耗尽后沿用任务暂停/恢复机制,鉴权或非法参数立即失败。视频分析与录屏默认关闭。
保存全局配置要求用户明确指令;只读配置不包含密钥,MCP 不接收密钥。密钥仍在网页或 `.env` 中填写。设置对新任务生效,模型请求重试不会触发测试用例自动重跑。历史步骤证据保留,设备动作回放已移除。 保存全局配置要求用户明确指令;只读配置不包含密钥,MCP 不接收密钥。密钥仍在网页或 `.env` 中填写。设置对新任务生效,模型请求重试不会触发测试用例自动重跑。历史步骤证据保留,设备动作回放已移除。
+26
View File
@@ -0,0 +1,26 @@
# ARTEMIS-Eapil 1.0.0
## 功能
- 简体中文网页,支持自然语言 Android 任务、设备选择与 Flash/Pro 执行模式。
- 统一使用 OpenAI Responses;预设 gpt-5.6-luna、mimo-v2.6-flash,也可输入并保存自定义模型。
- 思考强度最高可选 max,默认 high;单次请求超时 300 秒,失败后最多重试 5 次,间隔 10 秒。
- Excel 用例批量导入、执行与队列管理,历史按日期分组并支持搜索。
- Word、Excel 及批次 ZIP 报告,区分执行状态和实际验证结论。
- 提供完整流程 MCP 工具、可分发 Skill 和仓库 AGENTS.md 开发指南。
- Windows 一键安装支持国内镜像、已有环境复用、Git 快进更新、依赖检查及已有 MCP 配置刷新。
## 本次修复
- 模型名称不再使用浏览器原生建议下拉,改用页面内预设按钮和独立文本输入,关闭输入框自动填充。
- 前端、配置保存及模型请求均支持 max 思考强度;自定义模型名称可保存并重新加载。
- 移除设备动作回放,保留历史步骤和截图;视频分析与录屏默认关闭。
- 精简中文 README,移除 GitHub Actions 工作流。
## 安装与升级
Windows 解压源码后运行 start.bat,国内网络运行 start-cn.bat。首次运行需要联网安装依赖并构建网页。配置模型接口和密钥后即可使用。
Git 安装用户在无本地冲突的情况下通过启动脚本检查更新;ZIP 安装保留本地目录继续运行,需要自动源码更新时改用 Git 克隆。升级后在 AI 客户端重新加载 MCP,Skill 按客户端方式安装或更新。
源码包不包含虚拟环境、依赖目录、编译产物、密钥和任务数据。模型与 max 参数是否可用取决于所配置的 Responses 网关;不支持时会明确报错,不自动降级。
+1 -1
View File
@@ -114,7 +114,7 @@ async def mobile_config(
Only set persist_defaults=true when the user requests saved/global configuration. Only set persist_defaults=true when the user requests saved/global configuration.
model changes: model (preset or custom), base_url, reasoning_effort model changes: model (preset or custom), base_url, reasoning_effort
(default/none/minimal/low/medium/high/xhigh), timeout_seconds, max_retries, (default/none/minimal/low/medium/high/xhigh/max), timeout_seconds, max_retries,
retry_interval_seconds. 'default' omits reasoning; unsupported values fail visibly. retry_interval_seconds. 'default' omits reasoning; unsupported values fail visibly.
capabilities: video_enabled (recording + analysis), outputter_enabled, capabilities: video_enabled (recording + analysis), outputter_enabled,
step_summarizer_enabled. reports: formats=['docx','xlsx'] (at least one). step_summarizer_enabled. reports: formats=['docx','xlsx'] (at least one).
+1 -2
View File
@@ -14,7 +14,7 @@
[project] [project]
name = "artemis" name = "artemis"
version = "1.0" version = "1.0.0"
description = "Autonomous AI Assistant for Mobile Automation" description = "Autonomous AI Assistant for Mobile Automation"
authors = [ authors = [
@@ -254,4 +254,3 @@ dev = [
"pyright>=1.1.389", "pyright>=1.1.389",
"pre-commit>=3.5.0", "pre-commit>=3.5.0",
] ]
+1 -1
View File
@@ -27,7 +27,7 @@ description: 使用 ARTEMIS MCP 在 Android 真机或模拟器上完成自然语
## 配置和恢复边界 ## 配置和恢复边界
- `mobile_config` 读取非敏感设置。只有用户明确要求保存或修改默认配置时,才传 `changes` 与 `persist_defaults=true`;这些设置影响后续任务,不能为一次临时需求擅自改变以后所有任务。 - `mobile_config` 读取非敏感设置。只有用户明确要求保存或修改默认配置时,才传 `changes` 与 `persist_defaults=true`;这些设置影响后续任务,不能为一次临时需求擅自改变以后所有任务。
- 模型名称支持预设 `gpt-5.6-luna`、`mimo-v2.6-flash` 和自定义名称,取决于用户配置的 Responses 网关。默认思考强度 `high`、单次请求 300 秒、失败后最多重试 5 次、固定间隔 10 秒;默认关闭视频分析与录屏。不根据模型名称猜测网关能力。 - 模型名称支持预设 `gpt-5.6-luna`、`mimo-v2.6-flash` 和自定义名称,取决于用户配置的 Responses 网关。思考强度最高支持 `max`,默认 `high`、单次请求 300 秒、失败后最多重试 5 次、固定间隔 10 秒;默认关闭视频分析与录屏。不根据模型名称猜测网关能力。
- 强度选 `default` 表示不发送该参数,和 `none`(显式要求关闭)不同。接口拒绝强度参数时明确呈现失败,先询问用户是否改用 `default`,不能静默降级或换模型。 - 强度选 `default` 表示不发送该参数,和 `none`(显式要求关闭)不同。接口拒绝强度参数时明确呈现失败,先询问用户是否改用 `default`,不能静默降级或换模型。
- API Key 只通过网页或 `.env` 配置。不要请求用户把 key 发到聊天中,不读取/回显已有 key。保存后新任务生效;直接编辑环境文件后需重启相应服务,运行中的任务保留自己的配置。 - API Key 只通过网页或 `.env` 配置。不要请求用户把 key 发到聊天中,不读取/回显已有 key。保存后新任务生效;直接编辑环境文件后需重启相应服务,运行中的任务保留自己的配置。
- 当前视频开关仅提供保存的全局设置,没有单任务覆盖或独立视频文件分析接口。用户只要求临时分析且默认关闭时,明确说明这个限制;不能用“先开全局、执行后再关”影响同期任务。只有用户改为要求保存启用后,才修改配置并提交新任务。 - 当前视频开关仅提供保存的全局设置,没有单任务覆盖或独立视频文件分析接口。用户只要求临时分析且默认关闭时,明确说明这个限制;不能用“先开全局、执行后再关”影响同期任务。只有用户改为要求保存启用后,才修改配置并提交新任务。
@@ -125,16 +125,30 @@ def test_reasoning_retry_settings_roundtrip(isolated_settings):
assert parse_llm_config().operator.reasoning_effort == "default" assert parse_llm_config().operator.reasoning_effort == "default"
def test_responses_payload_has_reasoning_and_no_sdk_retry(monkeypatch): def test_max_reasoning_and_custom_model_survive_reload(isolated_settings):
result = service.save_model_settings(
service.ModelSettingsUpdate(model="custom/model-v2", reasoning_effort="max")
)
assert result["model"] == "custom/model-v2"
assert result["reasoning_effort"] == "max"
assert dotenv_values(isolated_settings)["OPENAI_REASONING_EFFORT"] == "max"
reloaded = Settings(_env_file=isolated_settings)
assert reloaded.OPENAI_REASONING_EFFORT == "max"
assert reloaded.OPENAI_MODEL == "custom/model-v2"
assert parse_llm_config().operator.reasoning_effort == "max"
@pytest.mark.parametrize("effort", ["high", "max"])
def test_responses_payload_has_reasoning_and_no_sdk_retry(monkeypatch, effort):
from artemis.llm.router import ModelEndpoint, ModelFactory from artemis.llm.router import ModelEndpoint, ModelFactory
from unittest.mock import Mock from unittest.mock import Mock
factory = Mock() factory = Mock()
monkeypatch.setattr("langchain_openai.ChatOpenAI", factory) monkeypatch.setattr("langchain_openai.ChatOpenAI", factory)
ModelFactory.create_model( ModelFactory.create_model(
ModelEndpoint(api_key="test-only", reasoning_effort="high", max_retries=5) ModelEndpoint(api_key="test-only", reasoning_effort=effort, max_retries=5)
) )
assert factory.call_args.kwargs["reasoning_effort"] == "high" assert factory.call_args.kwargs["reasoning_effort"] == effort
assert factory.call_args.kwargs["max_retries"] == 0 assert factory.call_args.kwargs["max_retries"] == 0
assert factory.call_args.kwargs["use_responses_api"] is True assert factory.call_args.kwargs["use_responses_api"] is True
ModelFactory.create_model(ModelEndpoint(api_key="test-only", reasoning_effort="default")) ModelFactory.create_model(ModelEndpoint(api_key="test-only", reasoning_effort="default"))
+9
View File
@@ -85,6 +85,15 @@ async def test_text_and_image_use_responses_transport(transport_model):
assert result.usage_metadata["total_tokens"] == 13 assert result.usage_metadata["total_tokens"] == 13
@pytest.mark.asyncio
async def test_max_reasoning_is_sent_unchanged_to_responses(transport_model):
model, requests, _ = transport_model
model.reasoning_effort = "max"
await model.ainvoke([HumanMessage(content="Check")])
assert requests[0].url.path == "/v1/responses"
assert json.loads(requests[0].content)["reasoning"]["effort"] == "max"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_function_call_roundtrip_uses_call_id(transport_model): async def test_function_call_roundtrip_uses_call_id(transport_model):
model, requests, outputs = transport_model model, requests, outputs = transport_model