Skip to content

Commit 6279701

Browse files
authored
Merge pull request #4 from sidwan02/routing-via-local-gemini
feat: Add `LocalGeminiClient` and configurable provider for routing
2 parents 7c98135 + 470fdc1 commit 6279701

6 files changed

Lines changed: 202 additions & 104 deletions

File tree

.gemini/settings.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
},
1919
"useModelRouter": {
2020
"useGemmaRouting": {
21-
"enabled": false,
22-
"host": "http://localhost:11434",
21+
"enabled": true,
22+
"host": "http://localhost:8000",
2323
// "model": "gemma3n:e4b"
2424
// "model": "gemma3n:e2b"
25-
"model": "gemma3:1b"
25+
"model": "gemma3:1b",
26+
"provider": "litert-lm"
2627
// "model": "gemma3:270m"
2728
}
2829
}

packages/core/src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export interface UseModelRouterSettings {
149149
enabled?: boolean;
150150
model?: string;
151151
host?: string;
152+
provider?: 'ollama' | 'litert-lm';
152153
};
153154
}
154155

@@ -565,6 +566,7 @@ export class Config {
565566
host:
566567
params.useModelRouter?.useGemmaRouting?.host ??
567568
'http://localhost:11434',
569+
provider: params.useModelRouter?.useGemmaRouting?.provider ?? 'ollama',
568570
},
569571
};
570572
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [];
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { GoogleGenAI } from '@google/genai';
8+
import type { Config } from '../config/config.js';
9+
import { debugLogger } from '../utils/debugLogger.js';
10+
import type { Content } from './ollamaChat.js';
11+
12+
/**
13+
* A client for making single, non-streaming calls to a local Gemini-compatible API
14+
* and expecting a JSON response.
15+
*/
16+
export class LocalGeminiClient {
17+
private readonly host: string;
18+
private readonly model: string;
19+
private readonly client: GoogleGenAI;
20+
21+
constructor(config: Config) {
22+
const useGemmaRoutingSettings = config.getUseGemmaRoutingSettings();
23+
this.host = useGemmaRoutingSettings?.host || 'http://localhost:8000';
24+
this.model = useGemmaRoutingSettings?.model || 'Gemma3-1B-IT';
25+
26+
if (!this.model.toLowerCase().startsWith('gemma')) {
27+
throw new Error(
28+
`Invalid model name: ${this.model}. Model name must start with "Gemma" (case-insensitive).`,
29+
);
30+
}
31+
32+
this.client = new GoogleGenAI({
33+
apiKey: 'no-api-key-needed',
34+
httpOptions: {
35+
baseUrl: this.host,
36+
},
37+
});
38+
}
39+
40+
/**
41+
* Sends a prompt to the local Gemini model and expects a JSON object in response.
42+
* @param contents The history and current prompt.
43+
* @param systemInstruction The system prompt.
44+
* @returns A promise that resolves to the parsed JSON object.
45+
*/
46+
async generateJson(
47+
contents: Content[],
48+
systemInstruction: string,
49+
): Promise<object> {
50+
debugLogger.log(
51+
`[LocalGeminiClient] Sending request to ${this.host} for model ${this.model}`,
52+
);
53+
54+
const geminiContents = contents.map((c) => ({
55+
role: c.role === 'model' ? 'model' : 'user',
56+
parts: c.parts.map((p) => ({ text: p.text })),
57+
}));
58+
59+
try {
60+
const result = await this.client.models.generateContent({
61+
model: this.model,
62+
contents: geminiContents,
63+
config: {
64+
responseMimeType: 'application/json',
65+
systemInstruction: systemInstruction
66+
? { parts: [{ text: systemInstruction }] }
67+
: undefined,
68+
},
69+
});
70+
71+
const text = result.text;
72+
if (!text) {
73+
throw new Error(
74+
'Invalid response from Local Gemini API: No text found',
75+
);
76+
}
77+
78+
return JSON.parse(text);
79+
} catch (error) {
80+
debugLogger.error(
81+
`[LocalGeminiClient] Failed to generate content:`,
82+
error,
83+
);
84+
throw error;
85+
}
86+
}
87+
}

packages/core/src/routing/strategies/classifierStrategy.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ import { promptIdContext } from '../../utils/promptIdContext.js';
2121
import type { Content } from '@google/genai';
2222
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
2323

24+
import type { OllamaClient as OllamaClientType } from '../../core/ollamaClient.js';
25+
import type { LocalGeminiClient as LocalGeminiClientType } from '../../core/localGeminiClient.js';
26+
2427
vi.mock('../../core/baseLlmClient.js');
2528
vi.mock('../../utils/promptIdContext.js');
29+
vi.mock('../../core/ollamaClient.js');
30+
vi.mock('../../core/localGeminiClient.js');
2631

