Merge branch 'simp/r3-26-G' into simp/integration3

This commit is contained in:
Teknium
2026-09-02 22:40:53 -07:00
9 changed files with 113 additions and 208 deletions
+7 -30
View File
@@ -13,11 +13,7 @@ from dataclasses import dataclass
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
from agent.image_gen_provider import (
ImageGenProvider,
error_response,
normalize_reference_images,
save_b64_image,
save_url_image,
ImageGenProvider, error_response, normalize_reference_images, save_b64_image, save_url_image
)
logger = logging.getLogger(__name__)
@@ -79,13 +75,8 @@ def load_image_gen_config(sub: Optional[str] = None) -> Dict[str, Any]:
def resolve_static_model(
models: Dict[str, Dict[str, Any]],
default: str,
*,
env_var: str,
config_key: str,
explicit: Optional[str] = None,
include_top_level: bool = True,
models: Dict[str, Dict[str, Any]], default: str, *, env_var: str, config_key: str,
explicit: Optional[str] = None, include_top_level: bool = True,
config: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict[str, Any]]:
"""Pick ``(model_id, meta)`` from a fixed catalog.
@@ -123,8 +114,7 @@ def collect_source_images(
def catalog_rows(
models: Dict[str, Dict[str, Any]],
fields: Iterable[str] = ("display", "speed", "strengths", "price"),
*,
fields: Iterable[str] = ("display", "speed", "strengths", "price"), *,
price: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Picker rows: ``id`` plus ``fields`` from each meta (missing ``display`` →
@@ -213,16 +203,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,
b64: Optional[str], url: Optional[str], *, prefix: str, label: str, provider: str, model: str,
prompt: str, aspect: str, log: logging.Logger = logger,
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""``(image_ref, None)`` or ``(None, error_dict)`` for a ``(b64_json, url)`` pair.
@@ -274,12 +256,7 @@ class HttpFailure:
def post_json(
url: str,
*,
headers: Dict[str, str],
payload: Dict[str, Any],
timeout: Any,
label: str,
url: str, *, headers: Dict[str, str], payload: Dict[str, Any], timeout: Any, label: str,
error_message: Callable[[Any, Exception], str] = requests_error_message,
catch_request_exception: bool = False,
) -> Tuple[Optional[Any], Optional[HttpFailure]]:
+3 -11
View File
@@ -19,19 +19,11 @@ from typing import Any, Dict, List, Optional
from agent.secret_scope import get_secret
from agent.image_gen_provider import (
DEFAULT_ASPECT_RATIO,
resolve_aspect_ratio,
save_b64_image,
save_url_image,
success_response,
DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, save_b64_image, save_url_image, success_response
)
from plugins.image_gen._common import (
StaticImageGenProvider,
error_factory,
import_openai,
load_image_gen_config,
prompt_required_error,
size_for,
StaticImageGenProvider, error_factory, import_openai, load_image_gen_config,
prompt_required_error, size_for,
)
logger = logging.getLogger(__name__)
+2 -6
View File
@@ -68,12 +68,8 @@ class FalImageGenProvider(StaticImageGenProvider):
return {"modalities": ["text"], "max_reference_images": 0, "supports_upscale": True}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
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
+73 -65
View File
@@ -22,13 +22,8 @@ import requests
from agent.secret_scope import get_secret
from agent.image_gen_provider import DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, save_url_image, success_response
from plugins.image_gen._common import (
StaticImageGenProvider,
collect_source_images,
error_factory,
load_image_gen_config,
post_json,
prompt_required_error,
resolve_static_model,
ErrorFn, StaticImageGenProvider, collect_source_images, error_factory, load_image_gen_config,
post_json, prompt_required_error, resolve_static_model,
)
logger = logging.getLogger(__name__)
@@ -202,11 +197,7 @@ def _is_terminal(job: Any) -> bool:
def _poll_krea_job(
base_url: str,
auth_token: str,
job_id: str,
*,
timeout_seconds: float = _POLL_TIMEOUT_SECONDS,
base_url: str, auth_token: str, job_id: str, *, timeout_seconds: float = _POLL_TIMEOUT_SECONDS,
on_error: Optional[Any] = None,
) -> Any:
"""Poll ``/jobs/{job_id}`` until terminal.
@@ -375,6 +366,68 @@ def _build_payload(
return payload
def _submit_job(
base_url: str, auth_token: str, model_path: str, payload: Dict[str, Any],
managed: bool, model_id: str, fail: ErrorFn,
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""POST the generation request; ``(job_id, None)`` or ``(None, error)``."""
submit_body, failure = post_json(
f"{base_url}/generate/image/krea/krea-2/{model_path}",
headers=_headers(auth_token, managed=managed, json_body=True),
payload=payload, timeout=30, label="Krea", error_message=_submit_error_message,
)
if failure is not None:
if failure.kind == "http":
status, err_msg = failure.status, failure.message
logger.error("Krea submit failed (%d): %s", status, err_msg)
# Managed 4xx: the model may not be enabled/priced on the Nous
# Portal, or the gateway's shared key hit its concurrency cap (429).
if managed and 400 <= status < 500:
hint = (
"Krea's shared-key concurrency cap was hit — retry shortly."
if status == 429
else (
f"Model '{model_id}' may not be enabled/priced on the "
"Nous Portal's Krea gateway. Set KREA_API_KEY to use "
"Krea directly, or pick a different model via "
"`hermes tools` → Image Generation."
)
)
return None, fail(
f"Nous Subscription Krea gateway rejected '{model_id}' "
f"(HTTP {status}): {err_msg}. {hint}",
"api_error",
)
return None, fail(failure.error, "api_error")
if failure.kind == "timeout":
return None, fail("Krea submit timed out (30s)", "timeout")
if failure.kind == "invalid_json":
return None, fail(f"Krea returned invalid JSON on submit: {failure.message}", "invalid_response")
return None, fail(failure.error, failure.error_type)
job_id = submit_body.get("job_id")
if not isinstance(job_id, str) or not job_id:
return None, fail("Krea submit response missing job_id", "invalid_response")
return job_id, None
def _terminal_result_url(
job: Dict[str, Any], job_id: str, fail: ErrorFn
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""Result URL of a terminal job; ``(url, None)`` or ``(None, error)``."""
result = job.get("result")
if job.get("status") == "failed":
err = result.get("error") if isinstance(result, dict) else None
return None, fail(f"Krea job {job_id} failed: {err or 'unknown error'}", "api_error")
if job.get("status") == "cancelled":
return None, fail(f"Krea job {job_id} was cancelled", "cancelled")
if not isinstance(result, dict):
return None, fail("Krea job completed but result was missing", "empty_response")
result_image_url = _extract_result_url(job)
if result_image_url is None:
return None, fail("Krea result contained no image URL", "empty_response")
return result_image_url, None
class KreaImageGenProvider(StaticImageGenProvider):
"""Krea ``Krea 2`` foundation image model backend (Medium + Large)."""
@@ -401,12 +454,8 @@ class KreaImageGenProvider(StaticImageGenProvider):
}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
prompt = (prompt or "").strip()
@@ -456,42 +505,9 @@ class KreaImageGenProvider(StaticImageGenProvider):
)
# 1. Submit job.
submit_body, failure = post_json(
f"{base_url}/generate/image/krea/krea-2/{meta['path']}",
headers=_headers(auth_token, managed=managed is not None, json_body=True),
payload=payload, timeout=30, label="Krea", error_message=_submit_error_message,
)
if failure is not None:
if failure.kind == "http":
status, err_msg = failure.status, failure.message
logger.error("Krea submit failed (%d): %s", status, err_msg)
# Managed 4xx: the model may not be enabled/priced on the Nous
# Portal, or the gateway's shared key hit its concurrency cap (429).
if managed is not None and 400 <= status < 500:
hint = (
"Krea's shared-key concurrency cap was hit — retry shortly."
if status == 429
else (
f"Model '{model_id}' may not be enabled/priced on the "
"Nous Portal's Krea gateway. Set KREA_API_KEY to use "
"Krea directly, or pick a different model via "
"`hermes tools` → Image Generation."
)
)
return fail(
f"Nous Subscription Krea gateway rejected '{model_id}' "
f"(HTTP {status}): {err_msg}. {hint}",
"api_error",
)
return fail(failure.error, "api_error")
if failure.kind == "timeout":
return fail("Krea submit timed out (30s)", "timeout")
if failure.kind == "invalid_json":
return fail(f"Krea returned invalid JSON on submit: {failure.message}", "invalid_response")
return fail(failure.error, failure.error_type)
job_id = submit_body.get("job_id")
if not isinstance(job_id, str) or not job_id:
return fail("Krea submit response missing job_id", "invalid_response")
job_id, err = _submit_job(base_url, auth_token, meta["path"], payload, managed is not None, model_id, fail)
if err is not None:
return err
# 2. Poll. Polling is bound to the same principal at the gateway, so the
# managed path polls the gateway's ``/jobs/{id}`` with the Nous token.
@@ -509,17 +525,9 @@ class KreaImageGenProvider(StaticImageGenProvider):
return fail("Krea returned non-dict job body", "invalid_response")
# 3. Terminal — extract result.
result = job.get("result")
if job.get("status") == "failed":
err = result.get("error") if isinstance(result, dict) else None
return fail(f"Krea job {job_id} failed: {err or 'unknown error'}", "api_error")
if job.get("status") == "cancelled":
return fail(f"Krea job {job_id} was cancelled", "cancelled")
if not isinstance(result, dict):
return fail("Krea job completed but result was missing", "empty_response")
result_image_url = _extract_result_url(job)
if result_image_url is None:
return fail("Krea result contained no image URL", "empty_response")
result_image_url, err = _terminal_result_url(job, job_id, fail)
if err is not None:
return err
# Krea Enhance pass. Precedence: explicit kwarg > ``image_gen.krea.upscale``
# config > per-model catalog default. Best-effort: failure falls back to
+5 -18
View File
@@ -15,20 +15,11 @@ from typing import Any, Dict, List, Optional, Tuple
from agent.secret_scope import get_secret
from agent.image_gen_provider import (
DEFAULT_ASPECT_RATIO,
resolve_aspect_ratio,
save_b64_image,
save_url_image,
success_response,
DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, save_b64_image, save_url_image, success_response
)
from plugins.image_gen._common import (
StaticImageGenProvider,
error_factory,
import_openai,
openai_importable,
prompt_required_error,
resolve_static_model,
size_for,
StaticImageGenProvider, error_factory, import_openai, openai_importable, prompt_required_error,
resolve_static_model, size_for,
)
logger = logging.getLogger(__name__)
@@ -90,12 +81,8 @@ class MetaImageGenProvider(StaticImageGenProvider):
return {"modalities": ["text"], "max_reference_images": 0}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
prompt = (prompt or "").strip()
+5 -15
View File
@@ -22,15 +22,9 @@ from typing import Any, Dict, List, Optional, Tuple
from agent.image_gen_provider import DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, save_b64_image, success_response
from plugins.image_gen._common import (
GPT_IMAGE_2_API_MODEL as API_MODEL,
GPT_IMAGE_2_DEFAULT as DEFAULT_MODEL,
GPT_IMAGE_2_TIERS,
StaticImageGenProvider,
collect_source_images,
error_factory,
prompt_required_error,
resolve_static_model,
size_for,
GPT_IMAGE_2_API_MODEL as API_MODEL, GPT_IMAGE_2_DEFAULT as DEFAULT_MODEL, GPT_IMAGE_2_TIERS,
StaticImageGenProvider, collect_source_images, error_factory, prompt_required_error,
resolve_static_model, size_for,
)
logger = logging.getLogger(__name__)
@@ -364,12 +358,8 @@ class OpenAICodexImageGenProvider(StaticImageGenProvider):
return {"modalities": ["text", "image"], "max_reference_images": _MAX_REFERENCE_IMAGES}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
prompt = (prompt or "").strip()
+5 -18
View File
@@ -13,18 +13,9 @@ from typing import Any, Dict, List, Optional, Tuple
from agent.secret_scope import get_secret
from agent.image_gen_provider import DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, success_response
from plugins.image_gen._common import (
GPT_IMAGE_2_API_MODEL as API_MODEL,
GPT_IMAGE_2_DEFAULT as DEFAULT_MODEL,
GPT_IMAGE_2_TIERS,
StaticImageGenProvider,
collect_source_images,
error_factory,
import_openai,
materialize_image,
openai_importable,
prompt_required_error,
resolve_static_model,
size_for,
GPT_IMAGE_2_API_MODEL as API_MODEL, GPT_IMAGE_2_DEFAULT as DEFAULT_MODEL, GPT_IMAGE_2_TIERS,
StaticImageGenProvider, collect_source_images, error_factory, import_openai, materialize_image,
openai_importable, prompt_required_error, resolve_static_model, size_for,
)
logger = logging.getLogger(__name__)
@@ -84,12 +75,8 @@ class OpenAIImageGenProvider(StaticImageGenProvider):
return {"modalities": ["text", "image"], "max_reference_images": 16}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
prompt = (prompt or "").strip()
+7 -26
View File
@@ -20,19 +20,11 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from agent.image_gen_provider import (
DEFAULT_ASPECT_RATIO,
ImageGenProvider,
error_response,
resolve_aspect_ratio,
save_b64_image,
save_url_image,
success_response,
DEFAULT_ASPECT_RATIO, ImageGenProvider, error_response, resolve_aspect_ratio, save_b64_image,
save_url_image, success_response,
)
from plugins.image_gen._common import (
HttpFailure as HttpFailure,
error_factory,
load_image_gen_config,
post_json,
HttpFailure as HttpFailure, error_factory, load_image_gen_config, post_json
)
logger = logging.getLogger(__name__)
@@ -574,15 +566,8 @@ class OpenRouterCompatImageProvider(ImageGenProvider):
"""
def __init__(
self,
*,
provider_name: str,
display_name: str,
runtime_name: str,
config_key: str,
model_env_var: str,
setup_schema: Dict[str, Any],
supports_image_api: bool = False,
self, *, provider_name: str, display_name: str, runtime_name: str, config_key: str,
model_env_var: str, setup_schema: Dict[str, Any], supports_image_api: bool = False,
) -> None:
self._name = provider_name
self._display = display_name
@@ -861,12 +846,8 @@ class OpenRouterCompatImageProvider(ImageGenProvider):
), None
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
image_url: Optional[str] = None, reference_image_urls: Optional[List[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
fail = error_factory(self._name, aspect_ratio)
+6 -19
View File
@@ -19,21 +19,12 @@ import requests
from agent.image_gen_provider import DEFAULT_ASPECT_RATIO, resolve_aspect_ratio, success_response
from plugins.image_gen._common import (
StaticImageGenProvider,
catalog_rows,
collect_source_images,
error_factory,
load_image_gen_config,
materialize_image,
post_json,
StaticImageGenProvider, catalog_rows, collect_source_images, error_factory,
load_image_gen_config, materialize_image, post_json,
)
from tools.xai_http import (
build_xai_storage_options,
hermes_xai_user_agent,
maybe_mark_xai_storage_notice_seen,
read_xai_imagine_storage_config,
resolve_xai_http_credentials,
xai_storage_notice_text,
build_xai_storage_options, hermes_xai_user_agent, maybe_mark_xai_storage_notice_seen,
read_xai_imagine_storage_config, resolve_xai_http_credentials, xai_storage_notice_text,
)
logger = logging.getLogger(__name__)
@@ -242,12 +233,8 @@ class XAIImageGenProvider(StaticImageGenProvider):
}
def generate(
self,
prompt: str,
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
self, prompt: str, aspect_ratio: str = DEFAULT_ASPECT_RATIO, *,
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 —