@@ -172,18 +172,81 @@ func (p *PromptService) ParseResponse(text string) (*domain.StructuredResult, er
172172 jsonEnd := strings .LastIndex (text , "}" )
173173 if jsonStart >= 0 && jsonEnd > jsonStart {
174174 extracted := text [jsonStart : jsonEnd + 1 ]
175- if err2 := json .Unmarshal ([]byte (extracted ), & result ); err2 != nil {
176- return nil , fmt .Errorf ("failed to parse LLM response as JSON: %w\n Raw output: %s" , err , text [:min (500 , len (text ))])
175+ if err2 := json .Unmarshal ([]byte (extracted ), & result ); err2 == nil {
176+ helpers .Log .Info ().Msg ("Structured JSON parsed successfully (extracted from text)" )
177+ return & result , nil
177178 }
178- } else {
179- return nil , fmt .Errorf ("failed to parse LLM response as JSON: %w\n Raw output: %s" , err , text [:min (500 , len (text ))])
180179 }
180+
181+ // Try to recover truncated JSON by extracting release_notes field
182+ if recovered := recoverTruncatedJSON (text ); recovered != nil {
183+ helpers .Log .Warn ().Msg ("LLM response was truncated — recovered partial release notes" )
184+ return recovered , nil
185+ }
186+
187+ return nil , fmt .Errorf ("failed to parse LLM response as JSON: %w\n Raw output: %s" , err , text [:min (500 , len (text ))])
181188 }
182189
183190 helpers .Log .Info ().Msg ("Structured JSON parsed successfully" )
184191 return & result , nil
185192}
186193
194+ // recoverTruncatedJSON attempts to extract release_notes from a truncated JSON response.
195+ // This handles cases where the LLM hits its token limit mid-response.
196+ func recoverTruncatedJSON (text string ) * domain.StructuredResult {
197+ // Look for "release_notes" field value
198+ re := regexp .MustCompile (`"release_notes"\s*:\s*"` )
199+ loc := re .FindStringIndex (text )
200+ if loc == nil {
201+ return nil
202+ }
203+
204+ // Extract the string value, handling escaped characters
205+ start := loc [1 ]
206+ var sb strings.Builder
207+ i := start
208+ for i < len (text ) {
209+ if text [i ] == '\\' && i + 1 < len (text ) {
210+ switch text [i + 1 ] {
211+ case '"' :
212+ sb .WriteByte ('"' )
213+ case 'n' :
214+ sb .WriteByte ('\n' )
215+ case 't' :
216+ sb .WriteByte ('\t' )
217+ case '\\' :
218+ sb .WriteByte ('\\' )
219+ default :
220+ sb .WriteByte (text [i + 1 ])
221+ }
222+ i += 2
223+ continue
224+ }
225+ if text [i ] == '"' {
226+ break
227+ }
228+ sb .WriteByte (text [i ])
229+ i ++
230+ }
231+
232+ content := strings .TrimSpace (sb .String ())
233+ if content == "" {
234+ return nil
235+ }
236+
237+ // Try to extract suggested_version too
238+ version := ""
239+ versionRe := regexp .MustCompile (`"suggested_version"\s*:\s*"([^"]*)"` )
240+ if m := versionRe .FindStringSubmatch (text ); len (m ) > 1 {
241+ version = m [1 ]
242+ }
243+
244+ return & domain.StructuredResult {
245+ ReleaseNotes : content ,
246+ SuggestedVersion : version ,
247+ }
248+ }
249+
187250func (p * PromptService ) GenerateOutputPaths (basePath string ) domain.OutputConfig {
188251 if basePath == "" {
189252 return domain.OutputConfig {
0 commit comments