// Probe 07: thought-signature strictness on function calling (Gemini 3.x). // 1) get a functionCall (records whether thoughtSignature is on the functionCall part) // 2) send functionResponse WITHOUT echoing the signature → record exact error (if any) // 3) send with the real signature → OK? 4) send with dummy "skip_thought_signature_validator" → OK? // 5) multi-turn text-only without echoing signature → OK? (signatures on text parts are optional per docs) import { ai, MODELS, SUFFIX, save, errInfo, shortText, withRetry, sleep, PACE_MS } from "./lib.js"; const model = MODELS[0]; const tool = { functionDeclarations: [{ name: "get_weather", description: "Get weather for a city.", parametersJsonSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } }] }; const results: any = { model }; const first = await withRetry(() => ai.models.generateContent({ model, contents: [{ role: "user", parts: [{ text: "Weather in Montreal? Use the tool." }] }], config: { tools: [tool], maxOutputTokens: 2000 } })); const parts: any[] = first.candidates?.[0]?.content?.parts ?? []; results.firstTurnParts = parts.map((p) => ({ keys: Object.keys(p), fc: p.functionCall, sigLen: p.thoughtSignature?.length })); console.log("first turn parts", JSON.stringify(results.firstTurnParts)); const fc = parts.find((p) => p.functionCall); if (!fc) { console.log("no function call; abort"); save(`07-signature${SUFFIX}.json`, results); process.exit(0); } async function roundTrip(label: string, modelParts: any[]) { await sleep(PACE_MS); const contents = [ { role: "user", parts: [{ text: "Weather in Montreal? Use the tool." }] }, { role: "model", parts: modelParts }, { role: "user", parts: [{ functionResponse: { name: fc.functionCall.name, id: fc.functionCall.id, response: { temperatureC: 21, condition: "sunny" } } }] }, ]; try { const r = await withRetry(() => ai.models.generateContent({ model, contents, config: { tools: [tool], maxOutputTokens: 2000 } })); results[label] = { ok: true, text: shortText(r), finishReason: r.candidates?.[0]?.finishReason }; } catch (e) { results[label] = { ok: false, error: errInfo(e) }; } console.log(label, JSON.stringify(results[label]).slice(0, 400)); } const stripSig = (p: any) => { const { thoughtSignature, ...rest } = p; return rest; }; await roundTrip("withSignature", parts); await roundTrip("withoutSignature", parts.map(stripSig)); await roundTrip("dummySignature", parts.map((p) => (p.functionCall ? { ...stripSig(p), thoughtSignature: "skip_thought_signature_validator" } : stripSig(p)))); // 5) text-only multi-turn without signatures await sleep(PACE_MS); try { const t1 = await withRetry(() => ai.models.generateContent({ model, contents: "Pick a random fruit and tell me only its name.", config: { maxOutputTokens: 1500 } })); const p1: any[] = (t1.candidates?.[0]?.content?.parts ?? []).map(stripSig); await sleep(PACE_MS); const t2 = await withRetry(() => ai.models.generateContent({ model, contents: [{ role: "user", parts: [{ text: "Pick a random fruit and tell me only its name." }] }, { role: "model", parts: p1 }, { role: "user", parts: [{ text: "What colour is it? One word." }] }], config: { maxOutputTokens: 1500 } })); results.textMultiTurnNoSig = { ok: true, fruit: shortText(t1), colour: shortText(t2), firstTurnHadSig: (t1.candidates?.[0]?.content?.parts ?? []).some((p: any) => p.thoughtSignature) }; } catch (e) { results.textMultiTurnNoSig = { ok: false, error: errInfo(e) }; } console.log("textMultiTurnNoSig", JSON.stringify(results.textMultiTurnNoSig)); save(`07-signature${SUFFIX}.json`, results);