mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add option to choose different models
This commit is contained in:
@@ -414,12 +414,9 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates cost of a Claude 3.5 Sonnet API request
|
|
||||||
calculateApiCost(inputTokens: number, outputTokens: number): number {
|
calculateApiCost(inputTokens: number, outputTokens: number): number {
|
||||||
const INPUT_COST_PER_MILLION = 3.0 // $3 per million input tokens
|
const inputCost = (this.api.getModel().info.inputPrice / 1_000_000) * inputTokens
|
||||||
const OUTPUT_COST_PER_MILLION = 15.0 // $15 per million output tokens
|
const outputCost = (this.api.getModel().info.outputPrice / 1_000_000) * outputTokens
|
||||||
const inputCost = (inputTokens / 1_000_000) * INPUT_COST_PER_MILLION
|
|
||||||
const outputCost = (outputTokens / 1_000_000) * OUTPUT_COST_PER_MILLION
|
|
||||||
const totalCost = inputCost + outputCost
|
const totalCost = inputCost + outputCost
|
||||||
return totalCost
|
return totalCost
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiHandler, withoutImageData } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../shared/api"
|
||||||
|
|
||||||
export class AnthropicHandler implements ApiHandler {
|
export class AnthropicHandler implements ApiHandler {
|
||||||
private options: ApiHandlerOptions
|
private options: ApiHandlerOptions
|
||||||
@@ -18,17 +18,20 @@ export class AnthropicHandler implements ApiHandler {
|
|||||||
): Promise<Anthropic.Messages.Message> {
|
): Promise<Anthropic.Messages.Message> {
|
||||||
return await this.client.messages.create(
|
return await this.client.messages.create(
|
||||||
{
|
{
|
||||||
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
|
model: this.getModel().id,
|
||||||
max_tokens: 8192, // beta max tokens
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools,
|
tools,
|
||||||
tool_choice: { type: "auto" },
|
tool_choice: { type: "auto" },
|
||||||
},
|
},
|
||||||
{
|
// https://x.com/alexalbert__/status/1812921642143900036
|
||||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||||
|
this.getModel().id === "claude-3-5-sonnet-20240620"
|
||||||
|
? {
|
||||||
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
|
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
|
||||||
}
|
}
|
||||||
|
: undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +44,21 @@ export class AnthropicHandler implements ApiHandler {
|
|||||||
>
|
>
|
||||||
): any {
|
): any {
|
||||||
return {
|
return {
|
||||||
model: "claude-3-5-sonnet-20240620",
|
model: this.getModel().id,
|
||||||
max_tokens: 8192,
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: { type: "auto" },
|
tool_choice: { type: "auto" },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getModel(): { id: AnthropicModelId; info: ModelInfo } {
|
||||||
|
const modelId = this.options.apiModelId
|
||||||
|
if (modelId && modelId in anthropicModels) {
|
||||||
|
const id = modelId as AnthropicModelId
|
||||||
|
return { id, info: anthropicModels[id] }
|
||||||
|
}
|
||||||
|
return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
|
||||||
import { ApiHandler, withoutImageData } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
|
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../shared/api"
|
||||||
|
|
||||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||||
export class AwsBedrockHandler implements ApiHandler {
|
export class AwsBedrockHandler implements ApiHandler {
|
||||||
@@ -28,8 +28,8 @@ export class AwsBedrockHandler implements ApiHandler {
|
|||||||
tools: Anthropic.Messages.Tool[]
|
tools: Anthropic.Messages.Tool[]
|
||||||
): Promise<Anthropic.Messages.Message> {
|
): Promise<Anthropic.Messages.Message> {
|
||||||
return await this.client.messages.create({
|
return await this.client.messages.create({
|
||||||
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
model: this.getModel().id,
|
||||||
max_tokens: 4096,
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools,
|
tools,
|
||||||
@@ -46,12 +46,21 @@ export class AwsBedrockHandler implements ApiHandler {
|
|||||||
>
|
>
|
||||||
): any {
|
): any {
|
||||||
return {
|
return {
|
||||||
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
model: this.getModel().id,
|
||||||
max_tokens: 4096,
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: { type: "auto" },
|
tool_choice: { type: "auto" },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getModel(): { id: BedrockModelId; info: ModelInfo } {
|
||||||
|
const modelId = this.options.apiModelId
|
||||||
|
if (modelId && modelId in bedrockModels) {
|
||||||
|
const id = modelId as BedrockModelId
|
||||||
|
return { id, info: bedrockModels[id] }
|
||||||
|
}
|
||||||
|
return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiConfiguration } from "../shared/api"
|
import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api"
|
||||||
import { AnthropicHandler } from "./anthropic"
|
import { AnthropicHandler } from "./anthropic"
|
||||||
import { AwsBedrockHandler } from "./bedrock"
|
import { AwsBedrockHandler } from "./bedrock"
|
||||||
import { OpenRouterHandler } from "./openrouter"
|
import { OpenRouterHandler } from "./openrouter"
|
||||||
@@ -19,6 +19,8 @@ export interface ApiHandler {
|
|||||||
| Anthropic.ToolResultBlockParam
|
| Anthropic.ToolResultBlockParam
|
||||||
>
|
>
|
||||||
): any
|
): any
|
||||||
|
|
||||||
|
getModel(): { id: ApiModelId; info: ModelInfo }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import OpenAI from "openai"
|
import OpenAI from "openai"
|
||||||
import { ApiHandler, withoutImageData } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
import {
|
||||||
|
ApiHandlerOptions,
|
||||||
|
ModelInfo,
|
||||||
|
openRouterDefaultModelId,
|
||||||
|
OpenRouterModelId,
|
||||||
|
openRouterModels,
|
||||||
|
} from "../shared/api"
|
||||||
|
|
||||||
export class OpenRouterHandler implements ApiHandler {
|
export class OpenRouterHandler implements ApiHandler {
|
||||||
private options: ApiHandlerOptions
|
private options: ApiHandlerOptions
|
||||||
@@ -41,8 +47,8 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const completion = await this.client.chat.completions.create({
|
const completion = await this.client.chat.completions.create({
|
||||||
model: "anthropic/claude-3.5-sonnet:beta",
|
model: this.getModel().id,
|
||||||
max_tokens: 4096,
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
messages: openAiMessages,
|
messages: openAiMessages,
|
||||||
tools: openAiTools,
|
tools: openAiTools,
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
@@ -258,11 +264,20 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
>
|
>
|
||||||
): any {
|
): any {
|
||||||
return {
|
return {
|
||||||
model: "anthropic/claude-3.5-sonnet:beta",
|
model: this.getModel().id,
|
||||||
max_tokens: 4096,
|
max_tokens: this.getModel().info.maxTokens,
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getModel(): { id: OpenRouterModelId; info: ModelInfo } {
|
||||||
|
const modelId = this.options.apiModelId
|
||||||
|
if (modelId && modelId in openRouterModels) {
|
||||||
|
const id = modelId as OpenRouterModelId
|
||||||
|
return { id, info: openRouterModels[id] }
|
||||||
|
}
|
||||||
|
return { id: openRouterDefaultModelId, info: openRouterModels[openRouterDefaultModelId] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { ClaudeDev } from "../ClaudeDev"
|
import { ClaudeDev } from "../ClaudeDev"
|
||||||
import { ApiProvider } from "../shared/api"
|
import { ApiModelId, ApiProvider } from "../shared/api"
|
||||||
import { ExtensionMessage } from "../shared/ExtensionMessage"
|
import { ExtensionMessage } from "../shared/ExtensionMessage"
|
||||||
import { WebviewMessage } from "../shared/WebviewMessage"
|
import { WebviewMessage } from "../shared/WebviewMessage"
|
||||||
import { downloadTask, getNonce, getUri, selectImages } from "../utils"
|
import { downloadTask, getNonce, getUri, selectImages } from "../utils"
|
||||||
@@ -11,7 +11,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey"
|
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey"
|
||||||
type GlobalStateKey = "apiProvider" | "awsRegion" | "maxRequestsPerTask" | "lastShownAnnouncementId"
|
type GlobalStateKey = "apiProvider" | "apiModelId" | "awsRegion" | "maxRequestsPerTask" | "lastShownAnnouncementId"
|
||||||
|
|
||||||
export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
||||||
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
||||||
@@ -132,15 +132,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
async initClaudeDevWithTask(task?: string, images?: string[]) {
|
async initClaudeDevWithTask(task?: string, images?: string[]) {
|
||||||
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||||
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
|
const { maxRequestsPerTask, apiConfiguration } = await this.getState()
|
||||||
await this.getState()
|
this.claudeDev = new ClaudeDev(this, apiConfiguration, maxRequestsPerTask, task, images)
|
||||||
this.claudeDev = new ClaudeDev(
|
|
||||||
this,
|
|
||||||
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
|
|
||||||
maxRequestsPerTask,
|
|
||||||
task,
|
|
||||||
images
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send any JSON serializable data to the react app
|
// Send any JSON serializable data to the react app
|
||||||
@@ -255,9 +248,17 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
break
|
break
|
||||||
case "apiConfiguration":
|
case "apiConfiguration":
|
||||||
if (message.apiConfiguration) {
|
if (message.apiConfiguration) {
|
||||||
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion } =
|
const {
|
||||||
message.apiConfiguration
|
apiProvider,
|
||||||
|
apiModelId,
|
||||||
|
apiKey,
|
||||||
|
openRouterApiKey,
|
||||||
|
awsAccessKey,
|
||||||
|
awsSecretKey,
|
||||||
|
awsRegion,
|
||||||
|
} = message.apiConfiguration
|
||||||
await this.updateGlobalState("apiProvider", apiProvider)
|
await this.updateGlobalState("apiProvider", apiProvider)
|
||||||
|
await this.updateGlobalState("apiModelId", apiModelId)
|
||||||
await this.storeSecret("apiKey", apiKey)
|
await this.storeSecret("apiKey", apiKey)
|
||||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||||
@@ -308,21 +309,12 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async postStateToWebview() {
|
async postStateToWebview() {
|
||||||
const {
|
const { apiConfiguration, maxRequestsPerTask, lastShownAnnouncementId } = await this.getState()
|
||||||
apiProvider,
|
|
||||||
apiKey,
|
|
||||||
openRouterApiKey,
|
|
||||||
awsAccessKey,
|
|
||||||
awsSecretKey,
|
|
||||||
awsRegion,
|
|
||||||
maxRequestsPerTask,
|
|
||||||
lastShownAnnouncementId,
|
|
||||||
} = await this.getState()
|
|
||||||
this.postMessageToWebview({
|
this.postMessageToWebview({
|
||||||
type: "state",
|
type: "state",
|
||||||
state: {
|
state: {
|
||||||
version: this.context.extension?.packageJSON?.version ?? "",
|
version: this.context.extension?.packageJSON?.version ?? "",
|
||||||
apiConfiguration: { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
|
apiConfiguration,
|
||||||
maxRequestsPerTask,
|
maxRequestsPerTask,
|
||||||
themeName: vscode.workspace.getConfiguration("workbench").get<string>("colorTheme"),
|
themeName: vscode.workspace.getConfiguration("workbench").get<string>("colorTheme"),
|
||||||
claudeMessages: this.claudeDev?.claudeMessages || [],
|
claudeMessages: this.claudeDev?.claudeMessages || [],
|
||||||
@@ -420,6 +412,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
async getState() {
|
async getState() {
|
||||||
const [
|
const [
|
||||||
apiProvider,
|
apiProvider,
|
||||||
|
apiModelId,
|
||||||
apiKey,
|
apiKey,
|
||||||
openRouterApiKey,
|
openRouterApiKey,
|
||||||
awsAccessKey,
|
awsAccessKey,
|
||||||
@@ -429,6 +422,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
|
this.getGlobalState("apiModelId") as Promise<ApiModelId | undefined>,
|
||||||
this.getSecret("apiKey") as Promise<string | undefined>,
|
this.getSecret("apiKey") as Promise<string | undefined>,
|
||||||
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
|
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
|
||||||
this.getSecret("awsAccessKey") as Promise<string | undefined>,
|
this.getSecret("awsAccessKey") as Promise<string | undefined>,
|
||||||
@@ -438,12 +432,15 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
|
apiConfiguration: {
|
||||||
apiProvider: apiProvider || "anthropic", // for legacy users that were using Anthropic by default
|
apiProvider: apiProvider || "anthropic", // for legacy users that were using Anthropic by default
|
||||||
|
apiModelId,
|
||||||
apiKey,
|
apiKey,
|
||||||
openRouterApiKey,
|
openRouterApiKey,
|
||||||
awsAccessKey,
|
awsAccessKey,
|
||||||
awsSecretKey,
|
awsSecretKey,
|
||||||
awsRegion,
|
awsRegion,
|
||||||
|
},
|
||||||
maxRequestsPerTask,
|
maxRequestsPerTask,
|
||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
|
export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
|
||||||
|
|
||||||
export interface ApiHandlerOptions {
|
export interface ApiHandlerOptions {
|
||||||
|
apiModelId?: ApiModelId
|
||||||
apiKey?: string // anthropic
|
apiKey?: string // anthropic
|
||||||
openRouterApiKey?: string
|
openRouterApiKey?: string
|
||||||
awsAccessKey?: string
|
awsAccessKey?: string
|
||||||
@@ -11,3 +12,69 @@ export interface ApiHandlerOptions {
|
|||||||
export type ApiConfiguration = ApiHandlerOptions & {
|
export type ApiConfiguration = ApiHandlerOptions & {
|
||||||
apiProvider?: ApiProvider
|
apiProvider?: ApiProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Models
|
||||||
|
|
||||||
|
export interface ModelInfo {
|
||||||
|
maxTokens: number
|
||||||
|
supportsImages: boolean
|
||||||
|
inputPrice: number
|
||||||
|
outputPrice: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiModelId = AnthropicModelId | OpenRouterModelId | BedrockModelId
|
||||||
|
|
||||||
|
// Anthropic
|
||||||
|
export type AnthropicModelId = keyof typeof anthropicModels
|
||||||
|
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20240620"
|
||||||
|
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||||
|
export const anthropicModels = {
|
||||||
|
"claude-3-5-sonnet-20240620": {
|
||||||
|
maxTokens: 8192,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 3.0, // $3 per million input tokens
|
||||||
|
outputPrice: 15.0, // $15 per million output tokens
|
||||||
|
},
|
||||||
|
"claude-3-opus-20240229": {
|
||||||
|
maxTokens: 4096,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 15.0,
|
||||||
|
outputPrice: 75.0,
|
||||||
|
},
|
||||||
|
"claude-3-sonnet-20240229": {
|
||||||
|
maxTokens: 4096,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 2.5,
|
||||||
|
outputPrice: 12.5,
|
||||||
|
},
|
||||||
|
"claude-3-haiku-20240307": {
|
||||||
|
maxTokens: 4096,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 2.5,
|
||||||
|
outputPrice: 12.5,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, ModelInfo>
|
||||||
|
|
||||||
|
// OpenRouter
|
||||||
|
export type OpenRouterModelId = keyof typeof openRouterModels
|
||||||
|
export const openRouterDefaultModelId: OpenRouterModelId = "anthropic/claude-3.5-sonnet:beta"
|
||||||
|
export const openRouterModels = {
|
||||||
|
"anthropic/claude-3.5-sonnet:beta": {
|
||||||
|
maxTokens: 4096,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 3.0,
|
||||||
|
outputPrice: 15.0,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, ModelInfo>
|
||||||
|
|
||||||
|
// AWS Bedrock
|
||||||
|
export type BedrockModelId = keyof typeof bedrockModels
|
||||||
|
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||||
|
export const bedrockModels = {
|
||||||
|
"anthropic.claude-3-5-sonnet-20240620-v1:0": {
|
||||||
|
maxTokens: 4096,
|
||||||
|
supportsImages: true,
|
||||||
|
inputPrice: 3.0,
|
||||||
|
outputPrice: 15.0,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, ModelInfo> // as const assertion makes the object deeply readonly (just declaring it as const makes it mutable)
|
||||||
|
|||||||
@@ -13,6 +13,86 @@ const rewire = require("rewire")
|
|||||||
const defaults = rewire("react-scripts/scripts/build.js")
|
const defaults = rewire("react-scripts/scripts/build.js")
|
||||||
const config = defaults.__get__("config")
|
const config = defaults.__get__("config")
|
||||||
|
|
||||||
|
/* Modifying Webpack Configuration for 'shared' dir
|
||||||
|
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
|
||||||
|
1. Uses the ModuleScopePlugin to whitelist files from the shared directory, allowing them to be imported despite being outside src/. (see: https://stackoverflow.com/questions/44114436/the-create-react-app-imports-restriction-outside-of-src-directory/58321458#58321458)
|
||||||
|
2. Modifies the TypeScript rule to include the shared directory in compilation. This essentially transpiles and includes the ts files in shared dir in the output main.js file.
|
||||||
|
Before, we would just import types from shared dir and specifying include (and alias to have cleaner paths) in tsconfig.json was enough. But now that we are creating values (i.e. models in api.ts) to import into the react app, we must also include these files in the webpack resolution.
|
||||||
|
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
|
||||||
|
- Example: import { someFunction } from '../../src/shared/utils/helpers'
|
||||||
|
*/
|
||||||
|
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
|
||||||
|
const path = require("path")
|
||||||
|
const fs = require("fs")
|
||||||
|
// Get all files in the shared directory
|
||||||
|
const sharedDir = path.resolve(__dirname, "../../src/shared")
|
||||||
|
function getAllFiles(dir) {
|
||||||
|
let files = []
|
||||||
|
fs.readdirSync(dir).forEach((file) => {
|
||||||
|
const filePath = path.join(dir, file)
|
||||||
|
if (fs.statSync(filePath).isDirectory()) {
|
||||||
|
files = files.concat(getAllFiles(filePath))
|
||||||
|
} else {
|
||||||
|
const withoutExtension = filePath.split(".")[0]
|
||||||
|
files.push(withoutExtension)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
const sharedFiles = getAllFiles(sharedDir)
|
||||||
|
// config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin))
|
||||||
|
// Instead of excluding the whole ModuleScopePlugin, we just whitelist specific files that can be imported from outside src.
|
||||||
|
config.resolve.plugins.forEach((plugin) => {
|
||||||
|
if (plugin instanceof ModuleScopePlugin) {
|
||||||
|
console.log("Whitelisting shared files: ", sharedFiles)
|
||||||
|
sharedFiles.forEach((file) => plugin.allowedFiles.add(file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
Webpack configuration
|
||||||
|
|
||||||
|
Webpack is a module bundler for JavaScript applications. It processes your project files, resolving dependencies and generating a deployable production build.
|
||||||
|
The webpack config is an object that tells webpack how to process and bundle your code. It defines entry points, output settings, and how to handle different file types.
|
||||||
|
This config.module section of the webpack config deals with how different file types (modules) should be treated.
|
||||||
|
config.module.rules:
|
||||||
|
Rules define how module files should be processed. Each rule can:
|
||||||
|
- Specify which files to process (test)
|
||||||
|
When webpack "processes" a file, it performs several operations:
|
||||||
|
1. Reads the file
|
||||||
|
2. Parses its content and analyzes dependencies
|
||||||
|
3. Applies transformations (e.g., converting TypeScript to JavaScript)
|
||||||
|
4. Potentially modifies the code (e.g., applying polyfills)
|
||||||
|
5. Includes the processed file in the final bundle
|
||||||
|
By specifying which files to process, we're telling webpack which files should go through this pipeline and be included in our application bundle. Files that aren't processed are ignored by webpack.
|
||||||
|
In our case, we're ensuring that TypeScript files in our shared directory are processed, allowing us to use them in our application.
|
||||||
|
- Define which folders to include or exclude
|
||||||
|
- Set which loaders to use for transformation
|
||||||
|
A loader transforms certain types of files into valid modules that webpack can process. For example, the TypeScript loader converts .ts files into JavaScript that webpack can understand.
|
||||||
|
By modifying these rules, we can change how webpack processes different files in our project, allowing us to include files from outside the standard src directory.
|
||||||
|
|
||||||
|
Why we need to modify the webpack config
|
||||||
|
|
||||||
|
Create React App (CRA) is designed to only process files within the src directory for security reasons. Our project structure includes a shared directory outside of src.
|
||||||
|
To use files from this shared directory, we need to:
|
||||||
|
1. Modify ModuleScopePlugin to allow imports from the shared directory.
|
||||||
|
2. Update the TypeScript loader rule to process TypeScript files from the shared directory.
|
||||||
|
These changes tell webpack it's okay to import from the shared directory and ensure that TypeScript files in this directory are properly converted to JavaScript.
|
||||||
|
|
||||||
|
Modify webpack configuration to process TypeScript files from shared directory
|
||||||
|
|
||||||
|
This code modifies the webpack configuration to allow processing of TypeScript files from our shared directory, which is outside the standard src folder.
|
||||||
|
1. config.module.rules[1]: In Create React App's webpack config, the second rule (index 1) typically contains the rules for processing JavaScript and TypeScript files.
|
||||||
|
2. .oneOf: This array contains a list of loaders, and webpack will use the first matching loader for each file. We iterate through these to find the TypeScript loader.
|
||||||
|
3. We check each rule to see if it applies to TypeScript files by looking for 'ts|tsx' in the test regex.
|
||||||
|
4. When we find the TypeScript rule, we add our shared directory to its 'include' array. This tells webpack to also process TypeScript files from the shared directory.
|
||||||
|
Note: This code assumes a specific structure in the CRA webpack config. If CRA updates its config structure in future versions, this code might need to be adjusted.
|
||||||
|
*/
|
||||||
|
config.module.rules[1].oneOf.forEach((rule) => {
|
||||||
|
if (rule.test && rule.test.toString().includes("ts|tsx")) {
|
||||||
|
rule.include = [...(rule.include || []), sharedDir]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Disable code splitting
|
// Disable code splitting
|
||||||
config.optimization.splitChunks = {
|
config.optimization.splitChunks = {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import React, { useEffect, useState, useCallback } from "react"
|
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
|
import { useEvent } from "react-use"
|
||||||
|
import { ApiConfiguration } from "../../src/shared/api"
|
||||||
|
import { ClaudeMessage, ExtensionMessage } from "../../src/shared/ExtensionMessage"
|
||||||
import "./App.css"
|
import "./App.css"
|
||||||
|
import { normalizeApiConfiguration } from "./components/ApiOptions"
|
||||||
import ChatView from "./components/ChatView"
|
import ChatView from "./components/ChatView"
|
||||||
import SettingsView from "./components/SettingsView"
|
import SettingsView from "./components/SettingsView"
|
||||||
import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
|
|
||||||
import WelcomeView from "./components/WelcomeView"
|
import WelcomeView from "./components/WelcomeView"
|
||||||
import { vscode } from "./utils/vscode"
|
import { vscode } from "./utils/vscode"
|
||||||
import { useEvent } from "react-use"
|
|
||||||
import { ApiConfiguration } from "@shared/api"
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
|
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
|
||||||
|
|
||||||
The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again.
|
The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again.
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
@@ -70,6 +69,10 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
useEvent("message", handleMessage)
|
useEvent("message", handleMessage)
|
||||||
|
|
||||||
|
const { selectedModelInfo } = useMemo(() => {
|
||||||
|
return normalizeApiConfiguration(apiConfiguration)
|
||||||
|
}, [apiConfiguration])
|
||||||
|
|
||||||
if (!didHydrateState) {
|
if (!didHydrateState) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -97,6 +100,7 @@ const App: React.FC = () => {
|
|||||||
isHidden={showSettings}
|
isHidden={showSettings}
|
||||||
vscodeThemeName={vscodeThemeName}
|
vscodeThemeName={vscodeThemeName}
|
||||||
showAnnouncement={showAnnouncement}
|
showAnnouncement={showAnnouncement}
|
||||||
|
selectedModelSupportsImages={selectedModelInfo.supportsImages}
|
||||||
hideAnnouncement={() => setShowAnnouncement(false)}
|
hideAnnouncement={() => setShowAnnouncement(false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,34 +1,78 @@
|
|||||||
import { ApiConfiguration } from "@shared/api"
|
|
||||||
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||||
import React from "react"
|
import React, { useMemo } from "react"
|
||||||
|
import {
|
||||||
|
ApiConfiguration,
|
||||||
|
ApiModelId,
|
||||||
|
ModelInfo,
|
||||||
|
anthropicDefaultModelId,
|
||||||
|
anthropicModels,
|
||||||
|
bedrockDefaultModelId,
|
||||||
|
bedrockModels,
|
||||||
|
openRouterDefaultModelId,
|
||||||
|
openRouterModels,
|
||||||
|
} from "../../../src/shared/api"
|
||||||
|
|
||||||
interface ApiOptionsProps {
|
interface ApiOptionsProps {
|
||||||
|
showModelOptions: boolean
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
|
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfiguration }) => {
|
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfiguration, setApiConfiguration }) => {
|
||||||
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
||||||
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
|
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||||
|
return normalizeApiConfiguration(apiConfiguration)
|
||||||
|
}, [apiConfiguration])
|
||||||
|
|
||||||
|
/*
|
||||||
|
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
|
||||||
|
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
|
||||||
|
|
||||||
|
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
|
||||||
|
|
||||||
|
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
|
||||||
|
*/
|
||||||
|
const createDropdown = (models: Record<string, ModelInfo>) => {
|
||||||
|
return (
|
||||||
|
<VSCodeDropdown
|
||||||
|
id="model-id"
|
||||||
|
value={selectedModelId}
|
||||||
|
onChange={handleInputChange("apiModelId")}
|
||||||
|
style={{ width: "100%" }}>
|
||||||
|
<VSCodeOption value="">Select a model...</VSCodeOption>
|
||||||
|
{Object.keys(models).map((modelId) => (
|
||||||
|
<VSCodeOption
|
||||||
|
key={modelId}
|
||||||
|
value={modelId}
|
||||||
|
style={{
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}>
|
||||||
|
{modelId}
|
||||||
|
</VSCodeOption>
|
||||||
|
))}
|
||||||
|
</VSCodeDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||||
<div className="dropdown-container">
|
<div className="dropdown-container">
|
||||||
<label htmlFor="api-provider">
|
<label htmlFor="api-provider">
|
||||||
<span style={{ fontWeight: 500 }}>API Provider</span>
|
<span style={{ fontWeight: 500 }}>API Provider</span>
|
||||||
</label>
|
</label>
|
||||||
<VSCodeDropdown
|
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
|
||||||
id="api-provider"
|
|
||||||
value={apiConfiguration?.apiProvider || "anthropic"}
|
|
||||||
onChange={handleInputChange("apiProvider")}>
|
|
||||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||||
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
||||||
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
||||||
</VSCodeDropdown>
|
</VSCodeDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apiConfiguration?.apiProvider === "anthropic" && (
|
{selectedProvider === "anthropic" && (
|
||||||
<div>
|
<div>
|
||||||
<VSCodeTextField
|
<VSCodeTextField
|
||||||
value={apiConfiguration?.apiKey || ""}
|
value={apiConfiguration?.apiKey || ""}
|
||||||
@@ -51,7 +95,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{apiConfiguration?.apiProvider === "openrouter" && (
|
{selectedProvider === "openrouter" && (
|
||||||
<div>
|
<div>
|
||||||
<VSCodeTextField
|
<VSCodeTextField
|
||||||
value={apiConfiguration?.openRouterApiKey || ""}
|
value={apiConfiguration?.openRouterApiKey || ""}
|
||||||
@@ -74,7 +118,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{apiConfiguration?.apiProvider === "bedrock" && (
|
{selectedProvider === "bedrock" && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||||
<VSCodeTextField
|
<VSCodeTextField
|
||||||
value={apiConfiguration?.awsAccessKey || ""}
|
value={apiConfiguration?.awsAccessKey || ""}
|
||||||
@@ -100,9 +144,9 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
|
|||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
onChange={handleInputChange("awsRegion")}>
|
onChange={handleInputChange("awsRegion")}>
|
||||||
<VSCodeOption value="">Select a region...</VSCodeOption>
|
<VSCodeOption value="">Select a region...</VSCodeOption>
|
||||||
{/* Currently Claude 3.5 Sonnet is only available in us-east-1 */}
|
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
|
||||||
<VSCodeOption value="us-east-1">US East (N. Virginia)</VSCodeOption>
|
<VSCodeOption value="us-east-1">US East (N. Virginia)</VSCodeOption>
|
||||||
{/* <VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
|
<VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
|
||||||
<VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption>
|
<VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption>
|
||||||
<VSCodeOption value="us-west-2">US West (Oregon)</VSCodeOption>
|
<VSCodeOption value="us-west-2">US West (Oregon)</VSCodeOption>
|
||||||
<VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption>
|
<VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption>
|
||||||
@@ -120,7 +164,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
|
|||||||
<VSCodeOption value="eu-west-3">Europe (Paris)</VSCodeOption>
|
<VSCodeOption value="eu-west-3">Europe (Paris)</VSCodeOption>
|
||||||
<VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption>
|
<VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption>
|
||||||
<VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption>
|
<VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption>
|
||||||
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption> */}
|
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption>
|
||||||
</VSCodeDropdown>
|
</VSCodeDropdown>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
@@ -138,8 +182,91 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showModelOptions && (
|
||||||
|
<>
|
||||||
|
<div className="dropdown-container">
|
||||||
|
<label htmlFor="model-id">
|
||||||
|
<span style={{ fontWeight: 500 }}>Model</span>
|
||||||
|
</label>
|
||||||
|
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
|
||||||
|
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
|
||||||
|
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModelInfoView modelInfo={selectedModelInfo} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 500,
|
||||||
|
color: modelInfo.supportsImages
|
||||||
|
? "var(--vscode-testing-iconPassed)"
|
||||||
|
: "var(--vscode-errorForeground)",
|
||||||
|
}}>
|
||||||
|
<i
|
||||||
|
className={`codicon codicon-${modelInfo.supportsImages ? "check" : "x"}`}
|
||||||
|
style={{
|
||||||
|
marginRight: 4,
|
||||||
|
marginBottom: modelInfo.supportsImages ? 1 : -1,
|
||||||
|
fontSize: modelInfo.supportsImages ? 11 : 13,
|
||||||
|
fontWeight: 700,
|
||||||
|
display: "inline-block",
|
||||||
|
verticalAlign: "bottom",
|
||||||
|
}}></i>
|
||||||
|
{modelInfo.supportsImages ? "Supports images" : "Does not support images"}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens.toLocaleString()} tokens
|
||||||
|
<br />
|
||||||
|
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)} per million tokens
|
||||||
|
<br />
|
||||||
|
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)} per million
|
||||||
|
tokens
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
||||||
|
const provider = apiConfiguration?.apiProvider || "anthropic"
|
||||||
|
const modelId = apiConfiguration?.apiModelId
|
||||||
|
|
||||||
|
const getProviderData = (models: Record<string, ModelInfo>, defaultId: ApiModelId) => {
|
||||||
|
let selectedModelId: ApiModelId
|
||||||
|
let selectedModelInfo: ModelInfo
|
||||||
|
if (modelId && modelId in models) {
|
||||||
|
selectedModelId = modelId
|
||||||
|
selectedModelInfo = models[modelId]
|
||||||
|
} else {
|
||||||
|
selectedModelId = defaultId
|
||||||
|
selectedModelInfo = models[defaultId]
|
||||||
|
}
|
||||||
|
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
|
||||||
|
}
|
||||||
|
switch (provider) {
|
||||||
|
case "anthropic":
|
||||||
|
return getProviderData(anthropicModels, anthropicDefaultModelId)
|
||||||
|
case "openrouter":
|
||||||
|
return getProviderData(openRouterModels, openRouterDefaultModelId)
|
||||||
|
case "bedrock":
|
||||||
|
return getProviderData(bedrockModels, bedrockDefaultModelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default ApiOptions
|
export default ApiOptions
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
|
|
||||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
import Markdown from "react-markdown"
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
|
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||||
import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences"
|
import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences"
|
||||||
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
||||||
import CodeBlock from "./CodeBlock/CodeBlock"
|
import CodeBlock from "./CodeBlock/CodeBlock"
|
||||||
import Markdown from "react-markdown"
|
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
|
|
||||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"
|
import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"
|
||||||
import DynamicTextArea from "react-textarea-autosize"
|
import DynamicTextArea from "react-textarea-autosize"
|
||||||
import { useEvent, useMount } from "react-use"
|
import { useEvent, useMount } from "react-use"
|
||||||
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||||
|
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
import { combineApiRequests } from "../utils/combineApiRequests"
|
import { combineApiRequests } from "../utils/combineApiRequests"
|
||||||
import { combineCommandSequences } from "../utils/combineCommandSequences"
|
import { combineCommandSequences } from "../utils/combineCommandSequences"
|
||||||
import { getApiMetrics } from "../utils/getApiMetrics"
|
import { getApiMetrics } from "../utils/getApiMetrics"
|
||||||
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
|
import Announcement from "./Announcement"
|
||||||
import ChatRow from "./ChatRow"
|
import ChatRow from "./ChatRow"
|
||||||
import TaskHeader from "./TaskHeader"
|
import TaskHeader from "./TaskHeader"
|
||||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
|
||||||
import Announcement from "./Announcement"
|
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface ChatViewProps {
|
interface ChatViewProps {
|
||||||
@@ -21,6 +21,7 @@ interface ChatViewProps {
|
|||||||
isHidden: boolean
|
isHidden: boolean
|
||||||
vscodeThemeName?: string
|
vscodeThemeName?: string
|
||||||
showAnnouncement: boolean
|
showAnnouncement: boolean
|
||||||
|
selectedModelSupportsImages: boolean
|
||||||
hideAnnouncement: () => void
|
hideAnnouncement: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ const ChatView = ({
|
|||||||
isHidden,
|
isHidden,
|
||||||
vscodeThemeName,
|
vscodeThemeName,
|
||||||
showAnnouncement,
|
showAnnouncement,
|
||||||
|
selectedModelSupportsImages,
|
||||||
hideAnnouncement,
|
hideAnnouncement,
|
||||||
}: ChatViewProps) => {
|
}: ChatViewProps) => {
|
||||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
|
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
|
||||||
@@ -278,6 +280,11 @@ const ChatView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
|
if (shouldDisableImages) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const items = e.clipboardData.items
|
const items = e.clipboardData.items
|
||||||
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
|
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
|
||||||
const imageItems = Array.from(items).filter((item) => {
|
const imageItems = Array.from(items).filter((item) => {
|
||||||
@@ -412,6 +419,12 @@ const ChatView = ({
|
|||||||
return [text, false]
|
return [text, false]
|
||||||
}, [task, messages])
|
}, [task, messages])
|
||||||
|
|
||||||
|
const shouldDisableImages =
|
||||||
|
!selectedModelSupportsImages ||
|
||||||
|
textAreaDisabled ||
|
||||||
|
selectedImages.length >= MAX_IMAGES_PER_MESSAGE ||
|
||||||
|
isInputPipingToStdin
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -590,9 +603,7 @@ const ChatView = ({
|
|||||||
height: "calc(100% - 20px)", // Full height minus top and bottom padding
|
height: "calc(100% - 20px)", // Full height minus top and bottom padding
|
||||||
}}>
|
}}>
|
||||||
<VSCodeButton
|
<VSCodeButton
|
||||||
disabled={
|
disabled={shouldDisableImages}
|
||||||
textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE || isInputPipingToStdin
|
|
||||||
}
|
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
aria-label="Attach Images"
|
aria-label="Attach Images"
|
||||||
onClick={selectImages}
|
onClick={selectImages}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiConfiguration } from "@shared/api"
|
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
|
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import ApiOptions from "./ApiOptions"
|
import ApiOptions from "./ApiOptions"
|
||||||
@@ -60,7 +60,19 @@ const SettingsView = ({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
padding: "10px 18px 18px 20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
|
<div style={{ flexGrow: 1, overflow: "auto" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -73,7 +85,11 @@ const SettingsView = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
<ApiOptions
|
||||||
|
apiConfiguration={apiConfiguration}
|
||||||
|
setApiConfiguration={setApiConfiguration}
|
||||||
|
showModelOptions={true}
|
||||||
|
/>
|
||||||
{apiErrorMessage && (
|
{apiErrorMessage && (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
@@ -100,8 +116,8 @@ const SettingsView = ({
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
|
If Claude Dev reaches this limit, it will pause and ask for your permission before making
|
||||||
requests.
|
additional requests.
|
||||||
</p>
|
</p>
|
||||||
{maxRequestsErrorMessage && (
|
{maxRequestsErrorMessage && (
|
||||||
<p
|
<p
|
||||||
@@ -114,24 +130,22 @@ const SettingsView = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<VSCodeDivider />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "20px",
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
lineHeight: "1.2",
|
lineHeight: "1.2",
|
||||||
}}>
|
}}>
|
||||||
<p style={{ wordWrap: "break-word" }}>
|
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||||
If you have any questions or feedback, feel free to open an issue at{" "}
|
If you have any questions or feedback, feel free to open an issue at{" "}
|
||||||
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev" style={{ display: "inline" }}>
|
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev" style={{ display: "inline" }}>
|
||||||
https://github.com/saoudrizwan/claude-dev
|
https://github.com/saoudrizwan/claude-dev
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontStyle: "italic" }}>v{version}</p>
|
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
|||||||
import React, { useEffect, useRef, useState } from "react"
|
import React, { useEffect, useRef, useState } from "react"
|
||||||
import TextTruncate from "react-text-truncate"
|
import TextTruncate from "react-text-truncate"
|
||||||
import { useWindowSize } from "react-use"
|
import { useWindowSize } from "react-use"
|
||||||
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import { ClaudeMessage } from "@shared/ExtensionMessage"
|
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface TaskHeaderProps {
|
interface TaskHeaderProps {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiConfiguration } from "@shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import { validateApiConfiguration } from "../utils/validate"
|
import { validateApiConfiguration } from "../utils/validate"
|
||||||
@@ -40,7 +40,11 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfi
|
|||||||
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
|
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
|
||||||
|
|
||||||
<div style={{ marginTop: "15px" }}>
|
<div style={{ marginTop: "15px" }}>
|
||||||
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
<ApiOptions
|
||||||
|
apiConfiguration={apiConfiguration}
|
||||||
|
setApiConfiguration={setApiConfiguration}
|
||||||
|
showModelOptions={false}
|
||||||
|
/>
|
||||||
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
|
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
|
||||||
Let's go!
|
Let's go!
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClaudeMessage } from "@shared/ExtensionMessage"
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines API request start and finish messages in an array of ClaudeMessages.
|
* Combines API request start and finish messages in an array of ClaudeMessages.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClaudeMessage } from "@shared/ExtensionMessage"
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines sequences of command and command_output messages in an array of ClaudeMessages.
|
* Combines sequences of command and command_output messages in an array of ClaudeMessages.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClaudeMessage } from "@shared/ExtensionMessage"
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
|
|
||||||
interface ApiMetrics {
|
interface ApiMetrics {
|
||||||
totalTokensIn: number
|
totalTokensIn: number
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ClaudeMessage } from "@shared/ExtensionMessage";
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
|
|
||||||
export const mockMessages: ClaudeMessage[] = [
|
export const mockMessages: ClaudeMessage[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiConfiguration } from "@shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
|
|
||||||
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
||||||
if (apiConfiguration) {
|
if (apiConfiguration) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WebviewMessage } from "@shared/WebviewMessage"
|
import { WebviewMessage } from "../../../src/shared/WebviewMessage"
|
||||||
import type { WebviewApi } from "vscode-webview"
|
import type { WebviewApi } from "vscode-webview"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,10 +14,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx"
|
||||||
"paths": {
|
|
||||||
"@shared/*": ["../src/shared/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src", "../src/shared"]
|
"include": ["src", "../src/shared"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user