refactor(image_gen,browser): compact module/function docstrings and comments across providers (WHYs kept)

This commit is contained in:
Teknium
2026-09-03 00:06:34 -07:00
parent f564cba625
commit d1a0cdae2c
11 changed files with 95 additions and 156 deletions

View File

@@ -1,11 +1,7 @@
"""Shared base for the bundled cloud-browser provider plugins.
Every vendor (Browserbase, Browser Use, Firecrawl) speaks the same REST shape:
POST to create a session, one request to release it. :class:`CloudBrowserProvider`
owns that lifecycle; subclasses supply class attributes and small hooks. Log
messages carry the vendor label and go to the subclass module's logger so the
emitted text matches the pre-refactor per-vendor modules.
"""
"""Shared base for the bundled cloud-browser plugins. Every vendor speaks the same REST shape
(POST to create a session, one request to release it); :class:`CloudBrowserProvider` owns that
lifecycle, subclasses supply attributes + hooks. Logs go to the subclass module's logger with the
vendor label so emitted text matches the pre-refactor per-vendor modules."""
from __future__ import annotations
@@ -21,26 +17,19 @@ _CLOSE_OK = {200, 201, 204}
class CloudBrowserProvider(BrowserProvider):
"""REST cloud-browser provider driven by class attributes + hooks.
Subclasses set ``provider_id`` / ``label``, ``release_method`` / ``release_path``
(``{session_id}`` placeholder, appended to ``config["base_url"]``), implement
``_get_config_or_none()`` and ``_headers(config)``, and write ``create_session``
on top of :meth:`_post_create` / :meth:`_check_created` / :meth:`_session_name`.
"""
"""Subclasses set ``provider_id``/``label``, ``release_method``/``release_path`` (``{session_id}``
placeholder appended to ``config["base_url"]``), implement ``_get_config_or_none()`` and
``_headers(config)``, and build ``create_session`` on ``_post_create``/``_check_created``."""
provider_id: str
label: str
release_method: str
release_path: str
missing_credentials_error: str = ""
# Setup-picker metadata; ``setup_tag=None`` hides the provider from the picker.
setup_tag: Optional[str] = None
setup_tag: Optional[str] = None # ``None`` hides the provider from the setup picker
setup_env_vars: List[Dict[str, str]] = []
# Rendered noun in "Failed to create <label> session"; Firecrawl says "browser session".
create_label_suffix: str = ""
# Browserbase's close warning historically omits the vendor name.
close_fail_fmt: Optional[str] = None
create_label_suffix: str = "" # "Failed to create <label><suffix> session"; Firecrawl: " browser"
close_fail_fmt: Optional[str] = None # Browserbase's close warning historically omits the vendor
@property
def name(self) -> str:
@@ -90,8 +79,8 @@ class CloudBrowserProvider(BrowserProvider):
def _post_create(
self, url: str, headers: Dict[str, str], payload: Dict[str, object], *, wrap_errors: bool = True
) -> requests.Response:
"""POST the create request; network failures become RuntimeError unless
the caller (managed gateway) needs the raw exception to retry."""
"""POST the create request; network failures → RuntimeError unless the managed gateway
caller needs the raw exception to retry."""
try:
return requests.post(url, headers=headers, json=payload, timeout=30)
except requests.RequestException as exc:

View File

