Skip to content

Managing Applications

An Application is the unit you manage: one upstream LLM with G1-Hummingbird in the middle, owning its own policy (nine thresholds + enforcement modes), knowledge base, cost centre, compliance posture and API keys. One deployment serves many, isolated from each other, on shared hardware.


Call it

What it does. Creates the Application, writes its policy, and mints the key it will authenticate with. Read /apps/meta first — it reports the axes the served detector actually has, and a policy naming any other axis is rejected with a 400.

BASE=http://localhost:8080; J='-H Content-Type:application/json'

curl -s $BASE/v1/glad/apps/meta | jq '.axes'

APP=$(curl -s $BASE/v1/glad/apps $J -d '{
  "name":"Support Bot",
  "config":{"binding":{"upstream_type":"ollama",
                       "base_url":"http://localhost:11434","model":"llama3.1:8b"}}
}' | jq -r .app_id)

curl -s -X PUT $BASE/v1/glad/apps/$APP/policy $J -d '{
  "policy":{"thresholds":{"prompt_safety":0.80},
            "enforcement":{"answer_safety":"block"},
            "scope":"Answers questions about our travel booking service."}
}' | jq '.config_version'

curl -s $BASE/v1/glad/apps/$APP/keys $J -d '{"role":"invoke"}' | jq -r .api_key
import httpx

c = httpx.Client(base_url="http://localhost:8080", timeout=60)
# If a platform token is configured:
# c.headers["X-Geodesia-Admin-Key"] = os.environ["GEODESIA_ADMIN_TOKEN"]

meta = c.get("/v1/glad/apps/meta").json()
print("axes on this deployment:", meta["axes"])

app = c.post("/v1/glad/apps", json={
    "name": "Support Bot",
    "config": {"binding": {"upstream_type": "ollama",
                           "base_url": "http://localhost:11434",
                           "model": "llama3.1:8b"}},
}).json()
app_id = app["app_id"]

c.put(f"/v1/glad/apps/{app_id}/policy", json={"policy": {
    "thresholds": {"prompt_safety": 0.80},
    "enforcement": {"answer_safety": "block"},
    # out_of_scope stays silent until something declares the scope
    "scope": "Answers questions about our travel booking service.",
}})

key = c.post(f"/v1/glad/apps/{app_id}/keys", json={"role": "invoke"}).json()
print(key["api_key"])        # returned ONCE
const BASE = "http://localhost:8080"
const H = { "Content-Type": "application/json" }

const meta = await fetch(`${BASE}/v1/glad/apps/meta`).then(r => r.json())

const app = await fetch(`${BASE}/v1/glad/apps`, {
  method: "POST", headers: H,
  body: JSON.stringify({ name: "Support Bot",
    config: { binding: { upstream_type: "ollama",
                         base_url: "http://localhost:11434", model: "llama3.1:8b" } } }),
}).then(r => r.json())

await fetch(`${BASE}/v1/glad/apps/${app.app_id}/policy`, {
  method: "PUT", headers: H,
  body: JSON.stringify({ policy: { thresholds: { prompt_safety: 0.80 },
                                   scope: "Answers questions about our travel booking service." } }),
})

const key = await fetch(`${BASE}/v1/glad/apps/${app.app_id}/keys`, {
  method: "POST", headers: H, body: JSON.stringify({ role: "invoke" }),
}).then(r => r.json())

What comes back{app_id, config_version, status, app: {…}} from the create, the merged policy plus a bumped config_version from the PUT, and from the key call an api_key shown only once: only its hash is stored.

Field-by-field request/response reference: Control Plane API.

Creating an Application from the UI

Open Applications (ApplicationsView) and click New Application to open the creation modal. The modal performs live model discovery against the upstream you choose, so you pick a real model instead of typing one:

Upstream Discovery source
Ollama GET /api/tags
vLLM / OpenAI (and OpenAI-compatible) GET /v1/models
Bedrock Curated AWS Bedrock model catalog
Vertex Curated Google Vertex model catalog

Discovery is served by the control-plane endpoint POST /v1/glad/apps/upstream/models, which returns the model list and its source (or an error you can act on).

In the same modal you set the cost for the Application — €/Mtok for input and output tokens — so the cost center is configured from the start.

Once created, the Application opens to its detail view, organised into tabs:

