runpod: capability pods default to the published images

Now that ghcr.io/nextime/coderai-<capability> is public, a capability pod
needs no image name: leaving it blank uses the published image for that
capability. Enabling a capability on RunPod is one dropdown.

Capabilities that ride another profile's dependency set are mapped —
rerank to embeddings, speaker to stt, stems/audio_clean/audio_gen to
audio. The four with no published image (loras, characters, environments,
pipelines) return no default and say so, rather than defaulting to a tag
that 404s minutes into a pod boot.

CODERAI_CAPABILITY_IMAGE_REPO / _TAG point the whole set at a fork or a
private registry; a row's own `image` still wins. The UI shows the
default as each row's placeholder, so it is visible rather than implied.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 465eb10f
...@@ -950,7 +950,7 @@ ...@@ -950,7 +950,7 @@
</div> </div>
</div> </div>
<div id="s-remotes-rows" style="margin-top:.4rem"></div> <div id="s-remotes-rows" style="margin-top:.4rem"></div>
<p class="form-hint" style="margin-top:.5rem">A pod row needs an image you have pushed to a registry RunPod can pull from — see <code>packaging/runpod/publish_capability_images.sh</code>. Give several capabilities the same <b>pool</b> name and they share one pod and one budget.</p> <p class="form-hint" style="margin-top:.5rem">A pod row left blank uses the published <code>ghcr.io/nextime/coderai-&lt;capability&gt;</code> image — a trimmed coderai carrying only that capability. Override it to run your own build (<code>packaging/runpod/publish_capability_images.sh</code>). Give several capabilities the same <b>pool</b> name and they share one pod and one budget.</p>
</div> </div>
</div> </div>
...@@ -2224,6 +2224,19 @@ const REMOTE_CAPS = [ ...@@ -2224,6 +2224,19 @@ const REMOTE_CAPS = [
['pipelines','Pipelines','/v1/pipelines/*'], ['pipelines','Pipelines','/v1/pipelines/*'],
]; ];
// The published capability images (packaging/runpod/). A pod row left blank uses
// the one for its capability, so enabling a capability needs no image typed in;
// mirrors default_capability_image() in codai/api/runpod_worker.py.
const CAP_IMAGE_REPO = 'ghcr.io/nextime/coderai';
const CAP_IMAGE_PUBLISHED = ['images','video','embeddings','ocr','tts','stt',
'voice','audio','faceswap'];
const CAP_IMAGE_ALIASES = {rerank:'embeddings', speaker:'stt', stems:'audio',
audio_clean:'audio', audio_gen:'audio'};
function _defaultCapImage(cap){
const name = CAP_IMAGE_ALIASES[cap] || cap;
return CAP_IMAGE_PUBLISHED.includes(name) ? `${CAP_IMAGE_REPO}-${name}:latest` : '';
}
function toggleRemotesFields(){ function toggleRemotesFields(){
const on = document.getElementById('s-remotes-enabled').checked; const on = document.getElementById('s-remotes-enabled').checked;
document.getElementById('remotes-fields').style.display = on ? 'block' : 'none'; document.getElementById('remotes-fields').style.display = on ? 'block' : 'none';
...@@ -2244,7 +2257,7 @@ function _remoteRowHtml(cap, label, paths){ ...@@ -2244,7 +2257,7 @@ function _remoteRowHtml(cap, label, paths){
<input type="text" id="rc-${cap}-url" class="form-input" style="flex:1;min-width:200px;display:none" placeholder="http://gpu-box:8000"> <input type="text" id="rc-${cap}-url" class="form-input" style="flex:1;min-width:200px;display:none" placeholder="http://gpu-box:8000">
<div id="rc-${cap}-pod" style="display:none;flex:1;min-width:320px"> <div id="rc-${cap}-pod" style="display:none;flex:1;min-width:320px">
<div style="display:flex;gap:.4rem;flex-wrap:wrap"> <div style="display:flex;gap:.4rem;flex-wrap:wrap">
<input type="text" id="rc-${cap}-image" class="form-input" style="flex:2;min-width:200px" placeholder="ghcr.io/you/coderai-${cap}:latest"> <input type="text" id="rc-${cap}-image" class="form-input" style="flex:2;min-width:200px" placeholder="${_defaultCapImage(cap) || 'image required — no published one for this capability'}">
<input type="text" id="rc-${cap}-pool" class="form-input" style="flex:1;min-width:90px" placeholder="pool (share)"> <input type="text" id="rc-${cap}-pool" class="form-input" style="flex:1;min-width:90px" placeholder="pool (share)">
</div> </div>
<div style="display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.35rem"> <div style="display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.35rem">
......
...@@ -129,6 +129,41 @@ DEFAULT_POD_IMAGE = "vllm/vllm-openai:latest" ...@@ -129,6 +129,41 @@ DEFAULT_POD_IMAGE = "vllm/vllm-openai:latest"
# limited architecture list, and it needs the original repo for the tokenizer). # limited architecture list, and it needs the original repo for the tokenizer).
LLAMACPP_POD_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-cuda" LLAMACPP_POD_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-cuda"
#: Published trimmed coderai images, one per capability — a whole coderai
#: carrying only that capability's dependencies (packaging/runpod/). A capability
#: pod with no `image` uses the one for its own capability, so turning a
#: capability remote needs no image name typed in. Override per capability with
#: `image`, or point the whole set elsewhere with CODERAI_CAPABILITY_IMAGE_REPO
#: (e.g. your own fork, or a private registry).
CAPABILITY_IMAGE_REPO = os.environ.get(
"CODERAI_CAPABILITY_IMAGE_REPO", "ghcr.io/nextime/coderai")
CAPABILITY_IMAGE_TAG = os.environ.get("CODERAI_CAPABILITY_IMAGE_TAG", "latest")
#: Capabilities with a published image. One without a row here has no image of
#: its own and needs `image` set explicitly — better than defaulting to a tag
#: that does not exist and failing at pull time, minutes into a pod boot.
PUBLISHED_CAPABILITY_IMAGES = (
"images", "video", "embeddings", "ocr", "tts", "stt", "voice", "audio",
"faceswap",
)
#: Capabilities served by a published image other than their own name.
_CAPABILITY_IMAGE_ALIASES = {
"rerank": "embeddings", # same sentence-transformers stack
"speaker": "stt", # diarization/voiceprints ride the STT deps
"stems": "audio",
"audio_clean": "audio",
"audio_gen": "audio",
}
def default_capability_image(capability: str) -> str:
"""The published image for a capability, or '' when there is none."""
name = _CAPABILITY_IMAGE_ALIASES.get(capability, capability)
if name not in PUBLISHED_CAPABILITY_IMAGES:
return ""
return f"{CAPABILITY_IMAGE_REPO}-{name}:{CAPABILITY_IMAGE_TAG}"
def _looks_like_gguf(name: str) -> bool: def _looks_like_gguf(name: str) -> bool:
return (name or "").strip().lower().endswith(".gguf") return (name or "").strip().lower().endswith(".gguf")
...@@ -175,7 +210,9 @@ def pod_plan(mcfg: "RunpodModelConfig", served: str, model_key: str = "", ...@@ -175,7 +210,9 @@ def pod_plan(mcfg: "RunpodModelConfig", served: str, model_key: str = "",
if not mcfg.image: if not mcfg.image:
raise RuntimeError( raise RuntimeError(
f"RunPod {engine} pod: set `image` on the runpod block to the image " f"RunPod {engine} pod: set `image` on the runpod block to the image "
"to run (coderai does not publish one to a registry for you).") "to run. Capability pods default to the published "
f"{CAPABILITY_IMAGE_REPO}-<capability> image; this one has none, so "
"name it explicitly.")
image = mcfg.image image = mcfg.image
args = mcfg.docker_args # usually blank: the image's entrypoint serves args = mcfg.docker_args # usually blank: the image's entrypoint serves
else: else:
...@@ -929,6 +966,13 @@ def get_capability_pool(capability: str, block: dict): ...@@ -929,6 +966,13 @@ def get_capability_pool(capability: str, block: dict):
"capability at a URL instead.") "capability at a URL instead.")
b = dict(block or {}) b = dict(block or {})
b.setdefault("engine", "coderai") b.setdefault("engine", "coderai")
# A capability pod with no image uses the published one for that capability,
# so enabling a capability is a single choice rather than an image name the
# user has to know. A shared pool keeps whatever image it was created with.
if not str(b.get("image") or "").strip() and b.get("engine") == "coderai":
_img = default_capability_image(capability)
if _img:
b["image"] = _img
shared = str(b.get("pool") or "").strip().lower() shared = str(b.get("pool") or "").strip().lower()
mcfg = parse_model_runpod(b) mcfg = parse_model_runpod(b)
key = f"capability:{shared or capability}" key = f"capability:{shared or capability}"
......
...@@ -135,6 +135,16 @@ endpoint and streams the answer back: ...@@ -135,6 +135,16 @@ endpoint and streams the answer back:
} }
``` ```
A pod row with no `image` uses the published one for its capability —
`ghcr.io/nextime/coderai-<capability>:latest`, a trimmed coderai carrying only
that capability's dependencies — so turning a capability remote is one choice,
not an image name you have to know. `rerank` rides the `embeddings` image,
`speaker` the `stt` one, and `stems`/`audio_clean`/`audio_gen` the `audio` one.
`loras`, `characters`, `environments` and `pipelines` have no published image and
need one named explicitly. Point the whole set at your own build with
`CODERAI_CAPABILITY_IMAGE_REPO` / `CODERAI_CAPABILITY_IMAGE_TAG`, or override a
single row's `image`.
Capability keys: `images`, `video`, `embeddings`, `rerank`, `ocr`, `tts`, `stt`, Capability keys: `images`, `video`, `embeddings`, `rerank`, `ocr`, `tts`, `stt`,
`voice`, `speaker`, `audio_gen`, `stems`, `audio_clean`, `spatial`, `faceswap`, `voice`, `speaker`, `audio_gen`, `stems`, `audio_clean`, `spatial`, `faceswap`,
`loras`, `characters`, `environments`, `pipelines`. Env equivalents: `loras`, `characters`, `environments`, `pipelines`. Env equivalents:
......
...@@ -575,3 +575,53 @@ def test_unreachable_remote_is_a_clean_502(gateway_app): ...@@ -575,3 +575,53 @@ def test_unreachable_remote_is_a_clean_502(gateway_app):
r = client.post("/v1/images/generations", json={"model": "sdxl"}) r = client.post("/v1/images/generations", json={"model": "sdxl"})
assert r.status_code == 502 assert r.status_code == 502
assert "unreachable" in r.json()["error"]["message"] assert "unreachable" in r.json()["error"]["message"]
def test_capability_pods_default_to_the_published_images():
"""Turning a capability remote should not require knowing an image name."""
from codai.api.runpod_worker import default_capability_image
assert default_capability_image("images") == "ghcr.io/nextime/coderai-images:latest"
assert default_capability_image("video") == "ghcr.io/nextime/coderai-video:latest"
# Capabilities that ride another profile's dependency set.
assert default_capability_image("rerank").endswith("coderai-embeddings:latest")
assert default_capability_image("speaker").endswith("coderai-stt:latest")
assert default_capability_image("stems").endswith("coderai-audio:latest")
# No published image: say so rather than default to a tag that 404s minutes
# into a pod boot.
assert default_capability_image("pipelines") == ""
assert default_capability_image("loras") == ""
def test_the_default_image_repo_is_overridable(monkeypatch):
"""A fork or a private registry must be able to take over the whole set."""
import importlib
monkeypatch.setenv("CODERAI_CAPABILITY_IMAGE_REPO", "registry.example.com/me/cai")
monkeypatch.setenv("CODERAI_CAPABILITY_IMAGE_TAG", "0.2.2")
import codai.api.runpod_worker as rw
importlib.reload(rw)
try:
assert rw.default_capability_image("tts") == "registry.example.com/me/cai-tts:0.2.2"
finally:
monkeypatch.undo()
importlib.reload(rw)
def test_a_capability_pool_picks_up_the_default_image(monkeypatch):
import codai.api.runpod_worker as rw
class _Acct:
enabled = True
monkeypatch.setattr("codai.models.manager.get_active_runpod_config", lambda: _Acct())
monkeypatch.setattr(rw, "_pools", {})
monkeypatch.setattr(rw, "_ensure_scaler", lambda: None)
pool = rw.get_capability_pool("video", {}) # nothing configured at all
assert pool.mcfg.image == "ghcr.io/nextime/coderai-video:latest"
assert pool.mcfg.engine == "coderai"
# An explicit image still wins.
monkeypatch.setattr(rw, "_pools", {})
own = rw.get_capability_pool("video", {"image": "me/mine:1"})
assert own.mcfg.image == "me/mine:1"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment