Skip to content

Commit 4125dec

Browse files
committed
add in manual stop sequence detection
1 parent 6f4f56b commit 4125dec

File tree

2 files changed

+96
-8
lines changed

2 files changed

+96
-8
lines changed

backend/src/llm-apis/vercel-ai-sdk/ai-sdk.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { buildArray } from '@codebuff/common/util/array'
99
import { convertCbToModelMessages } from '@codebuff/common/util/messages'
1010
import { errorToObject } from '@codebuff/common/util/object'
1111
import { withTimeout } from '@codebuff/common/util/promise'
12+
import { StopSequenceHandler } from '@codebuff/common/util/stop-sequence'
1213
import { generateCompactId } from '@codebuff/common/util/string'
1314
import { APICallError, generateObject, generateText, streamText } from 'ai'
1415

@@ -99,10 +100,6 @@ export const promptAiSdkStream = async function* (
99100
},
100101
'Skipping stream due to canceled user input',
101102
)
102-
yield {
103-
type: 'text',
104-
text: '',
105-
}
106103
return
107104
}
108105
const startTime = Date.now()
@@ -117,8 +114,18 @@ export const promptAiSdkStream = async function* (
117114
})
118115

119116
let content = ''
117+
const stopSequenceHandler = new StopSequenceHandler(options.stopSequences)
120118

121119
for await (const chunk of response.fullStream) {
120+
if (chunk.type !== 'text-delta') {
121+
const flushed = stopSequenceHandler.flush()
122+
if (flushed) {
123+
yield {
124+
type: 'text',
125+
text: flushed,
126+
}
127+
}
128+
}
122129
if (chunk.type === 'error') {
123130
logger.error(
124131
{
@@ -161,13 +168,33 @@ export const promptAiSdkStream = async function* (
161168
}
162169
}
163170
if (chunk.type === 'text-delta') {
164-
content += chunk.text
165-
yield {
166-
type: 'text',
167-
text: chunk.text,
171+
if (!options.stopSequences) {
172+
content += chunk.text
173+
if (chunk.text) {
174+
yield {
175+
type: 'text',
176+
text: chunk.text,
177+
}
178+
}
179+
continue
180+
}
181+
182+
const stopSequenceResult = stopSequenceHandler.process(chunk.text)
183+
if (stopSequenceResult.text) {
184+
yield {
185+
type: 'text',
186+
text: stopSequenceResult.text,
187+
}
168188
}
169189
}
170190
}
191+
const flushed = stopSequenceHandler.flush()
192+
if (flushed) {
193+
yield {
194+
type: 'text',
195+
text: flushed,
196+
}
197+
}
171198

172199
const messageId = (await response.response).id
173200
if (options.resolveMessageId) {

common/src/util/stop-sequence.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { suffixPrefixOverlap } from './string'
2+
3+
export class StopSequenceHandler {
4+
private buffer: string = ''
5+
private finished: boolean = false
6+
private stopSequences: string[]
7+
8+
constructor(stopSequences?: string[]) {
9+
this.stopSequences = stopSequences ?? []
10+
}
11+
12+
public process(
13+
text: string,
14+
):
15+
| { text: string; endOfStream: boolean }
16+
| { text: null; endOfStream: true } {
17+
if (this.finished) {
18+
return {
19+
text: null,
20+
endOfStream: true,
21+
}
22+
}
23+
this.buffer += text
24+
let longestOverlap = ''
25+
26+
for (const stopSequence of this.stopSequences) {
27+
const index = this.buffer.indexOf(stopSequence)
28+
if (index !== -1) {
29+
this.finished = true
30+
return {
31+
text: this.buffer.slice(0, index),
32+
endOfStream: true,
33+
}
34+
}
35+
}
36+
37+
for (const stopSequence of this.stopSequences) {
38+
const overlap = suffixPrefixOverlap(this.buffer, stopSequence)
39+
longestOverlap =
40+
overlap.length > longestOverlap.length ? overlap : longestOverlap
41+
}
42+
43+
const index = this.buffer.length - longestOverlap.length
44+
const processed = this.buffer.slice(0, index)
45+
this.buffer = this.buffer.slice(index)
46+
47+
return {
48+
text: processed,
49+
endOfStream: false,
50+
}
51+
}
52+
53+
public flush(): string {
54+
if (this.finished) {
55+
return ''
56+
}
57+
const b = this.buffer
58+
this.buffer = ''
59+
return b
60+
}
61+
}

0 commit comments

Comments
 (0)