Tab What you configure
Model Upstream type, base URL, model, region, logprobs, and credential reference — with live model discovery and a Test connection probe (see below).
Policy One threshold slider per served axis — the list comes from the running checkpoint, not a hard-coded number, so a 9-axis head shows nine rows and an older one fewer — plus per-axis enforcement (block / annotate / off), block_input, CI injection, and the streaming brake. prompt_complexity is not a safety axis: it routes to Model B and never blocks, so block is not offered for it.
Cost & Budget Per-Mtok rates, monthly budget, alert percentages, and the budget-exceeded action. See Cost & Budget.
Governance Applicable laws, risk classification, retention, FRIA link, and human-oversight thresholds.
API Keys Create, list, and revoke the Application's g1k_live_… keys.

Saving any tab validates the full AppConfig, bumps config_version, and takes effect on the next request.

Read-only Overview, then edit with auto-save

The detail view opens on a read-only Overview tab — a safe summary of the Application's binding, policy, cost, and governance that you can't change by accident. To make changes, click Modifica, which switches to the editable settings tabs: Model, Policy, Cost & Budget, Governance, and API Keys.

There is no "Save" button. Every change in those tabs is saved automatically, debounced shortly after you stop editing — adjust a threshold slider, change the budget, edit alert_recipients, and it persists on its own. An inline status indicator shows where each edit stands: "Saving…" while the debounced write is in flight, then "Saved ✓" once it lands (and the underlying config_version bumps). Because saving is automatic, dependent UI — such as the Cost & Budget projection chart — redraws immediately against the new values.

The Model tab: discover and test

The editable Model tab (under Modifica) does more than hold the binding fields — it lets you bind to a real upstream model with confidence:

  • Live model discovery. The Model field is an input backed by a datalist, so you can pick a discovered model or type your own. Models are discovered from the chosen upstream — Ollama (/api/tags), vLLM / OpenAI-compatible (/v1/models), or the curated Bedrock / Vertex catalogs — via POST /v1/glad/apps/upstream/models. Discovery runs automatically and debounced whenever you change the upstream type or the base URL (so a URL typed character-by-character only fires one query when you pause), and a ↻ discover button re-runs it on demand. An inline hint shows how many models were found, the source, or an actionable error if the upstream couldn't be listed.

  • Test connection. A Test connection button probes the bound upstream for reachability (with a latency reading) and runs a logprob probe that reports whether the closed-book axis is available or unavailable for that model — since halluc_closedbook depends on per-token log-probabilities. A reachable upstream also refreshes the discovered-model list.

The closed-book axis needs no per-model setup

The closed-book fabrication detector is cross-model — it works on any bound upstream out of the box, with no calibration or training step. Binding to a new model is just discover → test → save.

These controls are now per-Application

Test connection and model discovery used to live in the global Settings → Gateway card. In Studio they are per-Application, on the Model tab, so each Application is discovered and tested against its own bound upstream.

Where each setting lives: Applications vs. Settings

Studio splits configuration cleanly between the per-Application detail view and the platform-wide Settings page:

Configured per-Application (Applications → Modifica, auto-saved) Configured platform-wide (the Settings page)
Model binding (upstream, base URL, model, region, logprobs, credential ref) Plan & license
Detection policy & per-axis thresholds, enforcement, block_input, CI injection, streaming brake The platform Gateway card — the exposed API bind (host / port) and the numeric solver
RAG knowledge base Appearance (theme)
Cost rates, budget & governance Provider identity
System (server health / version)
Demo reset

Settings is platform-only

Anything specific to one Application — model, detection policy & thresholds, RAG, cost & governance — is set on that Application (and saved automatically). The global Settings page now keeps only platform-wide settings: plan & license, the platform gateway card (exposed API bind + numeric solver), appearance, provider identity, system, and demo reset.


Configuration model

Each Application stores a validated AppConfig JSON document (config_json), versioned by config_version (incremented on every config update). The document has five sections — binding, complex_routing, policy, cost, governance — plus two top-level fields. Missing axes are always back-filled with the defaults, so the axis contract always holds downstream.

Top-level field Type Default Meaning
schema_version int 1 Config document schema version.
calibration_profile string "default" Calibration profile id for this Application's thresholds (set to the model key when seeded).
binding object see below Upstream LLM binding (Model A).
complex_routing object disabled Optional complexity routing to a second model (Model B).
policy object see below Detection policy: thresholds + enforcement + options.
cost object see below Cost rates and budget (FinOps).
governance object see below Compliance and human-oversight settings.

binding — the upstream LLM

