Repository navigation
Phase 8 追補: Gemini実装を追加し、既定プロバイダをGeminiに(TBD-006) - #14
Conversation
サブスク(ChatGPT Plus / Gemini Advanced / Claude Pro)にAPI利用は含まれず 従量課金は避けられないが、レシートOCRはFlash系で十分な精度が出るうえ 月100枚でも数十円で収まる(Claude Opusの10分の1以下)。 - convex/ai/gemini.ts: @google/genai の models.generateContent に inlineData(画像)+プロンプトを渡し、responseJsonSchema で構造化出力を受ける。 JSONの構文エラーもスキーマ不適合も ReceiptSchemaError にして1回リトライ - convex/ai/schema.ts: 抽出スキーマ(zod)とプロンプトをプロバイダ間で共有。 Gemini用のJSON Schemaは z.toJSONSchema() で生成して二重管理をやめる - convex/ai/config.ts: RECEIPT_AI_MODEL がプロバイダと噛み合わないときは 設定ミスとして止める(黙って既定値に落とすと「設定したのに効かない」になる)。 APIキー未設定も「何を設定すればよいか」を画面に出す(Claude側にも適用) - 既定プロバイダを gemini / 既定モデルを gemini-2.5-flash に - レート制限テストのプロバイダ固定を、実装済みのgeminiから実在しない名前に変更 (実APIを叩かないため) - テスト12件追加(計188件) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
WalkthroughGeminiによる画像レシート解析を実装し、共通のスキーマ・設定検証を追加しました。未設定時の既定プロバイダをGeminiへ変更し、Claude実装とドキュメントも更新しています。 ChangesAIレシート解析
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ReceiptParser
participant GeminiReceiptParser
participant GeminiAPI
participant ReceiptSchema
ReceiptParser->>GeminiReceiptParser: parse(imageBase64, mediaType, options)
GeminiReceiptParser->>GeminiAPI: generateContent with image and receiptJsonSchema
GeminiAPI-->>GeminiReceiptParser: JSON response text
GeminiReceiptParser->>ReceiptSchema: safeParse(parsed JSON)
ReceiptSchema-->>GeminiReceiptParser: ParsedReceipt or ReceiptSchemaError
GeminiReceiptParser-->>ReceiptParser: ParsedReceipt
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
aro guard: policies 違反を検出しました
検証ルールは base 側の |
aro guard: policies 違反を検出しました
検証ルールは base 側の |
この機能がやっているのは「画像から項目を構造化して取り出す」=データ抽出で、 生のOCR転写ではない。第三者ベンチ(Roboflow Vision Evals)の抽出スコアは gemini-3.6-flash が94.8%(2位/21)、gemini-3.5-flash-lite が90.7%(6位/21)で、 「20項目に1個」か「11項目に1個」かの差になる。 月100枚のコスト差は¥20程度、レイテンシも2.5秒/1.3秒でどちらも要件 (通常15秒以内)に余裕があるため、精度側を取った。レシートの品目と金額の 対応付けはテーブル的な読み取りで、Flash-Liteは前世代比でテーブルが 落ちているという報告がある点も踏まえている。 日本語の感熱紙レシートで測った数字ではないので、実レシートでの確認は TBD-001として残す(切り替えは RECEIPT_AI_MODEL だけで済む)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
綴り間違いや提供終了は404で返るが、汎用の「読み取りに失敗しました」だと 原因にたどり着けない。モデルIDはSDKの型で縛れない(model は任意の文字列)ので 間違いは実行時にしか出ず、気づけないと詰まる。 「AIモデル『◯◯』が使えません。ConvexのRECEIPT_AI_MODELを確認してください」に して、環境変数を直せば済むと分かるようにした。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude側と同じく、404(モデルIDの綴り間違い・提供終了)は 「AIモデル『◯◯』が使えません。ConvexのRECEIPT_AI_MODELを確認してください」 にする。モデルIDはSDKの型で縛れない(model は任意の文字列)ため、間違いは 実行時にしか出ない。既定値が合わなかった場合も環境変数だけで直せる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
Codexレビュー: 3巡、未対応が1件(判断が分かれた点)
据え置きました。 SDK の中身を確認したところ、前提が事実と異なります:
SDK に載っている 外れたときの代償は下げてありますモデルIDは型で縛れない以上、間違いは実行時にしか出ません。そこで 404 を専用の文言にしました(Claude 側にも同じ手当てを入れています):
|
aro guard: policies 違反を検出しました
検証ルールは base 側の |
実キーで動かしたところ 429(残高不足)が汎用の「読み取りに失敗しました」に 丸められ、ログにも種別しか出ていないため原因にたどり着けなかった。 - convex/ai/config.ts に configErrorMessage() を追加し、モデルID(400/404)・ APIキー(401/403)・利用上限や残高(429)を、それぞれ直せる文言にする。 一時的な障害(5xx)は従来どおり汎用文言に任せる - receipts.ts のログにHTTPステータスとAPI側の文言を出す。どちらも 「リクエストの作り方が悪い」というAPIの説明で、レシートの中身は含まれない - テスト4件追加(計194件) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
docs/implementation-plan.md (1)
934-934: 🚀 Performance & Scalability | 🔵 TrivialGeminiの本番使用量監視をデプロイ手順に追加してください。
既定プロバイダをGeminiへ変更した一方、後続のコスト管理手順はAnthropicコンソールのみを確認しています。Geminiの利用量・請求アラートも追加するか、選択中のプロバイダに応じた監視手順へ一般化してください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/implementation-plan.md` at line 934, Update the Convex production deployment checklist item to include monitoring usage, costs, and billing alerts for Gemini when it is the selected provider, or generalize the instructions to require monitoring the currently configured provider rather than only Anthropic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@convex/ai/schema.ts`:
- Line 9: Update the purchased_at schema field to use z.iso.date().nullable()
instead of z.string().nullable(), preserving null for unreadable dates. Add or
update validation tests to confirm valid YYYY-MM-DD values pass and malformed or
impossible dates such as slash-formatted or out-of-range dates are rejected.
In `@docs/implementation-plan.html`:
- Around line 549-550:
P8の正本参照をGemini対応後の内容に更新してください。p8-t1・p8-t2で示すGemini既定、共通スキーマ、Convex環境変数、設定検証を含む説明に改め、「claude.ts」の全コードだけを案内する記述を修正してください。
- Line 549: Update the p8-t1 checklist item in docs/implementation-plan.html to
replace the generic PROVIDER and MODEL environment variable names with the
implementation names RECEIPT_AI_PROVIDER and RECEIPT_AI_MODEL, while retaining
GEMINI_API_KEY and the existing ReceiptParser wording.
In `@docs/implementation-plan.md`:
- Around line 744-748:
計画書内のReceiptParser.parseの型定義とClaude実装例を、convex/receipts.tsが渡す第3引数の{ timeoutMs
}に対応させて更新してください。Claudeの解析処理ではparsed_output ===
nullだけでなく、APIエラーを除外した構造化出力・Zod検証例外をReceiptSchemaErrorへ変換し、呼び出し側の1回限定リトライと合計30秒制限が成立する契約を明記してください。
- Line 925: AI設定未完了時の案内を、選択中のプロバイダに応じて表示する内容へ修正してください。RECEIPT_AI_PROVIDER=gemini
では GEMINI_API_KEY、claude では ANTHROPIC_API_KEY を案内し、常に GEMINI_API_KEY
と記載しないでください。
- Around line 755-773:
Geminiのコスト比較表と関連説明を、公式単価(入力$1.50/M・出力$7.50/M)、入力3,000〜4,000トークン、出力1,000トークン、月100枚の前提で再計算してください。`gemini-3.6-flash`の月額を正しいドル額から明示した為替レートで円換算し、想定リトライ分も見積もり式に反映して、「約¥25」およびそれに基づくClaude
Haikuとの差額説明を更新してください。
---
Nitpick comments:
In `@docs/implementation-plan.md`:
- Line 934: Update the Convex production deployment checklist item to include
monitoring usage, costs, and billing alerts for Gemini when it is the selected
provider, or generalize the instructions to require monitoring the currently
configured provider rather than only Anthropic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6f466914-4b92-47fb-8e28-b926b67eb8e3
⛔ Files ignored due to path filters (2)
convex/_generated/api.d.tsis excluded by!**/_generated/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
convex/ai/claude.tsconvex/ai/config.test.tsconvex/ai/config.tsconvex/ai/gemini.tsconvex/ai/index.tsconvex/ai/schema.test.tsconvex/ai/schema.tsconvex/receipts.test.tsdocs/implementation-plan.htmldocs/implementation-plan.mdpackage.json
|
|
||
| export const ReceiptSchema = z.object({ | ||
| store_name: z.string().nullable(), | ||
| purchased_at: z.string().nullable(), // YYYY-MM-DD。判読不能ならnull |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -eu
# Convex 固有ルールと、インストール済み Zod の date API を確認する。
sed -n '1,220p' convex/_generated/ai/guidelines.md
rg -n -C2 'iso\s*:|date\s*:' node_modules/zod -g '*.d.ts' -g '*.ts'
rg -n -C2 'purchased_at' convex/ai/schema.ts convex/ai/schema.test.tsRepository: yamk12nfu/warikapp
Length of output: 45783
🏁 Script executed:
#!/usr/bin/env bash
set -eu
mkdir -p /tmp/zodprobe
cat >/tmp/zodprobe/probe.js <<'JS'
const z = require('zod');
const cases = ["2026-07-20", "2026/7/20", "2026-01-32", "", ""];
for (const formatter of ["date()", "iso.date()"]) {
console.log(`\n## ${formatter}`);
const schema = z.object({ purchased_at: (formatter === "date()" ? z[formatter] : z[formatter]()).nullable() });
for (const value of cases) {
const result = schema.safeParse({ purchased_at: value });
console.log(`${JSON.stringify(value)} => ${result.success ? "success:" + JSON.stringify(result.data) : "error"}`);
}
}
JS
node /tmp/zodprobe/probe.jsRepository: yamk12nfu/warikapp
Length of output: 1046
🏁 Script executed:
#!/usr/bin/env bash
set -eu
node --input-type=module --input '
import { createRequire } from "module";
const require = createRequire(process.cwd());
const z = require("zod");
const cases = ["2026-07-20", "2026/7/20", "2026-01-32", "aaa", ""];
for (const formatter of ["date()", "iso.date()"]) {
console.log(`\n## ${formatter}`);
const formatterFn = formatter === "date()" ? z.date : z.iso.date;
const schema = z.object({ purchased_at: formatterFn().nullable() });
for (const value of cases) {
const result = schema.safeParse({ purchased_at: value });
console.log(`${JSON.stringify(value)} => ${result.success ? "success:" + JSON.stringify(result.data) : "error"}`);
}
}
'Repository: yamk12nfu/warikapp
Length of output: 182
🏁 Script executed:
#!/usr/bin/env bash
set -eu
cd "$(pwd)"
node - <<'JS'
const { createRequire } = require('module');
const require = createRequire(process.cwd());
const z = require("zod");
const cases = ["2026-07-20", "2026/7/20", "2026-01-32", "aaa", ""];
for (const formatter of ["date()", "iso.date()"]) {
console.log(`\n## ${formatter}`);
const formatterFn = formatter === "date()" ? z.date : z.iso.date;
const schema = z.object({ purchased_at: formatterFn().nullable() });
for (const value of cases) {
const result = schema.safeParse({ purchased_at: value });
console.log(`${JSON.stringify(value)} => ${result.success ? "success:" + JSON.stringify(result.data) : "error"}`);
}
}
JSRepository: yamk12nfu/warikapp
Length of output: 808
🏁 Script executed:
#!/usr/bin/env bash
set -eu
node --check "
const root = process.cwd();
const orig = process.chdir;
process.chdir = (dir) => { throw new Error('unexpected cwd change'); };
const { createRequire } = require('module');
const require = createRequire(root);
const z = require('zod');
for (const formatter of ['z.date()', 'z.iso.date()']) {
const fn = formatter === 'z.date()' ? z.date : z.iso.date;
const schema = z.object({ purchased_at: fn().nullable() });
const cases = ['2026-07-20', '2026\/7\/20', '2026-01-32', 'aaa'];
console.log('\n## ' + formatter);
for (const value of cases) {
const result = schema.safeParse({ purchased_at: value });
console.log(JSON.stringify(value) + ' => ' + (result.success ? 'success:' + JSON.stringify(result.data) : 'error'));
}
}
"Repository: yamk12nfu/warikapp
Length of output: 1162
🏁 Script executed:
#!/usr/bin/env bash
set -eu
node --check <<'JS'
const root = process.cwd();
process.chdir = (dir) => { throw new Error('unexpected cwd change'); };
const { createRequire } = require('module');
const require = createRequire(root);
const z = require('zod');
for (const formatter of ['z.date()', 'z.iso.date()']) {
const fn = formatter === 'z.date()' ? z.date : z.iso.date;
const schema = z.object({ purchased_at: fn().nullable() });
const cases = ['2026-07-20', '2026/7/20', '2026-01-32', 'aaa'];
console.log('\n## ' + formatter);
for (const value of cases) {
const result = schema.safeParse({ purchased_at: value });
console.log(JSON.stringify(value) + ' => ' + (result.success ? 'success:' + JSON.stringify(result.data) : 'error'));
}
}
JSRepository: yamk12nfu/warikapp
Length of output: 925
🏁 Script executed:
#!/usr/bin/env bash
set -eu
exec node - <<'JS'
const { createRequire } = require('module');
const require = createRequire(process.cwd());
const zod = require('zod');
for (const formatter of ['z.string().nullable()', 'z.date().nullable()', 'z.iso.date().nullable()']) {
const schema = zod.object({ purchased_at: zod.string().nullable() });
}
JSRepository: yamk12nfu/warikapp
Length of output: 808
🌐 Web query:
Zod v4 iso.date YYYY-MM-DD validation z.iso.date().nullable documentation
💡 Result:
In Zod v4, z.iso.date() is the standard, top-level method for validating ISO 8601 date strings in the format YYYY-MM-DD [1][2]. To use z.iso.date() with support for null values, chain the .nullable() method to the schema [1]. Example usage: const schema = z.iso.date.nullable; schema.parse("2026-07-26"); // ✅ Valid: "2026-07-26" schema.parse(null); // ✅ Valid: null schema.parse("2026-07-32"); // ❌ Invalid: Date format check fails Key points: - API Location: z.iso.date() is now a top-level function exported from the main zod module [3][2]. While older method forms like z.string().date() (if available) are deprecated, the new top-level API is the recommended approach for tree-shaking and consistency in v4 [2][4]. - Functionality: z.iso.date() performs regex-based format validation [1]. It does not provide built-in date-level constraint methods (like .min() or .max()) that accept Date objects; these methods are reserved for z.date() schemas [1][5][6]. - Date Constraints: If you require minimum or maximum date validation for ISO strings, the maintainers recommend using the .refine() method combined with a dedicated date-handling library (e.g., date-fns or dayjs) [5][6].
Citations:
- 1: https://zod.dev/api
- 2: https://zod.dev/v4
- 3: https://zod.dev/v4/changelog
- 4: https://github.com/colinhacks/zod/blob/c7805073/packages/docs/content/v4/changelog.mdx
- 5: Feature Request : Require Zod Iso Date extended functionality for min, max and range like date does. colinhacks/zod#4988
- 6: iso.date() not have min and max dates colinhacks/zod#5518
購入日前日を z.iso.date() で検証してください。
purchased_at は YYYY-MM-DD の日付として扱う前提ですが、現状は任意の文字列を許容するため 2026/7/20 や 2026-01-32 が入力時にリトライ対象になりません。z.iso.date().nullable() にして、正常値・不正値の判別もテストしてください。
修正案
- purchased_at: z.string().nullable(), // YYYY-MM-DD。判読不能ならnull
+ purchased_at: z.iso.date().nullable(), // YYYY-MM-DD。判読不能ならnull📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| purchased_at: z.string().nullable(), // YYYY-MM-DD。判読不能ならnull | |
| purchased_at: z.iso.date().nullable(), // YYYY-MM-DD。判読不能ならnull |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@convex/ai/schema.ts` at line 9, Update the purchased_at schema field to use
z.iso.date().nullable() instead of z.string().nullable(), preserving null for
unreadable dates. Add or update validation tests to confirm valid YYYY-MM-DD
values pass and malformed or impossible dates such as slash-formatted or
out-of-range dates are rejected.
| <ul class="tasks"> | ||
| <li><label><input type="checkbox" id="p8-t1"><span>ReceiptParser抽象化(convex/ai/)+Convex環境変数(ANTHROPIC_API_KEY / PROVIDER / MODEL)を登録</span></label></li> | ||
| <li><label><input type="checkbox" id="p8-t2"><span>claude.ts(構造化出力 output_config.format+30秒タイムアウト / 既定 claude-opus-5)</span></label></li> | ||
| <li><label><input type="checkbox" id="p8-t1"><span>ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / PROVIDER / MODEL)を登録</span></label></li> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Convex環境変数の実名を明記してください。
PROVIDER / MODEL は実装上の RECEIPT_AI_PROVIDER / RECEIPT_AI_MODEL と一致しないため、チェックリストどおりに設定すると設定が反映されません。
修正例
-ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / PROVIDER / MODEL)を登録
+ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / RECEIPT_AI_PROVIDER / RECEIPT_AI_MODEL)を登録📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <li><label><input type="checkbox" id="p8-t1"><span>ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / PROVIDER / MODEL)を登録</span></label></li> | |
| <li><label><input type="checkbox" id="p8-t1"><span>ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / RECEIPT_AI_PROVIDER / RECEIPT_AI_MODEL)を登録</span></label></li> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/implementation-plan.html` at line 549, Update the p8-t1 checklist item
in docs/implementation-plan.html to replace the generic PROVIDER and MODEL
environment variable names with the implementation names RECEIPT_AI_PROVIDER and
RECEIPT_AI_MODEL, while retaining GEMINI_API_KEY and the existing ReceiptParser
wording.
| <li><label><input type="checkbox" id="p8-t1"><span>ReceiptParser抽象化(convex/ai/)+Convex環境変数(GEMINI_API_KEY / PROVIDER / MODEL)を登録</span></label></li> | ||
| <li><label><input type="checkbox" id="p8-t2"><span>gemini.ts(既定・responseJsonSchema)とclaude.ts(output_config.format)。30秒は読み取り全体で共有</span></label></li> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
P8の正本参照をGemini対応後の内容に更新してください。
このタスク内容はGemini既定・共通設定へ更新されていますが、直後の Line [566] は「claude.ts の全コード」と案内しており、Gemini実装・共通スキーマ・設定検証を見落とします。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/implementation-plan.html` around lines 549 - 550,
P8の正本参照をGemini対応後の内容に更新してください。p8-t1・p8-t2で示すGemini既定、共通スキーマ、Convex環境変数、設定検証を含む説明に改め、「claude.ts」の全コードだけを案内する記述を修正してください。
| - [x] `convex/ai/index.ts`: 環境変数 `RECEIPT_AI_PROVIDER` で `claude` / `gemini` を切り替えて実装を返す。**両方を実装済み。既定は `gemini`**(TBD-006)。`claude.ts` / `gemini.ts` / `index.ts` は SDK を読むので先頭に `"use node";` を書く(`types.ts` / `schema.ts` / `config.ts` は SDK に触れないので不要) | ||
| - [x] `convex/ai/schema.ts`: 抽出スキーマ(zod)とプロンプトをプロバイダ間で共有する。GeminiにはJSON Schemaが要るので `z.toJSONSchema()` で変換する(二重管理をやめる) | ||
| - [x] `convex/ai/config.ts`: `resolveModel()` / `requireApiKey()`。`RECEIPT_AI_MODEL` は1つしかないので、**プロバイダとモデルIDが噛み合わないときは設定ミスとして止める**(黙って既定値に落とすと「設定したのに効かない」になる)。APIキー未設定も「何を設定すればよいか」を画面に出す | ||
| - [x] **モデルIDの設定ミスは専用の文言で出す**: 綴り間違い・提供終了は404で返るが、汎用の「読み取りに失敗しました」だと原因にたどり着けない。「AIモデル『◯◯』が使えません。ConvexのRECEIPT_AI_MODELを確認してください」にして、環境変数を直せば済むと分かるようにする(モデルIDはSDKの型では縛れない=`model` は任意の文字列なので、間違いは実行時にしか出ない) | ||
| - [x] ⚠️ **スキーマ不適合は例外で飛んでくる**: SDKの `messages.parse()` は不正JSON・Zod検証エラーを `AnthropicError("Failed to parse structured output: ...")` として**throwする**(`parsed_output` が null になるより先に例外)。これを `ReceiptSchemaError` に変換しないと「1回だけリトライ」が動かない。API側のエラー(`APIError` 系)はリトライ対象外なので、変換前に除外する |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
計画書の型契約と実装フローを一致させてください。
計画書の ReceiptParser.parse 例は2引数のみですが、実装側の convex/receipts.ts は { timeoutMs } を第3引数で渡しています。また、本文はClaudeの解析例外を ReceiptSchemaError に変換すると説明する一方、掲載コードは parsed_output === null しか確認していません。1回限定リトライと合計30秒制限の正本として、型定義とClaudeコード例を更新してください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/implementation-plan.md` around lines 744 - 748,
計画書内のReceiptParser.parseの型定義とClaude実装例を、convex/receipts.tsが渡す第3引数の{ timeoutMs
}に対応させて更新してください。Claudeの解析処理ではparsed_output ===
nullだけでなく、APIエラーを除外した構造化出力・Zod検証例外をReceiptSchemaErrorへ変換し、呼び出し側の1回限定リトライと合計30秒制限が成立する契約を明記してください。
| > 💡 **なぜ既定がGeminiか(コスト)**。サブスク(ChatGPT Plus / Gemini Advanced / Claude Pro)にAPI利用は含まれず、サーバーから叩くぶんは必ず従量課金になる。レシート1枚を「入力3,000〜4,000トークン+出力1,000トークン」として月100枚で見積もると: | ||
| > | ||
| > | モデル | 月100枚 | | ||
| > |---|---| | ||
| > | `claude-opus-5` | 約 ¥650 | | ||
| > | `claude-haiku-4-5` | 約 ¥130 | | ||
| > | `gemini-3.6-flash`(既定) | 約 ¥25 | | ||
| > | `gemini-3.5-flash-lite` | 約 ¥6 | | ||
| > | ||
| > Gemini APIにはFlash系の**無料枠**もあるが、無料枠は**入力・出力がGoogleの製品改善に使われる**(有料枠は使われない)。レシートは家計の中身なので、常用は有料枠を推奨する。有料枠でもこの金額なので、無料枠を選ぶ実益はほぼない。 | ||
|
|
||
| > 💡 **なぜ Flash-Lite ではなく `gemini-3.6-flash` か(精度)**。この機能がやっているのは「画像から項目を構造化して取り出す」=**データ抽出**で、生のOCR転写ではない。第三者ベンチ(Roboflow Vision Evals)の抽出スコアは: | ||
| > | ||
| > | モデル | データ抽出 | OCR転写 | 抽出のレイテンシ | | ||
| > |---|---|---|---| | ||
| > | `gemini-3.6-flash` | **94.8%(2位/21)** | 88.4% | 2.5秒 | | ||
| > | `gemini-3.5-flash-lite` | 90.7%(6位/21) | 87.4% | 1.3秒 | | ||
| > | ||
| > 差は「20項目に1個間違える」か「11項目に1個間違える」かで、手直しの手間に直結する。月100枚でもコスト差は¥20程度、レイテンシもどちらも要件(通常15秒以内)に対して余裕があるので、精度側を取った。レシートの「品目|金額」の対応付けはテーブル的な読み取りで、Flash-Lite は前世代比でテーブルが落ちているという報告がある点も踏まえている。 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file excerpt =="
sed -n '740,785p' docs/implementation-plan.md
echo
echo "== nearby context and pricing/model mentions =="
rg -n "pricing|Gemini|gemini|3\.6-flash|flash-lite|トークン|料金|単価|USD|JPY|Claude|Claude|Roboflow|ベンチ" docs/implementation-plan.md README.md package.json 2>/dev/null || trueRepository: yamk12nfu/warikapp
Length of output: 6770
🌐 Web query:
Google Gemini 3.6 flash API pricing input $1.50 output $7.50 per million tokens
💡 Result:
As of July 26, 2026, the official pricing for the Google Gemini 3.6 Flash API is $1.50 per 1 million input tokens and $7.50 per 1 million output tokens [1][2][3]. This pricing structure was introduced alongside the model's release on July 21, 2026 [2][3][4]. The output pricing of $7.50 includes any reasoning or thinking tokens generated by the model during its response process [2]. Additional costs may apply for features such as context caching, which is priced at $0.15 per 1 million tokens for reads and $1.00 per 1 million tokens per hour for storage [1][2].
Citations:
- 1: https://ai.google.dev/gemini-api/docs/pricing
- 2: https://apidog.com/blog/gemini-3-6-flash-pricing/
- 3: https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-6-flash-3-5-flash-lite-3-5-flash-cyber/
- 4: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-6-flash
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from decimal import Decimal, ROUND_HALF_UP
inputs = [Decimal("3000"), Decimal("4000")]
outputs = [Decimal("1000")]
in_price = Decimal("1.50") / Decimal("1_000_000")
out_price = Decimal("7.50") / Decimal("1_000_000")
for inputs_per in inputs:
for outputs_per in outputs:
usd = (inputs_per * in_price + outputs_per * out_price) * 100
jpy_140 = round(float(usd * 140))
jpy_155 = round(float(usd * 155))
jpy_xxx = int((usd * Decimal("xxx")).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP).replace("E1", "E0"))
print(f"input={inputs_per}/output={outputs_per} -> ${usd} `@140`=y{ jpy_140}, `@155`=y{ jpy_155}, `@xxx`=y{ jpy_xxx}")
PYRepository: yamk12nfu/warikapp
Length of output: 293
🌐 Web query:
Roboflow Vision Evals Gemini 3.6 flash data extraction score 94.8% 2/21 OCR transcription 88.4% flash lite date
💡 Result:
According to Roboflow's Vision Evals, Gemini 3.6 Flash achieves a data extraction score of 94.8% and an OCR transcription score of 88.4% [1][2]. Regarding Gemini Flash-Lite, the models are part of the Gemini 3.x series, such as the Gemini 3.5 Flash-Lite (which was noted for being the most cost-efficient in its tier, often evaluated alongside other Gemini models) [3][4][5]. The "Gemini 3.5 Flash-Lite" is a recognized model in the series, whereas "Gemini 3.6 Flash-Lite" does not appear as a distinct public release in current documentation; Gemini 3.6 Flash itself was released in July 2026 [1][2]. Roboflow's Vision Evals are frequently updated to incorporate new models and benchmark results [6].
Citations:
- 1: https://playground.roboflow.com/models/compare/claude-sonnet-5-vs-gemini-3-6-flash
- 2: https://playground.roboflow.com/models/compare/gemini-3-6-flash-vs-gemini-3-flash
- 3: https://blog.roboflow.com/gemini-3-6-flash-for-vision/
- 4: https://playground.roboflow.com/models/compare/gemini-3-5-flash-lite-vs-gpt-5-6-sol
- 5: https://blog.roboflow.com/gemini-computer-vision/
- 6: https://playground.roboflow.com/models/google/gemini-3-5-flash
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from decimal import Decimal, ROUND_HALF_UP
inputs = [Decimal("3000"), Decimal("4000")]
outputs = [Decimal("1000")]
in_price = Decimal("1.50") / Decimal("1_000_000")
out_price = Decimal("7.50") / Decimal("1_000_000")
for inputs_per in inputs:
for outputs_per in outputs:
usd = (inputs_per * in_price + outputs_per * out_price) * 100
for rate in [Decimal("140"), Decimal("155"), Decimal("185")]:
jpy = (usd * rate).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
print(f"input={inputs_per}/output={outputs_per} `@USD/JPY`={rate} -> ${usd} = ¥{jpy}")
PYRepository: yamk12nfu/warikapp
Length of output: 510
🌐 Web query:
"Roboflow Vision Evals" "data extraction" "OCR transcription" "Gemini 3.6 flash"
💡 Result:
Gemini 3.6 Flash is a multimodal AI model released by Google on July 21, 2026, designed for improved performance in coding, agentic workflows, and knowledge tasks [1][2][3]. It serves as a more token-efficient and cost-effective successor to Gemini 3.5 Flash, specifically addressing developer feedback regarding output verbosity and reasoning steps [1][3][4]. Regarding your interest in Roboflow Vision Evals, Gemini 3.6 Flash is currently included in their benchmarking suite [5]. Roboflow Vision Evals tests models on real-world visual tasks, including Optical Character Recognition (OCR) and Data Extraction, providing ground-truth scoring rather than subjective rankings [5][6][7]. Key details: - OCR Performance: In the Roboflow Vision Evals OCR task, models are measured by mean similarity to ground-truth transcriptions of text within images [5][8]. - Data Extraction: In this task, models must identify and return specific information from an image [5][9]. - Model Status: Gemini 3.6 Flash is available for developers via the Gemini API (Google AI Studio) and for enterprise use, with support for text, image, video, and audio inputs [10][3][4]. For the most current comparisons, you can view the live leaderboards on the Roboflow Vision Evals playground website, where Gemini 3.6 Flash performance metrics for data extraction and OCR are reported alongside other leading vision-language models [5][6].
Citations:
- 1: https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-6-flash-3-5-flash-lite-3-5-flash-cyber/
- 2: https://deepmind.google/models/model-cards/gemini-3-6-flash/
- 3: https://ai.google.dev/gemini-api/docs/changelog
- 4: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-6-flash
- 5: https://playground.roboflow.com/evals
- 6: https://playground.roboflow.com/evals?category=visual-understanding
- 7: https://blog.roboflow.com/best-computer-vision-models/
- 8: https://playground.roboflow.com/evals/ocr
- 9: https://playground.roboflow.com/evals/data-extraction
- 10: https://ai.google.dev/gemini-api/docs/models/gemini-3.6-flash
Geminiのコスト見積もりを修正してください。
この前提(入力3,000〜4,000トークン+出力1,000トークン、月100枚、公式単価$input1.50/M + $7.50/M)ではGemini 3.6 Flashは月$1.20〜$1.35になり、表中の「約¥25」は一致しません。Geminiの「約¥25」はClaude Claude haikuを大幅に下回る選定の根拠なので、単価・為替・リトライを含んだ見積もり式を再計算して更新してください。
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 765-765: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/implementation-plan.md` around lines 755 - 773,
Geminiのコスト比較表と関連説明を、公式単価(入力$1.50/M・出力$7.50/M)、入力3,000〜4,000トークン、出力1,000トークン、月100枚の前提で再計算してください。`gemini-3.6-flash`の月額を正しいドル額から明示した為替レートで円換算し、想定リトライ分も見積もり式に反映して、「約¥25」およびそれに基づくClaude
Haikuとの差額説明を更新してください。
| - 31回連続で解析するとレート制限エラーになる(確認は任意。`convex/receipts.test.ts` で自動テスト済み) | ||
|
|
||
| > ⚠️ AI読み取りの実地確認には Convexダッシュボードでの環境変数登録(`ANTHROPIC_API_KEY` / `RECEIPT_AI_PROVIDER` / `RECEIPT_AI_MODEL`)が必要。未登録のうちは読み取りが「読み取りに失敗しました。手入力に切り替えますか?」になる(圧縮・アップロード・帰属検証・レート制限・調整行・ドラフト復帰はテストで検証済み)。 | ||
| > ⚠️ AI読み取りの実地確認には Convexダッシュボードでの環境変数登録(`GEMINI_API_KEY`。Claudeを使うなら `ANTHROPIC_API_KEY`)が必要。未登録のうちは「AIの設定が未完了です。Convexの環境変数に GEMINI_API_KEY を登録してください」が画面に出る(圧縮・アップロード・帰属検証・レート制限・調整行・ドラフト復帰はテストで検証済み)。 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Claude選択時の未設定キー表示を修正してください。
RECEIPT_AI_PROVIDER=claude で ANTHROPIC_API_KEY が未設定の場合、実装は ANTHROPIC_API_KEY を案内します。この行は常に GEMINI_API_KEY が表示されると記載しているため、プロバイダに応じた表示へ修正してください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/implementation-plan.md` at line 925,
AI設定未完了時の案内を、選択中のプロバイダに応じて表示する内容へ修正してください。RECEIPT_AI_PROVIDER=gemini では
GEMINI_API_KEY、claude では ANTHROPIC_API_KEY を案内し、常に GEMINI_API_KEY と記載しないでください。
実キーで動かして初めて分かった問題: - 購入日が常に当日になっていた。AIに渡すスキーマ/プロンプトに日付の形式を 書いておらず、「2026年7月21日」の形で返ると normalizePurchasedAt が弾いて 静かに当日へフォールバックしていた。各フィールドの説明を .describe() で 持たせ(コメントと違いJSON Schemaに載ってAIに渡る)、プロンプトにも "YYYY-MM-DD" を明記した - 数量が「牛乳 1000ml x2 ×2」と二重に付いていた。AIは数量を品目名に 含めたり含めなかったりするので、すでに入っている場合は足さないようにし、 プロンプトでも name に数量を含めないよう指示した - 元のAPIエラーを cause に残し、画面用の文言に置き換えてもログに HTTPステータスとAPI側の説明が残るようにした(Codex指摘) - テスト5件追加(計199件) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
品目名がすでに「×2」で終わっていると付け足しを抑えていたが、そのまま 50文字に切り詰めると末尾の数量だけが落ちる。quantityは1に畳んであるので、 数量の情報が完全に消えてしまう。 「入っていれば足さない」ではなく「いったん外して付け直す」形にし、 切り詰めるのは品目名の側だけにした。表記も「×N」に揃う。 プロンプトは略称を正式名に展開するよう指示しているので長い名前は現実に起きる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
- 実物のレシートで読み取り成功(10〜12秒。要件の15秒以内を満たす) - 実地でしか出なかった2件(日付形式・数量の二重表示)の原因と教訓を明記 - Geminiの思考レベルを既定のままにしている理由と、遅い場合の一手を記録 - 動作確認チェックの1・2をHTML側でも済みにする Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
数量1では「×N」を足さないのに末尾の数量パターンを削っていたため、 「商品X1」のように x1 で終わる正当な品目名(型番など)が「商品」に 化けていた。数量1なら名前に手を触れないようにする。 数量2以上のときだけ外して付け直す。「商品X2」を数量2で買った場合は 「商品 ×2」になるが、表示する情報は変わらないので許容する (放置すると「商品X2 ×2」になるほうが困る)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
Geminiのモデル一覧APIは "models/gemini-2.5-flash" の形で返し、SDKは短いIDと この形の両方を受ける。一覧からコピペされたときにプロバイダ不一致として 弾いていたので、判定では先頭の "models/" を無視する (SDKにはユーザーが設定した文字列をそのまま渡す)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
GeminiはAPIキーが無効なときも400(INVALID_ARGUMENT)を返すため、 400を一律「モデルIDを確認してください」に割り当てていると、 間違った設定を直させることになり、追加した診断が逆効果になる。 400はAPIキーだと分かるときだけ案内し、それ以外は汎用文言に任せる。 モデルIDの案内は404に絞った(実際そちらで返る)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aro guard: policies 違反を検出しました
検証ルールは base 側の |
Phase 8 追補: Gemini実装を追加し、既定プロバイダをGeminiに(TBD-006)
このPRは #13 の上に積んでいます。#13 をマージしてからこちらをマージしてください(#13 のマージ後、GitHubがこのPRのベースを自動で
mainに付け替えます)。なぜ
「サブスクの範囲内でやれないか」の答えとして調べた結果:
レシート1枚を「入力3,000〜4,000トークン+出力1,000トークン」として月100枚で見積もると:
claude-opus-5(#13 の既定)claude-haiku-4-5gemini-3.6-flash(このPRの既定)gemini-3.5-flash-liteGemini API には Flash 系の無料枠もありますが、無料枠は入力・出力がGoogleの製品改善に使われます(有料枠は使われない)。レシートは家計の中身なので、常用は有料枠を推奨します。有料枠でも上記の金額なので、無料枠を選ぶ実益はほぼありません(精度を試す期間だけ無料枠、という使い方は可能です)。
OpenAI は恒久的な無料枠が実質なく、コストも Gemini Flash 系より高いため見送りました。
既定モデルに
gemini-3.6-flashを選んだ理由この機能がやっているのは「画像から項目を構造化して取り出す」=データ抽出で、生のOCR転写ではありません。第三者ベンチ(Roboflow Vision Evals)の数字:
gemini-3.6-flashgemini-3.5-flash-lite「20項目に1個間違える」か「11項目に1個間違える」かの差で、手直しの手間に直結します。月100枚でもコスト差は¥20程度、レイテンシもどちらも要件(通常15秒以内)に余裕があるため、精度側を取りました。レシートの「品目|金額」の対応付けはテーブル的な読み取りで、Flash-Lite は前世代比でテーブルが落ちているという報告がある点も踏まえています。
RECEIPT_AI_MODELだけでコード変更は要りません。実装内容
convex/ai/gemini.ts:@google/genaiのmodels.generateContentにinlineData(画像)+プロンプトを渡し、responseMimeType: "application/json"+responseJsonSchemaで構造化出力を受ける。JSONの構文エラーもスキーマ不適合もReceiptSchemaErrorに変換して1回だけリトライ(Phase 8: AIレシート読み取り(F-003) #13 と同じ挙動)。タイムアウトはhttpOptions.timeoutに読み取り全体の残り時間を渡すconvex/ai/schema.ts(新規): 抽出スキーマ(zod)とプロンプトを両プロバイダで共有。Gemini用のJSON Schemaはz.toJSONSchema()で生成し、二重管理をやめたconvex/ai/config.ts(新規):resolveModel():RECEIPT_AI_MODELは1つしかないので、プロバイダと噛み合わないモデルIDは設定ミスとして止める(geminiに切り替えたのにclaude-opus-5が残っている、など)。黙って既定値に落とすと「設定したのに効かない」になるためrequireApiKey(): APIキー未設定を「読み取り失敗」で片付けず、何を設定すればよいかを画面に出す(Claude側にも適用)gemini、既定モデルをgemini-3.6-flashにgeminiから実在しない名前に変更(実APIを叩かないため)実施した検証
npx tsc --noEmit/npm run lint(新規の警告なし)/npx vitest run→ 190件 passednpx convex dev --once(Gemini SDKもConvexのNodeランタイムで問題なくバンドルされる)Convexダッシュボード → Settings → Environment Variables に
GEMINI_API_KEYだけ登録してください。RECEIPT_AI_PROVIDER/RECEIPT_AI_MODELは省略でgemini/gemini-3.6-flashになります。APIキーは Google AI Studio で発行できます。常用するなら課金を有効にした有料枠にしてください(無料枠は入力がモデル改善に使われます)。
Claude と比べたくなったら
ANTHROPIC_API_KEYを足してRECEIPT_AI_PROVIDER=claudeにすれば、画面そのままで切り替わります(TBD-001)。手動で確認してほしいこと
GEMINI_API_KEYを登録し、実物のレシートで品目・金額・店名・日付が自動入力されることgemini-3.5-flash-liteを試す)gemini-3.5-flash-liteを試す(TBD-001)🤖 Generated with Claude Code
Summary by CodeRabbit
新機能
改善
ドキュメント