"""Smoke tests for Gemini tools in generateContent (API Atlas). Cheap tests (always run, < $0.002 total on gemini-3.5-flash-lite): forced function call (mode ANY) + functionResponse round trip, thoughtSignature is mandatory on echo (400), parallel calls, parametersJsonSchema, mode NONE, behavior NON_BLOCKING rejected outside Live (400), code execution parts, URL context metadata, Google Maps grounding chunks. Expensive / restricted (RUN_EXPENSIVE_TESTS=true): Google Search grounding (429 RESOURCE_EXHAUSTED on this key on 2026-09-18 → ACCOUNT_RESTRICTED; the test accepts 200 or 429), legacy computerUse on gemini-2.5-computer-use-preview-10-2025 (429 free-tier quota 0). Reference run: 2026-09-18 (tmp-live/gemini-tools/*.json). """ from __future__ import annotations import pytest MODEL = "gemini-3.5-flash-lite" WEATHER = {"name": "get_weather", "description": "Get the current weather for a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}} TIME = {"name": "get_time", "description": "Get the current local time in a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}} def _gen(gemini, body, note="", model=MODEL): st, r, _ = gemini("POST", f"/v1beta/models/{model}:generateContent", body, note=f"test_tools {note}") return st, r def parts(r): return r["candidates"][0]["content"]["parts"] # ----------------------------------------------------------------------------- cheap def test_forced_function_call_then_response(gemini): user = {"role": "user", "parts": [{"text": "Weather in Paris?"}]} st, r = _gen(gemini, {"contents": [user], "tools": [{"functionDeclarations": [WEATHER]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY", "allowedFunctionNames": ["get_weather"]}}, "generationConfig": {"maxOutputTokens": 64}}, "forced") assert st == 200 fc = [p for p in parts(r) if "functionCall" in p] assert len(fc) == 1 and fc[0]["functionCall"]["name"] == "get_weather" and "city" in fc[0]["functionCall"]["args"] assert fc[0]["functionCall"]["id"].startswith("call_") and fc[0].get("thoughtSignature") model_turn = r["candidates"][0]["content"] st2, r2 = _gen(gemini, {"contents": [user, model_turn, {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"temperature_c": 18}}}]}], "tools": [{"functionDeclarations": [WEATHER]}], "generationConfig": {"maxOutputTokens": 32}}, "functionResponse") assert st2 == 200 and any("text" in p for p in parts(r2)) # dropping the thoughtSignature is rejected stripped = {"role": "model", "parts": [{k: v for k, v in p.items() if k != "thoughtSignature"} for p in model_turn["parts"]]} st3, r3 = _gen(gemini, {"contents": [user, stripped, {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"temperature_c": 18}}}]}], "tools": [{"functionDeclarations": [WEATHER]}], "generationConfig": {"maxOutputTokens": 32}}, "no thoughtSignature") assert st3 == 400 and "thought_signature" in r3["error"]["message"] def test_parallel_function_calls(gemini): st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "What is the weather in Paris and the time in Tokyo? Use both tools."}]}], "tools": [{"functionDeclarations": [WEATHER, TIME]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 128}}, "parallel") assert st == 200 names = sorted(p["functionCall"]["name"] for p in parts(r) if "functionCall" in p) assert names == ["get_time", "get_weather"] def test_parameters_json_schema_and_mode_none(gemini): decl = {"name": "get_weather", "parametersJsonSchema": {"type": "object", "properties": {"city": {"type": "string"}, "unit": {"type": "string", "enum": ["C", "F"]}}, "required": ["city"]}} st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Rome in Celsius?"}]}], "tools": [{"functionDeclarations": [decl]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 64}}, "parametersJsonSchema") assert st == 200 and any(p.get("functionCall", {}).get("args", {}).get("city") for p in parts(r)) st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Paris? 3 words."}]}], "tools": [{"functionDeclarations": [WEATHER]}], "toolConfig": {"functionCallingConfig": {"mode": "NONE"}}, "generationConfig": {"maxOutputTokens": 32}}, "mode NONE") assert st == 200 and not any("functionCall" in p for p in parts(r)) def test_behavior_non_blocking_is_live_only(gemini): st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Paris?"}]}], "tools": [{"functionDeclarations": [dict(WEATHER, behavior="NON_BLOCKING")]}], "generationConfig": {"maxOutputTokens": 16}}, "NON_BLOCKING") assert st == 400 and "BidiGenerateContent" in r["error"]["message"] def test_code_execution_parts(gemini): st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Run print(2+2) in Python and report the output."}]}], "tools": [{"codeExecution": {}}], "generationConfig": {"maxOutputTokens": 128}}, "codeExecution") assert st == 200 kinds = [k for p in parts(r) for k in p if k != "thoughtSignature"] assert "executableCode" in kinds and "codeExecutionResult" in kinds res = next(p["codeExecutionResult"] for p in parts(r) if "codeExecutionResult" in p) assert res["outcome"] == "OUTCOME_OK" and "4" in res["output"] assert r["usageMetadata"].get("toolUsePromptTokenCount", 0) > 0 def test_url_context_metadata(gemini): st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Summarize https://example.com in 5 words."}]}], "tools": [{"urlContext": {}}], "generationConfig": {"maxOutputTokens": 48}}, "urlContext") assert st == 200 md = r["candidates"][0]["urlContextMetadata"]["urlMetadata"] assert md[0]["retrievedUrl"].startswith("https://example.com") and md[0]["urlRetrievalStatus"].startswith("URL_RETRIEVAL_STATUS_") def test_google_maps_grounding(gemini): st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Name one cafe near me. 5 words."}]}], "tools": [{"googleMaps": {}}], "toolConfig": {"retrievalConfig": {"latLng": {"latitude": 43.6532, "longitude": -79.3832}}}, "generationConfig": {"maxOutputTokens": 48}}, "googleMaps") assert st == 200 gm = r["candidates"][0].get("groundingMetadata") if not gm or not gm.get("groundingChunks"): pytest.skip("model answered without Maps grounding this time (observed intermittently on 2026-09-19)") assert "maps" in gm["groundingChunks"][0] and {"uri", "title", "placeId"} <= set(gm["groundingChunks"][0]["maps"]) # ----------------------------------------------------------------------------- expensive / restricted @pytest.mark.run_expensive_tests def test_google_search_grounding(gemini): st, r = _gen(gemini, {"contents": [{"parts": [{"text": "What is today's date in Toronto? 5 words."}]}], "tools": [{"googleSearch": {}}], "generationConfig": {"maxOutputTokens": 48}}, "googleSearch") assert st in (200, 429), r if st == 429: pytest.skip("ACCOUNT_RESTRICTED: Search grounding quota is 0 on this key (RESOURCE_EXHAUSTED, 2026-09-18)") gm = r["candidates"][0]["groundingMetadata"] assert "webSearchQueries" in gm and gm["groundingChunks"][0]["web"]["uri"] @pytest.mark.run_expensive_tests def test_legacy_computer_use_model(gemini): st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Do nothing."}]}], "tools": [{"computerUse": {"environment": "ENVIRONMENT_BROWSER"}}], "generationConfig": {"maxOutputTokens": 32}}, "computerUse legacy", model="gemini-2.5-computer-use-preview-10-2025") assert st in (200, 429), r if st == 429: pytest.skip("ACCOUNT_RESTRICTED: free-tier quota 0 for computer-use-preview (2026-09-18)")