Skip to content

Commit 7a7cc33

Browse files
committed
fix: robust AI response parsing and fallbacks
1 parent 51797c9 commit 7a7cc33

2 files changed

Lines changed: 94 additions & 18 deletions

File tree

server/api/link/ai.get.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,35 @@ export default eventHandler(async (event) => {
6161

6262
const response = await AI.run(aiModel as keyof AiModels, {
6363
messages,
64-
chat_template_kwargs: {
65-
enable_thinking: false,
66-
},
6764
}) as AiChatResponse
6865

6966
let content = response.response ?? response.choices?.[0]?.message?.content ?? ''
7067

68+
if (!content || content.trim() === '') {
69+
console.error('AI.run() returned an empty response. Full response object:', JSON.stringify(response))
70+
// Fallback: try to generate a simple slug from the URL domain/path
71+
try {
72+
const urlObj = new URL(url)
73+
const pathSegments = urlObj.pathname.split('/').filter(Boolean)
74+
const domain = urlObj.hostname.split('.').filter(p => !['www', 'com', 'org', 'net', 'io'].includes(p))[0] || urlObj.hostname.split('.')[0]
75+
const fallbackSlug = pathSegments.length > 0 ? pathSegments[pathSegments.length - 1] : domain
76+
77+
if (fallbackSlug) {
78+
return {
79+
slug: fallbackSlug.toLowerCase().replace(/[^a-z0-9-]/g, '-').substring(0, 50),
80+
}
81+
}
82+
}
83+
catch (e) {
84+
console.error('Fallback slug generation failed:', e)
85+
}
86+
87+
throw createError({
88+
statusCode: 500,
89+
statusMessage: 'AI returned an empty response and fallback failed',
90+
})
91+
}
92+
7193
// 1. Try to strip markdown code block wrapper (e.g. ```json\n{...}\n```)
7294
const codeBlockMatch = content.match(/```(?:json)?\n([\s\S]*?)```/)
7395
if (codeBlockMatch?.[1]) {
@@ -123,7 +145,7 @@ export default eventHandler(async (event) => {
123145
parsed = { slug: bestCandidate.toLowerCase() }
124146
}
125147
else {
126-
console.error('AI response parsing failed. Raw content:', content)
148+
console.error('AI response parsing failed. Final content:', content)
127149
throw createError({
128150
statusCode: 500,
129151
statusMessage: 'Invalid AI response format',

server/api/link/og-ai.get.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,28 @@ export default eventHandler(async (event) => {
5454

5555
const response = await AI.run(aiModel as keyof AiModels, {
5656
messages,
57-
chat_template_kwargs: {
58-
enable_thinking: false,
59-
},
6057
}) as AiChatResponse
6158

6259
let content = response.response ?? response.choices?.[0]?.message?.content ?? ''
6360

61+
if (!content || content.trim() === '') {
62+
console.error('AI OG response is empty. Fallback initiated.')
63+
try {
64+
const urlObj = new URL(url)
65+
const domain = urlObj.hostname.replace('www.', '')
66+
return {
67+
title: domain,
68+
description: `Short link for ${url}`,
69+
}
70+
}
71+
catch {
72+
return {
73+
title: 'Short Link',
74+
description: 'Check out this link on Sink.',
75+
}
76+
}
77+
}
78+
6479
// 1. Try to strip markdown code block wrapper (e.g. ```json\n{...}\n```)
6580
const codeBlockMatch = content.match(/```(?:json)?\n([\s\S]*?)```/)
6681
if (codeBlockMatch?.[1]) {
@@ -91,22 +106,61 @@ export default eventHandler(async (event) => {
91106
// Ensure we always return an object with title and description properties
92107
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
93108
// Attempt to extract title and description as best effort from the raw content
94-
// E.g. "Title: My Title\nDescription: My description"
95-
const titleMatch = content.match(/title:\s*([^\n]+)/i) || content.match(/"title"\s*:\s*"((?:[^"\\]|\\.)+)"/)
96-
const descMatch = content.match(/description:\s*([^\n]+)/i) || content.match(/"description"\s*:\s*"((?:[^"\\]|\\.)+)"/)
109+
// Check multiple common formats: JSON-like, YAML-like, or plain text labels
110+
const titlePatterns = [
111+
/"title"\s*:\s*"([^"]+)"/,
112+
/title:\s*([^\n]+)/i,
113+
/^title\s*=\s*(.*)/im,
114+
/<title>([^<]+)<\/title>/i,
115+
]
116+
117+
const descPatterns = [
118+
/"description"\s*:\s*"([^"]+)"/,
119+
/description:\s*([^\n]+)/i,
120+
/^description\s*=\s*(.*)/im,
121+
]
122+
123+
let extractedTitle = ''
124+
let extractedDesc = ''
125+
126+
for (const pattern of titlePatterns) {
127+
const match = content.match(pattern)
128+
if (match?.[1]) {
129+
extractedTitle = match[1].replace(/^["']|["']$/g, '').trim()
130+
break
131+
}
132+
}
97133

98-
if (titleMatch && titleMatch[1] && descMatch && descMatch[1]) {
134+
for (const pattern of descPatterns) {
135+
const match = content.match(pattern)
136+
if (match?.[1]) {
137+
extractedDesc = match[1].replace(/^["']|["']$/g, '').trim()
138+
break
139+
}
140+
}
141+
142+
if (extractedTitle || extractedDesc) {
99143
parsed = {
100-
title: titleMatch[1].replace(/^["']|["']$/g, '').trim(),
101-
description: descMatch[1].replace(/^["']|["']$/g, '').trim(),
144+
title: extractedTitle || 'Link',
145+
description: extractedDesc || 'No description provided.',
102146
}
103147
}
104148
else {
105-
console.error('AI OG response parsing failed. Raw content:', content)
106-
throw createError({
107-
statusCode: 500,
108-
statusMessage: 'Invalid AI response format',
109-
})
149+
console.error('AI OG response parsing failed. Final content:', content)
150+
// Final fallback instead of throwing
151+
try {
152+
const urlObj = new URL(url)
153+
parsed = {
154+
title: urlObj.hostname,
155+
description: `Short link for ${url}`,
156+
}
157+
}
158+
catch {
159+
parsed = {
160+
title: 'Short Link',
161+
description: 'No metadata available.',
162+
}
163+
}
110164
}
111165
}
112166

0 commit comments

Comments
 (0)