@visual-z/xeno 0.1.4 → 0.2.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/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup.ts +400 -268
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** Config parsing owns provider wire vocabulary; identity/filter policy lives in core. */
|
|
2
|
+
import { mergeCatalogs, modelEnabled } from "../core/catalog-merge.ts";
|
|
3
|
+
import { modelKey } from "../core/session-position.ts";
|
|
4
|
+
import { THINKING_LEVELS } from "../core/thinking.ts";
|
|
5
|
+
import { SOURCE_PROVIDERS } from "./model-data.ts";
|
|
6
|
+
import { DEFAULT_COMPAT, type ModelSpec, type Effort } from "./model.ts";
|
|
7
|
+
import { isProviderApi, SDK_APIS } from "./provider-protocols.ts";
|
|
8
|
+
import { providerAccess, setProviderAccess, type ProviderAccess } from "./provider-access.ts";
|
|
9
|
+
|
|
10
|
+
function object(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
11
|
+
function own(value: Record<string, unknown>, key: string): boolean { return Object.hasOwn(value, key); }
|
|
12
|
+
function positive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; }
|
|
13
|
+
function nonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
14
|
+
function error(path: string, message: string): never { throw new Error(`${path}: ${message}`); }
|
|
15
|
+
function strings(value: unknown, path: string): string[] | undefined {
|
|
16
|
+
if (value === undefined) return undefined;
|
|
17
|
+
if (!Array.isArray(value) || !value.every(x => typeof x === "string")) error(path, "must be an array of strings");
|
|
18
|
+
return value as string[];
|
|
19
|
+
}
|
|
20
|
+
function stringMap(value: unknown, path: string, nullable = false): Record<string, string | null> | undefined {
|
|
21
|
+
if (value === undefined) return undefined;
|
|
22
|
+
if (!object(value) || !Object.values(value).every(x => typeof x === "string" || (nullable && x === null))) error(path, "must contain string values" + (nullable ? " or null" : ""));
|
|
23
|
+
return Object.fromEntries(Object.entries(value as Record<string, string | null>).map(([key, v]) => [key, typeof v === "string" ? configValue(v) : v]));
|
|
24
|
+
}
|
|
25
|
+
/** OpenCode's env reference and Pi's interpolation share one resolver. No implicit uppercase lookup. */
|
|
26
|
+
function configValue(value: string): string { return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "$${$1}"); }
|
|
27
|
+
const RESERVED_BODY = ["model", "modelId", "messages", "system", "systemInstruction", "system_instruction", "instructions", "input", "contents", "context", "tools", "toolConfig", "stream", "previous_response_id", "store", "conversation", "conversation_id", "sessionId", "session_id", "httpOptions", "http_options", "headers", "apiKey", "authToken", "baseUrl", "baseURL", "fetch", "abortSignal", "signal", "onPayload", "onResponse"];
|
|
28
|
+
const REQUEST_OPTIONS = ["temperature", "samplingParams", "cacheRetention", "timeoutMs", "maxRetryDelayMs", "metadata"];
|
|
29
|
+
|
|
30
|
+
function payloadPatch(value: unknown, path: string): Record<string, unknown> | undefined {
|
|
31
|
+
if (value === undefined) return undefined;
|
|
32
|
+
if (!object(value)) error(path, "must be an object");
|
|
33
|
+
for (const [key, child] of Object.entries(value)) {
|
|
34
|
+
if (RESERVED_BODY.includes(key)) error(`${path}.${key}`, "is owned by the log projection and request adapter");
|
|
35
|
+
if ((key === "config" || key === "options") && !object(child)) error(`${path}.${key}`, "must be an object");
|
|
36
|
+
if (object(child)) payloadPatch(child, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function accessPatch(raw: Record<string, unknown>, path: string, parent: ProviderAccess = {}): ProviderAccess {
|
|
42
|
+
if (raw["options"] !== undefined && !object(raw["options"])) error(`${path}.options`, "must be an object");
|
|
43
|
+
const options = object(raw["options"]) ? raw["options"] : {};
|
|
44
|
+
const apiKey = own(raw, "apiKey") ? raw["apiKey"] : options["apiKey"];
|
|
45
|
+
if (apiKey !== undefined && (typeof apiKey !== "string" || !apiKey.length)) error(`${path}.apiKey`, "must be a non-empty string; use auth: false for a keyless endpoint");
|
|
46
|
+
const auth = raw["auth"];
|
|
47
|
+
if (auth !== undefined && auth !== false && auth !== "ambient") error(`${path}.auth`, "must be false or ambient");
|
|
48
|
+
const authHeader = raw["authHeader"];
|
|
49
|
+
if (authHeader !== undefined && typeof authHeader !== "boolean" && (typeof authHeader !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(authHeader))) error(`${path}.authHeader`, "must be a boolean or header name");
|
|
50
|
+
const headers = stringMap(raw["headers"] ?? options["headers"] ?? raw["defaultHeaders"], `${path}.headers`, true);
|
|
51
|
+
if (headers && Object.keys(headers).some(key => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key))) error(`${path}.headers`, "contains an invalid header name");
|
|
52
|
+
const env = stringMap(raw["env"], `${path}.env`) as Record<string, string> | undefined;
|
|
53
|
+
if (env && Object.keys(env).some(key => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))) error(`${path}.env`, "contains an invalid variable name");
|
|
54
|
+
const body = payloadPatch(raw["body"] ?? raw["extraBody"] ?? options["body"] ?? options["extraBody"], `${path}.body`);
|
|
55
|
+
const requestOptions = Object.fromEntries(Object.entries(options).filter(([key]) => !["apiKey", "baseURL", "baseUrl", "headers", "body", "extraBody"].includes(key)));
|
|
56
|
+
for (const key of Object.keys(requestOptions)) {
|
|
57
|
+
if (!REQUEST_OPTIONS.includes(key)) error(`${path}.options.${key}`, "is not a configurable request option; use env for native cloud settings or body for API parameters");
|
|
58
|
+
}
|
|
59
|
+
payloadPatch(requestOptions["samplingParams"], `${path}.options.samplingParams`);
|
|
60
|
+
for (const key of ["temperature", "timeoutMs", "maxRetryDelayMs"]) if (requestOptions[key] !== undefined && !nonnegative(requestOptions[key])) error(`${path}.options.${key}`, "must be a finite nonnegative number");
|
|
61
|
+
if (requestOptions["cacheRetention"] !== undefined && !["none", "short", "long"].includes(String(requestOptions["cacheRetention"]))) error(`${path}.options.cacheRetention`, "must be none, short or long");
|
|
62
|
+
stringMap(requestOptions["metadata"], `${path}.options.metadata`);
|
|
63
|
+
return { ...parent,
|
|
64
|
+
...(typeof apiKey === "string" ? { apiKey: configValue(apiKey) } : {}),
|
|
65
|
+
...(auth === false || auth === "ambient" ? { auth } : {}),
|
|
66
|
+
...(typeof authHeader === "boolean" || typeof authHeader === "string" ? { authHeader } : {}),
|
|
67
|
+
...(headers ? { headers: { ...parent.headers, ...headers } } : {}),
|
|
68
|
+
...(env ? { env: { ...parent.env, ...env } } : {}),
|
|
69
|
+
...(object(body) ? { body: { ...parent.body, ...body } } : {}),
|
|
70
|
+
...(Object.keys(requestOptions).length ? { options: { ...parent.options, ...requestOptions } } : {}) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function modelPatch(raw: Record<string, unknown>, base: ModelSpec | undefined, provider: string, id: string, path: string, partial: boolean): ModelSpec {
|
|
74
|
+
const options = object(raw["options"]) ? raw["options"] : {};
|
|
75
|
+
const endpoint = raw["baseUrl"] ?? raw["baseURL"] ?? options["baseURL"] ?? options["baseUrl"];
|
|
76
|
+
if (raw["npm"] !== undefined && (typeof raw["npm"] !== "string" || !SDK_APIS[raw["npm"]]) && raw["api"] === undefined) error(`${path}.npm`, "unsupported SDK; select a supported api explicitly");
|
|
77
|
+
const api = raw["api"] ?? (typeof raw["npm"] === "string" ? SDK_APIS[raw["npm"]] : undefined) ?? base?.api ?? SOURCE_PROVIDERS[provider]?.api;
|
|
78
|
+
if (!isProviderApi(api)) error(`${path}.api`, "unknown or missing protocol");
|
|
79
|
+
if (endpoint !== undefined && typeof endpoint !== "string") error(`${path}.baseUrl`, "must be a string");
|
|
80
|
+
const spec: ModelSpec = { ...(base ?? {}), provider, id, name: base?.name ?? id, api,
|
|
81
|
+
reasoning: base?.reasoning ?? false, ...(partial ? {} : { driver: "pi" }),
|
|
82
|
+
...(typeof endpoint === "string" ? { baseUrl: configValue(endpoint) } : base?.baseUrl !== undefined ? {} : SOURCE_PROVIDERS[provider]?.baseUrl ? { baseUrl: SOURCE_PROVIDERS[provider].baseUrl } : {}) };
|
|
83
|
+
if (raw["api"] !== undefined && raw["api"] !== base?.api) { delete spec.compat; delete spec.nativeCompat; delete spec.thinkingLevelMap; }
|
|
84
|
+
if (typeof raw["name"] === "string" && raw["name"].length) spec.name = raw["name"];
|
|
85
|
+
if (own(raw, "reasoning")) { if (typeof raw["reasoning"] !== "boolean") error(`${path}.reasoning`, "must be boolean"); spec.reasoning = raw["reasoning"]; }
|
|
86
|
+
const limit = object(raw["limit"]) ? raw["limit"] : {};
|
|
87
|
+
for (const [key, value] of [["contextWindow", raw["contextWindow"] ?? limit["context"]], ["maxTokens", raw["maxTokens"] ?? limit["output"]]] as const) {
|
|
88
|
+
if (value !== undefined) { if (!positive(value)) error(`${path}.${key}`, "must be a finite positive number"); spec[key] = value; }
|
|
89
|
+
}
|
|
90
|
+
if (own(raw, "input")) {
|
|
91
|
+
if (!Array.isArray(raw["input"]) || !raw["input"].every(x => x === "text" || x === "image")) error(`${path}.input`, "must contain only text or image");
|
|
92
|
+
spec.input = raw["input"] as ("text" | "image")[];
|
|
93
|
+
}
|
|
94
|
+
if (raw["credentialEnv"] !== undefined) {
|
|
95
|
+
const names = strings(raw["credentialEnv"], `${path}.credentialEnv`)!;
|
|
96
|
+
if (names.some(name => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) error(`${path}.credentialEnv`, "must contain variable names");
|
|
97
|
+
spec.credentialEnv = names;
|
|
98
|
+
}
|
|
99
|
+
const price = raw["price"] ?? raw["cost"];
|
|
100
|
+
if (price !== undefined) {
|
|
101
|
+
if (!object(price) || !nonnegative(price["input"]) || !nonnegative(price["output"]) || ["cacheRead", "cacheWrite"].some(k => own(price, k) && !nonnegative(price[k]))) error(`${path}.price`, "requires finite nonnegative input/output and optional cache prices");
|
|
102
|
+
spec.price = { input: price["input"], output: price["output"],
|
|
103
|
+
...(nonnegative(price["cacheRead"]) ? { cacheRead: price["cacheRead"] } : {}),
|
|
104
|
+
...(nonnegative(price["cacheWrite"]) ? { cacheWrite: price["cacheWrite"] } : {}) };
|
|
105
|
+
}
|
|
106
|
+
if (raw["cache"] !== undefined) { if (raw["cache"] !== "explicit" && raw["cache"] !== "automatic") error(`${path}.cache`, "must be explicit or automatic"); spec.cache = raw["cache"]; }
|
|
107
|
+
if (raw["rates"] !== undefined) {
|
|
108
|
+
const rates = raw["rates"];
|
|
109
|
+
if (!object(rates) || !nonnegative(rates["read"]) || !nonnegative(rates["write"])) error(`${path}.rates`, "requires finite nonnegative read/write");
|
|
110
|
+
spec.rates = { read: rates["read"], write: rates["write"] };
|
|
111
|
+
}
|
|
112
|
+
if (raw["thinkingLevelMap"] !== undefined) {
|
|
113
|
+
const map = raw["thinkingLevelMap"];
|
|
114
|
+
if (!object(map) || !Object.entries(map).every(([key, value]) => (THINKING_LEVELS as readonly string[]).includes(key) && (value === null || typeof value === "string"))) error(`${path}.thinkingLevelMap`, "requires supported effort keys and string/null values");
|
|
115
|
+
spec.thinkingLevelMap = map as Partial<Record<Effort, string | null>>;
|
|
116
|
+
}
|
|
117
|
+
for (const key of ["compat", "nativeCompat", "samplingParams"] as const) {
|
|
118
|
+
const value = raw[key];
|
|
119
|
+
if (value !== undefined) {
|
|
120
|
+
if (!object(value)) error(`${path}.${key}`, "must be an object");
|
|
121
|
+
if (key === "samplingParams") payloadPatch(value, `${path}.${key}`);
|
|
122
|
+
else for (const [field, flag] of Object.entries(value)) {
|
|
123
|
+
if (/^(supports|requires)/.test(field) && typeof flag !== "boolean") error(`${path}.${key}.${field}`, "must be boolean");
|
|
124
|
+
if (field === "maxTokensField" && flag !== "max_tokens" && flag !== "max_completion_tokens") error(`${path}.${key}.${field}`, "must be max_tokens or max_completion_tokens");
|
|
125
|
+
}
|
|
126
|
+
if (key === "compat") {
|
|
127
|
+
spec.nativeCompat = { ...spec.nativeCompat, ...value };
|
|
128
|
+
spec.compat = { ...DEFAULT_COMPAT, ...base?.compat, ...value } as NonNullable<ModelSpec["compat"]>;
|
|
129
|
+
} else spec[key] = { ...spec[key], ...value };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (raw["driver"] === "pi") spec.driver = "pi";
|
|
133
|
+
if (typeof raw["wireId"] === "string") spec.wireId = raw["wireId"];
|
|
134
|
+
return spec;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseModelConfiguration(json: unknown, builtin: readonly ModelSpec[] = []): { models: ModelSpec[]; errors: string[] } {
|
|
138
|
+
let models = [...builtin];
|
|
139
|
+
const errors: string[] = [];
|
|
140
|
+
const report = (err: unknown) => errors.push(err instanceof Error ? err.message : "invalid model configuration");
|
|
141
|
+
if (!Array.isArray(json) && !object(json)) return { models, errors: ["user catalog must be an array or an object with models/providers/provider"] };
|
|
142
|
+
const root = object(json) ? json : {};
|
|
143
|
+
const rows = Array.isArray(json) ? json : root["models"];
|
|
144
|
+
if (rows !== undefined && !Array.isArray(rows)) errors.push("user catalog .models is not an array");
|
|
145
|
+
const indexed = new Map(models.map(model => [modelKey(model), model]));
|
|
146
|
+
if (Array.isArray(rows)) for (const [i, row] of rows.entries()) {
|
|
147
|
+
const path = `models[${i}]` + (object(row) && typeof row["id"] === "string" ? ` (${row["id"]})` : "");
|
|
148
|
+
try {
|
|
149
|
+
if (!object(row)) error(path, "must be an object");
|
|
150
|
+
if (typeof row["id"] !== "string" || !row["id"].length) error(path, "has no id");
|
|
151
|
+
if (typeof row["provider"] !== "string" || !row["provider"].length) error(path, "has no provider");
|
|
152
|
+
const id = row["id"], provider = row["provider"];
|
|
153
|
+
if (row["disabled"] === true || row["enabled"] === false) { indexed.delete(modelKey({ provider, id })); continue; }
|
|
154
|
+
const previous = indexed.get(modelKey({ provider, id }));
|
|
155
|
+
const model = modelPatch(row, previous, provider, id, path, true);
|
|
156
|
+
const access = accessPatch(row, path, previous ? providerAccess(previous) : {});
|
|
157
|
+
if (Object.keys(access).length) { model.driver = "pi"; setProviderAccess(model, access); }
|
|
158
|
+
indexed.set(modelKey(model), model);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
report(err);
|
|
161
|
+
if (object(row) && typeof row["id"] === "string" && typeof row["provider"] === "string") {
|
|
162
|
+
const key = modelKey({ id: row["id"], provider: row["provider"] });
|
|
163
|
+
const previous = indexed.get(key);
|
|
164
|
+
if (previous) { const copy = { ...previous }; setProviderAccess(copy, { invalid: `${path}: invalid model configuration` }); indexed.set(key, copy); }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
models = [...indexed.values()];
|
|
169
|
+
for (const groupKey of ["providers", "provider"] as const) {
|
|
170
|
+
const providers = root[groupKey];
|
|
171
|
+
if (providers === undefined) continue;
|
|
172
|
+
if (!object(providers)) { errors.push(`${groupKey}: must be an object`); continue; }
|
|
173
|
+
for (const [provider, config] of Object.entries(providers)) {
|
|
174
|
+
const path = `${groupKey}.${provider}`;
|
|
175
|
+
try {
|
|
176
|
+
if (!object(config)) error(path, "must be an object");
|
|
177
|
+
if (config["disabled"] === true || config["enabled"] === false) { models = models.filter(model => model.provider !== provider); continue; }
|
|
178
|
+
const access = accessPatch(config, path);
|
|
179
|
+
const allow = strings(config["allowlist"] ?? config["whitelist"], `${path}.allowlist`);
|
|
180
|
+
const deny = strings(config["denylist"] ?? config["blacklist"], `${path}.denylist`);
|
|
181
|
+
const peers: ModelSpec[] = [];
|
|
182
|
+
const defaults = { ...config }; delete defaults["name"];
|
|
183
|
+
for (const previous of models.filter(model => model.provider === provider)) {
|
|
184
|
+
const next = modelPatch(defaults, previous, provider, previous.id, path, false);
|
|
185
|
+
setProviderAccess(next, accessPatch(config, path, providerAccess(previous))); peers.push(next);
|
|
186
|
+
}
|
|
187
|
+
const definitions = config["models"];
|
|
188
|
+
if (definitions !== undefined && !Array.isArray(definitions) && !object(definitions)) error(`${path}.models`, "must be an array or object");
|
|
189
|
+
const additions: [string | undefined, unknown][] = Array.isArray(definitions) ? definitions.map(row => [undefined, row]) : object(definitions) ? Object.entries(definitions) : [];
|
|
190
|
+
const patches = config["modelOverrides"];
|
|
191
|
+
if (patches !== undefined && !object(patches)) error(`${path}.modelOverrides`, "must be an object");
|
|
192
|
+
const entries = [...additions.map(([id, row]) => ({ id, row, override: false })), ...Object.entries(object(patches) ? patches : {}).map(([id, row]) => ({ id, row, override: true }))];
|
|
193
|
+
for (const [index, entry] of entries.entries()) {
|
|
194
|
+
const label = `${path}.${entry.override ? "modelOverrides" : "models"}.${entry.id ?? index}`;
|
|
195
|
+
try {
|
|
196
|
+
if (!object(entry.row)) error(label, "must be an object");
|
|
197
|
+
const id = entry.id ?? entry.row["id"];
|
|
198
|
+
if (typeof id !== "string" || !id.length) error(label, "has no id");
|
|
199
|
+
if (entry.row["disabled"] === true || entry.row["enabled"] === false) { const at = peers.findIndex(model => model.id === id); if (at >= 0) peers.splice(at, 1); continue; }
|
|
200
|
+
const base = peers.find(model => model.id === id);
|
|
201
|
+
if (entry.override && !base) error(label, "cannot override an unknown model");
|
|
202
|
+
const inherited = base ?? modelPatch(defaults, undefined, provider, id, path, false);
|
|
203
|
+
const model = modelPatch(entry.row, inherited, provider, id, label, false);
|
|
204
|
+
if (entry.id && typeof entry.row["id"] === "string" && entry.row["id"] !== id) model.wireId = entry.row["id"];
|
|
205
|
+
setProviderAccess(model, accessPatch(entry.row, label, base ? providerAccess(base) : access));
|
|
206
|
+
const at = peers.findIndex(candidate => candidate.id === id);
|
|
207
|
+
if (at < 0) peers.push(model); else peers[at] = model;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
report(err);
|
|
210
|
+
const id = entry.id ?? (object(entry.row) ? entry.row["id"] : undefined);
|
|
211
|
+
const at = peers.findIndex(model => model.id === id);
|
|
212
|
+
if (at >= 0) { const copy = { ...peers[at]! }; setProviderAccess(copy, { invalid: `${label}: invalid model configuration` }); peers[at] = copy; }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
models = mergeCatalogs(models.filter(model => model.provider !== provider), peers.filter(model => modelEnabled(model.id, allow, deny)));
|
|
216
|
+
} catch (err) {
|
|
217
|
+
report(err);
|
|
218
|
+
// An invalid explicit provider config must not silently use a different credential/endpoint.
|
|
219
|
+
models = models.map(model => {
|
|
220
|
+
if (model.provider !== provider) return model;
|
|
221
|
+
const copy = { ...model }; setProviderAccess(copy, { invalid: `${path}: invalid provider configuration` }); return copy;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const enabled = strings(root["enabled_providers"], "enabled_providers");
|
|
228
|
+
const disabled = strings(root["disabled_providers"], "disabled_providers");
|
|
229
|
+
models = models.filter(model => modelEnabled(model.provider, enabled, disabled));
|
|
230
|
+
} catch (err) { report(err); }
|
|
231
|
+
return { models, errors };
|
|
232
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
The generated model catalog contains data adapted from:
|
|
2
|
+
|
|
3
|
+
Pi (https://github.com/earendil-works/pi)
|
|
4
|
+
Copyright (c) 2025 Mario Zechner
|
|
5
|
+
|
|
6
|
+
Models.dev (https://github.com/anomalyco/models.dev)
|
|
7
|
+
Copyright (c) 2025 models.dev
|
|
8
|
+
|
|
9
|
+
Both sources are distributed under the following MIT License:
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|