Compare commits

..
Author SHA1 Message Date
abeatrix 66b1b86d04 feat: add draggable threshold adjustment to AutoCondenseMarker
Add interactive drag functionality to adjust the auto-condense threshold marker. This includes:

- New props for threshold change callback and progress bar reference
- Mouse event handlers for drag operations with global listeners
- Separate wider hit area (12px) for easier interaction and visible 1px marker line
- Visual feedback during dragging (opacity change and bold label)
- Percentage display shown during drag interaction

This allows users to dynamically adjust the threshold by dragging the marker on the progress bar.
2025-11-20 15:51:08 -08:00
abeatrix 3633608c3f feat: integrate auto condense feature flag with user settings
Add feature flag support for AUTO_CONDENSE to allow independent control over the auto condense feature through both user settings and feature flags for enabling auto condense threshold for testing.

Changes:
- Add AUTO_CONDENSE feature flag definition and default value (dev-only)
- Create getUseAutoCondenseEnabled() method in FeatureFlagsService
- Update useAutoCondense state structure to track both user preference and feature flag status
- Modify ExtensionState type to use ClineFeatureSetting for useAutoCondense
- Update state initialization to use feature flag as fallback default value
2025-11-20 15:40:46 -08:00
canvrno 0b56a45a65 Add thinking level setting for Gemini 3.0 Pro (#7539)
* Added thinking level setting for Gemini 3.0 Pro

* changset
2025-11-20 10:48:31 -08:00
Bee d6ebd2438a fix: parse mentions/commands in tool results and before auto-condense (#7575)
* fix: parse mentions/commands in tool results and before auto-condense

**Changes:**
- Move `loadContext` call before auto-condense check to ensure slash commands and mentions are parsed before context condensing occurs
- Extract parsing logic into reusable `parseInputBlock` helper function
- Add recursive handling for `tool_result` blocks containing nested content arrays
- Remove duplicate `loadContext` calls from conditional branches

**Why:**
Previously, mentions (@file.ts) and slash commands in tool results (like attempt_completion feedback) were not expanded because parsing only handled top-level text blocks. Tool handlers return content arrays within tool_result blocks per Anthropic's API format.

Additionally, `loadContext` was called after the auto-condense check, meaning if condensing was triggered, user commands wouldn't be parsed and could be lost during summarization.

**Result:**
- All user feedback with @mentions or /commands is properly expanded regardless of nesting level
- Commands are detected before context management operations
- Cleaner code flow with single parsing point

* preserve array structure in backward-compatible tool results

When using the backward-compatible "cline" tool use ID, spread array
content directly into userMessageContent instead of wrapping it in
createToolResultBlock. This prevents array content from being
JSON.stringify'd and losing its block structure (e.g., tool_result
blocks with array content).

Previously, array content like [{type: "tool_result", content: [...]}]
was being converted to {type: "text", text: "[...]"}, which prevented
loadContext from properly parsing tool_result blocks.

* clean up

* fix(task): preserve block structure when processing string content

Instead of returning only the processed content, now properly updates the
block.content property with the processed text wrapped in an array and
returns the complete block object. This ensures the block structure is
maintained throughout the processing pipeline rather than being discarded.
2025-11-20 10:38:27 -08:00
Alex KerandAlexKer 4c07c7e5c5 added Kimi K2 Thinking to static models list and set as default (#7511)
* kimik2 thinking added to static model dropdown

* numerical separators

* don't set kimi k2 thinking as default

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-11-20 00:58:19 -08:00
Bee ab66a5fd93 fix: disable switch for required rules to prevent toggling (#7580)
Update RuleRow component to use isDisabled for both the switch disabled state and tooltip visibility, ensuring required rules cannot be toggled off by users. Previously used separate logic that may have miscalibrated disabling for required remote rules.
2025-11-19 23:25:30 -08:00
celestial-vault bd1d6159fc automatically derive openrouter modelinfo based on modelId when calling getModel() (#7568) 2025-11-19 21:16:48 -08:00
Bee 7f2d28716f chore: clear onboarding models on deactivate (#7569) 2025-11-19 18:01:58 -08:00
Toshii 653727db2a updating parser for webfetch to handle current format (#7571) 2025-11-19 17:44:01 -08:00
Bee d3c2f1878d fix(api): attach reasoning details to tool blocks (#7567)
* fix: Centralize reasoning details within thinking blocks

This commit refactors how reasoning details are managed across the system, integrating them directly into `ClineAssistantThinkingBlock` to improve consistency and reduce complexity.

Previously, `reasoning_details` were often explicitly deleted or inconsistently handled, leading to their loss or difficulty in tracking. This change ensures that reasoning details are always associated with their corresponding thinking blocks.

Key changes include:
- `StreamResponseHandler`: The `ReasoningHandler`'s `getCurrentReasoning` method now directly returns a `ClineAssistantThinkingBlock` which encapsulates the reasoning content and its `summary` (formerly `details`). The separate `getThinkingBlock` method has been removed.
- `convertToOpenAiMessages`: Explicit deletion of `part.reasoning_details` for `thinking` parts is replaced by setting it to `undefined` with a comment, indicating that these details are now expected to be part of the thinking block in the stream.
- `Task`: Simplified streaming logic by directly consuming the `ClineAssistantThinkingBlock` from `reasonsHandler.getCurrentReasoning()`. Redundant temporary variables for reasoning content and details have been removed.

This refactoring centralizes the management of reasoning details, providing a more robust and streamlined approach to handling assistant thinking processes.

* fix(api): attach reasoning details to tool blocks and improve logging

Updates validity of reasoning details within tool blocks and enhances debugging visibility.

- Modify `StreamResponseHandler` to append reasoning details/summary to finalized tool use blocks.
- Update `convertToOpenAiMessages` to extract and aggregate `reasoning_details` from tool messages instead of discarding them.
- Add `Logger.debug` calls in `ClineHandler` and OpenAI transformation for better observability of message chunks and conversion.
- Remove redundant `continue` statements in `ClineHandler` stream processing loop.

* clean up

* remove Logger
2025-11-19 17:28:20 -08:00
CellenLee ba92be9401 feat: add kimi-k2-thinking and kimi-k2-thinking-turbo (#7386) 2025-11-19 15:49:46 -08:00
Bee 66eb5a62ba feat: Enable native tool calling for Baseten and Kimi K2 models (#7562)
* feat: Enable native tool calling for Baseten and Kimi K2 models

Introduces native tool calling capabilities for Baseten and Kimi K2 models, aligning with the OpenAI Chat Completions API specification for function calling.

This change includes:
- Updating the `ApiHandler` interface and `createMessage` methods to accept an optional `tools` parameter.
- Implementing a `ToolCallProcessor` to incrementally build and emit tool call payloads from streaming deltas.
- Modifying the `BasetenHandler` to pass `tools` to the Baseten API and process `tool_calls` deltas.
- Updating the `ClineHandler` to process `tool_calls` deltas received from the Kimi K2 model.
- Enhancing the `CompletionStreamChunk` to include `ToolCall` and `ToolCallDelta` types.
- Marking Baseten and Kimi K2 models in their respective definitions with `native_tool_calling: true`.
- Adjustments to streaming logic in handlers to allow multiple delta types (content, tool_calls, reasoning) to be processed from a single chunk.

* add changeset

* clean up
2025-11-19 15:37:10 -08:00
Bee 499ee22b3b fix(task): update UI with final usage after stream completion (#7552)
Ensure the UI displays accurate token usage and costs by updating the API request message when the stream completes. This commit adds a call to updateApiReqMsg followed by saving messages and posting state to the webview, which occurs before finalizing tool calls. This ensures users see the final usage statistics (input/output tokens, cache tokens, and total cost) reflected in the interface immediately after stream processing.
2025-11-19 02:54:31 -08:00
CandiedUniverse 7abeae5019 feat(hooks): Implement TaskComplete hook (#7510) 2025-11-18 23:43:57 -08:00
Bee accf47cb52 fix: await presentAssistantMessage calls to prevent race condition (#7548)
* fix: await presentAssistantMessage calls to prevent race condition

Add await to all presentAssistantMessage() calls to ensure proper
sequencing of message presentation. Previously, the method was called
without awaiting, which could cause race conditions when streaming
tool use content blocks. This ensures that message presentation
completes before continuing execution, particularly important when
handling multiple content blocks or tool interactions.

* revert pr change
2025-11-18 17:48:15 -08:00
Bee 556d3e6f79 fix: rules modal positioning (#7546)
Add overflow-y-auto to the modal container to allow scrolling when content exceeds viewport height. This fixes an issue where modal content would be inaccessible on smaller screens or with large rule sets.
2025-11-18 16:23:48 -08:00
Bee 9e35048db2 feat: add support for Responses API for openai-native provider [ENG-1227] [ENG-1311] (#7504)
* feat: add support for Responses API for openai-native provider [ENG-1227]

- Upgrade openai dependency to v6.9.0 to use the Responses API
- Implement internal handling for reasoning/thinking and redacted output
- Align Anthropic handler message types with the latest SDK interfaces
- Clean up obsolete tooling imports related to tool-use handling
- Enable newer OpenAI capabilities while keeping provider APIs consistent

* Add openai_native_response_api feature flag

* clean up

* clean up 2

* add back gpt-5.1 models

* use call_id
2025-11-18 16:20:45 -08:00
Ara 9b0f2b82ef v3.38.1 Release Notes (#7544) 2025-11-18 14:32:41 -08:00
BeeandArafatkatze c2c23054b9 fix: Remove 'signature' from sanitizeAnthropicContentBlock (#7543)
* fix: Remove 'signature' from sanitizeAnthropicContentBlock

Remove 'signature' from sanitizeAnthropicContentBlock as the signature field is required by Anthropic when thinking is enabled.

* Add Changeset

* empty commit

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-18 14:10:34 -08:00
Beeandellipsis-dev[bot] 3baaa5c8b4 refactor: replace custom UI toggle with shadcn Switch component (#7308)
* refactor(webview-ui): replace custom UI toggle with shadcn Switch component

- Add @radix-ui/react-switch dependency (v1.2.6) https://ui.shadcn.com/docs/components/switch
- Refactor ClineRulesToggleModal to use Radix Switch instead of VSCode buttons
- Improve button styling with reduced padding and adjusted icon sizes
- Enhance form layout with conditional rendering based on expansion state
- Update input field styling with better focus states and border handling

This change provides a more consistent UI experience by leveraging Radix UI's
accessible Switch component while maintaining the same functionality.

* clean up

* clean up

* update switch color

* adjust

* revert unrelated changes

* size

* toggle

* Update webview-ui/src/components/cline-rules/RuleRow.tsx

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-11-18 12:39:50 -08:00
github-actions[bot]andArafatkatze abafcc7290 Changeset version bump (#7473)
* v3.38.0 Release Notes

- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation

- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- SAP AI SDK JS packages upgraded to latest major version
- SAP provider OrchestrationClient now matches OrchestrationModuleConfig type and no longer uses invalid promptTemplating property
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation

* Update CHANGELOG.md

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-18 12:15:10 -08:00
Bee d82aa0add9 feat: add Gemini 3.0 Pro to onboarding list (#7542)
- Add `google/gemini-3-pro-preview` to `CLINE_ONBOARDING_MODELS`.
- Configure model details including context window, pricing, and capabilities (images, prompt cache).
- Enable users to select the new Gemini 3.0 Pro model during setup.
2025-11-18 11:49:08 -08:00
Bee abe4721a0b feat: add thought signature support for Gemini SDK [ENG-1320] (#7536)
* feat: add thought signature support for Gemini SDK

Update @google/genai dependency from v1.15.0 to v1.30.0, including nested deps like google-auth-library. Enhance API interfaces with JSDoc comments and new fields such as signature, id, and redacted_data in ApiStreamThinkingChunk to support thought signatures from Gemini SDK as requested. This improves integration with Gemini's reasoning capabilities and ensures compatibility with updated SDK features.

* add changeset

* meaning val check

* typo

* either

* Do not use think budget with gemini-3
2025-11-18 10:57:53 -08:00
Bee 60d55b69a8 fix: Only update reasoning UI when content changes (#7540)
This commit addresses two issues related to how reasoning messages are processed and displayed.

Previously, the `say` function was called on every iteration of the reasoning stream loop, even if the current chunk contained no new reasoning content. This caused unnecessary UI updates and could lead to errors if a task was cancelled mid-stream. The `say` call is now conditional, only executing when new `chunk.reasoning` is available.

Additionally, the final reasoning block was only appended to the assistant's message history if a signature was present. This meant reasoning could be lost from the UI if the task was cancelled before a signature was generated. The logic is now updated to append the reasoning block if either a message or a signature exists.
2025-11-18 10:57:11 -08:00
Ara d18e0271d3 Fix cancellation for background terminal commands (#7521)
* refactor(task): improve background command cancellation with better error handling

Enhance the cancelBackgroundCommand method with:
- Consolidated early return conditions for cleaner code
- Proper async/await for process termination
- Comprehensive error handling with try-catch blocks for each operation
- Improved logging for termination success/failure scenarios
- Updated cancellation notification message
- Use finally block to ensure notification is always sent

Improve StandaloneTerminalProcess.terminate() with:
- Better guard clauses and early returns
- Enhanced error handling for SIGTERM and SIGKILL operations
- More detailed logging for graceful vs forced termination
- Fallback to SIGKILL if SIGTERM fails immediately

Fix critical issue where terminate() method was not accessible on the merged promise object returned by executeCommand, preventing Task.cancelBackgroundCommand() from properly killing background processes.

* Fix: add cancel ui

* Fix: add cancel ui

* Fix: add cancel ui

* Fix: add cancel ui
2025-11-18 10:41:58 -08:00
Ara af71f9da90 fix: resolve double quote escaping in Windows cmd.exe for Background Exec mode (#7523)
Fixes #7470

When Terminal Execution Mode is set to "Background Exec", commands with
double quotes were being incorrectly escaped on Windows cmd.exe, causing
commands like `echo "\""` or `type "test.txt"` to fail.

The issue was that cmd.exe requires the /s flag and outer quotes when
passing commands with special characters via spawn(). Changed from
`["/c", command]` to `["/s", "/c", `"${command}"`]` for cmd.exe only.

This is a minimal Windows-specific fix that:
- Only affects Windows cmd.exe (PowerShell and Unix shells unchanged)
- Uses standard Windows cmd.exe syntax for proper quote handling
- No changes to process execution flow or behavior
2025-11-18 10:17:37 -08:00
canvrno 2a1c8826aa Add Gemini 3.0 to featuredModels (#7537) 2025-11-18 10:13:06 -08:00
Ara 9a54f2d246 fix(auth): enable provider persistence when applying model changes (#7530)
- Change `UpdateProviderPartial` persist flag from false to true in `applyModelChange`
- Add missing newline at end of state.proto file

This ensures that model changes are properly persisted to storage when users
update their provider configuration through the wizard.
2025-11-18 10:05:01 -08:00
canvrnoandBee d928d58a40 Feat: Gemini 3.0 prompt/tool changes (#7532)
* Enhanced Gemini 3.0 support in Cline

* Updated Gemini 3.0 snapshots

* Update src/utils/model-utils.ts

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

* Updated system prompt

* Update src/core/api/providers/gemini.ts

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

* Pricing change, narrowed native tool spec to just gemini 3 on vertex

* Update src/core/prompts/system-prompt/registry/ClineToolSet.ts

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

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-11-18 10:02:43 -08:00
canvrno 31859b5fda Add Gemini 3.0 to Gemini provider (#7533)
* Added Gemini 3.0 to Gemnini provider

* Add thinking option for Gemini 3.0
2025-11-18 09:20:19 -08:00
Ara 0d5d89e8c7 feat(bedrock): add context window error detection and retry handling (#7515)
* feat(bedrock): add context window error detection and retry handling

Add proper context window error detection for AWS Bedrock provider to enable automatic retry with context truncation. Previously, context window errors were yielded as error text instead of being thrown, preventing the retry mechanism from handling them.

Changes:
- Detect ValidationException errors matching context window patterns in both Converse API and stream processing
- Throw context window errors instead of yielding them as text to trigger retry logic
- Add checkIsBedrockContextWindowError() function to identify Bedrock-specific context limit errors
- Support multiple error message patterns (input too long, context exceed, maximum tokens, etc.)
- Handle nested error structures from Vercel AI SDK and AWS SDK

This enables automatic context management when Bedrock models hit token limits, improving reliability and user experience.

* Fix: raise errors
2025-11-18 09:09:47 -08:00
Bee af34451eec fix: remove h-full from TaskTimeline (#7525) 2025-11-18 02:10:35 -08:00
Bee 027a4f6386 fix: remove automatic native tool calls inference (#7522)
Remove automatic enablement of native tool calls for next-gen models and providers. The feature should be controlled exclusively by explicit user settings (feature flag and global state) rather than being automatically inferred based on the model type during experimental state.

Changes:
- Removed `isNextGenModelProvider` import (no longer needed)
- Eliminated `inferredNativeToolCalls` logic that auto-enabled the feature for next-gen models
- Simplified `enableNativeToolCalls` to only check explicit feature flag and global state settings
- Makes behavior more predictable and user-controlled
2025-11-18 00:49:36 -08:00
Bee 49642882c5 fix: ensure tool arguments are streamed during native tool calling [ENG-1305] (#7508)
* fix: ensure tool arguments are streamed during file operations

- Update userMessageContentReady condition to include streaming tool arguments, not just new content blocks
- Add null check for input object in tool-use-handler to prevent errors
- Improve partial JSON parsing with better fallback handling
- Replace console.log with Logger.debug for tool call chunks
- Add clarifying comments for lock mechanism and streaming behavior

This fixes an issue where new file content was not being properly streamed to tools during write operations, causing the UI to stop updating while tool arguments were being received.

* Add changeset

* typo

* fix(task): reset content index to execute tool blocks during streaming

Reset the currentStreamingContentIndex to the first tool block position
when tool blocks are present in the assistant message. This ensures that
tool blocks are properly executed instead of being skipped when the index
advances past them or goes out of bounds during content streaming.

Previously, the index could advance beyond tool blocks, causing them to
remain unexecuted. Now, when tool blocks are detected, the index is
explicitly set to textBlocks.length (the start of tool blocks) and
userMessageContentReady is set to false to trigger execution.

* fix(task): reset stream index to enable tool block execution

Reset currentStreamingContentIndex to the first tool block position when
tool blocks are present in the assistant message. This ensures that
presentAssistantMessage processes tool blocks instead of text blocks
during streaming, allowing tool blocks to be executed properly while
streaming is in progress.

The index is set to textBlocks.length, which points to where tool blocks
start in the content array, enabling correct sequential processing of
tools during the streaming phase.

* fix(streaming): improve tool execution flow and prevent control flow fall-through

- Add continue statements after yielding content in cline provider to prevent unintended fall-through behavior
- Mark all streamed tool uses as partial to ensure proper state tracking
- Allow complete tool blocks to bypass presentation lock for immediate execution during streaming
- Simplify userMessageContentReady reset logic and remove redundant tool_call check

These changes improve tool execution responsiveness by allowing completed tools to execute without waiting for the presentation lock, while ensuring proper control flow and state management throughout the streaming process.

* revert WriteToFileToolHandler
2025-11-17 23:50:04 -08:00
BeeandCopilot 21ed6bc432 fix: do not add MCP tool with invalid names as native tools (#7516)
* fix: do not add MCP tool with invalid names as native tools

- Filter out MCP tools with names >= 64 characters to avoid provider API rejection
- Reduce nanoid length from default (21) to 5 characters for server UIDs

Provider APIs reject tool registration when tool names exceed 64 characters.
This change prevents registration errors by skipping tools with long names
and generating shorter UIDs to minimize the constructed name length
(uid + identifier + tool name).

* Add Changeset

* Update src/services/mcp/McpHub.ts

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

* Update src/core/prompts/system-prompt/registry/ClineToolSet.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 23:24:17 -08:00
Bee da2689f885 fix: correct TaskTimeline height (#7520)
* fix: correct TaskTimeline height

- Remove TIMELINE_HEIGHT constant in favor of h-full and h-4 utilities
- Replace inline styles with Tailwind classes for better maintainability
- Change timeline blocks from rounded-xs to rounded-full for consistency
- Fix timeline display being cut off due to incorrect height constraints

This refactor resolves the visual layout issue where timeline items were
truncated while improving code consistency by leveraging Tailwind's
utility-first approach throughout the component.

* add changeset
2025-11-17 23:05:09 -08:00
CandiedUniverse 5049326f02 fix(hooks): Fix two minor cancel-resume issues (#7502)
* fix(hooks): Fix cancel: true returned by TaskResume

* fix(hooks): Prevent TaskCancel from being triggered twice by TaskStart cancel and by TaskResume cancel scenarios
2025-11-17 14:44:16 -08:00
Ara b02ce46a57 Fix: Vercel provider token usage (#7481) 2025-11-17 14:15:32 -08:00
Saoud Rizwan de974737c8 fix: improve layout and styling in OnboardingView component for small width viewport (#7391) 2025-11-17 13:35:13 -08:00
Bee d072156e9a fix(account): memoize credits history table component (#7439)
Use React.memo to wrap CreditsHistoryTable, reducing unnecessary re-renders
when props are unchanged and improving performance of the account view that makes it looks like it glinches.
2025-11-17 11:36:19 -08:00
celestial-vault 4939309a09 fix openrouter defaulting modelId when modelInfo is not present (#7482) 2025-11-15 13:50:29 -08:00
CandiedUniverse 1a07ca7906 fix(hooks): Honor '"cancel": true' in hook JSON output (#7479) 2025-11-14 20:37:16 -08:00
Bee c1eefbad3f refactor(api): unify provider message type with ClineStorageMessage (#7478)
* refactor: replace Anthropic MessageParam with ClineStorageMessage type

Replace Anthropic SDK's MessageParam type with the new ClineStorageMessage type across API providers and tests in the effort of storing api messages in a type safe environment that we can expand from and avoid adding undocumented properties to Anthropc Message type that are not visible to the downstream services.

This change:

- Removes dependency on @anthropic-ai/sdk types in multiple providers
- Introduces ClineStorageMessage from shared messages module
- Updates method signatures in Dify, OpenAI, LiteLLM, and ClaudeCode handlers
- Updates corresponding test files to use the new type

This decouples the codebase from Anthropic-specific types and standardizes message handling using an internal storage format across all providers that  improves type-safety, preparing for the properties added by the Response API use.

As ClineStorageMessage is an extension of the Anthropic Message type, everything should work the same with no breaking changes. Green CI is expected.

* clean up
2025-11-14 20:24:56 -08:00
canvrnoandBee 1bfdce9b84 Remove new_task from system prompts (#7350)
* Removed new_task from system prompts, updated slash command prompt, added helper function for native tool calling checks

* Update src/core/prompts/system-prompt/registry/PromptBuilder.ts

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

* Update src/core/task/index.ts

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

* Updates with requested changes for PR #7350

* Updated package-lock.json

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-11-14 17:57:47 -08:00
canvrno b002cdacdb Maint: package updates (#7477)
* maint: package updates

* Updated download-ripgrep script for compatability with new tar dependency
2025-11-14 16:09:03 -08:00
CandiedUniverse cf4005b25e fix(hooks): Reorder the UI elements so that PreToolUse appears above tool (#7449)
* fix(hooks): Reorder the UI elements so that PreToolUse appears above tool

* fix(hooks): Prevent PreToolUse hook from migrating down the screen

* fix(hooks): PreToolUse reordering should apply to 'tool', 'command', 'use_mcp_server', and 'browser_action_launch'  message types
2025-11-14 15:40:23 -08:00
Bee 1494d145d5 feat: support feature flag payload & remote dynamic onboarding model list (#7454)
* feat: support feature flag payload & dynamic onboarding model list

- Updated proto to use OnboardingModelGroup instead of bool flag for flexible onboarding
- Added getClineOnboardingModels function with caching and remote overrides for dynamic model fetching
- Modified controller to fetch and pass onboarding models to webview
- Updated UI to use dynamic models for selection, enabling flexible onboarding
- Enhanced feature flag service to support non-boolean payloads for better configurability

* clearOnboardingModelsCache
2025-11-14 14:49:05 -08:00
canvrno 1ab4b3cc24 fix:SAP provider type error - See PR #6547 (#7475) 2025-11-14 14:15:13 -08:00
canvrno 535b653228 Added stronger prompting around the use of act_mode_respond (#7448) 2025-11-14 12:12:34 -08:00
Igor Tceglevskii 1335fa5452 Retire firebase (#7362) 2025-11-14 09:29:47 -08:00
yuvalman b2a4395f71 feat: upgrade sap ai-sdk-js packages major version (#6547)
* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version
2025-11-14 09:11:03 -08:00
Toshii ae34a3a8c5 adding state variable for clineWebToolsEnabled (noop) (#7455)
* adding state variable for clineWebToolsEnabled

* removing console log
2025-11-14 08:20:01 -08:00
0fb4a6c7e9 v3.37.1 Release Notes (#7451)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for version 3.37.1

---------

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-11-13 17:42:42 -08:00
Saoud Rizwan 855db7d8d8 feat(models): Add free minimax/mimax-m2 model to the model picker (#7453)
* feat(models): Add free minimax/mimax-m2 model to the model picker

* Add free minimax/mimax-m2 model to model picker
2025-11-13 17:41:16 -08:00
CandiedUniverse bb375b78ca fix(hooks): Prevent PreToolUse hook from running before attempt_completion tool (#7450) 2025-11-13 16:14:13 -08:00
CandiedUniverse 31af254f0a fix(hooks): Run PreToolUse only after approval (#7446)
* fix(hooks): Run PreToolUse only after approval; get it working for read_file first

* fix(hooks): Run PreToolUse only after approval; get it working for six more tools now

* fix(hooks): Run PreToolUse only after approval; get it working for the remaining six tools now

* fix(hooks): Implement HookExecution type to avoid using 'any'
2025-11-13 15:41:57 -08:00
github-actions[bot]andArafatkatze 2745cdd54b Changeset version bump (#7358)
* v3.37.0 Release Notes

- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)

- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion

- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)

* Adding image optimizations

* Adding image optimizations

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-13 11:47:42 -08:00
celestial-vault c94c6fd8f1 Fix CLI auth state persistence with explicit flush mechanism (#7445)
Fixes issue where 'cline auth' command would lose all configuration due to process termination race condition.

Root cause: StateManager uses 500ms debounced persistence, but CLI process terminated before setTimeout callback could fire.

Solution: Implemented explicit flushPendingState() mechanism:
- Added StateManager.flushPendingState() method for immediate persistence
- Refactored to extract shared persistence logic (DRY)
- Added flushPendingState gRPC endpoint
- CLI now calls flush instead of using 5s sleep workaround

Results:
- Deterministic persistence (no race condition)
- Faster auth flow (~2s vs 7s)
- Cleaner, more maintainable code
2025-11-13 11:34:17 -08:00
Ara a87664318e Adding Gpt-5.1 to model picker and onboarding list (#7444)
* feat(settings): add GPT-5.1 to Model picker featured models

* adding

* adding

* adding
2025-11-13 11:20:08 -08:00
AraandJuan Pablo ba70718c83 Remove Qwen3 coder from Cerebras providers (#7404)
* Remove Qwen3 coder from Cerebras providers

* Update Cerebras Docs to include new model `zai-glm-4.6` and remove `qwen-3-coder-480b-free` and `qwen-3-coder-480b` entries.

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
2025-11-13 11:13:25 -08:00
canvrno cf8dd1c150 Feat:Enhanced support for OpenAI GPT 5.1 (#7443)
* Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.

* Fix typos

* Fix more typos
2025-11-13 11:06:23 -08:00
Bee e714c74ec4 fix(anthropic): sanitize messages to SDK MessageParam shape (#7441)
- Use Anthropic SDK types and return Array<Messages.MessageParam>
- Replace removeReasoningDetails with removeUnknownParams
- Strip unsupported fields (e.g., reasoning_details) and normalize role
- Keep string content unchanged; map array content to known blocks
- Continue adding ephemeral cache_control to targeted user messages

This ensures only Anthropic-supported fields are sent, improving type safety and preventing API errors.
2025-11-13 10:28:40 -08:00
Sarah Fortune a6f8b33895 Change the label in the webview to say Enterprise Rules/Workflows instead of Remote Rules/Workflows (#7440) 2025-11-13 10:15:27 -08:00
Saoud RizwanandCopilot 02abbcf045 feat: add AGENTS.md support (#7437)
* feat: add AGENTS.md support

* Update webview-ui/src/components/cline-rules/RuleRow.tsx

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

* Update webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx

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

* Update font size for documentation link in ClineRulesToggleModal component

* docs: add support for AGENTS.md standard in Cline rules documentation

* fix: delete agents.md

* Add AGENTS.md support

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 09:42:24 -08:00
238 changed files with 7884 additions and 4923 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added Nous Research provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Prevents adding multiple tool results by adding existence check
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add AGENTS.md support
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix XML entity escaping in model content processor
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added change to hide the context window usage message from env details when using next gen models and before the usage has reached an elevated state
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix task timeline display height.
@@ -1,6 +0,0 @@
---
"claude-dev": patch
---
Docs: Add missing proto generation step in CONTRIBUTING.md and new `npm run dev` script for easier terminal workflow (fixes #7335)
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Created model-family breakouts for deep-planning prompting, and laid groundwork for similar changes for other slash commands.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Use HTTP proxies in more places
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Ensure tool arguments are streamed during file operations when native tool calling is enabled.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added thinking level setting for Gemini 3.0 Pro
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Feat: add thought signature support for Gemini SDK
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Feat: Enable native tool calling for Baseten and Kimi K2 models
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: restore commit msg generation functionality to command palette
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Nous Hermes 4 model family system prompt
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Kimi K2 Thinking to Baseten Provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix OpenAI Compatiblr provider to ensure temperature parameter is explicitly converted to number
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Skip MCP tool with invalid name (e.g. name too long) when native tool calling is enabled.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adjusted prompting around focus chain, particularly for next-get/native tool calling models.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix Anthropic provider missing signature param when thinking is enabled.
+3
View File
@@ -20,6 +20,9 @@ eslint-rules/**
.husky/**
.env
# cli
cli/**
# Custom
**/demo.gif
.nvmrc
+46
View File
@@ -1,5 +1,51 @@
# Changelog
## [3.38.1]
### Fixed
- Fixed handling of 'signature' field in sanitizeAnthropicContentBlock to properly preserve it when thinking is enabled, as required by Anthropic's API.
## [3.38.0]
### Added
- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation
### Fixed
- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation
## [3.37.1]
- Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
- Add AGENTS.md support
- feat(models): Add free minimax/mimax-m2 model to the model picker
## [3.37.0]
### Added
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Switched to Aqua Voice's Avalon model in speech to text transcription
- Added Linux support for speech to text
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
### Fixed
- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion
## Documentation
- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
## [3.36.1]
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
+5 -5
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
@@ -76,10 +75,11 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
}
}
// WORKAROUND: Wait for debounced state persistence to complete
// Fixes `cline auth` issue when ran in docker environments
// TODO: implement better solution w/ changes in StateManager
time.Sleep(600 * time.Millisecond)
// Flush pending state changes to disk immediately
// This ensures all configuration changes are persisted before the instance terminates
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
return fmt.Errorf("failed to flush pending state: %w", err)
}
// Success message
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
+1 -1
View File
@@ -517,7 +517,7 @@ func (pw *ProviderWizard) applyModelChange(provider cline.ApiProvider, modelID s
ModelInfo: modelInfo,
}
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, false)
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, true)
}
// SwitchToBYOProvider switches to a BYO provider that's already configured.
+1 -77
View File
@@ -221,83 +221,7 @@ func (p *ToolResultParser) ParseCodeDefinitions(content string) string {
// ParseWebFetch formats webFetch tool results with content preview
func (p *ToolResultParser) ParseWebFetch(content, url string) string {
if content == "" {
return fmt.Sprintf("*Fetched content from %s (empty response)*", url)
}
lines := strings.Split(content, "\n")
var result strings.Builder
// Try to extract title
var title string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "##") {
title = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
break
}
}
if title != "" {
result.WriteString(fmt.Sprintf("**Title:** %s\n\n", title))
}
// Show preview of content
result.WriteString("**Preview:**\n")
charCount := 0
maxChars := 500
previewLines := []string{}
for _, line := range lines {
// Skip markdown headers
if strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if charCount+len(trimmed) > maxChars {
break
}
previewLines = append(previewLines, trimmed)
charCount += len(trimmed)
}
result.WriteString(strings.Join(previewLines, " "))
result.WriteString("...\n\n")
// Extract sections
sections := []string{}
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "##") {
section := strings.TrimSpace(strings.TrimPrefix(trimmed, "##"))
sections = append(sections, section)
if len(sections) >= 5 {
break
}
}
}
if len(sections) > 0 {
result.WriteString("**Sections Found:**\n")
for _, section := range sections {
result.WriteString(fmt.Sprintf("- %s\n", section))
}
result.WriteString("\n")
}
// Word count estimate
wordCount := len(strings.Fields(content))
result.WriteString(fmt.Sprintf("*[Full content: ~%s]*", p.formatWordCount(wordCount)))
return result.String()
return ""
}
// detectLanguage returns syntax highlighting language based on file extension
+6 -34
View File
@@ -5146,28 +5146,6 @@
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -6490,9 +6468,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -10235,12 +10213,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -10609,9 +10581,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
+4
View File
@@ -14,5 +14,9 @@
"description": "",
"dependencies": {
"mintlify": "^4.2.23"
},
"overrides": {
"tar-fs": "^3.1.1",
"js-yaml": "^4.1.1"
}
}
+1 -2
View File
@@ -18,8 +18,7 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
Cline supports the following Cerebras models:
- `qwen-3-coder-480b-free` (Free tier) - High-performance coding model at no cost
- `qwen-3-coder-480b` - Flagship 480B parameter coding model
- `zai-glm-4.6` - Intelligent general purpose model with 1,500 tokens/s
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
- `llama-3.3-70b` - Meta's Llama 3.3 model optimized for speed
-1422
View File
File diff suppressed because it is too large Load Diff
+46 -45
View File
@@ -1,47 +1,48 @@
{
"name": "cline-evals",
"version": "0.1.0",
"description": "Evaluation scripts and tools for Cline",
"main": "cli/dist/index.js",
"scripts": {
"build:cli": "cd cli && tsc",
"start:cli": "cd cli && node dist/index.js",
"dev:cli": "cd cli && ts-node src/index.ts",
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cline",
"evaluation",
"benchmark",
"diff-edits"
],
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
"execa": "^5.1.1",
"node-fetch": "^2.7.0",
"ora": "^5.4.1",
"sqlite": "^4.1.2",
"tiktoken": "^1.0.21",
"uuid": "^9.0.0",
"yargs": "^17.6.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.3",
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.12",
"@types/uuid": "^9.0.0",
"@types/yargs": "^17.0.19",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"overrides": {
"tar-fs": "^3.1.1"
}
"name": "cline-evals",
"version": "0.1.0",
"description": "Evaluation scripts and tools for Cline",
"main": "cli/dist/index.js",
"scripts": {
"build:cli": "cd cli && tsc",
"start:cli": "cd cli && node dist/index.js",
"dev:cli": "cd cli && ts-node src/index.ts",
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cline",
"evaluation",
"benchmark",
"diff-edits"
],
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
"execa": "^5.1.1",
"node-fetch": "^2.7.0",
"ora": "^5.4.1",
"sqlite": "^4.1.2",
"tiktoken": "^1.0.21",
"uuid": "^9.0.0",
"yargs": "^17.6.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.3",
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.12",
"@types/uuid": "^9.0.0",
"@types/yargs": "^17.0.19",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"overrides": {
"tar-fs": "^3.1.1",
"js-yaml": "^4.1.1"
}
}
+407 -929
View File
File diff suppressed because it is too large Load Diff
+9 -8
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.36.1",
"version": "3.38.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -417,7 +417,7 @@
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^1.11.0",
"@google/genai": "^1.30.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
@@ -442,8 +442,8 @@
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@playwright/test": "^1.55.1",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sap-ai-sdk/ai-api": "^2.1.0",
"@sap-ai-sdk/orchestration": "^2.1.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
@@ -461,7 +461,6 @@
"exceljs": "^4.4.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.2.0",
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
@@ -471,7 +470,6 @@
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"nice-grpc": "^2.1.12",
@@ -479,7 +477,7 @@
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"openai": "^6.9.0",
"os-name": "^6.0.0",
"p-mutex": "^1.0.0",
"p-timeout": "^6.1.4",
@@ -504,7 +502,10 @@
"zod": "^3.24.2"
},
"overrides": {
"tar-fs": ">=3.1.1"
"tar-fs": ">=3.1.1",
"tar": "^7.5.2",
"vite": "^7.1.11",
"js-yaml": "^4.1.1"
},
"c8": {
"reporter": [
+3
View File
@@ -97,6 +97,7 @@ message OpenRouterModelInfo {
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
optional string name = 13;
optional double temperature = 14;
}
// Shared response message for model information
@@ -588,6 +589,7 @@ message ModelsApiConfiguration {
optional string plan_mode_aihubmix_model_id = 135;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
optional string plan_mode_nous_research_model_id = 137;
optional string gemini_plan_mode_thinking_level = 138;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -628,4 +630,5 @@ message ModelsApiConfiguration {
optional string act_mode_aihubmix_model_id = 235;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
optional string act_mode_nous_research_model_id = 237;
optional string gemini_act_mode_thinking_level = 238;
}
+16 -1
View File
@@ -31,6 +31,7 @@ service StateService {
rpc installClineCli(EmptyRequest) returns (Empty);
rpc checkCliInstallation(EmptyRequest) returns (Boolean);
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
rpc flushPendingState(EmptyRequest) returns (Empty);
}
message AutoApprovalActions {
@@ -361,7 +362,7 @@ message UpdateSettingsRequest {
optional int32 subagent_terminal_output_line_limit = 30;
optional string cline_env = 31;
optional bool native_tool_call_enabled = 32;
optional bool show_onboarding_flow = 33;
optional OnboardingModelGroup onboarding_models = 33;
}
message UpdateTerminalConnectionTimeoutRequest {
@@ -389,3 +390,17 @@ message OnboardingProgressRequest {
optional bool completed = 3;
optional string model_selected = 4;
}
message OnboardingModelGroup {
repeated OnboardingModel models = 1;
}
message OnboardingModel {
string id = 1;
string name = 2;
int32 score = 3;
int32 latency = 4;
string badge = 5;
string group = 6;
OpenRouterModelInfo info = 7;
}
+1
View File
@@ -32,6 +32,7 @@ enum ClineAsk {
CONDENSE = 13;
REPORT_BUG = 14;
SUMMARIZE_TASK = 15;
ACT_MODE_RESPOND = 16;
}
// Enum for ClineSay types
+1 -1
View File
@@ -11,7 +11,7 @@ import fs from "fs"
import https from "https"
import path from "path"
import { pipeline } from "stream/promises"
import tar from "tar"
import * as tar from "tar"
import { promisify } from "util"
import { createGunzip } from "zlib"
+3 -3
View File
@@ -1,5 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
@@ -33,7 +33,7 @@ export class DifyHandler implements ApiHandler {
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
@@ -255,7 +255,7 @@ export class DifyHandler implements ApiHandler {
}
}
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
private convertMessagesToQuery(systemPrompt: string, messages: ClineStorageMessage[]): string {
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
// The system prompt is typically configured in the Dify App itself.
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
-29
View File
@@ -9,14 +9,6 @@ export interface EnvironmentConfig {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
firebase: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId?: string
}
}
class ClineEndpoint {
@@ -63,14 +55,6 @@ class ClineEndpoint {
appBaseUrl: "https://staging-app.cline.bot",
apiBaseUrl: "https://core-api.staging.int.cline.bot",
mcpBaseUrl: "https://core-api.staging.int.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
authDomain: "cline-staging.firebaseapp.com",
projectId: "cline-staging",
storageBucket: "cline-staging.firebasestorage.app",
messagingSenderId: "853479478430",
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
},
}
case Environment.local:
return {
@@ -78,11 +62,6 @@ class ClineEndpoint {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
authDomain: "cline-preview.firebaseapp.com",
projectId: "cline-preview",
},
}
default:
return {
@@ -90,14 +69,6 @@ class ClineEndpoint {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
authDomain: "cline-prod.firebaseapp.com",
projectId: "cline-prod",
storageBucket: "cline-prod.firebasestorage.app",
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
}
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ClineTool } from "@/shared/tools"
import { AIhubmixHandler } from "./providers/aihubmix"
import { AnthropicHandler } from "./providers/anthropic"
@@ -47,9 +47,8 @@ import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
export type CommonApiHandlerOptions = {
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
}
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream
createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[], useResponseApi?: boolean): ApiStream
getModel(): ApiHandlerModel
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
}
@@ -95,6 +94,7 @@ function createHandlerForProvider(
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
})
case "bedrock":
return new AwsBedrockHandler({
@@ -167,6 +167,7 @@ function createHandlerForProvider(
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
thinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
ulid: options.ulid,
})
@@ -251,6 +252,7 @@ function createHandlerForProvider(
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
})
case "litellm":
return new LiteLlmHandler({
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { afterEach, beforeEach, describe, it } from "mocha"
import sinon from "sinon"
import "should"
import { ClaudeCodeHandler } from "@core/api/providers/claude-code"
import { ClineStorageMessage } from "@/shared/messages/content"
describe("ClaudeCodeHandler", () => {
let handler: ClaudeCodeHandler
@@ -71,7 +71,7 @@ describe("ClaudeCodeHandler", () => {
runClaudeCodeStub.returns(mockGenerator() as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const usageData: any[] = []
@@ -140,7 +140,7 @@ describe("ClaudeCodeHandler", () => {
runClaudeCodeStub.returns(mockGenerator() as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const usageData: any[] = []
@@ -199,7 +199,7 @@ describe("ClaudeCodeHandler", () => {
runClaudeCodeStub.returns(mockGenerator() as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const usageData: any[] = []
@@ -1,8 +1,8 @@
import Anthropic from "@anthropic-ai/sdk"
import { LiteLlmHandler, type LiteLlmModelInfoResponse } from "@core/api/providers/litellm"
import { convertToOpenAiMessages } from "@core/api/transform/openai-format"
import { expect } from "chai"
import sinon from "sinon"
import { ClineStorageMessage } from "@/shared/messages/content"
import { mockFetchForTesting } from "@/shared/net"
const fakeClient = {
@@ -109,7 +109,7 @@ describe("LiteLlmHandler", () => {
it("sends the system prompt and messages with the openai format", async () => {
const systemPrompt = "Test System Prompt"
const messages: Anthropic.Messages.MessageParam[] = [
const messages: ClineStorageMessage[] = [
{
role: "user",
content: "first message",
@@ -161,7 +161,7 @@ describe("LiteLlmHandler", () => {
it("inserts the cache control in the system prompt and the last two user messages", async () => {
const systemPrompt = "Test System Prompt"
const messages: Anthropic.Messages.MessageParam[] = [
const messages: ClineStorageMessage[] = [
{
role: "user",
content: "first message",
@@ -1,9 +1,9 @@
import { afterEach, before, beforeEach, describe, it } from "mocha"
import "should"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions } from "@shared/api"
import axios from "axios"
import sinon from "sinon"
import { ClineStorageMessage } from "@/shared/messages/content"
import { OllamaHandler } from "../ollama"
describe("OllamaHandler", () => {
@@ -59,7 +59,7 @@ describe("OllamaHandler", () => {
} as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const result = []
const usageInfo = []
@@ -114,7 +114,7 @@ describe("OllamaHandler", () => {
}
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
// Start the request and catch the error
let errorMessage = ""
@@ -158,7 +158,7 @@ describe("OllamaHandler", () => {
} as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const result = []
@@ -204,7 +204,7 @@ describe("OllamaHandler", () => {
}
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const result = []
+8 -20
View File
@@ -2,8 +2,8 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ClineTool } from "@/shared/tools"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
@@ -43,7 +43,7 @@ export class AnthropicHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: AnthropicTool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -74,19 +74,7 @@ export class AnthropicHandler implements ApiHandler {
case "claude-opus-4-1-20250805":
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307": {
/*
The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request..
*/
const userMsgIndices = messages.reduce((acc, msg, index) => {
if (msg.role === "user") {
acc.push(index)
}
return acc
}, [] as number[])
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
const anthropicMessages = sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex)
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
stream = await client.messages.create(
{
@@ -106,7 +94,7 @@ export class AnthropicHandler implements ApiHandler {
messages: anthropicMessages,
// tools, // cache breakpoints go from tools > system > messages, and since tools dont change, we can just set the breakpoint at the end of system (this avoids having to set a breakpoint at the end of tools which by itself does not meet min requirements for haiku caching)
stream: true,
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
tools: nativeToolsOn ? tools : undefined,
// tool_choice options:
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
@@ -135,9 +123,9 @@ export class AnthropicHandler implements ApiHandler {
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizeAnthropicMessages(messages),
// tools,
// tool_choice: { type: "auto" },
messages: sanitizeAnthropicMessages(messages, false),
tools: nativeToolsOn ? tools : undefined,
tool_choice: { type: "auto" },
stream: true,
})
break
@@ -216,7 +204,7 @@ export class AnthropicHandler implements ApiHandler {
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
// 'reasoning' type just displays in the UI, but reasoning with signature will be used to send the thinking traces back to the API
// 'reasoning' type just displays in the UI, but ant_thinking will be used to send the thinking traces back to the API
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
+2 -2
View File
@@ -1,5 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -47,7 +47,7 @@ export class AskSageHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
try {
const model = this.getModel()
+15 -7
View File
@@ -1,12 +1,14 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
interface BasetenHandlerOptions extends CommonApiHandlerOptions {
basetenApiKey?: string
@@ -98,10 +100,11 @@ export class BasetenHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const maxTokens = this.getOptimalMaxTokens(model)
const toolCallProcessor = new ToolCallProcessor()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -115,21 +118,22 @@ export class BasetenHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
temperature: 0,
tools,
tool_choice: tools && tools.length > 0 ? "auto" : undefined,
})
let didOutputUsage = false
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
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
if (delta && "reasoning" in delta && delta?.reasoning) {
const reasoning = typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning)
yield {
type: "reasoning",
reasoning: reasoningContent,
reasoning,
}
continue
}
// Handle content field
@@ -140,6 +144,10 @@ export class BasetenHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Handle usage information - only output once
if (!didOutputUsage && chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
+23 -16
View File
@@ -1,4 +1,3 @@
import { Anthropic } from "@anthropic-ai/sdk"
// Import proper AWS SDK types
import type { ContentBlock, Message } from "@aws-sdk/client-bedrock-runtime"
import {
@@ -12,6 +11,7 @@ import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
import { ExtensionRegistryInfo } from "@/registry"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToR1Format } from "../transform/r1-format"
@@ -121,7 +121,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
@withRetry({ maxRetries: 4 })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
// cross region inference requires prefixing the model id with the region
const rawModelId = await this.getModelId()
@@ -342,7 +342,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createDeepseekMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
@@ -480,7 +480,7 @@ export class AwsBedrockHandler implements ApiHandler {
* First uses convertToR1Format to merge consecutive messages with the same role,
* then converts to the string format that DeepSeek R1 expects
*/
private formatDeepseekR1Prompt(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
private formatDeepseekR1Prompt(systemPrompt: string, messages: ClineStorageMessage[]): string {
// First use convertToR1Format to merge consecutive messages with the same role
const r1Messages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
@@ -513,7 +513,7 @@ export class AwsBedrockHandler implements ApiHandler {
* Estimates token count based on text length (approximate)
* Note: This is a rough estimation, as the actual token count depends on the tokenizer
*/
private estimateInputTokens(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): number {
private estimateInputTokens(systemPrompt: string, messages: ClineStorageMessage[]): number {
// For Deepseek R1, we estimate the token count of the formatted prompt
// The formatted prompt includes special tokens and consistent formatting
const formattedPrompt = this.formatDeepseekR1Prompt(systemPrompt, messages)
@@ -680,11 +680,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error processing Converse API response:", error)
yield {
type: "text",
text: `[ERROR] Failed to process response: ${error instanceof Error ? error.message : String(error)}`,
}
throw error
}
}
@@ -703,9 +699,20 @@ export class AwsBedrockHandler implements ApiHandler {
text: `[ERROR] Model stream error: ${chunk.modelStreamErrorException.message}`,
}
} else if (chunk.validationException) {
// Check if this is a context window error - if so, throw it
// so the retry mechanism can handle truncation
const message = chunk.validationException.message || ""
const isContextError = /input.*too long|context.*exceed|maximum.*token|input length.*max.*tokens/i.test(message)
if (isContextError) {
// Throw as exception so context management can handle it
throw chunk.validationException
}
// Otherwise yield as error text
yield {
type: "text",
text: `[ERROR] Validation error: ${chunk.validationException.message}`,
text: `[ERROR] Validation error: ${message}`,
}
} else if (chunk.throttlingException) {
yield {
@@ -779,7 +786,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createAnthropicMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
modelId: string,
model: { id: string; info: ModelInfo },
enable1mContextWindow: boolean,
@@ -835,7 +842,7 @@ export class AwsBedrockHandler implements ApiHandler {
* Formats messages for models using the Converse API specification
* Used by both Anthropic and Nova models to avoid code duplication
*/
private formatMessagesForConverseAPI(messages: Anthropic.Messages.MessageParam[]): Message[] {
private formatMessagesForConverseAPI(messages: ClineStorageMessage[]): Message[] {
return messages.map((message) => {
// Determine role (user or assistant)
const role = message.role === "user" ? ConversationRole.USER : ConversationRole.ASSISTANT
@@ -968,7 +975,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createNovaMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
@@ -1008,7 +1015,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createOpenAIMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
@@ -1143,7 +1150,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createQwenMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -46,7 +46,7 @@ export class CerebrasHandler implements ApiHandler {
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 {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { ClaudeCodeModelId, claudeCodeDefaultModelId, claudeCodeModels } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { type ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { type ApiStream, ApiStreamUsageChunk } from "../transform/stream"
@@ -24,7 +24,7 @@ export class ClaudeCodeHandler implements ApiHandler {
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
// Filter out image blocks since Claude Code doesn't support them
const filteredMessages = filterMessagesForClaudeCode(messages)
+11 -4
View File
@@ -1,4 +1,3 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import axios from "axios"
@@ -8,7 +7,9 @@ import { ClineEnv } from "@/config"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { Logger } from "@/services/logging/Logger"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch, getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -26,6 +27,7 @@ interface ClineHandlerOptions extends CommonApiHandlerOptions {
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
clineAccountId?: string
geminiThinkingLevel?: string
}
export class ClineHandler implements ApiHandler {
@@ -96,7 +98,7 @@ export class ClineHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
try {
const client = await this.ensureClient()
@@ -114,11 +116,13 @@ export class ClineHandler implements ApiHandler {
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
this.options.geminiThinkingLevel,
)
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
Logger.debug("ClineHandler chunk:" + JSON.stringify(chunk))
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
@@ -149,6 +153,7 @@ export class ClineHandler implements ApiHandler {
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
@@ -180,7 +185,7 @@ export class ClineHandler implements ApiHandler {
"reasoning_details" in delta &&
delta.reasoning_details &&
// @ts-ignore-next-line
delta.reasoning_details.length && // exists and non-0
delta?.reasoning_details?.length && // exists and non-0
!shouldSkipReasoningForModel(this.options.openRouterModelId)
) {
yield {
@@ -190,11 +195,13 @@ export class ClineHandler implements ApiHandler {
}
}
console.log("didOutputUsage", didOutputUsage, chunk.usage)
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (this.getModel().id === "x-ai/grok-code-fast-1") {
if (this.getModel().id === "x-ai/grok-code-fast-1" || this.getModel().id === "minimax/minimax-m2") {
totalCost = 0
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -75,7 +75,7 @@ export class DeepSeekHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+3 -3
View File
@@ -1,4 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
@@ -97,7 +97,7 @@ export class DifyHandler implements ApiHandler {
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
@@ -384,7 +384,7 @@ export class DifyHandler implements ApiHandler {
}
}
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
private convertMessagesToQuery(systemPrompt: string, messages: ClineStorageMessage[]): string {
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
// The system prompt is typically configured in the Dify App itself.
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
+2 -2
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -50,7 +50,7 @@ export class DoubaoHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+2 -2
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -41,7 +41,7 @@ export class FireworksHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
+54 -47
View File
@@ -1,4 +1,3 @@
import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import {
ApiError,
@@ -7,10 +6,11 @@ import {
type GenerateContentResponseUsageMetadata,
GoogleGenAI,
FunctionDeclaration as GoogleTool,
Part,
ThinkingLevel,
} from "@google/genai"
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { telemetryService } from "@/services/telemetry"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { RetriableError, withRetry } from "../retry"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
@@ -28,6 +28,7 @@ interface GeminiHandlerOptions extends CommonApiHandlerOptions {
geminiApiKey?: string
geminiBaseUrl?: string
thinkingBudgetTokens?: number
thinkingLevel?: string
apiModelId?: string
ulid?: string
}
@@ -110,34 +111,49 @@ export class GeminiHandler implements ApiHandler {
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: GoogleTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: GoogleTool[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
// Configure thinking budget if supported
const thinkingBudget = this.options.thinkingBudgetTokens ?? 0
const _maxBudget = info.thinkingConfig?.maxBudget ?? 0
const _thinkingBudget = this.options.thinkingBudgetTokens ?? 0
const maxBudget = info.thinkingConfig?.maxBudget ?? 24576
const thinkingBudget = Math.min(_thinkingBudget, maxBudget)
// When ThinkingLevel is defineded, thinking budget cannot be zero
// and only level is used to control thinking behavior.
let thinkingLevel: ThinkingLevel | undefined
if (this.options.thinkingLevel === "low") {
thinkingLevel = ThinkingLevel.LOW
} else if (this.options.thinkingLevel === "high") {
thinkingLevel = ThinkingLevel.HIGH
}
// Set up base generation config
const requestConfig: GenerateContentConfig = {
// Add base URL if configured
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
...{ systemInstruction: systemPrompt },
systemInstruction: systemPrompt,
// Set temperature (default to 0)
temperature: 0,
// Gemini 3.0 recommends 1.0
temperature: info.temperature ?? 1,
}
// Add thinking config if the model supports it
if (thinkingBudget > 0) {
requestConfig.thinkingConfig = {
thinkingBudget: thinkingBudget,
includeThoughts: true,
}
requestConfig.thinkingConfig = {
// Turn off thinking:
// thinkingBudget: 0
// Turn on dynamic thinking:
// thinkingBudget: -1
// Turn on fixed thinking budget:
thinkingBudget: thinkingLevel ? undefined : thinkingBudget,
thinkingLevel,
includeThoughts: thinkingBudget > 0 || !!thinkingLevel,
}
// Generate content using the configured parameters
const sdkCallStartTime = Date.now()
let responseId: string | undefined
let sdkFirstChunkTime: number | undefined
let ttftSdkMs: number | undefined
let apiSuccess = false
@@ -148,7 +164,8 @@ export class GeminiHandler implements ApiHandler {
let thoughtsTokenCount = 0 // Initialize thought token counts
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
if (tools?.length) {
const isNativeToolCallsEnabled = tools?.length
if (isNativeToolCallsEnabled) {
requestConfig.tools = [{ functionDeclarations: tools }]
requestConfig.toolConfig = {
// Force the model to call 'any' function.
@@ -176,56 +193,45 @@ export class GeminiHandler implements ApiHandler {
}
// Handle thinking content from Gemini's response
const candidateForThoughts = chunk?.candidates?.[0]
const partsForThoughts = candidateForThoughts?.content?.parts
let thoughts = "" // Initialize as empty string
if (partsForThoughts) {
// This ensures partsForThoughts is a Part[] array
for (const part of partsForThoughts) {
const { thought, text } = part as Part
if (thought && text) {
// Ensure part.text exists
// Handle the thought part
thoughts += text + "\n" // Append thought and a newline
const parts = chunk?.candidates?.[0]?.content?.parts || []
for (const part of parts) {
if (part.thought && part.text) {
yield {
type: "reasoning",
id: chunk.responseId,
reasoning: part.text || "",
signature: part.thoughtSignature,
}
} else if (part.text) {
yield {
type: "text",
text: part.text,
id: chunk.responseId,
signature: part.thoughtSignature,
}
}
}
if (thoughts.trim() !== "") {
yield {
type: "reasoning",
reasoning: thoughts.trim(),
}
thoughts = "" // Reset thoughts after yielding
}
if (chunk.text) {
yield {
type: "text",
text: chunk.text,
}
}
if (tools && chunk.functionCalls && chunk.functionCalls?.length > 0) {
for (const functionCall of chunk.functionCalls) {
if (functionCall.args) {
console.log("[GeminiHandler] tool call received:", functionCall)
if (part.functionCall) {
const functionCall = part.functionCall
const args = Object.entries(functionCall.args || {}).filter(([_key, val]) => !!val)
if (functionCall.args && args.length > 0) {
yield {
type: "tool_calls",
id: chunk.responseId,
tool_call: {
function: {
id: functionCall.id || functionCall.name,
id: chunk.responseId,
name: functionCall.name,
arguments: JSON.stringify(functionCall.args),
},
},
signature: part.thoughtSignature,
}
}
}
}
if (chunk.usageMetadata) {
responseId = chunk.responseId
lastUsageMetadata = chunk.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
@@ -251,6 +257,7 @@ export class GeminiHandler implements ApiHandler {
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
id: responseId,
}
}
} catch (error) {
+2 -2
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -192,7 +192,7 @@ export class GroqHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -44,7 +44,7 @@ export class HicapHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.hicapModelId ?? ""
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -62,7 +62,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+2 -2
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -69,7 +69,7 @@ export class HuggingFaceHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
+2 -1
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { isAnthropicModelId } from "@/utils/model-utils"
import { ApiHandler, CommonApiHandlerOptions } from ".."
@@ -183,7 +184,7 @@ export class LiteLlmHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam | Anthropic.Messages.TextBlockParam = {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -40,7 +40,7 @@ export class LmStudioHandler implements ApiHandler {
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
+2 -1
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ClineTool } from "@/shared/tools"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
@@ -45,7 +46,7 @@ export class MinimaxHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,9 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { HTTPClient } from "@mistralai/mistralai/lib/http"
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -48,7 +48,7 @@ export class MistralHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const stream = await client.chat
.stream({
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -40,7 +40,7 @@ export class MoonshotHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -39,7 +39,7 @@ export class NebiusHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -37,7 +37,7 @@ export class NousResearchHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+7 -7
View File
@@ -1,7 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI, { APIError, OpenAIError } from "openai"
import type { FinalRequestOptions, Headers as OpenAIHeaders } from "openai/core"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import {
@@ -11,6 +9,7 @@ import {
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -38,7 +37,7 @@ export class OcaHandler implements ApiHandler {
protected initializeClient(options: OcaHandlerOptions) {
return new (class OCIOpenAI extends OpenAI {
protected override async prepareOptions(opts: FinalRequestOptions<unknown>): Promise<void> {
protected override async prepareOptions(opts: any): Promise<void> {
const token = await OcaAuthService.getInstance().getAuthToken()
if (!token) {
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
@@ -55,7 +54,7 @@ export class OcaHandler implements ApiHandler {
status: number | undefined,
error: Object | undefined,
message: string | undefined,
headers: OpenAIHeaders | undefined,
headers: any | undefined,
): APIError {
interface OciError {
code?: string
@@ -75,7 +74,8 @@ export class OcaHandler implements ApiHandler {
if (opcRequestId) {
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
}
return super.makeStatusError(status, error, ociErrorMessage, headers)
const statusCode = typeof status === "number" ? status : 500
return super.makeStatusError(statusCode, error ?? {}, ociErrorMessage, headers)
}
})({
baseURL:
@@ -139,7 +139,7 @@ export class OcaHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
@@ -151,7 +151,7 @@ export class OcaHandler implements ApiHandler {
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budgetTokens !== 0 ? true : false
const reasoningOn = budgetTokens !== 0
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import { ClineStorageMessage } from "@/shared/messages/content"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
@@ -48,7 +48,7 @@ export class OllamaHandler implements ApiHandler {
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
+223 -3
View File
@@ -1,12 +1,14 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, OpenAiNativeModelId, openAiNativeDefaultModelId, openAiNativeModels } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
@@ -61,7 +63,20 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
tools?: ChatCompletionTool[],
useResponseFormat = false,
): ApiStream {
if (useResponseFormat) {
yield* this.createResponseStream(systemPrompt, messages, tools)
} else {
yield* this.createCompletionStream(systemPrompt, messages, tools)
}
}
private async *createCompletionStream(
systemPrompt: string,
messages: ClineStorageMessage[],
tools?: ChatCompletionTool[],
): ApiStream {
const client = this.ensureClient()
@@ -114,7 +129,10 @@ export class OpenAiNativeHandler implements ApiHandler {
}
case "gpt-5-2025-08-07":
case "gpt-5-mini-2025-08-07":
case "gpt-5-nano-2025-08-07": {
case "gpt-5-nano-2025-08-07":
case "gpt-5.1-2025-11-13":
case "gpt-5.1-chat-latest":
case "gpt-5.1": {
const stream = await client.chat.completions.create({
model: model.id,
temperature: 1,
@@ -182,6 +200,208 @@ export class OpenAiNativeHandler implements ApiHandler {
}
}
private async *createResponseStream(
systemPrompt: string,
messages: ClineStorageMessage[],
tools?: ChatCompletionTool[],
): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
// Convert messages to Responses API input format
const input = convertToOpenAIResponsesInput(messages)
// Convert ChatCompletion tools to Responses API format if provided
const responseTools = tools
?.filter((tool) => tool.type === "function")
.map((tool: any) => ({
type: "function" as const,
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
strict: tool.function.strict ?? true, // Responses API defaults to strict mode
}))
Logger.debug("OpenAI Responses Input: " + JSON.stringify(input))
// const lastAssistantMessage = [...messages].reverse().find((msg) => msg.role === "assistant" && msg.id)
// const previous_response_id = lastAssistantMessage?.id
// Create the response using Responses API
const stream = await client.responses.create({
model: model.id,
instructions: systemPrompt,
input,
stream: true,
tools: responseTools,
// previous_response_id,
// store: true,
reasoning: { effort: "medium", summary: "auto" },
// include: ["reasoning.encrypted_content"],
})
// Process the response stream
for await (const chunk of stream) {
Logger.debug("OpenAI Responses Chunk: " + JSON.stringify(chunk))
// Handle different event types from Responses API
if (chunk.type === "response.output_item.added") {
const item = chunk.item
if (item.type === "function_call" && item.id) {
yield {
type: "tool_calls",
id: item.id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
}
}
if (item.type === "reasoning" && item.encrypted_content && item.id) {
yield {
type: "reasoning",
id: item.id,
reasoning: "",
redacted_data: item.encrypted_content,
}
}
}
if (chunk.type === "response.output_item.done") {
const item = chunk.item
if (item.type === "function_call") {
yield {
type: "tool_calls",
id: item.id || item.call_id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
}
}
if (item.type === "reasoning") {
yield {
type: "reasoning",
id: item.id,
details: item.summary,
reasoning: "",
}
}
}
if (chunk.type === "response.reasoning_summary_part.added") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.part.text,
}
}
if (chunk.type === "response.reasoning_summary_text.delta") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.delta,
}
}
if (chunk.type === "response.reasoning_summary_part.done") {
yield {
type: "reasoning",
id: chunk.item_id,
details: chunk.part,
reasoning: "",
}
}
if (chunk.type === "response.output_text.delta") {
// Handle text content deltas
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "text",
text: chunk.delta,
}
}
}
if (chunk.type === "response.reasoning_text.delta") {
// Handle reasoning content deltas
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "reasoning",
reasoning: chunk.delta,
}
}
}
if (chunk.type === "response.function_call_arguments.delta") {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.item_id,
arguments: chunk.delta,
},
},
}
}
if (chunk.type === "response.function_call_arguments.done") {
// Handle completed function call
if (chunk.item_id && chunk.name && chunk.arguments) {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.name,
arguments: chunk.arguments,
},
},
}
}
}
if (
chunk.type === "response.incomplete" &&
chunk.response?.status === "incomplete" &&
chunk.response?.incomplete_details?.reason === "max_output_tokens"
) {
console.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
console.log("Partial output:", chunk.response.output_text)
} else {
console.log("Ran out of tokens during reasoning")
}
}
if (chunk.type === "response.completed" && chunk.response?.usage) {
// Handle usage information when response is complete
const usage = chunk.response.usage
const inputTokens = usage.input_tokens || 0
const outputTokens = usage.output_tokens || 0
const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0
const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0
const totalTokens = usage.total_tokens || 0
Logger.log(`Total tokens from Responses API usage: ${totalTokens}`)
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
yield {
type: "usage",
inputTokens: nonCachedInputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
totalCost: totalCost,
id: chunk.response.id,
}
}
}
}
getModel(): { id: OpenAiNativeModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in openAiNativeModels) {
+2 -6
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI, { AzureOpenAI } from "openai"
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -65,11 +65,7 @@ export class OpenAiHandler implements ApiHandler {
}
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools?: ChatCompletionTool[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
+10 -7
View File
@@ -1,10 +1,11 @@
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { Anthropic } from "@anthropic-ai/sdk"
import { StateManager } from "@core/storage/StateManager"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import axios from "axios"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch, getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -20,6 +21,7 @@ interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
geminiThinkingLevel?: string
}
export class OpenRouterHandler implements ApiHandler {
@@ -54,7 +56,7 @@ export class OpenRouterHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
@@ -67,6 +69,7 @@ export class OpenRouterHandler implements ApiHandler {
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
this.options.geminiThinkingLevel,
)
let didOutputUsage: boolean = false
@@ -214,11 +217,11 @@ export class OpenRouterHandler implements ApiHandler {
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.openRouterModelId
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
const modelId = this.options.openRouterModelId || openRouterDefaultModelId
const cachedModelInfo = StateManager.get().getModelInfo("openRouter", modelId)
return {
id: modelId,
info: cachedModelInfo || openRouterDefaultModelInfo,
}
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { promises as fs } from "node:fs"
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import * as os from "os"
import * as path from "path"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -177,7 +177,7 @@ export class QwenCodeHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
await this.ensureAuthenticated()
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,4 +1,3 @@
import { Anthropic } from "@anthropic-ai/sdk"
import {
InternationalQwenModelId,
internationalQwenDefaultModelId,
@@ -11,6 +10,7 @@ import {
} from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -81,7 +81,7 @@ export class QwenHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
+2 -2
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { toRequestyServiceStringUrl } from "@/shared/clients/requesty"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -59,7 +59,7 @@ export class RequestyHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -42,7 +42,7 @@ export class SambanovaHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+26 -23
View File
@@ -4,10 +4,11 @@ import {
ConversationRole as BedrockConversationRole,
type Message as BedrockMessage,
} from "@aws-sdk/client-bedrock-runtime"
import { ChatMessages, LlmModuleConfig, OrchestrationClient, TemplatingModuleConfig } from "@sap-ai-sdk/orchestration"
import { ChatMessage, OrchestrationClient, OrchestrationModuleConfig } from "@sap-ai-sdk/orchestration"
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
import axios from "axios"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -115,7 +116,7 @@ namespace Bedrock {
* 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[] {
export function formatMessagesForConverseAPI(messages: ClineStorageMessage[]): BedrockMessage[] {
return messages.map((message) => {
// Determine role (user or assistant)
const role = message.role === "user" ? BedrockConversationRole.USER : BedrockConversationRole.ASSISTANT
@@ -315,7 +316,7 @@ namespace Gemini {
*/
export function prepareRequestPayload(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
messages: ClineStorageMessage[],
model: { id: SapAiCoreModelId; info: ModelInfo },
thinkingBudgetTokens?: number,
): any {
@@ -458,7 +459,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
if (this.options.sapAiCoreUseOrchestrationMode) {
yield* this.createMessageWithOrchestration(systemPrompt, messages)
} else {
@@ -490,29 +491,31 @@ export class SapAiCoreHandler implements ApiHandler {
this.isAiCoreEnvSetup = true
}
private async *createMessageWithOrchestration(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
private async *createMessageWithOrchestration(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
try {
// Ensure AI Core environment variable is set up (only runs once)
this.ensureAiCoreEnvSetup()
const model = this.getModel()
// Define the LLM to be used by the Orchestration pipeline
const llm: LlmModuleConfig = {
model_name: model.id,
const orchestrationConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: {
name: model.id,
},
prompt: {
template: [
{
role: "system",
content: systemPrompt,
},
],
},
},
}
const templating: TemplatingModuleConfig = {
template: [
{
role: "system",
content: systemPrompt,
},
],
}
const orchestrationClient = new OrchestrationClient(
{ llm, templating },
{ resourceGroup: this.options.sapAiResourceGroup || "default" },
)
const orchestrationClient = new OrchestrationClient(orchestrationConfig, {
resourceGroup: this.options.sapAiResourceGroup || "default",
})
const sapMessages = this.convertMessageParamToSAPMessages(messages)
@@ -538,7 +541,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
private async *createMessageWithDeployments(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
private async *createMessageWithDeployments(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const token = await this.getToken()
const headers = {
Authorization: `Bearer ${token}`,
@@ -1040,8 +1043,8 @@ export class SapAiCoreHandler implements ApiHandler {
}
return { id: sapAiCoreDefaultModelId, info: sapAiCoreModels[sapAiCoreDefaultModelId] }
}
private convertMessageParamToSAPMessages(messages: Anthropic.Messages.MessageParam[]): ChatMessages {
private convertMessageParamToSAPMessages(messages: ClineStorageMessage[]): ChatMessage[] {
// Use the existing OpenAI converter since the logic is identical
return convertToOpenAiMessages(messages) as ChatMessages
return convertToOpenAiMessages(messages) as ChatMessage[]
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -42,7 +42,7 @@ export class TogetherHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
+9 -16
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
@@ -47,7 +47,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
const modelInfo = this.getModel().info
@@ -82,8 +82,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
}
}
@@ -102,22 +101,16 @@ export class VercelAIGatewayHandler implements ApiHandler {
}
if (!didOutputUsage && chunk.usage) {
const inputTokens = chunk.usage.prompt_tokens || 0
const outputTokens =
(chunk.usage.completion_tokens || 0) + (chunk.usage.completion_tokens_details?.reasoning_tokens || 0)
const cacheReadTokens = chunk.usage.prompt_tokens_details?.cached_tokens || 0
// @ts-ignore - Vercel AI Gateway extends OpenAI types
const cacheWriteTokens = chunk.usage.cache_creation_input_tokens || 0
const totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
yield {
type: "usage",
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
// @ts-expect-error - Vercel AI Gateway extends OpenAI types
totalCost: chunk.usage.cost || 0,
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,
totalCost,
}
didOutputUsage = true
}
+4 -11
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { FunctionDeclaration as GoogleTool } from "@google/genai"
import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ClineTool } from "@/shared/tools"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -67,7 +67,7 @@ export class VertexHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[]): ApiStream {
const model = this.getModel()
const modelId = model.id
@@ -103,13 +103,6 @@ export class VertexHandler implements ApiHandler {
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
// Find indices of user messages for cache control
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
@@ -123,7 +116,7 @@ export class VertexHandler implements ApiHandler {
cache_control: { type: "ephemeral" },
},
],
messages: sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex),
messages: sanitizeAnthropicMessages(messages, true),
stream: true,
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
// tool_choice options:
@@ -149,7 +142,7 @@ export class VertexHandler implements ApiHandler {
type: "text",
},
],
messages: sanitizeAnthropicMessages(messages),
messages: sanitizeAnthropicMessages(messages, false),
stream: true,
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
// tool_choice options:
+2 -2
View File
@@ -1,8 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { calculateApiCostAnthropic } from "@utils/cost"
import * as vscode from "vscode"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
@@ -366,7 +366,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
// Ensure clean state before starting a new request
this.ensureCleanState()
const client: vscode.LanguageModelChat = await this.getClient()
+2 -2
View File
@@ -1,9 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, XAIModelId, xaiDefaultModelId, xaiModels } from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -44,7 +44,7 @@ export class XAIHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
+2 -2
View File
@@ -1,4 +1,3 @@
import { Anthropic } from "@anthropic-ai/sdk"
import {
internationalZAiDefaultModelId,
internationalZAiModelId,
@@ -10,6 +9,7 @@ import {
} from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { version as extensionVersion } from "../../../../package.json"
import { ApiHandler, CommonApiHandlerOptions } from ".."
@@ -76,7 +76,7 @@ export class ZAiHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+74 -61
View File
@@ -1,77 +1,90 @@
import { ClineStorageMessage } from "@/shared/messages/content"
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineStorageMessage, convertClineStorageToAnthropicMessage } from "@/shared/messages/content"
/**
* Sanitize Anthropic messages by removing reasoning details and adding ephemeral cache control
* to the last two user messages to prevent them from being stored in Anthropic's cache.
* Converts Cline storage messages to Anthropic API format with optional cache control.
* Adds ephemeral cache control to the last two user messages to prevent them from being
* stored in Anthropic's cache.
*
* @param clineMessages - Array of Cline storage messages to convert
* @param lastUserMsgIndex - Optional index of the last user message
* @param secondLastMsgUserIndex - Optional index of the second-to-last user message
* @returns Array of Anthropic-compatible messages with cache control applied
*/
export function sanitizeAnthropicMessages(
messages: Array<ClineStorageMessage>,
lastUserMsgIndex?: number,
secondLastMsgUserIndex?: number,
): Array<ClineStorageMessage> {
return messages.map((_message, index) => {
const message = removeReasoningDetails(_message)
const addCacheControl = lastUserMsgIndex !== undefined && secondLastMsgUserIndex !== undefined
clineMessages: Array<ClineStorageMessage | Anthropic.MessageParam>,
supportCache: boolean,
): Array<Anthropic.MessageParam> {
// The latest message will be the new user message, one before will be the assistant message from a previous request,
// and the user message before that will be a previously cached user message. So we need to mark the latest user message
// as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server
// know the last message to retrieve from the cache for the current request.
const userMsgIndices = clineMessages.reduce((acc, msg, index) => {
if (msg.role === "user") {
acc.push(index)
}
return acc
}, [] as number[])
// Set to -1 if there are no user messages so the indices are invalid
const indicesLength = userMsgIndices.length ?? -1
const lastUserMsgIndex = userMsgIndices[indicesLength - 1]
const secondLastMsgUserIndex = userMsgIndices[indicesLength - 2]
if (addCacheControl && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)) {
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
cache_control: {
type: "ephemeral",
},
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? {
...content,
cache_control: {
type: "ephemeral",
},
}
: content,
),
}
return clineMessages.map((msg, index) => {
const anthropicMsg = convertClineStorageToAnthropicMessage(msg)
// Add cache control to the last two user messages
if (supportCache && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)) {
return addCacheControl(anthropicMsg)
}
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
}
return anthropicMsg
})
}
const isThinkingBlock = (
block: Anthropic.ContentBlockParam,
): block is Anthropic.Messages.ThinkingBlockParam | Anthropic.Messages.RedactedThinkingBlockParam => {
return block.type === "thinking" || block.type === "redacted_thinking"
}
/**
* Remove reasoning details from a single Anthropic message parameter
* Adds ephemeral cache control to the last content block of a message.
* Returns a new message object without mutating the original.
*
* @param message - The Anthropic message to add cache control to
* @returns A new message with cache control added to the last content block
*/
function removeReasoningDetails(param: ClineStorageMessage): ClineStorageMessage {
if (Array.isArray(param.content)) {
function addCacheControl(message: Anthropic.MessageParam): Anthropic.MessageParam {
// Convert string content to array format
if (typeof message.content === "string") {
return {
...param,
content: param.content.map((item) => {
if (item.type === "text") {
return {
...item,
reasoning_details: undefined,
}
}
return item
}),
...message,
content: [
{
type: "text",
text: message.content,
cache_control: { type: "ephemeral" },
} satisfies Anthropic.TextBlockParam,
],
}
}
return param
// Handle array content - add cache control to the last block
const content = [...message.content]
const lastIndex = content.length - 1
if (lastIndex >= 0) {
const lastBlock = content[lastIndex]
// Only add cache_control to block types that support it (not ThinkingBlockParam)
if (!isThinkingBlock(lastBlock)) {
content[lastIndex] = {
...lastBlock,
cache_control: { type: "ephemeral" },
} satisfies Anthropic.ContentBlockParam
}
}
return { ...message, content }
}
+4 -2
View File
@@ -1,7 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Content, GenerateContentResponse, Part } from "@google/genai"
import { ClineStorageMessage } from "@/shared/messages/content"
export function convertAnthropicContentToGemini(content: string | Anthropic.ContentBlockParam[]): Part[] {
export function convertAnthropicContentToGemini(content: string | ClineStorageMessage["content"]): Part[] {
if (typeof content === "string") {
return [{ text: content }]
}
@@ -9,7 +10,7 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
.flatMap((block): Part | undefined => {
switch (block.type) {
case "text":
return { text: block.text }
return { text: block.text, thoughtSignature: block.signature }
case "image":
if (block.source.type !== "base64") {
throw new Error("Unsupported image source type")
@@ -26,6 +27,7 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
name: block.name,
args: block.input as Record<string, unknown>,
},
thoughtSignature: block.signature,
}
case "tool_result":
return {
+43 -21
View File
@@ -10,6 +10,15 @@ import {
ClineUserToolResultContentBlock,
} from "@/shared/messages/content"
/**
* Converts an array of ClineStorageMessage objects to OpenAI's Completions API format.
*
* Handles conversion of Cline-specific content types (tool uses, tool results, images, reasoning details)
* into OpenAI's expected message structure, including tool_calls and tool_call_id fields.
*
* @param anthropicMessages - Array of ClineStorageMessage objects to be converted
* @returns Array of OpenAI.Chat.ChatCompletionMessageParam objects
*/
export function convertToOpenAiMessages(
anthropicMessages: Omit<ClineStorageMessage, "modelInfo">[],
): OpenAI.Chat.ChatCompletionMessageParam[] {
@@ -150,6 +159,7 @@ export function convertToOpenAiMessages(
// delete part.reasoning_details
}
if (part.type === "thinking" && part.thinking) {
// Reasoning details should have been moved to the text block
thinkingBlock.push(part)
}
})
@@ -164,15 +174,26 @@ export function convertToOpenAiMessages(
}
// Process tool use messages
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
name: toolMessage.name,
// json string
arguments: JSON.stringify(toolMessage.input),
},
}))
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => {
const toolDetails = toolMessage.reasoning_details
if (toolDetails?.length) {
if (Array.isArray(toolDetails)) {
reasoningDetails.push(...toolDetails)
} else {
reasoningDetails.push(toolDetails)
}
}
return {
id: toolMessage.id,
type: "function",
function: {
name: toolMessage.name,
// json string
arguments: JSON.stringify(toolMessage.input),
},
}
})
// Set content to blank when tool_calls are present but content has no text, per OpenAI API spec
const hasToolCalls = tool_calls.length > 0
@@ -339,29 +360,30 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
}
try {
if (openAiMessage?.tool_calls?.length) {
anthropicMessage.content.push(
...openAiMessage.tool_calls
.map((toolCall): Anthropic.ToolUseBlock => {
const parsedName = toolCall.type === "function" && toolCall.function.name
let parsedInput = toolCall.function.arguments
const functionCalls = openAiMessage.tool_calls.filter((tc: any) => tc?.type === "function" && tc.function)
if (functionCalls.length > 0) {
anthropicMessage.content.push(
...functionCalls.map((toolCall: any): Anthropic.ToolUseBlock => {
let parsedInput = {}
try {
parsedInput = JSON.parse(toolCall.function.arguments || "{}")
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
} catch (error) {
console.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
id: toolCall.id,
name: parsedName || UNIQUE_ERROR_TOOL_NAME,
name: toolCall.function?.name || UNIQUE_ERROR_TOOL_NAME,
input: parsedInput,
}
})
// Filter out any tool uses with the UNIQUE_ERROR_TOOL_NAME, which indicates a parsing error
.filter((toolUse) => toolUse.name !== UNIQUE_ERROR_TOOL_NAME),
)
}),
)
}
return anthropicMessage
}
} catch (error) {
console.error("Failed to process tool calls:", error)
console.error("Error converting OpenAI message to Anthropic format:", error)
}
return anthropicMessage
@@ -0,0 +1,217 @@
import { ResponseInput, ResponseInputMessageContentList, ResponseReasoningItem } from "openai/resources/responses/responses"
import { ClineStorageMessage } from "@/shared/messages/content"
/**
* Converts an array of ClineStorageMessage objects (extension of Anthropic format) to a ResponseInput array to use with OpenAI's Responses API.
*
* ## Key Differences from Chat Completions API
*
* The Responses API has stricter requirements than the Chat Completions API:
*
* ### Chat Completions API:
* - Messages are simple role/content pairs
* - System prompts are separate messages with role="system"
* - No explicit reasoning item structure
* - More forgiving about message ordering
*
* ### Responses API:
* - Uses an "input" array of heterogeneous items (messages, reasoning, function_calls, etc.)
* - System prompts go in an "instructions" field, not as messages
* - Reasoning items MUST be immediately followed by a message or function_call
* - Strict ordering requirements match training data distribution
*
* ## The Reasoning Item Constraint
*
* **THE CRITICAL ERROR:** "Item 'rs_...' of type 'reasoning' was provided without its required following item"
*
* This error occurs when reasoning items are orphaned or separated from their corresponding output.
*
* ### What Causes This Error:
* ```
* ❌ WRONG - Reasoning orphaned between turns:
* [
* { role: "user", content: [...] },
* { type: "reasoning", id: "rs_abc", summary: [...] }, // ← ORPHANED!
* { type: "message", role: "assistant", content: [...] },
* { role: "user", content: [...] }
* ]
* ```
*
* ### The Fix - Keep Complete Assistant Turns Together:
* ```
* ✅ CORRECT - Reasoning paired with its message:
* [
* { role: "user", content: [...] },
* { type: "reasoning", id: "rs_abc", summary: [...] },
* { type: "message", role: "assistant", content: [...] }, // ← Immediately follows reasoning
* { role: "user", content: [...] }
* ]
* ```
*
* **Per OpenAI Engineering Guidance:**
* - ❌ WRONG: `content += filter(lambda x: x.type == "reasoning", resp.output)`
* - ✅ CORRECT: `content += resp.output`
*
* Never extract only reasoning items - always include the complete output sequence
* (reasoning + message/function_call) as provided by the API.
*
* ## Implementation Strategy
*
* 1. **Separate processing for assistant vs user messages** - Assistant turns need special
* handling to maintain reasoning-message pairing
* 2. **Collect all assistant items together** - Gather reasoning, messages, and function_calls
* for the entire assistant turn before validating
* 3. **Validate pairing within each turn** - Ensure each reasoning item is immediately followed
* by a message or function_call, inserting placeholders if needed
* 4. **Flush complete turns atomically** - Add all items from an assistant turn together to
* maintain proper sequencing
*
* @link https://community.openai.com/t/openai-api-error-function-call-was-provided-without-its-required-reasoning-item-the-real-issue/1355347
*
* @param messages - Array of ClineStorageMessage objects to be converted
* @returns ResponseInput array containing the transformed messages with proper reasoning pairing
*/
export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]): ResponseInput {
const allItems: any[] = []
const toolUseIdToCallId = new Map<string, string>()
for (const m of messages) {
if (typeof m.content === "string") {
allItems.push({ role: m.role, content: [{ type: "input_text", text: m.content }] })
continue
}
if (m.role === "assistant") {
// For assistant messages, we must ensure reasoning items are IMMEDIATELY followed
// by their corresponding message or function_call. Process the entire assistant
// turn and ensure proper pairing.
const assistantItems: any[] = []
for (const part of m.content) {
switch (part.type) {
case "thinking":
// Include reasoning item if it has a call_id, even if thinking is empty
// This is required because the API expects reasoning items to be paired with
// their corresponding function_calls, and will error if a function_call
// references a reasoning item that wasn't sent
if (part.call_id && part.call_id.length > 0) {
assistantItems.push({
id: part.call_id,
type: "reasoning",
summary: part.thinking
? [
{
type: "summary_text",
text: part.thinking,
},
]
: [],
} as ResponseReasoningItem)
}
break
case "redacted_thinking":
// Include reasoning item with encrypted content if it has a call_id
// Even if data is missing, we need to maintain the reasoning-function_call pairing
if (part.call_id && part.call_id.length > 0) {
const reasoningItem: any = {
id: part.call_id,
type: "reasoning",
summary: [],
}
// Only include encrypted_content if data exists
if (part.data) {
reasoningItem.encrypted_content = part.data
}
assistantItems.push(reasoningItem as ResponseReasoningItem)
}
break
case "text":
assistantItems.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: part.text }],
})
break
case "image":
assistantItems.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: `[image:${part.source.media_type}]` }],
})
break
case "tool_use": {
const call_id = part.call_id || part.id
if (part.call_id) {
toolUseIdToCallId.set(part.id, part.call_id)
}
assistantItems.push({
type: "function_call",
call_id,
id: part.id,
name: part.name,
arguments: JSON.stringify(part.input ?? {}),
})
break
}
}
}
// Ensure every reasoning item is followed by a message or function_call
for (let i = 0; i < assistantItems.length; i++) {
const item = assistantItems[i]
if (item.type === "reasoning") {
const nextItem = assistantItems[i + 1]
if (!nextItem || (nextItem.type !== "message" && nextItem.type !== "function_call")) {
// Insert a placeholder message immediately after this reasoning item
assistantItems.splice(i + 1, 0, {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "" }],
})
}
}
}
allItems.push(...assistantItems)
} else {
// User messages - collect all content
const messageContent: ResponseInputMessageContentList = []
for (const part of m.content) {
switch (part.type) {
case "text":
messageContent.push({ type: "input_text", text: part.text })
break
case "image":
messageContent.push({
type: "input_image",
detail: "auto",
image_url: `data:${part.source.media_type};base64,${part.source.data}`,
})
break
case "tool_result": {
// Flush any pending message content before adding tool result
if (messageContent.length > 0) {
allItems.push({ role: m.role, content: [...messageContent] })
messageContent.length = 0
}
const call_id = part.call_id || toolUseIdToCallId.get(part.tool_use_id) || part.tool_use_id
allItems.push({
type: "function_call_output",
call_id,
output: typeof part.content === "string" ? part.content : JSON.stringify(part.content),
})
break
}
}
}
// Flush any remaining user message content
if (messageContent.length > 0) {
allItems.push({ role: m.role, content: [...messageContent] })
}
}
}
return allItems
}
+14 -1
View File
@@ -21,6 +21,7 @@ export async function createOpenRouterStream(
thinkingBudgetTokens?: number,
openRouterProviderSorting?: string,
tools?: Array<ChatCompletionTool>,
geminiThinkingLevel?: string,
) {
// Convert Anthropic messages to OpenAI format
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -138,6 +139,10 @@ export async function createOpenRouterStream(
topP = 0.95
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
if (model.id.startsWith("google/gemini-3.0") || model.id === "google/gemini-3.0") {
// Recommended value from google
temperature = 1.0
}
let reasoning: { max_tokens: number } | undefined
switch (model.id) {
@@ -161,7 +166,12 @@ export async function createOpenRouterStream(
}
break
default:
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
if (
thinkingBudgetTokens &&
model.info?.thinkingConfig &&
thinkingBudgetTokens > 0 &&
!(model.id.includes("gemini") && geminiThinkingLevel)
) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
break
@@ -189,6 +199,9 @@ export async function createOpenRouterStream(
...(providerPreferences ? { provider: providerPreferences } : {}),
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
return stream
+62 -4
View File
@@ -1,9 +1,20 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk>
export type ApiStream = AsyncGenerator<ApiStreamChunk> & { id?: string }
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamThinkingChunk | ApiStreamUsageChunk | ApiStreamToolCallsChunk
export interface ApiStreamTextChunk {
type: "text"
/**
* Text content generated by the model
*/
text: string
/**
* The response ID associated with this chunk
*/
id?: string
/**
* The thought signature associated with this chunk used by Gemini
*/
signature?: string
}
export interface ApiStreamUsageChunk {
@@ -14,27 +25,74 @@ export interface ApiStreamUsageChunk {
cacheReadTokens?: number
thoughtsTokenCount?: number // openrouter
totalCost?: number // openrouter
/**
* The response ID associated with this response
*/
id?: string
}
export interface ApiStreamToolCallsChunk {
type: "tool_calls"
/**
* The tool call information
*/
tool_call: ApiStreamToolCall
/**
* The response ID associated with this chunk
*/
id?: string
/**
* The thought signature associated with this chunk used by Gemini
*/
signature?: string
}
export interface ApiStreamToolCall {
call_id?: string // The call / request ID associated with this tool call
/**
* The call ID associated with this tool call
*/
call_id?: string
// Information about the tool being called
function: {
id?: string // The tool call ID
/**
* The tool call ID
*/
id?: string
/**
* Name of the tool
*/
name?: string
/**
* The arguments passed to the tool execution
*/
arguments?: any
}
}
export interface ApiStreamThinkingChunk {
type: "reasoning"
/**
* The reasoning text generated by the model.
* Redacted reasoning block will have this field set to "[REDACTED]" or an empty string.
*/
reasoning: string
details?: unknown // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
/**
* openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
* This is also where we store the summary details for OpenAI.
*/
details?: unknown
/**
* It's used when sending the thinking block back to the API.
* API expects this in completed form, not as array of deltas.
* Also used by Gemini for thought signature associated with this chunk
*/
signature?: string
/**
* redacted data
*/
redacted_data?: string
/**
* The response ID associated with this chunk
*/
id?: string
}
@@ -79,7 +79,7 @@ export function getOpenAIToolParams(tools?: OpenAITool[]) {
? {
tools,
tool_choice: tools ? ("auto" as ChatCompletionToolChoiceOption) : undefined,
parallel_tool_calls: tools ? true : undefined,
parallel_tool_calls: tools ? false : undefined, // Set to false to force single tool calls
}
: {
tools: undefined,
+34 -4
View File
@@ -52,16 +52,46 @@ export interface ToolUse {
// params is a partial record, allowing only some or none of the possible parameters to be used
params: Partial<Record<ToolParamName, string>>
partial: boolean
// Whether this tool use was initiated by a native tool call
/**
* Whether this tool use was initiated by a native tool call
*/
isNativeToolCall?: boolean
/**
* The call / response ID this tool use is associated with.
*/
call_id?: string // optional call ID for tracking tool use calls
/**
* Thought signature associated with this tool use, used by Gemini
*/
signature?: string
}
export interface ReasoningStreamContent {
type: "reasoning"
/**
* The reasoning text generated by the model.
* Redacted reasoning block will have this field set to "[REDACTED]" or an empty string.
*/
reasoning: string
details?: any // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
/**
* openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
*/
details?: any
/**
* It's used when sending the thinking block back to the API.
* API expects this in completed form, not as array of deltas.
*/
signature?: string
redacted?: boolean // whether this reasoning block has been redacted
data?: string // redacted data
/**
* whether this reasoning block has been redacted
*/
redacted?: boolean
/**
* redacted data
*/
data?: string
/**
* Indicates whether this is a partial reasoning block
*/
partial: boolean
}
@@ -5,7 +5,8 @@ export function checkContextWindowExceededError(error: unknown): boolean {
checkIsOpenAIContextWindowError(error) ||
checkIsOpenRouterContextWindowError(error) ||
checkIsAnthropicContextWindowError(error) ||
checkIsCerebrasContextWindowError(error)
checkIsCerebrasContextWindowError(error) ||
checkIsBedrockContextWindowError(error)
)
}
@@ -70,3 +71,45 @@ function checkIsCerebrasContextWindowError(response: any): boolean {
return false
}
}
function checkIsBedrockContextWindowError(error: any): boolean {
try {
// Bedrock returns ValidationException for context window errors
const errorType = error?.name ?? error?.error?.type ?? error?.__type
const errorCode = error?.code ?? error?.error?.code ?? error?.$metadata?.httpStatusCode
// Handle nested error structures (e.g., through Vercel AI SDK)
const nestedError = error?.error?.param
const nestedErrorCode = nestedError?.statusCode ?? error?.details?.code
const nestedMessage = nestedError?.message ?? nestedError?.error
const message: string = String(error?.message || error?.error?.message || nestedMessage || "")
// Check for ValidationException with HTTP 400
const isValidationException =
errorType === "ValidationException" ||
errorType === "AI_APICallError" ||
String(errorCode) === "400" ||
String(nestedErrorCode) === "400" ||
error?.code === "stream_initialization_failed"
if (!isValidationException) {
return false
}
// Known Bedrock context window error patterns
const BEDROCK_CONTEXT_PATTERNS = [
/maximum tokens.*exceeds.*model limit/i,
/input length and max_tokens exceed context limit/i,
/context length.*exceeds/i,
/total number of tokens.*exceeds.*limit/i,
/requested.*tokens.*exceeds.*limit/i,
/reduce.*length.*messages.*completion/i,
/input is too long/i,
] as const
return BEDROCK_CONTEXT_PATTERNS.some((pattern) => pattern.test(message))
} catch {
return false
}
}
@@ -1,6 +1,3 @@
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import {
combineRuleToggles,
getRuleFilesTotalContent,
@@ -12,48 +9,10 @@ import { GlobalFileNames } from "@core/storage/disk"
import { listFiles } from "@services/glob/list-files"
import { ClineRulesToggles } from "@shared/cline-rules"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
// Types for better code clarity
type RuleSource = {
filePath: string
extension?: string
}
type RuleConfig = {
stateKey: "localWindsurfRulesToggles" | "localCursorRulesToggles" | "localAgentsRulesToggles"
sources: RuleSource[]
}
/**
* Check if a directory is a sensitive location (home directory or Desktop)
* Returns true if the directory is safe to process rules from
*/
function isSafeDirectory(workingDirectory: string): boolean {
const normalizedPath = path.resolve(workingDirectory)
const homeDir = os.homedir()
const desktopDir = path.join(homeDir, "Desktop")
// Don't process rules from home directory or Desktop
if (normalizedPath === homeDir || normalizedPath === desktopDir) {
return false
}
return true
}
/**
* Helper to synchronize a single rule source
*/
async function syncRuleSource(
workingDirectory: string,
source: RuleSource,
currentToggles: ClineRulesToggles,
): Promise<ClineRulesToggles> {
const fullPath = path.resolve(workingDirectory, source.filePath)
return await synchronizeRuleToggles(fullPath, currentToggles, source.extension)
}
/**
* Refreshes the toggles for windsurf, cursor, and agents rules
*/
@@ -65,84 +24,36 @@ export async function refreshExternalRulesToggles(
cursorLocalToggles: ClineRulesToggles
agentsLocalToggles: ClineRulesToggles
}> {
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(workingDirectory)) {
// Return empty toggles for unsafe directories
return {
windsurfLocalToggles: {},
cursorLocalToggles: {},
agentsLocalToggles: {},
}
}
// local windsurf toggles
const localWindsurfRulesToggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
const configs: Record<string, RuleConfig> = {
windsurf: {
stateKey: "localWindsurfRulesToggles",
sources: [{ filePath: GlobalFileNames.windsurfRules }],
},
cursor: {
stateKey: "localCursorRulesToggles",
sources: [
{ filePath: GlobalFileNames.cursorRulesDir, extension: ".mdc" },
{ filePath: GlobalFileNames.cursorRulesFile },
],
},
agents: {
stateKey: "localAgentsRulesToggles",
sources: [{ filePath: GlobalFileNames.agentsRulesFile }],
},
}
// local cursor toggles
const localCursorRulesToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
// Process windsurf
const windsurfConfig = configs.windsurf
const windsurfToggles = controller.stateManager.getWorkspaceStateKey(windsurfConfig.stateKey)
const windsurfLocalToggles = await syncRuleSource(workingDirectory, windsurfConfig.sources[0], windsurfToggles)
controller.stateManager.setWorkspaceState(windsurfConfig.stateKey, windsurfLocalToggles)
// cursor has two valid locations for rules files, so we need to check both and combine
// synchronizeRuleToggles will drop whichever rules files are not in each given path, but combining the results will result in no data loss
let localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesDir)
const updatedLocalCursorToggles1 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles, ".mdc")
// Process cursor (combine results from both sources)
const cursorConfig = configs.cursor
const cursorToggles = controller.stateManager.getWorkspaceStateKey(cursorConfig.stateKey)
const [cursorToggles1, cursorToggles2] = await Promise.all([
syncRuleSource(workingDirectory, cursorConfig.sources[0], cursorToggles),
syncRuleSource(workingDirectory, cursorConfig.sources[1], cursorToggles),
])
const cursorLocalToggles = combineRuleToggles(cursorToggles1, cursorToggles2)
controller.stateManager.setWorkspaceState(cursorConfig.stateKey, cursorLocalToggles)
localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesFile)
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
// Process agents
const agentsConfig = configs.agents
const agentsToggles = controller.stateManager.getWorkspaceStateKey(agentsConfig.stateKey)
const agentsLocalToggles = await syncRuleSource(workingDirectory, agentsConfig.sources[0], agentsToggles)
controller.stateManager.setWorkspaceState(agentsConfig.stateKey, agentsLocalToggles)
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
controller.stateManager.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
// local agents toggles
const localAgentsRulesToggles = controller.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
const localAgentsRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.agentsRulesFile)
const updatedLocalAgentsToggles = await synchronizeRuleToggles(localAgentsRulesFilePath, localAgentsRulesToggles)
controller.stateManager.setWorkspaceState("localAgentsRulesToggles", updatedLocalAgentsToggles)
return {
windsurfLocalToggles,
cursorLocalToggles,
agentsLocalToggles,
}
}
/**
* Helper to read a single rule file
*/
async function readRuleFile(filePath: string, toggles: ClineRulesToggles): Promise<string | undefined> {
// Check if file exists and is enabled
if (!(await fileExistsAtPath(filePath))) {
return undefined
}
if (await isDirectory(filePath)) {
return undefined
}
if (filePath in toggles && toggles[filePath] === false) {
return undefined
}
try {
const content = (await fs.readFile(filePath, "utf8")).trim()
return content || undefined
} catch (error) {
console.error(`Failed to read rule file at ${filePath}:`, error)
return undefined
windsurfLocalToggles: updatedLocalWindsurfToggles,
cursorLocalToggles: updatedLocalCursorToggles,
agentsLocalToggles: updatedLocalAgentsToggles,
}
}
@@ -150,50 +61,70 @@ async function readRuleFile(filePath: string, toggles: ClineRulesToggles): Promi
* Gather formatted windsurf rules
*/
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return undefined
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
let windsurfRulesFileInstructions: string | undefined
if (await fileExistsAtPath(windsurfRulesFilePath)) {
if (!(await isDirectory(windsurfRulesFilePath))) {
try {
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
}
}
}
const filePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
const content = await readRuleFile(filePath, toggles)
return content ? formatResponse.windsurfRulesLocalFileInstructions(cwd, content) : undefined
return windsurfRulesFileInstructions
}
/**
* Gather formatted cursor rules, which can come from two sources
*/
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return []
}
const results: (string | undefined)[] = []
// Check .cursorrules file
// we first check for the .cursorrules file
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
const fileContent = await readRuleFile(cursorRulesFilePath, toggles)
if (fileContent) {
results.push(formatResponse.cursorRulesLocalFileInstructions(cwd, fileContent))
}
let cursorRulesFileInstructions: string | undefined
// Check .cursor/rules directory
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
if ((await fileExistsAtPath(cursorRulesDirPath)) && (await isDirectory(cursorRulesDirPath))) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
results.push(formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent))
if (await fileExistsAtPath(cursorRulesFilePath)) {
if (!(await isDirectory(cursorRulesFilePath))) {
try {
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
}
} catch (error) {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}:`, error)
}
}
return results
// we then check for the .cursor/rules dir
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
let cursorRulesDirInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesDirPath)) {
if (await isDirectory(cursorRulesDirPath)) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
}
}
}
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
}
/**
@@ -201,18 +132,22 @@ export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggle
* Only searches if a top-level agents.md file exists
*/
async function findAgentsMdFiles(cwd: string): Promise<string[]> {
// First check if top-level agents.md exists
const topLevelAgentsPath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
if (!(await fileExistsAtPath(topLevelAgentsPath))) {
return []
}
try {
// First check if top-level agents.md exists
const topLevelAgentsPath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
const topLevelExists = await fileExistsAtPath(topLevelAgentsPath)
// Only search recursively if top-level agents.md exists
if (!topLevelExists) {
return []
}
// Search recursively for all agents.md files
const [allFiles] = await listFiles(cwd, true, 500)
const agentsFileName = GlobalFileNames.agentsRulesFile.toLowerCase()
return allFiles.filter((filePath) => path.basename(filePath).toLowerCase() === agentsFileName)
return allFiles.filter((filePath) => {
const basename = path.basename(filePath).toLowerCase()
return basename === GlobalFileNames.agentsRulesFile.toLowerCase()
})
} catch (error) {
console.error(`Failed to find agents.md files in ${cwd}:`, error)
return []
@@ -223,11 +158,6 @@ async function findAgentsMdFiles(cwd: string): Promise<string[]> {
* Gather formatted agents rules - searches recursively and combines all agents.md files
*/
export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggles) => {
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return undefined
}
const agentsRulesFilePath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
// Check if the top-level agents.md file is enabled
@@ -237,33 +167,35 @@ export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggle
try {
const agentsMdFiles = await findAgentsMdFiles(cwd)
if (agentsMdFiles.length === 0) {
return undefined
}
// Read and combine all agents.md files in parallel
const contentPromises = agentsMdFiles.map(async (filePath) => {
try {
const fullPath = path.resolve(cwd, filePath)
const content = (await fs.readFile(fullPath, "utf8")).trim()
if (!content) {
// Read and combine all agents.md files
const combinedContent = await Promise.all(
agentsMdFiles.map(async (filePath) => {
try {
const fullPath = path.resolve(cwd, filePath)
const content = (await fs.readFile(fullPath, "utf8")).trim()
if (content) {
const relativePath = path.relative(cwd, fullPath)
return `## ${relativePath}\n\n${content}`
}
return null
} catch (error) {
console.error(`Failed to read agents.md file at ${filePath}:`, error)
return null
}
}),
).then((contents) => contents.filter(Boolean).join("\n\n"))
const relativePath = path.relative(cwd, fullPath)
return `## ${relativePath}\n\n${content}`
} catch (error) {
console.error(`Failed to read agents.md file at ${filePath}:`, error)
return null
}
})
const contents = await Promise.all(contentPromises)
const combinedContent = contents.filter(Boolean).join("\n\n")
return combinedContent ? formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent) : undefined
if (combinedContent) {
return formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent)
}
} catch (error) {
console.error("Failed to read agents.md files:", error)
return undefined
}
return undefined
}
+22 -18
View File
@@ -1,29 +1,29 @@
import { Anthropic } from "@anthropic-ai/sdk"
import type { Anthropic } from "@anthropic-ai/sdk"
import { buildApiHandler } from "@core/api"
import { tryAcquireTaskLockWithRetry } from "@core/task/TaskLockUtils"
import { detectWorkspaceRoots } from "@core/workspace/detection"
import { setupWorkspaceManager } from "@core/workspace/setup"
import { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
import type { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog, McpMarketplaceItem } from "@shared/mcp"
import { Settings } from "@shared/storage/state-keys"
import { Mode } from "@shared/storage/types"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import type { ApiProvider, ModelInfo } from "@shared/api"
import type { ChatContent } from "@shared/ChatContent"
import type { ExtensionState, Platform } from "@shared/ExtensionMessage"
import type { HistoryItem } from "@shared/HistoryItem"
import type { McpMarketplaceCatalog, McpMarketplaceItem } from "@shared/mcp"
import type { Settings } from "@shared/storage/state-keys"
import type { Mode } from "@shared/storage/types"
import type { TelemetrySetting } from "@shared/TelemetrySetting"
import type { UserInfo } from "@shared/UserInfo"
import { fileExistsAtPath } from "@utils/fs"
import axios from "axios"
import fs from "fs/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import type { FolderLockWithRetryResult } from "src/core/locks/types"
import * as vscode from "vscode"
import type * as vscode from "vscode"
import { ClineEnv } from "@/config"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
@@ -35,7 +35,7 @@ import { getDistinctId } from "@/services/logging/distinctId"
import { telemetryService } from "@/services/telemetry"
import { getAxiosSettings } from "@/shared/net"
import { ShowMessageType } from "@/shared/proto/host/window"
import { AuthState } from "@/shared/proto/index.cline"
import type { AuthState } from "@/shared/proto/index.cline"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { PromptRegistry } from "../prompts/system-prompt"
@@ -47,10 +47,11 @@ import {
writeMcpMarketplaceCatalogToCache,
} from "../storage/disk"
import { fetchRemoteConfig } from "../storage/remote-config/fetch"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import { StreamingResponseHandler } from "./grpc-handler"
import type { StreamingResponseHandler } from "./grpc-handler"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { getClineOnboardingModels } from "./models/getClineOnboardingModels"
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
import { checkCliInstallation } from "./state/checkCliInstallation"
import { sendStateUpdate } from "./state/subscribeToState"
@@ -846,6 +847,7 @@ export class Controller {
async getStateToPostToWebview(): Promise<ExtensionState> {
// Get API configuration from cache for immediate access
const onboardingModels = getClineOnboardingModels()
const apiConfiguration = this.stateManager.getApiConfiguration()
const lastShownAnnouncementId = this.stateManager.getGlobalStateKey("lastShownAnnouncementId")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
@@ -858,7 +860,6 @@ export class Controller {
const mode = this.stateManager.getGlobalSettingsKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
@@ -933,7 +934,10 @@ export class Controller {
mode,
strictPlanModeEnabled,
yoloModeToggled,
useAutoCondense,
useAutoCondense: {
user: this.stateManager.getGlobalSettingsKey("useAutoCondense"),
featureFlag: featureFlagsService.getUseAutoCondenseEnabled(),
},
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
@@ -958,7 +962,7 @@ export class Controller {
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
showOnboardingFlow: featureFlagsService.getOnboardingEnabled(),
onboardingModels,
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
@@ -0,0 +1,51 @@
import { featureFlagsService } from "@/services/feature-flags"
import { CLINE_ONBOARDING_MODELS } from "@/shared/cline/onboarding"
import { OnboardingModel, OnboardingModelGroup } from "@/shared/proto/cline/state"
type OnboardingModelOverride = OnboardingModel & { hidden?: boolean }
let cached: OnboardingModelGroup | null = null
export function getClineOnboardingModels(): OnboardingModelGroup {
if (cached) {
return cached
}
const remoteOverrides = featureFlagsService.getOnboardingOverrides()
const models = new Map<string, OnboardingModel>(CLINE_ONBOARDING_MODELS.map((model) => [model.id, model]))
// Apply remote overrides if available
if (remoteOverrides) {
for (const [id, override] of Object.entries(remoteOverrides) as [string, OnboardingModelOverride][]) {
if (override.hidden) {
models.delete(id)
} else {
const baseModel = models.get(id)
models.set(id, mergeModelWithOverride(baseModel, override))
}
}
}
cached = { models: Array.from(models.values()) }
return cached
}
function mergeModelWithOverride(baseModel: OnboardingModel | undefined, override: OnboardingModelOverride): OnboardingModel {
const baseInfo = baseModel?.info
const overrideInfo = override.info
// Merge info with proper defaults
const mergedInfo = {
...baseInfo,
...overrideInfo,
supportsPromptCache: overrideInfo?.supportsPromptCache ?? baseInfo?.supportsPromptCache ?? false,
tiers: overrideInfo?.tiers ?? baseInfo?.tiers ?? [],
}
// Return merged model, using base as foundation if available
return baseModel ? { ...baseModel, ...override, info: mergedInfo } : { ...override, info: mergedInfo }
}
export function clearOnboardingModelsCache(): void {
cached = null
}
@@ -4,6 +4,7 @@ import axios from "axios"
import cloneDeep from "clone-deep"
import fs from "fs/promises"
import path from "path"
import { StateManager } from "@/core/storage/StateManager"
import {
ANTHROPIC_MAX_THINKING_BUDGET,
CLAUDE_SONNET_1M_TIERS,
@@ -78,7 +79,7 @@ interface OpenRouterRawModelInfo {
export async function refreshOpenRouterModels(controller: Controller): Promise<Record<string, ModelInfo>> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const models: Record<string, ModelInfo> = {}
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models", getAxiosSettings())
@@ -242,12 +243,17 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
// If we failed to fetch models, try to read cached models
const cachedModels = await controller.readOpenRouterModels()
if (cachedModels) {
// Cached models are already in application format (ModelInfo)
return appendClineStealthModels(cachedModels as Record<string, ModelInfo>)
models = cachedModels
}
}
// Append stealth models if any
return appendClineStealthModels(models)
const finalModels = appendClineStealthModels(models)
// Store in StateManager's in-memory cache
StateManager.get().setModelsCache("openRouter", finalModels)
return finalModels
}
/**
@@ -1,8 +1,8 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import axios from "axios"
import { getAxiosSettings } from "@/shared/net"
import { toRequestyServiceUrl } from "@/shared/clients/requesty"
import { getAxiosSettings } from "@/shared/net"
import { Controller } from ".."
/**
@@ -110,6 +110,8 @@ export async function updateApiConfigurationProto(
actModeAihubmixModelInfo: protoApiConfiguration.actModeAihubmixModelInfo
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeAihubmixModelInfo)
: undefined,
geminiPlanModeThinkingLevel: protoApiConfiguration.geminiPlanModeThinkingLevel,
geminiActModeThinkingLevel: protoApiConfiguration.geminiActModeThinkingLevel,
}
// Update the API configuration in storage
@@ -0,0 +1,17 @@
import type { EmptyRequest } from "@shared/proto/cline/common"
import { Empty } from "@shared/proto/cline/common"
import type { Controller } from "../index"
/**
* Flush all pending state changes immediately to disk
* Bypasses the debounced persistence and forces immediate writes
*/
export async function flushPendingState(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
await controller.stateManager.flushPendingState()
return Empty.create({})
} catch (error) {
console.error("[flushPendingState] Error flushing pending state:", error)
throw error
}
}
@@ -0,0 +1,10 @@
/**
* Error thrown when a PreToolUse hook requests cancellation.
* This signals to the tool handler that execution should be aborted.
*/
export class PreToolUseHookCancellationError extends Error {
constructor(message: string = "PreToolUse hook requested cancellation") {
super(message)
this.name = "PreToolUseHookCancellationError"
}
}
@@ -0,0 +1,7 @@
#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "COMPLETED: " + input.taskComplete.taskMetadata.result,
errorMessage: ""
}));
@@ -0,0 +1,3 @@
#!/usr/bin/env node
console.error("Hook execution error");
process.exit(1);

Some files were not shown because too many files have changed in this diff Show More