Upgrade guide
Migrations, rollback and certificates, per release.
On this page
Each release says whether it supports a rolling update (Updates without downtime): the previous version keeps running against the new schema while its instances are replaced. "No" means a planned stop: stop the gateway, admin API and Data API, migrate, start the new version.
POC 0.1.0
Initial demonstrator: .NET 10.0.12, Aspire 13.6, Vue 3, PostgreSQL 17 and Redis 8.6. Production deployment now uses chiseled-extra images, immutable base digests, file-mounted secrets, verified TLS, restricted DB roles/Redis ACL, static SPA serving and segmented Compose. See runbook for the actual deployment procedure.
0.8.5: Claude on the Foundry provider authenticates
- Rolling update: yes. No migration and no configuration change.
- Fixed: Claude deployments of an Azure AI Foundry provider with an API key failed with 401 (502 to the client): the key
went to the resource's Anthropic API in
api-key, which it does not read. It now goes inx-api-key. - Remove workarounds: a separate Anthropic provider kept only for Foundry's Claude deployments is no longer needed; see Providers and models.
0.8.4: model prices from models.dev, Claude on the Foundry provider, team admins, audio priced per unit
- Rolling update: yes. Four migrations, each adding nullable or defaulted columns or widening one, so the previous
version keeps working against the new schema while its instances are replaced:
AudioDurationPrecision,TeamCodes,ModelListPrices,ModelBaseModel. - Check before upgrading: prices of existing Anthropic, Ollama Cloud, OpenAI and OpenRouter models follow models.dev
from two minutes after the admin API starts (see below), the admin API needs outbound HTTPS to
models.dev, and known provider types now serve every endpoint of their API whatever was ticked.
Model prices and details from models.dev
- Rolling update: yes. Migration
ModelListPricesaddsModelDeployments.AutoUpdatePriceandProviderAccounts.AutoUpdatePrices(both defaulttrue),ModelDeployments.DescriptionandModelPrices.Source; the previous version ignores them. - Changed behaviour: prices of existing models can change. Two minutes after the admin API starts, models of
Anthropic, Ollama Cloud, OpenAI and OpenRouter providers whose upstream name is in the models.dev list get the list
price added to their price history when it differs from theirs (usage before keeps its cost), and empty descriptions,
capabilities and context windows are filled. A replaced price is in the price history and the audit log
(
system:price-list). For an agreed price, turn off Price from models.dev on the model (or the provider) and add it again. Such models no longer take a price by hand while they follow the list (POST /api/models/{id}/pricesanswers 409). - Azure AI Foundry: one provider for Claude too. Migration
ModelBaseModeladds the nullableModelDeployments.BaseModel(the model behind a deployment). A Foundry provider now discovers and serves Claude deployments (through the resource's…/anthropic/v1), so a separate Anthropic provider per resource is no longer needed. Existing Anthropic providers at a Foundry address keep working unchanged. To move to one provider, add the Claude deployments under the Foundry provider with Discover models, point routes at them, then delete the Anthropic one. Claude models added by hand under their own deployment name need Base model set (claude-…); until then they go to…/openai/v1, which does not serve Claude. Rolling update: yes; an old gateway ignores the column. - Azure deployments now get descriptions, capabilities and context windows from models.dev, and a price from it when
they have none (discovery and the daily update). Their prices are otherwise left alone. Models added before have no
base model: one whose deployment name differs from its model (
prod-chat) is not found in models.dev until it has one. Open Discover models on the Foundry provider and use Update … shown, which sets the base model and fills empty descriptions of models already added. - Provider capabilities follow from the provider type. OpenAI, Azure OpenAI and Foundry providers now serve every
OpenAI endpoint (Responses, speech to text, realtime, images), Anthropic chat and Messages, Ollama chat and embeddings,
Ollama Cloud chat, whatever was ticked before; which model gets a request still follows its kind. Only OpenAI-compatible
providers keep their chosen endpoints. A provider that had an endpoint unticked to keep requests away from it now gets
them for models of that kind: disable or remove those models instead.
capabilitiesonPOST/PUT /api/providersis now optional and ignored except forOpenAICompatible. No migration. - Outbound HTTPS from the admin API to
models.dev(about 6 MB once a day). Without internet access, setAdmin__PriceListUrlto a copy ofhttps://models.dev/api.jsonin your network, or empty to turn it off; a failed download only logs a warning and leaves prices as they are. - Admin API:
Modelhasdescription,autoUpdatePrice,priceModeandpriceListKey;ProviderhasautoUpdatePricesandpriceList;Pricehassource;DiscoveredModelhasdescriptionandbaseModel, andModelbaseModel; catalogue models havedescriptionandfeatures.POST/PUT /api/modelsand/api/providerstake the new settings (omitted keeps them).GET/POST /api/settings/model-pricesare new. The configuration export includes the settings, descriptions and model capabilities.
Teams manage their own keys
- Rolling update: yes. Migration
TeamCodesadds the nullableTeams.Codecolumn and a unique index on it; the previous version ignores both. While old admin API instances still run, a team admin's calls to them get 403. - Nothing changes until you opt in. No team has a code after the upgrade, so no one is a team admin. To start: give
a team a Team code in Organisation, then give its members the
team-adminrole and that code in theteamCodesclaim, or map groups (UME_OIDC_TEAM_CLAIM,UME_OIDC_GROUPS_TEAM_ADMIN; add both todeploy/.envand the compose file you deploy with, as indeploy/compose.prod.yaml). See Identity provider and roles. - Admin API:
Teamhas a newcodefield, andPOST/PUT /api/teamstake it;/bff/userreturnsteamCodes. Existing clients that do not sendcodeleave it unchanged.
Input audio priced per second, minute or hour
- Rolling update: yes. Migration
AudioDurationPrecisionwidensModelPrices.AudioPerMinuteUsdfromnumeric(18,6)tonumeric(18,10); the old version reads and writes the wider column unchanged.ModelPricesis small, so the rewrite is instant. - Changed behaviour: a model with
audioPerMinuteUsdabove 0 andaudioInputPerMillionUsdat 0 bills its audio input tokens by the duration only, no longer also atinputPerMillionUsd. Whisper-style models report no audio tokens and cost the same as before. A model whose provider charges both duration and audio tokens needs an audio input token price. - The admin API is unchanged: prices are still sent per minute. To enter a price per hour, divide it by 60 (or use the unit in the admin UI price form).
0.8.3: tool calls on gpt-5.6 and GPT-6 through Chat Completions
- Rolling update: yes. No migration and no configuration change.
- Changed behaviour: a Chat Completions request with
toolsto gpt-5.6 or GPT-6 (reasoning profile), and not settingreasoning_effort: "none", is now sent to the provider's/v1/responsesand answered in Chat Completions form. Before, the provider refused it with a 400. The provider must serve/v1/responses(Azure OpenAI and Foundry v1 do; the provider's Responses tick is not needed for this). See Providers and models. - Remove workarounds: a team or key policy that forces
reasoning_effort: "none"for these models is no longer needed, and on gpt-6-astra and gpt-6.1-sol it makes every request fail (they rejectnone). Clients set toapi_mode: responsesonly for these models can go back to Chat Completions.
0.8.2: provider time to first token, failed requests in Usage
- Rolling update: yes. The migration
ProviderFirstTokenadds a nullableProviderFirstTokenMscolumn toUsageRecords; older instances keep writing records without it while they are replaced. - Changed figures: provider p50/p95 on Overview, Health & operations and Provider stability are now the provider's time to first token on streamed answers, not the whole request's latency, so they are much lower and no longer grow with the length of the answers. Requests recorded before the upgrade have no such figure, so right after it the columns show — until streamed traffic arrives.
- Admin API rename:
p50LatencyMs/p95LatencyMsonGET /api/ops/healthandGET /api/ops/providers/statsare nowp50FirstTokenMs/p95FirstTokenMs. Update scripts that read them. - New fields and filters (additive):
providerFirstTokenMsonGET /api/usage/requestsand the Data API's/v1/usage/records;GET /api/usage/requeststakeserrors=true,modelandprovider; metricume.gateway.provider.time_to_first_token. - The old
CostSekcolumns and theExchangeRatestable are still kept in this release; they are dropped in a later one.
0.8.1: costs in USD only, SEK removed
- Rolling update: yes. The migration
SekNoLongerWrittenonly gives theCostSekcolumns ofUsageRecordsandUsageMonthlyTotalsa default of 0, so 0.8.0 keeps writing them while its instances are replaced. Nothing reads them any more; they and theExchangeRatestable are dropped in the next release (which will be Rolling update: no from 0.8.0, so upgrade through 0.8.1). - Breaking: SEK is gone from every interface. Update whatever reads these before upgrading:
- Gateway response headers
x-ume-cost-sekandx-ume-budget-remaining-sek: usex-ume-cost-usdandx-ume-budget-remaining-usd. - Budget alert webhook:
spentSekandlimitSekare gone; usespentUsdandlimitUsd. - Data API:
costSekis gone from/v1/usage/recordsand/v1/usage/aggregate(usecostUsd),limitSekfrom/v1/catalog/budgets(uselimitUsd), and/v1/catalog/exchange-ratesis removed. Convert to SEK in your report where needed. - Metric
ume.gateway.cost(SEK) is removed; useume.gateway.cost.usd. - Admin API:
GET/PUT /api/settings/exchange-rateare removed (404), and so is the Settings card.
- Gateway response headers
- Configuration:
Gateway:FallbackSekPerUsd(Gateway__FallbackSekPerUsd) is no longer read; remove it from your settings. The gateway's readiness no longer has anexchange-ratecheck, so it is no longer Degraded without a rate.
0.8.0: admin UI menu and phone layout, costs in USD
- Rolling update: yes. No migrations; 0.7.1 runs side by side while its instances are replaced.
- Usage CSV export in USD. The admin UI's export (
/api/usage/export.csv) has the columnkostnad USDinstead ofkostnad SEK. Update spreadsheets or imports that read the column by name, and convert to SEK yourself where needed. The Data API, gateway response headers and budget webhooks still carry SEK next to USD. - Admin API: usage summaries and the catalogue report USD (
totalCostUsd,costUsd,inputUsdPerMillion,outputUsdPerMillion); request rows no longer havecostSek. The catalogue no longer needs an exchange rate. - Developers: each npm project has an
.npmrcthat turns off install scripts and only takes package versions that are at least 7 days old when you add or update a package;npm ciinstalls the lockfile as before. See npm packages.
0.7.1: Redis ACL fix for admin sign-in
- Rolling update: yes. No migrations; 0.7.0 runs side by side while its instances are replaced.
- Redeploy the Redis ACL. Admin sessions are written with a time to live (
SETEX/PSETEX), which the ACL shipped with 0.6.4 and 0.7.0 did not allow, so admin sign-in failed with a 500 (RedisServerException) right after the identity provider returned. Redeploy the stack (Portainer regeneratesusers.aclon every start) or runinit-deployment.shand restart Redis (Compose); Kubernetes: give the Redis usergatewaythe commands indeploy/redis/users.acl.example.
0.7.0: content guard, token quotas, usage journal, retries, rate limits, request policies, prompt caching
Rolling update: yes, with the caveats below. Five additive migrations:
CacheWritePrice,KeyConcurrencyLimit,RequestPolicies,TokenBudgetsandContentGuard; 0.6.4 runs against the new schema while its instances are replaced.Usage journal: redeploy the Redis ACL. The gateway user needs
zrangebyscore(read-only) to take over usage batches of a stopped instance: redeploy the stack or runinit-deployment.sh(Compose) sousers.aclis regenerated fromdeploy/redis/users.acl.example, then restart Redis; Portainer regenerates it on redeploy. Without it the journal still works (no records are lost while the database is down) but batches of a stopped instance wait until it is back, and the log warns "Usage journal unavailable (… NOPERM …)". The journal's keys areume:usage-journal*(inside~ume:*).Content guard: migration
ContentGuardaddsVirtualKeys.ContentGuardPolicy(defaultOff, so nothing changes) andUsageRecords.GuardFlags. Nothing to configure unless you want it (docs/content-guard.md).Token quotas: migration
TokenBudgetsaddsBudgets.Unit(defaultUsd, so existing budgets are unchanged),LimitTokensandResidency, andAlertEvents.SpentTokens/LimitTokens. During a rolling update an old gateway instance does not enforce token quotas (it ignores the columns); create them after the rollout. The Data API budgets feed addsunit,limitTokensandresidency.Migrations:
CacheWritePrice,KeyConcurrencyLimitandRequestPoliciesonly add nullable columns (ModelPrices.CacheWriteInputPerMillionUsd,VirtualKeys.MaxConcurrentRequests,VirtualKeys.RequestPolicyandTeams.RequestPolicy, jsonb); the previous version ignores them. One caveat: until every admin API instance runs the new version, an old instance cannot read usage records of the new endpointAnthropicCountTokens(usage and PII pages may fail on it), so finish the rollout before clients use/v1/messages/count_tokens.app-roles.sqlgrants the Data API roleMaxConcurrentRequestsonVirtualKeys; the migration service applies it on its next start (the Data API keys feed gainsmaxConcurrentRequests).Behaviour changes: per-minute limits slide (a key that used its whole limit at the end of one minute waits into the next, and refused requests no longer count); the last candidate is retried once after a connection error or 502/503/504 (
Gateway__Retry__MaxRetries=0restores the old behaviour); OpenAI-format requests translated for Claude carry prompt-cache markers (Gateway__PromptCaching=Offrestores the old behaviour; with Claude add a Cache write price, see below).Redis: the new keys
ume:cooldown:*are inside the existing ACL (~ume:*); the cooldown script usessetandpexpire(already granted). Needs Redis 7 or later (PEXPIRE … GT); the shipped images use 8.6.Behaviour change: a 429/503 with
Retry-Afterno longer counts toward the provider's circuit breaker; the deployment is put on cooldown instead (CircuitBreaker__MaxRetryAfter=00:00:00restores the old behaviour).Existing Claude prices bill cache writes at the input price, as before; add a price with Cache write to bill them at Anthropic's rate from its effective date.
0.6.4: security hardening
- Rolling update: yes. Two additive migrations (
ProviderFeaturePolicy,AuditActorContext); 0.6.3 runs against the new schema while its instances are replaced. - Everyone is signed out once. Admin sessions move to Redis (
ume:admin-session:*, covered by the existing~ume:*ACL; commandsget,setex,psetex,del). The ACL shipped with 0.6.4 and 0.7.0 lackedsetex/psetex, so admin sign-in fails with a 500 (RedisServerException): redeploy with the currentdeploy/redis/users.acl.example. Old cookies are not accepted after the upgrade; users sign in again. During a rolling update a user can be signed out by an old instance once more. - New settings (defaults are fine):
Admin:MaxSessionHours(12) andGateway:AuthFailuresPerMinute(60). - Provider address edits need the credential again when the host, scheme or port changes. Scripts that move a provider
with
PUT /api/providers/{id}or a configuration import must sendcredential(orcredentialEnvironment). - PII policy: keys with Block or Redact may now refuse or change requests that passed before (personal data in the
userfield, a messagename, an Anthropic text document, or written with full-width digits or invisible characters). - Console logs are JSON outside development (all services). Log shippers and alerts that parse the plain-text format
(
warn: Category[id]lines) must read JSON, or setLogging__Console__FormatterName=simple. - Provider refusals are trimmed. A provider's 4xx reaches the client with only
message,type,codeandparam; clients that read other fields of a provider's error body no longer get them. - Repeated JSON properties (
{"model":"a","model":"b"}) get400 invalid_requestinstead of a500; on live audio such an event, or one that is not valid JSON, gets anerrorevent instead of being passed to the provider. - API reference:
/openapi/v1.jsonand/scalaron the gateway and Data API stay public by default; turn them off withGateway:PublishApiReference=false/DataApi:PublishApiReference=false./scalarnow has its own CSP. - Narrower database grants, applied by the migration service on start (no manual step). The gateway role can no longer
read
AuditLogorDataApiKeysor add Data Protection keys; the admin role can no longer insert or delete the settings row or changeUsageRetainedFrom. During a rolling update the old gateway version needs nothing it loses. - Internal CA: new installations get an RFC 5280-conformant CA. On an existing Compose installation run
deploy/init-deployment.sh --renew-caonce (it needs no other options) and restart the stack; Portainer replaces the CA on its own at the next redeploy. Integrations that trusted the oldca.pemmust get the new one. Afterwards movedeploy/certs/ca.keyoff the host. - Provider tools and storage are off for every key (migration
ProviderFeaturePolicyadds two columns, defaultfalse; rolling update still works). Clients that use web search, file search, code interpreter, MCP or other provider tools get400 tool_not_allowed, andstore: true,backgroundorconversationget400 storage_not_allowed, until an admin turns on Allow provider tools / Allow provider storage on the key. Responses API requests are now sent withstore: false, soprevious_response_idstops working for keys without provider storage. Check which applications rely on these before upgrading. - Audit entries record more (migration
AuditActorContextadds seven nullable columns toAuditLog; rolling update still works). New settingAdmin:AuditSourceAddress(Full;TruncatedorNoneif your privacy assessment requires). The admin API applies the gateway's trusted proxies to find the admin user's address. The Data API audit feed has the new fields, and its CSV has seven new columns at the end: consumers that read columns by position must allow for them. - New admin security events:
admin.signin,admin.signout,admin.session.extended,admin.access.denied,admin.data.read; exports areadmin.changewith actionexport. Route them in the collector likeadmin.change.
0.6.0: updates without downtime, budgets in USD
- Rolling update: no (renames budget columns 0.5.0 reads). Stop the gateway, admin API and Data API, migrate, start the new version. Later releases can use the rolling update described below.
- Budgets are in USD. The migration
BudgetsInUsdrenamesBudgets.LimitSektoLimitUsdandAlertEvents.SpentSek/LimitSektoSpentUsd/LimitUsd, and converts the amounts: budget limits at the USD exchange rate in effect at migration time (so each budget allows what it allowed just before the upgrade; with no rate entered, the default fallback of 10 SEK per USD), stored alerts at the rate in effect when they were raised. Check the converted limits on Budgets & alerts and round them if you like. Spend is now summed fromCostUsd. - The gateway's Redis spend counters move from
ume:spend:*(micro-SEK) toume:spend-usd:*(micro-USD). The new counters are seeded from the usage records on first use; the old ones are never read again and expire with their period. No Redis ACL change is needed (~ume:*). - API changes. Admin API budgets and alerts use
limitUsd/spentUsdinstead oflimitSek/spentSek. The gateway addsx-ume-cost-usdandx-ume-budget-remaining-usd;x-ume-cost-sekandx-ume-budget-remaining-sekstay (converted at the current rate). The alert webhook addsspentUsd/limitUsdand keepsspentSek/limitSek. The Data API budgets feed addslimitUsdand keepslimitSek(converted at the current rate). A budget-exceeded message states the limit in USD. - Error messages are in English in the gateway (
error.message, realtimeerrorevents), the admin API and the Data API (detail,title, field errors). Error codes are unchanged; clients that match on message text must match the code instead. - Every service now fails
/health/readyfrom the moment it is told to stop and accepts the new settingShutdown__DrainSeconds(default 0, no change). Load balancers that poll readiness see a stopping instance as down; a Composehealthcheckmay briefly show a stopping container as unhealthy. - New, opt-in:
compose.rolling.yamlandrolling-update.shfor Docker Compose, and Kustomize manifests indeploy/kubernetes. A single-instancecompose.prod.yamlinstallation needs no change. - The first rolling update to this version still stops instances of 0.5.0, which do not drain; a few POSTs can fail at the moment each old instance stops.
- The admin API now asks
api.github.comfor the newest release (shown in Settings > Version), at most every 6 hours. Without outbound HTTPS it logs a warning and the card says it could not reach GitHub; setAdmin__CheckForReleases=falseto stop it.
0.5.0: request logging
- Rolling update: yes (from 0.4.1).
- Successful requests are no longer written to the gateway log. Refused, failed and cancelled requests still are, and
every request is still a usage record. If dashboards or alerts count the "Request … status … in … ms" log lines, switch
them to the usage data or the
ume.gateway.requestsmetric, or turn on Settings > Request logging. - The migration
RequestLogSwitchaddsGatewaySettings.LogAllRequestsandLogAllRequestsUntil; it is additive, so the previous gateway and admin API keep working against the new schema. No grant changes. - The usage writer now writes with
COPY; it needs only the existingINSERTgrant onUsageRecords.
0.4.1: MCP gateway removed
- Rolling update: no (drops columns 0.4.0 reads).
- The migration
RemoveMcpGatewaydrops the MCP tables and columns of 0.4.0 (migrationMcpGateway) and deletes its history row. It is not expand/contract compatible with 0.4.0: a 0.4.0 gateway or admin API fails on the dropped columns, so stop them, migrate and start 0.4.1 together. On a database that never ran 0.4.0 it changes nothing. - It also deletes the rows of MCP traffic: usage records, monthly totals and refused-key records with
EndpointMcporMcpToolCall. 0.4.1 cannot read them, so they would break Usage > Requests and costs and PII incidents. Their cost no longer counts in usage reports or in budget counters rebuilt from Postgres. If you need the MCP calls for accounting, export them before upgrading:SELECT * FROM "UsageRecords" WHERE "Endpoint" IN ('Mcp', 'McpToolCall'). - LLM requests that ran inside an MCP tool loop (
/v1/responsesturns) are ordinary records and are kept; only theirParentRequestIdlink is dropped. - Redeploy the stack or run
init-deployment.shand restart Redis so the ACL no longer allowsume:mcp:events.
0.4.0: usage dashboard
- Rolling update: yes (from 0.3.x; all three 0.4.0 migrations are additive).
- The migration
RequestTimingsadds the nullable columnsUsageRecords.UpstreamMsandTimeToFirstTokenMs(no table rewrite). It is additive: the previous gateway keeps writing records without them. The existing grants cover the new columns (the gateway inserts and the Data API reads the whole table). - Records written before the upgrade have no provider time or time to first token, so the Gateway overhead and Time to first token cards fill in from the upgrade on.
- The Data API usage feed and
GET /api/usage/requestshave the new fieldsupstreamMsandtimeToFirstTokenMs(/api/usage/requestsalsocostUsd); clients that ignore unknown fields need no change.
0.4.0: data retention
- Data older than 365 days is removed the first night after the upgrade (usage records, refused-key records, ended
budget alerts, audit entries), with monthly usage totals kept. If your records-retention decision says otherwise, set
Settings > Data retention right after deploying, or deploy with
Retention__Enabled=falseon the admin API until it is set. - The migration
DataRetentionadds retention columns toGatewaySettings, the tableUsageMonthlyTotalsand the functionume_retention_step; it is additive. Re-run the role grants (the migration service does it) soume_adminmay execute the function: until then runs fail withSQL state 42501, shown on the Settings page.
0.4.0: client address restrictions
- The migration
KeyIpRangesTrustedProxiesaddsVirtualKeys.AllowedIpRanges(text[], default empty) and the tableGatewaySettings. It is additive: the previous gateway and admin API keep working against the new schema.app-roles.sqlgrants the admin role writes onGatewaySettingsand the Data API roleSELECTon the new column; the migration service applies it on start. Run the migration before starting the new Data API, whose key inventory reads the column. UME_FORWARDED_HEADERS/ASPNETCORE_FORWARDEDHEADERS_ENABLEDno longer affect the gateway. Behind a reverse proxy, list the proxy under Settings > Trusted proxies after the upgrade, or refused keys are recorded with the proxy's address and keys given an address restriction are refused. Check the proxy sendsX-Forwarded-For(see the runbook).- New 403 error code
address_not_allowedfor clients of keys with an address restriction.
0.3.0: shared routing rules
- Rolling update: no.
- The migration
RoutingRuleScopeSetsconvertsRoutingRules.ScopeIdinto the setScopeIds(uuid[]), dropsScopeId, addsIsTemplateand inserts four disabled template rules. It is not expand/contract compatible: gateways and the admin API of the previous version cannot read the new schema, so drain traffic, migrate and roll out both together. - Admin API clients: send
scopeIds: [...]instead ofscopeIdwhen creating, updating or reassigning a rule, and{ scope, ruleIds }to reorder. Responses carryscopeIdsandownersinstead ofscopeId/scopeName.
Upgrade procedure
- Review release/source changes, package/image advisories, licenses and SBOM; approve exact application/base digests. Keep Aspire SDK/hosting packages on the 13.6 release family.
- Stop the exact AppHost before local builds. Run all test suites, production deployment checks and accessibility/manual regression appropriate to the change.
- Back up the database, pepper, Data Protection key ring/certificate and nonsecret config. Rehearse restoring the backup in isolation; record RPO/RTO.
- Review migrations for expand/contract compatibility and mark the release Rolling update: yes/no above. For a rolling
update use
deploy/rolling-update.shordeploy/kubernetes/deploy.sh; otherwise drain gateway traffic and run the one-shot migration/role-grant job with only migrator credentials. Do not give DDL to apps. - Deploy digest-pinned API images; verify health, schema/version, queue drain, OIDC/CSRF, role scoping and a synthetic inference request. Preserve credentials/key encryption material.
- Observe error/fallback rates and spend reconciliation. Record release, digests, schema, test evidence and approval in change management.
Routing rules add the migrations RoutingRules (tables RoutingRules, RoutingRuleTargets) and
RoutingRuleOnUsage (two nullable columns on UsageRecords). Both are additive, so the previous gateway image keeps
working against the new schema. deploy/postgres/app-roles.sql now also grants the admin role write access to the
new tables; re-run the role-grant job with the migration.
Security events add the migrations AuthFailures (new table) and FeedRecordedAt (a RecordedAt column with a
database default on UsageRecords, AuthFailures and AuditLog; existing rows get the migration time). Both are
additive. app-roles.sql grants the gateway role INSERT on AuthFailures; re-run the role-grant job.
Speech to text and live audio add the migration AudioTranscription: AudioSeconds on UsageRecords, and
AudioPerMinuteUsd, AudioInputPerMillionUsd and AudioOutputPerMillionUsd on ModelPrices, all NOT NULL DEFAULT 0,
so it is additive and needs no new grants. After deploying, give the providers that serve speech models the Speech to
text capability, add the models with kind Speech to text (or rediscover them) and their per-minute or per-token
price, and create an alias such as ume/transcribe. If a proxy sits in front of the gateway, allow at least 32 MB
bodies and turn request buffering off (see the runbook).
Image generation (/v1/images/generations, /v1/images/edits) adds the migration ImageGeneration: PerImageUsd on
ModelPrices, NOT NULL DEFAULT 0, so it is additive and needs no new grants. After deploying, give the providers that serve
image models the Image generation and editing capability, add the models with kind Image generation (or rediscover them)
and their per-picture or per-token price, and create an alias such as ume/image. Image edits upload up to 50 MB: allow at
least 52 MB bodies at the proxy (Gateway__MaxImageRequestBodyBytes; raise both together).
Live audio (/v1/realtime, /v1/realtime/translations) needs three more steps:
- Redis ACL. The per-key session limit uses a sorted set: add
+zadd +zrem +zcard +zremrangebyscoreto thegatewayuser insecrets/redis/users.acl(the templatedeploy/redis/users.acl.examplehas them). Since 0.3.1 the stack's bootstrap andinit-deployment.shrewriteusers.aclfrom the template on every start, so a redeploy (or runninginit-deployment.shagain) followed by a Redis restart is enough; before, existing installations kept their generated file and had to edit it by hand. Until then sessions work, but the limit is not enforced and the gateway logs a warning per session. - Proxy. Pass the WebSocket upgrade: the
map $http_upgrade $ume_connectionblock and theUpgrade/Connectionheaders indeploy/proxy/nginx.conf.example. Without them, clients get 400 (not a WebSocket request). - Providers. Give the providers the Live audio (realtime) capability and add the models: speech-to-text models
for live transcription, Realtime (live) models, Live interpreting models (
gpt-realtime-translate), with their audio token or per-minute prices. Realtime models price audio tokens far above text tokens, so set the audio token prices or their cost is under-reported.
The control- and data-plane cleanup needs no migration, but check these before upgrading:
- Startup validation. The admin API refuses to start without
Oidc:Authority, withAdmin:RequestsPerMinuteoutside 1–10000, or with aGateway:BaseUrl/Gateway:OperationsUrlthat is not an absolute http(s) URL; the gateway validatesGateway:FallbackSekPerUsd(0.01–1000) and the migration service itsSeed:*settings. - Exchange rate. If no USD exchange rate has been entered, the gateway's readiness check
exchange-ratenow reports Degraded and each catalogue load logs a warning (costs still useGateway:FallbackSekPerUsd, 10). Enter the rate under Settings. - Audit details. New entries hold dedicated snapshots (camelCase, enum names, no derived fields); older entries
keep their shape, so a consumer of
details(Data APIsecurity.read, SIEM) must accept both. - CSV. The admin usage export quotes cells only when needed (it quoted every text cell) and no longer prefixes
numeric text such as
-1with'; the Data API now also prefixes formulas after leading spaces or starting with a line feed. - Alert webhooks are sent once, without retries, from their own queue.
Adding the Data API to an existing installation
New installations get the read-only database role ume_data from init.sql. An existing database was initialised
before that, and creating a role needs the database superuser, so it is one manual step. The Data API is optional;
skip this until you want it.
Re-run
deploy/init-deployment.sh(Portainer: redeploy the stack, which runs the bootstrap). Existing secrets are kept; it addssecrets/postgres/data_password,secrets/dataapi/andcerts/dataapi/.Create the role (idempotent; reads the password file inside the Postgres container and reloads
pg_hba.conf):docker compose -f deploy/compose.prod.yaml --env-file deploy/.env exec -T postgres \ sh -c 'PGPASSWORD=$(cat /run/postgres-secrets/bootstrap_password) psql -q -v ON_ERROR_STOP=1 -U postgres -d gatewaydb' \ < deploy/postgres/add-data-role.sqlThe Postgres container must be running with the new
pg_hba.conf(it listsume_data);docker compose up -dwith the updated files does that.Re-run the migrations job so
app-roles.sqlgrants the role its read access (it skips the grants while the role does not exist), then start the Data API: setCOMPOSE_PROFILES=dataindeploy/.envand rundocker compose ... up -d --wait. If the Data API was started before the role existed, give its health check a few seconds to recover.
Schema-breaking changes require an explicit downtime/rollback plan. Image rollback is safe only if the previous app supports the new schema. Do not automatically run destructive down-migrations; restore an approved backup in isolation first. Redis counters reconstructed after restart depend on committed usage and can lag in-flight requests.
Certificate and secret rotation
Renew TLS certificates with appropriate SANs/CA, stage trust changes, restart the exact services, verify strict validation. Rotate Redis ACL/client passwords together. Update existing Postgres role passwords through an approved DBA workflow as well as secret files. Keep old Data Protection decrypting certificates available during re-encryption/key-ring transition; replacing the encryption certificate blindly loses provider credentials/cookies. HMAC pepper rotation needs a designed key-hash migration or explicit replacement/revocation of all virtual keys; the POC does not support transparent dual-pepper rotation.
Dependency and supply-chain commands
dotnet list UmeLlmGateway.slnx package --vulnerable --include-transitive
Set-Location src\admin-ui
npm audit --audit-level=low
Set-Location ..\..\tests\e2e
npm audit --audit-level=lowGenerate an SPDX SBOM with the approved Microsoft sbom-tool version against each publish
directory; review package/image scans and provenance before promoting images. SBOM inputs
must exclude secret/certificate mounts, test results and developer environments. Dependency
audit success is time-bound, not proof of absence of vulnerabilities; container OS/package
scanning and signed artifact attestations remain municipal release responsibilities.