@@ -1,10 +1,7 @@
"""Browser Use cloud browser provider.
The only browser backend with dual auth: a direct ``BROWSER_USE_API_KEY``
(https://browser-use.com) or the managed Nous tool gateway (bills to a Nous
subscription). Direct key first, managed second, unless ``tool_gateway.browser:
gateway`` flips it. Config: ``browser.cloud_provider: "browser-use"``.
"""
"""Browser Use cloud browser provider — the only backend with dual auth: direct
``BROWSER_USE_API_KEY`` (https://browser-use.com) or the managed Nous tool gateway (bills to a
Nous subscription). Direct first, managed second, unless ``tool_gateway.browser: gateway`` flips
it. Config: ``browser.cloud_provider: "browser-use"``."""
from __future__ import annotations
@@ -20,9 +17,8 @@ from plugins.browser._common import CloudBrowserProvider
logger = logging.getLogger(__name__)
# Idempotency keys for managed-mode session creation: the gateway returns 409
# "already in progress" on retried POSTs, so the original key is forwarded.
# Cleared on success or terminal failure.
# Managed-mode create idempotency keys: the gateway answers retried POSTs with 409 "already in
# progress", so the original key is forwarded; cleared on success or terminal failure.
_pending_create_keys: Dict[str, str] = {}
_pending_create_keys_lock = threading.Lock()
@@ -47,9 +43,8 @@ def _clear_pending_create_key(task_id: str) -> None:
def _should_preserve_pending_create_key(response: requests.Response) -> bool:
"""Keep the idempotency key when the failure is retryable: any 5xx, or a 409
whose error message says the original request is still in flight. Any other
4xx (auth, bad request) will not succeed on retry, so the key is dropped."""
"""Keep the key when retryable: any 5xx, or a 409 saying the original request is still in
flight. Other 4xx (auth, bad request) won't succeed on retry, so the key is dropped."""
if response.status_code >= 500:
return True
if response.status_code != 409:
@@ -69,17 +64,15 @@ class BrowserUseBrowserProvider(CloudBrowserProvider):
label = "Browser Use"
release_method = "patch"
release_path = "/browsers/{session_id}"
# Hidden from the picker: the "Browser Use" row activates the CLI backend
# (tools/browser_use_cli.py). This provider stays registered for the Nous
# gateway path and un-migrated legacy cloud_provider configs.
# Hidden from the picker (its "Browser Use" row activates tools/browser_use_cli.py); stays
# registered for the Nous gateway path and legacy cloud_provider configs.
setup_tag = None
def is_available(self) -> bool:
return self._get_config_or_none(refresh_token=False) is not None
def _get_config_or_none(self, *, refresh_token: bool = True) -> Optional[Dict[str, Any]]:
# Lazy: managed_tool_gateway pulls in the Nous auth stack, which
# direct-API-key users never need.
# Lazy: managed_tool_gateway pulls in the Nous auth stack direct-key users never need.
from tools.managed_tool_gateway import peek_nous_access_token, resolve_managed_tool_gateway
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, read_selection
@@ -99,9 +92,8 @@ class BrowserUseBrowserProvider(CloudBrowserProvider):
selected = read_selection("browser")
direct = {"api_key": api_key, "base_url": _BASE_URL, "managed_mode": False}
# Strict selection: "nous" (or legacy use_gateway: true) → managed ONLY;
# any other stored selection → direct key ONLY (no silent managed
# fallback); never-configured → direct key when present, else managed.
# Strict: "nous" (or legacy use_gateway: true) → managed ONLY; any other stored selection →
# direct ONLY (no silent managed fallback); never-configured → direct if present, else managed.
if selected == NOUS_MANAGED_PROVIDER:
return _managed_config()
if selected is not None:
@@ -141,13 +133,11 @@ class BrowserUseBrowserProvider(CloudBrowserProvider):
headers = self._headers(config)
if managed_mode:
headers["X-Idempotency-Key"] = _get_or_create_pending_create_key(task_id)
# Keep gateway-backed sessions short so billing authorization does not
# default to a long Browser-Use timeout for a task-scoped browser.
# Short gateway sessions: billing authorization must not default to a long Browser-Use timeout.
payload = (
{"timeout": _DEFAULT_MANAGED_TIMEOUT_MINUTES, "proxyCountryCode": _DEFAULT_MANAGED_PROXY_COUNTRY_CODE}
if managed_mode else {})
# Managed mode propagates network errors raw so callers can retry with
# the preserved idempotency key; direct mode wraps them.
# Managed mode propagates network errors raw (retry with the preserved key); direct wraps them.
response = self._post_create(
f"{config['base_url']}/browsers", headers, payload, wrap_errors=not managed_mode)
if not response.ok and managed_mode and not _should_preserve_pending_create_key(response):
@@ -163,8 +153,7 @@ class BrowserUseBrowserProvider(CloudBrowserProvider):
"session_name": session_name,
"bb_session_id": session_data["id"],
"cdp_url": session_data.get("cdpUrl") or session_data.get("connectUrl") or "",
# Sessions have a fixed server-side lifetime; preserve the API's
# authority so the dispatcher retires an expired CDP endpoint.
# Fixed server-side lifetime: keep the API's authority so an expired CDP endpoint is retired.
"expires_at": session_data.get("timeoutAt"),
"features": {"browser_use": True},
"external_call_id": response.headers.get("x-external-call-id") if managed_mode else None,

