Skip to content

Commit 5033db3

Browse files
anbeckhamclaude
andcommitted
Fix Veo video generation: retire veo-2, repair polling loop
Two independent bugs caused every generate_video call to fail: 1. The default model "veo-2" (veo-2.0-generate-001) was retired from the Gemini API on 2026-04-02, so default calls hit a dead endpoint. Default is now veo-3.1-fast; veo-2 removed from config, map, tool schema, command doc, and skill matrix. 2. The poll loop called operation.reload(), which does not exist on google-genai's GenerateVideosOperation. Replaced with the canonical pattern: operation = client.operations.get(operation). Added a regression test that asserts operations.get is used. Also corrects the hooks/hooks.json manifest to the object-based schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e299a42 commit 5033db3

10 files changed

Lines changed: 85 additions & 36 deletions

File tree

commands/create-video.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ Follow this workflow strictly:
2626
- Clarify: motion style, camera movement, duration, mood
2727

2828
### 2. Choose Model
29-
- **veo-2** (default): Stable, reliable — good for most use cases
30-
- **veo-3.1**: Latest model, best quality — use for premium or complex scenes
31-
- **veo-3.1-fast**: Faster iteration — use when exploring ideas or testing prompts
29+
- **veo-3.1-fast** (default): Faster iteration — good for most use cases
30+
- **veo-3.1**: Best quality — use for premium or complex scenes
3231

3332
Ask the user which model to use if they haven't specified.
3433

hooks/hooks.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
[
2-
{
3-
"event": "SessionStart",
4-
"matcher": "*",
5-
"hooks": [
1+
{
2+
"description": "gemini-visual-design hooks — API key validation on session start",
3+
"hooks": {
4+
"SessionStart": [
65
{
7-
"type": "command",
8-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-api-key.sh"
6+
"matcher": "*",
7+
"hooks": [
8+
{
9+
"type": "command",
10+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-api-key.sh"
11+
}
12+
]
913
}
1014
]
1115
}
12-
]
16+
}

skills/visual-design-system/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ Use this skill when working on visual design tasks — image generation, UI mock
4747
| Iterative editing | Gemini Flash | Multi-turn editing support |
4848
| Final production assets | Imagen 4 | Highest quality output |
4949
| Design analysis | Gemini Flash | Multimodal understanding |
50-
| Short video clips | Veo 2 | Stable, reliable |
51-
| High-quality video | Veo 3.1 | Latest features |
50+
| Short video clips | Veo 3.1 Fast | Faster iteration, good quality |
51+
| High-quality video | Veo 3.1 | Best quality, latest features |
5252

5353
## Template Categories
5454

src/gemini_visual_mcp/config.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
GEMINI_FLASH_IMAGE = "gemini-3.1-flash-image-preview"
1616
GEMINI_FLASH_TEXT = "gemini-2.5-flash"
1717
IMAGEN_MODEL = "imagen-4.0-generate-001"
18-
VEO_2_MODEL = "veo-2.0-generate-001"
1918
VEO_3_MODEL = "veo-3.1-generate-preview"
2019
VEO_3_FAST_MODEL = "veo-3.1-fast-generate-preview"
2120

@@ -59,7 +58,7 @@
5958

6059
# Model selection labels
6160
MODEL_CHOICES_IMAGE = ["gemini", "imagen", "auto"]
62-
MODEL_CHOICES_VIDEO = ["veo-2", "veo-3.1", "veo-3.1-fast"]
61+
MODEL_CHOICES_VIDEO = ["veo-3.1", "veo-3.1-fast"]
6362

6463
# Analysis focus areas
6564
ANALYSIS_FOCUS_AREAS = ["color", "layout", "typography", "overall"]

src/gemini_visual_mcp/gemini_client.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
GEMINI_FLASH_IMAGE,
1414
GEMINI_FLASH_TEXT,
1515
IMAGEN_MODEL,
16-
VEO_2_MODEL,
1716
VEO_3_FAST_MODEL,
1817
VEO_3_MODEL,
1918
)
@@ -27,7 +26,6 @@
2726

2827
# Map friendly model names to API model IDs
2928
VIDEO_MODEL_MAP = {
30-
"veo-2": VEO_2_MODEL,
3129
"veo-3.1": VEO_3_MODEL,
3230
"veo-3.1-fast": VEO_3_FAST_MODEL,
3331
}
@@ -325,19 +323,19 @@ def _call():
325323
async def generate_video(
326324
self,
327325
prompt: str,
328-
model: str = "veo-2",
326+
model: str = "veo-3.1-fast",
329327
image_data: Optional[bytes] = None,
330328
image_mime_type: Optional[str] = None,
331329
) -> Any:
332330
"""Start async video generation. Returns an operation to poll.
333331
334332
Args:
335333
prompt: Video description
336-
model: One of "veo-2", "veo-3.1", "veo-3.1-fast"
334+
model: One of "veo-3.1", "veo-3.1-fast"
337335
image_data: Optional reference image bytes
338336
image_mime_type: MIME type of reference image
339337
"""
340-
model_id = VIDEO_MODEL_MAP.get(model, VEO_2_MODEL)
338+
model_id = VIDEO_MODEL_MAP.get(model, VEO_3_FAST_MODEL)
341339

342340
def _call():
343341
# Veo accepts a prompt alongside an image input — passing both lets
@@ -372,16 +370,19 @@ async def poll_video_operation(
372370
"""
373371

374372
def _poll():
373+
nonlocal operation
375374
start = time.monotonic()
376-
# Poll until complete
375+
# Poll until complete. The google-genai SDK's operation objects are
376+
# immutable pydantic models — fetch the updated state via
377+
# client.operations.get() rather than mutating in place.
377378
while not operation.done:
378379
if time.monotonic() - start > timeout_seconds:
379380
raise GeminiClientError(
380381
f"Video generation timed out after {timeout_seconds}s. "
381382
"The operation may still be running — try again later."
382383
)
383384
time.sleep(5)
384-
operation.reload()
385+
operation = self._client.operations.get(operation)
385386

386387
if operation.response and operation.response.generated_videos:
387388
results = []

src/gemini_visual_mcp/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ async def list_tools() -> list[Tool]:
214214
"model": {
215215
"type": "string",
216216
"enum": MODEL_CHOICES_VIDEO,
217-
"default": "veo-2",
218-
"description": "Video model: 'veo-2' (stable), 'veo-3.1' (latest), 'veo-3.1-fast'",
217+
"default": "veo-3.1-fast",
218+
"description": "Video model: 'veo-3.1' (best quality) or 'veo-3.1-fast' (faster iteration)",
219219
},
220220
"reference_image": {
221221
"type": "string",
@@ -451,7 +451,7 @@ async def _handle_tool(self, name: str, args: dict) -> Any:
451451
result = await generate_video(
452452
client=client,
453453
prompt=args["prompt"],
454-
model=args.get("model", "veo-2"),
454+
model=args.get("model", "veo-3.1-fast"),
455455
reference_image=args.get("reference_image"),
456456
cwd=self._cwd(),
457457
)

src/gemini_visual_mcp/video_gen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Video generation via Veo with async polling.
22
3-
Supports Veo 2, Veo 3.1, and Veo 3.1 Fast models.
3+
Supports Veo 3.1 and Veo 3.1 Fast models.
44
Generates short video clips with prompt enhancement.
55
"""
66

@@ -19,7 +19,7 @@
1919
async def generate_video(
2020
client: GeminiClient,
2121
prompt: str,
22-
model: str = "veo-2",
22+
model: str = "veo-3.1-fast",
2323
reference_image: Optional[str] = None,
2424
cwd: str = ".",
2525
use_profile: bool = True,
@@ -29,7 +29,7 @@ async def generate_video(
2929
Args:
3030
client: GeminiClient instance
3131
prompt: Video description
32-
model: "veo-2", "veo-3.1", or "veo-3.1-fast"
32+
model: "veo-3.1" or "veo-3.1-fast"
3333
reference_image: Optional path to a reference image
3434
cwd: Current working directory for style profile
3535
use_profile: Whether to apply style profile to prompt

tests/test_gemini_client.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ class TestVideoModelMap:
138138
def test_model_mapping(self):
139139
from gemini_visual_mcp.gemini_client import VIDEO_MODEL_MAP
140140

141-
assert "veo-2" in VIDEO_MODEL_MAP
142141
assert "veo-3.1" in VIDEO_MODEL_MAP
143142
assert "veo-3.1-fast" in VIDEO_MODEL_MAP
143+
# Retired model (removed from API on 2026-04-02) must not be listed.
144+
assert "veo-2" not in VIDEO_MODEL_MAP
144145

145146

146147
class TestVideoGenerationWithImage:
@@ -161,7 +162,7 @@ async def test_prompt_passed_with_image(self):
161162
client = GeminiClient(api_key="test-key")
162163
await client.generate_video(
163164
prompt="dolly forward through a misty forest",
164-
model="veo-2",
165+
model="veo-3.1-fast",
165166
image_data=b"image-bytes",
166167
image_mime_type="image/png",
167168
)
@@ -180,9 +181,54 @@ async def test_prompt_passed_without_image(self):
180181
client = GeminiClient(api_key="test-key")
181182
await client.generate_video(
182183
prompt="a calm ocean at sunset",
183-
model="veo-2",
184+
model="veo-3.1-fast",
184185
)
185186

186187
call_kwargs = mock_models.generate_videos.call_args.kwargs
187188
assert call_kwargs["prompt"] == "a calm ocean at sunset"
188189
assert call_kwargs["image"] is None
190+
191+
192+
class TestPollVideoOperation:
193+
"""Regression tests for video operation polling.
194+
195+
A previous bug called operation.reload() inside the poll loop, which does
196+
not exist on google-genai's GenerateVideosOperation pydantic model — every
197+
poll iteration would raise AttributeError after the first 5-second sleep.
198+
The correct SDK pattern is client.operations.get(operation), which returns
199+
a fresh operation object.
200+
"""
201+
202+
@pytest.mark.asyncio
203+
async def test_poll_uses_operations_get_not_reload(self):
204+
with patch("gemini_visual_mcp.gemini_client.genai") as mock_genai:
205+
mock_client_inst = mock_genai.Client.return_value
206+
207+
op_pending = MagicMock(done=False)
208+
op_done = MagicMock(done=True)
209+
op_done.response.generated_videos = [MagicMock(video="video-ref")]
210+
mock_client_inst.operations.get.return_value = op_done
211+
mock_client_inst.files.download.return_value = b"video-bytes"
212+
213+
with patch("gemini_visual_mcp.gemini_client.time.sleep"):
214+
client = GeminiClient(api_key="test-key")
215+
results = await client.poll_video_operation(op_pending)
216+
217+
mock_client_inst.operations.get.assert_called_with(op_pending)
218+
assert results == [{"data": b"video-bytes", "mime_type": "video/mp4"}]
219+
220+
@pytest.mark.asyncio
221+
async def test_poll_returns_immediately_when_done(self):
222+
with patch("gemini_visual_mcp.gemini_client.genai") as mock_genai:
223+
mock_client_inst = mock_genai.Client.return_value
224+
225+
op_done = MagicMock(done=True)
226+
op_done.response.generated_videos = [MagicMock(video="video-ref")]
227+
mock_client_inst.files.download.return_value = b"video-bytes"
228+
229+
client = GeminiClient(api_key="test-key")
230+
results = await client.poll_video_operation(op_done)
231+
232+
# Operation already done — no polling required.
233+
mock_client_inst.operations.get.assert_not_called()
234+
assert results == [{"data": b"video-bytes", "mime_type": "video/mp4"}]

tests/test_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async def test_generate_video(self, server):
7676
with patch("gemini_visual_mcp.server.generate_video") as mock_video:
7777
mock_video.return_value = {
7878
"path": "/tmp/video.mp4",
79-
"model": "veo-2",
79+
"model": "veo-3.1-fast",
8080
"enhanced_prompt": "enhanced",
8181
"warnings": [],
8282
}
@@ -85,7 +85,7 @@ async def test_generate_video(self, server):
8585
})
8686

8787
assert result["video_path"] == "/tmp/video.mp4"
88-
assert result["model"] == "veo-2"
88+
assert result["model"] == "veo-3.1-fast"
8989

9090
@pytest.mark.asyncio
9191
async def test_save_asset(self, server):

tests/test_video_gen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ async def test_text_to_video(self, tmp_path):
2525
result = await generate_video(
2626
client=mock_client,
2727
prompt="A slow camera pan across a mountain landscape at sunrise",
28-
model="veo-2",
28+
model="veo-3.1-fast",
2929
cwd=str(tmp_path),
3030
use_profile=False,
3131
)
3232

3333
mock_client.generate_video.assert_called_once()
3434
mock_client.poll_video_operation.assert_called_once_with(mock_operation)
3535
assert "path" in result
36-
assert result["model"] == "veo-2"
36+
assert result["model"] == "veo-3.1-fast"
3737
assert "enhanced_prompt" in result
3838
assert "warnings" in result
3939

@@ -55,7 +55,7 @@ async def test_reference_image_read_and_passed(self, tmp_path):
5555
await generate_video(
5656
client=mock_client,
5757
prompt="Animate this scene with gentle motion",
58-
model="veo-2",
58+
model="veo-3.1-fast",
5959
reference_image=str(ref_img),
6060
cwd=str(tmp_path),
6161
use_profile=False,

0 commit comments

Comments
 (0)