-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Enable managed local Ollama inference and add cloud/Ollama model selection #295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1c4e93c
13b38d4
999782d
3917d77
6f92270
8c9af49
3976998
f6cb4fd
b0ce85d
d942624
d168e62
4b4dfbc
cd369b6
a350085
ba42467
98a727b
cef43b1
7aca0b8
29ace38
8b83cf2
62a68b7
9428857
9138059
2893683
37cbbba
fb901cd
0c1af98
a83bb99
320fad0
415b9e8
7bffd0f
7d912cb
962c204
aa1eefe
20a0e52
80086b4
dedbae9
efe9678
ae59283
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| const INFERENCE_ROUTE_URL = "https://inference.local/v1"; | ||
| const DEFAULT_CLOUD_MODEL = "nvidia/nemotron-3-super-120b-a12b"; | ||
| const CLOUD_MODEL_OPTIONS = [ | ||
| { id: "nvidia/nemotron-3-super-120b-a12b", label: "Nemotron 3 Super 120B" }, | ||
| { id: "moonshotai/kimi-k2.5", label: "Kimi K2.5" }, | ||
| { id: "z-ai/glm5", label: "GLM-5" }, | ||
| { id: "minimaxai/minimax-m2.5", label: "MiniMax M2.5" }, | ||
| { id: "qwen/qwen3.5-397b-a17b", label: "Qwen3.5 397B A17B" }, | ||
| { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" }, | ||
| ]; | ||
| const DEFAULT_ROUTE_PROFILE = "inference-local"; | ||
| const DEFAULT_ROUTE_CREDENTIAL_ENV = "OPENAI_API_KEY"; | ||
| const MANAGED_PROVIDER_ID = "inference"; | ||
| const { DEFAULT_OLLAMA_MODEL } = require("./local-inference"); | ||
|
|
||
| function getProviderSelectionConfig(provider, model) { | ||
| switch (provider) { | ||
| case "nvidia-nim": | ||
| return { | ||
| endpointType: "custom", | ||
| endpointUrl: INFERENCE_ROUTE_URL, | ||
| ncpPartner: null, | ||
| model: model || DEFAULT_CLOUD_MODEL, | ||
| profile: DEFAULT_ROUTE_PROFILE, | ||
| credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, | ||
| provider, | ||
| providerLabel: "NVIDIA Cloud API", | ||
| }; | ||
| case "vllm-local": | ||
| return { | ||
| endpointType: "custom", | ||
| endpointUrl: INFERENCE_ROUTE_URL, | ||
| ncpPartner: null, | ||
| model: model || "vllm-local", | ||
| profile: DEFAULT_ROUTE_PROFILE, | ||
|
Comment on lines
+32
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the primary-model helper aligned with the provider defaults.
Suggested fix function getOpenClawPrimaryModel(provider, model) {
- const resolvedModel =
- model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL);
- return resolvedModel ? `${MANAGED_PROVIDER_ID}/${resolvedModel}` : null;
+ let resolvedModel = model;
+ if (!resolvedModel) {
+ switch (provider) {
+ case "nvidia-nim":
+ resolvedModel = DEFAULT_CLOUD_MODEL;
+ break;
+ case "vllm-local":
+ resolvedModel = "vllm-local";
+ break;
+ case "ollama-local":
+ resolvedModel = DEFAULT_OLLAMA_MODEL;
+ break;
+ default:
+ return null;
+ }
+ }
+
+ return `${MANAGED_PROVIDER_ID}/${resolvedModel}`;
}Also applies to: 59-62 🤖 Prompt for AI Agents |
||
| credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, | ||
| provider, | ||
| providerLabel: "Local vLLM", | ||
| }; | ||
| case "ollama-local": | ||
| return { | ||
| endpointType: "custom", | ||
| endpointUrl: INFERENCE_ROUTE_URL, | ||
| ncpPartner: null, | ||
| model: model || DEFAULT_OLLAMA_MODEL, | ||
| profile: DEFAULT_ROUTE_PROFILE, | ||
| credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, | ||
| provider, | ||
| providerLabel: "Local Ollama", | ||
| }; | ||
| default: | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function getOpenClawPrimaryModel(provider, model) { | ||
| const resolvedModel = | ||
| model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL); | ||
| return resolvedModel ? `${MANAGED_PROVIDER_ID}/${resolvedModel}` : null; | ||
| } | ||
|
|
||
| module.exports = { | ||
| CLOUD_MODEL_OPTIONS, | ||
| DEFAULT_CLOUD_MODEL, | ||
| DEFAULT_OLLAMA_MODEL, | ||
| DEFAULT_ROUTE_CREDENTIAL_ENV, | ||
| DEFAULT_ROUTE_PROFILE, | ||
| INFERENCE_ROUTE_URL, | ||
| MANAGED_PROVIDER_ID, | ||
| getOpenClawPrimaryModel, | ||
| getProviderSelectionConfig, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,179 @@ | ||||||||||||||||||||||||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||||||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const HOST_GATEWAY_URL = "http://host.openshell.internal"; | ||||||||||||||||||||||||
| const CONTAINER_REACHABILITY_IMAGE = "curlimages/curl:8.10.1"; | ||||||||||||||||||||||||
| const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getLocalProviderBaseUrl(provider) { | ||||||||||||||||||||||||
| switch (provider) { | ||||||||||||||||||||||||
| case "vllm-local": | ||||||||||||||||||||||||
| return `${HOST_GATEWAY_URL}:8000/v1`; | ||||||||||||||||||||||||
| case "ollama-local": | ||||||||||||||||||||||||
| return `${HOST_GATEWAY_URL}:11434/v1`; | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getLocalProviderHealthCheck(provider) { | ||||||||||||||||||||||||
| switch (provider) { | ||||||||||||||||||||||||
| case "vllm-local": | ||||||||||||||||||||||||
| return "curl -sf http://localhost:8000/v1/models 2>/dev/null"; | ||||||||||||||||||||||||
| case "ollama-local": | ||||||||||||||||||||||||
| return "curl -sf http://localhost:11434/api/tags 2>/dev/null"; | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getLocalProviderContainerReachabilityCheck(provider) { | ||||||||||||||||||||||||
| switch (provider) { | ||||||||||||||||||||||||
| case "vllm-local": | ||||||||||||||||||||||||
| return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:8000/v1/models 2>/dev/null`; | ||||||||||||||||||||||||
| case "ollama-local": | ||||||||||||||||||||||||
| return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null`; | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function validateLocalProvider(provider, runCapture) { | ||||||||||||||||||||||||
| const command = getLocalProviderHealthCheck(provider); | ||||||||||||||||||||||||
| if (!command) { | ||||||||||||||||||||||||
| return { ok: true }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+42
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail unknown local provider IDs instead of accepting them. If 💡 Proposed fix function validateLocalProvider(provider, runCapture) {
const command = getLocalProviderHealthCheck(provider);
if (!command) {
- return { ok: true };
+ return {
+ ok: false,
+ message: `Unsupported local inference provider: ${provider}.`,
+ };
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const output = runCapture(command, { ignoreError: true }); | ||||||||||||||||||||||||
| if (!output) { | ||||||||||||||||||||||||
| switch (provider) { | ||||||||||||||||||||||||
| case "vllm-local": | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: "Local vLLM was selected, but nothing is responding on http://localhost:8000.", | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| case "ollama-local": | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: "Local Ollama was selected, but nothing is responding on http://localhost:11434.", | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return { ok: false, message: "The selected local inference provider is unavailable." }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const containerCommand = getLocalProviderContainerReachabilityCheck(provider); | ||||||||||||||||||||||||
| if (!containerCommand) { | ||||||||||||||||||||||||
| return { ok: true }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const containerOutput = runCapture(containerCommand, { ignoreError: true }); | ||||||||||||||||||||||||
| if (containerOutput) { | ||||||||||||||||||||||||
| return { ok: true }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| switch (provider) { | ||||||||||||||||||||||||
| case "vllm-local": | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: | ||||||||||||||||||||||||
| "Local vLLM is responding on localhost, but containers cannot reach http://host.openshell.internal:8000. Ensure the server is reachable from containers, not only from the host shell.", | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| case "ollama-local": | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: | ||||||||||||||||||||||||
| "Local Ollama is responding on localhost, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.", | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||
| return { ok: false, message: "The selected local inference provider is unavailable from containers." }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function parseOllamaList(output) { | ||||||||||||||||||||||||
| return String(output || "") | ||||||||||||||||||||||||
| .split(/\r?\n/) | ||||||||||||||||||||||||
| .map((line) => line.trim()) | ||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||
| .filter((line) => !/^NAME\s+/i.test(line)) | ||||||||||||||||||||||||
| .map((line) => line.split(/\s{2,}/)[0]) | ||||||||||||||||||||||||
| .filter(Boolean); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getOllamaModelOptions(runCapture) { | ||||||||||||||||||||||||
| const output = runCapture("ollama list 2>/dev/null", { ignoreError: true }); | ||||||||||||||||||||||||
| const parsed = parseOllamaList(output); | ||||||||||||||||||||||||
| if (parsed.length > 0) { | ||||||||||||||||||||||||
| return parsed; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return [DEFAULT_OLLAMA_MODEL]; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getDefaultOllamaModel(runCapture) { | ||||||||||||||||||||||||
| const models = getOllamaModelOptions(runCapture); | ||||||||||||||||||||||||
| return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0]; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function shellQuote(value) { | ||||||||||||||||||||||||
| return `'${String(value).replace(/'/g, `'\\''`)}'`; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getOllamaWarmupCommand(model, keepAlive = "15m") { | ||||||||||||||||||||||||
| const payload = JSON.stringify({ | ||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||
| prompt: "hello", | ||||||||||||||||||||||||
| stream: false, | ||||||||||||||||||||||||
| keep_alive: keepAlive, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| return `nohup curl -s http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getOllamaProbeCommand(model, timeoutSeconds = 120, keepAlive = "15m") { | ||||||||||||||||||||||||
| const payload = JSON.stringify({ | ||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||
| prompt: "hello", | ||||||||||||||||||||||||
| stream: false, | ||||||||||||||||||||||||
| keep_alive: keepAlive, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| return `curl -sS --max-time ${timeoutSeconds} http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} 2>/dev/null`; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function validateOllamaModel(model, runCapture) { | ||||||||||||||||||||||||
| const output = runCapture(getOllamaProbeCommand(model), { ignoreError: true }); | ||||||||||||||||||||||||
| if (!output) { | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: | ||||||||||||||||||||||||
| `Selected Ollama model '${model}' did not answer the local probe in time. ` + | ||||||||||||||||||||||||
| "It may still be loading, too large for the host, or otherwise unhealthy.", | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| const parsed = JSON.parse(output); | ||||||||||||||||||||||||
| if (parsed && typeof parsed.error === "string" && parsed.error.trim()) { | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||
| message: `Selected Ollama model '${model}' failed the local probe: ${parsed.error.trim()}`, | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } catch {} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return { ok: true }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| module.exports = { | ||||||||||||||||||||||||
| CONTAINER_REACHABILITY_IMAGE, | ||||||||||||||||||||||||
| DEFAULT_OLLAMA_MODEL, | ||||||||||||||||||||||||
| HOST_GATEWAY_URL, | ||||||||||||||||||||||||
| getDefaultOllamaModel, | ||||||||||||||||||||||||
| getLocalProviderBaseUrl, | ||||||||||||||||||||||||
| getLocalProviderContainerReachabilityCheck, | ||||||||||||||||||||||||
| getLocalProviderHealthCheck, | ||||||||||||||||||||||||
| getOllamaModelOptions, | ||||||||||||||||||||||||
| getOllamaProbeCommand, | ||||||||||||||||||||||||
| getOllamaWarmupCommand, | ||||||||||||||||||||||||
| parseOllamaList, | ||||||||||||||||||||||||
| validateOllamaModel, | ||||||||||||||||||||||||
| validateLocalProvider, | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify local-inference status: Ollama vs vLLM.
This line says both Ollama and vLLM are experimental, but the PR objective indicates Ollama was promoted out of experimental mode. Please update wording so users aren’t misled.
📝 Suggested wording
📝 Committable suggestion
🤖 Prompt for AI Agents