Update
This commit is contained in:
+98
-33
@@ -3,23 +3,21 @@ import {
|
||||
XMLSerializer,
|
||||
type Element as XmlDomElement,
|
||||
} from "@xmldom/xmldom";
|
||||
import {
|
||||
LAB2_DEFAULT_OLLAMA_MODELS,
|
||||
LAB2_DEFAULT_OLLAMA_URL,
|
||||
} from "~/lib/courseware-runtime";
|
||||
|
||||
export const LAB2_CHAT_STORAGE_KEY = "lab2-objective5-chat-settings";
|
||||
export const LAB2_DEFAULT_ENDPOINT = "https://ai.zuccaro.me/api";
|
||||
export const LAB2_DEFAULT_ENDPOINT = LAB2_DEFAULT_OLLAMA_URL;
|
||||
export const LAB2_LEGACY_DEFAULT_ENDPOINT = "https://ai.zuccaro.me/api";
|
||||
export const LAB2_CUSTOM_MODEL_VALUE = "__custom__";
|
||||
export const LAB2_MAX_CONTEXT_MESSAGES = 10;
|
||||
export const LAB2_MAX_MESSAGE_LENGTH = 4000;
|
||||
export const LAB2_MAX_SVG_LENGTH = 20000;
|
||||
|
||||
export const LAB2_MODEL_OPTIONS = [
|
||||
{
|
||||
label: "Gemma 4 E2B Q8_0",
|
||||
value: "gemma4:e2b-it-q8_0",
|
||||
},
|
||||
{
|
||||
label: "Gemma 4 E2B Q4_K_M",
|
||||
value: "gemma4:e2b-it-q4_K_M",
|
||||
},
|
||||
...LAB2_DEFAULT_OLLAMA_MODELS,
|
||||
{
|
||||
label: "Custom model",
|
||||
value: LAB2_CUSTOM_MODEL_VALUE,
|
||||
@@ -111,6 +109,10 @@ export type SvgSanitizationResult =
|
||||
| SvgSanitizationFailure
|
||||
| SvgSanitizationSuccess;
|
||||
|
||||
const predefinedModelLabels = new Map<string, string>(
|
||||
LAB2_DEFAULT_OLLAMA_MODELS.map((model) => [model.value, model.label]),
|
||||
);
|
||||
|
||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
const allowedSvgElements = new Set([
|
||||
"svg",
|
||||
@@ -246,24 +248,35 @@ export function getModelListEndpointCandidates(endpoint: string) {
|
||||
const url = new URL(endpoint);
|
||||
const trimmedPath = url.pathname.replace(/\/+$/, "");
|
||||
|
||||
if (trimmedPath.endsWith("/models")) {
|
||||
if (
|
||||
trimmedPath.endsWith("/models") ||
|
||||
trimmedPath.endsWith("/api/tags")
|
||||
) {
|
||||
url.hash = "";
|
||||
return [url.toString()];
|
||||
}
|
||||
|
||||
const paths = new Set<string>();
|
||||
|
||||
if (trimmedPath.endsWith("/api")) {
|
||||
if (trimmedPath.endsWith("/ollama/api")) {
|
||||
paths.add("/ollama/api/tags");
|
||||
} else if (trimmedPath.endsWith("/ollama")) {
|
||||
paths.add("/ollama/api/tags");
|
||||
} else if (trimmedPath.endsWith("/api")) {
|
||||
paths.add("/api/tags");
|
||||
paths.add("/api/v1/models");
|
||||
paths.add("/api/models");
|
||||
} else if (trimmedPath.endsWith("/api/v1")) {
|
||||
paths.add("/api/tags");
|
||||
paths.add("/api/v1/models");
|
||||
paths.add("/api/models");
|
||||
} else if (trimmedPath.endsWith("/v1")) {
|
||||
paths.add("/v1/models");
|
||||
} else if (trimmedPath.length === 0) {
|
||||
paths.add("/api/tags");
|
||||
paths.add("/v1/models");
|
||||
} else {
|
||||
paths.add(`${trimmedPath}/api/tags`);
|
||||
paths.add(`${trimmedPath}/v1/models`);
|
||||
paths.add(`${trimmedPath}/models`);
|
||||
}
|
||||
@@ -277,21 +290,48 @@ export function getModelListEndpointCandidates(endpoint: string) {
|
||||
}
|
||||
|
||||
export function normalizeOllamaChatEndpoint(endpoint: string) {
|
||||
return getOllamaChatEndpointCandidates(endpoint)[0];
|
||||
}
|
||||
|
||||
export function getOllamaChatEndpointCandidates(endpoint: string) {
|
||||
const url = new URL(endpoint);
|
||||
const trimmedPath = url.pathname.replace(/\/+$/, "");
|
||||
|
||||
if (trimmedPath.endsWith("/ollama/api/chat")) {
|
||||
if (
|
||||
trimmedPath.endsWith("/api/chat") ||
|
||||
trimmedPath.endsWith("/ollama/api/chat")
|
||||
) {
|
||||
url.pathname = trimmedPath;
|
||||
} else if (trimmedPath.endsWith("/api") || trimmedPath.endsWith("/api/v1")) {
|
||||
url.pathname = "/ollama/api/chat";
|
||||
} else if (trimmedPath.length === 0) {
|
||||
url.pathname = "/ollama/api/chat";
|
||||
} else {
|
||||
url.pathname = `${trimmedPath}/ollama/api/chat`;
|
||||
url.hash = "";
|
||||
return [url.toString()];
|
||||
}
|
||||
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
const paths = new Set<string>();
|
||||
|
||||
if (trimmedPath.endsWith("/ollama/api")) {
|
||||
paths.add("/ollama/api/chat");
|
||||
} else if (trimmedPath.endsWith("/ollama")) {
|
||||
paths.add("/ollama/api/chat");
|
||||
} else if (trimmedPath.endsWith("/api")) {
|
||||
paths.add("/api/chat");
|
||||
paths.add("/ollama/api/chat");
|
||||
} else if (trimmedPath.endsWith("/api/v1") || trimmedPath.endsWith("/v1")) {
|
||||
paths.add("/ollama/api/chat");
|
||||
paths.add("/api/chat");
|
||||
} else if (trimmedPath.length === 0) {
|
||||
paths.add("/api/chat");
|
||||
paths.add("/ollama/api/chat");
|
||||
} else {
|
||||
paths.add(`${trimmedPath}/api/chat`);
|
||||
paths.add(`${trimmedPath}/ollama/api/chat`);
|
||||
}
|
||||
|
||||
return Array.from(paths).map((path) => {
|
||||
const candidate = new URL(url.toString());
|
||||
candidate.pathname = path;
|
||||
candidate.hash = "";
|
||||
return candidate.toString();
|
||||
});
|
||||
}
|
||||
|
||||
export function looksLikeOllamaModel(model: string) {
|
||||
@@ -373,28 +413,49 @@ export function extractAssistantTextContent(payload: ChatCompletionPayload) {
|
||||
}
|
||||
|
||||
export function extractModelOptions(payload: unknown): Objective5ModelOption[] {
|
||||
if (
|
||||
!payload ||
|
||||
typeof payload !== "object" ||
|
||||
!("data" in payload) ||
|
||||
!Array.isArray(payload.data)
|
||||
) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return payload.data
|
||||
if ("data" in payload && Array.isArray(payload.data)) {
|
||||
return payload.data
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
|
||||
const value =
|
||||
"id" in item && typeof item.id === "string" ? item.id.trim() : "";
|
||||
const label =
|
||||
"name" in item && typeof item.name === "string" && item.name.trim()
|
||||
? item.name.trim()
|
||||
: getModelLabel(value);
|
||||
|
||||
if (!value) return null;
|
||||
return { label, value } satisfies Objective5ModelOption;
|
||||
})
|
||||
.filter((item): item is Objective5ModelOption => item !== null);
|
||||
}
|
||||
|
||||
if (!("models" in payload) || !Array.isArray(payload.models)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return payload.models
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
|
||||
const value =
|
||||
"id" in item && typeof item.id === "string" ? item.id.trim() : "";
|
||||
const label =
|
||||
"name" in item && typeof item.name === "string" && item.name.trim()
|
||||
? item.name.trim()
|
||||
: value;
|
||||
("model" in item && typeof item.model === "string" && item.model.trim()
|
||||
? item.model
|
||||
: "name" in item && typeof item.name === "string"
|
||||
? item.name
|
||||
: ""
|
||||
).trim();
|
||||
|
||||
if (!value) return null;
|
||||
return { label, value } satisfies Objective5ModelOption;
|
||||
return {
|
||||
label: getModelLabel(value),
|
||||
value,
|
||||
} satisfies Objective5ModelOption;
|
||||
})
|
||||
.filter((item): item is Objective5ModelOption => item !== null);
|
||||
}
|
||||
@@ -600,6 +661,10 @@ export function getDefaultObjective5ModelOptions(): Objective5ModelOption[] {
|
||||
return [...LAB2_MODEL_OPTIONS];
|
||||
}
|
||||
|
||||
function getModelLabel(value: string) {
|
||||
return predefinedModelLabels.get(value) ?? value;
|
||||
}
|
||||
|
||||
function validateSvgNode(node: XmlDomElement): string | null {
|
||||
if (!allowedSvgElements.has(node.tagName)) {
|
||||
return `The SVG used a blocked element: <${node.tagName}>.`;
|
||||
|
||||
Reference in New Issue
Block a user