@@ -14,6 +14,24 @@ const CHAT_MODEL = process.env.OPENAI_CHAT_MODEL || 'gpt-5.4-mini'
1414/** Max documentation chunks returned per search to ground an answer. */
1515const SEARCH_LIMIT = 6
1616
17+ /** Candidates pulled before locale filtering, so a locale still yields SEARCH_LIMIT results. */
18+ const SEARCH_CANDIDATES = SEARCH_LIMIT * 4
19+
20+ /** Locales the docs are published in (mirrors the site search route). */
21+ const KNOWN_LOCALES = [ 'en' , 'es' , 'fr' , 'de' , 'ja' , 'zh' ]
22+ const DEFAULT_LOCALE = 'en'
23+
24+ /**
25+ * Match a chunk's source document to a locale, mirroring the site search route:
26+ * non-English docs are prefixed with their locale segment; unprefixed docs are
27+ * English.
28+ */
29+ function matchesLocale ( sourceDocument : string , locale : string ) : boolean {
30+ const firstSegment = sourceDocument . split ( '/' ) [ 0 ]
31+ if ( KNOWN_LOCALES . includes ( firstSegment ) ) return firstSegment === locale
32+ return locale === DEFAULT_LOCALE
33+ }
34+
1735/**
1836 * Abuse guards. This endpoint proxies a paid LLM, so an unauthenticated public
1937 * route is a target for scripted "free inference". These bounds cap the cost of
@@ -28,6 +46,8 @@ const MAX_MESSAGES = 200
2846const MAX_USER_INPUT_CHARS = 400_000
2947const MAX_OUTPUT_TOKENS = 4000
3048const MAX_STEPS = 6
49+ /** Backstop on the whole serialized payload — blocks stuffing assistant/tool parts past the user-text cap. */
50+ const MAX_TOTAL_CHARS = 1_000_000
3151
3252/** Total length of user-authored text across the conversation. */
3353function userInputChars ( messages : UIMessage [ ] ) : number {
@@ -84,7 +104,7 @@ Guidelines:
84104 * Vector search over the docs embeddings, returning the most relevant chunks
85105 * with their source links so the model can ground and cite its answer.
86106 */
87- async function searchDocs ( query : string ) {
107+ async function searchDocs ( query : string , locale : string ) {
88108 const embedding = await generateSearchEmbedding ( query )
89109 const vectorLiteral = JSON . stringify ( embedding )
90110
@@ -93,32 +113,47 @@ async function searchDocs(query: string) {
93113 title : docsEmbeddings . headerText ,
94114 url : docsEmbeddings . sourceLink ,
95115 content : docsEmbeddings . chunkText ,
96- similarity : sql < number > `1 - ( ${ docsEmbeddings . embedding } <=> ${ vectorLiteral } ::vector)` ,
116+ sourceDocument : docsEmbeddings . sourceDocument ,
97117 } )
98118 . from ( docsEmbeddings )
99119 . orderBy ( sql `${ docsEmbeddings . embedding } <=> ${ vectorLiteral } ::vector` )
100- . limit ( SEARCH_LIMIT )
101-
102- return rows . map ( ( row ) => ( {
103- title : row . title ,
104- url : row . url ,
105- content : row . content ,
106- } ) )
120+ . limit ( SEARCH_CANDIDATES )
121+
122+ return rows
123+ . filter ( ( row ) => matchesLocale ( row . sourceDocument , locale ) )
124+ . slice ( 0 , SEARCH_LIMIT )
125+ . map ( ( row ) => ( {
126+ title : row . title ,
127+ url : row . url ,
128+ content : row . content ,
129+ } ) )
107130}
108131
109132export async function POST ( req : Request ) {
110133 if ( ! isAllowedOrigin ( req ) ) {
111134 return new Response ( 'Forbidden' , { status : 403 } )
112135 }
113136
114- const { messages } : { messages : UIMessage [ ] } = await req . json ( )
137+ let body : { messages : UIMessage [ ] ; locale ?: string }
138+ try {
139+ body = await req . json ( )
140+ } catch {
141+ return new Response ( 'Invalid JSON' , { status : 400 } )
142+ }
143+ const { messages } = body
144+ const locale = KNOWN_LOCALES . includes ( body . locale ?? '' )
145+ ? ( body . locale as string )
146+ : DEFAULT_LOCALE
115147
116148 if ( ! Array . isArray ( messages ) || messages . length === 0 || messages . length > MAX_MESSAGES ) {
117149 return new Response ( 'Invalid request' , { status : 400 } )
118150 }
119151 if ( userInputChars ( messages ) > MAX_USER_INPUT_CHARS ) {
120152 return new Response ( 'Request too large' , { status : 413 } )
121153 }
154+ if ( JSON . stringify ( messages ) . length > MAX_TOTAL_CHARS ) {
155+ return new Response ( 'Request too large' , { status : 413 } )
156+ }
122157
123158 const result = streamText ( {
124159 model : openai ( CHAT_MODEL ) ,
@@ -133,7 +168,7 @@ export async function POST(req: Request) {
133168 inputSchema : z . object ( {
134169 query : z . string ( ) . describe ( 'A focused natural-language search query.' ) ,
135170 } ) ,
136- execute : async ( { query } ) => searchDocs ( query ) ,
171+ execute : async ( { query } ) => searchDocs ( query , locale ) ,
137172 } ) ,
138173 } ,
139174 } )
0 commit comments