Field Type Default Meaning
upstream_type string "openai" Backend type. One of the allowed types below.
base_url string "" Base URL of the upstream (no trailing slash).
model string "" Model name to request from the upstream.
region string | null null Cloud region (e.g. for Bedrock / Vertex).
api_key_ref string | null null Reference to the upstream credential as secret://…never a plaintext key.
request_overrides dict[str, float] {} Per-request upstream parameter overrides (e.g. temperature).
logprobs string "auto" Log-probability handling: auto, require, or off.

Allowed upstream_type values: openai, ollama, vllm, sglang, trtllm, bedrock, vertex, azure-openai, internal.

Credentials are never stored in plaintext

api_key_ref holds a reference, not a secret: secret://app/<name>. The secrets provider resolves it at request time (environment variable → file → AWS Secrets Manager). Bedrock and Vertex use the host's IAM role / Application Default Credentials and need no key at all. The plaintext upstream key is never persisted in the Application config.

The logprobs setting governs the closed-book fabrication axis: require forces a logprob-capable path, off disables it, and auto (default) uses logprobs when the upstream offers them. See Detection Axes for why halluc_closedbook depends on per-token log-probabilities.

complex_routing — a second model for hard prompts

Optional. Disabled by default, in which case the Application behaves exactly as a single-model deployment and no routing logic runs at all.

Field Type Default Meaning
enabled bool false Master switch.
threshold float 0.5 p(prompt_complexity) strictly greater than this routes to Model B. 0.5 is the classifier's training boundary.
complex_binding object | null null Partial override of binding for Model B. Every field is optional and inherits Model A when unset — usually only model is set.

See Token & Cost Control for what this buys you and how to size the threshold.

policy — detection thresholds and enforcement

The policy is scored across the nine G1-Hummingbird axes. Each axis has its own threshold (probability space, 0.01.0) and its own enforcement mode.

Field Type Default Meaning
thresholds dict[str, float] see table Per-axis flag threshold; a score threshold flags the axis.
enforcement dict[str, str] see table Per-axis action: block, annotate, or off.
block_input bool true If true, a flagged prompt is refused before the upstream is called.
inject_system bool true If true, the Constitutional Intelligence system prompt is prepended to every request.
ci_prompt_ref string "docs/G1_Constitutional_Prompt_Compact.md" Reference to the CI system prompt to inject.
scope string "" The Application's declared purpose, in one sentence. The only input of the out_of_scope axis — without it that axis is silent by construction. A client's own system message overrides it per conversation.
feedback_learning bool false Opt this Application into the approved-feedback exemplar bank without flipping the global default. false = byte-identical detection.
rag_collection string | null null RAG knowledge-base collection bound to this Application.
optional_detectors dict[str, bool] {causal_xai: false, self_consistency: false} Opt-in extra detectors.
streaming_brake dict {enabled: true, cadence_tokens: 32} Mid-stream re-scoring: whether the brake is on and how often (in tokens) it fires.

Defaults (DEFAULT_THRESHOLDS / DEFAULT_ENFORCEMENT — the serving calibration of the 9-axis head; prompt_safety and jailbreak share a joint 2 % false-positive budget measured on a multilingual benign pool):

Axis Region Default threshold Default enforcement
prompt_safety prompt 0.9215 block
jailbreak prompt 0.9997 block
rag_jailbreak prompt / context 0.2501 block
halluc_context answer 0.6475 annotate
halluc_closedbook answer 0.58 (advisory) annotate
answer_safety answer 0.7295 annotate
profanity prompt 0.90 annotate
out_of_scope prompt 0.90 annotate
prompt_complexity prompt 0.50 (routing boundary) off

Thresholds do not transfer across detector builds

These values belong to a specific checkpoint. A new detector build means a new calibration — an Application created before the change keeps the thresholds stored in its own config. Tune yours against real traffic with Policy Lens rather than by copying numbers.

Threshold direction

A higher threshold is more permissive (fewer flags); a lower threshold is stricter. The prompt-region axes default to block enforcement, while the answer-region hallucination/safety axes default to annotate so you can observe before you block. Validation rejects thresholds outside [0, 1], unknown axis names, and enforcement modes other than block / annotate / off.

The two optional_detectors are off by default: causal_xai adds causal token attribution (see Causal XAI), and self_consistency draws multiple samples to reduce closed-book fabrication false positives.

cost — rates and budget (FinOps)

