From 0e955da17108acc339325180269ac6157f33b3e8 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:11:06 -0500 Subject: [PATCH] feat(cli): add /chat debug command for nightly builds (#16339) --- .../src/services/BuiltinCommandLoader.test.ts | 39 +++- .../cli/src/services/BuiltinCommandLoader.ts | 14 +- .../cli/src/ui/commands/chatCommand.test.ts | 63 ++++++- packages/cli/src/ui/commands/chatCommand.ts | 41 ++++ packages/core/src/config/config.ts | 10 + .../src/core/loggingContentGenerator.test.ts | 48 +++++ .../core/src/core/loggingContentGenerator.ts | 7 + packages/core/src/index.ts | 2 + .../core/src/utils/apiConversionUtils.test.ts | 177 ++++++++++++++++++ packages/core/src/utils/apiConversionUtils.ts | 57 ++++++ scripts/send_gemini_request.sh | 102 ++++++++++ 11 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/apiConversionUtils.test.ts create mode 100644 packages/core/src/utils/apiConversionUtils.ts create mode 100755 scripts/send_gemini_request.sh diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 545168e88d..44ddaeb039 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -53,16 +53,29 @@ vi.mock('../ui/commands/permissionsCommand.js', async () => { import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { BuiltinCommandLoader } from './BuiltinCommandLoader.js'; import type { Config } from '@google/gemini-cli-core'; +import { isNightly } from '@google/gemini-cli-core'; import { CommandKind } from '../ui/commands/types.js'; import { restoreCommand } from '../ui/commands/restoreCommand.js'; +vi.mock('@google/gemini-cli-core', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isNightly: vi.fn().mockResolvedValue(false), + }; +}); + vi.mock('../ui/commands/authCommand.js', () => ({ authCommand: {} })); vi.mock('../ui/commands/agentsCommand.js', () => ({ agentsCommand: { name: 'agents' }, })); vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} })); -vi.mock('../ui/commands/chatCommand.js', () => ({ chatCommand: {} })); +vi.mock('../ui/commands/chatCommand.js', () => ({ + chatCommand: { name: 'chat', subCommands: [] }, + debugCommand: { name: 'debug' }, +})); vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} })); vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} })); vi.mock('../ui/commands/corgiCommand.js', () => ({ corgiCommand: {} })); @@ -209,6 +222,30 @@ describe('BuiltinCommandLoader', () => { const agentsCmd = commands.find((c) => c.name === 'agents'); expect(agentsCmd).toBeUndefined(); }); + + describe('chat debug command', () => { + it('should NOT add debug subcommand to chatCommand if not a nightly build', async () => { + vi.mocked(isNightly).mockResolvedValue(false); + const loader = new BuiltinCommandLoader(mockConfig); + const commands = await loader.loadCommands(new AbortController().signal); + + const chatCmd = commands.find((c) => c.name === 'chat'); + expect(chatCmd?.subCommands).toBeDefined(); + const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug'); + expect(hasDebug).toBe(false); + }); + + it('should add debug subcommand to chatCommand if it is a nightly build', async () => { + vi.mocked(isNightly).mockResolvedValue(true); + const loader = new BuiltinCommandLoader(mockConfig); + const commands = await loader.loadCommands(new AbortController().signal); + + const chatCmd = commands.find((c) => c.name === 'chat'); + expect(chatCmd?.subCommands).toBeDefined(); + const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug'); + expect(hasDebug).toBe(true); + }); + }); }); describe('BuiltinCommandLoader profile', () => { diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 5193b5fe9c..263a17fd3a 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -12,12 +12,12 @@ import { type CommandContext, } from '../ui/commands/types.js'; import type { MessageActionReturn, Config } from '@google/gemini-cli-core'; -import { startupProfiler } from '@google/gemini-cli-core'; +import { isNightly, startupProfiler } from '@google/gemini-cli-core'; import { aboutCommand } from '../ui/commands/aboutCommand.js'; import { agentsCommand } from '../ui/commands/agentsCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; import { bugCommand } from '../ui/commands/bugCommand.js'; -import { chatCommand } from '../ui/commands/chatCommand.js'; +import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js'; import { copyCommand } from '../ui/commands/copyCommand.js'; @@ -65,12 +65,20 @@ export class BuiltinCommandLoader implements ICommandLoader { */ async loadCommands(_signal: AbortSignal): Promise { const handle = startupProfiler.start('load_builtin_commands'); + + const isNightlyBuild = await isNightly(process.cwd()); + const allDefinitions: Array = [ aboutCommand, ...(this.config?.isAgentsEnabled() ? [agentsCommand] : []), authCommand, bugCommand, - chatCommand, + { + ...chatCommand, + subCommands: isNightlyBuild + ? [...(chatCommand.subCommands || []), debugCommand] + : chatCommand.subCommands, + }, clearCommand, compressCommand, copyCommand, diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts index 20d0be1e06..6edae787d2 100644 --- a/packages/cli/src/ui/commands/chatCommand.test.ts +++ b/packages/cli/src/ui/commands/chatCommand.test.ts @@ -12,7 +12,7 @@ import type { Content } from '@google/genai'; import { AuthType, type GeminiClient } from '@google/gemini-cli-core'; import * as fsPromises from 'node:fs/promises'; -import { chatCommand } from './chatCommand.js'; +import { chatCommand, debugCommand } from './chatCommand.js'; import { serializeHistoryToMarkdown, exportHistoryToFile, @@ -693,5 +693,66 @@ Hi there!`; const result = serializeHistoryToMarkdown(history as Content[]); expect(result).toBe(expectedMarkdown); }); + describe('debug subcommand', () => { + let mockGetLatestApiRequest: ReturnType; + + beforeEach(() => { + mockGetLatestApiRequest = vi.fn(); + mockContext.services.config!.getLatestApiRequest = + mockGetLatestApiRequest; + vi.spyOn(process, 'cwd').mockReturnValue('/project/root'); + vi.spyOn(Date, 'now').mockReturnValue(1234567890); + mockFs.writeFile.mockClear(); + }); + + it('should return an error if no API request is found', async () => { + mockGetLatestApiRequest.mockReturnValue(undefined); + + const result = await debugCommand.action?.(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'No recent API request found to export.', + }); + expect(mockFs.writeFile).not.toHaveBeenCalled(); + }); + + it('should convert and write the API request to a json file', async () => { + const mockRequest = { + contents: [{ role: 'user', parts: [{ text: 'test' }] }], + }; + mockGetLatestApiRequest.mockReturnValue(mockRequest); + + const result = await debugCommand.action?.(mockContext, ''); + + const expectedFilename = 'gcli-request-1234567890.json'; + const expectedPath = path.join('/project/root', expectedFilename); + + expect(mockFs.writeFile).toHaveBeenCalledWith( + expectedPath, + expect.stringContaining('"role": "user"'), + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: `Debug API request saved to ${expectedFilename}`, + }); + }); + + it('should handle errors during file write', async () => { + const mockRequest = { contents: [] }; + mockGetLatestApiRequest.mockReturnValue(mockRequest); + mockFs.writeFile.mockRejectedValue(new Error('Write failed')); + + const result = await debugCommand.action?.(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Error saving debug request: Write failed', + }); + }); + }); }); }); diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 7c9b632b1a..89a770e1f8 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -27,6 +27,7 @@ import type { } from '../types.js'; import { MessageType } from '../types.js'; import { exportHistoryToFile } from '../utils/historyExportUtils.js'; +import { convertToRestPayload } from '@google/gemini-cli-core'; const getSavedChatTags = async ( context: CommandContext, @@ -334,6 +335,46 @@ const shareCommand: SlashCommand = { }, }; +export const debugCommand: SlashCommand = { + name: 'debug', + description: 'Export the most recent API request as a JSON payload', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: async (context): Promise => { + const req = context.services.config?.getLatestApiRequest(); + if (!req) { + return { + type: 'message', + messageType: 'error', + content: 'No recent API request found to export.', + }; + } + + const restPayload = convertToRestPayload(req); + const filename = `gcli-request-${Date.now()}.json`; + const filePath = path.join(process.cwd(), filename); + + try { + await fsPromises.writeFile( + filePath, + JSON.stringify(restPayload, null, 2), + ); + return { + type: 'message', + messageType: 'info', + content: `Debug API request saved to ${filename}`, + }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + return { + type: 'message', + messageType: 'error', + content: `Error saving debug request: ${errorMessage}`, + }; + } + }, +}; + export const chatCommand: SlashCommand = { name: 'chat', description: 'Manage conversation history', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 6fb07754ec..60783cffab 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -73,6 +73,7 @@ import type { ModelConfigServiceConfig } from '../services/modelConfigService.js import { ModelConfigService } from '../services/modelConfigService.js'; import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js'; import { ContextManager } from '../services/contextManager.js'; +import type { GenerateContentParameters } from '@google/genai'; // Re-export OAuth config type export type { MCPOAuthConfig, AnyToolInvocation }; @@ -499,6 +500,7 @@ export class Config { private contextManager?: ContextManager; private terminalBackground: string | undefined = undefined; private remoteAdminSettings: GeminiCodeAssistSetting | undefined; + private latestApiRequest: GenerateContentParameters | undefined; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -897,6 +899,14 @@ export class Config { return this.terminalBackground; } + getLatestApiRequest(): GenerateContentParameters | undefined { + return this.latestApiRequest; + } + + setLatestApiRequest(req: GenerateContentParameters): void { + this.latestApiRequest = req; + } + getRemoteAdminSettings(): GeminiCodeAssistSetting | undefined { return this.remoteAdminSettings; } diff --git a/packages/core/src/core/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator.test.ts index e591f86be9..92286d207c 100644 --- a/packages/core/src/core/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator.test.ts @@ -218,6 +218,54 @@ describe('LoggingContentGenerator', () => { const errorEvent = vi.mocked(logApiError).mock.calls[0][1]; expect(errorEvent.duration_ms).toBe(1000); }); + + it('should set latest API request in config for main agent requests', async () => { + const req = { + contents: [{ role: 'user', parts: [{ text: 'hello' }] }], + model: 'gemini-pro', + }; + // Main agent prompt IDs end with exactly 8 hashes and a turn counter + const mainAgentPromptId = 'session-uuid########1'; + config.setLatestApiRequest = vi.fn(); + + async function* createAsyncGenerator() { + yield { candidates: [] } as unknown as GenerateContentResponse; + } + vi.mocked(wrapped.generateContentStream).mockResolvedValue( + createAsyncGenerator(), + ); + + await loggingContentGenerator.generateContentStream( + req, + mainAgentPromptId, + ); + + expect(config.setLatestApiRequest).toHaveBeenCalledWith(req); + }); + + it('should NOT set latest API request in config for sub-agent requests', async () => { + const req = { + contents: [{ role: 'user', parts: [{ text: 'hello' }] }], + model: 'gemini-pro', + }; + // Sub-agent prompt IDs contain fewer hashes, typically separating the agent name and ID + const subAgentPromptId = 'codebase_investigator#12345'; + config.setLatestApiRequest = vi.fn(); + + async function* createAsyncGenerator() { + yield { candidates: [] } as unknown as GenerateContentResponse; + } + vi.mocked(wrapped.generateContentStream).mockResolvedValue( + createAsyncGenerator(), + ); + + await loggingContentGenerator.generateContentStream( + req, + subAgentPromptId, + ); + + expect(config.setLatestApiRequest).not.toHaveBeenCalled(); + }); }); describe('getWrapped', () => { diff --git a/packages/core/src/core/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator.ts index e559846366..cc5ab05890 100644 --- a/packages/core/src/core/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator.ts @@ -258,6 +258,13 @@ export class LoggingContentGenerator implements ContentGenerator { req, 'generateContentStream', ); + + // For debugging: Capture the latest main agent request payload. + // Main agent prompt IDs end with exactly 8 hashes and a turn counter (e.g. "...########1") + if (/########\d+$/.test(userPromptId)) { + this.config.setLatestApiRequest(req); + } + this.logApiRequest( toContents(req.contents), req.model, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 75acd00143..ce62f9fcfa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -89,6 +89,8 @@ export * from './utils/extensionLoader.js'; export * from './utils/package.js'; export * from './utils/version.js'; export * from './utils/checkpointUtils.js'; +export * from './utils/apiConversionUtils.js'; +export * from './utils/channel.js'; // Export services export * from './services/fileDiscoveryService.js'; diff --git a/packages/core/src/utils/apiConversionUtils.test.ts b/packages/core/src/utils/apiConversionUtils.test.ts new file mode 100644 index 0000000000..615bcb1de8 --- /dev/null +++ b/packages/core/src/utils/apiConversionUtils.test.ts @@ -0,0 +1,177 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { convertToRestPayload } from './apiConversionUtils.js'; +import type { GenerateContentParameters } from '@google/genai'; +import { + FunctionCallingConfigMode, + HarmCategory, + HarmBlockThreshold, +} from '@google/genai'; + +describe('apiConversionUtils', () => { + describe('convertToRestPayload', () => { + it('handles minimal requests with no config', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + }; + + const result = convertToRestPayload(req); + + expect(result).toStrictEqual({ + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + }); + expect(result['generationConfig']).toBeUndefined(); + }); + + it('normalizes string systemInstruction to REST format', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + config: { + systemInstruction: 'You are a helpful assistant.', + }, + }; + + const result = convertToRestPayload(req); + + expect(result['systemInstruction']).toStrictEqual({ + parts: [{ text: 'You are a helpful assistant.' }], + }); + expect(result['generationConfig']).toBeUndefined(); + }); + + it('preserves object-based systemInstruction', () => { + const sysInstruction = { parts: [{ text: 'Object instruction' }] }; + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + config: { + systemInstruction: sysInstruction, + }, + }; + + const result = convertToRestPayload(req); + + expect(result['systemInstruction']).toStrictEqual(sysInstruction); + }); + + it('hoists capabilities (tools, safety, cachedContent) to the root level', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + config: { + tools: [{ functionDeclarations: [{ name: 'myTool' }] }], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingConfigMode.ANY }, + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + ], + cachedContent: 'cached-content-id', + }, + }; + + const result = convertToRestPayload(req); + + expect(result['tools']).toBeDefined(); + expect(result['toolConfig']).toBeDefined(); + expect(result['safetySettings']).toBeDefined(); + expect(result['cachedContent']).toBe('cached-content-id'); + // generationConfig should be omitted since no pure hyperparameters were passed + expect(result['generationConfig']).toBeUndefined(); + }); + + it('retains pure hyperparameters in generationConfig', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + config: { + temperature: 0.7, + topP: 0.9, + maxOutputTokens: 100, + }, + }; + + const result = convertToRestPayload(req); + + expect(result['generationConfig']).toStrictEqual({ + temperature: 0.7, + topP: 0.9, + maxOutputTokens: 100, + }); + }); + + it('strips JS-specific abortSignal from the final payload', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + config: { + temperature: 0.5, + abortSignal: new AbortController().signal, + }, + }; + + const result = convertToRestPayload(req); + + expect(result['generationConfig']).toStrictEqual({ + temperature: 0.5, + }); + expect(result['abortSignal']).toBeUndefined(); + // @ts-expect-error Checking that the key doesn't exist inside generationConfig + expect(result['generationConfig']?.abortSignal).toBeUndefined(); + }); + + it('handles a complex kitchen-sink request correctly', () => { + const req: GenerateContentParameters = { + model: 'gemini-3-flash', + contents: [{ role: 'user', parts: [{ text: 'Kitchen sink' }] }], + config: { + systemInstruction: 'Be witty.', + temperature: 0.8, + tools: [{ functionDeclarations: [{ name: 'test' }] }], + abortSignal: new AbortController().signal, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + }, + ], + topK: 40, + }, + }; + + const result = convertToRestPayload(req); + + // Root level checks + expect(result['contents']).toBeDefined(); + expect(result['systemInstruction']).toStrictEqual({ + parts: [{ text: 'Be witty.' }], + }); + expect(result['tools']).toStrictEqual([ + { functionDeclarations: [{ name: 'test' }] }, + ]); + expect(result['safetySettings']).toStrictEqual([ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + }, + ]); + expect(result['abortSignal']).toBeUndefined(); + + // Generation config checks + expect(result['generationConfig']).toStrictEqual({ + temperature: 0.8, + topK: 40, + }); + }); + }); +}); diff --git a/packages/core/src/utils/apiConversionUtils.ts b/packages/core/src/utils/apiConversionUtils.ts new file mode 100644 index 0000000000..2e22a3a3ed --- /dev/null +++ b/packages/core/src/utils/apiConversionUtils.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { GenerateContentParameters } from '@google/genai'; + +/** + * Transforms a standard SDK GenerateContentParameters object into the + * equivalent REST API payload format. This is primarily used for debugging + * and exporting requests. + */ +export function convertToRestPayload( + req: GenerateContentParameters, +): Record { + // Extract top-level REST fields from the SDK config object. + // 'pureGenerationConfig' will capture any remaining hyperparameters (e.g., temperature, topP). + const { + systemInstruction: sdkSystemInstruction, + tools: sdkTools, + toolConfig: sdkToolConfig, + safetySettings: sdkSafetySettings, + cachedContent: sdkCachedContent, + abortSignal: _sdkAbortSignal, // Exclude JS-specific abort controller + ...pureGenerationConfig + } = req.config || {}; + + // Normalize systemInstruction to the expected REST Content format. + let restSystemInstruction; + if (typeof sdkSystemInstruction === 'string') { + restSystemInstruction = { + parts: [{ text: sdkSystemInstruction }], + }; + } else if (sdkSystemInstruction !== undefined) { + restSystemInstruction = sdkSystemInstruction; + } + + const restPayload: Record = { + contents: req.contents, + }; + + // Only include generationConfig if actual hyperparameters exist. + if (Object.keys(pureGenerationConfig).length > 0) { + restPayload['generationConfig'] = pureGenerationConfig; + } + + // Assign extracted capabilities to the root level. + if (restSystemInstruction) + restPayload['systemInstruction'] = restSystemInstruction; + if (sdkTools) restPayload['tools'] = sdkTools; + if (sdkToolConfig) restPayload['toolConfig'] = sdkToolConfig; + if (sdkSafetySettings) restPayload['safetySettings'] = sdkSafetySettings; + if (sdkCachedContent) restPayload['cachedContent'] = sdkCachedContent; + + return restPayload; +} diff --git a/scripts/send_gemini_request.sh b/scripts/send_gemini_request.sh new file mode 100755 index 0000000000..18cedfa5bf --- /dev/null +++ b/scripts/send_gemini_request.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Gemini API Replay Script +# ----------------------------------------------------------------------------- +# Purpose: +# This script is used to replay a Gemini API request using a raw JSON payload. +# It is particularly useful for debugging the exact requests made by the +# Gemini CLI. +# +# Prerequisites: +# 1. Export your Gemini API key: +# export GEMINI_API_KEY="your_api_key_here" +# +# 2. Generate a request payload from the Gemini CLI: +# Inside the CLI, run the `/chat debug` command. This will save the most +# recent API request to a file named `gcli-request-.json`. +# +# Usage: +# ./scripts/send_gemini_request.sh --payload --model [--stream] +# +# Options: +# --payload Path to the JSON request payload. +# --model The Gemini model ID (e.g., gemini-3-flash-preview). +# --stream (Optional) Use the streaming API endpoint. Defaults to non-streaming. +# +# Example: +# ./scripts/send_gemini_request.sh --payload gcli-request.json --model gemini-3-flash-preview +# ----------------------------------------------------------------------------- + +set -e -E + +# Load environment variables from .env if it exists +if [ -f ".env" ]; then + echo "Loading environment variables from .env file..." + set -a # Automatically export all variables + source .env + set +a +fi + +# Function to print usage +usage() { + echo "Usage: $0 --payload --model [--stream]" + echo "Ensure GEMINI_API_KEY environment variable is set." + exit 1 +} + +STREAM_MODE=false + +# Parse command line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --payload) PAYLOAD_FILE="$2"; shift ;; + --model) MODEL_ID="$2"; shift ;; + --stream) STREAM_MODE=true ;; + *) echo "Unknown parameter passed: $1"; usage ;; + esac + shift +done + +# Validate inputs +if [ -z "$PAYLOAD_FILE" ] || [ -z "$MODEL_ID" ]; then + echo "Error: Missing required arguments." + usage +fi + +if [ -z "$GEMINI_API_KEY" ]; then + echo "Error: GEMINI_API_KEY environment variable is not set." + exit 1 +fi + +if [ ! -f "$PAYLOAD_FILE" ]; then + echo "Error: Payload file '$PAYLOAD_FILE' does not exist." + exit 1 +fi + +# API Endpoint definition +if [ "$STREAM_MODE" = true ]; then + GENERATE_CONTENT_API="streamGenerateContent" + echo "Mode: Streaming" +else + GENERATE_CONTENT_API="generateContent" + echo "Mode: Non-streaming (Default)" +fi + +echo "Sending request to model: $MODEL_ID" +echo "Using payload from: $PAYLOAD_FILE" +echo "----------------------------------------" + +# Make the cURL request. If non-streaming, pipe through jq for readability if available. +if [ "$STREAM_MODE" = false ] && command -v jq &> /dev/null; then + curl -s -X POST \ + -H "Content-Type: application/json" \ + "https://generativelanguage.googleapis.com/v1beta/models/${MODEL_ID}:${GENERATE_CONTENT_API}?key=${GEMINI_API_KEY}" \ + -d "@${PAYLOAD_FILE}" | jq . +else + curl -X POST \ + -H "Content-Type: application/json" \ + "https://generativelanguage.googleapis.com/v1beta/models/${MODEL_ID}:${GENERATE_CONTENT_API}?key=${GEMINI_API_KEY}" \ + -d "@${PAYLOAD_FILE}" +fi + +echo -e "\n----------------------------------------"