Live Replicate client: poll/API contract, pinned models, and hero JPEG #5

Open
opened 2026-08-31 05:33:04 +00:00 by PlasticDigits · 4 comments
PlasticDigits commented 2026-08-31 05:33:04 +00:00 (Migrated from gitlab.com)

Summary

Make the live Replicate client match production HTTP, pin model/version in config, poll until terminal status, and write hero JPEG bytes so frontmatter image does not 404. Replace --skip-image default-true with an explicit --image opt-in. Add cassette tests for poll/API shape.

Gap: gaps/GAP_1788152435.md §2 (hero, model pin), §5 (redirects, budget-before-POST, poll), §6.2 (skip_image).

Voice/image contract remains CL8Y-web blog_gen/SKILL.md and blog_gen/generate_image.py (one predictions.create per command, poll by id, never retry create).


Current codebase

src/replicate.rs:

  • FixturePredictor is well tested (create-once, budget, drop-after-create).
  • HttpPredictor POSTs https://api.replicate.com/v1/predictions with JSON { "model", "input": { prompt, step } }unverified vs current Replicate API (classic wanted version; models API is POST /v1/models/{owner}/{name}/predictions).
  • Client is built without redirect(Policy::none()) (indexer client correctly disables redirects).
  • Budget + create_count increment before POST. Network failure burns budget with no id.
  • poll returns on first HTTP 200 even if status is starting / processing. Loop is 8 × 400ms — too short for image/text jobs.
  • step_text sends model: "fixture-or-replicate" for every text step.

src/pipeline.rs image step: create_once for "image" then does not download or write a JPEG. Frontmatter still sets image: /images/blog/<slug>-hero.jpg.

src/main.rs: --skip-image default_value_t = true. Enabling a hero is --skip-image=false. Fixture hero fixtures/assets/hero.jpg is a 158-byte 1×1 JPEG.

DEFAULT_WEEKLY_CREATE_BUDGET is 6 (plan, outline, draft, editor bundle, optional image).


Why this is needed

--live cannot be trusted in production. Visitors will 404 heroes. Operators cannot recover creates. Model ids must be pinned so spend and output are reproducible.


Constraints / guardrails

  1. One predictions.create per step. Never retry create. Recover with cl8y-research fetch PREDICTION_ID / poll.
  2. Charge weekly budget after Replicate accepts and returns an id (or document failed-create-without-id as not charged).
  3. Poll may retry transient GET errors; poll must wait for succeeded / failed / canceled with backoff suitable for GPT-image and long text (minutes, not 3.2s).
  4. HTTPS to api.replicate.com only; no redirects; token never logged.
  5. Pin text + image model ids (and optional version hashes) in config/env. Fixtures keep FixturePredictor.
  6. Image: at most one create per week unless --image is off. Do not loop "until it looks good."
  7. --image opt-in (default off). When off, still write a valid JPEG placeholder so the frontmatter path exists (copy fixtures/assets/hero.jpg or a committed placeholder). When on, download prediction output and write <out>/hero.jpg / API blob. Cap download size; verify JPEG magic bytes.
  8. Output treated as untrusted until lint/sanitize.

Relevant files

Path Role
src/replicate.rs HTTP client, budget, poll
src/pipeline.rs step_text, image create without write
src/main.rs --live, --skip-image
src/config.rs weekly_create_budget, token
src/invariants.rs DEFAULT_WEEKLY_CREATE_BUDGET
fixtures/happy-week/replicate.json Fixture outputs
fixtures/assets/hero.jpg Tiny placeholder
CL8Y-web/blog_gen/generate_image.py Canonical image create-once

  1. Config: REPLICATE_TEXT_MODEL, REPLICATE_IMAGE_MODEL (and optional version). Fail --live if unset.
  2. Align HTTP with current Replicate docs; record httpmock cassettes for create + poll startingsucceeded + image bytes.
  3. poll: loop while starting/processing; honor Retry-After if present; cap total wait.
  4. Image: parse output URL(s) or base64; write JPEG; verify magic FF D8.
  5. Clap: --image flag (default false). Deprecate --skip-image.
  6. No-redirect client; charge after id.

