Skip to content

release: dev → main (cost-tracking)#497

Merged
cocoyoon merged 17 commits into
mainfrom
dev
May 14, 2026
Merged

release: dev → main (cost-tracking)#497
cocoyoon merged 17 commits into
mainfrom
dev

Conversation

@cocoyoon
Copy link
Copy Markdown
Member

Summary

Release dev → main. Includes:

Highlights

  • Gemini 호출 (raw_post 6 + post_editorial 7) 비용을 호출 시점에 DB 적립
  • 단가 코드 하드코딩 0 — gemini_pricing DB 테이블 SOT (SCD-2)
  • /admin/gemini-cost 대시보드 — KPI / Daily chart / step·model·pipeline 별 / Top expensive raw_posts / Pricing editor
  • fire-and-forget recorder, 실패도 row 적립 (safety_block / quota / parse_error / timeout / no_pricing 가시화)

Test plan

  • dev PR feat(cost-tracking): Gemini API per-call cost tracking + admin dashboard #496 — 모든 CI 체크 PASS (review / check / invariants / notify / Vercel)
  • e2e — 실제 SubjectParser 클래스 호출 → cost_tracking 자동 row 적립 확인 (cost $0.000489, pricing_snapshot 정확)
  • main 머지 후 prod 배포 → 운영 raw_post 인입 시 gemini_usage_events 적립 확인
  • admin UI 직접 확인 (차트 / pricing inline edit)

🤖 Generated with Claude Code

