Chat API¶
What it does. POST /v1/chat/completions is the endpoint you point your existing OpenAI client at. G1-Proxy screens the prompt, forwards the request to your upstream LLM, scores the answer across the nine detection axes, and hands back the ordinary OpenAI response with a geodesia object attached. Blocked requests come back with finish_reason: "content_filter", which standard OpenAI clients already handle.
Everything the endpoint does not recognise as a Geodesia control field is forwarded verbatim to the upstream — that is what makes it a drop-in replacement for vLLM, SGLang or the OpenAI API itself.
Three wire formats, one pipeline:
| Endpoint | Format |
|---|---|
POST /v1/chat/completions | OpenAI chat (SSE streaming) |
POST /api/chat | Ollama chat (NDJSON streaming) |
POST /v1/completions | OpenAI legacy text completion — the proxy wraps prompt into a one-message chat and unwraps the result |
Call it¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"stream": false,
"messages": [{"role": "user", "content": "What is the capital of France?"}]
}' | jq '{
answer: .choices[0].message.content,
decision: .geodesia.decision,
reason: .geodesia.reason,
scores: (.geodesia.axes | map_values(.score))
}'
from openai import OpenAI
# Point the official client at the proxy. Nothing else changes.
client = OpenAI(base_url="http://localhost:8080/gw/v1", api_key="not-needed-locally")
r = client.chat.completions.create(
model="my-model",
messages=[{"role": "user", "content": "What is the capital of France?"}],
stream=False,
extra_body={ # Geodesia extension fields go here
"context": "",
"thinking_level": 0,
},
)
print(r.choices[0].message.content)
# The verdict rides on the raw response, outside the typed model.
g = r.model_extra["geodesia"]
print(g["decision"], (g["reason"] or {}).get("axis")) # "allowed" / "flagged" / "blocked"
for axis, a in g["axes"].items():
if not a["available"]: # not measured is never "clean"
print(f" {axis:20s} n/a ({a.get('unavailable_reason')})")
continue
print(f" {axis:20s} score={a['score']:.3f} thr={a['threshold']:.3f} flagged={a['flagged']}")
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "http://localhost:8080/gw/v1",
apiKey: "not-needed-locally",
})
const r = (await client.chat.completions.create({
model: "my-model",
stream: false,
messages: [{ role: "user", content: "What is the capital of France?" }],
// @ts-expect-error — Geodesia extension fields are not in the OpenAI types
thinking_level: 0,
})) as any
console.log(r.choices[0].message.content)
const g = r.geodesia
console.log(g.decision, g.reason?.axis ?? null) // "allowed" / "flagged" / "blocked"
console.log(g.axes.jailbreak.score, g.axes.jailbreak.threshold, g.axes.jailbreak.flagged)
What comes back¶
The standard OpenAI body with exactly one extra top-level key, geodesia. Everything Geodesia measured and decided for the turn lives inside it (abridged from a live response; details of halluc_closedbook and the certificate omitted):
{
"id": "chatcmpl-geodesia-1790234386650",
"object": "chat.completion",
"created": 1790234393,
"model": "ministral3",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "The capital of France is **Paris**." },
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 1826, "completion_tokens": 23, "total_tokens": 1849 },
"geodesia": {
"schema_version": "1.0",
"event": "final",
"decision": "allowed",
"mode": "blocking",
"reason": null,
"axes": {
"halluc_context": { "score": null, "threshold": 0.7551, "flagged": false, "available": false,
"role": "enforce", "unavailable_reason": "no context supplied" },
"halluc_closedbook": { "score": 0.4687, "threshold": 0.8555, "flagged": false, "available": true, "role": "advisory" },
"prompt_safety": { "score": 0.0076, "threshold": 0.6377, "flagged": false, "available": true, "role": "enforce" },
"answer_safety": { "score": 0.021, "threshold": 0.7953, "flagged": false, "available": true, "role": "enforce" },
"jailbreak": { "score": 0.3333, "threshold": 0.9864, "flagged": false, "available": true, "role": "enforce",
"details": { "head_score": 0.00059, "linear_probe_z": -9.6043 } },
"rag_jailbreak": { "score": null, "threshold": 0.5768, "flagged": false, "available": false,
"role": "advisory", "unavailable_reason": "no context supplied" }
},
"additional_axes": {
"profanity": { "score": 0.026, "threshold": 0.7, "flagged": false, "available": true, "role": "advisory" },
"out_of_scope": { "score": 0.0678, "threshold": 0.9534, "flagged": false, "available": true, "role": "advisory" },
"prompt_complexity": { "score": 0.0155, "threshold": 0.5, "flagged": false, "available": true,
"role": "classifier", "label": "simple" }
},
"grounding": {
"available": true, "verdict": "grounded", "score": 0.7261, "risk": 0.4687,
"margin": -0.4521, "regime": "closed_book", "axis": "halluc_closedbook"
},
"pii": { "enabled": true, "input": { "count": 0, "by_type": {} }, "output": { "count": 0, "by_type": {} } }
}
}
An axis that could not be measured on this turn reports available: false and score: null — never 0. Treat it as "not measured", not as "clean".
When the request is blocked, the content is replaced, finish_reason becomes content_filter, and reason says where and why:
{
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "[Geodesia blocked — jailbreak (input)]" },
"finish_reason": "content_filter"
}
],
"geodesia": {
"schema_version": "1.0",
"event": "final",
"decision": "blocked",
"mode": "blocking",
"reason": { "stage": "input", "axis": "jailbreak", "detail": null },
"axes": {
"prompt_safety": { "score": 0.9143, "threshold": 0.6377, "flagged": true, "available": true, "role": "enforce" },
"jailbreak": { "score": 0.9981, "threshold": 0.9864, "flagged": true, "available": true, "role": "enforce",
"details": { "head_score": 0.993179, "linear_probe_z": 4.8373 } }
}
}
}
(Other axes omitted for brevity; a real response lists every axis the deployment serves.) In passthrough mode the same request returns the real answer with decision: "flagged" and the same reason. A client that only needs "was this turn a violation?" can test decision in ("flagged", "blocked").
out_of_scope stays silent without a declared scope
"Off topic" is undefined until something says what the topic is. Send the Application's purpose as a system message, or set policy.scope once per Application, or that axis will score near zero on everything — see Detection Axes.
Request reference¶
Standard OpenAI fields¶
Forwarded to the upstream unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
messages | array | ✅ | Conversation history. role is user, assistant, system or tool; content is a string or a content-parts array. |
model | string | — | Falls back to the gateway's configured upstream_model, or the Application's binding. |
stream | boolean | — | true for SSE. |
max_tokens, temperature, top_p, n, stop, frequency_penalty, presence_penalty, seed, logit_bias, response_format, tools, tool_choice, … | (various) | — | Forwarded verbatim. |
vLLM-only sampling keys — top_k, min_p, min_tokens, ignore_eos, stop_token_ids, best_of, repetition_penalty, guided_*, … | (various) | — | Also forwarded verbatim to self-hosted upstreams. |
Strict upstreams get a whitelist
Hosted OpenAI and Azure OpenAI reject unknown arguments with a 400. For those two upstream types the proxy keeps only OpenAI-recognised generation parameters and drops the rest — vLLM-only sampling keys and any control field a client attached. Self-hosted OpenAI-compatible servers tolerate extras and keep the full drop-in parameter set.
Geodesia extension fields¶
Recognised by the proxy and never forwarded to the upstream.
| Field | Type | Default | Description |
|---|---|---|---|
context | string | "" | Explicit grounding text. Scored against the answer on the halluc_context axis and injected into the upstream request so the model can use it. |
rag | object | — | Knowledge-base retrieval for this turn: {collection_id, top_k, rerank, verify, verify_deep}. See Knowledge Base. |
web_search | boolean | false | Search the live web, screen every page through the detector, and ground the answer in the safe ones. Requires GW_WEBSEARCH_ENABLED=1 (the default). A web-search request is always answered as an SSE stream, whatever stream says. See Live Web Search. |
mode | string | (config) | "block" (or "blocking") withholds flagged content; "passthrough" returns it with decision: "flagged". Overrides the deployment/Application enforcement for this request only. See Enforcement Modes. |
threshold_overrides | object | — | Per-axis thresholds for this request. Only the five base axes are honoured: halluc_context, halluc_closedbook, prompt_safety, answer_safety, jailbreak. Keys for the other axes are silently ignored — change those in the Application policy instead. |
thinking_level | integer | 2 | Detection depth, 0–3 (3 = MAX). Clamped, never rejected. Falls back to the deepest level the deployment can serve; geodesia.thinking reports what actually ran. The deployment default is set with GW_DEFAULT_THINKING_LEVEL. See Thinking Levels. |
axes | array | string | (all) | Restrict the axes reported for this request (a list, or a comma/space-separated string). Axes that can withhold content are always scored regardless. |
domain / geodesia_domain | string | "general" | Selects the domain-conditional calibration bucket for the closed-book axis. general / default / all / empty all mean domain-agnostic. |
pass_extra | integer | 1 | Extra answer samples for closed-book uncertainty. Only applied when the upstream exposes log-probabilities and the turn has no explicit context. Each extra sample costs another generation. |
self_consistency | boolean | false | Shorthand for pass_extra > 1. |
self_consistency_samples | integer | — | How many extra samples when self_consistency is on. |
scan | boolean | true | Set to false to bypass detection entirely for this request — pure pass-through to the upstream, no scoring, no blocking; the response carries decision: "not_scanned". With GW_API_TOKEN set on the gateway, only a caller presenting that token may do it (403 otherwise). |
pii_guard | boolean | (config) | Per-request override of PII redaction. When the guard is on, the response carries geodesia.pii (counts per entity type, never the values). |
enable_judge_for_context_hallucination | boolean | false | Run the per-claim context judge on this turn; the result appears as geodesia.context_judge (advisory, uncalibrated). Costs extra GPU seconds. |
constitutional_ai | boolean | (config) | Per-request override of the constitutional system prompt. Wins over the deployment config and the Application policy. |
application_id / app_id | string | — | Route this request to a specific Application. Equivalent to the X-Geodesia-App header. |
session_id | string | — | Groups turns into one conversation in the audit log. |
upstream_api_key / upstream_type | string | — | Override the upstream binding for this request. |
The glad_* request aliases were removed
The glad_mode, glad_thinking_level, glad_scan, glad_pii_guard, glad_axes and glad_enable_judge_for_context_hallucination aliases are no longer recognised. Use the unprefixed names above. See Migrating from the pre-1.0 payload.
scan: false turns the guardrail off
It is there for health checks, for replaying traffic you have already scored and for generating explanations of a block (the explanation quotes the blocked words and would otherwise be blocked again). A request served with scan: false is not screened, not scored and not blocked, and reports decision: "not_scanned". Set GW_API_TOKEN so that only operator-side callers can use it, and never let application code set it from user input.
How an Application is resolved¶
The proxy picks the Application in this order:
application_idorapp_idon the body,- the
X-Geodesia-Appheader, - the Application that owns the
g1k_…key inAuthorization: Bearer …, - otherwise
default.
An explicit id always wins, including an explicit "default". The API-key fallback exists so a raw client authenticating with nothing but its key still lands on its own Application. A paused or killed Application keeps its id on the audit record but is served with the global configuration.
Streaming¶
With stream: true the response is SSE (OpenAI format) or NDJSON (Ollama format).
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/gw/v1", api_key="not-needed-locally")
stream = client.chat.completions.create(
model="my-model",
messages=[{"role": "user", "content": "Explain TLS in two sentences."}],
stream=True,
)
verdict = None
for chunk in stream:
g = (chunk.model_extra or {}).get("geodesia")
if g and g["event"] == "final": # the only event that carries the verdict
verdict = g
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
print(delta, end="", flush=True)
print("\n[geodesia]", verdict["decision"], verdict["reason"])
if verdict["decision"] == "blocked": # halted mid-stream: finish_reason was content_filter
print("-- halted by Geodesia --")
const stream = await client.chat.completions.create({
model: "my-model",
stream: true,
messages: [{ role: "user", content: "Explain TLS in two sentences." }],
})
let verdict: any = null
for await (const chunk of stream) {
const g = (chunk as any).geodesia
if (g?.event === "final") verdict = g // the only event that carries the verdict
process.stdout.write(chunk.choices[0]?.delta?.content ?? "")
}
console.log("\n[geodesia]", verdict?.decision, verdict?.reason)
if (verdict?.decision === "blocked") console.log("-- halted by Geodesia --")
Where the payload arrives
Every chunk that carries Geodesia information has a geodesia object whose event says what it reports:
event | When | Contains |
|---|---|---|
input_scan | On empty-delta chunks, once per detector tier as soon as it has scored the prompt (Geodesia-G first, then H, then A) | axes / additional_axes for the prompt axes (prompt_safety, jailbreak, …) and thinking.tiers_used so far — so you can show safety status before the first answer token. Each one replaces the previous. No decision. |
research | Web search only, before the answer | research: one progress event. See Live Web Search. |
progress | Periodic re-scoring during generation, and once per tier while the finished answer is scored | axes with the current scores. No decision. |
final | Last chunk, the one with finish_reason | The complete verdict — same shape as the non-streaming geodesia object. |
Realtime tier scores. At thinking level ≥ 1 the prompt is scored by several detector tiers in sequence (Geodesia-G, then Geodesia-H, then Geodesia-A). The stream opens immediately and sends one input_scan per tier as soon as that tier has scored — Geodesia-G typically within tens of milliseconds, before the slower tiers and before the model's first token. Each input_scan carries thinking: {level, tiers_used} and replaces the previous one; the last one lists every tier that contributed. The same happens on the answer: one progress event per tier before final. In blocking mode the model is called only after the complete prompt verdict; in passthrough mode answer tokens and tier scores interleave.
Take the verdict only from the final event; input_scan and progress are informational. A violation found after the answer has already been fully streamed is reported as decision: "flagged", because the text was already delivered.
data: {"id":"chatcmpl-geodesia-1790234386649","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":null}],
"geodesia":{"schema_version":"1.0","event":"input_scan","axes":{"prompt_safety":{…},"jailbreak":{…}},"additional_axes":{…}}}
data: {"id":"chatcmpl-geodesia-1790234386649","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Ocean’s breath, …"},"finish_reason":null}]}
data: {"id":"chatcmpl-geodesia-1790234386649","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
"geodesia":{"schema_version":"1.0","event":"final","decision":"allowed","mode":"blocking","reason":null,"axes":{…},…}}
data: [DONE]
The mid-stream brake. The proxy re-reads the accumulating generation every cadence_tokens tokens (default 32). If an output-region axis fires before generation finishes it injects \n\n[Geodesia: generation halted — energy barrier], sends a final chunk with finish_reason: "content_filter", decision: "blocked" and reason: {"stage": "output", "axis": "<axis>", "detail": "halted mid-stream"}, then data: [DONE].
Partial flagged content is visible before the brake
A user may see the first tokens of a dangerous answer before it is cut. If your application must never render partial flagged content, use non-streaming mode, or buffer client-side until you see a terminal finish_reason.
Tool calling — tools / tool_choice¶
tools and tool_choice are forwarded to the upstream unchanged (see Request reference above) — the proxy does not rewrite your tool schemas. What it does do is translate whatever the upstream's serving stack returns for a tool call back into standard OpenAI shape: an accumulating delta.tool_calls on each streaming chunk, message.tool_calls on the non-streaming response, and finish_reason: "tool_calls". Any provider-native tool-call syntax the underlying model would otherwise leak into plain text (e.g. a bracketed marker or an XML-ish block, depending on the serving stack) never reaches the client as content.
This requires the upstream's own serving stack to have tool-call parsing turned on — for example vLLM's --enable-auto-tool-choice --tool-call-parser <family> (the parser family must match the model's chat template). Check GET /v1/models — a "tool_calling" entry in capabilities confirms the deployment is configured for it; if it's absent, tools is still forwarded but the model's raw tool-call output may come back as plain content instead of tool_calls.
curl -N -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"stream": false,
"tool_choice": "auto",
"tools": [{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web and return ranked results with URLs.",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" },
"max_results": { "type": "integer" }
},
"required": ["query"]
}
}
}],
"messages": [{"role": "user", "content": "Search the web for the current prime minister of Italy."}]
}'
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/gw/v1", api_key="not-needed-locally")
r = client.chat.completions.create(
model="my-model",
tool_choice="auto",
tools=[{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web and return ranked results with URLs.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}, "max_results": {"type": "integer"}},
"required": ["query"],
},
},
}],
messages=[{"role": "user", "content": "Search the web for the current prime minister of Italy."}],
)
call = r.choices[0].message.tool_calls[0]
print(call.function.name, call.function.arguments) # web_search {"query": "...", "max_results": ...}
What comes back (non-streaming):
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "web_search", "arguments": "{\"query\": \"...\", \"max_results\": 5}" }
}]
},
"finish_reason": "tool_calls"
}]
}
Streaming carries the same call as incremental delta.tool_calls fragments — id/type/function.name on the first fragment for a given index, function.arguments concatenated across the rest — exactly the shape any OpenAI-compatible SDK already knows how to accumulate. Parallel tool calls (the model requesting more than one function in the same turn) come back as separate entries keyed by index, each with its own id.
Completing the round trip¶
After you execute the tool, send the conversation back with the assistant's tool_calls from the previous turn and one role: "tool" result message per call — the proxy forwards both unchanged to the upstream, same as any OpenAI-compatible endpoint:
{
"model": "my-model",
"tools": [ /* same schema as before */ ],
"messages": [
{"role": "user", "content": "Search the web for the current prime minister of Italy."},
{"role": "assistant", "content": null, "tool_calls": [
{"id": "call_abc123", "type": "function",
"function": {"name": "web_search", "arguments": "{\"query\": \"prime minister of Italy\"}"}}
]},
{"role": "tool", "tool_call_id": "call_abc123", "content": "Giorgia Meloni is the Prime Minister of Italy."}
]
}
Tool-result messages count as grounding context
A role: "tool" message is treated the same way as context for the halluc_context axis — the final answer is scored for faithfulness against what the tool actually returned, not just against the user's original question.
Ollama format — POST /api/chat¶
The same pipeline in Ollama's wire format. Every Geodesia extension field above works identically.
curl -s http://localhost:8080/gw/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2",
"stream": false,
"messages": [{"role": "user", "content": "What is the capital of France?"}],
"context": "",
"mode": "block"
}' | jq '{answer: .message.content, decision: .geodesia.decision, reason: .geodesia.reason}'
const r = await fetch("http://localhost:8080/gw/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3.2",
stream: false,
messages: [{ role: "user", content: "What is the capital of France?" }],
mode: "block",
}),
}).then(r => r.json())
console.log(r.message.content, r.geodesia.decision)
What comes back
{
"model": "llama3.2",
"message": { "role": "assistant", "content": "The capital of France is Paris." },
"done": true,
"geodesia": {
"schema_version": "1.0",
"event": "final",
"decision": "allowed",
"mode": "blocking",
"reason": null,
"axes": { "…": "…" }
}
}
The geodesia object is identical to the OpenAI format. When streaming (NDJSON), the same input_scan / progress / final objects ride on the Ollama frames.
Response reference¶
The proxy adds a single top-level key, geodesia (schema version 1.0), to the standard OpenAI or Ollama body. Its main fields:
| Field | When present | Description |
|---|---|---|
schema_version | always | "1.0". Always the first key. Changes only when the shape of the object changes. |
event | always | final on a non-streaming response; input_scan, progress, research or final on streaming chunks. |
decision | final | allowed, flagged (violation detected, content delivered — passthrough mode, or found after the stream ended) or blocked (content withheld, finish_reason: "content_filter"). |
mode | final | The enforcement mode actually applied: blocking or passthrough. |
reason | final | null when allowed; otherwise {stage, axis, detail} — stage is input, output, tools or quota, axis the axis that decided. |
axes | when scored | Primary axes, each {score, threshold, flagged, available, role, …}. available: false (with score: null and an unavailable_reason) means the axis could not be scored on this turn — most often halluc_closedbook against an upstream with no log-probabilities. |
additional_axes | when scored | Annotation-only axes (profanity, out_of_scope, prompt_complexity), same shape. They never block. |
grounding | when scored | One fused grounding number across halluc_context and halluc_closedbook. See Grounding Score. |
thinking | thinking level ≥ 1 | {level, tiers_used, escalated} — the level this turn actually ran at and the detector tiers that contributed. See Thinking Levels. |
pii | PII guard on | {enabled, input: {count, by_type}, output: {count, by_type}} — counts only. |
routing | complexity routing on | {enabled, used_complex_model, axis, score, threshold, model} — see Token & Cost Control. |
token_saving | token_saving switch on | {enabled, cached_prompt_tokens?, prompt_tokens?, cache_write_prompt_tokens?, prompt_cache_key?, upstream_closed_early?, closed_reason?} — see Token & Cost Control. |
rag | RAG active | {collection_id, sources, source_count, verification}. |
context_judge | claim judge on | Per-claim grounding verdicts (advisory, uncalibrated): {available, calibrated, score, threshold, flagged, verdict_counts, claims, …}. See Context judge. |
certificate | GW_CERTIFICATE=on | Signed decision certificate (geodesia-cert-3) built from this response's axes. Its verdict is the policy verdict, independent of mode: a passthrough turn can carry decision: "flagged" with certificate.verdict: "blocked". See Certificate. |
Optional sections are omitted when they do not apply. Full field-by-field treatment, including reasoning_budget, quota, tool_guard and every axis details field: API Response Format.
More examples¶
Grounded request — hallucination scored against your own text¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"stream": false,
"context": "The Eiffel Tower was built between 1887 and 1889 and stands 330 metres tall.",
"messages": [{"role":"user","content":"How tall is the Eiffel Tower?"}]
}' | jq '{answer: .choices[0].message.content, halluc: .geodesia.axes.halluc_context, grounding: .geodesia.grounding}'
Passthrough — see what the model would have said¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"stream": false,
"mode": "passthrough",
"messages": [{"role":"user","content":"How do I pick a lock?"}]
}' | jq '{answer: .choices[0].message.content, decision: .geodesia.decision, reason: .geodesia.reason}'
Loosen two axes for one request¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"stream": false,
"threshold_overrides": {"prompt_safety": 0.70, "jailbreak": 0.55},
"messages": [{"role":"user","content":"Explain a penetration test methodology."}]
}'
Answer from a knowledge base¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-H "X-Geodesia-App: support_bot" \
-d '{
"model": "my-model",
"stream": false,
"rag": {"collection_id": "my-policy-docs", "top_k": 5, "verify": true},
"messages": [{"role":"user","content":"What is our refund policy?"}]
}' | jq '{answer: .choices[0].message.content, sources: [.geodesia.rag.sources[].title]}'
Route by Application API key¶
curl -s http://localhost:8080/gw/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer g1k_live_9c1f2a7b4e0d" \
-d '{"model":"my-model","stream":false,
"messages":[{"role":"user","content":"Hello"}]}'
No application_id needed — the key identifies the Application, and its policy, thresholds, cost centre and upstream binding all apply.