Repository navigation
Conversation
…, music) ModelRunner (modelrunner.ai) exposes hosted video, image, speech, and music model families behind one API key and one async queue contract. This adds four provider tools sharing a small client: - tools/modelrunner_client.py — submit/poll/result/upload/download plumbing. Monotonic poll deadline with sleeps AND per-request timeouts capped to the remaining budget, at most five consecutive transport errors tolerated, unknown statuses treated as in-flight. The API key is only ever attached to the two first-party hosts, and authenticated calls never follow redirects; output URLs are validated (https, public host) before download and re-validated on every redirect hop; upload destinations from the initiate response are held to the same bar, and uploads stream from disk. A submission whose queue envelope turns out unusable raises with the request_id preserved so billing provenance survives. - tools/modelrunner_models.py — route catalog verified against the live catalog API on 2026-08-28, including the resolution-tiered per-second rate tables several video routes bill with. - modelrunner_video / modelrunner_image / modelrunner_tts / modelrunner_music — one tool per capability, provider="modelrunner", validated inputs that fail before billing: extra_params cannot override validated fields, poll controls are parsed before any paid submit, payload validation runs before the storage upload, and an explicitly pinned endpoint is never silently rewritten to a sibling route. Post-submit failures return the request id and status/response URLs with billing_status="possibly_billed" instead of pretending nothing was spent. A COMPLETED result carrying a non-empty error (a normalized provider failure) is treated as failure, never output. The video tool deliberately does not advertise `image_url` in its schema (the seedance_ark convention) so the video_selector hands local reference images through for ModelRunner-storage hosting instead of fal-uploading them. - .agents/skills/modelrunner/SKILL.md — Layer 3 route/price tables and the result/failure contract. - scripts/modelrunner_media_smoke.py — opt-in paid smoke (~$0.69) covering all four capabilities through the selectors. - Pipeline manifests + matching asset-director skills list modelrunner_music beside the existing music tools (music has no selector); video, image, and TTS route through the existing selectors with no selector code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tests/contracts/test_modelrunner_tools.py — identity, three-alias key detection, no-key-fails-before-network, catalog metadata, AST-checked lazy requests imports, estimate_cost never raising, registry discovery, video/image selector exact-model pinning, the schema-must-not-advertise- image_url selector contract, model-enum/alias consistency, per-route payload shapes, and every pricing-mode calculator including the resolution-tiered video rates and size-tiered Seedream Pro rates. - tests/tools/test_modelrunner_video.py — faked-requests behavior for all four capabilities: the submit -> poll -> result -> download cycle, the storage upload flow (streamed bodies materialized by the fake transport), multi-image output naming, FAILED and COMPLETED-with-error results, poll timeout on a fake monotonic clock with the possibly_billed recovery payload, transport-error tolerance, and the auth boundary: no key to off-host URLs, non-https/private outputs refused, redirects on authenticated calls refused, download redirect hops re-validated (private targets never requested), and an unusable queue envelope still surfacing request_id provenance. Fail-before-billing guards: a pinned image-to-video endpoint is never rewritten to its text sibling, extra_params cannot override validated fields, malformed poll controls fail pre-submit, and invalid payloads fail before the media upload. - tests/tools/test_video_selector_routing.py — the selector hands a local reference image through to modelrunner_video untouched (no fal upload, no FAL_KEY needed), using the real tool schema so drift breaks the test. - tests/test_network_guard.py — the four ModelRunner tools fail instead of billing with a key present under the session network guard. - tests/contracts/test_phase3_contracts.py — "modelrunner" joins the exact TTS provider set the registry catalog asserts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/PROVIDERS.md — env summary line, a ModelRunner gateway section with the per-route rate tables (kept resolution-tiered where billing is), Provider-to-Tool Mapping row, and Capability Coverage updates. - docs/ARCHITECTURE.md — tool inventory rows and counts. - AGENT_GUIDE.md — the modelrunner Layer 3 skill in the video, image, and audio category rows. - skills/INDEX.md — installed-skill row for the modelrunner skill. - README.md + README_zh-CN.md — API-key snippet lines and Supported Providers rows in both languages. - PROJECT_CONTEXT.md — Key Files rows for the shared client and catalog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calesthio
left a comment
There was a problem hiding this comment.
Thank you for the documented ModelRunner catalog, validation and auth boundaries. The current public catalog is active, and all 107 focused contract/transport tests pass. The billing/recovery contract needs correction before approval.
[P1] Preserve unknown charges after accepted or ambiguous submissions. All four _failure methods set cost_usd=0.0 even when queue data says billing_status=possibly_billed; a lost POST response also falls into the zero-cost path. An error after acceptance or download failure can still represent a completed billable generation. Return None for unknown actual charges, retain all known request coordinates, and explicitly mark ambiguous submit outcomes as unsafe to resubmit. A billing note alongside zero does not fix the ledger value.
[P1] Add collection/resume without a second paid POST. Each execute method always submits; there is no queue/resume input that consumes the status_url/response_url returned on failure. The recovery information is therefore insufficient for the agent to finish the same job through the tool. Add a validated GET-only collect path (including artifact download recovery), persist accepted queue coordinates, and test that rerunning collection does not submit.
[P2] Reconcile delivered video usage rather than requested seconds. ModelRunnerVideo returns estimate_cost using payload duration even though the PR's own billed example shows a requested 4s Seedance clip billed at delivered 4.1s. Use verified provider billing/usage, or preserve an explicitly estimated/unknown charge; also reserve enough to cover documented output rounding.
No live or paid request was made.
…t submit Addresses the review on this PR: - Failures never report a silent 0.0 for a job that may exist. cost_usd is the charge read back from the ModelRunner request record, or None when it cannot be verified (ToolResult.cost_usd is now Optional[float]; CostTracker counts an unknown charge at its reservation). Every failure carries billing_status and resubmit_safe. - An accepted job can be collected by passing request_id (or status_url / response_url) back to any of the four tools: poll + download, no POST. - Submits send a per-attempt X-Idempotency-Key and retry lost / 5xx / 429 answers under the same key, so a retry never starts a second paid job. A refusal is confirmed by one same-key re-send before it is reported as not submitted. - Per-second video and TTS estimates add 1s of headroom: billing is on delivered seconds (4s requested -> 4.1s delivered). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. All three points were right. Here's how this push addresses each one. P1a: a failure reported
P1b: there was no way to collect a submitted job without resubmitting. P2: estimates were lower than the charge. One core-type change that needs your OK: Tests extend the existing suites: Live check against production (Kokoro TTS). The same |
|
@calesthio all three points are addressed in 0167758 (details in the comment above). Could you take another look when you have a moment? |
Conflicts resolved: - tools/base_tool.py: main already made cost_usd Optional[float]; kept the comment explaining that None means an unverified charge. - tests/tools/test_video_selector_routing.py: kept both the ModelRunner local-reference test and main's image_to_video inference tests. - docs/ARCHITECTURE.md: took main's package table and counted the four ModelRunner modules into it. - docs/PROVIDERS.md: took main's rows (adds Pika H3 Flex) and re-added the ModelRunner entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review asked for accepted queue coordinates to be persisted. They were only returned in the ToolResult, so a process that died while polling lost the request id. Each submit now writes a job file through provider_jobs.save_job, at job_path (default <output_path>.job.json): the attempt id before the POST, then the request id and queue URLs once the job is accepted. An unwritable job path fails before anything is sent. Failures name the file in their recovery hint, and the saved request_id is all a collect call needs. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Status update, two commits since the reply above:
Two things in my earlier reply no longer apply:
CI on this PR is waiting for workflow approval, so it hasn't run yet. |
Summary
Adds ModelRunner as a multi-model provider gateway: one
MODELRUNNER_KEYand one async queue contract in front of 17 verified routes across all four paid generation capabilities — video, image, TTS, and music. This is the implementation proposed in #569 and follows the accepted Atlas Cloud gateway shape: a shared client module, an explicit verified route catalog, and one thinBaseToolper capability, discovered by the existing selectors with zero selector code changes.Disclosure: I work at ModelRunner. I'm contributing and committing to maintain this integration, and the testing evidence below includes live billed requests against production endpoints.
Related issue
Closes #569
Why ModelRunner belongs (provider viability)
tools/modelrunner_models.pycarries an explicit pricing mode and rate table (including resolution-tiered per-second video rates), snapshot-dated (2026-08-28) with source comments, feeding truthfulestimate_cost()before any paid call.Changes
Provider gateway (new)
tools/modelrunner_client.py— shared queue client: submit → pollstatus_url→ fetchresponse_url; monotonic poll deadline (sleeps and per-request timeouts capped to the remaining budget), bounded transport-error tolerance, result-envelope normalization (scalar vs list outputs per route), storage upload (streamed from disk) for image-to-video. Auth boundary: the key is attached only to the two first-party hosts, authenticated calls never follow redirects, output-download URLs are validated (https, public host) and re-validated on every redirect hop, and upload destinations from the initiate response are held to the same bar. A submission whose queue envelope turns out unusable still surfaces therequest_idfor billing provenance.requestsis imported lazily so registry discovery never depends on it.tools/modelrunner_models.py— verified route catalog: 17 routes (5 video, 5 image, 4 TTS, 3 music) with per-route pricing modes, configuration-keyed rate tables, output specs, and capability flags.tools/video/modelrunner_video.py,tools/graphics/modelrunner_image.py,tools/audio/modelrunner_tts.py,tools/audio/modelrunner_music.py— oneBaseToolper capability (provider="modelrunner",stability=beta). Fail-before-billing validation:extra_paramscannot override validated fields, poll controls are parsed before any paid submit, payload validation runs before the storage upload, and an explicitly pinned endpoint is never silently rewritten to a sibling route. Post-submit failures returncost_usd=0.0withrequest_id/status_url/response_urlandbilling_status="possibly_billed"; nothing is ever auto-resubmitted. The video tool deliberately does not advertiseimage_urlin its schema (theseedance_arkconvention), sovideo_selectorhands local reference images through for ModelRunner-storage hosting instead of fal-uploading them — image-to-video works on a machine with onlyMODELRUNNER_KEY.Skill & docs
.agents/skills/modelrunner/SKILL.md— Layer-3 skill: route/price tables, canonical execute example, result and failure contract, troubleshooting.docs/PROVIDERS.md,docs/ARCHITECTURE.md,AGENT_GUIDE.md,skills/INDEX.md,README.md,README_zh-CN.md,PROJECT_CONTEXT.md,.env.example— provider registration, env keys (MODELRUNNER_KEY, plusMODELRUNNER_API_KEY/MRUN_API_KEYaliases), route/price tables.Pipeline exposure
modelrunner_music(manifests are the authoritative tool lists; video/image/TTS flow through selectors automatically and need no manifest edits).Tests (per
docs/PR_REVIEW_GUIDE.mdminimum coverage for a new provider — all mocked, no sockets)tests/contracts/test_modelrunner_tools.py— identity/metadata contracts, 3-alias status checks, no-key fails withcost_usd == 0.0before any socket, registry discovery, selector exact-model narrowing, the schema-must-not-advertise-image_urlselector contract, payload builders per route, lazy-import guarantee (AST-checked), pricing-calculator branches incl. resolution tiers.tests/tools/test_modelrunner_video.py— happy path per modality, FAILED / COMPLETED-with-error, poll timeout withpossibly_billeddata, transport-error tolerance, image-to-video upload flow, auth boundary (noAuthorizationoff-host, redirects on authenticated calls refused, download redirect hops re-validated, unusable queue envelopes still carryrequest_id), and the fail-before-billing guards above.tests/tools/test_video_selector_routing.py— the selector passes a local reference image through tomodelrunner_videountouched (no FAL upload), using the real tool schema so drift breaks the test.tests/test_network_guard.py— ModelRunner fails-instead-of-billing case.tests/contracts/test_phase3_contracts.py— registry TTS provider set extended withmodelrunner.Testing
make lint && make test: 1947 passed, 12 skipped, 3 xfailed locally (macOS, Python 3.10 venv; syntax is 3.10-compatible).MODELRUNNER_KEYset, each capability menu reportsmodelrunnerconfigured (video 1/27, image 1/17, TTS 1/11, music 1/6).video_selectorrank mode includesmodelrunner_video; an exact-model pin (wan-video/wan/v2.7/text-to-video) narrows the ranking to['modelrunner_video'];tts_selectorroutespreferred_provider="modelrunner"+model_idto the tool. No selector code was changed.scripts/modelrunner_media_smoke.py, opt-in--allow-paid, routed through the selectors so it also proves discovery and routing). Billed prices read back from ModelRunner's request records:tts_selector)hexgrad/kokoro-82mVJchT3Z20ZRxc2kJz4Z2Gimage_selector)bytedance/seedream-v5/text-to-imageCykylRXcvtbblKDUk0iqAgoogle/lyria-3/clipt6r427XUjLdAO1yogfcQWvideo_selector)bytedance/seedance-v2-mini/text-to-video376rdaTOCPXtsKgVVdurwvideo_selector)wan-video/wan/v2.7/text-to-videoTinqrOx4RlmgaLJS8HuzzThree exact estimate↔billed matches; Kokoro billed below its upper-bound estimate; Seedance billed on its delivered 4.1s output (per-second routes bill delivered length — now noted in the skill).
google/lyria2happened to be failing upstream. The gateway surfaced each failure as a failedToolResultwithcost_usd=0.0, and ModelRunner's request records confirmbillingStatus: failed/ $0 for all three attempts (nR9nDLH2r88QNwPndrhJE,3fUgghHkyeVvPrwjge2Hf,KZ2gMsILqMYiJwzFtoTFN) — a real-world exercise of the fail-honestly contract, including the COMPLETED-with-error envelope the client is built to reject.Preserved deliberately
seedance_arkconvention for self-hosted local references.supportsflags — tools only advertise what every default route guarantees; route-specific extras (lip-sync, native-audio toggles) live inmodel_catalogmetadata so ranking isn't inflated.stability=betaand noquality_scoreself-promotion for an affiliated provider.If you'd prefer a smaller first landing, I'm happy to split this into core+video/image first with TTS/music as a follow-up.
Checklist
make test-contracts/make test) where applicable.🤖 Generated with Claude Code