View File

@@ -1,11 +1,8 @@
"""Browserbase cloud browser provider.
Direct ``BROWSERBASE_API_KEY`` + ``BROWSERBASE_PROJECT_ID`` only (the Nous
subscription routes through Browser Use). Config: ``browser.cloud_provider:
"""Browserbase cloud browser provider: direct ``BROWSERBASE_API_KEY`` + ``BROWSERBASE_PROJECT_ID``
only (the Nous subscription routes through Browser Use). Config ``browser.cloud_provider:
"browserbase"``; knobs ``BROWSERBASE_BASE_URL``, ``BROWSERBASE_PROXIES`` (true),
``BROWSERBASE_ADVANCED_STEALTH`` (false), ``BROWSERBASE_KEEP_ALIVE`` (true),
``BROWSERBASE_SESSION_TIMEOUT`` (seconds, max 21600).
"""
``BROWSERBASE_SESSION_TIMEOUT`` (seconds, max 21600)."""
from __future__ import annotations
@@ -22,7 +19,8 @@ logger = logging.getLogger(__name__)
_PAID_FEATURE_FALLBACKS = (
("keepAlive", "keepAlive may require paid plan (402), retrying without it. "
"Sessions may timeout during long operations."),
("proxies", "Proxies unavailable (402), retrying without proxies. Bot detection may be less effective."),
("proxies", "Proxies unavailable (402), retrying without proxies. "
"Bot detection may be less effective."),
)

View File

@@ -1,11 +1,7 @@
"""Firecrawl cloud browser provider (``/v2/browser`` only — the firecrawl WEB
plugin under ``plugins/web/firecrawl/`` shares ``FIRECRAWL_API_KEY``).
Config: ``browser.cloud_provider: "firecrawl"`` (explicit selection only — not
in the legacy auto-detect walk). Env: ``FIRECRAWL_API_KEY``,
``FIRECRAWL_API_URL`` (default https://api.firecrawl.dev), ``FIRECRAWL_BROWSER_TTL``
(default 300 seconds).
"""
"""Firecrawl cloud browser (``/v2/browser`` only; the web plugin under ``plugins/web/firecrawl/``
shares ``FIRECRAWL_API_KEY``). Config ``browser.cloud_provider: "firecrawl"`` (explicit only — not
in the legacy auto-detect walk). Env: ``FIRECRAWL_API_KEY``, ``FIRECRAWL_API_URL`` (default
https://api.firecrawl.dev), ``FIRECRAWL_BROWSER_TTL`` (default 300 s)."""
from __future__ import annotations
@@ -41,8 +37,8 @@ class FirecrawlBrowserProvider(CloudBrowserProvider):
return {"base_url": self._api_url()} if get_secret("FIRECRAWL_API_KEY") else None
def _get_config(self) -> Dict[str, Any]:
# Never raises: a missing key surfaces from _headers() inside the request
# try-block, so close_session logs it as an exception (legacy behaviour).
# Never raises: a missing key surfaces from _headers() inside the request try-block, so
# close_session logs it as an exception (legacy behaviour).
return {"base_url": self._api_url()}
def _headers(self, config: Optional[Dict[str, Any]] = None) -> Dict[str, str]:

View File

