Skip to content

Commit 7b0b4f1

Browse files
yeahhe365claude
andcommitted
migrate robotics models from ER 1.6 to ER 2 preview
ER 1.6 shut down 2026-08-31. Replace all references with gemini-robotics-er-2-preview, defined once as ROBOTICS_MODEL in src/constants/modelConfiguration.ts; update registry, tests, and bilingual READMEs (migration notice + API key restriction prerequisite). Add scripts/smoke_robotics.mjs (google-genai interactions-based pointing smoke test with structured-output schema) and DEPLOY_CHECKLIST_ER2.md (staging/prod verification + rollback notes). Full suite: 363 files, 2057 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 09c626f commit 7b0b4f1

17 files changed

Lines changed: 271 additions & 44 deletions

DEPLOY_CHECKLIST_ER2.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# ER 2 迁移 · 部署验证 Checklist
2+
3+
Gemini Robotics ER 1.6 已于 2026-08-31 停服;本项目已迁移到
4+
`gemini-robotics-er-2-preview`(唯一模型常量:`ROBOTICS_MODEL`,见
5+
`src/constants/modelConfiguration.ts`)。本清单用于上线 ER 2 前后的验证。
6+
7+
## 前置条件
8+
9+
- [ ] `GEMINI_API_KEY` 已在 [AI Studio](https://aistudio.google.com/api-keys) 添加 API 限制
10+
(无限制 Key 对 Robotics 模型返回 `403 Forbidden`)。
11+
- [ ] `node_modules` 已安装 `@google/genai@^1.50.1`(支持 `interactions.create`
12+
`thinking_level`;无需再升级)。
13+
- [ ] 全量测试通过:`npx vitest run`(当前基线:363 文件 / 2057 测试全绿)。
14+
- [ ] 全局搜索确认零残留:`grep -rinE "er-1\.6|er 1\.6|robotics-1\.6|robotics-er-1"` 无结果。
15+
16+
## 阶段 A · Staging 验证
17+
18+
1. **跑冒烟脚本**(连真实 API,在 staging 环境或本地执行):
19+
```bash
20+
GEMINI_API_KEY=<受限key> node scripts/smoke_robotics.mjs
21+
```
22+
- 期望:退出码 0;每项 `point=[y,x]`(0–1000 整数)与非空 `label`
23+
- 若自建端点/代理:`GEMINI_API_BASE=<endpoint>` 同命令验证代理链路
24+
(注意:interactions 端点由 SDK 走 `POST /v1beta/interactions`,代理需支持该路径)。
25+
2. **观察指标**(连续 ≥10 次调用或 ≥30 分钟):
26+
- 错误率:`5xx / 403 / 429` 计数。
27+
- 平均延迟与 P95;高分辨率输入或 `thinking_level: high` 会显著增加延迟(官方限制说明)。
28+
- 输出一致性:同一输入多次查询的 point 方差(官方建议取平均做高精度任务)。
29+
3. **功能回归**:验证应用内 Robotics 模型可用——模型选择、生成、思考配置
30+
`thinking_level` 默认 medium,UI 高精度任务保留 high)。
31+
32+
## 阶段 B · 生产验证
33+
34+
1. 确认 UI 显示模型名为 "Gemini Robotics-ER 2"(`modelRegistry.ts`),无 1.6 字样。
35+
2. 上线后观察同一组指标(错误率 / 403/429 / 平均延迟),与 staging 对比应无异常升高。
36+
3. 若出现空间定位精度回退:
37+
- 先对同一输入多次查询取平均(官方建议的一致性做法);
38+
- 再评估是否对高精度调用点调高 `thinking_level: high`
39+
- 最后检查输入图像质量(裁剪/放大目标物体、改善对比度)。
40+
41+
## 回滚说明
42+
43+
- 停服前(原 1.6 已停服,此窗口已关闭):将 `ROBOTICS_MODEL` 改回旧模型名即可。
44+
- 停服后:不可回退到 1.6;唯一出路是继续使用 ER 2 并调整调用参数(见阶段 B-3)。

README.en.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ The project currently focuses on one main application shape: a **Vite + React SP
5151
- **Docker mode**: a `web + api` deployment where regular Gemini requests call `/api/gemini/*`, while Live API connects directly from the browser with the local key.
5252
- **Static frontend + standalone API mode**: deploy the frontend to Pages/CDN and run the Node API service separately.
5353

