From 1b71937509c00c2a6ee31b0da8d4e0352e2571ba Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Thu, 30 Jul 2026 16:16:13 +0100 Subject: [PATCH 1/2] Advertise the full Chat model catalog, not just the SDK enum /v1/models and `og-veil models` were both derived from the SDK's TEE_LLM enum. That enum ships on the SDK's release cadence and lags the network: at the pinned 1.1.1 it was missing ten models the gateway already serves and the Chat app already offers -- the GPT-5.6 trio, Claude Sonnet 5 / Opus 5 / Fable 5, Gemini 3.6 Flash and 3.5 Flash Lite, and Grok 4.5 / 4.3 -- so an agent probing /v1/models never learned they existed. Add veil/models.py, a curated catalog mirroring the Chat app's lib/constants/models.ts: same models, same display names, same order, with bare wire ids (the gateway is sent `claude-opus-5`, not `anthropic/claude-opus-5`, matching both the SDK and the Chat app). The enum is folded in on top, so anything a later SDK adds still shows up and nothing that used to be listed can regress -- 42 ids advertised before, 59 now, none dropped. - /v1/models reports the provider as `owned_by` instead of a flat "opengradient", and keeps superseded ids listed so an agent already configured for one keeps working. - `og-veil models` groups by provider with descriptions; `--all` adds superseded and SDK-only ids. - `og-veil test` defaulted to `--model gpt-4.1`, which is not a model the network serves (the enum has gpt-4.1-nano / -mini / -2025-04-14). Default to claude-haiku-4-5, the Chat app's own free-tier default. - README gains a model table and drops the superseded claude-sonnet-4-6 from its examples. --- README.md | 31 +++- pyproject.toml | 2 +- tests/test_cli.py | 23 ++- tests/test_models.py | 92 ++++++++++ tests/test_server.py | 16 ++ uv.lock | 2 +- veil/cli.py | 59 +++++-- veil/models.py | 405 +++++++++++++++++++++++++++++++++++++++++++ veil/server.py | 19 +- 9 files changed, 620 insertions(+), 29 deletions(-) create mode 100644 tests/test_models.py create mode 100644 veil/models.py diff --git a/README.md b/README.md index 0e23808..4f5fa87 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ from openai import OpenAI client = OpenAI() # picks up the env vars above r = client.chat.completions.create( - model="claude-sonnet-4-6", + model="claude-sonnet-5", messages=[{"role": "user", "content": "Explain TEE attestation in one line."}], ) print(r.choices[0].message.content) @@ -75,7 +75,7 @@ With `og-veil` running, set a custom endpoint — either via the CLI: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:11434/v1 hermes config set OPENAI_API_KEY og-veil # ignored; your Chat login authenticates -hermes config set model.default claude-sonnet-4-6 +hermes config set model.default claude-sonnet-5 ``` …or by editing `~/.hermes/config.yaml` directly: @@ -91,6 +91,30 @@ model: Now `hermes` runs against attested, end-to-end-encrypted inference with no other changes. Confirm it's flowing through the enclave with `og-veil status`. +## Models + +The same lineup [OpenGradient Chat](https://chat.opengradient.ai) offers, reached +through the same verified TEE path. `og-veil models` prints it (add `--all` for +superseded ones), and `GET /v1/models` serves it to any agent that probes. + +| Provider | Models | +|----------|--------| +| Anthropic | `claude-haiku-4-5`, `claude-sonnet-5`, `claude-opus-5`, `claude-fable-5` | +| OpenAI | `gpt-4.1-nano`, `gpt-5-mini`, `gpt-5`, `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol` | +| Google | `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | +| xAI | `grok-4.5`, `grok-4.3`, `grok-4-fast`, `grok-4` | +| DeepSeek | `deepseek-v4-flash`, `deepseek-v4-pro` | +| ByteDance | `seed-2.0-lite`, `seed-1.8`, `seed-1.6` | +| Nous Research | `hermes-4-405b`, `hermes-4-70b` | +| Z.ai | `glm-5.2` | +| Image generation | `gpt-image-2`, `gemini-3.1-flash-image`, `seedream-5.0-lite`, `seedance-4.5`, `glm-image` | + +Names are **bare** - send `claude-opus-5`, not `anthropic/claude-opus-5`. Earlier +generations (`claude-opus-4-8`, `gpt-5.5`, `grok-4-1-fast`, `o3`, …) stay +routable and stay listed, so an agent already configured for one keeps working. +Image models are called like any other chat completion; the images come back on +the response. + --- ## How it works @@ -147,7 +171,8 @@ the local OpenAI-compatible server. | `og-veil stop` | Stop the background server. | | `og-veil restart` | Stop and start the background server — e.g. after `og-veil update`. | | `og-veil status` | Login + network config + whether the server is running. | -| `og-veil test ["prompt"]` | Send a one-off prompt to the running server and print the verified reply. Automatically targets the port the server was started on; override with `--host`/`--port`. | +| `og-veil test ["prompt"]` | Send a one-off prompt to the running server and print the verified reply. Automatically targets the port the server was started on; override with `--host`/`--port`. Pick a model with `--model`. | +| `og-veil models [--all]` | List the models you can send, grouped by provider. | | `og-veil update` | Update og-veil to the latest version. | | `og-veil login` | Authorize / re-authorize this device. | | `og-veil setup` | Re-run the setup wizard. | diff --git a/pyproject.toml b/pyproject.toml index 048e283..0b10ad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "opengradient-veil" -version = "0.2.9" +version = "0.3.0" description = "OpenGradient Veil — a drop-in, self-verifying private-inference proxy for AI agents. Point your OpenAI SDK at it; it routes prompts through OpenGradient's decentralized network of attestable Nitro TEE gateways and cryptographically verifies every response before a single token reaches your code." readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_cli.py b/tests/test_cli.py index 8f15923..f8f3ce1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -65,14 +65,29 @@ def test_env_prints_env_vars(): assert result.exit_code == 0 -def test_models_lists_known_models(): +def test_models_lists_the_catalog_grouped_by_provider(): result = CliRunner().invoke(cli.main, ["models"]) assert result.exit_code == 0 - # Should print at least one model name derived from the SDK's TEE_LLM enum. + assert "Anthropic" in result.output + assert "claude-opus-5" in result.output + assert "hermes-4-405b" in result.output + # Superseded models are held back for --all. + assert "claude-opus-4-8" not in result.output + + +def test_models_all_adds_superseded_and_sdk_only_models(): + result = CliRunner().invoke(cli.main, ["models", "--all"]) + assert result.exit_code == 0 + assert "claude-opus-4-8" in result.output + # An id only the SDK's enum knows about still gets listed. from opengradient import TEE_LLM - sample = next(iter(TEE_LLM)).value.split("/", 1)[1] - assert sample in result.output + from veil import models as catalog + + curated = {m.id for m in catalog.CATALOG} + sdk_only = sorted({m.value.split("/", 1)[1] for m in TEE_LLM} - curated) + if sdk_only: + assert sdk_only[0] in result.output def test_test_command_posts_prompt_to_localhost_and_prints_reply(): diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..4fa6674 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,92 @@ +"""The advertised model catalog. + +The point of these is that the listing can only ever grow: the catalog mirrors +what the Chat app offers, the SDK's ``TEE_LLM`` enum is folded in on top, and +neither source can silently drop a model an agent is already configured for. +""" + +from __future__ import annotations + +from opengradient import TEE_LLM + +from veil import models + + +def test_catalog_ids_are_bare_wire_names(): + # The gateway is sent `claude-opus-5`, never `anthropic/claude-opus-5` -- + # both the SDK and the Chat app strip the provider prefix before the wire. + for model in models.CATALOG: + assert "/" not in model.id, model.id + + +def test_catalog_ids_are_unique(): + ids = [m.id for m in models.CATALOG] + assert len(ids) == len(set(ids)) + + +def test_every_provider_has_a_label(): + for model in models.CATALOG: + assert model.provider in models.PROVIDER_LABELS, model.provider + + +def test_catalog_covers_the_models_the_chat_app_offers(): + # The ten the SDK enum lagged on, which is what this catalog exists to fix, + # plus a couple of long-standing ones to catch a wholesale regression. + expected = { + "gpt-5.6-luna", + "gpt-5.6-terra", + "gpt-5.6-sol", + "claude-sonnet-5", + "claude-opus-5", + "claude-fable-5", + "gemini-3.6-flash", + "gemini-3.5-flash-lite", + "grok-4.5", + "grok-4.3", + "hermes-4-405b", + "glm-5.2", + "deepseek-v4-pro", + } + assert expected <= {m.id for m in models.CATALOG} + + +def test_known_models_is_a_superset_of_the_sdk_enum(): + # Folding the enum in is what keeps a model the Chat app has retired (but + # the network still serves) listed, and picks up anything a future SDK adds. + sdk_ids = {m.value.split("/", 1)[1] for m in TEE_LLM} + assert sdk_ids <= set(models.known_model_ids()) + + +def test_known_models_includes_the_whole_catalog_first(): + known = models.known_model_ids() + assert known[: len(models.CATALOG)] == [m.id for m in models.CATALOG] + assert len(known) == len(set(known)) + + +def test_default_model_is_in_the_catalog(): + assert models.get(models.DEFAULT_MODEL) is not None + + +def test_owned_by_reports_the_provider_for_curated_models(): + assert models.owned_by("claude-opus-5") == "anthropic" + assert models.owned_by("deepseek-v4-pro") == "deepseek" + # An id we don't curate keeps the generic owner rather than guessing. + assert models.owned_by("some-future-model") == "opengradient" + + +def test_by_provider_hides_superseded_models_by_default(): + visible = {m.id for _label, entries in models.by_provider() for m in entries} + assert "claude-opus-5" in visible + assert "claude-opus-4-8" not in visible + + everything = { + m.id for _label, entries in models.by_provider(include_legacy=True) for m in entries + } + assert "claude-opus-4-8" in everything + + +def test_by_provider_separates_chat_from_image_models(): + chat = {m.id for _label, entries in models.by_provider() for m in entries} + image = {m.id for _label, entries in models.by_provider(kind=models.IMAGE) for m in entries} + assert "gpt-image-2" in image + assert not chat & image diff --git a/tests/test_server.py b/tests/test_server.py index cc2feb6..14f3aa1 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,6 +11,7 @@ from opengradient import RelayError, VerificationError, VerifiedChatResponse from opengradient.client.tee_verify import TeeProof +from veil import models from veil.server import create_app @@ -139,3 +140,18 @@ def test_models_and_health(): health = client.get("/health").get_json() assert health["status"] == "ok" assert health["active_tee_id"] == "0xabc" + + +def test_models_lists_the_advertised_catalog_in_openai_shape(): + client = _client(_StubGateway()) + data = client.get("/v1/models").get_json()["data"] + + assert [m["id"] for m in data] == models.known_model_ids() + assert all(m["object"] == "model" for m in data) + by_id = {m["id"]: m for m in data} + # Bare wire names, and the provider is reported as the owner. + assert "/" not in "".join(by_id) + assert by_id["claude-opus-5"]["owned_by"] == "anthropic" + # Superseded models stay listed: an agent already configured for one must + # keep working after it drops out of the Chat app's UI. + assert "claude-opus-4-8" in by_id diff --git a/uv.lock b/uv.lock index ec2cc1b..c344bee 100644 --- a/uv.lock +++ b/uv.lock @@ -2329,7 +2329,7 @@ wheels = [ [[package]] name = "opengradient-veil" -version = "0.2.8" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/veil/cli.py b/veil/cli.py index 822e26e..1eb40ae 100644 --- a/veil/cli.py +++ b/veil/cli.py @@ -14,6 +14,7 @@ import click from veil.config import DEFAULT_APP_URL, ServerConfig +from veil.models import DEFAULT_MODEL from veil.session import AuthError, Session, login, login_manual @@ -276,20 +277,53 @@ def env_cmd() -> None: @main.command() -def models() -> None: +@click.option( + "-a", + "--all", + "show_all", + is_flag=True, + help="Also show superseded models and any extras the SDK knows about.", +) +def models(show_all: bool) -> None: """List the models available through OpenGradient Veil. - Derived from the SDK's canonical ``TEE_LLM`` enum — the same source the - local server uses for ``GET /v1/models`` — so it stays in sync with the - network as models are added or retired. + The same lineup the OpenGradient Chat app offers, grouped by provider. Names + printed here are what an agent puts in its ``model`` field. """ - from opengradient import TEE_LLM + from veil import models as catalog + + def _print(entries: list[catalog.Model]) -> None: + width = max(len(m.id) for m in entries) + for model in entries: + suffix = " (superseded)" if model.legacy else "" + click.echo(f" {model.id.ljust(width)} {model.description}{suffix}") - # Each enum value is ``provider/model``; agents send the bare model name. - names = sorted(m.value.split("/", 1)[1] for m in TEE_LLM) click.echo("Models available through OpenGradient Veil:") - for name in names: - click.echo(f" {name}") + for label, entries in catalog.by_provider(include_legacy=show_all): + click.secho(f"\n {label}", bold=True) + _print(entries) + + image = [ + model + for _label, entries in catalog.by_provider(include_legacy=show_all, kind=catalog.IMAGE) + for model in entries + ] + if image: + click.secho("\n Image generation", bold=True) + click.echo(" Same /v1/chat/completions call; the images come back on the response.") + _print(image) + + if show_all: + # Older generations the Chat app has retired from its UI. Still routable, + # and still valid on an agent config that already names one. + curated = {m.id for m in catalog.CATALOG} + extra = [m for m in catalog.known_model_ids() if m not in curated] + if extra: + click.secho("\n Also routable", bold=True) + for name in extra: + click.echo(f" {name}") + else: + click.echo("\nRun `og-veil models --all` to include superseded models.") @main.command(name="login") @@ -315,7 +349,12 @@ def login_cmd(app_url: str, no_browser: bool, manual: bool) -> None: @main.command(name="test") @click.argument("prompt", nargs=-1) -@click.option("--model", default="gpt-4.1", show_default=True, help="Model to send the prompt to.") +@click.option( + "--model", + default=DEFAULT_MODEL, + show_default=True, + help="Model to send the prompt to (see `og-veil models`).", +) @click.option( "--host", default=None, help="Override the host to reach (default: the running server's)." ) diff --git a/veil/models.py b/veil/models.py new file mode 100644 index 0000000..aef74ee --- /dev/null +++ b/veil/models.py @@ -0,0 +1,405 @@ +"""The model catalog Veil advertises. + +This mirrors the catalog the OpenGradient Chat app ships (`lib/constants/models.ts`), +so an agent pointed at Veil can reach the same lineup a browser user sees. Ids +here are the **bare** wire names -- the gateway is sent `claude-opus-5`, not +`anthropic/claude-opus-5` -- which is what the SDK sends (`model.split("/")[1]`) +and what the Chat app sends (`modelToGatewayModel`). The provider lives in its +own field, for grouping and for `owned_by` on `/v1/models`. + +Why a hand-maintained list rather than deriving everything from the SDK's +``TEE_LLM`` enum, as this used to: the enum ships on the SDK's release cadence +and lags the network. As of `opengradient` 1.1.3 it is missing ten models the +gateway already serves and the Chat app already offers -- the GPT-5.6 trio, +Claude Sonnet 5 / Opus 5 / Fable 5, Gemini 3.6 Flash and 3.5 Flash Lite, and +Grok 4.5 / 4.3 -- so an agent probing `/v1/models` never learned they existed. + +The enum is still folded in (see :func:`known_model_ids`) so anything it adds +later shows up without an edit here, and nothing that used to be listed can +regress. Neither list is a gate: the gateway is the source of truth for what is +routable, and Veil forwards whatever `model` the agent sends. This catalog is a +listing, so it can be generous. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +#: Text models, reached through ``/v1/chat/completions``. +CHAT = "chat" +#: Image-output models. Also reached through ``/v1/chat/completions`` (the +#: gateway returns the images on the final frame, alongside the signed text +#: content), which is how the Chat app's Image Studio calls them. +IMAGE = "image" + + +@dataclass(frozen=True) +class Model: + """One model as Veil advertises it.""" + + #: Bare wire name, sent verbatim in the request's ``model`` field. + id: str + #: Display name, matching the Chat app's label for the same model. + name: str + #: Upstream provider, used for grouping and ``owned_by``. + provider: str + description: str + kind: str = CHAT + #: Superseded by a newer model but still routable, and still valid on + #: existing agent configs. Mirrors the Chat app's `hidden` flag: kept out of + #: `og-veil models` unless `--all`, always present on `/v1/models`. + legacy: bool = False + + +#: Human-readable heading per provider, in the order `og-veil models` prints +#: them. Keyed by the provider prefix the Chat app's model ids carry. +PROVIDER_LABELS: dict[str, str] = { + "anthropic": "Anthropic", + "openai": "OpenAI", + "google": "Google", + "x-ai": "xAI", + "deepseek": "DeepSeek", + "bytedance": "ByteDance", + "nous": "Nous Research", + "zai": "Z.ai", +} + + +# The curated catalog, mirroring `CHAT_MODELS` and `IMAGE_GENERATION_MODELS` in +# the Chat app -- same models, same display names, same order within a provider. +# DeepSeek is served through ByteDance's ModelArk but is credited to DeepSeek, +# as the Chat app does (`getModelFamily`). +CATALOG: tuple[Model, ...] = ( + # ---- Anthropic / Claude ---- + Model( + id="claude-haiku-4-5", + name="Haiku 4.5", + provider="anthropic", + description="Fast, low-cost replies for everyday tasks", + ), + Model( + id="claude-sonnet-5", + name="Sonnet 5", + provider="anthropic", + description="Balanced all-rounder for writing and code", + ), + Model( + id="claude-sonnet-4-6", + name="Sonnet 4.6", + provider="anthropic", + description="Balanced all-rounder for writing and code", + legacy=True, + ), + Model( + id="claude-opus-5", + name="Opus 5", + provider="anthropic", + description="Latest Opus -- top-tier reasoning for complex work", + ), + Model( + id="claude-opus-4-8", + name="Opus 4.8", + provider="anthropic", + description="Top-tier reasoning for complex work", + legacy=True, + ), + Model( + id="claude-fable-5", + name="Fable 5", + provider="anthropic", + description="Anthropic's most capable model for demanding work", + ), + # ---- OpenAI / ChatGPT ---- + Model( + id="gpt-4.1-nano", + name="GPT-4.1 Nano", + provider="openai", + description="Cheapest option for simple tasks", + ), + Model( + id="gpt-5-mini", + name="GPT-5 Mini", + provider="openai", + description="Fast and affordable for daily use", + ), + Model( + id="gpt-5", + name="GPT-5", + provider="openai", + description="Powerful all-rounder, previous generation", + ), + Model( + id="gpt-5.6-luna", + name="GPT-5.6 Luna", + provider="openai", + description="Latest GPT model for fast everyday work", + ), + Model( + id="gpt-5.6-terra", + name="GPT-5.6 Terra", + provider="openai", + description="Latest GPT model with strong reasoning at balanced cost", + ), + Model( + id="gpt-5.6-sol", + name="GPT-5.6 Sol", + provider="openai", + description="Most capable latest GPT model", + ), + Model( + id="gpt-5.5", + name="GPT-5.5", + provider="openai", + description="Most capable for hard problems", + legacy=True, + ), + # ---- Google / Gemini ---- + Model( + id="gemini-3.6-flash", + name="Gemini 3.6 Flash", + provider="google", + description="Latest Gemini for agentic and multimodal work", + ), + Model( + id="gemini-3.5-flash-lite", + name="Gemini 3.5 Flash Lite", + provider="google", + description="Low-cost Gemini for fast, high-volume tasks", + ), + Model( + id="gemini-2.5-pro", + name="Gemini 2.5 Pro", + provider="google", + description="Deep reasoning with a huge context window", + ), + Model( + id="gemini-2.5-flash-lite", + name="Gemini 2.5 Flash Lite", + provider="google", + description="Cheapest for high-volume tasks", + ), + Model( + id="gemini-3.5-flash", + name="Gemini 3.5 Flash", + provider="google", + description="Latest Gemini - fast and highly capable", + legacy=True, + ), + # ---- xAI / Grok ---- + Model( + id="grok-4.5", + name="Grok 4.5", + provider="x-ai", + description="Latest Grok with strong reasoning and live web awareness", + ), + Model( + id="grok-4.3", + name="Grok 4.3", + provider="x-ai", + description="Top-tier reasoning with live web and X", + ), + Model( + id="grok-4-fast", + name="Grok 4 Fast", + provider="x-ai", + description="Quick answers at low cost", + ), + Model( + id="grok-4", + name="Grok 4", + provider="x-ai", + description="Solid all-around performance", + ), + # ---- DeepSeek (via ByteDance ModelArk) ---- + Model( + id="deepseek-v4-flash", + name="DeepSeek V4 Flash", + provider="deepseek", + description="Fast, low-cost DeepSeek model", + ), + Model( + id="deepseek-v4-pro", + name="DeepSeek V4 Pro", + provider="deepseek", + description="Deep reasoning model with a 1024K context window", + ), + # ---- ByteDance / Seed ---- + Model( + id="seed-2.0-lite", + name="Seed 2.0 Lite", + provider="bytedance", + description="Lightweight and budget-friendly", + ), + Model( + id="seed-1.8", + name="Seed 1.8", + provider="bytedance", + description="Most capable Seed model, strong multilingual", + ), + Model( + id="seed-1.6", + name="Seed 1.6", + provider="bytedance", + description="Balanced choice for everyday use", + ), + # ---- Nous Research / Hermes ---- + # Open-weight, neutrally-aligned models, served through the gateway's Nous + # (OpenAI-compatible) provider. + Model( + id="hermes-4-405b", + name="Hermes 4 405B", + provider="nous", + description="Flagship Hermes -- strong reasoning, steerable and uncensored", + ), + Model( + id="hermes-4-70b", + name="Hermes 4 70B", + provider="nous", + description="Fast, low-cost open-weight assistant", + ), + # ---- Z.ai / GLM ---- + Model( + id="glm-5.2", + name="GLM-5.2", + provider="zai", + description="Frontier-class reasoning at low cost", + ), + # ---- Image generation ---- + # Called like any other chat completion; the generated images come back on + # the response (they sit outside the signed output hash, like the Chat app's + # Image Studio path). Ids are already bare on the wire in the Chat app. + Model( + id="gemini-3.1-flash-image", + name="Nano Banana 2", + provider="google", + description="Latest, highest quality", + kind=IMAGE, + ), + Model( + id="gemini-2.5-flash-image", + name="Nano Banana", + provider="google", + description="Faster and lower cost", + kind=IMAGE, + legacy=True, + ), + Model( + id="gpt-image-2", + name="GPT Image 2", + provider="openai", + description="OpenAI's high-fidelity image model", + kind=IMAGE, + ), + Model( + id="seedream-5.0-lite", + name="Seedream 5.0 Lite", + provider="bytedance", + description="Newest Seedream -- fast, high-resolution images", + kind=IMAGE, + ), + # Wire id and display name genuinely differ here; kept as the Chat app has + # them so the id an agent sends stays the one the gateway knows. + Model( + id="seedance-4.5", + name="Seedream 4.5", + provider="bytedance", + description="High-resolution cinematic image generation", + kind=IMAGE, + ), + Model( + id="seedream-4.0", + name="Seedream 4.0", + provider="bytedance", + description="Sharp, high-resolution images", + kind=IMAGE, + legacy=True, + ), + Model( + id="glm-image", + name="GLM-Image", + provider="zai", + description="Low-cost image generation", + kind=IMAGE, + ), + Model( + id="grok-2-image", + name="Aurora", + provider="x-ai", + description="Photorealistic generation", + kind=IMAGE, + legacy=True, + ), +) + +#: What `og-veil test` sends when no `--model` is given. Cheap and fast, so the +#: one-off smoke test costs next to nothing, and it is the Chat app's own +#: free-tier default (`FREE_TIER_DEFAULT_MODEL`) so every account can run it. +DEFAULT_MODEL = "claude-haiku-4-5" + +_BY_ID: dict[str, Model] = {m.id: m for m in CATALOG} + + +def get(model_id: str) -> Model | None: + """The catalog entry for a bare model id, or ``None`` if it isn't curated. + + ``None`` does not mean unroutable -- the gateway decides that, and ids from + the SDK enum are routable without appearing here. + """ + return _BY_ID.get(model_id) + + +def _sdk_model_ids() -> list[str]: + """Bare model ids from the SDK's ``TEE_LLM`` enum. + + Each enum value is ``provider/model``; a value without a provider prefix is + taken as-is rather than dropped. + """ + from opengradient import TEE_LLM + + ids = [] + for member in TEE_LLM: + _provider, _, name = member.value.partition("/") + ids.append(name or member.value) + return ids + + +def known_model_ids() -> list[str]: + """Every model id Veil advertises: the catalog, then SDK-only extras. + + The catalog comes first in its curated order, followed by anything in the + SDK's ``TEE_LLM`` enum that isn't in it (sorted) -- older generations the + Chat app has retired from its UI, plus any model a future SDK adds before + this catalog is updated. Union, never intersection: this list only ever + grows relative to what either source knows. + """ + extra = sorted(set(_sdk_model_ids()) - _BY_ID.keys()) + return [m.id for m in CATALOG] + extra + + +def owned_by(model_id: str) -> str: + """The ``owned_by`` value for a model on ``/v1/models``. + + The upstream provider for a curated model. SDK-only ids keep the generic + ``opengradient`` this endpoint has always reported, since the enum's + provider prefix has already been stripped by the time we see the id. + """ + model = _BY_ID.get(model_id) + return model.provider if model else "opengradient" + + +def by_provider(*, include_legacy: bool = False, kind: str = CHAT) -> list[tuple[str, list[Model]]]: + """Catalog models of one ``kind``, grouped for display. + + Returns ``(provider label, models)`` pairs in :data:`PROVIDER_LABELS` order, + skipping providers that have nothing to show. + """ + groups = [] + for provider, label in PROVIDER_LABELS.items(): + models = [ + m + for m in CATALOG + if m.provider == provider and m.kind == kind and (include_legacy or not m.legacy) + ] + if models: + groups.append((label, models)) + return groups diff --git a/veil/server.py b/veil/server.py index 6ecee4f..c5b6d01 100644 --- a/veil/server.py +++ b/veil/server.py @@ -16,9 +16,10 @@ import logging from flask import Flask, Response, jsonify, request -from opengradient import TEE_LLM, RelayError, VerificationError +from opengradient import RelayError, VerificationError from opengradient.client.tee_verify import UnsupportedRequestError +from veil import models from veil.config import ServerConfig from veil.gateway import Gateway, GatewayError from veil.pii import PiiSetupError @@ -26,14 +27,6 @@ logger = logging.getLogger(__name__) -# Model list for clients that probe /v1/models. Derived from the SDK's canonical -# ``TEE_LLM`` enum so it stays in sync with the network as models are added or -# retired — no hand-maintained copy to go stale. Each enum value is -# ``provider/model``; callers (and the gateway) use the bare model name, which is -# what the SDK sends over the wire (``model.split("/")[1]``). The gateway remains -# the source of truth for what's actually routable; this is a convenience. -_KNOWN_MODELS = [m.value.split("/", 1)[1] for m in TEE_LLM] - def create_app(gateway: Gateway) -> Flask: app = Flask(__name__) @@ -52,11 +45,17 @@ def health(): @app.get("/v1/models") def list_models(): + # The catalog Veil advertises (see veil.models): the models the Chat app + # offers, plus anything else the SDK's enum knows about. The gateway is + # the source of truth for what's actually routable -- a model absent + # here is still forwarded if an agent asks for it -- so this listing + # errs on the side of completeness. return jsonify( { "object": "list", "data": [ - {"id": m, "object": "model", "owned_by": "opengradient"} for m in _KNOWN_MODELS + {"id": m, "object": "model", "owned_by": models.owned_by(m)} + for m in models.known_model_ids() ], } ) From 4d9b0018f8b4dac0c73328539f0834a427ad6d11 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Thu, 30 Jul 2026 16:40:50 +0100 Subject: [PATCH 2/2] Say what the image guarantee actually covers, and why there's no video Two accuracy gaps in the models docs the previous commit added. Generated image bytes ride out-of-band and are not signed -- the enclave hashes the assistant text (see the SDK's response_content_for_hash). The request is OHTTP-sealed exactly as a text prompt is, so privacy is unchanged, but `X-OpenGradient-Verified: true` on an image-generation response attests the exchange, not the pixels. Worth stating plainly in a README whose whole subject is what the verification covers. And note the absence of video. Chat's Video Studio (Veo, Kling, Seedance, Wan) doesn't run over this path: those requests go to chat-api, which holds the fal/BytePlus/Atlas keys and calls the providers directly -- no enclave, no OHTTP, no signature. Listing those models here would advertise a guarantee that doesn't exist, so the README says so rather than leaving the omission to be guessed at. --- README.md | 17 +++++++++++++++-- veil/cli.py | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4f5fa87..3d9f432 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,21 @@ superseded ones), and `GET /v1/models` serves it to any agent that probes. Names are **bare** - send `claude-opus-5`, not `anthropic/claude-opus-5`. Earlier generations (`claude-opus-4-8`, `gpt-5.5`, `grok-4-1-fast`, `o3`, …) stay routable and stay listed, so an agent already configured for one keeps working. -Image models are called like any other chat completion; the images come back on -the response. + +**Image models are called like any other chat completion** and the images come +back on the response — but note what the guarantee covers: the request is +OHTTP-sealed exactly as a text prompt is, so the privacy story is unchanged, +while the enclave's signature covers the assistant text, *not* the generated +image bytes. Those ride out-of-band and are unsigned (see the SDK's +`response_content_for_hash`). So `X-OpenGradient-Verified: true` on an +image-generation response attests the exchange, not the pixels. + +**No video models.** Chat's Video Studio (Veo, Kling, Seedance, Wan) doesn't run +over this path at all — those requests go to chat-api, which holds the +fal/BytePlus/Atlas keys and calls those providers directly, with no enclave, no +OHTTP and no signature anywhere in the chain. Listing them here would advertise +a guarantee that doesn't exist. It needs the gateway to proxy video generation +in-enclave first. --- diff --git a/veil/cli.py b/veil/cli.py index 1eb40ae..936805e 100644 --- a/veil/cli.py +++ b/veil/cli.py @@ -310,7 +310,8 @@ def _print(entries: list[catalog.Model]) -> None: ] if image: click.secho("\n Image generation", bold=True) - click.echo(" Same /v1/chat/completions call; the images come back on the response.") + click.echo(" Same /v1/chat/completions call. The images come back on the response,") + click.echo(" outside the signed output hash -- the exchange is verified, not the pixels.") _print(image) if show_all: