localpi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/src/cli/cli.js +57 -0
- package/dist/src/cli/main.js +12 -0
- package/dist/src/common/json.js +21 -0
- package/dist/src/common/result.js +9 -0
- package/dist/src/llm/openai.js +78 -0
- package/dist/src/llm/types.js +1 -0
- package/dist/src/localpi/catalog.js +191 -0
- package/dist/src/localpi/llama-server.js +505 -0
- package/dist/src/localpi/managed-runtime.js +225 -0
- package/dist/src/localpi/models.js +169 -0
- package/dist/src/localpi/options.js +240 -0
- package/dist/src/localpi/provider-registry.js +121 -0
- package/dist/src/localpi/runtime-connection.js +75 -0
- package/dist/src/localpi/runtime-selection.js +75 -0
- package/dist/src/localpi/runtime-types.js +1 -0
- package/dist/src/localpi/runtime.js +89 -0
- package/dist/src/pi/config.js +108 -0
- package/dist/src/pi/extensions.js +348 -0
- package/dist/src/pi/launch.js +64 -0
- package/docs/2026-06-15-model-catalog-implementation-plan.md +220 -0
- package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +129 -0
- package/docs/implementation-plan.md +75 -0
- package/docs/runtime-specification.md +148 -0
- package/docs/structured-output.md +9 -0
- package/package.json +54 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { asObject, optionalString } from "../common/json.js";
|
|
5
|
+
import { normalizeBaseUrl } from "../llm/openai.js";
|
|
6
|
+
import { llamaBaseUrl } from "./llama-server.js";
|
|
7
|
+
export async function providerConfigs(options) {
|
|
8
|
+
switch (options.runtime) {
|
|
9
|
+
case "auto":
|
|
10
|
+
return autoProviderConfigs(options, await configuredProviderConfigs(options));
|
|
11
|
+
case "lmstudio":
|
|
12
|
+
return [lmStudioProvider(options.baseUrl)];
|
|
13
|
+
case "vllm":
|
|
14
|
+
return [vllmProvider(options.baseUrl)];
|
|
15
|
+
case "openai-compatible": {
|
|
16
|
+
const providerId = options.provider ?? options.customProviderId;
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
id: providerId,
|
|
20
|
+
name: providerId,
|
|
21
|
+
type: "openai-compatible",
|
|
22
|
+
baseUrl: requiredBaseUrl(options),
|
|
23
|
+
discover: true
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
case "llama-server":
|
|
28
|
+
return [managedLlamaProvider()];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function autoProviderConfigs(options, configured) {
|
|
32
|
+
const managedBaseUrl = llamaBaseUrl(options);
|
|
33
|
+
return dedupeProviderConfigs([
|
|
34
|
+
lmStudioProvider(),
|
|
35
|
+
vllmProvider(),
|
|
36
|
+
...configured,
|
|
37
|
+
managedLlamaProvider()
|
|
38
|
+
]).filter((config) => shouldProbeProvider(config, managedBaseUrl));
|
|
39
|
+
}
|
|
40
|
+
function shouldProbeProvider(config, managedBaseUrl) {
|
|
41
|
+
return (config.type === "managed-llama-server" ||
|
|
42
|
+
config.baseUrl === undefined ||
|
|
43
|
+
normalizeBaseUrl(config.baseUrl) !== managedBaseUrl);
|
|
44
|
+
}
|
|
45
|
+
function lmStudioProvider(baseUrl = "http://127.0.0.1:1234/v1") {
|
|
46
|
+
return {
|
|
47
|
+
id: "lmstudio",
|
|
48
|
+
name: "LM Studio",
|
|
49
|
+
type: "openai-compatible",
|
|
50
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
51
|
+
discover: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function vllmProvider(baseUrl = "http://127.0.0.1:8000/v1") {
|
|
55
|
+
return {
|
|
56
|
+
id: "vllm",
|
|
57
|
+
name: "vLLM",
|
|
58
|
+
type: "openai-compatible",
|
|
59
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
60
|
+
discover: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function managedLlamaProvider() {
|
|
64
|
+
return {
|
|
65
|
+
id: "llama-server",
|
|
66
|
+
name: "llama-server",
|
|
67
|
+
type: "managed-llama-server",
|
|
68
|
+
discover: true
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function configuredProviderConfigs(options) {
|
|
72
|
+
const configPath = configuredProvidersPath(options);
|
|
73
|
+
if (configPath === undefined) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const raw = await readFile(expandHome(configPath), "utf8");
|
|
77
|
+
const root = asObject(JSON.parse(raw), "provider registry");
|
|
78
|
+
const providers = root["providers"];
|
|
79
|
+
if (providers === undefined) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return Object.entries(asObject(providers, "provider registry providers")).map(([id, value]) => configuredProvider(id, value));
|
|
83
|
+
}
|
|
84
|
+
function configuredProvider(id, value) {
|
|
85
|
+
const entry = asObject(value, `provider ${id}`);
|
|
86
|
+
const type = optionalString(entry["type"]);
|
|
87
|
+
if (type !== "openai-compatible") {
|
|
88
|
+
throw new Error(`provider ${id} type must be openai-compatible`);
|
|
89
|
+
}
|
|
90
|
+
const baseUrl = optionalString(entry["baseUrl"]);
|
|
91
|
+
if (baseUrl === undefined) {
|
|
92
|
+
throw new Error(`provider ${id} must define baseUrl`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
name: optionalString(entry["name"]) ?? id,
|
|
97
|
+
type,
|
|
98
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
99
|
+
discover: entry["discover"] !== false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function configuredProvidersPath(options) {
|
|
103
|
+
return options.providersFile ?? process.env["LOCALPI_MODELS_FILE"];
|
|
104
|
+
}
|
|
105
|
+
function dedupeProviderConfigs(configs) {
|
|
106
|
+
const byId = new Map();
|
|
107
|
+
for (const config of configs) {
|
|
108
|
+
byId.set(config.id, config);
|
|
109
|
+
}
|
|
110
|
+
return [...byId.values()];
|
|
111
|
+
}
|
|
112
|
+
function requiredBaseUrl(options) {
|
|
113
|
+
if (options.baseUrl === undefined) {
|
|
114
|
+
throw new Error("--runtime openai-compatible requires --base-url");
|
|
115
|
+
}
|
|
116
|
+
return options.baseUrl;
|
|
117
|
+
}
|
|
118
|
+
function expandHome(value) {
|
|
119
|
+
const home = os.homedir();
|
|
120
|
+
return value === "~" || value.startsWith("~/") ? path.join(home, value.slice(2)) : value;
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { managedModelSupportsReasoning } from "./catalog.js";
|
|
2
|
+
export function connectionStatus(connection) {
|
|
3
|
+
return ([
|
|
4
|
+
`runtime: ${connection.runtime}`,
|
|
5
|
+
`provider: ${connection.providerId}`,
|
|
6
|
+
`base url: ${connection.baseUrl}`,
|
|
7
|
+
`model: ${connection.model}`,
|
|
8
|
+
`available models: ${connection.availableModels.join(", ")}`,
|
|
9
|
+
`context window: ${String(connection.contextWindow ?? "unspecified")}`,
|
|
10
|
+
...connection.warnings.map((warning) => `warning: ${warning}`)
|
|
11
|
+
].join("\n") + "\n");
|
|
12
|
+
}
|
|
13
|
+
export function statusModelList(models) {
|
|
14
|
+
return models.length === 0
|
|
15
|
+
? "none"
|
|
16
|
+
: models.map((model) => `${model.providerId}/${model.modelId}`).join(", ");
|
|
17
|
+
}
|
|
18
|
+
export function catalogRuntimeConnection(options, selected, catalog) {
|
|
19
|
+
const providerModels = catalog.models.filter((model) => model.providerId === selected.providerId && model.availability === "loaded");
|
|
20
|
+
return {
|
|
21
|
+
runtime: connectionRuntimeName(selected),
|
|
22
|
+
providerId: selected.providerId,
|
|
23
|
+
providerName: selected.providerName,
|
|
24
|
+
baseUrl: selected.baseUrl,
|
|
25
|
+
model: selected.modelId,
|
|
26
|
+
availableModels: providerModels.map((model) => model.modelId),
|
|
27
|
+
catalogModels: catalog.models.filter((model) => model.availability === "loaded"),
|
|
28
|
+
warnings: catalog.warnings,
|
|
29
|
+
...optionalContextWindow(options.contextWindow ?? selected.contextWindow)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function catalogModelFromModelInfo(providerId, providerName, runtime, baseUrl, model, options, contextWindow) {
|
|
33
|
+
return {
|
|
34
|
+
providerId,
|
|
35
|
+
providerName,
|
|
36
|
+
runtime,
|
|
37
|
+
baseUrl,
|
|
38
|
+
modelId: model.id,
|
|
39
|
+
aliases: [],
|
|
40
|
+
displayName: `${providerName} / ${model.id}`,
|
|
41
|
+
maxTokens: options.maxTokens,
|
|
42
|
+
...(runtime === "managed-llama-server"
|
|
43
|
+
? { reasoning: managedModelSupportsReasoning(model.id) }
|
|
44
|
+
: {}),
|
|
45
|
+
capabilities: ["text"],
|
|
46
|
+
availability: "loaded",
|
|
47
|
+
...optionalContextWindow(contextWindow ?? model.contextWindow)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function connectionCatalogModels(providerId, providerName, runtime, baseUrl, modelIds, options, contextWindow) {
|
|
51
|
+
return modelIds.map((modelId) => catalogModelFromModelInfo(providerId, providerName, runtime, baseUrl, contextWindow === undefined ? { id: modelId } : { id: modelId, contextWindow }, options, contextWindow));
|
|
52
|
+
}
|
|
53
|
+
export function replaceManagedLoadedModels(models, selected, loaded) {
|
|
54
|
+
return [
|
|
55
|
+
...models.filter((model) => model !== selected &&
|
|
56
|
+
!(model.providerId === "llama-server" && model.availability === "loaded")),
|
|
57
|
+
...loaded
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
export function modelChoiceList(models) {
|
|
61
|
+
return models
|
|
62
|
+
.map((model) => ` ${model.providerId}/${model.modelId} (${model.displayName})`)
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
65
|
+
export function optionalContextWindow(contextWindow) {
|
|
66
|
+
return contextWindow === undefined ? {} : { contextWindow };
|
|
67
|
+
}
|
|
68
|
+
function connectionRuntimeName(selected) {
|
|
69
|
+
if (selected.runtime === "managed-llama-server") {
|
|
70
|
+
return "llama-server";
|
|
71
|
+
}
|
|
72
|
+
return selected.providerId === "lmstudio" || selected.providerId === "vllm"
|
|
73
|
+
? selected.providerId
|
|
74
|
+
: selected.runtime;
|
|
75
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { customPathCatalogModel } from "./managed-runtime.js";
|
|
2
|
+
import { defaultLlamaModelName } from "./models.js";
|
|
3
|
+
import { modelChoiceList } from "./runtime-connection.js";
|
|
4
|
+
export async function selectCatalogModel(options, catalog) {
|
|
5
|
+
const selection = normalizedSelection(options, catalog.models);
|
|
6
|
+
const providerFiltered = modelsForProvider(catalog.models, selection.provider);
|
|
7
|
+
if (providerFiltered.length === 0) {
|
|
8
|
+
const customPath = await customPathCatalogModel(options, selection.provider, selection.model);
|
|
9
|
+
if (customPath !== undefined) {
|
|
10
|
+
return customPath;
|
|
11
|
+
}
|
|
12
|
+
if (selection.provider !== undefined) {
|
|
13
|
+
throw new Error(`provider ${selection.provider} did not report usable models`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (selection.model !== "auto") {
|
|
17
|
+
return selectExplicitCatalogModel(options, providerFiltered, selection.provider, selection.model);
|
|
18
|
+
}
|
|
19
|
+
return selectAutomaticCatalogModel(providerFiltered, catalog.warnings);
|
|
20
|
+
}
|
|
21
|
+
async function selectExplicitCatalogModel(options, models, provider, requested) {
|
|
22
|
+
const matches = matchingCatalogModels(models, requested);
|
|
23
|
+
const [onlyMatch] = matches;
|
|
24
|
+
if (onlyMatch !== undefined && matches.length === 1) {
|
|
25
|
+
return onlyMatch;
|
|
26
|
+
}
|
|
27
|
+
if (matches.length > 1) {
|
|
28
|
+
throw new Error(`model ${requested} is available from multiple providers; choose one with --provider:\n${modelChoiceList(matches)}`);
|
|
29
|
+
}
|
|
30
|
+
const customPath = await customPathCatalogModel(options, provider, requested);
|
|
31
|
+
if (customPath !== undefined) {
|
|
32
|
+
return customPath;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`model ${requested} is not available; choices:\n${modelChoiceList(models)}`);
|
|
35
|
+
}
|
|
36
|
+
function selectAutomaticCatalogModel(models, warnings) {
|
|
37
|
+
const loaded = models.filter((model) => model.availability === "loaded");
|
|
38
|
+
const [onlyLoaded] = loaded;
|
|
39
|
+
if (onlyLoaded !== undefined) {
|
|
40
|
+
return onlyLoaded;
|
|
41
|
+
}
|
|
42
|
+
const fallback = startableFallback(models);
|
|
43
|
+
if (fallback !== undefined) {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`no loaded models available${warnings.length === 0 ? "" : `; ${warnings.join("; ")}`}`);
|
|
47
|
+
}
|
|
48
|
+
function modelsForProvider(models, provider) {
|
|
49
|
+
return provider === undefined ? models : models.filter((model) => model.providerId === provider);
|
|
50
|
+
}
|
|
51
|
+
function normalizedSelection(options, models) {
|
|
52
|
+
const requested = options.model ?? "auto";
|
|
53
|
+
if (options.provider !== undefined || requested === "auto" || isGgufFilePathRequest(requested)) {
|
|
54
|
+
return { provider: options.provider, model: requested };
|
|
55
|
+
}
|
|
56
|
+
const separator = requested.indexOf("/");
|
|
57
|
+
if (separator <= 0) {
|
|
58
|
+
return { provider: options.provider, model: requested };
|
|
59
|
+
}
|
|
60
|
+
const provider = requested.slice(0, separator);
|
|
61
|
+
if (!models.some((model) => model.providerId === provider)) {
|
|
62
|
+
return { provider: options.provider, model: requested };
|
|
63
|
+
}
|
|
64
|
+
return { provider, model: requested.slice(separator + 1) };
|
|
65
|
+
}
|
|
66
|
+
function matchingCatalogModels(models, requested) {
|
|
67
|
+
return models.filter((model) => model.modelId === requested || model.aliases.includes(requested));
|
|
68
|
+
}
|
|
69
|
+
function isGgufFilePathRequest(value) {
|
|
70
|
+
return value.toLowerCase().endsWith(".gguf") || value.includes("\\");
|
|
71
|
+
}
|
|
72
|
+
function startableFallback(models) {
|
|
73
|
+
const startable = models.filter((model) => model.availability === "startable");
|
|
74
|
+
return (startable.find((model) => model.aliases.includes(defaultLlamaModelName()) || model.modelId === defaultLlamaModelName()) ?? startable[0]);
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { discoverModelCatalog } from "./catalog.js";
|
|
2
|
+
import { llamaBaseUrl, llamaServerStatus, stopManagedLlamaServer } from "./llama-server.js";
|
|
3
|
+
import { listModelAliases } from "./models.js";
|
|
4
|
+
import { catalogRuntimeConnection, connectionStatus, statusModelList } from "./runtime-connection.js";
|
|
5
|
+
import { resolveLlamaRuntime, resolveSelectedLlamaRuntime } from "./managed-runtime.js";
|
|
6
|
+
import { selectCatalogModel } from "./runtime-selection.js";
|
|
7
|
+
export { connectionStatus } from "./runtime-connection.js";
|
|
8
|
+
export async function resolveRuntime(options) {
|
|
9
|
+
if (options.runtime === "auto") {
|
|
10
|
+
return resolveCatalogRuntime(options);
|
|
11
|
+
}
|
|
12
|
+
switch (options.runtime) {
|
|
13
|
+
case "llama-server":
|
|
14
|
+
return resolveLlamaRuntime(options);
|
|
15
|
+
case "lmstudio":
|
|
16
|
+
return resolveCatalogRuntime(options);
|
|
17
|
+
case "vllm":
|
|
18
|
+
return resolveCatalogRuntime(options);
|
|
19
|
+
case "openai-compatible":
|
|
20
|
+
return resolveCatalogRuntime(options);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function stopRuntime(options) {
|
|
24
|
+
if (options.runtime === "lmstudio" ||
|
|
25
|
+
options.runtime === "vllm" ||
|
|
26
|
+
options.runtime === "openai-compatible") {
|
|
27
|
+
return `runtime ${options.runtime} is externally managed; nothing stopped`;
|
|
28
|
+
}
|
|
29
|
+
return stopManagedLlamaServer(options);
|
|
30
|
+
}
|
|
31
|
+
export async function statusOutput(options) {
|
|
32
|
+
if (options.runtime === "llama-server") {
|
|
33
|
+
return `${await llamaServerStatus(options)}\n${await aliasListOutput()}`;
|
|
34
|
+
}
|
|
35
|
+
if (statusShouldUseCatalog(options)) {
|
|
36
|
+
return catalogStatusOutput(options);
|
|
37
|
+
}
|
|
38
|
+
const connection = await resolveRuntime(options);
|
|
39
|
+
return connectionStatus(connection);
|
|
40
|
+
}
|
|
41
|
+
export async function aliasListOutput() {
|
|
42
|
+
const aliases = await listModelAliases();
|
|
43
|
+
return aliases
|
|
44
|
+
.map((alias) => {
|
|
45
|
+
const context = alias.contextWindow === undefined ? "" : ` ctx=${String(alias.contextWindow)}`;
|
|
46
|
+
return `${alias.name}: id=${alias.id}${context}\n ${alias.paths.join("\n ")}`;
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
export function effectiveBaseUrl(options) {
|
|
51
|
+
if (options.runtime === "llama-server") {
|
|
52
|
+
return llamaBaseUrl(options);
|
|
53
|
+
}
|
|
54
|
+
if (options.runtime === "openai-compatible") {
|
|
55
|
+
return requiredOpenAiBaseUrl(options);
|
|
56
|
+
}
|
|
57
|
+
return options.baseUrl ?? defaultExternalBaseUrl(options.runtime);
|
|
58
|
+
}
|
|
59
|
+
async function resolveCatalogRuntime(options) {
|
|
60
|
+
const catalog = await discoverModelCatalog(options);
|
|
61
|
+
const selected = await selectCatalogModel(options, catalog);
|
|
62
|
+
if (selected.runtime === "managed-llama-server") {
|
|
63
|
+
return resolveSelectedLlamaRuntime(options, selected, catalog);
|
|
64
|
+
}
|
|
65
|
+
return catalogRuntimeConnection(options, selected, catalog);
|
|
66
|
+
}
|
|
67
|
+
function statusShouldUseCatalog(options) {
|
|
68
|
+
return options.runtime === "auto" || options.model === undefined || options.model === "auto";
|
|
69
|
+
}
|
|
70
|
+
async function catalogStatusOutput(options) {
|
|
71
|
+
const catalog = await discoverModelCatalog(options);
|
|
72
|
+
const loaded = catalog.models.filter((model) => model.availability === "loaded");
|
|
73
|
+
const startable = catalog.models.filter((model) => model.availability === "startable");
|
|
74
|
+
return ([
|
|
75
|
+
`runtime: ${options.runtime}`,
|
|
76
|
+
`loaded models: ${statusModelList(loaded)}`,
|
|
77
|
+
`startable models: ${statusModelList(startable)}`,
|
|
78
|
+
...catalog.warnings.map((warning) => `warning: ${warning}`)
|
|
79
|
+
].join("\n") + "\n");
|
|
80
|
+
}
|
|
81
|
+
function requiredOpenAiBaseUrl(options) {
|
|
82
|
+
if (options.baseUrl === undefined) {
|
|
83
|
+
throw new Error("--runtime openai-compatible requires --base-url");
|
|
84
|
+
}
|
|
85
|
+
return options.baseUrl;
|
|
86
|
+
}
|
|
87
|
+
function defaultExternalBaseUrl(runtime) {
|
|
88
|
+
return runtime === "vllm" ? "http://127.0.0.1:8000/v1" : "http://127.0.0.1:1234/v1";
|
|
89
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function writeRuntimeConfig(options, connection) {
|
|
4
|
+
const configDir = path.join(options.stateDir, "pi-config-runtime");
|
|
5
|
+
await mkdir(configDir, { recursive: true });
|
|
6
|
+
const modelsPath = path.join(configDir, "models.json");
|
|
7
|
+
const settingsPath = path.join(configDir, "settings.json");
|
|
8
|
+
await writeFile(modelsPath, `${JSON.stringify(modelsConfig(options, connection), null, 2)}\n`);
|
|
9
|
+
await writeFile(settingsPath, `${JSON.stringify(settingsConfig(options, connection), null, 2)}\n`);
|
|
10
|
+
return { configDir, modelsPath, settingsPath };
|
|
11
|
+
}
|
|
12
|
+
function modelsConfig(options, connection) {
|
|
13
|
+
const models = connection.catalogModels.length === 0 ? fallbackCatalog(connection) : connection.catalogModels;
|
|
14
|
+
return {
|
|
15
|
+
providers: Object.fromEntries(groupedByProvider(models).map((entry) => [
|
|
16
|
+
entry.providerId,
|
|
17
|
+
providerConfig(options, connection, entry)
|
|
18
|
+
]))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function groupedByProvider(models) {
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const model of models) {
|
|
24
|
+
const existing = groups.get(model.providerId);
|
|
25
|
+
groups.set(model.providerId, existing === undefined
|
|
26
|
+
? { providerId: model.providerId, baseUrl: model.baseUrl, models: [model] }
|
|
27
|
+
: { ...existing, models: [...existing.models, model] });
|
|
28
|
+
}
|
|
29
|
+
return [...groups.values()];
|
|
30
|
+
}
|
|
31
|
+
function providerConfig(options, connection, group) {
|
|
32
|
+
return {
|
|
33
|
+
baseUrl: group.baseUrl,
|
|
34
|
+
api: "openai-completions",
|
|
35
|
+
apiKey: "local",
|
|
36
|
+
compat: {
|
|
37
|
+
supportsDeveloperRole: false,
|
|
38
|
+
supportsReasoningEffort: false
|
|
39
|
+
},
|
|
40
|
+
models: group.models.map((model) => modelConfig(options, connection, model))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function modelConfig(options, connection, model) {
|
|
44
|
+
return withoutUndefined({
|
|
45
|
+
id: model.modelId,
|
|
46
|
+
name: model.displayName,
|
|
47
|
+
reasoning: model.reasoning ?? false,
|
|
48
|
+
compat: model.thinkingFormat === undefined ? undefined : { thinkingFormat: model.thinkingFormat },
|
|
49
|
+
input: ["text"],
|
|
50
|
+
contextWindow: modelContextWindow(options, model),
|
|
51
|
+
maxTokens: model.maxTokens ?? options.maxTokens,
|
|
52
|
+
cost: {
|
|
53
|
+
input: 0,
|
|
54
|
+
output: 0,
|
|
55
|
+
cacheRead: 0,
|
|
56
|
+
cacheWrite: 0
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function modelContextWindow(options, model) {
|
|
61
|
+
if (options.contextWindow !== undefined) {
|
|
62
|
+
return options.contextWindow;
|
|
63
|
+
}
|
|
64
|
+
return model.contextWindow;
|
|
65
|
+
}
|
|
66
|
+
function fallbackCatalog(connection) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
providerId: connection.providerId,
|
|
70
|
+
providerName: connection.providerName,
|
|
71
|
+
runtime: connection.runtime.startsWith("llama-server")
|
|
72
|
+
? "managed-llama-server"
|
|
73
|
+
: "openai-compatible",
|
|
74
|
+
baseUrl: connection.baseUrl,
|
|
75
|
+
modelId: connection.model,
|
|
76
|
+
aliases: [],
|
|
77
|
+
displayName: `Local model (${connection.model})`,
|
|
78
|
+
reasoning: false,
|
|
79
|
+
capabilities: ["text"],
|
|
80
|
+
availability: "loaded",
|
|
81
|
+
...(connection.contextWindow === undefined ? {} : { contextWindow: connection.contextWindow })
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
function withoutUndefined(value) {
|
|
86
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
|
|
87
|
+
}
|
|
88
|
+
function settingsConfig(options, connection) {
|
|
89
|
+
const contextWindow = options.contextWindow ?? connection.contextWindow;
|
|
90
|
+
return {
|
|
91
|
+
defaultProvider: connection.providerId,
|
|
92
|
+
defaultModel: connection.model,
|
|
93
|
+
defaultThinkingLevel: options.thinking,
|
|
94
|
+
enableInstallTelemetry: false,
|
|
95
|
+
quietStartup: true,
|
|
96
|
+
compaction: compactionConfig(contextWindow)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function compactionConfig(contextWindow) {
|
|
100
|
+
if (contextWindow === undefined) {
|
|
101
|
+
return { enabled: false };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
enabled: true,
|
|
105
|
+
reserveTokens: Math.max(256, Math.min(16384, Math.floor(contextWindow / 4))),
|
|
106
|
+
keepRecentTokens: Math.max(512, Math.min(20000, Math.floor(contextWindow / 2)))
|
|
107
|
+
};
|
|
108
|
+
}
|