Add Kodu provider

This commit is contained in:
Saoud Rizwan
2024-08-24 09:18:27 -04:00
parent cc9637e0fe
commit df4e8e7afc
19 changed files with 380 additions and 305 deletions

70
package-lock.json generated
View File

@@ -1,16 +1,17 @@
{
"name": "claude-dev",
"version": "1.3.43",
"version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "1.3.43",
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
"@kodu-ai/cloud-api": "^1.0.1",
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"default-shell": "^2.2.0",
@@ -2768,6 +2769,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kodu-ai/cloud-api": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@kodu-ai/cloud-api/-/cloud-api-1.0.1.tgz",
"integrity": "sha512-CnmZP4Gm72ReqSO8qZgRYgbuGbo6A7RBbG/kNjMSS9KsDmtXCQu8OIEj/ZBVdaW+vm32t633r7q4nLKv3SaUHQ==",
"dependencies": {
"@trpc/client": "^11.0.0-rc.485",
"@trpc/server": "^11.0.0-rc.485",
"superjson": "^2.2.1"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4480,6 +4491,25 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/@trpc/client": {
"version": "11.0.0-rc.485",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.485.tgz",
"integrity": "sha512-Ld1gQjdYyrku0rjP/4QMg/SdsKgujr0P5XNoWkCyPRjdw3PuJbZFebauQPRC17cbbqGcpJrR+T3vnkhjMb1sgw==",
"funding": [
"https://trpc.io/sponsor"
],
"peerDependencies": {
"@trpc/server": "11.0.0-rc.485+1c1d824cd"
}
},
"node_modules/@trpc/server": {
"version": "11.0.0-rc.485",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.485.tgz",
"integrity": "sha512-U9SK9jbqCjR8S9wGSe4UBu2e0fqxhQWriZiDb5BLzdxXzls4Jv+XhAkI65yBzlcTbt6VqXegZDAXB3IARPhUCg==",
"funding": [
"https://trpc.io/sponsor"
]
},
"node_modules/@types/diff": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz",
@@ -5399,6 +5429,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -7199,6 +7243,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -9318,6 +9373,17 @@
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"node_modules/superjson": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz",
"integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/supports-color": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",

View File

@@ -134,6 +134,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
"@kodu-ai/cloud-api": "^1.0.1",
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"default-shell": "^2.2.0",

View File

@@ -3,7 +3,7 @@ import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api"
import { AnthropicHandler } from "./anthropic"
import { AwsBedrockHandler } from "./bedrock"
import { OpenRouterHandler } from "./openrouter"
import { MaestroHandler } from "./maestro"
import { KoduHandler } from "./kodu"
export interface ApiHandler {
createMessage(
@@ -33,8 +33,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new OpenRouterHandler(options)
case "bedrock":
return new AwsBedrockHandler(options)
case "maestro":
return new MaestroHandler(options)
case "kodu":
return new KoduHandler(options)
default:
return new AnthropicHandler(options)
}

125
src/api/kodu.ts Normal file
View File

@@ -0,0 +1,125 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler, withoutImageData } from "."
import { ApiHandlerOptions, koduDefaultModelId, KoduModelId, koduModels, ModelInfo } from "../shared/api"
import axios from "axios"
import * as vscode from "vscode"
const KODU_BASE_URL = "https://claude-dev.com"
export function didClickKoduSignIn() {
const loginUrl = `${KODU_BASE_URL}/auth/login?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1`
vscode.env.openExternal(vscode.Uri.parse(loginUrl))
}
export function didClickKoduAddCredits() {
const addCreditsUrl = `${KODU_BASE_URL}/user/addCredits?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1`
vscode.env.openExternal(vscode.Uri.parse(addCreditsUrl))
}
export async function fetchKoduCredits({ apiKey }: { apiKey: string }) {
const response = await axios.get(`${KODU_BASE_URL}/api/credits`, {
headers: {
"x-api-key": apiKey,
},
})
return (response.data.credits as number) || 0
}
export class KoduHandler implements ApiHandler {
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
}
async createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools: Anthropic.Messages.Tool[]
): Promise<Anthropic.Messages.Message> {
const modelId = this.getModel().id
let requestBody: Anthropic.Beta.PromptCaching.Messages.MessageCreateParamsNonStreaming
switch (modelId) {
case "claude-3-5-sonnet-20240620":
case "claude-3-haiku-20240307":
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[]
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
requestBody = {
model: modelId,
max_tokens: this.getModel().info.maxTokens,
system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
cache_control: { type: "ephemeral" },
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: { type: "ephemeral" } }
: content
),
}
}
return message
}),
tools,
tool_choice: { type: "auto" },
}
break
default:
requestBody = {
model: modelId,
max_tokens: this.getModel().info.maxTokens,
system: [{ text: systemPrompt, type: "text" }],
messages,
tools,
tool_choice: { type: "auto" },
}
}
const response = await axios.post(`${KODU_BASE_URL}/api/inference`, requestBody, {
headers: {
"x-api-key": this.options.koduApiKey,
},
})
return response.data
}
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any {
return {
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
getModel(): { id: KoduModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in koduModels) {
const id = modelId as KoduModelId
return { id, info: koduModels[id] }
}
return { id: koduDefaultModelId, info: koduModels[koduDefaultModelId] }
}
}