Field Type Default Meaning
currency string "EUR" Currency for all rates and budgets.
input_per_mtok float 0.0 Cost per million input tokens.
output_per_mtok float 0.0 Cost per million output tokens.
glad_compute_per_mtok float 0.0 Cost per million tokens for detection compute.
complex_input_per_mtok float | null null Model B input rate. null ⇒ inherits the Model A rate, so an Application that has not set it still bills correctly.
complex_output_per_mtok float | null null Model B output rate. null ⇒ inherits the Model A rate.
budget_month float 0.0 Monthly budget for this Application.
alert_pct list[float] [0.8, 1.0] Budget-fraction alert points; must be ascending.
on_budget_exceeded string "alert" Action when the budget is exceeded: alert or block.
alert_recipients list[string] [] Addresses carried inside the budget alert/block webhook payload — never SMTP, never a URL. Your relay at the other end of GEODESIA_ALERT_WEBHOOK turns them into email / Slack / PagerDuty. See Cost & Budget.

All rate fields must be ≥ 0; alert_pct is rejected if not ascending; on_budget_exceeded must be alert or block. The cost rate is snapshotted at call time so historical costs stay stable when you later change rates.

FinOps engine

These fields feed the per-Application usage ledger, daily roll-ups, and budget forecasting. See Cost & Budget for the FinOps engine and the forecasting models.

governance — compliance and oversight

Field Type Default Meaning
fria_id string | null null Linked Fundamental Rights Impact Assessment record.
applicable_laws list[string] ["EU_AI_ACT", "GDPR"] Legal frameworks this Application reports against.
retention_months int 6 Audit-data retention period in months.
risk_classification string "high" EU-AI-Act-style risk class for this Application.
human_oversight dict {auto_trigger: true, safety_threshold: 0.70, halluc_threshold: 0.75} Auto-escalation to human review.

Allowed risk_classification values (RISK_CLASSES): minimal, limited, high, unacceptable, unknown.

The human_oversight object controls when a call is auto-escalated for human review: auto_trigger enables it, and safety_threshold / halluc_threshold set the scores above which a call is routed to the oversight queue. See Human Oversight and FRIA.


Lifecycle states

Every Application is in exactly one of three states, stored in the status column:

State Meaning Effect on traffic
active Normal operation. Requests are scored and routed to the upstream.
paused Temporarily halted. Use to suspend an Application without losing its configuration, keys, or history.
killed Hard-stopped (kill-switch). The Application is retained for audit but treated as stopped.

Diagram

State transitions go through set_status, which only accepts active, paused, or killed. Pausing and resuming are the same operation with a different target state.

The default Application

The default Application is synthesised on first boot from your existing runs/gateway_config.json so a pre-existing single-upstream deployment keeps working unchanged. Treat it as your fallback Application — do not delete it. Deleting an Application also deletes all of its API keys.


API keys

Each Application has its own API keys. A key is the Application's runtime identity on the data plane: an invoke key sent as Authorization: Bearer g1k_live_… on a chat or RAG request now routes that request to its Application and scopes the request's policy, cost, quota, and compliance to it — even when no X-Geodesia-App header or application_id is supplied. (Previously an invoke key was control-plane-only — a read-only credential; it is now also the data-plane routing identity.) An explicit application_id / X-Geodesia-App still wins over the key. See Data-plane routing.

Property Value
Format g1k_live_… (prefix g1k_live_ + URL-safe random)
Storage Only the SHA-256 hash is persisted (key_hash).
Preview A non-reversible preview is stored: g1k_***<last4>.
Plaintext Returned exactly once, at creation time. It is never stored and cannot be retrieved again.
Roles invoke (call the Application) or admin (manage the Application). Defaults to invoke; an unknown role falls back to invoke.
Expiry Optional expires_at; an expired key fails verification.

Copy the key immediately

The plaintext g1k_live_… key is shown once when you create it. If you lose it, you cannot recover it — revoke the lost key and create a new one.

Operations:

  • Create — generates a new key, returns the plaintext once along with key_id, app_id, key_preview, and role.
  • List — returns key metadata (key_id, key_preview, role, active, created_at, last_used_at, expires_at) for an Application — never the plaintext or the hash.
  • Revoke — sets active = 0. Revoked and expired keys fail verification immediately.

A request authenticates by presenting the plaintext key; verification hashes it, looks up the active, unexpired record, touches last_used_at, and resolves it to {app_id, role}. The full create/list/revoke routes are documented in the Control Plane API.


Free-tier limit

The number of Applications an Organization may create is capped by its license entitlement (max_applications). On the free tier the cap is one Application, so the New Application button is disabled once the default Application exists — you can still fully configure, key, and operate that one Application.

Raising the cap

Lifting the limit (and unlocking additional Applications) is a licensing change. See Licensing for entitlement tiers and how max_applications is set.