From 8c0eaf17c31460d6244804a0cef25841ab1bb982 Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:08:19 +0200 Subject: [PATCH 1/3] Migrate Gemini core to Interactions API --- src/code.gs | 301 ++++++++++++++++++++++++++++--------------- src/testFunctions.gs | 64 +++++++++ 2 files changed, 261 insertions(+), 104 deletions(-) diff --git a/src/code.gs b/src/code.gs index adaa641..68b48be 100644 --- a/src/code.gs +++ b/src/code.gs @@ -63,6 +63,10 @@ const GenAIApp = (function () { let previous_response_id; let last_response_id = null; + let previous_interaction_id; + let last_gemini_interaction_id = null; + let pending_gemini_input = null; + let last_gemini_content_count = 0; let maxNumOfChunks = 10; let onlyChunks = false; @@ -366,6 +370,13 @@ const GenAIApp = (function () { return last_response_id; }; + /** + * Returns the last Gemini Interactions API interaction Id for this chat. + */ + this.retrieveLastInteractionId = function () { + return last_gemini_interaction_id; + }; + /** * Defines the input token threshold that should trigger a warning log. * @param {number} input_token_threshold - Input token threshold for warning. @@ -388,6 +399,16 @@ const GenAIApp = (function () { return this; }; + /** + * Sets the previous Gemini Interactions API interaction Id used to continue a conversation. + * @param {string} previousInteractionId - The id of the previous Gemini interaction. + */ + this.setPreviousInteractionId = function (previousInteractionId) { + previous_interaction_id = previousInteractionId; + last_gemini_interaction_id = previousInteractionId || last_gemini_interaction_id; + return this; + }; + /** * Enable or disable server-side context compaction for OpenAI Responses API requests. * @param {boolean} enabled - True to enable compaction. @@ -436,6 +457,7 @@ const GenAIApp = (function () { this._toJson = function () { return { messages: messages, + contents: contents, tools: tools, model: model, temperature: temperature, @@ -444,7 +466,8 @@ const GenAIApp = (function () { compaction_enabled: compaction_enabled, compaction_threshold: compaction_threshold, maximumAPICalls: maximumAPICalls, - numberOfAPICalls: numberOfAPICalls + numberOfAPICalls: numberOfAPICalls, + last_gemini_interaction_id: last_gemini_interaction_id }; }; @@ -528,17 +551,17 @@ const GenAIApp = (function () { if (geminiKey) { // Public endpoint / Generative Language API // https://console.cloud.google.com/apis/api/generativelanguage.googleapis.com - endpointUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`; + endpointUrl = `https://generativelanguage.googleapis.com/v1beta/interactions`; } else { // Enterprise endpoint / Vertex AI API // https://console.cloud.google.com/apis/api/aiplatform.googleapis.com // requires scope "https://www.googleapis.com/auth/cloud-platform.read-only" in access token if (!region || model.includes("gemini-3")) { // Gemini 3 requires global endpoint when using Vertex AI API - endpointUrl = `https://aiplatform.googleapis.com/v1/projects/${gcpProjectId}/locations/global/publishers/google/models/${model}:generateContent`; + endpointUrl = `https://aiplatform.googleapis.com/v1beta1/projects/${gcpProjectId}/locations/global/interactions`; } else { - endpointUrl = `https://${region}-aiplatform.googleapis.com/v1/projects/${gcpProjectId}/locations/${region}/publishers/google/models/${model}:generateContent`; + endpointUrl = `https://${region}-aiplatform.googleapis.com/v1beta1/projects/${gcpProjectId}/locations/${region}/interactions`; } } } @@ -562,10 +585,15 @@ const GenAIApp = (function () { this._lastGeneratedDriveFileUrl = createdFile.getUrl(); } - // OpenAI Responses API returns top-level "id" + // OpenAI Responses API and Gemini Interactions API return a top-level "id". if (!model.includes("gemini")) { last_response_id = responseMessage?.id ?? null; } + else { + last_gemini_interaction_id = responseMessage?.id ?? last_gemini_interaction_id; + previous_interaction_id = last_gemini_interaction_id || previous_interaction_id; + last_gemini_content_count = contents.length; + } numberOfAPICalls++; } else { @@ -597,32 +625,28 @@ const GenAIApp = (function () { if (tools.length > 0) { // Check if AI model wanted to call a function if (model.includes("gemini")) { - if (responseMessage?.parts?.some(p => p?.functionCall)) { - contents = _handleGeminiToolCalls(responseMessage, tools, contents); + const functionCalls = _extractGeminiFunctionCalls(responseMessage); + if (functionCalls.length > 0) { + const geminiToolResult = _handleGeminiToolCalls(responseMessage, tools, contents); + contents = geminiToolResult.contents; + pending_gemini_input = geminiToolResult.input; // check if endWithResults or onlyReturnArguments - if (contents[contents.length - 1].role == "model") { - if (contents[contents.length - 1].parts.text == "endWithResult") { - if (verbose) { - console.log("[GenAIApp] - Conversation stopped because end function has been called"); - } - // Do not return anything specific as the goal is simply to end here. - return "OK"; + if (geminiToolResult.endWithResult) { + if (verbose) { + console.log("[GenAIApp] - Conversation stopped because end function has been called"); } - else if (contents[contents.length - 1].parts.text == "onlyReturnArguments") { - if (verbose) { - console.log("[GenAIApp] - Conversation stopped because argument return has been enabled - No function has been called"); - } - // return the argument(s) of the last function called - return contents[contents.length - 2].parts - .find(p => p && p.functionCall) - ?.functionCall.args; + return "OK"; + } + else if (geminiToolResult.onlyReturnArguments) { + if (verbose) { + console.log("[GenAIApp] - Conversation stopped because argument return has been enabled - No function has been called"); } + return geminiToolResult.onlyReturnArguments; } } else { // if no function has been found, stop here - const part = responseMessage?.parts?.find(p => !p.thought && p.text); - return part?.text || null; + return _extractGeminiResponseText(responseMessage); } } else { @@ -666,8 +690,7 @@ const GenAIApp = (function () { } else { if (model.includes("gemini")) { - const part = responseMessage?.parts?.find(p => !p.thought && p.text); - return part?.text || null; + return _extractGeminiResponseText(responseMessage); } else { if (this._lastGeneratedDriveFileUrl) { @@ -923,31 +946,38 @@ const GenAIApp = (function () { */ this._buildGeminiPayload = function (advancedParametersObject) { const payload = { - 'contents': contents, - 'model': model, - 'generationConfig': { - maxOutputTokens: max_tokens, - temperature: temperature, - }, - 'tool_config': { - function_calling_config: { - mode: "AUTO" - }, - includeServerSideToolInvocations: tool_combination_enabled - }, + model: model, + input: pending_gemini_input || _geminiContentsToInteractionInput(previous_interaction_id ? contents.slice(last_gemini_content_count) : contents), + max_output_tokens: max_tokens, + temperature: temperature, tools: [] }; + pending_gemini_input = null; + + // Continue Gemini conversations using the Interactions API state handle instead of resending + // the full previous contents array. + if (previous_interaction_id) { + payload.previous_interaction_id = previous_interaction_id; + } + + if (tool_combination_enabled) { + payload.include_server_side_tool_invocations = true; + } if (advancedParametersObject?.function_call) { - payload.tool_config.function_calling_config.mode = "ANY"; - payload.tool_config.function_calling_config.allowed_function_names = advancedParametersObject.function_call; + payload.tool_choice = { + type: "function", + allowed_function_names: Array.isArray(advancedParametersObject.function_call) + ? advancedParametersObject.function_call + : [advancedParametersObject.function_call] + }; delete advancedParametersObject.function_call; } if (tools.length > 0) { // the user has added functions, enable function calling - const payloadTools = Object.keys(tools).map(t => { - const toolFunction = tools[t].function._toJson(); + payload.tools = tools.map(t => { + const toolFunction = t.function._toJson(); const parameters = toolFunction.parameters; if (parameters?.type) { @@ -955,23 +985,20 @@ const GenAIApp = (function () { } return { + type: "function", name: toolFunction.name, description: toolFunction.description, parameters: toolFunction.parameters }; }); - - payload.tools = [{ - functionDeclarations: payloadTools - }]; } if (browsing) { payload.tools.push({ - url_context: {} + type: "url_context" }); payload.tools.push({ - google_search: {} + type: "google_search" }); } @@ -1629,9 +1656,11 @@ const GenAIApp = (function () { // The request was successful, exit the loop. const parsedResponse = JSON.parse(response.getContentText()); if (endpoint.includes("google")) { - const firstCandidate = parsedResponse.candidates?.[0]; - responseMessage = firstCandidate?.content || null; - finish_reason = firstCandidate?.finishReason || null; + responseMessage = parsedResponse; + finish_reason = parsedResponse.status + || parsedResponse.finishReason + || parsedResponse.finish_reason + || (_extractGeminiFunctionCalls(parsedResponse).length > 0 ? "tool_calls" : "completed"); } else { responseMessage = parsedResponse; @@ -1706,47 +1735,119 @@ const GenAIApp = (function () { * @param {Array} contents - Array representing the conversational content, updated with each tool call and its result. * @returns {Array} - The updated contents array, representing the conversation flow with function calls and responses. */ + function _geminiContentsToInteractionInput(contents) { + return (contents || []).map(content => { + const parts = Array.isArray(content.parts) ? content.parts : [content.parts]; + const item = { + role: content.role === "model" ? "assistant" : content.role, + content: [] + }; + parts.forEach(part => { + if (!part) return; + if (part.text) { + item.content.push({ type: "input_text", text: part.text }); + } + else if (part.inline_data) { + item.content.push({ + type: "input_file", + mime_type: part.inline_data.mime_type, + data: part.inline_data.data + }); + } + }); + return item; + }).filter(item => item.content.length > 0); + } + + function _extractGeminiResponseText(responseMessage) { + const steps = responseMessage?.steps || []; + const texts = []; + steps.forEach(step => { + if (step?.type !== "model_output") return; + const content = Array.isArray(step.content) ? step.content : [step.content]; + content.forEach(item => { + if (!item) return; + if (typeof item === "string") texts.push(item); + else if (item.text) texts.push(item.text); + else if (item.type === "text" && item.content) texts.push(item.content); + else if (item.type === "output_text" && item.text) texts.push(item.text); + }); + if (step.text) texts.push(step.text); + if (step.output_text) texts.push(step.output_text); + }); + if (texts.length > 0) return texts.join("\n"); + + // Backward-compatible fallback for legacy content-shaped responses. + const parts = responseMessage?.parts || []; + const part = parts.find(p => !p.thought && p.text); + return part?.text || responseMessage?.output_text || null; + } + + function _extractGeminiFunctionCalls(responseMessage) { + const calls = []; + const steps = responseMessage?.steps || []; + steps.forEach(step => { + if (step?.type !== "function_call") return; + calls.push({ + id: step.id || step.call_id || step.function_call_id, + name: step.name || step.function?.name || step.functionCall?.name, + args: step.args || step.arguments || step.function?.arguments || step.functionCall?.args || {} + }); + }); + if (calls.length > 0) return calls; + + // Backward-compatible fallback for legacy content-shaped responses. + const parts = responseMessage?.parts || []; + parts.forEach(part => { + if (part?.functionCall?.name) { + calls.push({ + id: part.functionCall.id, + name: part.functionCall.name, + args: part.functionCall.args || {} + }); + } + }); + return calls; + } + + /** + * Processes tool calls from a Gemini Interactions API response message. + * + * @private + * @param {Object} responseMessage - The response message from Gemini containing tool-call steps. + * @param {Array} tools - List of available tools, each with metadata including function names and argument requirements. + * @param {Array} contents - Array representing the conversational content, updated for backward compatibility. + * @returns {Object} - Tool continuation input and state flags for the Chat run loop. + */ function _handleGeminiToolCalls(responseMessage, tools, contents) { - // Append function call to contents - // The thought signature is also sent back - // https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#thinking - // Note: thoughtSignature seems to be only included in Generative Language API, not Vertex AI API - contents.push(responseMessage); - - const parts = (responseMessage && responseMessage.parts) || []; - const responseParts = []; + const functionCalls = _extractGeminiFunctionCalls(responseMessage); + const functionResults = []; let shouldEndWithResult = false; + let onlyReturnArguments = null; - for (let i = 0; i < parts.length; i++) { - const part = parts[i] || {}; - if (!part.functionCall || !part.functionCall.name) continue; - - const functionName = part.functionCall.name; - const functionArgs = part.functionCall.args; + functionCalls.forEach(functionCall => { + const functionName = functionCall.name; + const functionArgs = functionCall.args || {}; + if (!functionName) return; let argsOrder = []; let endWithResult = false; - let onlyReturnArguments = false; + let onlyArgs = false; for (const t in tools) { const currentFunction = tools[t].function._toJson(); if (currentFunction.name == functionName) { - argsOrder = currentFunction.argumentsInRightOrder; // get the args in the right order + argsOrder = currentFunction.argumentsInRightOrder; endWithResult = currentFunction.endingFunction; - onlyReturnArguments = currentFunction.onlyArgs; + onlyArgs = currentFunction.onlyArgs; break; } } // No actual call to the function - if (onlyReturnArguments) { - contents.push({ - "role": "model", - "parts": { - text: "onlyReturnArguments" - } - }); - return contents; + if (onlyArgs) { + onlyReturnArguments = functionArgs; + return; } if (endWithResult) { @@ -1758,43 +1859,35 @@ const GenAIApp = (function () { console.log(`[GenAIApp] - function ${functionName}() called by Gemini.`); } if (typeof functionResponse != "string") { - if (typeof functionResponse == "object") { - functionResponse = JSON.stringify(functionResponse); - } - else { - functionResponse = String(functionResponse); - } + functionResponse = typeof functionResponse == "object" ? JSON.stringify(functionResponse) : String(functionResponse); } - // Append result of the function execution to contents - // https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#step-4 - responseParts.push({ - functionResponse: { - name: functionName, - response: { functionResponse } - } + functionResults.push({ + type: "function_result", + call_id: functionCall.id, + name: functionName, + result: functionResponse }); - } + }); - // Append all function results in a single turn - if (responseParts.length > 0) { + if (functionResults.length > 0) { contents.push({ role: 'user', - parts: responseParts - }); - } - - if (shouldEndWithResult) { - // User defined that if this function has been called, we do not call back the AI endpoint. - contents.push({ - "role": "model", - "parts": { - text: "endWithResult" - } + parts: functionResults.map(result => ({ + functionResponse: { + name: result.name, + response: { functionResponse: result.result } + } + })) }); } - return contents; + return { + contents: contents, + input: functionResults, + endWithResult: shouldEndWithResult, + onlyReturnArguments: onlyReturnArguments + }; } /** diff --git a/src/testFunctions.gs b/src/testFunctions.gs index fdb96a8..f5a95c1 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -15,6 +15,9 @@ function testAll() { testVision(); testMaximumAPICalls(); testInputTokenWarning(); + testGeminiInteractionThreading(); + testGeminiRetrieveLastInteractionId(); + testGeminiFunctionCallingInteractionContinuation(); // OpenAI-only tests - require valid Drive file IDs. if (TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) { testCodeInterpreterExcel(TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID); @@ -188,3 +191,64 @@ function testCodeInterpreterPDF(driveFileId) { function getWeather(cityName) { return `The weather in ${cityName} is 19°C today.`; } + +function testGeminiInteractionThreading() { + GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); + const chat = GenAIApp.newChat(); + chat.addMessage("Remember this keyword for the next turn: papaya."); + const firstResponse = chat.run({ model: GEMINI_MODEL, max_tokens: 500 }); + const interactionId = chat.retrieveLastInteractionId(); + if (!interactionId) { + throw new Error("Gemini interaction ID was not captured after the first response."); + } + console.log(`Gemini first interaction id: ${interactionId}`); + chat.addMessage("What keyword did I ask you to remember?"); + const secondResponse = chat.run({ model: GEMINI_MODEL, max_tokens: 500 }); + console.log(`Gemini threaded response:\n${secondResponse}`); +} + +function testGeminiRetrieveLastInteractionId() { + GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); + const chat = GenAIApp.newChat(); + chat.addMessage("Reply with one short sentence about Apps Script."); + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 300 }); + const interactionId = chat.retrieveLastInteractionId(); + if (typeof interactionId !== "string" || interactionId.length === 0) { + throw new Error("retrieveLastInteractionId() did not return a valid Gemini interaction ID."); + } + console.log(`Gemini retrieveLastInteractionId response:\n${response}`); + console.log(`Gemini retrieved interaction id: ${interactionId}`); +} + +function testGeminiFunctionCallingInteractionContinuation() { + GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); + const weatherFunction = GenAIApp.newFunction() + .setName("getWeather") + .setDescription("To retrieve the weather in a city in °C") + .addParameter("cityName", "string", "The name of the city."); + + const chat = GenAIApp.newChat(); + chat + .addMessage("What's the weather in Paris? Use the available function, then answer normally.") + .addFunction(weatherFunction); + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 1000 }); + const interactionId = chat.retrieveLastInteractionId(); + if (!interactionId) { + throw new Error("Gemini interaction ID was not captured after function-call continuation."); + } + console.log(`Gemini function continuation response:\n${response}`); + console.log(`Gemini function continuation interaction id: ${interactionId}`); +} + +function testGeminiVertexInteractionThreading() { + GenAIApp.setGeminiAuth(GCP_PROJECT_ID, REGION); + const chat = GenAIApp.newChat(); + chat.addMessage("Reply with the word vertex and a one-sentence explanation of interactions."); + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 500 }); + const interactionId = chat.retrieveLastInteractionId(); + if (!interactionId) { + throw new Error("Vertex Gemini interaction ID was not captured."); + } + console.log(`Vertex Gemini interaction response:\n${response}`); + console.log(`Vertex Gemini interaction id: ${interactionId}`); +} From 41ad4d1dbd2845c167c26e9f259c3e6451ebb520 Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:37:04 +0200 Subject: [PATCH 2/3] Fix Gemini Interactions generation config payload --- src/code.gs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/code.gs b/src/code.gs index 68b48be..d4032f9 100644 --- a/src/code.gs +++ b/src/code.gs @@ -568,9 +568,10 @@ const GenAIApp = (function () { responseMessage = _callGenAIApi(endpointUrl, payload); if (responseMessage?.usage) { this._lastUsage = responseMessage.usage; + const inputTokens = this._lastUsage?.input_tokens ?? this._lastUsage?.total_input_tokens; if (this._inputTokenWarningThreshold !== null - && this._lastUsage?.input_tokens > this._inputTokenWarningThreshold) { - console.warn(`[GenAIApp] - Warning: input token usage (${this._lastUsage.input_tokens}) exceeded configured threshold (${this._inputTokenWarningThreshold}) for response ${responseMessage.id}`); + && inputTokens > this._inputTokenWarningThreshold) { + console.warn(`[GenAIApp] - Warning: input token usage (${inputTokens}) exceeded configured threshold (${this._inputTokenWarningThreshold}) for response ${responseMessage.id}`); } } this._generatedFiles = this._extractContainerFileCitations(responseMessage); @@ -948,8 +949,10 @@ const GenAIApp = (function () { const payload = { model: model, input: pending_gemini_input || _geminiContentsToInteractionInput(previous_interaction_id ? contents.slice(last_gemini_content_count) : contents), - max_output_tokens: max_tokens, - temperature: temperature, + generation_config: { + max_output_tokens: max_tokens, + temperature: temperature + }, tools: [] }; pending_gemini_input = null; @@ -966,10 +969,12 @@ const GenAIApp = (function () { if (advancedParametersObject?.function_call) { payload.tool_choice = { - type: "function", - allowed_function_names: Array.isArray(advancedParametersObject.function_call) - ? advancedParametersObject.function_call - : [advancedParametersObject.function_call] + allowed_tools: { + mode: "any", + tools: Array.isArray(advancedParametersObject.function_call) + ? advancedParametersObject.function_call + : [advancedParametersObject.function_call] + } }; delete advancedParametersObject.function_call; } @@ -1745,11 +1750,11 @@ const GenAIApp = (function () { parts.forEach(part => { if (!part) return; if (part.text) { - item.content.push({ type: "input_text", text: part.text }); + item.content.push({ type: "text", text: part.text }); } else if (part.inline_data) { item.content.push({ - type: "input_file", + type: part.inline_data.mime_type?.startsWith("image/") ? "image" : "document", mime_type: part.inline_data.mime_type, data: part.inline_data.data }); From 536656108eb896329c480ca7b0764ae4c2d36d4f Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:13:29 +0200 Subject: [PATCH 3/3] Use Gemini step-list input and selectable tests --- src/code.gs | 19 +++++++++------- src/testFunctions.gs | 53 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/code.gs b/src/code.gs index d4032f9..4c09519 100644 --- a/src/code.gs +++ b/src/code.gs @@ -1743,25 +1743,25 @@ const GenAIApp = (function () { function _geminiContentsToInteractionInput(contents) { return (contents || []).map(content => { const parts = Array.isArray(content.parts) ? content.parts : [content.parts]; - const item = { - role: content.role === "model" ? "assistant" : content.role, + const step = { + type: content.role === "model" ? "model_output" : "user_input", content: [] }; parts.forEach(part => { if (!part) return; if (part.text) { - item.content.push({ type: "text", text: part.text }); + step.content.push({ type: "text", text: part.text }); } else if (part.inline_data) { - item.content.push({ + step.content.push({ type: part.inline_data.mime_type?.startsWith("image/") ? "image" : "document", mime_type: part.inline_data.mime_type, data: part.inline_data.data }); } }); - return item; - }).filter(item => item.content.length > 0); + return step; + }).filter(step => step.content.length > 0); } function _extractGeminiResponseText(responseMessage) { @@ -1871,7 +1871,10 @@ const GenAIApp = (function () { type: "function_result", call_id: functionCall.id, name: functionName, - result: functionResponse + result: [{ + type: "text", + text: functionResponse + }] }); }); @@ -1881,7 +1884,7 @@ const GenAIApp = (function () { parts: functionResults.map(result => ({ functionResponse: { name: result.name, - response: { functionResponse: result.result } + response: { functionResponse: result.result?.[0]?.text ?? result.result } } })) }); diff --git a/src/testFunctions.gs b/src/testFunctions.gs index f5a95c1..26a7d9d 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -1,8 +1,37 @@ const GPT_MODEL = "gpt-5.4"; const REASONING_MODEL = "o4-mini"; -const GEMINI_MODEL = "gemini-2.5-pro"; +const GEMINI_MODEL = "gemini-3.5-flash"; const TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID = ""; const TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID = ""; +let TEST_MODEL_TARGETS = ["gpt", "thinking", "gemini"]; + +/** + * Restrict cross-model tests to one or more model families: "gpt", "thinking", or "gemini". + * @param {string|string[]} targets - A model family label or list of labels. + */ +function setTestModelTargets(targets) { + TEST_MODEL_TARGETS = (Array.isArray(targets) ? targets : [targets]) + .map(target => String(target).toLowerCase()); +} + +function testAllGpt() { + setTestModelTargets("gpt"); + testAll(); +} + +function testAllThinking() { + setTestModelTargets("thinking"); + testAll(); +} + +function testAllGemini() { + setTestModelTargets("gemini"); + testAll(); +} + +function _shouldRunModelLabel(label) { + return TEST_MODEL_TARGETS.indexOf(String(label).toLowerCase()) !== -1; +} // Run all tests function testAll() { @@ -15,14 +44,16 @@ function testAll() { testVision(); testMaximumAPICalls(); testInputTokenWarning(); - testGeminiInteractionThreading(); - testGeminiRetrieveLastInteractionId(); - testGeminiFunctionCallingInteractionContinuation(); + if (_shouldRunModelLabel("gemini")) { + testGeminiInteractionThreading(); + testGeminiRetrieveLastInteractionId(); + testGeminiFunctionCallingInteractionContinuation(); + } // OpenAI-only tests - require valid Drive file IDs. - if (TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) { + if (_shouldRunModelLabel("gpt") && TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) { testCodeInterpreterExcel(TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID); } - if (TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID) { + if (_shouldRunModelLabel("gpt") && TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID) { testCodeInterpreterPDF(TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID); } } @@ -35,10 +66,10 @@ function runTestAcrossModels(testName, setupFunction, runOptions = {}) { GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); const models = [ - { name: GPT_MODEL, label: "GPT" }, - { name: REASONING_MODEL, label: "reasoning" }, + { name: GPT_MODEL, label: "gpt" }, + { name: REASONING_MODEL, label: "thinking" }, { name: GEMINI_MODEL, label: "gemini" } - ]; + ].filter(model => _shouldRunModelLabel(model.label)); models.forEach(model => { const chat = GenAIApp.newChat(); @@ -138,6 +169,10 @@ function testMaximumAPICalls() { function testInputTokenWarning() { + if (!_shouldRunModelLabel("gpt")) { + console.log("Input token warning test skipped because GPT tests are disabled."); + return; + } GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); // Case 1: low threshold should log warning (manual log inspection).