{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/llama/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA2B,MAAM,gCAAgC,CAAC;AAyC5F,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmL7D","sourcesContent":["import type { ExtensionAPI, ExtensionCommandContext } from \"../../core/extensions/types.ts\";\nimport { formatBytes, LlamaClient, type LlamaModelInfo, normalizeLlamaServerUrl } from \"./client.ts\";\nimport { findHuggingFaceToken, HuggingFaceClient } from \"./huggingface.ts\";\nimport { createLlamaProvider, LLAMA_PROVIDER_ID } from \"./provider.ts\";\nimport { type LlamaUi, runWithProgress, showLlamaUi } from \"./ui.ts\";\n\nfunction modelIsLoaded(model: LlamaModelInfo): boolean {\n\treturn model.status.value === \"loaded\" || model.status.value === \"sleeping\";\n}\n\nfunction isConnectionError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst message = `${error.name} ${error.message}`.toLowerCase();\n\treturn message.includes(\"fetch failed\") || message.includes(\"timeout\") || message.includes(\"network\");\n}\n\nfunction connectionErrorMessage(error: unknown): string {\n\tif (isConnectionError(error)) return \"Could not connect to the server.\";\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction parseHuggingFaceModel(value: string): { repository: string; quantization?: string } {\n\tconst colon = value.indexOf(\":\", value.indexOf(\"/\") + 1);\n\treturn colon < 0\n\t\t? { repository: value }\n\t\t: { repository: value.slice(0, colon), quantization: value.slice(colon + 1) };\n}\n\nasync function configuredClient(ctx: ExtensionCommandContext): Promise<LlamaClient | undefined> {\n\tconst result = await ctx.modelRegistry.getProviderAuth(LLAMA_PROVIDER_ID);\n\tif (!result) {\n\t\tctx.ui.notify(`Configure llama.cpp with /login ${LLAMA_PROVIDER_ID}`, \"warning\");\n\t\treturn undefined;\n\t}\n\tconst configuredUrl = result.env?.LLAMA_BASE_URL;\n\tconst serverUrl = normalizeLlamaServerUrl(\n\t\ttypeof configuredUrl === \"string\" && configuredUrl ? configuredUrl : (result.auth.baseUrl ?? \"\"),\n\t);\n\treturn new LlamaClient(serverUrl, result.auth.apiKey);\n}\n\nexport default function llamaExtension(pi: ExtensionAPI): void {\n\tconst provider = createLlamaProvider();\n\tpi.registerProvider(provider.provider);\n\n\tconst syncCatalog = async (\n\t\tctx: ExtensionCommandContext,\n\t\tclient: LlamaClient,\n\t\tcatalog?: LlamaModelInfo[],\n\t): Promise<LlamaModelInfo[]> => {\n\t\tconst current = catalog ?? (await client.list());\n\t\tprovider.setCatalog(current, client.serverUrl);\n\t\tawait ctx.modelRegistry.refresh();\n\t\treturn current;\n\t};\n\n\tconst loadModel = async (\n\t\tctx: ExtensionCommandContext,\n\t\tui: LlamaUi,\n\t\tclient: LlamaClient,\n\t\tcatalog: LlamaModelInfo[],\n\t\ttarget: LlamaModelInfo,\n\t): Promise<void> => {\n\t\tconst loaded = catalog.filter((model) => model.id !== target.id && modelIsLoaded(model));\n\t\tlet replace = false;\n\t\tif (loaded.length > 0) {\n\t\t\tconst choice = await ui.select(`${loaded.length} model${loaded.length === 1 ? \" is\" : \"s are\"} loaded`, [\n\t\t\t\t\"Unload all and load\",\n\t\t\t\t\"Keep loaded and load\",\n\t\t\t\t\"Cancel\",\n\t\t\t]);\n\t\t\tif (!choice || choice === \"Cancel\") return;\n\t\t\treplace = choice === \"Unload all and load\";\n\t\t}\n\n\t\tconst restoreLoaded = async (): Promise<void> => {\n\t\t\tctx.ui.notify(\"Restoring previously loaded models\");\n\t\t\tfor (const model of loaded) await client.loadAndWait(model.id, () => {});\n\t\t\tawait syncCatalog(ctx, client);\n\t\t};\n\t\tif (replace) {\n\t\t\tfor (const model of loaded) await client.unloadAndWait(model.id);\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runWithProgress(ui, {\n\t\t\t\ttitle: \"Loading model\",\n\t\t\t\tmodel: target.id,\n\t\t\t\tinitialMessage: \"Starting…\",\n\t\t\t\tcancelTitle: \"Stop loading?\",\n\t\t\t\tcancelMessage: target.id,\n\t\t\t\trun: (signal, update) => client.loadAndWait(target.id, update, signal),\n\t\t\t\tcancel: () => client.unload(target.id),\n\t\t\t});\n\t\t\tif (result.cancelled) {\n\t\t\t\tif (replace) await restoreLoaded();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst refreshed = await syncCatalog(ctx, client);\n\t\t\tconst loadedModel = refreshed.find((model) => model.id === target.id);\n\t\t\tctx.ui.notify(\n\t\t\t\tloadedModel?.status.value === \"loaded\" ? `Loaded ${target.id}` : `Load started for ${target.id}`,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (replace) {\n\t\t\t\ttry {\n\t\t\t\t\tawait restoreLoaded();\n\t\t\t\t} catch {\n\t\t\t\t\t// Preserve the original load error.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tconst unloadModel = async (\n\t\tctx: ExtensionCommandContext,\n\t\tui: LlamaUi,\n\t\tclient: LlamaClient,\n\t\tmodel: LlamaModelInfo,\n\t): Promise<void> => {\n\t\tif (!(await ui.confirm(\"Unload model?\", model.id))) return;\n\t\tawait client.unloadAndWait(model.id);\n\t\tawait syncCatalog(ctx, client);\n\t\tctx.ui.notify(`Unloaded ${model.id}`);\n\t};\n\n\tconst downloadModel = async (ctx: ExtensionCommandContext, ui: LlamaUi, client: LlamaClient): Promise<void> => {\n\t\tconst huggingFace = new HuggingFaceClient(await findHuggingFaceToken());\n\t\tconst selected = await ui.searchModels((query, signal) => huggingFace.search(query, signal));\n\t\tif (!selected) return;\n\t\tconst parsed = parseHuggingFaceModel(selected);\n\t\tui.showStatus(\"Loading model details\", parsed.repository);\n\t\tconst details = await huggingFace.details(parsed.repository);\n\t\tif (details.gated) {\n\t\t\tconst approval = details.gated === \"manual\" ? \"Manual approval is required\" : \"Accept the access terms\";\n\t\t\tconst choice = await ui.select(\n\t\t\t\t`Hugging Face access required\\n${details.id}\\n\\n${approval} at:\\nhttps://huggingface.co/${details.id}\\n\\nThe llama.cpp server needs HF_TOKEN with access.`,\n\t\t\t\t[\"Continue\", \"Back\"],\n\t\t\t);\n\t\t\tif (choice !== \"Continue\") return;\n\t\t}\n\t\tlet quantization = parsed.quantization;\n\t\tif (!quantization && details.quantizations.length > 0) {\n\t\t\tconst options = details.quantizations.map((entry) => {\n\t\t\t\tconst detail = [\n\t\t\t\t\tentry.size === undefined ? undefined : formatBytes(entry.size),\n\t\t\t\t\tentry.name === \"Q4_K_M\" ? \"recommended\" : undefined,\n\t\t\t\t]\n\t\t\t\t\t.filter((value): value is string => Boolean(value))\n\t\t\t\t\t.join(\" · \");\n\t\t\t\treturn detail ? `${entry.name} · ${detail}` : entry.name;\n\t\t\t});\n\t\t\tconst choice = await ui.select(`Select quantization\\n${details.id}`, options);\n\t\t\tif (!choice) return;\n\t\t\tquantization = details.quantizations[options.indexOf(choice)]?.name;\n\t\t\tif (!quantization) return;\n\t\t}\n\t\tconst model = quantization ? `${details.id}:${quantization}` : details.id;\n\t\tconst result = await runWithProgress(ui, {\n\t\t\ttitle: \"Downloading model\",\n\t\t\tmodel,\n\t\t\tinitialMessage: \"Starting…\",\n\t\t\tcancelTitle: \"Stop download?\",\n\t\t\tcancelMessage: model,\n\t\t\trun: (signal, update) => client.downloadAndWait(model, update, signal),\n\t\t\tcancel: () => client.unload(model),\n\t\t});\n\t\tif (result.cancelled) return;\n\t\tawait syncCatalog(ctx, client, result.value);\n\t\tctx.ui.notify(`Downloaded ${model}`);\n\t};\n\n\tpi.registerCommand(\"llama\", {\n\t\tdescription: \"Manage llama.cpp router models\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\tctx.ui.notify(\"/llama is available in interactive mode\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst client = await configuredClient(ctx);\n\t\t\tif (!client) return;\n\t\t\tawait showLlamaUi(ctx, async (ui) => {\n\t\t\t\tconst readCatalog = async (): Promise<LlamaModelInfo[] | undefined> => {\n\t\t\t\t\twhile (true) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn await syncCatalog(ctx, client);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tif ((await ui.connectionError(client.serverUrl, connectionErrorMessage(error))) === \"close\") {\n\t\t\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tlet catalog = await readCatalog();\n\t\t\t\tif (!catalog) return;\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst action = await ui.showModels(client.serverUrl, catalog);\n\t\t\t\t\tif (action.type === \"close\") return;\n\t\t\t\t\tlet actionError: unknown;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (action.type === \"download\") await downloadModel(ctx, ui, client);\n\t\t\t\t\t\telse if (modelIsLoaded(action.model)) await unloadModel(ctx, ui, client, action.model);\n\t\t\t\t\t\telse if (action.model.status.value === \"unloaded\")\n\t\t\t\t\t\t\tawait loadModel(ctx, ui, client, catalog, action.model);\n\t\t\t\t\t\telse ctx.ui.notify(`${action.model.id} is ${action.model.status.value}`, \"warning\");\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tactionError = error;\n\t\t\t\t\t}\n\t\t\t\t\tconst refreshed = await readCatalog();\n\t\t\t\t\tif (!refreshed) return;\n\t\t\t\t\tcatalog = refreshed;\n\t\t\t\t\tif (actionError && !isConnectionError(actionError)) {\n\t\t\t\t\t\tctx.ui.notify(actionError instanceof Error ? actionError.message : String(actionError), \"error\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t});\n}\n"]}