Repository navigation
feat(cli): add streamlined gemini gemma local model setup
#25498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 25 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
ff84679
docs: add codebase understanding guide
Samee24 760054e
docs: expand codebase understanding guide with technical depth
Samee24 f5b60c7
docs: finalize codebase understanding guide with advanced technical d…
Samee24 0f59d48
docs: add codebase understanding from antigravity
Samee24 bc6c644
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 99f2499
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 e930084
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 3dc0bb2
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 be2db23
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 7aec00d
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 dc30bd1
Merge branch 'main' of https://github.com/google-gemini/gemini-cli
Samee24 f7d4307
feat(cli): add `gemini gemma` command for streamlined local model setup
Samee24 8f023b5
feat(cli): add `gemini gemma logs` command to view LiteRT server logs
Samee24 82e87c4
docs: add gemma setup quick-start guide
Samee24 e0f043a
fix(core): set apiVersion to empty string for LiteRT-LM client
Samee24 ab8dc2d
Revert "fix(core): set apiVersion to empty string for LiteRT-LM client"
Samee24 d0dd169
feat(cli): show gemma router settings in /settings dialog
Samee24 8d3ac52
fix(gemma): resolve 404 errors and improve port resolution (#25340)
Abhijit-2592 c0117b4
chore(docs): regenerate settings schema and docs
509060e
rm docs
3b2243d
fix(cli): correctness and cross-platform fixes for gemma commands
Samee24 5223295
Merge branch 'main' into sameez/gemma-auto-setup
Samee24 85a5a97
chore: remove gemma setup guide doc for now
Samee24 eb5a3b9
chore: remove unnecessary comments across gemma commands
Samee24 c83376c
fix(cli): harden gemma router setup and server handling
Samee24 637af65
fix(cli): keep gemma logs attached to tail
Samee24 21bd3bd
Update packages/cli/src/commands/gemma/platform.ts
Samee24 6fed15f
Update packages/cli/src/gemini.tsx
Samee24 403a4c0
fix(cli): harden gemma setup and stop safety
Samee24 078aeb3
fix(cli): repair gemma platform config parsing
Samee24 dec7329
fix(cli): prettier
Samee24 c79ade9
Merge branch 'main' into sameez/gemma-auto-setup
Samee24 8f2a331
fix(cli): default autostart to false
Samee24 364eb1c
fix(cli): make server check more robust
Samee24 dac00da
fix(cli): scope gemma settings for security and project isolation
Samee24 e17478b
fix(cli): handle missing tail command and log stale PID cleanup
Samee24 6bad211
fix(cli): use generateContent endpoint for server health check
Samee24 e1a2899
Merge branch 'main' into sameez/gemma-auto-setup
Samee24 d3576a2
fix(cli): default autoStartServer to false in schema
Samee24 2d3e3ab
fix(cli): fix unsafe type assertion in logs error handler
Samee24 14e60b3
format
Samee24 b894f16
fix(chore): update docs
8ff6330
fix(cli): update tests for autoStartServer default and cross-platform…
Samee24 c065641
Merge branch 'main' into sameez/gemma-auto-setup
Samee24 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import type { CommandModule, Argv } from 'yargs'; | ||
| import { initializeOutputListenersAndFlush } from '../gemini.js'; | ||
| import { defer } from '../deferred.js'; | ||
| import { setupCommand } from './gemma/setup.js'; | ||
| import { startCommand } from './gemma/start.js'; | ||
| import { stopCommand } from './gemma/stop.js'; | ||
| import { statusCommand } from './gemma/status.js'; | ||
| import { logsCommand } from './gemma/logs.js'; | ||
|
|
||
| export const gemmaCommand: CommandModule = { | ||
| command: 'gemma', | ||
| describe: 'Manage local Gemma model routing', | ||
| builder: (yargs: Argv) => | ||
| yargs | ||
| .middleware((argv) => { | ||
| initializeOutputListenersAndFlush(); | ||
| argv['isCommand'] = true; | ||
| }) | ||
| .command(defer(setupCommand, 'gemma')) | ||
| .command(defer(startCommand, 'gemma')) | ||
| .command(defer(stopCommand, 'gemma')) | ||
| .command(defer(statusCommand, 'gemma')) | ||
| .command(defer(logsCommand, 'gemma')) | ||
| .demandCommand(1, 'You need at least one command before continuing.') | ||
| .version(false), | ||
| handler: () => {}, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import path from 'node:path'; | ||
| import { Storage } from '@google/gemini-cli-core'; | ||
|
|
||
| export const LITERT_RELEASE_VERSION = 'v0.9.0-alpha03'; | ||
| export const LITERT_RELEASE_BASE_URL = | ||
| 'https://github.com/google-ai-edge/LiteRT-LM/releases/download'; | ||
| export const GEMMA_MODEL_NAME = 'gemma3-1b-gpu-custom'; | ||
| export const DEFAULT_PORT = 9379; | ||
| export const HEALTH_CHECK_TIMEOUT_MS = 5000; | ||
| export const SERVER_START_WAIT_MS = 3000; | ||
|
|
||
| export const PLATFORM_BINARY_MAP: Record<string, string> = { | ||
| 'darwin-arm64': 'lit.macos_arm64', | ||
| 'linux-x64': 'lit.linux_x86_64', | ||
| 'win32-x64': 'lit.windows_x86_64.exe', | ||
| }; | ||
|
|
||
| export function getLiteRtBinDir(): string { | ||
| return path.join(Storage.getGlobalGeminiDir(), 'bin', 'litert'); | ||
| } | ||
|
|
||
| export function getPidFilePath(): string { | ||
| return path.join(Storage.getGlobalTempDir(), 'litert-server.pid'); | ||
| } | ||
|
|
||
| export function getLogFilePath(): string { | ||
| return path.join(Storage.getGlobalTempDir(), 'litert-server.log'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
| import { readLastLines } from './logs.js'; | ||
|
|
||
| describe('readLastLines', () => { | ||
| const tempFiles: string[] = []; | ||
|
|
||
| afterEach(async () => { | ||
| await Promise.all( | ||
| tempFiles | ||
| .splice(0) | ||
| .map((filePath) => fs.promises.rm(filePath, { force: true })), | ||
| ); | ||
| }); | ||
|
|
||
| it('returns only the requested tail lines without reading the whole file eagerly', async () => { | ||
| const filePath = path.join( | ||
| os.tmpdir(), | ||
| `gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`, | ||
| ); | ||
| tempFiles.push(filePath); | ||
|
|
||
| const content = Array.from({ length: 2000 }, (_, i) => `line-${i + 1}`) | ||
| .join('\n') | ||
| .concat('\n'); | ||
| await fs.promises.writeFile(filePath, content, 'utf-8'); | ||
|
|
||
| await expect(readLastLines(filePath, 3)).resolves.toBe( | ||
| 'line-1998\nline-1999\nline-2000\n', | ||
| ); | ||
| }); | ||
|
|
||
| it('returns an empty string when zero lines are requested', async () => { | ||
| const filePath = path.join( | ||
| os.tmpdir(), | ||
| `gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`, | ||
| ); | ||
| tempFiles.push(filePath); | ||
| await fs.promises.writeFile(filePath, 'line-1\nline-2\n', 'utf-8'); | ||
|
|
||
| await expect(readLastLines(filePath, 0)).resolves.toBe(''); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import type { CommandModule } from 'yargs'; | ||
| import fs from 'node:fs'; | ||
| import { spawn } from 'node:child_process'; | ||
| import { debugLogger } from '@google/gemini-cli-core'; | ||
| import { exitCli } from '../utils.js'; | ||
| import { getLogFilePath } from './constants.js'; | ||
|
|
||
| export async function readLastLines( | ||
| filePath: string, | ||
| count: number, | ||
| ): Promise<string> { | ||
| if (count <= 0) { | ||
| return ''; | ||
| } | ||
|
|
||
| const CHUNK_SIZE = 64 * 1024; | ||
| const fileHandle = await fs.promises.open(filePath, fs.constants.O_RDONLY); | ||
|
|
||
| try { | ||
| const stats = await fileHandle.stat(); | ||
| if (stats.size === 0) { | ||
| return ''; | ||
| } | ||
|
|
||
| const chunks: Buffer[] = []; | ||
| let totalBytes = 0; | ||
| let newlineCount = 0; | ||
| let position = stats.size; | ||
|
|
||
| while (position > 0 && newlineCount <= count) { | ||
| const readSize = Math.min(CHUNK_SIZE, position); | ||
| position -= readSize; | ||
|
|
||
| const buffer = Buffer.allocUnsafe(readSize); | ||
| const { bytesRead } = await fileHandle.read( | ||
| buffer, | ||
| 0, | ||
| readSize, | ||
| position, | ||
| ); | ||
|
|
||
| if (bytesRead === 0) { | ||
| break; | ||
| } | ||
|
|
||
| const chunk = | ||
| bytesRead === readSize ? buffer : buffer.subarray(0, bytesRead); | ||
| chunks.unshift(chunk); | ||
| totalBytes += chunk.length; | ||
|
|
||
| for (const byte of chunk) { | ||
| if (byte === 0x0a) { | ||
| newlineCount += 1; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const content = Buffer.concat(chunks, totalBytes).toString('utf-8'); | ||
| const lines = content.split('\n'); | ||
|
|
||
| if (position > 0 && lines.length > 0) { | ||
| const boundary = Buffer.allocUnsafe(1); | ||
| const { bytesRead } = await fileHandle.read(boundary, 0, 1, position - 1); | ||
| if (bytesRead === 1 && boundary[0] !== 0x0a) { | ||
| lines.shift(); | ||
| } | ||
| } | ||
|
|
||
| if (lines.length > 0 && lines[lines.length - 1] === '') { | ||
| lines.pop(); | ||
| } | ||
|
|
||
| if (lines.length === 0) { | ||
| return ''; | ||
| } | ||
|
|
||
| return lines.slice(-count).join('\n') + '\n'; | ||
| } finally { | ||
| await fileHandle.close(); | ||
| } | ||
| } | ||
|
|
||
| const isWindows = process.platform === 'win32'; | ||
|
|
||
| export const logsCommand: CommandModule = { | ||
| command: 'logs', | ||
| describe: 'View LiteRT-LM server logs', | ||
| builder: (yargs) => | ||
| yargs | ||
| .option('lines', { | ||
| alias: 'n', | ||
| type: 'number', | ||
| description: 'Show the last N lines and exit (omit to follow live)', | ||
| }) | ||
| .option('follow', { | ||
| alias: 'f', | ||
| type: 'boolean', | ||
| default: true, | ||
| description: 'Follow log output (default when --lines is not set)', | ||
| }), | ||
| handler: async (argv) => { | ||
| const logPath = getLogFilePath(); | ||
|
|
||
| try { | ||
| await fs.promises.access(logPath, fs.constants.F_OK); | ||
| } catch { | ||
| debugLogger.log(`No log file found at ${logPath}`); | ||
| debugLogger.log( | ||
| 'Is the LiteRT server running? Start it with: gemini gemma start', | ||
| ); | ||
| await exitCli(1); | ||
| return; | ||
| } | ||
|
|
||
| const rawLines = argv['lines']; | ||
| const lines = Number.isFinite(rawLines) ? Number(rawLines) : undefined; | ||
|
|
||
| if (lines !== undefined) { | ||
| if (isWindows) { | ||
| process.stdout.write(await readLastLines(logPath, lines)); | ||
| await exitCli(0); | ||
| return; | ||
| } | ||
| const tailArgs = ['-n', String(lines), logPath]; | ||
| const child = spawn('tail', tailArgs, { stdio: 'inherit' }); | ||
|
Samee24 marked this conversation as resolved.
Resolved
|
||
| child.on('close', async (code) => { | ||
| await exitCli(code ?? 0); | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if (isWindows) { | ||
| debugLogger.log( | ||
| 'Live log following is not supported on Windows. Use --lines N to view recent logs.', | ||
| ); | ||
| await exitCli(1); | ||
| return; | ||
| } | ||
|
|
||
| debugLogger.log(`Tailing ${logPath} (Ctrl+C to stop)\n`); | ||
| const tailArgs = ['-f', '-n', '20', logPath]; | ||
| const child = spawn('tail', tailArgs, { stdio: 'inherit' }); | ||
|
Samee24 marked this conversation as resolved.
Resolved
|
||
|
|
||
| process.on('SIGINT', () => { | ||
| child.kill('SIGTERM'); | ||
| }); | ||
|
|
||
| child.on('close', async (code) => { | ||
| await exitCli(code ?? 0); | ||
| }); | ||
| }, | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.