Skip to content

Commit e3ffd13

Browse files
committed
Merge branch 'main' into feature/vertex-credentials-auth
2 parents 01f83b4 + d2c2029 commit e3ffd13

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+5993
-1762
lines changed

.changeset/wise-pears-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Improved observability of openai compatible APIs, by sending x-title and http-referer headers, as per Open Router standard.

src/__mocks__/vscode.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ const vscode = {
8484
this.uri = uri
8585
}
8686
},
87+
RelativePattern: class {
88+
constructor(base, pattern) {
89+
this.base = base
90+
this.pattern = pattern
91+
}
92+
},
8793
}
8894

8995
module.exports = vscode

src/activate/registerCommands.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@ import delay from "delay"
33

44
import { ClineProvider } from "../core/webview/ClineProvider"
55

6+
// Store panel references in both modes
7+
let sidebarPanel: vscode.WebviewView | undefined = undefined
8+
let tabPanel: vscode.WebviewPanel | undefined = undefined
9+
10+
/**
11+
* Get the currently active panel
12+
* @returns WebviewPanel或WebviewView
13+
*/
14+
export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined {
15+
return tabPanel || sidebarPanel
16+
}
17+
18+
/**
19+
* Set panel references
20+
*/
21+
export function setPanel(
22+
newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined,
23+
type: "sidebar" | "tab",
24+
): void {
25+
if (type === "sidebar") {
26+
sidebarPanel = newPanel as vscode.WebviewView
27+
tabPanel = undefined
28+
} else {
29+
tabPanel = newPanel as vscode.WebviewPanel
30+
sidebarPanel = undefined
31+
}
32+
}
33+
634
export type RegisterCommandOptions = {
735
context: vscode.ExtensionContext
836
outputChannel: vscode.OutputChannel
@@ -15,12 +43,28 @@ export const registerCommands = (options: RegisterCommandOptions) => {
1543
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
1644
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
1745
}
46+
47+
// Human Relay Dialog Command
48+
context.subscriptions.push(
49+
vscode.commands.registerCommand(
50+
"roo-cline.showHumanRelayDialog",
51+
(params: { requestId: string; promptText: string }) => {
52+
if (getPanel()) {
53+
getPanel()?.webview.postMessage({
54+
type: "showHumanRelayDialog",
55+
requestId: params.requestId,
56+
promptText: params.promptText,
57+
})
58+
}
59+
},
60+
),
61+
)
1862
}
1963

2064
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
2165
return {
2266
"roo-cline.plusButtonClicked": async () => {
23-
await provider.clearTask()
67+
await provider.removeClineFromStack()
2468
await provider.postStateToWebview()
2569
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
2670
},
@@ -65,20 +109,28 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
65109

66110
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
67111

68-
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
112+
const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
69113
enableScripts: true,
70114
retainContextWhenHidden: true,
71115
localResourceRoots: [context.extensionUri],
72116
})
73117

