Skip to content

Commit 2ca4738

Browse files
committed
Revert "Fix bug where OpenRouter/Cline providers generation endpoint failed (RooCodeInc#2262)"
This reverts commit 8cc7759.
1 parent b4a2d5a commit 2ca4738

File tree

5 files changed

+72
-143
lines changed

5 files changed

+72
-143
lines changed

src/api/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { OllamaHandler } from "./providers/ollama"
99
import { LmStudioHandler } from "./providers/lmstudio"
1010
import { GeminiHandler } from "./providers/gemini"
1111
import { OpenAiNativeHandler } from "./providers/openai-native"
12-
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
12+
import { ApiStream } from "./transform/stream"
1313
import { DeepSeekHandler } from "./providers/deepseek"
1414
import { RequestyHandler } from "./providers/requesty"
1515
import { TogetherHandler } from "./providers/together"
@@ -25,7 +25,6 @@ import { SambanovaHandler } from "./providers/sambanova"
2525
export interface ApiHandler {
2626
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
2727
getModel(): { id: string; info: ModelInfo }
28-
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
2928
}
3029

3130
export interface SingleCompletionHandler {

src/api/providers/cline.ts

Lines changed: 23 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33
import { ApiHandler } from "../"
44
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
5-
import { createOpenRouterStream } from "../transform/openrouter-stream"
6-
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
5+
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
6+
import { ApiStream } from "../transform/stream"
77
import axios from "axios"
8-
import { OpenRouterErrorResponse } from "./types"
98

109
export class ClineHandler implements ApiHandler {
1110
private options: ApiHandlerOptions
1211
private client: OpenAI
13-
lastGenerationId?: string
1412

1513
constructor(options: ApiHandlerOptions) {
1614
this.options = options
@@ -21,79 +19,37 @@ export class ClineHandler implements ApiHandler {
2119
}
2220

2321
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
24-
this.lastGenerationId = undefined
25-
26-
const stream = await createOpenRouterStream(
22+
const model = this.getModel()
23+
const genId = yield* streamOpenRouterFormatRequest(
2724
this.client,
2825
systemPrompt,
2926
messages,
30-
this.getModel(),
27+
model,
3128
this.options.o3MiniReasoningEffort,
3229
this.options.thinkingBudgetTokens,
3330
this.options.openRouterProviderSorting,
3431
)
3532

36-
for await (const chunk of stream) {
37-
// openrouter returns an error object instead of the openai sdk throwing an error
38-
if ("error" in chunk) {
39-
const error = chunk.error as OpenRouterErrorResponse["error"]
40-
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
41-
// Include metadata in the error message if available
42-
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
43-
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
44-
}
45-
46-
if (!this.lastGenerationId && chunk.id) {
47-
this.lastGenerationId = chunk.id
48-
}
49-
50-
const delta = chunk.choices[0]?.delta
51-
if (delta?.content) {
52-
yield {
53-
type: "text",
54-
text: delta.content,
55-
}
56-
}
57-
58-
// Reasoning tokens are returned separately from the content
59-
if ("reasoning" in delta && delta.reasoning) {
60-
yield {
61-
type: "reasoning",
62-
// @ts-ignore-next-line
63-
reasoning: delta.reasoning,
64-
}
65-
}
66-
}
67-
68-
const apiStreamUsage = await this.getApiStreamUsage()
69-
if (apiStreamUsage) {
70-
yield apiStreamUsage
71-
}
72-
}
73-
74-
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
75-
if (this.lastGenerationId) {
76-
try {
77-
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${this.lastGenerationId}`, {
78-
headers: {
79-
Authorization: `Bearer ${this.options.clineApiKey}`,
80-
},
81-
timeout: 15_000, // this request hangs sometimes
82-
})
83-
84-
const generation = response.data
85-
return {
86-
type: "usage",
87-
inputTokens: generation?.native_tokens_prompt || 0,
88-
outputTokens: generation?.native_tokens_completion || 0,
89-
totalCost: generation?.total_cost || 0,
90-
}
91-
} catch (error) {
92-
// ignore if fails
93-
console.error("Error fetching cline generation details:", error)
33+
try {
34+
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${genId}`, {
35+
headers: {
36+
Authorization: `Bearer ${this.options.clineApiKey}`,
37+
},
38+
timeout: 5_000, // this request hangs sometimes
39+
})
40+
41+
const generation = response.data
42+
console.log("cline generation details:", generation)
43+
yield {
44+
type: "usage",
45+
inputTokens: generation?.native_tokens_prompt || 0,
46+
outputTokens: generation?.native_tokens_completion || 0,
47+
totalCost: generation?.total_cost || 0,
9448
}
49+
} catch (error) {
50+
// ignore if fails
51+
console.error("Error fetching cline generation details:", error)
9552
}
96-
return undefined
9753
}
9854

9955
getModel(): { id: string; info: ModelInfo } {

src/api/providers/openrouter.ts

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import axios from "axios"
33
import delay from "delay"
44
import OpenAI from "openai"
5+
import { withRetry } from "../retry"
56
import { ApiHandler } from "../"
67
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
7-
import { withRetry } from "../retry"
8-
import { createOpenRouterStream } from "../transform/openrouter-stream"
9-
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
8+
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
9+
import { ApiStream } from "../transform/stream"
10+
import { convertToR1Format } from "../transform/r1-format"
1011
import { OpenRouterErrorResponse } from "./types"
1112

1213
export class OpenRouterHandler implements ApiHandler {
1314
private options: ApiHandlerOptions
1415
private client: OpenAI
15-
lastGenerationId?: string
1616

1717
constructor(options: ApiHandlerOptions) {
1818
this.options = options
@@ -28,64 +28,24 @@ export class OpenRouterHandler implements ApiHandler {
2828

2929
@withRetry()
3030
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
31-
this.lastGenerationId = undefined
32-
33-
const stream = await createOpenRouterStream(
31+
const model = this.getModel()
32+
const genId = yield* streamOpenRouterFormatRequest(
3433
this.client,
3534
systemPrompt,
3635
messages,
37-
this.getModel(),
36+
model,
3837
this.options.o3MiniReasoningEffort,
3938
this.options.thinkingBudgetTokens,
4039
this.options.openRouterProviderSorting,
4140
)
4241

43-
for await (const chunk of stream) {
44-
// openrouter returns an error object instead of the openai sdk throwing an error
45-
if ("error" in chunk) {
46-
const error = chunk.error as OpenRouterErrorResponse["error"]
47-
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
48-
// Include metadata in the error message if available
49-
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
50-
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
51-
}
52-
53-
if (!this.lastGenerationId && chunk.id) {
54-
this.lastGenerationId = chunk.id
55-
}
56-
57-
const delta = chunk.choices[0]?.delta
58-
if (delta?.content) {
59-
yield {
60-
type: "text",
61-
text: delta.content,
62-
}
63-
}
64-
65-
// Reasoning tokens are returned separately from the content
66-
if ("reasoning" in delta && delta.reasoning) {
67-
yield {
68-
type: "reasoning",
69-
// @ts-ignore-next-line
70-
reasoning: delta.reasoning,
71-
}
72-
}
73-
}
74-
75-
const apiStreamUsage = await this.getApiStreamUsage()
76-
if (apiStreamUsage) {
77-
yield apiStreamUsage
78-
}
79-
}
80-
81-
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
82-
if (this.lastGenerationId) {
42+
if (genId) {
8343
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
8444
try {
85-
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
45+
const generationIterator = this.fetchGenerationDetails(genId)
8646
const generation = (await generationIterator.next()).value
8747
// console.log("OpenRouter generation details:", generation)
88-
return {
48+
yield {
8949
type: "usage",
9050
// cacheWriteTokens: 0,
9151
// cacheReadTokens: 0,
@@ -99,7 +59,6 @@ export class OpenRouterHandler implements ApiHandler {
9959
console.error("Error fetching OpenRouter generation details:", error)
10060
}
10161
}
102-
return undefined
10362
}
10463

10564
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
@@ -110,7 +69,7 @@ export class OpenRouterHandler implements ApiHandler {
11069
headers: {
11170
Authorization: `Bearer ${this.options.openRouterApiKey}`,
11271
},
113-
timeout: 15_000, // this request hangs sometimes
72+
timeout: 5_000, // this request hangs sometimes
11473
})
11574
yield response.data?.data
11675
} catch (error) {

src/api/transform/openrouter-stream.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import { Anthropic } from "@anthropic-ai/sdk"
66
import OpenAI from "openai"
77
import { OpenRouterErrorResponse } from "../providers/types"
88

9-
export async function createOpenRouterStream(
9+
export async function* streamOpenRouterFormatRequest(
1010
client: OpenAI,
1111
systemPrompt: string,
1212
messages: Anthropic.Messages.MessageParam[],
1313
model: { id: string; info: ModelInfo },
1414
o3MiniReasoningEffort?: string,
1515
thinkingBudgetTokens?: number,
1616
openRouterProviderSorting?: string,
17-
) {
17+
): AsyncGenerator<ApiStreamChunk, string | undefined, unknown> {
1818
// Convert Anthropic messages to OpenAI format
1919
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
2020
{ role: "system", content: systemPrompt },
@@ -149,5 +149,39 @@ export async function createOpenRouterStream(
149149
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
150150
})
151151

152-
return stream
152+
let genId: string | undefined
153+
154+
for await (const chunk of stream) {
155+
// openrouter returns an error object instead of the openai sdk throwing an error
156+
if ("error" in chunk) {
157+
const error = chunk.error as OpenRouterErrorResponse["error"]
158+
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
159+
// Include metadata in the error message if available
160+
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
161+
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
162+
}
163+
164+
if (!genId && chunk.id) {
165+
genId = chunk.id
166+
}
167+
168+
const delta = chunk.choices[0]?.delta
169+
if (delta?.content) {
170+
yield {
171+
type: "text",
172+
text: delta.content,
173+
}
174+
}
175+
176+
// Reasoning tokens are returned separately from the content
177+
if ("reasoning" in delta && delta.reasoning) {
178+
yield {
179+
type: "reasoning",
180+
// @ts-ignore-next-line
181+
reasoning: delta.reasoning,
182+
}
183+
}
184+
}
185+
186+
return genId
153187
}

src/core/Cline.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3374,15 +3374,13 @@ export class Cline {
33743374
let assistantMessage = ""
33753375
let reasoningMessage = ""
33763376
this.isStreaming = true
3377-
let didReceiveUsageChunk = false
33783377
try {
33793378
for await (const chunk of stream) {
33803379
if (!chunk) {
33813380
continue
33823381
}
33833382
switch (chunk.type) {
33843383
case "usage":
3385-
didReceiveUsageChunk = true
33863384
inputTokens += chunk.inputTokens
33873385
outputTokens += chunk.outputTokens
33883386
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
@@ -3452,23 +3450,6 @@ export class Cline {
34523450
this.isStreaming = false
34533451
}
34543452

3455-
// OpenRouter/Cline may not return token usage as part of the stream (since it may abort early), so we fetch after the stream is finished
3456-
// (updateApiReq below will update the api_req_started message with the usage details. we do this async so it updates the api_req_started message in the background)
3457-
if (!didReceiveUsageChunk) {
3458-
this.api.getApiStreamUsage?.().then(async (apiStreamUsage) => {
3459-
if (apiStreamUsage) {
3460-
inputTokens += apiStreamUsage.inputTokens
3461-
outputTokens += apiStreamUsage.outputTokens
3462-
cacheWriteTokens += apiStreamUsage.cacheWriteTokens ?? 0
3463-
cacheReadTokens += apiStreamUsage.cacheReadTokens ?? 0
3464-
totalCost = apiStreamUsage.totalCost
3465-
}
3466-
updateApiReqMsg()
3467-
await this.saveClineMessages()
3468-
await this.providerRef.deref()?.postStateToWebview()
3469-
})
3470-
}
3471-
34723453
// need to call here in case the stream was aborted
34733454
if (this.abort) {
34743455
throw new Error("Cline instance aborted")

0 commit comments

Comments
 (0)