Repository navigation
fix(ai): retry unparseable article output and make AI failures traceable - #23
Merged
Merged
Conversation
X Article translation returned 500 in production on every run: the model
kept emitting structurally invalid JSON for `blocks` (one extra `}` per
block), and `Output.object` threw AI_NoObjectGeneratedError. The retry
loop only handled placeholder mismatches, so the exception aborted the
whole article.
- feed the model a `{ blockKey: text }` object instead of an array of
`{ key, text }`: the model mirrored each array element as its own
object and closed `blocks` after every key
- spell out the single-`blocks`-object contract in the system prompt
(all keys, no early close, two-key example)
- wrap `generateText` in try/catch so a parse failure is retried like a
placeholder mismatch instead of aborting the article
- keep the blocks that did validate when a batch fails; throw when
nothing was translated so a failure never surfaces as an empty success
- normalizeAIError now keeps `generatedText` (truncated) and
`finishReason` for NoObjectGeneratedError
- log the batch error on `ai.translate.article.batch`
- AC-TWEET-020: drive the real generateText + Output.object with
ai/test MockLanguageModelV4 to cover retry, partial acceptance,
fail-loudly and the input shape
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- feature doc: the batch input is now a `{ blockKey: text }` object, a
parse error is retried, and an all-batches failure is thrown instead
of returning an empty translation; note why the shape can only be
enforced by the prompt (openai-compatible does not send the schema)
- dev log: the real-endpoint reproduction (json_object sent, no schema,
finish_reason=stop, brace depth -5), the fix and the vitest pitfall
that mock-thrown errors are reported as unhandled
- AC list bumped to v1.7 / AC-TWEET-012~020
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
AI failures only returned a generic message and aiError.type — no
tweetId / articleId / igPostId, no model/provider — so a production
failure could not be traced back to the content that caused it.
- add buildAIFailure(error, { errorCode, status?, message?, context? }):
one failure body with success/error/status/message/targetId/
targetType/model/provider/aiError, plus an ai.fail structured log
(targetId via suffix)
- wire it into /api/ai-translation (article / ins / twitter),
/api/ai-vision (generate / translate / save), /api/ai-test and
/api/ig/translate; add targetId/targetType to the 4xx paths and hoist
model/provider above the try so the catch can report them
- leave the HTTP 200 contract untouched: the failure class lives in
body.status, so business rejections never flood Vercel with errors
- attach targetId to the ai.translate.article.batch event
- document the context fields on ErrorResponse and refresh the bundled
OpenAPI snapshot
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Several failure paths were impossible to trace in production logs.
- api/tweet/search.ts logged nothing at all on failure; log q/type/cursor
- api/proxy/image.ts did not log the upstream status or the proxied url
- article.tsx / tweet.tsx loaders sat behind a bare `catch {}`, so a page
that failed to render logged no tweetId
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- AC-OBS-002: unit-test buildAIFailure (body shape, status/message override, ai.fail log fields) - integration: an unknown provider throws inside the route, so all three /api/ai-translation branches are exercised without touching the network; assert targetId/targetType/model/provider and that the HTTP 200 + body.status contract still holds - AC-obs.md v1.1 (AC-OBS-001~002) + verify README coverage row Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- dev log: the missing-context symptom, the owner's clarification that HTTP 200 is deliberate, the buildAIFailure fix and the audit findings - backlog: add the remaining non-AI failure-response cleanup, note that HTTP 200 is by convention and that /api/ig/translate still differs Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改了什么
两块:文章翻译的解析失败鲁棒性 + AI 失败响应的定位上下文。
1. 文章翻译解析失败(AC-TWEET-020)
/api/ai-translation(type=article)稳定 500:deepseek-flash把blocks写成逐块多打一个}的非法 JSON,Output.object抛AI_NoObjectGeneratedError;而重试循环只处理占位符不匹配,异常直接冒泡、整篇失败。{ 块key: 原文 }对象(与要求的blocks输出同构);prompt 明确「一个blocks对象含全部 key、禁止中途闭合」并给两个 key 的示例。generateText包 try/catch:解析失败也重试;单批失败保留已成功的块;整篇都没翻出来时才显式报错(不再以 success 返回空译文)。normalizeAIError对NoObjectGeneratedError补generatedText(截断)与finishReason。2. AI 失败可定位(AC-OBS-002)
buildAIFailure:统一失败 body,带targetId/targetType/model/provider/aiError,并落一条ai.fail结构化日志。/api/ai-translation(article / ins / twitter)、/api/ai-vision(generate / translate / save)、/api/ai-test、/api/ig/translate;4xx 路径补targetId/targetType。body.status区分,避免 Vercel 日志被业务拒绝刷屏)——本轮未改动该约定。tweet/search(此前完全无日志)、proxy/image(不记 url 与上游 status)、article.tsx/tweet.tsx的裸catch {}。为什么
线上只看到
No object generated: could not parse the response.——既没有 tweetId / articleId,也没有 model / provider,无法判断是哪条内容挂了;而这个 500 本身就是文章翻译的确定性 bug(真实 DeepSeek 端点 3/3 复现)。检查项
bun run typecheck、bun run lint、bun run build通过bun run verify/index.ts --exit-on-fail通过(64 文件 / 504 passed / 7 skipped)验证方式
bun run verify/index.ts --ac AC-TWEET-020:用ai/test的MockLanguageModelV4驱动真实generateText+Output.object复现解析失败,覆盖重试 / 部分成功保留 / 全失败报错 / 输入形态;把修复反向改掉时其中 3 条用例确实变红。bun run verify/index.ts --ac AC-OBS-002:集成用「未知 provider」在路由内触发 catch(不触网、不需要 key),断言三分支分别回带article.id/igPost.id/tweet.id_str,且res.status === 200+body.status === 500。