mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add Gemini CLI provider integration
- Add @google/gemini-cli-core dependency - Create GeminiCliHandler provider implementation using OAuth authentication - Add type definitions for gemini-cli models - Update provider exports and buildApiHandler - Add comprehensive tests for the new provider This integration allows users to authenticate with Gemini CLI using OAuth and access Gemini models through the official CLI library.
This commit is contained in:
parent
cd9e92fa9b
commit
0184d6eafb
9 changed files with 1494 additions and 17 deletions
|
|
@ -62,5 +62,8 @@
|
|||
"form-data": ">=4.0.4",
|
||||
"bluebird": ">=3.7.2"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "^0.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
featherlessModels,
|
||||
fireworksModels,
|
||||
geminiModels,
|
||||
geminiCliModels,
|
||||
groqModels,
|
||||
ioIntelligenceModels,
|
||||
mistralModels,
|
||||
|
|
@ -470,7 +471,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
|
|||
}
|
||||
|
||||
export const MODELS_BY_PROVIDER: Record<
|
||||
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama">,
|
||||
Exclude<ProviderName, "fake-ai" | "human-relay" | "lmstudio" | "openai" | "ollama">,
|
||||
{ id: ProviderName; label: string; models: string[] }
|
||||
> = {
|
||||
anthropic: {
|
||||
|
|
@ -515,6 +516,11 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
label: "Google Gemini",
|
||||
models: Object.keys(geminiModels),
|
||||
},
|
||||
"gemini-cli": {
|
||||
id: "gemini-cli",
|
||||
label: "Google Gemini CLI",
|
||||
models: Object.keys(geminiCliModels),
|
||||
},
|
||||
groq: { id: "groq", label: "Groq", models: Object.keys(groqModels) },
|
||||
"io-intelligence": {
|
||||
id: "io-intelligence",
|
||||
|
|
|
|||
54
packages/types/src/providers/gemini-cli.ts
Normal file
54
packages/types/src/providers/gemini-cli.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
// Gemini CLI models - using the same models as regular Gemini
|
||||
// The CLI provides access to the same models through OAuth authentication
|
||||
export type GeminiCliModelId = keyof typeof geminiCliModels
|
||||
|
||||
export const geminiCliDefaultModelId: GeminiCliModelId = "gemini-2.0-flash-001"
|
||||
|
||||
// Re-use the same model definitions as the regular Gemini provider
|
||||
// since Gemini CLI provides access to the same models
|
||||
export const geminiCliModels = {
|
||||
"gemini-2.0-flash-001": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.4,
|
||||
cacheReadsPrice: 0.025,
|
||||
cacheWritesPrice: 1.0,
|
||||
},
|
||||
"gemini-1.5-flash-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.15, // This is the pricing for prompts above 128k tokens.
|
||||
outputPrice: 0.6,
|
||||
cacheReadsPrice: 0.0375,
|
||||
cacheWritesPrice: 1.0,
|
||||
tiers: [
|
||||
{
|
||||
contextWindow: 128_000,
|
||||
inputPrice: 0.075,
|
||||
outputPrice: 0.3,
|
||||
cacheReadsPrice: 0.01875,
|
||||
},
|
||||
{
|
||||
contextWindow: Infinity,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
cacheReadsPrice: 0.0375,
|
||||
},
|
||||
],
|
||||
},
|
||||
"gemini-1.5-pro-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
|
@ -8,6 +8,7 @@ export * from "./doubao.js"
|
|||
export * from "./featherless.js"
|
||||
export * from "./fireworks.js"
|
||||
export * from "./gemini.js"
|
||||
export * from "./gemini-cli.js"
|
||||
export * from "./glama.js"
|
||||
export * from "./groq.js"
|
||||
export * from "./huggingface.js"
|
||||
|
|
|
|||
888
pnpm-lock.yaml
generated
888
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,7 @@ import {
|
|||
OpenAiHandler,
|
||||
LmStudioHandler,
|
||||
GeminiHandler,
|
||||
GeminiCliHandler,
|
||||
OpenAiNativeHandler,
|
||||
DeepSeekHandler,
|
||||
MoonshotHandler,
|
||||
|
|
@ -162,8 +163,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
return new FeatherlessHandler(options)
|
||||
case "vercel-ai-gateway":
|
||||
return new VercelAiGatewayHandler(options)
|
||||
case "gemini-cli":
|
||||
return new GeminiCliHandler(options)
|
||||
default:
|
||||
apiProvider satisfies "gemini-cli" | undefined
|
||||
apiProvider satisfies undefined
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
268
src/api/providers/__tests__/gemini-cli.spec.ts
Normal file
268
src/api/providers/__tests__/gemini-cli.spec.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { GeminiCliHandler } from "../gemini-cli"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
// Mock the @google/gemini-cli-core module
|
||||
vi.mock("@google/gemini-cli-core", () => ({
|
||||
GeminiClient: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
startChat: vi.fn().mockResolvedValue(undefined),
|
||||
addHistory: vi.fn().mockResolvedValue(undefined),
|
||||
sendMessageStream: vi.fn().mockImplementation(async function* () {
|
||||
yield { type: "content", value: "Test response" }
|
||||
return {
|
||||
getDebugResponses: vi.fn().mockReturnValue([
|
||||
{
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
},
|
||||
},
|
||||
]),
|
||||
}
|
||||
}),
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [{ text: "Test completion response" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
getDebugResponses: vi.fn().mockReturnValue([
|
||||
{
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
},
|
||||
},
|
||||
]),
|
||||
})),
|
||||
Config: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getGeminiClient: vi.fn().mockReturnValue({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
startChat: vi.fn().mockResolvedValue(undefined),
|
||||
addHistory: vi.fn().mockResolvedValue(undefined),
|
||||
sendMessageStream: vi.fn().mockImplementation(async function* () {
|
||||
yield { type: "content", value: "Test response" }
|
||||
return {
|
||||
getDebugResponses: vi.fn().mockReturnValue([
|
||||
{
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
},
|
||||
},
|
||||
]),
|
||||
}
|
||||
}),
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [{ text: "Test completion response" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
AuthType: {
|
||||
LOGIN_WITH_GOOGLE: "oauth-personal",
|
||||
},
|
||||
createContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||
model: "gemini-2.0-flash-001",
|
||||
authType: "oauth-personal",
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("GeminiCliHandler", () => {
|
||||
let handler: GeminiCliHandler
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
handler = new GeminiCliHandler({})
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an instance", () => {
|
||||
expect(handler).toBeInstanceOf(GeminiCliHandler)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
it("should return default model when no apiModelId is provided", () => {
|
||||
const model = handler.getModel()
|
||||
expect(model.id).toBe("gemini-2.0-flash-001")
|
||||
expect(model.info).toBeDefined()
|
||||
expect(model.info.contextWindow).toBe(1_048_576)
|
||||
})
|
||||
|
||||
it("should return specified model when apiModelId is provided", () => {
|
||||
const customHandler = new GeminiCliHandler({
|
||||
apiModelId: "gemini-1.5-flash-002",
|
||||
})
|
||||
const model = customHandler.getModel()
|
||||
expect(model.id).toBe("gemini-1.5-flash-002")
|
||||
})
|
||||
|
||||
it("should fall back to default model for invalid apiModelId", () => {
|
||||
const customHandler = new GeminiCliHandler({
|
||||
apiModelId: "invalid-model",
|
||||
})
|
||||
const model = customHandler.getModel()
|
||||
expect(model.id).toBe("gemini-2.0-flash-001")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
it("should stream messages from Gemini CLI", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello, how are you?",
|
||||
},
|
||||
]
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const results = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
results.push(chunk)
|
||||
}
|
||||
|
||||
// Should have at least text chunk
|
||||
expect(results.length).toBeGreaterThanOrEqual(1)
|
||||
expect(results[0]).toEqual({ type: "text", text: "Test response" })
|
||||
|
||||
// Usage chunk may or may not be present depending on mock
|
||||
const usageChunk = results.find((r) => r.type === "usage")
|
||||
if (usageChunk) {
|
||||
expect(usageChunk).toMatchObject({
|
||||
type: "usage",
|
||||
inputTokens: expect.any(Number),
|
||||
outputTokens: expect.any(Number),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle reasoning/thought events", async () => {
|
||||
// Mock a thought event
|
||||
const mockClient = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
startChat: vi.fn().mockResolvedValue(undefined),
|
||||
addHistory: vi.fn().mockResolvedValue(undefined),
|
||||
sendMessageStream: vi.fn().mockImplementation(async function* () {
|
||||
yield { type: "thought", value: { subject: "Analysis", description: "Thinking about the problem" } }
|
||||
yield { type: "content", value: "Final answer" }
|
||||
return {
|
||||
getDebugResponses: vi.fn().mockReturnValue([]),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const { Config } = await import("@google/gemini-cli-core")
|
||||
;(Config as any).mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockClient),
|
||||
}))
|
||||
|
||||
const customHandler = new GeminiCliHandler({})
|
||||
const stream = customHandler.createMessage("System", [{ role: "user", content: "Test" }])
|
||||
const results = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
results.push(chunk)
|
||||
}
|
||||
|
||||
expect(results[0]).toEqual({ type: "reasoning", text: "Analysis: Thinking about the problem" })
|
||||
expect(results[1]).toEqual({ type: "text", text: "Final answer" })
|
||||
})
|
||||
|
||||
it("should handle authentication errors", async () => {
|
||||
const mockClient = {
|
||||
initialize: vi.fn().mockRejectedValue(new Error("OAuth authentication failed")),
|
||||
}
|
||||
|
||||
const { Config } = await import("@google/gemini-cli-core")
|
||||
;(Config as any).mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockClient),
|
||||
}))
|
||||
|
||||
const customHandler = new GeminiCliHandler({})
|
||||
const stream = customHandler.createMessage("System", [{ role: "user", content: "Test" }])
|
||||
|
||||
await expect(async () => {
|
||||
for await (const _ of stream) {
|
||||
// Should throw before yielding anything
|
||||
}
|
||||
}).rejects.toThrow(/auth/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt", () => {
|
||||
it("should complete a prompt", async () => {
|
||||
// Need to mock the initialized client properly
|
||||
const mockClient = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [{ text: "Test completion response" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
const { Config } = await import("@google/gemini-cli-core")
|
||||
;(Config as any).mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockClient),
|
||||
}))
|
||||
|
||||
const customHandler = new GeminiCliHandler({})
|
||||
const result = await customHandler.completePrompt("What is 2 + 2?")
|
||||
expect(result).toBe("Test completion response")
|
||||
})
|
||||
|
||||
it("should handle empty response", async () => {
|
||||
const mockClient = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [],
|
||||
}),
|
||||
}
|
||||
|
||||
const { Config } = await import("@google/gemini-cli-core")
|
||||
;(Config as any).mockImplementation(() => ({
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockClient),
|
||||
}))
|
||||
|
||||
const customHandler = new GeminiCliHandler({})
|
||||
const result = await customHandler.completePrompt("Test")
|
||||
expect(result).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("countTokens", () => {
|
||||
it("should fall back to base implementation", async () => {
|
||||
const content: Anthropic.Messages.ContentBlockParam[] = [
|
||||
{
|
||||
type: "text",
|
||||
text: "Test content for token counting",
|
||||
},
|
||||
]
|
||||
|
||||
// The implementation falls back to the base class
|
||||
const count = await handler.countTokens(content)
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
283
src/api/providers/gemini-cli.ts
Normal file
283
src/api/providers/gemini-cli.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import {
|
||||
GeminiClient,
|
||||
Config,
|
||||
ConfigParameters,
|
||||
AuthType,
|
||||
ContentGeneratorConfig,
|
||||
createContentGeneratorConfig,
|
||||
} from "@google/gemini-cli-core"
|
||||
import { Content, GenerateContentResponse } from "@google/genai"
|
||||
|
||||
import { type ModelInfo, type GeminiCliModelId, geminiCliDefaultModelId, geminiCliModels } from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { t } from "i18next"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { convertAnthropicContentToGemini, convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
|
||||
/**
|
||||
* Handler for Google Gemini CLI integration using OAuth authentication.
|
||||
* This provider uses the @google/gemini-cli-core library to authenticate
|
||||
* and interact with Gemini models through the official CLI OAuth flow.
|
||||
*/
|
||||
export class GeminiCliHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private client?: GeminiClient
|
||||
private config: Config
|
||||
private initialized = false
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
this.options = options
|
||||
|
||||
// Create configuration for the Gemini CLI client
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: uuidv4(),
|
||||
targetDir: process.cwd(),
|
||||
cwd: process.cwd(),
|
||||
debugMode: false,
|
||||
model: this.getModel().id,
|
||||
interactive: false,
|
||||
}
|
||||
|
||||
this.config = new Config(configParams)
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.config.initialize()
|
||||
this.client = this.config.getGeminiClient()
|
||||
|
||||
// Initialize the content generator with OAuth
|
||||
const contentGeneratorConfig: ContentGeneratorConfig = createContentGeneratorConfig(
|
||||
this.config,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
)
|
||||
|
||||
await this.client.initialize(contentGeneratorConfig)
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(
|
||||
systemInstruction: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
await this.ensureInitialized()
|
||||
|
||||
if (!this.client) {
|
||||
throw new Error("Gemini CLI client not initialized")
|
||||
}
|
||||
|
||||
const { id: model, info, maxTokens } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
try {
|
||||
// Start a chat session
|
||||
await this.client.startChat()
|
||||
|
||||
// Add system instruction as initial context
|
||||
if (systemInstruction) {
|
||||
await this.client.addHistory({
|
||||
role: "user",
|
||||
parts: [{ text: `System: ${systemInstruction}` }],
|
||||
})
|
||||
}
|
||||
|
||||
// Add message history
|
||||
for (const content of contents) {
|
||||
await this.client.addHistory(content)
|
||||
}
|
||||
|
||||
// Get the last user message
|
||||
const lastUserMessage = contents[contents.length - 1]
|
||||
if (!lastUserMessage || !lastUserMessage.parts || lastUserMessage.parts.length === 0) {
|
||||
throw new Error("No user message found")
|
||||
}
|
||||
|
||||
// Send the message and stream the response
|
||||
const abortController = new AbortController()
|
||||
const promptId = uuidv4()
|
||||
|
||||
const stream = this.client.sendMessageStream(lastUserMessage.parts, abortController.signal, promptId)
|
||||
|
||||
let totalInputTokens = 0
|
||||
let totalOutputTokens = 0
|
||||
let turnResult: any = null
|
||||
|
||||
for await (const event of stream) {
|
||||
// The stream returns Turn objects at the end
|
||||
turnResult = event
|
||||
|
||||
// Handle content events
|
||||
if (event.type === "content" && event.value) {
|
||||
yield { type: "text", text: event.value }
|
||||
}
|
||||
|
||||
// Handle thought events (reasoning)
|
||||
if (event.type === "thought" && event.value) {
|
||||
const thought = event.value
|
||||
yield { type: "reasoning", text: `${thought.subject}: ${thought.description}` }
|
||||
}
|
||||
}
|
||||
|
||||
// The Turn object contains debug responses with usage metadata
|
||||
if (turnResult && turnResult.getDebugResponses) {
|
||||
const responses = turnResult.getDebugResponses()
|
||||
if (responses && responses.length > 0) {
|
||||
const lastResponse = responses[responses.length - 1]
|
||||
if (lastResponse.usageMetadata) {
|
||||
totalInputTokens = lastResponse.usageMetadata.promptTokenCount || 0
|
||||
totalOutputTokens = lastResponse.usageMetadata.candidatesTokenCount || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yield usage information
|
||||
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
totalCost: this.calculateCost({
|
||||
info,
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
}),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Check if it's an authentication error
|
||||
if (error.message.includes("auth") || error.message.includes("OAuth")) {
|
||||
throw new Error(
|
||||
t("common:errors.gemini_cli.auth_failed", {
|
||||
error: error.message,
|
||||
help: "Please authenticate using the Gemini CLI",
|
||||
}),
|
||||
)
|
||||
}
|
||||
throw new Error(t("common:errors.gemini_cli.generate_stream", { error: error.message }))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
override getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
let id = modelId && modelId in geminiCliModels ? (modelId as GeminiCliModelId) : geminiCliDefaultModelId
|
||||
let info: ModelInfo = geminiCliModels[id]
|
||||
const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options })
|
||||
|
||||
return { id, info, ...params }
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
if (!this.client) {
|
||||
throw new Error("Gemini CLI client not initialized")
|
||||
}
|
||||
|
||||
try {
|
||||
const { id: model } = this.getModel()
|
||||
|
||||
// Use generateContent method from the client
|
||||
const contents: Content[] = [{ role: "user", parts: [{ text: prompt }] }]
|
||||
|
||||
const response = await this.client.generateContent(
|
||||
contents,
|
||||
{
|
||||
temperature: this.options.modelTemperature ?? 0,
|
||||
maxOutputTokens: this.options.modelMaxTokens,
|
||||
},
|
||||
new AbortController().signal,
|
||||
model,
|
||||
)
|
||||
|
||||
// Extract text from the response
|
||||
if (response && response.candidates && response.candidates.length > 0) {
|
||||
const candidate = response.candidates[0]
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
const textParts = candidate.content.parts
|
||||
.filter((part) => "text" in part)
|
||||
.map((part) => part.text)
|
||||
.join("")
|
||||
return textParts
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Check if it's an authentication error
|
||||
if (error.message.includes("auth") || error.message.includes("OAuth")) {
|
||||
throw new Error(
|
||||
t("common:errors.gemini_cli.auth_failed", {
|
||||
error: error.message,
|
||||
help: "Please authenticate using the Gemini CLI",
|
||||
}),
|
||||
)
|
||||
}
|
||||
throw new Error(t("common:errors.gemini_cli.generate_complete_prompt", { error: error.message }))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
|
||||
// The Gemini CLI library doesn't expose a direct token counting method
|
||||
// Fall back to the base implementation
|
||||
return super.countTokens(content)
|
||||
}
|
||||
|
||||
private calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens = 0,
|
||||
}: {
|
||||
info: ModelInfo
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens?: number
|
||||
}) {
|
||||
if (!info.inputPrice || !info.outputPrice) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let inputPrice = info.inputPrice
|
||||
let outputPrice = info.outputPrice
|
||||
let cacheReadsPrice = info.cacheReadsPrice || 0
|
||||
|
||||
// If there's tiered pricing then adjust the input and output token prices
|
||||
// based on the input tokens used.
|
||||
if (info.tiers) {
|
||||
const tier = info.tiers.find((tier) => inputTokens <= tier.contextWindow)
|
||||
|
||||
if (tier) {
|
||||
inputPrice = tier.inputPrice ?? inputPrice
|
||||
outputPrice = tier.outputPrice ?? outputPrice
|
||||
cacheReadsPrice = tier.cacheReadsPrice ?? cacheReadsPrice
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract the cached input tokens from the total input tokens.
|
||||
const uncachedInputTokens = inputTokens - cacheReadTokens
|
||||
|
||||
let cacheReadCost = cacheReadTokens > 0 ? cacheReadsPrice * (cacheReadTokens / 1_000_000) : 0
|
||||
|
||||
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
|
||||
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
|
||||
const totalCost = inputTokensCost + outputTokensCost + cacheReadCost
|
||||
|
||||
return totalCost
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ export { DoubaoHandler } from "./doubao"
|
|||
export { MoonshotHandler } from "./moonshot"
|
||||
export { FakeAIHandler } from "./fake-ai"
|
||||
export { GeminiHandler } from "./gemini"
|
||||
export { GeminiCliHandler } from "./gemini-cli"
|
||||
export { GlamaHandler } from "./glama"
|
||||
export { GroqHandler } from "./groq"
|
||||
export { HuggingFaceHandler } from "./huggingface"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue