saeeol 1.0.9 → 1.1.1
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/npm/bin/saeeol +42 -0
- package/npm/package.json +39 -0
- package/npm/postinstall.js +162 -0
- package/package.json +2 -2
- package/src/cli/cmd/mcp-refresh.ts +47 -0
- package/src/cli/cmd/mcp.ts +3 -1
- package/src/cli/cmd/tui/app-commands-core.tsx +11 -0
- package/src/cli/cmd/tui/app-commands-system.tsx +20 -0
- package/src/cli/cmd/tui/app-events.ts +43 -0
- package/src/cli/cmd/tui/app.tsx +4 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +1 -1
- package/src/cli/cmd/tui/component/use-connected.tsx +1 -1
- package/src/cli/cmd/tui/context/local.tsx +10 -3
- package/src/cli/cmd/tui/context/route.tsx +5 -1
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +1 -1
- package/src/cli/cmd/tui/plugin/api.tsx +7 -3
- package/src/cli/cmd/tui/routes/local-models.tsx +151 -0
- package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +1 -1
- package/src/cli/cmd/tui/util/model.ts +1 -1
- package/src/config/config-schema.ts +44 -0
- package/src/ltm/config.ts +124 -0
- package/src/ltm/events.ts +50 -0
- package/src/ltm/index.ts +12 -0
- package/src/ltm/memory/episodic.ts +83 -0
- package/src/ltm/memory/procedural.ts +102 -0
- package/src/ltm/memory/semantic.ts +80 -0
- package/src/ltm/pipeline.ts +155 -0
- package/src/ltm/retrieval.ts +62 -0
- package/src/ltm/scheduler.ts +55 -0
- package/src/ltm/store.ts +150 -0
- package/src/ltm/types.ts +108 -0
- package/src/mcp/index.ts +32 -1
- package/src/provider/custom-loaders.ts +12 -0
- package/src/provider/loader-local.ts +185 -0
- package/src/provider/local/embedder.ts +220 -0
- package/src/provider/local/events.ts +74 -0
- package/src/provider/local/gpu.ts +93 -0
- package/src/provider/local/hub.ts +174 -0
- package/src/provider/local/index.ts +10 -0
- package/src/provider/local/model-manager.ts +113 -0
- package/src/provider/local/orchestrator.ts +301 -0
- package/src/provider/local/rag.ts +112 -0
- package/src/provider/local/types.ts +142 -0
- package/src/provider/provider-conversion.ts +2 -0
- package/src/provider/provider-schema.ts +17 -2
- package/src/provider/provider-schemas.ts +10 -3
- package/src/provider/provider-state.ts +10 -2
- package/src/provider/provider.ts +2 -1
- package/src/saeeol/plugins/sidebar-usage.tsx +1 -1
- package/src/server/routes/instance/config.ts +1 -1
- package/src/server/routes/instance/httpapi/api.ts +2 -0
- package/src/server/routes/instance/httpapi/groups/local.ts +87 -0
- package/src/server/routes/instance/httpapi/groups/mcp.ts +10 -0
- package/src/server/routes/instance/httpapi/handlers/local.ts +95 -0
- package/src/server/routes/instance/httpapi/handlers/mcp.ts +5 -0
- package/src/server/routes/instance/httpapi/handlers/provider.ts +1 -1
- package/src/server/routes/instance/httpapi/server.ts +2 -0
- package/src/server/routes/instance/provider.ts +2 -2
- package/src/session/prompt-reminders.ts +29 -0
- package/test/fake/provider.ts +1 -0
- package/test/provider/local.test.ts +208 -0
- package/test/provider/provider-category.test.ts +190 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** RAG support — embedding models, rerankers, vector DB management */
|
|
2
|
+
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
import * as Log from "@saeeol/core/util/log"
|
|
5
|
+
import type { RAGAsset, RAGAssetType } from "./types"
|
|
6
|
+
import * as Hub from "./hub"
|
|
7
|
+
import * as Manager from "./model-manager"
|
|
8
|
+
|
|
9
|
+
const log = Log.create({ service: "local/rag" })
|
|
10
|
+
|
|
11
|
+
/** Well-known embedding models with typical dimensions */
|
|
12
|
+
export const EMBEDDING_MODELS: Array<{ id: string; repo: string; name: string; dimensions: number; sizeBytes: number }> = [
|
|
13
|
+
{ id: "bge-small-en", repo: "BAAI/bge-small-en-v1.5", name: "BGE Small English", dimensions: 384, sizeBytes: 130_000_000 },
|
|
14
|
+
{ id: "bge-base-en", repo: "BAAI/bge-base-en-v1.5", name: "BGE Base English", dimensions: 768, sizeBytes: 420_000_000 },
|
|
15
|
+
{ id: "bge-large-en", repo: "BAAI/bge-large-en-v1.5", name: "BGE Large English", dimensions: 1024, sizeBytes: 1_300_000_000 },
|
|
16
|
+
{ id: "bge-m3", repo: "BAAI/bge-m3", name: "BGE M3 Multilingual", dimensions: 1024, sizeBytes: 2_200_000_000 },
|
|
17
|
+
{ id: "nomic-embed", repo: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text", dimensions: 768, sizeBytes: 550_000_000 },
|
|
18
|
+
{ id: "all-minilm-l6", repo: "sentence-transformers/all-MiniLM-L6-v2", name: "MiniLM L6", dimensions: 384, sizeBytes: 80_000_000 },
|
|
19
|
+
{ id: "gte-small", repo: "Alibaba-NLP/gte-base-en-v1.5", name: "GTE Base", dimensions: 768, sizeBytes: 430_000_000 },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/** Well-known reranker models */
|
|
23
|
+
export const RERANKER_MODELS: Array<{ id: string; repo: string; name: string; sizeBytes: number }> = [
|
|
24
|
+
{ id: "bge-reranker-base", repo: "BAAI/bge-reranker-base", name: "BGE Reranker Base", sizeBytes: 420_000_000 },
|
|
25
|
+
{ id: "bge-reranker-large", repo: "BAAI/bge-reranker-large", name: "BGE Reranker Large", sizeBytes: 1_300_000_000 },
|
|
26
|
+
{ id: "ms-marco-minilm", repo: "cross-encoder/ms-marco-MiniLM-L-6-v2", name: "MS MARCO MiniLM", sizeBytes: 80_000_000 },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/** Build a RAGAsset from a known embedding model */
|
|
30
|
+
export function embeddingAsset(model: typeof EMBEDDING_MODELS[number]): RAGAsset {
|
|
31
|
+
return {
|
|
32
|
+
id: model.id,
|
|
33
|
+
name: model.name,
|
|
34
|
+
type: "embedding",
|
|
35
|
+
repo: model.repo,
|
|
36
|
+
format: "safetensors",
|
|
37
|
+
sizeBytes: model.sizeBytes,
|
|
38
|
+
dimensions: model.dimensions,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build a RAGAsset from a known reranker model */
|
|
43
|
+
export function rerankerAsset(model: typeof RERANKER_MODELS[number]): RAGAsset {
|
|
44
|
+
return {
|
|
45
|
+
id: model.id,
|
|
46
|
+
name: model.name,
|
|
47
|
+
type: "reranker",
|
|
48
|
+
repo: model.repo,
|
|
49
|
+
format: "safetensors",
|
|
50
|
+
sizeBytes: model.sizeBytes,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Install an embedding model for RAG */
|
|
55
|
+
export async function installEmbedding(
|
|
56
|
+
model: typeof EMBEDDING_MODELS[number],
|
|
57
|
+
opts?: { onProgress?: (downloaded: number, total: number) => void; signal?: AbortSignal },
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const asset = embeddingAsset(model)
|
|
60
|
+
log.info("installing embedding model", { repo: asset.repo })
|
|
61
|
+
|
|
62
|
+
// Download model config and weights
|
|
63
|
+
const files = ["config.json", "tokenizer.json", "tokenizer_config.json"]
|
|
64
|
+
let lastPath = ""
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
try {
|
|
67
|
+
lastPath = await Manager.installRAG(
|
|
68
|
+
{ repo: asset.repo, filename: file, sizeBytes: 0 },
|
|
69
|
+
opts,
|
|
70
|
+
)
|
|
71
|
+
} catch { /* non-critical */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Download safetensors weights
|
|
75
|
+
const weightFile = "model.safetensors"
|
|
76
|
+
lastPath = await Manager.installRAG(
|
|
77
|
+
{ repo: asset.repo, filename: weightFile, sizeBytes: asset.sizeBytes },
|
|
78
|
+
opts,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return lastPath
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Install a reranker model */
|
|
85
|
+
export async function installReranker(
|
|
86
|
+
model: typeof RERANKER_MODELS[number],
|
|
87
|
+
opts?: { onProgress?: (downloaded: number, total: number) => void; signal?: AbortSignal },
|
|
88
|
+
): Promise<string> {
|
|
89
|
+
const asset = rerankerAsset(model)
|
|
90
|
+
log.info("installing reranker model", { repo: asset.repo })
|
|
91
|
+
|
|
92
|
+
const weightFile = "model.safetensors"
|
|
93
|
+
return Manager.installRAG(
|
|
94
|
+
{ repo: asset.repo, filename: weightFile, sizeBytes: asset.sizeBytes },
|
|
95
|
+
opts,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** List recommended RAG assets that fit in available VRAM */
|
|
100
|
+
export async function recommendForVRAM(vramMB: number): Promise<{
|
|
101
|
+
embeddings: typeof EMBEDDING_MODELS
|
|
102
|
+
rerankers: typeof RERANKER_MODELS
|
|
103
|
+
}> {
|
|
104
|
+
const embeddings = EMBEDDING_MODELS.filter((m) => m.sizeBytes * 1.2 <= vramMB * 1024 * 1024)
|
|
105
|
+
const rerankers = RERANKER_MODELS.filter((m) => m.sizeBytes * 1.2 <= vramMB * 1024 * 1024)
|
|
106
|
+
return { embeddings, rerankers }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Search for RAG-compatible models on HuggingFace */
|
|
110
|
+
export async function search(query?: string, type?: RAGAssetType) {
|
|
111
|
+
return Hub.searchRAG(type ?? "embedding", query, 20)
|
|
112
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** Local model orchestration — shared types */
|
|
2
|
+
|
|
3
|
+
import { Schema } from "effect"
|
|
4
|
+
import { optionalOmitUndefined } from "@/util/schema"
|
|
5
|
+
|
|
6
|
+
// ── GPU ──
|
|
7
|
+
|
|
8
|
+
export const GPUInfo = Schema.Struct({
|
|
9
|
+
index: Schema.Number,
|
|
10
|
+
name: Schema.String,
|
|
11
|
+
vramTotalMB: Schema.Number,
|
|
12
|
+
vramUsedMB: Schema.Number,
|
|
13
|
+
vramFreeMB: Schema.Number,
|
|
14
|
+
computeCapability: optionalOmitUndefined(Schema.String),
|
|
15
|
+
driverVersion: optionalOmitUndefined(Schema.String),
|
|
16
|
+
cudaVersion: optionalOmitUndefined(Schema.String),
|
|
17
|
+
})
|
|
18
|
+
export type GPUInfo = Schema.Schema.Type<typeof GPUInfo>
|
|
19
|
+
|
|
20
|
+
export const GPUProfile = Schema.Struct({
|
|
21
|
+
gpus: Schema.Array(GPUInfo),
|
|
22
|
+
totalVRAMMB: Schema.Number,
|
|
23
|
+
availableVRAMMB: Schema.Number,
|
|
24
|
+
cudaAvailable: Schema.Boolean,
|
|
25
|
+
})
|
|
26
|
+
export type GPUProfile = Schema.Schema.Type<typeof GPUProfile>
|
|
27
|
+
|
|
28
|
+
// ── Model Artifact ──
|
|
29
|
+
|
|
30
|
+
export const Quantization = Schema.Literals([
|
|
31
|
+
"q2_k", "q3_k_s", "q3_k_m", "q3_k_l",
|
|
32
|
+
"q4_0", "q4_1", "q4_k_s", "q4_k_m",
|
|
33
|
+
"q5_0", "q5_1", "q5_k_s", "q5_k_m",
|
|
34
|
+
"q6_k", "q8_0", "fp16", "bf16", "fp32",
|
|
35
|
+
])
|
|
36
|
+
export type Quantization = Schema.Schema.Type<typeof Quantization>
|
|
37
|
+
|
|
38
|
+
export const ModelFormat = Schema.Literals(["gguf", "safetensors", "pytorch", "onnx", "awq", "gptq"])
|
|
39
|
+
export type ModelFormat = Schema.Schema.Type<typeof ModelFormat>
|
|
40
|
+
|
|
41
|
+
export const ModelArtifact = Schema.Struct({
|
|
42
|
+
id: Schema.String,
|
|
43
|
+
repo: Schema.String, // e.g. "meta-llama/Llama-3.1-8B-Instruct"
|
|
44
|
+
filename: Schema.String, // e.g. "model-q4_k_m.gguf"
|
|
45
|
+
format: ModelFormat,
|
|
46
|
+
quantization: Quantization,
|
|
47
|
+
sizeBytes: Schema.Number,
|
|
48
|
+
sha256: optionalOmitUndefined(Schema.String),
|
|
49
|
+
})
|
|
50
|
+
export type ModelArtifact = Schema.Schema.Type<typeof ModelArtifact>
|
|
51
|
+
|
|
52
|
+
// ── Model Instance (running process) ──
|
|
53
|
+
|
|
54
|
+
export const ModelStatus = Schema.Literals(["stopped", "starting", "running", "error", "downloading"])
|
|
55
|
+
export type ModelStatus = Schema.Schema.Type<typeof ModelStatus>
|
|
56
|
+
|
|
57
|
+
export const ModelInstance = Schema.Struct({
|
|
58
|
+
id: Schema.String,
|
|
59
|
+
artifact: ModelArtifact,
|
|
60
|
+
status: ModelStatus,
|
|
61
|
+
pid: optionalOmitUndefined(Schema.Number),
|
|
62
|
+
port: optionalOmitUndefined(Schema.Number),
|
|
63
|
+
gpuIndex: optionalOmitUndefined(Schema.Number),
|
|
64
|
+
vramUsageMB: optionalOmitUndefined(Schema.Number),
|
|
65
|
+
endpoint: optionalOmitUndefined(Schema.String),
|
|
66
|
+
error: optionalOmitUndefined(Schema.String),
|
|
67
|
+
})
|
|
68
|
+
export type ModelInstance = Schema.Schema.Type<typeof ModelInstance>
|
|
69
|
+
|
|
70
|
+
// ── Backend (Ollama, LM Studio, vLLM, etc.) ──
|
|
71
|
+
|
|
72
|
+
export const BackendType = Schema.Literals(["ollama", "lmstudio", "vllm", "llama.cpp", "text-generation-webui"])
|
|
73
|
+
export type BackendType = Schema.Schema.Type<typeof BackendType>
|
|
74
|
+
|
|
75
|
+
export const BackendStatus = Schema.Struct({
|
|
76
|
+
type: BackendType,
|
|
77
|
+
available: Schema.Boolean,
|
|
78
|
+
endpoint: Schema.String,
|
|
79
|
+
version: optionalOmitUndefined(Schema.String),
|
|
80
|
+
loadedModels: Schema.Array(Schema.String),
|
|
81
|
+
})
|
|
82
|
+
export type BackendStatus = Schema.Schema.Type<typeof BackendStatus>
|
|
83
|
+
|
|
84
|
+
// ── RAG / Vector DB ──
|
|
85
|
+
|
|
86
|
+
export const RAGAssetType = Schema.Literals(["embedding", "reranker", "vectordb"])
|
|
87
|
+
export type RAGAssetType = Schema.Schema.Type<typeof RAGAssetType>
|
|
88
|
+
|
|
89
|
+
export const RAGAsset = Schema.Struct({
|
|
90
|
+
id: Schema.String,
|
|
91
|
+
name: Schema.String,
|
|
92
|
+
type: RAGAssetType,
|
|
93
|
+
repo: Schema.String,
|
|
94
|
+
format: ModelFormat,
|
|
95
|
+
sizeBytes: Schema.Number,
|
|
96
|
+
dimensions: optionalOmitUndefined(Schema.Number),
|
|
97
|
+
})
|
|
98
|
+
export type RAGAsset = Schema.Schema.Type<typeof RAGAsset>
|
|
99
|
+
|
|
100
|
+
// ── Download Progress ──
|
|
101
|
+
|
|
102
|
+
export const DownloadState = Schema.Struct({
|
|
103
|
+
id: Schema.String,
|
|
104
|
+
bytesDownloaded: Schema.Number,
|
|
105
|
+
bytesTotal: Schema.Number,
|
|
106
|
+
speedMBps: Schema.Finite,
|
|
107
|
+
eta: optionalOmitUndefined(Schema.Number),
|
|
108
|
+
done: Schema.Boolean,
|
|
109
|
+
error: optionalOmitUndefined(Schema.String),
|
|
110
|
+
})
|
|
111
|
+
export type DownloadState = Schema.Schema.Type<typeof DownloadState>
|
|
112
|
+
|
|
113
|
+
// ── HuggingFace Hub ──
|
|
114
|
+
|
|
115
|
+
export const HFModelSearch = Schema.Struct({
|
|
116
|
+
id: Schema.String,
|
|
117
|
+
name: Schema.String,
|
|
118
|
+
author: Schema.String,
|
|
119
|
+
downloads: Schema.Number,
|
|
120
|
+
likes: Schema.Number,
|
|
121
|
+
tags: Schema.Array(Schema.String),
|
|
122
|
+
pipelineTag: optionalOmitUndefined(Schema.String),
|
|
123
|
+
libraryName: optionalOmitUndefined(Schema.String),
|
|
124
|
+
})
|
|
125
|
+
export type HFModelSearch = Schema.Schema.Type<typeof HFModelSearch>
|
|
126
|
+
|
|
127
|
+
export const HFSibling = Schema.Struct({
|
|
128
|
+
rfilename: Schema.String,
|
|
129
|
+
})
|
|
130
|
+
export type HFSibling = Schema.Schema.Type<typeof HFSibling>
|
|
131
|
+
|
|
132
|
+
export const HFModelInfo = Schema.Struct({
|
|
133
|
+
id: Schema.String,
|
|
134
|
+
modelId: Schema.String,
|
|
135
|
+
sha: Schema.String,
|
|
136
|
+
siblings: Schema.Array(HFSibling),
|
|
137
|
+
tags: Schema.Array(Schema.String),
|
|
138
|
+
downloads: Schema.Number,
|
|
139
|
+
likes: Schema.Number,
|
|
140
|
+
private: Schema.Boolean,
|
|
141
|
+
})
|
|
142
|
+
export type HFModelInfo = Schema.Schema.Type<typeof HFModelInfo>
|
|
@@ -115,6 +115,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
115
115
|
id: ProviderID.make(provider.id),
|
|
116
116
|
source: "custom",
|
|
117
117
|
name: provider.name,
|
|
118
|
+
category: "custom" as const,
|
|
118
119
|
env: [...(provider.env ?? [])],
|
|
119
120
|
options: {},
|
|
120
121
|
models,
|
|
@@ -135,6 +136,7 @@ export function applyConfigModels(
|
|
|
135
136
|
env: provider.env ?? existing?.env ?? [],
|
|
136
137
|
options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
|
137
138
|
source: "config",
|
|
139
|
+
category: existing?.category ?? "cloud",
|
|
138
140
|
models: existing?.models ?? {},
|
|
139
141
|
}
|
|
140
142
|
|
|
@@ -79,17 +79,22 @@ export const Model = Schema.Struct({
|
|
|
79
79
|
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
80
80
|
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
|
81
81
|
|
|
82
|
+
// Provider categories
|
|
83
|
+
export const ProviderCategory = Schema.Literals(["local", "custom", "cloud"])
|
|
84
|
+
export type ProviderCategory = typeof ProviderCategory.Type
|
|
85
|
+
|
|
82
86
|
export const Info = Schema.Struct({
|
|
83
87
|
id: ProviderID,
|
|
84
88
|
name: Schema.String,
|
|
89
|
+
category: ProviderCategory,
|
|
85
90
|
source: Schema.Literals(["env", "config", "custom", "api"]),
|
|
86
91
|
env: Schema.Array(Schema.String),
|
|
87
92
|
key: optionalOmitUndefined(Schema.String),
|
|
88
93
|
options: Schema.Record(Schema.String, Schema.Any),
|
|
89
94
|
models: Schema.Record(Schema.String, Model),
|
|
90
95
|
})
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
.annotate({ identifier: "Provider" })
|
|
97
|
+
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
93
98
|
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
|
94
99
|
|
|
95
100
|
const DefaultModelIDs = Schema.Record(Schema.String, Schema.String)
|
|
@@ -99,11 +104,21 @@ export const ListResult = Schema.Struct({
|
|
|
99
104
|
default: DefaultModelIDs,
|
|
100
105
|
connected: Schema.Array(Schema.String),
|
|
101
106
|
failed: Schema.Array(Schema.String),
|
|
107
|
+
categories: Schema.Struct({
|
|
108
|
+
local: Schema.Array(Schema.String),
|
|
109
|
+
custom: Schema.Array(Schema.String),
|
|
110
|
+
cloud: Schema.Array(Schema.String),
|
|
111
|
+
}),
|
|
102
112
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
103
113
|
export type ListResult = Types.DeepMutable<Schema.Schema.Type<typeof ListResult>>
|
|
104
114
|
|
|
105
115
|
export const ConfigProvidersResult = Schema.Struct({
|
|
106
116
|
providers: Schema.Array(Info),
|
|
107
117
|
default: DefaultModelIDs,
|
|
118
|
+
categories: Schema.Struct({
|
|
119
|
+
local: Schema.Array(Schema.String),
|
|
120
|
+
custom: Schema.Array(Schema.String),
|
|
121
|
+
cloud: Schema.Array(Schema.String),
|
|
122
|
+
}),
|
|
108
123
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
109
124
|
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
|
@@ -25,8 +25,11 @@ export const Model = Schema.Struct({
|
|
|
25
25
|
}).annotate({ identifier: "Model" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
26
26
|
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
|
27
27
|
|
|
28
|
+
export const ProviderCategory = Schema.Literals(["local", "custom", "cloud"])
|
|
29
|
+
export type ProviderCategory = typeof ProviderCategory.Type
|
|
30
|
+
|
|
28
31
|
export const Info = Schema.Struct({
|
|
29
|
-
id: ProviderID, name: Schema.String, source: Schema.Literals(["env", "config", "custom", "api"]), env: Schema.Array(Schema.String),
|
|
32
|
+
id: ProviderID, name: Schema.String, category: ProviderCategory, source: Schema.Literals(["env", "config", "custom", "api"]), env: Schema.Array(Schema.String),
|
|
30
33
|
key: optionalOmitUndefined(Schema.String), options: Schema.Record(Schema.String, Schema.Any), models: Schema.Record(Schema.String, Model),
|
|
31
34
|
}).annotate({ identifier: "Provider" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
32
35
|
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
|
@@ -38,7 +41,11 @@ export const ConfigProvidersResult = Schema.Struct({ providers: Schema.Array(Inf
|
|
|
38
41
|
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
|
39
42
|
|
|
40
43
|
export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
|
|
41
|
-
return mapValues(providers, (item) =>
|
|
44
|
+
return mapValues(providers, (item) => {
|
|
45
|
+
const models = Object.values(item.models ?? {})
|
|
46
|
+
if (models.length === 0) return undefined
|
|
47
|
+
return sortModels(models)[0].id
|
|
48
|
+
})
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|
@@ -79,7 +86,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
|
-
return { id: ProviderID.make(provider.id), source: "custom", name: provider.name, env: [...(provider.env ?? [])], options: {}, models }
|
|
89
|
+
return { id: ProviderID.make(provider.id), source: "custom", name: provider.name, category: "custom" as const, env: [...(provider.env ?? [])], options: {}, models }
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
|
@@ -4,10 +4,11 @@ import * as Log from "@saeeol/core/util/log"
|
|
|
4
4
|
import { EffectBridge } from "@/effect/bridge"
|
|
5
5
|
import { InstanceState } from "@/effect/instance-state"
|
|
6
6
|
import { applyConfigModels, cleanupProviders, fromModelsDevProvider } from "./provider-conversion"
|
|
7
|
-
import { custom } from "./custom-loaders"
|
|
7
|
+
import { custom, LOCAL_PROVIDERS, CUSTOM_PROVIDERS, CLOUD_PROVIDERS } from "./custom-loaders"
|
|
8
8
|
import { ModelID, ProviderID } from "./schema"
|
|
9
9
|
import type { State, CustomModelLoader, CustomVarsLoader, CustomDiscoverModels } from "./provider-types"
|
|
10
|
-
import type { Info } from "./provider-
|
|
10
|
+
import type { Info } from "./provider-schemas"
|
|
11
|
+
import type { ProviderCategory } from "./provider-schemas"
|
|
11
12
|
import {
|
|
12
13
|
saeeolCustomLoaders,
|
|
13
14
|
patchCustomLoaderResult,
|
|
@@ -53,8 +54,15 @@ export function initState(deps: {
|
|
|
53
54
|
|
|
54
55
|
log.info("init")
|
|
55
56
|
|
|
57
|
+
function getCategory(id: string): ProviderCategory {
|
|
58
|
+
if (LOCAL_PROVIDERS.includes(id as any)) return "local"
|
|
59
|
+
if (CUSTOM_PROVIDERS.includes(id as any)) return "custom"
|
|
60
|
+
return "cloud"
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
|
57
64
|
const existing = providers[providerID]
|
|
65
|
+
if (!provider.category) provider.category = getCategory(providerID)
|
|
58
66
|
if (existing) {
|
|
59
67
|
// @ts-expect-error
|
|
60
68
|
providers[providerID] = mergeDeep(existing, provider)
|
package/src/provider/provider.ts
CHANGED
|
@@ -31,6 +31,7 @@ export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError",
|
|
|
31
31
|
export const InitError = namedSchemaError("ProviderInitError", { providerID: ProviderID })
|
|
32
32
|
|
|
33
33
|
export { defaultLayer, list, getModelExport as getModel, getLanguage, getSmallModel, defaultModel } from "./provider-layer"
|
|
34
|
-
export { fromModelsDevProvider } from "./provider-schemas"
|
|
34
|
+
export { fromModelsDevProvider, ProviderCategory } from "./provider-schemas"
|
|
35
|
+
export { LOCAL_PROVIDERS, CUSTOM_PROVIDERS, CLOUD_PROVIDERS } from "./custom-loaders"
|
|
35
36
|
|
|
36
37
|
export * as Provider from "./provider"
|
|
@@ -22,7 +22,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
22
22
|
const last = messages.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
23
23
|
if (!last) return null
|
|
24
24
|
const tokens = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
25
|
-
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
25
|
+
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models?.[last.modelID]
|
|
26
26
|
const limit = model?.limit.context ?? 0
|
|
27
27
|
if (!limit) return { pct: 0, label: "", color: theme().textMuted }
|
|
28
28
|
const pct = Math.round((tokens / limit) * 100)
|
|
@@ -115,7 +115,7 @@ export const ConfigRoutes = lazy(() =>
|
|
|
115
115
|
const token = saeeolAuth?.type === "oauth" ? saeeolAuth.access : saeeolAuth?.key
|
|
116
116
|
const organizationId = saeeolAuth?.type === "oauth" ? saeeolAuth.accountId : undefined
|
|
117
117
|
const saeeolApiDefault = yield* Effect.promise(() => fetchDefaultModel(token, organizationId))
|
|
118
|
-
if (saeeolApiDefault && providers[ProviderID.saeeol]?.models[saeeolApiDefault]) {
|
|
118
|
+
if (saeeolApiDefault && providers[ProviderID.saeeol]?.models?.[saeeolApiDefault]) {
|
|
119
119
|
defaults[ProviderID.saeeol] = ModelID.make(saeeolApiDefault)
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -2,6 +2,7 @@ import { Schema } from "effect"
|
|
|
2
2
|
import { HttpApi } from "effect/unstable/httpapi"
|
|
3
3
|
import { BusEvent } from "@/bus/bus-event"
|
|
4
4
|
import { SyncEvent } from "@/sync"
|
|
5
|
+
import { LocalApi } from "./groups/local"
|
|
5
6
|
import { ConfigApi } from "./groups/config"
|
|
6
7
|
import { ControlApi } from "./groups/control"
|
|
7
8
|
import { EventApi } from "./event"
|
|
@@ -34,6 +35,7 @@ export const InstanceHttpApi = HttpApi.make("saeeol-instance")
|
|
|
34
35
|
.addHttpApi(ExperimentalApi)
|
|
35
36
|
.addHttpApi(FileApi)
|
|
36
37
|
.addHttpApi(InstanceApi)
|
|
38
|
+
.addHttpApi(LocalApi)
|
|
37
39
|
.addHttpApi(McpApi)
|
|
38
40
|
.addHttpApi(ProjectApi)
|
|
39
41
|
.addHttpApi(PtyApi)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
3
|
+
import { Authorization } from "../middleware/authorization"
|
|
4
|
+
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
|
5
|
+
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
|
6
|
+
import { described } from "./metadata"
|
|
7
|
+
import {
|
|
8
|
+
GPUProfile, BackendStatus, ModelInstance,
|
|
9
|
+
RAGAsset, HFModelSearch,
|
|
10
|
+
} from "@/provider/local/types"
|
|
11
|
+
|
|
12
|
+
const root = "/local"
|
|
13
|
+
|
|
14
|
+
// Request schemas
|
|
15
|
+
const InstallPayload = Schema.Struct({
|
|
16
|
+
repo: Schema.String,
|
|
17
|
+
filename: Schema.String,
|
|
18
|
+
format: Schema.String,
|
|
19
|
+
quantization: Schema.String,
|
|
20
|
+
sizeBytes: Schema.Number,
|
|
21
|
+
sha256: Schema.optional(Schema.String),
|
|
22
|
+
})
|
|
23
|
+
const LoadPayload = Schema.Struct({
|
|
24
|
+
repo: Schema.String,
|
|
25
|
+
filename: Schema.String,
|
|
26
|
+
backend: Schema.optional(Schema.String),
|
|
27
|
+
gpuIndex: Schema.optional(Schema.Number),
|
|
28
|
+
})
|
|
29
|
+
const UnloadPayload = Schema.Struct({ instanceId: Schema.String })
|
|
30
|
+
const InstallRAGPayload = Schema.Struct({ id: Schema.String, repo: Schema.String, type: Schema.String })
|
|
31
|
+
const InstalledModel = Schema.Struct({ repo: Schema.String, filename: Schema.String, path: Schema.String, sizeBytes: Schema.Number })
|
|
32
|
+
const RAGRecommendResult = Schema.Struct({ embeddings: Schema.Array(RAGAsset), rerankers: Schema.Array(RAGAsset) })
|
|
33
|
+
|
|
34
|
+
export const LocalApi = HttpApi.make("local")
|
|
35
|
+
.add(
|
|
36
|
+
HttpApiGroup.make("local")
|
|
37
|
+
.add(
|
|
38
|
+
HttpApiEndpoint.get("gpu", `${root}/gpu`, {
|
|
39
|
+
success: described(GPUProfile, "GPU profile with VRAM info"),
|
|
40
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.gpu", summary: "Get GPU profile" })),
|
|
41
|
+
HttpApiEndpoint.get("backends", `${root}/backends`, {
|
|
42
|
+
success: described(Schema.Array(BackendStatus), "Detected local backends"),
|
|
43
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.backends", summary: "List detected backends" })),
|
|
44
|
+
HttpApiEndpoint.get("models", `${root}/models`, {
|
|
45
|
+
success: described(Schema.Array(InstalledModel), "Installed local models"),
|
|
46
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.models", summary: "List installed models" })),
|
|
47
|
+
HttpApiEndpoint.get("instances", `${root}/instances`, {
|
|
48
|
+
success: described(Schema.Array(ModelInstance), "Running model instances"),
|
|
49
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.instances", summary: "List running instances" })),
|
|
50
|
+
HttpApiEndpoint.get("hub_search", `${root}/hub/search`, {
|
|
51
|
+
requestSearchParams: Schema.Struct({ q: Schema.String, limit: Schema.String }),
|
|
52
|
+
success: described(Schema.Array(HFModelSearch), "Search results from HuggingFace Hub"),
|
|
53
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.hub.search", summary: "Search HuggingFace Hub" })),
|
|
54
|
+
HttpApiEndpoint.get("rag_recommend", `${root}/rag/recommend`, {
|
|
55
|
+
requestSearchParams: Schema.Struct({ vramMB: Schema.String }),
|
|
56
|
+
success: described(RAGRecommendResult, "RAG recommendations"),
|
|
57
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.rag.recommend", summary: "Get RAG model recommendations" })),
|
|
58
|
+
HttpApiEndpoint.post("install", `${root}/models/install`, {
|
|
59
|
+
payload: InstallPayload,
|
|
60
|
+
success: described(Schema.String, "Installed file path"),
|
|
61
|
+
error: HttpApiError.BadRequest,
|
|
62
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.install", summary: "Install model from HuggingFace" })),
|
|
63
|
+
HttpApiEndpoint.post("uninstall", `${root}/models/uninstall`, {
|
|
64
|
+
payload: InstallPayload,
|
|
65
|
+
success: described(Schema.Boolean, "Success"),
|
|
66
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.uninstall", summary: "Uninstall a local model" })),
|
|
67
|
+
HttpApiEndpoint.post("load", `${root}/instances/load`, {
|
|
68
|
+
payload: LoadPayload,
|
|
69
|
+
success: described(ModelInstance, "Started model instance"),
|
|
70
|
+
error: HttpApiError.BadRequest,
|
|
71
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.load", summary: "Load and start a model" })),
|
|
72
|
+
HttpApiEndpoint.post("unload", `${root}/instances/unload`, {
|
|
73
|
+
payload: UnloadPayload,
|
|
74
|
+
success: described(Schema.Boolean, "Success"),
|
|
75
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.unload", summary: "Stop a running model" })),
|
|
76
|
+
HttpApiEndpoint.post("rag_install", `${root}/rag/install`, {
|
|
77
|
+
payload: InstallRAGPayload,
|
|
78
|
+
success: described(Schema.String, "Installed path"),
|
|
79
|
+
error: HttpApiError.BadRequest,
|
|
80
|
+
}).annotateMerge(OpenApi.annotations({ identifier: "local.rag.install", summary: "Install RAG asset" })),
|
|
81
|
+
)
|
|
82
|
+
.annotateMerge(OpenApi.annotations({ title: "local", description: "Local model management API" }))
|
|
83
|
+
.middleware(InstanceContextMiddleware)
|
|
84
|
+
.middleware(WorkspaceRoutingMiddleware)
|
|
85
|
+
.middleware(Authorization),
|
|
86
|
+
)
|
|
87
|
+
.annotateMerge(OpenApi.annotations({ title: "saeeol local models", version: "0.0.1" }))
|
|
@@ -30,6 +30,7 @@ export class UnsupportedOAuthError extends Schema.ErrorClass<UnsupportedOAuthErr
|
|
|
30
30
|
|
|
31
31
|
export const McpPaths = {
|
|
32
32
|
status: "/mcp",
|
|
33
|
+
refresh: "/mcp/refresh",
|
|
33
34
|
auth: "/mcp/:name/auth",
|
|
34
35
|
authCallback: "/mcp/:name/auth/callback",
|
|
35
36
|
authAuthenticate: "/mcp/:name/auth/authenticate",
|
|
@@ -125,6 +126,15 @@ export const McpApi = HttpApi.make("mcp")
|
|
|
125
126
|
description: "Disconnect an MCP server.",
|
|
126
127
|
}),
|
|
127
128
|
),
|
|
129
|
+
HttpApiEndpoint.post("refresh", McpPaths.refresh, {
|
|
130
|
+
success: described(StatusMap, "MCP servers refreshed"),
|
|
131
|
+
}).annotateMerge(
|
|
132
|
+
OpenApi.annotations({
|
|
133
|
+
identifier: "mcp.refresh",
|
|
134
|
+
summary: "Refresh MCP servers",
|
|
135
|
+
description: "Re-read config and reconnect all MCP servers.",
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
128
138
|
)
|
|
129
139
|
.annotateMerge(
|
|
130
140
|
OpenApi.annotations({
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
|
3
|
+
import { InstanceHttpApi } from "../api"
|
|
4
|
+
import * as GPU from "@/provider/local/gpu"
|
|
5
|
+
import * as Hub from "@/provider/local/hub"
|
|
6
|
+
import * as Manager from "@/provider/local/model-manager"
|
|
7
|
+
import * as Orchestrator from "@/provider/local/orchestrator"
|
|
8
|
+
import * as RAG from "@/provider/local/rag"
|
|
9
|
+
import type { ModelArtifact } from "@/provider/local/types"
|
|
10
|
+
|
|
11
|
+
function toArtifact(payload: { repo: string; filename: string; format: string; quantization: string; sizeBytes: number; sha256?: string }): ModelArtifact {
|
|
12
|
+
return Hub.buildArtifact(payload.repo, payload.filename, payload.sizeBytes, payload.sha256)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const localHandlers = HttpApiBuilder.group(InstanceHttpApi, "local", (handlers) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const gpu = Effect.fn("LocalHttpApi.gpu")(function* () {
|
|
18
|
+
return yield* GPU.profile
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const backends = Effect.fn("LocalHttpApi.backends")(function* () {
|
|
22
|
+
return yield* Effect.promise(() => Orchestrator.detectBackends())
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const models = Effect.fn("LocalHttpApi.models")(function* () {
|
|
26
|
+
return yield* Effect.promise(() => Manager.list())
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const instances = Effect.fn("LocalHttpApi.instances")(function* () {
|
|
30
|
+
return Orchestrator.runningInstances()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const hubSearch = Effect.fn("LocalHttpApi.hubSearch")(function* (ctx: { requestSearchParams: { q: string; limit?: string } }) {
|
|
34
|
+
return yield* Effect.promise(() => Hub.search(ctx.requestSearchParams.q, { limit: Number(ctx.requestSearchParams.limit ?? "20") }))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const ragRecommend = Effect.fn("LocalHttpApi.ragRecommend")(function* (ctx: { requestSearchParams: { vramMB: string } }) {
|
|
38
|
+
return yield* Effect.promise(() => RAG.recommendForVRAM(Number(ctx.requestSearchParams.vramMB)))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const install = Effect.fn("LocalHttpApi.install")(function* (ctx: { payload: { repo: string; filename: string; format: string; quantization: string; sizeBytes: number; sha256?: string } }) {
|
|
42
|
+
const artifact = toArtifact(ctx.payload)
|
|
43
|
+
return yield* Effect.promise(() => Manager.install(artifact))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const uninstall = Effect.fn("LocalHttpApi.uninstall")(function* (ctx: { payload: { repo: string; filename: string; format: string; quantization: string; sizeBytes: number; sha256?: string } }) {
|
|
47
|
+
const artifact = toArtifact(ctx.payload)
|
|
48
|
+
yield* Effect.promise(() => Manager.uninstall(artifact))
|
|
49
|
+
return true
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const load = Effect.fn("LocalHttpApi.load")(function* (ctx: { payload: { repo: string; filename: string; backend?: string; gpuIndex?: number } }) {
|
|
53
|
+
// Find or create artifact from installed models
|
|
54
|
+
const installed = yield* Effect.promise(() => Manager.list())
|
|
55
|
+
const match = installed.find((m) => m.repo === ctx.payload.repo && m.filename === ctx.payload.filename)
|
|
56
|
+
if (!match) {
|
|
57
|
+
return yield* Effect.fail(new HttpApiError.BadRequest({}))
|
|
58
|
+
}
|
|
59
|
+
const artifact = Hub.buildArtifact(ctx.payload.repo, ctx.payload.filename, match.sizeBytes)
|
|
60
|
+
return yield* Effect.promise(() => Orchestrator.load(artifact, { backend: ctx.payload.backend as any, gpuIndex: ctx.payload.gpuIndex }))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const unload = Effect.fn("LocalHttpApi.unload")(function* (ctx: { payload: { instanceId: string } }) {
|
|
64
|
+
yield* Effect.promise(() => Orchestrator.unload(ctx.payload.instanceId))
|
|
65
|
+
return true
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const ragInstall = Effect.fn("LocalHttpApi.ragInstall")(function* (ctx: { payload: { id: string; repo: string; type: string } }) {
|
|
69
|
+
const allEmbeddings = RAG.EMBEDDING_MODELS
|
|
70
|
+
const allRerankers = RAG.RERANKER_MODELS
|
|
71
|
+
const emb = allEmbeddings.find((m) => m.id === ctx.payload.id)
|
|
72
|
+
if (emb) {
|
|
73
|
+
return yield* Effect.promise(() => RAG.installEmbedding(emb))
|
|
74
|
+
}
|
|
75
|
+
const reranker = allRerankers.find((m) => m.id === ctx.payload.id)
|
|
76
|
+
if (reranker) {
|
|
77
|
+
return yield* Effect.promise(() => RAG.installReranker(reranker))
|
|
78
|
+
}
|
|
79
|
+
return yield* Effect.fail(new HttpApiError.BadRequest({}))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return handlers
|
|
83
|
+
.handle("gpu", gpu)
|
|
84
|
+
.handle("backends", backends)
|
|
85
|
+
.handle("models", models)
|
|
86
|
+
.handle("instances", instances)
|
|
87
|
+
.handle("hub_search", hubSearch)
|
|
88
|
+
.handle("rag_recommend", ragRecommend)
|
|
89
|
+
.handle("install", install)
|
|
90
|
+
.handle("uninstall", uninstall)
|
|
91
|
+
.handle("load", load)
|
|
92
|
+
.handle("unload", unload)
|
|
93
|
+
.handle("rag_install", ragInstall)
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
@@ -55,6 +55,10 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler
|
|
|
55
55
|
return true
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
+
const refresh = Effect.fn("McpHttpApi.refresh")(function* () {
|
|
59
|
+
return yield* mcp.refresh()
|
|
60
|
+
})
|
|
61
|
+
|
|
58
62
|
return handlers
|
|
59
63
|
.handle("status", status)
|
|
60
64
|
.handle("add", add)
|
|
@@ -64,5 +68,6 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler
|
|
|
64
68
|
.handle("authRemove", authRemove)
|
|
65
69
|
.handle("connect", connect)
|
|
66
70
|
.handle("disconnect", disconnect)
|
|
71
|
+
.handle("refresh", refresh)
|
|
67
72
|
}),
|
|
68
73
|
)
|
|
@@ -40,7 +40,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
|
|
40
40
|
)
|
|
41
41
|
return {
|
|
42
42
|
all: Object.values(validProviders),
|
|
43
|
-
default: Provider.defaultModelIDs(pickBy(validProviders, (item) => Object.keys(item.models).length > 0)),
|
|
43
|
+
default: Provider.defaultModelIDs(pickBy(validProviders, (item) => Object.keys(item.models ?? {}).length > 0)),
|
|
44
44
|
connected: Object.keys(connected),
|
|
45
45
|
failed,
|
|
46
46
|
}
|