Compare commits

..
Author SHA1 Message Date
NightTrek f07fefeb07 feat(telemetry): add policy-based telemetry control with env override
Implement TelemetrySettingsPolicyManager to centrally manage telemetry
state with support for environment-level overrides. This allows
enterprises to completely disable all telemetry via
OTEL_TELEMETRY_ENABLED=0 environment variable, regardless of user
preferences or IDE settings.

Key changes:
- Add policy manager that evaluates telemetry state from multiple sources
- Support OTEL_TELEMETRY_ENABLED=0 to force-disable all telemetry
- Dynamically switch between active and NoOp telemetry providers
- Update .env.example with clearer documentation about override behavior
- Make telemetry state updates async to properly await policy changes
- Subscribe to policy changes for runtime provider switching

This ensures enterprise environments have a reliable kill switch for
all telemetry that cannot be overridden by user settings.
2025-10-28 10:59:04 -07:00
487 changed files with 6987 additions and 28839 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added change to hide the context window usage message from env details when using next gen models and before the usage has reached an elevated state
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed proto naming issue - RPC >>> Rpc
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Support Feature Flags default values
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Adding oca as a provider to cline cli
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
+45 -10
View File
@@ -1,19 +1,54 @@
#!/usr/bin/env bash
# PostToolUse Hook Example
#
# This hook runs AFTER a tool is executed. It can:
# 1. Observe tool results and outcomes
# 2. Add context for FUTURE tool uses via contextModification
# 3. Log or track tool usage patterns
#
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
# The tool has already completed when this hook runs.
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
# Read the hook input (JSON via stdin)
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
# Extract tool information
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName // "unknown"')
parameters=$(echo "$input" | jq -r '.postToolUse.parameters // {}')
result=$(echo "$input" | jq -r '.postToolUse.result // ""')
success=$(echo "$input" | jq -r '.postToolUse.success // false')
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
# Example 1: Learning from file operations
# Track successful file creations to build context about project structure
# if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
# path=$(echo "$parameters" | jq -r '.path // ""')
# cat <<EOF
# {
# "shouldContinue": true,
# "contextModification": "FILE_OPERATIONS: Successfully created '$path'. Future operations should maintain consistency with this file's patterns and structure."
# }
# EOF
# exit 0
# fi
# Example 2: Performance monitoring
# Warn about slow operations
# if [[ "$execution_time" -gt 5000 ]]; then
# cat <<EOF
# {
# "shouldContinue": true,
# "contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms to complete. Consider optimizing future similar operations or breaking them into smaller steps."
# }
# EOF
# exit 0
# fi
# Example 3: Context injection for future tool uses
# The context will be available in the NEXT API request
cat <<EOF
{
"cancel": false,
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PostToolUse hook custom errorMessage"
"shouldContinue": true,
"contextModification": "TOOL_RESULT: The tool '$tool_name' completed with success=$success. Consider validating the results before proceeding to the next step."
}
EOF
+33 -10
View File
@@ -1,19 +1,42 @@
#!/usr/bin/env bash
# PreToolUse Hook Example
#
# This hook runs BEFORE a tool is executed. It can:
# 1. Block execution by returning {"shouldContinue": false}
# 2. Add context for FUTURE tool uses via contextModification
# 3. Validate tool parameters
#
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
# The tool parameters are already determined when this hook runs.
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
# Read the hook input (JSON via stdin)
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
# Extract tool information
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
# Example 1: Validation - Block invalid operations
# Uncomment to prevent creating .js files in a TypeScript project
# if [[ "$tool_name" == "write_to_file" ]]; then
# path=$(echo "$parameters" | jq -r '.path // ""')
# if [[ "$path" == *.js ]]; then
# cat <<EOF
# {
# "shouldContinue": false,
# "errorMessage": "VALIDATION FAILED: Cannot create .js files in TypeScript project. Please use .ts extension instead.",
# "contextModification": "WORKSPACE_RULES: This is a strict TypeScript project. All new files must use .ts or .tsx extensions."
# }
# EOF
# exit 0
# fi
# fi
# Example 2: Context injection for future tool uses
# The context will be available in the NEXT API request after this tool completes
cat <<EOF
{
"cancel": false,
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PreToolUse hook custom errorMessage"
"shouldContinue": true,
"contextModification": "WORKSPACE_RULES: [For future tool uses] This is a TypeScript React project. When creating files, use .ts/.tsx extensions and include detailed comments explaining the purpose and usage of each function."
}
EOF
+76 -124
View File
@@ -3,8 +3,8 @@
## Overview
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
- **Global hooks directory**: `~/Documents/Cline/Rules/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to specific workspace)
Hooks run automatically when enabled.
@@ -17,54 +17,17 @@ Hooks run automatically when enabled.
## Available Hooks
### TaskStart Hook
- **When**: Runs when a NEW task is started (not when resuming)
- **Purpose**: Initialize task context, validate task requirements, set up environment
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
- **Workspace Location**: `.clinerules/hooks/TaskStart`
### TaskResume Hook
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
- **Workspace Location**: `.clinerules/hooks/TaskResume`
### TaskCancel Hook
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
- **Purpose**: Clean up resources, log cancellation, save state
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
- **Note**: This hook is NOT cancellable
### TaskComplete Hook (coming soon!)
- **When**: Runs when a task is marked as complete
- **Purpose**: Log completion status, perform final cleanup, generate reports
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
### UserPromptSubmit Hook
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
### PreToolUse Hook
- **When**: Runs BEFORE a tool is executed
- **Purpose**: Validate parameters, block execution, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PreToolUse` (all platforms)
- **Workspace Location**: `.clinerules/hooks/PreToolUse` (all platforms)
### PostToolUse Hook
- **When**: Runs AFTER a tool completes
- **Purpose**: Observe results, track patterns, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse`
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
### PreCompact Hook (coming soon!)
- **When**: Runs BEFORE the conversation context is compacted/truncated
- **Purpose**: Observe compaction events, log context management, track token usage
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
- **Workspace Location**: `.clinerules/hooks/PreCompact`
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PostToolUse` (all platforms)
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
## Cross-Platform Hook Format
@@ -74,12 +37,13 @@ Cline uses a git-style approach for hooks that works consistently across all pla
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
- **Windows**: Not currently supported.
- **Windows**: No special permissions needed - hooks are executed through the shell
### How It Works
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
- On Unix/Linux/macOS: Native shell execution with shebang support
- On Windows: Shell execution handles shebang interpretation
This means:
- ✅ Same hook script works on all platforms
@@ -91,10 +55,16 @@ This means:
**On Unix/Linux/macOS:**
```bash
# Create hook file
nano ~/Documents/Cline/Hooks/PreToolUse
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
# Make executable
chmod +x ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
```
**On Windows:**
```batch
REM Create hook file (note: no file extension)
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
```
## Context Injection Timing
@@ -137,46 +107,11 @@ All hooks receive:
```json
{
"clineVersion": "string",
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
"hookName": "PreToolUse" | "PostToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": { // Only for TaskStart
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
},
"taskResume": { // Only for TaskResume
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
},
"taskCancel": { // Only for TaskCancel
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
},
"taskComplete": { // Only for TaskComplete
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
},
"userPromptSubmit": { // Only for UserPromptSubmit
"prompt": "string",
"attachments": ["string"]
},
"preToolUse": { // Only for PreToolUse
"toolName": "string",
"parameters": {}
@@ -187,11 +122,6 @@ All hooks receive:
"result": "string",
"success": boolean,
"executionTimeMs": number
},
"preCompact": { // Only for PreCompact
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
@@ -201,21 +131,38 @@ All hooks receive:
All hooks must return:
```json
{
"cancel": boolean, // Required: false to continue, true to block execution
"contextModification": "string", // Optional: Context for future AI decisions
"shouldContinue": boolean, // Required: Allow or block execution
"contextModification": "string", // Optional: Context for future tool uses
"errorMessage": "string" // Optional: Error details if blocking
}
```
**Note**: The `cancel` field works as follows:
- `false` (or omitted): Allow execution to continue
- `true`: Block execution and show error message to user
## Context Modification Format
Use structured prefixes to help the AI understand context type:
- `WORKSPACE_RULES:` - Project conventions and requirements
- `FILE_OPERATIONS:` - File creation/modification patterns
- `TOOL_RESULT:` - Outcomes of tool executions
- `PERFORMANCE:` - Performance concerns
- `VALIDATION:` - Validation results
- Custom prefixes as needed
Example:
```bash
cat <<EOF
{
"shouldContinue": true,
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
}
EOF
```
## Hook Execution Limits
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
- **Timeout**: Hooks must complete within 30 seconds
- **Context Size**: Context modifications are limited to 50KB
- **Error Handling**: Unexpected file system errors are propagated; expected errors (file not found, permission denied) are handled silently
## Common Use Cases
@@ -230,15 +177,15 @@ path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
cat <<EOF
{
"cancel": true,
"shouldContinue": false,
"errorMessage": "Cannot create .js files in TypeScript project",
"contextModification": "Use .ts/.tsx extensions only"
"contextModification": "WORKSPACE_RULES: Use .ts/.tsx extensions only"
}
EOF
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
### 2. Context Building - Learn from Operations
@@ -253,12 +200,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
"shouldContinue": true,
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
}
EOF
else
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
fi
```
@@ -273,12 +220,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
if [[ "$execution_time" -gt 5000 ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
"shouldContinue": true,
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
}
EOF
else
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
fi
```
@@ -292,7 +239,7 @@ input=$(cat)
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
# Allow execution
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
## Global vs Workspace Hooks
@@ -300,40 +247,44 @@ echo '{"cancel": false}'
Cline supports two levels of hooks:
### Global Hooks
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
- **Location**: `~/Documents/Cline/Rules/Hooks/` (macOS/Linux) or `%USERPROFILE%\Documents\Cline\Rules\Hooks\` (Windows)
- **Scope**: Apply to ALL workspaces and projects
- **Use Case**: Organization-wide policies, personal preferences, universal validations
- **Priority**: Order not guaranteed when combined with workspace hooks
- **Priority**: Execute FIRST, before workspace hooks
### Workspace Hooks
- **Location**: `.clinerules/hooks/` in each workspace root
- **Scope**: Apply only to the specific workspace
- **Use Case**: Project-specific rules, team conventions, repository requirements
- **Priority**: Order not guaranteed when combined with global hooks
- **Priority**: Execute AFTER global hooks
### Hook Execution
When multiple hooks exist (global and/or workspace):
- All hooks for a given step are executed **concurrently** using `Promise.all`
- **Execution order is not guaranteed** - hooks run in parallel
- If ALL hooks allow execution (`cancel: false`), the tool proceeds
- If ANY hook blocks (`cancel: true`), execution is blocked
- All hooks for a given step (PreToolUse or PostToolUse) are executed
- **Execution order is not guaranteed** - hooks may run concurrently
- If ALL hooks allow execution (`shouldContinue: true`), the tool proceeds
- If ANY hook blocks (`shouldContinue: false`), execution is blocked
**Result Combination:**
- `cancel`: If ANY hook returns `true`, execution is blocked
- `contextModification`: All context strings are concatenated with double newlines (`\n\n`)
- `errorMessage`: All error messages are concatenated with single newlines (`\n`)
- `shouldContinue`: Must be `true` from ALL hooks for execution to proceed
- `contextModification`: All context strings are concatenated
- `errorMessage`: All error messages are concatenated
### Setting Up Global Hooks
1. The global hooks directory is automatically created at:
- macOS/Linux: `~/Documents/Cline/Hooks/`
- macOS/Linux: `~/Documents/Cline/Rules/Hooks/`
- Windows: `%USERPROFILE%\Documents\Cline\Rules\Hooks\`
2. Add your hook script:
```bash
# Unix/Linux/macOS
nano ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Hooks/PreToolUse
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
# Windows
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
```
3. Enable hooks in Cline settings
@@ -343,18 +294,18 @@ When multiple hooks exist (global and/or workspace):
**Global Hook** (applies to all projects):
```bash
#!/usr/bin/env bash
# ~/Documents/Cline/Hooks/PreToolUse
# ~/Documents/Cline/Rules/Hooks/PreToolUse
# Universal rule: Never delete package.json
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
echo '{"shouldContinue": false, "errorMessage": "Global policy: Cannot modify package.json"}'
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
**Workspace Hook** (applies to specific project):
@@ -367,11 +318,11 @@ tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
echo '{"shouldContinue": false, "errorMessage": "Project rule: Use .ts files only"}'
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
@@ -380,7 +331,7 @@ echo '{"cancel": false}'
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
- **cancel**: If ANY hook returns `true`, execution is blocked
- **shouldContinue**: If ANY hook returns false, execution is blocked
- **contextModification**: All context modifications are concatenated
- **errorMessage**: All error messages are concatenated
@@ -401,6 +352,7 @@ If you have multiple workspace roots, you can place hooks in each root's `.cline
### Context Not Affecting Behavior
- Remember: context affects FUTURE decisions, not the current tool
- Use PreToolUse for validation (blocking) if you need immediate effect
- Ensure context modifications are clear and actionable
- Check that context isn't being truncated (50KB limit)
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskCancel hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskResume hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskStart hook custom errorMessage"
}
EOF
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "UserPromptSubmit hook custom errorMessage"
}
EOF
+2 -2
View File
@@ -122,9 +122,9 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
title="Previous Updates:"
classNames={{
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
title: "font-bold text-(--vscode-foreground)",
title: "font-bold text-[var(--vscode-foreground)]",
indicator:
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
+4 -1
View File
@@ -30,7 +30,10 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# Can run alongside PostHog or independently
# Primary focus: Logs (events), with optional metrics support
# Enable OpenTelemetry (set to 1 to enable)
# Enable/Disable OpenTelemetry (set to 1 to enable, 0 to completely disable ALL telemetry)
# IMPORTANT: Setting OTEL_TELEMETRY_ENABLED=0 will disable ALL telemetry providers,
# regardless of user preferences or IDE settings. Use this for enterprise environments
# where no telemetry should leave the network.
# OTEL_TELEMETRY_ENABLED=1
# Exporters: "console" for local debugging, "otlp" for remote collector
+3 -2
View File
@@ -74,9 +74,10 @@ jobs:
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
run: npm run publish:marketplace:nightly
+3 -2
View File
@@ -99,11 +99,12 @@ jobs:
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
run: |
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
-43
View File
@@ -1,48 +1,5 @@
# Changelog
## [3.36.1]
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
- fix: disable native tool callings for grok code models
- Add MCP tool usage to GLM
- Removes reasoning_details content field from Anthropic providers
## [3.36.0]
- Add: Hooks allow you to inject custom logic into Cline's workflow
- Add: new provider AIhubmix
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
- Fix: Oca Token Refresh logic
- Fix: issues where assistant message with empty content is added to conversation history
- Fix: bug where the checkbox shows in the model selector dropdown
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
- Fix: support for `<think>` tags for better compatibility with open-source models
- Fix: refinements to the GLM-4.6 system prompt
## [3.35.1]
- Add: Hicap API integration as provider
- Fix: enable Add Header button in OpenAICompatibleProvider UI
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
- Fix: render model description in markdown
## [3.35.0]
- Add native tool calling support with configurable setting.
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
- added zai-glm-4.6 as a Cerebras model
- Created GPT5 family specific system prompt template
- Fix: show reasoning budget slider to models with valid thinking config
- Requesty base URL, and API key fixes
- Delete all Auth Tokens when logging out
- Support for <think> tags for models that prefer that over <thinking>
## [3.34.1]
- Added support for MiniMax provider with MiniMax-M2 model
- Remove Cline/code-supernova-1-million model
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
## [3.34.0]
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
+2 -2
View File
@@ -2,7 +2,7 @@
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline
# Cline \#1 on OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
@@ -43,7 +43,7 @@ Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.c
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
> [!TIP]
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
+13 -13
View File
@@ -70,7 +70,7 @@
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "info"
"noAssignInExpressions": "warn"
},
"complexity": {
"noUselessConstructor": "off",
@@ -82,7 +82,7 @@
"noStaticOnlyClass": "off"
},
"security": {
"noDangerouslySetInnerHtml": "info"
"noDangerouslySetInnerHtml": "warn"
}
}
},
@@ -114,17 +114,17 @@
"files": {
"includes": [
"**",
"!**/dist",
"!**/dist-*",
"!**/out",
"!**/evals",
"!**/playwright",
"!**/test-results",
"!**/node_modules",
"!**/webview-ui/build",
"!**/generated",
"!**/proto",
"!**/tests/specs"
"!**/dist/**",
"!**/dist-*/**",
"!**/out/**",
"!**/evals/**",
"!**/playwright/**",
"!**/test-results/**",
"!**/node_modules/**",
"!**/webview-ui/build/**",
"!**/generated/**",
"!**/proto/**",
"!**/tests/specs/**"
]
},
"plugins": [
+2 -2
View File
@@ -182,7 +182,7 @@ see the manual page: man cline`,
rootCmd.AddCommand(cli.NewVersionCommand())
rootCmd.AddCommand(cli.NewAuthCommand())
rootCmd.AddCommand(cli.NewLogsCommand())
// rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
rootCmd.AddCommand(cli.NewDoctorCommand())
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1)
@@ -345,4 +345,4 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
}
return content.String(), nil
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.3",
"version": "1.0.0-nightly.18",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
@@ -20,7 +20,7 @@
"vscode-uri"
],
"engines": {
"node": ">=20.0.0"
"node": ">=18.0.0"
},
"keywords": [
"cline",
+1 -7
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
@@ -29,7 +28,7 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
}
// Create task manager for state operations
manager, err := createTaskManager(ctx)
manager, err := task.NewManagerForDefault(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
@@ -76,11 +75,6 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
}
}
// WORKAROUND: Wait for debounced state persistence to complete
// Fixes `cline auth` issue when ran in docker environments
// TODO: implement better solution w/ changes in StateManager
time.Sleep(600 * time.Millisecond)
// Success message
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
fmt.Printf(" Model: %s\n", finalModelID)
+3 -13
View File
@@ -112,7 +112,6 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
cline.ApiProvider_OLLAMA,
cline.ApiProvider_CEREBRAS,
cline.ApiProvider_OCA,
cline.ApiProvider_HICAP,
}
// Check each provider to see if it's ready to use
@@ -213,15 +212,13 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
// Returns (provider, ok) where ok is false if the provider is unknown
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
normalizedStr := strings.ToLower(providerStr)
// Map string values to enum values
switch normalizedStr {
switch providerStr {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, true
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
case "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
return cline.ApiProvider_OPENAI, true
case "openai-native": // This is the native, official Open AI provider
case "openai", "openai-native": // This is the native, official Open AI provider
return cline.ApiProvider_OPENAI_NATIVE, true
case "openrouter":
return cline.ApiProvider_OPENROUTER, true
@@ -239,8 +236,6 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
return cline.ApiProvider_CLINE, true
case "oca":
return cline.ApiProvider_OCA, true
case "hicap":
return cline.ApiProvider_HICAP, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
@@ -272,8 +267,6 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
return "cline"
case cline.ApiProvider_OCA:
return "oca"
case cline.ApiProvider_HICAP:
return "hicap"
default:
return ""
}
@@ -351,8 +344,6 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
return "Cline (Official)"
case cline.ApiProvider_OCA:
return "Oracle Code Assist"
case cline.ApiProvider_HICAP:
return "Hicap"
default:
return "Unknown"
}
@@ -474,7 +465,6 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
{cline.ApiProvider_GEMINI, "geminiApiKey"},
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
{cline.ApiProvider_HICAP, "hicapApiKey"},
}
for _, providerCheck := range providersToCheck {
@@ -154,14 +154,6 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
}, nil
case cline.ApiProvider_HICAP:
return ProviderFields{
APIKeyField: "hicapApiKey",
PlanModeModelInfoField: "planModeHicapModelInfo",
ActModeModelInfoField: "actModeHicapModelInfo",
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
@@ -276,8 +268,6 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
apiConfig.ClineApiKey = value
case "ocaApiKey":
apiConfig.OcaApiKey = value
case "hicapApiKey":
apiConfig.HicapApiKey = value
}
}
@@ -299,9 +289,6 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
case "planModeOcaModelId":
apiConfig.PlanModeOcaModelId = value
apiConfig.ActModeOcaModelId = value
case "planModeHicapModelId":
apiConfig.PlanModeHicapModelId = value
apiConfig.ActModeHicapModelId = value
}
}
+2 -5
View File
@@ -123,10 +123,7 @@ func setCommand() *cobra.Command {
Use: "set <key=value> [key=value...]",
Aliases: []string{"s"},
Short: "Set configuration variables",
Long: `Set one or more global configuration variables using key=value format.
This command merges the provided settings with existing values, preserving
unspecified fields. Only the fields you explicitly set will be updated.`,
Long: `Set one or more global configuration variables using key=value format.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -142,7 +139,7 @@ unspecified fields. Only the fields you explicitly set will be updated.`,
return err
}
// Update settings (server-side merge handles preserving existing values)
// Update settings
return configManager.UpdateSettings(ctx, settings, secrets)
},
}
+1 -1
View File
@@ -189,7 +189,7 @@ func renderAutoApprovalSettings(value interface{}, censor bool) error {
}
}
} else {
// Print other fields normally (enabled, enableNotifications, favorites)
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
+2 -11
View File
@@ -106,14 +106,6 @@ func (tr *ToolRenderer) generateToolHeader(tool *types.ToolMessage, verbTense st
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeFileDeleted):
if verbTense == "wants to" {
action = "wants to delete"
} else {
action = "is deleting"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeListFilesTopLevel):
if verbTense == "wants to" {
action = "wants to list files in"
@@ -207,7 +199,7 @@ func (tr *ToolRenderer) GenerateToolContentPreview(tool *types.ToolMessage) stri
previewMd := fmt.Sprintf("```\n%s\n```", preview)
return tr.renderMarkdown(previewMd)
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch), string(types.ToolTypeFileDeleted):
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch):
// No preview for read/fetch operations
return ""
@@ -234,8 +226,7 @@ func (tr *ToolRenderer) GenerateToolContentBody(tool *types.ToolMessage) string
toolParser := NewToolResultParser(tr.mdRenderer)
switch tool.Tool {
case string(types.ToolTypeReadFile),
string(types.ToolTypeFileDeleted):
case string(types.ToolTypeReadFile):
// readFile: show header only, no body
return ""
+21
View File
@@ -52,6 +52,8 @@ func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
return h.handleResumeCompletedTask(msg, dc)
case string(types.AskTypeMistakeLimitReached):
return h.handleMistakeLimitReached(msg, dc)
case string(types.AskTypeAutoApprovalMaxReached):
return h.handleAutoApprovalMaxReached(msg, dc)
case string(types.AskTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc)
case string(types.AskTypeUseMcpServer):
@@ -253,6 +255,25 @@ func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *Disp
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
}
// handleAutoApprovalMaxReached handles auto-approval max reached
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.SystemRenderer != nil {
details := make(map[string]string)
if msg.Text != "" {
details["reason"] = msg.Text
}
dc.SystemRenderer.RenderError(
"warning",
"Auto-Approval Limit Reached",
"The maximum number of auto-approved requests has been reached. Manual approval is now required.",
details,
)
fmt.Printf("\n**Approval required to continue.**\n")
return nil
}
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text), true)
}
// handleBrowserActionLaunch handles browser action launch requests
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
url := strings.TrimSpace(msg.Text)
+3 -3
View File
@@ -208,9 +208,9 @@ func listLogFiles(logsDir string) ([]logFileInfo, error) {
})
}
// Sort by created time (oldest first)
// Sort by created time (newest first)
sort.Slice(logs, func(i, j int) bool {
return logs[i].created.Before(logs[j].created)
return logs[i].created.After(logs[j].created)
})
return logs, nil
@@ -379,4 +379,4 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
fmt.Println()
return nil
}
}
-2
View File
@@ -256,8 +256,6 @@ func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
case types.ToolTypeEditedExistingFile,
types.ToolTypeNewFileCreated:
return "edit_files", nil
case types.ToolTypeFileDeleted:
return "apply_patch", nil
default:
return "", fmt.Errorf("unsupported tool type: %s", toolMsg.Tool)
}
+5 -4
View File
@@ -282,6 +282,7 @@ func (m *Manager) CheckSendEnabled(ctx context.Context) error {
errorTypes := []string{
string(types.AskTypeAPIReqFailed), // "api_req_failed"
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
string(types.AskTypeAutoApprovalMaxReached), // "auto_approval_max_req_reached"
}
isError := false
@@ -1238,16 +1239,16 @@ func (m *Manager) updateMode(stateJson string) {
// UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
boolPtr := func(b bool) *bool { return &b }
settings := &cline.Settings{
AutoApprovalSettings: &cline.AutoApprovalSettings{
Actions: &cline.AutoApprovalActions{},
Enabled: true,
MaxRequests: 20, // Important: avoid maxRequests=0 bug
Actions: &cline.AutoApprovalActions{},
},
}
// Set the specific action to true based on actionKey
truePtr := boolPtr(true)
truePtr := func() *bool { b := true; return &b }()
switch actionKey {
case "read_files":
+19 -5
View File
@@ -180,6 +180,8 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
case "plan_mode_oca_model_id":
settings.PlanModeOcaModelId = strPtr(value)
case "plan_mode_vercel_ai_gateway_model_id":
settings.PlanModeVercelAiGatewayModelId = strPtr(value)
case "act_mode_api_model_id":
settings.ActModeApiModelId = strPtr(value)
case "act_mode_reasoning_effort":
@@ -216,6 +218,8 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
case "act_mode_oca_model_id":
settings.ActModeOcaModelId = strPtr(value)
case "act_mode_vercel_ai_gateway_model_id":
settings.ActModeVercelAiGatewayModelId = strPtr(value)
// Boolean fields
case "aws_use_cross_region_inference":
@@ -412,12 +416,24 @@ func setNestedField(settings *cline.Settings, parentField string, childFields ma
func setAutoApprovalSettings(settings *cline.AutoApprovalSettings, fields map[string]string) error {
for key, value := range fields {
switch key {
case "enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.Enabled = val
case "max_requests":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.MaxRequests = val
case "enable_notifications":
val, err := parseBool(value)
if err != nil {
return err
}
settings.EnableNotifications = boolPtr(val)
settings.EnableNotifications = val
case "actions":
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
default:
@@ -656,8 +672,6 @@ func parseApiProvider(value string) (cline.ApiProvider, error) {
return cline.ApiProvider_DIFY, nil
case "oca":
return cline.ApiProvider_OCA, nil
case "minimax":
return cline.ApiProvider_MINIMAX, nil
default:
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("invalid api_provider '%s'", value)
}
@@ -732,14 +746,14 @@ func setSecretField(secrets *cline.Secrets, key, value string) error {
secrets.HuaweiCloudMaasApiKey = strPtr(value)
case "baseten_api_key":
secrets.BasetenApiKey = strPtr(value)
case "vercel_ai_gateway_api_key":
secrets.VercelAiGatewayApiKey = strPtr(value)
case "dify_api_key":
secrets.DifyApiKey = strPtr(value)
case "oca_api_key":
secrets.OcaApiKey = strPtr(value)
case "oca_refresh_token":
secrets.OcaRefreshToken = strPtr(value)
case "hicap_api_key":
secrets.HicapApiKey = strPtr(value)
default:
return fmt.Errorf("unsupported secret field '%s'", key)
}
+13 -11
View File
@@ -37,16 +37,17 @@ const (
type AskType string
const (
AskTypeFollowup AskType = "followup"
AskTypePlanModeRespond AskType = "plan_mode_respond"
AskTypeCommand AskType = "command"
AskTypeCommandOutput AskType = "command_output"
AskTypeCompletionResult AskType = "completion_result"
AskTypeTool AskType = "tool"
AskTypeAPIReqFailed AskType = "api_req_failed"
AskTypeResumeTask AskType = "resume_task"
AskTypeResumeCompletedTask AskType = "resume_completed_task"
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
AskTypeFollowup AskType = "followup"
AskTypePlanModeRespond AskType = "plan_mode_respond"
AskTypeCommand AskType = "command"
AskTypeCommandOutput AskType = "command_output"
AskTypeCompletionResult AskType = "completion_result"
AskTypeTool AskType = "tool"
AskTypeAPIReqFailed AskType = "api_req_failed"
AskTypeResumeTask AskType = "resume_task"
AskTypeResumeCompletedTask AskType = "resume_completed_task"
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
AskTypeAutoApprovalMaxReached AskType = "auto_approval_max_req_reached"
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
AskTypeUseMcpServer AskType = "use_mcp_server"
AskTypeNewTask AskType = "new_task"
@@ -107,7 +108,6 @@ const (
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
ToolTypeNewFileCreated ToolType = "newFileCreated"
ToolTypeReadFile ToolType = "readFile"
ToolTypeFileDeleted ToolType = "fileDeleted"
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
@@ -247,6 +247,8 @@ func convertProtoAskType(askType cline.ClineAsk) string {
return string(AskTypeResumeCompletedTask)
case cline.ClineAsk_MISTAKE_LIMIT_REACHED:
return string(AskTypeMistakeLimitReached)
case cline.ClineAsk_AUTO_APPROVAL_MAX_REQ_REACHED:
return string(AskTypeAutoApprovalMaxReached)
case cline.ClineAsk_BROWSER_ACTION_LAUNCH:
return string(AskTypeBrowserActionLaunch)
case cline.ClineAsk_USE_MCP_SERVER:
+2 -2
View File
@@ -375,7 +375,7 @@ func showFailureMessage(channel string) {
func getCacheFilePath() string {
configDir := filepath.Join(os.Getenv("HOME"), ".cline", "data")
return filepath.Join(configDir, "cli-update-cache")
return filepath.Join(configDir, ".update-cache")
}
func loadCache() (cacheData, error) {
@@ -406,4 +406,4 @@ func saveCache(cache cacheData) error {
}
return os.WriteFile(cacheFile, data, 0644)
}
}
+1 -27
View File
@@ -4,9 +4,7 @@ import (
"context"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"time"
"google.golang.org/grpc"
@@ -126,16 +124,6 @@ func NormalizeAddressForGRPC(address string) (string, error) {
return address, nil
}
// GetNodeVersion returns the current Node.js version, or "unknown" if unable to detect
func GetNodeVersion() string {
cmd := exec.Command("node", "--version")
output, err := cmd.Output()
if err != nil {
return "unknown"
}
return strings.TrimSpace(string(output))
}
// RetryOperation performs an operation with retry logic
func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
var lastErr error
@@ -167,19 +155,5 @@ func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation f
}
}
return fmt.Errorf(`operation failed to after %d attempts: %w
This is usually caused by an incompatible Node.js version
REQUIREMENTS:
Node.js version 20+ is required
Current Node.js version: %s
DEBUGGING STEPS:
1. View recent logs: cline log list
2. Logs are available in: ~/.cline/logs/
3. The most recent cline-core log file is usually valuable
For additional help, visit: https://github.com/cline/cline/issues
`, maxRetries, lastErr, GetNodeVersion())
return fmt.Errorf("operation failed after %d attempts: %w", maxRetries, lastErr)
}
+1 -10
View File
@@ -443,16 +443,7 @@ var rawConfigFields = ` [
"required": false,
"fieldType": "string",
"placeholder": ""
},
{
"name": "hicapApiKey",
"type": "string",
"comment": "",
"category": "general",
"required": true,
"fieldType": "password",
"placeholder": "Enter your API key"
},
}
]`
// Raw model definitions data (parsed from TypeScript)
@@ -1,324 +0,0 @@
---
title: "GitHub Actions Integration"
description: "Automatically respond to GitHub issues by mentioning @cline in comments using Cline CLI in GitHub Actions."
---
# GitHub Integration Sample
Automate GitHub issue analysis with AI. Mention `@cline` in any issue comment to trigger an autonomous investigation that reads files, analyzes code, and provides actionable insights - all running automatically in GitHub Actions.
<Note>
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](../github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
</Note>
## The Workflow
Trigger Cline by mentioning `@cline` in any issue comment:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss0a-comment.png" alt="Issue comment with @cline mention" width="600" />
</Frame>
Cline's automated analysis appears as a new comment, with insights drawn from your actual codebase:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss0b-final.png" alt="Automated analysis response from Cline" width="600" />
</Frame>
The entire investigation runs autonomously in GitHub Actions - from file exploration to posting results.
Let's configure your repository.
## Prerequisites
Before you begin, you'll need:
- **Cline CLI knowledge** - Completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and understand basic usage
- **GitHub repository** - With admin access to configure Actions and secrets
- **GitHub Actions familiarity** - Basic understanding of workflows and CI/CD
- **API provider account** - OpenRouter, Anthropic, or similar with API key
## Setup
### 1. Copy the Workflow File
Copy the workflow file from this sample to your repository. The workflow file must be placed in the `.github/workflows/` directory in your repository root for GitHub Actions to detect and run it. In this case, we'll name it `cline-responder.yml`.
```bash
# In your repository root
mkdir -p .github/workflows
curl -o .github/workflows/cline-responder.yml https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-integration/cline-responder.yml
```
Alternatively, you can copy the full workflow file directly into `.github/workflows/cline-responder.yml`:
<Accordion title="Click to view the complete cline-responder.yml workflow">
```yaml
name: Cline Issue Assistant
on:
issue_comment:
types: [created, edited]
permissions:
issues: write
jobs:
respond:
runs-on: ubuntu-latest
environment: cline-actions
steps:
- name: Check for @cline mention
id: detect
uses: actions/github-script@v7
with:
script: |
const body = context.payload.comment?.body || "";
const isPR = !!context.payload.issue?.pull_request;
const hit = body.toLowerCase().includes("@cline");
core.setOutput("hit", (!isPR && hit) ? "true" : "false");
core.setOutput("issue_number", String(context.payload.issue?.number || ""));
core.setOutput("issue_url", context.payload.issue?.html_url || "");
core.setOutput("comment_body", body);
- name: Checkout repository
if: steps.detect.outputs.hit == 'true'
uses: actions/checkout@v4
# Node v20 is needed for Cline CLI on GitHub Actions Linux
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Cline CLI
if: steps.detect.outputs.hit == 'true'
run: |
# Install the Cline CLI
sudo npm install -g cline
- name: Create Cline Instance
if: steps.detect.outputs.hit == 'true'
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
CLINE_DIR: ${{ runner.temp }}/cline
run: |
# Create instance and capture output
INSTANCE_OUTPUT=$(cline instance new 2>&1)
# Parse address from output (format: " Address: 127.0.0.1:36733")
CLINE_ADDRESS=$(echo "$INSTANCE_OUTPUT" | grep "Address:" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]+')
echo "CLINE_ADDRESS=$CLINE_ADDRESS" >> $GITHUB_ENV
# Configure API key
cline config set open-router-api-key=$OPENROUTER_API_KEY --address $CLINE_ADDRESS -v
- name: Download analyze script
if: steps.detect.outputs.hit == 'true'
run: |
export GITORG="YOUR-GITHUB-ORG"
export GITREPO="YOUR-GITHUB-REPO"
curl -L https://raw.githubusercontent.com/${GITORG}/${GITREPO}/refs/heads/main/git-scripts/analyze-issue.sh -o analyze-issue.sh
chmod +x analyze-issue.sh
- name: Run analysis
if: steps.detect.outputs.hit == 'true'
id: analyze
env:
ISSUE_URL: ${{ steps.detect.outputs.issue_url }}
COMMENT: ${{ steps.detect.outputs.comment_body }}
CLINE_ADDRESS: ${{ env.CLINE_ADDRESS }}
run: |
set -euo pipefail
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}" "$CLINE_ADDRESS")
{
echo 'result<<EOF'
printf "%s\n" "$RESULT"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Post response
if: steps.detect.outputs.hit == 'true'
uses: actions/github-script@v7
env:
ISSUE_NUMBER: ${{ steps.detect.outputs.issue_number }}
RESULT: ${{ steps.analyze.outputs.result }}
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.ISSUE_NUMBER),
body: process.env.RESULT || "(no output)"
});
```
</Accordion>
<Warning>
**You MUST edit the workflow file before committing!**
Open `.github/workflows/cline-responder.yml` and update the "Download analyze script" step within the workflow to specify your GitHub organization and repository where the analysis script is stored:
```yaml
export GITORG="YOUR-GITHUB-ORG" # Change this!
export GITREPO="YOUR-GITHUB-REPO" # Change this!
```
**Example:** If your repository is `github.com/acme/myproject`, set:
```yaml
export GITORG="acme"
export GITREPO="myproject"
```
This tells the workflow where to download the analysis script from your repository after you commit it in step 3.
</Warning>
The workflow will look for new or updated issues, check for `@cline` mentions, and then
start up an instance of the Cline CLI to dig into the issue, providing feedback
as a reply to the issue.
### 2. Configure API Keys
Add your AI provider API keys as repository secrets:
1. Go to your GitHub repository
2. Navigate to **Settings** → **Environment** and Add a new environment.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss01-environment.png" alt="Navigate to Actions secrets" width="600" />
</Frame>
Make sure to name it "cline-actions" so that it matches the `environment`
value at the top of the `cline-responder.yml` file.
3. Click **New repository secret**
4. Add a secret for the `OPENROUTER_API_KEY` with a value of an API key from
[openrouter.com](https://openrouter.com).
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss02-api-key.png" alt="Add API key secret" width="600" />
</Frame>
5. Verify your secret is configured:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss03-ready.png" alt="API key configured" width="600" />
</Frame>
Now you're ready to supply Cline with the credentials it needs in a GitHub Action.
### 3. Add Analysis Script
Add the analysis script from the `github-issue-rca` sample to your repository. **First, you'll need to create a `git-scripts` directory in your repository root where the script will be located.** Choose one of these options:
**Option A: Download directly (Recommended)**
```bash
# In your repository root, create the directory and download the script
mkdir -p git-scripts
curl -o git-scripts/analyze-issue.sh https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
chmod +x git-scripts/analyze-issue.sh
```
**Option B: Manual copy-paste**
Create the directory and file manually, then paste the script content:
```bash
# In your repository root
mkdir -p git-scripts
# Create and edit the file with your preferred editor
nano git-scripts/analyze-issue.sh # or use vim, code, etc.
```
<Accordion title="Click to view the complete analyze-issue.sh script">
```bash
#!/bin/bash
# Analyze a GitHub issue using Cline CLI
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
exit 1
fi
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
# Ask Cline for its analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
After pasting the script content, make it executable:
```bash
chmod +x git-scripts/analyze-issue.sh
```
</Accordion>
This analysis script calls Cline to execute a prompt on a GitHub issue,
summarizing the output to populate the reply to the issue.
### 4. Commit and Push
```bash
git add .github/workflows/cline-responder.yml
git add git-scripts/analyze-issue.sh
git commit -m "Add Cline issue assistant workflow"
git push
```
## Usage
Once set up, simply mention `@cline` in any issue comment:
```
@cline what's causing this error?
@cline analyze the root cause
@cline what are the security implications?
```
GitHub Actions will:
1. Detect the `@cline` mention
2. Start a Cline CLI instance
3. Download the analysis script
4. Analyze the issue using act mode with yolo (fully autonomous)
5. Post Cline's analysis as a new comment
**Note**: The workflow only triggers on issue comments, not pull request
comments.
## How It Works
The workflow (`cline-responder.yml`):
1. **Triggers** on issue comments (created or edited)
2. **Detects** `@cline` mentions (case-insensitive)
3. **Installs** Cline CLI globally using npm
4. **Creates** a Cline instance using `cline instance new`
5. **Configures** authentication using `cline config set open-router-api-key=...
--address ...`
6. **Downloads** the reusable `analyze-issue.sh` script from the
`github-issue-rca` sample
7. **Runs** analysis with the instance address
8. **Posts** the analysis result as a comment
## Related Samples
- **[github-issue-rca](./github-issue-rca)**: The reusable script that powers this integration
-383
View File
@@ -1,383 +0,0 @@
---
title: "GitHub Issue RCA Sample"
description: "Automated GitHub issue analysis using Cline CLI to identify root causes."
---
# GitHub Root Cause Analysis
Automated GitHub issue analysis using Cline CLI. This script uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues, outputting clean, parseable results that can be easily integrated into your development workflows.
<Note>
**New to Cline CLI?** This sample assumes you have already completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and authenticated with `cline auth`. If you haven't set up Cline CLI yet, please start there first.
</Note>
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/cli-rca.gif" alt="CLI Root Cause Analysis Demo" width="600" />
</Frame>
## Prerequisites
This sample assumes you have already:
- **Cline CLI** installed and authenticated ([Installation Guide](https://docs.cline.bot/cline-cli/installation))
- **At least one AI model provider** configured (e.g., OpenRouter, Anthropic, OpenAI)
- **Basic familiarity** with Cline CLI commands
Additionally, you'll need:
- **GitHub CLI** (`gh`) installed and authenticated
- **jq** installed for JSON parsing
- **bash** shell (or compatible shell)
### Installation Instructions
#### macOS
<Note>
These instructions require [Homebrew](https://brew.sh/) to be installed. If you don't have Homebrew, install it first by running:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
</Note>
```bash
# Install GitHub CLI
brew install gh
# Install jq
brew install jq
# Authenticate with GitHub
gh auth login
```
#### Linux
```bash
# Install GitHub CLI (Debian/Ubuntu)
sudo apt install gh
# Or for other Linux distributions, see: https://cli.github.com/manual/installation
# Install jq (Debian/Ubuntu)
sudo apt install jq
# Authenticate with GitHub
gh auth login
```
## Getting the Script
**Option 1: Download directly with curl**
```bash
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
```
**Option 2: Copy the full script**
<Accordion title="Click to view the complete analyze-issue.sh script">
```bash
#!/bin/bash
# Analyze a GitHub issue using Cline CLI
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
exit 1
fi
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
# Ask Cline for its analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
</Accordion>
<Note>
**After downloading or creating the script**, make it executable by running:
```bash
chmod +x analyze-issue.sh
```
</Note>
## Quick Usage Examples
### Basic Usage
Run this command in your terminal from the directory where you saved the script to analyze an issue with the default root cause prompt:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/123
```
This will:
- Fetch issue #123 from the repository
- Analyze the issue to identify root causes
- Provide detailed analysis with recommendations
### Custom Analysis Prompt
Ask specific questions about the issue:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/456 "What is the security impact?"
```
### Using Specific Cline Instance
Target a particular Cline instance by address:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/123 \
"What is the root cause of this issue?" \
127.0.0.1:46529
```
<Warning>
This is useful when:
- Running multiple Cline instances
- Using a remote Cline server
- Testing with specific configurations
</Warning>
<Note>
The script will automatically handle everything: fetching the issue, analyzing it with Cline, and displaying the results. The analysis typically takes 30-60 seconds depending on the issue complexity.
</Note>
## How It Works
Let's analyze each component of the script to understand how it works.
### Argument Validation
The script validates input and provides usage instructions:
```bash
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'Analyze security impact' 127.0.0.1:46529"
exit 1
fi
```
**Key Points:**
- Validates required GitHub issue URL
- Shows clear usage examples
- Supports optional custom prompt
- Supports optional Cline instance address
### Argument Parsing
The script extracts and sets up the arguments:
```bash
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
```
**Explanation:**
- `ISSUE_URL="$1"` - First argument is always the issue URL
- `PROMPT="${2:-...}"` - Second argument is optional, defaults to root cause analysis
- `ADDRESS` - Third argument is optional, only set if provided
### The Core Analysis Pipeline
This is where the magic happens:
```bash
# Ask Cline for his analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
<Accordion title="Pipeline Breakdown: Understanding Each Component">
**1. `cline -y "$PROMPT: $ISSUE_URL"`**
- `-y` enables yolo mode (no user interaction)
- Constructs prompt with issue URL
**2. `--mode act`**
- Enables act mode for active investigation
- Allows Cline to use tools (read files, run commands, etc.)
**3. `$ADDRESS`**
- Optional address flag for specific instance
- Expands to `--address <ip:port>` if set
**4. `-F json`**
- Outputs in JSON format for parsing
**5. `sed -n '/^{/,$p'`**
- Extracts JSON from output
- Skips any non-JSON prefix lines
**6. `jq -r 'select(.say == "completion_result") | .text'`**
- Filters for completion result messages
- Extracts the text field
- `-r` outputs raw strings (no JSON quotes)
**7. `sed 's/\\n/\n/g'`**
- Converts escaped newlines to actual newlines
- Makes output readable
</Accordion>
## Sample Output
Here's an example analyzing a real Flutter issue:
```bash
$ ./analyze-issue.sh https://github.com/csells/flutter_counter/issues/2
```
**Output:**
```markdown
**Root Cause Analysis of Issue #2: "setState isn't cutting it"**
After examining the GitHub issue and analyzing the Flutter counter codebase,
I've identified the root cause of why setState() is insufficient for this
project's needs:
## Current Implementation Problems
The current Flutter counter app uses setState() for state management, which
has several limitations:
1. **Local State Only**: setState() only works within a single widget, making
it difficult to share state across the app
2. **Rebuild Overhead**: Every setState() call rebuilds the entire widget tree,
causing performance issues with complex UIs
3. **No State Persistence**: State is lost when the widget is disposed
4. **Testing Challenges**: setState-based logic is tightly coupled to the UI,
making unit testing difficult
## Why This Matters
As the app grows beyond a simple counter, these limitations become critical:
- Multiple screens need to access the count
- State needs to persist across navigation
- Business logic should be testable independently
- UI should only rebuild when necessary
## Recommended Solutions
The issue mentions "Provider or Bloc" - both are excellent alternatives:
1. **Provider**: Simple, lightweight state management using InheritedWidget
- Easy migration path from setState
- Good for small to medium apps
- Official Flutter recommendation
2. **Bloc**: More structured approach with clear separation between events,
states, and business logic
- Better for complex apps
- Excellent testability
- Clear architectural patterns
3. **Riverpod**: Modern alternative to Provider with better performance and
developer experience
- Compile-time safety
- Better testing support
- More flexible than Provider
4. **GetX**: Full-featured solution with state management, routing, and
dependency injection
- Minimal boilerplate
- Fast and lightweight
- All-in-one solution
## Next Steps
The current codebase needs refactoring to implement proper state management
architecture to handle more complex state scenarios effectively. Provider
would be the easiest migration path while Bloc provides better long-term
scalability.
```
## When to Use This Pattern
This script pattern is ideal for various development scenarios where automated GitHub issue analysis can accelerate your workflow.
### Bug Investigation
Quickly analyze bug reports and identify root causes without manual code exploration:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/123 \
"What is the root cause of this bug?"
```
### Feature Request Analysis
Understand context and implications of feature requests:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/456 \
"What are the implementation challenges?"
```
### Security Audits
Assess security implications of reported issues:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/789 \
"What are the security implications?"
```
### Documentation Generation
Generate detailed technical documentation from issues:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/654 \
"Provide detailed technical documentation for this issue"
```
### Code Review Assistance
Get second opinions on proposed changes:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/987 \
"Review the proposed solution approach"
```
## Conclusion
This sample demonstrates how to build an autonomous GitHub issue analysis tool using Cline CLI:
1. **Building autonomous CLI tools** using Cline's capabilities
2. **Parsing structured JSON output** from Cline CLI
3. **Creating flexible automation scripts** with custom prompting
4. **Integrating with GitHub** for issue analysis
5. **Handling command-line arguments** effectively
This pattern can be adapted for many other automation scenarios, from pull request reviews to documentation generation to code quality analysis.
## Related Resources
- [CLI Installation Guide](https://docs.cline.bot/cline-cli/installation)
- [CLI Reference Documentation](https://docs.cline.bot/cline-cli/cli-reference)
- [Three Core Flows](https://docs.cline.bot/cline-cli/three-core-flows)
-32
View File
@@ -1,32 +0,0 @@
---
title: "Samples Overview"
description: Example implementations demonstrating Cline CLI capabilities
---
This section provides sample implementations that demonstrate various Cline CLI features and capabilities. Each sample includes complete code, detailed explanations, and real-world usage examples.
## Available Samples
<CardGroup cols={1}>
<Card
title="GitHub Root Cause Analysis"
icon="magnifying-glass-chart"
href="/cline-cli/samples/github-issue-rca"
>
A command-line script that uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues. Features JSON output parsing and non-interactive execution.
</Card>
<Card
title="GitHub Integration (Actions)"
icon="github"
href="/cline-cli/samples/github-integration"
>
Automatically respond to GitHub issues by mentioning @cline in comments. Uses Cline CLI in GitHub Actions to create an AI-powered issue assistant that analyzes and responds autonomously.
</Card>
</CardGroup>
## Additional Resources
- [CLI Installation Guide](/cline-cli/installation)
- [CLI Reference Documentation](/cline-cli/cli-reference)
- [Three Core Flows](/cline-cli/three-core-flows)
-13
View File
@@ -88,14 +88,6 @@
"cline-cli/overview",
"cline-cli/installation",
"cline-cli/three-core-flows",
{
"group": "CLI Samples",
"pages": [
"cline-cli/samples/overview",
"cline-cli/samples/github-issue-rca",
"cline-cli/samples/github-integration"
]
},
"cline-cli/cli-reference"
]
},
@@ -138,7 +130,6 @@
"features/drag-and-drop",
"features/editing-messages",
"features/focus-chain",
"features/hooks",
"features/multiroot-workspace",
"features/plan-and-act",
{
@@ -324,10 +315,6 @@
{
"source": "/getting-started/your-first-task",
"destination": "/getting-started/your-first-project"
},
{
"source": "/cline-cli/samples",
"destination": "/cline-cli/samples/overview"
}
],
"search": {
@@ -42,17 +42,17 @@ To open Cline in the right sidebar:
4. Set the value to `vertical`
5. Restart Cursor for the changes to take effect
</Step>
<Step title="Open the AI Pane">
Click the Cursor cube icon button (AI Pane) that opens Cursor's agent (right side view panel)
<Step title="Open Agent Panel">
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
</Step>
<Step title="Drag Cline to the AI Pane Sidebar">
Drag the Cline icon directly into the AI Pane sidebar.
<Step title="Drag to Three Dots">
Drag the Cline icon directly onto the three dots button - it doesn't work if you just drag it to the top, it has to be the three dots
</Step>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/Cursor-sidebar.gif"
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
alt="Cursor Right Sidebar Setup"
/>
</Frame>
-391
View File
@@ -1,391 +0,0 @@
---
title: "Hooks"
sidebarTitle: "Hooks"
description: "Inject custom logic into Cline's workflow to validate operations, monitor tool usage, and shape AI decisions"
---
Hooks let you inject custom logic into Cline's workflow at key moments. Think of them as automated checkpoints where you can validate operations before they execute, monitor tool usage as it happens, and shape how Cline makes decisions.
Hooks run automatically when specific events happen during development. They receive detailed information about each operation, can block problematic actions before they cause issues, and can inject context that guides future AI decisions.
The real power comes from combining these capabilities. You can:
- Stop operations before they cause problems (like creating `.js` files in a TypeScript project)
- Learn from what's happening and build up project knowledge over time
- Monitor performance and catch issues as they emerge
- Track everything for analytics or compliance
- Trigger external tools or services at the right moments
<Warning>
Hooks are currently supported on macOS and Linux only. Windows support is not available.
</Warning>
## Getting Started
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/hooks.gif" alt="Hooks in action" />
</Frame>
Enabling hooks in Cline is straightforward. Here's what you need to do:
<Steps>
<Step title="Enable Hooks in Settings">
Open Cline settings and check the **"Enable Hooks"** checkbox.
You can find this setting by:
1. Opening Cline
2. Click the "Settings" button on the top right corner
3. Click the "Feature" section in the left side navigation menu.
4. Scroll down until you see the "Enable Hooks" checkbox and check it.
</Step>
<Step title="Choose Your Hook Location">
Decide where to place your hooks:
**For personal or organization-wide hooks:**
- Create hooks in `~/Documents/Cline/Rules/Hooks/`
- These apply to all workspaces automatically
**For project-specific hooks:**
- Create hooks in `.clinerules/hooks/` in your project root
- These only apply to the specific workspace
- Commit them to version control so your team can use them too
</Step>
<Step title="Create Your First Hook">
Hook files must have exact names with no file extensions. For example, to create a TaskStart hook:
```bash
# Create the hook file
vim .clinerules/hooks/TaskStart
```
Add your script (must start with shebang)
``` bash
#!/usr/bin/env bash
# Store piped input into a variable
input=$(cat)
# Dump the entire JSON payload
echo "$input" | jq .
# Get the type of a field
echo "$input" | jq -r '.timestamp | type'
```
This example script demonstrates the key mechanics of hook input/output: reading the JSON payload from stdin with `input=$(cat)`, and using `jq` to inspect the data structure and field types that your hook receives. This helps you understand what data is available before building more complex hook logic.
#### Make it executable
```bash
chmod +x .clinerules/hooks/TaskStart
```
</Step>
<Step title="Test Your Hook">
Start a task in Cline and verify your hook executes.
</Step>
</Steps>
<Tip>
Start with a simple hook that just logs information before building complex validation logic. This helps you understand the data structure and timing.
</Tip>
## Hook Types
Cline provides multiple hook types that let you tap into different stages of the AI workflow. They're organized into categories based on their trigger points and use cases.
<Note>
The hook names below are the exact file names you need to create. For example, to use the TaskStart hook, create a file named `TaskStart` (no file extension) in your hooks directory.
</Note>
Each hook receives base fields in addition to its specific data: `clineVersion`, `hookName`, `timestamp`, `taskId`, `workspaceRoots`, `userId`.
### Tool Execution
These hooks intercept and validate tool operations before and after they execute. Use them to enforce policies, track changes, and learn from operations.
#### PreToolUse
Runs before any tool executes. Use it to block invalid operations, validate parameters, and enforce project policies before changes happen.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preToolUse": {
"toolName": "string",
"parameters": {}
}
}
```
#### PostToolUse
Runs after a tool completes. Use it to learn from results, track performance metrics, and build project knowledge based on operations performed.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PostToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"postToolUse": {
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
}
}
```
### User Interaction
These hooks monitor and enhance user communication with Cline. Use them to validate input, inject context, and track interaction patterns.
#### UserPromptSubmit
Runs when a user sends a message to Cline. Use it to validate input, inject context based on the prompt, and track interaction patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "UserPromptSubmit",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"userPromptSubmit": {
"prompt": "string",
"attachments": ["string"]
}
}
```
### Task Lifecycle
These hooks monitor and respond to task state changes from start to finish. Use them to track progress, restore state, and trigger workflows.
#### TaskStart
Runs when a new task begins. Use it to detect project type, initialize tracking, and inject initial context that shapes how Cline approaches the work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskStart",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
}
}
```
#### TaskResume
Runs when a task resumes after interruption. Use it to restore state, refresh context, and log resumption for analytics or external system notifications.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskResume",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskResume": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
}
}
```
#### TaskCancel
Runs when a task is cancelled. Use it to cleanup resources, log cancellation details, and notify external systems about interrupted work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskCancel",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskCancel": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
}
}
```
{/*
#### TaskComplete
Runs when a task finishes successfully. Use it for final cleanup, tracking metrics, generating reports, and triggering post-task workflows.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskComplete",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskComplete": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
}
}
```
*/}
### System Events
These hooks monitor internal Cline operations and system-level events. Use them to track context usage, log system behavior, and analyze performance patterns.
{/*
#### PreCompact
Runs before conversation context is truncated to fit token limits. Use it to monitor compaction frequency, log events, and track context usage patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreCompact",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preCompact": {
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
*/}
### JSON Communication
Hooks receive JSON via stdin and return JSON via stdout.
**Output structure:**
```json
{
"cancel": false,
"contextModification": "WORKSPACE_RULES: Use TypeScript",
"errorMessage": "Error details if blocking"
}
```
Your hook script can output logging or diagnostic information to stdout during execution, as long as the JSON response is the last thing written. Cline will parse only the final JSON object from stdout.
For example:
```bash
#!/usr/bin/env bash
echo "Processing hook..." # This is fine
echo "Tool: $tool_name" # This is also fine
# The JSON must be last:
echo '{"cancel": false}'
```
The `cancel` field controls whether execution continues. Set it to `true` to block an action, `false` to allow it.
The `contextModification` field injects text into the conversation. This affects future AI decisions, not the current one. Use prefixes like `WORKSPACE_RULES:` or `PERFORMANCE:` to help categorize the context.
### Understanding Context Timing
Context injection affects future decisions, not current ones. When a hook runs:
1. The AI has already decided what to do
2. The hook can block or allow it
3. Any context gets added to the conversation
4. The next AI request sees that context
This means PreToolUse hooks are for blocking bad actions, while PostToolUse hooks are for learning from completed ones.
## What You Can Build
Once you understand the basics, hooks open up creative possibilities:
- **Intelligent Code Review**:
Run linters or custom validators before files get saved. Block commits that don't pass checks. Track code quality metrics over time.
- **Security Enforcement**:
Prevent operations that violate security policies. Detect when sensitive data might be exposed. Audit all file access for compliance.
- **Development Analytics**: Measure how long different operations take. Identify patterns in how the AI works. Generate productivity reports from hook data.
- **Integration Hub**: Connect to issue trackers when certain keywords appear. Update project management tools. Sync with external APIs at the right moments.
The key is combining hooks with external tools. A hook can be the glue between Cline's workflow and the rest of your development ecosystem.
## Troubleshooting
### Hook Not Running
- Ensure the "Enable Hooks" setting is checked
- Verify the hook file is executable (`chmod +x hookname`)
- Check the hook file has no syntax errors
- Look for errors in VSCode's Output panel (Cline channel)
### Hook Timing Out
- Reduce complexity of the hook script
- Avoid expensive operations (network calls, heavy computations)
- Consider moving complex logic to a background process
### Context Not Affecting Behavior
- Remember: context affects FUTURE decisions, not the current tool
- The current AI behavior is based on the previous "API Request..." block
- Your `contextModification` gets injected into the NEXT "API Request..." block
- Use PreToolUse for validation (blocking) if you need immediate effect
- Ensure context modifications are clear and actionable
- Check that context isn't being truncated (50KB limit)
<Warning>
Hooks run with the same permissions as VS Code. They can access all workspace files and environment variables. Review hooks from untrusted sources before enabling them.
</Warning>
## Related Features
Hooks complement other Cline features:
- [Cline Rules](/features/cline-rules) define high-level guidance that hooks can enforce
- [Checkpoints](/features/checkpoints) let you roll back changes if a hook didn't catch an issue
- [Auto-Approve](/features/auto-approve) works well with hooks as safety nets for automated operations
+1 -2
View File
@@ -42,8 +42,7 @@ h5,
h6,
img {
opacity: 1 !important;
font-family:
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
}
/* Also apply to any h1 elements within content areas */
+1 -5
View File
@@ -123,11 +123,7 @@ const copyWasmFiles = {
},
}
const buildEnvVars = {
"import.meta.url": "_importMetaUrl",
"process.env.IS_STANDALONE": JSON.stringify(standalone),
}
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
if (production) {
// IS_DEV is always disable in production builds.
buildEnvVars["process.env.IS_DEV"] = "false"
+33 -36
View File
@@ -1,7 +1,7 @@
import chalk from "chalk"
import execa from "execa"
import * as fs from "fs"
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import chalk from "chalk"
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
const EVALS_DIR = path.resolve(__dirname, "../../../")
@@ -23,7 +23,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
console.log(`Cloning Exercism repository to ${exercismDir}...`)
await execa("git", ["clone", "https://github.com/Aider-AI/polyglot-benchmark.git", exercismDir])
console.log("Exercism repository cloned successfully")
// Unskip all JavaScript and Java tests after cloning
this.unskipAllJavaScriptTests(exercismDir)
this.unskipAllJavaTests(exercismDir)
@@ -34,7 +34,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
console.log("Pulling latest changes...")
await execa("git", ["pull"], { cwd: exercismDir })
console.log("Repository updated successfully")
// Unskip tests again after pulling
this.unskipAllJavaScriptTests(exercismDir)
this.unskipAllJavaTests(exercismDir)
@@ -137,7 +137,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
// Read config.json to get solution and test files
const configPath = path.join(task.workspacePath, ".meta", "config.json")
let config: any = { files: { solution: [], test: [] } }
if (fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
}
@@ -159,8 +159,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
const solutionFiles = config.files.solution || []
const fileList = solutionFiles.join(", ")
description += `\n\nUse the above instructions to modify the supplied files: ${fileList}. Don't change the names of existing functions or classes, as they may be referenced from other code like unit tests, etc. Only use standard libraries, don't suggest installing any packages.`
description +=
" You should ignore all test or test related files in this directory. The final test file has been removed and will be used to evaluate your work after your implementation is complete. Think deeply about the problem prior to working on the implementation. Consider all edge cases and test your solution prior to finalizing."
description += " You should ignore all test or test related files in this directory. The final test file has been removed and will be used to evaluate your work after your implementation is complete. Think deeply about the problem prior to working on the implementation. Consider all edge cases and test your solution prior to finalizing."
// Move test files to temp directory
if (config.files.test) {
@@ -314,7 +313,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
const cppAllPassedMatch = output.match(/All tests passed \(.*?(\d+) test cases?\)/)
const cppTestCasesMatch = output.match(/test cases?: (\d+) \| (\d+) passed/)
const cppFailedMatch = output.match(/(\d+) failed/)
if (cppAllPassedMatch) {
// All tests passed - extract total test cases
testsPassed = parseInt(cppAllPassedMatch[1])
@@ -323,7 +322,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
// Mixed results - extract passed count and calculate failed
const totalTests = parseInt(cppTestCasesMatch[1])
testsPassed = parseInt(cppTestCasesMatch[2])
testsFailed = cppFailedMatch ? parseInt(cppFailedMatch[1]) : totalTests - testsPassed
testsFailed = cppFailedMatch ? parseInt(cppFailedMatch[1]) : (totalTests - testsPassed)
}
break
@@ -435,14 +434,14 @@ export class ExercismAdapter implements BenchmarkAdapter {
*/
private unskipAllJavaScriptTests(repoPath: string): void {
const jsDir = path.join(repoPath, "javascript", "exercises", "practice")
if (!fs.existsSync(jsDir)) {
console.log("JavaScript exercises directory not found, skipping test unskipping")
return
}
// Walk through all exercise directories
const exercises = fs.readdirSync(jsDir).filter((dir) => {
const exercises = fs.readdirSync(jsDir).filter(dir => {
const fullPath = path.join(jsDir, dir)
return fs.statSync(fullPath).isDirectory()
})
@@ -450,25 +449,25 @@ export class ExercismAdapter implements BenchmarkAdapter {
let filesModified = 0
for (const exercise of exercises) {
const exerciseDir = path.join(jsDir, exercise)
// Find all .spec.js files
const files = fs.readdirSync(exerciseDir).filter((file) => file.endsWith(".spec.js"))
const files = fs.readdirSync(exerciseDir).filter(file => file.endsWith('.spec.js'))
for (const file of files) {
const filePath = path.join(exerciseDir, file)
let content = fs.readFileSync(filePath, "utf-8")
let content = fs.readFileSync(filePath, 'utf-8')
const originalContent = content
// Replace xtest with test to unskip tests
content = content.replace(/xtest\(/g, "test(")
content = content.replace(/xtest\(/g, 'test(')
if (content !== originalContent) {
fs.writeFileSync(filePath, content)
filesModified++
}
}
}
console.log(`Unskipped tests in ${filesModified} JavaScript test files`)
}
@@ -478,14 +477,14 @@ export class ExercismAdapter implements BenchmarkAdapter {
*/
private unskipAllJavaTests(repoPath: string): void {
const javaDir = path.join(repoPath, "java", "exercises", "practice")
if (!fs.existsSync(javaDir)) {
console.log("Java exercises directory not found, skipping test unskipping")
return
}
// Walk through all exercise directories
const exercises = fs.readdirSync(javaDir).filter((dir) => {
const exercises = fs.readdirSync(javaDir).filter(dir => {
const fullPath = path.join(javaDir, dir)
return fs.statSync(fullPath).isDirectory()
})
@@ -493,29 +492,29 @@ export class ExercismAdapter implements BenchmarkAdapter {
let filesModified = 0
for (const exercise of exercises) {
const testDir = path.join(javaDir, exercise, "src", "test", "java")
if (!fs.existsSync(testDir)) {
continue
}
// Find all .java test files
const files = fs.readdirSync(testDir).filter((file) => file.endsWith(".java"))
const files = fs.readdirSync(testDir).filter(file => file.endsWith('.java'))
for (const file of files) {
const filePath = path.join(testDir, file)
let content = fs.readFileSync(filePath, "utf-8")
let content = fs.readFileSync(filePath, 'utf-8')
const originalContent = content
// Remove @Disabled("Remove to run test") annotations
content = content.replace(/@Disabled\("Remove to run test"\)\s*\n/g, "")
content = content.replace(/@Disabled\("Remove to run test"\)\s*\n/g, '')
if (content !== originalContent) {
fs.writeFileSync(filePath, content)
filesModified++
}
}
}
console.log(`Unskipped tests in ${filesModified} Java test files`)
}
@@ -598,9 +597,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
const duration = Date.now() - startTime
console.log(
chalk.green(
`Task completed in ${(duration / 1000).toFixed(1)}s after ${attempts} attempt${attempts > 1 ? "s" : ""}`,
),
chalk.green(`Task completed in ${(duration / 1000).toFixed(1)}s after ${attempts} attempt${attempts > 1 ? "s" : ""}`),
)
return finalVerification
+1 -1
View File
@@ -1,5 +1,5 @@
import { ExercismAdapter } from "./exercism"
import { BenchmarkAdapter } from "./types"
import { ExercismAdapter } from "./exercism"
// Registry of all available adapters
const adapters: Record<string, BenchmarkAdapter> = {
+2 -2
View File
@@ -1,7 +1,7 @@
import chalk from "chalk"
import * as fs from "fs"
import ora from "ora"
import * as path from "path"
import chalk from "chalk"
import ora from "ora"
import { ResultsDatabase } from "../db"
import { generateMarkdownReport } from "../utils/markdown"
+7 -3
View File
@@ -1,6 +1,6 @@
import { v4 as uuidv4 } from "uuid"
import chalk from "chalk"
import ora from "ora"
import { v4 as uuidv4 } from "uuid"
import { getAdapter } from "../adapters"
import { ResultsDatabase } from "../db"
import { storeTaskResult } from "../utils/results"
@@ -73,11 +73,15 @@ export async function runHandler(options: RunOptions): Promise<void> {
if (verification.success) {
console.log(
chalk.green(`Tests passed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
chalk.green(
`Tests passed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`,
),
)
} else {
console.log(
chalk.red(`Tests failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
chalk.red(
`Tests failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`,
),
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import Database from "better-sqlite3"
import * as fs from "fs"
import * as path from "path"
import * as fs from "fs"
import Database from "better-sqlite3"
import { SCHEMA } from "./schema"
const EVALS_DIR = path.resolve(__dirname, "../../../")
+10 -10
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env node
import chalk from "chalk"
import { Command } from "commander"
import { reportHandler } from "./commands/report"
import { runHandler } from "./commands/run"
import { runDiffEvalHandler } from "./commands/runDiffEval"
import chalk from "chalk"
import { setupHandler } from "./commands/setup"
import { runHandler } from "./commands/run"
import { reportHandler } from "./commands/report"
import { runDiffEvalHandler } from "./commands/runDiffEval"
// Create the CLI program
const program = new Command()
@@ -16,7 +16,11 @@ program.name("cline-eval").description("CLI tool for orchestrating Cline evaluat
program
.command("setup")
.description("Clone and set up benchmark repositories")
.option("-b, --benchmarks <benchmarks>", "Comma-separated list of benchmarks to set up", "exercism")
.option(
"-b, --benchmarks <benchmarks>",
"Comma-separated list of benchmarks to set up",
"exercism",
)
.action(async (options) => {
try {
await setupHandler(options)
@@ -64,11 +68,7 @@ program
.option("--output-path <path>", "Path to the directory to save the test output JSON files")
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option(
"-n, --valid-attempts-per-case <number>",
"Number of valid attempts per test case per model (will retry until this many valid attempts are collected)",
"1",
)
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
+5 -1
View File
@@ -6,7 +6,11 @@ import * as fs from "fs"
* @param benchmarkReports Benchmark-specific reports
* @param outputPath Output file path
*/
export function generateMarkdownReport(summary: any, benchmarkReports: Record<string, any>, outputPath: string): void {
export function generateMarkdownReport(
summary: any,
benchmarkReports: Record<string, any>,
outputPath: string,
): void {
let markdown = `# Cline Evaluation Report\n\n`
// Generate summary section
+59 -272
View File
@@ -10,7 +10,7 @@
"license": "MIT",
"dependencies": {
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"better-sqlite3": "^11.10.0",
"chalk": "5.6.2",
"cline": "^1.0.1",
"commander": "^9.4.1",
@@ -211,111 +211,6 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bare-events": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz",
"integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz",
"integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -336,17 +231,13 @@
]
},
"node_modules/better-sqlite3": {
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x"
}
},
"node_modules/bindings": {
@@ -414,6 +305,11 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -1554,15 +1450,6 @@
"node": ">=6"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -1593,12 +1480,6 @@
"node": ">=6"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -1639,6 +1520,11 @@
"node": ">= 6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -2245,17 +2131,6 @@
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.2.1.tgz",
"integrity": "sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw=="
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2317,37 +2192,29 @@
}
},
"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==",
"license": "MIT",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tiktoken": {
@@ -2710,73 +2577,15 @@
"proxy-from-env": "^1.1.0"
}
},
"b4a": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
"requires": {}
},
"bare-events": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
"requires": {}
},
"bare-fs": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz",
"integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==",
"optional": true,
"requires": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
}
},
"bare-os": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
"optional": true
},
"bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"optional": true,
"requires": {
"bare-os": "^3.0.1"
}
},
"bare-stream": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
"optional": true,
"requires": {
"streamx": "^2.21.0"
}
},
"bare-url": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz",
"integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==",
"optional": true,
"requires": {
"bare-path": "^3.0.0"
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"better-sqlite3": {
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"requires": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
@@ -2823,6 +2632,11 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -3514,14 +3328,6 @@
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
},
"events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"requires": {
"bare-events": "^2.7.0"
}
},
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -3543,11 +3349,6 @@
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
},
"fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -3570,6 +3371,11 @@
"mime-types": "^2.1.12"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3853,7 +3659,7 @@
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^3.1.1",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
}
},
@@ -3954,16 +3760,6 @@
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.2.1.tgz",
"integrity": "sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw=="
},
"streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"requires": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -4009,32 +3805,26 @@
}
},
"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": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"requires": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0",
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
"tar-stream": "^2.1.4"
}
},
"tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"requires": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"requires": {
"b4a": "^1.6.4"
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
},
"tiktoken": {
@@ -4179,8 +3969,5 @@
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
},
"overrides": {
"tar-fs": ">=2.1.4"
}
}
+3 -5
View File
@@ -20,7 +20,7 @@
"license": "MIT",
"dependencies": {
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"better-sqlite3": "^11.10.0",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
@@ -30,7 +30,8 @@
"sqlite": "^4.1.2",
"tiktoken": "^1.0.21",
"uuid": "^9.0.0",
"yargs": "^17.6.2"
"yargs": "^17.6.2",
"cline": "^1.0.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.3",
@@ -40,8 +41,5 @@
"@types/yargs": "^17.0.19",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"overrides": {
"tar-fs": "^3.1.1"
}
}
+987 -1631
View File
File diff suppressed because it is too large Load Diff
+4 -9
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.36.1",
"version": "3.34.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -310,7 +310,6 @@
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs",
"protos-go": "node scripts/build-go-proto.mjs",
"protos-python": "node scripts/build-python-proto.mjs",
"cli-providers": "node scripts/cli-providers.mjs",
"download-ripgrep": "node scripts/download-ripgrep.mjs",
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
@@ -405,8 +404,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
"@aws-sdk/credential-providers": "^3.922.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -439,7 +438,6 @@
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
@@ -459,6 +457,7 @@
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
@@ -466,7 +465,6 @@
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"nice-grpc": "^2.1.12",
"node-machine-id": "^1.1.12",
"ollama": "^0.5.13",
@@ -474,7 +472,6 @@
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-mutex": "^1.0.0",
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
@@ -485,12 +482,10 @@
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.2",
"tailwindcss": "^4.1.14",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
"ulid": "^2.4.0",
"undici": "^7.16.0",
"uuid": "^11.1.0",
"vscode-uri": "^3.1.0",
"web-tree-sitter": "^0.22.6",
-2
View File
@@ -40,8 +40,6 @@ service AccountService {
rpc openrouterAuthClicked(EmptyRequest) returns (Empty);
rpc requestyAuthClicked(StringRequest) returns (Empty);
// Returns a link the webview can use to redirect back to the user's IDE.
rpc getRedirectUrl(EmptyRequest) returns (String);
}
+1 -1
View File
@@ -29,7 +29,7 @@ message HookInput {
// Output message for all hooks
message HookOutput {
string context_modification = 1;
bool cancel = 2;
bool should_continue = 2;
string error_message = 3;
}
+4 -216
View File
@@ -11,8 +11,6 @@ option java_package = "bot.cline.proto";
// Service for model-related operations
service ModelsService {
// Refreshes and returns Cline models
rpc refreshClineModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Fetches available models from Ollama
rpc getOllamaModels(StringRequest) returns (StringArray);
// Fetches available models from LM Studio
@@ -25,18 +23,14 @@ service ModelsService {
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hicap models
rpc refreshHicapModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Vercel AI Gateway models
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration (legacy - uses combined configuration)
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Updates API configuration (new - uses separate options and secrets)
rpc updateApiConfiguration(UpdateApiConfigurationRequestNew) returns (Empty);
// Updates API configuration with partial values (only updates fields that are explicitly set)
rpc updateApiConfigurationPartial(UpdateApiConfigurationPartialRequest) returns (Empty);
// Refreshes and returns Groq models
@@ -47,8 +41,6 @@ service ModelsService {
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
// Fetches available models from OCA
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
// Fetches available models from AIhubmix
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -100,7 +92,6 @@ message OpenRouterModelInfo {
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
optional string name = 13;
}
// Shared response message for model information
@@ -137,197 +128,12 @@ message SapAiCoreModelsResponse {
bool orchestration_available = 2;
}
// API secrets (credentials, API keys)
message ModelsApiSecrets {
optional string api_key = 1;
optional string cline_api_key = 2;
optional string lite_llm_api_key = 3;
optional string open_router_api_key = 4;
optional string aws_access_key = 5;
optional string aws_secret_key = 6;
optional string aws_session_token = 7;
optional string aws_bedrock_api_key = 8;
optional string open_ai_api_key = 9;
optional string ollama_api_key = 10;
optional string gemini_api_key = 11;
optional string open_ai_native_api_key = 12;
optional string deep_seek_api_key = 13;
optional string requesty_api_key = 14;
optional string together_api_key = 15;
optional string fireworks_api_key = 16;
optional string qwen_api_key = 17;
optional string doubao_api_key = 18;
optional string mistral_api_key = 19;
optional string nebius_api_key = 20;
optional string asksage_api_key = 21;
optional string xai_api_key = 22;
optional string sambanova_api_key = 23;
optional string cerebras_api_key = 24;
optional string sap_ai_core_client_id = 25;
optional string sap_ai_core_client_secret = 26;
optional string moonshot_api_key = 27;
optional string cline_account_id = 28;
optional string groq_api_key = 29;
optional string hugging_face_api_key = 30;
optional string huawei_cloud_maas_api_key = 31;
optional string baseten_api_key = 32;
optional string zai_api_key = 33;
optional string vercel_ai_gateway_api_key = 34;
optional string dify_api_key = 35;
optional string oca_api_key = 36;
optional string oca_refresh_token = 37;
optional string minimax_api_key = 38;
optional string aihubmix_api_key = 39;
}
// API configuration options (non-secret settings)
message ModelsApiOptions {
// Global configuration fields (not mode-specific)
optional string ulid = 1;
optional string lite_llm_base_url = 2;
optional bool lite_llm_use_prompt_cache = 3;
map<string, string> open_ai_headers = 4;
optional string anthropic_base_url = 5;
optional string open_router_provider_sorting = 6;
optional string aws_region = 7;
optional bool aws_use_cross_region_inference = 8;
optional bool aws_bedrock_use_prompt_cache = 9;
optional bool aws_use_profile = 10;
optional string aws_profile = 11;
optional string aws_bedrock_endpoint = 12;
optional string claude_code_path = 13;
optional string vertex_project_id = 14;
optional string vertex_region = 15;
optional string open_ai_base_url = 16;
optional string ollama_base_url = 17;
optional string ollama_api_options_ctx_num = 18;
optional string lm_studio_base_url = 19;
optional string gemini_base_url = 20;
optional string requesty_base_url = 21;
optional int64 fireworks_model_max_completion_tokens = 22;
optional int64 fireworks_model_max_tokens = 23;
optional string azure_api_version = 24;
optional string qwen_api_line = 25;
optional string asksage_api_url = 26;
optional int64 request_timeout_ms = 27;
optional string sap_ai_resource_group = 28;
optional string sap_ai_core_token_url = 29;
optional string sap_ai_core_base_url = 30;
optional bool sap_ai_core_use_orchestration_mode = 31;
optional string moonshot_api_line = 32;
optional string aws_authentication = 33;
optional string zai_api_line = 34;
optional string lm_studio_max_tokens = 35;
optional string qwen_code_oauth_path = 36;
optional string dify_base_url = 37;
optional string oca_base_url = 38;
optional string oca_mode = 39;
optional bool aws_use_global_inference = 40;
optional string minimax_api_line = 41;
optional string aihubmix_base_url = 42;
optional string aihubmix_app_code = 43;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int64 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_requesty_model_id = 115;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_sap_ai_core_deployment_id = 120;
optional string plan_mode_groq_model_id = 121;
optional OpenRouterModelInfo plan_mode_groq_model_info = 122;
optional string plan_mode_hugging_face_model_id = 123;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 124;
optional string plan_mode_huawei_cloud_maas_model_id = 125;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 126;
optional string plan_mode_baseten_model_id = 127;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
optional string plan_mode_vercel_ai_gateway_model_id = 129;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
optional string plan_mode_aihubmix_model_id = 133;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 134;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int64 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_requesty_model_id = 215;
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_sap_ai_core_deployment_id = 220;
optional string act_mode_groq_model_id = 221;
optional OpenRouterModelInfo act_mode_groq_model_info = 222;
optional string act_mode_hugging_face_model_id = 223;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 224;
optional string act_mode_huawei_cloud_maas_model_id = 225;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 226;
optional string act_mode_baseten_model_id = 227;
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
optional string act_mode_vercel_ai_gateway_model_id = 229;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
optional string act_mode_aihubmix_model_id = 233;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 234;
}
// Request for updating API configuration (legacy - uses combined configuration)
// Request for updating API configuration
message UpdateApiConfigurationRequest {
Metadata metadata = 1;
ModelsApiConfiguration api_configuration = 2;
}
// Combined API configuration containing both options and secrets
message ApiConfiguration {
ModelsApiOptions options = 1;
ModelsApiSecrets secrets = 2;
}
// Request for updating API configuration (new - uses separate options and secrets)
message UpdateApiConfigurationRequestNew {
Metadata metadata = 1;
ApiConfiguration updates = 2;
// Required field mask specifying which fields to update.
// Field paths use dot notation with camelCase field names:
// - "options.ulid" (for options fields)
// - "options.openAiHeaders" (for options fields)
// - "secrets.apiKey" (for secrets fields)
// - "secrets.openRouterApiKey" (for secrets fields)
google.protobuf.FieldMask update_mask = 3;
}
// Request for partially updating API configuration using FieldMask
// Only fields specified in update_mask will be updated from api_configuration
message UpdateApiConfigurationPartialRequest {
@@ -423,9 +229,6 @@ enum ApiProvider {
QWEN_CODE = 33;
DIFY = 34;
OCA = 35;
MINIMAX = 36;
HICAP = 37;
AIHUBMIX = 38;
}
// Model info for OpenAI-compatible models
@@ -543,13 +346,6 @@ message ModelsApiConfiguration {
optional string oca_refresh_token = 75;
optional string oca_mode = 76;
optional bool aws_use_global_inference = 77;
optional string minimax_api_key = 78;
optional string minimax_api_line = 79;
optional string hicap_model_id = 80;
optional string hicap_api_key = 81;
optional string aihubmix_api_key = 82;
optional string aihubmix_base_url = 83;
optional string aihubmix_app_code = 84;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -585,10 +381,6 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
optional string plan_mode_hicap_model_id = 133;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
optional string plan_mode_aihubmix_model_id = 135;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -624,8 +416,4 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
optional string act_mode_hicap_model_id = 233;
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
optional string act_mode_aihubmix_model_id = 235;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
}
+10 -21
View File
@@ -23,7 +23,6 @@ service StateService {
rpc updateSettingsCli(UpdateSettingsRequestCli) returns (Empty);
rpc updateTaskSettings(UpdateTaskSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc captureOnboardingProgress(OnboardingProgressRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
rpc updateModelBannerVersion(Int64Request) returns (Empty);
@@ -47,8 +46,11 @@ message AutoApprovalActions {
// Auto approval settings for task execution
message AutoApprovalSettings {
int32 version = 1;
AutoApprovalActions actions = 2;
optional bool enable_notifications = 3;
bool enabled = 2;
AutoApprovalActions actions = 3;
int32 max_requests = 4;
bool enable_notifications = 5;
repeated string favorites = 6;
}
message Secrets {
@@ -88,7 +90,6 @@ message Secrets {
optional string dify_api_key = 36;
optional string oca_api_key = 37;
optional string oca_refresh_token = 38;
optional string hicap_api_key = 39;
}
message Settings {
@@ -216,13 +217,6 @@ message Settings {
optional int32 max_consecutive_mistakes = 124;
optional bool subagents_enabled = 125;
optional int32 subagent_terminal_output_line_limit = 126;
optional string aihubmix_api_key = 127;
optional string aihubmix_base_url = 128;
optional string aihubmix_app_code = 129;
optional string plan_mode_aihubmix_model_id = 130;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
}
message DictationSettings {
@@ -289,8 +283,11 @@ message ResetStateRequest {
message AutoApprovalSettingsRequest {
Metadata metadata = 1;
int32 version = 2;
AutoApprovalActions actions = 3;
bool enable_notifications = 4;
bool enabled = 3;
AutoApprovalActions actions = 4;
int32 max_requests = 5;
bool enable_notifications = 6;
repeated string favorites = 7;
}
enum TelemetrySettingEnum {
@@ -359,7 +356,6 @@ message UpdateSettingsRequest {
optional bool subagents_enabled = 29;
optional int32 subagent_terminal_output_line_limit = 30;
optional string cline_env = 31;
optional bool native_tool_call_enabled = 32;
}
message UpdateTerminalConnectionTimeoutRequest {
@@ -380,10 +376,3 @@ message ProcessInfo {
optional string version = 2;
optional int64 uptime_ms = 3;
}
message OnboardingProgressRequest {
int32 step = 1;
optional string action = 2;
optional bool completed = 3;
optional string model_selected = 4;
}
+7 -7
View File
@@ -26,12 +26,13 @@ enum ClineAsk {
RESUME_TASK = 7;
RESUME_COMPLETED_TASK = 8;
MISTAKE_LIMIT_REACHED = 9;
BROWSER_ACTION_LAUNCH = 10;
USE_MCP_SERVER = 11;
NEW_TASK = 12;
CONDENSE = 13;
REPORT_BUG = 14;
SUMMARIZE_TASK = 15;
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
BROWSER_ACTION_LAUNCH = 11;
USE_MCP_SERVER = 12;
NEW_TASK = 13;
CONDENSE = 14;
REPORT_BUG = 15;
SUMMARIZE_TASK = 16;
}
// Enum for ClineSay types
@@ -77,7 +78,6 @@ enum ClineSayToolType {
LIST_CODE_DEFINITION_NAMES = 5;
SEARCH_FILES = 6;
WEB_FETCH = 7;
FILE_DELETED = 8;
}
// Enum for browser actions
Binary file not shown.
-1
View File
@@ -328,7 +328,6 @@ export function generateApiKeyDisplayName(fieldName) {
sapAiCoreClientId: "SAP AI Core Client ID",
sapAiCoreClientSecret: "SAP AI Core Client Secret",
huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key",
hicapApiKey: "Hicap API Key",
}
if (specialCases[fieldName]) {
-426
View File
@@ -1,426 +0,0 @@
#!/usr/bin/env node
import chalk from "chalk"
import { execSync } from "child_process"
import * as fs from "fs/promises"
import { globby } from "globby"
import * as path from "path"
import { fileURLToPath } from "url"
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const PROTO_DIR = path.join(ROOT_DIR, "proto")
const PY_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-python")
const PY_CLIENT_DIR = path.join(PY_OUT_DIR, "client")
function hasCommand(cmd) {
try {
if (process.platform === "win32") {
execSync(`where ${cmd}`, { stdio: "pipe" })
} else {
execSync(`which ${cmd}`, { stdio: "pipe" })
}
return true
} catch {
return false
}
}
function resolvePython() {
// Allow override via env.PYTHON pointing to a specific interpreter
const envPy = process.env.PYTHON
if (envPy) {
try {
execSync(`"${envPy}" --version`, { stdio: "pipe" })
return envPy
} catch {
console.warn(chalk.yellow(`Warning: PYTHON override "${envPy}" is not usable, falling back to discovery.`))
}
}
const candidates = ["python3", "python"]
for (const c of candidates) {
if (hasCommand(c)) {
try {
execSync(`${c} --version`, { stdio: "pipe" })
return c
} catch {
// continue
}
}
}
return null
}
function checkGrpcTools(pythonExe) {
try {
execSync(`"${pythonExe}" -c "import grpc_tools"`, { stdio: "pipe" })
return true
} catch {
return false
}
}
async function ensureDir(dir) {
await fs.mkdir(dir, { recursive: true })
}
async function ensureInitPy(dir) {
try {
await fs.writeFile(path.join(dir, "__init__.py"), "", { flag: "wx" })
} catch {
// exists
}
}
/**
* Parse proto files to extract service names with their source file and package.
* Returns array of:
* { serviceName: string, serviceKey: string, protoPackage: "cline"|"host", moduleBase: string }
*/
async function parseServicesWithFiles(protoDir, protoFiles) {
const services = []
for (const relPath of protoFiles) {
const full = path.join(protoDir, relPath)
const content = await fs.readFile(full, "utf8")
const pkg = relPath.startsWith("host/") ? "host" : "cline"
const moduleBase = path.basename(relPath, ".proto")
const serviceRe = /service\s+(\w+Service)\s*\{([\s\S]*?)\}/g
for (const m of content.matchAll(serviceRe)) {
const serviceName = m[1] // e.g., TaskService
const serviceKey = serviceName.replace(/Service$/, "").toLowerCase() // task
const body = m[2]
const methodRe = /rpc\s+(\w+)\s*\((stream\s)?([\w.]+)\)\s*returns\s*\((stream\s)?([\w.]+)\)/g
const methods = []
for (const mm of body.matchAll(methodRe)) {
methods.push({
name: mm[1],
isRequestStreaming: !!mm[2],
requestType: mm[3],
isResponseStreaming: !!mm[4],
responseType: mm[5],
})
}
services.push({ serviceName, serviceKey, protoPackage: pkg, moduleBase, methods })
}
}
return services
}
function upperFirst(s) {
return s.length ? s[0].toUpperCase() + s.slice(1) : s
}
async function generateConnectionPy(outDir) {
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
# Generated by scripts/build-python-proto.mjs
import grpc
import time
from typing import Optional
class ConnectionManager:
def __init__(self, address: str, timeout: float = 30.0):
self.address = address
self.timeout = timeout
self._channel: Optional[grpc.Channel] = None
def connect(self) -> None:
if self._channel is not None:
return
self._channel = grpc.insecure_channel(self.address)
# Wait for channel to be ready within timeout
grpc.channel_ready_future(self._channel).result(timeout=self.timeout)
def disconnect(self) -> None:
if self._channel is not None:
self._channel.close()
self._channel = None
@property
def channel(self) -> Optional[grpc.Channel]:
return self._channel
def is_connected(self) -> bool:
return self._channel is not None
`
await fs.mkdir(outDir, { recursive: true })
await fs.writeFile(path.join(outDir, "connection.py"), content)
await ensureInitPy(outDir)
}
async function generateClineClientPy(outDir, services) {
// Import per-service wrapper clients
const importLines = []
const seen = new Set()
for (const s of services) {
const fileBase = `${s.serviceKey}_client`
const className = `${s.serviceName.replace(/Service$/, "")}Client`
const importKey = `${fileBase}:${className}`
if (!seen.has(importKey)) {
importLines.push(`from .services.${fileBase} import ${className}`)
seen.add(importKey)
}
}
// Build wrapper initializations on connect (like Go New<Service>Client)
const initLines = services.map((s) => {
const shortName = s.serviceName.replace(/Service$/, "") // Task
const className = `${shortName}Client`
return ` self.${shortName} = ${className}(self._conn.channel)`
})
// Build attribute resets on disconnect
const nilLines = services.map((s) => {
const shortName = s.serviceName.replace(/Service$/, "")
return ` self.${shortName} = None`
})
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
# Generated by scripts/build-python-proto.mjs
from typing import Optional
import grpc
from .connection import ConnectionManager
${importLines.join("\n")}
class ClineClient:
"""
Unified Python client analogous to src/generated/grpc-go/client/ClineClient.
Usage:
client = ClineClient("localhost:17611")
client.connect()
# Call wrappers, e.g.: client.Task.SomeRpc(...)
client.disconnect()
"""
def __init__(self, address: str, timeout: float = 30.0):
self._conn = ConnectionManager(address, timeout=timeout)
self._connected = False
${services.map((s) => ` self.${s.serviceName.replace(/Service$/, "")}: Optional[object] = None`).join("\n")}
def connect(self) -> None:
if self._connected:
return
self._conn.connect()
${initLines.join("\n")}
self._connected = True
def disconnect(self) -> None:
if not self._connected:
return
self._conn.disconnect()
${nilLines.join("\n")}
self._connected = False
def is_connected(self) -> bool:
return self._connected
@property
def channel(self) -> Optional[grpc.Channel]:
return self._conn.channel
`
const clientDir = outDir
await fs.mkdir(clientDir, { recursive: true })
await fs.writeFile(path.join(clientDir, "cline_client.py"), content)
}
async function generatePythonClient(protoDir, pyOutDir, clientDir, protoFiles) {
// Ensure package structure for client
await fs.mkdir(clientDir, { recursive: true })
await ensureInitPy(pyOutDir)
await ensureInitPy(clientDir)
const services = await parseServicesWithFiles(protoDir, protoFiles)
// connection.py
await generateConnectionPy(clientDir)
// services/ per-service wrappers (mirror Go client/services)
const servicesDir = path.join(clientDir, "services")
await fs.mkdir(servicesDir, { recursive: true })
await ensureInitPy(servicesDir)
await generateServiceClientsPy(servicesDir, services)
// cline_client.py (unified that composes service wrappers)
await generateClineClientPy(clientDir, services)
}
async function generateServiceClientsPy(outDir, services) {
await fs.mkdir(outDir, { recursive: true })
await ensureInitPy(outDir)
for (const s of services) {
const shortName = s.serviceName.replace(/Service$/, "") // Task
const className = `${shortName}Client`
const fileName = `${s.serviceKey}_client.py`
const aliasPb2 = `${s.protoPackage}_${s.moduleBase}_pb2`
const aliasGrpc = `${s.protoPackage}_${s.moduleBase}_pb2_grpc`
const methodLines = s.methods
.map((m) => {
const reqTypeName = m.requestType.split(".").pop()
const respTypeName = m.responseType.split(".").pop()
if (m.isResponseStreaming) {
return `
def ${m.name}(self, req):
"""
Server-streaming RPC.
:param req: ${aliasPb2}.${reqTypeName}
:return: iterator of ${aliasPb2}.${respTypeName}
"""
return self._stub.${m.name}(req)`
} else {
return `
def ${m.name}(self, req):
"""
Unary RPC.
:param req: ${aliasPb2}.${reqTypeName}
:return: ${aliasPb2}.${respTypeName}
"""
return self._stub.${m.name}(req)`
}
})
.join("\n")
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
# Generated by scripts/build-python-proto.mjs
import grpc
from ${s.protoPackage} import ${s.moduleBase}_pb2 as ${aliasPb2}
from ${s.protoPackage} import ${s.moduleBase}_pb2_grpc as ${aliasGrpc}
class ${className}:
def __init__(self, channel: grpc.Channel):
self._stub = ${aliasGrpc}.${s.serviceName}Stub(channel)
${methodLines}
`
await fs.writeFile(path.join(outDir, fileName), content)
}
}
async function generatePyproject(outDir) {
const content = `[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "cline-grpc-python"
version = "0.1.0"
description = "Generated Python gRPC stubs and client wrappers for Cline protos"
license = { text: "Apache-2.0" }
requires-python = ">=3.9"
dependencies = [
"grpcio>=1.56.0",
"protobuf>=4.21.0"
]
[tool.setuptools.packages.find]
where = ["."]
`
await fs.writeFile(path.join(outDir, "pyproject.toml"), content)
}
async function main() {
console.log(chalk.cyan("Starting Python protobuf code generation..."))
// Verify proto dir exists
try {
const stat = await fs.stat(PROTO_DIR)
if (!stat.isDirectory()) {
console.error(chalk.red(`Proto directory is not a folder: ${PROTO_DIR}`))
process.exit(1)
}
} catch {
console.error(chalk.red(`Proto directory not found: ${PROTO_DIR}`))
process.exit(1)
}
// Resolve Python
const python = resolvePython()
if (!python) {
console.error(
chalk.red("Python not found on PATH. Please install Python 3 and ensure it is available (python3 or python)."),
)
process.exit(1)
}
console.log(chalk.green(`✓ Using Python executable: ${python}`))
// Check grpcio-tools
if (!checkGrpcTools(python)) {
console.error(chalk.red("Missing dependency: grpcio-tools"))
console.log(chalk.yellow("Install with:"))
console.log(chalk.yellow(` ${python} -m pip install grpcio-tools --user --break-system-packages`))
process.exit(1)
}
console.log(chalk.green("✓ grpcio-tools available"))
// Discover proto files
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
if (!protoFiles.length) {
console.error(chalk.red("No .proto files found under ./proto"))
process.exit(1)
}
console.log(chalk.cyan(`Found ${protoFiles.length} proto files`))
// Ensure output directory
await ensureDir(PY_OUT_DIR)
// Build and run protoc command via grpc_tools
const quoted = (s) => `"${s}"`
const pythonCmd = quoted(python)
const cmd =
`${pythonCmd} -m grpc_tools.protoc ` +
`-I ${quoted(PROTO_DIR)} ` +
`--python_out=${quoted(PY_OUT_DIR)} ` +
`--grpc_python_out=${quoted(PY_OUT_DIR)} ` +
protoFiles.map((f) => quoted(f)).join(" ")
try {
console.log(chalk.cyan(`Generating Python code into ${PY_OUT_DIR}...`))
execSync(cmd, { cwd: ROOT_DIR, stdio: "inherit", env: process.env })
} catch (error) {
console.error(chalk.red("Error generating Python code:"), error?.message || error)
process.exit(1)
}
// Ensure package structure (__init__.py) for imports
await ensureInitPy(PY_OUT_DIR)
try {
const clineDir = path.join(PY_OUT_DIR, "cline")
const hostDir = path.join(PY_OUT_DIR, "host")
// These may or may not exist depending on which protos are present
await fs
.stat(clineDir)
.then(() => ensureInitPy(clineDir))
.catch(() => {})
await fs
.stat(hostDir)
.then(() => ensureInitPy(hostDir))
.catch(() => {})
} catch {
// ignore
}
// Generate Python client structure analogous to src/generated/grpc-go/client
await generatePythonClient(PROTO_DIR, PY_OUT_DIR, PY_CLIENT_DIR, protoFiles)
// Generate a minimal pyproject.toml in the generated output so it can be pip-installed if desired
await generatePyproject(PY_OUT_DIR)
console.log(chalk.green("✓ Python protobuf and client code generation completed successfully!"))
console.log(chalk.cyan(`Output directory: ${PY_OUT_DIR}`))
console.log(chalk.cyan(`Client directory: ${PY_CLIENT_DIR}`))
console.log(chalk.cyan(`PyProject: ${path.join(PY_OUT_DIR, "pyproject.toml")}`))
console.log(chalk.gray("Note: To import, add the output dir to your PYTHONPATH or pip install -e src/generated/grpc-python"))
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(chalk.red("Unexpected error in build-python-proto.mjs:"), err)
process.exit(1)
})
}
-1
View File
@@ -1,6 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "@shared/api"
import { fetch } from "@/shared/net"
import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
-15
View File
@@ -14,7 +14,6 @@ import { HostProvider } from "@/hosts/host-provider"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
import { StateManager } from "./core/storage/StateManager"
import { ExtensionRegistryInfo } from "./registry"
import { BannerService } from "./services/banner/BannerService"
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
import { ErrorService } from "./services/error"
import { featureFlagsService } from "./services/feature-flags"
@@ -72,20 +71,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
await showVersionUpdateAnnouncement(context)
// Initialize banner service
BannerService.initialize(webview.controller)
BannerService.get()
.fetchActiveBanners()
.then((banners) => {
if (banners.length > 0) {
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
// Banners are now cached and can be accessed by the frontend when needed
}
})
.catch((error) => {
Logger.error("BannerService: Failed to fetch banners on startup", error)
})
telemetryService.captureExtensionActivated()
return webview
+1 -1
View File
@@ -62,7 +62,7 @@ class ClineEndpoint {
environment: Environment.staging,
appBaseUrl: "https://staging-app.cline.bot",
apiBaseUrl: "https://core-api.staging.int.cline.bot",
mcpBaseUrl: "https://core-api.staging.int.cline.bot/v1/mcp",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
authDomain: "cline-staging.firebaseapp.com",
+5 -32
View File
@@ -1,8 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { ClineTool } from "@/shared/tools"
import { AIhubmixHandler } from "./providers/aihubmix"
import { AnthropicHandler } from "./providers/anthropic"
import { AskSageHandler } from "./providers/asksage"
import { BasetenHandler } from "./providers/baseten"
@@ -16,12 +14,10 @@ import { DoubaoHandler } from "./providers/doubao"
import { FireworksHandler } from "./providers/fireworks"
import { GeminiHandler } from "./providers/gemini"
import { GroqHandler } from "./providers/groq"
import { HicapHandler } from "./providers/hicap"
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
import { HuggingFaceHandler } from "./providers/huggingface"
import { LiteLlmHandler } from "./providers/litellm"
import { LmStudioHandler } from "./providers/lmstudio"
import { MinimaxHandler } from "./providers/minimax"
import { MistralHandler } from "./providers/mistral"
import { MoonshotHandler } from "./providers/moonshot"
import { NebiusHandler } from "./providers/nebius"
@@ -48,7 +44,7 @@ export type CommonApiHandlerOptions = {
}
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
getModel(): ApiHandlerModel
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
}
@@ -367,10 +363,10 @@ function createHandlerForProvider(
return new VercelAIGatewayHandler({
onRetryAttempt: options.onRetryAttempt,
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
vercelAiGatewayModelId:
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
vercelAiGatewayModelInfo:
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
})
case "zai":
return new ZAiHandler({
@@ -393,29 +389,6 @@ function createHandlerForProvider(
: options.actModeOcaModelInfo?.supportsPromptCache,
taskId: options.ulid,
})
case "aihubmix":
return new AIhubmixHandler({
onRetryAttempt: options.onRetryAttempt,
apiKey: options.aihubmixApiKey,
baseURL: options.aihubmixBaseUrl,
appCode: options.aihubmixAppCode,
modelId: mode === "plan" ? (options as any).planModeAihubmixModelId : (options as any).actModeAihubmixModelId,
modelInfo:
mode === "plan" ? (options as any).planModeAihubmixModelInfo : (options as any).actModeAihubmixModelInfo,
})
case "minimax":
return new MinimaxHandler({
onRetryAttempt: options.onRetryAttempt,
minimaxApiKey: options.minimaxApiKey,
minimaxApiLine: options.minimaxApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "hicap":
return new HicapHandler({
onRetryAttempt: options.onRetryAttempt,
hicapApiKey: options.hicapApiKey,
hicapModelId: mode === "plan" ? options.planModeHicapModelId : options.actModeHicapModelId,
})
default:
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
@@ -3,7 +3,6 @@ import { LiteLlmHandler, type LiteLlmModelInfoResponse } from "@core/api/provide
import { convertToOpenAiMessages } from "@core/api/transform/openai-format"
import { expect } from "chai"
import sinon from "sinon"
import { mockFetchForTesting } from "@/shared/net"
const fakeClient = {
chat: {
@@ -11,12 +10,12 @@ const fakeClient = {
create: sinon.stub(),
},
},
baseURL: "https://fake.example",
baseURL: "fake",
}
describe("LiteLlmHandler", () => {
const originalFetch = global.fetch
const mockFetch = sinon.stub()
let doneMockingFetch: (value: any) => void = () => {}
const mockModelFetch = (modelInfo: LiteLlmModelInfoResponse["data"][number]) => {
mockFetch.resolves({
@@ -46,11 +45,7 @@ describe("LiteLlmHandler", () => {
}
beforeEach(() => {
mockFetchForTesting(mockFetch, () => {
return new Promise((resolve) => {
doneMockingFetch = resolve
})
})
global.fetch = mockFetch
// Configure the stub to return a stream that closes immediately with usage data
fakeClient.chat.completions.create.resolves(
@@ -73,7 +68,8 @@ describe("LiteLlmHandler", () => {
afterEach(() => {
sinon.reset()
doneMockingFetch(void 0)
global.fetch = originalFetch
})
const createAsyncIterable = (data: any[] = []) => {
-319
View File
@@ -1,319 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
import { ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface AIhubmixHandlerOptions extends CommonApiHandlerOptions {
apiKey?: string
baseURL?: string
appCode?: string
modelId?: string
modelInfo?: ModelInfo
thinkingBudgetTokens?: number
}
export class AIhubmixHandler implements ApiHandler {
private options: AIhubmixHandlerOptions
private anthropicClient: Anthropic | undefined
private openaiClient: OpenAI | undefined
private geminiClient: GoogleGenAI | undefined
constructor(options: AIhubmixHandlerOptions) {
const { baseURL, appCode, ...rest } = options
this.options = {
baseURL: baseURL ?? "https://aihubmix.com",
appCode: appCode ?? "KUWF9311",
...rest,
}
}
private ensureAnthropicClient(): Anthropic {
if (!this.anthropicClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.anthropicClient = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.baseURL,
defaultHeaders: {
"APP-Code": this.options.appCode,
},
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.anthropicClient
}
private ensureOpenaiClient(): OpenAI {
if (!this.openaiClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.openaiClient = new OpenAI({
apiKey: this.options.apiKey,
baseURL: `${this.options.baseURL}/v1`,
defaultHeaders: {
"APP-Code": this.options.appCode,
},
})
} catch (error) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.openaiClient
}
private ensureGeminiClient(): GoogleGenAI {
if (!this.geminiClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.geminiClient = new GoogleGenAI({
apiKey: this.options.apiKey,
httpOptions: {
baseUrl: `${this.options.baseURL}/gemini`,
headers: {
// @ts-expect-error
"APP-Code": this.options.appCode,
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
},
},
})
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
return this.geminiClient
}
private routeModel(modelName: string): "anthropic" | "openai" | "gemini" | "openai-response" {
const id = modelName || ""
if (id.startsWith("claude")) {
return "anthropic"
}
if (id.startsWith("gemini") && !id.endsWith("-nothink") && !id.endsWith("-search")) {
return "gemini"
}
if (id === "gpt-5-pro" || id === "gpt-5-codex") {
return "openai-response"
}
return "openai"
}
private fixToolChoice(requestBody: any): any {
if (requestBody.tools?.length === 0 && requestBody.tool_choice) {
delete requestBody.tool_choice
}
return requestBody
}
@withRetry()
async *createMessage(systemPrompt: string, messages: any[]): ApiStream {
const modelId = this.options.modelId || ""
const route = this.routeModel(modelId)
switch (route) {
case "anthropic":
yield* this.createAnthropicMessage(systemPrompt, messages)
break
case "gemini":
yield* this.createGeminiMessage(systemPrompt, messages)
break
case "openai-response":
yield* this.createOpenaiResponseMessage(systemPrompt, messages)
break
case "openai":
yield* this.createOpenaiMessage(systemPrompt, messages)
break
default:
throw new Error(`Unsupported model route: ${route}`)
}
}
private async *createAnthropicMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureAnthropicClient()
const modelId = this.options.modelId || "claude-3-5-sonnet-20241022"
const stream = await client.messages.create({
model: modelId,
temperature: 0,
max_tokens: this.options.modelInfo?.maxTokens || 8192,
system: [{ text: systemPrompt, type: "text" }],
messages,
stream: true,
})
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "content_block_start":
if (chunk.content_block.type === "text") {
yield {
type: "text",
text: chunk.content_block.text,
}
}
break
case "content_block_delta":
if (chunk.delta.type === "text_delta") {
yield {
type: "text",
text: chunk.delta.text,
}
}
break
}
}
}
private async *createOpenaiResponseMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureOpenaiClient()
const modelId = this.options.modelId || "gpt-4o-mini"
const input = (messages || []).map((m: any) => {
const role = m.role || "user"
const contentArray = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }]
const content = contentArray
.filter((c: any) => c != null)
.map((c: any) => {
if (c.type === "image" || c.type === "input_image" || c.type === "image_url") {
return { type: "input_image", image_url: c.image_url || c.url || c.source?.url }
}
const text = c.text ?? (typeof c === "string" ? c : "")
return { type: role === "assistant" ? "output_text" : "input_text", text }
})
return { role, content }
})
const stream = await (client as any).responses.stream({
model: modelId,
instructions: systemPrompt,
input,
})
for await (const event of stream as any) {
if (event?.type === "response.output_text.delta") {
yield { type: "text", text: event.delta || "" }
continue
}
if (event?.type === "response.completed") {
const usage = event.response?.usage || {}
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
}
continue
}
if (event?.type === "response.error") {
throw new Error(event.error?.message || "responses error")
}
}
}
private async *createOpenaiMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureOpenaiClient()
const modelId = this.options.modelId || "gpt-4o-mini"
const openaiMessages = [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const requestBody = {
model: modelId,
messages: openaiMessages,
temperature: 0,
stream: true,
}
const fixedRequestBody = this.fixToolChoice(requestBody)
const stream = await client.chat.completions.create(fixedRequestBody)
for await (const chunk of stream as any) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
private async *createGeminiMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureGeminiClient()
const modelId = this.options.modelId || "gemini-2.0-flash-exp"
const contents = messages.map(convertAnthropicMessageToGemini)
const requestConfig: GenerateContentConfig = {
systemInstruction: systemPrompt,
temperature: 0,
}
if (this.options.thinkingBudgetTokens) {
requestConfig.thinkingConfig = {
thinkingBudget: this.options.thinkingBudgetTokens,
includeThoughts: true,
}
}
const stream = await client.models.generateContentStream({
model: modelId,
contents,
config: requestConfig,
})
for await (const chunk of stream as any) {
if (chunk?.text) {
yield { type: "text", text: chunk.text }
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.modelId || "gpt-4o-mini",
info: this.options.modelInfo || {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
description: "AIhubmix unified model provider",
},
}
}
}
+37 -58
View File
@@ -1,12 +1,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 { fetch } from "@/shared/net"
import { ClineTool } from "@/shared/tools"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
import { ApiStream } from "../transform/stream"
interface AnthropicHandlerOptions extends CommonApiHandlerOptions {
@@ -33,7 +29,6 @@ export class AnthropicHandler implements ApiHandler {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
@@ -43,7 +38,7 @@ export class AnthropicHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -53,9 +48,6 @@ export class AnthropicHandler implements ApiHandler {
const enable1mContextWindow = model.id.endsWith(CLAUDE_SONNET_1M_SUFFIX)
const budget_tokens = this.options.thinkingBudgetTokens || 0
// Tools are available only when native tools are enabled.
const nativeToolsOn = tools?.length && tools?.length > 0
const reasoningOn = !!(
(modelId.includes("3-7") || modelId.includes("4-") || modelId.includes("4-5")) &&
budget_tokens !== 0
@@ -64,7 +56,6 @@ export class AnthropicHandler implements ApiHandler {
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-haiku-4-5-20251001":
case "claude-sonnet-4-5-20250929:1m":
case "claude-sonnet-4-5-20250929":
case "claude-sonnet-4-20250514":
case "claude-3-7-sonnet-20250219":
@@ -77,17 +68,12 @@ export class AnthropicHandler implements ApiHandler {
/*
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 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
const anthropicMessages = sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex)
stream = await client.messages.create(
{
model: modelId,
@@ -103,16 +89,39 @@ export class AnthropicHandler implements ApiHandler {
cache_control: { type: "ephemeral" },
},
], // setting cache breakpoint for system prompt so new tasks can reuse it
messages: anthropicMessages,
messages: messages.map((message, index) => {
if (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 message
}),
// 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)
// tool_choice: { type: "auto" },
// tools: tools,
stream: true,
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.
// - any: tells Claude that it must use one of the provided tools, but doesnt force a particular tool.
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
},
(() => {
// 1m context window beta header
@@ -135,7 +144,7 @@ export class AnthropicHandler implements ApiHandler {
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizeAnthropicMessages(messages),
messages,
// tools,
// tool_choice: { type: "auto" },
stream: true,
@@ -145,7 +154,6 @@ export class AnthropicHandler implements ApiHandler {
}
let thinkingDeltaAccumulator = ""
const lastStartedToolCall = { id: "", name: "", arguments: "" }
for await (const chunk of stream) {
switch (chunk?.type) {
@@ -200,14 +208,6 @@ export class AnthropicHandler implements ApiHandler {
data: chunk.content_block.data,
}
break
case "tool_use":
if (chunk.content_block.id && chunk.content_block.name) {
// Convert Anthropic tool_use to OpenAI-compatible format
lastStartedToolCall.id = chunk.content_block.id
lastStartedToolCall.name = chunk.content_block.name
lastStartedToolCall.arguments = ""
}
break
case "text":
// we may receive multiple text blocks, in which case just insert a line break between them
if (chunk.index > 0) {
@@ -250,30 +250,9 @@ export class AnthropicHandler implements ApiHandler {
text: chunk.delta.text,
}
break
case "input_json_delta":
if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
// // Convert Anthropic tool_use to OpenAI-compatible format
yield {
type: "tool_calls",
tool_call: {
...lastStartedToolCall,
function: {
...lastStartedToolCall,
id: lastStartedToolCall.id,
name: lastStartedToolCall.name,
arguments: chunk.delta.partial_json,
},
},
}
}
break
}
break
case "content_block_stop":
lastStartedToolCall.id = ""
lastStartedToolCall.name = ""
lastStartedToolCall.arguments = ""
break
}
}
-1
View File
@@ -1,6 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
-2
View File
@@ -2,7 +2,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -32,7 +31,6 @@ export class BasetenHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://inference.baseten.co/v1",
apiKey: this.options.basetenApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Baseten client: ${error.message}`)
+1 -5
View File
@@ -270,12 +270,8 @@ export class AwsBedrockHandler implements ApiHandler {
},
}
}
// TODO: Add proxy support for AWS SDK
// AWS SDK uses a different architecture than fetch-based SDKs.
// To add proxy support, we need to provide a custom requestHandler.
return new BedrockRuntimeClient({
userAgentAppId: `cline#${ExtensionRegistryInfo.version}`,
defaultUserAgentProvider: () => Promise.resolve([["cline", ExtensionRegistryInfo.version]]),
region: this.getRegion(),
...auth,
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
-2
View File
@@ -1,7 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
@@ -32,7 +31,6 @@ export class CerebrasHandler implements ApiHandler {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
+7 -13
View File
@@ -3,18 +3,15 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
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 { ClineEnv } from "@/config"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { fetch, getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
import { OpenRouterErrorResponse } from "./types"
interface ClineHandlerOptions extends CommonApiHandlerOptions {
@@ -96,7 +93,7 @@ export class ClineHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = await this.ensureClient()
@@ -113,11 +110,8 @@ export class ClineHandler implements ApiHandler {
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
)
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
@@ -156,10 +150,6 @@ export class ClineHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Reasoning tokens are returned separately from the content
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
@@ -194,6 +184,10 @@ export class ClineHandler implements ApiHandler {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (this.getModel().id === "cline/code-supernova-1-million") {
totalCost = 0
}
if (this.getModel().id === "x-ai/grok-code-fast-1") {
totalCost = 0
}
@@ -204,7 +198,8 @@ export class ClineHandler implements ApiHandler {
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,
// @ts-ignore-next-line
totalCost: totalCost,
}
didOutputUsage = true
}
@@ -240,7 +235,6 @@ export class ClineHandler implements ApiHandler {
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
headers,
timeout: 15_000, // this request hangs sometimes
...getAxiosSettings(),
})
const generation = response.data
+1 -12
View File
@@ -2,14 +2,11 @@ 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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface DeepSeekHandlerOptions extends CommonApiHandlerOptions {
deepSeekApiKey?: string
@@ -33,7 +30,6 @@ export class DeepSeekHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
@@ -75,7 +71,7 @@ export class DeepSeekHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -98,11 +94,8 @@ export class DeepSeekHandler implements ApiHandler {
stream_options: { include_usage: true },
// Only set temperature for non-reasoner models
...(model.id === "deepseek-reasoner" ? {} : { temperature: 0 }),
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -112,10 +105,6 @@ export class DeepSeekHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
-1
View File
@@ -1,5 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { fetch } from "@/shared/net"
import { ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
-2
View File
@@ -1,7 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -28,7 +27,6 @@ export class DoubaoHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
-2
View File
@@ -1,7 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -31,7 +30,6 @@ export class FireworksHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
+2 -38
View File
@@ -1,14 +1,6 @@
import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import {
ApiError,
FunctionCallingConfigMode,
type GenerateContentConfig,
type GenerateContentResponseUsageMetadata,
GoogleGenAI,
FunctionDeclaration as GoogleTool,
Part,
} from "@google/genai"
import { ApiError, type GenerateContentConfig, type GenerateContentResponseUsageMetadata, GoogleGenAI, Part } from "@google/genai"
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { telemetryService } from "@/services/telemetry"
import { ApiHandler, CommonApiHandlerOptions } from "../"
@@ -110,7 +102,7 @@ export class GeminiHandler implements ApiHandler {
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: GoogleTool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -148,16 +140,6 @@ export class GeminiHandler implements ApiHandler {
let thoughtsTokenCount = 0 // Initialize thought token counts
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
if (tools?.length) {
requestConfig.tools = [{ functionDeclarations: tools }]
requestConfig.toolConfig = {
// Force the model to call 'any' function.
functionCallingConfig: {
mode: FunctionCallingConfigMode.ANY,
},
}
}
try {
const result = await client.models.generateContentStream({
model: modelId,
@@ -207,24 +189,6 @@ export class GeminiHandler implements ApiHandler {
}
}
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",
tool_call: {
function: {
id: functionCall.id || functionCall.name,
name: functionCall.name,
arguments: JSON.stringify(functionCall.args),
},
},
}
}
}
}
if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
+1 -11
View File
@@ -2,13 +2,10 @@ 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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface GroqHandlerOptions extends CommonApiHandlerOptions {
groqApiKey?: string
@@ -103,7 +100,6 @@ export class GroqHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: this.options.groqApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Groq client: ${error.message}`)
@@ -192,7 +188,7 @@ export class GroqHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
@@ -217,7 +213,6 @@ export class GroqHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
temperature,
...getOpenAIToolParams(tools),
}
// Add any special parameters for specific model families
@@ -225,7 +220,6 @@ export class GroqHandler implements ApiHandler {
Object.assign(requestParams, modelFamily.specialParams)
}
const toolCallProcessor = new ToolCallProcessor()
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
@@ -241,10 +235,6 @@ export class GroqHandler implements ApiHandler {
continue
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Handle content field - trust the parsed output from Groq
if (delta?.content) {
yield {
-104
View File
@@ -1,104 +0,0 @@
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 { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
hicapApiKey?: string
hicapModelId?: string
}
export class HicapHandler implements ApiHandler {
private options: OpenAiHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.hicapApiKey) {
throw new Error("Hicap API key is required")
}
if (!this.options.hicapModelId) {
throw new Error("Model ID is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.hicap.ai/v2/openai",
apiKey: this.options.hicapApiKey,
defaultHeaders: {
"api-key": this.options.hicapApiKey,
},
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.hicapModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const temperature: number = 1
let reasoningEffort: ChatCompletionReasoningEffort | undefined
let maxTokens: number | undefined
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
max_tokens: maxTokens,
reasoning_effort: reasoningEffort,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.hicapModelId ?? "",
info: hicapModelInfoSaneDefaults,
}
}
}
+1 -12
View File
@@ -1,13 +1,10 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface HuaweiCloudMaaSHandlerOptions extends CommonApiHandlerOptions {
huaweiCloudMaasApiKey?: string
@@ -31,7 +28,6 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.modelarts-maas.com/v1/",
apiKey: this.options.huaweiCloudMaasApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
@@ -62,7 +58,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -76,15 +72,12 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
temperature: 0,
...getOpenAIToolParams(tools),
})
let reasoning: string | null = null
let didOutputUsage: boolean = false
let finalUsage: any = null
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
@@ -100,10 +93,6 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Handle reasoning output
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
+1 -11
View File
@@ -2,13 +2,10 @@ 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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface HuggingFaceHandlerOptions extends CommonApiHandlerOptions {
huggingFaceApiKey?: string
@@ -38,7 +35,6 @@ export class HuggingFaceHandler implements ApiHandler {
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
@@ -69,7 +65,7 @@ export class HuggingFaceHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
@@ -86,10 +82,8 @@ export class HuggingFaceHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
temperature: 0,
...getOpenAIToolParams(tools),
}
const toolCallProcessor = new ToolCallProcessor()
const stream = (await client.chat.completions.create(requestParams)) as any
let _chunkCount = 0
@@ -107,10 +101,6 @@ export class HuggingFaceHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
-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 { fetch } from "@/shared/net"
import { isAnthropicModelId } from "@/utils/model-utils"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -56,7 +55,6 @@ export class LiteLlmHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
+1 -14
View File
@@ -1,13 +1,10 @@
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 { fetch } from "@/shared/net"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import type { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface LmStudioHandlerOptions extends CommonApiHandlerOptions {
lmStudioBaseUrl?: string
@@ -30,7 +27,6 @@ export class LmStudioHandler implements ApiHandler {
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
apiKey: "noop",
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
@@ -40,7 +36,7 @@ export class LmStudioHandler implements ApiHandler {
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -54,11 +50,7 @@ export class LmStudioHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
max_completion_tokens: this.options.lmStudioMaxTokens ? Number(this.options.lmStudioMaxTokens) : undefined,
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const choice = chunk.choices[0]
const delta = choice?.delta
@@ -74,11 +66,6 @@ export class LmStudioHandler implements ApiHandler {
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield {
type: "usage",
-212
View File
@@ -1,212 +0,0 @@
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 { fetch } from "@/shared/net"
import { ClineTool } from "@/shared/tools"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
interface MinimaxHandlerOptions extends CommonApiHandlerOptions {
minimaxApiKey?: string
minimaxApiLine?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
export class MinimaxHandler implements ApiHandler {
private options: MinimaxHandlerOptions
private client: Anthropic | undefined
constructor(options: MinimaxHandlerOptions) {
this.options = options
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.minimaxApiKey) {
throw new Error("MiniMax API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.minimaxApiKey,
baseURL:
this.options.minimaxApiLine === "china"
? "https://api.minimaxi.com/anthropic"
: "https://api.minimax.io/anthropic",
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating MiniMax client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
// Tools are available only when native tools are enabled
const nativeToolsOn = tools?.length && tools?.length > 0
// MiniMax M2 uses Anthropic API format
// Note: According to MiniMax docs, some Anthropic parameters like 'thinking' are ignored
// but we'll include the standard Anthropic streaming pattern for consistency
const stream: AnthropicStream<Anthropic.RawMessageStreamEvent> = await client.messages.create({
model: model.id,
max_tokens: model.info.maxTokens || 8192,
temperature: 1.0, // MiniMax recommends 1.0, range is (0.0, 1.0]
system: [{ text: systemPrompt, type: "text" }],
messages,
stream: true,
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
tool_choice: nativeToolsOn ? { type: "any" } : undefined,
})
let thinkingDeltaAccumulator = ""
const lastStartedToolCall = { id: "", name: "", arguments: "" }
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
// tells us cache reads/writes/input/output
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
// tells us stop_reason, stop_sequence, and output tokens along the way and at the end of the message
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "message_stop":
// no usage data, just an indicator that the message is done
break
case "content_block_start":
switch (chunk.content_block.type) {
case "thinking":
yield {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
const thinking = chunk.content_block.thinking
const signature = chunk.content_block.signature
if (thinking && signature) {
yield {
type: "ant_thinking",
thinking,
signature,
}
}
break
case "redacted_thinking":
// Content is encrypted, and we don't want to pass placeholder text back to the API
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
yield {
type: "ant_redacted_thinking",
data: chunk.content_block.data,
}
break
case "tool_use":
if (chunk.content_block.id && chunk.content_block.name) {
// Store tool call information for streaming
lastStartedToolCall.id = chunk.content_block.id
lastStartedToolCall.name = chunk.content_block.name
lastStartedToolCall.arguments = ""
}
break
case "text":
// we may receive multiple text blocks, in which case just insert a line break between them
if (chunk.index > 0) {
yield {
type: "text",
text: "\n",
}
}
yield {
type: "text",
text: chunk.content_block.text,
}
break
}
break
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
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
thinkingDeltaAccumulator += chunk.delta.thinking
break
case "signature_delta":
// It's used when sending the thinking block back to the API
// API expects this in completed form, not as array of deltas
if (thinkingDeltaAccumulator && chunk.delta.signature) {
yield {
type: "ant_thinking",
thinking: thinkingDeltaAccumulator,
signature: chunk.delta.signature,
}
}
break
case "text_delta":
yield {
type: "text",
text: chunk.delta.text,
}
break
case "input_json_delta":
if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
// Convert Anthropic tool_use to OpenAI-compatible format for internal processing
yield {
type: "tool_calls",
tool_call: {
...lastStartedToolCall,
function: {
...lastStartedToolCall,
id: lastStartedToolCall.id,
name: lastStartedToolCall.name,
arguments: chunk.delta.partial_json,
},
},
}
}
break
}
break
case "content_block_stop":
lastStartedToolCall.id = ""
lastStartedToolCall.name = ""
lastStartedToolCall.arguments = ""
break
}
}
}
getModel(): { id: MinimaxModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in minimaxModels) {
const id = modelId as MinimaxModelId
return { id, info: minimaxModels[id] }
}
return { id: minimaxDefaultModelId, info: minimaxModels[minimaxDefaultModelId] }
}
}
+2 -29
View File
@@ -1,10 +1,6 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToMistralMessages } from "../transform/mistral-format"
@@ -29,16 +25,8 @@ export class MistralHandler implements ApiHandler {
throw new Error("Mistral API key is required")
}
try {
// Create HTTP client with custom fetch for proxy support
const httpClient = new HTTPClient({
fetcher: (request) => {
return fetch(request)
},
})
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
httpClient,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
@@ -48,7 +36,7 @@ export class MistralHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const stream = await client.chat
.stream({
@@ -57,8 +45,6 @@ export class MistralHandler implements ApiHandler {
temperature: 0,
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
stream: true,
tools: tools?.length ? (tools as MistralTool[]) : undefined,
toolChoice: tools?.length ? "any" : undefined,
})
.catch((err) => {
// The Mistal SDK uses statusCode instead of status
@@ -72,20 +58,7 @@ export class MistralHandler implements ApiHandler {
for await (const chunk of stream) {
const delta = chunk.data.choices[0]?.delta
if (delta.toolCalls) {
for (const toolCall of delta.toolCalls) {
yield {
type: "tool_calls",
tool_call: {
function: {
id: toolCall.id,
name: toolCall.function.name,
arguments: JSON.stringify(toolCall.function.arguments),
},
},
}
}
} else if (delta?.content) {
if (delta?.content) {
let content: string = ""
if (typeof delta.content === "string") {
content = delta.content
+1 -13
View File
@@ -1,13 +1,10 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface MoonshotHandlerOptions extends CommonApiHandlerOptions {
moonshotApiKey?: string
@@ -30,7 +27,6 @@ export class MoonshotHandler implements ApiHandler {
baseURL:
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
apiKey: this.options.moonshotApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Moonshot client: ${error.message}`)
@@ -40,7 +36,7 @@ export class MoonshotHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -56,11 +52,7 @@ export class MoonshotHandler implements ApiHandler {
max_tokens: model.info.maxTokens,
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -70,10 +62,6 @@ export class MoonshotHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
+1 -11
View File
@@ -1,14 +1,11 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface NebiusHandlerOptions extends CommonApiHandlerOptions {
nebiusApiKey?: string
@@ -29,7 +26,6 @@ export class NebiusHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Nebius client: ${error.message}`)
@@ -39,7 +35,7 @@ export class NebiusHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -53,9 +49,7 @@ export class NebiusHandler implements ApiHandler {
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -65,10 +59,6 @@ export class NebiusHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
+1 -12
View File
@@ -2,7 +2,6 @@ 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 {
DEFAULT_EXTERNAL_OCA_BASE_URL,
@@ -11,12 +10,10 @@ import {
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { fetch } from "@/shared/net"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
ocaBaseUrl?: string
@@ -82,7 +79,6 @@ export class OcaHandler implements ApiHandler {
options.ocaBaseUrl ||
(options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL),
apiKey: "noop",
fetch, // Use configured fetch with proxy support
})
}
@@ -139,7 +135,7 @@ export class OcaHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
@@ -191,8 +187,6 @@ export class OcaHandler implements ApiHandler {
return message
})
const toolCallProcessor = new ToolCallProcessor()
const stream = await client.chat.completions.create({
model: this.options.ocaModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
@@ -204,7 +198,6 @@ export class OcaHandler implements ApiHandler {
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
...(this.options.taskId && {
litellm_session_id: `cline-${this.options.taskId}`,
...getOpenAIToolParams(tools),
}), // Add session ID for LiteLLM tracking
})
@@ -235,10 +228,6 @@ export class OcaHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Handle token usage information
if (chunk.usage) {
const totalCost =
+4 -28
View File
@@ -2,13 +2,11 @@ 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 { fetch } from "@/shared/net"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions {
openAiNativeApiKey?: string
@@ -32,7 +30,6 @@ export class OpenAiNativeHandler implements ApiHandler {
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
@@ -59,14 +56,9 @@ export class OpenAiNativeHandler implements ApiHandler {
}
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools?: ChatCompletionTool[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const toolCallProcessor = new ToolCallProcessor()
switch (model.id) {
case "o1":
@@ -122,7 +114,6 @@ export class OpenAiNativeHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
...getOpenAIToolParams(tools),
})
for await (const chunk of stream) {
@@ -133,17 +124,8 @@ export class OpenAiNativeHandler implements ApiHandler {
text: delta.content,
}
}
if (delta?.tool_calls) {
try {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
} catch (error) {
console.error("Error processing tool call delta:", error, delta.tool_calls)
}
}
if (chunk.usage) {
// Only last chunk contains usage - stream is ending
// Only last chunk contains usage
yield* this.yieldUsage(model.info, chunk.usage)
}
}
@@ -156,7 +138,6 @@ export class OpenAiNativeHandler implements ApiHandler {
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
for await (const chunk of stream) {
@@ -167,13 +148,8 @@ export class OpenAiNativeHandler implements ApiHandler {
text: delta.content,
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
// Only last chunk contains usage - stream is ending
// Only last chunk contains usage
yield* this.yieldUsage(model.info, chunk.usage)
}
}
+7 -19
View File
@@ -1,14 +1,12 @@
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 { fetch } from "@/shared/net"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
openAiApiKey?: string
@@ -47,14 +45,12 @@ export class OpenAiHandler implements ApiHandler {
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
fetch, // Use configured fetch with proxy support
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
fetch, // Use configured fetch with proxy support
})
}
} catch (error: any) {
@@ -65,17 +61,16 @@ export class OpenAiHandler implements ApiHandler {
}
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools?: ChatCompletionTool[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
const isReasoningModelFamily =
["o1", "o3", "o4", "gpt-5"].some((prefix) => modelId.includes(prefix)) && !modelId.includes("chat")
modelId.includes("o1") ||
modelId.includes("o3") ||
modelId.includes("o4") ||
(modelId.includes("gpt-5") && !modelId.includes("chat"))
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -109,11 +104,7 @@ export class OpenAiHandler implements ApiHandler {
reasoning_effort: reasoningEffort,
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -130,15 +121,12 @@ export class OpenAiHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
+1 -12
View File
@@ -4,13 +4,10 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
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 { fetch, getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
import { OpenRouterErrorResponse } from "./types"
interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
@@ -44,7 +41,6 @@ export class OpenRouterHandler implements ApiHandler {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
@@ -54,7 +50,7 @@ export class OpenRouterHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
@@ -66,11 +62,9 @@ export class OpenRouterHandler implements ApiHandler {
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
)
let didOutputUsage: boolean = false
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
@@ -118,10 +112,6 @@ export class OpenRouterHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Reasoning tokens are returned separately from the content
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
@@ -203,7 +193,6 @@ export class OpenRouterHandler implements ApiHandler {
Authorization: `Bearer ${this.options.openRouterApiKey}`,
},
timeout: 15_000, // this request hangs sometimes
...getAxiosSettings(),
})
yield response.data?.data
} catch (error) {
+1 -10
View File
@@ -2,15 +2,12 @@ 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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
// --- Constants for Qwen OAuth2 ---
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
@@ -177,7 +174,7 @@ export class QwenCodeHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
await this.ensureAuthenticated()
const client = this.ensureClient()
const model = this.getModel()
@@ -196,12 +193,10 @@ export class QwenCodeHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
max_completion_tokens: model.info.maxTokens,
...getOpenAIToolParams(tools),
}
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
const toolCallProcessor = new ToolCallProcessor()
let fullContent = ""
for await (const apiChunk of stream) {
@@ -245,10 +240,6 @@ export class QwenCodeHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
// Handle reasoning content (o1-style)
if ("reasoning_content" in delta && delta.reasoning_content) {
yield {
+1 -16
View File
@@ -10,14 +10,11 @@ import {
QwenApiRegions,
} from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface QwenHandlerOptions extends CommonApiHandlerOptions {
qwenApiKey?: string
@@ -53,7 +50,6 @@ export class QwenHandler implements ApiHandler {
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating Alibaba client: ${error.message}`)
@@ -81,7 +77,7 @@ export class QwenHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
@@ -116,11 +112,8 @@ export class QwenHandler implements ApiHandler {
stream_options: { include_usage: true },
temperature,
...thinkingArgs,
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -130,14 +123,6 @@ export class QwenHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
try {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
} catch (error) {
console.error("Error processing tool call delta:", error, delta.tool_calls)
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
+1 -3
View File
@@ -2,8 +2,7 @@ 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 { fetch } from "@/shared/net"
import { toRequestyServiceStringUrl } from "@/shared/providers/requesty"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -49,7 +48,6 @@ export class RequestyHandler implements ApiHandler {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating Requesty client: ${error.message}`)
+1 -11
View File
@@ -1,14 +1,11 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface SambanovaHandlerOptions extends CommonApiHandlerOptions {
sambanovaApiKey?: string
@@ -32,7 +29,6 @@ export class SambanovaHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating SambaNova client: ${error.message}`)
@@ -42,7 +38,7 @@ export class SambanovaHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
@@ -57,14 +53,12 @@ export class SambanovaHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const toolCallProcessor = new ToolCallProcessor()
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
for await (const chunk of stream) {
@@ -76,10 +70,6 @@ export class SambanovaHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield {
type: "usage",
+2 -8
View File
@@ -8,7 +8,6 @@ import { ChatMessages, LlmModuleConfig, OrchestrationClient, TemplatingModuleCon
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
import axios from "axios"
import OpenAI from "openai"
import { getAxiosSettings } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -385,7 +384,6 @@ export class SapAiCoreHandler implements ApiHandler {
const tokenUrl = this.options.sapAiCoreTokenUrl!.replace(/\/+$/, "") + "/oauth/token"
const response = await axios.post(tokenUrl, payload, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
...getAxiosSettings(),
})
const token = response.data as Token
token.expires_at = Date.now() + token.expires_in * 1000
@@ -412,7 +410,7 @@ export class SapAiCoreHandler implements ApiHandler {
const url = `${this.options.sapAiCoreBaseUrl}/v2/lm/deployments?$top=10000&$skip=0`
try {
const response = await axios.get(url, { headers, ...getAxiosSettings() })
const response = await axios.get(url, { headers })
const deployments = response.data.resources
return deployments
@@ -557,7 +555,6 @@ export class SapAiCoreHandler implements ApiHandler {
}
const anthropicModels = [
"anthropic--claude-4.5-sonnet",
"anthropic--claude-4-sonnet",
"anthropic--claude-4-opus",
"anthropic--claude-3.7-sonnet",
@@ -602,7 +599,6 @@ export class SapAiCoreHandler implements ApiHandler {
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
if (
model.id === "anthropic--claude-4.5-sonnet" ||
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
@@ -681,11 +677,10 @@ export class SapAiCoreHandler implements ApiHandler {
const response = await axios.post(url, JSON.stringify(payload, null, 2), {
headers,
responseType: "stream",
...getAxiosSettings(),
})
if (model.id === "o3-mini") {
const response = await axios.post(url, JSON.stringify(payload, null, 2), { headers, ...getAxiosSettings() })
const response = await axios.post(url, JSON.stringify(payload, null, 2), { headers })
// Yield the usage information
if (response.data.usage) {
@@ -715,7 +710,6 @@ export class SapAiCoreHandler implements ApiHandler {
} else if (openAIModels.includes(model.id)) {
yield* this.streamCompletionGPT(response.data, model)
} else if (
model.id === "anthropic--claude-4.5-sonnet" ||
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
+1 -11
View File
@@ -1,14 +1,11 @@
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 { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
interface TogetherHandlerOptions extends CommonApiHandlerOptions {
togetherApiKey?: string
@@ -32,7 +29,6 @@ export class TogetherHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating Together client: ${error.message}`)
@@ -42,7 +38,7 @@ export class TogetherHandler implements ApiHandler {
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@@ -62,9 +58,7 @@ export class TogetherHandler implements ApiHandler {
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...getOpenAIToolParams(tools),
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -74,10 +68,6 @@ export class TogetherHandler implements ApiHandler {
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",

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