| name | unity-ai-gateway-model-service |
| description | Create, configure, query, and govern Databricks Unity AI Gateway "model services" (UC securables at catalog.schema.service that front FMAPI models), and wire a deployed Mosaic AI agent endpoint to call one. Covers the live 2.1 REST API (create-or-get, routing/traffic-split, rate limits, inference tables, guardrails/service-policies, usage tracking), OpenAI-compatible + ChatDatabricks querying, observability via system tables, and the dedicated-service-principal "dance" required for deployed agents to authenticate to a model service. Use when working with Unity AI Gateway, model services, /ai-gateway/mlflow/v1, use_ai_gateway=True, or a deployed agent that 404s calling a gateway model service. |
Unity AI Gateway — model service management & agent auth
Verified live on a Beta workspace, 2026-07-06. Code samples are in resources/.
What a "model service" is
A Unity Catalog securable at catalog.schema.<service> (a "Service" object alongside tables/functions/models) that fronts one or more FMAPI foundation models behind governed routing, rate limits, inference logging, guardrails, and usage tracking. Callers invoke it by its fully-qualified name over an OpenAI-compatible API and inherit all governance. Governed with normal UC privileges (USE CATALOG/USE SCHEMA/EXECUTE). system.ai.* ships one pre-provisioned service per Databricks-hosted model (non-deletable).
API surface — 2.1 (live) vs 2.2 (docs)
2.1 is what's live on Beta rings today — AIP/Google-style: parent + model_service_id query params, a config{} body, and PATCH with update_mask. Use this. No dedicated Python SDK / Terraform / CREATE SERVICE SQL yet → drive it with WorkspaceClient().api_client.do(...).
2.2 appears in public AWS docs (flat body: catalog_name/schema_name/routes/rate_limits{tpm,qpm}) but 404s on Beta workspaces — it's the forward/Public-Preview shape. Don't target it until it's on your ring.
- SQL supports only
GRANT EXECUTE ON MODEL SERVICE … / DROP MODEL SERVICE ….
Base path: /api/2.1/unity-catalog/model-services. See resources/manage_model_service.py for a full CRUD + config module.
CRUD (all verified)
| Op | Call |
|---|
| List | GET /api/2.1/unity-catalog/model-services |
| Get | GET .../model-services/{cat}.{sch}.{name} |
| Create | POST .../model-services?parent=schemas/{cat}.{sch}&model_service_id={name} body {"config":{"destinations":[…]}} |
| Update | PATCH .../model-services/{cat}.{sch}.{name}?update_mask=config.<field> |
| Delete | DELETE .../model-services/{cat}.{sch}.{name} |
parent and model_service_id are query params (not body). model_service_id can't contain ./space///%.
Config fields (exact shapes)
- Destination (a fronted model):
{"name":"system.ai.<model>","type":"DESTINATION_TYPE_PAY_PER_TOKEN_FOUNDATION_MODEL","traffic_percentage":N,"pay_per_token_config":{"model":"models/system.ai.<model>"}}. Note the resource-name form models/system.ai.<model>.
- Traffic split — multiple destinations whose
traffic_percentage sum to 100; PATCH update_mask=config.destinations. Settable via API. Max 5 destinations.
- Fallback chain — nested
config.routing.fallback.destinations[] (each traffic_percentage:0), used on 429/5xx. UI-only in Beta — update_mask=config.routing.fallback / config.fallback / config.routing all reject with "not yet supported by UpdateModelService". Configure on the service's Routing tab.
- Rate limits —
config.rate_limits[] (array): {"key":"RATE_LIMIT_KEY_SERVICE","renewal_period":"RATE_LIMIT_RENEWAL_PERIOD_MINUTE","requests":"100"}. Separate entries for requests (QPM) vs tokens (TPM); string values. RATE_LIMIT_KEY_USER requires a principal. Max 20/service, 5 group-specific.
- Inference table (object, not a string):
{"parent":"schemas/{cat}.{sch}","table_name_prefix":"<prefix>","enabled":true} → auto-creates <prefix>_payload. Needs CREATE TABLE. Read-back adds table/table_id.
- Guardrails —
config.service_policies[]: {"name":"…","policy_type":"POLICY_TYPE_BUILTIN","handler":"system.ai.block_jailbreak","rank":1,"options":{"model_service":"model-services/system.ai.gpt-5-nano","phases":"pre_call","dry_run":"false"}}. A small judge model evaluates each request. POLICY_TYPE_CUSTOM handler = a UC UDF functions/{cat}.{sch}.{fn}. Built-ins: jailbreak (system.ai.block_jailbreak verified), PII redact/block, unsafe content, hallucination.
config.usage_tracking.enabled is mandatory — PATCHing it false → 400. config.tracing.enabled on by default.
Granting access — use the permissions REST API (avoids DDL ambiguity)
PATCH /api/2.1/unity-catalog/permissions/{securable_type}/{full_name} with {"changes":[{"principal":"<sp-app-id-or-user>","add":["<PRIV>"]}]}. Securable types: catalog (USE_CATALOG), schema (USE_SCHEMA), model_service (EXECUTE). This sidesteps the GRANT EXECUTE ON SERVICE vs ON MODEL SERVICE keyword uncertainty.
Querying a model service
- OpenAI-compatible:
OpenAI(api_key=<token>, base_url="https://<host>/ai-gateway/mlflow/v1"), model="<catalog.schema.service>". Tool-calling works through the gateway.
- LangChain:
ChatDatabricks(model="<fqn>", use_ai_gateway=True). Add workspace_client=<SP client> to authenticate as a specific SP (see below). ChatDatabricks also has use_ai_gateway_native_api and a profile field.
- Do NOT pass
temperature to Claude/Sonnet-5 through the gateway — it 400s (does not support the temperature parameter).
Observability
system.ai_gateway.usage — tokens, latency, TTFT, requester, status_code, endpoint_name (the model-service FQN). Account-admin read only. Rejected/NOT_FOUND calls are not logged here.
system.billing.usage — DBU cost with ai_gateway.* metadata.
- The inference
_payload table — request/response payloads (aggregated every few minutes).
- Budgets live in the Account Console (not a service API field). Lineage isn't in Beta.
The dedicated-SP "dance" (deployed agent → model service auth)
Problem: a Mosaic AI agent deployed via agents.deploy() runs under a hidden, Databricks-managed system service principal whose automatic-auth credentials are scoped to declared mlflow.models.resources. There is no DatabricksModelService resource type, so that SP can't see the model service and the deployed agent's LLM call fails with 404 NOT_FOUND: '<fqn>' does not exist' (UC hides securables the caller lacks USE CATALOG/USE SCHEMA/EXECUTE on). The hidden SP is undiscoverable/ungrantable, and its rejected call leaves no system.ai_gateway.usage row. (Deployed agents aren't yet first-class Unity AI Gateway callers — "coming next quarter" per the internal FAQ.)
Fix (verified end-to-end): authenticate the LLM hop as a dedicated, grantable SP — scoped to only that hop.
- Create/reuse a dedicated SP and store its OAuth creds in a secret scope. Workspace-admin SCIM is sufficient (no account admin):
w.service_principals.create(...) + w.service_principal_secrets_proxy.create(service_principal_id=scim_id).secret. See resources/agent_sp_auth.py.
- Grant it
USE_CATALOG + USE_SCHEMA + EXECUTE on the model service via the permissions REST API (above).
- Pass an SP-authed client only to the LLM:
ChatDatabricks(model=fqn, use_ai_gateway=True, workspace_client=WorkspaceClient(host=…, client_id=SP_CLIENT_ID, client_secret=SP_CLIENT_SECRET, auth_type="oauth-m2m")). Auth is per-client, so only the LLM runs as the SP; UCFunctionToolkit tools keep the endpoint's automatic passthrough (leave their DatabricksFunction resources declared).
- Deploy with the creds injected as secret-backed
environment_vars, under SP_CLIENT_ID/SP_CLIENT_SECRET — NOT DATABRICKS_CLIENT_ID/SECRET (those are auto-detected by the SDK default auth chain and would make the whole agent run as the SP, disabling tool passthrough).
- Probe with retry after granting — grants + service reconfiguration take a few seconds to propagate to the gateway; a fresh grant + immediate invoke can transiently 404. Retry a tiny SP invoke until it succeeds before relying on it.
Deploy gotchas (all hit during validation)
pip_requirements must be explicit. MLflow models-from-code auto-inference drops databricks-langchain/langgraph/unitycatalog-ai → "missing Python dependency" load failure. List them.
- Pin the langgraph pair. An unpinned
--upgrade/inference leaves langgraph core and langgraph-prebuilt skewed → ImportError: cannot import name 'ExecutionInfo' from 'langgraph.runtime'. Pin langgraph==1.2.6 + langgraph-prebuilt==1.1.0 (a verified-compatible pair) in BOTH the notebook %pip and the served model's pip_requirements.
deploy_feedback_model=False — the auto-deployed feedback model is an extra served entity that can intermittently block a multi-model endpoint update (known gunicorn/mlflow-3.10+ deadlock family; platform-fixed 2026-04-24 — re-log rather than pin mlflow to an old 3.9).
- Stuck subsequent deploys: a first deploy to a fresh endpoint usually succeeds; a subsequent in-place update to a wedged endpoint can hang → delete + recreate, or deploy under a fresh endpoint name.
Environment / Beta notes
- Enable via Account Console → Previews. Not on GovCloud/CSP/HIPAA yet; GCP has no inference tables yet.
- Destinations are Databricks-hosted pay-per-token FMAPI models only in Beta (no PT / external providers).