@@ -189,7 +189,8 @@ def import_openai(provider: str, aspect: str) -> Tuple[Any, Optional[Dict[str, A
def materialize_image(
b64: Optional[str], url: Optional[str], *, prefix: str, label: str, provider: str, model: str,
prompt: str, aspect: str, log: logging.Logger = logger, on_url_fail: Optional[Callable[[Exception], None]] = None,
prompt: str, aspect: str, log: logging.Logger = logger,
on_url_fail: Optional[Callable[[Exception], None]] = None,
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""``(image_ref, None)`` or ``(None, error)`` for a ``(b64_json, url)`` pair. Base64 is always
cached (write failure → ``io_error``); a URL is cached best-effort, falling back to the bare URL."""

View File

@@ -1,15 +1,8 @@
"""DeepInfra image generation backend.
Exposes DeepInfra's image-gen catalog (FLUX, Qwen-Image-Edit, …) through the
OpenAI-compatible ``/v1/openai/images/generations`` endpoint. Model discovery
is fully dynamic: ``list_models()`` filters the tagged catalog via
:func:`hermes_cli.models._fetch_deepinfra_models_by_tag` (``image-gen``), so
no model ids are hardcoded here.
Model selection: ``DEEPINFRA_IMAGE_MODEL`` env → ``image_gen.deepinfra.model``
→ first model from the live catalog; when all are absent ``generate()`` errors
rather than guessing.
"""
"""DeepInfra image generation (FLUX, Qwen-Image-Edit, …) via the OpenAI-compatible
``/v1/openai/images/generations`` endpoint. The catalog is fully dynamic (``image-gen``-tagged
models from :func:`hermes_cli.models._fetch_deepinfra_models_by_tag`; no ids hardcoded).
Selection: ``DEEPINFRA_IMAGE_MODEL`` → ``image_gen.deepinfra.model`` → first live model;
when all are absent ``generate()`` errors rather than guessing."""
from __future__ import annotations
@@ -57,8 +50,7 @@ def _format_catalog_row(item: Dict[str, Any]) -> Dict[str, Any]:
def _resolve_model(catalog: List[Dict[str, Any]], cfg: Dict[str, Any]) -> Optional[str]:
"""Model id: env > config > first live result, else None. Takes the loaded
``image_gen.deepinfra`` config so ``generate()`` reads config once."""
"""env > config > first live result, else None (``cfg`` = loaded ``image_gen.deepinfra``)."""
env_override = os.environ.get("DEEPINFRA_IMAGE_MODEL", "").strip()
if env_override:
return env_override
@@ -123,7 +115,7 @@ class DeepInfraImageGenProvider(StaticImageGenProvider):
size = size_for(aspect)
from hermes_cli.models import deepinfra_base_url
# OpenAI-compatible endpoint — the openai SDK supplies retry, timeout and error mapping.
# The openai SDK supplies retry, timeout and error mapping.
openai, err = import_openai("deepinfra", aspect)
if err:
return err
@@ -143,14 +135,15 @@ class DeepInfraImageGenProvider(StaticImageGenProvider):
if not data:
return fail("DeepInfra returned no image data", "empty_response")
first = data[0]
# Drop the ``vendor/`` prefix and any colons so the saved filename
# stays a single path component on every OS. Delivery URLs are often
# short-lived; materialise locally, best-effort.
# Prefix drops ``vendor/`` and colons (single path component on every OS); delivery URLs
# are short-lived, so materialise locally best-effort.
image_ref, err = materialize_image(
getattr(first, "b64_json", None), getattr(first, "url", None),
prefix=f"deepinfra_{model_id.split('/', 1)[-1].replace(':', '_')}", label="DeepInfra",
provider="deepinfra", model=model_id, prompt=prompt, aspect=aspect,
on_url_fail=lambda exc: logger.debug("DeepInfra: caching delivery URL failed (%s); returning URL", exc),
on_url_fail=lambda exc: logger.debug(
"DeepInfra: caching delivery URL failed (%s); returning URL", exc,
),
)
if err:
return err

View File

@@ -1,10 +1,6 @@
"""FAL.ai image generation backend — registration adapter.
Catalog, payload construction, submission, managed-Nous-gateway selection and
Clarity Upscaler chaining live in :mod:`tools.image_generation_tool`; this
plugin reaches into it at call time (``import tools.image_generation_tool as
_it``) so tests keep patching ``image_tool.*`` and there is one FAL code path.
"""
"""FAL.ai registration adapter. Catalog, payload, submission, managed-gateway selection and
Clarity Upscaler chaining live in :mod:`tools.image_generation_tool`; this plugin imports it at
call time so tests keep patching ``image_tool.*`` and there is one FAL code path."""
from __future__ import annotations
@@ -49,8 +45,7 @@ class FalImageGenProvider(StaticImageGenProvider):
return _it.DEFAULT_MODEL
def capabilities(self) -> Dict[str, Any]:
# Image-to-image depends on the selected FAL model (``edit_endpoint``);
# Clarity Upscaler chains on request for any model.
# Image-to-image depends on the selected model (``edit_endpoint``); upscale works for any.
import tools.image_generation_tool as _it
try:
@@ -70,8 +65,7 @@ class FalImageGenProvider(StaticImageGenProvider):
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Forward to :func:`tools.image_generation_tool.image_generate_tool` and
reshape its JSON-string response into the provider-ABC dict."""
"""Forward to ``image_generate_tool`` and reshape its JSON-string response into the ABC dict."""
import tools.image_generation_tool as _it
aspect = resolve_aspect_ratio(aspect_ratio)
@@ -101,8 +95,7 @@ class FalImageGenProvider(StaticImageGenProvider):
"success": False, "image": None, "error": "FAL pipeline returned a non-dict response",
"error_type": "provider_contract",
}
# Stamp the uniform provider shape; the legacy pipeline resolves the
# model internally, so query it after the fact.
# Stamp the uniform shape; the legacy pipeline resolves the model internally.
response.setdefault("provider", "fal")
response.setdefault("prompt", prompt)
response.setdefault("aspect_ratio", aspect)

View File

@@ -1,11 +1,7 @@
"""Meta Model API image generation backend (``muse-image``).
The Meta Model API (https://api.meta.ai/v1) is OpenAI-compatible, so the OpenAI
SDK is pointed at Meta's base URL with ``META_MODEL_API_KEY``. Output is base64
WebP → ``$HERMES_HOME/cache/images/``. Selection: ``model`` kwarg →
``META_IMAGE_MODEL`` → ``image_gen.meta-ai.model`` → ``image_gen.model`` →
:data:`DEFAULT_MODEL`.
"""
"""Meta Model API (``muse-image``): OpenAI-compatible (https://api.meta.ai/v1), so the OpenAI SDK
is pointed at Meta's base URL with ``META_MODEL_API_KEY``. Output is base64 WebP → image cache.
Selection: ``model`` kwarg → ``META_IMAGE_MODEL`` → ``image_gen.meta-ai.model`` → ``image_gen.model``
→ :data:`DEFAULT_MODEL`."""
from __future__ import annotations
@@ -23,13 +19,11 @@ from plugins.image_gen._common import (
logger = logging.getLogger(__name__)
DEFAULT_BASE_URL = "https://api.meta.ai/v1"
# Auth env vars, in priority order (mirrors the ``meta-ai`` chat provider):
# MODEL_API_KEY is Meta's documented var; the rest are accepted aliases.
# Auth env vars in priority order (mirrors the ``meta-ai`` chat provider); MODEL_API_KEY is Meta's
# documented var, the rest are aliases. ``API_KEY_ENV`` is the one shown in setup/errors.
API_KEY_ENVS = ("MODEL_API_KEY", "META_API_KEY", "META_MODEL_API_KEY")
# Primary key shown in setup prompts / error messages.
API_KEY_ENV = "META_MODEL_API_KEY"
# Optional base-url override (same var the chat provider honors).
BASE_URL_ENV = "META_BASE_URL"
BASE_URL_ENV = "META_BASE_URL" # optional override, same var the chat provider honors
def _resolve_api_key() -> Optional[str]:

View File

@@ -30,7 +30,8 @@ logger = logging.getLogger(__name__)
_MAX_ERROR_BODY_CHARS = 500
_MODELS: Dict[str, Dict[str, Any]] = dict(GPT_IMAGE_2_TIERS)
_CODEX_CHAT_MODEL = "gpt-5.5" # hosts the ``image_generation`` tool call; ``API_MODEL`` does the image work
# Hosts the ``image_generation`` tool call; ``API_MODEL`` does the image work.
_CODEX_CHAT_MODEL = "gpt-5.5"
_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
_CODEX_INSTRUCTIONS = (
"You are an assistant that must fulfill image generation and image editing "

View File

@@ -1,8 +1,6 @@
"""OpenAI image generation backend: ``gpt-image-2`` at three quality tiers
(``gpt-image-2-low/-medium/-high``) exposed as virtual model ids. Output is
base64 → ``$HERMES_HOME/cache/images/``. Selection: ``OPENAI_IMAGE_MODEL`` →
``image_gen.openai.model`` → ``image_gen.model`` → :data:`DEFAULT_MODEL`.
"""
"""OpenAI ``gpt-image-2`` at three quality tiers (virtual ids ``gpt-image-2-low/-medium/-high``);
base64 output → image cache. Selection: ``OPENAI_IMAGE_MODEL`` → ``image_gen.openai.model`` →
``image_gen.model`` → :data:`DEFAULT_MODEL`."""
from __future__ import annotations
@@ -44,8 +42,7 @@ def _load_image_bytes(ref: str) -> Tuple[bytes, str]:
header, _, b64 = ref.partition(",")
ext = (header.split("image/", 1)[1].split(";", 1)[0] if "image/" in header else "") or "png"
return base64.b64decode(b64), f"image.{ext}"
# Local file path — enforce the shared credential-read guard before reading.
from agent.file_safety import raise_if_read_blocked
from agent.file_safety import raise_if_read_blocked # credential-read guard before local bytes
raise_if_read_blocked(ref)
with open(ref, "rb") as fh:
@@ -110,7 +107,9 @@ class OpenAIImageGenProvider(StaticImageGenProvider):
# gpt-image-2 returns b64_json unconditionally and REJECTS
# ``response_format`` as an unknown parameter. Don't send it.
request: Dict[str, Any] = dict(model=API_MODEL, prompt=prompt, size=size, n=1, quality=meta["quality"])
request: Dict[str, Any] = dict(
model=API_MODEL, prompt=prompt, size=size, n=1, quality=meta["quality"],
)
if is_edit:
try:
files = [_named_bytes_io(ref) for ref in sources]

View File

@@ -1,12 +1,6 @@
"""xAI image generation backend.
Exposes xAI's ``grok-imagine-image`` models as an :class:`ImageGenProvider`:
text-to-image (``/v1/images/generations``) and image editing
(``/v1/images/edits``), base64 output saved to cache.
Selection: ``model`` kwarg → ``XAI_IMAGE_MODEL`` → ``image_gen.xai.model`` →
:data:`DEFAULT_MODEL`.
"""
"""xAI ``grok-imagine-image`` backend: text-to-image (``/v1/images/generations``) and editing
(``/v1/images/edits``), base64 output saved to cache. Selection: ``model`` kwarg →
``XAI_IMAGE_MODEL`` → ``image_gen.xai.model`` → :data:`DEFAULT_MODEL`."""
from __future__ import annotations
@@ -46,9 +40,8 @@ DEFAULT_MODEL = "grok-imagine-image"
# xAI documents the quality model as the edit-capable baseline.
_EDIT_FALLBACK_MODEL = "grok-imagine-image-quality"
# Live catalog cache: (models_dict, fetched_monotonic). ``/image-generation-models``
# is the source of truth so new Imagine models appear without a code change;
# ``_MODELS`` is the offline fallback and supplies curated text for known ids.
# Live catalog cache ``(models, fetched_monotonic)``: ``/image-generation-models`` is the source of
# truth (new models need no code change); ``_MODELS`` is the offline fallback + curated text.
_LIVE_CACHE: Optional[Tuple[Dict[str, Dict[str, Any]], float]] = None
_LIVE_CACHE_TTL = 300.0
_LIVE_TIMEOUT = 10.0
@@ -72,8 +65,7 @@ def _base_url(creds: Dict[str, Any]) -> str:
def _fetch_live_models() -> Dict[str, Dict[str, Any]]:
"""``{model_id: {"input_modalities", "aliases"}}`` from ``/image-generation-models``;
raises on any failure (callers fall back to the static table)."""
"""``{model_id: {"input_modalities", "aliases"}}`` from the live endpoint; raises on failure."""
creds = resolve_xai_http_credentials()
api_key = str(creds.get("api_key") or "").strip()
if not api_key:
@@ -109,9 +101,8 @@ def _live_models() -> Dict[str, Dict[str, Any]]:
def _catalog() -> Dict[str, Dict[str, Any]]:
"""Merged catalog: live ids + curated static metadata. Unknown live models get
generic text; curated entries the live list omits are kept; static table
alone when the API is unreachable."""
"""Live ids + curated metadata (unknown live models get generic text; curated entries the live
list omits are kept); the static table alone when the API is unreachable."""
live = _live_models()
if not live:
return dict(_MODELS)
@@ -132,8 +123,7 @@ def _configured_model() -> Optional[str]:
def _resolve_model(caller_model: Optional[str] = None) -> Tuple[str, Dict[str, Any]]:
"""``(model_id, meta)``: caller kwarg → ``XAI_IMAGE_MODEL`` → config → default,
each validated against the merged live+static catalog."""
"""caller kwarg → ``XAI_IMAGE_MODEL`` → config → default, validated against the merged catalog."""
catalog = _catalog()
for candidate in (caller_model, os.environ.get("XAI_IMAGE_MODEL"), _configured_model()):
if candidate and candidate in catalog:
@@ -142,8 +132,7 @@ def _resolve_model(caller_model: Optional[str] = None) -> Tuple[str, Dict[str, A
def _resolve_edit_model(caller_model: Optional[str] = None) -> str:
"""Model for ``/v1/images/edits``: an explicitly selected model that accepts
image input is honored; otherwise the documented quality baseline."""
"""Edit model: an explicit selection that accepts image input, else the documented quality baseline."""
catalog = _catalog()
explicit = caller_model or os.environ.get("XAI_IMAGE_MODEL") or _configured_model()
if explicit and explicit in catalog and "image" in (catalog[explicit].get("input_modalities") or []):
@@ -157,15 +146,13 @@ def _resolve_resolution() -> str:
def _xai_image_field(source: str) -> Dict[str, str]:
"""xAI ``image`` field for an edit request: public HTTPS URL or base64 data
URI; local paths are read and encoded into a ``data:`` URI."""
"""Edit ``image`` field: URL / data URI pass through; local paths are inlined as ``data:`` URIs."""
source = source.strip()
if source.lower().startswith(_REMOTE_PREFIXES):
return {"url": source, "type": "image_url"}
import base64
# Shared credential-read guard before reading local bytes.
from agent.file_safety import raise_if_read_blocked
from agent.file_safety import raise_if_read_blocked # credential-read guard before local bytes
raise_if_read_blocked(source)
with open(os.path.expanduser(source), "rb") as fh: # windows-footgun: ok
@@ -213,20 +200,21 @@ class XAIImageGenProvider(StaticImageGenProvider):
return next(iter(_catalog()), None)
def get_setup_schema(self) -> Dict[str, Any]:
# Auth resolution is delegated to the shared ``xai_grok`` post_setup hook
# so every xAI service shows the same OAuth-or-API-key choice.
# Auth goes through the shared ``xai_grok`` post_setup hook (same OAuth-or-key choice everywhere).
storage_notice = xai_storage_notice_text("image_gen")
tag = "grok-imagine-image - text-to-image & image editing; uses xAI Grok OAuth or XAI_API_KEY"
if storage_notice:
tag += f". {storage_notice}"
return {
"name": "xAI Grok Imagine (image)", "badge": "paid", "tag": tag, "env_vars": [], "post_setup": "xai_grok",
"name": "xAI Grok Imagine (image)", "badge": "paid", "tag": tag, "env_vars": [],
"post_setup": "xai_grok",
}
def capabilities(self) -> Dict[str, Any]:
# /v1/images/edits accepts up to 3 total source images.
return {
"modalities": ["text", "image"], "max_reference_images": 2, "max_source_images": _MAX_SOURCE_IMAGES,
"modalities": ["text", "image"], "max_reference_images": 2,
"max_source_images": _MAX_SOURCE_IMAGES,
}
def generate(
@@ -234,9 +222,8 @@ class XAIImageGenProvider(StaticImageGenProvider):
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Text-to-image, or image editing via ``/v1/images/edits`` (JSON body —
xAI does not support the SDK's multipart ``images.edit()``) when source
images are supplied."""
"""Text-to-image, or editing via ``/v1/images/edits`` (JSON body — xAI does not support the
SDK's multipart ``images.edit()``) when source images are supplied."""
creds = resolve_xai_http_credentials()
api_key = str(creds.get("api_key") or "").strip()
provider_name = str(creds.get("provider") or "xai").strip() or "xai"
@@ -308,8 +295,7 @@ class XAIImageGenProvider(StaticImageGenProvider):
if public_url:
image_ref = public_url
else:
# grok-imagine-image URLs (``imgen.x.ai/xai-tmp-*``) 404 within minutes;
# materialise locally so downstream consumers get a stable path.
# ``imgen.x.ai/xai-tmp-*`` URLs 404 within minutes; materialise locally for a stable path.
image_ref, err = materialize_image(
first.get("b64_json"), first.get("url"), prefix=f"xai_{model_id}", label="xAI", provider="xai",
model=model_id, prompt=prompt, aspect=aspect, log=logger)