Skip to content

Commit 423e2af

Browse files
authored
Continuing work on support for OpenRouter compression (#43)
1 parent 0ac3dd5 commit 423e2af

File tree

12 files changed

+3140
-7466
lines changed

12 files changed

+3140
-7466
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Roo Cline Changelog
22

3+
## [2.1.11]
4+
5+
- Incorporate lloydchang's [PR](https://github.com/RooVetGit/Roo-Cline/pull/42) to add support for OpenRouter compression
6+
37
## [2.1.10]
48

59
- Incorporate HeavenOSK's [PR](https://github.com/cline/cline/pull/818) to add sound effects to Cline

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ After installation, Roo Cline will appear in your VSCode-compatible editor's ins
2727
<a href="https://discord.gg/cline" target="_blank"><strong>Join the Discord</strong></a>
2828
</td>
2929
<td align="center">
30-
<a href="https://github.com/cline/cline/wiki" target="_blank"><strong>Docs</strong></a>
30+
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
3131
</td>
3232
<td align="center">
33-
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
33+
<a href="https://cline.bot/join-us" target="_blank"><strong>We're Hiring!</strong></a>
3434
</td>
3535
</tbody>
3636
</table>
@@ -112,7 +112,7 @@ Try asking Cline to "test the app", and watch as he runs a command like `npm run
112112

113113
## Contributing
114114

115-
To contribute to the project, start by exploring [open issues](https://github.com/cline/cline/issues) or checking our [feature request board](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop). We'd also love to have you join our [Discord](https://discord.gg/cline) to share ideas and connect with other contributors.
115+
To contribute to the project, start by exploring [open issues](https://github.com/cline/cline/issues) or checking our [feature request board](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop). We'd also love to have you join our [Discord](https://discord.gg/cline) to share ideas and connect with other contributors. If you're interested in joining the team, check out our [careers page](https://cline.bot/join-us)!
116116

117117
<details>
118118
<summary>Local Development Instructions</summary>

bin/roo-cline-2.1.0.vsix

-23.8 MB
Binary file not shown.

package-lock.json

Lines changed: 405 additions & 1555 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Cline",
44
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "2.1.10",
6+
"version": "2.1.11",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",
@@ -136,7 +136,6 @@
136136
},
137137
"scripts": {
138138
"vscode:prepublish": "npm run package",
139-
"vsix": "vsce package --out bin",
140139
"compile": "npm run check-types && npm run lint && node esbuild.js",
141140
"watch": "npm-run-all -p watch:*",
142141
"watch:esbuild": "node esbuild.js --watch",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { OpenRouterHandler } from '../openrouter'
2+
import { ApiHandlerOptions, ModelInfo } from '../../../shared/api'
3+
import OpenAI from 'openai'
4+
import axios from 'axios'
5+
import { Anthropic } from '@anthropic-ai/sdk'
6+
7+
// Mock dependencies
8+
jest.mock('openai')
9+
jest.mock('axios')
10+
jest.mock('delay', () => jest.fn(() => Promise.resolve()))
11+
12+
describe('OpenRouterHandler', () => {
13+
const mockOptions: ApiHandlerOptions = {
14+
openRouterApiKey: 'test-key',
15+
openRouterModelId: 'test-model',
16+
openRouterModelInfo: {
17+
name: 'Test Model',
18+
description: 'Test Description',
19+
maxTokens: 1000,
20+
contextWindow: 2000,
21+
supportsPromptCache: true,
22+
inputPrice: 0.01,
23+
outputPrice: 0.02
24+
} as ModelInfo
25+
}
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks()
29+
})
30+
31+
test('constructor initializes with correct options', () => {
32+
const handler = new OpenRouterHandler(mockOptions)
33+
expect(handler).toBeInstanceOf(OpenRouterHandler)
34+
expect(OpenAI).toHaveBeenCalledWith({
35+
baseURL: 'https://openrouter.ai/api/v1',
36+
apiKey: mockOptions.openRouterApiKey,
37+
defaultHeaders: {
38+
'HTTP-Referer': 'https://cline.bot',
39+
'X-Title': 'Cline',
40+
},
41+
})
42+
})
43+
44+
test('getModel returns correct model info when options are provided', () => {
45+
const handler = new OpenRouterHandler(mockOptions)
46+
const result = handler.getModel()
47+
48+
expect(result).toEqual({
49+
id: mockOptions.openRouterModelId,
50+
info: mockOptions.openRouterModelInfo
51+
})
52+
})
53+
54+
test('createMessage generates correct stream chunks', async () => {
55+
const handler = new OpenRouterHandler(mockOptions)
56+
const mockStream = {
57+
async *[Symbol.asyncIterator]() {
58+
yield {
59+
id: 'test-id',
60+
choices: [{
61+
delta: {
62+
content: 'test response'
63+
}
64+
}]
65+
}
66+
}
67+
}
68+
69+
// Mock OpenAI chat.completions.create
70+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
71+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
72+
completions: { create: mockCreate }
73+
} as any
74+
75+
// Mock axios.get for generation details
76+
;(axios.get as jest.Mock).mockResolvedValue({
77+
data: {
78+
data: {
79+
native_tokens_prompt: 10,
80+
native_tokens_completion: 20,
81+
total_cost: 0.001
82+
}
83+
}
84+
})
85+
86+
const systemPrompt = 'test system prompt'
87+
const messages: Anthropic.Messages.MessageParam[] = [{ role: 'user' as const, content: 'test message' }]
88+
89+
const generator = handler.createMessage(systemPrompt, messages)
90+
const chunks = []
91+
92+
for await (const chunk of generator) {
93+
chunks.push(chunk)
94+
}
95+
96+
// Verify stream chunks
97+
expect(chunks).toHaveLength(2) // One text chunk and one usage chunk
98+
expect(chunks[0]).toEqual({
99+
type: 'text',
100+
text: 'test response'
101+
})
102+
expect(chunks[1]).toEqual({
103+
type: 'usage',
104+
inputTokens: 10,
105+
outputTokens: 20,
106+
totalCost: 0.001,
107+
fullResponseText: 'test response'
108+
})
109+
110+
// Verify OpenAI client was called with correct parameters
111+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
112+
model: mockOptions.openRouterModelId,
113+
temperature: 0,
114+
messages: expect.arrayContaining([
115+
{ role: 'system', content: systemPrompt },
116+
{ role: 'user', content: 'test message' }
117+
]),
118+
stream: true
119+
}))
120+
})
121+
})

src/api/providers/openrouter.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ import OpenAI from "openai"
44
import { ApiHandler } from "../"
55
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
66
import { convertToOpenAiMessages } from "../transform/openai-format"
7-
import { ApiStream } from "../transform/stream"
7+
import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
88
import delay from "delay"
99

10+
// Add custom interface for OpenRouter params
11+
interface OpenRouterChatCompletionParams extends OpenAI.Chat.ChatCompletionCreateParamsStreaming {
12+
transforms?: string[];
13+
}
14+
15+
// Add custom interface for OpenRouter usage chunk
16+
interface OpenRouterApiStreamUsageChunk extends ApiStreamUsageChunk {
17+
fullResponseText: string;
18+
}
19+
1020
export class OpenRouterHandler implements ApiHandler {
1121
private options: ApiHandlerOptions
1222
private client: OpenAI
@@ -23,7 +33,7 @@ export class OpenRouterHandler implements ApiHandler {
2333
})
2434
}
2535

26-
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
36+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): AsyncGenerator<ApiStreamChunk> {
2737
// Convert Anthropic messages to OpenAI format
2838
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
2939
{ role: "system", content: systemPrompt },
@@ -95,17 +105,21 @@ export class OpenRouterHandler implements ApiHandler {
95105
maxTokens = 8_192
96106
break
97107
}
108+
// https://openrouter.ai/docs/transforms
109+
let fullResponseText = "";
98110
const stream = await this.client.chat.completions.create({
99111
model: this.getModel().id,
100112
max_tokens: maxTokens,
101113
temperature: 0,
102114
messages: openAiMessages,
103115
stream: true,
104-
})
116+
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
117+
...(this.options.openRouterUseMiddleOutTransform && { transforms: ["middle-out"] })
118+
} as OpenRouterChatCompletionParams);
105119

106120
let genId: string | undefined
107121

108-
for await (const chunk of stream) {
122+
for await (const chunk of stream as unknown as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {
109123
// openrouter returns an error object instead of the openai sdk throwing an error
110124
if ("error" in chunk) {
111125
const error = chunk.error as { message?: string; code?: number }
@@ -119,10 +133,11 @@ export class OpenRouterHandler implements ApiHandler {
119133

120134
const delta = chunk.choices[0]?.delta
121135
if (delta?.content) {
136+
fullResponseText += delta.content;
122137
yield {
123138
type: "text",
124139
text: delta.content,
125-
}
140+
} as ApiStreamChunk;
126141
}
127142
// if (chunk.usage) {
128143
// yield {
@@ -153,13 +168,14 @@ export class OpenRouterHandler implements ApiHandler {
153168
inputTokens: generation?.native_tokens_prompt || 0,
154169
outputTokens: generation?.native_tokens_completion || 0,
155170
totalCost: generation?.total_cost || 0,
156-
}
171+
fullResponseText
172+
} as OpenRouterApiStreamUsageChunk;
157173
} catch (error) {
158174
// ignore if fails
159175
console.error("Error fetching OpenRouter generation details:", error)
160176
}
161-
}
162177

178+
}
163179
getModel(): { id: string; info: ModelInfo } {
164180
const modelId = this.options.openRouterModelId
165181
const modelInfo = this.options.openRouterModelInfo

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type GlobalStateKey =
6161
| "azureApiVersion"
6262
| "openRouterModelId"
6363
| "openRouterModelInfo"
64+
| "openRouterUseMiddleOutTransform"
6465
| "allowedCommands"
6566
| "soundEnabled"
6667

@@ -391,6 +392,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
391392
azureApiVersion,
392393
openRouterModelId,
393394
openRouterModelInfo,
395+
openRouterUseMiddleOutTransform,
394396
} = message.apiConfiguration
395397
await this.updateGlobalState("apiProvider", apiProvider)
396398
await this.updateGlobalState("apiModelId", apiModelId)
@@ -416,6 +418,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
416418
await this.updateGlobalState("azureApiVersion", azureApiVersion)
417419
await this.updateGlobalState("openRouterModelId", openRouterModelId)
418420
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
421+
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
419422
if (this.cline) {
420423
this.cline.api = buildApiHandler(message.apiConfiguration)
421424
}
@@ -943,6 +946,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
943946
azureApiVersion,
944947
openRouterModelId,
945948
openRouterModelInfo,
949+
openRouterUseMiddleOutTransform,
946950
lastShownAnnouncementId,
947951
customInstructions,
948952
alwaysAllowReadOnly,
@@ -977,6 +981,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
977981
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
978982
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
979983
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
984+
this.getGlobalState("openRouterUseMiddleOutTransform") as Promise<boolean | undefined>,
980985
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
981986
this.getGlobalState("customInstructions") as Promise<string | undefined>,
982987
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
@@ -1028,6 +1033,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10281033
azureApiVersion,
10291034
openRouterModelId,
10301035
openRouterModelInfo,
1036+
openRouterUseMiddleOutTransform,
10311037
},
10321038
lastShownAnnouncementId,
10331039
customInstructions,

0 commit comments

Comments
 (0)