View File

@@ -1,114 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler, withoutImageData } from "."
import { ApiHandlerOptions, maestroDefaultModelId, MaestroModelId, maestroModels, ModelInfo } from "../shared/api"
export class MaestroHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Anthropic({ apiKey: this.options.apiKey })
}
async createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools: Anthropic.Messages.Tool[]
): Promise<Anthropic.Messages.Message> {
const modelId = this.getModel().id
switch (modelId) {
case "claude-3-5-sonnet-20240620":
case "claude-3-haiku-20240307":
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[]
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
return await this.client.beta.promptCaching.messages.create(
{
model: modelId,
max_tokens: this.getModel().info.maxTokens,
system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
cache_control: { type: "ephemeral" },
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: { type: "ephemeral" } }
: content
),
}
}
return message
}),
tools,
tool_choice: { type: "auto" },
},
(() => {
switch (modelId) {
case "claude-3-5-sonnet-20240620":
return {
headers: {
"anthropic-beta": "prompt-caching-2024-07-31",
},
}
case "claude-3-haiku-20240307":
return {
headers: { "anthropic-beta": "prompt-caching-2024-07-31" },
}
default:
return undefined
}
})()
)
default:
return await this.client.messages.create({
model: modelId,
max_tokens: this.getModel().info.maxTokens,
system: [{ text: systemPrompt, type: "text" }],
messages,
tools,
tool_choice: { type: "auto" },
})
}
}
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any {
return {
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
getModel(): { id: MaestroModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in maestroModels) {
const id = modelId as MaestroModelId
return { id, info: maestroModels[id] }
}
return { id: maestroDefaultModelId, info: maestroModels[maestroDefaultModelId] }
}
}

View File

@@ -111,14 +111,11 @@ export function activate(context: vscode.ExtensionContext) {
// URI Handler
const handleUri = async (uri: vscode.Uri) => {
const query = new URLSearchParams(uri.query)
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const token = query.get("token")
const fixedToken = token?.replaceAll("jwt?token=", "")
console.log(fixedToken)
console.log(uri)
if (fixedToken) {
await sidebarProvider.saveMaestroToken(fixedToken)
const email = query.get("email")
if (token) {
await sidebarProvider.saveKoduApiKey(token, email || undefined)
}
}
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))

View File