118+
// Save as tab type panel
119+
setPanel(newPanel, "tab")
120+
74121
// TODO: use better svg icon with light and dark variants (see
75122
// https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
76-
panel.iconPath = {
123+
newPanel.iconPath = {
77124
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
78125
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
79126
}
80127

81-
await tabProvider.resolveWebviewView(panel)
128+
await tabProvider.resolveWebviewView(newPanel)
129+
130+
// Handle panel closing events
131+
newPanel.onDidDispose(() => {
132+
setPanel(undefined, "tab")
133+
})
82134

83135
// Lock the editor group so clicking on files doesn't open them over the panel
84136
await delay(100)

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { VsCodeLmHandler } from "./providers/vscode-lm"
1919
import { ApiStream } from "./transform/stream"
2020
import { UnboundHandler } from "./providers/unbound"
2121
import { RequestyHandler } from "./providers/requesty"
22+
import { HumanRelayHandler } from "./providers/human-relay"
2223

2324
export interface SingleCompletionHandler {
2425
completePrompt(prompt: string): Promise<string>
@@ -72,6 +73,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
7273
return new UnboundHandler(options)
7374
case "requesty":
7475
return new RequestyHandler(options)
76+
case "human-relay":
77+
return new HumanRelayHandler(options)
7578
default:
7679
return new AnthropicHandler(options)
7780
}

src/api/providers/__tests__/deepseek.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe("DeepSeekHandler", () => {
7272
mockOptions = {
7373
deepSeekApiKey: "test-api-key",
7474
apiModelId: "deepseek-chat",
75-
deepSeekBaseUrl: "https://api.deepseek.com/v1",
75+
deepSeekBaseUrl: "https://api.deepseek.com",
7676
}
7777
handler = new DeepSeekHandler(mockOptions)
7878
mockCreate.mockClear()
@@ -110,7 +110,7 @@ describe("DeepSeekHandler", () => {
110110
// The base URL is passed to OpenAI client internally
111111
expect(OpenAI).toHaveBeenCalledWith(
112112
expect.objectContaining({
113-
baseURL: "https://api.deepseek.com/v1",
113+
baseURL: "https://api.deepseek.com",
114114
}),
115115
)
116116
})

src/api/providers/__tests__/openai.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ describe("OpenAiHandler", () => {
9090
})
9191
expect(handlerWithCustomUrl).toBeInstanceOf(OpenAiHandler)
9292
})
93+
94+
it("should set default headers correctly", () => {
95+
// Get the mock constructor from the jest mock system
96+
const openAiMock = jest.requireMock("openai").default
97+
98+
expect(openAiMock).toHaveBeenCalledWith({
99+
baseURL: expect.any(String),
100+
apiKey: expect.any(String),
101+
defaultHeaders: {
102+
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
103+
"X-Title": "Roo Code",
104+
},
105+
})
106+
})
93107
})
94108

