mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
feat(compose): support AI models in Compose specification
This commit is contained in:
parent
9fa98de9c1
commit
29cdc815db
164
apps/dokploy/__test__/compose/model/model.test.ts
Normal file
164
apps/dokploy/__test__/compose/model/model.test.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import type { Compose, ComposeSpecification } from "@dokploy/server";
|
||||
import {
|
||||
addAppNameToPreventCollision,
|
||||
addSuffixToAllProperties,
|
||||
} from "@dokploy/server";
|
||||
import { addDomainToCompose } from "@dokploy/server/utils/docker/domain";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
vi.mock("@dokploy/server/utils/process/execAsync", async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import("@dokploy/server/utils/process/execAsync")
|
||||
>()),
|
||||
execAsyncRemote: vi.fn(),
|
||||
}));
|
||||
|
||||
const modelsComposeFile = `
|
||||
services:
|
||||
chat:
|
||||
image: my-chat-app
|
||||
models:
|
||||
- llm
|
||||
worker:
|
||||
image: my-worker
|
||||
models:
|
||||
embed:
|
||||
endpoint_var: EMBED_URL
|
||||
model_var: EMBED_MODEL
|
||||
models:
|
||||
llm:
|
||||
model: ai/smollm2
|
||||
context_size: 2048
|
||||
runtime_flags:
|
||||
- "--verbose"
|
||||
embed:
|
||||
model: ai/all-minilm
|
||||
name: embeddings
|
||||
`;
|
||||
|
||||
const expectedModels = {
|
||||
llm: {
|
||||
model: "ai/smollm2",
|
||||
context_size: 2048,
|
||||
runtime_flags: ["--verbose"],
|
||||
},
|
||||
embed: {
|
||||
model: "ai/all-minilm",
|
||||
name: "embeddings",
|
||||
},
|
||||
};
|
||||
|
||||
const expectedChatModels = ["llm"];
|
||||
const expectedWorkerModels = {
|
||||
embed: {
|
||||
endpoint_var: "EMBED_URL",
|
||||
model_var: "EMBED_MODEL",
|
||||
},
|
||||
};
|
||||
|
||||
const assertModelsPreserved = (
|
||||
spec: ComposeSpecification | null,
|
||||
serviceNames: { chat: string; worker: string } = {
|
||||
chat: "chat",
|
||||
worker: "worker",
|
||||
},
|
||||
) => {
|
||||
expect(spec?.models).toEqual(expectedModels);
|
||||
expect(spec?.services?.[serviceNames.chat]?.models).toEqual(
|
||||
expectedChatModels,
|
||||
);
|
||||
expect(spec?.services?.[serviceNames.worker]?.models).toEqual(
|
||||
expectedWorkerModels,
|
||||
);
|
||||
};
|
||||
|
||||
const rawCompose = (overrides?: Record<string, unknown>) =>
|
||||
({
|
||||
appName: "chat-app",
|
||||
composeFile: modelsComposeFile,
|
||||
composePath: "./docker-compose.yml",
|
||||
composeType: "docker-compose",
|
||||
isolatedDeployment: false,
|
||||
isolatedDeploymentsVolume: false,
|
||||
randomize: false,
|
||||
serverId: null,
|
||||
sourceType: "raw",
|
||||
suffix: "",
|
||||
...overrides,
|
||||
}) as unknown as Compose;
|
||||
|
||||
test("compose without models is unchanged besides existing suffix behavior", () => {
|
||||
const composeData = parse(`
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- web_data:/data
|
||||
volumes:
|
||||
web_data:
|
||||
`) as ComposeSpecification;
|
||||
const updated = addSuffixToAllProperties(composeData, "testhash");
|
||||
|
||||
expect(updated.models).toBeUndefined();
|
||||
expect(updated.services).toEqual({
|
||||
"web-testhash": {
|
||||
image: "nginx:latest",
|
||||
volumes: ["web_data-testhash:/data"],
|
||||
},
|
||||
});
|
||||
expect(updated.volumes).toEqual({
|
||||
"web_data-testhash": null,
|
||||
});
|
||||
});
|
||||
|
||||
test("suffixing does not rename model identifiers or drop model config", () => {
|
||||
const updated = addSuffixToAllProperties(
|
||||
parse(modelsComposeFile) as ComposeSpecification,
|
||||
"testhash",
|
||||
);
|
||||
|
||||
assertModelsPreserved(updated, {
|
||||
chat: "chat-testhash",
|
||||
worker: "worker-testhash",
|
||||
});
|
||||
expect(updated.services).not.toHaveProperty("chat");
|
||||
expect(updated.models).not.toHaveProperty("llm-testhash");
|
||||
});
|
||||
|
||||
test("isolated deployment preserves model identifiers", () => {
|
||||
assertModelsPreserved(
|
||||
addAppNameToPreventCollision(
|
||||
parse(modelsComposeFile) as ComposeSpecification,
|
||||
"chat-app",
|
||||
false,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("raw remote compose conversion preserves models", async () => {
|
||||
vi.mocked(execAsyncRemote).mockResolvedValue({
|
||||
stdout: "services:\n dropped:\n image: alpine:latest\n",
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const converted = await addDomainToCompose(
|
||||
rawCompose({ serverId: "remote-server" }),
|
||||
[],
|
||||
);
|
||||
|
||||
assertModelsPreserved(converted);
|
||||
expect(execAsyncRemote).not.toHaveBeenCalled();
|
||||
|
||||
const written = parse(
|
||||
stringify(converted, { lineWidth: 1000 }),
|
||||
) as ComposeSpecification;
|
||||
assertModelsPreserved(written);
|
||||
});
|
||||
|
||||
test("invalid YAML still fails at parse", async () => {
|
||||
await expect(
|
||||
addDomainToCompose(rawCompose({ composeFile: "services: [" }), []),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
@ -30,6 +30,7 @@ const dockerComposeServices = [
|
||||
{ label: "networks", type: "keyword", info: "Define networks" },
|
||||
{ label: "configs", type: "keyword", info: "Define configuration files" },
|
||||
{ label: "secrets", type: "keyword", info: "Define secrets" },
|
||||
{ label: "models", type: "keyword", info: "Define AI models" },
|
||||
].map((opt) => ({
|
||||
...opt,
|
||||
apply: (
|
||||
@ -83,6 +84,11 @@ const dockerComposeServiceOptions = [
|
||||
},
|
||||
{ label: "restart", type: "keyword", info: "Restart policy" },
|
||||
{ label: "networks", type: "keyword", info: "Networks to join" },
|
||||
{
|
||||
label: "models",
|
||||
type: "keyword",
|
||||
info: "AI models to use, referencing top-level models",
|
||||
},
|
||||
].map((opt) => ({
|
||||
...opt,
|
||||
apply: (
|
||||
|
||||
@ -35,6 +35,16 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"models": {
|
||||
"id": "#/properties/models",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/model"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"networks": {
|
||||
"id": "#/properties/networks",
|
||||
"type": "object",
|
||||
@ -335,6 +345,30 @@
|
||||
"mem_swappiness": { "type": ["integer", "string"] },
|
||||
"memswap_limit": { "type": ["number", "string"] },
|
||||
"network_mode": { "type": "string" },
|
||||
"models": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/definitions/list_of_strings" },
|
||||
{
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoint_var": { "type": "string" },
|
||||
"model_var": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": { "^x-": {} }
|
||||
},
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"networks": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/definitions/list_of_strings" },
|
||||
@ -890,6 +924,23 @@
|
||||
"patternProperties": { "^x-": {} }
|
||||
},
|
||||
|
||||
"model": {
|
||||
"id": "#/definitions/model",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"model": { "type": "string" },
|
||||
"context_size": { "type": "integer" },
|
||||
"runtime_flags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["model"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": { "^x-": {} }
|
||||
},
|
||||
|
||||
"command": {
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
|
||||
@ -497,6 +497,7 @@ export interface ComposeSpecification {
|
||||
*/
|
||||
include?: DefinitionsInclude[];
|
||||
services?: PropertiesServices;
|
||||
models?: PropertiesModels;
|
||||
networks?: PropertiesNetworks;
|
||||
volumes?: PropertiesVolumes;
|
||||
secrets?: PropertiesSecrets;
|
||||
@ -648,6 +649,23 @@ export interface DefinitionsService {
|
||||
mem_swappiness?: number;
|
||||
memswap_limit?: number | string;
|
||||
network_mode?: string;
|
||||
models?:
|
||||
| ListOfStrings
|
||||
| {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
[k: string]: {
|
||||
endpoint_var?: string;
|
||||
model_var?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
networks?:
|
||||
| ListOfStrings
|
||||
| {
|
||||
@ -877,3 +895,21 @@ export interface DefinitionsConfig {
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface PropertiesModels {
|
||||
[k: string]: DefinitionsModel;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `PropertiesModels`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export interface DefinitionsModel {
|
||||
name?: string;
|
||||
model: string;
|
||||
context_size?: number;
|
||||
runtime_flags?: string[];
|
||||
/**
|
||||
* This interface was referenced by `DefinitionsModel`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user