Service Names & base_url Naming Conventions¶
This page documents how the platform names services and how the
user_access_interfaces[].base_url field must reference that name. The
rules exist so customers can predict how to reach your services, so the
gateway can resolve every published URL unambiguously, and so future
platform features can reserve URL namespaces (memoization, request logs,
async fan-out, etc.) without breaking existing seller catalogs.
The validator that enforces these rules lives in
unitysvc-core and runs on
every usvc_seller data validate invocation. CI / upload pipelines
reject non-conformant catalogs before they reach the platform.
service_name = listing.name¶
The platform service identifier is service_name, and it is exactly
listing.name — written verbatim by the seller. There is no
composition and no fallback: listing.name is the single source of
truth. It is:
- Required. Every
listing.{json,toml}must declarename. - The routable, customer-facing identifier — the value the gateway
routes by, the value
usvc_seller … --nameselects on, and the value{{ service_name }}renders to in abase_url.
listing.name vs offering.name¶
These are two different names with two different jobs — do not confuse them:
| Field | Role | Faces | Example |
|---|---|---|---|
listing.name |
service_name — the platform service identifier |
Customers | cohere/command-r-plus |
offering.name |
the upstream name, kept in sync with the upstream provider's own service id | The upstream provider | command-r-plus |
offering.name describes the thing you are reselling as the upstream
calls it (e.g. the model id at the provider's API). listing.name is
what your customers see and route to. offering.name never
becomes service_name — only listing.name does.
# offering.{json,toml} — the upstream's name for the service
name = "command-r-plus"
# listing.{json,toml} — the customer-facing service identifier (service_name)
name = "cohere/command-r-plus"
Namespaced vs top-level names¶
Whether a name is namespaced or top-level is purely syntactic — does
the name part contain a /?
- Has
/⇒ namespaced (cohere/command-r-plus): self-service. The first segment must equal your provider slug (provider_v1.name), so you cannot registerotherprovider/…under your own provider. - No
/⇒ top-level (ntfy,http-relay): a request that an admin must accept (reserved-name allowlist). Sellers cannot self-register top-level names;usvc_seller data validateaccepts the grammar locally, but the backend gates the name at registration.
# Namespaced — first segment is the provider slug (self-service)
name = "cohere/command-r-plus"
name = "huggingface/Qwen/Qwen2.5-Coder-7B-Instruct" # hierarchical
name = "cohere/command-r-plus@byok" # with variant tag
# Top-level — admin-gated (e.g. a canonical open protocol or gateway-native service)
name = "ntfy"
base_url must route by {{ service_name }}¶
A user_access_interfaces[].base_url does not hard-code the service
path. It references the service identifier through the
{{ service_name }} Jinja variable, which the platform renders to
listing.name when the access interface is materialized. This keeps the
routable path bound to the name automatically.
# The canonical form — service_name leads the path
base_url = "${API_GATEWAY_BASE_URL}/{{ service_name }}"
# With a static or dynamic suffix
base_url = "${API_GATEWAY_BASE_URL}/{{ service_name }}/v1/chat/completions"
base_url = "${API_GATEWAY_BASE_URL}/{{ service_name }}/{{ enrollment.code }}"
# Wrapper-stack primitive prefixes may precede it
base_url = "${API_GATEWAY_BASE_URL}/u/{{ service_name }}"
# The /a/ movable-pointer convention (#1139)
base_url = "${API_GATEWAY_BASE_URL}/a/cohere-latest"
Rejected:
# Literal <provider>/<service> path — use {{ service_name }} instead
base_url = "${API_GATEWAY_BASE_URL}/cohere/command-r-plus"
# The removed /p/ route primitive
base_url = "${API_GATEWAY_BASE_URL}/p/cohere"
A base_url is accepted when, after ${API_GATEWAY_BASE_URL}, it
references {{ service_name }}, is an /a/<alias> movable
pointer, is the gateway root (${API_GATEWAY_BASE_URL} alone), or is
entirely dynamic from its first segment (e.g. a BYOE
${API_GATEWAY_BASE_URL}/{{ params.endpoint }}).
The name grammar¶
listing.name (and the alias after /a/) is validated per-segment.
The identifier has the form <name>[@<variant>]; each /-separated
piece of <name> is a segment:
| Rule | Detail |
|---|---|
| Minimum length | Every segment must be 2 or more characters. Single-character segments are reserved (see below). |
| Allowed characters | Letters (A-Z, a-z), digits (0-9), ., -, _. |
| First character | Must be alphanumeric. Leading -, _, . are rejected. |
@ variant tag |
At most one @ separates the name part from an optional seller-defined variant suffix (@byok, @premium-eu, etc.). The variant has the same per-segment character rules but no minimum-length requirement. |
| Hierarchical names | Multi-segment names like HuggingFace's huggingface/Qwen/Qwen2.5-Coder-7B-Instruct are accepted; each segment is validated individually. |
# Accepted
name = "cohere/command-r-plus"
name = "cohere/command-r-plus@byok"
name = "huggingface/Qwen/Qwen2.5-Coder-7B-Instruct"
# Rejected
name = "co/x" # single-char segment
name = "a@b@c" # multiple '@'
name = "/leading" # leading '/'
name = "trailing/" # trailing '/'
name = "with space" # space not allowed
name = "-leading-dash" # must start alphanumeric
Reserved single-letter prefixes¶
Single-character first segments are reserved to keep the gateway's wrapper / primitive namespace free of collisions with seller paths. The platform uses these prefixes to layer behavior on top of any service URL without changing the seller's published path:
| Prefix | Reserved for | Purpose |
|---|---|---|
a/ |
Aliases | Customer-defined URL aliases and seller "movable pointer" naming (see below). |
b/ |
Broadcast | Fan-out a single request to multiple services. |
c/ |
Chain | Sequence two or more services. |
d/ |
Delayed dispatch | Register a one-shot future call. |
f/ |
Failover | Secondary path if the primary fails. |
g/ |
Groups | Address a service group rather than a single listing. |
l/ |
Logging | Force a request to be captured in the customer's call log. |
m/ |
Memoize | Cache the response in the gateway. |
p/ |
(removed) | Was the explicit /p/<provider> prefix; superseded by {{ service_name }} (#1138). |
r/ |
Recurrent | Register a recurring scheduled call. |
t/ |
Tee | Fire-and-forget mirror of a request to a second service. |
The a/ movable-pointer convention (#1139)¶
/a/<alias> is a customer-facing movable pointer: "this URL is a
movable pointer — the publisher reserves the right to re-point the
underlying target at a newer listing later." It carries no special
routing behavior at the listing layer; gateway-side, a/<rest> resolves
like any other listing path. The benefit is social: customers see which
URLs are stable ({{ service_name }} → a sticky listing identifier) and
which are intentionally mutable (a/cohere-latest).
base_url = "${API_GATEWAY_BASE_URL}/a/cohere-latest"
base_url = "${API_GATEWAY_BASE_URL}/a/anthropic/claude-opus-latest"
base_url = "${API_GATEWAY_BASE_URL}/a/cohere-latest/{{ enrollment.code }}"
After the leading a/ is stripped, the remaining alias is validated
under the normal grammar: bare a/ is rejected, a/x is rejected
(single-char), and other primitive prefixes are not part of the
carve-out. Use /a/ only when you intend to re-point the URL over time;
for the common case — a stable, sticky service — use
${API_GATEWAY_BASE_URL}/{{ service_name }}.
Selecting services by name on the CLI¶
The CLI selects services by service_name (= listing.name). The
--name option is an fnmatch pattern: a literal name matches one
service, while wildcards (cohere/*, *llama*) match a set. * spans
/, and matching is case-sensitive.
# Local data commands — exact name (one service) or a pattern (a set)
usvc_seller data run-tests --name cohere/command-r-plus
usvc_seller data list-tests --name 'cohere/*'
usvc_seller data upload --name 'cohere/*'
usvc_seller data show-test cohere/command-r-plus
# Remote service commands — every backend row whose service_name matches
# (a name can also map to several rows, e.g. an active service + its
# pending revision)
usvc_seller services submit --name 'cohere/*'
usvc_seller services set-visibility public --name cohere/command-r-plus
--provider remains a separate axis: it scopes by the provider slug and
is the only way to select a provider's top-level services (whose
service_name is bare, with no provider/ prefix, so a provider/*
pattern can't reach them).
Validating locally¶
Before uploading, run:
usvc_seller data validate # schema + naming validation
usvc_seller data format --check # CI-style formatting check
Both run the same validators the platform uses on upload; catching
issues locally avoids a round-trip through usvc_seller data upload just
to see the rejection.
Related¶
- Naming convention &
/p/removal —unitysvc/unitysvc#1138. /a/movable-pointer umbrella —unitysvc/unitysvc#1139.- Validator implementation —
unitysvc-core(validate_listing_gateway_base_urls,validate_service_identifier).