Matrix-Fossil-Agent / config /settings.py
hmgill's picture
Upload 29 files
85f0898 verified
Raw
History Blame Contribute Delete
11 kB
"""
Configuration β€” every model ID, key, path and threshold in one place.
Framework: the **OpenAI Agents SDK** (`openai-agents`).
# The routing problem that no longer exists
An earlier build of this project ran on Google ADK, whose LiteLLM bridge calls
OpenAI's `/v1/chat/completions`. That endpoint refuses function tools alongside a
reasoning model:
BadRequestError: Function tools with reasoning_effort are not supported for
gpt-5.6-luna in /v1/chat/completions. To use function tools, use /v1/responses
or set reasoning_effort to 'none'.
This pipeline is nothing but function tools driven by a reasoning model, so it hit
that on the first call, and the fix was an ugly model-string rewrite
(`openai/responses/gpt-5.6-luna`) plus a global LiteLLM flag.
The Agents SDK talks to **/v1/responses natively** β€” a bare OpenAI model name
resolves to `OpenAIResponsesModel`. Tools and reasoning coexist. All of that
routing machinery is deleted, not ported. If you are reading this while porting
something else off ADK: that was the whole bug.
Non-OpenAI providers still go through LiteLLM (`LitellmModel`), which does use
chat-completions β€” fine, because the tools/reasoning split is OpenAI-specific.
"""
from __future__ import annotations
import os
from pathlib import Path
try: # optional β€” the pipeline runs from a plain environment too
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
except ImportError: # pragma: no cover
pass
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_ROOT = Path(os.getenv("FOSSIL_OUTPUT_DIR", str(PROJECT_ROOT / "output")))
SKILLS_ROOT = PROJECT_ROOT / "skills"
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
#
# FOSSIL_MODEL_ID is the single knob for the whole pipeline.
#
# "gpt-5.6-luna" -> OpenAI, /v1/responses, native. The default.
# "anthropic/claude-..." -> LiteLLM bridge.
# "openrouter/..." -> LiteLLM bridge.
#
# A provider prefix ("<something>/") is what routes a model through LiteLLM.
# Anything else is handed to the SDK as-is and resolves to OpenAI's Responses API.
def _normalise_model_id(model_id: str) -> str:
"""
Migration guard: "openai/gpt-5.6-luna" -> "gpt-5.6-luna".
Under Google ADK, the "openai/" prefix was REQUIRED β€” it told LiteLLM which
provider to use. Under the Agents SDK the same prefix means the opposite: it
routes the model AWAY from the native OpenAI path and through LiteLLM, i.e.
back to /v1/chat/completions, i.e. straight back into the BadRequestError this
migration exists to eliminate.
An old .env would therefore fail in a way that looks nothing like its cause. So
strip it, and say so.
"""
if model_id.startswith("openai/") and not model_id.startswith("openai/responses/"):
stripped = model_id.split("/", 1)[1]
print(
f"[config] FOSSIL_MODEL_ID={model_id!r} -> {stripped!r}. The 'openai/' "
f"prefix was an ADK/LiteLLM requirement; under the Agents SDK it would "
f"route you back to /v1/chat/completions. Update your .env.",
)
return stripped
if model_id.startswith("openai/responses/"):
stripped = model_id.rsplit("/", 1)[1]
print(f"[config] FOSSIL_MODEL_ID={model_id!r} -> {stripped!r}. The Agents SDK "
f"uses /v1/responses natively; the routing hack is no longer needed.")
return stripped
return model_id
ORCHESTRATOR_MODEL_ID = _normalise_model_id(os.getenv("FOSSIL_MODEL_ID", "gpt-5.6-luna"))
VISION_MODEL_ID = _normalise_model_id(
os.getenv("FOSSIL_VISION_MODEL_ID", ORCHESTRATOR_MODEL_ID))
# low | medium | high. Keypoint selection IS the model looking at a fossil and
# thinking about where the points go, so this is load-bearing β€” not a knob to turn
# down for speed without expecting worse masks.
REASONING_EFFORT = os.getenv("FOSSIL_REASONING_EFFORT", "medium")
def is_litellm_model(model_id: str) -> bool:
"""A provider prefix routes through LiteLLM. Bare names go to OpenAI."""
return "/" in model_id
def build_model(model_id: str):
"""Return something `Agent(model=...)` accepts."""
if not is_litellm_model(model_id):
# The SDK resolves a bare name to OpenAIResponsesModel β€” /v1/responses,
# where function tools and reasoning coexist. Nothing to do.
return model_id
from agents.extensions.models.litellm_model import LitellmModel
return LitellmModel(model=model_id)
def build_model_settings(model_id: str):
"""
ModelSettings for a model.
Reasoning effort is only meaningful on OpenAI reasoning models; sending it to
a LiteLLM provider that has never heard of it is a 400 waiting to happen, so
it is set only on the native path.
"""
from agents import ModelSettings
if is_litellm_model(model_id):
return ModelSettings()
from openai.types.shared import Reasoning
return ModelSettings(reasoning=Reasoning(effort=REASONING_EFFORT))
def describe_routing(model_id: str) -> dict:
"""What will actually be sent. Used by `python main.py --doctor`."""
from importlib.metadata import version
litellm_path = is_litellm_model(model_id)
try:
sdk = version("openai-agents")
except Exception: # noqa: BLE001
sdk = "unknown"
return {
"FOSSIL_MODEL_ID": model_id,
"framework": f"openai-agents {sdk}",
"route": "LiteLLM bridge" if litellm_path else "OpenAI native",
"endpoint": "/v1/chat/completions" if litellm_path else "/v1/responses",
"reasoning_effort": "n/a (LiteLLM)" if litellm_path else REASONING_EFFORT,
"api_key_env": "provider-specific" if litellm_path else "OPENAI_API_KEY",
"api_key_present": (
True if litellm_path else bool(os.getenv("OPENAI_API_KEY"))
),
"verdict": (
"OK β€” LiteLLM bridge. Non-OpenAI providers use chat-completions, which "
"is fine: the tools/reasoning split is an OpenAI-specific constraint."
if litellm_path
else "OK β€” /v1/responses. Function tools and reasoning both supported."
),
}
# ---------------------------------------------------------------------------
# Credentials
# ---------------------------------------------------------------------------
HF_TOKEN = os.getenv("HF_TOKEN")
# Read from the environment (see .env, gitignored) β€” never hard-coded here,
# because this file is tracked. The key currently in use is the one from
# hmgill/organoid, which is committed in a public repo and should be treated as
# already compromised. Rotate before the Meshy call goes live.
MESHY_API_KEY = os.getenv("MESHY_API_KEY")
def _flag(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
# Meshy costs credits per task. Off unless explicitly enabled; otherwise the tool
# returns the request it *would* have made.
#
# This is the process-wide DEFAULT. A UI sets it per run on FossilContext, which
# wins β€” a Space serves many users from one process, and a global would let one
# person's switch spend another person's credits.
MESHY_ENABLED = _flag("FOSSIL_MESHY_ENABLED", False)
# For a fossil the surface texture usually IS the data β€” the ornament, the
# fracture, the matrix. So it defaults ON. Turn it off for raw geometry
# (morphometrics, 3D printing) or when you will retexture from calibrated photos.
#
# PBR maps are INFERRED by a generative model, not measured off the specimen. They
# make renders look better; they are not data. Off by default, deliberately.
MESHY_TEXTURE = _flag("FOSSIL_MESHY_TEXTURE", True)
MESHY_PBR = _flag("FOSSIL_MESHY_PBR", False)
MESHY_HD_TEXTURE = _flag("FOSSIL_MESHY_HD_TEXTURE", False)
# ---------------------------------------------------------------------------
# Background removal
# ---------------------------------------------------------------------------
#
# "hf" β€” BRIA RMBG-2.0 via the HF Inference API. Needs HF_TOKEN.
# "local" β€” GrabCut seeded by Lab chroma distance. No token, no network, no GPU.
# This is what makes the pipeline testable in CI, and the reason the
# whole thing degrades rather than dies without credentials.
BG_BACKEND = os.getenv("FOSSIL_BG_BACKEND", "hf")
# When the "hf" backend fails, should we fall back to offline GrabCut?
#
# On a laptop: yes β€” a worse matte beats a dead pipeline.
# In a deployment that chose RMBG deliberately: NO. GrabCut produces a worse but
# *plausible* matte, so the run still looks successful and the masks are merely a
# bit off β€” which nobody notices until a mesh has been built on top of them. A
# loud stop is cheaper than a quiet degradation. The Space sets this to 1.
BG_STRICT = os.getenv("FOSSIL_BG_STRICT", "").lower() in {"1", "true", "yes", "on"}
SAM3_CHECKPOINT = os.getenv("FOSSIL_SAM3_CHECKPOINT", "facebook/sam3")
# ---------------------------------------------------------------------------
# Keypoint selection
# ---------------------------------------------------------------------------
#
# The AGENT selects the keypoints (fossil_agents/vision_analyst). Nothing here
# picks a point. These only shape the aids it is given:
#
# GRID_STEP_TARGET β€” roughly how many gridlines across the canvas the agent
# reads its coordinates from. More lines = finer ruler,
# but a cluttered image the model reads worse. ~10 is the
# sweet spot.
# MAX_KEYPOINT_ROUNDS β€” how many times the agent may call check_keypoints_tool
# and revise before it must commit.
GRID_STEP_TARGET = int(os.getenv("FOSSIL_GRID_STEPS", "10"))
MAX_KEYPOINT_ROUNDS = int(os.getenv("FOSSIL_MAX_KEYPOINT_ROUNDS", "3"))
# ---------------------------------------------------------------------------
# Geometric keypoint proposal β€” OFFLINE SMOKE-TEST PATH ONLY
# ---------------------------------------------------------------------------
#
# helpers/geometry.py can propose points without a model. It is NOT in the agent
# pipeline: it exists so `--dry-run` and the test suite can exercise steps 1-3 with
# no API key. Do not reintroduce it into the agent path β€” point selection is the
# agent's job.
N_POSITIVE_POINTS = int(os.getenv("FOSSIL_N_POSITIVE", "4"))
N_NEGATIVE_POINTS = int(os.getenv("FOSSIL_N_NEGATIVE", "6"))
CORE_DT_FRACTION = float(os.getenv("FOSSIL_CORE_DT_FRACTION", "0.35"))
# The model's *view* of an image is downscaled to this; tools always work at full
# resolution on the original file.
PREVIEW_MAX_SIDE = int(os.getenv("FOSSIL_PREVIEW_MAX_SIDE", "1024"))