blob: b44ee5b10d650e576998ff996f98d7412624c543 [file] [view]
# Proposals for llm.apache.org
llmao is an Apache project, [apache/tooling-llmao](https://github.com/apache/tooling-llmao),
and what follows is meant to go to it as pull requests, as
[its guide to pointing a harness at the gateway](https://github.com/apache/tooling-llmao/blob/main/docs/llmao-harnesses.md)
already did. Each proposal starts from what goes wrong without it, and from what the
Foundation gets, not from a setting: the settings are the easy part, and the motivation
is what a reviewer weighs. Where an open standard covers the need, the proposal names
the standard, so the gateway's surface stays llmao's own and LiteLLM stays an
implementation behind it.
Written 11 October 2026, from building this reference implementation against the gateway
and from llmao at `266122c`. What the gateway answered that day, with a PAT:
| Route | Answer |
|---|---|
| `/v1/models` | `gemma4-26b`, `qwen3-8b`, `qwen3.8-27b`; `qwen3-8b` is listed but often answers that it does not exist |
| `/v1beta/models/<model>:generateContent` | 200: the Gemini route is deployed, and Gemini CLI and Antigravity run on it |
| `/v1beta/models` | 404: the Gemini-shaped model list is not routed |
| `/v1/mcp/server` | 200 with an empty list: the proxy's MCP registry is reachable, with no servers on it yet |
Each proposal ends with potential implementation details, naming the proxy's own
setting where one exists. The setting names are from LiteLLM's documentation and should
be checked against the version the gateway runs, which llmao reads from the proxy's
readiness endpoint.
What llmao already has, and these proposals build on: Apache identity on every key,
keys per project, user and purpose, budgets per project, `drop_params`, and a health
roll-up per model on its own pages.
## 1. One trace from a task to a model's answer
**Why.** When an agent's run goes wrong, or costs more than it should, the question is
always which task, which session, which model call, which tool call. Today the answer
stops at the edge of each system: our launcher traces a session up to the request it
sends, the gateway records spend per key, and nothing joins the two. Cost per task,
the audit of what an agent decided, and debugging a failed run across our wrapper, the
gateway and a tool server all need one trace. For a fleet of agents working around the
clock on any task they can claim, this is how anyone finds out what happened.
**Standard.** W3C Trace Context (`traceparent`), W3C Baggage for the session and task,
and OpenTelemetry's semantic conventions for generative AI for the attributes: model,
tokens, finish reason, `gen_ai.conversation.id`.
**Proposal.** The gateway continues a trace it is handed and exports its own spans under
its own service name, `llm.apache.org`, with a request, a model call and, once tool
servers are on it, a tool call. Its collector keeps the proxy's internal spans out, so
what a caller sees is the gateway's contract. Each response carries the gateway's request
id in a header, so a caller can join the two by id even without a shared trace backend. A
shared backend where a PAT's owner can query their own traces can come later.
**Ours.** The launcher's egress sends `traceparent` and `baggage` on every request and
records the gateway's request id on its span.
**Potential implementation details.**
- LiteLLM's OpenTelemetry callback, `litellm_settings: callbacks: ["otel"]`, configured
by `OTEL_EXPORTER`, `OTEL_ENDPOINT` and `OTEL_HEADERS`, with `OTEL_SERVICE_NAME` set to
`llm.apache.org`. It continues an incoming `traceparent`, so the gateway's spans join
the caller's trace without code in llmao.
- An OpenTelemetry Collector between the proxy and the backend, with a `filter` or
`transform` processor that drops or renames the proxy's internal spans and strips
prompt and response attributes, so what leaves is llmao's contract.
- The request id the proxy already returns, `x-litellm-call-id`, documented as llmao's
request id; llmao can rename it at its vhost if the name should stay hidden.
## 2. Sessions as a caller the gateway can tell apart
**Why.** A key is per project, user and purpose. A fleet of agents runs on one
automation key, and to the gateway every session on it looks the same: one session cannot
be limited, revoked or billed apart from the rest, and a runaway agent can spend the
project's budget before anyone notices. The same gap blocks a hosted memory server, which
must know which session is asking, since every read is checked against what that session
has already read and the ceiling of where its model calls go.
**Standard.** Baggage for a session id now; OAuth 2.0 Token Exchange (RFC 8693) for a
session-scoped token later.
**Proposal.** In two steps.
1. A session id in baggage, recorded on the spend row and the span, scoped to the key
that sent it, so a guessed id can only confuse its owner's own sessions. Limits per
key beside the budget per project, so an automation key has a ceiling of its own.
2. A session token: the caller trades its PAT for a short-lived token bound to one
session, which can be revoked without rotating the PAT, and which a hosted tool or
memory server can trust. This is what a hosted memory server waits for.
**Ours.** The egress already holds the PAT so that a harness never sees it; it would set
the session id, strip any the harness sends, and later do the exchange.
**Potential implementation details.**
- The session id rides on the proxy's own fields: `litellm_session_id` in the request,
which its spend logs and UI group by, and `tags`, or the `x-litellm-tags` header, for
the task. A small pre-call hook in llmao maps W3C `baggage` onto them, so callers
speak the standard and never the proxy's names.
- Limits per key are parameters of the proxy's key API that llmao already calls to
issue PATs: `max_budget` with `budget_duration`, and `tpm_limit`, `rpm_limit` and
`max_parallel_requests`. Automation keys would get them by default.
- The session token is llmao code over the same key API: on a token exchange, llmao
checks the PAT and mints a virtual key with a short `duration`, the PAT's team, the
session in its `metadata`, and its own limits; revoking it is a key delete. No new
store, and the proxy enforces it like any key.
## 3. A model list that tells the truth
**Why.** A harness discovers models from the list and fails on the first call to one
that does not answer. `qwen3-8b` has been listed and often unanswering since at least 5 October, and every
caller pays a failed run to find out. Drift detection, which watches whether the model
behind a name has changed, cannot tell a deliberate release from a silent swap while
only aliases are listed.
**Proposal.** The API's model list follows the health roll-up llmao already shows on its
pages. Each alias also names the version it points at, a versioned name or a field, so a
change of version is visible. The Gemini-shaped list at `/v1beta/models` is routed like
the rest. Logprobs pass through where vLLM returns them: they are the most sensitive
drift signal there is, and nothing else shows a model changing before its answers do.
**Potential implementation details.**
- llmao's model status already decides "in service"; the same loop can remove a
deployment that fails its probes from the proxy, with `/model/delete`, and push it
back with `/model/new` when it recovers, which llmao already uses. The proxy's
`general_settings: background_health_checks` with `health_check_interval`, and the
router's `allowed_fails` and `cooldown_time`, keep a failing deployment out of
routing in between, but not out of the list.
- Versions in the `model_info` llmao sends with `/model/new`, such as `version` and
`base_model`, which the proxy serves on `/v1/model/info`; or versioned model names with
the alias kept as a second name.
- `logprobs` and `top_logprobs` are OpenAI parameters the proxy passes to `hosted_vllm`
and vLLM honours; the check is that `drop_params` does not drop them for these
deployments.
- The Gemini-shaped list is a small route in llmao or its vhost that answers
`GET /v1beta/models` from `/v1/models`, in Gemini's shape.
## 4. Compatibility for harnesses, fixed once in the gateway
**Why.** The harnesses are written for their vendors' APIs, and four differences break
them on the gateway's models. Our egress rewrites each one, as llmao's harness guide
records, and so does anyone else who gets it working, or they give up. Fixed in the
gateway, every harness works by hand, and every Apache contributor's first hour on it is
spent working rather than debugging.
| Difference | Harness | What breaks |
|---|---|---|
| a system message after the conversation | Claude Code | Qwen's chat template refuses the request |
| a Gemini tool result under the model's role | Gemini CLI, Antigravity | the proxy's Gemini translation drops it, and the model answers without its tool's result |
| an output limit of 128k tokens | Muse Code | a 131k-context model refuses once the prompt is counted |
| a reasoning effort of `high` | Claude Code | vLLM takes low, medium and xhigh; the model supports the parameter, so `drop_params` passes it on and the value is refused |
**Proposal.** The gateway normalises each, in a pre-call hook: fold the system message,
move the tool result to the user's role, cap output at the model's room, map effort to
the nearest level the model takes. Each is a few lines with a test, and the upstream fix
where there is one: the Gemini translation's issue was closed by a stale bot, unfixed.
**Potential implementation details.**
- One `CustomLogger` in llmao with an `async_pre_call_hook`, registered under
`litellm_settings: callbacks`, that rewrites the request before it is routed: fold a
late system message into the first, move a Gemini tool result to the user's role, clamp
`max_tokens` to the model's room, and map `reasoning_effort` to the nearest level the
model takes. Each is a few lines with a test.
- `litellm_settings: modify_params: true` already fixes some message shapes for some
providers; whether it covers these four on `hosted_vllm` is worth one test before the
hook is written.
- The Gemini translation bug is also worth an upstream fix, since the hook only works
around it.
## 5. Tool servers: what the registry needs to hold
**Why.** The proxy can manage MCP servers itself, so `llm.apache.org` can list the tool
servers a key may reach, each running on its own host. Two things from building this make that more pressing.
The vendors' own tools do not cross the gateway. Codex's `web_search` runs inside OpenAI's
API and Claude Code's web search inside Anthropic's. On the gateway's open models they
silently do nothing: in the decision contexts our egress recorded up to 11 October, Codex
offered `web_search` on all 56 requests to the gateway and not one search came back. A
search server and a fetch server in the registry close that gap for every harness at once.
And a tool server is an injection surface. A tool's description is read by the model as
instructions, and what a tool returns, a mail thread or a web page, is written by anyone.
**Proposal.** For each server in the registry:
- **Grants per key and per tool,** private by default, which a caller
can narrow further per session.
- **Pinned tool definitions:** a digest of the tool list and descriptions, versioned, so
a changed description is visible before a model reads it.
- **A label on what a tool returns,** a classification such as public, internal or
restricted, so a caller raises the session's label on the result and its ceiling holds.
- **Acting as the person:** a tool that changes something acts as the key's owner with
their scopes, never a shared account, and declares which of its tools are irreversible.
- **The same trace,** session and limits as model calls, from proposals 1 and 2.
**Ours.** The egress gets a route per registered server, `/mcp/<name>`, with the PAT
injected so the harness never holds it. A tool call through it is then observed by the
egress, logged with its policy decision, and labelled, where today a harness's tool calls
reach the record only through its own transcript, a self-report. What a tool returns is
data, never instructions: the memory server's write scan applies before any of it is
written to the record.
**Potential implementation details.**
- Servers registered in the proxy's MCP gateway, in its `mcp_servers` configuration or
through its API, each pointing at its own host.
- Grants through the proxy's object permissions on keys and teams, which name the MCP
servers a key may reach; llmao sets them as it issues keys, private by default. Where
the deployed version cannot narrow to single tools, llmao's seam filters the tool
list.
- Pinned definitions are llmao code: at registration it records a digest of the
server's tool list and descriptions, and a server whose list changes is taken out of
the registry until a person approves the new digest.
- The label is registry metadata llmao keeps beside each server and returns with its
results in a header, for a caller's egress to read.
- Acting as the person: the server receives the caller's identity from llmao, never a
shared credential, and holds per-person tokens itself; how the proxy forwards a
caller's credential to a server is to be checked against the deployed version.
The memory server itself stays beside the harness, per session, until proposal 2's
session token exists; before then, a hosted read-only search over public entries is the
safe first step.
## Where things stand, 12 October 2026
A reviewer at llmao will ask two things before any of the above: whether LiteLLM, the
proxy behind the gateway, already offers memory the Foundation could use instead of
this design, and what llmao itself has done since these proposals were written. Both,
as of 12 October.
### LiteLLM's own memory, and why it is not the record
LiteLLM ships one feature it calls memory, and it is a key-value store, not a record.
- **`/v1/memory`** stores a text value, with optional JSON metadata, under a globally
unique key, in a Postgres table, scoped to the caller's user and team: create, read,
update, delete, and list by key prefix
([docs](https://docs.litellm.ai/docs/proxy/memory),
[PR #26218](https://github.com/BerriAI/litellm/pull/26218), merged 25 April 2026, in
the open-source proxy since about v1.83). Nothing reads it while a model call runs:
remembering and recalling are the client's job. It was on a draft deprecation list in
July and kept after the community objected
([discussion #32090](https://github.com/BerriAI/litellm/discussions/32090)). The
gateway runs LiteLLM 1.102.2 (llmao `4493935`, 8 October), so the route exists; with
an ordinary PAT it answers 401, "Only proxy admin can be used…", so no user can reach
it today.
- **Memory the gateway would keep by itself** was built and dropped. Two PRs by a
BerriAI engineer had the gateway inject save, search and read tools into requests and
run hidden tool rounds, with saving and recall switched on per user
([#40844](https://github.com/BerriAI/litellm/pull/40844), closed 12 September;
[#40894](https://github.com/BerriAI/litellm/pull/40894), closed 4 October without
comment). Neither was merged and nothing has replaced them.
- **Near it, and not memory**: Responses API sessions, which rebuild a conversation from
the proxy's spend logs and S3 when `previous_response_id` is given
([docs](https://docs.litellm.ai/docs/response_api)); vector stores, whose data stays
at the provider, the managed kind Enterprise-licensed
([docs](https://docs.litellm.ai/docs/completion/knowledgebase)); prompt management,
which is templates ([docs](https://docs.litellm.ai/docs/proxy/prompt_management));
semantic caching, which LiteLLM's own docs warn "goes badly wrong on agentic traffic"
([docs](https://docs.litellm.ai/docs/proxy/caching_semantic)); and Anthropic's memory
tool, passed through for the client to execute. The A2A and MCP gateways keep no
memory of their own.
None of this is the record the paper describes. `/v1/memory` has no trust tiers, no
labels or ceilings, no provenance, no search beyond a key prefix, no erasure with a
receipt, and no domain to partition by. And the dropped gateway memory went the
opposite way from this design: memory written and recalled out of sight of the harness
is the poisoning surface the paper's gates exist for, where here every read and write
is a tool call the harness makes and the policy decides, with its provenance kept. If
that work comes back upstream, llmao should leave it switched off.
What LiteLLM does offer this design is its MCP registry, with an approval step and a
cost per tool call: the place, under proposal 5, where the memory server and the taps
would be registered as tool servers. Its Anthropic memory-tool pass-through is a second,
smaller opening: the tool's commands could be served by the memory server, so a harness
that speaks only that tool still writes to the record.
### llmao since these proposals
These proposals were written against llmao at `266122c` (10 October). Since then:
| Proposal | In llmao |
|---|---|
| 1. One trace from a task to a model's answer | Nothing yet. `76a6899` (12 October) times each LiteLLM admin request and each page handler, to find why My Keys is slow: llmao's own pages, not model calls. Nothing in the code reads or sends `traceparent`. |
| 2. Sessions as a caller the gateway can tell apart | Nothing yet. |
| 3. A model list that tells the truth | On the human side only. The Models page was redesigned from [issue #46](https://github.com/apache/tooling-llmao/issues/46): status per deployment rather than per model, privacy computed from every configured deployment (Private, External or Mixed, a down one included), and whether a model thinks by default (`266122c`, `689c4ae`, `45da9c2`, `957d607`). The `/v1/models` list harnesses read is unchanged. |
| 4. Compatibility for harnesses | By documentation rather than in the gateway. The guide to pointing a harness at the gateway by hand (`811d775`, 9 October, contributed from this work) says what this launcher's egress fixes that a manual setup does not, the system-message fold among them; llmao's help now has a page per harness, [PR #51](https://github.com/apache/tooling-llmao/pull/51) (open) tightens the Claude Code setup, and [issue #44](https://github.com/apache/tooling-llmao/issues/44) asks for configuration snippets to copy. The fixes themselves are not in the gateway. |
| 5. Tool servers | [Issue #25](https://github.com/apache/tooling-llmao/issues/25), "MCP server registration through llmao" (open since 8 September, no replies), proposes LiteLLM's MCP registry with its review workflow, project access groups and per-tool cost: the place proposal 5 builds on. |
Beside them, [issue #43](https://github.com/apache/tooling-llmao/issues/43) (open)
considers metrics in Prometheus or OpenMetrics form for Infra's Datadog. The numbers it
would publish per key and model, calls, tokens and time, are the ones this launcher's
egress now keeps per session from the other side (`amem usage`, `amem cost`), which is
the argument for proposals 1 and 2: with a trace and a session on each call, the two
sets of numbers join. Everything else in llmao since 10 October is the vLLM hosting
fleet, the help pages and the navigation.