Spaces:
Sleeping
Sleeping
| """ | |
| 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")) | |