Daptin Plus an LLM Router Daptin published a walkthrough for running a self-hosted LLM router on its platform, letting operators connect multiple LLM providers and expose chosen models to users through the same accounts and groups used in their applications. The guide covers configuring an OpenRouter credential and exposing GPT as a model named `assistant`, adding Gemini as a fallback, streaming, image input, and per-account plans with request limits, token allowances and spending budgets, all managed through Daptin's resource APIs. The walkthrough runs Daptin from the `daptin/daptin:latest` Docker image on port 6336 with a SQLite database, and notes Postgres is preferred for production. Repository navigation Walkthrough LLM Router 1 revision https://github.com/daptin/daptin/wiki/Walkthrough-LLM-Router/ history Daptin handles users, permissions, data, and APIs. Now you can use it to run your own LLM router too. Connect different LLM providers, expose the models you want, and give your users access through the same accounts and groups they use in your application. Each account can have a plan with request limits, a token allowance, and a spending budget. Providers, models, routes, plans, and usage records are all available through Daptin's resource APIs, so you can inspect and change the setup as you go. We'll build this up one piece at a time: 1. Get Daptin running : Start a Docker container with persistent storage and check that the server is ready. 2. Set up authentication : Create the first account, sign in, claim administrator access, and get the token we'll use for configuration. 3. Connect our first model : Configure an OpenRouter credential, create a provider, and expose GPT through a model named assistant . This introduces the three resources behind routing: providers, models, and deployments. 4. Create a regular user : Create an user account and a group, grant that group permission to invoke assistant , and check that guests remain denied. We'll also revoke access and try the request again. 5. Add a fallback : Connect Gemini to the same public model name, then disable the primary deployment to see the fallback handle a request. 6. Try streaming : Perform the same request with and without streaming so you can see how the responses differ. 7. Save a conversation : Conversation continuation: retain a response and continue from its ID, with Daptin loading the earlier conversation for us. 8. Send an image to LLM : Enable image input and make a request containing both text and an image URL. 9. Configure budget for user : Create a plan, assign its membership to the account, configure model prices, and inspect the recorded usage. Also test exhausted a limit and check that Daptin rejects the next request. 10. Set operational limits : Configure capacity limits and per-IP HTTP rate protection, and explain how these differ from a user's plan. We will use curl in this walkthru but all of it can be configured using the built in web dashboard as well with actual responses and checks along the way. You can follow each configuration change top to bottom on your local daptin instance. Response examples show the fields relevant for that step. Expand Full response to inspect the complete response. JWTs are replaced with placeholders throughout; copy your own token from your sign-in response. Pull the daptin docker image docker pull daptin/daptin:latest Create a docker volume for persisting the sqlite db that we will use for the local setup prefer postgres for a production setup docker volume create daptin-router-data docker run -d \ --name daptin-router \ --restart unless-stopped \ -p 127.0.0.1:6336:8080 \ -v daptin-router-data:/var/lib/daptin \ -e DAPTIN DB TYPE=sqlite3 \ -e DAPTIN DB CONNECTION STRING=/var/lib/daptin/daptin.db \ -e DAPTIN DISABLE SMTP=true \ daptin/daptin:latest Docker logs should give you something like this INFO 2026-11-13 06:51:48 Our admin is No one INFO 2026-11-13 06:51:48 Listening at: :8080 Full startup log excerpt . truncated for brevity . . INFO 2026-11-13 06:51:48 /live/contact/:referenceId/content/yjs YJS websocket endpoint for contact content INFO 2026-11-13 06:51:48 Listening websocket server at ... /live WARN 2026-11-13 06:51:48 Failed to execute query 789: SELECT u . email FROM user account user account id has usergroup usergroup id AS uu LEFT JOIN user account AS u ON uu . user account id = u . id WHERE uu . usergroup id = ? ORDER BY uu . created at ASC LIMIT ? == 2 1 WARN 2026-11-13 06:51:48 Failed to scan user group id from the result 830: sql: no rows in result set INFO 2026-11-13 06:51:48 Our admin is No one INFO 2026-11-13 06:51:48 ProcessId=1 Listening at port: :8080 INFO 2026-11-13 06:51:48 198 No config value set for enable https : sql: no rows in result set INFO 2026-11-13 06:51:48 198 No config value set for http.read header timeout seconds : sql: no rows in result set INFO 2026-11-13 06:51:48 198 No config value set for http.idle timeout seconds : sql: no rows in result set INFO 2026-11-13 06:51:48 HTTPS is disabled INFO 2026-11-13 06:51:48 Listening at: :8080 You can call the /ready endpoint also for checking over api if the server is up and ready to handle requests. curl http://localhost:6336/ready Ready Signup and setup the administrator account first: curl http://localhost:6336/action/user account/signup \ -H 'Content-Type: application/json' \ -d '{ "attributes": { "name": "Administrator", "email": "admin@example.com", "password": "YOUR PASSWORD", "passwordConfirm": "YOUR PASSWORD" } }' The response to signup will be something like this { "ResponseType": "client.notify", "Attributes": { "message": "Sign-up successful. Redirecting to sign in", "type": "success" } } Full response { "ResponseType": "client.notify", "Attributes": { " type": "client.notify", "message": "Sign-up successful. Redirecting to sign in", "title": "Success", "type": "success" } }, { "ResponseType": "client.redirect", "Attributes": { " type": "client.redirect", "delay": 2000, "location": "/auth/signin", "window": "self" } } curl http://localhost:6336/action/user account/signin \ -H 'Content-Type: application/json' \ -d '{ "attributes": { "email": "admin@example.com", "password": "YOUR PASSWORD" } }' { "ResponseType": "client.store.set", "Attributes": { "key": "token", "value": "YOUR ADMIN JWT" } } Full response { "ResponseType": "client.store.set", "Attributes": { "key": "token", "value": "YOUR ADMIN JWT" } }, { "ResponseType": "client.cookie.set", "Attributes": { "key": "token", "value": "YOUR ADMIN JWT; SameSite=Strict" } }, { "ResponseType": "client.notify", "Attributes": { "message": "Logged in", "title": "Success", "type": "success" } }, { "ResponseType": "client.redirect", "Attributes": { "delay": 2000, "location": "/", "window": "self" } } In the response, find the client.store.set item whose Attributes.key is token . Copy its Attributes.value . Use that value wherever the following commands say ADMIN TOKEN . ADMIN TOKEN=YOUR ADMIN JWT Only the first user can claim admin, and only 1 user can claim admin, After 1 administrator is claimed more administrators can be added via the dashboard directly. curl http://localhost:6336/action/world/become an administrator \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/json' \ -d '{}' A successful response will look like this { "ResponseType": "Restart", "Attributes": null } Full response { "ResponseType": "client.redirect", "Attributes": { "delay": 7000, "location": "/", "window": "self" } }, { "ResponseType": "Restart", "Attributes": null } A failed response will be as follows if admin already exists { "ResponseType": "client.notify", "Attributes": { "message": "Unauthorized", "type": "error" } } Full response { "ResponseType": "client.notify", "Attributes": { "message": "Unauthorized", "title": "failed", "type": "error" } } We will use OpenRouter for this guide. The router configuration uses four Daptin resources: | Resource | What it represents | Example in this guide | |---|---|---| | credential | The API key used to authenticate with the upstream provider | openrouter-key | | llm provider | A provider account and the credential it uses | openrouter | | llm model | The name clients request, its allowed operations, and its access policy | assistant | | llm deployment | A route from that public model to a specific upstream model and provider account | assistant-primary → openai/gpt-4o-mini | A model can have several deployments. Clients keep requesting assistant , while Daptin selects a route from the deployments attached to it. llm model defines what your user requests . llm deployment defines where Daptin sends that request . | | llm model | llm deployment | |---|---|---| | Purpose | Public model and its access policy | Concrete upstream route | | Name example | assistant | assistant-primary | | Model name | Name clients use: assistant | Provider's name: openai/gpt-4o-mini | | Configuration | Allowed operations, capabilities, defaults, execute permissions | Provider account, priority, weight, timeouts, concurrency, pricing | | Relationships | Can have multiple deployments | Links one public model to one provider account | The setup we will build looks like this. We will add the primary route in this section and the fallback route in section 5: Application requests: "assistant" ← llm model │ ├── assistant-primary │ OpenRouter → openai/gpt-4o-mini │ Priority 0 │ └── assistant-fallback OpenRouter → google/gemini-2.5-flash Priority 1 ↑ llm deployment rows Your application keeps sending "model": "assistant" . You can change upstream models, replace provider credentials, or add fallback routes by changing deployments. Permissions belong to the public model: who may invoke assistant . Provider and Deployment configuration controls execution: which upstream handles it and under what operational limits . curl http://localhost:6336/api/credential \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "credential", "attributes": { "name": "openrouter-key", "content": "{\"api key\":\"sk-or-v1-YOUR OPENROUTER KEY\"}" } } }' The escaped quotes in content are required because this field contains a JSON string. The response to a successful creation will be like this { "data": { "type": "credential", "id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "attributes": { "name": "openrouter-key", "content": "ENCRYPTED VALUE" } } } Full response { "data": { "type": "credential", "id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "attributes": { " type": "credential", "content": "kJdzKDH16Brnfp1mPZU5DQqK9ZCQqNjYvPHt063axp9JEx5C5ukZP7XFc-7SrCjFvM 0P1cSXXGxHlooXWnwRUPr NxoN-yyokM-6bE23F9VdYbyn2665cRnt39dyvvnhdVDdhTLJg==", "created at": "2026-11-13T07:12:01.909312051Z", "name": "openrouter-key", "permission": 2097151, "reference id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "updated at": "2026-11-13T07:12:01.909313259Z", "user account id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e", "version": 1 }, "relationships": { "llm provider id": { "data": null }, "user account id": { "data": { "type": "user account", "id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e" } }, "usergroup id": { "data": } } } } Note that since the "content" field of the credential entity is marked as "encrypted" column type, its value will never be returned or CRUD api response. The base64 value above is the encrypted payload. You can use encrypted columns in your own custom entities as well in the same way. Copy data.id from the response. That is your $CREDENTIAL ID . CREDENTIAL ID=01a1d3bf-c173-79bb-ab9c-3583d7cae432 curl http://localhost:6336/api/llm provider \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm provider", "attributes": { "name": "openrouter", "provider type": "openrouter", "provider parameters": {}, "credential id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "enable": true } } }' The reponse will be something like this { "data": { "type": "llm provider", "id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "attributes": { "name": "openrouter", "provider type": "openrouter", "credential id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "provider parameters": {}, "enable": 1 } } } Full response { "data": { "type": "llm provider", "id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "attributes": { " type": "llm provider", "allow insecure": 0, "allow private network": 0, "base url": null, "created at": "2026-11-13T07:18:19.497550961Z", "credential id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432", "enable": 1, "name": "openrouter", "permission": 2097151, "provider parameters": {}, "provider type": "openrouter", "reference id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "updated at": "2026-11-13T07:18:19.497551503Z", "user account id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e", "version": 1 }, "relationships": { "credential id": { "data": { "type": "credential", "id": "01a1d3bf-c173-79bb-ab9c-3583d7cae432" } }, "llm deployment id": { "data": }, "user account id": { "data": { "type": "user account", "id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e" } }, "usergroup id": { "data": } } } } Copy data.id . That is your PROVIDER ID . curl http://localhost:6336/api/llm model \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm model", "attributes": { "name": "assistant", "operations": " \"chat\",\"responses\" ", "routing strategy": "priority weighted", "enable": true } } }' Which gives us the response { "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "attributes": { "name": "assistant", "operations": "chat", "responses" , "permission": 2097151 } } } Full response { "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "attributes": { " type": "llm model", "capabilities": {}, "created at": "2026-11-13T07:22:23.107978838Z", "default parameters": {}, "enable": 1, "fallback models": , "name": "assistant", "operations": "chat", "responses" , "permission": 2097151, "reference id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "routing strategy": "priority weighted", "unsupported parameter policy": "reject", "updated at": "2026-11-13T07:22:23.107979755Z", "user account id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e", "version": 1 }, "relationships": { "llm deployment id": { "data": }, "user account id": { "data": { "type": "user account", "id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e" } }, "usergroup id": { "data": } } } } Copy data.id . That is your MODEL ID . The permission value you are seeing in the respoonse above includes GUEST EXECUTE so anyone will be able to use the model, lets lock it down to admin only for now and we will reopen it to users/guests later curl -X PATCH http://localhost:6336/api/llm model/01a1d3c9-3c03-7ac3-a864-7b7cf5705b14 \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "attributes": {"permission": 0} } }' curl http://localhost:6336/api/llm deployment \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm deployment", "attributes": { "name": "assistant-primary", "llm model id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "llm provider id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "upstream model": "openai/gpt-4o-mini", "operations": " \"chat\",\"responses\" ", "priority": 0, "weight": 1, "enable": true } } }' A successful request will give the following response { "data": { "type": "llm deployment", "id": "01a1d3ce-2c3c-7bfe-bb8e-6be92b322368", "attributes": { "name": "assistant-primary", "llm model id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "llm provider id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "upstream model": "openai/gpt-4o-mini", "priority": 0, "weight": 1 } } } Full response { "data": { "type": "llm deployment", "id": "01a1d3ce-2c3c-7bfe-bb8e-6be92b322368", "attributes": { " type": "llm deployment", "connect timeout ms": 10000, "created at": "2026-11-13T07:27:46.749376377Z", "enable": 1, "health check": {}, "llm model id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "llm provider id": "01a1d3c5-8468-77e9-b962-25eb573dbd58", "max concurrency": -1, "name": "assistant-primary", "operations": "chat", "responses" , "parameters": {}, "permission": 2097151, "pricing": {}, "priority": 0, "reference id": "01a1d3ce-2c3c-7bfe-bb8e-6be92b322368", "request timeout ms": 90000, "rpm": -1, "tpm": -1, "updated at": "2026-11-13T07:27:46.749376793Z", "upstream model": "openai/gpt-4o-mini", "user account id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e", "version": 1, "weight": 1 }, "relationships": { "llm model id": { "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14" } }, "llm provider id": { "data": { "type": "llm provider", "id": "01a1d3c5-8468-77e9-b962-25eb573dbd58" } }, "user account id": { "data": { "type": "user account", "id": "01a1d3b5-0c88-7a44-8571-1c8211790a0e" } }, "usergroup id": { "data": } } } } Wait a few seconds for the catalog to reload or you can restart the container/process . At this point we have done all the configuration and we can start calling the llm apis, we will start with the model list which should list our public facing names the one we created in llm deployment curl http://localhost:6336/v1/models \ -H "Authorization: Bearer $ADMIN TOKEN" Response: { "data": { "id": "assistant", "object": "model" } , "object": "list" } Full response { "data": { "created": 0, "id": "assistant", "llmgateway": { "capabilities": {}, "operations": "chat", "responses" }, "object": "model", "owned by": "llmgateway" } , "object": "list" } curl http://localhost:6336/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "assistant", "messages": {"role": "user", "content": "Hello "} , "max tokens": 128 }' This will giv permission denied error since there is no auth token and we had set the permission for the llm model to 0 earlier. curl http://localhost:6336/v1/chat/completions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $ADMIN TOKEN" \ -d '{ "model": "assistant", "messages": {"role": "user", "content": "Hello "} , "max tokens": 128 }' Response: { "model": "assistant", "choices": { "message": { "role": "assistant", "content": "Hello How can I assist you today?" }, "finish reason": "stop" } , "usage": { "prompt tokens": 9, "completion tokens": 9, "total tokens": 18 } } Full response { "choices": { "finish reason": "stop", "index": 0, "logprobs": null, "message": { "content": "Hello How can I assist you today?", "role": "assistant" } } , "created": 1794555847, "id": "gen-1794555847-wSj38rh3abVghxCJDUsD", "model": "assistant", "object": "chat.completion", "usage": { "prompt tokens": 9, "completion tokens": 9, "total tokens": 18 } } We have a very basic setup for the LLM router now. Admin can use the /v1/ apis to list models or invoke /completions Next will do some advanced stuff, including adding a fallback model, changing permission to allow users/guests and also budgeting/token limiting. These execution grants have different meanings: | Permission | Meaning | |---|---| | Model row 0 | No row-level execution grant; administrators and separately granted groups may still execute | | Model row 32 | Allows guest invocation | | Model row 4096 | Allows the record's owner—not every signed-in user | | Model row 2097152 | Allows any authenticated account | | Model's group relationship 524288 | GroupExecute : grants execution to members of that group | Permission values are bitsets. PATCHing a permission value replaces that record's current bitset. Apply model permissions through a separate PATCH after creation. For the group-specific setup below, keep the model row's permission at 0 . As administrator, create an application account if you do not have one or you can enable the signup action to let guests signup : curl http://localhost:6336/api/user account \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "user account", "attributes": { "name": "Router application", "email": "app@example.com", "password": "YOUR USER PASSWORD" } } }' Response { "data": { "type": "user account", "id": "01a1d46b-46f6-7b6c-a6ea-3f9fe8cc26fc" } } And then we create a new usergroup curl http://localhost:6336/api/usergroup \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "usergroup", "attributes": {"name": "assistant-users"} } }' Response { "data": { "type": "usergroup", "id": "01a1d46c-2c35-7502-a00b-3244b16944a2" } } Copy the group response's data.id as GROUP ID . Add the newly created user account to this new usergroup curl -X PATCH http://localhost:6336/api/user account/01a1d46b-46f6-7b6c-a6ea-3f9fe8cc26fc \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "user account", "id": "01a1d46b-46f6-7b6c-a6ea-3f9fe8cc26fc", "relationships": { "usergroup id": { "data": {"type": "usergroup", "id": "01a1d46c-2c35-7502-a00b-3244b16944a2"} } } } }' Expect HTTP 200. This adds membership and preserves existing memberships. Response { "data": { "type": "user account", "id": "01a1d46b-46f6-7b6c-a6ea-3f9fe8cc26fc" } } We can verify the actual group IDs with a separate read: curl http://localhost:6336/api/user account/01a1d46b-46f6-7b6c-a6ea-3f9fe8cc26fc/relationships/usergroup id \ -H "Authorization: Bearer $ADMIN TOKEN" Response { "data": { "type": "usergroup", "id": "01a1d46c-2c35-7502-a00b-3244b16944a2" }, { "type": "usergroup", "id": "01a1d45c-4675-7f29-985e-f7a678355c66" } } Full response { "links": { "current page": 1, "from": 0, "last page": 1, "per page": 10, "to": 10, "total": 2 }, "data": { "type": "usergroup", "id": "01a1d46c-2c35-7502-a00b-3244b16944a2" }, { "type": "usergroup", "id": "01a1d45c-4675-7f29-985e-f7a678355c66" } } User is part of the usergroup. We will do the same with the llm model entity we created earlier. Add it to the same usergroup curl -X PATCH http://localhost:6336/api/llm model/01a1d3c9-3c03-7ac3-a864-7b7cf5705b14 \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "relationships": { "usergroup id": { "data": {"type": "usergroup", "id": "01a1d46c-2c35-7502-a00b-3244b16944a2"} } } } }' Response { "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14" } } The new additions to the usergroup are created with the default permissions, we will update the group permission of the llm model and allow GROUP EXECUTE so that members of that group can execute llm calls on this model curl -X PATCH http://localhost:6336/api/usergroup/01a1d46c-2c35-7502-a00b-3244b16944a2/relationships/llm model id \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "attributes": {"permission": 524288} } }' This PATCH returns nothing in response body. This permission update replaces the relation's entire bitmask with 524288 . It does not change the model row's own permission, which should remain 0 . Verify the permission persisted curl http://localhost:6336/api/llm model/01a1d3c9-3c03-7ac3-a864-7b7cf5705b14/usergroup id \ -H "Authorization: Bearer $ADMIN TOKEN" In the response to this one of the usergroup will be the one we just modified { "data": { "type": "usergroup", "id": "01a1d482-1632-77ae-b3d3-e8cdd85b8b9d", "attributes": { "name": "assistant-users", "permission": 524288, "relation reference id": "01a1d46c-2c35-7502-a00b-3244b16944a2" } } } Now we should be able to login as the new user and use the llm model through that account Sign in as the application account and copy the token as before: curl http://localhost:6336/action/user account/signin \ -H 'Content-Type: application/json' \ -d '{ "attributes": { "email": "app@example.com", "password": "YOUR USER PASSWORD" } }' Response { "ResponseType": "client.store.set", "Attributes": { "key": "token", "value": "YOUR APPLICATION JWT" } } Copy Attributes.value from the client.store.set item into a separate shell variable for the application account: USER DAPTIN TOKEN='YOUR APPLICATION JWT' Use $USER DAPTIN TOKEN for model calls from this point onward. Keep $ADMIN TOKEN for configuration and inspecting administrative resources. Try a request as the application account: curl http://localhost:6336/v1/chat/completions \ -H "Authorization: Bearer $USER DAPTIN TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "model": "assistant", "messages": {"role": "user", "content": "Hello "} , "max tokens": 128, "stream": false }' { "model": "assistant", "choices": { "message": { "role": "assistant", "content": "Hello How can I assist you today?" }, "finish reason": "stop" } , "usage": { "prompt tokens": 9, "completion tokens": 9, "total tokens": 18 } } Full response { "choices": { "finish reason": "stop", "index": 0, "logprobs": null, "message": { "content": "Hello How can I assist you today?", "role": "assistant" } } , "created": 1794567109, "id": "gen-1794567109-Mnjrxj9MqH12b4rkmM75", "model": "assistant", "object": "chat.completion", "usage": { "prompt tokens": 9, "completion tokens": 9, "total tokens": 18 } } Now all users that belong to this new usergroup will be able to execute the /v1 llm apis for this model. We can set back the permission to restrict it again Set the existing model–group relation's permission to 0 : curl -X PATCH http://localhost:6336/api/usergroup/01a1d46c-2c35-7502-a00b-3244b16944a2/relationships/llm model id \ -H "Authorization: Bearer $ADMIN TOKEN" \ -H 'Content-Type: application/vnd.api+json' \ -d '{ "data": { "type": "llm model", "id": "01a1d3c9-3c03-7ac3-a864-7b7cf5705b14", "attributes": {"permission": 0} } }' To continue the guide, restore the grant by repeating the group-side permission PATCH above with "permission": 524288 . Setting the relation's permission to 0 leaves the link in place but removes its group execution grant. It does not revoke owner, guest, authenticated, other group, or administrator access supplied independently. curl -X DELETE http://localhost:6336/api/user account/