cocoyoon and others added 17 commits May 7, 2026 17:27
* feat(raw_posts): StarStyle.com adapter (#466)

WordPress SSR fashion 사이트(starstyle.com) 어댑터 추가. wots(#465) 인프라
재사용 — discovery_target='raw_posts' 글로벌 피드 모델, PrelabeledData 로
vision 단계 우회.

핵심 차이: per-item 사진 URL 이 없어 thumbnail_url=None → items_thumbnail
processor 가 spots bbox 로 hero crop fallback path 사용. brand/title 분리는
보수적으로 product 통째로 (verify 단계 admin 처리).

- adapters/_starstyle_html.py: parse_post / parse_sitemap /
  decode_skimresources (skim affiliate 디코드)
- adapters/starstyle.py: thin httpx wrapper, fetch + sitemap discover
- scripts/backfill_starstyle_posts.py: sitemap → JSONL streaming → PostgREST
  bulk INSERT (wots 미러)
- service.rs: parse_starstyle_source — slug-sp{ID} 형식 검증
- admin UI: PLATFORM_FILTERS / PLATFORM_TABS / DiscoveryPipelineCard / URL builder
- migration: pipeline_settings starstyle row (모든 cycle OFF)
- 24 단위 테스트 추가 (parser + adapter + prelabeled round-trip)

Closes #466.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(starstyle): force HTTPS on og:image URL (#466)

starstyle 의 og:image 가 http:// 로 노출되는데 admin 은 HTTPS 라
mixed-content 로 이미지가 차단되던 문제. CDN 자체는 HTTPS 정상이라
파서 단계에서 https 강제 변환.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#484)

starstyle 의 og:image 는 admin (HTTPS) 에서 직접 hotlink 가 막힌다:
  - HTTPS 요청 → 301 redirect to HTTP → mixed-content 차단
  - HTTP 요청 → User-Agent / Referer 가 비어 있으면 403

백필 시점에 boto3 로 upstream 이미지 다운로드 → R2 (RAW_POSTS_R2_BUCKET) 에
``starstyle/{shard}/{external_id}.jpg`` 키로 업로드하고, raw_posts.image_url
을 R2 public URL 로 저장한다. admin 은 R2 직접 fetch (HTTPS clean, hotlink
없음) → Vercel image-proxy 우회.

- HEAD 로 R2 객체 존재 확인 → 재실행 시 중복 업로드 skip (idempotent)
- 다운로드/업로드 실패 시 upstream URL 로 fallback (image_url 그대로)
- RAW_POSTS_R2_* env 미설정 시 mirror skip + warning
- PostData @DataClass(frozen=True) → frozen 제거 (image_url 교체 위해)

검증: 500 row 재백필, 479/500 R2 mirrored. 샘플 R2 URL HTTPS 200 확인.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/ai-server/scripts/backfill_starstyle_posts.py
* release: starstyle R2 mirror fix (#466) (#485)

* feat(raw_posts): StarStyle.com adapter (#466) (#481)

* feat(raw_posts): StarStyle.com adapter (#466)

WordPress SSR fashion 사이트(starstyle.com) 어댑터 추가. wots(#465) 인프라
재사용 — discovery_target='raw_posts' 글로벌 피드 모델, PrelabeledData 로
vision 단계 우회.

핵심 차이: per-item 사진 URL 이 없어 thumbnail_url=None → items_thumbnail
processor 가 spots bbox 로 hero crop fallback path 사용. brand/title 분리는
보수적으로 product 통째로 (verify 단계 admin 처리).

- adapters/_starstyle_html.py: parse_post / parse_sitemap /
  decode_skimresources (skim affiliate 디코드)
- adapters/starstyle.py: thin httpx wrapper, fetch + sitemap discover
- scripts/backfill_starstyle_posts.py: sitemap → JSONL streaming → PostgREST
  bulk INSERT (wots 미러)
- service.rs: parse_starstyle_source — slug-sp{ID} 형식 검증
- admin UI: PLATFORM_FILTERS / PLATFORM_TABS / DiscoveryPipelineCard / URL builder
- migration: pipeline_settings starstyle row (모든 cycle OFF)
- 24 단위 테스트 추가 (parser + adapter + prelabeled round-trip)

Closes #466.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(starstyle): force HTTPS on og:image URL (#466)

starstyle 의 og:image 가 http:// 로 노출되는데 admin 은 HTTPS 라
mixed-content 로 이미지가 차단되던 문제. CDN 자체는 HTTPS 정상이라
파서 단계에서 https 강제 변환.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: retrigger CI with bump label

* fix(starstyle-backfill): mirror hero images to R2 before INSERT (#466) (#484)

starstyle 의 og:image 는 admin (HTTPS) 에서 직접 hotlink 가 막힌다:
  - HTTPS 요청 → 301 redirect to HTTP → mixed-content 차단
  - HTTP 요청 → User-Agent / Referer 가 비어 있으면 403

백필 시점에 boto3 로 upstream 이미지 다운로드 → R2 (RAW_POSTS_R2_BUCKET) 에
``starstyle/{shard}/{external_id}.jpg`` 키로 업로드하고, raw_posts.image_url
을 R2 public URL 로 저장한다. admin 은 R2 직접 fetch (HTTPS clean, hotlink
없음) → Vercel image-proxy 우회.

- HEAD 로 R2 객체 존재 확인 → 재실행 시 중복 업로드 skip (idempotent)
- 다운로드/업로드 실패 시 upstream URL 로 fallback (image_url 그대로)
- RAW_POSTS_R2_* env 미설정 시 mirror skip + warning
- PostData @DataClass(frozen=True) → frozen 제거 (image_url 교체 위해)

검증: 500 row 재백필, 479/500 R2 mirrored. 샘플 R2 URL HTTPS 200 확인.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): backend bump ai=1.5.1 api=0.10.1 [skip ci]

* feat(raw_posts): R2 cleanup on raw_post delete (#466)

raw_post 삭제 시 image_url 이 raw_posts R2 객체이고 verified posts 가
같은 URL 을 참조하지 않으면 R2 객체도 같이 삭제. verify 후에는 posts.
image_url 가 raw_post.image_url 그대로 복사되므로 (#333) — verified post
가 참조 중이면 보존해야 깨지지 않는다.

또한 storage 명명을 db 패턴(db / assets_db) 에 맞춤:
  - storage_client → operation_storage
  - 신규 RAW_POSTS_R2_* → assets_storage
  - AppConfig.storage / assets_storage, AppState.operation_storage /
    assets_storage 일관 매핑.

config:
  - StorageConfig 두 개 (storage / assets_storage), 별도 R2 버킷 wiring
  - RAW_POSTS_R2_{ACCOUNT_ID, ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET, PUBLIC_URL}

service::delete_item:
  - 시그니처 (assets_db, prod_db, storage, public_url, id) — 4개의 DI
  - raw_post.image_url 추출 → DELETE 진행
  - extract_assets_r2_key 로 R2 key 추출 (public_url prefix strip)
  - posts.image_url COUNT — 0 이면 storage.delete(key) 호출
  - 실패는 best-effort warn (이미 raw_post 삭제는 성공)

테스트: 4개 단위 (key 추출 - prefix/trailing slash/other domain/empty url).

solutions/tests.rs 의 AdminSolutionListQuery 빌드에 has_url=None 추가
(unrelated 사전 컴파일 에러 — 본 PR 의 cargo test --lib 통과 위해 보강).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: decoded-ci <ci@decodedcorp.com>
Store the judged image URL with vision filter audit rows so Instagram skip decisions can still render admin thumbnails after raw_posts cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
작업 1 (마이그레이션, 1080 row) 후속 — 신규 verify 데이터도 동일한 single-source
컨벤션 (r2.decoded.style/posts/{post_id} / r2.decoded.style/items/{solution_id})
유지하도록 verify_raw_post 흐름 수정.

기존: raw_post.image_url (assets R2 dev pub URL) 그대로 posts.image_url 복사
→ 운영 DB 가 dev 도메인 URL 들고 있는 버그.

새 흐름:
  1. prod_txn 진입 전 R2 copy:
     - post_id 사전 생성 → assets_storage.download(raw_key) →
       operation_storage.upload("posts/{post_id}")
     - 각 visible item: solution_id 사전 생성 → items thumbnail 동일 패턴
  2. prod_txn: create_post_from_raw / create_solution_for_verify 에 미리 생성한
     UUID + 새 operation URL 전달 (시그니처에 명시 인자 추가)
  3. assets_txn: raw_post.image_url 도 operation URL 로 갱신 (단일 진실 소스)
  4. 두 트랜잭션 commit 후 raw 객체 best-effort delete (warn 만, orphan 허용)

코드 변경:
  - StorageClient trait 에 `download(key) -> (bytes, Option<content_type>)` 추가
  - CloudflareR2Client GetObject 구현, NotFound 매핑
  - DummyStorageClient stub 구현
  - 신규 `raw_posts/relocate.rs` — relocate_raw_to_operation helper + 4 단위 테스트
  - extract_assets_r2_key → pub(crate) 격상 (relocate 가 재사용)
  - create_post_from_raw 시그니처: post_id, image_url 명시 인자 (내부 Uuid::new_v4 제거)
  - create_solution_for_verify 시그니처: solution_id 명시 인자
  - verify_raw_post: prod_txn 전 R2 copy 단계 + raw cleanup post-commit

실패 처리:
  - copy 도중 실패 → BadRequest/NotFound/ExternalService Err, prod_txn 미시작 (DB 부작용 0)
  - 부분 성공 (hero copy OK / item N copy fail) → op bucket 에 hero orphan
    잔존 (lifecycle rule / 별도 GC out of scope)
  - raw delete 실패 → warn 로깅만, verify 자체는 성공 유지

테스트:
  - 4 단위 (relocate happy/fallback/non-raw-rejection/not-found-propagation)
  - 24 회귀 통과 (parse_source_identifier_*, verify_*)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#494)

Wiki lint 가 docs/wiki/schema/tags.md 에 등록 안 된 태그 거부.
api / ui 만 남겨 dev→main 머지 차단 해제.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/api-server/src/domains/raw_posts/service.rs
…ard (#496)

* feat(cost-tracking): Gemini API per-call cost tracking + admin dashboard

ai-server 의 Gemini 호출 (raw_post 6 + editorial 7) 모두를 호출 시점에
DB 적립 + 어드민 대시보드에서 일/step/model/pipeline 별 spend 가시화.

핵심 결정:
- 단가는 코드 하드코딩 없음 — `gemini_pricing` DB 테이블 SOT (SCD-2,
  effective_from/to). 어드민 UI 에서 inline upsert.
- per-call `pricing_snapshot` 보존 — 단가 변경 후에도 과거 cost 재현 가능.
- fire-and-forget recorder — 추적이 본 파이프라인 latency/실패에 영향 0.
- 실패도 row 적립 (ok=false, error_class) — 안전·정책 차단, quota,
  parse_error, timeout, no_pricing 등 가시화.
- 5분 in-process pricing cache — 어드민 변경 후 최악 5분 stale 허용.

DB (`supabase-assets/migrations/20260514140000_gemini_cost_tracking.sql`):
- gemini_pricing  (SCD-2, 12 seed row — Pro / Flash / Flash-Lite / Flash-Image / grounding)
- gemini_usage_events (per-call, BRIN + composite index 시간순)
- gemini_spend_daily (view, 대시보드 read-side)

ai-server (`src/services/cost_tracking/`):
- `track_call(step, model, extract, coro)` — coroutine wrapper. usage_metadata
  자동 추출 + estimator (token × DB 단가 × cached 우대) + recorder INSERT.
- raw_post scheduler / post_editorial service 에서 `set_context()` 1회 호출
  → 이후 모든 Gemini 호출이 raw_post_id / post_id / pipeline 자동 적립.
- 13 call site wrap: items/subject/spots/nano_banana(hero+thumbnail)/url_grounded/url_filter
  (raw_post 6), design_spec/image_analysis/item_search/news_research/editorial/review/celeb_search (editorial 7).

api-server (`src/domains/admin/gemini_cost.rs`):
- GET /spend/{daily,by-step,by-model,by-pipeline,today,top-raw-posts}
- GET / POST /pricing  (POST 가 기존 active row close + 새 row insert TX)
- DELETE /pricing/{id} (retire)

web (`/admin/gemini-cost`):
- KPI row (Today / Yesterday / 7d / 30d)
- Daily spend AreaChart (7/14/30d period selector)
- Spend by step / model / pipeline (가로 Bar)
- Top expensive raw_posts (drill-down → /admin/raw-posts/[id])
- Pricing editor (inline upsert + history toggle + retire)
- /admin home 에 `<GeminiCostMini>` 카드 + sidebar 새 "Observability" 그룹

검증:
- migration syntax check (local DB BEGIN; ... ROLLBACK;)
- e2e unit test (scripts/cost_tracking_e2e_test.py) — Pro/Flash/Image/cached/
  failure/no-pricing 6 case 모두 단가 계산 정확 (1000 in × \$1.25/M + 500 out
  × \$10/M = \$0.006250 등 모두 ✓)
- 실측 (scripts/cost_probe.py) — 운영 raw_post 1건 풀 파이프라인:
  fixed \$0.041 + per-item \$0.075 = 3 items \$0.268, 5 items 추정 ~\$0.42
- ai-server import / api-server cargo check / web typecheck + lint 모두 clean

Out of scope (별도 트랙):
- Cloud Billing Catalog API 자동 sync (Vertex 이전 후)
- daily threshold alert (Slack/텔레그램)
- 비용 hot path 최적화 (thumbnail/grounded 감축) — 데이터 적립 후 결정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cost-tracking): real-parser wrap verification script

기존 cost_tracking_e2e_test.py 는 `track_call` 을 직접 호출 (mock coroutine).
이 스크립트는 실제 `SubjectParser` 클래스를 import + 호출 → 그 내부에 박힌
wrap 코드 라인이 작동해서 DB row 가 적립되는지 검증한다.

검증 결과 (운영 Pinterest 이미지 1장, ~$0.0005):
  step=subject_parser, model=gemini-2.5-flash, pipeline=raw_post,
  prompt_tokens=813, completion_tokens=98, cost_usd=$0.000489,
  pricing_snapshot={input_token: 0.0000003, output_token: 0.0000025}

→ wrap 코드 경로 + context 자동 전파 + 단가 cache lookup + INSERT 모두
   실제로 작동함을 확인.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

Project Deployment Actions Updated (UTC)
decoded-app Ready Ready Preview, Comment May 14, 2026 6:03am

@cocoyoon cocoyoon added the bump:minor Backward-compatible additions label May 14, 2026
@cocoyoon cocoyoon merged commit 3b9db0e into main May 14, 2026
10 of 11 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in decoded-monorepo May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump:minor Backward-compatible additions

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants