mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
feat: add Glama gateway
This commit is contained in:
@@ -135,7 +135,7 @@ Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.ant
|
|||||||
|
|
||||||
### Use any API and Model
|
### Use any API and Model
|
||||||
|
|
||||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
Cline supports API providers like OpenRouter, Anthropic, Glama, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||||
|
|
||||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||||
|
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -31,7 +31,7 @@
|
|||||||
"isbinaryfile": "^5.0.2",
|
"isbinaryfile": "^5.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||||
"openai": "^4.61.0",
|
"openai": "^4.73.1",
|
||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
"isbinaryfile": "^5.0.2",
|
"isbinaryfile": "^5.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||||
"openai": "^4.61.0",
|
"openai": "^4.73.1",
|
||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import { GlamaHandler } from "./providers/glama"
|
||||||
import { ApiConfiguration, ModelInfo } from "../shared/api"
|
import { ApiConfiguration, ModelInfo } from "../shared/api"
|
||||||
import { AnthropicHandler } from "./providers/anthropic"
|
import { AnthropicHandler } from "./providers/anthropic"
|
||||||
import { AwsBedrockHandler } from "./providers/bedrock"
|
import { AwsBedrockHandler } from "./providers/bedrock"
|
||||||
@@ -26,6 +27,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
|||||||
switch (apiProvider) {
|
switch (apiProvider) {
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
return new AnthropicHandler(options)
|
return new AnthropicHandler(options)
|
||||||
|
case "glama":
|
||||||
|
return new GlamaHandler(options)
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
return new OpenRouterHandler(options)
|
return new OpenRouterHandler(options)
|
||||||
case "bedrock":
|
case "bedrock":
|
||||||
|
|||||||
134
src/api/providers/glama.ts
Normal file
134
src/api/providers/glama.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import axios from "axios"
|
||||||
|
import OpenAI from "openai"
|
||||||
|
import { ApiHandler } from "../"
|
||||||
|
import { ApiHandlerOptions, ModelInfo, glamaDefaultModelId, glamaDefaultModelInfo } from "../../shared/api"
|
||||||
|
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||||
|
import { ApiStream } from "../transform/stream"
|
||||||
|
import delay from "delay"
|
||||||
|
|
||||||
|
export class GlamaHandler implements ApiHandler {
|
||||||
|
private options: ApiHandlerOptions
|
||||||
|
private client: OpenAI
|
||||||
|
|
||||||
|
constructor(options: ApiHandlerOptions) {
|
||||||
|
this.options = options
|
||||||
|
this.client = new OpenAI({
|
||||||
|
baseURL: "https://glama.ai/api/gateway/openai/v1",
|
||||||
|
apiKey: this.options.glamaApiKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||||
|
// Convert Anthropic messages to OpenAI format
|
||||||
|
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...convertToOpenAiMessages(messages),
|
||||||
|
]
|
||||||
|
|
||||||
|
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||||
|
if (this.getModel().id.startsWith("anthropic/claude-3")) {
|
||||||
|
openAiMessages[0] = {
|
||||||
|
role: "system",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: systemPrompt,
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
cache_control: { type: "ephemeral" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cache_control to the last two user messages
|
||||||
|
// (note: this works because we only ever add one user message at a time,
|
||||||
|
// but if we added multiple we'd need to mark the user message before the last assistant message)
|
||||||
|
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
|
||||||
|
lastTwoUserMessages.forEach((msg) => {
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
msg.content = [{ type: "text", text: msg.content }]
|
||||||
|
}
|
||||||
|
if (Array.isArray(msg.content)) {
|
||||||
|
// NOTE: this is fine since env details will always be added at the end.
|
||||||
|
// but if it weren't there, and the user added a image_url type message,
|
||||||
|
// it would pop a text part before it and then move it after to the end.
|
||||||
|
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||||
|
|
||||||
|
if (!lastTextPart) {
|
||||||
|
lastTextPart = { type: "text", text: "..." }
|
||||||
|
msg.content.push(lastTextPart)
|
||||||
|
}
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required by Anthropic
|
||||||
|
// Other providers default to max tokens allowed.
|
||||||
|
let maxTokens: number | undefined
|
||||||
|
|
||||||
|
if (this.getModel().id.startsWith("anthropic/")) {
|
||||||
|
maxTokens = 8_192
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: completion, response } = await this.client.chat.completions.create({
|
||||||
|
model: this.getModel().id,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
temperature: 0,
|
||||||
|
messages: openAiMessages,
|
||||||
|
stream: true,
|
||||||
|
}).withResponse();
|
||||||
|
|
||||||
|
const completionRequestUuid = response.headers.get(
|
||||||
|
'x-completion-request-uuid',
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const chunk of completion) {
|
||||||
|
const delta = chunk.choices[0]?.delta
|
||||||
|
|
||||||
|
if (delta?.content) {
|
||||||
|
yield {
|
||||||
|
type: "text",
|
||||||
|
text: delta.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The usage information is only available after a few moments after the completion
|
||||||
|
await delay(1000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://glama.ai/api/gateway/v1/completion-requests/${completionRequestUuid}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.options.glamaApiKey}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const completionRequest = response.data;
|
||||||
|
|
||||||
|
if (completionRequest.tokenUsage) {
|
||||||
|
yield {
|
||||||
|
type: "usage",
|
||||||
|
inputTokens: completionRequest.tokenUsage.promptTokens,
|
||||||
|
outputTokens: completionRequest.tokenUsage.completionTokens,
|
||||||
|
totalCost: completionRequest.totalCostUsd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore if fails
|
||||||
|
console.error("Error fetching Glama generation details:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getModel(): { id: string; info: ModelInfo } {
|
||||||
|
const modelId = this.options.glamaModelId
|
||||||
|
const modelInfo = this.options.glamaModelInfo
|
||||||
|
|
||||||
|
if (modelId && modelInfo) {
|
||||||
|
return { id: modelId, info: modelInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: glamaDefaultModelId, info: glamaDefaultModelInfo }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
|||||||
|
|
||||||
type SecretKey =
|
type SecretKey =
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
|
| "glamaApiKey"
|
||||||
| "openRouterApiKey"
|
| "openRouterApiKey"
|
||||||
| "awsAccessKey"
|
| "awsAccessKey"
|
||||||
| "awsSecretKey"
|
| "awsSecretKey"
|
||||||
@@ -44,6 +45,8 @@ type SecretKey =
|
|||||||
type GlobalStateKey =
|
type GlobalStateKey =
|
||||||
| "apiProvider"
|
| "apiProvider"
|
||||||
| "apiModelId"
|
| "apiModelId"
|
||||||
|
| "glamaModelId"
|
||||||
|
| "glamaModelInfo"
|
||||||
| "awsRegion"
|
| "awsRegion"
|
||||||
| "awsUseCrossRegionInference"
|
| "awsUseCrossRegionInference"
|
||||||
| "vertexProjectId"
|
| "vertexProjectId"
|
||||||
@@ -82,6 +85,7 @@ type GlobalStateKey =
|
|||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
apiConversationHistory: "api_conversation_history.json",
|
apiConversationHistory: "api_conversation_history.json",
|
||||||
uiMessages: "ui_messages.json",
|
uiMessages: "ui_messages.json",
|
||||||
|
glamaModels: "glama_models.json",
|
||||||
openRouterModels: "openrouter_models.json",
|
openRouterModels: "openrouter_models.json",
|
||||||
mcpSettings: "cline_mcp_settings.json",
|
mcpSettings: "cline_mcp_settings.json",
|
||||||
}
|
}
|
||||||
@@ -385,6 +389,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.readGlamaModels().then((glamaModels) => {
|
||||||
|
if (glamaModels) {
|
||||||
|
this.postMessageToWebview({ type: "glamaModels", glamaModels })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.refreshGlamaModels().then(async (glamaModels) => {
|
||||||
|
if (glamaModels) {
|
||||||
|
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||||
|
const { apiConfiguration } = await this.getState()
|
||||||
|
if (apiConfiguration.glamaModelId) {
|
||||||
|
await this.updateGlobalState(
|
||||||
|
"glamaModelInfo",
|
||||||
|
glamaModels[apiConfiguration.glamaModelId],
|
||||||
|
)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case "newTask":
|
case "newTask":
|
||||||
// Code that should run in response to the hello message command
|
// Code that should run in response to the hello message command
|
||||||
@@ -403,6 +425,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
apiProvider,
|
apiProvider,
|
||||||
apiModelId,
|
apiModelId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
glamaModelId,
|
||||||
|
glamaModelInfo,
|
||||||
|
glamaApiKey,
|
||||||
openRouterApiKey,
|
openRouterApiKey,
|
||||||
awsAccessKey,
|
awsAccessKey,
|
||||||
awsSecretKey,
|
awsSecretKey,
|
||||||
@@ -430,6 +455,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.updateGlobalState("apiProvider", apiProvider)
|
await this.updateGlobalState("apiProvider", apiProvider)
|
||||||
await this.updateGlobalState("apiModelId", apiModelId)
|
await this.updateGlobalState("apiModelId", apiModelId)
|
||||||
await this.storeSecret("apiKey", apiKey)
|
await this.storeSecret("apiKey", apiKey)
|
||||||
|
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||||
|
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||||
|
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||||
await this.storeSecret("awsSecretKey", awsSecretKey)
|
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||||
@@ -525,6 +553,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
const lmStudioModels = await this.getLmStudioModels(message.text)
|
const lmStudioModels = await this.getLmStudioModels(message.text)
|
||||||
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
|
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
|
||||||
break
|
break
|
||||||
|
case "refreshGlamaModels":
|
||||||
|
await this.refreshGlamaModels()
|
||||||
|
break
|
||||||
case "refreshOpenRouterModels":
|
case "refreshOpenRouterModels":
|
||||||
await this.refreshOpenRouterModels()
|
await this.refreshOpenRouterModels()
|
||||||
break
|
break
|
||||||
@@ -831,6 +862,93 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
return cacheDir
|
return cacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readGlamaModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||||
|
const glamaModelsFilePath = path.join(
|
||||||
|
await this.ensureCacheDirectoryExists(),
|
||||||
|
GlobalFileNames.glamaModels,
|
||||||
|
)
|
||||||
|
const fileExists = await fileExistsAtPath(glamaModelsFilePath)
|
||||||
|
if (fileExists) {
|
||||||
|
const fileContents = await fs.readFile(glamaModelsFilePath, "utf8")
|
||||||
|
return JSON.parse(fileContents)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshGlamaModels() {
|
||||||
|
const glamaModelsFilePath = path.join(
|
||||||
|
await this.ensureCacheDirectoryExists(),
|
||||||
|
GlobalFileNames.glamaModels,
|
||||||
|
)
|
||||||
|
|
||||||
|
let models: Record<string, ModelInfo> = {}
|
||||||
|
try {
|
||||||
|
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"added": "2024-12-24T15:12:49.324Z",
|
||||||
|
"capabilities": [
|
||||||
|
"adjustable_safety_settings",
|
||||||
|
"caching",
|
||||||
|
"code_execution",
|
||||||
|
"function_calling",
|
||||||
|
"json_mode",
|
||||||
|
"json_schema",
|
||||||
|
"system_instructions",
|
||||||
|
"tuning",
|
||||||
|
"input:audio",
|
||||||
|
"input:image",
|
||||||
|
"input:text",
|
||||||
|
"input:video",
|
||||||
|
"output:text"
|
||||||
|
],
|
||||||
|
"id": "google-vertex/gemini-1.5-flash-002",
|
||||||
|
"maxTokensInput": 1048576,
|
||||||
|
"maxTokensOutput": 8192,
|
||||||
|
"pricePerToken": {
|
||||||
|
"cacheRead": null,
|
||||||
|
"cacheWrite": null,
|
||||||
|
"input": "0.000000075",
|
||||||
|
"output": "0.0000003"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (response.data) {
|
||||||
|
const rawModels = response.data;
|
||||||
|
const parsePrice = (price: any) => {
|
||||||
|
if (price) {
|
||||||
|
return parseFloat(price) * 1_000_000
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
for (const rawModel of rawModels) {
|
||||||
|
const modelInfo: ModelInfo = {
|
||||||
|
maxTokens: rawModel.maxTokensOutput,
|
||||||
|
contextWindow: rawModel.maxTokensInput,
|
||||||
|
supportsImages: rawModel.capabilities?.includes("input:image"),
|
||||||
|
supportsPromptCache: rawModel.capabilities?.includes("caching"),
|
||||||
|
inputPrice: parsePrice(rawModel.pricePerToken?.input),
|
||||||
|
outputPrice: parsePrice(rawModel.pricePerToken?.output),
|
||||||
|
description: undefined,
|
||||||
|
cacheWritesPrice: parsePrice(rawModel.pricePerToken?.cacheWrite),
|
||||||
|
cacheReadsPrice: parsePrice(rawModel.pricePerToken?.cacheRead),
|
||||||
|
}
|
||||||
|
|
||||||
|
models[rawModel.id] = modelInfo
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Invalid response from Glama API")
|
||||||
|
}
|
||||||
|
await fs.writeFile(glamaModelsFilePath, JSON.stringify(models))
|
||||||
|
console.log("Glama models fetched and saved", models)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Glama models:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.postMessageToWebview({ type: "glamaModels", glamaModels: models })
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||||
const openRouterModelsFilePath = path.join(
|
const openRouterModelsFilePath = path.join(
|
||||||
await this.ensureCacheDirectoryExists(),
|
await this.ensureCacheDirectoryExists(),
|
||||||
@@ -1153,6 +1271,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
storedApiProvider,
|
storedApiProvider,
|
||||||
apiModelId,
|
apiModelId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
glamaApiKey,
|
||||||
|
glamaModelId,
|
||||||
|
glamaModelInfo,
|
||||||
openRouterApiKey,
|
openRouterApiKey,
|
||||||
awsAccessKey,
|
awsAccessKey,
|
||||||
awsSecretKey,
|
awsSecretKey,
|
||||||
@@ -1200,6 +1321,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||||
this.getSecret("apiKey") as Promise<string | undefined>,
|
this.getSecret("apiKey") as Promise<string | undefined>,
|
||||||
|
this.getSecret("glamaApiKey") as Promise<string | undefined>,
|
||||||
|
this.getGlobalState("glamaModelId") as Promise<string | undefined>,
|
||||||
|
this.getGlobalState("glamaModelInfo") as Promise<ModelInfo | 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>,
|
||||||
this.getSecret("awsSecretKey") as Promise<string | undefined>,
|
this.getSecret("awsSecretKey") as Promise<string | undefined>,
|
||||||
@@ -1264,6 +1388,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
apiProvider,
|
apiProvider,
|
||||||
apiModelId,
|
apiModelId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
glamaApiKey,
|
||||||
|
glamaModelId,
|
||||||
|
glamaModelInfo,
|
||||||
openRouterApiKey,
|
openRouterApiKey,
|
||||||
awsAccessKey,
|
awsAccessKey,
|
||||||
awsSecretKey,
|
awsSecretKey,
|
||||||
@@ -1402,6 +1529,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
const secretKeys: SecretKey[] = [
|
const secretKeys: SecretKey[] = [
|
||||||
"apiKey",
|
"apiKey",
|
||||||
|
"glamaApiKey",
|
||||||
"openRouterApiKey",
|
"openRouterApiKey",
|
||||||
"awsAccessKey",
|
"awsAccessKey",
|
||||||
"awsSecretKey",
|
"awsSecretKey",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface ExtensionMessage {
|
|||||||
| "workspaceUpdated"
|
| "workspaceUpdated"
|
||||||
| "invoke"
|
| "invoke"
|
||||||
| "partialMessage"
|
| "partialMessage"
|
||||||
|
| "glamaModels"
|
||||||
| "openRouterModels"
|
| "openRouterModels"
|
||||||
| "openAiModels"
|
| "openAiModels"
|
||||||
| "mcpServers"
|
| "mcpServers"
|
||||||
@@ -34,6 +35,7 @@ export interface ExtensionMessage {
|
|||||||
lmStudioModels?: string[]
|
lmStudioModels?: string[]
|
||||||
filePaths?: string[]
|
filePaths?: string[]
|
||||||
partialMessage?: ClineMessage
|
partialMessage?: ClineMessage
|
||||||
|
glamaModels?: Record<string, ModelInfo>
|
||||||
openRouterModels?: Record<string, ModelInfo>
|
openRouterModels?: Record<string, ModelInfo>
|
||||||
openAiModels?: string[]
|
openAiModels?: string[]
|
||||||
mcpServers?: McpServer[]
|
mcpServers?: McpServer[]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface WebviewMessage {
|
|||||||
| "openFile"
|
| "openFile"
|
||||||
| "openMention"
|
| "openMention"
|
||||||
| "cancelTask"
|
| "cancelTask"
|
||||||
|
| "refreshGlamaModels"
|
||||||
| "refreshOpenRouterModels"
|
| "refreshOpenRouterModels"
|
||||||
| "refreshOpenAiModels"
|
| "refreshOpenAiModels"
|
||||||
| "alwaysAllowBrowser"
|
| "alwaysAllowBrowser"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type ApiProvider =
|
export type ApiProvider =
|
||||||
| "anthropic"
|
| "anthropic"
|
||||||
|
| "glama"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "bedrock"
|
| "bedrock"
|
||||||
| "vertex"
|
| "vertex"
|
||||||
@@ -14,6 +15,9 @@ export interface ApiHandlerOptions {
|
|||||||
apiModelId?: string
|
apiModelId?: string
|
||||||
apiKey?: string // anthropic
|
apiKey?: string // anthropic
|
||||||
anthropicBaseUrl?: string
|
anthropicBaseUrl?: string
|
||||||
|
glamaModelId?: string
|
||||||
|
glamaModelInfo?: ModelInfo
|
||||||
|
glamaApiKey?: string
|
||||||
openRouterApiKey?: string
|
openRouterApiKey?: string
|
||||||
openRouterModelId?: string
|
openRouterModelId?: string
|
||||||
openRouterModelInfo?: ModelInfo
|
openRouterModelInfo?: ModelInfo
|
||||||
@@ -309,6 +313,23 @@ export const bedrockModels = {
|
|||||||
},
|
},
|
||||||
} as const satisfies Record<string, ModelInfo>
|
} as const satisfies Record<string, ModelInfo>
|
||||||
|
|
||||||
|
// Glama
|
||||||
|
// https://glama.ai/models
|
||||||
|
export const glamaDefaultModelId = "anthropic/claude-3-5-sonnet" // will always exist in openRouterModels
|
||||||
|
export const glamaDefaultModelInfo: ModelInfo = {
|
||||||
|
maxTokens: 8192,
|
||||||
|
contextWindow: 200_000,
|
||||||
|
supportsImages: true,
|
||||||
|
supportsComputerUse: true,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
inputPrice: 3.0,
|
||||||
|
outputPrice: 15.0,
|
||||||
|
cacheWritesPrice: 3.75,
|
||||||
|
cacheReadsPrice: 0.3,
|
||||||
|
description:
|
||||||
|
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._",
|
||||||
|
}
|
||||||
|
|
||||||
// OpenRouter
|
// OpenRouter
|
||||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||||
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels
|
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
deepSeekModels,
|
deepSeekModels,
|
||||||
geminiDefaultModelId,
|
geminiDefaultModelId,
|
||||||
geminiModels,
|
geminiModels,
|
||||||
|
glamaDefaultModelId,
|
||||||
|
glamaDefaultModelInfo,
|
||||||
openAiModelInfoSaneDefaults,
|
openAiModelInfoSaneDefaults,
|
||||||
openAiNativeDefaultModelId,
|
openAiNativeDefaultModelId,
|
||||||
openAiNativeModels,
|
openAiNativeModels,
|
||||||
@@ -38,6 +40,7 @@ import OpenRouterModelPicker, {
|
|||||||
OPENROUTER_MODEL_PICKER_Z_INDEX,
|
OPENROUTER_MODEL_PICKER_Z_INDEX,
|
||||||
} from "./OpenRouterModelPicker"
|
} from "./OpenRouterModelPicker"
|
||||||
import OpenAiModelPicker from "./OpenAiModelPicker"
|
import OpenAiModelPicker from "./OpenAiModelPicker"
|
||||||
|
import GlamaModelPicker from "./GlamaModelPicker"
|
||||||
|
|
||||||
interface ApiOptionsProps {
|
interface ApiOptionsProps {
|
||||||
showModelOptions: boolean
|
showModelOptions: boolean
|
||||||
@@ -131,6 +134,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||||||
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
|
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
|
||||||
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
||||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||||
|
<VSCodeOption value="glama">Glama</VSCodeOption>
|
||||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||||
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
||||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||||
@@ -193,6 +197,34 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedProvider === "glama" && (
|
||||||
|
<div>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={apiConfiguration?.glamaApiKey || ""}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
type="password"
|
||||||
|
onInput={handleInputChange("glamaApiKey")}
|
||||||
|
placeholder="Enter API Key...">
|
||||||
|
<span style={{ fontWeight: 500 }}>Glama API Key</span>
|
||||||
|
</VSCodeTextField>
|
||||||
|
{!apiConfiguration?.glamaApiKey && (
|
||||||
|
<VSCodeLink
|
||||||
|
href="https://glama.ai/settings/api-keys"
|
||||||
|
style={{ display: "inline", fontSize: "inherit" }}>
|
||||||
|
You can get an Glama API key by signing up here.
|
||||||
|
</VSCodeLink>
|
||||||
|
)}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
This key is stored locally and only used to make API requests from this extension.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedProvider === "openai-native" && (
|
{selectedProvider === "openai-native" && (
|
||||||
<div>
|
<div>
|
||||||
<VSCodeTextField
|
<VSCodeTextField
|
||||||
@@ -666,9 +698,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedProvider === "glama" && showModelOptions && <GlamaModelPicker />}
|
||||||
|
|
||||||
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker />}
|
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker />}
|
||||||
|
|
||||||
{selectedProvider !== "openrouter" &&
|
{selectedProvider !== "glama" &&
|
||||||
|
selectedProvider !== "openrouter" &&
|
||||||
selectedProvider !== "openai" &&
|
selectedProvider !== "openai" &&
|
||||||
selectedProvider !== "ollama" &&
|
selectedProvider !== "ollama" &&
|
||||||
selectedProvider !== "lmstudio" &&
|
selectedProvider !== "lmstudio" &&
|
||||||
@@ -872,6 +907,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
|||||||
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
|
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
|
||||||
case "openai-native":
|
case "openai-native":
|
||||||
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
|
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
|
||||||
|
case "glama":
|
||||||
|
return {
|
||||||
|
selectedProvider: provider,
|
||||||
|
selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
|
||||||
|
selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
|
||||||
|
}
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
return {
|
return {
|
||||||
selectedProvider: provider,
|
selectedProvider: provider,
|
||||||
|
|||||||
396
webview-ui/src/components/settings/GlamaModelPicker.tsx
Normal file
396
webview-ui/src/components/settings/GlamaModelPicker.tsx
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
|
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { useRemark } from "react-remark"
|
||||||
|
import { useMount } from "react-use"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { glamaDefaultModelId } from "../../../../src/shared/api"
|
||||||
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
import { vscode } from "../../utils/vscode"
|
||||||
|
import { highlight } from "../history/HistoryView"
|
||||||
|
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||||
|
|
||||||
|
const GlamaModelPicker: React.FC = () => {
|
||||||
|
const { apiConfiguration, setApiConfiguration, glamaModels } = useExtensionState()
|
||||||
|
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.glamaModelId || glamaDefaultModelId)
|
||||||
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||||
|
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const handleModelChange = (newModelId: string) => {
|
||||||
|
// could be setting invalid model id/undefined info but validation will catch it
|
||||||
|
setApiConfiguration({
|
||||||
|
...apiConfiguration,
|
||||||
|
glamaModelId: newModelId,
|
||||||
|
glamaModelInfo: glamaModels[newModelId],
|
||||||
|
})
|
||||||
|
setSearchTerm(newModelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||||
|
return normalizeApiConfiguration(apiConfiguration)
|
||||||
|
}, [apiConfiguration])
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
vscode.postMessage({ type: "refreshGlamaModels" })
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const modelIds = useMemo(() => {
|
||||||
|
return Object.keys(glamaModels).sort((a, b) => a.localeCompare(b))
|
||||||
|
}, [glamaModels])
|
||||||
|
|
||||||
|
const searchableItems = useMemo(() => {
|
||||||
|
return modelIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
html: id,
|
||||||
|
}))
|
||||||
|
}, [modelIds])
|
||||||
|
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(searchableItems, {
|
||||||
|
keys: ["html"], // highlight function will update this
|
||||||
|
threshold: 0.6,
|
||||||
|
shouldSort: true,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
ignoreLocation: false,
|
||||||
|
includeMatches: true,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
})
|
||||||
|
}, [searchableItems])
|
||||||
|
|
||||||
|
const modelSearchResults = useMemo(() => {
|
||||||
|
let results: { id: string; html: string }[] = searchTerm
|
||||||
|
? highlight(fuse.search(searchTerm), "model-item-highlight")
|
||||||
|
: searchableItems
|
||||||
|
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
|
||||||
|
return results
|
||||||
|
}, [searchableItems, searchTerm, fuse])
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!isDropdownVisible) return
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
event.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
|
||||||
|
break
|
||||||
|
case "ArrowUp":
|
||||||
|
event.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
|
||||||
|
break
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault()
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
|
||||||
|
handleModelChange(modelSearchResults[selectedIndex].id)
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "Escape":
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
setSelectedIndex(-1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasInfo = useMemo(() => {
|
||||||
|
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
|
||||||
|
}, [modelIds, searchTerm])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(-1)
|
||||||
|
if (dropdownListRef.current) {
|
||||||
|
dropdownListRef.current.scrollTop = 0
|
||||||
|
}
|
||||||
|
}, [searchTerm])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
|
||||||
|
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedIndex])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.model-item-highlight {
|
||||||
|
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="model-search">
|
||||||
|
<span style={{ fontWeight: 500 }}>Model</span>
|
||||||
|
</label>
|
||||||
|
<DropdownWrapper ref={dropdownRef}>
|
||||||
|
<VSCodeTextField
|
||||||
|
id="model-search"
|
||||||
|
placeholder="Search and select a model..."
|
||||||
|
value={searchTerm}
|
||||||
|
onInput={(e) => {
|
||||||
|
handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase())
|
||||||
|
setIsDropdownVisible(true)
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsDropdownVisible(true)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={{ width: "100%", zIndex: GLAMA_MODEL_PICKER_Z_INDEX, position: "relative" }}>
|
||||||
|
{searchTerm && (
|
||||||
|
<div
|
||||||
|
className="input-icon-button codicon codicon-close"
|
||||||
|
aria-label="Clear search"
|
||||||
|
onClick={() => {
|
||||||
|
handleModelChange("")
|
||||||
|
setIsDropdownVisible(true)
|
||||||
|
}}
|
||||||
|
slot="end"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VSCodeTextField>
|
||||||
|
{isDropdownVisible && (
|
||||||
|
<DropdownList ref={dropdownListRef}>
|
||||||
|
{modelSearchResults.map((item, index) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={item.id}
|
||||||
|
ref={(el) => (itemRefs.current[index] = el)}
|
||||||
|
isSelected={index === selectedIndex}
|
||||||
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
|
onClick={() => {
|
||||||
|
handleModelChange(item.id)
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: item.html,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownList>
|
||||||
|
)}
|
||||||
|
</DropdownWrapper>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasInfo ? (
|
||||||
|
<ModelInfoView
|
||||||
|
selectedModelId={selectedModelId}
|
||||||
|
modelInfo={selectedModelInfo}
|
||||||
|
isDescriptionExpanded={isDescriptionExpanded}
|
||||||
|
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: 0,
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
The extension automatically fetches the latest list of models available on{" "}
|
||||||
|
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://glama.ai/models">
|
||||||
|
Glama.
|
||||||
|
</VSCodeLink>
|
||||||
|
If you're unsure which model to choose, Cline works best with{" "}
|
||||||
|
<VSCodeLink
|
||||||
|
style={{ display: "inline", fontSize: "inherit" }}
|
||||||
|
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
|
||||||
|
anthropic/claude-3.5-sonnet.
|
||||||
|
</VSCodeLink>
|
||||||
|
You can also try searching "free" for no-cost options currently available.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlamaModelPicker
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
|
||||||
|
const DropdownWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GLAMA_MODEL_PICKER_Z_INDEX = 1_001
|
||||||
|
|
||||||
|
const DropdownList = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% - 3px);
|
||||||
|
left: 0;
|
||||||
|
width: calc(100% - 2px);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--vscode-dropdown-background);
|
||||||
|
border: 1px solid var(--vscode-list-activeSelectionBackground);
|
||||||
|
z-index: ${GLAMA_MODEL_PICKER_Z_INDEX - 1};
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DropdownItem = styled.div<{ isSelected: boolean }>`
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--vscode-list-activeSelectionBackground);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
|
||||||
|
const StyledMarkdown = styled.div`
|
||||||
|
font-family:
|
||||||
|
var(--vscode-font-family),
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Open Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
|
||||||
|
p,
|
||||||
|
li,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ModelDescriptionMarkdown = memo(
|
||||||
|
({
|
||||||
|
markdown,
|
||||||
|
key,
|
||||||
|
isExpanded,
|
||||||
|
setIsExpanded,
|
||||||
|
}: {
|
||||||
|
markdown?: string
|
||||||
|
key: string
|
||||||
|
isExpanded: boolean
|
||||||
|
setIsExpanded: (isExpanded: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const [reactContent, setMarkdown] = useRemark()
|
||||||
|
const [showSeeMore, setShowSeeMore] = useState(false)
|
||||||
|
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const textRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMarkdown(markdown || "")
|
||||||
|
}, [markdown, setMarkdown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current && textContainerRef.current) {
|
||||||
|
const { scrollHeight } = textRef.current
|
||||||
|
const { clientHeight } = textContainerRef.current
|
||||||
|
const isOverflowing = scrollHeight > clientHeight
|
||||||
|
setShowSeeMore(isOverflowing)
|
||||||
|
}
|
||||||
|
}, [reactContent, setIsExpanded])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
|
||||||
|
<div
|
||||||
|
ref={textContainerRef}
|
||||||
|
style={{
|
||||||
|
overflowY: isExpanded ? "auto" : "hidden",
|
||||||
|
position: "relative",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
ref={textRef}
|
||||||
|
style={{
|
||||||
|
display: "-webkit-box",
|
||||||
|
WebkitLineClamp: isExpanded ? "unset" : 3,
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
|
{reactContent}
|
||||||
|
</div>
|
||||||
|
{!isExpanded && showSeeMore && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
height: "1.2em",
|
||||||
|
background:
|
||||||
|
"linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<VSCodeLink
|
||||||
|
style={{
|
||||||
|
fontSize: "inherit",
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingLeft: 3,
|
||||||
|
backgroundColor: "var(--vscode-sideBar-background)",
|
||||||
|
}}
|
||||||
|
onClick={() => setIsExpanded(true)}>
|
||||||
|
See more
|
||||||
|
</VSCodeLink>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</StyledMarkdown>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -37,6 +37,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
browserViewportSize,
|
browserViewportSize,
|
||||||
setBrowserViewportSize,
|
setBrowserViewportSize,
|
||||||
openRouterModels,
|
openRouterModels,
|
||||||
|
glamaModels,
|
||||||
setAllowedCommands,
|
setAllowedCommands,
|
||||||
allowedCommands,
|
allowedCommands,
|
||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
@@ -56,7 +57,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
const [commandInput, setCommandInput] = useState("")
|
const [commandInput, setCommandInput] = useState("")
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||||
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||||
|
|
||||||
setApiErrorMessage(apiValidationResult)
|
setApiErrorMessage(apiValidationResult)
|
||||||
setModelIdErrorMessage(modelIdValidationResult)
|
setModelIdErrorMessage(modelIdValidationResult)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionM
|
|||||||
import {
|
import {
|
||||||
ApiConfiguration,
|
ApiConfiguration,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
|
glamaDefaultModelId,
|
||||||
|
glamaDefaultModelInfo,
|
||||||
openRouterDefaultModelId,
|
openRouterDefaultModelId,
|
||||||
openRouterDefaultModelInfo,
|
openRouterDefaultModelInfo,
|
||||||
} from "../../../src/shared/api"
|
} from "../../../src/shared/api"
|
||||||
@@ -16,6 +18,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
didHydrateState: boolean
|
didHydrateState: boolean
|
||||||
showWelcome: boolean
|
showWelcome: boolean
|
||||||
theme: any
|
theme: any
|
||||||
|
glamaModels: Record<string, ModelInfo>
|
||||||
openRouterModels: Record<string, ModelInfo>
|
openRouterModels: Record<string, ModelInfo>
|
||||||
openAiModels: string[],
|
openAiModels: string[],
|
||||||
mcpServers: McpServer[]
|
mcpServers: McpServer[]
|
||||||
@@ -69,6 +72,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
const [theme, setTheme] = useState<any>(undefined)
|
const [theme, setTheme] = useState<any>(undefined)
|
||||||
const [filePaths, setFilePaths] = useState<string[]>([])
|
const [filePaths, setFilePaths] = useState<string[]>([])
|
||||||
|
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||||
|
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||||
|
})
|
||||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||||
})
|
})
|
||||||
@@ -85,6 +91,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const hasKey = config
|
const hasKey = config
|
||||||
? [
|
? [
|
||||||
config.apiKey,
|
config.apiKey,
|
||||||
|
config.glamaApiKey,
|
||||||
config.openRouterApiKey,
|
config.openRouterApiKey,
|
||||||
config.awsRegion,
|
config.awsRegion,
|
||||||
config.vertexProjectId,
|
config.vertexProjectId,
|
||||||
@@ -123,6 +130,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "glamaModels": {
|
||||||
|
const updatedModels = message.glamaModels ?? {}
|
||||||
|
setGlamaModels({
|
||||||
|
[glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||||
|
...updatedModels,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
case "openRouterModels": {
|
case "openRouterModels": {
|
||||||
const updatedModels = message.openRouterModels ?? {}
|
const updatedModels = message.openRouterModels ?? {}
|
||||||
setOpenRouterModels({
|
setOpenRouterModels({
|
||||||
@@ -154,6 +169,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
didHydrateState,
|
didHydrateState,
|
||||||
showWelcome,
|
showWelcome,
|
||||||
theme,
|
theme,
|
||||||
|
glamaModels,
|
||||||
openRouterModels,
|
openRouterModels,
|
||||||
openAiModels,
|
openAiModels,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiConfiguration, openRouterDefaultModelId } from "../../../src/shared/api"
|
import { ApiConfiguration, glamaDefaultModelId, openRouterDefaultModelId } from "../../../src/shared/api"
|
||||||
import { ModelInfo } from "../../../src/shared/api"
|
import { ModelInfo } from "../../../src/shared/api"
|
||||||
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
||||||
if (apiConfiguration) {
|
if (apiConfiguration) {
|
||||||
@@ -8,6 +8,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
|||||||
return "You must provide a valid API key or choose a different provider."
|
return "You must provide a valid API key or choose a different provider."
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "glama":
|
||||||
|
if (!apiConfiguration.glamaApiKey) {
|
||||||
|
return "You must provide a valid API key or choose a different provider."
|
||||||
|
}
|
||||||
|
break
|
||||||
case "bedrock":
|
case "bedrock":
|
||||||
if (!apiConfiguration.awsRegion) {
|
if (!apiConfiguration.awsRegion) {
|
||||||
return "You must choose a region to use with AWS Bedrock."
|
return "You must choose a region to use with AWS Bedrock."
|
||||||
@@ -59,10 +64,21 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
|||||||
|
|
||||||
export function validateModelId(
|
export function validateModelId(
|
||||||
apiConfiguration?: ApiConfiguration,
|
apiConfiguration?: ApiConfiguration,
|
||||||
|
glamaModels?: Record<string, ModelInfo>,
|
||||||
openRouterModels?: Record<string, ModelInfo>,
|
openRouterModels?: Record<string, ModelInfo>,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (apiConfiguration) {
|
if (apiConfiguration) {
|
||||||
switch (apiConfiguration.apiProvider) {
|
switch (apiConfiguration.apiProvider) {
|
||||||
|
case "glama":
|
||||||
|
const glamaModelId = apiConfiguration.glamaModelId || glamaDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||||
|
if (!glamaModelId) {
|
||||||
|
return "You must provide a model ID."
|
||||||
|
}
|
||||||
|
if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) {
|
||||||
|
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
|
||||||
|
return "The model ID you provided is not available. Please choose a different model."
|
||||||
|
}
|
||||||
|
break
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user