Acceptance criteria

  • --live without pinned model env fails closed (no "fixture-or-replicate" on the wire).
  • Mock: create returns starting, poll 2× processing then succeeded → one create, output used.
  • Mock: create accepted then TCP drop → id logged; fetch recovers; no second create.
  • Mock: redirect to evil host on create/poll → rejected.
  • --image: JPEG written next to post.mdx; frontmatter path matches file.
  • Without --image: placeholder JPEG still written so the path does not 404.
  • Budget not incremented when POST never returns an id.
  • Fixture tests do not need REPLICATE_API_TOKEN.

Test plan — functional paths

  1. Existing fixture create-once / budget tests.
  2. Poll until succeeded (mock sequence).
  3. Poll failed — emit fails closed; id in report.
  4. Fetch recovery after drop-after-accept.
  5. Image write — mock output file URL; bytes on disk; image/jpeg.
  6. Pinned model — request contains config model, not fixture-or-replicate.
  7. --image clap — default off; on writes generated file.

Test plan — attack, hack, and abuse

  1. Cost amplification — failed poll does not create again; budget cap aborts extra steps.
  2. Open redirect / SSRF — Replicate client must not follow redirects to 169.254.169.254.
  3. Output injection — prediction output containing REPLICATE_API_TOKEN / <script> fails emit.
  4. Image bomb — cap download size; reject non-JPEG.
  5. Token in reportreport.md has prediction ids only, not bearer token.
  6. Wait forever — poll has a hard deadline.

Verification criteria

  1. Cassette tests in CI (no live token).
  2. Optional operator: one live --fetch against a real prediction id.
  3. Inspect runs/latest for a JPEG when --image is used, and a placeholder JPEG when it is not.
