Skip to content

Commit 89826f0

Browse files
authored
fix: filter out markdown delimiters from next edit responses (#8056)
* fix: filter out markdown delimiters from next edit responses * fix: silence noisy console logs (debug now) and capture accept/reject feedback * fix: console.log -> debug * fix: console.log -> debug * fix feedback * fix: respect Telemetry * requestId * fix: lint * bump
1 parent a57eb80 commit 89826f0

File tree

17 files changed

+254
-33
lines changed

17 files changed

+254
-33
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { postprocessCompletion } from "./index";
2+
3+
describe("postprocessCompletion - removeBackticks", () => {
4+
const mockLLM = { model: "test-model" } as any;
5+
6+
it("should remove first line starting with ``` and last line that is ```", () => {
7+
const completion =
8+
"```typescript\nfunction hello() {\n return 'world';\n}\n```";
9+
const result = postprocessCompletion({
10+
completion,
11+
llm: mockLLM,
12+
prefix: "",
13+
suffix: "",
14+
});
15+
expect(result).toBe("function hello() {\n return 'world';\n}");
16+
});
17+
18+
it("should remove only first line if it starts with ```", () => {
19+
const completion = "```javascript\nconst x = 5;\nconsole.log(x);";
20+
const result = postprocessCompletion({
21+
completion,
22+
llm: mockLLM,
23+
prefix: "",
24+
suffix: "",
25+
});
26+
expect(result).toBe("const x = 5;\nconsole.log(x);");
27+
});
28+
29+
it("should remove only last line if it is ```", () => {
30+
const completion = "const y = 10;\nconsole.log(y);\n```";
31+
const result = postprocessCompletion({
32+
completion,
33+
llm: mockLLM,
34+
prefix: "",
35+
suffix: "",
36+
});
37+
expect(result).toBe("const y = 10;\nconsole.log(y);");
38+
});
39+
40+
it("should not modify completion without backticks", () => {
41+
const completion = "function test() {\n return true;\n}";
42+
const result = postprocessCompletion({
43+
completion,
44+
llm: mockLLM,
45+
prefix: "",
46+
suffix: "",
47+
});
48+
expect(result).toBe("function test() {\n return true;\n}");
49+
});
50+
51+
it("should handle completion with backticks in the middle", () => {
52+
const completion = "const str = `template ${literal}`;\nconsole.log(str);";
53+
const result = postprocessCompletion({
54+
completion,
55+
llm: mockLLM,
56+
prefix: "",
57+
suffix: "",
58+
});
59+
expect(result).toBe(
60+
"const str = `template ${literal}`;\nconsole.log(str);",
61+
);
62+
});
63+
64+
it("should handle first line with leading whitespace before ```", () => {
65+
const completion = " ```python\ndef hello():\n pass\n```";
66+
const result = postprocessCompletion({
67+
completion,
68+
llm: mockLLM,
69+
prefix: "",
70+
suffix: "",
71+
});
72+
expect(result).toBe("def hello():\n pass");
73+
});
74+
75+
it("should handle last line with whitespace around ```", () => {
76+
const completion = "```\ncode here\n ``` ";
77+
const result = postprocessCompletion({
78+
completion,
79+
llm: mockLLM,
80+
prefix: "",
81+
suffix: "",
82+
});
83+
expect(result).toBe("code here");
84+
});
85+
86+
it("should handle single line completion", () => {
87+
const completion = "const x = 5;";
88+
const result = postprocessCompletion({
89+
completion,
90+
llm: mockLLM,
91+
prefix: "",
92+
suffix: "",
93+
});
94+
expect(result).toBe("const x = 5;");
95+
});
96+
97+
it("should handle empty completion", () => {
98+
const completion = "";
99+
const result = postprocessCompletion({
100+
completion,
101+
llm: mockLLM,
102+
prefix: "",
103+
suffix: "",
104+
});
105+
expect(result).toBeUndefined();
106+
});
107+
108+
it("should not remove ``` if it's not on its own line at the end", () => {
109+
const completion = "```typescript\nconst x = 5; // end```";
110+
const result = postprocessCompletion({
111+
completion,
112+
llm: mockLLM,
113+
prefix: "",
114+
suffix: "",
115+
});
116+
expect(result).toBe("const x = 5; // end```");
117+
});
118+
});

core/autocomplete/postprocessing/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,38 @@ function isBlank(completion: string): boolean {
5252
return completion.trim().length === 0;
5353
}
5454

55+
/**
56+
* Removes markdown code block delimiters from completion.
57+
* Removes the first line if it starts with "```" and the last line if it is exactly "```".
58+
*/
59+
function removeBackticks(completion: string): string {
60+
const lines = completion.split("\n");
61+
62+
if (lines.length === 0) {
63+
return completion;
64+
}
65+
66+
let startIdx = 0;
67+
let endIdx = lines.length;
68+
69+
// Remove first line if it starts with ```
70+
if (lines[0].trimStart().startsWith("```")) {
71+
startIdx = 1;
72+
}
73+
74+
// Remove last line if it is exactly ```
75+
if (lines.length > startIdx && lines[lines.length - 1].trim() === "```") {
76+
endIdx = lines.length - 1;
77+
}
78+
79+
// If we removed lines, return the modified completion
80+
if (startIdx > 0 || endIdx < lines.length) {
81+
return lines.slice(startIdx, endIdx).join("\n");
82+
}
83+
84+
return completion;
85+
}
86+
5587
export function postprocessCompletion({
5688
completion,
5789
llm,
@@ -156,5 +188,8 @@ export function postprocessCompletion({
156188
completion = completion.slice(1);
157189
}
158190

191+
// Remove markdown code block delimiters
192+
completion = removeBackticks(completion);
193+
159194
return completion;
160195
}

core/config/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ declare global {
7878
7979
export interface ILLM extends LLMOptions {
8080
get providerName(): string;
81-
81+
8282
uniqueId: string;
83+
lastRequestId?: string;
8384
model: string;
8485
8586
title?: string;

core/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,7 @@ export class Core {
929929
});
930930

931931
on("files/closed", async ({ data }) => {
932-
console.log("deleteChain called from files/closed");
932+
console.debug("deleteChain called from files/closed");
933933
await NextEditProvider.getInstance().deleteChain();
934934

935935
try {

core/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ export interface ILLM
9999

100100
autocompleteOptions?: Partial<TabAutocompleteOptions>;
101101

102+
lastRequestId?: string;
103+
102104
complete(
103105
prompt: string,
104106
signal: AbortSignal,

core/llm/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,15 @@ export abstract class BaseLLM implements ILLM {
193193

194194
isFromAutoDetect?: boolean;
195195

196+
lastRequestId: string | undefined;
197+
196198
private _llmOptions: LLMOptions;
197199

198200
protected openaiAdapter?: BaseLlmApi;
199201

200202
constructor(_options: LLMOptions) {
201203
this._llmOptions = _options;
204+
this.lastRequestId = undefined;
202205

203206
// Set default options
204207
const options = {
@@ -594,6 +597,7 @@ export abstract class BaseLLM implements ILLM {
594597
signal: AbortSignal,
595598
options: LLMFullCompletionOptions = {},
596599
): AsyncGenerator<string> {
600+
this.lastRequestId = undefined;
597601
const { completionOptions, logEnabled } =
598602
this._parseCompletionOptions(options);
599603
const interaction = logEnabled
@@ -623,6 +627,9 @@ export abstract class BaseLLM implements ILLM {
623627
signal,
624628
);
625629
for await (const chunk of stream) {
630+
if (!this.lastRequestId && typeof (chunk as any).id === "string") {
631+
this.lastRequestId = (chunk as any).id;
632+
}
626633
const result = fromChatCompletionChunk(chunk);
627634
if (result) {
628635
const content = renderChatMessage(result);
@@ -706,6 +713,7 @@ export abstract class BaseLLM implements ILLM {
706713
signal: AbortSignal,
707714
options: LLMFullCompletionOptions = {},
708715
) {
716+
this.lastRequestId = undefined;
709717
const { completionOptions, logEnabled, raw } =
710718
this._parseCompletionOptions(options);
711719
const interaction = logEnabled
@@ -745,6 +753,7 @@ export abstract class BaseLLM implements ILLM {
745753
{ ...toCompleteBody(prompt, completionOptions), stream: false },
746754
signal,
747755
);
756+
this.lastRequestId = response.id ?? this.lastRequestId;
748757
completion = response.choices[0]?.text ?? "";
749758
yield completion;
750759
} else {
@@ -756,6 +765,9 @@ export abstract class BaseLLM implements ILLM {
756765
},
757766
signal,
758767
)) {
768+
if (!this.lastRequestId && typeof (chunk as any).id === "string") {
769+
this.lastRequestId = (chunk as any).id;
770+
}
759771
const content = chunk.choices[0]?.text ?? "";
760772
completion += content;
761773
interaction?.logItem({
@@ -835,6 +847,7 @@ export abstract class BaseLLM implements ILLM {
835847
signal: AbortSignal,
836848
options: LLMFullCompletionOptions = {},
837849
) {
850+
this.lastRequestId = undefined;
838851
const { completionOptions, logEnabled, raw } =
839852
this._parseCompletionOptions(options);
840853
const interaction = logEnabled
@@ -876,6 +889,7 @@ export abstract class BaseLLM implements ILLM {
876889
},
877890
signal,
878891
);
892+
this.lastRequestId = result.id ?? this.lastRequestId;
879893
completion = result.choices[0].text;
880894
} else {
881895
completion = await this._complete(prompt, signal, completionOptions);
@@ -985,6 +999,7 @@ export abstract class BaseLLM implements ILLM {
985999
options: LLMFullCompletionOptions = {},
9861000
messageOptions?: MessageOption,
9871001
): AsyncGenerator<ChatMessage, PromptLog> {
1002+
this.lastRequestId = undefined;
9881003
let { completionOptions, logEnabled } =
9891004
this._parseCompletionOptions(options);
9901005
const interaction = logEnabled
@@ -1054,6 +1069,7 @@ export abstract class BaseLLM implements ILLM {
10541069
{ ...body, stream: false },
10551070
signal,
10561071
);
1072+
this.lastRequestId = response.id ?? this.lastRequestId;
10571073
const msg = fromChatResponse(response);
10581074
yield msg;
10591075
completion = this._formatChatMessage(msg);
@@ -1071,6 +1087,12 @@ export abstract class BaseLLM implements ILLM {
10711087
signal,
10721088
);
10731089
for await (const chunk of stream) {
1090+
if (
1091+
!this.lastRequestId &&
1092+
typeof (chunk as any).id === "string"
1093+
) {
1094+
this.lastRequestId = (chunk as any).id;
1095+
}
10741096
const result = fromChatCompletionChunk(chunk);
10751097
if (result) {
10761098
completion += this._formatChatMessage(result);

core/nextEdit/NextEditLoggingService.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { COUNT_COMPLETION_REJECTED_AFTER } from "../util/parameters";
22

3+
import { fetchwithRequestOptions } from "@continuedev/fetch";
4+
import { getControlPlaneEnvSync } from "../control-plane/env";
35
import { DataLogger } from "../data/log";
46
import { Telemetry } from "../util/posthog";
57
import { NextEditOutcome } from "./types";
@@ -93,6 +95,9 @@ export class NextEditLoggingService {
9395
outcome.accepted = true;
9496
outcome.aborted = false;
9597
this.logNextEditOutcome(outcome);
98+
if (outcome.requestId) {
99+
void this.logAcceptReject(outcome.requestId, true);
100+
}
96101
this._outcomes.delete(completionId);
97102
return outcome;
98103
}
@@ -111,6 +116,9 @@ export class NextEditLoggingService {
111116
outcome.accepted = false;
112117
outcome.aborted = false;
113118
this.logNextEditOutcome(outcome);
119+
if (outcome.requestId) {
120+
void this.logAcceptReject(outcome.requestId, false);
121+
}
114122
this._outcomes.delete(completionId);
115123
return outcome;
116124
}
@@ -121,6 +129,7 @@ export class NextEditLoggingService {
121129
clearTimeout(this._logRejectionTimeouts.get(completionId)!);
122130
this._logRejectionTimeouts.delete(completionId);
123131
}
132+
124133
if (this._outcomes.has(completionId)) {
125134
this._outcomes.delete(completionId);
126135
}
@@ -142,6 +151,9 @@ export class NextEditLoggingService {
142151
outcome.accepted = false;
143152
outcome.aborted = false;
144153
this.logNextEditOutcome(outcome);
154+
if (outcome.requestId) {
155+
void this.logAcceptReject(outcome.requestId, false);
156+
}
145157
this._logRejectionTimeouts.delete(completionId);
146158
this._outcomes.delete(completionId);
147159
}, COUNT_COMPLETION_REJECTED_AFTER);
@@ -245,4 +257,30 @@ export class NextEditLoggingService {
245257
// const { prompt, completion, prefix, suffix, ...restOfOutcome } = outcome;
246258
void Telemetry.capture("nextEditOutcome", outcome, true);
247259
}
260+
261+
private async logAcceptReject(
262+
requestId: string,
263+
accepted: boolean,
264+
): Promise<void> {
265+
try {
266+
if (!Telemetry.client) {
267+
return;
268+
}
269+
270+
const controlPlaneEnv = getControlPlaneEnvSync("production");
271+
await fetchwithRequestOptions(
272+
new URL("model-proxy/v1/feedback", controlPlaneEnv.CONTROL_PLANE_URL),
273+
{
274+
method: "POST",
275+
headers: {
276+
"Content-Type": "application/json",
277+
},
278+
body: JSON.stringify({
279+
requestId,
280+
accepted,
281+
}),
282+
},
283+
);
284+
} catch (error) {}
285+
}
248286
}

core/nextEdit/providers/BaseNextEditProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export abstract class BaseNextEditModelProvider {
309309
outcomeCtx.completionId || outcomeCtx.helper.input.completionId,
310310
gitRepo: await outcomeCtx.ide.getRepoName(outcomeCtx.helper.filepath),
311311
uniqueId: await outcomeCtx.ide.getUniqueId(),
312+
requestId: outcomeCtx.llm.lastRequestId,
312313
timestamp: Date.now(),
313314
fileUri: outcomeCtx.helper.filepath,
314315
workspaceDirUri:

core/nextEdit/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface NextEditOutcome extends TabAutocompleteOptions {
5050
completionId: string;
5151
gitRepo?: string;
5252
uniqueId: string;
53+
requestId?: string;
5354
timestamp: number;
5455

5556
// New for Next Edit.

0 commit comments

Comments
 (0)