2732
describe('ClassifierStrategy', () => {
2833
let strategy: ClassifierStrategy;
@@ -50,6 +55,7 @@ describe('ClassifierStrategy', () => {
5055
getResolvedConfig: vi.fn().mockReturnValue(mockResolvedConfig),
5156
},
5257
getPreviewFeatures: () => false,
58+
getUseGemmaRoutingSettings: () => ({ enabled: false }),
5359
} as unknown as Config;
5460
mockBaseLlmClient = {
5561
generateJson: vi.fn(),
@@ -278,4 +284,88 @@ describe('ClassifierStrategy', () => {
278284
);
279285
consoleWarnSpy.mockRestore();
280286
});
287+
288+
describe('Local Routing', () => {
289+
beforeEach(() => {
290+
vi.resetModules();
291+
});
292+
293+
it('should use OllamaClient when local routing is enabled with default provider', async () => {
294+
const mockOllamaGenerateJson = vi.fn().mockResolvedValue({
295+
reasoning: 'Local Ollama reasoning',
296+
model_choice: 'flash',
297+
});
298+
299+
// We need to mock the module before importing/using it in the test context if we want to capture the constructor
300+
// However, since we are using vi.mock at the top level, we can just mock the implementation here.
301+
const { OllamaClient } = await import('../../core/ollamaClient.js');
302+
vi.mocked(OllamaClient).mockImplementation(
303+
() =>
304+
({
305+
generateJson: mockOllamaGenerateJson,
306+
}) as unknown as OllamaClientType,
307+
);
308+
309+
vi.spyOn(mockConfig, 'getUseGemmaRoutingSettings').mockReturnValue({
310+
enabled: true,
311+
provider: 'ollama',
312+
});
313+
314+
const decision = await strategy.route(
315+
mockContext,
316+
mockConfig,
317+
mockBaseLlmClient,
318+
);
319+
320+
expect(OllamaClient).toHaveBeenCalled();
321+
expect(mockOllamaGenerateJson).toHaveBeenCalled();
322+
expect(decision).toEqual({
323+
model: DEFAULT_GEMINI_FLASH_MODEL,
324+
metadata: {
325+
source: 'Classifier',
326+
latencyMs: expect.any(Number),
327+
reasoning: 'Local Ollama reasoning',
328+
},
329+
});
330+
});
331+
332+
it('should use LocalGeminiClient when local routing is enabled with litert-lm provider', async () => {
333+
const mockLocalGeminiGenerateJson = vi.fn().mockResolvedValue({
334+
reasoning: 'Local Gemini reasoning',
335+
model_choice: 'pro',
336+
});
337+
338+
const { LocalGeminiClient } = await import(
339+
'../../core/localGeminiClient.js'
340+
);
341+
vi.mocked(LocalGeminiClient).mockImplementation(
342+
() =>
343+
({
344+
generateJson: mockLocalGeminiGenerateJson,
345+
}) as unknown as LocalGeminiClientType,
346+
);
347+
348+
vi.spyOn(mockConfig, 'getUseGemmaRoutingSettings').mockReturnValue({
349+
enabled: true,
350+
provider: 'litert-lm',
351+
});
352+
353+
const decision = await strategy.route(
354+
mockContext,
355+
mockConfig,
356+
mockBaseLlmClient,
357+
);
358+
359+
expect(LocalGeminiClient).toHaveBeenCalled();
360+
expect(mockLocalGeminiGenerateJson).toHaveBeenCalled();
361+
expect(decision).toEqual({
362+
model: DEFAULT_GEMINI_MODEL,
363+
metadata: {
364+
source: 'Classifier',
365+
latencyMs: expect.any(Number),
366+
reasoning: 'Local Gemini reasoning',
367+
},
368+
});
369+
});
370+
});
281371
});

