Skip to content

Commit 96b2d96

Browse files
committed
feat: Add support for Gemini Free Tier in API options and related components
1 parent e9da44d commit 96b2d96

File tree

24 files changed

+197
-15
lines changed

24 files changed

+197
-15
lines changed

evals/packages/types/src/roo-code.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ export const providerSettingsSchema = z.object({
357357
lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
358358
// Gemini
359359
geminiApiKey: z.string().optional(),
360+
// geminiFreeTier: z.boolean().optional(), // Moved to globalSettingsSchema
361+
geminiModelInfo: modelInfoSchema.optional(), // Keep this uncommented
360362
googleGeminiBaseUrl: z.string().optional(),
361363
// OpenAI Native
362364
openAiNativeApiKey: z.string().optional(),
@@ -444,6 +446,8 @@ const providerSettingsRecord: ProviderSettingsRecord = {
444446
lmStudioSpeculativeDecodingEnabled: undefined,
445447
// Gemini
446448
geminiApiKey: undefined,
449+
// geminiFreeTier: undefined, // Moved to globalSettingsRecord
450+
geminiModelInfo: undefined, // Keep this uncommented
447451
googleGeminiBaseUrl: undefined,
448452
// OpenAI Native
449453
openAiNativeApiKey: undefined,
@@ -538,6 +542,7 @@ export const globalSettingsSchema = z.object({
538542
language: languagesSchema.optional(),
539543

540544
telemetrySetting: telemetrySettingsSchema.optional(),
545+
geminiFreeTier: z.boolean().optional(), // Added Gemini Free Tier setting
541546

542547
mcpEnabled: z.boolean().optional(),
543548
enableMcpServerCreation: z.boolean().optional(),
@@ -613,6 +618,7 @@ const globalSettingsRecord: GlobalSettingsRecord = {
613618
language: undefined,
614619

615620
telemetrySetting: undefined,
621+
geminiFreeTier: undefined, // Added Gemini Free Tier setting
616622

617623
mcpEnabled: undefined,
618624
enableMcpServerCreation: undefined,

src/core/Cline.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,15 +1820,18 @@ export class Cline extends EventEmitter<ClineEvents> {
18201820
tokensOut: outputTokens,
18211821
cacheWrites: cacheWriteTokens,
18221822
cacheReads: cacheReadTokens,
1823+
// Check for Gemini free tier before calculating cost
18231824
cost:
1824-
totalCost ??
1825-
calculateApiCostAnthropic(
1826-
this.api.getModel().info,
1827-
inputTokens,
1828-
outputTokens,
1829-
cacheWriteTokens,
1830-
cacheReadTokens,
1831-
),
1825+
this.apiConfiguration.apiProvider === "gemini" && this.apiConfiguration.geminiFreeTier === true
1826+
? 0
1827+
: (totalCost ??
1828+
calculateApiCostAnthropic(
1829+
this.api.getModel().info,
1830+
inputTokens,
1831+
outputTokens,
1832+
cacheWriteTokens,
1833+
cacheReadTokens,
1834+
)),
18321835
cancelReason,
18331836
streamingFailedMessage,
18341837
} satisfies ClineApiReqInfo)

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
645645
await updateGlobalState("diffEnabled", diffEnabled)
646646
await provider.postStateToWebview()
647647
break
648+
// case "geminiFreeTier": // Removed - Now part of apiConfiguration
649+
// await provider.setValue("geminiFreeTier", message.bool ?? false) // Use provider.setValue
650+
// await provider.postStateToWebview()
651+
// break
648652
case "showGreeting":
649653
const showGreeting = message.bool ?? true
650654
await updateGlobalState("showGreeting", showGreeting)

src/exports/roo-code.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ type ProviderSettings = {
129129
lmStudioSpeculativeDecodingEnabled?: boolean | undefined
130130
geminiApiKey?: string | undefined
131131
googleGeminiBaseUrl?: string | undefined
132+
geminiFreeTier?: boolean | undefined
132133
openAiNativeApiKey?: string | undefined
133134
mistralApiKey?: string | undefined
134135
mistralCodestralUrl?: string | undefined

src/exports/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ type ProviderSettings = {
130130
lmStudioSpeculativeDecodingEnabled?: boolean | undefined
131131
geminiApiKey?: string | undefined
132132
googleGeminiBaseUrl?: string | undefined
133+
geminiFreeTier?: boolean | undefined
133134
openAiNativeApiKey?: string | undefined
134135
mistralApiKey?: string | undefined
135136
mistralCodestralUrl?: string | undefined

src/schemas/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export const providerSettingsSchema = z.object({
377377
// Gemini
378378
geminiApiKey: z.string().optional(),
379379
googleGeminiBaseUrl: z.string().optional(),
380+
geminiFreeTier: z.boolean().optional(),
380381
// OpenAI Native
381382
openAiNativeApiKey: z.string().optional(),
382383
// Mistral
@@ -466,6 +467,7 @@ const providerSettingsRecord: ProviderSettingsRecord = {
466467
// Gemini
467468
geminiApiKey: undefined,
468469
googleGeminiBaseUrl: undefined,
470+
geminiFreeTier: undefined,
469471
// OpenAI Native
470472
openAiNativeApiKey: undefined,
471473
// Mistral
@@ -494,7 +496,7 @@ const providerSettingsRecord: ProviderSettingsRecord = {
494496
fakeAi: undefined,
495497
}
496498

497-
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
499+
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as (keyof ProviderSettings)[]
498500

499501
/**
500502
* GlobalSettings
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React from "react"
2+
import { render, screen } from "@testing-library/react"
3+
import TaskHeader from "../TaskHeader"
4+
5+
// Mock the translation function
6+
jest.mock("react-i18next", () => ({
7+
useTranslation: () => ({ t: (key: string) => key }),
8+
}))
9+
10+
// Mock the vscode API
11+
jest.mock("@/utils/vscode", () => ({
12+
vscode: {
13+
postMessage: jest.fn(),
14+
},
15+
}))
16+
17+
// Mock the ExtensionStateContext
18+
jest.mock("../../context/ExtensionStateContext", () => ({
19+
useExtensionState: () => ({
20+
apiConfiguration: {
21+
apiProvider: "anthropic",
22+
},
23+
currentTaskItem: null,
24+
}),
25+
}))
26+
27+
describe("TaskHeader", () => {
28+
const defaultProps = {
29+
task: { text: "Test task", images: [] },
30+
tokensIn: 100,
31+
tokensOut: 50,
32+
doesModelSupportPromptCache: true,
33+
totalCost: 0.05,
34+
contextTokens: 200,
35+
onClose: jest.fn(),
36+
}
37+
38+
it("should display cost when totalCost is greater than 0", () => {
39+
render(
40+
<TaskHeader
41+
{...defaultProps}
42+
task={{
43+
type: "say",
44+
ts: Date.now(),
45+
text: "Test task",
46+
images: [],
47+
}}
48+
/>,
49+
)
50+
expect(screen.getByText("$0.0500")).toBeInTheDocument()
51+
})
52+
53+
it("should not display cost when totalCost is 0", () => {
54+
render(
55+
<TaskHeader
56+
{...defaultProps}
57+
totalCost={0}
58+
task={{
59+
type: "say",
60+
ts: Date.now(),
61+
text: "Test task",
62+
images: [],
63+
}}
64+
/>,
65+
)
66+
expect(screen.queryByText("$0.0000")).not.toBeInTheDocument()
67+
})
68+
69+
it("should not display cost when totalCost is null", () => {
70+
render(
71+
<TaskHeader
72+
{...defaultProps}
73+
totalCost={null as any}
74+
task={{
75+
type: "say",
76+
ts: Date.now(),
77+
text: "Test task",
78+
images: [],
79+
}}
80+
/>,
81+
)
82+
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
83+
})
84+
85+
it("should not display cost when totalCost is undefined", () => {
86+
render(
87+
<TaskHeader
88+
{...defaultProps}
89+
totalCost={undefined as any}
90+
task={{
91+
type: "say",
92+
ts: Date.now(),
93+
text: "Test task",
94+
images: [],
95+
}}
96+
/>,
97+
)
98+
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
99+
})
100+
101+
it("should not display cost when totalCost is NaN", () => {
102+
render(
103+
<TaskHeader
104+
{...defaultProps}
105+
totalCost={NaN}
106+
task={{
107+
type: "say",
108+
ts: Date.now(),
109+
text: "Test task",
110+
images: [],
111+
}}
112+
/>,
113+
)
114+
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
115+
})
116+
})

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ const ApiOptions = ({
7777
setErrorMessage,
7878
}: ApiOptionsProps) => {
7979
const { t } = useAppTranslation()
80-
8180
const [ollamaModels, setOllamaModels] = useState<string[]>([])
8281
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
8382
const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
@@ -767,6 +766,19 @@ const ApiOptions = ({
767766
/>
768767
)}
769768
</div>
769+
<div>
770+
<Checkbox
771+
checked={apiConfiguration?.geminiFreeTier ?? false} // Use apiConfiguration state
772+
onChange={(checked: boolean) => {
773+
setApiConfigurationField("geminiFreeTier", checked)
774+
}}>
775+
{t("settings:providers.useFreeTier")}
776+
</Checkbox>
777+
{/* Keep description separate as in original */}
778+
<div className="text-sm text-vscode-descriptionForeground ml-6">
779+
{t("settings:providers.useFreeTierDescription")}
780+
</div>
781+
</div>
770782
</>
771783
)}
772784

@@ -1669,6 +1681,7 @@ const ApiOptions = ({
16691681
modelInfo={selectedModelInfo}
16701682
isDescriptionExpanded={isDescriptionExpanded}
16711683
setIsDescriptionExpanded={setIsDescriptionExpanded}
1684+
apiConfiguration={apiConfiguration} // Pass the config down
16721685
/>
16731686
<ThinkingBudget
16741687
key={`${selectedProvider}-${selectedModelId}`}

webview-ui/src/components/settings/ModelInfoView.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
55
import { formatPrice } from "@/utils/formatPrice"
66
import { cn } from "@/lib/utils"
77

8-
import { ModelInfo, geminiModels } from "../../../../src/shared/api"
8+
import { ModelInfo, geminiModels, ApiConfiguration } from "../../../../src/shared/api" // Added ApiConfiguration import
99

1010
import { ModelDescriptionMarkdown } from "./ModelDescriptionMarkdown"
1111

@@ -14,16 +14,20 @@ type ModelInfoViewProps = {
1414
modelInfo: ModelInfo
1515
isDescriptionExpanded: boolean
1616
setIsDescriptionExpanded: (isExpanded: boolean) => void
17+
apiConfiguration?: ApiConfiguration // Added optional apiConfiguration prop
1718
}
1819

1920
export const ModelInfoView = ({
2021
selectedModelId,
2122
modelInfo,
2223
isDescriptionExpanded,
2324
setIsDescriptionExpanded,
25+
apiConfiguration, // Destructure the new prop
2426
}: ModelInfoViewProps) => {
2527
const { t } = useAppTranslation()
2628
const isGemini = useMemo(() => Object.keys(geminiModels).includes(selectedModelId), [selectedModelId])
29+
// Determine if Gemini free tier is active
30+
const isGeminiFreeTier = apiConfiguration?.apiProvider === "gemini" && apiConfiguration?.geminiFreeTier === true
2731

2832
const infoItems = [
2933
<ModelInfoSupportsItem
@@ -49,16 +53,18 @@ export const ModelInfoView = ({
4953
{modelInfo.maxTokens?.toLocaleString()} tokens
5054
</>
5155
),
52-
modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
56+
// Display input price (show $0 if free tier is active)
57+
modelInfo.inputPrice !== undefined && (
5358
<>
5459
<span className="font-medium">{t("settings:modelInfo.inputPrice")}:</span>{" "}
55-
{formatPrice(modelInfo.inputPrice)} / 1M tokens
60+
{formatPrice(isGeminiFreeTier ? 0 : modelInfo.inputPrice)} / 1M tokens
5661
</>
5762
),
58-
modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
63+
// Display output price (show $0 if free tier is active)
64+
modelInfo.outputPrice !== undefined && (
5965
<>
6066
<span className="font-medium">{t("settings:modelInfo.outputPrice")}:</span>{" "}
61-
{formatPrice(modelInfo.outputPrice)} / 1M tokens
67+
{formatPrice(isGeminiFreeTier ? 0 : modelInfo.outputPrice)} / 1M tokens
6268
</>
6369
),
6470
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
"getDeepSeekApiKey": "Obtenir clau API de DeepSeek",
123123
"geminiApiKey": "Clau API de Gemini",
124124
"getGeminiApiKey": "Obtenir clau API de Gemini",
125+
"useFreeTier": "Utilitza el nivell gratuït",
126+
"useFreeTierDescription": "Estableix els preus d'entrada i sortida a zero per al càlcul de costos.",
125127
"openAiApiKey": "Clau API d'OpenAI",
126128
"openAiBaseUrl": "URL base",
127129
"getOpenAiApiKey": "Obtenir clau API d'OpenAI",

0 commit comments

Comments
 (0)