mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbb11b2931 |
@@ -1,88 +0,0 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix_prep:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- id: set-matrix
|
||||
run: |
|
||||
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
needs: matrix_prep
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: playwright-recordings-${{ matrix.runner }}
|
||||
path: |
|
||||
test-results/playwright/
|
||||
@@ -68,10 +68,6 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install local modules on windows
|
||||
if: runner.os == 'Windows' && steps.root-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
|
||||
+3
-3
@@ -34,8 +34,8 @@ src/shared/proto/host/*.ts
|
||||
# Webview
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
# Host bridge
|
||||
src/hosts/vscode/*/methods.ts
|
||||
src/hosts/vscode/*/index.ts
|
||||
src/hosts/vscode/client/host-grpc-client.ts
|
||||
src/hosts/vscode/host-grpc-service-config.ts
|
||||
src/standalone/server-setup.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { defineConfig } from "@vscode/test-cli"
|
||||
import path from "path"
|
||||
|
||||
export default defineConfig({
|
||||
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
|
||||
files: "{out/**/*.test.js,src/**/*.test.js}",
|
||||
mocha: {
|
||||
ui: "bdd",
|
||||
timeout: 20000, // Maximum time (in ms) that a test can run before failing
|
||||
|
||||
+4
-21
@@ -1,16 +1,15 @@
|
||||
# Default
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/
|
||||
dist-standalone/
|
||||
node_modules/
|
||||
out/**
|
||||
dist-standalone/**
|
||||
node_modules/**
|
||||
src/**
|
||||
standalone/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
esbuild.js
|
||||
vsc-extension-quickstart.md
|
||||
tsconfig*.json
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
@@ -24,17 +23,6 @@ eslint-rules/**
|
||||
.nvmrc
|
||||
.gitattributes
|
||||
.prettierignore
|
||||
.husky/
|
||||
.github/
|
||||
eslint-rules/
|
||||
old_docs/
|
||||
evals/
|
||||
.changie.yaml
|
||||
.codespellrc
|
||||
.mocharc.json
|
||||
buf.yaml
|
||||
.changeset/
|
||||
.clinerules/
|
||||
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
webview-ui/src/**
|
||||
@@ -59,8 +47,3 @@ old_docs/**
|
||||
|
||||
# Include icons
|
||||
!assets/icons/**
|
||||
|
||||
# Ignore E2E build files
|
||||
e2e-build.js
|
||||
e2e.vsix
|
||||
test-results/
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.5]
|
||||
|
||||
- Add Groq as a new API provider with support for all Groq models including Kimi-K2
|
||||
- Add user role display in organization UI for Cline account users
|
||||
- Fix message dialogs not showing option buttons properly
|
||||
- Fix authentication issues when using multiple VSCode windows
|
||||
|
||||
## [3.19.4]
|
||||
|
||||
- Add ability to choose Chinese endpoint for Moonshot provider
|
||||
|
||||
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. 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, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. 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.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: "Claude Code"
|
||||
description: "Use your Claude Max or Pro subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
|
||||
description: "Use your Claude Max subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
|
||||
---
|
||||
|
||||
**Website:** [https://docs.anthropic.com/en/docs/claude-code/setup](https://docs.anthropic.com/en/docs/claude-code/setup)
|
||||
|
||||
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max or Pro, this means you can use Claude in Cline without paying extra API costs.
|
||||
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max, this means you can use Claude in Cline without paying extra API costs.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
|
||||
+1
-12
@@ -5,7 +5,6 @@ const path = require("path")
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const e2eBuild = process.argv.includes("--e2e-build")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
/**
|
||||
@@ -161,18 +160,8 @@ const standaloneConfig = {
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
}
|
||||
|
||||
// E2E build script configuration
|
||||
const e2eBuildConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/test/e2e/utils/build.ts"],
|
||||
outfile: `${destDir}/e2e-build.js`,
|
||||
external: ["@vscode/test-electron", "execa"],
|
||||
sourcemap: false,
|
||||
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
|
||||
const config = standalone ? standaloneConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
await extensionCtx.watch()
|
||||
|
||||
@@ -36,7 +36,7 @@ It starts with our test cases. Each one is a JSON file in `./cases` that has the
|
||||
Then, for every test run, we set up a specific configuration. This includes which LLM we're testing, which system prompt it gets, which function we use to parse the model's raw output, and which function we use to actually apply the diff. Here's the command I've been using:
|
||||
|
||||
```bash
|
||||
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet,x-ai/grok-3-beta,anthropic/claude-3.7-sonnet,anthropic/claude-sonnet-4,google/gemini-2.5-pro-preview,google/gemini-2.5-flash" --max-cases 5 --valid-attempts-per-case 5 --parallel --diff-edit-function diff-06-26-25 --verbose
|
||||
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet-20241022,x-ai/grok-3-beta" --max-cases 4 --valid-attempts-per-case 2 --verbose --parallel
|
||||
```
|
||||
|
||||
This will build the eval script, run it, and then open the streamlit dashboard to show the results.
|
||||
|
||||
Generated
+124
-3891
File diff suppressed because it is too large
Load Diff
+3
-7
@@ -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.19.5",
|
||||
"version": "3.19.4",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -330,7 +330,7 @@
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
@@ -345,8 +345,6 @@
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
@@ -385,8 +383,7 @@
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/utils": "^8.33.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -428,7 +425,6 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isGitHubAction = !!process.env.CI
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: 20000,
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
})
|
||||
@@ -9,22 +9,22 @@ import chalk from "chalk"
|
||||
import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.mjs"
|
||||
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
|
||||
const PROTO_DIR = path.resolve("proto")
|
||||
const TS_OUT_DIR = path.resolve("src/shared/proto")
|
||||
const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto")
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const TS_PROTO_PLUGIN = isWindows
|
||||
? path.resolve("node_modules/.bin/protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
@@ -37,7 +37,12 @@ const TS_PROTO_OPTIONS = [
|
||||
]
|
||||
|
||||
// Service directories derived from imported serviceNameMap
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("src/core/controller", serviceKey))
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
|
||||
// Host service directories derived from imported hostServiceNameMap
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
|
||||
)
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
@@ -56,8 +61,8 @@ async function main() {
|
||||
await ensureProtoFilesExist()
|
||||
|
||||
// Process all proto files
|
||||
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR)
|
||||
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
|
||||
|
||||
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
|
||||
// grpc-js is used to generate service impls for the ProtoBus service.
|
||||
@@ -68,7 +73,7 @@ async function main() {
|
||||
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
PROTOC,
|
||||
`--proto_path="${PROTO_DIR}"`,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
@@ -84,12 +89,11 @@ async function main() {
|
||||
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateProtoBusServiceConfig()
|
||||
await generateProtoBusMethodRegistrations()
|
||||
await generateProtoBusGrpcClientConfig()
|
||||
|
||||
await generateHostBridgeServiceConfig()
|
||||
await generateHostBridgeMethodRegistrations()
|
||||
await generateMethodRegistrations()
|
||||
await generateHostMethodRegistrations()
|
||||
await generateServiceConfig()
|
||||
await generateHostServiceConfig()
|
||||
await generateGrpcClientConfig()
|
||||
|
||||
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
|
||||
}
|
||||
@@ -98,7 +102,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const command = [
|
||||
PROTOC,
|
||||
`--proto_path="${PROTO_DIR}"`,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
|
||||
`--ts_proto_out="${outDir}"`,
|
||||
`--ts_proto_opt=${protoOptions.join(",")} `,
|
||||
@@ -118,7 +122,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
* Generate a gRPC client configuration file for the webview
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateProtoBusGrpcClientConfig() {
|
||||
async function generateGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -143,7 +147,7 @@ async function generateProtoBusGrpcClientConfig() {
|
||||
|
||||
// Generate the file content
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createGrpcClient } from "./grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
@@ -154,7 +158,7 @@ export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
}
|
||||
@@ -217,12 +221,12 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
return streamingMethodsMap
|
||||
}
|
||||
|
||||
async function generateProtoBusMethodRegistrations() {
|
||||
async function generateMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const protoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, PROTO_DIR)
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
|
||||
|
||||
for (const serviceDir of serviceDirs) {
|
||||
const serviceName = path.basename(serviceDir)
|
||||
@@ -239,7 +243,7 @@ async function generateProtoBusMethodRegistrations() {
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
@@ -288,7 +292,7 @@ export function registerAllMethods(): void {
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
@@ -323,7 +327,7 @@ registerAllMethods()`
|
||||
* Generate a service configuration file that maps service names to their handlers
|
||||
* This eliminates the need for manual switch/case statements in grpc-handler.ts
|
||||
*/
|
||||
async function generateProtoBusServiceConfig() {
|
||||
async function generateServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -343,7 +347,7 @@ async function generateProtoBusServiceConfig() {
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { Controller } from "./index"
|
||||
import { StreamingResponseHandler } from "./grpc-handler"
|
||||
@@ -363,7 +367,7 @@ export interface ServiceHandlerConfig {
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.resolve("src/core/controller/grpc-service-config.ts")
|
||||
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
@@ -376,7 +380,7 @@ async function ensureProtoFilesExist() {
|
||||
log_verbose(chalk.cyan("Checking for missing proto files..."))
|
||||
|
||||
// Get existing proto files
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
|
||||
|
||||
// Check each service in serviceNameMap
|
||||
@@ -413,7 +417,7 @@ service ${serviceClassName} {
|
||||
`
|
||||
|
||||
// Write the template proto file
|
||||
const protoFilePath = path.join(PROTO_DIR, `${serviceName}.proto`)
|
||||
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
|
||||
await fs.writeFile(protoFilePath, protoContent)
|
||||
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
}
|
||||
@@ -423,22 +427,17 @@ service ${serviceClassName} {
|
||||
/**
|
||||
* Generate method registration files for host services
|
||||
*/
|
||||
async function generateHostBridgeMethodRegistrations() {
|
||||
async function generateHostMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating host method registration files..."))
|
||||
// Host service directories derived from imported hostServiceNameMap
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join("src/hosts/vscode/hostbridge", serviceKey),
|
||||
)
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(PROTO_DIR, "host") })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(PROTO_DIR, "host"))
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
|
||||
|
||||
for (const serviceDir of hostServiceDirs) {
|
||||
const serviceName = path.basename(serviceDir)
|
||||
const fullServiceName = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
const outputDir = path.join("src/generated/hosts/vscode/hostbridge", serviceName)
|
||||
|
||||
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
|
||||
@@ -450,7 +449,7 @@ async function generateHostBridgeMethodRegistrations() {
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
@@ -458,7 +457,7 @@ import { registerMethod } from "./index"\n`
|
||||
// Import implementations directly
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
methodsContent += `import { ${baseName} } from "@hosts/vscode/hostbridge/${serviceName}/${baseName}"\n`
|
||||
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
|
||||
}
|
||||
|
||||
// Add streaming methods information
|
||||
@@ -492,17 +491,17 @@ export function registerAllMethods(): void {
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
const registryFile = path.join(outputDir, "methods.ts")
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
await writeFileWithMkdirs(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "@hosts/vscode/hostbridge-grpc-service"
|
||||
import { StreamingResponseHandler } from "@hosts/vscode/hostbridge-grpc-handler"
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
|
||||
import { StreamingResponseHandler } from "../host-grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create ${serviceName} service registry
|
||||
@@ -522,7 +521,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
const indexFile = path.join(outputDir, "index.ts")
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
await writeFileWithMkdirs(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
@@ -533,7 +532,7 @@ registerAllMethods()`
|
||||
/**
|
||||
* Generate a service configuration file for host services
|
||||
*/
|
||||
async function generateHostBridgeServiceConfig() {
|
||||
async function generateHostServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating host service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -543,7 +542,7 @@ async function generateHostBridgeServiceConfig() {
|
||||
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
serviceImports.push(
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "@generated/hosts/vscode/hostbridge/${dirName}/index"`,
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
|
||||
)
|
||||
serviceConfigs.push(`
|
||||
"${fullServiceName}": {
|
||||
@@ -553,9 +552,9 @@ async function generateHostBridgeServiceConfig() {
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { StreamingResponseHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
/**
|
||||
@@ -572,7 +571,7 @@ export interface HostServiceHandlerConfig {
|
||||
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const filePath = "src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
|
||||
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
|
||||
}
|
||||
@@ -584,33 +583,15 @@ async function cleanup() {
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
await rmdir("src/generated")
|
||||
await rmdir(path.join(ROOT_DIR, "src", "generated"))
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
|
||||
await rmdir("src/standalone/services")
|
||||
await fs.rm("hosts/vscode", { force: true, recursive: true })
|
||||
await rmdir("hosts")
|
||||
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
|
||||
await rmdir(path.join(ROOT_DIR, "hosts"))
|
||||
|
||||
await fs.rm("src/standalone/server-setup.ts", { force: true })
|
||||
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
|
||||
const oldhostbridgefiles = [
|
||||
"src/hosts/vscode/workspace/methods.ts",
|
||||
"src/hosts/vscode/workspace/index.ts",
|
||||
"src/hosts/vscode/diff/methods.ts",
|
||||
"src/hosts/vscode/diff/index.ts",
|
||||
"src/hosts/vscode/env/methods.ts",
|
||||
"src/hosts/vscode/env/index.ts",
|
||||
"src/hosts/vscode/window/methods.ts",
|
||||
"src/hosts/vscode/window/index.ts",
|
||||
"src/hosts/vscode/watch/methods.ts",
|
||||
"src/hosts/vscode/watch/index.ts",
|
||||
"src/hosts/vscode/uri/methods.ts",
|
||||
"src/hosts/vscode/uri/index.ts",
|
||||
]
|
||||
for (const file of oldhostbridgefiles) {
|
||||
await fs.rm(file, { force: true })
|
||||
}
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
+1
-15
@@ -10,7 +10,6 @@ import "common.proto";
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
@@ -22,18 +21,5 @@ message OpenDiffRequest {
|
||||
}
|
||||
|
||||
message OpenDiffResponse {
|
||||
// A unique identifier for the diff view that was opened.
|
||||
optional string diff_id = 1;
|
||||
}
|
||||
|
||||
message ReplaceTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional string content = 3;
|
||||
optional int32 start_line = 4;
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message ReplaceTextResponse {
|
||||
// TBD
|
||||
// TODO(sfortune) the host needs to return a unique id for the diff editor.
|
||||
}
|
||||
|
||||
+3
-9
@@ -23,8 +23,6 @@ service ModelsService {
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -122,10 +120,9 @@ enum ApiProvider {
|
||||
XAI = 21;
|
||||
SAMBANOVA = 22;
|
||||
CEREBRAS = 23;
|
||||
GROQ = 24;
|
||||
SAPAICORE = 25;
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
SAPAICORE = 24;
|
||||
CLAUDE_CODE = 25;
|
||||
MOONSHOT = 26;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -243,7 +240,4 @@ message ModelsApiConfiguration {
|
||||
optional string aws_bedrock_api_key = 75;
|
||||
optional string moonshot_api_key = 76;
|
||||
optional string moonshot_api_line = 77;
|
||||
optional string groq_api_key = 78;
|
||||
optional string groq_model_id = 79;
|
||||
optional OpenRouterModelInfo groq_model_info = 80;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -222,13 +221,6 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: options.groqModelId,
|
||||
groqModelInfo: options.groqModelInfo,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
|
||||
@@ -69,6 +69,12 @@ export class ClineHandler implements ApiHandler {
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
try {
|
||||
// Only continue the request if the user:
|
||||
// 1. Has signed in to Cline with a token
|
||||
// 2. Has more than 0 credits
|
||||
// Or an error is thrown.
|
||||
await this.clineAccountService.validateRequest()
|
||||
|
||||
const client = await this.ensureClient()
|
||||
|
||||
this.lastGenerationId = undefined
|
||||
@@ -179,11 +185,12 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
const requestId = error?.request_id ? `\n | Request ID: ${error.request_id}` : ""
|
||||
const requestId = error?.request_id ? ` (Request ID: ${error.request_id})` : ""
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
|
||||
} else if (error.code === "insufficient_credits" || error.status === 402) {
|
||||
if (error.error) {
|
||||
error.error.message = error.error.message + requestId
|
||||
throw new Error(JSON.stringify(error.error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface GroqHandlerOptions {
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
// Model family definitions for enhanced behavior
|
||||
interface GroqModelFamily {
|
||||
name: string
|
||||
supportedFeatures: {
|
||||
streaming: boolean
|
||||
temperature: boolean
|
||||
vision: boolean
|
||||
tools: boolean
|
||||
}
|
||||
maxTokensOverride?: number
|
||||
specialParams?: Record<string, any>
|
||||
}
|
||||
|
||||
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
|
||||
// Moonshort 4 Family - Latest generation with vision support
|
||||
"kimi-k2": {
|
||||
name: "kimi-k2",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 4 Family - Latest generation with vision support
|
||||
llama4: {
|
||||
name: "Llama 4",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 3.3 Family - Balanced performance
|
||||
"llama3.3": {
|
||||
name: "Llama 3.3",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Llama 3.1 Family - Fast inference
|
||||
"llama3.1": {
|
||||
name: "Llama 3.1",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 131072,
|
||||
},
|
||||
// DeepSeek Family - Reasoning-optimized
|
||||
deepseek: {
|
||||
name: "DeepSeek",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
specialParams: {
|
||||
top_p: 0.95,
|
||||
reasoning_format: "parsed",
|
||||
},
|
||||
},
|
||||
// Qwen Family - Enhanced for Q&A
|
||||
qwen: {
|
||||
name: "Qwen",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Compound Models - Hybrid architectures
|
||||
compound: {
|
||||
name: "Compound",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
}
|
||||
|
||||
export class GroqHandler implements ApiHandler {
|
||||
private options: GroqHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: GroqHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.groqApiKey) {
|
||||
throw new Error("Groq API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Groq client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
const inputTokens = usage?.prompt_tokens || 0
|
||||
const outputTokens = usage?.completion_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the model family based on the model ID
|
||||
*/
|
||||
private detectModelFamily(modelId: string): GroqModelFamily {
|
||||
if (modelId.includes("kimi-k2")) {
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
// Llama 4 variants
|
||||
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
|
||||
return MODEL_FAMILIES.llama4
|
||||
}
|
||||
// Llama 3.3 variants
|
||||
if (modelId.includes("llama-3.3")) {
|
||||
return MODEL_FAMILIES["llama3.3"]
|
||||
}
|
||||
// Llama 3.1 variants
|
||||
if (modelId.includes("llama-3.1")) {
|
||||
return MODEL_FAMILIES["llama3.1"]
|
||||
}
|
||||
// DeepSeek variants
|
||||
if (modelId.includes("deepseek")) {
|
||||
return MODEL_FAMILIES.deepseek
|
||||
}
|
||||
// Qwen variants
|
||||
if (modelId.includes("qwen")) {
|
||||
return MODEL_FAMILIES.qwen
|
||||
}
|
||||
// Compound variants
|
||||
if (modelId.includes("compound")) {
|
||||
return MODEL_FAMILIES.compound
|
||||
}
|
||||
|
||||
// Default fallback to Llama 3.3 behavior
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal max_tokens based on model family and capabilities
|
||||
*/
|
||||
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
|
||||
// Use model-specific max tokens if available
|
||||
if (model.info.maxTokens && model.info.maxTokens > 0) {
|
||||
return model.info.maxTokens
|
||||
}
|
||||
|
||||
// Use family override if available
|
||||
if (modelFamily.maxTokensOverride) {
|
||||
return modelFamily.maxTokensOverride
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 8192
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
|
||||
// Optimize parameters based on model family
|
||||
const temperature = 0
|
||||
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// Build request parameters with model-specific optimizations
|
||||
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
|
||||
reasoning_format?: "parsed" | "raw" | "hidden"
|
||||
top_p?: number
|
||||
} = {
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
}
|
||||
|
||||
// Add any special parameters for specific model families
|
||||
if (modelFamily.specialParams) {
|
||||
Object.assign(requestParams, modelFamily.specialParams)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
const reasoningContent = (delta as any).reasoning as string
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle content field - trust the parsed output from Groq
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports vision/images
|
||||
*/
|
||||
supportsImages(): boolean {
|
||||
const model = this.getModel()
|
||||
return model.info.supportsImages === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports tools
|
||||
*/
|
||||
supportsTools(): boolean {
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
return modelFamily.supportedFeatures.tools
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model information with enhanced family detection
|
||||
*/
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
// First priority: groqModelId and groqModelInfo (like Requesty does)
|
||||
const groqModelId = this.options.groqModelId
|
||||
const groqModelInfo = this.options.groqModelInfo
|
||||
if (groqModelId && groqModelInfo) {
|
||||
return { id: groqModelId, info: groqModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: groqModelId with static model info
|
||||
if (groqModelId && groqModelId in groqModels) {
|
||||
const id = groqModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
if (apiModelId && apiModelId in groqModels) {
|
||||
const id = apiModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: groqDefaultModelId,
|
||||
info: groqModels[groqDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model family information for debugging/introspection
|
||||
*/
|
||||
getModelFamily(): GroqModelFamily {
|
||||
const model = this.getModel()
|
||||
return this.detectModelFamily(model.id)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
|
||||
@@ -461,6 +461,11 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Auth
|
||||
public async validateAuthState(state: string | null): Promise<boolean> {
|
||||
return state === this.authService.authNonce
|
||||
}
|
||||
|
||||
async handleAuthCallback(customToken: string, provider: string | null = null) {
|
||||
try {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { groqModels } from "../../../shared/api"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
|
||||
/**
|
||||
* Refreshes the Groq models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Groq models
|
||||
*/
|
||||
export async function refreshGroqModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
|
||||
// Get the Groq API key from the controller's state
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
const groqApiKey = apiConfiguration?.groqApiKey
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
if (!groqApiKey) {
|
||||
console.log("No Groq API key found, using static models as fallback")
|
||||
// Don't throw an error, just use static models
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure the API key is properly formatted
|
||||
const cleanApiKey = groqApiKey.trim()
|
||||
if (!cleanApiKey.startsWith("gsk_")) {
|
||||
throw new Error("Invalid Groq API key format. Groq API keys should start with 'gsk_'")
|
||||
}
|
||||
|
||||
console.log("Fetching Groq models with API key:", cleanApiKey.substring(0, 10) + "...")
|
||||
|
||||
const response = await axios.get("https://api.groq.com/openai/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cleanApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Cline-VSCode-Extension",
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
// Filter out non-chat models and validate model capabilities
|
||||
if (!isValidChatModel(rawModel)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have static pricing information for this model
|
||||
const staticModelInfo = groqModels[rawModel.id as keyof typeof groqModels]
|
||||
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens || 8192,
|
||||
contextWindow: rawModel.context_window || staticModelInfo?.contextWindow || 8192,
|
||||
supportsImages: detectImageSupport(rawModel, staticModelInfo),
|
||||
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
|
||||
inputPrice: staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: staticModelInfo?.outputPrice || 0,
|
||||
cacheWritesPrice: (staticModelInfo as any)?.cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (staticModelInfo as any).cacheReadsPrice || 0,
|
||||
description: generateModelDescription(rawModel, staticModelInfo),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from Groq API")
|
||||
}
|
||||
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
|
||||
console.log("Groq models fetched and saved", models)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Groq models:", error)
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = "Unknown error occurred"
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
errorMessage = "Invalid Groq API key. Please check your API key in settings."
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = "Access forbidden. Please verify your Groq API key has the correct permissions."
|
||||
} else if (error.response?.status === 429) {
|
||||
errorMessage = "Rate limit exceeded. Please try again later."
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timeout. Please check your internet connection."
|
||||
} else {
|
||||
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
console.error("Groq API Error:", errorMessage)
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readGroqModels(controller)
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
console.log("Using cached Groq models")
|
||||
models = cachedModels
|
||||
} else {
|
||||
// Fall back to static models from shared/api.ts
|
||||
console.log("Using static Groq models as fallback")
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
|
||||
// by filling in any missing required fields with defaults
|
||||
const typedModels: Record<string, OpenRouterModelInfo> = {}
|
||||
for (const [key, model] of Object.entries(models)) {
|
||||
typedModels[key] = {
|
||||
maxTokens: model.maxTokens ?? 8192,
|
||||
contextWindow: model.contextWindow ?? 8192,
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
supportsPromptCache: model.supportsPromptCache ?? false,
|
||||
inputPrice: model.inputPrice ?? 0,
|
||||
outputPrice: model.outputPrice ?? 0,
|
||||
cacheWritesPrice: model.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: model.cacheReadsPrice ?? 0,
|
||||
description: model.description ?? "",
|
||||
tiers: model.tiers ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached Groq models from disk
|
||||
*/
|
||||
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
const fileExists = await fileExistsAtPath(groqModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(groqModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
} catch (error) {
|
||||
console.error("Error reading cached Groq models:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a model is suitable for chat completions
|
||||
*/
|
||||
function isValidChatModel(rawModel: any): boolean {
|
||||
// Check if model is active (if the property exists)
|
||||
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
|
||||
return false
|
||||
}
|
||||
// Filter out non-chat models (whisper, TTS, guard models, etc.)
|
||||
if (
|
||||
rawModel.id.includes("whisper") ||
|
||||
rawModel.id.includes("tts") ||
|
||||
rawModel.id.includes("guard") ||
|
||||
rawModel.id.includes("embedding") ||
|
||||
rawModel.id.includes("moderation") ||
|
||||
rawModel.id.includes("allam")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if model supports chat completions
|
||||
if (rawModel.object === "model" && rawModel.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a model supports image input
|
||||
*/
|
||||
function detectImageSupport(rawModel: any, staticModelInfo?: any): boolean {
|
||||
// Use static info if available
|
||||
if (staticModelInfo?.supportsImages !== undefined) {
|
||||
return staticModelInfo.supportsImages
|
||||
}
|
||||
|
||||
// Detect based on model name patterns
|
||||
const modelId = rawModel.id.toLowerCase()
|
||||
if (modelId.includes("vision") || modelId.includes("maverick") || modelId.includes("scout")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a descriptive name for the model
|
||||
*/
|
||||
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
|
||||
// Use static description if available
|
||||
if (staticModelInfo?.description) {
|
||||
return staticModelInfo.description
|
||||
}
|
||||
|
||||
// Generate description based on model characteristics
|
||||
const modelId = rawModel.id
|
||||
const contextWindow = rawModel.context_window || 8192
|
||||
const ownedBy = rawModel.owned_by || "Unknown"
|
||||
|
||||
// Special handling for new models
|
||||
if (modelId.includes("compound")) {
|
||||
return `${ownedBy}'s ${modelId} model with ${contextWindow.toLocaleString()} token context window - Advanced compound architecture`
|
||||
}
|
||||
|
||||
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
@@ -42,17 +42,6 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
}
|
||||
})
|
||||
|
||||
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// update model info in state for Groq
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
if (apiConfiguration.groqModelId && response.models[apiConfiguration.groqModelId]) {
|
||||
await updateGlobalState(controller.context, "groqModelInfo", response.models[apiConfiguration.groqModelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
|
||||
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
|
||||
// (see normalizeApiConfiguration > openrouter)
|
||||
|
||||
@@ -13,7 +13,6 @@ export const GlobalFileNames = {
|
||||
contextHistory: "context_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
groqModels: "groq_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
|
||||
@@ -26,7 +26,6 @@ export type SecretKey =
|
||||
| "cerebrasApiKey"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "groqApiKey"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -118,7 +117,5 @@ export type GlobalStateKey =
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
| "groqModelId"
|
||||
| "groqModelInfo"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -167,7 +167,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
@@ -189,8 +188,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -247,7 +244,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
@@ -269,8 +265,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -345,7 +339,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (storedApiProvider) {
|
||||
// Use the explicitly stored provider - this respects user's selection
|
||||
apiProvider = storedApiProvider
|
||||
} else {
|
||||
// Either new user or legacy user that doesn't have the apiProvider stored in state
|
||||
@@ -449,9 +442,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -564,9 +554,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -605,8 +592,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
requestyModelInfo,
|
||||
togetherModelId,
|
||||
fireworksModelId,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
sapAiCoreModelId,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
@@ -667,7 +652,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
@@ -712,7 +696,6 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"groqApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
]
|
||||
|
||||
+2
-14
@@ -81,7 +81,7 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
@@ -1575,9 +1575,8 @@ export class Task {
|
||||
|
||||
await this.migrateDisableBrowserToolSetting()
|
||||
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
|
||||
const modelInfo = this.api.getModel()
|
||||
// cline browser tool uses image recognition for navigation (requires model image support).
|
||||
const modelSupportsBrowserUse = modelInfo.info.supportsImages ?? false
|
||||
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
@@ -1662,17 +1661,6 @@ export class Task {
|
||||
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
|
||||
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
|
||||
|
||||
const { statusCode, message, requestId } = extractErrorDetails(error)
|
||||
|
||||
// Capture provider failure telemetry
|
||||
telemetryService.captureProviderApiError({
|
||||
taskId: this.taskId,
|
||||
model: modelInfo.id,
|
||||
errorMessage: message,
|
||||
errorStatus: statusCode,
|
||||
requestId,
|
||||
})
|
||||
|
||||
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
|
||||
this.taskState.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
this.messageStateHandler.getApiConversationHistory(),
|
||||
|
||||
@@ -6,20 +6,13 @@ import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { ApiHandler } from "@/api"
|
||||
|
||||
export function formatErrorWithStatusCode(error: any): string {
|
||||
const { statusCode, message } = extractErrorDetails(error)
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
|
||||
// Only prepend the statusCode if it's not already part of the message
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
const requestId = error.request_id || error.response?.request_id || undefined
|
||||
|
||||
return { message, statusCode, requestId }
|
||||
}
|
||||
|
||||
export const showNotificationForApprovalIfAutoApprovalEnabled = (
|
||||
message: string,
|
||||
autoApprovalSettingsEnabled: boolean,
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
import * as vscode from "vscode"
|
||||
import { Uri } from "vscode"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { sendDidBecomeVisibleEvent } from "@core/controller/ui/subscribeToDidBecomeVisible"
|
||||
import { WebviewProvider } from "."
|
||||
import { sendDidBecomeVisibleEvent } from "../controller/ui/subscribeToDidBecomeVisible"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -262,16 +262,13 @@ export abstract class WebviewProvider {
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
// Only show the error message if not in development mode.
|
||||
if (!process.env.IS_DEV) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
}),
|
||||
)
|
||||
}
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
}),
|
||||
)
|
||||
|
||||
return this.getHtmlContent()
|
||||
}
|
||||
|
||||
+27
-4
@@ -32,12 +32,12 @@ import {
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
|
||||
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
|
||||
import { VscodeDiffViewProvider } from "./integrations/editor/VscodeDiffViewProvider"
|
||||
import { getHostBridgeProvider } from "@hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
|
||||
/*
|
||||
@@ -301,12 +301,35 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
break
|
||||
}
|
||||
case "/auth": {
|
||||
const authService = AuthService.getInstance()
|
||||
console.log("Auth callback received:", uri.toString())
|
||||
|
||||
const token = query.get("idToken")
|
||||
const state = query.get("state")
|
||||
const provider = query.get("provider")
|
||||
|
||||
console.log("Auth callback received:", { provider })
|
||||
console.log("Auth callback received:", {
|
||||
token: token,
|
||||
state: state,
|
||||
provider: provider,
|
||||
})
|
||||
|
||||
// Ask user to confirm on state mismatch. This enables signins initiated from
|
||||
// outside the extension (e.g. Cline web) to be handled correctly.
|
||||
if (authService.authNonce !== state) {
|
||||
const userConfirmation = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid auth state",
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
if (userConfirmation === "Cancel") {
|
||||
console.log("User declined to continue with auth callback due to state mismatch")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
await visibleWebview?.controller.handleAuthCallback(token, provider)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { GrpcHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
import { GrpcHandler } from "../host-grpc-handler"
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
|
||||
// Generic type for any protobuf service definition
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { createGrpcClient } from "@hosts/vscode/hostbridge/client/host-grpc-client-base"
|
||||
import { createGrpcClient } from "@hosts/vscode/client/host-grpc-client-base"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
import * as host from "@shared/proto/index.host"
|
||||
|
||||
Vendored
Vendored
@@ -1,5 +1,5 @@
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "@generated/hosts/vscode/hostbridge-grpc-service-config"
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StreamingResponseHandler } from "./hostbridge-grpc-handler"
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
|
||||
/**
|
||||
* Generic type for service method handlers
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
|
||||
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as fsSync from "fs"
|
||||
import { SubscribeToFileRequest, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
|
||||
|
||||
// Debounce configuration
|
||||
const DEBOUNCE_DELAY = 100 // ms
|
||||
+5
-7
@@ -1,24 +1,22 @@
|
||||
import { window } from "vscode"
|
||||
import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
|
||||
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
|
||||
const { message, type, options } = request
|
||||
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
|
||||
const option = { modal, detail }
|
||||
const { modal, detail, items } = options || {}
|
||||
const option = items ? { modal, items } : { modal, detail }
|
||||
|
||||
let selectedOption: string | undefined = undefined
|
||||
|
||||
switch (type) {
|
||||
case ShowMessageType.ERROR:
|
||||
selectedOption = await window.showErrorMessage(message, option, ...items)
|
||||
selectedOption = await window.showErrorMessage(message, option)
|
||||
break
|
||||
case ShowMessageType.WARNING:
|
||||
selectedOption = await window.showWarningMessage(message, option, ...items)
|
||||
selectedOption = await window.showWarningMessage(message, option)
|
||||
break
|
||||
default:
|
||||
selectedOption = await window.showInformationMessage(message, option, ...items)
|
||||
selectedOption = await window.showInformationMessage(message, option)
|
||||
break
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { arePathsEqual } from "@/utils/path"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { DecorationController } from "@integrations/editor/DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "./DiffViewProvider"
|
||||
|
||||
export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
@@ -82,6 +82,42 @@ export class ClineAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the user has sufficient credits to make API requests.
|
||||
* This checks the user's balance and throws an error if the balance is insufficient or if the request fails.
|
||||
* @throws Error if the user has insufficient credits or if the request fails
|
||||
* @returns {Promise<void>} A promise that resolves if the user has sufficient credits.
|
||||
*/
|
||||
async validateRequest(): Promise<void> {
|
||||
try {
|
||||
const { organizations, id } = await this.authenticatedRequest<UserResponse>(`/api/v1/users/me`)
|
||||
const activeOrganization = organizations.find((org) => org.active)
|
||||
console.log("SwitchAuthToken: Active Organization", activeOrganization?.name || "No active organization")
|
||||
|
||||
// Skip balance check for active organizations
|
||||
if (activeOrganization) {
|
||||
return
|
||||
}
|
||||
|
||||
const balance = await this.authenticatedRequest<BalanceResponse>(`/api/v1/users/${id}/balance`)
|
||||
const currentBalance = Number(balance?.balance) || 0
|
||||
|
||||
// Throw error if insufficient credits (balance <= 0)
|
||||
if (currentBalance <= 0) {
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
code: "insufficient_credits",
|
||||
current_balance: currentBalance,
|
||||
message: "Not enough credits available",
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Invalid Cline API request:", error)
|
||||
throw error instanceof Error ? error : new Error(`Invalid Request: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC variant that fetches the user's current credit balance without posting to webview
|
||||
* @returns Balance data or undefined if failed
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { EmptyRequest, String } from "../../shared/proto/common"
|
||||
import { AuthState, UserInfo } from "../../shared/proto/account"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/grpc-handler"
|
||||
@@ -50,6 +51,7 @@ export class AuthService {
|
||||
private _authenticated: boolean = false
|
||||
private _clineAuthInfo: ClineAuthInfo | null = null
|
||||
private _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
private readonly _authNonce = crypto.randomBytes(32).toString("hex")
|
||||
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
|
||||
private _context: vscode.ExtensionContext
|
||||
|
||||
@@ -156,6 +158,10 @@ export class AuthService {
|
||||
this._setProvider(providerName)
|
||||
}
|
||||
|
||||
get authNonce(): string {
|
||||
return this._authNonce
|
||||
}
|
||||
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
@@ -214,6 +220,7 @@ export class AuthService {
|
||||
|
||||
// Use URL object for more graceful query construction
|
||||
const authUrl = new URL(this._config.URI)
|
||||
authUrl.searchParams.set("state", this._authNonce)
|
||||
authUrl.searchParams.set("callback_url", callbackUrl)
|
||||
|
||||
const authUrlString = authUrl.toString()
|
||||
|
||||
@@ -29,11 +29,6 @@ interface Collection {
|
||||
*/
|
||||
type TelemetryCategory = "checkpoints" | "browser"
|
||||
|
||||
/**
|
||||
* Maximum length for error messages to prevent excessive data
|
||||
*/
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 500
|
||||
|
||||
class TelemetryService {
|
||||
// Map to control specific telemetry categories (event types)
|
||||
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
|
||||
@@ -88,8 +83,6 @@ class TelemetryService {
|
||||
BROWSER_ERROR: "task.browser_error",
|
||||
// Tracks Gemini API specific performance metrics
|
||||
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
|
||||
// Tracks when API providers return errors
|
||||
PROVIDER_API_ERROR: "task.provider_api_error",
|
||||
// Collection of all task events
|
||||
TASK_COLLECTION: "task.collection",
|
||||
},
|
||||
@@ -727,38 +720,6 @@ class TelemetryService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records telemetry when an API provider returns an error
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param model Identifier of the model used
|
||||
* @param requestId Unique identifier for the specific API request
|
||||
* @param errorMessage Detailed error message from the API provider
|
||||
* @param errorStatus HTTP status code of the error response, if available
|
||||
* @param collect Optional flag to determine if the event should be collected for batch sending
|
||||
*/
|
||||
public captureProviderApiError(
|
||||
args: {
|
||||
taskId: string
|
||||
model: string
|
||||
errorMessage: string
|
||||
errorStatus?: number | undefined
|
||||
requestId?: string | undefined
|
||||
},
|
||||
collect: boolean = true,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.PROVIDER_API_ERROR,
|
||||
properties: {
|
||||
...args,
|
||||
errorMessage: args.errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH), // Truncate long error messages
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
collect,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is enabled
|
||||
* @returns Boolean indicating whether telemetry is enabled
|
||||
|
||||
@@ -28,7 +28,6 @@ export type ApiProvider =
|
||||
| "sambanova"
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
| "groq"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -100,9 +99,6 @@ export interface ApiHandlerOptions {
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
cerebrasApiKey?: string
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
requestTimeoutMs?: number
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
@@ -2407,95 +2403,6 @@ export const cerebrasModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Groq
|
||||
// https://console.groq.com/docs/models
|
||||
// https://groq.com/pricing/
|
||||
export type GroqModelId = keyof typeof groqModels
|
||||
export const groqDefaultModelId: GroqModelId = "moonshotai/kimi-k2-instruct"
|
||||
export const groqModels = {
|
||||
// Compound Beta Models - Hybrid architectures optimized for tool use
|
||||
"compound-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description:
|
||||
"Compound model using Llama 4 Scout for core reasoning with Llama 3.3 70B for routing and tool use. Excellent for plan/act workflows.",
|
||||
},
|
||||
"compound-beta-mini": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description: "Lightweight compound model for faster inference while maintaining tool use capabilities.",
|
||||
},
|
||||
// DeepSeek Models - Reasoning-optimized
|
||||
"deepseek-r1-distill-llama-70b": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.75,
|
||||
outputPrice: 0.99,
|
||||
description:
|
||||
"DeepSeek R1 reasoning capabilities distilled into Llama 70B architecture. Excellent for complex problem-solving and planning.",
|
||||
},
|
||||
// Llama 4 Models
|
||||
"meta-llama/llama-4-maverick-17b-128e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.6,
|
||||
description: "Meta's Llama 4 Maverick 17B model with 128 experts, supports vision and multimodal tasks.",
|
||||
},
|
||||
"meta-llama/llama-4-scout-17b-16e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.11,
|
||||
outputPrice: 0.34,
|
||||
description: "Meta's Llama 4 Scout 17B model with 16 experts, optimized for fast inference and general tasks.",
|
||||
},
|
||||
// Llama 3.3 Models
|
||||
"llama-3.3-70b-versatile": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.59,
|
||||
outputPrice: 0.79,
|
||||
description: "Meta's latest Llama 3.3 70B model optimized for versatile use cases with excellent performance and speed.",
|
||||
},
|
||||
// Llama 3.1 Models - Fast inference
|
||||
"llama-3.1-8b-instant": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.08,
|
||||
description: "Fast and efficient Llama 3.1 8B model optimized for speed, low latency, and reliable tool execution.",
|
||||
},
|
||||
// Mistral Models
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 3.0,
|
||||
description:
|
||||
"Kimi K2 is Moonshot AI's state-of-the-art Mixture-of-Experts (MoE) language model with 1 trillion total parameters and 32 billion activated parameters.",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Requesty
|
||||
// https://requesty.ai/models
|
||||
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
|
||||
|
||||
@@ -236,8 +236,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.SAMBANOVA
|
||||
case "cerebras":
|
||||
return ProtoApiProvider.CEREBRAS
|
||||
case "groq":
|
||||
return ProtoApiProvider.GROQ
|
||||
case "sapaicore":
|
||||
return ProtoApiProvider.SAPAICORE
|
||||
case "claude-code":
|
||||
@@ -300,8 +298,6 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "sambanova"
|
||||
case ProtoApiProvider.CEREBRAS:
|
||||
return "cerebras"
|
||||
case ProtoApiProvider.GROQ:
|
||||
return "groq"
|
||||
case ProtoApiProvider.SAPAICORE:
|
||||
return "sapaicore"
|
||||
case ProtoApiProvider.CLAUDE_CODE:
|
||||
@@ -382,9 +378,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
sambanovaApiKey: config.sambanovaApiKey,
|
||||
cerebrasApiKey: config.cerebrasApiKey,
|
||||
groqApiKey: config.groqApiKey,
|
||||
groqModelId: config.groqModelId,
|
||||
groqModelInfo: convertModelInfoToProtoOpenRouter(config.groqModelInfo),
|
||||
requestTimeoutMs: config.requestTimeoutMs,
|
||||
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
@@ -468,9 +461,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
reasoningEffort: protoConfig.reasoningEffort,
|
||||
sambanovaApiKey: protoConfig.sambanovaApiKey,
|
||||
cerebrasApiKey: protoConfig.cerebrasApiKey,
|
||||
groqApiKey: protoConfig.groqApiKey,
|
||||
groqModelId: protoConfig.groqModelId,
|
||||
groqModelInfo: convertProtoToModelInfo(protoConfig.groqModelInfo),
|
||||
requestTimeoutMs: protoConfig.requestTimeoutMs,
|
||||
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
|
||||
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
|
||||
|
||||
@@ -2,27 +2,17 @@ import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
private activeDiffEditorId: string | undefined
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
const response = await getHostBridgeProvider().diffClient.openDiff({
|
||||
path: this.absolutePath,
|
||||
content: this.originalContent ?? "",
|
||||
})
|
||||
this.activeDiffEditorId = response.diffId
|
||||
getHostBridgeProvider().diffClient.openDiff({ path: this.absolutePath, content: this.originalContent ?? "" })
|
||||
}
|
||||
override async replaceText(
|
||||
override replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
_currentLine: number,
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
await getHostBridgeProvider().diffClient.replaceText({
|
||||
diffId: this.activeDiffEditorId,
|
||||
content: content,
|
||||
startLine: rangeToReplace.startLine,
|
||||
endLine: rangeToReplace.endLine,
|
||||
})
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# E2E Tests
|
||||
|
||||
This directory contains the end-to-end tests for the extension using Playwright. These tests simulate user interactions with the extension in a real VS Code environment.
|
||||
|
||||
## Running Tests
|
||||
|
||||
To build the test environment and run all E2E tests:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
To run all E2E tests without re-building the test environment (e.g. only test files were updated):
|
||||
|
||||
```bash
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
To run E2E tests in debug mode:
|
||||
|
||||
```bash
|
||||
npm run test:e2e -- --debug
|
||||
# Or only run the tests without re-building
|
||||
npm run e2e -- --debug
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
TBC
|
||||
@@ -1,57 +0,0 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { e2e } from "./utils/helpers"
|
||||
|
||||
e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
|
||||
// Verify initial state
|
||||
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).toBeVisible()
|
||||
await expect(sidebar.getByRole("button", { name: "Use your own API key" })).toBeVisible()
|
||||
|
||||
// Navigate to API key setup
|
||||
await sidebar.getByRole("button", { name: "Use your own API key" }).click()
|
||||
|
||||
const providerSelector = sidebar.locator("#api-provider div").first()
|
||||
|
||||
// Verify provider selector is visible and set to OpenRouter
|
||||
await expect(sidebar.locator("slot").filter({ hasText: /^OpenRouter$/ })).toBeVisible()
|
||||
|
||||
// Test Cline provider option
|
||||
await providerSelector.click({ delay: 100 })
|
||||
await expect(sidebar.getByRole("option", { name: "Cline" })).toBeVisible()
|
||||
await sidebar.getByRole("option", { name: "Cline" }).click({ delay: 100 })
|
||||
await expect(sidebar.getByRole("button", { name: "Sign Up with Cline" })).toBeVisible()
|
||||
|
||||
// Switch to OpenRouter and complete setup
|
||||
await providerSelector.click({ delay: 100 })
|
||||
await sidebar.getByRole("option", { name: "OpenRouter" }).click({ delay: 100 })
|
||||
|
||||
const apiKeyInput = sidebar.getByRole("textbox", { name: "OpenRouter API Key" })
|
||||
await apiKeyInput.fill("test-api-key")
|
||||
await expect(apiKeyInput).toHaveValue("test-api-key")
|
||||
await apiKeyInput.click({ delay: 100 })
|
||||
const submitButton = sidebar.getByRole("button", { name: "Let's go!" })
|
||||
await expect(submitButton).toBeEnabled()
|
||||
await submitButton.click({ delay: 100 })
|
||||
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).not.toBeVisible()
|
||||
|
||||
// Verify start up page is no longer visible
|
||||
await expect(apiKeyInput).not.toBeVisible()
|
||||
await expect(providerSelector).not.toBeVisible()
|
||||
|
||||
// Verify you are now in the chat page after setup was completed
|
||||
const clineLogo = sidebar.getByRole("img").filter({ hasText: /^$/ }).locator("path")
|
||||
await expect(clineLogo).toBeVisible()
|
||||
const chatInputBox = sidebar.getByTestId("chat-input")
|
||||
await expect(chatInputBox).toBeVisible()
|
||||
|
||||
// Verify the help improve banner is visible and can be closed.
|
||||
const helpBanner = sidebar.getByText("Help Improve Cline")
|
||||
await expect(helpBanner).toBeVisible()
|
||||
await sidebar.getByRole("button", { name: "Close banner and enable" }).click()
|
||||
await expect(helpBanner).not.toBeVisible()
|
||||
|
||||
// Verify the release banner is visible for new installs and can be closed.
|
||||
const releaseBanner = sidebar.getByRole("heading", { name: /^🎉 New in v\d/ })
|
||||
await expect(releaseBanner).toBeVisible()
|
||||
await sidebar.getByTestId("close-button").locator("span").first().click()
|
||||
await expect(releaseBanner).not.toBeVisible()
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { e2e, signin } from "./utils/helpers"
|
||||
|
||||
e2e("Chat - can send messages and switch between modes", async ({ page, sidebar }) => {
|
||||
// Sign in
|
||||
await signin(sidebar)
|
||||
|
||||
// Submit a message
|
||||
const inputbox = sidebar.getByTestId("chat-input")
|
||||
await expect(inputbox).toBeVisible()
|
||||
await inputbox.fill("Hello, Cline!")
|
||||
await expect(inputbox).toHaveValue("Hello, Cline!")
|
||||
await sidebar.getByTestId("send-button").click({ delay: 100 })
|
||||
await expect(inputbox).toHaveValue("")
|
||||
|
||||
// Loading State initially
|
||||
await expect(sidebar.getByText("API Request...")).toBeVisible()
|
||||
|
||||
// The request should eventually fail
|
||||
await expect(sidebar.getByText("API Request Failed")).toBeVisible()
|
||||
|
||||
await expect(inputbox).toBeVisible()
|
||||
|
||||
await expect(sidebar.getByRole("button", { name: "Retry" })).toBeVisible()
|
||||
await expect(sidebar.getByRole("button", { name: "Start New Task" })).toBeVisible()
|
||||
|
||||
// Starting a new task should clear the current chat view and show the recent tasks
|
||||
await sidebar.getByRole("button", { name: "Start New Task" }).click()
|
||||
await expect(sidebar.getByText("API Request Failed")).not.toBeVisible()
|
||||
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
|
||||
await expect(sidebar.getByText("Hello, Cline!")).toBeVisible()
|
||||
|
||||
// Makes sure the act and plan switches are working correctly
|
||||
// Aria-checked state should be true for Act and false for Plan
|
||||
const actButton = sidebar.getByRole("switch", { name: "Act" })
|
||||
const planButton = sidebar.getByRole("switch", { name: "Plan" })
|
||||
|
||||
await expect(actButton).toBeChecked()
|
||||
await expect(planButton).not.toBeChecked()
|
||||
|
||||
await actButton.click()
|
||||
await expect(actButton).not.toBeChecked()
|
||||
await expect(planButton).toBeChecked()
|
||||
|
||||
await sidebar.getByTestId("chat-input").fill("Plan mode submission")
|
||||
await sidebar.getByTestId("send-button").click()
|
||||
|
||||
await expect(sidebar.getByText("API Request Failed")).toBeVisible()
|
||||
})
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
.vscode
|
||||
@@ -1,3 +0,0 @@
|
||||
# Test Workspace
|
||||
|
||||
This workspace is used for testing the extension in a controlled environment.
|
||||
@@ -1,10 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Test Workspace</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Workspace</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Script to install dependencies for running E2E tests in GitHub Actions.
|
||||
*/
|
||||
import { SilentReporter, downloadAndUnzipVSCode } from "@vscode/test-electron"
|
||||
import { execa } from "execa"
|
||||
|
||||
const TIMEOUT_MINUTE = 1
|
||||
const INSTALL_TIMEOUT_MS = TIMEOUT_MINUTE * 60 * 1000
|
||||
|
||||
async function installVSCode() {
|
||||
const VSCODE_APP_TYPE = "stable"
|
||||
console.log("Downloading VS Code...")
|
||||
return await downloadAndUnzipVSCode(VSCODE_APP_TYPE, undefined, new SilentReporter())
|
||||
}
|
||||
|
||||
async function installChromium() {
|
||||
console.log("Installing Playwright Chromium...")
|
||||
try {
|
||||
await execa("npm", ["exec", "playwright", "install", "chromium"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
console.log("Playwright Chromium installation completed successfully")
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to install Playwright Chromium: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function installDependencies() {
|
||||
return Promise.all([installVSCode(), installChromium()])
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Installation timed out.")), INSTALL_TIMEOUT_MS),
|
||||
)
|
||||
await Promise.race([installDependencies(), timeoutPromise])
|
||||
console.log("Installation complete.")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Failed to install dependencies for E2E test", error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,187 +0,0 @@
|
||||
import { type ElectronApplication, type Frame, type Page, test, expect } from "@playwright/test"
|
||||
import { type PathLike, type RmOptions, mkdtempSync, rmSync } from "node:fs"
|
||||
import { _electron } from "playwright"
|
||||
import { SilentReporter, downloadAndUnzipVSCode } from "@vscode/test-electron"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
|
||||
interface E2ETestDirectories {
|
||||
workspaceDir: string
|
||||
userDataDir: string
|
||||
extensionsDir: string
|
||||
}
|
||||
|
||||
// Constants
|
||||
const CODEBASE_ROOT_DIR = path.resolve(__dirname, "..", "..", "..", "..")
|
||||
const E2E_TESTS_DIR = path.join(CODEBASE_ROOT_DIR, "src", "test", "e2e")
|
||||
|
||||
// Path utilities
|
||||
const escapeToPath = (text: string): string => text.trim().toLowerCase().replaceAll(/\W/g, "_")
|
||||
const getResultsDir = (testName = "", label?: string): string => {
|
||||
const testDir = path.join(CODEBASE_ROOT_DIR, "test-results", "playwright", escapeToPath(testName))
|
||||
return label ? path.join(testDir, label) : testDir
|
||||
}
|
||||
|
||||
async function waitUntil(predicate: () => boolean | Promise<boolean>, maxDelay = 5000): Promise<void> {
|
||||
let delay = 10
|
||||
const start = Date.now()
|
||||
|
||||
while (!(await predicate())) {
|
||||
if (Date.now() - start > maxDelay) {
|
||||
throw new Error(`waitUntil timeout after ${maxDelay}ms`)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
delay = Math.min(delay << 1, 1000) // Cap at 1s
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSidebar(page: Page): Promise<Frame> {
|
||||
let cachedFrame: Frame | null = null
|
||||
|
||||
const findSidebarFrame = async (): Promise<Frame | null> => {
|
||||
// Check cached frame first
|
||||
if (cachedFrame && !cachedFrame.isDetached()) {
|
||||
return cachedFrame
|
||||
}
|
||||
|
||||
for (const frame of page.frames()) {
|
||||
if (frame.isDetached()) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const title = await frame.title()
|
||||
if (title.startsWith("Cline")) {
|
||||
cachedFrame = frame
|
||||
return frame
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (!error.message.includes("detached") && !error.message.includes("navigation")) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
await waitUntil(async () => (await findSidebarFrame()) !== null)
|
||||
return (await findSidebarFrame()) || page.mainFrame()
|
||||
}
|
||||
|
||||
export async function rmForRetries(path: PathLike, options?: RmOptions): Promise<void> {
|
||||
const maxAttempts = 3 // Reduced from 5
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
rmSync(path, options)
|
||||
return
|
||||
} catch (error) {
|
||||
if (attempt === maxAttempts) {
|
||||
throw new Error(`Failed to rmSync ${path} after ${maxAttempts} attempts: ${error}`)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * attempt)) // Progressive delay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function signin(webview: Frame): Promise<void> {
|
||||
const byokButton = webview.getByRole("button", { name: "Use your own API key" })
|
||||
await expect(byokButton).toBeVisible()
|
||||
|
||||
await byokButton.click()
|
||||
|
||||
// Complete setup with OpenRouter
|
||||
const apiKeyInput = webview.getByRole("textbox", { name: "OpenRouter API Key" })
|
||||
await apiKeyInput.fill("test-api-key")
|
||||
await webview.getByRole("button", { name: "Let's go!" }).click()
|
||||
|
||||
// Verify start up page is no longer visible
|
||||
await expect(webview.locator("#api-provider div").first()).not.toBeVisible()
|
||||
await expect(byokButton).not.toBeVisible()
|
||||
}
|
||||
|
||||
export async function openClineSidebar(page: Page): Promise<void> {
|
||||
await page.getByRole("tab", { name: /Cline/ }).locator("a").click()
|
||||
}
|
||||
|
||||
export async function runCommandPalette(page: Page, command: string): Promise<void> {
|
||||
await page.locator("li").filter({ hasText: "[Extension Development Host]" }).first().click()
|
||||
const editorSearchBar = page.getByRole("textbox", { name: "Search files by name (append" })
|
||||
await expect(editorSearchBar).toBeVisible()
|
||||
await editorSearchBar.click()
|
||||
await editorSearchBar.fill(`>${command}`)
|
||||
await page.keyboard.press("Enter")
|
||||
}
|
||||
|
||||
// Test configuration
|
||||
export const e2e = test
|
||||
.extend<E2ETestDirectories>({
|
||||
workspaceDir: async ({}, use) => {
|
||||
await use(path.join(E2E_TESTS_DIR, "fixtures", "workspace"))
|
||||
},
|
||||
userDataDir: async ({}, use) => {
|
||||
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
|
||||
},
|
||||
extensionsDir: async ({}, use) => {
|
||||
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
|
||||
},
|
||||
})
|
||||
.extend<{ openVSCode: () => Promise<ElectronApplication> }>({
|
||||
openVSCode: async ({ workspaceDir, userDataDir, extensionsDir }, use, testInfo) => {
|
||||
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
|
||||
|
||||
await use(async () => {
|
||||
const app = await _electron.launch({
|
||||
executablePath,
|
||||
env: { ...process.env, IS_DEV: "true", TEMP_PROFILE: "true", E2E_TEST: "true" },
|
||||
recordVideo: { dir: getResultsDir(testInfo.title, "recordings") },
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-updates",
|
||||
"--disable-workspace-trust",
|
||||
"--skip-welcome",
|
||||
"--skip-release-notes",
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
`--extensions-dir=${extensionsDir}`,
|
||||
`--install-extension=${path.join(CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`,
|
||||
`--extensionDevelopmentPath=${CODEBASE_ROOT_DIR}`,
|
||||
workspaceDir,
|
||||
],
|
||||
})
|
||||
await waitUntil(() => app.windows().length > 0)
|
||||
return app
|
||||
})
|
||||
},
|
||||
})
|
||||
.extend<{ app: ElectronApplication }>({
|
||||
app: async ({ openVSCode, userDataDir, extensionsDir }, use) => {
|
||||
const app = await openVSCode()
|
||||
|
||||
try {
|
||||
await use(app)
|
||||
} finally {
|
||||
await app.close()
|
||||
// Cleanup in parallel
|
||||
await Promise.allSettled([
|
||||
rmForRetries(userDataDir, { recursive: true }),
|
||||
rmForRetries(extensionsDir, { recursive: true }),
|
||||
])
|
||||
}
|
||||
},
|
||||
})
|
||||
.extend({
|
||||
page: async ({ app }, use) => {
|
||||
const page = await app.firstWindow()
|
||||
await runCommandPalette(page, "notifications: toggle do not disturb")
|
||||
await openClineSidebar(page)
|
||||
await use(page)
|
||||
},
|
||||
})
|
||||
.extend<{ sidebar: Frame }>({
|
||||
sidebar: async ({ page }, use) => {
|
||||
const sidebar = await getSidebar(page)
|
||||
await use(sidebar)
|
||||
},
|
||||
})
|
||||
|
||||
export { getResultsDir }
|
||||
@@ -1,22 +0,0 @@
|
||||
import { rmSync } from "node:fs"
|
||||
import { getResultsDir } from "./helpers"
|
||||
|
||||
export default async function (): Promise<void> {
|
||||
const path = getResultsDir()
|
||||
const options = { recursive: true, force: true }
|
||||
|
||||
const maxAttempts = 2
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
rmSync(path, options)
|
||||
return
|
||||
} catch (error) {
|
||||
if (attempt === maxAttempts) {
|
||||
throw new Error(`Failed to rmSync ${path} after ${maxAttempts} attempts: ${error}`)
|
||||
}
|
||||
console.error(`Failed to rmSync ${path} after ${attempt} attempts: ${error}`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * attempt)) // Progressive delay
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import type { FullConfig } from "playwright/test"
|
||||
import { getResultsDir, rmForRetries } from "./helpers"
|
||||
|
||||
export default async function (_: FullConfig) {
|
||||
const assetsDir = getResultsDir()
|
||||
|
||||
try {
|
||||
const results = await fs.readdir(assetsDir, { withFileTypes: true })
|
||||
await Promise.all(
|
||||
results
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map(async (entry) => {
|
||||
const dirPath = path.join(assetsDir, entry.name)
|
||||
const recordingsPath = getResultsDir(entry.name, "recordings")
|
||||
const recordings = await fs.readdir(recordingsPath)
|
||||
// If there is only one recording, it means the test passed as no retries were needed.
|
||||
if (recordings.length === 1) {
|
||||
await rmForRetries(dirPath, { recursive: true, force: true })
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
// Silently handle case where assets directory doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -34,5 +34,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*"],
|
||||
"exclude": ["node_modules", ".vscode-test", "webview-ui", "src/test/e2e/**/*"]
|
||||
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*.test.ts"],
|
||||
"exclude": ["src/test/**/*.js", "src/**/__tests__/*", "src/test/e2e/**/*.test.ts"]
|
||||
"exclude": ["src/test/**/*.js", "src/**/__tests__/*"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeDivider,
|
||||
VSCodeLink,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeTag,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useCallback, useEffect, useState, useRef } from "react"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
@@ -102,15 +95,6 @@ const AccountView = ({ onDone }: AccountViewProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const getMainRole = (roles?: string[]) => {
|
||||
if (!roles) return undefined
|
||||
|
||||
if (roles.includes("owner")) return "Owner"
|
||||
if (roles.includes("admin")) return "Admin"
|
||||
|
||||
return "Member"
|
||||
}
|
||||
|
||||
export const ClineAccountView = () => {
|
||||
const { clineUser, handleSignIn, handleSignOut } = useClineAuth()
|
||||
const { userInfo, apiConfiguration } = useExtensionState()
|
||||
@@ -237,28 +221,21 @@ export const ClineAccountView = () => {
|
||||
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{user.email}</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-center mt-1">
|
||||
{userOrganizations && (
|
||||
<VSCodeDropdown
|
||||
key={activeOrganization?.organizationId || "personal"}
|
||||
currentValue={activeOrganization?.organizationId || ""}
|
||||
onChange={handleOrganizationChange}
|
||||
disabled={isSwitchingOrg || isLoading}
|
||||
className="w-full">
|
||||
<VSCodeOption value="">Personal</VSCodeOption>
|
||||
{userOrganizations.map((org: UserOrganization) => (
|
||||
<VSCodeOption key={org.organizationId} value={org.organizationId}>
|
||||
{org.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
)}
|
||||
{activeOrganization?.roles && (
|
||||
<VSCodeTag className="text-xs p-2" title="Role">
|
||||
{getMainRole(activeOrganization.roles)}
|
||||
</VSCodeTag>
|
||||
)}
|
||||
</div>
|
||||
{userOrganizations && (
|
||||
<VSCodeDropdown
|
||||
key={activeOrganization?.organizationId || "personal"}
|
||||
currentValue={activeOrganization?.organizationId || ""}
|
||||
onChange={handleOrganizationChange}
|
||||
disabled={isSwitchingOrg || isLoading}
|
||||
style={{ width: "100%", marginTop: "4px" }}>
|
||||
<VSCodeOption value="">Personal</VSCodeOption>
|
||||
{userOrganizations.map((org: UserOrganization) => (
|
||||
<VSCodeOption key={org.organizationId} value={org.organizationId}>
|
||||
{org.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -427,10 +427,6 @@ export const ChatRowContent = memo(
|
||||
}
|
||||
|
||||
if (apiRequestFailedMessage) {
|
||||
const errorData = parseErrorText(apiRequestFailedMessage)
|
||||
if (errorData?.code === "insufficient_credits") {
|
||||
return <span style={{ color: errorColor, fontWeight: "bold" }}>Credit Limit Reached</span>
|
||||
}
|
||||
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||
}
|
||||
// New: Check for retryStatus to modify the title
|
||||
|
||||
@@ -1720,7 +1720,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
}}>
|
||||
<ApiOptions showModelOptions={true} modelIdErrorMessage={undefined} isPopup={true} />
|
||||
<ApiOptions
|
||||
showModelOptions={true}
|
||||
apiErrorMessage={undefined}
|
||||
modelIdErrorMessage={undefined}
|
||||
isPopup={true}
|
||||
/>
|
||||
</ModelSelectorTooltip>
|
||||
)}
|
||||
</ModelContainer>
|
||||
|
||||
@@ -22,14 +22,10 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
|
||||
// We have to divide because the balance is stored in microcredits
|
||||
return (
|
||||
<div className="p-2 border-none rounded-md mb-2 bg-[var(--vscode-textBlockQuote-background)]">
|
||||
<div className="mb-3 font-azeret-mono">
|
||||
<div style={{ color: "var(--vscode-errorForeground)", marginBottom: "8px" }}>{message}</div>
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<div style={{ color: "var(--vscode-foreground)" }}>
|
||||
Current Balance: <span style={{ fontWeight: "bold" }}>{currentBalance.toFixed(2)}</span>
|
||||
</div>
|
||||
<div style={{ color: "var(--vscode-foreground)" }}>Total Spent: {totalSpent.toFixed(2)}</div>
|
||||
<div style={{ color: "var(--vscode-foreground)" }}>Total Promotions: {totalPromotions.toFixed(2)}</div>
|
||||
<div className="mb-2">{message}</div>
|
||||
<div className="mb-3">
|
||||
<div className="text-[var(--vscode-foreground)]">
|
||||
Current Balance: <span className="font-bold">${currentBalance.toFixed(4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +35,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
|
||||
width: "100%",
|
||||
marginBottom: "8px",
|
||||
}}>
|
||||
<span className="codicon codicon-credit-card mr-[6px] text-[14px]" />
|
||||
<span className="codicon codicon-credit-card mr-0.5 text-sm" />
|
||||
Buy Credits
|
||||
</VSCodeButtonLink>
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
|
||||
import { BooleanRequest, StringRequest } from "@shared/proto/common"
|
||||
import { VSCodeButton, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useInterval } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
|
||||
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
|
||||
import { ClineProvider } from "./providers/ClineProvider"
|
||||
import { OpenRouterProvider } from "./providers/OpenRouterProvider"
|
||||
import { MistralProvider } from "./providers/MistralProvider"
|
||||
@@ -35,10 +37,8 @@ import { LiteLlmProvider } from "./providers/LiteLlmProvider"
|
||||
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
|
||||
import { LMStudioProvider } from "./providers/LMStudioProvider"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
import { GroqProvider } from "./providers/GroqProvider"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showSubmitButton?: boolean
|
||||
showModelOptions: boolean
|
||||
apiErrorMessage?: string
|
||||
modelIdErrorMessage?: string
|
||||
@@ -69,7 +69,7 @@ declare module "vscode" {
|
||||
}
|
||||
}
|
||||
|
||||
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, showSubmitButton }: ApiOptionsProps) => {
|
||||
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup }: ApiOptionsProps) => {
|
||||
// Use full context state for immediate save payload
|
||||
const { apiConfiguration, uriScheme } = useExtensionState()
|
||||
|
||||
@@ -79,14 +79,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await StateServiceClient.setWelcomeViewCompleted(BooleanRequest.create({ value: true }))
|
||||
} catch (error) {
|
||||
console.error("Failed to update API configuration or complete welcome view:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Poll ollama/vscode-lm models
|
||||
const requestLocalModels = useCallback(async () => {
|
||||
if (selectedProvider === "ollama") {
|
||||
@@ -130,9 +122,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeDropdown
|
||||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={(e: any) => {
|
||||
handleFieldChange("apiProvider", e.target.value)
|
||||
}}
|
||||
onChange={(e: any) => handleFieldChange("apiProvider", e.target.value)}
|
||||
style={{
|
||||
minWidth: 130,
|
||||
position: "relative",
|
||||
@@ -142,13 +132,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||
<VSCodeOption value="claude-code">Claude Code</VSCodeOption>
|
||||
<VSCodeOption value="bedrock">Amazon Bedrock</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||
<VSCodeOption value="groq">Groq</VSCodeOption>
|
||||
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="mistral">Mistral</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
|
||||
<VSCodeOption value="requesty">Requesty</VSCodeOption>
|
||||
<VSCodeOption value="fireworks">Fireworks</VSCodeOption>
|
||||
@@ -242,9 +231,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
|
||||
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
|
||||
|
||||
{apiConfiguration && selectedProvider === "groq" && (
|
||||
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
{apiConfiguration && selectedProvider === "litellm" && (
|
||||
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
@@ -297,12 +283,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
{modelIdErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showSubmitButton && (
|
||||
<VSCodeButton onClick={handleSubmit} disabled={apiErrorMessage != null} className="mt-0.75" title="Submit">
|
||||
Let's go!
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
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 { groqDefaultModelId, groqModels } from "@shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
|
||||
export interface GroqModelPickerProps {
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup }) => {
|
||||
const { apiConfiguration, groqModels: dynamicGroqModels, setGroqModels } = useExtensionState()
|
||||
const { handleFieldsChange } = useApiConfigurationHandlers()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.groqModelId || groqDefaultModelId)
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
// Use dynamic models if available, otherwise fall back to static models
|
||||
const modelInfo = dynamicGroqModels?.[newModelId] || groqModels[newModelId as keyof typeof groqModels]
|
||||
|
||||
handleFieldsChange({
|
||||
groqModelId: newModelId,
|
||||
groqModelInfo: modelInfo,
|
||||
})
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
useMount(() => {
|
||||
ModelsServiceClient.refreshGroqModels(EmptyRequest.create({}))
|
||||
.then((response) => {
|
||||
setGroqModels({
|
||||
[groqDefaultModelId]: groqModels[groqDefaultModelId],
|
||||
...response.models,
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to refresh Groq models:", err)
|
||||
})
|
||||
})
|
||||
|
||||
// Debounce search term to reduce re-renders
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm)
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchTerm])
|
||||
|
||||
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 allGroqModels = useMemo(() => {
|
||||
// Merge static models with dynamic models, with dynamic taking precedence
|
||||
return { ...groqModels, ...(dynamicGroqModels || {}) }
|
||||
}, [dynamicGroqModels])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
return Object.keys(allGroqModels).sort((a, b) => a.localeCompare(b))
|
||||
}, [allGroqModels])
|
||||
|
||||
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 }[] = debouncedSearchTerm
|
||||
? highlight(fuse.search(debouncedSearchTerm), "model-item-highlight")
|
||||
: searchableItems
|
||||
return results
|
||||
}, [searchableItems, debouncedSearchTerm, 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(() => {
|
||||
try {
|
||||
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [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 (
|
||||
<div className="w-full">
|
||||
<style>
|
||||
{`
|
||||
.model-item-highlight {
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
color: inherit;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="model-search">
|
||||
<span className="font-medium">Model</span>
|
||||
</label>
|
||||
<div ref={dropdownRef} className="relative w-full">
|
||||
<VSCodeTextField
|
||||
id="model-search"
|
||||
placeholder="Search and select a model..."
|
||||
value={searchTerm}
|
||||
onInput={(e) => {
|
||||
setSearchTerm((e.target as HTMLInputElement)?.value || "")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
onFocus={() => setIsDropdownVisible(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
width: "100%",
|
||||
zIndex: GROQ_MODEL_PICKER_Z_INDEX,
|
||||
position: "relative",
|
||||
}}>
|
||||
{searchTerm && (
|
||||
<div
|
||||
className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
setSearchTerm("")
|
||||
setIsDropdownVisible(true)
|
||||
}}
|
||||
slot="end"
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
{isDropdownVisible && (
|
||||
<div
|
||||
ref={dropdownListRef}
|
||||
className="absolute top-[calc(100%-3px)] left-0 w-[calc(100%-2px)] max-h-[200px] overflow-y-auto border border-[var(--vscode-list-activeSelectionBackground)] rounded-b-[3px]"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
zIndex: GROQ_MODEL_PICKER_Z_INDEX - 1,
|
||||
}}>
|
||||
{modelSearchResults.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
ref={(el: HTMLDivElement | null) => (itemRefs.current[index] = el)}
|
||||
className={`px-2.5 py-1.5 cursor-pointer break-all whitespace-normal hover:bg-[var(--vscode-list-activeSelectionBackground)] ${
|
||||
index === selectedIndex ? "bg-[var(--vscode-list-activeSelectionBackground)]" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onClick={() => {
|
||||
handleModelChange(item.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.html,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasInfo ? (
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
) : (
|
||||
<p className="text-xs mt-0 text-[var(--vscode-descriptionForeground)]">
|
||||
<>
|
||||
The extension automatically fetches the latest list of models available on{" "}
|
||||
<VSCodeLink className="inline text-inherit" href="https://console.groq.com/docs/models">
|
||||
Groq.
|
||||
</VSCodeLink>
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink className="inline text-inherit" onClick={() => handleModelChange("llama-3.3-70b-versatile")}>
|
||||
llama-3.3-70b-versatile.
|
||||
</VSCodeLink>
|
||||
</>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const GROQ_MODEL_PICKER_Z_INDEX = 1_000
|
||||
|
||||
export default GroqModelPicker
|
||||
@@ -33,7 +33,6 @@ export const ApiKeyField = ({
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={(e: any) => setLocalValue(e.target.value)}
|
||||
required={true}
|
||||
placeholder={placeholder}>
|
||||
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import GroqModelPicker from "../GroqModelPicker"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
/**
|
||||
* Props for the GroqProvider component
|
||||
*/
|
||||
interface GroqProviderProps {
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Groq provider configuration component
|
||||
*/
|
||||
export const GroqProvider = ({ showModelOptions, isPopup }: GroqProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
initialValue={apiConfiguration?.groqApiKey || ""}
|
||||
onChange={(value) => handleFieldChange("groqApiKey", value)}
|
||||
providerName="Groq"
|
||||
signupUrl="https://console.groq.com/keys"
|
||||
/>
|
||||
|
||||
{showModelOptions && <GroqModelPicker isPopup={isPopup} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,8 +44,6 @@ import {
|
||||
sapAiCoreDefaultModelId,
|
||||
claudeCodeDefaultModelId,
|
||||
claudeCodeModels,
|
||||
groqModels,
|
||||
groqDefaultModelId,
|
||||
} from "@shared/api"
|
||||
|
||||
/**
|
||||
@@ -180,14 +178,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
|
||||
case "cerebras":
|
||||
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
|
||||
case "groq":
|
||||
const result = {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.groqModelId || groqDefaultModelId,
|
||||
selectedModelInfo: apiConfiguration?.groqModelInfo || groqModels[groqDefaultModelId],
|
||||
}
|
||||
|
||||
return result
|
||||
case "sapaicore":
|
||||
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
|
||||
default:
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
|
||||
import { useCallback } from "react"
|
||||
|
||||
export const useApiConfigurationHandlers = () => {
|
||||
const { apiConfiguration, uriScheme } = useExtensionState()
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
|
||||
/**
|
||||
* Updates a single field in the API configuration.
|
||||
*
|
||||
@@ -17,24 +17,21 @@ export const useApiConfigurationHandlers = () => {
|
||||
* @param field - The field key to update
|
||||
* @param value - The new value for the field
|
||||
*/
|
||||
const handleFieldChange = useCallback(
|
||||
<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[field]: value,
|
||||
}
|
||||
const handleFieldChange = <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[field]: value,
|
||||
}
|
||||
|
||||
const protoConfig = convertApiConfigurationToProto(updatedConfig)
|
||||
ModelsServiceClient.updateApiConfigurationProto(
|
||||
UpdateApiConfigurationRequest.create({
|
||||
apiConfiguration: protoConfig,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error(`Failed to update API configuration field ${field}:`, error)
|
||||
})
|
||||
},
|
||||
[apiConfiguration],
|
||||
)
|
||||
const protoConfig = convertApiConfigurationToProto(updatedConfig)
|
||||
ModelsServiceClient.updateApiConfigurationProto(
|
||||
UpdateApiConfigurationRequest.create({
|
||||
apiConfiguration: protoConfig,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error(`Failed to update API configuration field ${field}:`, error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates multiple fields in the API configuration at once.
|
||||
@@ -45,24 +42,21 @@ export const useApiConfigurationHandlers = () => {
|
||||
*
|
||||
* @param updates - An object containing the fields to update and their new values
|
||||
*/
|
||||
const handleFieldsChange = useCallback(
|
||||
(updates: Partial<ApiConfiguration>) => {
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
...updates,
|
||||
}
|
||||
const handleFieldsChange = (updates: Partial<ApiConfiguration>) => {
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
...updates,
|
||||
}
|
||||
|
||||
const protoConfig = convertApiConfigurationToProto(updatedConfig)
|
||||
ModelsServiceClient.updateApiConfigurationProto(
|
||||
UpdateApiConfigurationRequest.create({
|
||||
apiConfiguration: protoConfig,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error("Failed to update API configuration fields:", error)
|
||||
})
|
||||
},
|
||||
[apiConfiguration],
|
||||
)
|
||||
const protoConfig = convertApiConfigurationToProto(updatedConfig)
|
||||
ModelsServiceClient.updateApiConfigurationProto(
|
||||
UpdateApiConfigurationRequest.create({
|
||||
apiConfiguration: protoConfig,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error("Failed to update API configuration fields:", error)
|
||||
})
|
||||
}
|
||||
|
||||
return { handleFieldChange, handleFieldsChange, uriScheme, apiConfiguration }
|
||||
return { handleFieldChange, handleFieldsChange }
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState, memo } from "react"
|
||||
import { useEffect, useState, memo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { validateApiConfiguration } from "@/utils/validate"
|
||||
import ApiOptions from "@/components/settings/ApiOptions"
|
||||
import ClineLogoWhite from "@/assets/ClineLogoWhite"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { AccountServiceClient, ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest, BooleanRequest } from "@shared/proto/common"
|
||||
|
||||
const WelcomeView = memo(() => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [showApiOptions, setShowApiOptions] = useState(false)
|
||||
|
||||
const disableLetsGoButton = apiErrorMessage != null
|
||||
|
||||
const handleLogin = () => {
|
||||
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
|
||||
console.error("Failed to get login URL:", err),
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await StateServiceClient.setWelcomeViewCompleted(BooleanRequest.create({ value: true }))
|
||||
} catch (error) {
|
||||
console.error("Failed to update API configuration or complete welcome view:", error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setApiErrorMessage(validateApiConfiguration(apiConfiguration))
|
||||
}, [apiConfiguration])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 p-0 flex flex-col">
|
||||
<div className="h-full px-5 overflow-auto">
|
||||
@@ -49,7 +67,16 @@ const WelcomeView = memo(() => {
|
||||
</VSCodeButton>
|
||||
)}
|
||||
|
||||
<div className="mt-4.5">{showApiOptions && <ApiOptions showModelOptions={false} showSubmitButton={true} />}</div>
|
||||
<div className="mt-4.5">
|
||||
{showApiOptions && (
|
||||
<div>
|
||||
<ApiOptions showModelOptions={false} />
|
||||
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} className="mt-0.75">
|
||||
Let's go!
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
openRouterDefaultModelInfo,
|
||||
requestyDefaultModelId,
|
||||
requestyDefaultModelInfo,
|
||||
groqDefaultModelId,
|
||||
groqModels,
|
||||
} from "../../../src/shared/api"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
@@ -40,7 +38,6 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
requestyModels: Record<string, ModelInfo>
|
||||
groqModels: Record<string, ModelInfo>
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
@@ -61,7 +58,6 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
setGroqModels: (value: Record<string, ModelInfo>) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -209,9 +205,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
const [groqModelsState, setGroqModels] = useState<Record<string, ModelInfo>>({
|
||||
[groqDefaultModelId]: groqModels[groqDefaultModelId],
|
||||
})
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
|
||||
|
||||
@@ -256,6 +249,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
if (response.stateJson) {
|
||||
try {
|
||||
const stateData = JSON.parse(response.stateJson) as ExtensionState
|
||||
console.log("[DEBUG] parsed state JSON, updating state")
|
||||
setState((prevState) => {
|
||||
// Versioning logic for autoApprovalSettings
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
|
||||
@@ -637,7 +631,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
openRouterModels,
|
||||
openAiModels,
|
||||
requestyModels,
|
||||
groqModels: groqModelsState,
|
||||
mcpServers,
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
@@ -677,7 +670,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setGroqModels: (models: Record<string, ModelInfo>) => setGroqModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
|
||||
Reference in New Issue
Block a user