Compare commits

..

4 Commits

Author SHA1 Message Date
celestial-vault a07554f191 rework the docker:shell script to reuse an existing container 2025-11-14 21:39:59 -08:00
celestial-vault 474c655240 update script documentation for next steps after docker build 2025-11-14 11:11:25 -08:00
celestial-vault b5157a2376 code comment 2025-11-14 11:02:53 -08:00
celestial-vault b14db72140 add docker setup for cli development 2025-11-13 21:31:54 -08:00
217 changed files with 4776 additions and 8515 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix: do not retry request automatically on auth failure.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix task timeline display height.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Ensure tool arguments are streamed during file operations when native tool calling is enabled.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added thinking level setting for Gemini 3.0 Pro
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Feat: add thought signature support for Gemini SDK
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Feat: Enable native tool calling for Baseten and Kimi K2 models
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Replaces generic robot icon with Cline logo across VS Code UI
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add Kimi K2 Thinking to Baseten Provider
-5
View File
@@ -1,5 +0,0 @@
---
"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
---
Fix Anthropic provider missing signature param when thinking is enabled.
-21
View File
@@ -165,27 +165,6 @@
},
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Open Storybook",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"storybook"
],
"cwd": "${workspaceFolder}/webview-ui",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"serverReadyAction": {
"pattern": "Local:.*http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
},
"env": {
"IS_DEV": "true"
}
}
]
}
-20
View File
@@ -263,26 +263,6 @@
"watch"
],
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
},
{
"type": "npm",
"script": "storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
"label": "npm: storybook",
"dependsOn": [
"npm: protos",
"npm: build:webview"
],
"presentation": {
"reveal": "always"
},
"options": {
"env": {
"IS_DEV": "true"
}
}
}
],
"inputs": [
+6 -22
View File
@@ -1,30 +1,14 @@
# Changelog
## [3.38.1]
## 3.37.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
- cf8dd1c: Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
- 02abbcf: Add AGENTS.md support
- 855db7d: feat(models): Add free minimax/mimax-m2 model to the model picker
## [3.37.0]
### Added
## 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
@@ -34,7 +18,7 @@
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
### Fixed
## Fixed
- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
-30
View File
@@ -1,30 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "cline-bot",
"majorVersion": 1,
"minorVersion": 0,
"fontURL": "https://cline.bot",
"designerURL": "https://cline.bot",
"licenseURL": "https://cline.bot",
"version": "Version 1.0",
"fontId": "cline-bot",
"psName": "cline-bot",
"subFamily": "Regular",
"fullName": "cline-bot",
"description": "Font generated by IcoMoon."
}
]]>
</json>
</metadata>
<defs>
<font id="cline-bot" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="cline" data-tags="cline" horiz-adv-x="977" d="M964.553 383.11l-60.285 121.406v69.495c0 115.545-92.939 209.321-207.647 209.321h-102.986c7.536 15.071 11.722 32.654 11.722 51.074 0 64.471-51.912 116.383-115.545 116.383s-115.545-51.912-115.545-116.383 4.186-35.166 11.722-51.074h-102.986c-114.708 0-207.647-93.776-207.647-209.321v-69.495l-61.959-121.406c-5.861-11.722-5.861-26.793 0-38.515l61.959-119.732v-69.495c0-115.545 92.939-209.321 207.647-209.321h415.294c114.708 0 207.647 93.776 207.647 209.321v69.495l60.285 119.732c5.861 11.722 5.861 25.956 0 38.515v0zM426.178 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132zM731.787 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.
+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, true)
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, false)
}
// SwitchToBYOProvider switches to a BYO provider that's already configured.
+77 -1
View File
@@ -221,7 +221,83 @@ func (p *ToolResultParser) ParseCodeDefinitions(content string) string {
// ParseWebFetch formats webFetch tool results with content preview
func (p *ToolResultParser) ParseWebFetch(content, url string) string {
return ""
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()
}
// detectLanguage returns syntax highlighting language based on file extension
+48
View File
@@ -0,0 +1,48 @@
# Git
.git
.gitignore
.gitattributes
# Node modules
node_modules
npm-debug.log
# Build artifacts
dist
dist-standalone
build
*.log
# Generated code
src/generated
# CLI build artifacts
cli/bin
cli/dist
# Webview build artifacts
webview-ui/dist
webview-ui/build
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Documentation
*.md
!README.md
# Tests
tests
*.test.js
*.spec.js
# CI/CD
.github
.gitlab-ci.yml
+49
View File
@@ -0,0 +1,49 @@
FROM node:22-slim
# TARGETARCH enables multi-architecture support without emulation warnings:
# - Docker automatically sets TARGETARCH to the build platform's architecture
# - On arm64 machines (Apple Silicon): TARGETARCH=arm64, uses linux-arm64 binaries
# - On amd64 machines (Intel/AMD): TARGETARCH=amd64, uses linux-x64 binaries
# The corresponding platform-specific binaries and native modules (better-sqlite3)
# are pre-built by scripts/package-standalone.mjs during the build process.
ARG TARGETARCH
# Install only runtime dependencies
RUN apt-get update && apt-get install -y \
git curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/cline
# Copy the entire pre-built distribution
COPY dist-standalone/ ./
# Create symlink for Linux native modules
# Map Docker's TARGETARCH (arm64/amd64) to Node's platform naming (x64 for amd64)
RUN if [ "$TARGETARCH" = "amd64" ]; then \
ln -sf /opt/cline/binaries/linux-x64/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
else \
ln -sf /opt/cline/binaries/linux-$TARGETARCH/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
fi
# Set up CLI binaries
# The Linux binaries are already in /opt/cline/bin/ from dist-standalone
# Just need to create symlinks to the platform-specific ones
RUN cd /opt/cline/bin && \
ln -sf cline-linux-$TARGETARCH cline && \
ln -sf cline-host-linux-$TARGETARCH cline-host && \
chmod +x cline-linux-$TARGETARCH cline-host-linux-$TARGETARCH cline cline-host
# Add binaries to PATH
ENV PATH="/opt/cline/bin:${PATH}"
ENV NODE_ENV=production
ENV CLINE_HOME=/root/.cline
RUN mkdir -p $CLINE_HOME
WORKDIR /workspace
EXPOSE 8000
ENTRYPOINT ["/opt/cline/bin/cline"]
CMD ["--help"]
+2 -2
View File
@@ -189,7 +189,6 @@
"provider-config/fireworks",
"provider-config/zai",
"provider-config/gcp-vertex-ai",
"provider-config/baseten",
{
"group": "AWS Bedrock",
"pages": [
@@ -216,7 +215,8 @@
"provider-config/vscode-language-model-api",
"provider-config/sap-aicore",
"provider-config/vercel-ai-gateway",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/baseten"
]
}
]
+34 -6
View File
@@ -5146,6 +5146,28 @@
"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",
@@ -6468,9 +6490,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -10213,6 +10235,12 @@
"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",
@@ -10581,9 +10609,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
"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==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
-4
View File
@@ -14,9 +14,5 @@
"description": "",
"dependencies": {
"mintlify": "^4.2.23"
},
"overrides": {
"tar-fs": "^3.1.1",
"js-yaml": "^4.1.1"
}
}
+43 -14
View File
@@ -3,7 +3,7 @@ title: "Baseten"
description: "Learn how to configure and use Baseten's Model APIs with Cline. Access frontier open-source models with enterprise-grade performance, reliability, and competitive pricing."
---
Baseten provides on-demand frontier model APIs designed for production applications, not just experimentation. Built on the Baseten Inference Stack, these APIs deliver optimized inference for leading open-source models from OpenAI, DeepSeek, Moonshot AI, and Alibaba Cloud.
Baseten provides on-demand frontier model APIs designed for production applications, not just experimentation. Built on the Baseten Inference Stack, these APIs deliver enterprise-grade performance and reliability with optimized inference for leading open-source models from OpenAI, DeepSeek, Meta, Moonshot AI, and Alibaba Cloud.
**Website:** [https://www.baseten.co/products/model-apis/](https://www.baseten.co/products/model-apis/)
@@ -14,21 +14,13 @@ Baseten provides on-demand frontier model APIs designed for production applicati
3. **Create a Key:** Generate a new API key. Give it a descriptive name (e.g., "Cline").
4. **Copy the Key:** Copy the API key immediately and store it securely.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Baseten" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Baseten API key into the "Baseten API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
**IMPORTANT: For Kimi K2 Thinking:** To use the `moonshotai/Kimi-K2-Thinking` model, you must enable **Native Tool Call (Experimental)** in Cline settings. This setting allows Cline to call tools through their native tool processor and is required for this reasoning model to function properly.
### Supported Models
Cline supports all current models under Baseten Model APIs, including:
For the most updated pricing, please visit: https://www.baseten.co/products/model-apis/
Note: Kimi K2 0711, Llama 4 Maverick, and Llama 4 Scout Model APIs have been deprecated at 5pm PT on October 8th.
https://www.baseten.co/resources/changelog/model-api-deprecation-notice-kimi-k2-0711-scout-maverick/
- `moonshotai/Kimi-K2-Thinking` (Moonshot AI) - Enhanced reasoning capabilities with step-by-step thought processes (262K context) - \$0.60/\$2.50 per 1M tokens
- `zai-org/GLM-4.6` (Z AI) - Frontier open model with advanced agentic, reasoning and coding capabilities by Z AI (200k context) \$0.60/\$2.20 per 1M tokens
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
@@ -39,6 +31,13 @@ For the most updated pricing, please visit: https://www.baseten.co/products/mode
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - \$0.50/\$1.50 per 1M tokens
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - \$0.77/\$0.77 per 1M tokens
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Baseten" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Baseten API key into the "Baseten API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
### Production-First Architecture
Baseten's Model APIs are built for production environments with several key advantages:
@@ -60,17 +59,47 @@ Baseten's Model APIs are built for production environments with several key adva
#### Developer Experience
- **OpenAI compatible API** - migrate by swapping a single URL
- **Drop-in replacement** for closed models with comprehensive observability and analytics
- **Drop-in replacement** for closed models with comprehensive observability
- **Seamless scaling** from Model APIs to dedicated deployments
### Special Features
#### Function Calling & Tool Use
All Baseten models support structured outputs, function calling, and tool use as part of the Baseten Inference Stack, making them ideal for agentic applications and coding workflows.
All Baseten models support structured outputs, function calling, and tool use as part of the Baseten Inference Stack, making them ideal for agentic applications.
#### Reasoning Capabilities
DeepSeek models offer enhanced reasoning with step-by-step thought processes, while maintaining production-ready performance.
#### Long Context Support
- **Up to 1 million tokens** for Llama 4 models (Maverick and Scout)
- **262K tokens** for Qwen3 models
- **163K tokens** for DeepSeek models
- **Perfect for code repositories** and complex multi-turn conversations
#### Quantization Optimizations
Models are deployed with advanced quantization techniques (fp4, fp8, fp16) for optimal performance while maintaining quality.
### Migration from Other Providers
Baseten's OpenAI compatibility makes migration straightforward:
**From OpenAI:**
- Swap `api.openai.com` with `inference.baseten.co/v1`
- Keep existing request/response formats
- Benefit from significant cost savings
**From Other Providers:**
- Use standard OpenAI SDK format
- Maintain existing prompting strategies
- Access to newer open-source models
### Tips and Notes
- **Dynamic Model Updates:** Cline automatically fetches the latest model list from Baseten, ensuring access to new models as they're released in real time.
- **Model Selection:** Choose models based on your specific use case - reasoning models for complex tasks, coding models for development work, and flagship models for general applications.
- **Cost Optimization:** Baseten offers some of the most competitive pricing in the market, especially for open-source models.
- **Context Windows:** Take advantage of large context windows (up to 1M tokens) for including substantial codebases and documentation.
- **Enterprise Ready:** Baseten is designed for production use with enterprise-grade security, compliance, and reliability.
- **Dynamic Model Updates:** Cline automatically fetches the latest model list from Baseten, ensuring access to new models as they're released.
- **Multi-Cloud Capacity Management (MCM):** Baseten's multi-cloud infrastructure ensures high availability and low latency globally.
- **Support:** Baseten provides dedicated support for production deployments and can work with you on dedicated resources as you scale.
+1422
View File
File diff suppressed because it is too large Load Diff
+45 -46
View File
@@ -1,48 +1,47 @@
{
"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"
}
"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"
}
}
+933 -407
View File
File diff suppressed because it is too large Load Diff
+15 -21
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.38.1",
"version": "3.37.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -46,15 +46,6 @@
],
"main": "./dist/extension.js",
"contributes": {
"icons": {
"cline-icon": {
"description": "cline",
"default": {
"fontPath": "assets/icons/cline-bot.woff",
"fontCharacter": "\\e900"
}
}
},
"walkthroughs": [
{
"id": "ClineWalkthrough",
@@ -183,7 +174,10 @@
"command": "cline.generateGitCommitMessage",
"title": "Generate Commit Message with Cline",
"category": "Cline",
"icon": "$(cline-icon)"
"icon": {
"light": "assets/icons/robot_panel_light.png",
"dark": "assets/icons/robot_panel_dark.png"
}
},
{
"command": "cline.abortGitCommitMessage",
@@ -312,6 +306,8 @@
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
"build:npm": "scripts/build-npm-package.sh",
"build:docker:dev": "node scripts/build-docker-dev.mjs",
"docker:shell": "node scripts/docker-shell.mjs",
"test:install": "bash scripts/test-install.sh",
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
"postcompile-standalone": "node scripts/package-standalone.mjs",
@@ -365,8 +361,7 @@
"docs": "cd docs && npm run dev",
"docs:check-links": "cd docs && npm run check",
"docs:rename-file": "cd docs && npm run rename",
"report-issue": "node scripts/report-issue.js",
"storybook": "cd webview-ui && npm run storybook"
"report-issue": "node scripts/report-issue.js"
},
"lint-staged": {
"*": [
@@ -424,7 +419,7 @@
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^1.30.0",
"@google/genai": "^1.11.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
@@ -449,8 +444,8 @@
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@playwright/test": "^1.55.1",
"@sap-ai-sdk/ai-api": "^2.1.0",
"@sap-ai-sdk/orchestration": "^2.1.0",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
@@ -468,6 +463,7 @@
"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",
@@ -477,6 +473,7 @@
"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",
@@ -484,7 +481,7 @@
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
"openai": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-mutex": "^1.0.0",
"p-timeout": "^6.1.4",
@@ -509,10 +506,7 @@
"zod": "^3.24.2"
},
"overrides": {
"tar-fs": ">=3.1.1",
"tar": "^7.5.2",
"vite": "^7.1.11",
"js-yaml": "^4.1.1"
"tar-fs": ">=3.1.1"
},
"c8": {
"reporter": [
-70
View File
@@ -69,18 +69,6 @@ service FileService {
// Opens or creates a focus chain checklist markdown file for editing
rpc openFocusChainFile(StringRequest) returns (Empty);
// Refreshes all hook toggles (discovers hooks and their enabled state)
rpc refreshHooks(EmptyRequest) returns (HooksToggles);
// Toggles a hook on or off via chmod +x/-x
rpc toggleHook(ToggleHookRequest) returns (ToggleHookResponse);
// Creates a new hook from template
rpc createHook(CreateHookRequest) returns (CreateHookResponse);
// Deletes an existing hook file
rpc deleteHook(DeleteHookRequest) returns (DeleteHookResponse);
}
// Response for refreshRules operation
@@ -220,61 +208,3 @@ message ToggleWorkflowRequest {
bool enabled = 3;
RuleScope scope = 4; // Scope of the workflow (local, global, or remote)
}
// Maps from hook name to enabled/disabled status
message HookInfo {
string name = 1;
bool enabled = 2;
string absolutePath = 3;
}
message WorkspaceHooks {
string workspace_name = 1;
repeated HookInfo hooks = 2;
}
message HooksToggles {
repeated HookInfo global_hooks = 1;
repeated WorkspaceHooks workspace_hooks = 2;
bool is_windows = 3; // Whether the system is Windows (toggles disabled)
}
// Request to toggle a hook
message ToggleHookRequest {
Metadata metadata = 1;
string hook_name = 2; // Name of the hook (e.g., "TaskStart")
bool is_global = 3; // Whether this is a global or workspace hook
bool enabled = 4; // Whether to enable (chmod +x) or disable (chmod -x)
optional string workspace_name = 5; // For multi-root workspaces, specifies which workspace
}
// Response for toggleHook operation
message ToggleHookResponse {
HooksToggles hooks_toggles = 1;
}
// Request to create a hook
message CreateHookRequest {
Metadata metadata = 1;
string hook_name = 2; // Name of the hook to create
bool is_global = 3; // Whether to create in global or workspace hooks directory
optional string workspace_name = 4; // For multi-root workspaces, specifies which workspace
}
// Response for createHook operation
message CreateHookResponse {
HooksToggles hooks_toggles = 1;
}
// Request to delete a hook
message DeleteHookRequest {
Metadata metadata = 1;
string hook_name = 2; // Name of the hook to delete
bool is_global = 3; // Whether this is a global or workspace hook
optional string workspace_name = 4; // For multi-root workspaces, specifies which workspace
}
// Response for deleteHook operation
message DeleteHookResponse {
HooksToggles hooks_toggles = 1;
}
-3
View File
@@ -97,7 +97,6 @@ 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
@@ -589,7 +588,6 @@ 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;
@@ -630,5 +628,4 @@ 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;
}
+1 -15
View File
@@ -362,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 OnboardingModelGroup onboarding_models = 33;
optional bool show_onboarding_flow = 33;
}
message UpdateTerminalConnectionTimeoutRequest {
@@ -390,17 +390,3 @@ 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;
}
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env node
import { execSync } from "child_process"
/**
* Build Docker image for Cline CLI
* This script builds a Docker image using pre-built binaries from dist-standalone/
*
* Prerequisites:
* - Run `npm run compile-standalone` first to build all platform binaries
* - Run `npm run compile-cli` first to build CLI binaries
*/
function runCommand(command, description) {
console.log(`\n${description}...`)
try {
execSync(command, { stdio: "inherit" })
console.log("✓ Success\n")
} catch (error) {
console.error(`✗ Failed: ${error.message}`)
process.exit(1)
}
}
function getCommandOutput(command) {
try {
return execSync(command, { encoding: "utf-8" }).trim()
} catch (error) {
return ""
}
}
function buildPrerequisites() {
console.log("Building prerequisites...\n")
// Build standalone (includes cline-core and platform-specific native modules)
runCommand("npm run compile-standalone", "Running npm run compile-standalone")
// Build CLI binaries for all platforms
runCommand("npm run compile-cli-all-platforms", "Running npm run compile-cli-all-platforms")
console.log("✓ All prerequisites built successfully\n")
}
function main() {
console.log("🐳 Building Cline CLI Docker Image\n")
// Remove existing container to ensure clean state after rebuild
const containerId = getCommandOutput(`docker ps -aq --filter "name=^cline-cli-dev$"`)
if (containerId) {
console.log("🗑️ Removing existing container to ensure fresh start...")
try {
execSync(`docker rm -f cline-cli-dev`, { stdio: "inherit" })
console.log("✓ Container removed\n")
} catch (error) {
console.log("Note: Container cleanup failed, continuing anyway\n")
}
}
buildPrerequisites()
// Build Docker image for native platform
// Docker will automatically use the correct architecture (arm64 on Apple Silicon, amd64 on Intel)
runCommand("docker build -f docker/Dockerfile -t cline-cli:dev .", "Building Docker image")
console.log("✅ Docker image built successfully!")
console.log("\n📋 Next steps:\n")
console.log("Interactive shell:")
console.log(" npm run docker:shell\n")
console.log("This will:")
console.log(" • Reuse existing 'cline-cli-dev' container if running")
console.log(" • Start stopped container if it exists")
console.log(" • Create new persistent container if none exists")
console.log(" • Mount current directory at /workspace")
console.log(" • Provide all CLI commands (cline auth, cline task, etc.)")
console.log("\nContainer persists between sessions. To remove:")
console.log(" docker rm -f cline-cli-dev\n")
}
main()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env node
import { execSync } from "child_process"
import { platform } from "os"
const CONTAINER_NAME = "cline-cli-dev"
function runCommand(command) {
try {
return execSync(command, { encoding: "utf-8" }).trim()
} catch (error) {
return ""
}
}
function getCurrentDirectory() {
// Get current working directory in a cross-platform way
return process.cwd()
}
function main() {
console.log("🐳 Cline CLI Docker Shell\n")
// Check if container exists (running or stopped)
const containerId = runCommand(`docker ps -a --filter "name=^${CONTAINER_NAME}$" --format "{{.ID}}"`)
if (containerId) {
// Check if container is running
const isRunning = runCommand(`docker ps --filter "id=${containerId}" --format "{{.ID}}"`)
if (isRunning) {
console.log(`📦 Connecting to running container: ${CONTAINER_NAME}\n`)
try {
execSync(`docker exec -it ${containerId} /bin/bash`, { stdio: "inherit" })
} catch (error) {
// User exited shell normally
}
} else {
console.log(`▶️ Starting stopped container: ${CONTAINER_NAME}\n`)
try {
execSync(`docker start ${containerId}`, { stdio: "inherit" })
execSync(`docker exec -it ${containerId} /bin/bash`, { stdio: "inherit" })
} catch (error) {
// User exited shell normally
}
}
} else {
console.log(`🚀 Creating new container: ${CONTAINER_NAME}\n`)
const cwd = getCurrentDirectory()
try {
// Use different volume mount syntax for Windows vs Unix
const isWindows = platform() === "win32"
const volumeMount = isWindows ? `${cwd.replace(/\\/g, "/")}:/workspace` : `${cwd}:/workspace`
execSync(
`docker run -it --name ${CONTAINER_NAME} -v "${volumeMount}" -w /workspace --entrypoint /bin/bash cline-cli:dev`,
{ stdio: "inherit" },
)
} catch (error) {
// User exited shell normally
}
}
}
main()
+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 * as tar from "tar"
import tar from "tar"
import { promisify } from "util"
import { createGunzip } from "zlib"
+1
View File
@@ -24,6 +24,7 @@ const TARGET_PLATFORMS = [
{ platform: "darwin", arch: "x64", targetDir: "darwin-x64" },
{ platform: "darwin", arch: "arm64", targetDir: "darwin-arm64" },
{ platform: "linux", arch: "x64", targetDir: "linux-x64" },
{ platform: "linux", arch: "arm64", targetDir: "linux-arm64" },
]
const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
+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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): string {
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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,6 +9,14 @@ export interface EnvironmentConfig {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
firebase: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId?: string
}
}
class ClineEndpoint {
@@ -55,6 +63,14 @@ 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 {
@@ -62,6 +78,11 @@ 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 {
@@ -69,6 +90,14 @@ 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",
},
}
}
}
+3 -5
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,8 +47,9 @@ import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
export type CommonApiHandlerOptions = {
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
}
export interface ApiHandler {
createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[], useResponseApi?: boolean): ApiStream
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream
getModel(): ApiHandlerModel
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
}
@@ -94,7 +95,6 @@ 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,7 +167,6 @@ 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,
})
@@ -252,7 +251,6 @@ 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: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ 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: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ 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: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ 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: ClineStorageMessage[] = [
const messages: Anthropic.Messages.MessageParam[] = [
{
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: ClineStorageMessage[] = [
const messages: Anthropic.Messages.MessageParam[] = [
{
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: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
const usageInfo = []
@@ -114,7 +114,7 @@ describe("OllamaHandler", () => {
}
const systemPrompt = "You are a helpful assistant."
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ 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: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
@@ -204,7 +204,7 @@ describe("OllamaHandler", () => {
}
const systemPrompt = "You are a helpful assistant."
const messages: ClineStorageMessage[] = [{ role: "user", content: "Hello" }]
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
+20 -8
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: ClineStorageMessage[], tools?: AnthropicTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -74,7 +74,19 @@ export class AnthropicHandler implements ApiHandler {
case "claude-opus-4-1-20250805":
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307": {
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
/*
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)
stream = await client.messages.create(
{
@@ -94,7 +106,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 : undefined,
tools: nativeToolsOn ? (tools as AnthropicTool[]) : 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.
@@ -123,9 +135,9 @@ export class AnthropicHandler implements ApiHandler {
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizeAnthropicMessages(messages, false),
tools: nativeToolsOn ? tools : undefined,
tool_choice: { type: "auto" },
messages: sanitizeAnthropicMessages(messages),
// tools,
// tool_choice: { type: "auto" },
stream: true,
})
break
@@ -204,7 +216,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 ant_thinking will be used to send the thinking traces back to the API
// 'reasoning' type just displays in the UI, but reasoning with signature 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const model = this.getModel()
+7 -15
View File
@@ -1,14 +1,12 @@
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
@@ -100,11 +98,10 @@ export class BasetenHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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 },
@@ -118,22 +115,21 @@ 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 && "reasoning" in delta && delta?.reasoning) {
const reasoning = typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning)
if ((delta as any)?.reasoning) {
const reasoningContent = (delta as any).reasoning as string
yield {
type: "reasoning",
reasoning,
reasoning: reasoningContent,
}
continue
}
// Handle content field
@@ -144,10 +140,6 @@ 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)
+16 -23
View File
@@ -1,3 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
// Import proper AWS SDK types
import type { ContentBlock, Message } from "@aws-sdk/client-bedrock-runtime"
import {
@@ -11,7 +12,6 @@ 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
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: ClineStorageMessage[]): string {
private formatDeepseekR1Prompt(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): number {
private estimateInputTokens(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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,7 +680,11 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
throw 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)}`,
}
}
}
@@ -699,20 +703,9 @@ 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: ${message}`,
text: `[ERROR] Validation error: ${chunk.validationException.message}`,
}
} else if (chunk.throttlingException) {
yield {
@@ -786,7 +779,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createAnthropicMessage(
systemPrompt: string,
messages: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: string; info: ModelInfo },
enable1mContextWindow: boolean,
@@ -842,7 +835,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: ClineStorageMessage[]): Message[] {
private formatMessagesForConverseAPI(messages: Anthropic.Messages.MessageParam[]): Message[] {
return messages.map((message) => {
// Determine role (user or assistant)
const role = message.role === "user" ? ConversationRole.USER : ConversationRole.ASSISTANT
@@ -975,7 +968,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createNovaMessage(
systemPrompt: string,
messages: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
@@ -1015,7 +1008,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createOpenAIMessage(
systemPrompt: string,
messages: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: string; info: ModelInfo },
): ApiStream {
@@ -1150,7 +1143,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private async *createQwenMessage(
systemPrompt: string,
messages: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Filter out image blocks since Claude Code doesn't support them
const filteredMessages = filterMessagesForClaudeCode(messages)
+3 -10
View File
@@ -1,3 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import axios from "axios"
@@ -7,9 +8,7 @@ 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"
@@ -27,7 +26,6 @@ interface ClineHandlerOptions extends CommonApiHandlerOptions {
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
clineAccountId?: string
geminiThinkingLevel?: string
}
export class ClineHandler implements ApiHandler {
@@ -98,7 +96,7 @@ export class ClineHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
try {
const client = await this.ensureClient()
@@ -116,13 +114,11 @@ 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"]
@@ -153,7 +149,6 @@ export class ClineHandler implements ApiHandler {
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
@@ -185,7 +180,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 {
@@ -195,8 +190,6 @@ 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)
+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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+3 -3
View File
@@ -1,4 +1,4 @@
import { ClineStorageMessage } from "@/shared/messages/content"
import { Anthropic } from "@anthropic-ai/sdk"
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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): string {
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
+47 -54
View File
@@ -1,3 +1,4 @@
import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import {
ApiError,
@@ -6,11 +7,10 @@ import {
type GenerateContentResponseUsageMetadata,
GoogleGenAI,
FunctionDeclaration as GoogleTool,
ThinkingLevel,
Part,
} 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,7 +28,6 @@ interface GeminiHandlerOptions extends CommonApiHandlerOptions {
geminiApiKey?: string
geminiBaseUrl?: string
thinkingBudgetTokens?: number
thinkingLevel?: string
apiModelId?: string
ulid?: string
}
@@ -111,49 +110,34 @@ export class GeminiHandler implements ApiHandler {
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: GoogleTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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 ?? 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
}
const thinkingBudget = this.options.thinkingBudgetTokens ?? 0
const _maxBudget = info.thinkingConfig?.maxBudget ?? 0
// 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)
// Gemini 3.0 recommends 1.0
temperature: info.temperature ?? 1,
temperature: 0,
}
// Add thinking config if the model supports it
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,
if (thinkingBudget > 0) {
requestConfig.thinkingConfig = {
thinkingBudget: thinkingBudget,
includeThoughts: true,
}
}
// 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
@@ -164,8 +148,7 @@ export class GeminiHandler implements ApiHandler {
let thoughtsTokenCount = 0 // Initialize thought token counts
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
const isNativeToolCallsEnabled = tools?.length
if (isNativeToolCallsEnabled) {
if (tools?.length) {
requestConfig.tools = [{ functionDeclarations: tools }]
requestConfig.toolConfig = {
// Force the model to call 'any' function.
@@ -193,45 +176,56 @@ export class GeminiHandler implements ApiHandler {
}
// Handle thinking content from Gemini's response
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,
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
}
}
if (part.functionCall) {
const functionCall = part.functionCall
const args = Object.entries(functionCall.args || {}).filter(([_key, val]) => !!val)
if (functionCall.args && args.length > 0) {
}
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)
yield {
type: "tool_calls",
id: chunk.responseId,
tool_call: {
function: {
id: chunk.responseId,
id: functionCall.id || functionCall.name,
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
@@ -257,7 +251,6 @@ 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
+1 -2
View File
@@ -1,7 +1,6 @@
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 ".."
@@ -184,7 +183,7 @@ export class LiteLlmHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
+1 -2
View File
@@ -2,7 +2,6 @@ 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"
@@ -46,7 +45,7 @@ export class MinimaxHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+7 -7
View File
@@ -1,5 +1,7 @@
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 {
@@ -9,7 +11,6 @@ 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"
@@ -37,7 +38,7 @@ export class OcaHandler implements ApiHandler {
protected initializeClient(options: OcaHandlerOptions) {
return new (class OCIOpenAI extends OpenAI {
protected override async prepareOptions(opts: any): Promise<void> {
protected override async prepareOptions(opts: FinalRequestOptions<unknown>): 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")
@@ -54,7 +55,7 @@ export class OcaHandler implements ApiHandler {
status: number | undefined,
error: Object | undefined,
message: string | undefined,
headers: any | undefined,
headers: OpenAIHeaders | undefined,
): APIError {
interface OciError {
code?: string
@@ -74,8 +75,7 @@ export class OcaHandler implements ApiHandler {
if (opcRequestId) {
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
}
const statusCode = typeof status === "number" ? status : 500
return super.makeStatusError(statusCode, error ?? {}, ociErrorMessage, headers)
return super.makeStatusError(status, error, ociErrorMessage, headers)
}
})({
baseURL:
@@ -139,7 +139,7 @@ export class OcaHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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
const reasoningOn = budgetTokens !== 0 ? true : false
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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
+2 -219
View File
@@ -1,14 +1,12 @@
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"
@@ -63,20 +61,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(
systemPrompt: string,
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[],
messages: Anthropic.Messages.MessageParam[],
tools?: ChatCompletionTool[],
): ApiStream {
const client = this.ensureClient()
@@ -200,208 +185,6 @@ 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) {
+6 -2
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,7 +65,11 @@ export class OpenAiHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools?: ChatCompletionTool[],
): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
+7 -10
View File
@@ -1,11 +1,10 @@
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { StateManager } from "@core/storage/StateManager"
import { Anthropic } from "@anthropic-ai/sdk"
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"
@@ -21,7 +20,6 @@ interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
geminiThinkingLevel?: string
}
export class OpenRouterHandler implements ApiHandler {
@@ -56,7 +54,7 @@ export class OpenRouterHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
@@ -69,7 +67,6 @@ export class OpenRouterHandler implements ApiHandler {
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
this.options.geminiThinkingLevel,
)
let didOutputUsage: boolean = false
@@ -217,11 +214,11 @@ export class OpenRouterHandler implements ApiHandler {
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.openRouterModelId || openRouterDefaultModelId
const cachedModelInfo = StateManager.get().getModelInfo("openRouter", modelId)
return {
id: modelId,
info: cachedModelInfo || openRouterDefaultModelInfo,
const modelId = this.options.openRouterModelId
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
await this.ensureAuthenticated()
const client = this.ensureClient()
const model = this.getModel()
+2 -2
View File
@@ -1,3 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import {
InternationalQwenModelId,
internationalQwenDefaultModelId,
@@ -10,7 +11,6 @@ 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
+23 -26
View File
@@ -4,11 +4,10 @@ import {
ConversationRole as BedrockConversationRole,
type Message as BedrockMessage,
} from "@aws-sdk/client-bedrock-runtime"
import { ChatMessage, OrchestrationClient, OrchestrationModuleConfig } from "@sap-ai-sdk/orchestration"
import { ChatMessages, LlmModuleConfig, OrchestrationClient, TemplatingModuleConfig } 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"
@@ -116,7 +115,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: ClineStorageMessage[]): BedrockMessage[] {
export function formatMessagesForConverseAPI(messages: Anthropic.Messages.MessageParam[]): BedrockMessage[] {
return messages.map((message) => {
// Determine role (user or assistant)
const role = message.role === "user" ? BedrockConversationRole.USER : BedrockConversationRole.ASSISTANT
@@ -316,7 +315,7 @@ namespace Gemini {
*/
export function prepareRequestPayload(
systemPrompt: string,
messages: ClineStorageMessage[],
messages: Anthropic.Messages.MessageParam[],
model: { id: SapAiCoreModelId; info: ModelInfo },
thinkingBudgetTokens?: number,
): any {
@@ -459,7 +458,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
if (this.options.sapAiCoreUseOrchestrationMode) {
yield* this.createMessageWithOrchestration(systemPrompt, messages)
} else {
@@ -491,31 +490,29 @@ export class SapAiCoreHandler implements ApiHandler {
this.isAiCoreEnvSetup = true
}
private async *createMessageWithOrchestration(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
private async *createMessageWithOrchestration(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
// Ensure AI Core environment variable is set up (only runs once)
this.ensureAiCoreEnvSetup()
const model = this.getModel()
const orchestrationConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: {
name: model.id,
},
prompt: {
template: [
{
role: "system",
content: systemPrompt,
},
],
},
},
// Define the LLM to be used by the Orchestration pipeline
const llm: LlmModuleConfig = {
model_name: model.id,
}
const orchestrationClient = new OrchestrationClient(orchestrationConfig, {
resourceGroup: this.options.sapAiResourceGroup || "default",
})
const templating: TemplatingModuleConfig = {
template: [
{
role: "system",
content: systemPrompt,
},
],
}
const orchestrationClient = new OrchestrationClient(
{ llm, templating },
{ resourceGroup: this.options.sapAiResourceGroup || "default" },
)
const sapMessages = this.convertMessageParamToSAPMessages(messages)
@@ -541,7 +538,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
private async *createMessageWithDeployments(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
private async *createMessageWithDeployments(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const token = await this.getToken()
const headers = {
Authorization: `Bearer ${token}`,
@@ -1043,8 +1040,8 @@ export class SapAiCoreHandler implements ApiHandler {
}
return { id: sapAiCoreDefaultModelId, info: sapAiCoreModels[sapAiCoreDefaultModelId] }
}
private convertMessageParamToSAPMessages(messages: ClineStorageMessage[]): ChatMessage[] {
private convertMessageParamToSAPMessages(messages: Anthropic.Messages.MessageParam[]): ChatMessages {
// Use the existing OpenAI converter since the logic is identical
return convertToOpenAiMessages(messages) as ChatMessage[]
return convertToOpenAiMessages(messages) as ChatMessages
}
}
+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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
+16 -9
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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
const modelInfo = this.getModel().info
@@ -82,7 +82,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
@@ -101,16 +102,22 @@ 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 totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const cacheWriteTokens = chunk.usage.cache_creation_input_tokens || 0
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
totalCost,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
// @ts-expect-error - Vercel AI Gateway extends OpenAI types
totalCost: chunk.usage.cost || 0,
}
didOutputUsage = true
}
+11 -4
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: ClineStorageMessage[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
const model = this.getModel()
const modelId = model.id
@@ -103,6 +103,13 @@ 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,
@@ -116,7 +123,7 @@ export class VertexHandler implements ApiHandler {
cache_control: { type: "ephemeral" },
},
],
messages: sanitizeAnthropicMessages(messages, true),
messages: sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex),
stream: true,
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
// tool_choice options:
@@ -142,7 +149,7 @@ export class VertexHandler implements ApiHandler {
type: "text",
},
],
messages: sanitizeAnthropicMessages(messages, false),
messages: sanitizeAnthropicMessages(messages),
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: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], 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,3 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import {
internationalZAiDefaultModelId,
internationalZAiModelId,
@@ -9,7 +10,6 @@ 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: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+63 -77
View File
@@ -1,90 +1,76 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineStorageMessage, convertClineStorageToAnthropicMessage } from "@/shared/messages/content"
import Anthropic from "@anthropic-ai/sdk"
import { ClineStorageMessage } from "@/shared/messages/content"
/**
* 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
* 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.
*/
export function sanitizeAnthropicMessages(
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]
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)
messages: Array<ClineStorageMessage>,
lastUserMsgIndex?: number,
secondLastMsgUserIndex?: number,
): Array<Anthropic.Messages.MessageParam> {
return messages.map((_message, index) => {
const message = removeUnknownParams(_message)
const addCacheControl = lastUserMsgIndex !== undefined && secondLastMsgUserIndex !== undefined
// Construct message
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 anthropicMsg
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
}
})
}
const isThinkingBlock = (
block: Anthropic.ContentBlockParam,
): block is Anthropic.Messages.ThinkingBlockParam | Anthropic.Messages.RedactedThinkingBlockParam => {
return block.type === "thinking" || block.type === "redacted_thinking"
}
/**
* 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
* Remove reasoning details and other known params that are not Anthropic specific.
*/
function addCacheControl(message: Anthropic.MessageParam): Anthropic.MessageParam {
// Convert string content to array format
if (typeof message.content === "string") {
return {
...message,
content: [
{
type: "text",
text: message.content,
cache_control: { type: "ephemeral" },
} satisfies Anthropic.TextBlockParam,
],
}
function removeUnknownParams(param: ClineStorageMessage): Anthropic.Messages.MessageParam {
// Construct new content array with known Anthropic content blocks only.
return {
role: param.role === "user" ? "user" : "assistant",
content: Array.isArray(param.content)
? param.content.map((item) => {
return {
...item,
// Ensure reasoning_details is removed
reasoning_details: undefined,
}
})
: param.content, // String content remains unchanged
}
// 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 }
}
+2 -4
View File
@@ -1,8 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Content, GenerateContentResponse, Part } from "@google/genai"
import { ClineStorageMessage } from "@/shared/messages/content"
export function convertAnthropicContentToGemini(content: string | ClineStorageMessage["content"]): Part[] {
export function convertAnthropicContentToGemini(content: string | Anthropic.ContentBlockParam[]): Part[] {
if (typeof content === "string") {
return [{ text: content }]
}
@@ -10,7 +9,7 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
.flatMap((block): Part | undefined => {
switch (block.type) {
case "text":
return { text: block.text, thoughtSignature: block.signature }
return { text: block.text }
case "image":
if (block.source.type !== "base64") {
throw new Error("Unsupported image source type")
@@ -27,7 +26,6 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
name: block.name,
args: block.input as Record<string, unknown>,
},
thoughtSignature: block.signature,
}
case "tool_result":
return {
+21 -43
View File
@@ -10,15 +10,6 @@ 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[] {
@@ -159,7 +150,6 @@ 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)
}
})
@@ -174,26 +164,15 @@ export function convertToOpenAiMessages(
}
// Process tool use messages
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),
},
}
})
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),
},
}))
// Set content to blank when tool_calls are present but content has no text, per OpenAI API spec
const hasToolCalls = tool_calls.length > 0
@@ -360,30 +339,29 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
}
try {
if (openAiMessage?.tool_calls?.length) {
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 = {}
anthropicMessage.content.push(
...openAiMessage.tool_calls
.map((toolCall): Anthropic.ToolUseBlock => {
const parsedName = toolCall.type === "function" && toolCall.function.name
let parsedInput = toolCall.function.arguments
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: toolCall.function?.name || UNIQUE_ERROR_TOOL_NAME,
name: parsedName || UNIQUE_ERROR_TOOL_NAME,
input: parsedInput,
}
}),
)
}
return anthropicMessage
})
// Filter out any tool uses with the UNIQUE_ERROR_TOOL_NAME, which indicates a parsing error
.filter((toolUse) => toolUse.name !== UNIQUE_ERROR_TOOL_NAME),
)
}
} catch (error) {
console.error("Error converting OpenAI message to Anthropic format:", error)
console.error("Failed to process tool calls:", error)
}
return anthropicMessage
@@ -1,217 +0,0 @@
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
}
+1 -14
View File
@@ -21,7 +21,6 @@ export async function createOpenRouterStream(
thinkingBudgetTokens?: number,
openRouterProviderSorting?: string,
tools?: Array<ChatCompletionTool>,
geminiThinkingLevel?: string,
) {
// Convert Anthropic messages to OpenAI format
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -139,10 +138,6 @@ 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) {
@@ -166,12 +161,7 @@ export async function createOpenRouterStream(
}
break
default:
if (
thinkingBudgetTokens &&
model.info?.thinkingConfig &&
thinkingBudgetTokens > 0 &&
!(model.id.includes("gemini") && geminiThinkingLevel)
) {
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
break
@@ -199,9 +189,6 @@ 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
+4 -62
View File
@@ -1,20 +1,9 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk> & { id?: string }
export type ApiStream = AsyncGenerator<ApiStreamChunk>
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 {
@@ -25,74 +14,27 @@ 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 {
/**
* The call ID associated with this tool call
*/
call_id?: string
call_id?: string // The call / request ID associated with this tool call
// Information about the tool being called
function: {
/**
* The tool call ID
*/
id?: string
/**
* Name of the tool
*/
id?: string // The tool call ID
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
/**
* 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
*/
details?: unknown // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
signature?: string
/**
* redacted data
*/
redacted_data?: string
/**
* The response ID associated with this chunk
*/
id?: string
}
@@ -2,12 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
import { JSONParser } from "@streamparser/json"
import { McpHub } from "@/services/mcp/McpHub"
import { CLINE_MCP_TOOL_IDENTIFIER } from "@/shared/mcp"
import {
ClineAssistantRedactedThinkingBlock,
ClineAssistantThinkingBlock,
ClineAssistantToolUseBlock,
ClineReasoningDetailParam,
} from "@/shared/messages/content"
import { ClineAssistantToolUseBlock } from "@/shared/messages/content"
import { ClineDefaultTool } from "@/shared/tools"
export interface PendingToolUse {
@@ -15,7 +10,6 @@ export interface PendingToolUse {
name: string
input: string
parsedInput?: unknown
signature?: string
jsonParser?: JSONParser
call_id?: string
}
@@ -25,23 +19,6 @@ interface ToolUseDeltaBlock {
type?: string
name?: string
input?: string
signature?: string
}
export interface ReasoningDelta {
id?: string
reasoning?: string
signature?: string
details?: any[]
redacted_data?: any
}
export interface PendingReasoning {
id?: string
content: string
signature: string
redactedThinking: ClineAssistantRedactedThinkingBlock[]
summary: unknown[] | ClineReasoningDetailParam[]
}
const ESCAPE_MAP: Record<string, string> = {
@@ -54,40 +31,10 @@ const ESCAPE_MAP: Record<string, string> = {
const ESCAPE_PATTERN = /\\[ntr"\\]/g
export class StreamResponseHandler {
private toolUseHandler = new ToolUseHandler()
private reasoningHandler = new ReasoningHandler()
private _requestId: string | undefined
public setRequestId(id?: string) {
if (!this._requestId && id) {
this._requestId = id
}
}
public get requestId() {
return this._requestId
}
public getHandlers() {
return {
toolUseHandler: this.toolUseHandler,
reasonsHandler: this.reasoningHandler,
}
}
public reset() {
this._requestId = undefined
this.toolUseHandler = new ToolUseHandler()
this.reasoningHandler = new ReasoningHandler()
}
}
/**
* Handles streaming native tool use blocks and converts them to ClineAssistantToolUseBlock format
*/
class ToolUseHandler {
export class ToolUseHandler {
private pendingToolUses = new Map<string, PendingToolUse>()
processToolUseDelta(delta: ToolUseDeltaBlock, call_id?: string): void {
@@ -103,17 +50,12 @@ class ToolUseHandler {
if (delta.name) {
pending.name = delta.name
}
if (delta.signature) {
pending.signature = delta.signature
}
if (delta.input) {
pending.input += delta.input
try {
pending.jsonParser?.write(delta.input)
} catch {
// Expected during streaming - JSONParser may not have complete JSON yet
// Expected during streaming
}
}
}
@@ -140,17 +82,15 @@ class ToolUseHandler {
id: pending.id,
name: pending.name,
input,
signature: pending.signature,
call_id: pending.call_id,
}
}
getAllFinalizedToolUses(summary?: ClineAssistantToolUseBlock["reasoning_details"]): ClineAssistantToolUseBlock[] {
getAllFinalizedToolUses(): ClineAssistantToolUseBlock[] {
const results: ClineAssistantToolUseBlock[] = []
for (const id of this.pendingToolUses.keys()) {
const toolUse = this.getFinalizedToolUse(id)
if (toolUse) {
results.push({ ...toolUse, reasoning_details: summary })
results.push(toolUse)
}
}
return results
@@ -162,24 +102,19 @@ class ToolUseHandler {
getPartialToolUsesAsContent(): ToolUse[] {
const results: ToolUse[] = []
const pendingToolUses = this.pendingToolUses.values()
for (const pending of pendingToolUses) {
for (const pending of this.pendingToolUses.values()) {
if (!pending.name) {
continue
}
// Try to get the most up-to-date parsed input
// Priority: parsedInput (from JSONParser) > fallback to manual parsing
let input: any = {}
if (pending.parsedInput != null) {
input = pending.parsedInput
} else if (pending.input) {
// Try full JSON parse first
try {
input = JSON.parse(pending.input)
} catch {
// Fall back to extracting partial fields from incomplete JSON
input = this.extractPartialJsonFields(pending.input)
}
}
@@ -196,12 +131,10 @@ class ToolUseHandler {
},
partial: true,
isNativeToolCall: true,
signature: pending.signature,
call_id: pending.call_id,
})
} else {
const params: Record<string, string> = {}
if (typeof input === "object" && input !== null) {
if (typeof input === "object") {
for (const [key, value] of Object.entries(input)) {
params[key] = typeof value === "string" ? value : JSON.stringify(value)
}
@@ -211,14 +144,12 @@ class ToolUseHandler {
name: pending.name as ClineDefaultTool,
params: params as any,
partial: true,
signature: pending.signature,
isNativeToolCall: true,
call_id: pending.call_id,
})
}
}
// Ensure all returned tool uses are marked as partial
return results.map((t) => ({ ...t, partial: true }))
return results
}
reset(): void {
@@ -234,7 +165,6 @@ class ToolUseHandler {
parsedInput: undefined,
jsonParser,
call_id,
signature: undefined,
}
jsonParser.onValue = (info: any) => {
@@ -260,70 +190,3 @@ class ToolUseHandler {
return result
}
}
/**
* Handles streaming reasoning content and converts it to the appropriate message format
*/
class ReasoningHandler {
private pendingReasoning: PendingReasoning | null = null
processReasoningDelta(delta: ReasoningDelta): void {
// Initialize pending reasoning if we have an ID but no pending reasoning yet
if (!this.pendingReasoning) {
this.pendingReasoning = {
id: delta.id,
content: "",
signature: "",
redactedThinking: [],
summary: [],
}
}
if (!this.pendingReasoning) {
return
}
// Update fields from delta
if (delta.reasoning) {
this.pendingReasoning.content += delta.reasoning
}
if (delta.signature) {
this.pendingReasoning.signature = delta.signature
}
if (delta.details) {
if (Array.isArray(delta.details)) {
this.pendingReasoning.summary.push(...delta.details)
} else {
this.pendingReasoning.summary.push(delta.details)
}
}
if (delta.redacted_data) {
this.pendingReasoning.redactedThinking.push({
type: "redacted_thinking",
data: delta.redacted_data,
call_id: delta.id || this.pendingReasoning.id,
})
}
}
getCurrentReasoning(): ClineAssistantThinkingBlock | null {
if (!this.pendingReasoning) {
return null
}
return {
type: "thinking",
thinking: this.pendingReasoning.content,
signature: this.pendingReasoning.signature,
summary: this.pendingReasoning.summary,
}
}
getRedactedThinking(): ClineAssistantRedactedThinkingBlock[] {
return this.pendingReasoning?.redactedThinking || []
}
reset(): void {
this.pendingReasoning = null
}
}
+4 -34
View File
@@ -52,46 +52,16 @@ 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
/**
* 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.
*/
details?: any // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
signature?: string
/**
* whether this reasoning block has been redacted
*/
redacted?: boolean
/**
* redacted data
*/
data?: string
/**
* Indicates whether this is a partial reasoning block
*/
redacted?: boolean // whether this reasoning block has been redacted
data?: string // redacted data
partial: boolean
}
@@ -5,8 +5,7 @@ export function checkContextWindowExceededError(error: unknown): boolean {
checkIsOpenAIContextWindowError(error) ||
checkIsOpenRouterContextWindowError(error) ||
checkIsAnthropicContextWindowError(error) ||
checkIsCerebrasContextWindowError(error) ||
checkIsBedrockContextWindowError(error)
checkIsCerebrasContextWindowError(error)
)
}
@@ -71,45 +70,3 @@ 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
}
}
-55
View File
@@ -1,55 +0,0 @@
import { CreateHookRequest, CreateHookResponse } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { HookDiscoveryCache } from "../../hooks/HookDiscoveryCache"
import { getHookTemplate } from "../../hooks/templates"
import { isValidHookType, resolveHooksDirectory, VALID_HOOK_TYPES } from "../../hooks/utils"
import { Controller } from ".."
import { refreshHooks } from "./refreshHooks"
export async function createHook(
controller: Controller,
request: CreateHookRequest,
globalHooksDirOverride?: string,
): Promise<CreateHookResponse> {
const { hookName, isGlobal, workspaceName } = request
// Validate hook name is one of the valid hook types
if (!isValidHookType(hookName)) {
throw new Error(`Invalid hook type: "${hookName}". Valid hook types are: ${VALID_HOOK_TYPES.join(", ")}`)
}
// Determine target directory
const hooksDir = await resolveHooksDirectory(isGlobal, workspaceName, globalHooksDirOverride)
// Ensure directory exists
await fs.mkdir(hooksDir, { recursive: true })
const hookPath = path.join(hooksDir, hookName)
// Check if already exists
try {
await fs.stat(hookPath)
throw new Error(`Hook ${hookName} already exists at ${hookPath}`)
} catch (error) {
// Good - file doesn't exist yet
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error
}
}
// Get template content
const templateContent = getHookTemplate(hookName)
// Write file WITHOUT executable permissions (644) so hook is toggled off by default
// User can enable it later when they're ready
const mode = 0o644
await fs.writeFile(hookPath, templateContent, { mode })
// Invalidate hook discovery cache
await HookDiscoveryCache.getInstance().invalidateAll()
// Return updated hooks state
const hooksToggles = await refreshHooks(controller, undefined, globalHooksDirOverride)
return CreateHookResponse.create({ hooksToggles })
}
-37
View File
@@ -1,37 +0,0 @@
import { DeleteHookRequest, DeleteHookResponse } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { HookDiscoveryCache } from "../../hooks/HookDiscoveryCache"
import { resolveHooksDirectory } from "../../hooks/utils"
import { Controller } from ".."
import { refreshHooks } from "./refreshHooks"
export async function deleteHook(
controller: Controller,
request: DeleteHookRequest,
globalHooksDirOverride?: string,
): Promise<DeleteHookResponse> {
const { hookName, isGlobal, workspaceName } = request
// Determine hook path
const hooksDir = await resolveHooksDirectory(isGlobal, workspaceName, globalHooksDirOverride)
const hookPath = path.join(hooksDir, hookName)
// Verify hook exists before attempting deletion
try {
await fs.stat(hookPath)
} catch {
throw new Error(`Hook ${hookName} does not exist at ${hookPath}`)
}
// Delete the hook file
await fs.unlink(hookPath)
// Invalidate hook discovery cache
await HookDiscoveryCache.getInstance().invalidateAll()
// Return updated hooks state
const hooksToggles = await refreshHooks(controller, undefined, globalHooksDirOverride)
return DeleteHookResponse.create({ hooksToggles })
}
-93
View File
@@ -1,93 +0,0 @@
import { HookInfo, HooksToggles, WorkspaceHooks } from "@shared/proto/cline/file"
import fs from "fs/promises"
import os from "os"
import path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { VALID_HOOK_TYPES } from "../../hooks/utils"
import { Controller } from ".."
export async function refreshHooks(
_controller: Controller,
_request?: any,
globalHooksDirOverride?: string,
): Promise<HooksToggles> {
const globalHooksDir = globalHooksDirOverride || path.join(os.homedir(), "Documents", "Cline", "Hooks")
const isWindows = process.platform === "win32"
// Collect global hooks
const globalHooks: HookInfo[] = []
for (const hookName of VALID_HOOK_TYPES) {
const hookPath = path.join(globalHooksDir, hookName)
try {
const stat = await fs.stat(hookPath)
if (stat.isFile()) {
globalHooks.push(
HookInfo.create({
name: hookName,
enabled: await isExecutable(hookPath),
absolutePath: hookPath,
}),
)
}
} catch {
// File doesn't exist, skip
}
}
// Collect workspace hooks from all workspace folders
const workspacePaths = await HostProvider.workspace.getWorkspacePaths({})
const workspaceHooksList: WorkspaceHooks[] = []
for (const workspacePath of workspacePaths.paths) {
const workspaceHooksDir = path.join(workspacePath, ".clinerules", "hooks")
const hooks: HookInfo[] = []
for (const hookName of VALID_HOOK_TYPES) {
const hookPath = path.join(workspaceHooksDir, hookName)
try {
const stat = await fs.stat(hookPath)
if (stat.isFile()) {
hooks.push(
HookInfo.create({
name: hookName,
enabled: await isExecutable(hookPath),
absolutePath: hookPath,
}),
)
}
} catch {
// File doesn't exist, skip
}
}
// Add all workspaces, even if they have no hooks yet
// This allows users to create their first hook via the dropdown
const workspaceName = path.basename(workspacePath)
workspaceHooksList.push(
WorkspaceHooks.create({
workspaceName,
hooks,
}),
)
}
return HooksToggles.create({
globalHooks,
workspaceHooks: workspaceHooksList,
isWindows,
})
}
async function isExecutable(filePath: string): Promise<boolean> {
if (process.platform === "win32") {
// On Windows, files are "enabled" if they exist
return true
}
try {
await fs.access(filePath, fs.constants.X_OK)
return true
} catch {
return false
}
}
-41
View File
@@ -1,41 +0,0 @@
import { ToggleHookRequest, ToggleHookResponse } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { HookDiscoveryCache } from "../../hooks/HookDiscoveryCache"
import { resolveHooksDirectory } from "../../hooks/utils"
import { Controller } from ".."
import { refreshHooks } from "./refreshHooks"
export async function toggleHook(
controller: Controller,
request: ToggleHookRequest,
globalHooksDirOverride?: string,
): Promise<ToggleHookResponse> {
const { hookName, isGlobal, enabled, workspaceName } = request
// Determine hook path
const hooksDir = await resolveHooksDirectory(isGlobal, workspaceName, globalHooksDirOverride)
const hookPath = path.join(hooksDir, hookName)
// Verify hook exists
try {
await fs.stat(hookPath)
} catch {
throw new Error(`Hook ${hookName} does not exist at ${hookPath}`)
}
// On Windows, we can't use chmod, so we just return the current state
// without modifying the file. The frontend will disable the toggle.
if (process.platform !== "win32") {
// Toggle executable bit (Unix-like systems only)
await fs.chmod(hookPath, enabled ? 0o755 : 0o644)
}
// Invalidate cache
await HookDiscoveryCache.getInstance().invalidateAll()
// Return updated state
const hooksToggles = await refreshHooks(controller, undefined, globalHooksDirOverride)
return ToggleHookResponse.create({ hooksToggles })
}
+17 -19
View File
@@ -1,29 +1,29 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { 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 type { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
import { 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 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 { 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 { 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 type * as vscode from "vscode"
import * 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 type { AuthState } from "@/shared/proto/index.cline"
import { AuthState } from "@/shared/proto/index.cline"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { PromptRegistry } from "../prompts/system-prompt"
@@ -47,11 +47,10 @@ import {
writeMcpMarketplaceCatalogToCache,
} from "../storage/disk"
import { fetchRemoteConfig } from "../storage/remote-config/fetch"
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import type { StreamingResponseHandler } from "./grpc-handler"
import { 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"
@@ -847,7 +846,6 @@ 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")
@@ -960,7 +958,7 @@ export class Controller {
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
onboardingModels,
showOnboardingFlow: featureFlagsService.getOnboardingEnabled(),
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
@@ -982,7 +980,7 @@ export class Controller {
},
hooksEnabled: {
user: this.stateManager.getGlobalStateKey("hooksEnabled"),
featureFlag: featureFlagsService.getHooksEnabled(),
featureFlag: true, // Hooks feature is now always available
},
lastDismissedInfoBannerVersion,
lastDismissedModelBannerVersion,
@@ -1,51 +0,0 @@
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,7 +4,6 @@ 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,
@@ -79,7 +78,7 @@ interface OpenRouterRawModelInfo {
export async function refreshOpenRouterModels(controller: Controller): Promise<Record<string, ModelInfo>> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
let models: Record<string, ModelInfo> = {}
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models", getAxiosSettings())
@@ -243,17 +242,12 @@ 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) {
models = cachedModels
// Cached models are already in application format (ModelInfo)
return appendClineStealthModels(cachedModels as Record<string, ModelInfo>)
}
}
// Append stealth models if any
const finalModels = appendClineStealthModels(models)
// Store in StateManager's in-memory cache
StateManager.get().setModelsCache("openRouter", finalModels)
return finalModels
return appendClineStealthModels(models)
}
/**
@@ -1,8 +1,8 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import axios from "axios"
import { toRequestyServiceUrl } from "@/shared/clients/requesty"
import { getAxiosSettings } from "@/shared/net"
import { toRequestyServiceUrl } from "@/shared/clients/requesty"
import { Controller } from ".."
/**
@@ -110,8 +110,6 @@ export async function updateApiConfigurationProto(
actModeAihubmixModelInfo: protoApiConfiguration.actModeAihubmixModelInfo
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeAihubmixModelInfo)
: undefined,
geminiPlanModeThinkingLevel: protoApiConfiguration.geminiPlanModeThinkingLevel,
geminiActModeThinkingLevel: protoApiConfiguration.geminiActModeThinkingLevel,
}
// Update the API configuration in storage
@@ -1,7 +0,0 @@
#!/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: ""
}));
@@ -1,3 +0,0 @@
#!/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