Compare commits

...

194 Commits

Author SHA1 Message Date
kvyb b04ff45c79 fix: wait for machineId readiness before sending telemetry_enabled/extension_activated to avoid stub id 2025-08-13 20:57:34 +08:00
kvyb 2e9fdb6726 refactor(telemetry): update distinctId when hostbridge is ready; defer first event until machineId available 2025-08-13 20:10:37 +08:00
kvyb 48db9b1068 feat: Use hostbridge machine ID for posthog distinctId across hosts; VS Code only settings link in warning,, generic warning on other hosts. 2025-08-13 15:21:16 +08:00
pashpashpash 675cd1779b using ulid instead of taskid (#5524)
* using ulid instead of taskid

* protos

* Update src/services/browser/BrowserSession.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-12 21:19:28 -07:00
pashpashpash af0f0b3d7c make cline better at git (#5525)
* make cline better at git

* typo
2025-08-12 19:09:48 -07:00
Saoud Rizwan 45767b87fd Fix usage endpoint call using oauth token instead of saved refresh token (#5483)
* Fix usage endpoint call using oauth token instead of saved refresh token

* Create odd-ladybugs-punch.md

* Update src/api/providers/cline.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-12 19:08:23 -07:00
Sarah Fortune 8f4c6038dd Change the host bridge RPC closeDiff to close*All*Diffs (#5521)
* Change the host bridge RPC closeDiff to closeAllDiffs

In the vscode diff view provider when the diff is closed, it
closes _all_ open diff views.

I thought in the HostBridge, we would just only be closing the
current diff, but we do need to close all the open diff view
because there can be checkpoint diffs open as well.

* Update proto/host/diff.proto

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update src/integrations/editor/DiffViewProvider.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-13 01:16:42 +01:00
Peter Dave Hello 9dc021a881 Remove deprecated GPT-4.5 Preview (#5493) 2025-08-12 15:59:46 -07:00
Max Höhl 3e2bdf8b12 Set CLINE_ACTIVE environment variable for new terminals (#5367)
Resolves #5366
2025-08-12 15:44:51 -07:00
Sarah Fortune d4a99a4060 Task Refactor: Move logic for showing multi-file diffs out of Task (#5517)
* Move the logic for showing the multi-file diffs out of the Task class which >2000 lines long.

Split the logic up into functions, add tests.

Use try/finally to ensure that `sendRelinquishControlEvent` is always sent when the function returns.

* Fix warning about use of !!

* Fix tests

Remove asserts on console logs because they are not able to be stubbed properly.
Move test file to correct directory.

* Formatting
2025-08-12 23:27:53 +01:00
Auroter f4bbb45b07 fix: request_id was being incorrectly extracted from the API response… (#5504)
* fix: request_id was being incorrectly extracted from the API response -- it can always be found in the response header under X-Request-ID

* fix: leave error alone, no need to re-create it

* Update src/services/error/ClineError.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-08-12 12:12:35 -07:00
Bee 088deebd63 Add VSCode theme colors to Tailwind config (#5516)
* Add VSCode theme colors to Tailwind config

- Add comprehensive VSCode theme color palette to Tailwind config
- Replace hardcoded VSCode CSS variables with Tailwind utility classes for HomeHeader
- Update button and text styling to use new theme-aware classes

* format
2025-08-12 10:42:45 -07:00
celestial-vault dcc744dd87 move browser settings menu to tailwind (#5507) 2025-08-12 10:20:31 -07:00
Bee 9ad8525bd0 Guard ActionButtons when no task (#5508)
Guard ActionButtons when no task; fix scroll deps/empty state

- Return null from ActionButtons if no task to avoid rendering controls without context
- Add missing setExpandedRows dependency and remove unnecessary deps to prevent stale closures and re-renders
- Hide “scroll to bottom” button when there are no messages
- Clean up unused index-tracking logic in scrollToMessage
2025-08-12 09:50:15 -07:00
Sarah Fortune 8bf6268952 Add multi-file diff to the host bridge. (#5515)
Add an RPC to the Host Bridge to open a diff for multiple files, this is
used when comparing check points or to display the changes cline has made
when it is finished editing.

Switch the file mentions unit test to an integration test because
now it is pulling vscode dependencies and they cannot be mocked
in the unit tests.
2025-08-12 09:24:16 -07:00
requesty-JohnCosta27 2081bb8dc6 fix requesty's api key url (#5498) 2025-08-11 21:56:14 -07:00
Bee ac22b63796 refactor: centralize action buttons state (#5462)
* Refactor action buttons: centralize state, remove useButtonState

- Replace useButtonState hook with centralized ButtonConfig logic in ActionButtons, mapping task/ask/tool states to button enablement and labels
- Update ActionButtons API to accept task, messages, mode; compute streaming/enablement internally; remove isStreaming prop
- Always render ActionButtons from ChatView; adjust props accordingly
- Update useIsStreaming call to pass task instead of enableButtons/primaryText
- Clean up useMessageHandlers to reset UI state consistently (input, quotes, files, images, autoscroll)
- Remove deprecated hook and align types

Why: unify and simplify button behavior across task lifecycle, reduce duplicated state/props, and make streaming/approval flows more predictable.

* clean up

* Refactor input clearing and streaming detection logic

This commit:
- Separates input clearing logic into a separate useEffect in ActionButtons
- Removes StreamingIndicator component and its useIsStreaming hook

* Revert newly added button states

Remove switch_to_act_mode button config and associated plan mode conditionals in getButtonConfig function, will do any UI change in follow-up

* simplify further

* Add test suite for button configuration logic

This commit introduces a new test file for the `buttonConfig` module, covering various scenarios such as:
- Default button configurations
- Streaming and partial message handling
- Error recovery states
- Tool approval states
- Command execution states
- Specific ask state configurations
- API request state testing

The tests ensure robust button configuration selection based on different message types and states.

* update button styles
2025-08-11 19:14:53 -07:00
Ara 44eb2cc65e Fixing context exceeded error (#5479)
* Detect OpenAI context window errors and auto-retry

- Add checkIsOpenAIContextWindowError to identify OpenAI context length issues (context_length_exceeded, 400 + context-length patterns)
- Integrate into Task: detect OpenAiHandler/OpenAiNativeHandler and handle first-chunk failures as context window overflows
- On detection, aggressively truncate history ("quarter"), persist changes, show truncation notice, wait 1s, then retry once
- Align behavior with Anthropic/OpenRouter handling to reduce failures from oversized prompts

* Fixing OpenAI context exceeded errors

* Fixing OpenAI context exceeded errors

* Fixing OpenAI context exceeded errors

* use OpenAI sdk error types

* Making errors for this generic and adding cerebras

* Making errors for this generic and adding cerebras

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-08-11 17:56:48 -07:00
Saoud Rizwan 2cf1d8628b Set gpt5 max tokens to 8_192 to fix context window exceeded error (#5478)
* Set gpt5 max tokens to 8_192 to fix context window exceeded error

* Create healthy-crabs-sip.md
2025-08-11 15:50:32 -07:00
celestial-vault 669e018b85 Move rest of state to cache (#5404)
* move rest of state to cacheService

* finish moving state to cache

* remove console logs

* fix types

* don't type cast

* add eslint rule banning use of direct storage apis

* fix types

* move vscode state eslint rule to separate rule since it's error and the others aren't

* fix eslint rules parsing
2025-08-11 14:46:39 -07:00
Sarah Fortune 2aa5156905 Update ExternalDiffViewProvider to return diagnostics before/after diff (#5500)
The diff view is supposed to return any new errors or warnings after the
file is edited. The ExternalDiffViewProvider was just returning *all*
the errors.

When the DiffViewProvider is being reset, reset *all* the properties.

Add unit tests for diagnostics functionality

Refactoring:
- Move diagnostics into the parent DiffViewProvider, remove duplicate implementations in VscodeDiffViewProvider and ExternalDiffViewProvider
- Move duplicated code for converting FileDiagnostics to string to `diagnosticsToProblemsString`.
- Use a single implementation of `getDiagnostics` and `diagnosticsToProblemsString` using the HostBridge protos.
2025-08-11 19:54:41 +01:00
celestial-vault 51b619e0d5 remove workspace tracker (#5346)
* remove workspace tracker

* remove console log

* fix search when clicking folder option

* create enum for searchType

* use hostbridge for active files

* use util function for relative path

* Fix into interests error where false security warning is being triggered
2025-08-11 09:51:47 -07:00
Sarah Fortune 85fb76a996 Call teardown() when cline-core is stopped. (#5496) 2025-08-11 17:29:27 +01:00
Dennise Bartlett d73a7cfd06 Fix package-lock version and update CODEOWNERS (#5490) 2025-08-11 01:43:08 -07:00
Ara 489dfbc932 Fixing Read of Workspace root by index.ts (#5482)
* Fixing Read of Workspace root by index.ts

* Fixing Read of Workspace root by index.ts
2025-08-09 20:50:50 -07:00
Bee 314c416788 remove PostHog exception autocapture (#5481)
Remove enableExceptionAutocapture option from PostHog client configuration.
2025-08-09 16:49:07 -07:00
Igor Tceglevskii 3b19c2ec95 Click from a file name in chat to editor (#5422) 2025-08-09 15:48:12 -07:00
Sarah Fortune e04cbea504 Add an endpoint to the HostBridge for integration testing (#5476)
Add an RPC to the HostBridge that returns the contents of the
webview, for use in integration tests.
2025-08-09 23:14:29 +01:00
Toshii affac119f5 stop double counting tokens (#5426)
* updated

* cleanup
2025-08-09 13:05:36 -07:00
Sarah Fortune 3847a2545c Test is still flaking, increase the timeout (#5473) 2025-08-09 16:25:02 +01:00
Sarah Fortune 15593bac2a Use the active webview in ProtoBus getWebviewHtml. (#5444) 2025-08-09 16:24:46 +01:00
Sarah Fortune 84267efb9e Don't use activate() in cline-core (#5448)
* Don't use activate() in cline-core

Have separate code paths to set up the extension and cline-core.

This means the cline-core is not running all the vscode setup and is
only using one `Controller` (the one from the WebviewProvider).

Move the shared logic into common.ts.

* Comments and logging
2025-08-09 14:49:23 +01:00
github-actions[bot] 985ce56809 v3.23.0 Release Notes (#5466)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-09 02:16:07 -07:00
xiongxiong cad28c4c0c fix: calibrate input token when using anthropic models of sap ai core provider (#5469) 2025-08-09 02:13:01 -07:00
Saoud Rizwan 4a22f7dbd2 Fix plan/act hover color and act mode color (#5470)
* Fix plan/act hover color and act mode color

* Revert plan color change
2025-08-09 02:10:49 -07:00
Saoud Rizwan a430226caa Revert unnecessary terminal command issue workarounds (#5463)
* Revert terminal process logic to pre-capture where we didn't use grace periods or fast command workarounds

* Create empty-pears-fail.md
2025-08-09 02:09:40 -07:00
Bee 5885a3cc1d improve mode switch background color (#5467)
* fix: mode switch styling

Replace the use of `--vscode-toolbar-hoverBackground` which is a `-hoverBackground` that tends to be transperant or opacity change on some themes. Replace it with `-background` which uses solid color instead. See https://code.visualstudio.com/api/references/theme-color

- Update Plan/Act mode switch colors var for better visibility across themes
- Remove hover effects from switch options
- Add background classes to active switch options

* changeset added
2025-08-09 00:30:07 -07:00
Oliver Schirmer 759ef873ae Feat: Prompt Caching in SAP AI Core (#5399)
* add: caching support for bedrock (claude)

* refactor: gemini message handling to adhere closer to original implementation (and make implicit caching clear)

* remove: unused bedrock conversion functions

* fix: payload for converse stream (older claude models)
remove: caching support flag for older claude models

* add: changeset

* Update package-lock.json
2025-08-09 00:12:26 -07:00
Saoud Rizwan 782e4ff6e0 Fix credit error tests (#5465)
* Fix credit error tests

* Create odd-tables-pump.md
2025-08-08 21:17:33 -07:00
github-actions[bot] 4bb00241bf v3.22.0 Release Notes (#5424)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-08 20:47:54 -07:00
Saoud Rizwan c325faf8db Fix bug where running out of credits on cline accounts would show '402 empty body' response instead of 'buy credits' component (#5464)
* fix: show credits purchase component when user runs out of credits and we receive 402 status from server

* revert unnecessary change

* Create many-adults-end.md
2025-08-08 20:43:43 -07:00
Igor Tceglevskii 20f8f9c9cf Request for requesting a current PR number for pr_review workflow (#5406) 2025-08-08 13:14:50 -07:00
Yechao LI 5be163f49d Fix safari does not support isComposing of input event (#4118)
* fix: safari does not support isComposing of input event

* formatting

---------

Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-08-08 12:28:46 -07:00
Kevin Taylor 7843ab937a Add cerebras rate limit handling (#5408)
* Update cerebras.ts

* Create friendly-geckos-accept.md
2025-08-08 12:28:34 -07:00
tjandy98 5ed4319d21 Add support for GPT-5 models to SAP AI Core Provider (#5428)
* add gpt-5 models

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

* add changeset

* remove max_tokens & temperature

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-08-08 10:16:45 -07:00
Igor Tceglevskii a8971b807a Possibility to install binaries to a separate folder (#5446) 2025-08-08 15:24:05 +01:00
Sarah Fortune 51c4e0aceb Don't instantiate the AuthService at the top level of the module. (#5443)
I am working separating the initialization for the extension and cline-core
and this is causing a circular dependency.
2025-08-08 06:51:43 -07:00
Sarah Fortune 1cf62941cd Fix webview on IntelliJ (#5439)
The ExternalWebviewProvider has to return /something/ for `getWebview()` or
the rest of the code thinks that is not set up and it won't generate the HTML
for IntelliJ.

The Vscode webview panel, `resolveWebview()` and other Vscode specific parts are
planned to be moved out of the WebviewProvider and into VscodeWebviewProvider,
but that depends other changes to how the webview is initialized in extension.ts
to need to happen first.

Move the WebviewProvider out of index.ts and into a file name `WebviewProvider`,
this follows best practises.
2025-08-08 13:50:28 +01:00
Sarah Fortune cc2472f500 Sssh McpHub (#5434) 2025-08-08 11:58:14 +01:00
Ara 677e544c51 Fix Gpt 5 context window (#5414)
* Fix Gpt 5 context window

* Fix Gpt 5 context window
2025-08-08 01:04:12 -07:00
akfoster d3c8fbbf1d chore: remove unused parseAssistantmessageV1 (#5425)
* chore: remove unused parseAssistantmessageV1

* chore: add PR number to comments

* fix: include full path to PR
2025-08-07 22:21:17 -07:00
Sam 1b06633253 fix: LiteLLM provider cost calculations (#4990)
* fix: LiteLLM provider cost calculations

* fix: LiteLLM provider cost calculations

* Update src/api/providers/litellm.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-07 22:06:04 -07:00
Bee 4ab8559fce feat: support sending context to active editor panels (#5239)
* feat: add client-specific targeting for addToInput events

- Add client-specific targeting for addToInput events
- Update subscribeToAddToInput to accept client ID parameter
- Replace global event broadcasting with targeted client messaging
- Remove automatic sidebar focus when adding code to chat
- Use last active webview instance for context menu actions
- Maintain backward compatibility with subscription management

* add changeset

* remove debug profiler

* e2e test

* add type

* Add e2e test

* update teardown
2025-08-07 19:32:40 -07:00
github-actions[bot] 259368e0a3 v3.21.0 Release Notes (#5392)
* changeset version bump

* Updating CHANGELOG.md format

* release notes

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-07 10:03:39 -07:00
pashpashpash 9b7839efcd Pashpashpash/gpt 5 release (#5413)
* preparing for gpt5 release

* Update generic system prompt with needs_more_exploration param for plan mode

* changeset

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-07 09:47:04 -07:00
Ara 47a2ae83de Switch to ULID from UUID for tasks telemetry (#5407)
* Switch to ULID from UUID for tasks

* Switch to ULID from UUID for tasks

* Switch to ULID from UUID for tasks
2025-08-06 19:51:19 -07:00
Sarah Fortune 1b5590e26c Refactoring: move postMessageToWebview into vscode specific code (#5396) 2025-08-07 02:49:12 +01:00
Toshii 9e493341d2 Add ollama key for cloud endpoint (#5400)
* base

Co-authored-by: EndoTheDev <endothedev@gmail.com>

* toggle showing key box

* typing

---------

Co-authored-by: EndoTheDev <endothedev@gmail.com>
2025-08-06 14:41:48 -07:00
kvyb 1d4cd3187b Hostbridge diff diagnostics (#5368)
* feat: migrate diff edit diagnostics to hostbridge; Migrate diagnostics functionality from direct VS Code API calls to the hostbridge layer to enable multi-host support (VS Code + IntelliJ).

* remove test logging

* refactor: migrate diagnostics to workspace service and host separation
2025-08-06 18:16:17 +03:00
Ara 32f0f9618c Adding UUID to task creation for tracking the metrics of a Task in telemetry (#5379) 2025-08-06 00:14:32 -07:00
Ara 3001f883c2 Add walkthrough button and enable quick wins for new users (#5047)
* Add walkthrough button and enable quick wins for new users

- Add openWalkthrough RPC method to ui.proto
- Enable quick wins display for users with <3 tasks in history
- Add "Take a Tour" button in HomeHeader when quick wins are shown
- Update WelcomeSection to pass shouldShowQuickWins prop to HomeHeader

* Adding Gpt-oss through groq

* Adding Gpt-oss through groq

* Support prompt caching and thinking for Opus 4.1

* Support prompt caching and thinking for Opus 4.1

* Support prompt caching and thinking for Opus 4.1
2025-08-06 00:13:31 -07:00
github-actions[bot] a64e60b8f6 v3.20.13 Release Notes (#5391)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-06 00:12:50 -07:00
Saoud Rizwan 3a0e6a471b Add prompt caching for Opus 4.1 (#5389)
* Add prompt caching for Opus 4.1

* Create forty-poets-doubt.md
2025-08-06 00:10:50 -07:00
github-actions[bot] c10f4e0a66 v3.20.12 Release Notes (#5387)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.20.12

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-05 22:25:33 -07:00
Ara 3e11271cf8 Fix support for prompt caching and thinking for Opus 4.1 (#5386)
* Support prompt caching and thinking for Opus 4.1

* Support prompt caching and thinking for Opus 4.1
2025-08-05 22:12:14 -07:00
Jim Tang 29ae2c286d Update index.ts while tree possible as a null. (#5285) 2025-08-05 20:03:22 -07:00
github-actions[bot] 82aee44a9a v3.20.11 Release Notes (#5377) 2025-08-05 16:07:23 -07:00
omercelik 031604ddf6 feat: Added Claude Opus 4.1 to Bedrock (#5381)
* feat: Added Claude Opus 4.1 to Bedrock

* Create ninety-owls-develop.md
2025-08-05 15:03:15 -07:00
Bee f2101e375f fix: update Playwright config and teardown error handling (#5383)
- Remove teardown dependency on e2e tests to fix execution order
- Move server cleanup before file operations in teardown
- Add proper error handling and logging for cleanup operations
2025-08-05 14:22:19 -07:00
Tomás Barreiro 8a65f0c68b feat: Add Opus 4.1 to claude-code (#5382)
* Add opus-4-1 to claude-code

* Add changeset
2025-08-05 13:42:40 -07:00
Bee 32b8fa44cb refactor: Integrate Posthog into Feature Flags & Telemetry & Error Services (#5275)
* refactor: posthog services: feature flags + error + telemtry

- Convert PostHogClientProvider to singleton with lifecycle management
- Update ErrorServices to use PostHogClientProvider
- Update Telementry Service
- Update and enable Feature Flags service

* replace logger

* fix imports - part 1

* update distinct ID

* update

* update

* clean up

* revert autoformat

* fix merge conflicts

* clean up autoformat

* revert autoformatter

* clean up logs
2025-08-05 13:00:44 -07:00
Sarah Fortune 0d067f7470 Add getCallbackUri to the HostProvider (#5361) 2025-08-05 12:06:08 -07:00
Kevin Taylor de6166392c Update Cerebras gpt-oss-120b (#5376)
* Add Cerebras gpt-oss-120b

* Change completion tokens
2025-08-05 12:00:02 -07:00
Kevin Taylor 5f21a9162a Add Cerebras gpt-oss-120b (#5375)
* Add Cerebras gpt-oss-120b

* Update src/shared/api.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-05 11:27:11 -07:00
github-actions[bot] 4de991f1b0 v3.20.10 Release Notes (#5374) 2025-08-05 11:22:52 -07:00
pashpashpash 6e5d4a3f9e openai model in hugging face correct maxtokens (#5371)
* openai model in hugging face correct maxtokens

* maxtokens

* maxtokens 131k i guess?

* maxtokens swap

* changeset

* Adding Gpt-oss through groq

---------

Co-authored-by: arafatkatze <arafat.da.khan@gmail.com>
2025-08-05 11:08:53 -07:00
github-actions[bot] 95af95badf v3.20.9 Release Notes (#5354)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.20.9 patch release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-05 10:12:09 -07:00
Ara 61dcbd390c Adding Anthropic opus 4.1 (#5369)
* Adding Anthropic opus 4.1

* Adding Anthropic opus 4.1
2025-08-05 09:59:15 -07:00
pashpashpash 6255ac0a51 added provider flag to diff edit cli (#5334)
* added provider flag to diff edit cli

* dashboard ux

* more dashboard improvmeents

* native handler instead of just openai
2025-08-05 09:21:12 -07:00
Sarah Fortune 616800fcb9 Refactoring: move vscode specific property out of the WebviewProvider into the VscodeViewProvider (#5364) 2025-08-05 06:48:12 +01:00
Sarah Fortune df3826a59f In the webview grpc client, JSON encode/decode the messages when not running in Vscode (#5362) 2025-08-04 22:39:32 -07:00
Sarah Fortune 873917810d Remove left code from ProtoBus migration (#5363)
There is one place in the McpHub that sends messages mcp notification messages directly to the webview (not using the ProtoBus).

There is nowhere in the webview that is listening for this message, so this code is not doing anything.
2025-08-04 22:39:20 -07:00
Alex Ker e3c966f4e9 Baseten provider (#5238)
* kimi working

* fixed description rendering

* nit

* changeset

* revert openai version in package.json

* revert package-lock.json

* added space back in

* maintained previous protofield map order

* fixed import error due to location change from main

* updated Mode import for BasetenModelPicker

* revert readme since baseten is openai compatible

* refactored extensionStateContext

* added didOutputUsage flag

* fixed frontend loading

* no support for images on llama

* shifted VSCode Option order

* deleted typo

---------

Co-authored-by: Alex Ker <alexker@mac.mynetworksettings.com>
Co-authored-by: Alex Ker <alexker@Alexs-MacBook-Pro.local>
2025-08-04 20:07:59 -07:00
Sarah Fortune 7eeb43ab41 Simplify the GrpcHandler and add tests (#5356)
* Simplify the GrpcHandler

* Use two functions handleUnaryRequest and handleStreamingRequest, instead of creating a GrpcHandler object and calling class methods on it.
* Remove redundant try/catch and empty finally blocks. Each of the two handler functions has it's own try/catch.
* Each of the two functions is responsible for posting the result to the webview- Instead of unary and streaming responses being handled at different levels.
* Use the GrpcRequest and GrpcCancel types.

* Update comment
2025-08-04 19:15:29 -07:00
Bee 7620f177ac fix: clear streamingFailedMessage when user manually retries (#5222)
* fix: clear streamingFailedMessage when user manually retries

- Clear streamingFailedMessage when user manually retries
- Convert imports to type-only where appropriate
- Reorder imports for better organization
- Add explicit type annotations for better type safety
- Move node:timers/promises import to top

* add changeset

* merge main and reset fail flag

* revert autoformat
2025-08-04 17:04:42 -07:00
Bee 67bab94911 Revert "Add getCallUri to the HostProvider (#5322)" (#5359)
This reverts commit b8227c19c3.
2025-08-04 16:30:04 -07:00
Bee eb91bfd738 Update ChatView footer background to use sidebar theme (#5357)
Change footer background from editor to sidebar background color
and remove border styling.
2025-08-04 16:06:02 -07:00
Sarah Fortune b8227c19c3 Add getCallUri to the HostProvider (#5322)
**Centralize callback URI management** through the HostProvider instead of having it in multiple places in the codebase.

**Simplify error handling** by making the callback URI required rather than optional

The changes are related to **authentication callback URI handling** in the Cline extension. Here's what's being modified:

  - Simplified callback URI retrieval
- Changed return type from `Promise<string | undefined>` to `Promise<string>`
- Now throws an error if AuthHandler is not enabled instead of returning undefined

- Added a new `getCallbackUri` property that returns a `Promise<string>`
- This allows the host provider to supply callback URIs for authentication

  - Implemented callback URI provider

  - Updated to use HostProvider for callback URI

  - Updated to match new signature
2025-08-05 00:03:07 +01:00
Sarah Fortune 8fee09f09e Add logging to the webview ProtoBus client if it recieves a badly formed message (#5353) 2025-08-04 22:34:08 +01:00
Bee 1c026c26d2 fix: chatbox position styling (#5352)
* fix: chatbox position styling

* add changeset
2025-08-04 14:18:53 -07:00
Sarah Fortune 5bc4e5a4a0 Add comments for HostBridge RPC showSaveDialog (#5351) 2025-08-04 21:15:45 +01:00
Ara 2cfce5734e Change Vscode LM token counts to use approx counting method (#5280) 2025-08-04 12:41:34 -07:00
Bee e2045bf5c3 fix: flaky check for editor search bar (#5347)
* fix: flaky check for editor search bar

* remove disabling notification
2025-08-04 12:37:33 -07:00
Sarah Fortune 0d933e804f Support mentions for filenames with spaces (#5309)
* feat: support file mentions with spaces using quoted syntax

This change allows users to reference files with spaces in their names, which was previously impossible due to the space-delimited mention syntax.
File names with spaces can be @ mentioned by quoting the file name, e.g. @"/path with spaces/file.txt".

- Update mention regex in `src/shared/context-mentions.ts` to accept quoted file paths
  - Add support for quoted file paths that can contain spaces.
  - Allow multiple trailing punctuation chars; previously only a single limited punctuation characters were allowed.
  - Maintain support for unquoted paths, URLs, git hashes, and special keywords

- Update `src/core/mentions/index.ts` to handle quoted file names in mention parsing
  - Process quoted file paths by removing quotes when accessing the file system
  - Preserve existing functionality for all other mention types

- Update `webview-ui/src/utils/context-mentions.ts` to auto-quote file names with spaces
  - `insertMention()` and `insertMentionDirectly()` now wrap file paths containing spaces in quotes
  - Non-file mentions (URLs, keywords) remain unquoted

- Add comprehensive unit tests:
  - New test file `src/core/mentions/__tests__/index.test.ts` covering all mention types
  - New test file `webview-ui/src/utils/__tests__/context-mentions.test.ts` for webview mention insertion
  - Expanded `src/shared/__tests__/context-mentions.test.ts` to cover quoted paths and edge cases

* Use const instead of var
2025-08-04 19:42:44 +01:00
Sarah Fortune 16f73532f4 Remove things that were left over from the ProtoBus migration. (#5321) 2025-08-04 18:04:54 +01:00
Sarah Fortune 88bea8eeb4 Update saveOpenDocumentIfDirty to return if the doc was saved or not (#5343) 2025-08-04 18:04:36 +01:00
Sarah Fortune 1a570e98ba Update the GitHub test action to produce more readable output (#5333) 2025-08-04 18:04:21 +01:00
Toshii d86b7dd036 Add optional way to enforce no file edits in plan mode (#5299)
* base implementation

* base messaging implementation & ui

* update state
2025-08-04 09:55:59 -07:00
Sarah Fortune 0178c3fa90 Fix flakey test getOpenTabs and re-enable unit tests (#5332)
- Replace fixed 100ms timeout with pWaitFor polling mechanism
- Set 2-second timeout with 50ms polling interval
- Test now waits exactly as long as needed for tabs to be created
2025-08-03 20:51:05 -07:00
github-actions[bot] a107f45c6a v3.20.8 Release Notes (#5330)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-03 17:17:54 -07:00
Saoud Rizwan 3dd2ed9161 Add comment about testing fix (#5329)
* Add comment about testing fix

* Create cool-cherries-brush.md
2025-08-03 17:15:04 -07:00
Saoud Rizwan 9a6603fdfb Disable unit tests in publish pipeline (#5327) 2025-08-03 17:09:59 -07:00
Sarah Fortune 6d5c3e6aa4 Switch remaining uses of vscode.window.show*Message to the HostBridge (#5324)
* Move remaining uses of vscode.window.show*Message to the HostBridge

Switch over the remaining uses.

Turn on the linter check to prevent these APIs being reintroduced later.

Exclude test files from the linter check.

* Update unit test
2025-08-04 01:03:43 +01:00
Sarah Fortune 24b9e821bb refactor: update navbar styling and remove shadow (#5323)
- Replace database icon with MCP server icon (codicon-server)
- Remove shadow-sm class for a flatter appearance
- Maintain consistent button styling with VSCodeButton components
- Add tooltips using HeroTooltip
2025-08-04 00:59:04 +01:00
github-actions[bot] 9960a3c57c v3.20.7 Release Notes (#5328)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-03 16:57:50 -07:00
Sarah Fortune 88947592f0 Fix errors in tests (#5294)
* Fix errors in tests:

```
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command that lists files
[TerminalProcess] Starting command: "sleep 0.5 && echo 'Done sleeping'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
  at async Context.<anonymous> (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.test.js:119:13)
FakeTimers: clearTimeout was invoked to clear a native timer instead of one created by this library.
To automatically clean-up native timers, use `shouldClearNativeTimers`.
      ✔ should handle a longer running command (3007ms)
[TerminalProcess] Starting command: "echo 'Line 1' 'Line 2'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command with arguments
[TerminalProcess] Starting command: "echo "Line 1" && echo 'Line 2'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command with quotes
  ```

* Create brown-papayas-protect.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-03 16:55:21 -07:00
yuvalman ef4d11df19 fix: circular dependency that affect the github workflow Tests / test (pull_request) (#5317)
* fix: circular dependency that affect test env

* fix: circular dependency that affect test env
2025-08-04 00:38:41 +01:00
github-actions[bot] 23dec509bc v3.20.6 Release Notes (#5326)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-03 16:24:15 -07:00
celestial-vault 07ab6b19b8 check auth after initialize cacheservice (#5325)
* initialize cachService in controller constructor; remove authService as a class level variable on Controller

* changeset
2025-08-03 16:19:40 -07:00
Bee 0ddef94d1f feat: use auth callback handling with custom AuthHandler (#5223)
* feat: use auth callback handling with custom AuthHandler

- Add AuthHandler class to manage OAuth flow with local HTTP server
- Move callback logic from extension.ts to SharingUriHandler, making that shared between the original and new authentication ways
- Enabling Custom HTTP for "core only" environments
- Async starting and stopping HTTP server
2025-08-02 15:10:16 -07:00
Bee b9ae83b1cd fix: standalone navbar style with chat layout refactor (#5308)
* fix: standalone navbar style with chat layout refactor

* Update webview-ui/src/components/chat/chat-view/components/layout/MessagesArea.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-02 14:17:38 -07:00
Sarah Fortune e4eaf34827 test: Fix and re-enable unit tests (#5298)
* test: Fix and re-enable unit tests

Re-enable unit tests in CI workflow that were previously disabled

The cline-api test requires VSCode SDK which cannot be easily mocked in unit tests,
so it has been moved to integration tests where the full VSCode environment is available.

The @google/genai module is ES6-only which causes issues when running integration tests
compiled to CommonJS. A mock implementation has been added and the module resolution
is intercepted in test-setup.js to use the mock instead.

The bedrock unit tests for getModelId() functionality are removed as they were failing
and fixing them is out of scope for this PR.

- Move cline-api.test.ts from exports to test directory as it depends on VSCode SDK
- Add gemini-mock.test.ts to mock @google/genai ES6 module for CommonJS compatibility
- Add module interception in test-setup.js to redirect @google/genai to mock
- Remove failing bedrock unit tests introduced in PR #4209 (out of scope)

* Update src/api/providers/__tests__/bedrock.test.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Formatting

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-02 01:47:27 +01:00
github-actions[bot] 6d3ed43c74 v3.20.5 Release Notes (#5297)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.20.5

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-01 16:50:04 -07:00
celestial-vault cbb67b48f2 fix secrets persistence (#5296) 2025-08-01 16:41:14 -07:00
Sarah Fortune a5f6a97be8 Fix eslint unit tests (#5295) 2025-08-01 23:16:18 +01:00
github-actions[bot] f309b062e7 v3.20.4 Release Notes
v3.20.4 Release Notes
2025-08-01 13:20:27 -07:00
canvrno 768df130ab Fix for delete task popup (#5260)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-08-01 13:05:21 -07:00
Toshii 9980cb0938 fix grok browser_user (#5278) 2025-08-01 10:14:19 -07:00
Ara aca4f842fa Update Cerebras models (#5282)
* Update Cerebras models

* Add changeset

* Modify completion token limits

* Split qwen3 coder into free/paid

* Change -paid to base model name

* Update Cerebras models

* Update Cerebras models

* Update Cerebras models

* Update api.ts

* Update Cerebras models

---------

Co-authored-by: Kevin Taylor <kevin.taylor@cerebras.net>
2025-08-01 01:11:53 -07:00
Bee 3fc91e2afe fix: E2E test stability by reordering sidebar and notification setup (#5279)
* fix: E2E test stability by reordering sidebar and notification setup

- Extract editor menu locator to variable for better readability
- Move sidebar opening to page fixture to ensure it's available earlier
- Wait for chat input visibility before disabling notifications
- Prevents race conditions in test initialization

* fix
2025-07-31 18:03:14 -07:00
celestial-vault 5f4700ce95 Move apiconfiguration to cache layer (#5210)
* remove chatSettings object

* use cache for apiCongfiguration state

* add state persistence debounced, batch state updates, make setters synchronous

* fix types after merge conflicts

* fix global state reset

* remove clearCache; make dispose function private; remove vscode api dependency; call reInitialize in reset functions instead of dispose/initialize
2025-07-31 16:58:47 -07:00
Jim Tang dbaf5e3ee3 Update system.ts for formating the code. (#5270) 2025-07-31 16:19:01 -07:00
Toshii 576176c24f add grok4 to advanced list (#5276) 2025-07-31 14:54:45 -07:00
Akshay Raj Gollahalli c8abcbfdf9 Do not ignore pkg folder (#4483) (#4505) 2025-07-31 12:11:31 -07:00
celestial-vault 8e984f2d98 clean up getStateToPostToWebview in preparation for migration to StateManager service (#5266) 2025-07-31 11:47:06 -07:00
github-actions[bot] 81564faa4e v3.20.3 Release Notes (#5185)
* changeset version bump

* Updating CHANGELOG.md format

* releaseee

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-30 21:49:57 -07:00
pashpashpash f8b5f1fd72 adding redirectUrl to credits purchasing experience (#5158) 2025-07-30 17:43:04 -07:00
celestial-vault 2eb57384ab remove useEffect (#5261) 2025-07-30 17:11:01 -07:00
Sarah Fortune c80bae504a Add a flag to build the webview without minification, compact, etc. (#5262)
When the webview is built wuth build:test:
* don't compact the compiled code
* don't minify
* use inline source maps (the embedded JCEF browser can't load source maps from .map files).
2025-07-30 19:53:36 -04:00
Sarah Fortune 80f955be9e Add vscode.workspace.findFiles to no-direct-vscode-api eslint rule. (#5263)
Add `vscode.workspace.findFiles` to the list of the Vscode API calls that should not be re-introduced to the extension unintentionally.
2025-07-30 19:49:15 -04:00
wangyijing130 7435ffcd2f fix: use Uri.from to generate valid diff URI (#4882)
* fix: use Uri.from to generate valid diff URI

* fix the conflicts for VscodeDiffViewProvider.ts has been moved

---------

Co-authored-by: wangyj20 <wangyj20@asiainfo.com>
2025-07-30 10:42:19 -07:00
Bee a05d438612 refactor: setup e2e tests to use shared mock server (#5245)
* refactor: e2e test setup to use Playwright projects with global server

- Replace globalSetup/globalTeardown with Playwright projects configuration
- Rename setup.ts to global.setup.ts and teardown.ts to global.teardown.ts
- Convert ClineApiServerMock to use shared global server instance
- Add proper dependency management between setup, tests, and cleanup phases
- Improve server connection tracking and cleanup handling

* Rename Playwright test project names to match

* IS_DEV

* update helpers
2025-07-30 00:16:08 -07:00
Daniel Steigman b4b7512d9f Improve Cline accounts support telemetry (#5242)
* fixed linter rule and added identified telemtry stuff

* Updated the error handling

* fixed error handling
2025-07-29 17:23:50 -07:00
Toshii e08c65618e read_file can read images (png, jpg, jpeg, webp) (#4411)
* base

* throw

* chat ui

Co-authored-by: Ding Fei <fding@feysh.com>

* chat row logic for image file reads

* dim check change

---------

Co-authored-by: Ding Fei <fding@feysh.com>
2025-07-29 15:41:31 -07:00
Bee d653f1cc27 test: update Playwright test timeouts (#5241)
- Rename isGitHubAction to isCI for broader CI detection
- Adjust timeout logic to use CI or Windows conditions
- Reduce expect timeout from 40s/20s to 5s/2s for faster feedback
- Decrease streaming chunk delay from 50ms to 20ms in server mock
2025-07-29 15:27:46 -07:00
Bee c3a97c3eda e2e test: add mock service for cline API & new test for diff editor (#5196)
* Add mock api service and E2E test infrastructure

- Create AuthServiceMock for testing with mock user data and API responses
- Add AuthProvider interface to standardize authentication providers
- Implement E2E test fixtures with mock server and workspace setup
- Add comprehensive E2E tests for authentication and core functionality
- Export DEFAULT_CLINE_APP_URL config and make getEnvironmentConfig more flexible
- Update AuthService to use mock implementation during E2E tests

* format

* import

* refactor mock server

* rename data

* wait for text

* wait for edit

* increase timeout for windows

* clean up

* rename test and add orgs
2025-07-29 12:39:49 -07:00
celestial-vault 0e56272d65 remove chatSettings object (#5178)
* remove chatSettings object

* fix types after merge conflicts
2025-07-29 12:31:52 -07:00
Kevin Taylor fdc2e2655a Add Cerebras model Qwen 3 235b instruct (#5236) 2025-07-29 11:55:48 -07:00
Wintertee 6050413b8b fix: remove duplicate tool registration for claude4-experimental (#4748) 2025-07-29 10:48:41 -07:00
Bee c54f0da737 feat: adds navigation bar component and restructure app layout (#5220) 2025-07-29 13:05:01 -04:00
Sarah Fortune 6cbfb2b8b0 Remove duplication define property in esbuild.json (#5234) 2025-07-29 08:54:12 -07:00
Sarah Fortune 22788f0f12 Move the OutputChannel to the HostProvider (#5189)
* Move the OutputChannel to the HostProvider

Replace `OutputChannel.appendLine` with `HostProvider.logToChannel`.

Remove places where the cline OutputChannel was being passed around. Now it is stored in the HostProvider, so we don't need to do this.

# Conflicts:
#	src/hosts/vscode/VscodeWebviewProvider.ts

* Dont log the timestamp in logger.ts, the cline-core logger already outputs the timestamp

* Fix imports
2025-07-28 22:56:43 -07:00
DongDong Ling 708b785a97 Add Huawei Cloud MaaS Provider (#5071)
* Add Huawei Cloud MaaS Provider

* Fix case error

* Add missing modelid

* add huawei specific modelId and modelInfo

* add huawei specific model id and model info in state.proto

* more huawei maas specific change
2025-07-28 21:47:21 -07:00
Jose R. Perez 099bc44d42 docs: fix Global Rules directory location for Linux/WSL systems (#5219) 2025-07-28 23:54:03 -04:00
Toshii b9f4678dba add try-catch handling (#5227) 2025-07-28 20:21:51 -07:00
Bee f7d17384f6 refactor & fix: improve account view with better states management (#5182)
* refactor & fix: improve account view with better states management

The previous AccountView implementation suffered from several critical state management issues:

- Incorrect info on display: The active account is not ready when component is mounted because the fetching only start on mount but doesn't get reset correctly
- Excessive re-renders: All data was fetched on component mount, causing cascading state updates
- Race conditions: Multiple concurrent API calls and state dependencies created unpredictable behavior, e.g. 403 rate limits errors
- Monolithic state management: All account data, organizations, and auth state was managed in a single massive component
- Poor user experience: Users saw empty states and loading flickers when switching between organizations
- Tight coupling: User and org info logic was deeply embedded within the account view that cause Effect dependency loops

Solution: Centralized Authentication Context

- Extracted auth logic into dedicated ClineAuthContext with organizations state management
- Eliminated prop drilling by providing clineUser, organizations, and activeOrganization at the context level
- Reduced component re-renders by managing auth state separately from UI state
- Performed authentication guard at higher level and only displays user account to authenticated user. The component will get dismounted when user is not autheticated.
- Move handleSignIn and handleSignout into individual functions instead as they are regular functions with no state dependency

* 60secs

* Optimize state updates in AccountView to prevent unnecessary re-renders

Remove conditional checks before setState calls and use functional updates
with deep equality comparison to avoid redundant state changes and
dependency array bloat in useCallback hooks.

* add docs

* fix format

* fix error test

* setuser on logout
2025-07-28 15:46:23 -07:00
Sarah Fortune bb5a64afb3 Quiet spammy MCP debug logs (#5224) 2025-07-28 15:44:38 -07:00
Sarah Fortune 61224734f8 Change timeout, token budget and line limit fields in the ProtoBus from int64 to int32. (#5221) 2025-07-28 17:40:15 -04:00
Bee a28b995ab1 Fix styled-components prop warnings (#5181)
* Fix styled-components prop warnings

- Fix styled-components shouldForwardProp warnings by filtering non-DOM props
- Clean up unused imports in ChatTextArea and other components

* use mjs

* later

* remove unused imports
2025-07-28 11:19:32 -07:00
Bee a91878efc6 Fix: webview panel state change steals focus (#5193)
* Fix: webview panel state change steals focus

Fix webview visibility detection to check both visible and active states before taking focus. If a panel is visible but not active (focused), it should not steals editor focus.

Also removes unused import & add type imports

* add changeset
2025-07-28 10:59:00 -07:00
Jonathan Barazany 65c21e7b7d Bug fix: VSCode LM API token counting for Claude models (#5051)
* Improve token counting for Claude models in VSCode LM provider

- Reorder imports for better organization
- Add extractTextFromMessage helper method
- Add isClaudeModel detection method
- Use 4:1 character-to-token ratio for Claude models instead of VSCode's inaccurate counting
- Fallback to existing VSCode LM token counting for non-Claude models

* Update version to 3.18.3-r1 and refactor token calculation in VsCodeLmHandler

* 3.19.5-r1

* Add smart jobs impress changeset for VSCode LM API token counting fix

---------

Co-authored-by: Jonathan Barazany <jbarazany@microsoft.com>
2025-07-28 02:09:40 -07:00
Sarah Fortune 56e388c90f Add a check to the proto scripts to warn about using int64 types. (#5174)
* Add a check to the proto scripts to warn about using int64 types.

Javascript cannot represent the full range of int64. So, when the protos are deserialized from JSON int64's are converted to strings. The typescript code is expecting a number and not a string, and this causes errors.

This was noticed before now because in the vscode protobus and hostbridge, the proto messages are not serialized and deserialized, they are just passed around as JS objects.

However, in IntelliJ the protos are serialized when they are sent through the ProtoBus. When the response messages contains and int64, it is deserialized to a string instead of a number for safety. This is causes parts of Cline to fail in IntelliJ, e.g. the task history view won't load because `Task.getTotalTasksSize()` returns a string when it is expecting a number.

* Make checkProtos shorter

* Update scripts/build-proto.mjs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update scripts/build-proto.mjs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update scripts/build-proto.mjs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Fix typo

* Fix typo

* Fix bad merge

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-27 22:02:12 -04:00
pashpashpash 586d804a01 Revert "Spruce up HistoryPreview (#4101)" (#5207)
This reverts commit cdfffb8464.
2025-07-27 16:48:49 -07:00
Saoud Rizwan 85fbbcbe3f Revise contributing guidelines and fix PR template link to feature requests board (#5195)
* Revise contributing guidelines and fix PR template link to feature requests board

* Fix wording
2025-07-26 12:43:59 -07:00
ZhangZhiheng 25c5310383 Fix url no trim (#4641)
* fix: trim input value for URL fields in BaseUrlField and DebouncedTextField components (#4051)

* chore: add changeset
2025-07-26 12:23:09 -07:00
Saoud Rizwan 19ef843d4f Revert "feat: update Gemini models - remove deprecated and add 2.5 Flash-Lite…" (#5194)
This reverts commit f6273e0661.
2025-07-26 11:53:06 -07:00
dependabot[bot] 2b3dd14271 Bump the npm_and_yarn group with 3 updates (#4186)
Bumps the npm_and_yarn group with 3 updates: [brace-expansion](https://github.com/juliangruber/brace-expansion), [tar-fs](https://github.com/mafintosh/tar-fs) and [undici](https://github.com/nodejs/undici).


Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `tar-fs` from 3.0.8 to 3.0.9
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

Updates `undici` from 6.21.1 to 6.21.3
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.21.3)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.21.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 08:03:48 -07:00
Karan Vaidya 721e7ae305 Add Composio to adding-mcp-servers-from-github.mdx (#4624)
* Add Composio to adding-mcp-servers-from-github.mdx

* Update adding-mcp-servers-from-github.mdx
2025-07-26 07:52:27 -07:00
Eugene Demkin f6273e0661 feat: update Gemini models - remove deprecated and add 2.5 Flash-Lite (#4681)
- Remove deprecated experimental models:
  - gemini-1.5-flash-8b-exp-0827
  - gemini-1.5-flash-exp-0827
  - gemini-1.5-pro-exp-0827
- Add gemini-2.5-flash-lite-preview-06-17 with latest pricing
2025-07-26 07:45:29 -07:00
CellenLee 68d0af2afc feat: moonshot provider ui polish (#5034) 2025-07-26 07:21:50 -07:00
Saoud Rizwan 4286f301d2 Remove feature_contribution issue type (#5184) 2025-07-26 06:18:41 -07:00
dependabot[bot] 28536084bf Bump form-data in /webview-ui in the npm_and_yarn group (#5095)
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 06:07:58 -07:00
Utkarsh dfbac3eefd Support for Deepseek R1 0528 (#3903) (#5183) 2025-07-26 05:45:31 -07:00
Saoud Rizwan 04f0710a03 Update bug report template to make system information and logs fields optional (#5159) 2025-07-26 01:58:48 -07:00
kvyb 195e15db32 Move vscode.commands.executeCommand("vscode.open") to Hostbridge (#5173)
* feat: add openFile host bridge for vscode.open command

* fix: simplify openFile hostbridge to follow gRPC best practices:
- Remove success boolean field from OpenFileResponse proto
- Use gRPC exceptions for error handling instead of success/failure booleans
- Simplify hostbridge implementation to just move existing vscode.open code

* fix: remove create wrapper from openFile call

* fix proto merge conflict
2025-07-26 04:19:03 -04:00
Sarah Fortune 19cb70de55 Use npm moduleopen to open URLs in the external browser (instead of the host bridge) (#5013)
* Use npm `open` to open URLs in the external browser

# Conflicts:
#	src/utils/env.ts

# Conflicts:
#	src/utils/env.ts

* Change log statement

* Use the simple-open-url module to open URLs in the system browser.

Log failures of ProtoBus RPCs

* Remove vscode hostbridge handler for openExternal

* Rm unused imports

* Switch back to `open` module.

Update esbuild.js to ES6 and move to esbuild.mjs

* Update src/utils/env.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* remove IS_DEV from e2e setup build

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-26 04:06:53 -04:00
github-actions[bot] e2a2ecde44 v3.20.2 Release Notes (#5155)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-26 00:00:19 -07:00
Saoud Rizwan 1a466c0a44 Fix cursor state after restoring files to be disabled after checked out (#5179)
* Fix cursor state after restoring files to be disabled after checked out

* Create silver-llamas-pretend.md
2025-07-25 23:51:34 -07:00
Sarah Fortune 31cbf489c4 Fix launch configuration for the cline-core (#5170) 2025-07-26 02:38:38 -04:00
Saoud Rizwan c667d34f27 Fix issue where checkpointing blocked UI (#5177)
* Fix issue where checkpointing blocked UI

* Create wet-fishes-study.md
2025-07-25 23:24:39 -07:00
Sarah Fortune a1bf1f95a3 refactor(proto): Align proto directory structure with package names to follow best practices (#5171)
* Reorganized proto directory structure to match package naming convention

Moved cline package protos from the proto directory to proto/cline/ directory
Host package protos remain in proto/host/ directory

Updated all import statements across codebase to reflect new proto paths

Removed proto linter exception for package/directory mismatch rule

Fix Vscode proto indexing errors by setting the proto path in the Vscode settings.

* Update imports to use new package

Update imports from @shared/proto/<thing> to @share/proto/cline/<thing>
2025-07-26 00:58:16 -04:00
Bee 5da75af616 Fix Qwen API options inconsistency (#5162)
* Fix Qwen API option inconsistency

Refactor Qwen API region handling with enum and improved type safety

Changes:

- Replace string literals with QwenApiRegions enum for better type safety
- Add default region initialization in QwenHandler constructor
- Extract useChinaApi() method for cleaner conditional logic
- Update UI dropdown to use enum values with proper memoization
- Improve code maintainability and reduce magic strings

* changeset added

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix type with conversion

* Refactor Qwen model defaults to use first model dynamically

Move type definitions and enums after model objects and set default
models by selecting the first key from each model object instead of
hardcoding specific model IDs.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-25 11:15:01 -07:00
Sarah Fortune 38babe12f7 Replace vscode show message API calls with the host bridge (#5161) 2025-07-25 11:48:16 -04:00
Sarah Fortune 66fb8835a4 Add an RPC to the host bridge to scroll the diff viewer. (#5151) 2025-07-24 23:09:33 -04:00
Bee 214e157360 Fix organization state reset when switching between accounts (#5154)
* Fix organization state reset when switching between accounts

Move user authentication check into getUserOrganizations callback to properly reset state when switching between personal and organization accounts. This prevents stale organization data from persisting across account switches.

* add changeset

* Add error handling and refactor credit display components

Fix issues with balance display out of sync on org change or when API calls received 405 (rate limited) error

- Add error handling for failed API calls in getUserCredits and getOrganizationCredits
- Extract animated credit display logic into reusable StyledCreditDisplay component
- Simplify AccountView by removing inline credit animation code
- Improve organization state management and loading behavior

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* format

* reset on mount

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-24 19:59:42 -07:00
Igor Tceglevskii 7a9dce4f86 Moved open and visible tab retrieval to a separate hostbridge module (#5150) 2025-07-24 17:46:31 -07:00
Tomás Barreiro af7e3a4d20 Change the CLAUDE_CODE_MAX_OUTPUT_TOKENS (#5142)
* Change the CLAUDE_CODE_MAX_OUTPUT_TOKENS

* Add changeset

* Add comment and variable to explain the changes
2025-07-24 09:24:30 -07:00
schardosin 3abdc9ad0f Fixed issue affecting first-time credential entry for SAP AI Core (#5132)
* removed the reduced mask, which was making the client secret to fail in the first save

* added changeset
2025-07-23 22:19:47 -07:00
Ara 28b15d8b9b Adding gitbash terminal support and docs for solving windows terminal issues (#5110)
* Adding gitbash terminal support and docs for solving windows terminal issues

* Update docs/troubleshooting/terminal-integration-guide.mdx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 14:07:02 -07:00
github-actions[bot] bc6b3e54be v3.20.1 Release Notes (#5128)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-23 11:46:55 -07:00
canvrno 9fad0aa4ae Fix file deletion bug (#5125)
* Fix for files being deleted when switching modes or closing tasks

* changeset

* Added check to see if we are waiting for API response

* More targetted fix

* Create hot-onions-promise.md

* Delete .changeset/hot-onions-promise.md

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-23 11:44:24 -07:00
Ara 23fea0e16b Stop auto focus of Cline window on Every update (#5117) 2025-07-23 04:03:28 -07:00
Bee e8aaa61494 Improve auth state management for account view (#5107)
- Fix AccountView state management when user is not authenticated or is authenticated after the webview is loaded
- Add proper loading state reset and conditional data fetching
2025-07-22 20:00:14 -07:00
github-actions[bot] d93080304c v3.20.0 Release Notes (#5096)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and version for 3.20.0 release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-22 19:52:16 -07:00
Ara 1ce5f72bc1 Updating the new release announcement note (#5101)
* Updating the new release announcement note

* Adding gitbash terminal support and docs for solving windows terminal issues
2025-07-22 19:37:44 -07:00
Saoud Rizwan fb676add2e Fix hugging face model description (#5105) 2025-07-22 19:06:51 -07:00
Ara 31cda0ce5e Adding support for new models Qwen 3 models on Qwen provider (#5106) 2025-07-22 19:00:49 -07:00
Ara e173dad69c Updating the ordering to move cerebras provider upwards in the list of providers (#5102) 2025-07-22 16:22:52 -07:00
Toshii 92f32522c5 devtral medium (#5100) 2025-07-22 16:07:00 -07:00
Bee 7817d5f261 Host bridge migration: showInputBox (#4747)
* Update showInputBox

* Simplify

* Remove undefined handler for ShowInputBoxResponse
2025-07-22 13:13:03 -07:00
Bee db6d288efa Display credit balance for all accounts (#4992)
* Display credit balance for all accounts

The credit balance display was previously only shown for personal accounts. This change removes the check for `activeOrganization === null` and displays the credit balance and "Add Credits" button for all account types, including organization accounts. A divider is added above the balance section for visual separation once the backend change is deployed.

* changeset

* Improve refresh logic

Refactors the `AccountView` component to properly display and manage credits for both user and organization accounts. It introduces the `getOrganizationCredits` API call to fetch organization-specific credits and updates the UI accordingly. The refresh logic has also been improved to ensure data consistency and prevent unnecessary API calls.

Key changes:

- Implemented `getOrganizationCredits` to fetch credits for the active organization.
- Modified the credit display to show organization credits when an organization is active.
- Updated the refresh logic to use `useCallback` and `debounce` for better performance and to prevent race conditions.
- Added a periodic refresh to update account data every 30 seconds.
- Improved error handling and loading state management.
- Removed the interval ref and replaced it with a simpler useEffect for periodic refresh.
- Added last fetch time to the UI.

* clean up

* deepEqual

* org management

* prevent race condition
2025-07-22 12:03:37 -07:00
github-actions[bot] 87ff00d87e v3.19.8 Release Notes (#5022)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for v3.19.8 release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-22 11:35:39 -07:00
Sarah Fortune e602efc7a6 Dont export the protobus handlers from the grpc-client protobus-services.ts (#5091) 2025-07-22 01:30:01 -07:00
kvyb e6462af336 automate announcement display for major.minor releases (#5081)
* automate announcement display for major.minor releases

* fix: simplify onDidShowAnnouncement
2025-07-21 23:35:27 -07:00
483 changed files with 16563 additions and 25262 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Improve cerebras Qwen model performance by removing thinking tokens from the model input
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixes the API Keys URL for Requesty
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Set gpt5 max tokens to 8_192 to fix 'context window exceeded' error
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Change available Cerebras models - limit to Qwen and llama 3.3 70b
-8
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: mcp servers are not started when disabled
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor Git commit message generation to support output streaming.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix issue where fallback request to retrieve cost was not using correct auth token
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Introduce Claude Code support on Windows and fix E2BIG issues
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Remove deprecated GPT-4.5 Preview
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding OpenAI context window exceeded error handling
+5
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Change Cerebras Qwen 3 32b context window from 16k to 64k
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
calibrate input token counts when using anthropic models of sap ai core provider
+1 -1
View File
@@ -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)
}
+3
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1,3 @@
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett @saito-sv @Garoth
/docs/
/.github/ @saoudrizwan @dcbartlett
/README.md @saoudrizwan @nickbaumann98
+12 -18
View File
@@ -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
+4 -5
View File
@@ -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
View File
@@ -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
+3 -5
View File
@@ -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"
}
]
}
+5 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -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.**
-1
View File
@@ -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
+13 -1
View File
@@ -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 -4
View File
@@ -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.
+1
View File
@@ -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)
+1
View File
@@ -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`
-1
View File
@@ -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'
+2 -2
View File
@@ -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
+12 -7
View File
@@ -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",
},
],
},
],
})
+3
View File
@@ -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 -14
View File
@@ -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 {
+144
View File
@@ -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)
},
}
},
})
+3
View File
@@ -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
+1
View File
@@ -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")
+30 -19
View File
@@ -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,
+17 -6
View File
@@ -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) => ({
+107 -7
View File
@@ -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**
+1
View File
@@ -104,5 +104,6 @@ export interface TestInput {
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
diffApplyFile?: string
provider?: string
isVerbose: boolean
}
+236 -16829
View File
File diff suppressed because it is too large Load Diff
+13 -9
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.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
View File
@@ -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;
}
+13 -3
View File
@@ -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 -1
View File
@@ -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;
+41 -18
View File
@@ -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 -1
View File
@@ -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;
+5 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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 {}
+3 -3
View File
@@ -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);
}
+17
View File
@@ -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;
}
+1 -1
View File
@@ -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
View File
@@ -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;
}
+49 -5
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env node
const { execSync } = require("child_process")
const esbuild = require("esbuild")
+8 -6
View File
@@ -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)
})
}
+12 -6
View File
@@ -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)
})
}
+8 -4
View File
@@ -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
View File
+28 -7
View File
@@ -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,
+85 -84
View File
@@ -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/)
// })
// })
})
+2
View File
@@ -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":
+165
View File
@@ -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
}
}
+63 -5
View File
@@ -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
View File
@@ -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
+53
View File
@@ -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
}
+6 -6
View File
@@ -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.")
}
}
}
+132
View File
@@ -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
View File
@@ -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,
+14 -2
View File
@@ -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}`)
}
+27
View File
@@ -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,
+16 -42
View File
@@ -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
+15 -7
View File
@@ -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],
+4 -1
View File
@@ -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
View File
@@ -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,
}
})
}
}
+2 -1
View File
@@ -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":
+34 -76
View File
@@ -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 multiMB 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 = ""
+3
View File
@@ -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
View File
@@ -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()
}
+2 -1
View File
@@ -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