SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
8.0 KB · 119 lines python
Raw Blame History
1"""Smoke tests for Gemini tools in generateContent (API Atlas).23Cheap tests (always run, < $0.002 total on gemini-3.5-flash-lite): forced function call (mode ANY) + functionResponse round trip,4thoughtSignature is mandatory on echo (400), parallel calls, parametersJsonSchema, mode NONE, behavior NON_BLOCKING rejected outside5Live (400), code execution parts, URL context metadata, Google Maps grounding chunks.6Expensive / restricted (RUN_EXPENSIVE_TESTS=true): Google Search grounding (429 RESOURCE_EXHAUSTED on this key on 2026-09-18 →7ACCOUNT_RESTRICTED; the test accepts 200 or 429), legacy computerUse on gemini-2.5-computer-use-preview-10-2025 (429 free-tier quota 0).8Reference run: 2026-09-18 (tmp-live/gemini-tools/*.json).9"""10from __future__ import annotations1112import pytest1314MODEL = "gemini-3.5-flash-lite"15WEATHER = {"name": "get_weather", "description": "Get the current weather for a city.",16           "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}}17TIME = {"name": "get_time", "description": "Get the current local time in a city.",18        "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}}192021def _gen(gemini, body, note="", model=MODEL):22    st, r, _ = gemini("POST", f"/v1beta/models/{model}:generateContent", body, note=f"test_tools {note}")23    return st, r242526def parts(r):27    return r["candidates"][0]["content"]["parts"]282930# ----------------------------------------------------------------------------- cheap31def test_forced_function_call_then_response(gemini):32    user = {"role": "user", "parts": [{"text": "Weather in Paris?"}]}33    st, r = _gen(gemini, {"contents": [user], "tools": [{"functionDeclarations": [WEATHER]}],34                          "toolConfig": {"functionCallingConfig": {"mode": "ANY", "allowedFunctionNames": ["get_weather"]}}, "generationConfig": {"maxOutputTokens": 64}}, "forced")35    assert st == 20036    fc = [p for p in parts(r) if "functionCall" in p]37    assert len(fc) == 1 and fc[0]["functionCall"]["name"] == "get_weather" and "city" in fc[0]["functionCall"]["args"]38    assert fc[0]["functionCall"]["id"].startswith("call_") and fc[0].get("thoughtSignature")39    model_turn = r["candidates"][0]["content"]40    st2, r2 = _gen(gemini, {"contents": [user, model_turn, {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"temperature_c": 18}}}]}],41                            "tools": [{"functionDeclarations": [WEATHER]}], "generationConfig": {"maxOutputTokens": 32}}, "functionResponse")42    assert st2 == 200 and any("text" in p for p in parts(r2))43    # dropping the thoughtSignature is rejected44    stripped = {"role": "model", "parts": [{k: v for k, v in p.items() if k != "thoughtSignature"} for p in model_turn["parts"]]}45    st3, r3 = _gen(gemini, {"contents": [user, stripped, {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"temperature_c": 18}}}]}],46                            "tools": [{"functionDeclarations": [WEATHER]}], "generationConfig": {"maxOutputTokens": 32}}, "no thoughtSignature")47    assert st3 == 400 and "thought_signature" in r3["error"]["message"]484950def test_parallel_function_calls(gemini):51    st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "What is the weather in Paris and the time in Tokyo? Use both tools."}]}],52                          "tools": [{"functionDeclarations": [WEATHER, TIME]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 128}}, "parallel")53    assert st == 20054    names = sorted(p["functionCall"]["name"] for p in parts(r) if "functionCall" in p)55    assert names == ["get_time", "get_weather"]565758def test_parameters_json_schema_and_mode_none(gemini):59    decl = {"name": "get_weather", "parametersJsonSchema": {"type": "object", "properties": {"city": {"type": "string"}, "unit": {"type": "string", "enum": ["C", "F"]}}, "required": ["city"]}}60    st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Rome in Celsius?"}]}], "tools": [{"functionDeclarations": [decl]}],61                          "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 64}}, "parametersJsonSchema")62    assert st == 200 and any(p.get("functionCall", {}).get("args", {}).get("city") for p in parts(r))63    st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Paris? 3 words."}]}], "tools": [{"functionDeclarations": [WEATHER]}],64                          "toolConfig": {"functionCallingConfig": {"mode": "NONE"}}, "generationConfig": {"maxOutputTokens": 32}}, "mode NONE")65    assert st == 200 and not any("functionCall" in p for p in parts(r))666768def test_behavior_non_blocking_is_live_only(gemini):69    st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Weather in Paris?"}]}], "tools": [{"functionDeclarations": [dict(WEATHER, behavior="NON_BLOCKING")]}],70                          "generationConfig": {"maxOutputTokens": 16}}, "NON_BLOCKING")71    assert st == 400 and "BidiGenerateContent" in r["error"]["message"]727374def test_code_execution_parts(gemini):75    st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Run print(2+2) in Python and report the output."}]}], "tools": [{"codeExecution": {}}], "generationConfig": {"maxOutputTokens": 128}}, "codeExecution")76    assert st == 20077    kinds = [k for p in parts(r) for k in p if k != "thoughtSignature"]78    assert "executableCode" in kinds and "codeExecutionResult" in kinds79    res = next(p["codeExecutionResult"] for p in parts(r) if "codeExecutionResult" in p)80    assert res["outcome"] == "OUTCOME_OK" and "4" in res["output"]81    assert r["usageMetadata"].get("toolUsePromptTokenCount", 0) > 0828384def test_url_context_metadata(gemini):85    st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Summarize https://example.com in 5 words."}]}], "tools": [{"urlContext": {}}], "generationConfig": {"maxOutputTokens": 48}}, "urlContext")86    assert st == 20087    md = r["candidates"][0]["urlContextMetadata"]["urlMetadata"]88    assert md[0]["retrievedUrl"].startswith("https://example.com") and md[0]["urlRetrievalStatus"].startswith("URL_RETRIEVAL_STATUS_")899091def test_google_maps_grounding(gemini):92    st, r = _gen(gemini, {"contents": [{"parts": [{"text": "Name one cafe near me. 5 words."}]}], "tools": [{"googleMaps": {}}],93                          "toolConfig": {"retrievalConfig": {"latLng": {"latitude": 43.6532, "longitude": -79.3832}}}, "generationConfig": {"maxOutputTokens": 48}}, "googleMaps")94    assert st == 20095    gm = r["candidates"][0].get("groundingMetadata")96    if not gm or not gm.get("groundingChunks"):97        pytest.skip("model answered without Maps grounding this time (observed intermittently on 2026-09-19)")98    assert "maps" in gm["groundingChunks"][0] and {"uri", "title", "placeId"} <= set(gm["groundingChunks"][0]["maps"])99100101# ----------------------------------------------------------------------------- expensive / restricted102@pytest.mark.run_expensive_tests103def test_google_search_grounding(gemini):104    st, r = _gen(gemini, {"contents": [{"parts": [{"text": "What is today's date in Toronto? 5 words."}]}], "tools": [{"googleSearch": {}}], "generationConfig": {"maxOutputTokens": 48}}, "googleSearch")105    assert st in (200, 429), r106    if st == 429:107        pytest.skip("ACCOUNT_RESTRICTED: Search grounding quota is 0 on this key (RESOURCE_EXHAUSTED, 2026-09-18)")108    gm = r["candidates"][0]["groundingMetadata"]109    assert "webSearchQueries" in gm and gm["groundingChunks"][0]["web"]["uri"]110111112@pytest.mark.run_expensive_tests113def test_legacy_computer_use_model(gemini):114    st, r = _gen(gemini, {"contents": [{"role": "user", "parts": [{"text": "Do nothing."}]}], "tools": [{"computerUse": {"environment": "ENVIRONMENT_BROWSER"}}],115                          "generationConfig": {"maxOutputTokens": 32}}, "computerUse legacy", model="gemini-2.5-computer-use-preview-10-2025")116    assert st in (200, 429), r117    if st == 429:118        pytest.skip("ACCOUNT_RESTRICTED: free-tier quota 0 for computer-use-preview (2026-09-18)")119