genblaze-replicate 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/.gitignore +6 -1
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/PKG-INFO +6 -2
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/README.md +5 -1
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/genblaze_replicate/provider.py +115 -5
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/pyproject.toml +1 -1
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/tests/test_replicate_provider.py +179 -1
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/genblaze_replicate/__init__.py +0 -0
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/genblaze_replicate/_errors.py +0 -0
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/genblaze_replicate/_version.py +0 -0
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/genblaze_replicate/py.typed +0 -0
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/tests/__init__.py +0 -0
- {genblaze_replicate-0.3.2 → genblaze_replicate-0.3.3}/tests/test_catalog_decoupling.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: genblaze-replicate
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Replicate provider adapter for genblaze
|
|
5
5
|
Project-URL: Homepage, https://github.com/backblaze-labs/genblaze
|
|
6
6
|
Project-URL: Documentation, https://github.com/backblaze-labs/genblaze
|
|
@@ -48,7 +48,11 @@ Supports any model hosted on Replicate — including:
|
|
|
48
48
|
- **Video** — text-to-video and image-to-video models on Replicate
|
|
49
49
|
- **Audio** — music and speech models on Replicate
|
|
50
50
|
|
|
51
|
-
Use the full `owner/model` slug from https://replicate.com/explore
|
|
51
|
+
Use the full `owner/model` slug from https://replicate.com/explore — official
|
|
52
|
+
and community models alike. `submit()` runs each via the right Replicate
|
|
53
|
+
endpoint: community models resolve to their latest published version (cached
|
|
54
|
+
per-slug), official models run version-less. Pin an exact community version
|
|
55
|
+
with `owner/model:<version-hash>`.
|
|
52
56
|
|
|
53
57
|
## Install
|
|
54
58
|
|
|
@@ -21,7 +21,11 @@ Supports any model hosted on Replicate — including:
|
|
|
21
21
|
- **Video** — text-to-video and image-to-video models on Replicate
|
|
22
22
|
- **Audio** — music and speech models on Replicate
|
|
23
23
|
|
|
24
|
-
Use the full `owner/model` slug from https://replicate.com/explore
|
|
24
|
+
Use the full `owner/model` slug from https://replicate.com/explore — official
|
|
25
|
+
and community models alike. `submit()` runs each via the right Replicate
|
|
26
|
+
endpoint: community models resolve to their latest published version (cached
|
|
27
|
+
per-slug), official models run version-less. Pin an exact community version
|
|
28
|
+
with `owner/model:<version-hash>`.
|
|
25
29
|
|
|
26
30
|
## Install
|
|
27
31
|
|
|
@@ -72,6 +72,12 @@ _FALLBACK_SPEC = ModelSpec(
|
|
|
72
72
|
input_mapping=route_by_media_type({"image": "image", "video": "video", "audio": "audio"}),
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
+
# Sentinel distinguishing "slug not yet resolved" from "slug resolved to no
|
|
76
|
+
# version" in ``_version_cache``. The latter caches ``None`` — a real result
|
|
77
|
+
# meaning "official model, submit via the ``model=`` path" — so a plain
|
|
78
|
+
# ``dict.get(slug)`` can't tell a miss from a cached official model.
|
|
79
|
+
_VERSION_UNRESOLVED: Any = object()
|
|
80
|
+
|
|
75
81
|
|
|
76
82
|
class ReplicateProvider(BaseProvider):
|
|
77
83
|
"""Provider adapter for Replicate (replicate.com)."""
|
|
@@ -131,6 +137,19 @@ class ReplicateProvider(BaseProvider):
|
|
|
131
137
|
self._validation_cache: dict[str, tuple[float, ValidationResult]] = {}
|
|
132
138
|
self._validation_lock = threading.RLock()
|
|
133
139
|
self._validation_ttl: float = DEFAULT_TTL_SECONDS
|
|
140
|
+
# Per-slug submit-time resolution cache: ``owner/name`` → version
|
|
141
|
+
# hash, or ``None`` for official/versionless models that run via the
|
|
142
|
+
# ``model=`` path (see ``_resolve_version``). Deliberately *not*
|
|
143
|
+
# TTL-bounded like ``_validation_cache``: genblaze runs a process per
|
|
144
|
+
# pipeline, so pinning the slug→version resolved at first sight gives
|
|
145
|
+
# version consistency across a run (call ``validate_model(refresh=
|
|
146
|
+
# True)`` to re-resolve a newly-published *latest* version). Kept as a
|
|
147
|
+
# small purpose-built dict rather than reusing the base ``_probe_cache``
|
|
148
|
+
# machinery — the value is a bare hash, not a ``LiveProbeResult``,
|
|
149
|
+
# resolution is a cheap idempotent token-free GET, and it is seeded for
|
|
150
|
+
# free from ``validate_model``'s probe. Guarded by the same lock as the
|
|
151
|
+
# validation cache since both are populated from one ``models.get()``.
|
|
152
|
+
self._version_cache: dict[str, str | None] = {}
|
|
134
153
|
# Wire a discovery cache for the first-page snapshot. The fetcher
|
|
135
154
|
# closes over ``self`` so it picks up the (possibly-late-bound)
|
|
136
155
|
# ``self._client``.
|
|
@@ -207,8 +226,14 @@ class ReplicateProvider(BaseProvider):
|
|
|
207
226
|
return base_result
|
|
208
227
|
|
|
209
228
|
if refresh:
|
|
229
|
+
slug, _ = self._split_model_id(model_id)
|
|
210
230
|
with self._validation_lock:
|
|
211
231
|
self._validation_cache.pop(model_id, None)
|
|
232
|
+
# Keep the version cache in lockstep — a caller explicitly
|
|
233
|
+
# asking to refresh a slug's validation almost always wants
|
|
234
|
+
# a fresh "latest_version" too (e.g. the model owner just
|
|
235
|
+
# published a new version).
|
|
236
|
+
self._version_cache.pop(slug, None)
|
|
212
237
|
else:
|
|
213
238
|
cached = self._lookup_validation_cache(model_id)
|
|
214
239
|
if cached is not None:
|
|
@@ -242,9 +267,13 @@ class ReplicateProvider(BaseProvider):
|
|
|
242
267
|
* Other errors → ``UNKNOWN_PERMISSIVE`` (we couldn't verify; let
|
|
243
268
|
the upstream call surface the real error if there is one).
|
|
244
269
|
"""
|
|
270
|
+
# Strip any inline ``:version`` — Replicate's model-existence GET
|
|
271
|
+
# (/v1/models/{owner}/{name}) only accepts the bare slug; a compound
|
|
272
|
+
# ``owner/name:hash`` key 404s even when the model exists.
|
|
273
|
+
slug, _ = self._split_model_id(model_id)
|
|
245
274
|
try:
|
|
246
275
|
client = self._get_client()
|
|
247
|
-
client.models.get(
|
|
276
|
+
model = client.models.get(slug)
|
|
248
277
|
except Exception as exc:
|
|
249
278
|
err_code = map_replicate_error(exc)
|
|
250
279
|
if err_code is ProviderErrorCode.MODEL_ERROR:
|
|
@@ -258,11 +287,78 @@ class ReplicateProvider(BaseProvider):
|
|
|
258
287
|
exc,
|
|
259
288
|
)
|
|
260
289
|
return ValidationResult.unknown_permissive(detail=f"probe inconclusive: {exc}")
|
|
290
|
+
# This probe already fetched the model object that ``submit()`` needs
|
|
291
|
+
# to resolve a version — seed the version cache so a Pipeline run
|
|
292
|
+
# (preflight validate_model, then submit) never fetches it twice.
|
|
293
|
+
self._cache_model_version(slug, model)
|
|
261
294
|
return ValidationResult.ok_authoritative(
|
|
262
295
|
ValidationSource.PROBE,
|
|
263
296
|
detail="confirmed via /v1/models/{owner}/{name}",
|
|
264
297
|
)
|
|
265
298
|
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _split_model_id(model_id: str) -> tuple[str, str | None]:
|
|
301
|
+
"""Split ``owner/name[:version-hash]`` into ``(slug, inline_version)``.
|
|
302
|
+
|
|
303
|
+
Replicate slugs never contain a bare ``:`` themselves, so the first
|
|
304
|
+
colon (if any) unambiguously separates an inline-pinned version hash.
|
|
305
|
+
Shared by every call site that talks to ``/v1/models/{owner}/{name}``
|
|
306
|
+
(which rejects the compound form) or that needs a version hash.
|
|
307
|
+
"""
|
|
308
|
+
slug, _, inline_version = model_id.partition(":")
|
|
309
|
+
return slug, (inline_version or None)
|
|
310
|
+
|
|
311
|
+
def _cache_model_version(self, slug: str, model: Any) -> str | None:
|
|
312
|
+
"""Extract and cache ``model.latest_version.id`` for ``slug``.
|
|
313
|
+
|
|
314
|
+
Shared by ``_authoritative_lookup`` (validate_model's per-slug probe)
|
|
315
|
+
and ``_resolve_version`` (submit-time resolution) so a single
|
|
316
|
+
``models.get()`` round-trip seeds both caches. ``slug`` must already
|
|
317
|
+
be stripped of any inline version (see ``_split_model_id``). Returns —
|
|
318
|
+
and caches — the version hash, or ``None`` for official/versionless
|
|
319
|
+
models. Caching the ``None`` result too means a fan-out of submits over
|
|
320
|
+
an official model probes once rather than re-resolving per prediction.
|
|
321
|
+
"""
|
|
322
|
+
latest_version = getattr(model, "latest_version", None)
|
|
323
|
+
version_id = getattr(latest_version, "id", None) or None
|
|
324
|
+
with self._validation_lock:
|
|
325
|
+
self._version_cache[slug] = version_id
|
|
326
|
+
return version_id
|
|
327
|
+
|
|
328
|
+
def _resolve_version(self, model_id: str) -> str | None:
|
|
329
|
+
"""Resolve ``model_id`` to a version hash for prediction submission.
|
|
330
|
+
|
|
331
|
+
Replicate has two kinds of runnable models submitted via two different
|
|
332
|
+
endpoints, so resolution returns either a hash or ``None`` (#109):
|
|
333
|
+
|
|
334
|
+
* **Community models** carry published versions and 404 on the
|
|
335
|
+
``predictions.create(model=<slug>)`` path — they must be run via
|
|
336
|
+
``predictions.create(version=<hash>)``. Returns their hash.
|
|
337
|
+
* **Official models** (e.g. ``black-forest-labs/flux-schnell``) expose
|
|
338
|
+
*no* version and can only be run via the ``model=<slug>`` path.
|
|
339
|
+
Returns ``None`` to tell ``submit()`` to take that fallback.
|
|
340
|
+
|
|
341
|
+
Resolution order:
|
|
342
|
+
|
|
343
|
+
1. Inline version (``owner/name:hash``) — already a hash; no network.
|
|
344
|
+
2. Cached resolution from a prior ``submit()``/``validate_model()`` for
|
|
345
|
+
this slug (``None`` is a valid cached "official model" result, hence
|
|
346
|
+
the ``_VERSION_UNRESOLVED`` sentinel to detect a genuine miss).
|
|
347
|
+
3. Otherwise ``client.models.get(slug)`` — one round-trip, cached
|
|
348
|
+
per-slug (hash or ``None``) thereafter.
|
|
349
|
+
"""
|
|
350
|
+
slug, inline_version = self._split_model_id(model_id)
|
|
351
|
+
if inline_version:
|
|
352
|
+
return inline_version
|
|
353
|
+
|
|
354
|
+
cached = self._version_cache.get(slug, _VERSION_UNRESOLVED)
|
|
355
|
+
if cached is not _VERSION_UNRESOLVED:
|
|
356
|
+
return cached
|
|
357
|
+
|
|
358
|
+
client = self._get_client()
|
|
359
|
+
model = client.models.get(slug)
|
|
360
|
+
return self._cache_model_version(slug, model)
|
|
361
|
+
|
|
266
362
|
def _get_client(self):
|
|
267
363
|
if self._client is None:
|
|
268
364
|
try:
|
|
@@ -287,11 +383,25 @@ class ReplicateProvider(BaseProvider):
|
|
|
287
383
|
client = self._get_client()
|
|
288
384
|
try:
|
|
289
385
|
input_params = self.prepare_payload(step)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
386
|
+
version = self._resolve_version(step.model)
|
|
387
|
+
if version is not None:
|
|
388
|
+
# Community model — run the resolved version via /v1/predictions.
|
|
389
|
+
prediction = client.predictions.create(
|
|
390
|
+
version=version,
|
|
391
|
+
input=input_params,
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
# Official/versionless model — only runnable via the model=
|
|
395
|
+
# path (/v1/models/{owner}/{name}/predictions). Pass the bare
|
|
396
|
+
# slug; that endpoint rejects a compound ``owner/name:hash``.
|
|
397
|
+
slug, _ = self._split_model_id(step.model)
|
|
398
|
+
prediction = client.predictions.create(
|
|
399
|
+
model=slug,
|
|
400
|
+
input=input_params,
|
|
401
|
+
)
|
|
294
402
|
return prediction.id
|
|
403
|
+
except ProviderError:
|
|
404
|
+
raise
|
|
295
405
|
except Exception as exc:
|
|
296
406
|
raise ProviderError(
|
|
297
407
|
f"Replicate submit failed: {exc}",
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "genblaze-replicate"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Replicate provider adapter for genblaze"
|
|
9
9
|
authors = [{name = "Jeronimo De Leon", email = "jdeleon@backblaze.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -34,6 +34,16 @@ def _make_step(**kwargs) -> Step:
|
|
|
34
34
|
return Step(**defaults)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _make_model_with_version(version_id: str | None):
|
|
38
|
+
"""A fake ``replicate.model.Model`` exposing a concrete ``latest_version``."""
|
|
39
|
+
model = MagicMock()
|
|
40
|
+
if version_id is None:
|
|
41
|
+
model.latest_version = None
|
|
42
|
+
else:
|
|
43
|
+
model.latest_version = MagicMock(id=version_id)
|
|
44
|
+
return model
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
# --- map_replicate_error tests ---
|
|
38
48
|
|
|
39
49
|
|
|
@@ -76,6 +86,7 @@ def test_map_error_accepts_exception():
|
|
|
76
86
|
def test_submit_sends_prompt_and_params():
|
|
77
87
|
provider = ReplicateProvider(api_token="test-token")
|
|
78
88
|
mock_client = MagicMock()
|
|
89
|
+
mock_client.models.get.return_value = _make_model_with_version("v-resolved")
|
|
79
90
|
mock_client.predictions.create.return_value = FakePrediction()
|
|
80
91
|
provider._client = mock_client
|
|
81
92
|
|
|
@@ -83,7 +94,7 @@ def test_submit_sends_prompt_and_params():
|
|
|
83
94
|
result = provider.submit(step)
|
|
84
95
|
|
|
85
96
|
mock_client.predictions.create.assert_called_once_with(
|
|
86
|
-
|
|
97
|
+
version="v-resolved",
|
|
87
98
|
input={"width": 1024, "prompt": "a cat"},
|
|
88
99
|
)
|
|
89
100
|
assert result == "pred-abc123"
|
|
@@ -92,6 +103,7 @@ def test_submit_sends_prompt_and_params():
|
|
|
92
103
|
def test_submit_includes_negative_prompt():
|
|
93
104
|
provider = ReplicateProvider(api_token="test-token")
|
|
94
105
|
mock_client = MagicMock()
|
|
106
|
+
mock_client.models.get.return_value = _make_model_with_version("v-resolved")
|
|
95
107
|
mock_client.predictions.create.return_value = FakePrediction()
|
|
96
108
|
provider._client = mock_client
|
|
97
109
|
|
|
@@ -102,6 +114,168 @@ def test_submit_includes_negative_prompt():
|
|
|
102
114
|
assert call_input["negative_prompt"] == "blurry"
|
|
103
115
|
|
|
104
116
|
|
|
117
|
+
# --- version resolution (community-model 404 fix, #109) ---
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_submit_resolves_community_model_slug_to_version():
|
|
121
|
+
"""A bare ``owner/name`` slug (the common case — community models) must
|
|
122
|
+
resolve to a version hash and submit via ``version=``, not ``model=``.
|
|
123
|
+
``predictions.create(model=...)`` only resolves official models and 404s
|
|
124
|
+
for the vast majority of Replicate's public catalog (#109)."""
|
|
125
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
126
|
+
mock_client = MagicMock()
|
|
127
|
+
mock_client.models.get.return_value = _make_model_with_version("abc123hash")
|
|
128
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
129
|
+
provider._client = mock_client
|
|
130
|
+
|
|
131
|
+
step = _make_step(model="sczhou/codeformer")
|
|
132
|
+
provider.submit(step)
|
|
133
|
+
|
|
134
|
+
mock_client.models.get.assert_called_once_with("sczhou/codeformer")
|
|
135
|
+
mock_client.predictions.create.assert_called_once_with(
|
|
136
|
+
version="abc123hash",
|
|
137
|
+
input={"prompt": "a cat"},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_submit_uses_inline_version_without_network_call():
|
|
142
|
+
"""``owner/name:hash`` carries the version inline — no ``models.get()``
|
|
143
|
+
round-trip needed to resolve it."""
|
|
144
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
145
|
+
mock_client = MagicMock()
|
|
146
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
147
|
+
provider._client = mock_client
|
|
148
|
+
|
|
149
|
+
step = _make_step(model="sczhou/codeformer:deadbeef1234")
|
|
150
|
+
provider.submit(step)
|
|
151
|
+
|
|
152
|
+
mock_client.models.get.assert_not_called()
|
|
153
|
+
mock_client.predictions.create.assert_called_once_with(
|
|
154
|
+
version="deadbeef1234",
|
|
155
|
+
input={"prompt": "a cat"},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_submit_caches_resolved_version_across_calls():
|
|
160
|
+
"""The version resolution is cached per-slug so repeated submits (e.g.
|
|
161
|
+
across a batch run) don't re-issue ``models.get()`` per prediction."""
|
|
162
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
163
|
+
mock_client = MagicMock()
|
|
164
|
+
mock_client.models.get.return_value = _make_model_with_version("cached-version")
|
|
165
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
166
|
+
provider._client = mock_client
|
|
167
|
+
|
|
168
|
+
provider.submit(_make_step(model="sczhou/codeformer"))
|
|
169
|
+
provider.submit(_make_step(model="sczhou/codeformer"))
|
|
170
|
+
|
|
171
|
+
assert mock_client.models.get.call_count == 1
|
|
172
|
+
assert mock_client.predictions.create.call_count == 2
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_submit_falls_back_to_model_path_for_official_versionless_model():
|
|
176
|
+
"""Official Replicate models (e.g. ``black-forest-labs/flux-schnell``)
|
|
177
|
+
expose no version and are only runnable via ``predictions.create(model=…)``
|
|
178
|
+
(POST /v1/models/{owner}/{name}/predictions). When ``models.get`` reports
|
|
179
|
+
no ``latest_version``, submit() must fall back to the ``model=`` path with
|
|
180
|
+
the bare slug rather than raise — raising would break every official model,
|
|
181
|
+
including several this connector documents as supported (#109)."""
|
|
182
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
183
|
+
mock_client = MagicMock()
|
|
184
|
+
mock_client.models.get.return_value = _make_model_with_version(None)
|
|
185
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
186
|
+
provider._client = mock_client
|
|
187
|
+
|
|
188
|
+
result = provider.submit(_make_step(model="black-forest-labs/flux-schnell"))
|
|
189
|
+
|
|
190
|
+
mock_client.predictions.create.assert_called_once_with(
|
|
191
|
+
model="black-forest-labs/flux-schnell",
|
|
192
|
+
input={"prompt": "a cat"},
|
|
193
|
+
)
|
|
194
|
+
# A versionless official model must NOT be submitted with a version= kwarg.
|
|
195
|
+
assert "version" not in mock_client.predictions.create.call_args.kwargs
|
|
196
|
+
assert result == "pred-abc123"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_submit_caches_versionless_resolution_across_calls():
|
|
200
|
+
"""The "official / no version" decision is cached per-slug just like a
|
|
201
|
+
resolved hash — a fan-out of submits over an official model issues one
|
|
202
|
+
``models.get`` probe, not one per prediction."""
|
|
203
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
204
|
+
mock_client = MagicMock()
|
|
205
|
+
mock_client.models.get.return_value = _make_model_with_version(None)
|
|
206
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
207
|
+
provider._client = mock_client
|
|
208
|
+
|
|
209
|
+
provider.submit(_make_step(model="black-forest-labs/flux-schnell"))
|
|
210
|
+
provider.submit(_make_step(model="black-forest-labs/flux-schnell"))
|
|
211
|
+
|
|
212
|
+
assert mock_client.models.get.call_count == 1
|
|
213
|
+
assert mock_client.predictions.create.call_count == 2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_validate_model_seeds_version_cache_for_submit():
|
|
217
|
+
"""``validate_model``'s per-slug probe and ``submit``'s version resolution
|
|
218
|
+
share one ``models.get()`` round-trip per slug — the pipeline preflight
|
|
219
|
+
phase already fetches the model, so submit() shouldn't fetch it again."""
|
|
220
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
221
|
+
mock_client = MagicMock()
|
|
222
|
+
mock_client.models.get.return_value = _make_model_with_version("seeded-version")
|
|
223
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
224
|
+
provider._client = mock_client
|
|
225
|
+
|
|
226
|
+
provider.validate_model("sczhou/codeformer")
|
|
227
|
+
provider.submit(_make_step(model="sczhou/codeformer"))
|
|
228
|
+
|
|
229
|
+
assert mock_client.models.get.call_count == 1
|
|
230
|
+
mock_client.predictions.create.assert_called_once_with(
|
|
231
|
+
version="seeded-version",
|
|
232
|
+
input={"prompt": "a cat"},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_validate_model_strips_inline_version_before_probe():
|
|
237
|
+
"""A ``Pipeline.run()`` preflight validates ``step.model`` verbatim,
|
|
238
|
+
including any inline ``:version`` suffix. Replicate's model-existence
|
|
239
|
+
GET (``/v1/models/{owner}/{name}``) only accepts the bare slug — a
|
|
240
|
+
compound ``owner/name:hash`` key 404s even when the model exists, which
|
|
241
|
+
would otherwise abort the run with a false "model not found" before
|
|
242
|
+
``submit()`` ever runs. ``validate_model`` must probe the bare slug."""
|
|
243
|
+
from genblaze_core.providers import ValidationOutcome
|
|
244
|
+
|
|
245
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
246
|
+
mock_client = MagicMock()
|
|
247
|
+
mock_client.models.get.return_value = _make_model_with_version("resolved-hash")
|
|
248
|
+
provider._client = mock_client
|
|
249
|
+
|
|
250
|
+
result = provider.validate_model("sczhou/codeformer:deadbeef1234")
|
|
251
|
+
|
|
252
|
+
mock_client.models.get.assert_called_once_with("sczhou/codeformer")
|
|
253
|
+
assert result.outcome is ValidationOutcome.OK_AUTHORITATIVE
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_validate_model_refresh_also_evicts_version_cache():
|
|
257
|
+
"""``refresh=True`` is a request to re-check everything about a slug —
|
|
258
|
+
it must also drop any cached version resolution, not just the
|
|
259
|
+
validation-outcome cache, so a subsequent ``submit()`` re-resolves
|
|
260
|
+
``latest_version`` instead of silently reusing a stale hash."""
|
|
261
|
+
provider = ReplicateProvider(api_token="test-token")
|
|
262
|
+
mock_client = MagicMock()
|
|
263
|
+
mock_client.models.get.return_value = _make_model_with_version("v1")
|
|
264
|
+
mock_client.predictions.create.return_value = FakePrediction()
|
|
265
|
+
provider._client = mock_client
|
|
266
|
+
|
|
267
|
+
provider.validate_model("sczhou/codeformer")
|
|
268
|
+
provider.submit(_make_step(model="sczhou/codeformer"))
|
|
269
|
+
assert mock_client.predictions.create.call_args.kwargs["version"] == "v1"
|
|
270
|
+
|
|
271
|
+
# Model owner publishes a new version; caller refreshes.
|
|
272
|
+
mock_client.models.get.return_value = _make_model_with_version("v2")
|
|
273
|
+
provider.validate_model("sczhou/codeformer", refresh=True)
|
|
274
|
+
provider.submit(_make_step(model="sczhou/codeformer"))
|
|
275
|
+
|
|
276
|
+
assert mock_client.predictions.create.call_args.kwargs["version"] == "v2"
|
|
277
|
+
|
|
278
|
+
|
|
105
279
|
# --- poll tests ---
|
|
106
280
|
|
|
107
281
|
|
|
@@ -563,6 +737,10 @@ class TestReplicateCompliance(ProviderComplianceTests):
|
|
|
563
737
|
def make_provider(self):
|
|
564
738
|
provider = ReplicateProvider(api_token="test-token")
|
|
565
739
|
mock_client = MagicMock()
|
|
740
|
+
# submit() resolves a version via models.get().latest_version.id
|
|
741
|
+
# before creating a prediction — stub it explicitly rather than
|
|
742
|
+
# relying on MagicMock's auto-vivified (but opaque) attribute chain.
|
|
743
|
+
mock_client.models.get.return_value = _make_model_with_version("compliance-version")
|
|
566
744
|
mock_client.predictions.create.return_value = FakePrediction()
|
|
567
745
|
mock_client.predictions.get.return_value = FakePrediction()
|
|
568
746
|
provider._client = mock_client
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|