package adminapi import ( "net/http" llmgw "gitea.mynodes.xyz/ilker/llmgw" "gitea.mynodes.xyz/ilker/llmgw/internal/bench" "gitea.mynodes.xyz/ilker/llmgw/internal/bench/eval" "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" "gitea.mynodes.xyz/ilker/llmgw/internal/control/fabrics" "gitea.mynodes.xyz/ilker/llmgw/internal/control/federation" "gitea.mynodes.xyz/ilker/llmgw/internal/control/modelserve" "gitea.mynodes.xyz/ilker/llmgw/internal/control/nvlink" "gitea.mynodes.xyz/ilker/llmgw/internal/guard" "gitea.mynodes.xyz/ilker/llmgw/internal/notify" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // Route is one endpoint as the OpenAPI document describes it. // // A table written by hand beside Register rather than read out of the // handlers, because what a handler decodes and answers is not visible from // outside it. The table is checked against Register by a test, so an endpoint // cannot be added without being described; the bodies are Go values whose // types the generator reads, so a field added to one is in the document the // next time it is generated. type Route struct { Method string Path string // Summary is one line; Description, when there is more to say, the rest. Summary string Description string // Query is the query parameters the handler reads. Query []Param // Request is a value of the body's type; nil reads none. Request any // OptionalBody is set when the body may be left out. OptionalBody bool // Response is a value of the type a success answers with. Response any // Status is the success status; zero is 200. Status int // CSV is set when the answer is CSV unless JSON is asked for. CSV bool // Form is a request sent as multipart/form-data — a file uploaded — // whose fields are Request's. Form bool // Stream is an answer that may come as server-sent events instead. Stream bool // Binary is the type of an answer that is a file, not JSON. Binary string } // Param is a query parameter. type Param struct { Name string Description string // Type is a JSON Schema type: string when empty, or integer, number, // boolean. Type string Required bool } // PathParams describes the parameters in Routes' paths, by the segment // before them and their name. var PathParams = map[string]string{ "models/{name}": "The model's name in the catalog.", "presets/{name}": "The preset's name, as `llmgw ctl model presets` lists it.", "nodes/{id}": "The node's ID, as `llmgw ctl nodes` lists it.", "jobs/{id}": "The job's ID, as starting it answered.", "runs/{id}": "The quality run's ID, as starting it answered.", "requests/{id}": "The request's ID: the x-request-id a client was answered with.", "client-jobs/{id}": "The job's ID, job_….", "fabrics/{id}": "The physical fabric's ID, as GET /api/v1/fabrics lists it.", "quotas/{scope}": "The scope: org:NAME, team:ORG/TEAM, project:ORG/TEAM/PROJECT, or their IDs after the colon.", "federation/{peer}": "The federation peer's name, as `llmgw ctl federation list` lists it.", "holds/{domain}": "The domain as level:name, rack:r12-07, as `llmgw ctl facility` lists it.", "collections/{id}": "The collection's ID, its name when that is unique, or ORG/NAME.", "orgs/{id}": "The organization's ID or name.", "teams/{id}": "The team's ID.", "projects/{id}": "The project's ID.", "keys/{id}": "The key's ID, as listed (not the key itself).", "users/{id}": "The person's ID or email.", "artifacts/{kind}": "What the job made: peft, a gzipped tar of the PEFT adapter, or gguf, the adapter for llama.cpp.", } // Routes is every endpoint Register mounts. var Routes = []Route{ // The document itself. {Method: "GET", Path: "/api/v1/openapi.json", Summary: "This document", Description: "The admin API in OpenAPI 3.1, generated from the route table and built into the binary.", Response: map[string]any{}}, // The fleet. {Method: "GET", Path: "/api/v1/summary", Summary: "The whole fleet in one answer", Response: Summary{}}, {Method: "GET", Path: "/api/v1/nodes", Summary: "Every node, with its cards, load and fabrics", Response: []NodeView{}}, {Method: "POST", Path: "/api/v1/nodes/{id}/state", Summary: "Cordon, drain or ready a node", Request: NodeStateRequest{}, Response: NodeStateView{}}, {Method: "POST", Path: "/api/v1/nodes/{id}/upgrade", Summary: "Have a node run a release", Request: UpgradeRequest{}, Response: UpgradeView{}}, {Method: "POST", Path: "/api/v1/nodes/{id}/engines", Summary: "Choose an engine's build for a node", Description: "The node installs the build chosen. Build is auto, a build such as cu129, or none.", Request: NodeEngineRequest{}, Response: NodeEngineRequest{}}, {Method: "POST", Path: "/api/v1/nodes/{id}/labels", Summary: "Set or remove labels on a node, over its agent's own", Description: "Kept in the store. llmgw.io/site, room, row, rack and pdu say where the node is, for facility-aware placement.", Request: NodeLabelsRequest{}, Response: NodeLabelsView{}}, {Method: "GET", Path: "/api/v1/facility", Summary: "The failure domains, their power against their caps, and what is held", Response: FacilityView{}}, {Method: "POST", Path: "/api/v1/facility/holds", Summary: "Hold a domain for maintenance: nothing new is placed in it", Description: "With drain, each of its nodes is drained too, one replica at a time with the replacement up first.", Request: HoldRequest{}, Response: HoldView{}}, {Method: "DELETE", Path: "/api/v1/facility/holds/{domain}", Summary: "Release a held domain", Description: "Nodes a draining hold drained are made ready again.", Response: HoldView{}}, {Method: "DELETE", Path: "/api/v1/nodes/{id}", Summary: "Forget a node that has gone", Description: "Only a node that is lost, or unreachable with nothing on it; its certificate is revoked.", Response: NodeRemoved{}}, {Method: "GET", Path: "/api/v1/nodes/{id}/logs", Summary: "The last lines of a node's logs", Query: []Param{ {Name: "lines", Type: "integer", Description: "How many lines; 300 when absent, at most 2000."}, {Name: "source", Description: "Which log: agent when absent, or an engine the node lists in sources."}, }, Response: LogsView{}}, {Method: "GET", Path: "/api/v1/fabrics", Summary: "The physical fabrics and the names nodes give them", Response: fabrics.Report{}}, {Method: "GET", Path: "/api/v1/fabrics/{id}/topology", Summary: "One fabric's map, with the cluster's adapters live on it", Response: TopologyView{}}, {Method: "GET", Path: "/api/v1/nvlink", Summary: "The multi-node NVLink domains and what keeps nodes out of them", Response: nvlink.Report{}}, {Method: "POST", Path: "/api/v1/fabric-test", Summary: "Measure bandwidth between two nodes, rail by rail", Request: FabricTestRequest{}, Response: FabricTestView{}}, {Method: "GET", Path: "/api/v1/weights/orphans", Summary: "Weights on nodes that no model uses", Response: []OrphanView{}}, {Method: "POST", Path: "/api/v1/weights/collect", Summary: "Delete weights that no model uses", Request: CollectRequest{}, OptionalBody: true, Response: []CollectResult{}}, // The catalog. {Method: "GET", Path: "/api/v1/presets", Summary: "The models that come with llmgw: whether each fits this cluster, is in the catalog, was declined", Response: PresetsView{}}, {Method: "POST", Path: "/api/v1/presets/{name}", Summary: "Add a preset to the catalog, and clear its decline", Response: ModelAdded{}}, {Method: "PUT", Path: "/api/v1/presets/auto", Summary: "Turn on or off the adding of presets when a node fits them", Request: PresetsAutoRequest{}, Response: PresetsView{}}, {Method: "GET", Path: "/api/v1/models", Summary: "Every model, with its replicas and state", Response: []ModelView{}}, {Method: "POST", Path: "/api/v1/models", Summary: "Add a model to the catalog", Description: "Kept in the store across restarts. Refused when the name is taken, a model in " + "configuration has it, or the definition would make the catalog invalid.", Request: catalog.Model{}, Response: ModelAdded{}}, {Method: "GET", Path: "/api/v1/models/{name}", Summary: "One model's definition, as it is in force", Description: "With the defaults the catalog filled in. By its name only, not an alias.", Response: catalog.Model{}}, {Method: "DELETE", Path: "/api/v1/models/{name}", Summary: "Remove a model added at runtime", Description: "A model declared in the cluster's settings is refused: it is changed there.", Response: Removed{}}, {Method: "POST", Path: "/api/v1/models/{name}/scale", Summary: "Set how many replicas a model runs", Request: ScaleRequest{}, Response: ScaleResponse{}}, {Method: "POST", Path: "/api/v1/models/{name}/resources", Summary: "Set what a model's profiles reserve of a node", Request: ResourcesRequest{}, Response: catalog.Model{}}, {Method: "POST", Path: "/api/v1/models/{name}/class", Summary: "Set a model's capacity class", Request: ClassRequest{}, Response: ClassView{}}, {Method: "POST", Path: "/api/v1/models/{name}/owner", Summary: "Set the part of the tenancy tree a model's capacity counts against", Request: OwnerRequest{}, Response: OwnerView{}}, {Method: "POST", Path: "/api/v1/models/{name}/adapters", Summary: "Replace a model's LoRA adapters", Description: "Each new file is read first, to check it is an adapter for the model's architecture.", Request: AdaptersRequest{}, Response: AdaptersRequest{}}, {Method: "POST", Path: "/api/v1/models/{name}/retry", Summary: "Try a parked model again", Response: RetryView{}}, {Method: "GET", Path: "/api/v1/models/{name}/target", Summary: "How many replicas a throughput needs, and whether they fit", Query: []Param{{Name: "tokens_per_sec", Type: "number", Required: true, Description: "The output tokens a second wanted of the model."}}, Response: TargetView{}}, {Method: "GET", Path: "/api/v1/models/{name}/headroom", Summary: "How many more replicas of a model fit now, and where", Query: []Param{{Name: "limit", Type: "integer", Description: "The most to try placing; 16 when absent or past 64."}}, Response: HeadroomView{}}, {Method: "PUT", Path: "/api/v1/models/{name}/capacity", Summary: "Record what a replica was measured to serve", Description: "Replaces an earlier measurement of the same model on the same profile.", Request: bench.Capacity{}, Response: bench.Capacity{}}, {Method: "GET", Path: "/api/v1/capacity", Summary: "Every recorded measurement", Response: []CapacityView{}}, {Method: "GET", Path: "/api/v1/replicas", Summary: "Every replica, with its shards", Query: []Param{{Name: "model", Description: "Only this model's replicas."}}, Response: []ReplicaView{}}, {Method: "GET", Path: "/api/v1/explain-placement", Summary: "Why a model could not be placed", Query: []Param{{Name: "model", Required: true, Description: "The model's name or an alias."}}, Response: ExplainView{}}, {Method: "POST", Path: "/api/v1/restart", Summary: "Restart replicas, every replica of some models, or all", Request: RestartRequest{}, Response: RestartResult{}, Status: http.StatusAccepted}, // Serving from the registry, and newer weights. {Method: "GET", Path: "/api/v1/plan", Summary: "How a registry repository would be served here", Query: []Param{ {Name: "repo", Required: true, Description: "The repository, owner/name."}, {Name: "format", Description: "gguf or safetensors; gguf when absent."}, {Name: "context", Type: "integer", Description: "The context length to plan for, in tokens."}, }, Response: modelserve.Plan{}}, {Method: "POST", Path: "/api/v1/models/serve", Summary: "Fetch a registry repository and serve it", Request: modelserve.Request{}, Response: modelserve.Job{}, Status: http.StatusAccepted}, {Method: "GET", Path: "/api/v1/jobs", Summary: "Every job since the control plane started", Response: []modelserve.Job{}}, {Method: "GET", Path: "/api/v1/jobs/{id}", Summary: "One job", Response: modelserve.Job{}}, {Method: "GET", Path: "/api/v1/updates", Summary: "Newer weights the registry has for models", Response: []modelserve.UpdateInfo{}}, {Method: "POST", Path: "/api/v1/updates/check", Summary: "Ask the registry now, about one model or every one", Request: CheckRequest{}, OptionalBody: true, Response: []modelserve.UpdateInfo{}}, {Method: "POST", Path: "/api/v1/models/{name}/update", Summary: "Move a model to its repository's newer weights", Description: "With a quality suite, or the model's standing gate, the new weights are asked it first as a hidden canary.", Request: modelserve.Change{}, OptionalBody: true, Response: modelserve.Job{}, Status: http.StatusAccepted}, {Method: "POST", Path: "/api/v1/models/{name}/rollback", Summary: "Go back to the weights the last update replaced", Response: Result{}}, {Method: "POST", Path: "/api/v1/models/{name}/origin", Summary: "Record the repository a model's weights came from", Request: OriginRequest{}, Response: OriginView{}}, {Method: "GET", Path: "/api/v1/canaries", Summary: "Every canary, and how its two sides serve", Response: []modelserve.CanaryView{}}, {Method: "POST", Path: "/api/v1/models/{name}/canary", Summary: "Start a canary of a model's newer weights", Request: catalog.Canary{}, Response: modelserve.Job{}, Status: http.StatusAccepted}, {Method: "PUT", Path: "/api/v1/models/{name}/canary", Summary: "Change the share of requests a canary takes", Request: CanaryPercentRequest{}, Response: Result{}}, {Method: "DELETE", Path: "/api/v1/models/{name}/canary", Summary: "Take a canary out: its share first, then the candidate", Response: Result{}}, {Method: "POST", Path: "/api/v1/models/{name}/promote", Summary: "Make a canary's weights the model's", Description: "With a quality suite, or the model's standing gate, the canary is asked it first and kept below the score.", Request: modelserve.Change{}, OptionalBody: true, Response: modelserve.Job{}, Status: http.StatusAccepted}, // Quality suites. {Method: "GET", Path: "/api/v1/eval/suites", Summary: "The quality suites the control plane can run by name", Response: []eval.SuiteInfo{}}, {Method: "POST", Path: "/api/v1/models/{name}/eval", Summary: "Ask a model a quality suite", Description: "Started at once and answered with the run, to be followed at /api/v1/eval/runs/{id}.", Request: eval.Request{}, Response: eval.Live{}, Status: http.StatusAccepted}, {Method: "GET", Path: "/api/v1/eval/runs", Summary: "Quality runs, the newest first", Query: []Param{ {Name: "model", Description: "Only this model's runs."}, {Name: "limit", Type: "integer", Description: "How many; a sensible number when absent."}, }, Response: []eval.Live{}}, {Method: "GET", Path: "/api/v1/eval/runs/{id}", Summary: "One quality run, with each case", Response: eval.Live{}}, {Method: "PUT", Path: "/api/v1/models/{name}/eval-gate", Summary: "Hold a model's updates and promotions to a quality suite", Request: EvalGateRequest{}, Response: EvalGateView{}}, {Method: "DELETE", Path: "/api/v1/models/{name}/eval-gate", Summary: "Stop holding a model's updates to a suite", Response: EvalGateView{}}, // Federation: other llmgw clusters as providers. {Method: "GET", Path: "/api/v1/federation", Summary: "This cluster's id, and its federation peers with their health and models", Response: FederationView{}}, {Method: "POST", Path: "/api/v1/federation", Summary: "Add another llmgw cluster as a peer, or change one", Description: "The peer is checked first: its /ready, timed, and its /v1/models with the key. Its models join the catalog as PEER/MODEL.", Request: federation.AddRequest{}, Response: federation.Status{}, Status: http.StatusCreated}, {Method: "DELETE", Path: "/api/v1/federation/{peer}", Summary: "Remove a federation peer and its models", Description: "Refused while a model here falls back to one of the peer's.", Response: Removed{}}, {Method: "POST", Path: "/api/v1/federation/{peer}/sync", Summary: "Check a peer now, rather than at the next interval", Response: federation.Status{}}, {Method: "PUT", Path: "/api/v1/models/{name}/fallbacks", Summary: "Replace the models a model falls back to when it has no capacity", Description: "A model added at runtime only. A federation peer's model, PEER/MODEL, sends the overflow to the other cluster.", Request: FallbacksRequest{}, Response: FallbacksView{}}, // Tenancy: quotas and guardrails. {Method: "GET", Path: "/api/v1/quotas", Summary: "Every capacity quota, with what its scope holds now", Response: []QuotaView{}}, {Method: "PUT", Path: "/api/v1/quotas", Summary: "Set a scope's capacity quota", Description: "Replaces the scope's quota whole. The scope may be named by path or by ID; the answer names it by ID.", Request: QuotaRequest{}, Response: QuotaSet{}}, {Method: "DELETE", Path: "/api/v1/quotas/{scope}", Summary: "Remove a scope's capacity quota", Response: Removed{}}, {Method: "GET", Path: "/api/v1/guard", Summary: "The guardrail policies and where they are attached", Response: GuardView{}}, {Method: "GET", Path: "/api/v1/guard/events", Summary: "What the guardrails did: totals and the newest requests", Query: []Param{ {Name: "since", Description: "How far back: 90m, 24h, 7d; 24h when absent."}, }, Response: GuardEventsView{}}, {Method: "GET", Path: "/api/v1/guard/judges", Summary: "The judge models the policies ask, and what is wrong with each", Response: []JudgeView{}}, {Method: "POST", Path: "/api/v1/guard/judges/ask", Summary: "Have a judge read a text and say what it answered; nothing is kept", Request: JudgeAskRequest{}, Response: JudgeAskView{}}, {Method: "POST", Path: "/api/v1/guard/attachments", Summary: "Attach a guardrail policy", Request: GuardAttachmentRequest{}, Response: guard.Attachment{}}, {Method: "DELETE", Path: "/api/v1/guard/attachments", Summary: "Detach a guardrail policy", Request: GuardAttachmentRequest{}, Response: guard.Attachment{}}, // Knowledge bases: collections of documents, which people fill with // llmgw-kb from their own machines through the gateway. {Method: "GET", Path: "/api/v1/kb", Summary: "Every knowledge-base collection, with its documents' count and grants", Description: "Says why there are none when the store cannot hold them (SQLite, or PostgreSQL without pgvector).", Response: KBView{}}, {Method: "POST", Path: "/api/v1/kb/collections", Summary: "Make a collection, owned by an organization, team, project or person", Description: "The embedding model's dimension is asked of the model when dims is not given, so the model must be able to serve.", Request: KBCreateRequest{}, Response: KBCollectionView{}, Status: http.StatusCreated}, {Method: "DELETE", Path: "/api/v1/kb/collections/{id}", Summary: "Remove a collection with its documents and grants", Response: map[string]any{}}, {Method: "GET", Path: "/api/v1/kb/collections/{id}/documents", Summary: "A collection's documents", Response: []KBDocumentView{}}, {Method: "DELETE", Path: "/api/v1/kb/collections/{id}/documents", Summary: "Remove one document from a collection", Query: []Param{{Name: "name", Description: "The document's name, as listed.", Required: true}}, Response: map[string]any{}}, {Method: "POST", Path: "/api/v1/kb/grants", Summary: "Let a scope, and everything below it, read or write a collection", Description: "Never outside the collection's organization.", Request: KBGrantRequest{}, Response: KBGrantView{}}, {Method: "DELETE", Path: "/api/v1/kb/grants", Summary: "Take a grant away", Request: KBGrantRequest{}, Response: KBGrantView{}}, {Method: "POST", Path: "/api/v1/kb/defaults", Summary: "Set the collections a part of the tree answers from when a request names none", Description: "On an organization, team, project or key; the narrowest one with a default wins. No collections clears it. " + "A request's own X-Llmgw-Knowledge overrides it, and X-Llmgw-Knowledge: none turns it off.", Request: KBDefaultRequest{}, Response: KBDefaultView{}}, {Method: "POST", Path: "/api/v1/kb/collections/{id}/dataset", Summary: "Training data from a collection: questions and answers a chat model writes from each chunk", Description: "NDJSON, a line per pair as each chunk is read, or a line with error for a chunk that gave none. For people to correct before an adapter is trained on it.", Request: KBDatasetRequest{}, Response: KBDatasetLine{}}, {Method: "POST", Path: "/api/v1/kb/search", Summary: "Try a search of a collection, as an administrator", Description: "No tenancy applies: this is for checking what a collection holds and finds.", Request: KBSearchRequest{}, Response: []KBHitView{}}, // LoRA adapters trained on the nodes. // Organizations, teams, projects, keys and people. {Method: "GET", Path: "/api/v1/orgs", Summary: "The tenancy tree: every organization, its teams and their projects", Response: []OrgView{}}, {Method: "POST", Path: "/api/v1/orgs", Summary: "Add an organization", Request: TenancyCreateRequest{}, Response: TenancyItem{}, Status: http.StatusCreated}, {Method: "PATCH", Path: "/api/v1/orgs/{id}", Summary: "Rename an organization", Request: TenancyChangeRequest{}, Response: TenancyItem{}}, {Method: "DELETE", Path: "/api/v1/orgs/{id}", Summary: "Remove an organization that has no teams", Response: map[string]any{}}, {Method: "POST", Path: "/api/v1/teams", Summary: "Add a team to an organization", Request: TenancyCreateRequest{}, Response: TenancyItem{}, Status: http.StatusCreated}, {Method: "PATCH", Path: "/api/v1/teams/{id}", Summary: "Rename a team, or move it to another organization", Description: "Everything under it moves too, and from then on inherits the new organization's limits.", Request: TenancyChangeRequest{}, Response: TenancyItem{}}, {Method: "DELETE", Path: "/api/v1/teams/{id}", Summary: "Remove a team that has no projects", Response: map[string]any{}}, {Method: "POST", Path: "/api/v1/projects", Summary: "Add a project to a team", Request: TenancyCreateRequest{}, Response: TenancyItem{}, Status: http.StatusCreated}, {Method: "PATCH", Path: "/api/v1/projects/{id}", Summary: "Rename a project, or move it to another team", Request: TenancyChangeRequest{}, Response: TenancyItem{}}, {Method: "DELETE", Path: "/api/v1/projects/{id}", Summary: "Remove a project with no people, keys or collections", Description: "A project that ever had a key keeps it, revoked, because usage refers to it; rename such a project instead.", Response: map[string]any{}}, {Method: "GET", Path: "/api/v1/keys", Summary: "API keys, with their project and owner; never the keys themselves", Query: []Param{{Name: "project", Description: "Only this project's keys: ORG/TEAM/PROJECT or its ID."}}, Response: []KeyView{}}, {Method: "POST", Path: "/api/v1/keys", Summary: "Issue an API key", Description: "The answer holds the key, once: only its hash is kept. A lifetime longer than a scope above it allows is refused.", Request: KeyCreateRequest{}, Response: KeyCreated{}, Status: http.StatusCreated}, {Method: "GET", Path: "/api/v1/keys/{id}", Summary: "One API key", Response: KeyView{}}, {Method: "DELETE", Path: "/api/v1/keys/{id}", Summary: "Revoke an API key", Description: "A soft delete: usage refers to it. A gateway that has it cached accepts it until auth.cache_ttl.", Response: map[string]any{}}, {Method: "GET", Path: "/api/v1/users", Summary: "People, with where each is a member", Query: []Param{{Name: "state", Description: "Only people in this state: pending, active or disabled."}}, Response: []UserView{}}, {Method: "GET", Path: "/api/v1/users/{id}", Summary: "One person, with where they are a member", Response: UserView{}}, {Method: "POST", Path: "/api/v1/users/{id}/memberships", Summary: "Put a person in an organization, team or project", Description: "Already there, only the role changes.", Request: MembershipRequest{}, Response: UserView{}}, {Method: "DELETE", Path: "/api/v1/users/{id}/memberships", Summary: "Take a person out of an organization, team or project", Description: "Their keys in projects they no longer reach are revoked.", Request: MembershipRequest{}, Response: UserView{}}, {Method: "POST", Path: "/api/v1/train/jobs", Summary: "Queue a LoRA adapter's training on a node", Description: "The node trains when its agent is there, nothing else trains on it, and a card has gpu_memory_mib free; " + "the pairs are NDJSON of {\"q\", \"a\"}. An adapter learns how answers sound far better than what they say.", Request: TrainRequest{}, Response: TrainJobView{}, Status: http.StatusCreated}, {Method: "GET", Path: "/api/v1/train/jobs", Summary: "Training jobs, newest first", Query: []Param{{Name: "limit", Description: "How many; 50 when absent.", Type: "integer"}}, Response: []TrainJobView{}}, {Method: "GET", Path: "/api/v1/train/jobs/{id}", Summary: "One training job: how it goes, and what it made", Response: TrainJobView{}}, {Method: "POST", Path: "/api/v1/train/jobs/{id}/cancel", Summary: "Stop a training job", Response: TrainJobView{}}, {Method: "DELETE", Path: "/api/v1/train/jobs/{id}", Summary: "Remove an ended training job and what it made", Response: map[string]any{}}, {Method: "POST", Path: "/api/v1/train/jobs/{id}/publish", Summary: "Make what a finished job made the model's adapter", Description: "Kept on the mirror — the GGUF for a model llama.cpp serves, the PEFT files for one vLLM serves — and added to the model's adapters after the same check as POST /api/v1/models/{name}/adapters.", Response: TrainPublishView{}}, {Method: "GET", Path: "/api/v1/train/jobs/{id}/artifacts/{kind}", Summary: "What a training job made: peft (a gzipped tar) or gguf", Response: map[string]any{}}, {Method: "POST", Path: "/api/v1/agents/restart", Summary: "Have every node's agent start again; their engines keep serving", Response: AgentRestartView{}}, // The cluster's settings: everything in server.yaml that is not one // machine's own, the same on every control plane. {Method: "GET", Path: "/api/v1/settings", Summary: "The cluster's settings in force, or one earlier version", Query: []Param{{Name: "version", Description: "An earlier version; the newest when absent."}}, Response: SettingsView{}}, {Method: "GET", Path: "/api/v1/settings/history", Summary: "Versions of the cluster's settings, newest first", Query: []Param{{Name: "limit", Description: "How many; 20 when absent."}}, Response: []SettingsView{}}, {Method: "PUT", Path: "/api/v1/settings", Summary: "Change the cluster's settings: in force on every control plane within seconds", Request: SettingsRequest{}, Response: SettingsView{}}, {Method: "POST", Path: "/api/v1/settings/rollback", Summary: "Put an earlier version of the cluster's settings back in force", Request: SettingsRollbackRequest{}, Response: SettingsView{}}, // Usage, requests and the record of changes. {Method: "GET", Path: "/api/v1/usage", Summary: "What each group of the tenancy tree spent", Query: []Param{ {Name: "by", Description: "org, team or project; team when absent."}, {Name: "since", Description: "A duration back from now, like 24h; 24h when absent."}, }, Response: []ScopeUsageView{}}, {Method: "GET", Path: "/api/v1/usage/export", Summary: "Usage totals by period and dimension, as CSV or JSON", Query: []Param{ {Name: "from", Description: "A date (2026-09-01) or an RFC 3339 time; the first of this month when absent."}, {Name: "to", Description: "A date or an RFC 3339 time; now when absent."}, {Name: "period", Description: "hour, day or month; day when absent."}, {Name: "by", Description: "Dimensions, comma-separated: org, team, project, user, key, model."}, {Name: "format", Description: "json for JSON; CSV otherwise."}, }, Response: []store.ExportRow{}, CSV: true}, {Method: "GET", Path: "/api/v1/client-jobs", Summary: "Jobs clients asked for — 3D models — of every owner, newest first", Query: []Param{ {Name: "status", Description: "Only jobs in this status: queued, running, cancelling, completed, failed, cancelled, expired."}, {Name: "limit", Type: "integer", Description: "The most jobs to answer with; 50 when absent."}, }, Response: []JobView{}}, {Method: "GET", Path: "/api/v1/client-jobs/{id}", Summary: "One job a client asked for", Response: JobView{}}, {Method: "POST", Path: "/api/v1/client-jobs/{id}/cancel", Summary: "Stop a job, in the engine's queue too", Response: JobView{}}, {Method: "GET", Path: "/api/v1/requests/{id}", Summary: "What one request went through", Response: RequestView{}}, {Method: "GET", Path: "/api/v1/requests/{id}/body", Summary: "A kept request and its answer", Response: store.RequestBody{}}, {Method: "POST", Path: "/api/v1/requests/{id}/replay", Summary: "Send a kept request again, to any model", Request: ReplayRequest{}, Response: ReplayView{}}, {Method: "GET", Path: "/api/v1/audit", Summary: "Changes made, newest first", Query: []Param{ {Name: "since", Description: "How far back: 90m, 24h, 7d; 7d when absent."}, {Name: "limit", Type: "integer", Description: "The most events to answer with."}, {Name: "actor", Description: "Only changes by this actor."}, {Name: "contains", Description: "Only events whose action or target contains this."}, }, Response: []store.AuditEvent{}}, // Operations. {Method: "POST", Path: "/api/v1/notify/test", Summary: "Send a test notification to every webhook", Response: []notify.Result{}}, {Method: "POST", Path: "/api/v1/metrics/range", Summary: "Several Prometheus range queries at once", Description: "Read from the Prometheus monitoring.prometheus names; configured is false without one.", Request: MetricsRequest{}, Response: MetricsResult{}}, } // serveOpenAPI answers with the document generated from Routes, as it was // when this binary was built. func serveOpenAPI(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(llmgw.AdminOpenAPI) }