@@ -1,38 +0,0 @@
import axios from "axios"
import * as vscode from "vscode"
import { MaestroUser, MaestroUserSchema } from "../shared/maestro"
const MAESTRO_BASE_URL = "https://maestro.im-ada.ai"
export function didClickMaestroSignIn() {
const loginUrl = `${MAESTRO_BASE_URL}/auth/login?ext=1&redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev?token=jwt`
vscode.env.openExternal(vscode.Uri.parse(loginUrl))
}
export async function validateMaestroToken({
token,
showError = false,
}: {
token: string
showError?: boolean
}): Promise<MaestroUser> {
try {
const response = await axios.post(`${MAESTRO_BASE_URL}/api/extension/auth/callback`, { token })
const user = MaestroUserSchema.parse(response.data.user)
console.log("retrieved user", user)
return user
} catch (error) {
if (showError) {
if (axios.isAxiosError(error)) {
vscode.window.showErrorMessage(
"Failed to validate token:",
error.response?.status,
error.response?.data
)
} else {
vscode.window.showErrorMessage("An unexpected error occurred:", error)
}
}
throw error
}
}

View File

@@ -1 +0,0 @@
export * from "./auth"

View File

@@ -8,8 +8,7 @@ import { downloadTask, getNonce, getUri, selectImages } from "../utils"
import * as path from "path"
import fs from "fs/promises"
import { HistoryItem } from "../shared/HistoryItem"
import { didClickMaestroSignIn, validateMaestroToken } from "../maestro"
import { MaestroUser } from "../shared/maestro"
import { didClickKoduAddCredits, didClickKoduSignIn, fetchKoduCredits } from "../api/kodu"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -17,11 +16,13 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey" | "maestroToken"
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey" | "koduApiKey"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "koduEmail"
| "koduCredits"
| "maxRequestsPerTask"
| "lastShownAnnouncementId"
| "customInstructions"
@@ -34,11 +35,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
private view?: vscode.WebviewView | vscode.WebviewPanel
private claudeDev?: ClaudeDev
private latestAnnouncementId = "aug-17-2024" // update to some unique identifier when we add a new announcement
private maestroUser?: MaestroUser
constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
this.outputChannel.appendLine("ClaudeDevProvider instantiated")
this.fetchMaestroUser({})
}
/*
@@ -344,11 +343,23 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
case "exportTaskWithId":
this.exportTaskWithId(message.text!)
break
case "didClickMaestroSignIn":
didClickMaestroSignIn()
case "didClickKoduSignIn":
didClickKoduSignIn()
break
case "didClickMaestroSignOut":
await this.signOutMaestro()
case "didClickKoduSignOut":
await this.signOutKodu()
break
case "didClickKoduAddCredits":
didClickKoduAddCredits()
break
case "fetchKoduCredits":
const koduApiKey = await this.getSecret("koduApiKey")
if (koduApiKey) {
const credits = await fetchKoduCredits({ apiKey: koduApiKey })
await this.updateGlobalState("koduCredits", credits)
await this.postStateToWebview()
await this.postMessageToWebview({ type: "action", action: "koduCreditsFetched" })
}
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
@@ -359,33 +370,21 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
)
}
// Maestro
// Kodu
async saveMaestroToken(token: string) {
await this.storeSecret("maestroToken", token)
await this.updateGlobalState("apiProvider", "maestro")
await this.fetchMaestroUser({ showError: true })
this.claudeDev?.updateApi({ apiProvider: "maestro", maestroToken: token })
}
async fetchMaestroUser({ showError = false }: { showError?: boolean }): Promise<MaestroUser | undefined> {
if (this.maestroUser) {
return this.maestroUser
}
const token = await this.getSecret("maestroToken")
if (!token) {
return undefined
}
const user = await validateMaestroToken({ token, showError })
this.maestroUser = user
async saveKoduApiKey(apiKey: string, email?: string) {
await this.storeSecret("koduApiKey", apiKey)
await this.updateGlobalState("koduEmail", email)
await this.updateGlobalState("apiProvider", "kodu")
await this.postStateToWebview()
return user
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: apiKey })
}
async signOutMaestro() {
await this.storeSecret("maestroToken", undefined)
this.claudeDev?.updateApi({ apiProvider: "maestro", maestroToken: undefined })
this.maestroUser = undefined
async signOutKodu() {
await this.storeSecret("koduApiKey", undefined)
await this.updateGlobalState("koduEmail", undefined)
await this.updateGlobalState("koduCredits", undefined)
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: undefined })
await this.postStateToWebview()
}
@@ -476,8 +475,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
}
async postStateToWebview() {
const { apiConfiguration, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, taskHistory } =
await this.getState()
const {
apiConfiguration,
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
taskHistory,
koduCredits,
} = await this.getState()
this.postMessageToWebview({
type: "state",
state: {
@@ -489,7 +494,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
claudeMessages: this.claudeDev?.claudeMessages || [],
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
maestroUser: this.maestroUser,
koduCredits,
},
})
}
@@ -589,6 +594,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
awsAccessKey,
awsSecretKey,
awsRegion,
koduApiKey,
koduEmail,
koduCredits,
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
@@ -601,6 +609,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.getSecret("awsAccessKey") as Promise<string | undefined>,
this.getSecret("awsSecretKey") as Promise<string | undefined>,
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getSecret("koduApiKey") as Promise<string | undefined>,
this.getGlobalState("koduEmail") as Promise<string | undefined>,
this.getGlobalState("koduCredits") as Promise<number | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
this.getGlobalState("customInstructions") as Promise<string | undefined>,
@@ -616,8 +627,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to anthropic (openrouter has issues, bedrock is complicated)
apiProvider = "anthropic"
// New users should default to kodu
apiProvider = "kodu"
}
}
@@ -630,11 +641,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
awsAccessKey,
awsSecretKey,
awsRegion,
koduApiKey,
koduEmail,
},
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
taskHistory,
koduCredits,
}
}

View File

@@ -2,13 +2,17 @@
import { ApiConfiguration } from "./api"
import { HistoryItem } from "./HistoryItem"
import { MaestroUser } from "./maestro"
// webview will hold state
export interface ExtensionMessage {
type: "action" | "state" | "selectedImages"
text?: string
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
action?:
| "chatButtonTapped"
| "settingsButtonTapped"
| "historyButtonTapped"
| "didBecomeVisible"
| "koduCreditsFetched"
state?: ExtensionState
images?: string[]
}
@@ -22,7 +26,7 @@ export interface ExtensionState {
claudeMessages: ClaudeMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
maestroUser?: MaestroUser
koduCredits?: number
}
export interface ClaudeMessage {

View File

@@ -15,8 +15,10 @@ export interface WebviewMessage {
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
| "didClickMaestroSignIn"
| "didClickMaestroSignOut"
| "didClickKoduSignIn"
| "didClickKoduSignOut"
| "didClickKoduAddCredits"
| "fetchKoduCredits"
text?: string
askResponse?: ClaudeAskResponse
apiConfiguration?: ApiConfiguration

View File

@@ -1,4 +1,4 @@
export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "maestro"
export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "kodu"
export interface ApiHandlerOptions {
apiModelId?: ApiModelId
@@ -7,7 +7,8 @@ export interface ApiHandlerOptions {
awsAccessKey?: string
awsSecretKey?: string
awsRegion?: string
maestroToken?: string
koduApiKey?: string
koduEmail?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -234,9 +235,9 @@ export const openRouterModels = {
// },
} as const satisfies Record<string, ModelInfo>
// Maestro
export type MaestroModelId = keyof typeof maestroModels
export const maestroDefaultModelId: MaestroModelId = "claude-3-5-sonnet-20240620"
export const maestroModels = {
// Kodu
export type KoduModelId = keyof typeof koduModels
export const koduDefaultModelId: KoduModelId = "claude-3-5-sonnet-20240620"
export const koduModels = {
...anthropicModels,
} as const satisfies Record<string, ModelInfo>

View File

@@ -1,10 +0,0 @@
import { z } from "zod"
export const MaestroUserSchema = z.object({
id: z.string(),
image: z.string().nullable(),
email: z.string().email(),
name: z.string().nullable(),
emailVerified: z.coerce.date().nullable(),
})
export type MaestroUser = z.infer<typeof MaestroUserSchema>

View File

@@ -26,8 +26,7 @@
"react-virtuoso": "^4.7.13",
"rewire": "^7.0.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4",
"zod": "^3.23.8"
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/react-scroll": "^1.8.10",
@@ -21450,14 +21449,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -21,8 +21,7 @@
"react-virtuoso": "^4.7.13",
"rewire": "^7.0.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4",
"zod": "^3.23.8"
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -10,7 +10,6 @@ import WelcomeView from "./components/WelcomeView"
import { vscode } from "./utils/vscode"
import HistoryView from "./components/HistoryView"
import { HistoryItem } from "../../src/shared/HistoryItem"
import { MaestroUser } from "../../src/shared/maestro"
/*
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.
@@ -31,7 +30,7 @@ const App: React.FC = () => {
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [maestroUser, setMaestroUser] = useState<MaestroUser | undefined>(undefined)
const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -45,7 +44,8 @@ const App: React.FC = () => {
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
message.state!.apiConfiguration?.awsAccessKey !== undefined
message.state!.apiConfiguration?.awsAccessKey !== undefined ||
message.state!.apiConfiguration?.koduApiKey !== undefined
setShowWelcome(!hasKey)
setApiConfiguration(message.state!.apiConfiguration)
setMaxRequestsPerTask(
@@ -55,12 +55,12 @@ const App: React.FC = () => {
setVscodeThemeName(message.state!.themeName)
setClaudeMessages(message.state!.claudeMessages)
setTaskHistory(message.state!.taskHistory)
setKoduCredits(message.state!.koduCredits)
// don't update showAnnouncement to false if shouldShowAnnouncement is false
if (message.state!.shouldShowAnnouncement) {
setShowAnnouncement(true)
vscode.postMessage({ type: "didShowAnnouncement" })
}
setMaestroUser(message.state!.maestroUser)
setDidHydrateState(true)
break
case "action":
@@ -103,8 +103,8 @@ const App: React.FC = () => {
<SettingsView
version={version}
apiConfiguration={apiConfiguration}
maestroUser={maestroUser}
setApiConfiguration={setApiConfiguration}
koduCredits={koduCredits}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
customInstructions={customInstructions}

View File

@@ -5,7 +5,7 @@ import {
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import React, { useMemo } from "react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import {
ApiConfiguration,
ApiModelId,
@@ -14,27 +14,31 @@ import {
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
maestroDefaultModelId,
maestroModels,
koduDefaultModelId,
koduModels,
openRouterDefaultModelId,
openRouterModels,
} from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { MaestroUser } from "../../../src/shared/maestro"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../src/shared/ExtensionMessage"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maestroUser?: MaestroUser
koduCredits?: number
apiErrorMessage?: string
}
const ApiOptions: React.FC<ApiOptionsProps> = ({
showModelOptions,
apiConfiguration,
setApiConfiguration,
maestroUser,
koduCredits,
apiErrorMessage,
}) => {
const [didFetchKoduCredits, setDidFetchKoduCredits] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
@@ -75,6 +79,27 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
)
}
useEffect(() => {
if (selectedProvider === "kodu" && apiConfiguration?.koduApiKey) {
setDidFetchKoduCredits(false)
vscode.postMessage({ type: "fetchKoduCredits" })
}
}, [selectedProvider, apiConfiguration?.koduApiKey])
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action) {
case "koduCreditsFetched":
setDidFetchKoduCredits(true)
break
}
break
}
}, [])
useEvent("message", handleMessage)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
@@ -82,10 +107,10 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
<VSCodeOption value="kodu">Kodu</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="maestro">Maestro</VSCodeOption>
</VSCodeDropdown>
</div>
@@ -139,42 +164,54 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</div>
)}
{selectedProvider === "maestro" && (
{selectedProvider === "kodu" && (
<>
{maestroUser ? (
{apiConfiguration?.koduApiKey !== undefined ? (
<div>
<span
style={{
fontWeight: 500,
color: "var(--vscode-testing-iconPassed)",
}}>
<i
className={`codicon codicon-check`}
style={{
marginRight: 4,
marginBottom: 1,
fontSize: 11,
fontWeight: 700,
display: "inline-block",
verticalAlign: "bottom",
}}></i>
Signed in as {maestroUser.email}
</span>
<div style={{ margin: "4px 0px 2px 0px" }}>
<VSCodeButton
appearance="secondary"
onClick={() => vscode.postMessage({ type: "didClickMaestroSignOut" })}>
Sign out
</VSCodeButton>
<div style={{ marginBottom: 5, marginTop: 3 }}>
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
Signed in as {apiConfiguration?.koduEmail || "Unknown"}
</span>{" "}
<VSCodeLink
style={{ display: "inline" }}
onClick={() => vscode.postMessage({ type: "didClickKoduSignOut" })}>
(sign out?)
</VSCodeLink>
</div>
<div style={{ marginBottom: 7 }}>
Credits remaining:{" "}
<span style={{ fontWeight: 500, opacity: didFetchKoduCredits ? 1 : 0.6 }}>
{formatPrice(koduCredits || 0)}
</span>
</div>
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "didClickKoduAddCredits" })}
style={{
width: "fit-content",
}}>
Add Credits
</VSCodeButton>
<p
style={{
fontSize: "12px",
marginTop: "7px",
color: "var(--vscode-descriptionForeground)",
}}>
Kodu is recommended for its high rate limits and access to the latest features like
prompt caching.
<VSCodeLink href="https://kodu.ai/" style={{ display: "inline", fontSize: "12px" }}>
Learn more about Kodu here.
</VSCodeLink>
</p>
</div>
) : (
<div>
<div style={{ margin: "4px 0px" }}>
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "didClickMaestroSignIn" })}>
Sign in to Maestro
onClick={() => vscode.postMessage({ type: "didClickKoduSignIn" })}>
Sign in to Kodu
</VSCodeButton>
</div>
<p
@@ -183,7 +220,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
marginTop: 5,
color: "var(--vscode-descriptionForeground)",
}}>
This will open your browser to sign in to Maestro. You will be redirected back to the
This will open your browser to sign in to Kodu. You will be redirected back to the
extension after signing in.
</p>
</div>
@@ -256,6 +293,17 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</div>
)}
{apiErrorMessage && (
<p
style={{
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
{showModelOptions && (
<>
<div className="dropdown-container">
@@ -265,7 +313,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "maestro" && createDropdown(maestroModels)}
{selectedProvider === "kodu" && createDropdown(koduModels)}
</div>
<ModelInfoView modelInfo={selectedModelInfo} />
@@ -275,16 +323,16 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
)
}
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
const formatPrice = (price: number) => {
const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
}
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
return (
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
<ModelInfoSupportsItem
@@ -369,8 +417,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(openRouterModels, openRouterDefaultModelId)
case "bedrock":
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "maestro":
return getProviderData(maestroModels, maestroDefaultModelId)
case "kodu":
return getProviderData(koduModels, koduDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}

View File

@@ -4,13 +4,12 @@ import { ApiConfiguration } from "../../../src/shared/api"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
import { MaestroUser } from "../../../src/shared/maestro"
type SettingsViewProps = {
version: string
apiConfiguration?: ApiConfiguration
maestroUser?: MaestroUser
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
koduCredits?: number
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
customInstructions: string
@@ -21,8 +20,8 @@ type SettingsViewProps = {
const SettingsView = ({
version,
apiConfiguration,
maestroUser,
setApiConfiguration,
koduCredits,
maxRequestsPerTask,
setMaxRequestsPerTask,
customInstructions,
@@ -96,20 +95,11 @@ const SettingsView = ({
<div style={{ marginBottom: 5 }}>
<ApiOptions
apiConfiguration={apiConfiguration}
maestroUser={maestroUser}
setApiConfiguration={setApiConfiguration}
showModelOptions={true}
koduCredits={koduCredits}
apiErrorMessage={apiErrorMessage}
/>
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: 5 }}>

View File

@@ -18,10 +18,10 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "maestro":
// if (!apiConfiguration.maestroApiKey) {
// return "You must provide a valid API key or choose a different provider."
// }
case "kodu":
if (!apiConfiguration.koduApiKey) {
return "You must sign in to Kodu to use it as an API provider."
}
break
}
}