diff --git a/.changeset/automatic-tags-publish.md b/.changeset/automatic-tags-publish.md
new file mode 100644
index 0000000000..a3ff07fb4c
--- /dev/null
+++ b/.changeset/automatic-tags-publish.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Update GitHub Actions workflow to automatically create and push git tags during release
diff --git a/.changeset/lemon-bulldogs-unite.md b/.changeset/lemon-bulldogs-unite.md
new file mode 100644
index 0000000000..b87abc1bd2
--- /dev/null
+++ b/.changeset/lemon-bulldogs-unite.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+App tab layout fixes
diff --git a/.env.sample b/.env.sample
index 6cdaa1b3b1..4d6c24ac72 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,2 +1 @@
-# PostHog API Keys for telemetry
-POSTHOG_API_KEY=key-goes-here
\ No newline at end of file
+POSTHOG_API_KEY=key-goes-here
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index c6fd66b1b3..4ecd2af7a2 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -10,6 +10,8 @@ env:
jobs:
publish-extension:
runs-on: ubuntu-latest
+ permissions:
+ contents: write # Required for pushing tags
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
@@ -23,24 +25,24 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
+
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
+
- name: Install Dependencies
run: |
npm install -g vsce ovsx
npm run install:ci
+
- name: Create .env file
run: |
echo "# PostHog API Keys for telemetry" > .env
echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
- - name: Package and Publish Extension
- env:
- VSCE_PAT: ${{ secrets.VSCE_PAT }}
- OVSX_PAT: ${{ secrets.OVSX_PAT }}
+
+ - name: Package Extension
run: |
current_package_version=$(node -p "require('./package.json').version")
-
npm run vsix
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
echo "$package"
@@ -49,5 +51,18 @@ jobs:
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q ".env" || exit 1
+ - name: Create and Push Git Tag
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
+ git tag -a "v${current_package_version}" -m "Release v${current_package_version}"
+ git push origin "v${current_package_version}"
+ echo "Successfully created and pushed git tag v${current_package_version}"
+
+ - name: Publish Extension
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+ OVSX_PAT: ${{ secrets.OVSX_PAT }}
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
diff --git a/src/api/providers/__tests__/requesty.test.ts b/src/api/providers/__tests__/requesty.test.ts
index 7867b15ebc..47921a1c53 100644
--- a/src/api/providers/__tests__/requesty.test.ts
+++ b/src/api/providers/__tests__/requesty.test.ts
@@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
contextWindow: 4000,
supportsPromptCache: false,
supportsImages: true,
- inputPrice: 0,
- outputPrice: 0,
+ inputPrice: 1,
+ outputPrice: 10,
+ cacheReadsPrice: 0.1,
+ cacheWritesPrice: 1.5,
},
openAiStreamingEnabled: true,
includeMaxTokens: true, // Add this to match the implementation
@@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
yield {
choices: [{ delta: { content: " world" } }],
usage: {
- prompt_tokens: 10,
- completion_tokens: 5,
+ prompt_tokens: 30,
+ completion_tokens: 10,
+ prompt_tokens_details: {
+ cached_tokens: 15,
+ caching_tokens: 5,
+ },
},
}
},
@@ -105,10 +111,11 @@ describe("RequestyHandler", () => {
{ type: "text", text: " world" },
{
type: "usage",
- inputTokens: 10,
- outputTokens: 5,
- cacheWriteTokens: undefined,
- cacheReadTokens: undefined,
+ inputTokens: 30,
+ outputTokens: 10,
+ cacheWriteTokens: 5,
+ cacheReadTokens: 15,
+ totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
},
])
@@ -182,6 +189,9 @@ describe("RequestyHandler", () => {
type: "usage",
inputTokens: 10,
outputTokens: 5,
+ cacheWriteTokens: 0,
+ cacheReadTokens: 0,
+ totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
},
])
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index caa99def09..2af3f2da05 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if (chunk.usage) {
- yield this.processUsageMetrics(chunk.usage)
+ yield this.processUsageMetrics(chunk.usage, modelInfo)
}
}
} else {
@@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
type: "text",
text: response.choices[0]?.message.content || "",
}
- yield this.processUsageMetrics(response.usage)
+ yield this.processUsageMetrics(response.usage, modelInfo)
}
}
- protected processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
return {
type: "usage",
inputTokens: usage?.prompt_tokens || 0,
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 0060bfc5d8..434d6f4316 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -1,9 +1,20 @@
import axios from "axios"
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
-import { parseApiPrice } from "../../utils/cost"
+import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
import { ApiStreamUsageChunk } from "../transform/stream"
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
+import OpenAI from "openai"
+
+// Requesty usage includes an extra field for Anthropic use cases.
+// Safely cast the prompt token details section to the appropriate structure.
+interface RequestyUsage extends OpenAI.CompletionUsage {
+ prompt_tokens_details?: {
+ caching_tokens?: number
+ cached_tokens?: number
+ }
+ total_cost?: number
+}
export class RequestyHandler extends OpenAiHandler {
constructor(options: OpenAiHandlerOptions) {
@@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler {
}
}
- protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
+ const requestyUsage = usage as RequestyUsage
+ const inputTokens = requestyUsage?.prompt_tokens || 0
+ const outputTokens = requestyUsage?.completion_tokens || 0
+ const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
+ const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
+ const totalCost = modelInfo
+ ? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
+ : 0
return {
type: "usage",
- inputTokens: usage?.prompt_tokens || 0,
- outputTokens: usage?.completion_tokens || 0,
- cacheWriteTokens: usage?.cache_creation_input_tokens,
- cacheReadTokens: usage?.cache_read_input_tokens,
+ inputTokens: inputTokens,
+ outputTokens: outputTokens,
+ cacheWriteTokens: cacheWriteTokens,
+ cacheReadTokens: cacheReadTokens,
+ totalCost: totalCost,
}
}
}
diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts
index bf1215e238..0ce2a6e26a 100644
--- a/src/api/providers/vscode-lm.ts
+++ b/src/api/providers/vscode-lm.ts
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { SingleCompletionHandler } from "../"
-import { calculateApiCost } from "../../utils/cost"
+import { calculateApiCostAnthropic } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
- totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
+ totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
}
} catch (error: unknown) {
this.ensureCleanState()
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 3d1f980c7d..ba171ce3fa 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
import { GlobalFileNames } from "../shared/globalFileNames"
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
-import { calculateApiCost } from "../utils/cost"
+import { calculateApiCostAnthropic } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { parseMentions } from "./mentions"
@@ -875,7 +875,7 @@ export class Cline {
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
- //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
+ //const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
if (didEndLoop) {
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
@@ -3173,7 +3173,7 @@ export class Cline {
cacheReads: cacheReadTokens,
cost:
totalCost ??
- calculateApiCost(
+ calculateApiCostAnthropic(
this.api.getModel().info,
inputTokens,
outputTokens,
@@ -3798,6 +3798,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointDiffed(this.taskId)
+
if (!previousCommitHash && mode === "checkpoint") {
const previousCheckpoint = this.clineMessages
.filter(({ say }) => say === "checkpoint_saved")
@@ -3849,6 +3851,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointCreated(this.taskId)
+
// Start the checkpoint process in the background.
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
@@ -3880,6 +3884,8 @@ export class Cline {
try {
await service.restoreCheckpoint(commitHash)
+ telemetryService.captureCheckpointRestored(this.taskId)
+
await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
if (mode === "restore") {
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index b82a6a62e0..72cf56d35b 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2567,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
properties.apiProvider = apiConfiguration.apiProvider
}
+ // Add model ID if available
+ const currentCline = this.getCurrentCline()
+ if (currentCline?.api) {
+ const { id: modelId } = currentCline.api.getModel()
+ if (modelId) {
+ properties.modelId = modelId
+ }
+ }
+
return properties
}
}
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index f9fc5d3ece..2e9fcdf336 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.setValues).toBeDefined()
})
})
+
+describe("getTelemetryProperties", () => {
+ let provider: ClineProvider
+ let mockContext: vscode.ExtensionContext
+ let mockOutputChannel: vscode.OutputChannel
+ let mockCline: any
+
+ beforeEach(() => {
+ // Reset mocks
+ jest.clearAllMocks()
+
+ // Setup basic mocks
+ mockContext = {
+ globalState: {
+ get: jest.fn().mockImplementation((key: string) => {
+ if (key === "mode") return "code"
+ if (key === "apiProvider") return "anthropic"
+ return undefined
+ }),
+ update: jest.fn(),
+ keys: jest.fn().mockReturnValue([]),
+ },
+ secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
+ extensionUri: {} as vscode.Uri,
+ globalStorageUri: { fsPath: "/test/path" },
+ extension: { packageJSON: { version: "1.0.0" } },
+ } as unknown as vscode.ExtensionContext
+
+ mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
+ provider = new ClineProvider(mockContext, mockOutputChannel)
+
+ // Setup Cline instance with mocked getModel method
+ const { Cline } = require("../../Cline")
+ mockCline = new Cline()
+ mockCline.api = {
+ getModel: jest.fn().mockReturnValue({
+ id: "claude-3-7-sonnet-20250219",
+ info: { contextWindow: 200000 },
+ }),
+ }
+ })
+
+ test("includes basic properties in telemetry", async () => {
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("vscodeVersion")
+ expect(properties).toHaveProperty("platform")
+ expect(properties).toHaveProperty("appVersion", "1.0.0")
+ })
+
+ test("includes model ID from current Cline instance if available", async () => {
+ // Add mock Cline to stack
+ await provider.addClineToStack(mockCline)
+
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("modelId", "claude-3-7-sonnet-20250219")
+ })
+})
diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts
index 45a34bda4e..d3ea8bfb5f 100644
--- a/src/services/telemetry/TelemetryService.ts
+++ b/src/services/telemetry/TelemetryService.ts
@@ -22,6 +22,9 @@ class PostHogClient {
CONVERSATION_MESSAGE: "Conversation Message",
MODE_SWITCH: "Mode Switched",
TOOL_USED: "Tool Used",
+ CHECKPOINT_CREATED: "Checkpoint Created",
+ CHECKPOINT_RESTORED: "Checkpoint Restored",
+ CHECKPOINT_DIFFED: "Checkpoint Diffed",
},
}
@@ -246,6 +249,18 @@ class TelemetryService {
})
}
+ public captureCheckpointCreated(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId })
+ }
+
+ public captureCheckpointDiffed(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId })
+ }
+
+ public captureCheckpointRestored(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
+ }
+
/**
* Checks if telemetry is currently enabled
* @returns Whether telemetry is enabled
diff --git a/src/utils/__tests__/cost.test.ts b/src/utils/__tests__/cost.test.ts
index e390c4af7f..4501f86b88 100644
--- a/src/utils/__tests__/cost.test.ts
+++ b/src/utils/__tests__/cost.test.ts
@@ -1,8 +1,8 @@
-import { calculateApiCost } from "../cost"
+import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
import { ModelInfo } from "../../shared/api"
describe("Cost Utility", () => {
- describe("calculateApiCost", () => {
+ describe("calculateApiCostAnthropic", () => {
const mockModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
@@ -14,7 +14,7 @@ describe("Cost Utility", () => {
}
it("should calculate basic input/output costs correctly", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -23,7 +23,7 @@ describe("Cost Utility", () => {
})
it("should handle cache writes cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -33,7 +33,7 @@ describe("Cost Utility", () => {
})
it("should handle cache reads cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -43,7 +43,7 @@ describe("Cost Utility", () => {
})
it("should handle all cost components together", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -60,17 +60,17 @@ describe("Cost Utility", () => {
supportsPromptCache: true,
}
- const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
expect(cost).toBe(0)
})
it("should handle zero tokens", () => {
- const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)
expect(cost).toBe(0)
})
it("should handle undefined cache values", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -85,7 +85,7 @@ describe("Cost Utility", () => {
cacheReadsPrice: undefined,
}
- const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000)
// Should only include input and output costs
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
@@ -94,4 +94,97 @@ describe("Cost Utility", () => {
expect(cost).toBe(0.0105)
})
})
+
+ describe("calculateApiCostOpenAI", () => {
+ const mockModelInfo: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million tokens
+ outputPrice: 15.0, // $15 per million tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ }
+
+ it("should calculate basic input/output costs correctly", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle cache writes cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000)
+
+ // Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Total: 0.003 + 0.0075 + 0.0075 = 0.018
+ expect(cost).toBeCloseTo(0.018, 6)
+ })
+
+ it("should handle cache reads cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0009 = 0.0114
+ expect(cost).toBe(0.0114)
+ })
+
+ it("should handle all cost components together", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
+ expect(cost).toBe(0.0189)
+ })
+
+ it("should handle missing prices gracefully", () => {
+ const modelWithoutPrices: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle zero tokens", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle undefined cache values", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle missing cache prices", () => {
+ const modelWithoutCachePrices: ModelInfo = {
+ ...mockModelInfo,
+ cacheWritesPrice: undefined,
+ cacheReadsPrice: undefined,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000)
+
+ // Should only include input and output costs
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+ })
})
diff --git a/src/utils/cost.ts b/src/utils/cost.ts
index adc2ded0a8..48108b6348 100644
--- a/src/utils/cost.ts
+++ b/src/utils/cost.ts
@@ -1,26 +1,57 @@
import { ModelInfo } from "../shared/api"
-export function calculateApiCost(
+function calculateApiCostInternal(
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
- cacheCreationInputTokens?: number,
- cacheReadInputTokens?: number,
+ cacheCreationInputTokens: number,
+ cacheReadInputTokens: number,
): number {
- const modelCacheWritesPrice = modelInfo.cacheWritesPrice
- let cacheWritesCost = 0
- if (cacheCreationInputTokens && modelCacheWritesPrice) {
- cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
- }
- const modelCacheReadsPrice = modelInfo.cacheReadsPrice
- let cacheReadsCost = 0
- if (cacheReadInputTokens && modelCacheReadsPrice) {
- cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
- }
+ const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
+ const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
+// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens
+export function calculateApiCostAnthropic(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ return calculateApiCostInternal(
+ modelInfo,
+ inputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
+// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
+export function calculateApiCostOpenAI(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
+ return calculateApiCostInternal(
+ modelInfo,
+ nonCachedInputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index b537b9298e..389f5709fc 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
+type HumanRelayDialogState = {
+ isOpen: boolean
+ requestId: string
+ promptText: string
+}
+
const tabsByMessageAction: Partial
+
Get more details and discuss in{" "} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 3df3e87e9b..1002788dbc 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1,22 +1,25 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import DynamicTextArea from "react-textarea-autosize" + import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions" -import { useExtensionState } from "../../context/ExtensionStateContext" +import { WebviewMessage } from "../../../../src/shared/WebviewMessage" +import { Mode, getAllModes } from "../../../../src/shared/modes" + +import { vscode } from "@/utils/vscode" import { ContextMenuOptionType, getContextMenuOptions, insertMention, removeMention, shouldShowContextMenu, -} from "../../utils/context-mentions" +} from "@/utils/context-mentions" +import { SelectDropdown, DropdownOptionType } from "@/components/ui" + +import { useExtensionState } from "../../context/ExtensionStateContext" +import Thumbnails from "../common/Thumbnails" +import { convertToMentionPath } from "../../utils/path-mentions" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import Thumbnails from "../common/Thumbnails" -import { vscode } from "../../utils/vscode" -import { WebviewMessage } from "../../../../src/shared/WebviewMessage" -import { Mode, getAllModes } from "../../../../src/shared/modes" -import { convertToMentionPath } from "../../utils/path-mentions" -import { SelectDropdown, DropdownOptionType } from "../ui" interface ChatTextAreaProps { inputValue: string diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5ac7f50559..09dcdd3ca1 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1275,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie modeShortcutText={modeShortcutText} /> -
+ ) } diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 1910fc0bb6..b6aaebd518 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, useCallback } from "react" +import { useState, useCallback } from "react" import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons" import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { useRooPortal } from "@/components/ui/hooks" import { vscode } from "../../../utils/vscode" import { Checkpoint } from "./schema" @@ -14,9 +15,9 @@ type CheckpointMenuProps = { } export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => { - const [portalContainer, setPortalContainer] = useState- I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access - to tools that let me create & edit files, explore complex projects, use the browser, and execute - terminal commands (with your permission, of course). I can even use MCP to create new tools and extend - my own capabilities. -
- - To get started, this extension needs an API provider. - -