Remove Kodu provider

This commit is contained in:
Saoud Rizwan
2024-08-27 21:38:01 -04:00
parent ac6f712b9c
commit 843ef29a07
22 changed files with 95 additions and 689 deletions

View File

@@ -1261,8 +1261,8 @@ ${this.customInstructions.trim()}
)
const { message, userCredits } = await this.api.createMessage(systemPrompt, adjustedMessages, tools)
if (userCredits !== undefined) {
console.log("Updating kodu credits", userCredits)
this.providerRef.deref()?.updateKoduCredits(userCredits)
console.log("Updating credits", userCredits)
// TODO: update credits
}
return message
} catch (error) {

View File

@@ -3,7 +3,6 @@ import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api"
import { AnthropicHandler } from "./anthropic"
import { AwsBedrockHandler } from "./bedrock"
import { OpenRouterHandler } from "./openrouter"
import { KoduHandler } from "./kodu"
export interface ApiHandlerMessageResponse {
message: Anthropic.Messages.Message
@@ -38,8 +37,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new OpenRouterHandler(options)
case "bedrock":
return new AwsBedrockHandler(options)
case "kodu":
return new KoduHandler(options)
default:
return new AnthropicHandler(options)
}

View File

@@ -1,186 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
import { ApiHandlerOptions, koduDefaultModelId, KoduModelId, koduModels, ModelInfo } from "../shared/api"
import { getKoduCreditsUrl, getKoduInferenceUrl } from "../shared/kodu"
export async function fetchKoduCredits({ apiKey }: { apiKey: string }) {
const response = await axios.get(getKoduCreditsUrl(), {
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<ApiHandlerMessageResponse> {
const modelId = this.getModel().id
let requestBody: Anthropic.Beta.PromptCaching.Messages.MessageCreateParamsNonStreaming
switch (modelId) {
case "claude-3-5-sonnet-20240620":
case "claude-3-opus-20240229":
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(getKoduInferenceUrl(), requestBody, {
// headers: {
// "x-api-key": this.options.koduApiKey,
// },
// })
// const message = response.data
// const userCredits = response.headers["user-credits"]
// return { message, userCredits: userCredits !== undefined ? parseFloat(userCredits) : undefined }
// const thing = {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "x-api-key": this.options.koduApiKey || "",
// },
// body: JSON.stringify(requestBody),
// }
const response = await axios.post(getKoduInferenceUrl(), requestBody, {
headers: {
"Content-Type": "application/json",
"x-api-key": this.options.koduApiKey || "",
},
responseType: "stream",
})
if (response.data) {
const reader = response.data
const decoder = new TextDecoder("utf-8")
let finalResponse: any = null
let buffer = ""
for await (const chunk of reader) {
buffer += decoder.decode(chunk, { stream: true })
const lines = buffer.split("\n\n")
buffer = lines.pop() || ""
for (const line of lines) {
if (line.startsWith("data: ")) {
const eventData = JSON.parse(line.slice(6))
console.log("eventData", eventData)
if (eventData.code === 0) {
console.log("Health check received")
} else if (eventData.code === 1) {
finalResponse = eventData.body
console.log("finalResponse", finalResponse)
break
} else if (eventData.code === -1) {
throw new Error(`Error in SSE stream: ${JSON.stringify(eventData.json)}`)
}
}
}
if (finalResponse) {
break
}
}
if (!finalResponse) {
throw new Error("No final response received from the SSE stream")
}
const message: {
anthropic: Anthropic.Messages.Message
internal: {
userCredits: number
}
} = finalResponse
console.log("message", message)
return {
message: message.anthropic,
userCredits: message.internal?.userCredits,
}
} else {
throw new Error("No response data received")
}
}
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

@@ -109,16 +109,16 @@ export function activate(context: vscode.ExtensionContext) {
vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider)
)
// URI Handler
const handleUri = async (uri: vscode.Uri) => {
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const token = query.get("token")
const email = query.get("email")
if (token) {
await sidebarProvider.saveKoduApiKey(token, email || undefined)
}
}
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
// // URI Handler
// const handleUri = async (uri: vscode.Uri) => {
// const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
// const token = query.get("token")
// const email = query.get("email")
// if (token) {
// await sidebarProvider.saveKoduApiKey(token, email || undefined)
// }
// }
// context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
}
// This method is called when your extension is deactivated

View File

@@ -8,7 +8,6 @@ import { downloadTask, getNonce, getUri, selectImages } from "../utils"
import * as path from "path"
import fs from "fs/promises"
import { HistoryItem } from "../shared/HistoryItem"
import { fetchKoduCredits } from "../api/kodu"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -16,19 +15,16 @@ 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" | "koduApiKey"
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "koduEmail"
| "koduCredits"
| "maxRequestsPerTask"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
| "taskHistory"
| "shouldShowKoduPromo"
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.
@@ -40,6 +36,28 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
this.outputChannel.appendLine("ClaudeDevProvider instantiated")
this.revertKodu()
}
async revertKodu() {
const apiProvider = await this.getGlobalState("apiProvider")
if (apiProvider === "kodu") {
// switch back to previous provider
const anthropicKey = await this.getSecret("apiKey")
if (anthropicKey) {
await this.updateGlobalState("apiProvider", "anthropic" as ApiProvider)
} else {
const openRouterApiKey = await this.getSecret("openRouterApiKey")
if (openRouterApiKey) {
await this.updateGlobalState("apiProvider", "openrouter" as ApiProvider)
} else {
const awsAccessKey = await this.getSecret("awsAccessKey")
if (awsAccessKey) {
await this.updateGlobalState("apiProvider", "bedrock" as ApiProvider)
}
}
}
}
}
/*
@@ -359,25 +377,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
case "exportTaskWithId":
this.exportTaskWithId(message.text!)
break
case "didClickKoduSignOut":
await this.signOutKodu()
break
case "fetchKoduCredits":
const koduApiKey = await this.getSecret("koduApiKey")
if (koduApiKey) {
const credits = await fetchKoduCredits({ apiKey: koduApiKey })
await this.updateGlobalState("koduCredits", credits)
await this.postMessageToWebview({
type: "action",
action: "koduCreditsFetched",
state: await this.getStateToPostToWebview(),
})
}
break
case "didDismissKoduPromo":
await this.updateGlobalState("shouldShowKoduPromo", false)
await this.postStateToWebview()
break
case "resetState":
await this.resetState()
break
@@ -390,27 +389,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
)
}
// Kodu
async saveKoduApiKey(apiKey: string, email?: string) {
await this.storeSecret("koduApiKey", apiKey)
await this.updateGlobalState("koduEmail", email)
await this.updateGlobalState("apiProvider", "kodu")
await this.updateGlobalState("shouldShowKoduPromo", false)
await this.postStateToWebview()
await this.postMessageToWebview({ type: "action", action: "koduAuthenticated" })
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: apiKey })
}
async signOutKodu() {
await this.storeSecret("koduApiKey", undefined)
await this.updateGlobalState("koduEmail", undefined)
await this.updateGlobalState("koduCredits", undefined)
await this.updateGlobalState("apiProvider", "kodu")
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: undefined })
await this.postStateToWebview()
}
// Task history
async getTaskWithId(id: string): Promise<{
@@ -510,8 +488,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
customInstructions,
alwaysAllowReadOnly,
taskHistory,
koduCredits,
shouldShowKoduPromo,
} = await this.getState()
return {
version: this.context.extension?.packageJSON?.version ?? "",
@@ -524,8 +500,6 @@ 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,
koduCredits,
shouldShowKoduPromo,
}
}
@@ -624,15 +598,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
awsAccessKey,
awsSecretKey,
awsRegion,
koduApiKey,
koduEmail,
koduCredits,
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
taskHistory,
shouldShowKoduPromo,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<ApiModelId | undefined>,
@@ -641,15 +611,11 @@ 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>,
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("shouldShowKoduPromo") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
@@ -661,8 +627,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to kodu
apiProvider = "kodu"
// New users should default to anthropic
apiProvider = "anthropic"
}
}
@@ -675,16 +641,12 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
awsAccessKey,
awsSecretKey,
awsRegion,
koduApiKey,
koduEmail,
},
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
taskHistory,
koduCredits,
shouldShowKoduPromo: shouldShowKoduPromo ?? true,
}
}
@@ -700,10 +662,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
return history
}
async updateKoduCredits(credits: number) {
await this.updateGlobalState("koduCredits", credits)
}
// global
private async updateGlobalState(key: GlobalStateKey, value: any) {
@@ -755,7 +713,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
for (const key of this.context.globalState.keys()) {
await this.context.globalState.update(key, undefined)
}
const secretKeys: SecretKey[] = ["apiKey", "openRouterApiKey", "awsAccessKey", "awsSecretKey", "koduApiKey"]
const secretKeys: SecretKey[] = ["apiKey", "openRouterApiKey", "awsAccessKey", "awsSecretKey"]
for (const key of secretKeys) {
await this.storeSecret(key, undefined)
}

View File

@@ -7,13 +7,7 @@ import { HistoryItem } from "./HistoryItem"
export interface ExtensionMessage {
type: "action" | "state" | "selectedImages"
text?: string
action?:
| "chatButtonTapped"
| "settingsButtonTapped"
| "historyButtonTapped"
| "didBecomeVisible"
| "koduAuthenticated"
| "koduCreditsFetched"
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
state?: ExtensionState
images?: string[]
}
@@ -29,8 +23,6 @@ export interface ExtensionState {
claudeMessages: ClaudeMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
koduCredits?: number
shouldShowKoduPromo: boolean
}
export interface ClaudeMessage {

View File

@@ -16,9 +16,6 @@ export interface WebviewMessage {
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
| "didClickKoduSignOut"
| "fetchKoduCredits"
| "didDismissKoduPromo"
| "resetState"
text?: string
askResponse?: ClaudeAskResponse

View File

@@ -1,4 +1,4 @@
export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "kodu"
export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
export interface ApiHandlerOptions {
apiModelId?: ApiModelId
@@ -7,8 +7,6 @@ export interface ApiHandlerOptions {
awsAccessKey?: string
awsSecretKey?: string
awsRegion?: string
koduApiKey?: string
koduEmail?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -252,10 +250,3 @@ export const openRouterModels = {
// outputPrice: 1.5,
// },
} as const satisfies Record<string, ModelInfo>
// 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,21 +0,0 @@
const KODU_BASE_URL = "https://kodu.ai"
export function getKoduSignInUrl(uriScheme?: string) {
return `${KODU_BASE_URL}/auth/login?redirectTo=${uriScheme}://saoudrizwan.claude-dev&ext=1`
}
export function getKoduAddCreditsUrl(uriScheme?: string) {
return `${KODU_BASE_URL}/cloud/plan`
}
export function getKoduCreditsUrl() {
return `${KODU_BASE_URL}/api/credits`
}
export function getKoduInferenceUrl() {
return `${KODU_BASE_URL}/api/inference-stream`
}
export function getKoduHomepageUrl() {
return `${KODU_BASE_URL}`
}