Skip to content

Commit d10a136

Browse files
committed
release: NOVA AgentOS 1.0.3 local Ollama base
1 parent 408335f commit d10a136

9 files changed

Lines changed: 296 additions & 34 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Desktop.ini
6262
crashes/
6363
outputs/
6464
execution-events.jsonl
65+
backups/
6566

6667
# Credentials and local configuration
6768
.env

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 1.0.3
4+
5+
### Fixed
6+
7+
- Ollama native `/api/chat` endpoints are preserved instead of being rewritten to an invalid nested URL.
8+
- Native Ollama NDJSON streaming and tool-call payloads are supported alongside OpenAI-compatible `/v1/chat/completions`.
9+
- Missing local models and invalid Ollama endpoints now produce actionable diagnostics.
10+
- The default Ollama address uses `localhost`, avoiding an empty IPv4-only Ollama instance when the active model service is bound to IPv6.
11+
- Native Ollama runs now size `num_ctx` adaptively from 8K to 64K and use a bounded local output budget, preventing NOVA's system context from overflowing Ollama's 4096-token default.
12+
313
本项目遵循“先记录真实能力,再发布版本”的原则。未完成签名、公证和真实端到端基准的构建均标记为 Preview。
414

515
## 1.0.2

NovaDesktop.Electron/electron/main.cjs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ function modelDefaults(provider) {
263263
if (provider === "ollama") {
264264
return {
265265
model: "gpt-oss:20b",
266-
endpoint: "http://127.0.0.1:11434/v1/chat/completions"
266+
endpoint: "http://localhost:11434/api/chat"
267267
};
268268
}
269269
if (provider === "custom") {
@@ -317,6 +317,18 @@ function normalizeCompatibleEndpoint(provider, rawValue) {
317317
}
318318

319319
let pathname = endpoint.pathname.replace(/\/+$/, "");
320+
if (provider === "ollama" && /\/api\/chat$/i.test(pathname)) {
321+
endpoint.pathname = pathname;
322+
return endpoint.toString();
323+
}
324+
if (provider === "ollama" && (!pathname || pathname === "/")) {
325+
endpoint.pathname = "/api/chat";
326+
return endpoint.toString();
327+
}
328+
if (provider === "ollama" && /\/api$/i.test(pathname)) {
329+
endpoint.pathname = `${pathname}/chat`;
330+
return endpoint.toString();
331+
}
320332
if (!/\/chat\/completions$/i.test(pathname)) {
321333
pathname = pathname && pathname !== "/"
322334
? /\/v1$/i.test(pathname)
@@ -350,7 +362,9 @@ function normalizeModelConfiguration(value) {
350362
function modelsEndpoint(configuration) {
351363
if (configuration.provider === "ollama") {
352364
const endpoint = new URL(configuration.endpoint);
353-
endpoint.pathname = "/api/tags";
365+
endpoint.pathname = /\/api\/chat$/i.test(endpoint.pathname)
366+
? endpoint.pathname.replace(/\/api\/chat$/i, "/api/tags")
367+
: "/api/tags";
354368
return endpoint;
355369
}
356370
if (configuration.provider === "openai") return new URL("https://api.openai.com/v1/models");
@@ -896,12 +910,22 @@ function registerIpc() {
896910
senderWindow(event);
897911
const normalized = normalizeModelConfiguration(configuration);
898912
const discoveredModels = await probeModelConnection(normalized);
899-
if (
900-
normalized.provider === "ollama" &&
901-
discoveredModels.length &&
902-
!discoveredModels.includes(normalized.model)
903-
) {
904-
normalized.model = discoveredModels[0];
913+
if (normalized.provider === "ollama") {
914+
if (!discoveredModels.length) {
915+
throw new Error(
916+
`Ollama 服务已连接,但没有发现已安装模型。请先运行 ollama pull ${normalized.model}`
917+
);
918+
}
919+
const latestAlias = normalized.model.includes(":")
920+
? normalized.model
921+
: `${normalized.model}:latest`;
922+
if (!discoveredModels.includes(normalized.model) && discoveredModels.includes(latestAlias)) {
923+
normalized.model = latestAlias;
924+
} else if (!discoveredModels.includes(normalized.model)) {
925+
throw new Error(
926+
`Ollama 中未找到模型 ${normalized.model}。当前可用:${discoveredModels.join("、")}`
927+
);
928+
}
905929
}
906930
modelConnections.set(normalized.provider, normalized);
907931
return {

NovaDesktop.Electron/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NovaDesktop.Electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nova-agentos-electron",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"private": true,
55
"author": "NOVA AgentOS Project",
66
"description": "NOVA AgentOS next-generation desktop shell",

NovaDesktop.Electron/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ function App() {
14971497
spellCheck={false}
14981498
placeholder={
14991499
provider === "ollama"
1500-
? "http://127.0.0.1:11434"
1500+
? "http://localhost:11434"
15011501
: "https://your-provider.example/v1"
15021502
}
15031503
/>

NovaDesktop.SmokeTests/Program.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net;
22
using System.Text;
3+
using System.Text.Json;
34
using System.Text.Json.Nodes;
45
using System.Xml.Linq;
56
using Nova.Core;
@@ -671,6 +672,33 @@ await CheckAsync("Ollama OpenAI-compatible Agent runtime", async () =>
671672
Expect(handler.ThinkingExtensionWasOmitted, "Ollama received a provider-specific thinking extension.");
672673
});
673674

675+
await CheckAsync("Ollama native API Agent runtime", async () =>
676+
{
677+
var handler = new FakeOllamaNativeHandler();
678+
var runtime = new DeepSeekChatAgentRuntime(new HttpClient(handler), runtimeEvidence);
679+
var result = await runtime.RunAsync(
680+
new AgentRunRequest(
681+
"ollama-native-smoke",
682+
"Verify the native Ollama chat protocol.",
683+
@"D:\Agent",
684+
string.Empty,
685+
"ollama",
686+
"openbmb/minicpm5:latest",
687+
AgentExecutionMode.Build,
688+
Endpoint: "http://127.0.0.1:11434/api/chat"),
689+
_ => Task.CompletedTask,
690+
_ => throw new Exception("Ollama native smoke unexpectedly requested approval."),
691+
CancellationToken.None);
692+
693+
Expect(result.Provider == "ollama", "Ollama native provider metadata was not retained.");
694+
Expect(result.FinalText == "Ollama native API connected.", "Ollama NDJSON response was not assembled.");
695+
Expect(handler.UsedNativeEndpoint, "Ollama native endpoint was not used.");
696+
Expect(handler.UsedNativeRequestShape, "Ollama native request fields were not normalized.");
697+
Expect(handler.UsedExpandedContextWindow, "Ollama native request did not reserve an expanded context window.");
698+
Expect(handler.AcceptedNdjson, "Ollama native request did not advertise NDJSON.");
699+
Expect(handler.AuthorizationWasOmitted, "Ollama native request received an unnecessary Authorization header.");
700+
});
701+
674702
await CheckAsync("Kimi multimodal API and bounded attachments", async () =>
675703
{
676704
var temporaryDirectory = Path.Combine(
@@ -5516,6 +5544,30 @@ await CheckAsync("Electron 1.0 trustworthy cross-model delivery contract", async
55165544
"The 1.0 renderer can no longer opt into cross-model review or expose truthful partial delivery.");
55175545
});
55185546

5547+
await CheckAsync("Electron Ollama native endpoint contract", async () =>
5548+
{
5549+
var mainSource = await File.ReadAllTextAsync(
5550+
@"D:\Agent\NovaDesktop.Electron\electron\main.cjs");
5551+
var rendererSource = await File.ReadAllTextAsync(
5552+
@"D:\Agent\NovaDesktop.Electron\src\App.tsx");
5553+
Expect(
5554+
mainSource.Contains(
5555+
"endpoint: \"http://localhost:11434/api/chat\"",
5556+
StringComparison.Ordinal)
5557+
&& mainSource.Contains(
5558+
"provider === \"ollama\" && /\\/api\\/chat$/i.test(pathname)",
5559+
StringComparison.Ordinal)
5560+
&& mainSource.Contains(
5561+
"endpoint.pathname.replace(/\\/api\\/chat$/i, \"/api/tags\")",
5562+
StringComparison.Ordinal),
5563+
"Electron no longer preserves Ollama's native /api/chat endpoint and matching model probe path.");
5564+
Expect(
5565+
mainSource.Contains("Ollama 服务已连接,但没有发现已安装模型", StringComparison.Ordinal)
5566+
&& mainSource.Contains("Ollama 中未找到模型", StringComparison.Ordinal)
5567+
&& rendererSource.Contains("http://localhost:11434", StringComparison.Ordinal),
5568+
"Electron no longer gives an actionable missing-model error or localhost guidance.");
5569+
});
5570+
55195571
await CheckAsync("Electron bridge non-blocking start and lease retry contract", async () =>
55205572
{
55215573
var bridgeSource = await File.ReadAllTextAsync(
@@ -5921,6 +5973,46 @@ protected override async Task<HttpResponseMessage> SendAsync(
59215973
}
59225974
}
59235975

5976+
file sealed class FakeOllamaNativeHandler : HttpMessageHandler
5977+
{
5978+
public bool UsedNativeEndpoint { get; private set; }
5979+
public bool UsedNativeRequestShape { get; private set; }
5980+
public bool UsedExpandedContextWindow { get; private set; }
5981+
public bool AcceptedNdjson { get; private set; }
5982+
public bool AuthorizationWasOmitted { get; private set; }
5983+
5984+
protected override async Task<HttpResponseMessage> SendAsync(
5985+
HttpRequestMessage request,
5986+
CancellationToken cancellationToken)
5987+
{
5988+
UsedNativeEndpoint = request.RequestUri?.ToString()
5989+
.Equals("http://127.0.0.1:11434/api/chat", StringComparison.OrdinalIgnoreCase) == true;
5990+
AuthorizationWasOmitted = request.Headers.Authorization is null;
5991+
AcceptedNdjson = request.Headers.Accept.Any(item =>
5992+
item.MediaType?.Equals("application/x-ndjson", StringComparison.OrdinalIgnoreCase) == true);
5993+
var body = await request.Content!.ReadAsStringAsync(cancellationToken);
5994+
UsedNativeRequestShape = body.Contains("\"options\"", StringComparison.Ordinal)
5995+
&& body.Contains("\"num_predict\"", StringComparison.Ordinal)
5996+
&& !body.Contains("\"max_tokens\"", StringComparison.Ordinal)
5997+
&& !body.Contains("\"stream_options\"", StringComparison.Ordinal)
5998+
&& !body.Contains("\"tool_choice\"", StringComparison.Ordinal)
5999+
&& !body.Contains("\"thinking\"", StringComparison.Ordinal);
6000+
using var payload = JsonDocument.Parse(body);
6001+
UsedExpandedContextWindow = payload.RootElement
6002+
.GetProperty("options")
6003+
.GetProperty("num_ctx")
6004+
.GetInt32() >= 8192;
6005+
const string response = """
6006+
{"model":"openbmb/minicpm5:latest","created_at":"2026-07-31T10:00:00Z","message":{"role":"assistant","content":"Ollama native API connected."},"done":false}
6007+
{"model":"openbmb/minicpm5:latest","created_at":"2026-07-31T10:00:01Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop"}
6008+
""";
6009+
return new HttpResponseMessage(HttpStatusCode.OK)
6010+
{
6011+
Content = new StringContent(response, Encoding.UTF8, "application/x-ndjson")
6012+
};
6013+
}
6014+
}
6015+
59246016
file sealed class FakeDeepSeekHandler : HttpMessageHandler
59256017
{
59266018
public int RequestCount { get; private set; }

0 commit comments

Comments
 (0)