Compare commits

..

2 Commits

Author SHA1 Message Date
Arafatkatze d879f2cbfc Fixing stuff 2026-02-20 16:26:58 -08:00
Arafatkatze bdb3bf46d5 feat: add debug logging and abort support to Vercel AI Gateway handler
Implemented detailed debug logging for the Vercel AI Gateway integration:
- Added filesystem, OS, and path imports to write logs.
- Introduced configurable debug log path and trailing usage idle timeout via environment variables.
- Implemented a write queue to serialize log writes and added failure warnings.
- Logged client creation, reuse, and missing API key scenarios.
- Added abort handling with contextual logging for active streams.
- Updated imports to include OpenAI chat completion types.

These changes improve observability, simplify troubleshooting, and provide safer stream abort handling.
2026-02-20 15:46:24 -08:00
439 changed files with 3340 additions and 4711 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"cline": minor
---
Adds Messages API support to Oracle Code Assist, adding functionality for Claude models
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Add /q command to quit CLI
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix a streaming crash when a chunk has usage data but no `delta` by guarding reasoning field checks in provider handlers. Add regression tests for OpenRouter, Cline, Vercel AI Gateway, and Fireworks handlers to cover usage-only chunks.
-4
View File
@@ -1,4 +0,0 @@
"claude-dev": patch
---
Add missing smoke evaluation npm scripts so documented commands like `npm run eval:smoke:run` work from the repository root.
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
fix acp auth check so acp mode can be used with more providers
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
VSCode uses shared files for global, workspace and secret state.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Update SambaNova Provider models list and add temperature for models
+1 -3
View File
@@ -36,9 +36,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
node-version: "lts/*"
- name: Install root dependencies
run: npm ci --include=optional
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
node-version: "lts/*"
- name: Install root dependencies
run: npm install --include=optional
-7
View File
@@ -1,12 +1,5 @@
# Changelog
## [3.66.0]
### Added
- Gemini-3.1 Pro Preview
## [3.65.0]
### Added
-10
View File
@@ -1,15 +1,5 @@
# cline
## 2.4.2
### Added
- Gemini-3.1 Pro Preview
### Patch Changes
- VSCode uses shared files for global, workspace and secret state.
## [2.4.1]
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "2.4.2",
"version": "2.4.1",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/cli.mjs",
"bin": {
+1 -1
View File
@@ -211,7 +211,7 @@ export class ACPDiffViewProvider extends FileEditProvider {
* content via the ACP connection. Otherwise, it falls back to the
* FileEditProvider's local fs implementation.
*/
protected override async saveDocument(): Promise<boolean> {
protected override async saveDocument(): Promise<Boolean> {
// If we can't write files via ACP, fall back to FileEditProvider
if (!this.canWriteFile()) {
Logger.debug("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
+5 -3
View File
@@ -111,7 +111,7 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
constructor(
_clientCapabilities: acp.ClientCapabilities | undefined,
_sessionIdResolver: SessionIdResolver,
version = "1.0.0",
version: string = "1.0.0",
) {
this.version = version
}
@@ -191,6 +191,8 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
* Most operations are stubs that will be implemented using ACP extension methods.
*/
class ACPWindowServiceClient implements WindowServiceClientInterface {
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver) {}
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
// Next phase: Send ACP extension request to open document in the editor.
// This would tell the ACP client to open the specified file.
@@ -400,11 +402,11 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
version = "1.0.0",
version: string = "1.0.0",
) {
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
this.windowClient = new ACPWindowServiceClient()
this.windowClient = new ACPWindowServiceClient(clientCapabilities, sessionIdResolver)
this.diffClient = new ACPDiffServiceClient()
}
}
+37 -4
View File
@@ -38,6 +38,7 @@ import {
} from "@shared/api"
import type { ClineAsk, ClineMessage as ClineMessageType } from "@shared/ExtensionMessage"
import { CLI_ONLY_COMMANDS, VSCODE_ONLY_COMMANDS } from "@shared/slashCommands"
import { ProviderToApiKeyMap } from "@shared/storage"
import { getProviderModelIdKey } from "@shared/storage/provider-keys"
import { ClineEndpoint } from "@/config.js"
import { Controller } from "@/core/controller"
@@ -57,7 +58,6 @@ import { openExternal } from "@/utils/env"
import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js"
import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js"
import { AcpTerminalManager } from "../acp/AcpTerminalManager.js"
import { isAuthConfigured } from "../index.js"
import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models"
import { CliContextResult, initializeCliContext } from "../vscode-context.js"
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
@@ -265,7 +265,7 @@ export class ClineAgent implements acp.Agent {
*/
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
// Check if authentication is required
const isAuthenticated = await isAuthConfigured()
const isAuthenticated = await this.isAuthConfigured()
if (!isAuthenticated) {
throw RequestError.authRequired()
}
@@ -829,7 +829,7 @@ export class ClineAgent implements acp.Agent {
await this.emitSessionUpdate(sessionId, {
sessionUpdate,
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta },
})
}
@@ -1082,7 +1082,7 @@ export class ClineAgent implements acp.Agent {
// Use the permission handler callback pattern
return new Promise<acp.RequestPermissionResponse>((resolve) => {
this.permissionHandler?.({ toolCall, options }, resolve)
this.permissionHandler!({ toolCall, options }, resolve)
})
}
@@ -1146,6 +1146,39 @@ export class ClineAgent implements acp.Agent {
}
}
/**
* Check if the user has authentication configured.
* Returns true if they have either:
* - Cline provider with stored auth data
* - OpenAI Codex provider with OAuth credentials
* - BYO provider with an API key configured
*/
private async isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") as string
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
if (currentProvider === "cline") {
// For Cline provider, check if we have stored auth data
return Boolean(stateManager.getSecretKey("clineApiKey") || stateManager.getSecretKey("clineAccountId"))
}
// For OpenAI Codex provider, check OAuth credentials
if (currentProvider === "openai-codex") {
return await openAiCodexOAuthManager.isAuthenticated()
}
// For BYO providers, check if the API key is configured
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
if (!keyField) {
return false
}
const fields = Array.isArray(keyField) ? keyField : [keyField]
return fields.some((key) => stateManager.getSecretKey(key))
}
/**
* Handle OpenAI Codex OAuth authentication flow.
*
+7 -7
View File
@@ -983,13 +983,13 @@ describe("translateMessage - ask messages", () => {
// Should require permission
expect(result.requiresPermission).toBe(true)
expect(result.permissionRequest).toBeDefined()
expect(result.permissionRequest?.toolCall.toolCallId).toBe(call.toolCallId)
expect(result.permissionRequest!.toolCall.toolCallId).toBe(call.toolCallId)
// Should have standard permission options
expect(result.permissionRequest?.options).toHaveLength(3)
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("allow_once")
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("allow_always")
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("reject_once")
expect(result.permissionRequest!.options).toHaveLength(3)
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("allow_once")
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("allow_always")
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("reject_once")
// Should track pending tool call
expect(sessionState.pendingToolCalls.has(call.toolCallId)).toBe(true)
@@ -1087,8 +1087,8 @@ describe("translateMessage - ask messages", () => {
expect(result.requiresPermission).toBe(true)
// Browser actions have restricted options (no "always allow")
expect(result.permissionRequest?.options).toHaveLength(2)
expect(result.permissionRequest?.options.map((o) => o.kind)).not.toContain("allow_always")
expect(result.permissionRequest!.options).toHaveLength(2)
expect(result.permissionRequest!.options.map((o) => o.kind)).not.toContain("allow_always")
})
})
+2 -2
View File
@@ -233,7 +233,7 @@ function translateSayMessage(
if (message.text) {
updates.push({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: `\n${message.text}` },
content: { type: "text", text: "\n" + message.text },
})
}
break
@@ -622,7 +622,7 @@ function translateAskMessage(
if (message.text) {
updates.push({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: `\n${message.text}` },
content: { type: "text", text: "\n" + message.text },
})
}
break
+1 -1
View File
@@ -116,7 +116,7 @@ export function getPermissionOptionsForAskType(askType: ClineAsk): acp.Permissio
* @param askType - The original ClineAsk type that triggered the permission request
* @returns The translated result for Cline's handleWebviewAskResponse
*/
export function handlePermissionResponse(response: acp.RequestPermissionResponse, _askType: ClineAsk): PermissionHandlerResult {
export function handlePermissionResponse(response: acp.RequestPermissionResponse, askType: ClineAsk): PermissionHandlerResult {
// Check if cancelled
if (response.outcome.outcome === "cancelled") {
return {
+1 -1
View File
@@ -194,7 +194,7 @@ const errorTypes = ["api_req_failed", "mistake_limit_reached"]
/**
* Get button configuration based on message type and state
*/
export function getButtonConfig(message: ClineMessage | undefined, isStreaming = false): ButtonConfig {
export function getButtonConfig(message: ClineMessage | undefined, isStreaming: boolean = false): ButtonConfig {
if (!message) {
return BUTTON_CONFIGS.default
}
+39 -38
View File
@@ -145,7 +145,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Check if it's a number for option selection (only when no text typed yet)
const num = Number.parseInt(input, 10)
const num = parseInt(input, 10)
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
const selectedOption = parts.options[num - 1]
sendResponse("messageResponse", selectedOption)
@@ -401,42 +401,43 @@ function getCliMessagePrefixIcon(message: ClineMessage): string {
default:
return "❔"
}
}
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
}
}
}
+2 -2
View File
@@ -323,7 +323,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
}, [controller])
}, [])
// Start Cline auth flow
const startClineAuth = useCallback(async () => {
@@ -379,7 +379,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setStep("apikey")
}
},
[startOpenAiCodexAuth],
[startOcaAuth, startOpenAiCodexAuth],
)
const handleApiKeySubmit = useCallback(
+4 -4
View File
@@ -177,7 +177,7 @@ function getToolMainArg(_toolName: string, args: Record<string, unknown>): strin
// Command - truncate long commands
if (typeof args.command === "string") {
return args.command.length > 120 ? `${args.command.substring(0, 117)}...` : args.command
return args.command.length > 120 ? args.command.substring(0, 117) + "..." : args.command
}
// URL
@@ -221,7 +221,7 @@ const ToolCallText: React.FC<{
*/
function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return `${text.substring(0, maxLength - 3)}...`
return text.substring(0, maxLength - 3) + "..."
}
/**
@@ -245,7 +245,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
// User messages (task, user_feedback)
// If multi-line, extend background to full width for consistent appearance
if (say === "task" || say === "user_feedback") {
const content = `> ${text || ""}`
const content = "> " + (text || "")
const isMultiLine = content.includes("\n") || content.length > terminalWidth
if (isMultiLine) {
@@ -412,7 +412,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
: "tool: unknown"
let argsLines: string[] = []
if (parsed?.arguments?.trim() && parsed.arguments !== "{}") {
if (parsed?.arguments && parsed.arguments.trim() && parsed.arguments !== "{}") {
let formattedArgs = parsed.arguments
try {
formattedArgs = JSON.stringify(JSON.parse(parsed.arguments), null, 2)
+1 -1
View File
@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { ChatView } from "./ChatView"
// Helper to wait for async state updates
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
// Type for our exit mock function
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
+12 -12
View File
@@ -486,7 +486,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (taskState.mode && taskState.mode !== mode) {
setMode(taskState.mode as Mode)
}
}, [taskState.mode, mode])
}, [taskState.mode])
const toggleAutoApproveAll = useCallback(() => {
const newValue = !autoApproveAll
@@ -499,7 +499,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
const stateManager = StateManager.get()
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
}, [mode])
}, [mode, activePanel])
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
@@ -510,7 +510,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
const stateManager = StateManager.get()
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
}, [mode, provider])
}, [mode, provider, activePanel])
const toggleMode = useCallback(async () => {
const newMode: Mode = mode === "act" ? "plan" : "act"
@@ -549,7 +549,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (ctrl) {
ctrl.postStateToWebview()
}
}, [ctrl, clearState, storageKey, setCursorPos, setTextInput])
}, [ctrl, clearState, storageKey])
const refs = useRef({
searchTimeout: null as NodeJS.Timeout | null,
@@ -644,10 +644,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
const messages = taskState.clineMessages || []
// Refresh git diff stats when messages change (after file edits)
const _lastMsg = messages[messages.length - 1]
const lastMsg = messages[messages.length - 1]
useEffect(() => {
setGitDiffStats(getGitDiffStats(workspacePath))
}, [workspacePath])
}, [messages.length, lastMsg?.partial, lastMsg?.ts, workspacePath])
// Filter messages we want to display
const displayMessages = useMemo(() => {
@@ -819,7 +819,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Controller may be disposed
}
},
[ctrl, pendingAsk, pastedTexts, storageKey, setCursorPos, setTextInput],
[ctrl, pendingAsk, pastedTexts, storageKey],
)
// Handle cancel/interrupt
@@ -895,7 +895,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
break
}
},
[sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask, ctrl, setCursorPos, setTextInput],
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask],
)
// Handle task submission (new task)
@@ -939,7 +939,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
onError?.()
}
},
[ctrl, onError, pastedTexts, storageKey, setCursorPos, setTextInput],
[ctrl, onError, pastedTexts, storageKey],
)
// Auto-submit initial prompt if provided
@@ -998,7 +998,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
autoSubmit()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller, initialImages, initialPrompt, onError, taskController, taskId]) // Only run once on mount
}, []) // Only run once on mount
// Search for files when in mention mode
useEffect(() => {
@@ -1051,7 +1051,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
clearTimeout(r.searchTimeout)
}
}
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath, mentionInfo])
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
// Handle keyboard input
//
@@ -1172,7 +1172,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
setSlashMenuDismissed(true)
return
}
if (cmd.name === "exit" || cmd.name === "q") {
if (cmd.name === "exit") {
handleExit()
return
}
+1 -1
View File
@@ -120,7 +120,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
// Quick number selection for checkpoints
if (stage === "checkpoint") {
const num = Number.parseInt(input, 10)
const num = parseInt(input, 10)
if (!Number.isNaN(num) && num >= 1 && num <= checkpoints.length) {
setSelectedCheckpoint(num - 1)
setStage("restoreType")
+1 -1
View File
@@ -309,7 +309,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
const currentTabIndex = availableTabs.findIndex((t) => t.key === currentTab)
const targetIdx =
input >= "1" && input <= "5"
? Number.parseInt(input, 10) - 1
? Number.parseInt(input) - 1
: key.leftArrow
? (currentTabIndex - 1 + availableTabs.length) % availableTabs.length
: (currentTabIndex + 1) % availableTabs.length
+3 -3
View File
@@ -127,10 +127,10 @@ export function formatValue(value: unknown, maxLen = 50): string {
}
if (typeof value === "object") {
const json = JSON.stringify(value)
return json.length > maxLen ? `${json.slice(0, maxLen - 3)}...` : json
return json.length > maxLen ? json.slice(0, maxLen - 3) + "..." : json
}
const str = String(value)
return str.length > maxLen ? `${str.slice(0, maxLen - 3)}...` : str
return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str
}
export function parseValue(input: string, type: ValueType): unknown {
@@ -337,7 +337,7 @@ export const SkillRow: React.FC<{
{skill.description && (
<Box marginLeft={4}>
<Text color="gray">
{skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description}
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
</Text>
</Box>
)}
+2 -2
View File
@@ -20,11 +20,11 @@ interface FileMentionMenuProps {
/**
* Truncate path from the left if too long, keeping the filename visible
*/
function truncatePath(filePath: string, maxLength = 50): string {
function truncatePath(filePath: string, maxLength: number = 50): string {
if (filePath.length <= maxLength) {
return filePath
}
return `...${filePath.slice(-(maxLength - 3))}`
return "..." + filePath.slice(-(maxLength - 3))
}
export const FileMentionMenu: React.FC<FileMentionMenuProps> = ({
+1 -1
View File
@@ -115,7 +115,7 @@ const Header: React.FC<{
const isCompleted = completedCount === totalCount
const displayText = isCompleted ? "All tasks completed!" : currentTodo?.text || "To-Do list"
const truncatedText = displayText.length > 50 ? `${displayText.substring(0, 47)}...` : displayText
const truncatedText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText
return (
<Box flexDirection="row" gap={1}>
-4
View File
@@ -88,10 +88,6 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
{" "}
<Text color="white">/clear</Text> - Start a fresh task
</Text>
<Text>
{" "}
<Text color="white">/q</Text> - Quit Cline
</Text>
</Box>
<Text>
+1 -1
View File
@@ -98,7 +98,7 @@ export const HistoryPanelContent: React.FC<HistoryPanelContentProps> = ({ onClos
// Reset selection when search changes
useEffect(() => {
setSelectedIndex(0)
}, [])
}, [searchQuery])
const handleSelect = useCallback(
async (item: TaskHistoryItem) => {
+4 -4
View File
@@ -40,7 +40,7 @@ interface HistoryViewProps {
/**
* Format separator
*/
function formatSeparator(char = "─", width = 80): string {
function formatSeparator(char: string = "─", width: number = 80): string {
return char.repeat(Math.max(width, 10))
}
@@ -142,7 +142,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
return (
<Box flexDirection="column">
<Text bold color="white">
{`📜 Task History (${totalCount} total)`}
{"📜 Task History (" + totalCount + " total)"}
</Text>
<Text color="gray">Use /j/k to navigate, Enter to select</Text>
@@ -161,7 +161,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
<Text>No task history available.</Text>
) : (
<Box flexDirection="column">
{showUpIndicator && <Text color="gray">{`${startIndex} more above`}</Text>}
{showUpIndicator && <Text color="gray">{" ↑ " + startIndex + " more above"}</Text>}
{visibleTasks.map((task, index) => {
const actualIndex = startIndex + index
const isSelected = actualIndex === selectedIndex
@@ -197,7 +197,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
</Box>
)
})}
{showDownIndicator && <Text color="gray">{`${pageItems.length - endIndex} more below`}</Text>}
{showDownIndicator && <Text color="gray">{" ↓ " + (pageItems.length - endIndex) + " more below"}</Text>}
</Box>
)}
+1 -1
View File
@@ -77,7 +77,7 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
}, [keys, selectedIndex, onComplete])
useInput(
(_input, key) => {
(input, key) => {
if (key.escape) {
if (step === "confirm" && keys.length > 1) {
setStep("select")
-112
View File
@@ -1,112 +0,0 @@
import { render } from "ink-testing-library"
// biome-ignore lint/correctness/noUnusedImports: React must be in scope for JSX in this test file.
import React from "react"
import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock ink's useApp
const mockExit = vi.fn()
vi.mock("ink", async (importOriginal) => {
const actual = await importOriginal<typeof import("ink")>()
return {
...actual,
useApp: () => ({ exit: mockExit }),
}
})
// Mock child_process
vi.mock("child_process", () => ({
execSync: vi.fn().mockReturnValue(""),
exec: vi.fn(),
}))
// Mock dependencies
vi.mock("@/core/controller/slash/getAvailableSlashCommands", () => ({
getAvailableSlashCommands: vi.fn().mockResolvedValue({ commands: [] }),
}))
vi.mock("@/core/storage/StateManager", () => ({
StateManager: {
get: () => ({
getGlobalSettingsKey: vi.fn().mockReturnValue("act"),
getGlobalStateKey: vi.fn().mockReturnValue([]),
getApiConfiguration: vi.fn().mockReturnValue({}),
}),
},
}))
vi.mock("@/services/telemetry", () => ({
telemetryService: {
captureHostEvent: vi.fn(),
},
}))
vi.mock("@shared/services/Session", () => ({
Session: {
get: () => ({
getStats: vi.fn().mockReturnValue({}),
}),
},
}))
vi.mock("../context/TaskContext", () => ({
useTaskContext: () => ({
controller: {},
clearState: vi.fn(),
}),
useTaskState: () => ({
clineMessages: [],
}),
}))
vi.mock("../hooks/useStateSubscriber", () => ({
useIsSpinnerActive: () => ({ isActive: false, startTime: 0 }),
}))
import { ChatView } from "./ChatView"
// Helper to wait for async state updates
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
describe("Quit Command (/q and /exit)", () => {
const mockOnExit = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it("should exit the application when /q is selected from slash menu", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
await delay()
// Type /q
stdin.write("/q")
await delay()
// Press Enter
stdin.write("\r")
// handleExit has a 150ms timeout
await delay(200)
expect(mockExit).toHaveBeenCalled()
expect(mockOnExit).toHaveBeenCalled()
})
it("should exit the application when /exit is selected from slash menu", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
await delay()
// Type /exit
stdin.write("/exit")
await delay()
// Press Enter
stdin.write("\r")
// handleExit has a 150ms timeout
await delay(200)
expect(mockExit).toHaveBeenCalled()
expect(mockOnExit).toHaveBeenCalled()
})
})
+1 -1
View File
@@ -63,7 +63,7 @@ export function SearchableList<T extends SearchableListItem>({
// Reset index when search changes
useEffect(() => {
setIndex(0)
}, [])
}, [search])
useInput(
(input, key) => {
+6 -9
View File
@@ -237,7 +237,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
"not configured",
)
// Refresh trigger to force re-reading model IDs from state
const [_modelRefreshKey, setModelRefreshKey] = useState(0)
const [modelRefreshKey, setModelRefreshKey] = useState(0)
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
// OCA auth hook
@@ -273,7 +273,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey) as string) || "" : "",
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey) as string) || "" : "",
}
}, [stateManager])
}, [modelRefreshKey, stateManager])
// Toggle a feature setting
const toggleFeature = useCallback(
@@ -433,7 +433,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return () => {
cancelled = true
}
}, [isWaitingForClineAuth, controller, fetchAccountInfo, refreshModelIds])
}, [isWaitingForClineAuth, controller, fetchAccountInfo])
// Build items list based on current tab
const items: ListItem[] = useMemo(() => {
@@ -945,10 +945,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
actReasoningEffort,
planReasoningEffort,
rebuildTaskApi,
setReasoningEffortForMode, // Update telemetry providers to respect the new setting
controller,
handleTabChange,
provider,
setReasoningEffortForMode,
])
// Handle completion of the Bedrock custom ARN flow (ARN + base model selected)
@@ -1099,7 +1096,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setCodexAuthError(error instanceof Error ? error.message : String(error))
setIsWaitingForCodexAuth(false)
}
}, [controller, refreshModelIds])
}, [controller])
const handleProviderSelect = useCallback(
async (providerId: string) => {
@@ -1171,7 +1168,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setIsPickingProvider(false)
}
},
[stateManager, startCodexAuth, handleClineLogin, isOcaAuthenticated, controller, refreshModelIds],
[stateManager, startCodexAuth, handleClineLogin, startOcaAuth, isOcaAuthenticated, controller, refreshModelIds],
)
// Handle API key submission after provider selection
+1 -1
View File
@@ -248,7 +248,7 @@ const SkillRow: React.FC<{ skill: SkillInfo; isSelected: boolean }> = ({ skill,
{skill.description && (
<Box marginLeft={4}>
<Text color="gray">
{skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description}
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
</Text>
</Box>
)}
+1 -1
View File
@@ -24,7 +24,7 @@ export const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ commands, se
if (maxLength <= 0) return ""
if (text.length <= maxLength) return text
if (maxLength <= 3) return text.slice(0, maxLength)
return `${text.slice(0, maxLength - 3)}...`
return text.slice(0, maxLength - 3) + "..."
}
if (commands.length === 0) {
+2 -2
View File
@@ -50,7 +50,7 @@ function formatNumber(num: number): string {
/**
* Create a progress bar for context window usage
*/
function createContextBar(used: number, total: number, width = 8): string {
function createContextBar(used: number, total: number, width: number = 8): string {
const ratio = Math.min(used / total, 1)
const filled = Math.round(ratio * width)
const empty = width - filled
@@ -76,7 +76,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
const contextBar = createContextBar(totalTokens, contextWindowSize)
// Format model ID for display (shorten if needed)
const displayModel = modelId.length > 20 ? `${modelId.substring(0, 17)}...` : modelId
const displayModel = modelId.length > 20 ? modelId.substring(0, 17) + "..." : modelId
return (
<Box flexDirection="column">
+1 -1
View File
@@ -92,7 +92,7 @@ export const TaskJsonView: React.FC<TaskJsonViewProps> = ({ taskId: _taskId, ver
outputtedMessages.current.add(message.ts)
}
}, [state.clineMessages, verbose, getRole])
}, [state.clineMessages, verbose])
// Handle task completion
useEffect(() => {
+2 -2
View File
@@ -72,7 +72,7 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as string
return currentProvider || "cline"
}, [])
}, [controller])
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
@@ -165,7 +165,7 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
clearTimeout(r.searchTimeout)
}
}
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath, mentionInfo])
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
useInput(
(input, key) => {
+6 -12
View File
@@ -13,22 +13,16 @@ export interface FeaturedModel {
export const FEATURED_MODELS: { recommended: FeaturedModel[]; free: FeaturedModel[] } = {
recommended: [
{
id: "google/gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro Preview",
description: "Latest Gemini release with 1m ctx window and strong coding performance",
labels: ["NEW"],
},
{
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
description: "Latest Sonnet release with strong coding and agent performance",
labels: ["NEW"],
id: "anthropic/claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
description: "Best balance of speed, cost, and quality",
labels: ["BEST"],
},
{
id: "anthropic/claude-opus-4.6",
name: "Claude Opus 4.6",
description: "Most intelligent model for agents and coding",
labels: ["BEST"],
description: "State-of-the-art for complex coding",
labels: ["NEW"],
},
{
id: "openai/gpt-5.2-codex",
@@ -3,9 +3,14 @@
* Instead of rendering to a webview, this outputs to the terminal
*/
import type * as vscode from "vscode"
import { WebviewProvider } from "@/core/webview"
export class CliWebviewProvider extends WebviewProvider {
constructor(context: vscode.ExtensionContext) {
super(context)
}
override getWebviewUrl(path: string): string {
// CLI doesn't have webview URLs
return `file://${path}`
+2 -1
View File
@@ -79,7 +79,7 @@ export class CliDiffServiceClient implements DiffServiceClientInterface {
* CLI implementation of EnvService - handles environment operations
*/
export class CliEnvServiceClient implements EnvServiceClientInterface {
private clipboardContent = ""
private clipboardContent: string = ""
private getTelemetrySetting(): proto.host.Setting {
// Read from StateManager - defaults to ENABLED if not set or "unset"
@@ -182,6 +182,7 @@ export class CliWindowServiceClient implements WindowServiceClientInterface {
case proto.host.ShowMessageType.WARNING:
printWarning(message)
break
case proto.host.ShowMessageType.INFORMATION:
default:
printInfo(message)
break
+1 -1
View File
@@ -58,7 +58,7 @@ export const useCompletedAskMessages = () => {
*/
export const useLastCompletedAskMessage = () => {
const { state } = useTaskContext()
const _processed = useProcessedMessages()
const processed = useProcessedMessages()
const getLastCompletedAskMessage = useCallback((): ClineMessage | null => {
if (!state.clineMessages) {
+31 -5
View File
@@ -73,7 +73,24 @@ async function disposeTelemetryServices(): Promise<void> {
await Promise.allSettled([telemetryService.dispose(), PostHogClientProvider.getInstance().dispose()])
}
/**
* Restore yoloModeToggled to its original value from before this CLI session.
* This ensures the --yolo flag is session-only and doesn't leak into future runs.
* Must be called before flushPendingState so the restored value gets persisted.
*/
function restoreYoloState(): void {
if (savedYoloModeToggled !== null) {
try {
StateManager.get().setGlobalState("yoloModeToggled", savedYoloModeToggled)
savedYoloModeToggled = null
} catch {
// StateManager may not be initialized (e.g., early exit before init)
}
}
}
async function disposeCliContext(ctx: CliContext): Promise<void> {
restoreYoloState()
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
@@ -186,10 +203,12 @@ function applyTaskOptions(options: TaskOptions): void {
telemetryService.captureHostEvent("max_consecutive_mistakes_flag", String(maxConsecutiveMistakes))
}
// Set yolo mode as a session-scoped override so AutoApprove picks it up,
// but it is never persisted to disk (setSessionOverride never touches pendingGlobalState).
// Override yolo mode only if --yolo flag is explicitly passed.
// The original value is saved in initializeCli and restored on exit.
if (options.yolo) {
StateManager.get().setSessionOverride("yoloModeToggled", true)
const state = StateManager.get()
savedYoloModeToggled = state.getGlobalSettingsKey("yoloModeToggled") ?? false
state.setGlobalState("yoloModeToggled", true)
telemetryService.captureHostEvent("yolo_flag", "true")
}
@@ -294,6 +313,9 @@ let activeContext: CliContext | null = null
let isShuttingDown = false
// Track if we're in plain text mode (no Ink UI) - set by runTask when piped stdin detected
let isPlainTextMode = false
// Track the original yoloModeToggled value from before this CLI session so we can restore it on exit.
// The --yolo flag should only affect the current invocation, not persist across runs.
let savedYoloModeToggled: boolean | null = null
/**
* Wait for stdout to fully drain before exiting.
@@ -335,6 +357,10 @@ function setupSignalHandlers() {
printWarning(`${signal} received, shutting down...`)
try {
// Restore yolo state before any cleanup - this is idempotent and safe
// even if disposeCliContext also calls it (restoreYoloState checks savedYoloModeToggled !== null)
restoreYoloState()
if (activeContext) {
const task = activeContext.controller.task
if (task) {
@@ -797,7 +823,7 @@ devCommand
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
* and sets the flag accordingly.
*/
export async function isAuthConfigured(): Promise<boolean> {
async function isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
// Check welcomeViewCompleted first - this is the single source of truth
@@ -826,7 +852,7 @@ async function checkAnyProviderConfigured(): Promise<boolean> {
const config = stateManager.getApiConfiguration() as Record<string, unknown>
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
if (config.clineApiKey || config["cline:clineAccountId"]) return true
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
if (config["openai-codex-oauth-credentials"]) return true
+46 -45
View File
@@ -112,48 +112,49 @@ function getMessageIcon(message: ClineMessage): string {
default:
return "❔"
}
}
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️ "
default:
return " "
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️ "
default:
return " "
}
}
}
/**
* Format a ClineMessage for terminal display
*/
export function formatMessage(message: ClineMessage, verbose = false): string {
export function formatMessage(message: ClineMessage, verbose: boolean = false): string {
const icon = getMessageIcon(message)
const timestamp = formatTimestamp(message.ts)
const lines: string[] = []
@@ -243,7 +244,7 @@ function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolea
case "command_output":
const output = message.text || ""
const truncated = output.length > 500 ? `${output.substring(0, 500)}...` : output
const truncated = output.length > 500 ? output.substring(0, 500) + "..." : output
return `${prefix} ${style.dim("Output:")} ${truncated}`
case "tool":
@@ -288,7 +289,7 @@ function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolea
/**
* Display a horizontal separator
*/
export function separator(char = "─", width = 60): string {
export function separator(char: string = "─", width: number = 60): string {
return style.dim(char.repeat(width))
}
@@ -308,7 +309,7 @@ export function taskHeader(taskId: string, task?: string): string {
/**
* Format the current state for display
*/
export function formatState(state: ExtensionState, verbose = false): string {
export function formatState(state: ExtensionState, verbose: boolean = false): string {
const lines: string[] = []
if (state.currentTaskItem) {
@@ -319,7 +320,7 @@ export function formatState(state: ExtensionState, verbose = false): string {
if (state.clineMessages && state.clineMessages.length > 0) {
const messagesToShow = verbose
? state.clineMessages
: state.clineMessages.filter((_m) => {
: state.clineMessages.filter((m) => {
// Filter out noisy messages in non-verbose mode
// if (m.say === "api_req_started" || m.say === "api_req_finished") return false
return true
@@ -343,7 +344,7 @@ export class Spinner {
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
private frameIndex = 0
private interval: NodeJS.Timeout | null = null
private message = ""
private message: string = ""
start(message: string) {
this.message = message
@@ -366,7 +367,7 @@ export class Spinner {
if (finalMessage) {
process.stdout.write(`\r${style.success("✓")} ${finalMessage}\n`)
} else {
process.stdout.write(`\r${" ".repeat(this.message.length + 4)}\r`)
process.stdout.write("\r" + " ".repeat(this.message.length + 4) + "\r")
}
}
@@ -391,7 +392,7 @@ export function clearLine() {
/**
* Move cursor up n lines
*/
export function cursorUp(n = 1) {
export function cursorUp(n: number = 1) {
process.stdout.write(`\x1b[${n}A`)
}
@@ -443,7 +444,7 @@ export async function promptUser(question: string): Promise<string> {
})
return new Promise((resolve) => {
rl.question(`${style.info(question)} `, (answer: string) => {
rl.question(style.info(question) + " ", (answer: string) => {
rl.close()
resolve(answer.trim())
})
@@ -465,7 +466,7 @@ export async function promptConfirmation(question: string): Promise<boolean> {
export function setTerminalTitle(title: string): void {
if (process.stdout.isTTY) {
const maxLength = 80
const truncated = title.length > maxLength ? `${title.slice(0, maxLength)}...` : title
const truncated = title.length > maxLength ? title.slice(0, maxLength) + "..." : title
process.stdout.write(`\x1b]0;${truncated}\x07`)
}
}
+3 -3
View File
@@ -183,9 +183,9 @@ export async function listWorkspaceFiles(workspacePath: string, limit = 5000): P
function countGaps(positions: Iterable<number>): number {
let gaps = 0
let prev = Number.NEGATIVE_INFINITY
let prev = -Infinity
for (const pos of positions) {
if (prev !== Number.NEGATIVE_INFINITY && pos - prev > 1) {
if (prev !== -Infinity && pos - prev > 1) {
gaps++
}
prev = pos
@@ -255,5 +255,5 @@ export function insertMention(text: string, atIndex: number, filePath: string):
// Ensure path starts with / for proper mention format
const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`
const mention = normalizedPath.includes(" ") ? `@"${normalizedPath}"` : `@${normalizedPath}`
return `${text.slice(0, atIndex) + mention} ${text.slice(end).trimStart()}`
return text.slice(0, atIndex) + mention + " " + text.slice(end).trimStart()
}
+4 -4
View File
@@ -87,7 +87,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
// JSON mode: stream all messages to stdout (existing behavior)
if (jsonOutput) {
process.stdout.write(`${JSON.stringify(message)}\n`)
process.stdout.write(JSON.stringify(message) + "\n")
} else {
handleMessageForPipeMode(message, verbose || false)
}
@@ -119,7 +119,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
} catch (error) {
if (jsonOutput) {
process.stdout.write(
`${JSON.stringify({ type: "error", message: error instanceof Error ? error.message : String(error) })}\n`,
JSON.stringify({ type: "error", message: error instanceof Error ? error.message : String(error) }) + "\n",
)
} else {
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`)
@@ -164,7 +164,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error)
if (jsonOutput) {
process.stdout.write(`${JSON.stringify({ type: "error", message: errMsg })}\n`)
process.stdout.write(JSON.stringify({ type: "error", message: errMsg }) + "\n")
} else {
process.stderr.write(`Error: ${errMsg}\n`)
}
@@ -180,7 +180,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
.sort(([aTs], [bTs]) => aTs - bTs)
.map(([_, msg]) => msg)
.at(-1)
process.stdout.write(`${msg}\n`)
process.stdout.write(msg + "\n")
}
return !hasError
+11 -11
View File
@@ -76,30 +76,30 @@ export function printSessionSummary(): void {
"├─────────────────────────────────────────────────────────┤",
`${GRAY}Session ID:${RESET} ${stats.sessionId.padEnd(42)}`,
`${GRAY}Session Time:${RESET} ${sessionTimeStr.padEnd(42)}`,
`${`${GRAY}Tool Calls:${RESET} ${stats.totalToolCalls} ( ${GREEN}${stats.successfulToolCalls}${RESET} ${RED}${stats.failedToolCalls}${RESET} )`.padEnd(
`${GRAY}Tool Calls:${RESET} ${stats.totalToolCalls} ( ${GREEN}${stats.successfulToolCalls}${RESET} ${RED}${stats.failedToolCalls}${RESET} )`.padEnd(
70,
)}`,
`${`${GRAY}Success Rate:${RESET} ${session.getSuccessRate().toFixed(1)}%`.padEnd(60)}`,
) + "│",
`${GRAY}Success Rate:${RESET} ${session.getSuccessRate().toFixed(1)}%`.padEnd(60) + "│",
"├─────────────────────────────────────────────────────────┤",
`${BOLD}Performance${RESET}`,
`${GRAY}Wall Time:${RESET} ${formatDuration(wallTimeMs).padEnd(42)}`,
`${GRAY}Agent Active:${RESET} ${formatDuration(agentActiveMs).padEnd(42)}`,
`${`${GRAY} » API Time:${RESET} ${formatDuration(stats.apiTimeMs)} ${GRAY}(${formatPercent(stats.apiTimeMs, agentActiveMs)})${RESET}`.padEnd(
`${GRAY} » API Time:${RESET} ${formatDuration(stats.apiTimeMs)} ${GRAY}(${formatPercent(stats.apiTimeMs, agentActiveMs)})${RESET}`.padEnd(
60,
)}`,
`${`${GRAY} » Tool Time:${RESET} ${formatDuration(stats.toolTimeMs)} ${GRAY}(${formatPercent(stats.toolTimeMs, agentActiveMs)})${RESET}`.padEnd(
) + "│",
`${GRAY} » Tool Time:${RESET} ${formatDuration(stats.toolTimeMs)} ${GRAY}(${formatPercent(stats.toolTimeMs, agentActiveMs)})${RESET}`.padEnd(
60,
)}`,
) + "│",
"├─────────────────────────────────────────────────────────┤",
`${BOLD}Resources${RESET}`,
`${GRAY}Memory (RSS):${RESET} ${formatBytes(stats.resources.rss).padEnd(42)}`,
`${GRAY}Peak Memory:${RESET} ${formatBytes(stats.peakMemoryBytes).padEnd(42)}`,
`${`${GRAY}Heap Used:${RESET} ${formatBytes(stats.resources.heapUsed)} ${GRAY}/ ${formatBytes(stats.resources.heapTotal)}${RESET}`.padEnd(
`${GRAY}Heap Used:${RESET} ${formatBytes(stats.resources.heapUsed)} ${GRAY}/ ${formatBytes(stats.resources.heapTotal)}${RESET}`.padEnd(
60,
)}`,
`${`${GRAY}CPU Time:${RESET} ${formatDuration(stats.resources.userCpuMs + stats.resources.systemCpuMs)} ${GRAY}(user: ${formatDuration(stats.resources.userCpuMs)}, sys: ${formatDuration(stats.resources.systemCpuMs)})${RESET}`.padEnd(
) + "│",
`${GRAY}CPU Time:${RESET} ${formatDuration(stats.resources.userCpuMs + stats.resources.systemCpuMs)} ${GRAY}(user: ${formatDuration(stats.resources.userCpuMs)}, sys: ${formatDuration(stats.resources.systemCpuMs)})${RESET}`.padEnd(
60,
)}`,
) + "│",
"└─────────────────────────────────────────────────────────┘",
"",
]
+1 -1
View File
@@ -22,7 +22,7 @@ export interface VisibleWindow<T> {
* Centers the selected item in the visible window when possible.
* Returns the visible items and the start index for selection tracking.
*/
export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisible = 5): VisibleWindow<T> {
export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisible: number = 5): VisibleWindow<T> {
if (items.length <= maxVisible) {
return { items, startIndex: 0 }
}
+1 -1
View File
@@ -1,6 +1,6 @@
export function emitTaskStartedMessage(taskId: string, jsonOutput: boolean): void {
if (jsonOutput) {
process.stdout.write(`${JSON.stringify({ type: "task_started", taskId })}\n`)
process.stdout.write(JSON.stringify({ type: "task_started", taskId }) + "\n")
return
}
+1 -1
View File
@@ -10,7 +10,7 @@
export async function waitFor<T>(
condition: () => T | undefined | null,
timeoutMs: number,
pollIntervalMs = 100,
pollIntervalMs: number = 100,
): Promise<T | undefined> {
// Check immediately first
const immediate = condition()
+1 -1
View File
@@ -259,7 +259,7 @@ function parseVersion(version: string): ParsedVersion {
return {
base: nightlyMatch[1].split(".").map(Number),
isNightly: true,
timestamp: Number.parseInt(nightlyMatch[2], 10),
timestamp: parseInt(nightlyMatch[2], 10),
}
}
return {
-42
View File
@@ -73,8 +73,6 @@ Cline stores configuration in `~/.cline/data/`:
├── data/ # Configuration directory
│ ├── globalState.json # Global settings
│ ├── secrets.json # API keys (encrypted)
│ ├── settings/ # Settings files
│ │ └── cline_mcp_settings.json # MCP server configuration
│ ├── workspace/ # Workspace-specific state
│ └── tasks/ # Task history and data
└── log/ # Log files
@@ -174,46 +172,6 @@ cline --config ~/.cline-work "review this PR"
cline --config ~/.cline-personal "help me with this side project"
```
## MCP Server Configuration
Cline CLI supports [MCP (Model Context Protocol)](/mcp/mcp-overview) servers, giving you access to external tools and data sources directly from the terminal. The CLI uses the same MCP configuration format as the VS Code extension.
### Setting Up MCP Servers
To configure MCP servers for the CLI, create or edit the settings file at:
```
~/.cline/data/settings/cline_mcp_settings.json
```
The file uses the same JSON format as the VS Code extension:
```json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"],
"env": {
"API_KEY": "your_api_key"
},
"alwaysAllow": ["tool1", "tool2"],
"disabled": false
}
}
}
```
For the full configuration reference including STDIO and SSE transport types, see [Adding and Configuring MCP Servers](/mcp/adding-and-configuring-servers).
<Note>
The CLI does not yet have a `/mcp` slash command for managing MCP servers interactively. For now, you'll need to edit the `cline_mcp_settings.json` file directly.
</Note>
### Custom Config Directory
If you use the `CLINE_DIR` environment variable or `--config` flag, the MCP settings file will be located at `<your-config-dir>/data/settings/cline_mcp_settings.json` instead.
## Configuration for Local Providers
### Ollama
-9
View File
@@ -207,15 +207,6 @@ Chains multiple Cline invocations together for creative multi-step workflows.
| Session summary | ✓ | - |
| JSON output | - | `--json` |
| Piped input | - | ✓ |
| [MCP servers](/cline-cli/configuration#mcp-server-configuration) | ✓ | ✓ |
## MCP Server Support
Cline CLI supports [MCP (Model Context Protocol)](/mcp/mcp-overview) servers, the same extensibility system available in the VS Code extension. MCP servers give Cline access to external tools and data sources, from databases and APIs to browser automation and project management.
To use MCP servers with the CLI, add your server configuration to `~/.cline/data/settings/cline_mcp_settings.json`. The format is identical to the VS Code extension.
[Configure MCP servers for the CLI →](/cline-cli/configuration#mcp-server-configuration)
## Learn More
-2
View File
@@ -70,8 +70,6 @@ Cline does not come with any pre-installed MCP servers. You'll need to find and
## Integration with Cline
MCP servers work with both the **Cline VS Code extension** and the **[Cline CLI](/cline-cli/overview)**. If you use the CLI, see [MCP Server Configuration for the CLI](/cline-cli/configuration#mcp-server-configuration) to get set up.
Cline simplifies the building and use of MCP servers through its AI capabilities.
### Building MCP Servers
+1 -1
View File
@@ -19,7 +19,7 @@ Google Gemini is Google's family of multimodal AI models, offering some of the l
Cline supports the following Google Gemini models:
#### Gemini 3 Series (Latest)
- `gemini-3.1-pro-preview` (Default) - Latest pro model with 1M context, thinking support, and tiered pricing ($2.00-$4.00/M input)
- `gemini-3-pro-preview` (Default) - Latest pro model with 1M context, thinking support, and tiered pricing ($2.00-$4.00/M input)
- `gemini-3-flash-preview` - Fast model with 1M context and thinking level support ($0.30-$0.50/M input)
#### Gemini 2.5 Series
+23
View File
@@ -14,6 +14,29 @@ SambaNova provides fast AI inference on custom-built hardware, hosting popular o
3. **Create a Key:** Generate a new API key.
4. **Copy the Key:** Copy the API key immediately and store it securely.
### Supported Models
Cline supports the following SambaNova models:
#### Meta Llama Models
- `Llama-4-Maverick-17B-128E-Instruct` - Llama 4 Maverick with vision support ($0.63/$1.80 per 1M tokens)
- `Llama-4-Scout-17B-16E-Instruct` - Llama 4 Scout ($0.40/$0.70 per 1M tokens)
- `Meta-Llama-3.3-70B-Instruct` (Default) - Versatile 70B model with 128K context ($0.60/$1.20 per 1M tokens)
- `Meta-Llama-3.1-405B-Instruct` - Largest Llama model ($5.00/$10.00 per 1M tokens)
- `Meta-Llama-3.1-8B-Instruct` - Compact 8B model ($0.10/$0.20 per 1M tokens)
- `Meta-Llama-3.2-1B-Instruct` - Ultra-compact 1B model ($0.04/$0.08 per 1M tokens)
- `Meta-Llama-3.2-3B-Instruct` - Small 3B model ($0.08/$0.16 per 1M tokens)
#### DeepSeek Models
- `DeepSeek-R1` - Reasoning model ($5.00/$7.00 per 1M tokens)
- `DeepSeek-R1-Distill-Llama-70B` - Distilled reasoning model ($0.70/$1.40 per 1M tokens)
- `DeepSeek-V3-0324` - General-purpose model ($3.00/$4.50 per 1M tokens)
- `DeepSeek-V3.1` - Latest DeepSeek with hybrid reasoning ($3.00/$4.50 per 1M tokens)
#### Qwen Models
- `Qwen3-32B` - Dense 32B model ($0.40/$0.80 per 1M tokens)
- `QwQ-32B` - Reasoning-focused Qwen model ($0.50/$1.00 per 1M tokens)
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
+9 -7
View File
@@ -14,7 +14,8 @@ td,
th,
span:not(code *),
div:not(code *):not(pre *) {
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-family:
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
}
/* Ensure code blocks use Geist Mono */
@@ -24,12 +25,12 @@ pre,
pre code,
code *,
pre * {
font-family: "Geist Mono", "Monaco", "Courier New", monospace;
font-family: "Geist Mono", "Monaco", "Courier New", monospace !important;
}
/* Make h1 titles lighter in font weight */
h1 {
font-weight: 600;
font-weight: 600 !important;
}
/* Keep headings and images at full opacity */
@@ -40,8 +41,9 @@ h4,
h5,
h6,
img {
opacity: 1;
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
opacity: 1 !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 */
@@ -49,7 +51,7 @@ img {
.markdown h1,
article h1,
main h1 {
font-weight: 500;
font-weight: 500 !important;
}
/* JetBrains logo visibility fix for dark mode */
@@ -87,5 +89,5 @@ img[alt="JetBrains logo"]:hover {
/* Reduce list margin-top */
.steps {
margin-top: 5px;
margin-top: 5px !important;
}
+1 -1
View File
@@ -6,7 +6,7 @@ import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const production = process.argv.includes("--production") || process.env.IS_DEBUG_BUILD === "false"
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.66.0",
"version": "3.65.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.66.0",
"version": "3.65.0",
"license": "Apache-2.0",
"workspaces": [
".",
+3 -8
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.66.0",
"version": "3.65.0",
"icon": "assets/icons/icon.png",
"workspaces": [
".",
@@ -414,9 +414,8 @@
"lint:proto": "bash ./scripts/proto-lint.sh",
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
"format:all": "biome check --files-ignore-unknown=true --diagnostic-level=error",
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
"ci:check-all": "npx npm-run-all -p check-types lint format:all",
"ci:check-all": "npx npm-run-all -p check-types lint format",
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
"test": "npx npm-run-all test:unit test:integration",
@@ -445,11 +444,7 @@
"docs:rename-file": "cd docs && npm run rename",
"report-issue": "node scripts/report-issue.js",
"storybook": "cd webview-ui && npm run storybook",
"cli:unlink": "cd cli && npm run unlink",
"eval:smoke:build": "npm run cli:build && npm run cli:link",
"eval:smoke:run": "npx tsx evals/smoke-tests/run-smoke-tests.ts",
"eval:smoke": "npm run eval:smoke:build && npm run eval:smoke:run",
"eval:smoke:ci": "npm run eval:smoke:build && npm run eval:smoke:run -- --trials 1 --parallel"
"cli:unlink": "cd cli && npm run unlink"
},
"lint-staged": {
"src/shared/storage/state-keys.ts": [
-15
View File
@@ -19,8 +19,6 @@ service ModelsService {
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns recommended and free Cline models
rpc refreshClineRecommendedModelsRpc(EmptyRequest) returns (ClineRecommendedModelsResponse);
// Refreshes and returns Hugging Face models
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
@@ -115,18 +113,6 @@ message OpenRouterCompatibleModelInfo {
map<string, OpenRouterModelInfo> models = 1;
}
message ClineRecommendedModel {
string id = 1;
string name = 2;
string description = 3;
repeated string tags = 4;
}
message ClineRecommendedModelsResponse {
repeated ClineRecommendedModel recommended = 1;
repeated ClineRecommendedModel free = 2;
}
// Request for fetching OpenAI models
message OpenAiModelsRequest {
Metadata metadata = 1;
@@ -462,7 +448,6 @@ enum ApiFormat {
OPENAI_CHAT = 2;
R1_CHAT = 3;
OPENAI_RESPONSES = 4;
OPENAI_RESPONSES_WEBSOCKET_MODE = 5;
}
// Model info for OpenAI-compatible models
+1 -1
View File
@@ -104,7 +104,7 @@ message Secrets {
optional string oca_refresh_token = 42;
optional string mcp_o_auth_secrets = 43;
optional string cline_api_key = 44;
optional string openai_codex_oauth_credentials = 48;
optional string openai_codex_oauth_credentials = 47;
}
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
+5 -5
View File
@@ -241,7 +241,7 @@ function normalizeProviderName(providerPart) {
function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) {
// Special case 1: Bedrock needs AWS fields (if not already assigned)
const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"]
const bedrockFields = providerApiKeyMap.bedrock || []
const bedrockFields = providerApiKeyMap["bedrock"] || []
for (const field of awsFields) {
if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) {
@@ -257,19 +257,19 @@ function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedF
}
if (bedrockFields.length > 0) {
providerApiKeyMap.bedrock = bedrockFields
providerApiKeyMap["bedrock"] = bedrockFields
}
// Special case 2: Vertex needs project ID and region
if (providerApiKeyMap.vertex) {
if (providerApiKeyMap["vertex"]) {
// Vertex typically uses application default credentials,
// but requires project ID and region configuration
// These are already captured if they exist in ApiHandlerSecrets
}
// Special case 3: SAP AI Core multi-key authentication
if (providerApiKeyMap.sapaicore) {
const sapFields = providerApiKeyMap.sapaicore
if (providerApiKeyMap["sapaicore"]) {
const sapFields = providerApiKeyMap["sapaicore"]
const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"]
for (const field of requiredSapFields) {
+4 -3
View File
@@ -106,7 +106,7 @@ async function parseServicesWithFiles(protoDir, protoFiles) {
return services
}
function _upperFirst(s) {
function upperFirst(s) {
return s.length ? s[0].toUpperCase() + s.slice(1) : s
}
@@ -273,8 +273,8 @@ async function generateServiceClientsPy(outDir, services) {
:return: iterator of ${aliasPb2}.${respTypeName}
"""
return self._stub.${m.name}(req)`
}
return `
} else {
return `
def ${m.name}(self, req):
"""
Unary RPC.
@@ -282,6 +282,7 @@ async function generateServiceClientsPy(outDir, services) {
:return: ${aliasPb2}.${respTypeName}
"""
return self._stub.${m.name}(req)`
}
})
.join("\n")
+1 -1
View File
@@ -229,7 +229,7 @@ async function main() {
}
// Print summary
console.log(`\n${"=".repeat(50)}`)
console.log("\n" + "=".repeat(50))
console.log("📊 Summary:")
console.log("=".repeat(50))
+6 -4
View File
@@ -125,9 +125,9 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
return this.makeRequest((client) => client.${methodName}(request))
}`
}
// Generate streaming method
return ` ${methodName}(
} else {
// Generate streaming method
return ` ${methodName}(
request: ${requestType},
callbacks: StreamingCallbacks<${responseType}>,
): () => void {
@@ -150,6 +150,7 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
abortController.abort()
}
}\n`
}
})
.join("\n")
@@ -221,8 +222,9 @@ function generateVscodeClientImplementation(serviceName, serviceDefinition) {
const isStreamingResponse = methodDef.responseStream
if (!isStreamingResponse) {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
} else {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
}
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
})
.join("\n")
+2 -2
View File
@@ -157,8 +157,8 @@ async function generateStandaloneProtobusServiceSetup(protobusServices) {
handlerSetup.push(` server.addService(cline.${name}Service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
const requestType = `cline.${rpc.requestType.type.name}`
const responseType = `cline.${rpc.responseType.type.name}`
const requestType = "cline." + rpc.requestType.type.name
const responseType = "cline." + rpc.responseType.type.name
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
}
+1 -1
View File
@@ -357,7 +357,7 @@ function replaceMessage(protoContent, messageName, newMessageContent) {
}
// Message doesn't exist, append before the first message or at end
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
return `${protoContent}\n\n${newMessageContent}`
return protoContent + "\n\n" + newMessageContent
}
async function main() {
-136
View File
@@ -56,7 +56,6 @@ const log = {
const config = {
// The name and display name for the nightly version
nightlyName: "cline-nightly",
originalName: "claude-dev",
nightlyDisplayName: "Cline (Nightly)",
projectRoot: path.join(__dirname, ".."),
get packageJsonPath() {
@@ -71,15 +70,6 @@ const config = {
get vsixPath() {
return path.join(this.distDir, "cline-nightly.vsix")
},
get nodeModulesPath() {
return path.join(this.projectRoot, "node_modules")
},
get originalWorkspaceLinkPath() {
return path.join(this.nodeModulesPath, this.originalName)
},
get nightlyWorkspaceLinkPath() {
return path.join(this.nodeModulesPath, this.nightlyName)
},
}
// Utility class for managing the publish process
@@ -87,31 +77,6 @@ class NightlyPublisher {
constructor() {
this.originalPackageJson = null
this.hasBackup = false
this.didRenameWorkspaceLink = false
this.didCreateNightlyWorkspaceLink = false
}
/**
* Resolve symlink target to an absolute path.
*/
resolveSymlinkTarget(linkPath) {
const target = fs.readlinkSync(linkPath)
return path.resolve(path.dirname(linkPath), target)
}
/**
* Validate that a path is the expected workspace self-link to project root.
*/
isExpectedWorkspaceSelfLink(linkPath) {
try {
if (!fs.lstatSync(linkPath).isSymbolicLink()) {
return false
}
return this.resolveSymlinkTarget(linkPath) === path.resolve(config.projectRoot)
} catch {
return false
}
}
/**
@@ -180,98 +145,6 @@ class NightlyPublisher {
}
}
/**
* Keep workspace self-link consistent with package name during nightly packaging.
*
* The repo root is a workspace package ("."). When npm installs dependencies,
* it creates a self-link at node_modules/<package-name>. Nightly packaging
* changes package.json name from "claude-dev" to "cline-nightly". If we don't
* align this link, vsce's dependency detection (`npm list --production`) fails
* with ELSPROBLEMS (missing cline-nightly + extraneous claude-dev).
*/
reconcileWorkspaceSelfLinkForNightly() {
const originalPath = config.originalWorkspaceLinkPath
const nightlyPath = config.nightlyWorkspaceLinkPath
if (!fs.existsSync(config.nodeModulesPath)) {
log.warn("node_modules not found, skipping workspace self-link reconciliation")
return
}
if (fs.existsSync(nightlyPath)) {
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
throw new Error(
`Refusing to continue: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
)
}
log.info("Nightly workspace self-link already exists")
return
}
if (fs.existsSync(originalPath)) {
if (!this.isExpectedWorkspaceSelfLink(originalPath)) {
throw new Error(
`Refusing to continue: unexpected path at ${originalPath}. Expected a workspace symlink to ${config.projectRoot}`,
)
}
log.info(`Renaming workspace self-link: ${config.originalName} -> ${config.nightlyName}`)
fs.renameSync(originalPath, nightlyPath)
this.didRenameWorkspaceLink = true
return
}
// In some environments npm may not have created the workspace self-link yet.
// Create it explicitly so `npm list --production` can resolve the renamed
// package name during vsce dependency detection.
log.warn("Original workspace self-link not found, creating nightly workspace self-link")
fs.symlinkSync(config.projectRoot, nightlyPath, "dir")
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
throw new Error(`Failed to create expected workspace symlink at ${nightlyPath}`)
}
this.didCreateNightlyWorkspaceLink = true
}
/**
* Restore workspace self-link after packaging.
*/
restoreWorkspaceSelfLink() {
if (!this.didRenameWorkspaceLink && !this.didCreateNightlyWorkspaceLink) {
return
}
const originalPath = config.originalWorkspaceLinkPath
const nightlyPath = config.nightlyWorkspaceLinkPath
if (fs.existsSync(nightlyPath) && !fs.existsSync(originalPath)) {
if (this.didRenameWorkspaceLink) {
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
throw new Error(
`Refusing to restore: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
)
}
log.info(`Restoring workspace self-link: ${config.nightlyName} -> ${config.originalName}`)
fs.renameSync(nightlyPath, originalPath)
} else if (this.didCreateNightlyWorkspaceLink) {
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
throw new Error(
`Refusing to remove: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
)
}
log.info(`Removing temporary workspace self-link: ${config.nightlyName}`)
fs.unlinkSync(nightlyPath)
}
}
this.didRenameWorkspaceLink = false
this.didCreateNightlyWorkspaceLink = false
}
/**
* Generate new version with timestamp
* Format: major.minor.timestamp
@@ -427,9 +300,6 @@ class NightlyPublisher {
// Step 3: Update package.json
const newVersion = this.updatePackageJson()
// Step 3.5: Keep npm workspace self-link aligned with nightly package name
this.reconcileWorkspaceSelfLinkForNightly()
// Step 4: Package extension
this.packageExtension()
@@ -456,9 +326,6 @@ class NightlyPublisher {
log.error(`Publish failed: ${error.message}`)
process.exit(1)
} finally {
// Always restore workspace link first
this.restoreWorkspaceSelfLink()
// Always restore package.json
this.restorePackageJson()
}
@@ -469,20 +336,17 @@ class NightlyPublisher {
const publisher = new NightlyPublisher()
process.on("exit", () => {
publisher.restoreWorkspaceSelfLink()
publisher.restorePackageJson()
})
process.on("SIGINT", () => {
log.info("\nInterrupted, cleaning up...")
publisher.restoreWorkspaceSelfLink()
publisher.restorePackageJson()
process.exit(130)
})
process.on("SIGTERM", () => {
log.info("\nTerminated, cleaning up...")
publisher.restoreWorkspaceSelfLink()
publisher.restorePackageJson()
process.exit(143)
})
+1 -1
View File
@@ -26,7 +26,7 @@ const collectSystemInfo = () => {
if (process.platform === "darwin") {
cpuInfo = execSync("sysctl -n machdep.cpu.brand_string").toString().trim()
memoryInfo = execSync("sysctl -n hw.memsize").toString().trim()
memoryInfo = `${Math.round(Number.parseInt(memoryInfo, 10) / 1e9)} GB RAM`
memoryInfo = `${Math.round(parseInt(memoryInfo) / 1e9)} GB RAM`
} else {
// Linux specific commands
cpuInfo = execSync("lscpu").toString().split("\n").slice(0, 5).join("\n")
+2 -2
View File
@@ -70,7 +70,7 @@ function createMockService<T extends grpc.UntypedServiceImplementation>(serviceN
case "getMachineId":
callback(null, {
value: `fake-machine-id-${os.hostname()}`,
value: "fake-machine-id-" + os.hostname(),
})
return
@@ -103,7 +103,7 @@ function createMockService<T extends grpc.UntypedServiceImplementation>(serviceN
case "openDiff":
callback(null, {
diff_id: `fake-diff-${Date.now()}`,
diff_id: "fake-diff-" + Date.now(),
})
return
+2 -2
View File
@@ -204,8 +204,8 @@ async function main() {
const inputPath = args._[0]
const count = Number(args.count)
showServerLogs = Boolean(args["server-logs"])
fix = Boolean(args.fix)
coverage = Boolean(args.coverage)
fix = Boolean(args["fix"])
coverage = Boolean(args["coverage"])
if (!inputPath) {
console.error(
+7 -5
View File
@@ -9,7 +9,7 @@ import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from ".
describe("ClineEndpoint configuration", () => {
let sandbox: sinon.SinonSandbox
let tempDir: string
let _originalHomedir: typeof os.homedir
let originalHomedir: typeof os.homedir
beforeEach(async () => {
sandbox = sinon.createSandbox()
@@ -20,8 +20,10 @@ describe("ClineEndpoint configuration", () => {
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
// Stub os.homedir to return our temp directory
_originalHomedir = os.homedir
sandbox.stub(os, "homedir").returns(tempDir)
originalHomedir = os.homedir
sandbox
.stub(os, "homedir")
.returns(tempDir)
// Reset the singleton state using internal method
;(ClineEndpoint as any)._instance = null
@@ -541,7 +543,7 @@ describe("ClineEndpoint configuration", () => {
describe("bundled endpoints.json behavior", () => {
let bundledDir: string
let _setVscodeHostProviderMock: (mock: { extensionFsPath: string; globalStorageFsPath: string }) => void
let setVscodeHostProviderMock: (mock: { extensionFsPath: string; globalStorageFsPath: string }) => void
beforeEach(async () => {
// Create a separate directory for bundled config
@@ -550,7 +552,7 @@ describe("ClineEndpoint configuration", () => {
// Import HostProvider utilities
const hostProviderModule = await import("../test/host-provider-test-utils")
_setVscodeHostProviderMock = hostProviderModule.setVscodeHostProviderMock
setVscodeHostProviderMock = hostProviderModule.setVscodeHostProviderMock
})
afterEach(async () => {
+3
View File
@@ -1,3 +1,4 @@
import type * as vscode from "vscode"
import { WebviewProvider } from "./core/webview"
import "./utils/path" // necessary to have access to String.prototype.toPosix
@@ -24,6 +25,8 @@ import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
import { getLatestAnnouncementId } from "./utils/announcements"
import { arePathsEqual } from "./utils/path"
type SlimExtensionContext = Omit<vscode.ExtensionContext, "globalState" | "secrets" | "workspaceState">
/**
* Performs intialization for Cline that is common to all platforms.
*
+1 -1
View File
@@ -36,7 +36,7 @@ class ClineEndpoint {
private onPremiseConfig: EndpointsFileSchema | null = null
private environment: Environment = Environment.production
// Track if config came from bundled file (enterprise distribution)
private isBundled = false
private isBundled: boolean = false
private constructor() {
// Set environment at module load. Use override if provided.
+2 -1
View File
@@ -585,8 +585,9 @@ function reconstructWriteToFileResult(block: any, originalToolName: string, orig
// If no final_file_content found, create a simple success message
if (originalToolName === "write_to_file") {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully saved to ${filePath}.\n\nThe file has been created/updated with the new content.`
} else {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified.`
}
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified.`
}
const finalContent = finalContentMatch[2]
@@ -1083,7 +1083,8 @@ describe("AwsBedrockHandler", () => {
// Capture the command passed to executeConverseStream
let capturedCommand: any = null
handler["executeConverseStream"] = async function* (command: any, _modelInfo: any) {
const originalExecuteConverseStream = handler["executeConverseStream"].bind(handler)
handler["executeConverseStream"] = async function* (command: any, modelInfo: any) {
capturedCommand = command
// Yield nothing — we just want to capture the command
}
@@ -1,59 +0,0 @@
import "should"
import { openRouterDefaultModelInfo } from "@shared/api"
import sinon from "sinon"
import { ClineHandler } from "../cline"
describe("ClineHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: any[] = []) => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
it("should handle usage-only chunks when delta is missing", async () => {
const handler = Object.create(ClineHandler.prototype) as ClineHandler
;(handler as any).options = {}
const fakeClient = {
chat: {
completions: {
create: sinon.stub().resolves(
createAsyncIterable([
{
choices: [{}],
usage: {
prompt_tokens: 17,
completion_tokens: 9,
},
},
]),
),
},
},
}
sinon.stub(handler as any, "ensureClient").resolves(fakeClient as any)
sinon.stub(handler, "getModel").returns({
id: "openai/gpt-4o-mini",
info: openRouterDefaultModelInfo,
})
const chunks: any[] = []
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
chunks.push(chunk)
}
chunks.should.deepEqual([
{
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: 0,
inputTokens: 17,
outputTokens: 9,
totalCost: 0,
},
])
})
})
@@ -1,55 +0,0 @@
import "should"
import sinon from "sinon"
import { FireworksHandler } from "../fireworks"
describe("FireworksHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: any[] = []) => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
it("should handle usage-only chunks when delta is missing", async () => {
const handler = new FireworksHandler({
fireworksApiKey: "test-api-key",
fireworksModelId: "accounts/fireworks/models/llama-v3p1-8b-instruct",
})
const fakeClient = {
chat: {
completions: {
create: sinon.stub().resolves(
createAsyncIterable([
{
choices: [{}],
usage: {
prompt_tokens: 19,
completion_tokens: 4,
},
},
]),
),
},
},
}
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
const chunks: any[] = []
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
chunks.push(chunk)
}
chunks.should.deepEqual([
{
type: "usage",
inputTokens: 19,
outputTokens: 4,
cacheReadTokens: 0,
cacheWriteTokens: 0,
},
])
})
})
@@ -46,8 +46,6 @@ describe("LiteLlmHandler", () => {
}
beforeEach(() => {
fakeClient.chat.completions.create.resetHistory()
mockFetchForTesting(mockFetch, () => {
return new Promise((resolve) => {
doneMockingFetch = resolve
@@ -1,60 +0,0 @@
import "should"
import { openRouterDefaultModelInfo } from "@shared/api"
import sinon from "sinon"
import { OpenRouterHandler } from "../openrouter"
describe("OpenRouterHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: any[] = []) => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
it("should handle usage-only chunks when delta is missing", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-api-key",
})
const fakeClient = {
chat: {
completions: {
create: sinon.stub().resolves(
createAsyncIterable([
{
choices: [{}],
usage: {
prompt_tokens: 13,
completion_tokens: 5,
},
},
]),
),
},
},
}
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
sinon.stub(handler, "getModel").returns({
id: "openai/gpt-4o-mini",
info: openRouterDefaultModelInfo,
})
const chunks: any[] = []
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
chunks.push(chunk)
}
chunks.should.deepEqual([
{
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: 0,
inputTokens: 13,
outputTokens: 5,
totalCost: 0,
},
])
})
})
@@ -1,19 +1,8 @@
import "should"
import { openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import sinon from "sinon"
import { VercelAIGatewayHandler } from "../vercel-ai-gateway"
describe("VercelAIGatewayHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: any[] = []) => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
describe("getModel", () => {
it("should return configured model and info when both are provided", () => {
const customModelInfo = {
@@ -22,22 +11,22 @@ describe("VercelAIGatewayHandler", () => {
}
const handler = new VercelAIGatewayHandler({
openRouterModelId: "google/gemini-3.1-pro-preview",
openRouterModelId: "google/gemini-3-pro-preview",
openRouterModelInfo: customModelInfo,
})
const result = handler.getModel()
result.id.should.equal("google/gemini-3.1-pro-preview")
result.id.should.equal("google/gemini-3-pro-preview")
result.info.should.deepEqual(customModelInfo)
})
it("should preserve configured model ID when model info is missing", () => {
const handler = new VercelAIGatewayHandler({
openRouterModelId: "google/gemini-3.1-pro-preview",
openRouterModelId: "google/gemini-3-pro-preview",
})
const result = handler.getModel()
result.id.should.equal("google/gemini-3.1-pro-preview")
result.id.should.equal("google/gemini-3-pro-preview")
result.info.should.deepEqual(openRouterDefaultModelInfo)
})
@@ -49,46 +38,4 @@ describe("VercelAIGatewayHandler", () => {
result.info.should.deepEqual(openRouterDefaultModelInfo)
})
})
describe("createMessage", () => {
it("should handle usage-only chunks when delta is missing", async () => {
const handler = new VercelAIGatewayHandler({
vercelAiGatewayApiKey: "test-api-key",
})
const fakeClient = {
chat: {
completions: {
create: sinon.stub().resolves(
createAsyncIterable([
{
choices: [{}],
usage: {
prompt_tokens: 11,
completion_tokens: 7,
},
},
]),
),
},
},
}
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
const chunks: any[] = []
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
chunks.push(chunk)
}
chunks.should.deepEqual([
{
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: 0,
inputTokens: 11,
outputTokens: 7,
totalCost: 0,
},
])
})
})
})
+2 -1
View File
@@ -97,8 +97,9 @@ export class AnthropicHandler implements ApiHandler {
"anthropic-beta": "context-1m-2025-08-07",
},
}
} else {
return undefined
}
return undefined
})(),
)
} else {
+147 -143
View File
@@ -264,7 +264,7 @@ export class AwsBedrockHandler implements ApiHandler {
if (useProfile) {
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
} else {
delete process.env.AWS_PROFILE
delete process.env["AWS_PROFILE"]
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
@@ -537,7 +537,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
combinedContent += message.role === "user" ? `User: ${content}\n` : `Assistant: ${content}\n`
combinedContent += message.role === "user" ? "User: " + content + "\n" : "Assistant: " + content + "\n"
}
// Format according to DeepSeek R1's expected prompt format
@@ -604,178 +604,182 @@ export class AwsBedrockHandler implements ApiHandler {
* Common implementation for both Anthropic and Nova models
*/
private async *executeConverseStream(command: ConverseStreamCommand, modelInfo: ModelInfo): ApiStream {
const client = await this.getBedrockClient()
const response = await client.send(command)
try {
const client = await this.getBedrockClient()
const response = await client.send(command)
if (response.stream) {
// Buffer content by contentBlockIndex to handle multi-block responses correctly
const contentBuffers: Record<number, string> = {}
const blockTypes = new Map<number, "reasoning" | "text">()
const activeToolCalls: Map<number, { toolUseId: string; name: string }> = new Map()
if (response.stream) {
// Buffer content by contentBlockIndex to handle multi-block responses correctly
const contentBuffers: Record<number, string> = {}
const blockTypes = new Map<number, "reasoning" | "text">()
const activeToolCalls: Map<number, { toolUseId: string; name: string }> = new Map()
for await (const chunk of response.stream) {
// Debug logging to see actual response structure
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
for await (const chunk of response.stream) {
// Debug logging to see actual response structure
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
// Handle thinking response in additionalModelResponseFields (LangChain format)
const metadata = chunk.metadata as ExtendedMetadata | undefined
if (metadata?.additionalModelResponseFields?.thinkingResponse) {
const thinkingResponse = metadata.additionalModelResponseFields.thinkingResponse
if (thinkingResponse.reasoning && Array.isArray(thinkingResponse.reasoning)) {
for (const reasoningBlock of thinkingResponse.reasoning) {
if (reasoningBlock.type === "text" && reasoningBlock.text) {
yield {
type: "reasoning",
reasoning: reasoningBlock.text,
// Handle thinking response in additionalModelResponseFields (LangChain format)
const metadata = chunk.metadata as ExtendedMetadata | undefined
if (metadata?.additionalModelResponseFields?.thinkingResponse) {
const thinkingResponse = metadata.additionalModelResponseFields.thinkingResponse
if (thinkingResponse.reasoning && Array.isArray(thinkingResponse.reasoning)) {
for (const reasoningBlock of thinkingResponse.reasoning) {
if (reasoningBlock.type === "text" && reasoningBlock.text) {
yield {
type: "reasoning",
reasoning: reasoningBlock.text,
}
}
}
}
}
}
// Handle metadata events with token usage information
if (chunk.metadata?.usage) {
const inputTokens = chunk.metadata.usage.inputTokens || 0
const outputTokens = chunk.metadata.usage.outputTokens || 0
const cacheReadInputTokens = chunk.metadata.usage.cacheReadInputTokens || 0
const cacheWriteInputTokens = chunk.metadata.usage.cacheWriteInputTokens || 0
// Handle metadata events with token usage information
if (chunk.metadata?.usage) {
const inputTokens = chunk.metadata.usage.inputTokens || 0
const outputTokens = chunk.metadata.usage.outputTokens || 0
const cacheReadInputTokens = chunk.metadata.usage.cacheReadInputTokens || 0
const cacheWriteInputTokens = chunk.metadata.usage.cacheWriteInputTokens || 0
yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens: cacheReadInputTokens,
cacheWriteTokens: cacheWriteInputTokens,
totalCost: calculateApiCostOpenAI(
modelInfo,
yield {
type: "usage",
inputTokens,
outputTokens,
cacheWriteInputTokens,
cacheReadInputTokens,
),
}
}
// Handle content block start - check if Bedrock uses Anthropic SDK format
if (chunk.contentBlockStart) {
const blockStart = chunk.contentBlockStart as ContentBlockStart
const blockIndex = chunk.contentBlockStart.contentBlockIndex
if (blockStart.start?.toolUse) {
const toolUse = blockStart.start.toolUse
if (toolUse.toolUseId && toolUse.name && blockIndex !== undefined) {
activeToolCalls.set(blockIndex, {
toolUseId: toolUse.toolUseId,
name: toolUse.name,
})
cacheReadTokens: cacheReadInputTokens,
cacheWriteTokens: cacheWriteInputTokens,
totalCost: calculateApiCostOpenAI(
modelInfo,
inputTokens,
outputTokens,
cacheWriteInputTokens,
cacheReadInputTokens,
),
}
}
// Check for thinking block in various possible formats
if (
blockStart.start?.type === "thinking" ||
blockStart.contentBlock?.type === "thinking" ||
blockStart.type === "thinking"
) {
// Handle content block start - check if Bedrock uses Anthropic SDK format
if (chunk.contentBlockStart) {
const blockStart = chunk.contentBlockStart as ContentBlockStart
const blockIndex = chunk.contentBlockStart.contentBlockIndex
if (blockStart.start?.toolUse) {
const toolUse = blockStart.start.toolUse
if (toolUse.toolUseId && toolUse.name && blockIndex !== undefined) {
activeToolCalls.set(blockIndex, {
toolUseId: toolUse.toolUseId,
name: toolUse.name,
})
}
}
// Check for thinking block in various possible formats
if (
blockStart.start?.type === "thinking" ||
blockStart.contentBlock?.type === "thinking" ||
blockStart.type === "thinking"
) {
if (blockIndex !== undefined) {
blockTypes.set(blockIndex, "reasoning")
// Initialize content if provided
const initialContent =
blockStart.start?.thinking || blockStart.contentBlock?.thinking || blockStart.thinking || ""
if (initialContent) {
yield {
type: "reasoning",
reasoning: initialContent,
}
}
}
}
}
// Handle content block delta - accumulate content by block index
if (chunk.contentBlockDelta) {
const blockIndex = chunk.contentBlockDelta.contentBlockIndex
if (blockIndex !== undefined) {
blockTypes.set(blockIndex, "reasoning")
// Initialize content if provided
const initialContent =
blockStart.start?.thinking || blockStart.contentBlock?.thinking || blockStart.thinking || ""
if (initialContent) {
yield {
type: "reasoning",
reasoning: initialContent,
}
// Initialize buffer for this block if it doesn't exist
if (!(blockIndex in contentBuffers)) {
contentBuffers[blockIndex] = ""
}
}
}
}
// Handle content block delta - accumulate content by block index
if (chunk.contentBlockDelta) {
const blockIndex = chunk.contentBlockDelta.contentBlockIndex
// Check if this is a thinking block
const blockType = blockTypes.get(blockIndex)
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
if (blockIndex !== undefined) {
// Initialize buffer for this block if it doesn't exist
if (!(blockIndex in contentBuffers)) {
contentBuffers[blockIndex] = ""
}
// Check if this is a thinking block
const blockType = blockTypes.get(blockIndex)
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
// Handle thinking delta (Anthropic SDK format)
if (delta?.type === "thinking_delta" || delta?.thinking) {
const thinkingContent = delta.thinking || delta.text || ""
if (thinkingContent) {
yield {
type: "reasoning",
reasoning: thinkingContent,
// Handle thinking delta (Anthropic SDK format)
if (delta?.type === "thinking_delta" || delta?.thinking) {
const thinkingContent = delta.thinking || delta.text || ""
if (thinkingContent) {
yield {
type: "reasoning",
reasoning: thinkingContent,
}
}
}
} else if (delta?.reasoningContent?.text) {
// Handle reasoning content (Bedrock format)
const reasoningText = delta.reasoningContent.text
if (reasoningText) {
yield {
type: "reasoning",
reasoning: reasoningText,
} else if (delta?.reasoningContent?.text) {
// Handle reasoning content (Bedrock format)
const reasoningText = delta.reasoningContent.text
if (reasoningText) {
yield {
type: "reasoning",
reasoning: reasoningText,
}
}
}
} else if (delta?.toolUse?.input !== undefined) {
const toolCall = activeToolCalls.get(blockIndex)
const toolInput = delta.toolUse.input
if (toolCall && typeof toolInput === "string") {
yield {
type: "tool_calls",
tool_call: {
call_id: toolCall.toolUseId,
function: {
id: toolCall.toolUseId,
name: toolCall.name,
arguments: toolInput,
} else if (delta?.toolUse?.input !== undefined) {
const toolCall = activeToolCalls.get(blockIndex)
const toolInput = delta.toolUse.input
if (toolCall && typeof toolInput === "string") {
yield {
type: "tool_calls",
tool_call: {
call_id: toolCall.toolUseId,
function: {
id: toolCall.toolUseId,
name: toolCall.name,
arguments: toolInput,
},
},
},
}
}
}
} else if (chunk.contentBlockDelta.delta?.text) {
// Handle regular text content
const textContent = chunk.contentBlockDelta.delta.text
contentBuffers[blockIndex] += textContent
} else if (chunk.contentBlockDelta.delta?.text) {
// Handle regular text content
const textContent = chunk.contentBlockDelta.delta.text
contentBuffers[blockIndex] += textContent
// Stream based on block type
if (blockType === "reasoning") {
yield {
type: "reasoning",
reasoning: textContent,
}
} else {
yield {
type: "text",
text: textContent,
// Stream based on block type
if (blockType === "reasoning") {
yield {
type: "reasoning",
reasoning: textContent,
}
} else {
yield {
type: "text",
text: textContent,
}
}
}
}
}
}
// Handle content block stop - clean up buffers
if (chunk.contentBlockStop) {
const blockIndex = chunk.contentBlockStop.contentBlockIndex
// Handle content block stop - clean up buffers
if (chunk.contentBlockStop) {
const blockIndex = chunk.contentBlockStop.contentBlockIndex
if (blockIndex !== undefined) {
// Clean up buffers and tracking for this block
delete contentBuffers[blockIndex]
blockTypes.delete(blockIndex)
activeToolCalls.delete(blockIndex)
if (blockIndex !== undefined) {
// Clean up buffers and tracking for this block
delete contentBuffers[blockIndex]
blockTypes.delete(blockIndex)
activeToolCalls.delete(blockIndex)
}
}
}
// Handle errors with unified error handling
yield* this.handleBedrockStreamError(chunk)
// Handle errors with unified error handling
yield* this.handleBedrockStreamError(chunk)
}
}
} catch (error) {
throw error
}
}
@@ -1014,7 +1018,7 @@ export class AwsBedrockHandler implements ApiHandler {
if (item.source.media_type) {
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
if (formatMatch?.[1]) {
if (formatMatch && formatMatch[1]) {
const extractedFormat = formatMatch[1]
// Ensure format is one of the allowed values
if (["png", "jpeg", "gif", "webp"].includes(extractedFormat)) {
+5 -10
View File
@@ -84,8 +84,7 @@ export class CerebrasHandler implements ApiHandler {
.map((block) => {
if (block.type === "text") {
return block.text
}
if (block.type === "image") {
} else if (block.type === "image") {
return "[Image content not supported in Cerebras]"
}
return ""
@@ -196,18 +195,14 @@ export class CerebrasHandler implements ApiHandler {
// Rate limit error - will be handled by retry decorator with patient backoff
const _limits = this.getRateLimits()
throw new Error(`Cerebras API rate limit exceeded.`)
}
if (error?.status === 401) {
} else if (error?.status === 401) {
throw new Error("Cerebras API authentication failed. Please check your API key.")
}
if (error?.status === 403) {
} else if (error?.status === 403) {
throw new Error("Cerebras API access denied. Please check your API key permissions.")
}
if (error?.status >= 500) {
} else if (error?.status >= 500) {
// Server errors - retryable
throw new Error(`Cerebras API server error (${error.status}): ${error.message || "Unknown server error"}`)
}
if (error?.status === 400) {
} else if (error?.status === 400) {
// Client errors - not retryable
throw new Error(`Cerebras API bad request: ${error.message || "Invalid request parameters"}`)
}
+2 -2
View File
@@ -70,8 +70,8 @@ export class ClaudeCodeHandler implements ApiHandler {
if (message.stop_reason !== null) {
const content = "text" in message.content[0] ? message.content[0] : undefined
const isError = content?.text.startsWith(`API Error`)
if (content && isError) {
const isError = content && content.text.startsWith(`API Error`)
if (isError) {
// Error messages are formatted as: `API Error: <<status code>> <<json>>`
const errorMessageStart = content.text.indexOf("{")
const errorMessage = content.text.slice(errorMessageStart)
+2 -8
View File
@@ -126,7 +126,7 @@ export class ClineHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
Logger.debug(`ClineHandler chunk:${JSON.stringify(chunk)}`)
Logger.debug("ClineHandler chunk:" + JSON.stringify(chunk))
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
@@ -168,12 +168,7 @@ export class ClineHandler implements ApiHandler {
// 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 (
delta &&
"reasoning" in delta &&
delta.reasoning &&
!shouldSkipReasoningForModel(this.options.openRouterModelId)
) {
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
@@ -188,7 +183,6 @@ export class ClineHandler implements ApiHandler {
See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
*/
if (
delta &&
"reasoning_details" in delta &&
delta.reasoning_details &&
// @ts-expect-error-next-line
+17 -14
View File
@@ -78,11 +78,13 @@ export class DifyHandler implements ApiHandler {
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
private currentTaskId: string | null = null
private abortController: AbortController | null = null
constructor(options: DifyHandlerOptions) {
this.options = options
this.apiKey = this.options.difyApiKey || ""
this.baseUrl = this.options.difyBaseUrl || ""
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
Logger.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
@@ -339,7 +341,7 @@ export class DifyHandler implements ApiHandler {
}
hasYieldedContent = true
}
} catch (_e) {
} catch (e) {
// Not JSON, continue
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
@@ -427,7 +429,7 @@ export class DifyHandler implements ApiHandler {
* @param user User identifier (defaults to "cline-user")
* @returns Promise with file upload response
*/
async uploadFile(file: Buffer, filename: string, user = "cline-user"): Promise<DifyFileResponse> {
async uploadFile(file: Buffer, filename: string, user: string = "cline-user"): Promise<DifyFileResponse> {
const formData = new FormData()
formData.append("file", new Blob([new Uint8Array(file)]), filename)
formData.append("user", user)
@@ -452,7 +454,7 @@ export class DifyHandler implements ApiHandler {
* @param user User identifier (defaults to "cline-user")
* @returns Promise that resolves when generation is stopped
*/
async stopGeneration(taskId: string, user = "cline-user"): Promise<void> {
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
method: "POST",
headers: this.jsonHeaders(),
@@ -475,9 +477,9 @@ export class DifyHandler implements ApiHandler {
*/
async getConversationHistory(
conversationId: string,
user = "cline-user",
user: string = "cline-user",
firstId?: string,
limit = 20,
limit: number = 20,
): Promise<DifyHistoryResponse> {
const params = new URLSearchParams({ user, limit: limit.toString() })
if (firstId) {
@@ -505,10 +507,10 @@ export class DifyHandler implements ApiHandler {
* @returns Promise with conversations list
*/
async getConversations(
user = "cline-user",
user: string = "cline-user",
lastId?: string,
limit = 20,
sortBy = "-updated_at",
limit: number = 20,
sortBy: string = "-updated_at",
): Promise<DifyConversationsResponse> {
const params = new URLSearchParams({
user,
@@ -537,7 +539,7 @@ export class DifyHandler implements ApiHandler {
* @param user User identifier (defaults to "cline-user")
* @returns Promise that resolves when conversation is deleted
*/
async deleteConversation(conversationId: string, user = "cline-user"): Promise<void> {
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
method: "DELETE",
headers: this.jsonHeaders(),
@@ -560,9 +562,9 @@ export class DifyHandler implements ApiHandler {
*/
async renameConversation(
conversationId: string,
user = "cline-user",
user: string = "cline-user",
name?: string,
autoGenerate = false,
autoGenerate: boolean = false,
): Promise<DifyConversationResponse> {
const body: any = { user, auto_generate: autoGenerate }
if (name) {
@@ -595,7 +597,7 @@ export class DifyHandler implements ApiHandler {
messageId: string,
rating: "like" | "dislike",
content?: string,
user = "cline-user",
user: string = "cline-user",
): Promise<void> {
const body: any = { rating, user }
if (content) {
@@ -635,6 +637,7 @@ export class DifyHandler implements ApiHandler {
*/
resetConversation(): void {
this.conversationId = null
this.currentTaskId = null
}
private jsonHeaders() {
+1 -1
View File
@@ -71,7 +71,7 @@ export class FireworksHandler implements ApiHandler {
}
}
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
yield {
type: "reasoning",
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
@@ -1,6 +1,10 @@
// Mock for @google/genai module to avoid ESM compatibility issues in tests
export class GoogleGenAI {
constructor(_options: any) {
// Mock constructor
}
models = {
generateContentStream: async (_params: any) => {
// Mock implementation that returns an async iterator
+2 -2
View File
@@ -157,7 +157,7 @@ export class GeminiHandler implements ApiHandler {
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
systemInstruction: systemPrompt,
// Set temperature (default to 0)
// Gemini 3 recommends 1.0
// Gemini 3.0 recommends 1.0
temperature: info.temperature ?? 1,
}
@@ -297,7 +297,7 @@ export class GeminiHandler implements ApiHandler {
// https://github.com/googleapis/js-genai/blob/v1.11.0/src/_api_client.ts#L758
const response = this.attemptParse(error.message)
if (response?.error) {
if (response && response.error) {
const responseBody = this.attemptParse(response.error.message)
if (responseBody.error) {
+1 -1
View File
@@ -79,7 +79,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
})
let reasoning: string | null = null
let didOutputUsage = false
let didOutputUsage: boolean = false
let finalUsage: any = null
const toolCallProcessor = new ToolCallProcessor()
+39 -35
View File
@@ -66,49 +66,53 @@ export class HuggingFaceHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
try {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
...getOpenAIToolParams(tools),
}
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
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
const toolCallProcessor = new ToolCallProcessor()
const stream = (await client.chat.completions.create(requestParams)) as any
let _chunkCount = 0
let _totalContent = ""
let _chunkCount = 0
let _totalContent = ""
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
_totalContent += delta.content
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
_totalContent += delta.content
yield {
type: "text",
text: delta.content,
yield {
type: "text",
text: delta.content,
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
if (delta?.tool_calls) {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
} catch (error: any) {
throw error
}
}
+20 -19
View File
@@ -72,24 +72,26 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
if (response.ok) {
const data: LiteLlmModelInfoResponse = await response.json()
return data
}
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
Authorization: `Bearer ${apiKey}`,
...buildExternalBasicHeaders(),
},
})
} else {
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
Authorization: `Bearer ${apiKey}`,
...buildExternalBasicHeaders(),
},
})
if (retryResponse.ok) {
const data: LiteLlmModelInfoResponse = await retryResponse.json()
return data
if (retryResponse.ok) {
const data: LiteLlmModelInfoResponse = await retryResponse.json()
return data
} else {
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
}
}
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
} catch (error) {
Logger.error("Error fetching LiteLLM model info:", error)
throw error
@@ -100,7 +102,7 @@ export class LiteLlmHandler implements ApiHandler {
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
private modelInfoCache: LiteLlmModelInfoResponse | undefined
private modelInfoCacheTimestamp = 0
private modelInfoCacheTimestamp: number = 0
private readonly modelInfoCacheTTL = 5 * 60 * 1000 // 5 minutes
constructor(options: LiteLlmHandlerOptions) {
@@ -272,8 +274,7 @@ export class LiteLlmHandler implements ApiHandler {
},
] as any,
}
}
if (Array.isArray(message.content)) {
} else if (Array.isArray(message.content)) {
// Apply cache control to the last content item in the array
return {
...message,
+1 -1
View File
@@ -118,7 +118,7 @@ export class MistralHandler implements ApiHandler {
}
}
} else if (delta?.content) {
let content = ""
let content: string = ""
if (typeof delta.content === "string") {
content = delta.content
} else if (Array.isArray(delta.content)) {
+18 -132
View File
@@ -1,6 +1,5 @@
import { Anthropic, APIError as AnthropicAPIError } from "@anthropic-ai/sdk"
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
import OpenAI, { APIError as OpenAIAPIError, OpenAIError } from "openai"
import OpenAI, { APIError, OpenAIError } from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import {
@@ -17,12 +16,10 @@ import { ApiFormat } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
import { convertOpenAIToolsToAnthropicTools, handleAnthropicMessagesApiStreamResponse } from "../utils/messages_api_support"
import { handleResponsesApiStreamResponse } from "../utils/responses_api_support"
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
@@ -38,15 +35,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
export class OcaHandler implements ApiHandler {
protected options: OcaHandlerOptions
protected openAIClient: OpenAI | undefined
protected anthropicClient: Anthropic | undefined
protected client: OpenAI | undefined
protected externalHeaders: Record<string, string> = {}
constructor(options: OcaHandlerOptions) {
this.options = options
}
protected initializeOpenAIClient(options: OcaHandlerOptions): OpenAI {
protected initializeClient(options: OcaHandlerOptions): OpenAI {
const externalHeaders = buildExternalBasicHeaders()
return new (class OCIOpenAI extends OpenAI {
protected override async prepareOptions(opts: any): Promise<void> {
@@ -67,7 +63,7 @@ export class OcaHandler implements ApiHandler {
error: Object | undefined,
message: string | undefined,
headers: any | undefined,
): OpenAIAPIError {
): APIError {
interface OciError {
code?: string
message?: string
@@ -98,89 +94,23 @@ export class OcaHandler implements ApiHandler {
})
}
protected initializeAnthropicClient(options: OcaHandlerOptions): Anthropic {
const externalHeaders = buildExternalBasicHeaders()
return new (class OCIAnthropic extends Anthropic {
protected override async prepareOptions(opts: any): Promise<void> {
const token = await OcaAuthService.getInstance().getAuthToken()
if (!token) {
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
}
opts.headers ??= {}
// OCA Headers
const ociHeaders = await createOcaHeaders(token, options.taskId!)
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
return super.prepareOptions(opts)
}
protected override makeStatusError(
status: number | undefined,
error: Object | undefined,
message: string | undefined,
headers: any | undefined,
): AnthropicAPIError {
interface OciError {
code?: string
message?: string
}
let ociErrorMessage = message
if (typeof error === "object" && error !== null) {
try {
ociErrorMessage = JSON.stringify(error)
const ociErr = error as OciError
if (ociErr.code !== undefined && ociErr.message !== undefined) {
ociErrorMessage = `${ociErr.code}: ${ociErr.message}`
}
} catch {}
}
const opcRequestId = headers?.[OCI_HEADER_OPC_REQUEST_ID]
if (opcRequestId) {
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
}
const statusCode = typeof status === "number" ? status : 500
return super.makeStatusError(statusCode, error ?? {}, ociErrorMessage, headers)
}
})({
baseURL:
options.ocaBaseUrl ||
(options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL),
apiKey: "noop",
fetch, // Use configured fetch with proxy support
})
}
protected ensureOpenAIClient(): OpenAI {
if (!this.openAIClient) {
protected ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.ocaModelId) {
throw new Error("Oracle Code Assist (OCA) model is not selected")
}
try {
this.openAIClient = this.initializeOpenAIClient(this.options)
this.client = this.initializeClient(this.options)
} catch (error) {
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
}
}
return this.openAIClient
}
protected ensureAnthropicClient(): Anthropic {
if (!this.anthropicClient) {
if (!this.options.ocaModelId) {
throw new Error("Oracle Code Assist (OCA) model is not selected")
}
try {
this.anthropicClient = this.initializeAnthropicClient(this.options)
} catch (error) {
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
}
}
return this.anthropicClient
return this.client
}
async getApiCosts(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureOpenAIClient()
const client = this.ensureClient()
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
const token = await OcaAuthService.getInstance().getAuthToken()
if (!token) {
@@ -217,7 +147,7 @@ export class OcaHandler implements ApiHandler {
}
async calculateCost(
_modelInfo: ModelInfo,
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
_cacheWriteTokens?: number,
@@ -231,17 +161,15 @@ export class OcaHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
if (this.options.ocaModelInfo?.apiFormat === ApiFormat.OPENAI_RESPONSES) {
if (this.options.ocaModelInfo?.apiFormat == ApiFormat.OPENAI_RESPONSES) {
yield* this.createMessageResponsesApi(systemPrompt, messages, tools)
} else if (this.options.ocaModelInfo?.apiFormat == ApiFormat.ANTHROPIC_CHAT) {
yield* this.createMessageMessagesApi(systemPrompt, messages, tools)
} else {
yield* this.createMessageChatApi(systemPrompt, messages, tools)
}
}
async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureOpenAIClient()
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -310,7 +238,7 @@ export class OcaHandler implements ApiHandler {
}
if (this.options.ocaModelInfo?.supportsReasoningEffort) {
chatCompletionsParams.reasoning_effort = this.options.ocaReasoningEffort || ("medium" as any)
chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any)
}
const stream = await client.chat.completions.create(chatCompletionsParams)
@@ -378,8 +306,8 @@ export class OcaHandler implements ApiHandler {
}
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureOpenAIClient()
const inputMessages = convertToOpenAIResponsesInput(messages, { usePreviousResponseId: false }).input
const client = this.ensureClient()
const inputMessages = convertToOpenAIResponsesInput(messages).input
// Convert messages to Responses API input format
const input: OpenAI.Responses.ResponseInputItem[] = [{ role: "system", content: systemPrompt }, ...inputMessages]
@@ -401,56 +329,14 @@ export class OcaHandler implements ApiHandler {
tools: responseTools,
}
const ocaModelInfo = this.options.ocaModelInfo
if (!ocaModelInfo) {
throw new Error("Oracle Code Assist (OCA) model info is required for Responses API")
}
if (ocaModelInfo.supportsReasoning) {
responsesParams.reasoning = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) {
responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
}
// Create the response using Responses API
const stream = await client.responses.create(responsesParams)
yield* handleResponsesApiStreamResponse(stream, ocaModelInfo, this.calculateCost.bind(this))
}
async *createMessageMessagesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureAnthropicClient()
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = this.options.ocaModelInfo?.supportsReasoning && budgetTokens !== 0
let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0
const maxTokens: number | undefined = this.options.ocaModelInfo?.maxTokens || 8192
if (reasoningOn) {
temperature = 0
}
const anthropicTools = convertOpenAIToolsToAnthropicTools(tools)
const anthropicMessages = sanitizeAnthropicMessages(messages, this.options.ocaUsePromptCache ?? false)
const stream = await client.messages.create({
model: modelId,
max_tokens: maxTokens,
temperature: reasoningOn ? undefined : temperature,
system: [
{
text: systemPrompt,
type: "text",
cache_control: this.options.ocaUsePromptCache ? { type: "ephemeral" } : undefined,
},
],
messages: anthropicMessages,
stream: true,
tools: anthropicTools,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined,
})
yield* handleAnthropicMessagesApiStreamResponse(stream)
yield* handleResponsesApiStreamResponse(stream, this.options.ocaModelInfo!, this.calculateCost.bind(this))
}
getModel() {
+1 -1
View File
@@ -86,7 +86,7 @@ export class OllamaHandler implements ApiHandler {
try {
for await (const chunk of stream) {
Logger.debug(`[OllamaHandler] Message Chunk${JSON.stringify(chunk)}`)
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
const delta = chunk.message

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