packages/core/src/routing/strategies/classifierStrategy.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { OllamaClient } from '../../core/ollamaClient.js';
8+
import { LocalGeminiClient } from '../../core/localGeminiClient.js';
89
import { z } from 'zod';
910
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
1011
import { promptIdContext } from '../../utils/promptIdContext.js';
@@ -190,8 +191,10 @@ export class ClassifierStrategy implements RoutingStrategy {
190191

191192
let jsonResponse;
192193
if (config.getUseGemmaRoutingSettings()?.enabled) {
193-
debugLogger.log(`[Routing] Using OllamaClient for classifier routing.`);
194-
const ollamaClient = new OllamaClient(config);
194+
const settings = config.getUseGemmaRoutingSettings();
195+
const provider = settings?.provider ?? 'ollama';
196+
debugLogger.log(`[Routing] Using ${provider} for classifier routing.`);
197+
195198
const ollamaHistory = finalHistory
196199
.map(toOllamaContent)
197200
.filter((c): c is OllamaContent => c !== null);
@@ -202,12 +205,19 @@ export class ClassifierStrategy implements RoutingStrategy {
202205
ollamaHistory.push(ollamaRequest);
203206
}
204207

205-
// debugLogger.log(`[Routing] ollamaHistory:`, ollamaHistory);
206-
207-
jsonResponse = await ollamaClient.generateJson(
208-
ollamaHistory,
209-
CLASSIFIER_SYSTEM_PROMPT,
210-
);
208+
if (provider === 'litert-lm') {
209+
const client = new LocalGeminiClient(config);
210+
jsonResponse = await client.generateJson(
211+
ollamaHistory,
212+
CLASSIFIER_SYSTEM_PROMPT,
213+
);
214+
} else {
215+
const ollamaClient = new OllamaClient(config);
216+
jsonResponse = await ollamaClient.generateJson(
217+
ollamaHistory,
218+
CLASSIFIER_SYSTEM_PROMPT,
219+
);
220+
}
211221
} else {
212222
jsonResponse = await baseLlmClient.generateJson({
213223
modelConfigKey: { model: 'classifier' },

prompt.txt

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -484,96 +484,4 @@ comments.
484484

485485
Use hyphens instead of underscores in flag names (e.g. `my-flag` instead of
486486
`my_flag`).
487-
--- End of Context from: GEMINI.md ---
488-
489-
--- Context from: litellm/GEMINI.md ---
490-
# GEMINI.md
491-
492-
This file provides guidance to Gemini when working with code in this repository.
493-
494-
## Development Commands
495-
496-
### Installation
497-
- `make install-dev` - Install core development dependencies
498-
- `make install-proxy-dev` - Install proxy development dependencies with full feature set
499-
- `make install-test-deps` - Install all test dependencies
500-
501-
### Testing
502-
- `make test` - Run all tests
503-
- `make test-unit` - Run unit tests (tests/test_litellm) with 4 parallel workers
504-
- `make test-integration` - Run integration tests (excludes unit tests)
505-
- `pytest tests/` - Direct pytest execution
506-
507-
### Code Quality
508-
- `make lint` - Run all linting (Ruff, MyPy, Black, circular imports, import safety)
509-
- `make format` - Apply Black code formatting
510-
- `make lint-ruff` - Run Ruff linting only
511-
- `make lint-mypy` - Run MyPy type checking only
512-
513-
### Single Test Files
514-
- `poetry run pytest tests/path/to/test_file.py -v` - Run specific test file
515-
- `poetry run pytest tests/path/to/test_file.py::test_function -v` - Run specific test
516-
517-
## Architecture Overview
518-
519-
LiteLLM is a unified interface for 100+ LLM providers with two main components:
520-
521-
### Core Library (`litellm/`)
522-
- **Main entry point**: `litellm/main.py` - Contains core completion() function
523-
- **Provider implementations**: `litellm/llms/` - Each provider has its own subdirectory
524-
- **Router system**: `litellm/router.py` + `litellm/router_utils/` - Load balancing and fallback logic
525-
- **Type definitions**: `litellm/types/` - Pydantic models and type hints
526-
- **Integrations**: `litellm/integrations/` - Third-party observability, caching, logging
527-
- **Caching**: `litellm/caching/` - Multiple cache backends (Redis, in-memory, S3, etc.)
528-
529-
### Proxy Server (`litellm/proxy/`)
530-
- **Main server**: `proxy_server.py` - FastAPI application
531-
- **Authentication**: `auth/` - API key management, JWT, OAuth2
532-
- **Database**: `db/` - Prisma ORM with PostgreSQL/SQLite support
533-
- **Management endpoints**: `management_endpoints/` - Admin APIs for keys, teams, models
534-
- **Pass-through endpoints**: `pass_through_endpoints/` - Provider-specific API forwarding
535-
- **Guardrails**: `guardrails/` - Safety and content filtering hooks
536-
- **UI Dashboard**: Served from `_experimental/out/` (Next.js build)
537-
538-
## Key Patterns
539-
540-
### Provider Implementation
541-
- Providers inherit from base classes in `litellm/llms/base.py`
542-
- Each provider has transformation functions for input/output formatting
543-
- Support both sync and async operations
544-
- Handle streaming responses and function calling
545-
546-
### Error Handling
547-
- Provider-specific exceptions mapped to OpenAI-compatible errors
548-
- Fallback logic handled by Router system
549-
- Comprehensive logging through `litellm/_logging.py`
550-
551-
### Configuration
552-
- YAML config files for proxy server (see `proxy/example_config_yaml/`)
553-
- Environment variables for API keys and settings
554-
- Database schema managed via Prisma (`proxy/schema.prisma`)
555-
556-
## Development Notes
557-
558-
### Code Style
559-
- Uses Black formatter, Ruff linter, MyPy type checker
560-
- Pydantic v2 for data validation
561-
- Async/await patterns throughout
562-
- Type hints required for all public APIs
563-
564-
### Testing Strategy
565-
- Unit tests in `tests/test_litellm/`
566-
- Integration tests for each provider in `tests/llm_translation/`
567-
- Proxy tests in `tests/proxy_unit_tests/`
568-
- Load tests in `tests/load_tests/`
569-
570-
### Database Migrations
571-
- Prisma handles schema migrations
572-
- Migration files auto-generated with `prisma migrate dev`
573-
- Always test migrations against both PostgreSQL and SQLite
574-
575-
### Enterprise Features
576-
- Enterprise-specific code in `enterprise/` directory
577-
- Optional features enabled via environment variables
578-
- Separate licensing and authentication for enterprise features
579-
--- End of Context from: litellm/GEMINI.md ---
487+
--- End of Context from: GEMINI.md ---

0 commit comments

Comments
 (0)