54+
> 📢 **Gemini Robotics-ER model migration notice (2026-08-01)**
55+
> Support for `gemini-robotics-er-1.6-preview` has been dropped (the model was shut down on **2026-08-31**). The built-in Robotics model is now **`gemini-robotics-er-2-preview`**, defined once as the `ROBOTICS_MODEL` constant (`src/constants/modelConfiguration.ts`).
56+
> Before using Robotics models: 1) make sure your `GEMINI_API_KEY` has restrictions configured in [AI Studio](https://aistudio.google.com/api-keys) — unrestricted keys return `403 Forbidden`; 2) keep `thinking_level` at `medium` by default (best latency/performance balance), reserving `high` for high-precision spatial tasks; 3) for high-precision tasks, query multiple times and average the results to reduce variance.
57+
5458
## API Modes
5559

5660
### Gemini Native
@@ -447,12 +451,14 @@ AMC-WebUI/
447451

448452
## Gemini Native Default Models
449453

454+
> 📌 **Prerequisite for Gemini Robotics-ER models** (`gemini-robotics-er-2-preview` and other Robotics endpoints): Google requires the API key to have **restrictions configured in [AI Studio](https://aistudio.google.com/api-keys)**. Requests with unrestricted keys are rejected with a `403 Forbidden` error. Configure restrictions (e.g. limit by project/domain) for any key used with Robotics models, and keep this requirement in sync in deployment docs and CI secrets.
455+
450456
OpenAI Compatible mode uses a separate model list that you can manage manually or fetch from a compatible endpoint. The table below lists the built-in Gemini Native defaults.
451457

452458
| Type | Models |
453459
| :--------------- | :---------------------------------------------------------------------------------------------------------------------- |
454460
| Gemini 3.x | `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-flash-live-preview`, `gemini-3.1-pro-preview` |
455-
| Robotics | `gemini-robotics-er-1.6-preview` |
461+
| Robotics | `gemini-robotics-er-2-preview` |
456462
| Gemma 4 | `gemma-4-31b-it`, `gemma-4-26b-a4b-it` |
457463
| Image generation | `gemini-2.5-flash-image`, `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`, `gemini-3.1-flash-lite-image` |
458464
| TTS | `gemini-3.1-flash-tts-preview` with 30 voices |

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
- **Docker 部署模式**`web + api` 双服务部署,普通 Gemini 请求走 `/api/gemini/*`,第三方兼容接口走 `/api/openai/*`,Live API 走 `/api/live` 的 WebSocket 全代理
5252
- **静态前端 + 独立 API 模式**:前端部署到 Pages/CDN,后端单独托管 Node API 服务
5353

54+
> 📢 **Gemini Robotics-ER 模型迁移公告(2026-08-01)**
55+
> 项目已停止支持 `gemini-robotics-er-1.6-preview`(该模型已于 **2026-08-31** 停服),内置模型已迁移至 **`gemini-robotics-er-2-preview`**,全项目统一由常量 `ROBOTICS_MODEL``src/constants/modelConfiguration.ts`)定义。
56+
> 使用 Robotics 模型前:1) 请确保 `GEMINI_API_KEY` 已在 [AI Studio](https://aistudio.google.com/api-keys) 添加 API 限制,无限制 Key 会返回 `403 Forbidden`;2) 建议 `thinking_level` 默认 `medium`(延迟与性能平衡),仅高精度空间任务使用 `high`;3) 高精度场景可对同一输入多次查询取平均以降低方差。
57+
5458
## API 模式说明
5559

5660
### Gemini 原生模式
@@ -247,6 +251,8 @@ docker compose up -d --build
247251
248252
### 运行时配置与环境变量
249253

254+
> 📌 **Gemini Robotics-ER 模型的 API Key 前置条件**`gemini-robotics-er-2-preview` 等 Robotics 端点):Google 要求该模型的 API Key **必须已在 [AI Studio](https://aistudio.google.com/api-keys) 添加 API 限制**(restriction),无限制的 Key 会被拒绝并返回 `403 Forbidden`。使用 Robotics 模型前请在 AI Studio 为对应 Key 配置限制(如限定项目/域名),并在部署文档与 CI secrets 说明中同步该要求。
255+
250256
部署时请区分两类配置:
251257

252258
| 变量名 | 用途 | 公开性 | Docker 默认值 |
@@ -475,7 +481,7 @@ OpenAI 兼容模式使用独立模型列表,可在设置中手动维护或从
475481
| 类型 | 模型 |
476482
| :------------- | :-------------------------------------------------------------------------------------------------------------- |
477483
| **Gemini 3.x** | gemini-3.6-flash, gemini-3.5-flash-lite, gemini-3.1-flash-live-preview, gemini-3.1-pro-preview |
478-
| **Robotics** | gemini-robotics-er-1.6-preview |
484+
| **Robotics** | gemini-robotics-er-2-preview |
479485
| **Gemma 4** | gemma-4-31b-it, gemma-4-26b-a4b-it |
480486
| **图片生成** | gemini-2.5-flash-image, gemini-3-pro-image-preview, gemini-3.1-flash-image-preview, gemini-3.1-flash-lite-image |
481487
| **TTS** | gemini-3.1-flash-tts-preview (30 种语音) |

scripts/smoke_robotics.mjs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Smoke test for the Gemini Robotics ER 2 pointing endpoint.
4+
*
5+
* Uploads a test image and asks the model to point to objects, asserting the
6+
* response is a JSON array where every item has:
7+
* - point: integer [y, x] pair normalized to 0-1000
8+
* - label: non-empty string
9+
*
10+
* Prerequisites (see README):
11+
* - GEMINI_API_KEY set and restricted in AI Studio (unrestricted keys
12+
* return 403 Forbidden for Robotics models).
13+
* - Optional GEMINI_API_BASE for a custom endpoint/proxy.
14+
*
15+
* Usage:
16+
* GEMINI_API_KEY=... node scripts/smoke_robotics.mjs
17+
* GEMINI_API_KEY=... GEMINI_API_BASE=... node scripts/smoke_robotics.mjs [image-path]
18+
*
19+
* Exit code: 0 = all checks passed, 1 = any check failed, 2 = usage/API error.
20+
*/
21+
import { GoogleGenAI } from '@google/genai';
22+
import fs from 'node:fs';
23+
import path from 'node:path';
24+
25+
const MODEL = process.env.ROBOTICS_MODEL ?? 'gemini-robotics-er-2-preview';
26+
const DEFAULT_IMAGE = new URL('../public/app-logo.png', import.meta.url).pathname;
27+
28+
const PROMPT = `
29+
Point to no more than 10 items in the image. The label returned
30+
should be an identifying name for the object detected.
31+
The answer should follow the json format: [{"point": [y, x], "label": <label>}, ...].
32+
The points are in [y, x] format normalized to 0-1000.
33+
`;
34+
35+
// JSON schema for structured output: array of { point: [y, x], label: string }.
36+
const responseSchema = {
37+
type: 'array',
38+
items: {
39+
type: 'object',
40+
properties: {
41+
point: {
42+
type: 'array',
43+
items: { type: 'integer' },
44+
minItems: 2,
45+
maxItems: 2,
46+
},
47+
label: { type: 'string' },
48+
},
49+
required: ['point', 'label'],
50+
additionalProperties: false,
51+
},
52+
};
53+
54+
function fail(message) {
55+
console.error(`✗ FAIL: ${message}`);
56+
process.exitCode = 1;
57+
}
58+
59+
function parsePoints(rawText) {
60+
const match = rawText.match(/\[[\s\S]*\]/);
61+
if (!match) return { ok: false, error: `no JSON array found in response: ${rawText.slice(0, 200)}` };
62+
try {
63+
return { ok: true, items: JSON.parse(match[0]) };
64+
} catch (err) {
65+
return { ok: false, error: `JSON parse error: ${err.message}` };
66+
}
67+
}
68+
69+
async function main() {
70+
const imagePath = process.argv[2] ?? DEFAULT_IMAGE;
71+
if (!fs.existsSync(imagePath)) {
72+
fail(`image not found: ${imagePath}`);
73+
return;
74+
}
75+
76+
const apiKey = process.env.GEMINI_API_KEY;
77+
if (!apiKey) {
78+
console.error('GEMINI_API_KEY is not set (required; must be restricted in AI Studio for Robotics models).');
79+
process.exit(2);
80+
}
81+
82+
const client = new GoogleGenAI({ apiKey, baseUrl: process.env.GEMINI_API_BASE });
83+
84+
console.log(`Model: ${MODEL}`);
85+
console.log(`Image: ${imagePath}`);
86+
87+
let uploaded;
88+
try {
89+
uploaded = await client.files.upload({ file: imagePath });
90+
console.log(`Uploaded: ${uploaded.uri}`);
91+
} catch (err) {
92+
fail(`file upload failed: ${err.message}`);
93+
return;
94+
}
95+
96+
let response;
97+
try {
98+
response = await client.interactions.create({
99+
model: MODEL,
100+
input: [
101+
{ type: 'image', uri: uploaded.uri, mime_type: uploaded.mime_type },
102+
{ type: 'text', text: PROMPT },
103+
],
104+
generation_config: {
105+
thinking_config: { thinking_level: 'medium' },
106+
response_schema: responseSchema,
107+
response_mime_type: 'application/json',
108+
},
109+
});
110+
} catch (err) {
111+
fail(`interactions.create failed: ${err.message}`);
112+
return;
113+
}
114+
115+
const text = response.output_text ?? '';
116+
const { ok, items } = parsePoints(text);
117+
if (!ok) {
118+
fail(items.error);
119+
return;
120+
}
121+
if (!Array.isArray(items)) {
122+
fail(`response is not an array: ${JSON.stringify(items).slice(0, 200)}`);
123+
return;
124+
}
125+
126+
let pass = 0;
127+
const output = [];
128+
for (const item of items) {
129+
const problems = [];
130+
if (!Array.isArray(item?.point) || item.point.length !== 2) {
131+
problems.push('point is not a [y, x] pair');
132+
} else {
133+
const [y, x] = item.point;
134+
if (!Number.isInteger(y) || !Number.isInteger(x)) problems.push(`point [${y}, ${x}] is not integer`);
135+
if (y < 0 || y > 1000 || x < 0 || x > 1000) problems.push(`point [${y}, ${x}] out of 0-1000 range`);
136+
}
137+
if (typeof item?.label !== 'string' || item.label.trim() === '') {
138+
problems.push('label is empty');
139+
}
140+
const itemPass = problems.length === 0;
141+
if (itemPass) pass += 1;
142+
output.push(
143+
`${itemPass ? '✓' : '✗'} point=[${item?.point?.join(', ') ?? 'n/a'}] label="${item?.label ?? ''}"${
144+
problems.length ? ` (${problems.join('; ')})` : ''
145+
}`,
146+
);
147+
}
148+
149+
console.log('\n--- items ---');
150+
console.log(output.join('\n'));
151+
console.log(`\npassed ${pass}/${items.length} items, ${items.length} total`);
152+
153+
if (items.length === 0) {
154+
fail('response contains no items');
155+
}
156+
if (pass !== items.length) {
157+
fail(`${items.length - pass} item(s) failed validation`);
158+
}
159+
}
160+
161+
main().catch((err) => {
162+
console.error(`✗ unhandled error: ${err.stack ?? err}`);
163+
process.exit(2);
164+
});

src/components/header/HeaderModelSelector.test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ describe('HeaderModelSelector', () => {
145145
await act(async () => {
146146
renderer.root.render(
147147
<HeaderModelSelector
148-
currentModelName="Gemini Robotics-ER 1.6 Preview"
149-
availableModels={[{ id: 'gemini-robotics-er-1.6-preview', name: 'Gemini Robotics-ER 1.6 Preview' }]}
150-
selectedModelId="gemini-robotics-er-1.6-preview"
148+
currentModelName="Gemini Robotics-ER 2 Preview"
149+
availableModels={[{ id: 'gemini-robotics-er-2-preview', name: 'Gemini Robotics-ER 2 Preview' }]}
150+
selectedModelId="gemini-robotics-er-2-preview"
151151
onSelectModel={vi.fn()}
152152
isSwitchingModel={false}
153153
isLoading={false}
@@ -160,20 +160,20 @@ describe('HeaderModelSelector', () => {
160160
});
161161

162162
const label = Array.from(renderer.container.querySelectorAll('span')).find(
163-
(node) => node.textContent === 'Robotics-ER 1.6',
163+
(node) => node.textContent === 'Robotics-ER 2',
164164
);
165165
expect(label?.className).toContain('font-semibold');
166166
});
167167

168-
it('shows the fast toggle for Gemini Robotics-ER 1.6 and uses minimal thinking as fast mode', async () => {
168+
it('shows the fast toggle for Gemini Robotics-ER 2 and uses minimal thinking as fast mode', async () => {
169169
const onSetThinkingLevel = vi.fn();
170170

171171
await act(async () => {
172172
renderer.root.render(
173173
<HeaderModelSelector
174-
currentModelName="Gemini Robotics-ER 1.6 Preview"
175-
availableModels={[{ id: 'gemini-robotics-er-1.6-preview', name: 'Gemini Robotics-ER 1.6 Preview' }]}
176-
selectedModelId="gemini-robotics-er-1.6-preview"
174+
currentModelName="Gemini Robotics-ER 2 Preview"
175+
availableModels={[{ id: 'gemini-robotics-er-2-preview', name: 'Gemini Robotics-ER 2 Preview' }]}
176+
selectedModelId="gemini-robotics-er-2-preview"
177177
onSelectModel={vi.fn()}
178178
isSwitchingModel={false}
179179
isLoading={false}

src/components/header/HeaderModelSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const HeaderModelSelector: FC<HeaderModelSelectorProps> = ({
5858

5959
// Determine the target "Fast" level based on model capabilities
6060
// Gemini 3 Flash models support MINIMAL thinking for maximum speed
61-
// Gemini Robotics-ER 1.6 matches Flash here; other Gemini 3 models
61+
// Gemini Robotics-ER matches Flash here; other Gemini 3 models
6262
// (like Pro) typically bottom out at LOW.
6363
const targetFastLevel = isFlashModel || isGeminiRoboticsModel ? 'MINIMAL' : 'LOW';
6464

src/components/settings/controls/thinking/ThinkingControl.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,11 @@ describe('ThinkingControl image model behavior', () => {
138138
expect(setShowThoughts).toHaveBeenCalledWith(true);
139139
});
140140

141-
it('shows full thinking level options for Gemini Robotics-ER 1.6 in auto mode', async () => {
141+
it('shows full thinking level options for Gemini Robotics-ER 2 in auto mode', async () => {
142142
await act(async () => {
143143
renderer.root.render(
144144
<ThinkingControl
145-
modelId="gemini-robotics-er-1.6-preview"
145+
modelId="gemini-robotics-er-2-preview"
146146
thinkingBudget={-1}
147147
setThinkingBudget={vi.fn()}
148148
thinkingLevel="LOW"
@@ -163,7 +163,7 @@ describe('ThinkingControl image model behavior', () => {
163163
await act(async () => {
164164
renderer.root.render(
165165
<ThinkingControl
166-
modelId="gemini-robotics-er-1.6-preview"
166+
modelId="gemini-robotics-er-2-preview"
167167
thinkingBudget={-1}
168168
setThinkingBudget={vi.fn()}
169169
thinkingLevel="HIGH"
@@ -178,14 +178,14 @@ describe('ThinkingControl image model behavior', () => {
178178
expect(renderer.container.textContent).not.toContain('settingsReasoningBadgeEnabled');
179179
});
180180

181-
it('hides the off mode for Gemini Robotics-ER 1.6 and normalizes legacy off state to minimal', async () => {
181+
it('hides the off mode for Gemini Robotics-ER 2 and normalizes legacy off state to minimal', async () => {
182182
const setThinkingBudget = vi.fn();
183183
const setThinkingLevel = vi.fn();
184184

185185
await act(async () => {
186186
renderer.root.render(
187187
<ThinkingControl
188-
modelId="gemini-robotics-er-1.6-preview"
188+
modelId="gemini-robotics-er-2-preview"
189189
thinkingBudget={0}
190190
setThinkingBudget={setThinkingBudget}
191191
thinkingLevel="HIGH"

src/components/settings/sections/GenerationSection.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('GenerationSection', () => {
9595
await act(async () => {
9696
renderer.root.render(
9797
<GenerationSection
98-
modelId="gemini-robotics-er-1.6-preview"
98+
modelId="gemini-robotics-er-2-preview"
9999
currentSettings={ultraHighSettings}
100100
onUpdateSetting={vi.fn()}
101101
/>,

src/components/shared/ModelPicker.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('getModelIcon', () => {
4747

4848
it('uses the Gemini SVG brand icon for Gemini Robotics models', () => {
4949
const roboticsMarkup = renderToStaticMarkup(
50-
getModelIcon({ id: 'gemini-robotics-er-1.6-preview', name: 'Gemini Robotics-ER 1.6 Preview' }),
50+
getModelIcon({ id: 'gemini-robotics-er-2-preview', name: 'Gemini Robotics-ER 2 Preview' }),
5151
);
5252

5353
expect(roboticsMarkup).toContain('data-model-brand-icon="gemini"');

0 commit comments

Comments
 (0)