Skip to content

fix(ai): retry unparseable article output and make AI failures traceable - #23

Merged
Chilfish merged 9 commits into
mainfrom
fix/ai-translation-errors
Sep 27, 2026
Merged

Chilfish merged 9 commits into
mainfrom
fix/ai-translation-errors

Conversation

@Chilfish

Copy link
Copy Markdown
Owner

改了什么

两块:文章翻译的解析失败鲁棒性 + 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。
  • HTTP status 仍固定 200(既有约定:业务拦截与真正的网关故障由 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)
  • 新行为有对应测试和验收标准(AC-TWEET-020、AC-OBS-002)
  • 提交信息符合 Conventional Commits
  • 文档已同步(feature doc / dev log / backlog / AC / llms + OpenAPI + skill 快照)

验证方式

  • 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。
  • 真实 DeepSeek 端点直连脚本:修复前后对照,4 种 prompt/输入变体均通过。

Chilfish and others added 9 commits September 27, 2026 15:16
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>
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
anon-tweet Ready Ready Preview Sep 27, 2026 7:52am UTC

@Chilfish
Chilfish merged commit 2f56689 into main Sep 27, 2026
2 checks passed
@Chilfish
Chilfish deleted the fix/ai-translation-errors branch September 27, 2026 07:54

This branch was successfully deployed

1 active deployment
Preview — a5674fb9 Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant