resolving merge conflicts

This commit is contained in:
pashpashpash
2025-03-04 15:12:46 -08:00
31 changed files with 577 additions and 205 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed problem with “win+shift+a” shortcut not working in Windows
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Better Openrouter error typing and throwing more detailed messages
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add retries to Bedrock createMessage
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add webview to lint job
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Update Claude 3.5 -> Claude 3.7 in error message/recommendation
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add support for AskSage as model provider.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add timeout option to MCP servers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add APAC Support for Cross-Region Inference Profiles
+1 -1
View File
@@ -1 +1 @@
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [3.5.1]
- Add timeout option to MCP servers
- Add Gemini Flash models to Vertex provider (thanks @jpaodev!)
- Add prompt caching support for AWS Bedrock provider (thanks @buger!)
- Add AskSage provider (thanks @swhite24!)
## [3.5.0]
- Add 'Enable extended thinking' option for Claude 3.7 Sonnet, with ability to set different budgets for Plan and Act modes
+31 -2
View File
@@ -1,17 +1,18 @@
{
"name": "claude-dev",
"version": "3.5.0",
"version": "3.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.5.0",
"version": "3.5.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
@@ -36,6 +37,7 @@
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"ollama": "^0.5.13",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
@@ -3787,6 +3789,18 @@
"resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz",
"integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="
},
"node_modules/@google-cloud/vertexai": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz",
"integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.1.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@google/generative-ai": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.18.0.tgz",
@@ -11051,6 +11065,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ollama": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.13.tgz",
"integrity": "sha512-qK3eE2GjMYjCiTknEJfAHjbUzUqgVtf9qtzjxWrkwBZgBG7kOB6Z4+Ov4fbvDjmKKHv+rpuTsWFg4jZvVjNBtQ==",
"license": "MIT",
"dependencies": {
"whatwg-fetch": "^3.6.20"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -13401,6 +13424,12 @@
"node": ">=18"
}
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+4 -2
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.5.0",
"version": "3.5.1",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -236,7 +236,7 @@
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
"lint": "eslint src --ext ts",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "vscode-test",
@@ -275,6 +275,7 @@
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
@@ -299,6 +300,7 @@
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"ollama": "^0.5.13",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
+4 -2
View File
@@ -19,11 +19,13 @@ export class AnthropicHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0 ? true : false
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-3-7-sonnet-20250219":
+42 -5
View File
@@ -16,11 +16,12 @@ export class AwsBedrockHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0 ? true : false
// cross region inference requires prefixing the model id with the region
let modelId = await this.getModelId()
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
// Get model info and message indices for caching
const model = this.getModel()
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
@@ -33,11 +34,47 @@ export class AwsBedrockHandler implements ApiHandler {
const stream = await client.messages.create({
model: modelId,
max_tokens: this.getModel().info.maxTokens || 8192,
max_tokens: model.info.maxTokens || 8192,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
temperature: reasoningOn ? undefined : 0,
system: systemPrompt,
messages,
system: [
{
text: systemPrompt,
type: "text",
...(this.options.awsBedrockUsePromptCache === true && {
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,
...(this.options.awsBedrockUsePromptCache === true && {
cache_control: { type: "ephemeral" },
}),
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? {
...content,
...(this.options.awsBedrockUsePromptCache === true && {
cache_control: { type: "ephemeral" },
}),
}
: content,
),
}
}
return message
}),
stream: true,
})
+12 -17
View File
@@ -1,40 +1,35 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { Message, Ollama } from "ollama"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: Ollama
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
apiKey: "ollama",
})
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await this.client.chat({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
messages: ollamaMessages,
stream: true,
options: {
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
},
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
if (typeof chunk.message.content === "string") {
yield {
type: "text",
text: delta.content,
text: chunk.message.content,
}
}
}
+12 -1
View File
@@ -6,6 +6,7 @@ import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -32,20 +33,30 @@ export class OpenAiHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isO3Mini = modelId.includes("o3-mini")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
let temperature: number | undefined = 0
let reasoningEffort: ChatCompletionReasoningEffort | undefined = undefined
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
if (isO3Mini) {
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
temperature = undefined // does not support temperature
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
temperature,
reasoning_effort: reasoningEffort,
stream: true,
stream_options: { include_usage: true },
})
+2
View File
@@ -7,6 +7,8 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { OpenRouterErrorResponse } from "./types"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
+22
View File
@@ -0,0 +1,22 @@
// For the following openrouter error type sources, see the docs here:
// https://openrouter.ai/docs/api-reference/errors
export type OpenRouterErrorResponse = {
error: {
message: string
code: number
metadata?: OpenRouterProviderErrorMetadata | OpenRouterModerationErrorMetadata | Record<string, unknown>
}
}
export type OpenRouterProviderErrorMetadata = {
provider_name: string // The name of the provider that encountered the error
raw: unknown // The raw error from the provider
}
export type OpenRouterModerationErrorMetadata = {
reasons: string[] // Why your input was flagged
flagged_input: string // The text segment that was flagged, limited to 100 characters. If the flagged input is longer than 100 characters, it will be truncated in the middle and replaced with ...
provider_name: string // The name of the provider that requested moderation
model_slug: string
}
+199 -136
View File
@@ -4,19 +4,25 @@ import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { VertexAI } from "@google-cloud/vertexai"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: AnthropicVertex
private clientAnthropic: AnthropicVertex
private clientVertex: VertexAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new AnthropicVertex({
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
this.clientVertex = new VertexAI({
project: this.options.vertexProjectId,
location: this.options.vertexRegion,
})
}
@withRetry()
@@ -24,40 +30,66 @@ export class VertexHandler implements ApiHandler {
const model = this.getModel()
const modelId = model.id
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0 ? true : false
if (modelId.includes("claude")) {
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
let stream
switch (modelId) {
case "claude-3-7-sonnet@20250219":
case "claude-3-5-sonnet-v2@20241022":
case "claude-3-5-sonnet@20240620":
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
// Find indices of user messages for cache control
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
let stream
switch (modelId) {
case "claude-3-7-sonnet@20250219":
case "claude-3-5-sonnet-v2@20241022":
case "claude-3-5-sonnet@20240620":
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
// Find indices of user messages for cache control
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
stream = await this.client.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
temperature: reasoningOn ? undefined : 0,
system: [
{
text: systemPrompt,
type: "text",
cache_control: { type: "ephemeral" },
},
],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
stream = await this.clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
temperature: reasoningOn ? undefined : 0,
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,
content:
@@ -66,121 +98,152 @@ export class VertexHandler implements ApiHandler {
{
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,
),
: message.content,
}
}
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
}
}),
stream: true,
},
{
headers: {},
},
)
break
}
default: {
stream = await this.client.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [
{
text: systemPrompt,
type: "text",
}),
stream: true,
},
],
messages: messages.map((message) => ({
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
})),
stream: true,
})
break
{
headers: {},
},
)
break
}
default: {
stream = await this.clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [
{
text: systemPrompt,
type: "text",
},
],
messages: messages.map((message) => ({
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
})),
stream: true,
})
break
}
}
}
for await (const chunk of stream) {
switch (chunk.type) {
case "message_start":
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "message_stop":
break
case "content_block_start":
switch (chunk.content_block.type) {
case "text":
if (chunk.index > 0) {
for await (const chunk of stream) {
switch (chunk.type) {
case "message_start":
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "message_stop":
break
case "content_block_start":
switch (chunk.content_block.type) {
case "text":
if (chunk.index > 0) {
yield {
type: "text",
text: "\n",
}
}
yield {
type: "text",
text: "\n",
text: chunk.content_block.text,
}
break
}
break
case "content_block_delta":
switch (chunk.delta.type) {
case "text_delta":
yield {
type: "text",
text: chunk.delta.text,
}
break
}
break
case "content_block_stop":
break
}
}
} else {
// gemini
const generativeModel = this.clientVertex.getGenerativeModel({
model: this.getModel().id,
systemInstruction: {
role: "system",
parts: [{ text: systemPrompt }],
},
})
const request = {
contents: [
{
role: "user",
parts: messages.map((m) => {
if (typeof m.content === "string") {
return { text: m.content }
} else if (Array.isArray(m.content)) {
return {
text: m.content
.map((block) => {
if (typeof block === "string") {
return block
} else if (block.type === "text") {
return block.text
} else {
console.log("Unsupported block type", block)
return ""
}
})
.join(" "),
}
} else {
return { text: "" }
}
}),
},
],
}
const streamingResult = await generativeModel.generateContentStream(request)
for await (const chunk of streamingResult.stream) {
// If usage data is available, yield it similarly:
// yield { type: "usage", inputTokens: 0, outputTokens: 0 }
// Otherwise, just yield text:
const candidates = chunk.candidates || []
for (const candidate of candidates) {
for (const part of candidate.content?.parts || []) {
if (part.text) {
yield {
type: "text",
text: chunk.content_block.text,
text: part.text,
}
break
}
}
break
case "content_block_delta":
switch (chunk.delta.type) {
case "text_delta":
yield {
type: "text",
text: chunk.delta.text,
}
break
}
break
case "content_block_stop":
break
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Message } from "ollama"
export function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
const ollamaMessages: Message[] = []
for (const anthropicMessage of anthropicMessages) {
if (typeof anthropicMessage.content === "string") {
ollamaMessages.push({
role: anthropicMessage.role,
content: anthropicMessage.content,
})
} else {
if (anthropicMessage.role === "user") {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
}
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process tool result messages FIRST since they must follow the tool use messages
let toolResultImages: string[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
if (typeof toolMessage.content === "string") {
content = toolMessage.content
} else {
content =
toolMessage.content
?.map((part) => {
if (part.type === "image") {
toolResultImages.push(`data:${part.source.media_type};base64,${part.source.data}`)
return "(see following user message for image)"
}
return part.text
})
.join("\n") ?? ""
}
ollamaMessages.push({
role: "user",
images: toolResultImages.length > 0 ? toolResultImages : undefined,
content: content,
})
})
// Process non-tool messages
if (nonToolMessages.length > 0) {
ollamaMessages.push({
role: "user",
content: nonToolMessages
.map((part) => {
if (part.type === "image") {
return `data:${part.source.media_type};base64,${part.source.data}`
}
return part.text
})
.join("\n"),
})
}
} else if (anthropicMessage.role === "assistant") {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // assistant cannot send tool_result messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process non-tool messages
let content: string = ""
if (nonToolMessages.length > 0) {
content = nonToolMessages
.map((part) => {
if (part.type === "image") {
return "" // impossible as the assistant cannot send images
}
return part.text
})
.join("\n")
}
ollamaMessages.push({
role: "assistant",
content,
})
}
}
}
return ollamaMessages
}
+5 -2
View File
@@ -4,6 +4,7 @@ import { convertToR1Format } from "./r1-format"
import { ApiStream, ApiStreamChunk } from "./stream"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { OpenRouterErrorResponse } from "../providers/types"
export async function* streamOpenRouterFormatRequest(
client: OpenAI,
@@ -146,9 +147,11 @@ export async function* streamOpenRouterFormatRequest(
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!genId && chunk.id) {
+6
View File
@@ -79,6 +79,7 @@ type GlobalStateKey =
| "openAiModelInfo"
| "ollamaModelId"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
@@ -579,6 +580,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
@@ -624,6 +626,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
@@ -1901,6 +1904,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
@@ -1959,6 +1963,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
this.getGlobalState("ollamaApiOptionsCtxNum") as Promise<string | undefined>,
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
@@ -2040,6 +2045,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
+73
View File
@@ -46,6 +46,7 @@ export interface ApiHandlerOptions {
openAiModelInfo?: ModelInfo
ollamaModelId?: string
ollamaBaseUrl?: string
ollamaApiOptionsCtxNum?: string
lmStudioModelId?: string
lmStudioBaseUrl?: string
geminiApiKey?: string
@@ -298,6 +299,78 @@ export const vertexModels = {
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.03,
},
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.4,
},
"gemini-2.0-flash-thinking-exp-1219": {
maxTokens: 8192,
contextWindow: 32_767,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-exp": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-002": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-8b-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-pro-002": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-pro-exp-0827": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
export const openAiModelInfoSaneDefaults: ModelInfo = {
+1 -1
View File
@@ -815,7 +815,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
{isExpanded ? (
<div style={{ marginTop: -3 }}>
<span style={{ fontWeight: "bold", display: "block", marginBottom: "4px" }}>
Reasoning
Thinking
<span
className="codicon codicon-chevron-down"
style={{
@@ -1173,6 +1173,13 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
placeholder={"e.g. llama3.1"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
style={{ width: "100%" }}
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
placeholder={"e.g. 32768"}>
<span style={{ fontWeight: 500 }}>Model Context Window</span>
</VSCodeTextField>
{ollamaModels.length > 0 && (
<VSCodeRadioGroup
value={
@@ -59,7 +59,7 @@ const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
border-radius: 50%;
background: var(--vscode-foreground);
cursor: pointer;
border: 2px solid var(--vscode-progressBar-background);
border: 0px solid var(--vscode-progressBar-background);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
+15 -5
View File
@@ -180,7 +180,9 @@ export function getContextMenuOptions(
const seen = new Set()
const deduped = allItems.filter((item) => {
const key = `${item.type}-${item.value}`
if (seen.has(key)) return false
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
@@ -195,18 +197,26 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
const beforeCursor = text.slice(0, position)
const atIndex = beforeCursor.lastIndexOf("@")
if (atIndex === -1) return false
if (atIndex === -1) {
return false
}
const textAfterAt = beforeCursor.slice(atIndex + 1)
// Check if there's any whitespace after the '@'
if (/\s/.test(textAfterAt)) return false
if (/\s/.test(textAfterAt)) {
return false
}
// Don't show the menu if it's a URL
if (textAfterAt.toLowerCase().startsWith("http")) return false
if (textAfterAt.toLowerCase().startsWith("http")) {
return false
}
// Don't show the menu if it's a problems or terminal
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) {
return false
}
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks
+3 -1
View File
@@ -50,8 +50,10 @@ export const useShortcut = (shortcut: string, callback: any, options = { disable
if (Object.keys(modifierMap).includes(keyArray[0])) {
const finalKey = keyArray.pop()
if (!finalKey) return
if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) {
if (keyArray.every((k) => modifierMap[k]) && finalKey.toLowerCase() === event.key.toLowerCase()) {
event.preventDefault()
return callbackRef.current(event)
}
} else {
+3 -1
View File
@@ -35,7 +35,9 @@ export function findMatchingResourceOrTemplate(
): McpResource | McpResourceTemplate | undefined {
// First try to find an exact resource match
const exactMatch = resources.find((resource) => resource.uri === uri)
if (exactMatch) return exactMatch
if (exactMatch) {
return exactMatch
}
// If no exact match, try to find a matching template
return findMatchingTemplate(uri, templates)
@@ -36,7 +36,5 @@ export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
// We want to reschedule if any item in `deps` changed,
// or if `delay` changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, ...deps])
}
+3 -1
View File
@@ -35,7 +35,9 @@ export function hexToRGB(hexColor: string): { r: number; g: number; b: number }
export function colorToHex(colorVar: string): string {
const value = getComputedStyle(document.documentElement).getPropertyValue(colorVar).trim()
if (value.startsWith("#")) return value.slice(0, 7)
if (value.startsWith("#")) {
return value.slice(0, 7)
}
const rgbValues = value.match(/\d+/g)?.slice(0, 3).map(Number) || []
return `#${rgbValues.map((x) => x.toString(16).padStart(2, "0")).join("")}`