mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b04ff45c79 | |||
| 2e9fdb6726 | |||
| 48db9b1068 | |||
| 675cd1779b | |||
| af0f0b3d7c | |||
| 45767b87fd | |||
| 8f4c6038dd | |||
| 9dc021a881 | |||
| 3e2bdf8b12 | |||
| d4a99a4060 | |||
| f4bbb45b07 | |||
| 088deebd63 | |||
| dcc744dd87 | |||
| 9ad8525bd0 | |||
| 8bf6268952 | |||
| 2081bb8dc6 | |||
| ac22b63796 | |||
| 44eb2cc65e | |||
| 2cf1d8628b | |||
| 669e018b85 | |||
| 2aa5156905 | |||
| 51b619e0d5 | |||
| 85fb76a996 | |||
| d73a7cfd06 | |||
| 489dfbc932 | |||
| 314c416788 | |||
| 3b19c2ec95 | |||
| e04cbea504 | |||
| affac119f5 | |||
| 3847a2545c | |||
| 15593bac2a | |||
| 84267efb9e | |||
| 985ce56809 | |||
| cad28c4c0c | |||
| 4a22f7dbd2 | |||
| a430226caa | |||
| 5885a3cc1d | |||
| 759ef873ae | |||
| 782e4ff6e0 | |||
| 4bb00241bf | |||
| c325faf8db | |||
| 20f8f9c9cf | |||
| 5be163f49d | |||
| 7843ab937a | |||
| 5ed4319d21 | |||
| a8971b807a | |||
| 51c4e0aceb | |||
| 1cf62941cd | |||
| cc2472f500 | |||
| 677e544c51 | |||
| d3c8fbbf1d | |||
| 1b06633253 | |||
| 4ab8559fce | |||
| 259368e0a3 | |||
| 9b7839efcd | |||
| 47a2ae83de | |||
| 1b5590e26c | |||
| 9e493341d2 | |||
| 1d4cd3187b | |||
| 32f0f9618c | |||
| 3001f883c2 | |||
| a64e60b8f6 | |||
| 3a0e6a471b | |||
| c10f4e0a66 | |||
| 3e11271cf8 | |||
| 29ae2c286d | |||
| 82aee44a9a | |||
| 031604ddf6 | |||
| f2101e375f | |||
| 8a65f0c68b | |||
| 32b8fa44cb | |||
| 0d067f7470 | |||
| de6166392c | |||
| 5f21a9162a | |||
| 4de991f1b0 | |||
| 6e5d4a3f9e | |||
| 95af95badf | |||
| 61dcbd390c | |||
| 6255ac0a51 | |||
| 616800fcb9 | |||
| df3826a59f | |||
| 873917810d | |||
| e3c966f4e9 | |||
| 7eeb43ab41 | |||
| 7620f177ac | |||
| 67bab94911 | |||
| eb91bfd738 | |||
| b8227c19c3 | |||
| 8fee09f09e | |||
| 1c026c26d2 | |||
| 5bc4e5a4a0 | |||
| 2cfce5734e | |||
| e2045bf5c3 | |||
| 0d933e804f | |||
| 16f73532f4 | |||
| 88bea8eeb4 | |||
| 1a570e98ba | |||
| d86b7dd036 | |||
| 0178c3fa90 | |||
| a107f45c6a | |||
| 3dd2ed9161 | |||
| 9a6603fdfb | |||
| 6d5c3e6aa4 | |||
| 24b9e821bb | |||
| 9960a3c57c | |||
| 88947592f0 | |||
| ef4d11df19 | |||
| 23dec509bc | |||
| 07ab6b19b8 | |||
| 0ddef94d1f | |||
| b9ae83b1cd | |||
| e4eaf34827 | |||
| 6d3ed43c74 | |||
| cbb67b48f2 | |||
| a5f6a97be8 | |||
| f309b062e7 | |||
| 768df130ab | |||
| 9980cb0938 | |||
| aca4f842fa | |||
| 3fc91e2afe | |||
| 5f4700ce95 | |||
| dbaf5e3ee3 | |||
| 576176c24f | |||
| c8abcbfdf9 | |||
| 8e984f2d98 | |||
| 81564faa4e | |||
| f8b5f1fd72 | |||
| 2eb57384ab | |||
| c80bae504a | |||
| 80f955be9e | |||
| 7435ffcd2f | |||
| a05d438612 | |||
| b4b7512d9f | |||
| e08c65618e | |||
| d653f1cc27 | |||
| c3a97c3eda | |||
| 0e56272d65 | |||
| fdc2e2655a | |||
| 6050413b8b | |||
| c54f0da737 | |||
| 6cbfb2b8b0 | |||
| 22788f0f12 | |||
| 708b785a97 | |||
| 099bc44d42 | |||
| b9f4678dba | |||
| f7d17384f6 | |||
| bb5a64afb3 | |||
| 61224734f8 | |||
| a28b995ab1 | |||
| a91878efc6 | |||
| 65c21e7b7d | |||
| 56e388c90f | |||
| 586d804a01 | |||
| 85fbbcbe3f | |||
| 25c5310383 | |||
| 19ef843d4f | |||
| 2b3dd14271 | |||
| 721e7ae305 | |||
| f6273e0661 | |||
| 68d0af2afc | |||
| 4286f301d2 | |||
| 28536084bf | |||
| dfbac3eefd | |||
| 04f0710a03 | |||
| 195e15db32 | |||
| 19cb70de55 | |||
| e2a2ecde44 | |||
| 1a466c0a44 | |||
| 31cbf489c4 | |||
| c667d34f27 | |||
| a1bf1f95a3 | |||
| 5da75af616 | |||
| 38babe12f7 | |||
| 66fb8835a4 | |||
| 214e157360 | |||
| 7a9dce4f86 | |||
| af7e3a4d20 | |||
| 3abdc9ad0f | |||
| 28b15d8b9b | |||
| bc6b3e54be | |||
| 9fad0aa4ae | |||
| 23fea0e16b | |||
| e8aaa61494 | |||
| d93080304c | |||
| 1ce5f72bc1 | |||
| fb676add2e | |||
| 31cda0ce5e | |||
| e173dad69c | |||
| 92f32522c5 | |||
| 7817d5f261 | |||
| db6d288efa | |||
| 87ff00d87e | |||
| e602efc7a6 | |||
| e6462af336 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve cerebras Qwen model performance by removing thinking tokens from the model input
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixes the API Keys URL for Requesty
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Set gpt5 max tokens to 8_192 to fix 'context window exceeded' error
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Change available Cerebras models - limit to Qwen and llama 3.3 70b
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added checkpointTrackerErrorMessage to HistoryItem - restored with task, prevents re-initialization if timed out before
|
||||
Never re-init checkpoint tracker if it timed out before
|
||||
Warning at 7s that it's taking awhile, timeout and give up at 15s
|
||||
Fixed click to open settings - now opens to correct tab
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: mcp servers are not started when disabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactor Git commit message generation to support output streaming.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where fallback request to retrieve cost was not using correct auth token
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Introduce Claude Code support on Windows and fix E2BIG issues
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Remove deprecated GPT-4.5 Preview
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding OpenAI context window exceeded error handling
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding safety guard for workspace root
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add CLINE_ACTIVE environment variable to Cline-managed terminals
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Change Cerebras Qwen 3 32b context window from 16k to 64k
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
calibrate input token counts when using anthropic models of sap ai core provider
|
||||
@@ -716,7 +716,7 @@ The Controller class manages MCP servers through the McpHub service:
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
constructor(context: vscode.ExtensionContext, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,9 @@ EOF
|
||||
|
||||
## Basic PR Commands
|
||||
```bash
|
||||
# Get current PR number
|
||||
gh pr view --json number -q .number
|
||||
|
||||
# List open PRs
|
||||
gh pr list
|
||||
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-direct-vscode-api": "warn",
|
||||
"eslint-rules/no-direct-vscode-state-api": "error",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
@@ -29,5 +30,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
"ignorePatterns": ["out", "dist", "dist-standalone", "**/*.d.ts", "node_modules"]
|
||||
}
|
||||
|
||||
+3
-1
@@ -1 +1,3 @@
|
||||
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett @saito-sv @Garoth
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @dcbartlett
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude 3.5 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -24,7 +24,7 @@ body:
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
@@ -39,20 +39,19 @@ body:
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: What operating system are you using?
|
||||
placeholder: "e.g., Windows 11, macOS Sonoma, Ubuntu 22.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: What system information is relevant to the issue?
|
||||
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
|
||||
label: System Information
|
||||
description: What operating system and hardware are you using?
|
||||
placeholder: |
|
||||
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
|
||||
Hardware: CPU, GPU, RAM specifications if relevant
|
||||
e.g.,
|
||||
OS: Windows 11
|
||||
CPU: Intel Core i7-11700K
|
||||
GPU: NVIDIA GeForce RTX 3070
|
||||
RAM: 32GB DDR4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -63,8 +62,3 @@ body:
|
||||
placeholder: "e.g., 1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here, such as screenshots or related issues.
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
name: 💡 Feature Proposal & Contribution
|
||||
description: Propose a new feature or improvement, and optionally offer to implement feature as a contributor
|
||||
labels: ["proposal"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Feature Proposal & Contribution for Cline**
|
||||
|
||||
Thank you for proposing a feature or improvement for Cline! This template helps us understand the problem, evaluate the solution, and coordinate implementation.
|
||||
|
||||
**For detailed proposals:** Please provide comprehensive information to enable fast prioritization and discussion.
|
||||
**For contribution offers:** You can indicate your willingness to implement the feature yourself.
|
||||
|
||||
Before submitting:
|
||||
- Search existing [Issues](https://github.com/cline/cline/issues) and [Discussions](https://github.com/cline/cline/discussions) to avoid duplicates
|
||||
- Read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md) if you plan to contribute
|
||||
- Don't start implementation until the proposal is reviewed and approved
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: |
|
||||
Describe the problem clearly from a user's point of view. Focus on why this matters, who it affects, and when it occurs.
|
||||
|
||||
✅ Good examples:
|
||||
- "LLM provider returns 400 error when nearing the context window instead of truncating"
|
||||
- "Submit button is invisible in dark mode"
|
||||
- "Users can't easily share their Cline configurations with team members"
|
||||
|
||||
❌ Avoid vague descriptions:
|
||||
- "Performance is bad"
|
||||
- "UI needs work"
|
||||
|
||||
Your description should include:
|
||||
- Who is affected?
|
||||
- When does it happen?
|
||||
- What's the current vs expected behavior?
|
||||
- What is the impact?
|
||||
placeholder: Be specific about the problem, who it affects, and the impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: What's the proposed solution?
|
||||
description: |
|
||||
Describe how the problem should be solved. Be specific about UX, system behavior, and any flows that would change.
|
||||
|
||||
✅ Good examples:
|
||||
- "Add error handling immediately after attempting to create the llm stream and retry after manually truncating"
|
||||
- "Update button styling to ensure contrast in all themes"
|
||||
- "Add export/import functionality in settings with JSON format"
|
||||
|
||||
❌ Avoid vague solutions:
|
||||
- "Improve performance"
|
||||
- "Fix the bug"
|
||||
|
||||
Your solution should include:
|
||||
- What exactly will change?
|
||||
- How will users interact with it?
|
||||
- What's the expected outcome?
|
||||
placeholder: Describe the proposed changes and how they solve the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: contribution-intent
|
||||
attributes:
|
||||
label: Are you interested in implementing this?
|
||||
description: Let us know if you'd like to contribute to this feature
|
||||
options:
|
||||
- "No, just proposing the idea"
|
||||
- "Yes, I'd like to implement this myself"
|
||||
- "Yes, I'd like to collaborate with others"
|
||||
- "Maybe, depending on complexity and guidance"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: implementation-approach
|
||||
attributes:
|
||||
label: Implementation approach (if contributing)
|
||||
description: |
|
||||
**Only fill this out if you selected "Yes" above.**
|
||||
|
||||
How do you plan to implement this? Include:
|
||||
- High-level technical approach
|
||||
- Files/components that would be affected
|
||||
- Any new dependencies required
|
||||
- Potential challenges or considerations you've identified
|
||||
|
||||
This helps us provide better guidance and ensures alignment before you start coding.
|
||||
placeholder: "My implementation approach would be..."
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Proposal checklist
|
||||
options:
|
||||
- label: I've checked for existing issues or related proposals
|
||||
required: true
|
||||
- label: I understand this needs review before implementation can start
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution-checklist
|
||||
attributes:
|
||||
label: Contribution checklist (if contributing)
|
||||
description: Only check these if you plan to contribute
|
||||
options:
|
||||
- label: I've read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
- label: I'm willing to make changes based on feedback
|
||||
- label: I understand the code review process and requirements
|
||||
@@ -2,15 +2,14 @@
|
||||
Thank you for contributing to Cline!
|
||||
|
||||
⚠️ Important: Before submitting this PR, please ensure you have:
|
||||
- Opened an issue and discussed your proposed changes with the community / contributors
|
||||
- Received approval from a core Cline contributor prior to proceeding with the implementation
|
||||
- Link the associated issue in the "Related Issue" section
|
||||
- For feature requests: Created a discussion in our Feature Requests discussions board https://github.com/cline/cline/discussions/categories/feature-requests and received approval from core maintainers before implementation
|
||||
- For all changes: Link the associated issue/discussion in the "Related Issue" section below
|
||||
|
||||
Limited exceptions:
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly without prior discussion.
|
||||
|
||||
Why this requirement?
|
||||
We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
|
||||
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use our Feature Requests discussions board to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
|
||||
-->
|
||||
|
||||
### Related Issue
|
||||
|
||||
+12
-15
@@ -96,16 +96,15 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
@@ -117,7 +116,7 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
npm run test:coverage 2>&1 | tee webview_coverage.txt
|
||||
cd ..
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
@@ -132,21 +131,19 @@ jobs:
|
||||
path: |
|
||||
extension_coverage.txt
|
||||
webview-ui/webview_coverage.txt
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Print test results and check for failures
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ]; then
|
||||
echo "Extension Integration Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Webview Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Vendored
+3
-5
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Standalone Service",
|
||||
"name": "Run cline-core service",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
@@ -82,11 +82,9 @@
|
||||
// Turns on grpc debug log.
|
||||
//"GRPC_TRACE": "all",
|
||||
//"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
|
||||
|
||||
"HOST_BRIDGE_ADDRESS": "localhost:50052"
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
},
|
||||
"program": "standalone.js"
|
||||
"program": "cline-core.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -9,5 +9,9 @@
|
||||
"dist": true // set this to false to include "dist" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
// Protobuf settings
|
||||
"protoc": {
|
||||
"options": ["--proto_path=proto"]
|
||||
}
|
||||
}
|
||||
|
||||
+657
-552
File diff suppressed because it is too large
Load Diff
+5
-8
@@ -14,14 +14,11 @@ Bug reports help make Cline better for everyone! Before creating a new issue, pl
|
||||
## Before Contributing
|
||||
|
||||
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
|
||||
|
||||
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
|
||||
- **Create an issue**: Use appropriate templates:
|
||||
- **Contributions:** Use the "Contribution Request" template to propose what you'd like to work on.
|
||||
- **Bugs:** "Bug Report" template for reporting issues.
|
||||
- **Features:** "Detailed Feature Proposal" template for suggesting new features.
|
||||
- **Wait for approval**: A core Cline contributor must approve your contribution request before you start implementation.
|
||||
- **Claim issues**: Once approved, the issue will be assigned to you.
|
||||
**For features and contributions**:
|
||||
- First check the [Feature Requests discussions board](https://github.com/cline/cline/discussions/categories/feature-requests) for similar ideas
|
||||
- If your idea is new, create a new feature request
|
||||
- Wait for approval from core maintainers before starting implementation
|
||||
- Once approved, feel free to begin working on a PR with the help of our community!
|
||||
|
||||
**PRs without approved issues may be closed.**
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ lint:
|
||||
|
||||
except: # Add exceptions for current patterns that contradict STANDARD settings
|
||||
- RPC_PASCAL_CASE # rpcs are camel case (start with lowercase)
|
||||
- PACKAGE_DIRECTORY_MATCH # the protos in the cline package are not in a dir named cline.
|
||||
- RPC_REQUEST_RESPONSE_UNIQUE # request messages are not unique.
|
||||
- RPC_REQUEST_STANDARD_NAME # request messages dont all end with Request
|
||||
- RPC_RESPONSE_STANDARD_NAME # response messages dont all end with Response
|
||||
|
||||
@@ -11,7 +11,19 @@ You can create a rule by clicking the `+` button in the Rules tab. This will ope
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ There are multiple places online to find MCP servers:
|
||||
- [mcpservers.org](https://mcpservers.org/)
|
||||
- [mcp.so](https://mcp.so/)
|
||||
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
|
||||
- [mcp.composio.dev](https://mcp.composio.dev/)
|
||||
|
||||
These directories allow users to sort the servers by various criteria such as downloads, date, stars, and use case. Each entry provides information such as features, tools, and configuration instructions.
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ title: "Telemetry"
|
||||
|
||||
### Overview
|
||||
|
||||
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
To help make Cline better for everyone, we collect usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
|
||||
### Tracking Policy
|
||||
|
||||
Privacy is our priority. All collected data is anonymized before being sent to PostHog, with no personally identifiable information (PII) included. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
Privacy is our priority. By default, all collected data is anonymized. If you log in with a Cline account, your telemetry data will be associated with your account to help us improve the product and provide better support when you encounter issues. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
|
||||
### What We Track
|
||||
|
||||
We collect basic anonymous usage data including:
|
||||
We collect basic usage data including:
|
||||
|
||||
**Task Interactions:** When tasks start and finish, conversation flow (without content)\
|
||||
**Mode and Tool Usage:** Switches between plan/act modes, which tools are being used\
|
||||
@@ -28,7 +28,7 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- When you update or install our VS Code extension, you'll see a message about our telemetry
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
@@ -16,6 +16,7 @@ description: "Learn how to configure and use Anthropic Claude models with Cline.
|
||||
|
||||
Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
|
||||
@@ -52,6 +52,7 @@ If you're not sure where Claude Code is installed:
|
||||
The Claude Code provider supports these models:
|
||||
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
|
||||
@@ -43,7 +43,6 @@ While the "OpenAI Compatible" provider type allows connecting to various endpoin
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-4.5-preview`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ Cline is compatible with a variety of OpenAI models, including but not limited t
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-4.5-preview`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
- 'gpt-4.1'
|
||||
|
||||
@@ -10,7 +10,7 @@ Cline supports accessing models through the [Requesty](https://www.requesty.ai/)
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [Requesty website](https://www.requesty.ai/) and create an account or sign in.
|
||||
2. **Get API Key:** You can get an API key from the [API Management](https://app.requesty.ai/manage-api) section of your Requesty dashboard.
|
||||
2. **Get API Key:** You can get an API key from the [API Management](https://app.requesty.ai/api-keys) section of your Requesty dashboard.
|
||||
|
||||
### Supported Models
|
||||
|
||||
@@ -26,7 +26,7 @@ Requesty provides access to a wide range of models. Cline will automatically fet
|
||||
### Tips and Notes
|
||||
|
||||
- **Optimizations**: Requesty offers a range of in-flight cost optimizations to lower your costs.
|
||||
- **Unified and simplified billing**: Unrestricted access to all providers and models, automatic balance top ups and more via a single [API key](https://app.requesty.ai/manage-api).
|
||||
- **Unified and simplified billing**: Unrestricted access to all providers and models, automatic balance top ups and more via a single [API key](https://app.requesty.ai/api-keys).
|
||||
- **Cost tracking**: Track cost per model, coding language, changed file, and more via the [Cost dashboard](https://app.requesty.ai/cost-management) or the [Requesty VS Code extension](https://marketplace.visualstudio.com/items?itemName=Requesty.requesty).
|
||||
- **Stats and logs**: See your [coding stats dashboard](https://app.requesty.ai/usage-stats) or go through your [LLM interaction logs](https://app.requesty.ai/logs).
|
||||
- **Fallback policies**: Keep your LLM working for you with fallback policies when providers are down.
|
||||
|
||||
@@ -244,13 +244,60 @@ Recent macOS versions have stricter terminal permissions:
|
||||
|
||||
### Windows Issues
|
||||
|
||||
#### PowerShell Execution Policy
|
||||
If you're using Windows and still experiencing issues with shell integration after trying the previous steps, it's recommended you use Git Bash (or PowerShell).
|
||||
|
||||
If commands fail silently:
|
||||
### Git Bash
|
||||
|
||||
Git Bash is a terminal emulator that provides a Unix-like command line experience on Windows. To use Git Bash, you need to:
|
||||
|
||||
1. Download and run the Git for Windows installer from [https://git-scm.com/downloads/win](https://git-scm.com/downloads/win)
|
||||
2. Quit and re-open VSCode
|
||||
3. Press `Ctrl + Shift + P` to open the Command Palette
|
||||
4. Type "Terminal: Select Default Profile" and choose it
|
||||
5. Select "Git Bash"
|
||||
|
||||
### PowerShell
|
||||
|
||||
If you'd still like to use PowerShell, make sure you're using an updated version (at least v7+).
|
||||
- Check your current PowerShell version by running: `$PSVersionTable.PSVersion`
|
||||
- If your version is below 7, [update PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.4#installing-powershell-7).
|
||||
|
||||
You may also need to adjust your PowerShell execution policy. By default, PowerShell restricts script execution for security reasons.
|
||||
|
||||
#### Understanding PowerShell Execution Policies
|
||||
|
||||
PowerShell uses execution policies to determine which scripts can run on your system. Here are the most common policies:
|
||||
|
||||
- `Restricted`: No PowerShell scripts can run. This is the default setting.
|
||||
- `AllSigned`: All scripts, including local ones, must be signed by a trusted publisher.
|
||||
- `RemoteSigned`: Scripts created locally can run, but scripts downloaded from the internet must be signed.
|
||||
- `Unrestricted`: No restrictions. Any script can run, though you will be warned before running internet-downloaded scripts.
|
||||
|
||||
For development work in VSCode, the `RemoteSigned` policy is generally recommended. It allows locally created scripts to run without restrictions while maintaining security for downloaded scripts. To learn more about PowerShell execution policies and understand the security implications of changing them, visit Microsoft's documentation: [About Execution Policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies).
|
||||
|
||||
#### Steps to Change the Execution Policy
|
||||
|
||||
1. Open PowerShell as an Administrator: Press `Win + X` and select "Windows PowerShell (Administrator)" or "Windows Terminal (Administrator)".
|
||||
|
||||
2. Check Current Execution Policy by running this command:
|
||||
```powershell
|
||||
Get-ExecutionPolicy
|
||||
```
|
||||
- If the output is already `RemoteSigned`, `Unrestricted`, or `Bypass`, you likely don't need to change your execution policy. These policies should allow shell integration to work.
|
||||
- If the output is `Restricted` or `AllSigned`, you may need to change your policy to enable shell integration.
|
||||
|
||||
3. Change the Execution Policy by running the following command:
|
||||
```powershell
|
||||
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
- This sets the policy to `RemoteSigned` for the current user only, which is safer than changing it system-wide.
|
||||
|
||||
4. Confirm the Change by typing `Y` and pressing Enter when prompted.
|
||||
|
||||
5. Verify the Policy Change by running `Get-ExecutionPolicy` again to confirm the new setting.
|
||||
|
||||
6. Restart VSCode and try the shell integration again.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
#### WSL Integration
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const esbuild = require("esbuild")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
import fs from "node:fs"
|
||||
import * as esbuild from "esbuild"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
@@ -127,10 +131,8 @@ const baseConfig = {
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: production
|
||||
? {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
}
|
||||
: undefined,
|
||||
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
|
||||
: { "import.meta.url": "_importMetaUrl" },
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
@@ -141,6 +143,9 @@ const baseConfig = {
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
@@ -34,25 +34,30 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
// Should allow other vscode API calls
|
||||
{
|
||||
code: `vscode.window.showInformationMessage("Hello")`,
|
||||
filename: "test.ts",
|
||||
code: `vscode.commands.registerCommand("Hello")`,
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow postMessage calls on other objects
|
||||
{
|
||||
code: `window.postMessage({ type: "test" }, "*")`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow variables named vscode but not calling postMessage
|
||||
{
|
||||
code: `const vscode = { other: "method" }; vscode.other()`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "/foo/bar.test.ts",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Should disallow vscode.postMessage in regular files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "test", data: {} })`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
@@ -69,23 +74,13 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "test.test.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow property access for disallowed APIs
|
||||
{
|
||||
code: `const folders = vscode.workspace.workspaceFolders;`,
|
||||
filename: "workspace.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useHostBridge",
|
||||
messageId: "useHostBridgeWorkspace",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
const { RuleTester: StateApiRuleTester } = require("eslint")
|
||||
const noDirectVscodeStateApiRule = require("../no-direct-vscode-state-api")
|
||||
|
||||
const stateApiRuleTester = new StateApiRuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
stateApiRuleTester.run("no-direct-vscode-state-api", noDirectVscodeStateApiRule, {
|
||||
valid: [
|
||||
// Should allow state APIs in CacheService.ts
|
||||
{
|
||||
code: `await context.globalState.update("myKey", value);`,
|
||||
filename: "CacheService.ts",
|
||||
},
|
||||
{
|
||||
code: `const value = context.globalState.get("myKey");`,
|
||||
filename: "/src/core/storage/CacheService.ts",
|
||||
},
|
||||
{
|
||||
code: `await context.secrets.store("apiKey", value);`,
|
||||
filename: "CacheService.ts",
|
||||
},
|
||||
// Should allow state APIs in state-helpers.ts
|
||||
{
|
||||
code: `const value = context.globalState.get("myKey");`,
|
||||
filename: "state-helpers.ts",
|
||||
},
|
||||
{
|
||||
code: `await context.secrets.get("apiKey");`,
|
||||
filename: "/src/core/storage/utils/state-helpers.ts",
|
||||
},
|
||||
// Should allow state APIs in state-migrations.ts
|
||||
{
|
||||
code: `await context.globalState.update("myKey", value);`,
|
||||
filename: "state-migrations.ts",
|
||||
},
|
||||
{
|
||||
code: `const value = context.workspaceState.get("myKey");`,
|
||||
filename: "/src/core/storage/state-migrations.ts",
|
||||
},
|
||||
// Should allow state APIs in extension.ts
|
||||
{
|
||||
code: `const distinctId = context.globalState.get<string>("cline.distinctId");`,
|
||||
filename: "extension.ts",
|
||||
},
|
||||
{
|
||||
code: `await context.globalState.update("clineVersion", currentVersion);`,
|
||||
filename: "/src/extension.ts",
|
||||
},
|
||||
{
|
||||
code: `const secret = await context.secrets.get("clineAccountId");`,
|
||||
filename: "extension.ts",
|
||||
},
|
||||
// Should allow state APIs in test files
|
||||
{
|
||||
code: `context.globalState.get("testKey")`,
|
||||
filename: "/foo/bar.test.ts",
|
||||
},
|
||||
// Should allow non-state API calls
|
||||
{
|
||||
code: `const value = someOtherObject.globalState.get("myKey");`,
|
||||
filename: "some-file.ts",
|
||||
},
|
||||
{
|
||||
code: `await myContext.secrets.store("key", "value");`,
|
||||
filename: "some-file.ts",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Should disallow context.globalState.get
|
||||
{
|
||||
code: `const value = context.globalState.get("myKey");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceGlobalGet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.globalState.update
|
||||
{
|
||||
code: `await context.globalState.update("myKey", "myValue");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceGlobalSet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.workspaceState.get
|
||||
{
|
||||
code: `const value = context.workspaceState.get("myKey");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceWorkspaceGet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.workspaceState.update
|
||||
{
|
||||
code: `await context.workspaceState.update("myKey", "myValue");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceWorkspaceSet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.secrets.get
|
||||
{
|
||||
code: `const secret = await context.secrets.get("apiKey");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceSecretsGet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.secrets.store
|
||||
{
|
||||
code: `await context.secrets.store("apiKey", "secret-value");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceSecretsSet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow context.secrets.delete
|
||||
{
|
||||
code: `await context.secrets.delete("apiKey");`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceSecretsSet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow chained state API calls
|
||||
{
|
||||
code: `const value = await context.globalState.get("key") || "default";`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceGlobalGet",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow state API calls in Promise.all
|
||||
{
|
||||
code: `await Promise.all([context.secrets.get("key1"), context.secrets.get("key2")]);`,
|
||||
filename: "some-file.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useCacheServiceSecretsGet",
|
||||
},
|
||||
{
|
||||
messageId: "useCacheServiceSecretsGet",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,15 +1,18 @@
|
||||
// eslint-rules/index.js
|
||||
const noDirectVscodeApi = require("./no-direct-vscode-api")
|
||||
const noDirectVscodeStateApi = require("./no-direct-vscode-state-api")
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-direct-vscode-api": noDirectVscodeApi,
|
||||
"no-direct-vscode-state-api": noDirectVscodeStateApi,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: ["local"],
|
||||
rules: {
|
||||
"local/no-direct-vscode-api": "warn",
|
||||
"local/no-direct-vscode-state-api": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,23 +29,30 @@ const disallowedApis = {
|
||||
"vscode.workspace.applyEdit": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
// "vscode.env.openExternal": {
|
||||
// messageId: "useUtils",
|
||||
// },
|
||||
// "vscode.window.showWarningMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.onDidChangeActiveTextEditor": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.env.openExternal": {
|
||||
messageId: "useUtils",
|
||||
},
|
||||
"vscode.window.showWarningMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showOpenDialog": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
// There are too many warnings for these calls, uncomment the following
|
||||
// when the migration is finished.
|
||||
// "vscode.window.showErrorMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
// "vscode.window.showInformationMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showErrorMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showInformationMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showInputBox": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.workspace.findFiles": {
|
||||
messageId: "useNative",
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = createRule({
|
||||
@@ -86,6 +93,10 @@ module.exports = createRule({
|
||||
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useNative:
|
||||
"Use a native Javascript API instead of calling the vscode API.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
@@ -186,6 +197,10 @@ module.exports = createRule({
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
// Skip checking test files
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
const path = require("path")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
// Configuration for context-based state APIs
|
||||
const disallowedContextApis = {
|
||||
"globalState.get": {
|
||||
messageId: "useCacheServiceGlobalGet",
|
||||
},
|
||||
"globalState.update": {
|
||||
messageId: "useCacheServiceGlobalSet",
|
||||
},
|
||||
"workspaceState.get": {
|
||||
messageId: "useCacheServiceWorkspaceGet",
|
||||
},
|
||||
"workspaceState.update": {
|
||||
messageId: "useCacheServiceWorkspaceSet",
|
||||
},
|
||||
"secrets.get": {
|
||||
messageId: "useCacheServiceSecretsGet",
|
||||
},
|
||||
"secrets.store": {
|
||||
messageId: "useCacheServiceSecretsSet",
|
||||
},
|
||||
"secrets.delete": {
|
||||
messageId: "useCacheServiceSecretsSet",
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-direct-vscode-state-api",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow direct VSCode state API usage (context.globalState, context.workspaceState, context.secrets) in favor of CacheService",
|
||||
recommended: "error",
|
||||
},
|
||||
messages: {
|
||||
useCacheServiceGlobalGet:
|
||||
"Use CacheService.getGlobalStateKey() instead of context.globalState.get().\n" +
|
||||
"The CacheService provides fast in-memory access with automatic persistence.\n" +
|
||||
"Example: cacheService.getGlobalStateKey('myKey') instead of context.globalState.get('myKey').\n" +
|
||||
"Found: {{code}}",
|
||||
useCacheServiceGlobalSet:
|
||||
"Use CacheService.setGlobalState() instead of context.globalState.update().\n" +
|
||||
"The CacheService provides immediate updates with debounced persistence.\n" +
|
||||
"Example: cacheService.setGlobalState('myKey', value) instead of context.globalState.update('myKey', value).\n" +
|
||||
"Found: {{code}}",
|
||||
useCacheServiceWorkspaceGet:
|
||||
"Use CacheService.getWorkspaceStateKey() instead of context.workspaceState.get().\n" +
|
||||
"The CacheService provides fast in-memory access with automatic persistence.\n" +
|
||||
"Example: cacheService.getWorkspaceStateKey('myKey') instead of context.workspaceState.get('myKey').\n" +
|
||||
"Found: {{code}}",
|
||||
useCacheServiceWorkspaceSet:
|
||||
"Use CacheService.setWorkspaceState() instead of context.workspaceState.update().\n" +
|
||||
"The CacheService provides immediate updates with debounced persistence.\n" +
|
||||
"Example: cacheService.setWorkspaceState('myKey', value) instead of context.workspaceState.update('myKey', value).\n" +
|
||||
"Found: {{code}}",
|
||||
useCacheServiceSecretsGet:
|
||||
"Use CacheService.getSecretKey() instead of context.secrets.get().\n" +
|
||||
"The CacheService provides fast in-memory access with automatic persistence.\n" +
|
||||
"Example: cacheService.getSecretKey('mySecret') instead of context.secrets.get('mySecret').\n" +
|
||||
"Found: {{code}}",
|
||||
useCacheServiceSecretsSet:
|
||||
"Use CacheService.setSecret() instead of context.secrets.store() or context.secrets.delete().\n" +
|
||||
"The CacheService provides immediate updates with debounced persistence.\n" +
|
||||
"Example: cacheService.setSecret('mySecret', value) instead of context.secrets.store('mySecret', value).\n" +
|
||||
"For deletion, use: cacheService.setSecret('mySecret', undefined).\n" +
|
||||
"Found: {{code}}",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
|
||||
create(context) {
|
||||
function isExcluded(filename) {
|
||||
// Skip checking test files
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
// Skip checking specific state-related files that need direct access
|
||||
const basename = path.basename(filename)
|
||||
if (
|
||||
basename === "CacheService.ts" ||
|
||||
basename === "state-helpers.ts" ||
|
||||
basename === "state-migrations.ts" ||
|
||||
basename === "extension.ts" ||
|
||||
basename === "common.ts" // CI might report errors from this virtual file
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for context-based state API calls
|
||||
function checkContextStateApi(node) {
|
||||
if (isExcluded(context.filename)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a member expression like context.globalState.get
|
||||
if (
|
||||
node.type === "MemberExpression" &&
|
||||
node.object &&
|
||||
node.object.type === "MemberExpression" &&
|
||||
node.object.object &&
|
||||
node.object.object.type === "Identifier" &&
|
||||
node.object.object.name === "context"
|
||||
) {
|
||||
const stateType = node.object.property.name // e.g., "globalState", "workspaceState", "secrets"
|
||||
const method = node.property.name // e.g., "get", "update", "store", "delete"
|
||||
const apiPath = `${stateType}.${method}`
|
||||
|
||||
if (disallowedContextApis[apiPath]) {
|
||||
// For method calls, get the whole call expression
|
||||
let reportNode = node
|
||||
let parentNode = context.sourceCode.getAncestors(node).pop()
|
||||
if (parentNode && parentNode.type === "CallExpression" && parentNode.callee === node) {
|
||||
reportNode = parentNode
|
||||
}
|
||||
|
||||
const callText = context.sourceCode.getText(reportNode).trim()
|
||||
|
||||
context.report({
|
||||
node: reportNode,
|
||||
messageId: disallowedContextApis[apiPath].messageId,
|
||||
data: {
|
||||
code: callText,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Detect member expressions (e.g., context.globalState.get)
|
||||
MemberExpression(node) {
|
||||
checkContextStateApi(node)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -10,6 +10,7 @@ interface RunDiffEvalOptions {
|
||||
parsingFunction: string
|
||||
diffEditFunction: string
|
||||
thinkingBudget: number
|
||||
provider: string
|
||||
parallel: boolean
|
||||
verbose: boolean
|
||||
testPath: string
|
||||
@@ -39,6 +40,8 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
|
||||
options.parsingFunction,
|
||||
"--diff-edit-function",
|
||||
options.diffEditFunction,
|
||||
"--provider",
|
||||
options.provider,
|
||||
]
|
||||
|
||||
// Conditionally add the optional arguments
|
||||
|
||||
@@ -92,6 +92,7 @@ program
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
|
||||
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OpenRouterHandler } from "../../src/api/providers/openrouter"
|
||||
import { OpenAiNativeHandler } from "../../src/api/providers/openai-native"
|
||||
import { ApiHandlerOptions } from "../../src/shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import {
|
||||
parseAssistantMessageV1,
|
||||
parseAssistantMessageV2,
|
||||
parseAssistantMessageV3,
|
||||
AssistantMessageContent,
|
||||
@@ -17,7 +17,6 @@ type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
|
||||
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string | any>
|
||||
|
||||
const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
|
||||
parseAssistantMessageV1: parseAssistantMessageV1,
|
||||
parseAssistantMessageV2: parseAssistantMessageV2,
|
||||
parseAssistantMessageV3: parseAssistantMessageV3,
|
||||
}
|
||||
@@ -54,7 +53,7 @@ interface StreamResult {
|
||||
* Process the stream and return full response with timing data
|
||||
*/
|
||||
async function processStream(
|
||||
handler: OpenRouterHandler,
|
||||
handler: OpenRouterHandler | OpenAiNativeHandler,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): Promise<StreamResult> {
|
||||
@@ -190,19 +189,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
|
||||
}
|
||||
}
|
||||
|
||||
const options: ApiHandlerOptions = {
|
||||
openRouterApiKey: apiKey,
|
||||
openRouterModelId: modelId,
|
||||
thinkingBudgetTokens: thinkingBudgetTokens,
|
||||
openRouterModelInfo: {
|
||||
maxTokens: 10_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true, // may need to turn this on
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
}
|
||||
const provider = input.provider || "openrouter"
|
||||
|
||||
// Get the output of streaming output of this llm call
|
||||
let streamResult: StreamResult
|
||||
@@ -214,10 +201,34 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
|
||||
usage: { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalCost: 0 },
|
||||
}
|
||||
} else {
|
||||
// Live mode: existing API call logic
|
||||
// Live mode: provider-specific API call logic
|
||||
try {
|
||||
const openRouterHandler = new OpenRouterHandler(options)
|
||||
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
|
||||
let handler: OpenRouterHandler | OpenAiNativeHandler
|
||||
|
||||
if (provider === "openai") {
|
||||
const openAiOptions = {
|
||||
openAiNativeApiKey: apiKey,
|
||||
apiModelId: modelId,
|
||||
}
|
||||
handler = new OpenAiNativeHandler(openAiOptions)
|
||||
} else {
|
||||
const openRouterOptions = {
|
||||
openRouterApiKey: apiKey,
|
||||
openRouterModelId: modelId,
|
||||
thinkingBudgetTokens: thinkingBudgetTokens,
|
||||
openRouterModelInfo: {
|
||||
maxTokens: 10_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
}
|
||||
handler = new OpenRouterHandler(openRouterOptions)
|
||||
}
|
||||
|
||||
streamResult = await processStream(handler, systemPrompt, messages)
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -49,16 +49,25 @@ type TestResultSet = { [test_id: string]: (TestResult & { test_id?: string })[]
|
||||
|
||||
class NodeTestRunner {
|
||||
private apiKey: string | undefined
|
||||
private provider: string
|
||||
private currentRunId: string | null = null
|
||||
private systemPromptHash: string | null = null
|
||||
private processingFunctionsHash: string | null = null
|
||||
private caseIdMap: Map<string, string> = new Map() // test_id -> case_id mapping
|
||||
|
||||
constructor(isReplay: boolean) {
|
||||
constructor(isReplay: boolean, provider: string = "openrouter") {
|
||||
this.provider = provider
|
||||
if (!isReplay) {
|
||||
this.apiKey = process.env.OPENROUTER_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run.")
|
||||
if (provider === "openai") {
|
||||
this.apiKey = process.env.OPENAI_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENAI_API_KEY environment variable not set for a non-replay run with OpenAI provider.")
|
||||
}
|
||||
} else {
|
||||
this.apiKey = process.env.OPENROUTER_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run with OpenRouter provider.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,6 +644,7 @@ class NodeTestRunner {
|
||||
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
|
||||
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
|
||||
diffApplyFile: testConfig.diff_apply_file,
|
||||
provider: this.provider,
|
||||
isVerbose: isVerbose,
|
||||
}
|
||||
|
||||
@@ -927,6 +937,7 @@ async function main() {
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
|
||||
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
|
||||
@@ -959,7 +970,7 @@ async function main() {
|
||||
? parseInt(options.maxAttemptsPerCase, 10)
|
||||
: validAttemptsPerCase * 10;
|
||||
|
||||
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
|
||||
const runner = new NodeTestRunner(options.replay || !!options.replayRunId, options.provider)
|
||||
|
||||
if (options.replayRunId) {
|
||||
if (!options.diffApplyFile) {
|
||||
@@ -979,7 +990,7 @@ async function main() {
|
||||
log(isVerbose, "Warning: Could not load OpenRouter model data. Context window filtering might be affected for OpenRouter models.");
|
||||
}
|
||||
|
||||
const runner = new NodeTestRunner(options.replay)
|
||||
const runner = new NodeTestRunner(options.replay, options.provider)
|
||||
let allLoadedTestCases = runner.loadTestCases(testPath, isVerbose) // Pass isVerbose
|
||||
|
||||
const allProcessedTestCasesGlobal: ProcessedTestCase[] = allLoadedTestCases.map((tc) => ({
|
||||
|
||||
@@ -331,6 +331,42 @@ def get_performance_grade(success_rate):
|
||||
else:
|
||||
return "C", "poor"
|
||||
|
||||
def get_error_description(error_enum, error_string=None):
|
||||
"""Map error enum values to user-friendly descriptions"""
|
||||
error_map = {
|
||||
1: "No tool calls - Model didn't use the replace_in_file tool",
|
||||
2: "Multiple tool calls - Model called multiple tools instead of one",
|
||||
3: "Wrong tool call - Model used wrong tool (not replace_in_file)",
|
||||
4: "Missing parameters - Tool call missing required path or diff",
|
||||
5: "Wrong file edited - Model edited different file than expected",
|
||||
6: "Wrong tool call - Model used wrong tool type",
|
||||
7: "Wrong file edited - Model targeted incorrect file path",
|
||||
8: "API/Stream error - Problem with model API connection",
|
||||
9: "Configuration error - Invalid evaluation parameters",
|
||||
10: "Function error - Invalid parsing/diff functions",
|
||||
11: "Other error - Unexpected failure"
|
||||
}
|
||||
|
||||
base_description = error_map.get(error_enum, f"Unknown error (code: {error_enum})")
|
||||
|
||||
if error_string:
|
||||
return f"{base_description}: {error_string}"
|
||||
return base_description
|
||||
|
||||
def get_error_guidance(error_enum):
|
||||
"""Provide specific guidance based on error type"""
|
||||
guidance_map = {
|
||||
1: "💡 The model provided a response but didn't use the replace_in_file tool. Check the raw output to see what the model actually said.",
|
||||
2: "💡 The model called multiple tools when it should only call replace_in_file once. Check the parsed tool call section.",
|
||||
3: "💡 The model used a different tool instead of replace_in_file. This might indicate confusion about the task.",
|
||||
4: "💡 The model called replace_in_file but didn't provide the required 'path' or 'diff' parameters.",
|
||||
5: "💡 The model tried to edit a different file than expected. Check the parsed tool call to see which file it targeted.",
|
||||
6: "💡 The model used the wrong tool type. Check the raw output to see what tool it attempted to use.",
|
||||
7: "💡 The model tried to edit a different file path than expected. This could indicate path confusion or hallucination.",
|
||||
}
|
||||
|
||||
return guidance_map.get(error_enum, "")
|
||||
|
||||
def render_hero_section(current_run, model_performance):
|
||||
"""Render the hero section with key metrics"""
|
||||
run_title = current_run['description'] if current_run['description'] else f"Run {current_run['run_id'][:8]}..."
|
||||
@@ -570,12 +606,16 @@ def render_result_detail(result):
|
||||
"""Render detailed view of a single result"""
|
||||
st.markdown("### 🔬 Result Deep Dive")
|
||||
|
||||
# Check if this is a valid result
|
||||
is_valid = (result['error_enum'] not in [1, 6, 7]) if not pd.isna(result['error_enum']) else True
|
||||
# Check if this is a valid result (only invalid if no tool calls or wrong file)
|
||||
is_valid = True
|
||||
if not pd.isna(result['error_enum']):
|
||||
# Only these specific errors make a result "invalid" for the benchmark:
|
||||
# 1 = no_tool_calls, 5 = wrong_file_edited, 7 = wrong_file_edited
|
||||
is_valid = result['error_enum'] not in [1, 5, 7]
|
||||
|
||||
# Show validity warning if needed
|
||||
if not is_valid:
|
||||
st.warning("⚠️ **This is an invalid result** - The model didn't properly call the diff edit tool or edited the wrong file. This result is excluded from success rate calculations.")
|
||||
st.warning("⚠️ **This is an invalid result** - The model didn't call the replace_in_file tool or edited the wrong file. This result is excluded from success rate calculations.")
|
||||
|
||||
# Result metadata
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
@@ -591,7 +631,10 @@ def render_result_detail(result):
|
||||
st.markdown(f"**Round Trip:** {result['time_round_trip_ms']:.0f}ms")
|
||||
|
||||
with col4:
|
||||
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
|
||||
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
|
||||
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
|
||||
else:
|
||||
st.markdown(f"**Cost:** Free")
|
||||
|
||||
# Tabbed interface for different views
|
||||
tab1, tab2, tab3, tab4 = st.tabs(["📄 File & Edits", "🤖 Raw Output", "🔧 Parsed Tool Call", "📊 Metrics"])
|
||||
@@ -693,8 +736,46 @@ def render_file_and_edits_view(result):
|
||||
# Show error information
|
||||
st.error("❌ **Edit Failed**")
|
||||
|
||||
# Show detailed error reason
|
||||
if not pd.isna(result['error_enum']):
|
||||
st.markdown(f"**Error Code:** {result['error_enum']}")
|
||||
error_description = get_error_description(
|
||||
result['error_enum'],
|
||||
result.get('error_string')
|
||||
)
|
||||
st.markdown(f"**Reason:** {error_description}")
|
||||
|
||||
# Show specific guidance based on error type
|
||||
guidance = get_error_guidance(result['error_enum'])
|
||||
if guidance:
|
||||
st.info(guidance)
|
||||
|
||||
# For valid results that failed, check for diff application failures
|
||||
elif not result['succeeded']:
|
||||
# This is a valid result that failed - likely due to diff application issues
|
||||
raw_output = result.get('raw_model_output', '')
|
||||
|
||||
# Check if we have specific error information in the raw output
|
||||
if 'does not match anything in the file' in str(raw_output).lower():
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The SEARCH block in the diff didn't match any content in the original file. This usually means the model hallucinated code that doesn't exist.")
|
||||
elif 'malformatted' in str(raw_output).lower() or 'malformed' in str(raw_output).lower():
|
||||
st.warning("⚠️ **Diff Format Error**")
|
||||
st.info("💡 The diff format was incorrect. Check the raw tool call to see the formatting issues.")
|
||||
elif 'error:' in str(raw_output).lower():
|
||||
# Try to extract the specific error message
|
||||
lines = str(raw_output).split('\n')
|
||||
error_lines = [line for line in lines if 'error:' in line.lower()]
|
||||
if error_lines:
|
||||
error_msg = error_lines[0].strip()
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info(f"💡 {error_msg}")
|
||||
else:
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The diff couldn't be applied to the original file. Check the raw output and parsed tool call for more details.")
|
||||
else:
|
||||
# Generic diff application failure
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The model made a valid tool call but the diff couldn't be applied to the original file. This usually indicates a mismatch between the expected and actual file content.")
|
||||
else:
|
||||
# Show successful edit information
|
||||
st.success("✅ **Edit Successful**")
|
||||
@@ -725,8 +806,25 @@ def render_file_and_edits_view(result):
|
||||
if len(edited_lines) > 50:
|
||||
st.text(f"... ({len(edited_lines) - 50} more lines)")
|
||||
|
||||
# Show parsed tool call if available
|
||||
# Show raw and parsed tool calls if available
|
||||
if not pd.isna(result['parsed_tool_call_json']):
|
||||
with st.expander("View Raw Tool Call"):
|
||||
# Extract the raw tool call text from the model output
|
||||
raw_output = result['raw_model_output'] if not pd.isna(result['raw_model_output']) else ""
|
||||
|
||||
# Try to extract just the tool call portion
|
||||
if raw_output and '<replace_in_file>' in raw_output:
|
||||
# Find the tool call block
|
||||
start_idx = raw_output.find('<replace_in_file>')
|
||||
end_idx = raw_output.find('</replace_in_file>') + len('</replace_in_file>')
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
raw_tool_call = raw_output[start_idx:end_idx]
|
||||
st.code(raw_tool_call, language='xml')
|
||||
else:
|
||||
st.text("Tool call not found in raw output")
|
||||
else:
|
||||
st.text("No raw tool call available")
|
||||
|
||||
with st.expander("View Parsed Tool Call"):
|
||||
try:
|
||||
parsed_call = json.loads(result['parsed_tool_call_json'])
|
||||
@@ -795,8 +893,10 @@ def render_metrics_view(result):
|
||||
if not pd.isna(result['completion_tokens']):
|
||||
st.metric("Completion Tokens", int(result['completion_tokens']))
|
||||
|
||||
if not pd.isna(result['cost_usd']):
|
||||
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
|
||||
st.metric("Cost", f"${result['cost_usd']:.4f}")
|
||||
else:
|
||||
st.metric("Cost", "Free")
|
||||
|
||||
if not pd.isna(result['tokens_in_context']):
|
||||
st.metric("Context Tokens", int(result['tokens_in_context']))
|
||||
|
||||
@@ -70,246 +70,7 @@ export interface ToolUse {
|
||||
partial: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @description **Version 1**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version iterates through the message character by character, building an accumulator string.
|
||||
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
|
||||
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
|
||||
* the corresponding opening or closing tags.
|
||||
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
|
||||
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
|
||||
* occurrence of the closing tag.
|
||||
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentToolUseStartIndex = 0
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
let currentParamValueStartIndex = 0
|
||||
let accumulator = ""
|
||||
|
||||
for (let i = 0; i < assistantMessage.length; i++) {
|
||||
const char = assistantMessage[i]
|
||||
accumulator += char
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
// there should not be a param without a tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
|
||||
const paramClosingTag = `</${currentParamName}>`
|
||||
if (currentParamValue.endsWith(paramClosingTag)) {
|
||||
// End of param value found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
// no currentParamName
|
||||
if (currentToolUse) {
|
||||
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
|
||||
const toolUseClosingTag = `</${currentToolUse.name}>`
|
||||
|
||||
if (currentToolValue.endsWith(toolUseClosingTag)) {
|
||||
// End of a tool use found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Go back to parsing text or looking for next tool
|
||||
// Reset text start index in case text follows immediately
|
||||
currentTextContentStartIndex = i + 1
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Check if starting a new parameter within the current tool use
|
||||
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
|
||||
let foundParamStart = false
|
||||
for (const paramOpeningTag of possibleParamOpeningTags) {
|
||||
if (accumulator.endsWith(paramOpeningTag)) {
|
||||
// Start of a new parameter found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// Special case for write_to_file/new_rule content param allowing nested tags
|
||||
// Check if a </content> tag appears, potentially indicating the end of the content param
|
||||
// even if the main tool closing tag hasn't been seen yet.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
|
||||
// Use lastIndexOf to handle cases where </content> might appear within the content itself
|
||||
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
|
||||
|
||||
// Ensure we found valid start/end tags and end is after start
|
||||
if (
|
||||
contentStartIndex !== -1 &&
|
||||
contentEndIndex !== -1 &&
|
||||
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
|
||||
) {
|
||||
// Check if this content param was already being parsed. If so, update it.
|
||||
// If not, and we just found the closing tag, assign it.
|
||||
// This handles cases where the </content> detection might fire before
|
||||
// the <content> tag detection logic, or if the content is very short.
|
||||
if (currentParamName === contentParamName) {
|
||||
// Already parsing content, now we found the end tag
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
currentParamName = undefined // Finished with this param
|
||||
} else if (currentParamName === undefined) {
|
||||
// Not parsing a param, but found </content>. Assume it closes the content block.
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing Text (or looking for start of a tool use) ---
|
||||
// no currentToolUse
|
||||
let didStartToolUse = false
|
||||
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
|
||||
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
|
||||
if (accumulator.endsWith(toolUseOpeningTag)) {
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// Extract text content, removing the part that formed the tool opening tag
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
// Only add if there's actual content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check if there was text before this tool use started
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false, // Ended because tool use started
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didStartToolUse = true
|
||||
break // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
// Start of a new text block
|
||||
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
|
||||
// If accumulator starts from 0, start index is i
|
||||
if (contentBlocks.length === 0 && currentToolUse === undefined) {
|
||||
currentTextContentStartIndex = accumulator.length - 1 // i
|
||||
} else {
|
||||
// Re-calculate based on the actual start of the current text segment
|
||||
// Find the end of the last block
|
||||
let lastBlockEndIndex = 0
|
||||
if (contentBlocks.length > 0) {
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
|
||||
// Simpler: Assume text starts right after the last block ended implicitly at index i.
|
||||
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
|
||||
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
|
||||
// Let's stick to the accumulator slice approach for simplicity in this version.
|
||||
// The start index should be where the current *unmatched* text began.
|
||||
let lastProcessedIndex = -1
|
||||
if (contentBlocks.length > 0) {
|
||||
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
|
||||
// We'll approximate based on the current accumulator and start index logic.
|
||||
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
|
||||
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
|
||||
}
|
||||
// Reset start index to the beginning of the *current* potential text block
|
||||
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
|
||||
}
|
||||
|
||||
// If we just closed a tool, text starts *after* its closing tag
|
||||
// The logic needs refinement here for accurate start index after a tool closure.
|
||||
// Let's assume for now the start index logic inside the loop handles it via slicing.
|
||||
}
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Content will be filled by slicing accumulator
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Update text content based on the accumulator from its start index
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// If a tool use was open at the end
|
||||
if (currentToolUse) {
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// If text content was being accumulated at the end
|
||||
// Note: Only one of currentToolUse or currentTextContent can be defined here,
|
||||
// as starting a tool use finalizes the preceding text block.
|
||||
else if (currentTextContent) {
|
||||
// Update content one last time
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
|
||||
// Add the potentially partial text block only if it contains content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
|
||||
@@ -104,5 +104,6 @@ export interface TestInput {
|
||||
thinkingBudgetTokens: number
|
||||
originalDiffEditToolCallMessage?: string
|
||||
diffApplyFile?: string
|
||||
provider?: string
|
||||
isVerbose: boolean
|
||||
}
|
||||
|
||||
Generated
+236
-16829
File diff suppressed because it is too large
Load Diff
+13
-9
@@ -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.7",
|
||||
"version": "3.23.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -119,7 +119,8 @@
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "claude-dev.SidebarProvider",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"icon": "assets/icons/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -338,14 +339,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:esbuild": "node esbuild.mjs --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-protobus-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.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",
|
||||
@@ -361,8 +362,8 @@
|
||||
"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",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --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",
|
||||
@@ -446,6 +447,7 @@
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
@@ -490,6 +492,8 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"ulid": "^2.4.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
+21
-6
@@ -1,17 +1,32 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isGitHubAction = !!process.env.CI
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
reporter: isCI ? [["github"], ["list"]] : [["list"]],
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
teardown: "cleanup test environment",
|
||||
},
|
||||
{
|
||||
name: "cleanup test environment",
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: "e2e tests",
|
||||
testMatch: /.*\.test\.ts/,
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -127,4 +127,4 @@ message OrganizationUsageTransaction {
|
||||
int32 prompt_tokens = 10;
|
||||
int32 total_tokens = 11;
|
||||
string user_id = 12;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -55,6 +55,11 @@ message Boolean {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
// the same as Boolean, but avoiding name conflicts
|
||||
message BooleanResponse {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
message StringArray {
|
||||
repeated string values = 1;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -55,8 +55,11 @@ service FileService {
|
||||
// Toggles a workflow on or off
|
||||
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
|
||||
|
||||
// Subscribe to workspace file updates
|
||||
rpc subscribeToWorkspaceUpdates(EmptyRequest) returns (stream StringArray);
|
||||
// Check if file exists in the project
|
||||
rpc ifFileExistsRelativePath(StringRequest) returns (BooleanResponse);
|
||||
|
||||
// Open a file in editor by a relative path
|
||||
rpc openFileRelativePath(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Response for refreshRules operation
|
||||
@@ -87,12 +90,19 @@ message RelativePaths {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
// Enum for file search type filtering
|
||||
enum FileSearchType {
|
||||
FILE = 0;
|
||||
FOLDER = 1;
|
||||
}
|
||||
|
||||
// Request for file search operations
|
||||
message FileSearchRequest {
|
||||
Metadata metadata = 1;
|
||||
string query = 2; // Search query string
|
||||
optional string mentions_request_id = 3; // Optional request ID for tracking requests
|
||||
optional int32 limit = 4; // Optional limit for results (default: 20)
|
||||
optional FileSearchType selected_type = 5; // Optional selected type filter
|
||||
}
|
||||
|
||||
// Result for file search operations
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -27,6 +27,8 @@ service ModelsService {
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Baseten models
|
||||
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -129,6 +131,8 @@ enum ApiProvider {
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
HUAWEI_CLOUD_MAAS = 29;
|
||||
BASETEN = 30;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -171,7 +175,7 @@ message ModelsApiConfiguration {
|
||||
// Global configuration fields (not mode-specific)
|
||||
optional string api_key = 1;
|
||||
optional string cline_api_key = 2;
|
||||
optional string task_id = 3;
|
||||
optional string ulid = 3;
|
||||
optional string lite_llm_base_url = 4;
|
||||
optional string lite_llm_api_key = 5;
|
||||
optional bool lite_llm_use_prompt_cache = 6;
|
||||
@@ -229,6 +233,9 @@ message ModelsApiConfiguration {
|
||||
optional string cline_account_id = 58;
|
||||
optional string groq_api_key = 59;
|
||||
optional string hugging_face_api_key = 60;
|
||||
optional string huawei_cloud_maas_api_key = 61;
|
||||
optional string baseten_api_key = 62;
|
||||
optional string ollama_api_key = 63;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -255,6 +262,10 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
|
||||
optional string plan_mode_hugging_face_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 124;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
|
||||
optional string plan_mode_baseten_model_id = 126;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 127;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -281,6 +292,10 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
|
||||
optional string act_mode_hugging_face_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 224;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
|
||||
optional string act_mode_baseten_model_id = 226;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 227;
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(ResetStateRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
@@ -43,7 +43,7 @@ message TerminalProfileUpdateResponse {
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
ChatSettings chat_settings = 2;
|
||||
PlanActMode mode = 2;
|
||||
optional ChatContent chat_content = 3;
|
||||
}
|
||||
|
||||
@@ -52,10 +52,16 @@ enum PlanActMode {
|
||||
ACT = 1;
|
||||
}
|
||||
|
||||
message ChatSettings {
|
||||
PlanActMode mode = 1;
|
||||
optional string preferred_language = 2;
|
||||
optional string open_ai_reasoning_effort = 3;
|
||||
enum OpenaiReasoningEffort {
|
||||
LOW = 0;
|
||||
MEDIUM = 1;
|
||||
HIGH = 2;
|
||||
}
|
||||
|
||||
enum McpDisplayMode {
|
||||
RICH = 0;
|
||||
PLAIN = 1;
|
||||
MARKDOWN = 2;
|
||||
}
|
||||
|
||||
message ChatContent {
|
||||
@@ -108,12 +114,15 @@ message UpdateSettingsRequest {
|
||||
optional bool plan_act_separate_models_setting = 4;
|
||||
optional bool enable_checkpoints_setting = 5;
|
||||
optional bool mcp_marketplace_enabled = 6;
|
||||
optional ChatSettings chat_settings = 7;
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional int32 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional string mcp_display_mode = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
optional McpDisplayMode mcp_display_mode = 11;
|
||||
optional int32 terminal_output_line_limit = 12;
|
||||
optional PlanActMode mode = 13;
|
||||
optional string preferred_language = 14;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 15;
|
||||
optional bool strict_plan_mode_enabled = 16;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -121,7 +130,7 @@ message ApiConfiguration {
|
||||
// Global configuration fields (not mode-specific)
|
||||
optional string api_key = 1; // anthropic
|
||||
optional string cline_api_key = 2;
|
||||
optional string task_id = 3;
|
||||
optional string ulid = 3;
|
||||
optional string lite_llm_base_url = 4;
|
||||
optional string lite_llm_api_key = 5;
|
||||
optional bool lite_llm_use_prompt_cache = 6;
|
||||
@@ -153,8 +162,8 @@ message ApiConfiguration {
|
||||
optional string requesty_api_key = 32;
|
||||
optional string together_api_key = 33;
|
||||
optional string fireworks_api_key = 34;
|
||||
optional int64 fireworks_model_max_completion_tokens = 35;
|
||||
optional int64 fireworks_model_max_tokens = 36;
|
||||
optional int32 fireworks_model_max_completion_tokens = 35;
|
||||
optional int32 fireworks_model_max_tokens = 36;
|
||||
optional string qwen_api_key = 37;
|
||||
optional string doubao_api_key = 38;
|
||||
optional string mistral_api_key = 39;
|
||||
@@ -166,7 +175,7 @@ message ApiConfiguration {
|
||||
optional string xai_api_key = 45;
|
||||
optional string sambanova_api_key = 46;
|
||||
optional string cerebras_api_key = 47;
|
||||
optional int64 request_timeout_ms = 48;
|
||||
optional int32 request_timeout_ms = 48;
|
||||
optional string sap_ai_core_client_id = 49;
|
||||
optional string sap_ai_core_client_secret = 50;
|
||||
optional string sap_ai_resource_group = 51;
|
||||
@@ -174,11 +183,13 @@ message ApiConfiguration {
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
optional string huawei_cloud_maas_api_key = 56;
|
||||
optional string ollama_api_key = 57;
|
||||
|
||||
// Plan mode configurations
|
||||
optional string plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 102;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
@@ -196,11 +207,13 @@ message ApiConfiguration {
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 120;
|
||||
optional string plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
|
||||
// Act mode configurations
|
||||
optional string act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int64 act_mode_thinking_budget_tokens = 202;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
@@ -218,6 +231,8 @@ message ApiConfiguration {
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 220;
|
||||
optional string act_mode_huawei_cloud_maas_model_info = 221;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 300;
|
||||
@@ -228,3 +243,11 @@ message ApiConfiguration {
|
||||
|
||||
optional string cline_account_id = 303;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -227,7 +227,7 @@ service UiService {
|
||||
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
|
||||
|
||||
// Subscribe to addToInput events (when user adds content via context menu)
|
||||
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
|
||||
rpc subscribeToAddToInput(StringRequest) returns (stream String);
|
||||
|
||||
// Subscribe to MCP button clicked events
|
||||
rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
@@ -267,4 +267,7 @@ service UiService {
|
||||
|
||||
// Opens a URL in the default browser
|
||||
rpc openUrl(StringRequest) returns (Empty);
|
||||
|
||||
// Opens the Cline walkthrough
|
||||
rpc openWalkthrough(EmptyRequest) returns (Empty);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
+39
-9
@@ -4,22 +4,34 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for diff views.
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
|
||||
// Get the contents of the diff view.
|
||||
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
|
||||
|
||||
// Replace a text selection in the diff.
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
|
||||
rpc scrollDiff(ScrollDiffRequest) returns (ScrollDiffResponse);
|
||||
|
||||
// Truncate the diff document.
|
||||
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
|
||||
|
||||
// Save the diff document.
|
||||
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
|
||||
// Close the diff editor UI.
|
||||
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
|
||||
|
||||
// Close all the diff editor windows/tabs.
|
||||
// Any diff editors with unsaved content should not be closed.
|
||||
rpc closeAllDiffs(CloseAllDiffsRequest) returns (CloseAllDiffsResponse);
|
||||
|
||||
// Display a diff view comparing before/after states for multiple files.
|
||||
// Content is passed as in-memory data, not read from the file system.
|
||||
rpc openMultiFileDiff(OpenMultiFileDiffRequest) returns (OpenMultiFileDiffResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
@@ -54,20 +66,24 @@ message ReplaceTextRequest {
|
||||
|
||||
message ReplaceTextResponse {}
|
||||
|
||||
message ScrollDiffRequest {
|
||||
optional string diff_id = 1;
|
||||
optional int32 line = 2;
|
||||
}
|
||||
|
||||
message ScrollDiffResponse {}
|
||||
|
||||
message TruncateDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional int32 end_line = 5;
|
||||
optional int32 end_line = 3;
|
||||
}
|
||||
|
||||
message TruncateDocumentResponse {}
|
||||
|
||||
message CloseDiffRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
message CloseAllDiffsRequest {}
|
||||
|
||||
message CloseDiffResponse {}
|
||||
message CloseAllDiffsResponse {}
|
||||
|
||||
message SaveDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
@@ -75,3 +91,17 @@ message SaveDocumentRequest {
|
||||
}
|
||||
|
||||
message SaveDocumentResponse {}
|
||||
|
||||
message OpenMultiFileDiffRequest {
|
||||
optional string title = 1;
|
||||
repeated ContentDiff diffs = 2;
|
||||
}
|
||||
|
||||
message ContentDiff {
|
||||
// The absolute file path.
|
||||
optional string file_path = 1;
|
||||
optional string left_content = 2;
|
||||
optional string right_content = 3;
|
||||
}
|
||||
|
||||
message OpenMultiFileDiffResponse {}
|
||||
|
||||
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with the user's environment.
|
||||
service EnvService {
|
||||
@@ -14,6 +14,6 @@ service EnvService {
|
||||
// Reads text from the system clipboard.
|
||||
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
|
||||
|
||||
// Opens a URL in the user's default browser or application.
|
||||
rpc openExternal(cline.StringRequest) returns (cline.Empty);
|
||||
// Returns a stable machine identifier for telemetry distinctId purposes.
|
||||
rpc getMachineId(cline.EmptyRequest) returns (cline.String);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// This is for use in integration tests to get the contents of the webview.
|
||||
service TestingService {
|
||||
rpc getWebviewHtml(GetWebviewHtmlRequest) returns (GetWebviewHtmlResponse);
|
||||
}
|
||||
|
||||
message GetWebviewHtmlRequest {
|
||||
}
|
||||
|
||||
message GetWebviewHtmlResponse {
|
||||
optional string html = 1;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
|
||||
/**
|
||||
* The watch service is only here as example of a streaming rpc in the host bridge.
|
||||
|
||||
+44
-1
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with IDE windows and editors.
|
||||
service WindowService {
|
||||
@@ -12,7 +12,11 @@ service WindowService {
|
||||
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
|
||||
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
rpc showInputBox(ShowInputBoxRequest) returns (ShowInputBoxResponse);
|
||||
rpc showSaveDialog(ShowSaveDialogRequest) returns (ShowSaveDialogResponse);
|
||||
rpc openFile(OpenFileRequest) returns (OpenFileResponse);
|
||||
rpc getOpenTabs(GetOpenTabsRequest) returns (GetOpenTabsResponse);
|
||||
rpc getVisibleTabs(GetVisibleTabsRequest) returns (GetVisibleTabsResponse);
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
@@ -80,6 +84,8 @@ message ShowSaveDialogRequest {
|
||||
|
||||
message ShowSaveDialogOptions {
|
||||
optional string default_path = 1;
|
||||
// A map of file types to extensions, e.g
|
||||
// "Text Files": { "extensions": ["txt", "md"] }
|
||||
map<string, FileExtensionList> filters = 2;
|
||||
}
|
||||
|
||||
@@ -88,5 +94,42 @@ message FileExtensionList {
|
||||
}
|
||||
|
||||
message ShowSaveDialogResponse {
|
||||
// If the user cancelled the dialog, this will be empty.
|
||||
optional string selected_path = 1;
|
||||
}
|
||||
|
||||
message ShowInputBoxRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string title = 2;
|
||||
optional string prompt = 3;
|
||||
optional string value = 4;
|
||||
}
|
||||
|
||||
message ShowInputBoxResponse {
|
||||
optional string response = 1;
|
||||
}
|
||||
|
||||
message OpenFileRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
}
|
||||
|
||||
message OpenFileResponse {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
message GetVisibleTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetVisibleTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with workspaces/projects.
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
// Saves an open document if it's dirty
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (cline.Empty);
|
||||
// Saves an open document if it's open in the editor and has unsaved changes.
|
||||
// Returns true if the document was saved, returns false if the document was not found, or did not
|
||||
// need to be saved.
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (SaveOpenDocumentIfDirtyResponse);
|
||||
// Get diagnostics from the workspace.
|
||||
rpc getDiagnostics(GetDiagnosticsRequest) returns (GetDiagnosticsResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -28,6 +32,46 @@ message GetWorkspacePathsResponse {
|
||||
}
|
||||
|
||||
message SaveOpenDocumentIfDirtyRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
optional string file_path = 2;
|
||||
}
|
||||
message SaveOpenDocumentIfDirtyResponse {
|
||||
// Returns true if the document was saved.
|
||||
optional bool was_saved = 1;
|
||||
}
|
||||
|
||||
message GetDiagnosticsRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
}
|
||||
|
||||
message GetDiagnosticsResponse {
|
||||
repeated FileDiagnostics file_diagnostics = 1;
|
||||
}
|
||||
|
||||
message FileDiagnostics {
|
||||
string file_path = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
string message = 1;
|
||||
DiagnosticRange range = 2;
|
||||
DiagnosticSeverity severity = 3;
|
||||
optional string source = 4;
|
||||
}
|
||||
|
||||
message DiagnosticRange {
|
||||
DiagnosticPosition start = 1;
|
||||
DiagnosticPosition end = 2;
|
||||
}
|
||||
|
||||
message DiagnosticPosition {
|
||||
int32 line = 1;
|
||||
int32 character = 2;
|
||||
}
|
||||
|
||||
enum DiagnosticSeverity {
|
||||
DIAGNOSTIC_ERROR = 0;
|
||||
DIAGNOSTIC_WARNING = 1;
|
||||
DIAGNOSTIC_INFORMATION = 2;
|
||||
DIAGNOSTIC_HINT = 3;
|
||||
}
|
||||
|
||||
+55
-3
@@ -7,8 +7,10 @@ import { globby } from "globby"
|
||||
import { createRequire } from "module"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { rmrf } from "./file-utils.mjs"
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
|
||||
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
@@ -34,9 +36,14 @@ const TS_PROTO_OPTIONS = [
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
|
||||
|
||||
await cleanup()
|
||||
await compileProtos()
|
||||
await checkProtos()
|
||||
await generateProtoBusSetup()
|
||||
await generateHostBridgeClient()
|
||||
}
|
||||
async function compileProtos() {
|
||||
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
|
||||
|
||||
// Check for Apple Silicon compatibility before proceeding
|
||||
checkAppleSiliconCompatibility()
|
||||
@@ -180,6 +187,51 @@ function checkAppleSiliconCompatibility() {
|
||||
}
|
||||
}
|
||||
|
||||
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
|
||||
|
||||
async function checkProtos() {
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
const int64Fields = []
|
||||
|
||||
for (const [packageName, packageDef] of Object.entries(proto)) {
|
||||
for (const [messageName, def] of Object.entries(packageDef)) {
|
||||
// Skip service definitions
|
||||
if (def && typeof def === "object" && "service" in def) {
|
||||
continue
|
||||
}
|
||||
// Check message fields
|
||||
if (def && def.type && def.type.field) {
|
||||
for (const field of def.type.field) {
|
||||
if (int64TypeNames.includes(field.type)) {
|
||||
const name = `${packageName}.${messageName}.${field.name}`
|
||||
int64Fields.push({
|
||||
name: name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (int64Fields.length > 0) {
|
||||
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
|
||||
for (const field of int64Fields) {
|
||||
const typeNames = {
|
||||
TYPE_INT64: "int64",
|
||||
TYPE_UINT64: "uint64",
|
||||
TYPE_SINT64: "sint64",
|
||||
TYPE_FIXED64: "fixed64",
|
||||
TYPE_SFIXED64: "sfixed64",
|
||||
}
|
||||
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
|
||||
}
|
||||
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
|
||||
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
|
||||
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(s) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(s)
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-g
|
||||
/**
|
||||
* Main function to generate the host bridge client
|
||||
*/
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const { hostServices } = await loadServicesFromProtoDescriptor()
|
||||
|
||||
await generateTypesFile(hostServices)
|
||||
@@ -234,8 +234,10 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}")
|
||||
${methods}`
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import { loadServicesFromProtoDescriptor, getFqn } from "./proto-utils.mjs"
|
||||
const WEBVIEW_CLIENTS_FILE = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
const VSCODE_SERVICES_FILE = path.resolve("src/generated/hosts/vscode/protobus-services.ts")
|
||||
const VSCODE_SERVICE_TYPES_FILE = path.resolve("src/generated/hosts/vscode/protobus-service-types.ts")
|
||||
const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/standalone/protobus-server-setup.ts")
|
||||
const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalone/protobus-server-setup.ts")
|
||||
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const { protobusServices } = await loadServicesFromProtoDescriptor()
|
||||
await generateWebviewProtobusClients(protobusServices)
|
||||
await generateVscodeServiceTypes(protobusServices)
|
||||
@@ -40,11 +40,11 @@ async function generateWebviewProtobusClients(protobusServices) {
|
||||
}
|
||||
if (!rpc.responseStream) {
|
||||
rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest("${rpcName}", request)
|
||||
return this.makeUnaryRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON)
|
||||
}`)
|
||||
} else {
|
||||
rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void {
|
||||
return this.makeStreamingRequest("${rpcName}", request, callbacks)
|
||||
return this.makeStreamingRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON, callbacks)
|
||||
}`)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ async function generateVscodeProtobusServers(protobusServices) {
|
||||
const domain = getDomainName(serviceName)
|
||||
const dir = getDirName(serviceName)
|
||||
imports.push(`// ${domain} Service`)
|
||||
servers.push(`export const ${serviceName}Handlers: serviceTypes.${serviceName}Handlers = {`)
|
||||
servers.push(`const ${serviceName}Handlers: serviceTypes.${serviceName}Handlers = {`)
|
||||
for (const [rpcName, _rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
servers.push(` ${rpcName}: ${rpcName},`)
|
||||
@@ -205,4 +205,10 @@ function getDirName(serviceName) {
|
||||
return domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
}
|
||||
|
||||
main()
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
@@ -23,11 +23,15 @@ export function getFqn(name) {
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
export async function loadProtoDescriptorSet() {
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
return grpc.loadPackageDefinition(packageDefinition)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
|
||||
Regular → Executable
+28
-7
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiConfiguration, ModelInfo } from "../shared/api"
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "../shared/api"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
import { AwsBedrockHandler } from "./providers/bedrock"
|
||||
import { OpenRouterHandler } from "./providers/openrouter"
|
||||
@@ -29,8 +29,10 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { Mode } from "../shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
import { BasetenHandler } from "./providers/baseten"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -96,7 +98,7 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
taskId: options.taskId,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
case "openai":
|
||||
return new OpenAiHandler({
|
||||
@@ -111,6 +113,7 @@ function createHandlerForProvider(
|
||||
case "ollama":
|
||||
return new OllamaHandler({
|
||||
ollamaBaseUrl: options.ollamaBaseUrl,
|
||||
ollamaApiKey: options.ollamaApiKey,
|
||||
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
|
||||
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
@@ -129,7 +132,7 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
taskId: options.taskId,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
case "openai-native":
|
||||
return new OpenAiNativeHandler({
|
||||
@@ -166,7 +169,8 @@ function createHandlerForProvider(
|
||||
case "qwen":
|
||||
return new QwenHandler({
|
||||
qwenApiKey: options.qwenApiKey,
|
||||
qwenApiLine: options.qwenApiLine,
|
||||
qwenApiLine:
|
||||
options.qwenApiLine === QwenApiRegions.INTERNATIONAL ? QwenApiRegions.INTERNATIONAL : QwenApiRegions.CHINA,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
@@ -189,7 +193,7 @@ function createHandlerForProvider(
|
||||
case "cline":
|
||||
return new ClineHandler({
|
||||
clineAccountId: options.clineAccountId,
|
||||
taskId: options.taskId,
|
||||
ulid: options.ulid,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
@@ -206,7 +210,7 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
|
||||
taskId: options.taskId,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
case "moonshot":
|
||||
return new MoonshotHandler({
|
||||
@@ -255,6 +259,13 @@ function createHandlerForProvider(
|
||||
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "baseten":
|
||||
return new BasetenHandler({
|
||||
basetenApiKey: options.basetenApiKey,
|
||||
basetenModelId: mode === "plan" ? options.planModeBasetenModelId : options.actModeBasetenModelId,
|
||||
basetenModelInfo: mode === "plan" ? options.planModeBasetenModelInfo : options.actModeBasetenModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
@@ -263,6 +274,8 @@ function createHandlerForProvider(
|
||||
sapAiResourceGroup: options.sapAiResourceGroup,
|
||||
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler({
|
||||
@@ -271,6 +284,14 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "huawei-cloud-maas":
|
||||
return new HuaweiCloudMaaSHandler({
|
||||
huaweiCloudMaasApiKey: options.huaweiCloudMaasApiKey,
|
||||
huaweiCloudMaasModelId:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelId : options.actModeHuaweiCloudMaasModelId,
|
||||
huaweiCloudMaasModelInfo:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
|
||||
@@ -612,101 +612,102 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// TODO: Re-enable or remove these tests.
|
||||
// describe("getModelId", () => {
|
||||
// it("should return raw model ID for custom models", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// })
|
||||
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// it("should not encode custom model IDs with slashes", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "my-namespace/my-custom-model",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal("my-namespace/my-custom-model")
|
||||
modelId.should.not.match(/%2F/)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal("my-namespace/my-custom-model")
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// })
|
||||
|
||||
it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
const crossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "us-west-2",
|
||||
}
|
||||
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
// const crossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "us-west-2",
|
||||
// }
|
||||
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
|
||||
const modelId = await crossRegionHandler.getModelId()
|
||||
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await crossRegionHandler.getModelId()
|
||||
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply EU cross-region prefix", async () => {
|
||||
const euOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "eu-central-1",
|
||||
}
|
||||
const euHandler = new AwsBedrockHandler(euOptions)
|
||||
// it("should apply EU cross-region prefix", async () => {
|
||||
// const euOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "eu-central-1",
|
||||
// }
|
||||
// const euHandler = new AwsBedrockHandler(euOptions)
|
||||
|
||||
const modelId = await euHandler.getModelId()
|
||||
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await euHandler.getModelId()
|
||||
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply APAC cross-region prefix", async () => {
|
||||
const apacOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
// it("should apply APAC cross-region prefix", async () => {
|
||||
// const apacOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "ap-northeast-1",
|
||||
// }
|
||||
// const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
|
||||
const modelId = await apacHandler.getModelId()
|
||||
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await apacHandler.getModelId()
|
||||
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
// const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
// awsUseCrossRegionInference: true,
|
||||
// }
|
||||
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
|
||||
const modelId = await customCrossRegionHandler.getModelId()
|
||||
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
})
|
||||
// const modelId = await customCrossRegionHandler.getModelId()
|
||||
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
// })
|
||||
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
// it("should handle UltraThink model ARN correctly", async () => {
|
||||
// const ultraThinkOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
// }
|
||||
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
const modelId = await ultraThinkHandler.getModelId()
|
||||
// Should return the raw ARN without any encoding
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
modelId.should.not.match(/%2F/)
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
// const modelId = await ultraThinkHandler.getModelId()
|
||||
// // Should return the raw ARN without any encoding
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// modelId.should.not.match(/%3A/)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -55,6 +55,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-haiku-20240307": {
|
||||
/*
|
||||
@@ -122,6 +123,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { BasetenModelId, ModelInfo, basetenDefaultModelId, basetenModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface BasetenHandlerOptions {
|
||||
basetenApiKey?: string
|
||||
basetenModelId?: string
|
||||
basetenModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
export class BasetenHandler implements ApiHandler {
|
||||
private options: BasetenHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: BasetenHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.basetenApiKey) {
|
||||
throw new Error("Baseten API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Baseten client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal max_tokens based on model capabilities
|
||||
*/
|
||||
private getOptimalMaxTokens(model: { id: BasetenModelId; info: ModelInfo }): number {
|
||||
// Use model-specific max tokens if available
|
||||
if (model.info.maxTokens && model.info.maxTokens > 0) {
|
||||
return model.info.maxTokens
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 8192
|
||||
}
|
||||
|
||||
getModel(): { id: BasetenModelId; info: ModelInfo } {
|
||||
// First priority: basetenModelId and basetenModelInfo
|
||||
const basetenModelId = this.options.basetenModelId
|
||||
const basetenModelInfo = this.options.basetenModelInfo
|
||||
if (basetenModelId && basetenModelInfo) {
|
||||
return { id: basetenModelId as BasetenModelId, info: basetenModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: basetenModelId with static model info
|
||||
if (basetenModelId && basetenModelId in basetenModels) {
|
||||
const id = basetenModelId as BasetenModelId
|
||||
return { id, info: basetenModels[id] }
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
if (apiModelId && apiModelId in basetenModels) {
|
||||
const id = apiModelId as BasetenModelId
|
||||
return { id, info: basetenModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: basetenDefaultModelId,
|
||||
info: basetenModels[basetenDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private async *yieldUsage(modelInfo: ModelInfo, usage: any): ApiStream {
|
||||
if (usage.prompt_tokens || usage.completion_tokens) {
|
||||
const cost = calculateApiCostOpenAI(modelInfo, usage.prompt_tokens || 0, usage.completion_tokens || 0)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: cost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const maxTokens = this.getOptimalMaxTokens(model)
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let didOutputUsage = false
|
||||
|
||||
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
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information - only output once
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
// Baseten models support tools via OpenAI-compatible API
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,11 @@ export class CerebrasHandler implements ApiHandler {
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
@withRetry({
|
||||
maxRetries: 6, // More retries to be patient with rate limits
|
||||
baseDelay: 5000, // Start with 5 second delay
|
||||
maxDelay: 60000, // Allow up to 60 second delays to respect rate limits
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
@@ -102,6 +106,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
})
|
||||
|
||||
// Handle streaming response
|
||||
@@ -169,15 +174,39 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Enhanced error handling for Cerebras API
|
||||
if (error?.status === 429 || error?.code === "rate_limit_exceeded") {
|
||||
// Rate limit error - will be handled by retry decorator with patient backoff
|
||||
const limits = this.getRateLimits()
|
||||
throw new Error(`Cerebras API rate limit exceeded.`)
|
||||
} else if (error?.status === 401) {
|
||||
throw new Error("Cerebras API authentication failed. Please check your API key.")
|
||||
} else if (error?.status === 403) {
|
||||
throw new Error("Cerebras API access denied. Please check your API key permissions.")
|
||||
} else if (error?.status >= 500) {
|
||||
// Server errors - retryable
|
||||
throw new Error(`Cerebras API server error (${error.status}): ${error.message || "Unknown server error"}`)
|
||||
} else if (error?.status === 400) {
|
||||
// Client errors - not retryable
|
||||
throw new Error(`Cerebras API bad request: ${error.message || "Invalid request parameters"}`)
|
||||
}
|
||||
|
||||
// Re-throw original error for other cases
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
const originalModelId = this.options.apiModelId
|
||||
let apiModelId = originalModelId
|
||||
if (originalModelId === "qwen-3-coder-480b-free") {
|
||||
apiModelId = "qwen-3-coder-480b"
|
||||
return { id: apiModelId, info: cerebrasModels[originalModelId as CerebrasModelId] }
|
||||
}
|
||||
|
||||
if (originalModelId && originalModelId in cerebrasModels) {
|
||||
const id = originalModelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
}
|
||||
return {
|
||||
@@ -186,6 +215,35 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate limit information for the current model
|
||||
*
|
||||
* These limits are used for informational purposes and to calculate appropriate
|
||||
* retry delays. Since Cerebras inference is extremely fast, users hit these limits
|
||||
* quickly, so we need to be patient with retries to maximize usage efficiency.
|
||||
*
|
||||
* @returns Rate limit configuration for the model
|
||||
*/
|
||||
private getRateLimits(): { requestsPerMinute: number; tokensPerMinute: number } {
|
||||
const modelId = this.getModel().id
|
||||
|
||||
switch (modelId) {
|
||||
case "qwen-3-coder-480b":
|
||||
case "qwen-3-coder-480b-free":
|
||||
return { requestsPerMinute: 10, tokensPerMinute: 150_000 }
|
||||
case "qwen-3-235b-a22b-instruct-2507":
|
||||
case "qwen-3-235b-a22b-thinking-2507":
|
||||
return { requestsPerMinute: 30, tokensPerMinute: 60_000 }
|
||||
case "llama-3.3-70b":
|
||||
case "gpt-oss-120b":
|
||||
case "qwen-3-32b":
|
||||
return { requestsPerMinute: 30, tokensPerMinute: 64_000 }
|
||||
default:
|
||||
// Default rate limits for unknown models
|
||||
return { requestsPerMinute: 30, tokensPerMinute: 60_000 }
|
||||
}
|
||||
}
|
||||
|
||||
private calculateCost({ inputTokens, outputTokens }: { inputTokens: number; outputTokens: number }): number {
|
||||
const model = this.getModel()
|
||||
const inputPrice = model.info.inputPrice || 0
|
||||
|
||||
+23
-47
@@ -15,7 +15,7 @@ import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
|
||||
interface ClineHandlerOptions {
|
||||
taskId?: string
|
||||
ulid?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
openRouterProviderSorting?: string
|
||||
@@ -51,7 +51,7 @@ export class ClineHandler implements ApiHandler {
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": this.options.taskId || "",
|
||||
"X-Task-ID": this.options.ulid || "",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
})
|
||||
@@ -133,7 +133,6 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
const modelId = this.getModel().id
|
||||
|
||||
// const provider = modelId.split("/")[0]
|
||||
// // If provider is x-ai, set totalCost to 0 (we're doing a promo)
|
||||
@@ -141,27 +140,14 @@ export class ClineHandler implements ApiHandler {
|
||||
// totalCost = 0
|
||||
// }
|
||||
|
||||
if (modelId.includes("gemini")) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens:
|
||||
(chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost,
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost,
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: totalCost,
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
@@ -186,36 +172,26 @@ export class ClineHandler implements ApiHandler {
|
||||
try {
|
||||
// TODO: replace this with firebase auth
|
||||
// TODO: use global API Host
|
||||
|
||||
const clineAccountAuthToken = await this._authService.getAuthToken()
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
|
||||
}
|
||||
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.options.clineAccountId}`,
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
},
|
||||
timeout: 15_000, // this request hangs sometimes
|
||||
})
|
||||
|
||||
const generation = response.data
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Mock for @google/genai module to avoid ESM compatibility issues in tests
|
||||
|
||||
export class GoogleGenAI {
|
||||
constructor(options: any) {
|
||||
// Mock constructor
|
||||
}
|
||||
|
||||
models = {
|
||||
generateContentStream: async (params: any) => {
|
||||
// Mock implementation that returns an async iterator
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
text: "Mock response",
|
||||
candidates: [],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
thoughtsTokenCount: 0,
|
||||
cachedContentTokenCount: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
countTokens: async (params: any) => {
|
||||
// Mock token counting
|
||||
return {
|
||||
totalTokens: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Export mock types
|
||||
export interface GenerateContentConfig {
|
||||
httpOptions?: any
|
||||
systemInstruction?: string
|
||||
temperature?: number
|
||||
thinkingConfig?: any
|
||||
}
|
||||
|
||||
export interface GenerateContentResponseUsageMetadata {
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
thoughtsTokenCount?: number
|
||||
cachedContentTokenCount?: number
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
thought?: boolean
|
||||
text?: string
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
@@ -20,7 +20,7 @@ interface GeminiHandlerOptions {
|
||||
geminiBaseUrl?: string
|
||||
thinkingBudgetTokens?: number
|
||||
apiModelId?: string
|
||||
taskId?: string
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ interface GeminiHandlerOptions {
|
||||
*
|
||||
* Key features:
|
||||
* - One cache per task: Creates a single cache per task and reuses it for subsequent turns
|
||||
* - Stable cache keys: Uses taskId as a stable identifier for caches
|
||||
* - Stable cache keys: Uses ulid as a stable identifier for caches
|
||||
* - Efficient cache updates: Only updates caches when there's new content to add
|
||||
* - Split cost accounting: Separates immediate costs from ongoing cache storage costs
|
||||
*
|
||||
@@ -255,8 +255,8 @@ export class GeminiHandler implements ApiHandler {
|
||||
const throughputTokensPerSecSdk =
|
||||
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
|
||||
|
||||
if (this.options.taskId) {
|
||||
telemetryService.captureGeminiApiPerformance(this.options.taskId, modelId, {
|
||||
if (this.options.ulid) {
|
||||
telemetryService.captureGeminiApiPerformance(this.options.ulid, modelId, {
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
@@ -269,7 +269,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
|
||||
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { huaweiCloudMaasDefaultModelId, HuaweiCloudMaasModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
interface HuaweiCloudMaaSHandlerOptions {
|
||||
huaweiCloudMaasApiKey?: string
|
||||
huaweiCloudMaasModelId?: string
|
||||
huaweiCloudMaasModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
private options: HuaweiCloudMaaSHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
constructor(options: HuaweiCloudMaaSHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huaweiCloudMaasApiKey) {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: HuaweiCloudMaasModelId; info: ModelInfo } {
|
||||
// First priority: huaweiCloudMaasModelId and huaweiCloudMaasModelInfo (like Groq does)
|
||||
const huaweiCloudMaasModelId = this.options.huaweiCloudMaasModelId
|
||||
const huaweiCloudMaasModelInfo = this.options.huaweiCloudMaasModelInfo
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelInfo) {
|
||||
return { id: huaweiCloudMaasModelId as HuaweiCloudMaasModelId, info: huaweiCloudMaasModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: huaweiCloudMaasModelId with static model info
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelId in huaweiCloudMaasModels) {
|
||||
const id = huaweiCloudMaasModelId as HuaweiCloudMaasModelId
|
||||
return { id, info: huaweiCloudMaasModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: huaweiCloudMaasDefaultModelId,
|
||||
info: huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
if (reasoning || delta.content.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + delta.content
|
||||
} else if (!reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning output
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
|
||||
if (reasoningContent.trim()) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reasoning is complete
|
||||
if (reasoning?.includes("</think>")) {
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
// Store usage information for later output
|
||||
if (chunk.usage) {
|
||||
finalUsage = chunk.usage
|
||||
}
|
||||
|
||||
// Output usage when stream is finished
|
||||
if (!didOutputUsage && chunk.choices?.[0]?.finish_reason) {
|
||||
if (finalUsage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: finalUsage.prompt_tokens || 0,
|
||||
outputTokens: finalUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
}
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
-28
@@ -13,12 +13,32 @@ interface LiteLlmHandlerOptions {
|
||||
liteLlmModelInfo?: LiteLLMModelInfo
|
||||
thinkingBudgetTokens?: number
|
||||
liteLlmUsePromptCache?: boolean
|
||||
taskId?: string
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
interface LiteLlmModelInfoResponse {
|
||||
data: Array<{
|
||||
model_name: string
|
||||
litellm_params: {
|
||||
model: string
|
||||
[key: string]: any
|
||||
}
|
||||
model_info: {
|
||||
input_cost_per_token: number
|
||||
output_cost_per_token: number
|
||||
cache_creation_input_token_cost?: number
|
||||
cache_read_input_token_cost?: number
|
||||
[key: string]: any
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export class LiteLlmHandler implements ApiHandler {
|
||||
private options: LiteLlmHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
private modelInfoCache: LiteLlmModelInfoResponse | undefined
|
||||
private modelInfoCacheTimestamp: number = 0
|
||||
private readonly modelInfoCacheTTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
constructor(options: LiteLlmHandlerOptions) {
|
||||
this.options = options
|
||||
@@ -41,35 +61,112 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
private async fetchModelInfo(): Promise<LiteLlmModelInfoResponse | undefined> {
|
||||
// Check if cache is still valid
|
||||
const now = Date.now()
|
||||
if (this.modelInfoCache && now - this.modelInfoCacheTimestamp < this.modelInfoCacheTTL) {
|
||||
return this.modelInfoCache
|
||||
}
|
||||
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
// Handle base URLs that already include /v1 to avoid double /v1/v1/
|
||||
const baseUrl = client.baseURL.endsWith("/v1") ? client.baseURL : `${client.baseURL}/v1`
|
||||
const url = `${baseUrl}/model/info`
|
||||
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.options.liteLlmApiKey}`,
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": this.options.liteLlmApiKey || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
usage: {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
console.warn("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${this.options.liteLlmApiKey || ""}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.warn("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching LiteLLM model info:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async getModelCostInfo(publicModelName: string): Promise<{
|
||||
inputCostPerToken: number
|
||||
outputCostPerToken: number
|
||||
cacheCreationCostPerToken?: number
|
||||
cacheReadCostPerToken?: number
|
||||
}> {
|
||||
try {
|
||||
const modelInfo = await this.fetchModelInfo()
|
||||
|
||||
if (modelInfo?.data) {
|
||||
// Find the model by public name
|
||||
const matchingModel = modelInfo.data.find((model) => model.model_name === publicModelName)
|
||||
|
||||
if (matchingModel?.model_info) {
|
||||
return {
|
||||
inputCostPerToken: matchingModel.model_info.input_cost_per_token || 0,
|
||||
outputCostPerToken: matchingModel.model_info.output_cost_per_token || 0,
|
||||
cacheCreationCostPerToken: matchingModel.model_info.cache_creation_input_token_cost,
|
||||
cacheReadCostPerToken: matchingModel.model_info.cache_read_input_token_cost,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
return {
|
||||
inputCostPerToken: 0,
|
||||
outputCostPerToken: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async calculateCost(
|
||||
prompt_tokens: number,
|
||||
completion_tokens: number,
|
||||
cache_creation_tokens?: number,
|
||||
cache_read_tokens?: number,
|
||||
): Promise<number | undefined> {
|
||||
const publicModelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
|
||||
try {
|
||||
const costInfo = await this.getModelCostInfo(publicModelId)
|
||||
|
||||
// Calculate costs for different token types
|
||||
const inputCost = Math.max(0, prompt_tokens - (cache_read_tokens || 0)) * costInfo.inputCostPerToken
|
||||
const outputCost = completion_tokens * costInfo.outputCostPerToken
|
||||
const cacheCreationCost = (cache_creation_tokens || 0) * (costInfo.cacheCreationCostPerToken || 0)
|
||||
const cacheReadCost = (cache_read_tokens || 0) * (costInfo.cacheReadCostPerToken || 0)
|
||||
|
||||
const totalCost = inputCost + outputCost + cacheCreationCost + cacheReadCost
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
@@ -133,12 +230,9 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}` }), // Add session ID for LiteLLM tracking
|
||||
...(this.options.ulid && { litellm_session_id: `cline-${this.options.ulid}` }), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
const outputCost = (await this.calculateCost(0, 1e6)) || 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
@@ -165,9 +259,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
const usage = chunk.usage as {
|
||||
@@ -182,6 +273,15 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0
|
||||
|
||||
// Calculate cost using the actual token usage including cache tokens
|
||||
const totalCost =
|
||||
(await this.calculateCost(
|
||||
usage.prompt_tokens || 0,
|
||||
usage.completion_tokens || 0,
|
||||
cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
|
||||
cacheReadTokens > 0 ? cacheReadTokens : undefined,
|
||||
)) || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Message, Ollama } from "ollama"
|
||||
import { Message, Ollama, Config } from "ollama"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
@@ -8,6 +8,7 @@ import { withRetry } from "../retry"
|
||||
|
||||
interface OllamaHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiKey?: string
|
||||
ollamaModelId?: string
|
||||
ollamaApiOptionsCtxNum?: string
|
||||
requestTimeoutMs?: number
|
||||
@@ -24,7 +25,18 @@ export class OllamaHandler implements ApiHandler {
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl || "http://localhost:11434",
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
if (this.options.ollamaApiKey) {
|
||||
clientOptions.headers = {
|
||||
Authorization: `Bearer ${this.options.ollamaApiKey}`,
|
||||
}
|
||||
}
|
||||
|
||||
this.client = new Ollama(clientOptions)
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Ollama client: ${error.message}`)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,33 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "nectarine-alpha-new-reasoning-effort-2025-07-25":
|
||||
case "gpt-5-2025-08-07":
|
||||
case "gpt-5-mini-2025-08-07":
|
||||
case "gpt-5-nano-2025-08-07":
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
temperature: 1,
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
if (chunk.usage) {
|
||||
// Only last chunk contains usage
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
break
|
||||
default: {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -132,27 +132,14 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
@@ -174,27 +161,14 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
internationalQwenDefaultModelId,
|
||||
MainlandQwenModelId,
|
||||
InternationalQwenModelId,
|
||||
QwenApiRegions,
|
||||
} from "@shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -17,7 +18,7 @@ import { withRetry } from "../retry"
|
||||
|
||||
interface QwenHandlerOptions {
|
||||
qwenApiKey?: string
|
||||
qwenApiLine?: string
|
||||
qwenApiLine?: QwenApiRegions
|
||||
apiModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
@@ -27,7 +28,15 @@ export class QwenHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: QwenHandlerOptions) {
|
||||
this.options = options
|
||||
// Ensure options start with defaults but allow overrides
|
||||
this.options = {
|
||||
qwenApiLine: QwenApiRegions.CHINA,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
private useChinaApi(): boolean {
|
||||
return this.options.qwenApiLine === QwenApiRegions.CHINA
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
@@ -37,10 +46,9 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.qwenApiLine === "china"
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
baseURL: this.useChinaApi()
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -53,7 +61,7 @@ export class QwenHandler implements ApiHandler {
|
||||
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
// Branch based on API line to let poor typescript know what to do
|
||||
if (this.options.qwenApiLine === "china") {
|
||||
if (this.useChinaApi()) {
|
||||
return {
|
||||
id: (modelId as MainlandQwenModelId) ?? mainlandQwenDefaultModelId,
|
||||
info: mainlandQwenModels[modelId as MainlandQwenModelId] ?? mainlandQwenModels[mainlandQwenDefaultModelId],
|
||||
|
||||
@@ -74,7 +74,10 @@ export class RequestyHandler implements ApiHandler {
|
||||
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
|
||||
: { thinking: { type: "disabled" } }
|
||||
const thinkingArgs =
|
||||
model.id.includes("claude-3-7-sonnet") || model.id.includes("claude-sonnet-4") || model.id.includes("claude-opus-4")
|
||||
model.id.includes("claude-3-7-sonnet") ||
|
||||
model.id.includes("claude-sonnet-4") ||
|
||||
model.id.includes("claude-opus-4") ||
|
||||
model.id.includes("claude-opus-4-1")
|
||||
? thinking
|
||||
: {}
|
||||
|
||||
|
||||
+374
-145
@@ -5,6 +5,11 @@ import { ApiHandler } from "../"
|
||||
import { ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import {
|
||||
type Message as BedrockMessage,
|
||||
type ContentBlock as BedrockContentBlock,
|
||||
ConversationRole as BedrockConversationRole,
|
||||
} from "@aws-sdk/client-bedrock-runtime"
|
||||
|
||||
interface SapAiCoreHandlerOptions {
|
||||
sapAiCoreClientId?: string
|
||||
@@ -13,6 +18,7 @@ interface SapAiCoreHandlerOptions {
|
||||
sapAiResourceGroup?: string
|
||||
sapAiCoreBaseUrl?: string
|
||||
apiModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
interface Deployment {
|
||||
@@ -27,6 +33,307 @@ interface Token {
|
||||
token_type: string
|
||||
expires_at: number
|
||||
}
|
||||
|
||||
// Bedrock namespace containing caching-related functions
|
||||
namespace Bedrock {
|
||||
// Define cache point type for AWS Bedrock
|
||||
interface CachePointContentBlock {
|
||||
cachePoint: {
|
||||
type: "default"
|
||||
}
|
||||
}
|
||||
|
||||
// Define types for supported content types
|
||||
type SupportedContentType = "text" | "image" | "thinking"
|
||||
|
||||
interface ContentItem {
|
||||
type: SupportedContentType
|
||||
text?: string
|
||||
source?: {
|
||||
data: string | Buffer | Uint8Array
|
||||
media_type?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares system messages with optional caching support
|
||||
*/
|
||||
export function prepareSystemMessages(systemPrompt: string, enableCaching: boolean): any[] | undefined {
|
||||
if (!systemPrompt) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (enableCaching) {
|
||||
return [{ text: systemPrompt }, { cachePoint: { type: "default" } }]
|
||||
}
|
||||
|
||||
return [{ text: systemPrompt }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies cache control to messages for prompt caching using AWS Bedrock's cachePoint system
|
||||
* AWS Bedrock uses cachePoint objects instead of Anthropic's cache_control approach
|
||||
*/
|
||||
export function applyCacheControlToMessages(
|
||||
messages: BedrockMessage[],
|
||||
lastUserMsgIndex: number,
|
||||
secondLastMsgUserIndex: number,
|
||||
): BedrockMessage[] {
|
||||
return messages.map((message, index) => {
|
||||
// Add cachePoint to the last user message and second-to-last user message
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
// Clone the message to avoid modifying the original
|
||||
const messageWithCache = { ...message }
|
||||
|
||||
if (messageWithCache.content && Array.isArray(messageWithCache.content)) {
|
||||
// Add cachePoint to the end of the content array
|
||||
messageWithCache.content = [
|
||||
...messageWithCache.content,
|
||||
{
|
||||
cachePoint: {
|
||||
type: "default",
|
||||
},
|
||||
} as CachePointContentBlock, // Properly typed cache point for AWS SDK
|
||||
]
|
||||
}
|
||||
|
||||
return messageWithCache
|
||||
}
|
||||
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats messages for models using the Converse API specification
|
||||
* Used by both Anthropic and Nova models to avoid code duplication
|
||||
*/
|
||||
export function formatMessagesForConverseAPI(messages: Anthropic.Messages.MessageParam[]): BedrockMessage[] {
|
||||
return messages.map((message) => {
|
||||
// Determine role (user or assistant)
|
||||
const role = message.role === "user" ? BedrockConversationRole.USER : BedrockConversationRole.ASSISTANT
|
||||
|
||||
// Process content based on type
|
||||
let content: BedrockContentBlock[] = []
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
// Simple text content
|
||||
content = [{ text: message.content }]
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// Convert Anthropic content format to Converse API content format
|
||||
const processedContent = message.content
|
||||
.map((item) => {
|
||||
// Text content
|
||||
if (item.type === "text") {
|
||||
return { text: item.text }
|
||||
}
|
||||
|
||||
// Image content
|
||||
if (item.type === "image") {
|
||||
return processImageContent(item)
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is BedrockContentBlock => item !== null)
|
||||
|
||||
content = processedContent
|
||||
}
|
||||
|
||||
// Return formatted message
|
||||
return {
|
||||
role,
|
||||
content,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes image content with proper error handling and user notification
|
||||
*/
|
||||
function processImageContent(item: any): BedrockContentBlock | null {
|
||||
let imageData: Uint8Array
|
||||
let format: "png" | "jpeg" | "gif" | "webp" = "jpeg" // default format
|
||||
|
||||
// Extract format from media_type if available
|
||||
if (item.source.media_type) {
|
||||
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
|
||||
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
|
||||
if (formatMatch && formatMatch[1]) {
|
||||
const extractedFormat = formatMatch[1]
|
||||
// Ensure format is one of the allowed values
|
||||
if (["png", "jpeg", "gif", "webp"].includes(extractedFormat)) {
|
||||
format = extractedFormat as "png" | "jpeg" | "gif" | "webp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get image data with improved error handling
|
||||
try {
|
||||
if (typeof item.source.data === "string") {
|
||||
// Handle base64 encoded data
|
||||
const base64Data = item.source.data.replace(/^data:image\/\w+;base64,/, "")
|
||||
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
|
||||
} else if (item.source.data && typeof item.source.data === "object") {
|
||||
// Try to convert to Uint8Array
|
||||
imageData = new Uint8Array(Buffer.from(item.source.data as Buffer | Uint8Array))
|
||||
} else {
|
||||
throw new Error("Unsupported image data format")
|
||||
}
|
||||
|
||||
return {
|
||||
image: {
|
||||
format,
|
||||
source: {
|
||||
bytes: imageData,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
text: `[ERROR: Failed to process image - ${error instanceof Error ? error.message : "Unknown error"}]`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini namespace containing caching-related functions and types
|
||||
namespace Gemini {
|
||||
/**
|
||||
* Process Gemini streaming response with enhanced thinking content support and caching awareness
|
||||
*/
|
||||
export function processStreamChunk(data: any): {
|
||||
text?: string
|
||||
reasoning?: string
|
||||
usageMetadata?: {
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
thoughtsTokenCount?: number
|
||||
cachedContentTokenCount?: number
|
||||
}
|
||||
} {
|
||||
const result: ReturnType<typeof processStreamChunk> = {}
|
||||
|
||||
// Handle thinking content from Gemini's response
|
||||
const candidateForThoughts = data?.candidates?.[0]
|
||||
const partsForThoughts = candidateForThoughts?.content?.parts
|
||||
let thoughts = ""
|
||||
|
||||
if (partsForThoughts) {
|
||||
for (const part of partsForThoughts) {
|
||||
const { thought, text } = part
|
||||
if (thought && text) {
|
||||
thoughts += text + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (thoughts.trim() !== "") {
|
||||
result.reasoning = thoughts.trim()
|
||||
}
|
||||
|
||||
// Handle regular text content
|
||||
if (data.text) {
|
||||
result.text = data.text
|
||||
}
|
||||
|
||||
// Handle content parts for non-thought text
|
||||
if (data.candidates && data.candidates[0]?.content?.parts) {
|
||||
let nonThoughtText = ""
|
||||
for (const part of data.candidates[0].content.parts) {
|
||||
if (part.text && !part.thought) {
|
||||
nonThoughtText += part.text
|
||||
}
|
||||
}
|
||||
if (nonThoughtText && !result.text) {
|
||||
result.text = nonThoughtText
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage metadata with caching support
|
||||
if (data.usageMetadata) {
|
||||
result.usageMetadata = {
|
||||
promptTokenCount: data.usageMetadata.promptTokenCount,
|
||||
candidatesTokenCount: data.usageMetadata.candidatesTokenCount,
|
||||
thoughtsTokenCount: data.usageMetadata.thoughtsTokenCount,
|
||||
cachedContentTokenCount: data.usageMetadata.cachedContentTokenCount,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam) {
|
||||
const role = message.role === "assistant" ? "model" : "user"
|
||||
const parts = []
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
parts.push({ text: message.content })
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push({ text: block.text })
|
||||
} else if (block.type === "image") {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: block.source.media_type,
|
||||
data: block.source.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { role, parts }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare Gemini request payload with thinking configuration and implicit caching support
|
||||
*/
|
||||
export function prepareRequestPayload(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
thinkingBudgetTokens?: number,
|
||||
): any {
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
const payload = {
|
||||
contents,
|
||||
systemInstruction: {
|
||||
parts: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
generationConfig: {
|
||||
maxOutputTokens: model.info.maxTokens,
|
||||
temperature: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
// Add thinking config if the model supports it and budget is provided
|
||||
const thinkingBudget = thinkingBudgetTokens ?? 0
|
||||
const maxBudget = model.info.thinkingConfig?.maxBudget ?? 0
|
||||
|
||||
if (thinkingBudget > 0 && model.info.thinkingConfig) {
|
||||
// Add thinking configuration to the payload
|
||||
;(payload as any).thinkingConfig = {
|
||||
thinkingBudget: thinkingBudget,
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
export class SapAiCoreHandler implements ApiHandler {
|
||||
private options: SapAiCoreHandlerOptions
|
||||
private token?: Token
|
||||
@@ -142,7 +449,20 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
"anthropic--claude-3-opus",
|
||||
]
|
||||
|
||||
const openAIModels = ["gpt-4o", "gpt-4", "gpt-4o-mini", "o1", "gpt-4.1", "gpt-4.1-nano", "o3-mini", "o3", "o4-mini"]
|
||||
const openAIModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4",
|
||||
"gpt-4o-mini",
|
||||
"o1",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
"gpt-5",
|
||||
"gpt-5-nano",
|
||||
"gpt-5-mini",
|
||||
"o3-mini",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
]
|
||||
|
||||
const geminiModels = ["gemini-2.5-flash", "gemini-2.5-pro"]
|
||||
|
||||
@@ -151,21 +471,47 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
if (anthropicModels.includes(model.id)) {
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/invoke-with-response-stream`
|
||||
|
||||
// Format messages for Converse API. Note that the Invoke API has
|
||||
// the same format for messages as the Converse API.
|
||||
const formattedMessages = Bedrock.formatMessagesForConverseAPI(messages)
|
||||
|
||||
// Get message indices for caching
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
if (
|
||||
model.id === "anthropic--claude-4-sonnet" ||
|
||||
model.id === "anthropic--claude-4-opus" ||
|
||||
model.id === "anthropic--claude-3.7-sonnet"
|
||||
) {
|
||||
// Use converse-stream endpoint with caching support
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/converse-stream`
|
||||
|
||||
// Apply caching controls to messages (enabled by default)
|
||||
const messagesWithCache = Bedrock.applyCacheControlToMessages(
|
||||
formattedMessages,
|
||||
lastUserMsgIndex,
|
||||
secondLastMsgUserIndex,
|
||||
)
|
||||
|
||||
// Prepare system message with caching support (enabled by default)
|
||||
const systemMessages = Bedrock.prepareSystemMessages(systemPrompt, true)
|
||||
|
||||
payload = {
|
||||
inferenceConfig: {
|
||||
maxTokens: model.info.maxTokens,
|
||||
temperature: 0.0,
|
||||
},
|
||||
system: systemPrompt ? [{ text: systemPrompt }] : undefined,
|
||||
messages: this.formatAnthropicMessages(messages),
|
||||
system: systemMessages,
|
||||
messages: messagesWithCache,
|
||||
}
|
||||
} else {
|
||||
// Use invoke-with-response-stream endpoint
|
||||
// TODO: add caching support using Anthropic-native cache_control blocks
|
||||
payload = {
|
||||
max_tokens: model.info.maxTokens,
|
||||
system: systemPrompt,
|
||||
@@ -191,7 +537,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
if (["o1", "o3-mini", "o3", "o4-mini"].includes(model.id)) {
|
||||
if (["o1", "o3-mini", "o3", "o4-mini", "gpt-5", "gpt-5-nano", "gpt-5-mini"].includes(model.id)) {
|
||||
delete payload.max_tokens
|
||||
delete payload.temperature
|
||||
}
|
||||
@@ -202,7 +548,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
} else if (geminiModels.includes(model.id)) {
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
|
||||
payload = this.convertToGeminiFormat(systemPrompt, messages)
|
||||
payload = Gemini.prepareRequestPayload(systemPrompt, messages, model, this.options.thinkingBudgetTokens)
|
||||
} else {
|
||||
throw new Error(`Unsupported model: ${model.id}`)
|
||||
}
|
||||
@@ -359,9 +705,17 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
// Handle metadata (token usage)
|
||||
if (data.metadata?.usage) {
|
||||
const inputTokens = data.metadata.usage.inputTokens || 0
|
||||
let inputTokens = data.metadata.usage.inputTokens || 0
|
||||
const outputTokens = data.metadata.usage.outputTokens || 0
|
||||
|
||||
// calibrate input token
|
||||
const totalTokens = data.metadata.usage.totalTokens || 0
|
||||
const cacheReadInputTokens = data.metadata.usage.cacheReadInputTokens || 0
|
||||
const cacheWriteOutputTokens = data.metadata.usage.cacheWriteOutputTokens || 0
|
||||
if (inputTokens + outputTokens + cacheReadInputTokens + cacheWriteOutputTokens !== totalTokens) {
|
||||
inputTokens = totalTokens - outputTokens - cacheReadInputTokens - cacheWriteOutputTokens
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
@@ -493,50 +847,31 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const jsonData = line.slice(6)
|
||||
try {
|
||||
const data = JSON.parse(jsonData)
|
||||
const candidateForThoughts = data?.candidates?.[0]
|
||||
const partsForThoughts = candidateForThoughts?.content?.parts
|
||||
let thoughts = ""
|
||||
|
||||
if (partsForThoughts) {
|
||||
for (const part of partsForThoughts) {
|
||||
const { thought, text } = part
|
||||
if (thought && text) {
|
||||
thoughts += text + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use Gemini namespace to process the chunk
|
||||
const processed = Gemini.processStreamChunk(data)
|
||||
|
||||
if (thoughts.trim() !== "") {
|
||||
// Yield reasoning if present
|
||||
if (processed.reasoning) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: thoughts.trim(),
|
||||
reasoning: processed.reasoning,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.text) {
|
||||
// Yield text if present
|
||||
if (processed.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: data.text,
|
||||
text: processed.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.candidates && data.candidates[0]?.content?.parts) {
|
||||
for (const part of data.candidates[0].content.parts) {
|
||||
if (part.text && !part.thought) {
|
||||
// Only non-thought text
|
||||
yield {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.usageMetadata) {
|
||||
promptTokens = data.usageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = data.usageMetadata.candidatesTokenCount ?? outputTokens
|
||||
thoughtsTokenCount = data.usageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
|
||||
cacheReadTokens = data.usageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
if (processed.usageMetadata) {
|
||||
promptTokens = processed.usageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = processed.usageMetadata.candidatesTokenCount ?? outputTokens
|
||||
thoughtsTokenCount = processed.usageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
|
||||
cacheReadTokens = processed.usageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
@@ -544,6 +879,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -581,111 +917,4 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
return { id: sapAiCoreDefaultModelId, info: sapAiCoreModels[sapAiCoreDefaultModelId] }
|
||||
}
|
||||
|
||||
private getValidImageFormat(mediaType: string): string {
|
||||
const format = mediaType.split("/")[1]?.toLowerCase()
|
||||
const validFormats = ["png", "jpeg", "gif", "webp"]
|
||||
|
||||
if (validFormats.includes(format)) {
|
||||
return format
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`)
|
||||
}
|
||||
|
||||
private convertToGeminiFormat(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]) {
|
||||
const contents = messages.map(this.convertAnthropicMessageToGemini)
|
||||
|
||||
const payload = {
|
||||
contents,
|
||||
systemInstruction: {
|
||||
parts: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
generationConfig: {
|
||||
maxOutputTokens: this.getModel().info.maxTokens,
|
||||
temperature: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
private convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam) {
|
||||
const role = message.role === "assistant" ? "model" : "user"
|
||||
const parts = []
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
parts.push({ text: message.content })
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push({ text: block.text })
|
||||
} else if (block.type === "image") {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: block.source.media_type,
|
||||
data: block.source.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { role, parts }
|
||||
}
|
||||
private formatAnthropicMessages(messages: Anthropic.Messages.MessageParam[]): any[] {
|
||||
return messages.map((m) => {
|
||||
const contentBlocks: any[] = []
|
||||
|
||||
if (typeof m.content === "string") {
|
||||
contentBlocks.push({ text: m.content })
|
||||
} else if (Array.isArray(m.content)) {
|
||||
for (const block of m.content) {
|
||||
if (block.type === "text") {
|
||||
if (!block.text) {
|
||||
throw new Error('Text block is missing the "text" field.')
|
||||
}
|
||||
contentBlocks.push({ text: block.text })
|
||||
} else if (block.type === "image") {
|
||||
if (!block.source) {
|
||||
throw new Error('Image block is missing the "source" field.')
|
||||
}
|
||||
|
||||
const { type, media_type, data } = block.source
|
||||
|
||||
if (!type || !media_type || !data) {
|
||||
throw new Error('Image source must have "type", "media_type", and "data" fields.')
|
||||
}
|
||||
|
||||
if (type !== "base64") {
|
||||
throw new Error(`Unsupported image source type: ${type}. Only "base64" is supported.`)
|
||||
}
|
||||
|
||||
const format = this.getValidImageFormat(media_type)
|
||||
|
||||
contentBlocks.push({
|
||||
image: {
|
||||
format,
|
||||
source: {
|
||||
bytes: data,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported content format.")
|
||||
}
|
||||
|
||||
return {
|
||||
role: m.role,
|
||||
content: contentBlocks,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface VertexHandlerOptions {
|
||||
thinkingBudgetTokens?: number
|
||||
geminiApiKey?: string
|
||||
geminiBaseUrl?: string
|
||||
taskId?: string
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
export class VertexHandler implements ApiHandler {
|
||||
@@ -86,6 +86,7 @@ export class VertexHandler implements ApiHandler {
|
||||
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4-1@20250805":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
|
||||
interface VsCodeLmHandlerOptions {
|
||||
vsCodeLmModelSelector?: any
|
||||
@@ -237,82 +237,40 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
// Check for required dependencies
|
||||
if (!this.client) {
|
||||
console.warn("Cline <Language Model API>: No client available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!this.currentRequestCancellation) {
|
||||
console.warn("Cline <Language Model API>: No cancellation token available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if (!text) {
|
||||
console.debug("Cline <Language Model API>: Empty text provided for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle different input types
|
||||
let tokenCount: number
|
||||
|
||||
if (typeof text === "string") {
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else if (text instanceof vscode.LanguageModelChatMessage) {
|
||||
// For chat messages, ensure we have content
|
||||
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
|
||||
console.debug("Cline <Language Model API>: Empty chat message content")
|
||||
return 0
|
||||
}
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Invalid input type for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate the result
|
||||
if (typeof tokenCount !== "number") {
|
||||
console.warn("Cline <Language Model API>: Non-numeric token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (tokenCount < 0) {
|
||||
console.warn("Cline <Language Model API>: Negative token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
return tokenCount
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof vscode.CancellationError) {
|
||||
console.debug("Cline <Language Model API>: Token counting cancelled by user")
|
||||
return 0
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.warn("Cline <Language Model API>: Token counting failed:", errorMessage)
|
||||
|
||||
// Log additional error details if available
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.debug("Token counting error stack:", error.stack)
|
||||
}
|
||||
|
||||
return 0 // Fallback to prevent stream interruption
|
||||
private extractTextFromMessage(message: vscode.LanguageModelChatMessage): string {
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.filter((part) => part instanceof vscode.LanguageModelTextPart)
|
||||
.map((part) => (part as vscode.LanguageModelTextPart).value)
|
||||
.join("")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(
|
||||
systemPrompt: string,
|
||||
vsCodeLmMessages: vscode.LanguageModelChatMessage[],
|
||||
): Promise<number> {
|
||||
const systemTokens: number = await this.countTokens(systemPrompt)
|
||||
private isClaudeModel(): boolean {
|
||||
return this.client?.family?.startsWith("claude") || false
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
/**
|
||||
* NOTE (intentional trade-off):
|
||||
* We use a coarse chars/4 heuristic here instead of a real tokenizer (e.g., js-tiktoken with o200k_base).
|
||||
* Rationale:
|
||||
* - Avoid pulling multi‑MB rank files and increasing the extension install/download size.
|
||||
* - Eliminate encoder lifecycle/memory concerns in long-running sessions.
|
||||
* Consequences:
|
||||
* - This is not model-accurate and can under/over-estimate tokens, especially with tool/function calls.
|
||||
* - It is “good enough” for budgeting/context checks, and we accept the inaccuracy by design.
|
||||
* If precise accounting becomes a requirement, reintroduce a tokenizer behind a feature flag or backend-only path.
|
||||
*/
|
||||
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
|
||||
return Math.ceil((textContent || "").length / 4)
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
|
||||
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
|
||||
|
||||
return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
return messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
}
|
||||
|
||||
private ensureCleanState(): void {
|
||||
@@ -434,7 +392,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.currentRequestCancellation = new vscode.CancellationTokenSource()
|
||||
|
||||
// Calculate input tokens before starting the stream
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
|
||||
|
||||
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
|
||||
let accumulatedText: string = ""
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function createOpenRouterStream(
|
||||
// handles direct model.id match logic
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -82,6 +83,7 @@ export async function createOpenRouterStream(
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -117,6 +119,7 @@ export async function createOpenRouterStream(
|
||||
let reasoning: { max_tokens: number } | undefined = undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import { PostHogClientProvider } from "./services/posthog/PostHogClientProvider"
|
||||
import { EmptyRequest } from "./shared/proto/cline/common"
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { telemetryService } from "./services/posthog/PostHogClientProvider"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
*
|
||||
* @param context
|
||||
* @returns The webview provider
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Initialize PostHog client provider
|
||||
let distinctId = context.globalState.get<string>("cline.distinctId")
|
||||
if (!distinctId) {
|
||||
try {
|
||||
const response = await HostProvider.env.getMachineId(EmptyRequest.create({}))
|
||||
distinctId = response.value
|
||||
} catch (e) {
|
||||
// ignore; PostHogProvider will fall back to uuid
|
||||
}
|
||||
}
|
||||
PostHogClientProvider.getInstance(distinctId)
|
||||
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
|
||||
await migrateWelcomeViewCompleted(context)
|
||||
|
||||
// Migrate workspace storage values back to global storage (reverting previous migration)
|
||||
await migrateWorkspaceToGlobalStorage(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
const sidebarWebview = HostProvider.get().createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
return sidebarWebview
|
||||
}
|
||||
|
||||
async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
// Version checking for autoupdate notification
|
||||
const currentVersion = context.extension.packageJSON.version
|
||||
const previousVersion = context.globalState.get<string>("clineVersion")
|
||||
// Perform post-update actions if necessary
|
||||
try {
|
||||
if (!previousVersion || currentVersion !== previousVersion) {
|
||||
Logger.log(`Cline version changed: ${previousVersion} -> ${currentVersion}. First run or update detected.`)
|
||||
|
||||
// Use the same condition as announcements: focus when there's a new announcement to show
|
||||
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
|
||||
const latestAnnouncementId = getLatestAnnouncementId(context)
|
||||
|
||||
if (lastShownAnnouncementId !== latestAnnouncementId) {
|
||||
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
|
||||
const message = previousVersion
|
||||
? `Cline has been updated to v${currentVersion}`
|
||||
: `Welcome to Cline v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
// Always update the main version tracker for the next launch.
|
||||
await context.globalState.update("clineVersion", currentVersion)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs cleanup when Cline is deactivated that is common to all platforms.
|
||||
*/
|
||||
export async function tearDown(): Promise<void> {
|
||||
PostHogClientProvider.getInstance().dispose()
|
||||
|
||||
// Dispose all webview instances
|
||||
await WebviewProvider.disposeAllInstances()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
@@ -60,6 +60,7 @@ export const toolParamNames = [
|
||||
"steps_to_reproduce",
|
||||
"api_request_output",
|
||||
"additional_context",
|
||||
"needs_more_exploration",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -1,245 +1,6 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
/**
|
||||
* @description **Version 1**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version iterates through the message character by character, building an accumulator string.
|
||||
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
|
||||
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
|
||||
* the corresponding opening or closing tags.
|
||||
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
|
||||
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
|
||||
* occurrence of the closing tag.
|
||||
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentToolUseStartIndex = 0
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
let currentParamValueStartIndex = 0
|
||||
let accumulator = ""
|
||||
|
||||
for (let i = 0; i < assistantMessage.length; i++) {
|
||||
const char = assistantMessage[i]
|
||||
accumulator += char
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
// there should not be a param without a tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
|
||||
const paramClosingTag = `</${currentParamName}>`
|
||||
if (currentParamValue.endsWith(paramClosingTag)) {
|
||||
// End of param value found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
// no currentParamName
|
||||
if (currentToolUse) {
|
||||
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
|
||||
const toolUseClosingTag = `</${currentToolUse.name}>`
|
||||
|
||||
if (currentToolValue.endsWith(toolUseClosingTag)) {
|
||||
// End of a tool use found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Go back to parsing text or looking for next tool
|
||||
// Reset text start index in case text follows immediately
|
||||
currentTextContentStartIndex = i + 1
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Check if starting a new parameter within the current tool use
|
||||
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
|
||||
let foundParamStart = false
|
||||
for (const paramOpeningTag of possibleParamOpeningTags) {
|
||||
if (accumulator.endsWith(paramOpeningTag)) {
|
||||
// Start of a new parameter found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// Special case for write_to_file/new_rule content param allowing nested tags
|
||||
// Check if a </content> tag appears, potentially indicating the end of the content param
|
||||
// even if the main tool closing tag hasn't been seen yet.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
|
||||
// Use lastIndexOf to handle cases where </content> might appear within the content itself
|
||||
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
|
||||
|
||||
// Ensure we found valid start/end tags and end is after start
|
||||
if (
|
||||
contentStartIndex !== -1 &&
|
||||
contentEndIndex !== -1 &&
|
||||
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
|
||||
) {
|
||||
// Check if this content param was already being parsed. If so, update it.
|
||||
// If not, and we just found the closing tag, assign it.
|
||||
// This handles cases where the </content> detection might fire before
|
||||
// the <content> tag detection logic, or if the content is very short.
|
||||
if (currentParamName === contentParamName) {
|
||||
// Already parsing content, now we found the end tag
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
currentParamName = undefined // Finished with this param
|
||||
} else if (currentParamName === undefined) {
|
||||
// Not parsing a param, but found </content>. Assume it closes the content block.
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing Text (or looking for start of a tool use) ---
|
||||
// no currentToolUse
|
||||
let didStartToolUse = false
|
||||
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
|
||||
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
|
||||
if (accumulator.endsWith(toolUseOpeningTag)) {
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// Extract text content, removing the part that formed the tool opening tag
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
// Only add if there's actual content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check if there was text before this tool use started
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false, // Ended because tool use started
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didStartToolUse = true
|
||||
break // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
// Start of a new text block
|
||||
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
|
||||
// If accumulator starts from 0, start index is i
|
||||
if (contentBlocks.length === 0 && currentToolUse === undefined) {
|
||||
currentTextContentStartIndex = accumulator.length - 1 // i
|
||||
} else {
|
||||
// Re-calculate based on the actual start of the current text segment
|
||||
// Find the end of the last block
|
||||
let lastBlockEndIndex = 0
|
||||
if (contentBlocks.length > 0) {
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
|
||||
// Simpler: Assume text starts right after the last block ended implicitly at index i.
|
||||
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
|
||||
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
|
||||
// Let's stick to the accumulator slice approach for simplicity in this version.
|
||||
// The start index should be where the current *unmatched* text began.
|
||||
let lastProcessedIndex = -1
|
||||
if (contentBlocks.length > 0) {
|
||||
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
|
||||
// We'll approximate based on the current accumulator and start index logic.
|
||||
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
|
||||
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
|
||||
}
|
||||
// Reset start index to the beginning of the *current* potential text block
|
||||
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
|
||||
}
|
||||
|
||||
// If we just closed a tool, text starts *after* its closing tag
|
||||
// The logic needs refinement here for accurate start index after a tool closure.
|
||||
// Let's assume for now the start index logic inside the loop handles it via slicing.
|
||||
}
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Content will be filled by slicing accumulator
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Update text content based on the accumulator from its start index
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// If a tool use was open at the end
|
||||
if (currentToolUse) {
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// If text content was being accumulated at the end
|
||||
// Note: Only one of currentToolUse or currentTextContent can be defined here,
|
||||
// as starting a tool use finalizes the preceding text block.
|
||||
else if (currentTextContent) {
|
||||
// Update content one last time
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
|
||||
// Add the potentially partial text block only if it contains content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
|
||||
@@ -1,15 +1,68 @@
|
||||
export function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
import LengthFinishReasonError, { APIError } from "openai"
|
||||
|
||||
export function checkContextWindowExceededError(error: unknown): boolean {
|
||||
return (
|
||||
checkIsOpenAIContextWindowError(error) ||
|
||||
checkIsOpenRouterContextWindowError(error) ||
|
||||
checkIsAnthropicContextWindowError(error) ||
|
||||
checkIsCerebrasContextWindowError(error)
|
||||
)
|
||||
}
|
||||
|
||||
function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
try {
|
||||
return error.code === 400 && error.message?.includes("context length")
|
||||
} catch (e: unknown) {
|
||||
const status = error?.status ?? error?.code ?? error?.error?.status ?? error?.response?.status
|
||||
const message: string = String(error?.message || error?.error?.message || "")
|
||||
|
||||
// Known OpenAI/OpenRouter-style signal (code 400 and message includes "context length")
|
||||
const CONTEXT_ERROR_PATTERNS = [
|
||||
/\bcontext\s*(?:length|window)\b/i,
|
||||
/\bmaximum\s*context\b/i,
|
||||
/\b(?:input\s*)?tokens?\s*exceed/i,
|
||||
/\btoo\s*many\s*tokens?\b/i,
|
||||
] as const
|
||||
|
||||
return String(status) === "400" && CONTEXT_ERROR_PATTERNS.some((pattern) => pattern.test(message))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
// Docs: https://platform.openai.com/docs/guides/error-codes/api-errors
|
||||
function checkIsOpenAIContextWindowError(error: unknown): boolean {
|
||||
try {
|
||||
return response?.error?.error?.type === "invalid_request_error"
|
||||
} catch (e: unknown) {
|
||||
if (error instanceof LengthFinishReasonError) {
|
||||
return true
|
||||
}
|
||||
|
||||
const KNOWN_CONTEXT_ERROR_SUBSTRINGS = ["token", "context length"] as const
|
||||
|
||||
return (
|
||||
Boolean(error) &&
|
||||
error instanceof APIError &&
|
||||
error.code?.toString() === "400" &&
|
||||
KNOWN_CONTEXT_ERROR_SUBSTRINGS.some((substring) => error.message.includes(substring))
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
try {
|
||||
return response?.error?.error?.type === "invalid_request_error"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkIsCerebrasContextWindowError(response: any): boolean {
|
||||
try {
|
||||
const status = response?.status ?? response?.code ?? response?.error?.status ?? response?.response?.status
|
||||
const message: string = String(response?.message || response?.error?.message || "")
|
||||
|
||||
return String(status) === "400" && message.includes("Please reduce the length of the messages or completion")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user