Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## [Unreleased]

### 修复

- 模型页注册本机 GGUF / 已拉取的 Ollama 模型时,Windows 桌面不再返回笼统的 `INTERNAL_ERROR` 500;`ollama create` 使用带引号的 POSIX `FROM` 路径,并把真实失败原因(文件不存在、权限、create 失败)返回给界面。

### 变更

- 对话与工作区共用一份列表:侧栏「对话」进入工作区「对话」标签;点开一条才进入聊天画布。工作区侧栏默认打开「项目」标签。
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@
"INVITE_REVOKED": "This invite code has been revoked.",
"INVITE_RATE_LIMITED": "Too many invite attempts. Try again later.",
"WORKSPACE_ROOT_RESTRICTED": "The selected storage root is outside the allowed directory for this account.",
"TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({{used}}/{{quota}})."
"TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({{used}}/{{quota}}).",
"LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {{reason}}"
},
"nav": {
"chat": "Conversation",
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@
"INVITE_REVOKED": "该邀请码已被撤销。",
"INVITE_RATE_LIMITED": "邀请尝试次数过多,请稍后再试。",
"WORKSPACE_ROOT_RESTRICTED": "所选存储根目录超出了该账号允许的范围。",
"TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({{used}}/{{quota}})。"
"TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({{used}}/{{quota}})。",
"LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{{reason}}"
},
"nav": {
"chat": "对话",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "../../../../utils/desktopFolder";
import { providerApi } from "../../../../api/modules/provider";
import { message } from "../../../../utils/antdMessage";
import { apiErrorMessage } from "../../../../utils/apiError";
import { CONVERSATION_LIST_PATH } from "../../../../layouts/conversationHome";
import { isOllamaProviderRow } from "../presetUtils";

Expand Down Expand Up @@ -69,9 +70,11 @@ function mergeModels(
}

function canRegister(item: LocalInstalledModel): boolean {
if (item.source === "ollama" || item.registered) return false;
if (item.registered) return false;
if (item.registerable === false) return false;
return item.source === "gguf" || item.source === "ggml";
return (
item.source === "gguf" || item.source === "ggml" || item.source === "ollama"
);
}

export function LocalHardwarePanel() {
Expand Down Expand Up @@ -338,9 +341,7 @@ export function LocalHardwarePanel() {
message.success(t("models.localRegisterDone", { name: result.name }));
await refresh();
} catch (err) {
message.error(
err instanceof Error ? err.message : t("models.localRegisterFailed"),
);
message.error(apiErrorMessage(err, t("models.localRegisterFailed"), t));
} finally {
setRegistering(null);
}
Expand Down
70 changes: 60 additions & 10 deletions src/octop/api/routers/local_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import asyncio
import json
import logging
import subprocess
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field

from octop.api.deps import current_user, get_server, require_permission
Expand All @@ -24,6 +26,20 @@
from octop.infra.server import OctopServer
from octop.infra.utils.ollama_manager import OllamaModelManager, start_ollama_service_result

logger = logging.getLogger(__name__)

_REGISTER_FAIL_TYPES = (
OSError,
UnicodeError,
TimeoutError,
ValueError,
RuntimeError,
TypeError,
AttributeError,
ImportError,
subprocess.SubprocessError,
)

router = APIRouter()


Expand All @@ -41,10 +57,16 @@ class LocalScanBody(BaseModel):

class LocalRegisterBody(BaseModel):
path: str = Field(
min_length=1, max_length=1024, description="Absolute path to a GGUF/GGML file"
default="",
max_length=1024,
description="Absolute path to a GGUF/GGML file; empty for an already-pulled Ollama tag",
)
name: str | None = Field(default=None, max_length=80, description="Ollama model name to create")
source: str = Field(default="gguf", max_length=32, description="gguf, ggml, or safetensors")
source: str = Field(
default="gguf",
max_length=32,
description="gguf, ggml, safetensors, or ollama",
)
size: int = Field(default=0, ge=0, description="Size in bytes from the scan result")


Expand All @@ -55,9 +77,27 @@ class LocalEnsureBody(BaseModel):
)


def register_failure(exc: BaseException) -> OctopError:
"""Map a recoverable register/import failure to a 400 the UI can show."""
reason = str(exc).strip() or exc.__class__.__name__
return OctopError(
ErrorCode.LOCAL_MODEL_REGISTER_FAILED,
reason,
details={"reason": reason},
)


def _merge_registered(probe: dict[str, Any], rows: list[dict[str, Any]]) -> dict[str, Any]:
installed = list(probe.get("installed") or [])
registered_names = {str(row.get("name") or "") for row in rows if row.get("name")}
registered_paths = {str(row.get("path") or "") for row in rows if row.get("path")}
seen = {str(item.get("path") or item.get("name") or "") for item in installed}
for item in installed:
name = str(item.get("name") or "")
path = str(item.get("path") or "")
if name in registered_names or (path and path in registered_paths):
item["registered"] = True
item["registerable"] = False
for row in rows:
key = str(row.get("path") or row.get("name") or "")
if key and key in seen:
Expand Down Expand Up @@ -146,8 +186,11 @@ async def local_models_install(
try:
info = await asyncio.to_thread(OllamaModelManager.pull_model, body.name)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
registered = _register_ollama_model(server, info.name or body.name)
raise register_failure(exc) from exc
try:
registered = _register_ollama_model(server, info.name or body.name)
except Exception as exc:
raise register_failure(exc) from exc
return {
"ok": True,
"name": info.name,
Expand Down Expand Up @@ -214,10 +257,17 @@ async def local_models_register(
provider_repo=server.services.provider_repo,
settings_repo=server.services.settings_repo,
)
except OSError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except _REGISTER_FAIL_TYPES as exc:
raise register_failure(exc) from exc
if server.app_runtime is not None and result.get("ok"):
await server.app_runtime.agent_registry.on_provider_changed(
provider_name=str(result.get("provider_name") or "Ollama (Local)")
)
try:
await server.app_runtime.agent_registry.on_provider_changed(
provider_name=str(result.get("provider_name") or "Ollama (Local)")
)
except Exception as exc:
logger.warning(
"Registered %s but provider reload failed: %s",
result.get("name"),
exc,
)
return result
3 changes: 2 additions & 1 deletion src/octop/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@
"INVITE_REVOKED": "This invite code has been revoked.",
"INVITE_RATE_LIMITED": "Too many invite attempts. Try again later.",
"WORKSPACE_ROOT_RESTRICTED": "The selected storage root is outside the allowed directory for this account.",
"TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({used}/{quota})."
"TOKEN_QUOTA_EXCEEDED": "This account has reached its token quota. Please contact an administrator to raise the limit ({used}/{quota}).",
"LOCAL_MODEL_REGISTER_FAILED": "Could not register the local model: {reason}"
},
"desktop": {
"install_log_deps": "Installing Python dependencies…",
Expand Down
3 changes: 2 additions & 1 deletion src/octop/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@
"INVITE_REVOKED": "该邀请码已被撤销。",
"INVITE_RATE_LIMITED": "邀请尝试次数过多,请稍后再试。",
"WORKSPACE_ROOT_RESTRICTED": "所选存储根目录超出了该账号允许的范围。",
"TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({used}/{quota})。"
"TOKEN_QUOTA_EXCEEDED": "该账号已达到 token 配额上限,请联系管理员提升额度({used}/{quota})。",
"LOCAL_MODEL_REGISTER_FAILED": "无法注册本地模型:{reason}"
},
"desktop": {
"install_log_deps": "正在安装 Python 依赖…",
Expand Down
1 change: 1 addition & 0 deletions src/octop/infra/agents/providers/local_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def _ollama_models() -> tuple[bool, list[dict[str, Any]]]:
"path": "",
"size": int(getattr(item, "size", 0) or 0),
"source": "ollama",
"registerable": True,
}
)
return True, models
Expand Down
Loading
Loading