Models

Discovering models and choosing between them.

AOCore routes requests to the model your API key is allowed to use. This page covers three things:

  1. How to discover what models you can call.
  2. How model allowlists work on minted keys.
  3. The 404 anti-enumeration security property — why /v1/models/{model} and chat-style endpoints return 404, not 403, for models you can’t access.

Discovering available models

GET /v1/models — list

Returns the full set of models your API key is allowed to call. The list is intersected with your key’s models[] allowlist — out-of-allowlist models are filtered out of the response (not returned with a “forbidden” marker; simply absent).

curl -H "Authorization: Bearer sk-…" \
  https://gateway.core.aocyber.ai/v1/models
{
  "object": "list",
  "data": [
    { "id": "gpt-4o-mini",  "object": "model", "owned_by": "openai" },
    { "id": "claude-3-5-sonnet-20241022", "object": "model", "owned_by": "anthropic" }
  ]
}

If your key has an empty models[] (the default for keys minted without an explicit allowlist), the list shows your developer’s full allowlist. If your developer has no allowlist either, the list shows every registered model on the gateway. Empty allowlist == inherit upward.

GET /v1/models/{model} — point-lookup

Returns a single model’s metadata if you can call it; returns 404 if you can’t (see the next section for why this is 404 rather than 403).

curl -i -H "Authorization: Bearer sk-…" \
  https://gateway.core.aocyber.ai/v1/models/gpt-4o-mini

GET /developer/v1/models/available — what you can grant

This is a developer-side management endpoint (cookie auth, not API-key auth). It returns the models your developer role is allowed to grant when minting new keys — the intersection of your developer’s users.models[] and the registry.

Use it to populate the model picker in your key-mint UI. The endpoint never includes models outside your developer’s allowlist, so an SDK / portal built on top of it cannot expose restricted model names even by accident.

How allowlists work

There are three allowlists in the chain, each narrowing the one above it:

  1. Registry. The gateway’s models table — every model the deployment knows about. Set at deploy time by the administrator.
  2. Developer allowlistusers.models[] on your developer user. The administrator sets this when they grant you developer access. Empty = inherit the full registry.
  3. API-key allowlistapi_keys.models[] on each minted key. You choose this at mint time. Empty = inherit your developer allowlist.

A /v1/chat/completions request must satisfy all three: the model exists in the registry and is in your developer allowlist and is in your key allowlist.

Mint-time intersections are enforced server-side:

  • You can’t mint a key with a models[] that includes a model outside your developer allowlist — the request returns 400 with a clear error.
  • You can’t pass models: [] to mean “any model” — the empty array defaults to your developer allowlist (TestCreate_DefaultsModels). Explicit null is rejected.

This means a developer with ["gpt-4o-mini"] cannot mint a key for gpt-4, even if the registry contains gpt-4 — the intersection at mint time is the only place where this is enforced cleanly, before the key is created.

The 404 anti-enumeration property

When you call GET /v1/models/{model} for a model your key is not allowed to access, the response is 404 Not Found, not 403 Forbidden. The spec documents this verbatim:

Returned both when the model genuinely does not exist AND when the model exists but is not in the caller’s allowlist.

This is intentional. Distinguishing the two cases — “model doesn’t exist” vs “model exists but you can’t use it” — would let a caller enumerate the gateway’s full model catalog by probing each name and recording which return 403 vs 404. Treating both cases identically as 404 makes the model namespace opaque to unauthorized callers.

The same property extends to:

  • /developer/v1/models/available — restricted developers never see out-of-allowlist model names in the response. This is proven by TestAvailable_RestrictedDeveloper_NeverLeaksRestrictedModelName which scans the raw response body with strings.Contains for any restricted model name.
  • Body endpoints (/v1/chat/completions, /v1/embeddings, …) that mention an unauthorized model in the request body. These return 400 (validation error) or 404 (model-not-found), not 403. Same anti-enumeration logic.

Why not 403?

403 would semantically mean “the resource exists, but you can’t access it.” That answer leaks the resource’s existence. AOCore’s threat model treats the model catalog as confidential to non-privileged developers — listing the full gateway catalog should require explicit allowlisting, not negative inference.

The trade-off: an SDK author calling /v1/models/{model} cannot distinguish “this model name was typo’d” from “I don’t have permission to this model” in isolation. The remediation is to call /v1/models first (which lists exactly what you can call) before any point-lookup. Modern SDKs already do this for unrelated reasons (caching, model-feature detection).

Choosing a model

/v1/chat/completions is OpenAI-compatible. To call:

  • An OpenAI model — pass its OpenAI model id (e.g. "gpt-4o-mini").
  • An Anthropic model — use /v1/messages (native Anthropic shape) or pass the model id directly through /v1/chat/completions (gateway translates the request shape).
  • A Google AI model — use /v1beta/models/{model}:generateContent (native Google shape).

The gateway routes based on the model id’s prefix or registry entry. See examples/curl/chat-completion.sh and the API reference for the full list of registered models.

Common pitfall — empty models[] at mint time

Minting a key with models: [] (empty array) does not mean “no models allowed.” It means “inherit my developer’s allowlist.” If you want a key that genuinely can’t call anything, mint it with a single allowlist entry and revoke that single entry’s grant — the more pragmatic answer is to just not mint the key.

This invariant is locked by TestCreate_DefaultsModels:

when api_keys.models is empty/null at INSERT,
the developer's users.models[] is treated as the effective allowlist
on every /v1/* request.