mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-24 21:10:43 -07:00
feat(cli): add /chat debug command for nightly builds (#16339)
This commit is contained in:
@@ -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<typeof import('@google/gemini-cli-core')>();
|
||||
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', () => {
|
||||
|
||||
@@ -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<SlashCommand[]> {
|
||||
const handle = startupProfiler.start('load_builtin_commands');
|
||||
|
||||
const isNightlyBuild = await isNightly(process.cwd());
|
||||
|
||||
const allDefinitions: Array<SlashCommand | null> = [
|
||||
aboutCommand,
|
||||
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
|
||||
authCommand,
|
||||
bugCommand,
|
||||
chatCommand,
|
||||
{
|
||||
...chatCommand,
|
||||
subCommands: isNightlyBuild
|
||||
? [...(chatCommand.subCommands || []), debugCommand]
|
||||
: chatCommand.subCommands,
|
||||
},
|
||||
clearCommand,
|
||||
compressCommand,
|
||||
copyCommand,
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<MessageActionReturn> => {
|
||||
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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
177
packages/core/src/utils/apiConversionUtils.test.ts
Normal file
177
packages/core/src/utils/apiConversionUtils.test.ts
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
57
packages/core/src/utils/apiConversionUtils.ts
Normal file
57
packages/core/src/utils/apiConversionUtils.ts
Normal file
@@ -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<string, unknown> {
|
||||
// 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<string, unknown> = {
|
||||
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;
|
||||
}
|
||||
102
scripts/send_gemini_request.sh
Executable file
102
scripts/send_gemini_request.sh
Executable file
@@ -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-<timestamp>.json`.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/send_gemini_request.sh --payload <path_to_json> --model <model_id> [--stream]
|
||||
#
|
||||
# Options:
|
||||
# --payload <file> Path to the JSON request payload.
|
||||
# --model <id> 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 <path_to_json_file> --model <model_id> [--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----------------------------------------"
|
||||
Reference in New Issue
Block a user