## Summary Make the **live Replicate client** match production HTTP, pin **model/version in config**, **poll until terminal status**, and **write hero JPEG bytes** so frontmatter `image` does not 404. Replace `--skip-image` default-true with an explicit `--image` opt-in. Add cassette tests for poll/API shape. Gap: [`gaps/GAP_1788152435.md`](https://gitlab.com/PlasticDigits/cl8y-research/-/blob/main/gaps/GAP_1788152435.md) §2 (hero, model pin), §5 (redirects, budget-before-POST, poll), §6.2 (`skip_image`). Voice/image contract remains `CL8Y-web` `blog_gen/SKILL.md` and `blog_gen/generate_image.py` (one `predictions.create` per command, poll by id, never retry create). --- ## Current codebase `src/replicate.rs`: - `FixturePredictor` is well tested (create-once, budget, drop-after-create). - `HttpPredictor` POSTs `https://api.replicate.com/v1/predictions` with JSON `{ "model", "input": { prompt, step } }` — **unverified** vs current Replicate API (classic wanted `version`; models API is `POST /v1/models/{owner}/{name}/predictions`). - Client is built **without** `redirect(Policy::none())` (indexer client correctly disables redirects). - Budget + `create_count` increment **before** POST. Network failure burns budget with no id. - `poll` returns on first HTTP 200 even if `status` is `starting` / `processing`. Loop is 8 × 400ms — too short for image/text jobs. - `step_text` sends `model: "fixture-or-replicate"` for every text step. `src/pipeline.rs` image step: `create_once` for `"image"` then **does not download or write a JPEG**. Frontmatter still sets `image: /images/blog/<slug>-hero.jpg`. `src/main.rs`: `--skip-image` `default_value_t = true`. Enabling a hero is `--skip-image=false`. Fixture hero `fixtures/assets/hero.jpg` is a 158-byte 1×1 JPEG. `DEFAULT_WEEKLY_CREATE_BUDGET` is 6 (plan, outline, draft, editor bundle, optional image). --- ## Why this is needed `--live` cannot be trusted in production. Visitors will 404 heroes. Operators cannot recover creates. Model ids must be pinned so spend and output are reproducible. --- ## Constraints / guardrails 1. **One `predictions.create` per step.** Never retry create. Recover with `cl8y-research fetch PREDICTION_ID` / `poll`. 2. Charge weekly budget **after** Replicate accepts and returns an id (or document failed-create-without-id as not charged). 3. Poll may retry transient GET errors; poll must **wait for** `succeeded` / `failed` / `canceled` with backoff suitable for GPT-image and long text (minutes, not 3.2s). 4. HTTPS to `api.replicate.com` only; **no redirects**; token never logged. 5. Pin text + image model ids (and optional version hashes) in config/env. Fixtures keep `FixturePredictor`. 6. Image: at most **one** create per week unless `--image` is off. Do not loop "until it looks good." 7. `--image` opt-in (default off). When off, still write a valid JPEG placeholder so the frontmatter path exists (copy `fixtures/assets/hero.jpg` or a committed placeholder). When on, download prediction output and write `<out>/hero.jpg` / API blob. Cap download size; verify JPEG magic bytes. 8. Output treated as untrusted until lint/sanitize. --- ## Relevant files | Path | Role | | --- | --- | | `src/replicate.rs` | HTTP client, budget, poll | | `src/pipeline.rs` | `step_text`, image create without write | | `src/main.rs` | `--live`, `--skip-image` | | `src/config.rs` | `weekly_create_budget`, token | | `src/invariants.rs` | `DEFAULT_WEEKLY_CREATE_BUDGET` | | `fixtures/happy-week/replicate.json` | Fixture outputs | | `fixtures/assets/hero.jpg` | Tiny placeholder | | `CL8Y-web/blog_gen/generate_image.py` | Canonical image create-once | --- ## Recommended direction 1. Config: `REPLICATE_TEXT_MODEL`, `REPLICATE_IMAGE_MODEL` (and optional version). Fail `--live` if unset. 2. Align HTTP with current Replicate docs; record **httpmock cassettes** for create + poll `starting`→`succeeded` + image bytes. 3. `poll`: loop while `starting`/`processing`; honor `Retry-After` if present; cap total wait. 4. Image: parse output URL(s) or base64; write JPEG; verify magic `FF D8`. 5. Clap: `--image` flag (default false). Deprecate `--skip-image`. 6. No-redirect client; charge after id. --- ## Acceptance criteria - [ ] `--live` without pinned model env fails closed (no `"fixture-or-replicate"` on the wire). - [ ] Mock: create returns `starting`, poll 2× processing then succeeded → one create, output used. - [ ] Mock: create accepted then TCP drop → id logged; `fetch` recovers; **no second create**. - [ ] Mock: redirect to evil host on create/poll → rejected. - [ ] `--image`: JPEG written next to `post.mdx`; frontmatter path matches file. - [ ] Without `--image`: placeholder JPEG still written so the path does not 404. - [ ] Budget not incremented when POST never returns an id. - [ ] Fixture tests do not need `REPLICATE_API_TOKEN`. --- ## Test plan — functional paths 1. Existing fixture create-once / budget tests. 2. **Poll until succeeded** (mock sequence). 3. **Poll failed** — emit fails closed; id in report. 4. **Fetch recovery** after drop-after-accept. 5. **Image write** — mock output file URL; bytes on disk; `image/jpeg`. 6. **Pinned model** — request contains config model, not `fixture-or-replicate`. 7. **`--image` clap** — default off; on writes generated file. --- ## Test plan — attack, hack, and abuse 1. **Cost amplification** — failed poll does not create again; budget cap aborts extra steps. 2. **Open redirect / SSRF** — Replicate client must not follow redirects to `169.254.169.254`. 3. **Output injection** — prediction output containing `REPLICATE_API_TOKEN` / `<script>` fails emit. 4. **Image bomb** — cap download size; reject non-JPEG. 5. **Token in report** — `report.md` has prediction ids only, not bearer token. 6. **Wait forever** — poll has a hard deadline. --- ## Verification criteria 1. Cassette tests in CI (no live token). 2. Optional operator: one live `--fetch` against a real prediction id. 3. Inspect `runs/latest` for a JPEG when `--image` is used, and a placeholder JPEG when it is not.
PlasticDigits commented 2026-08-31 05:35:03 +00:00 (Migrated from gitlab.com)

marked as related to #10

marked as related to #10
PlasticDigits commented 2026-08-31 05:35:05 +00:00 (Migrated from gitlab.com)

marked as related to #6

marked as related to #6
PlasticDigits commented 2026-09-02 02:18:48 +00:00 (Migrated from gitlab.com)

mentioned in issue #12

mentioned in issue #12
PlasticDigits commented 2026-09-02 02:18:49 +00:00 (Migrated from gitlab.com)

marked as related to #12

marked as related to #12
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-research#5
No description provided.