95109
describe("createMessage", () => {

src/api/providers/deepseek.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class DeepSeekHandler extends OpenAiHandler {
88
...options,
99
openAiApiKey: options.deepSeekApiKey ?? "not-provided",
1010
openAiModelId: options.apiModelId ?? deepSeekDefaultModelId,
11-
openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1",
11+
openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com",
1212
openAiStreamingEnabled: true,
1313
includeMaxTokens: true,
1414
})

src/api/providers/human-relay.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// filepath: e:\Project\Roo-Code\src\api\providers\human-relay.ts
2+
import { Anthropic } from "@anthropic-ai/sdk"
3+
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
4+
import { ApiHandler, SingleCompletionHandler } from "../index"
5+
import { ApiStream } from "../transform/stream"
6+
import * as vscode from "vscode"
7+
import { ExtensionMessage } from "../../shared/ExtensionMessage"
8+
import { getPanel } from "../../activate/registerCommands" // Import the getPanel function
9+
10+
/**
11+
* Human Relay API processor
12+
* This processor does not directly call the API, but interacts with the model through human operations copy and paste.
13+
*/
14+
export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
15+
private options: ApiHandlerOptions
16+
17+
constructor(options: ApiHandlerOptions) {
18+
this.options = options
19+
}
20+
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
21+
return Promise.resolve(0)
22+
}
23+
24+
/**
25+
* Create a message processing flow, display a dialog box to request human assistance
26+
* @param systemPrompt System prompt words
27+
* @param messages Message list
28+
*/
29+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
30+
// Get the most recent user message
31+
const latestMessage = messages[messages.length - 1]
32+
33+
if (!latestMessage) {
34+
throw new Error("No message to relay")
35+
}
36+
37+
// If it is the first message, splice the system prompt word with the user message
38+
let promptText = ""
39+
if (messages.length === 1) {
40+
promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}`
41+
} else {
42+
promptText = getMessageContent(latestMessage)
43+
}
44+
45+
// Copy to clipboard
46+
await vscode.env.clipboard.writeText(promptText)
47+
48+
// A dialog box pops up to request user action
49+
const response = await showHumanRelayDialog(promptText)
50+
51+
if (!response) {
52+
// The user canceled the operation
53+
throw new Error("Human relay operation cancelled")
54+
}
55+
56+
// Return to the user input reply
57+
yield { type: "text", text: response }
58+
}
59+
60+
/**
61+
* Get model information
62+
*/
63+
getModel(): { id: string; info: ModelInfo } {
64+
// Human relay does not depend on a specific model, here is a default configuration
65+
return {
66+
id: "human-relay",
67+
info: {
68+
maxTokens: 16384,
69+
contextWindow: 100000,
70+
supportsImages: true,
71+
supportsPromptCache: false,
72+
supportsComputerUse: true,
73+
inputPrice: 0,
74+
outputPrice: 0,
75+
description: "Calling web-side AI model through human relay",
76+
},
77+
}
78+
}
79+
80+
/**
81+
* Implementation of a single prompt
82+
* @param prompt Prompt content
83+
*/
84+
async completePrompt(prompt: string): Promise<string> {
85+
// Copy to clipboard
86+
await vscode.env.clipboard.writeText(prompt)
87+
88+
// A dialog box pops up to request user action
89+
const response = await showHumanRelayDialog(prompt)
90+
91+
if (!response) {
92+
throw new Error("Human relay operation cancelled")
93+
}
94+
95+
return response
96+
}
97+
}
98+
99+
/**
100+
* Extract text content from message object
101+
* @param message
102+
*/
103+
function getMessageContent(message: Anthropic.Messages.MessageParam): string {
104+
if (typeof message.content === "string") {
105+
return message.content
106+
} else if (Array.isArray(message.content)) {
107+
return message.content
108+
.filter((item) => item.type === "text")
109+
.map((item) => (item.type === "text" ? item.text : ""))
110+
.join("\n")
111+
}
112+
return ""
113+
}
114+
/**
115+
* Displays the human relay dialog and waits for user response.
116+
* @param promptText The prompt text that needs to be copied.
117+
* @returns The user's input response or undefined (if canceled).
118+
*/
119+
async function showHumanRelayDialog(promptText: string): Promise<string | undefined> {
120+
return new Promise<string | undefined>((resolve) => {
121+
// Create a unique request ID
122+
const requestId = Date.now().toString()
123+
124+
// Register a global callback function
125+
vscode.commands.executeCommand(
126+
"roo-cline.registerHumanRelayCallback",
127+
requestId,
128+
(response: string | undefined) => {
129+
resolve(response)
130+
},
131+
)
132+
133+
// Open the dialog box directly using the current panel
134+
vscode.commands.executeCommand("roo-cline.showHumanRelayDialog", {
135+
requestId,
136+
promptText,
137+
})
138+
})
139+
}

src/api/providers/openai.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
1616
import { BaseProvider } from "./base-provider"
1717

1818
const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6
19-
export interface OpenAiHandlerOptions extends ApiHandlerOptions {
20-
defaultHeaders?: Record<string, string>
19+
20+
export const defaultHeaders = {
21+
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
22+
"X-Title": "Roo Code",
2123
}
2224

25+
export interface OpenAiHandlerOptions extends ApiHandlerOptions {}
26+
2327
export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
2428
protected options: OpenAiHandlerOptions
2529
private client: OpenAI
@@ -47,9 +51,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
4751
baseURL,
4852
apiKey,
4953
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
54+
defaultHeaders,
5055
})
5156
} else {
52-
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: this.options.defaultHeaders })
57+
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
5358
}
5459
}
5560

src/api/providers/openrouter.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { convertToR1Format } from "../transform/r1-format"
1313
import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
1414
import { getModelParams, SingleCompletionHandler } from ".."
1515
import { BaseProvider } from "./base-provider"
16+
import { defaultHeaders } from "./openai"
1617

1718
// Add custom interface for OpenRouter params.
1819
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -37,11 +38,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
3738
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
3839
const apiKey = this.options.openRouterApiKey ?? "not-provided"
3940

40-
const defaultHeaders = {
41-
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
42-
"X-Title": "Roo Code",
43-
}
44-
4541
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
4642
}
4743

0 commit comments

Comments
 (0)