@caupulican/pi-adaptative 0.81.19 → 0.81.21
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 +20 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +4 -1
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts +2 -0
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +92 -6
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/models/context-sizing.d.ts.map +1 -1
- package/dist/core/models/context-sizing.js +18 -26
- package/dist/core/models/context-sizing.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +57 -4
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +307 -30
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/models/perf-profile.d.ts +3 -0
- package/dist/core/models/perf-profile.d.ts.map +1 -1
- package/dist/core/models/perf-profile.js +42 -13
- package/dist/core/models/perf-profile.js.map +1 -1
- package/dist/core/models/runtime-arbiter.d.ts +45 -0
- package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
- package/dist/core/models/runtime-arbiter.js +115 -0
- package/dist/core/models/runtime-arbiter.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +24 -6
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perf-profile.js","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn prefillRateFromSample(sample) !== undefined || decodeRateFromSample(sample) !== undefined;\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tif (prefillRate === undefined && decodeRate === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || !prefillRate || profile.samples < 1 || input.promptTokens <= 0) return {};\n\n\tconst expectedPrefillMs = (input.promptTokens / prefillRate) * 1000;\n\tif (!Number.isFinite(expectedPrefillMs) || expectedPrefillMs <= 0) return {};\n\n\tconst targetMs = Math.ceil(Math.max(input.base.quietIdleMs, expectedPrefillMs * 3));\n\tconst ceilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst quietIdleMs = Math.min(targetMs, ceilingMs);\n\treturn quietIdleMs > input.base.quietIdleMs ? { quietIdleMs } : {};\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst inner = await streamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t},\n\t\t});\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\tfor await (const event of inner) {\n\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t}\n\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\tpromptTokens: event.message.usage.input,\n\t\t\t\t\t\trecorder,\n\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t});\n\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t}\n\t\t\t\touter.push(event);\n\t\t\t\tif (terminalPushed) return;\n\t\t\t}\n\t\t\touter.end();\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs: input.firstTokenAtMs - input.requestStartedAtMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tsample.headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = sample.headersToFirstTokenMs ?? sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"perf-profile.js","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tloadMs?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tloadMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n\tlocalClass?: boolean;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\t(record.loadMs === undefined || isPositiveFiniteNumber(record.loadMs)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn (\n\t\tprefillRateFromSample(sample) !== undefined ||\n\t\tdecodeRateFromSample(sample) !== undefined ||\n\t\tisPositiveFiniteNumber(sample.loadMs)\n\t);\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tconst loadMs = isPositiveFiniteNumber(sample.loadMs) ? sample.loadMs : undefined;\n\tif (prefillRate === undefined && decodeRate === undefined && loadMs === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\t...(loadMs !== undefined && { loadMs: updateEwma(current?.loadMs, loadMs, previousSamples) }),\n\t\t...(loadMs === undefined && current?.loadMs !== undefined ? { loadMs: current.loadMs } : {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst result: Partial<StreamIdleOptions> = {};\n\tconst localConnectDefaultMs = input.localClass\n\t\t? Math.max(input.base.connectMs, input.base.quietIdleMs)\n\t\t: input.base.connectMs;\n\tconst quietCeilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst connectCeilingMs = Math.max(localConnectDefaultMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\n\tconst expectedPrefillMs = expectedPrefillFromProfile(profile, input.promptTokens);\n\tif (expectedPrefillMs !== undefined) {\n\t\tconst quietIdleMs = adaptiveBound(input.base.quietIdleMs, quietCeilingMs, expectedPrefillMs);\n\t\tif (quietIdleMs > input.base.quietIdleMs) result.quietIdleMs = quietIdleMs;\n\t}\n\n\tif (input.localClass) {\n\t\tconst expectedConnectWorkMs =\n\t\t\texpectedPrefillMs !== undefined ? expectedPrefillMs + (profile?.loadMs ?? 0) : profile?.loadMs;\n\t\tconst connectMs =\n\t\t\texpectedConnectWorkMs !== undefined\n\t\t\t\t? adaptiveBound(localConnectDefaultMs, connectCeilingMs, expectedConnectWorkMs)\n\t\t\t\t: localConnectDefaultMs;\n\t\tif (connectMs > input.base.connectMs) result.connectMs = connectMs;\n\t}\n\n\treturn result;\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst inner = await streamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t},\n\t\t});\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\tfor await (const event of inner) {\n\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t}\n\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\tpromptTokens: event.message.usage.input,\n\t\t\t\t\t\trecorder,\n\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t});\n\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t}\n\t\t\t\touter.push(event);\n\t\t\t\tif (terminalPushed) return;\n\t\t\t}\n\t\t\touter.end();\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs: input.firstTokenAtMs - input.requestStartedAtMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tsample.headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction expectedPrefillFromProfile(profile: ModelPerfProfile | undefined, promptTokens: number): number | undefined {\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || profile.samples < 1 || !prefillRate || promptTokens <= 0) return undefined;\n\tconst expectedPrefillMs = (promptTokens / prefillRate) * 1000;\n\treturn Number.isFinite(expectedPrefillMs) && expectedPrefillMs > 0 ? expectedPrefillMs : undefined;\n}\n\nfunction adaptiveBound(defaultMs: number, ceilingMs: number, expectedMs: number): number {\n\treturn Math.min(Math.ceil(Math.max(defaultMs, expectedMs * 3)), ceilingMs);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = isPositiveFiniteNumber(sample.headersToFirstTokenMs)\n\t\t? sample.headersToFirstTokenMs\n\t\t: sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OllamaRuntime, TransformersRuntime } from "./local-runtime.ts";
|
|
1
2
|
export type ResidencyControl = "full" | "keep-alive" | "advisory";
|
|
2
3
|
export type RuntimeRole = "active" | "router" | "bench" | "probe";
|
|
3
4
|
export interface RuntimeResidentModel {
|
|
@@ -49,6 +50,50 @@ export interface RuntimeResidencyAdapter {
|
|
|
49
50
|
ensureResident(model: string): Promise<void>;
|
|
50
51
|
release(model: string): Promise<void>;
|
|
51
52
|
}
|
|
53
|
+
export declare class RuntimeResidencyArbiter {
|
|
54
|
+
private readonly budgetBytes;
|
|
55
|
+
private readonly adapters;
|
|
56
|
+
constructor(args: {
|
|
57
|
+
budgetBytes: number;
|
|
58
|
+
adapters: RuntimeResidencyAdapter[];
|
|
59
|
+
});
|
|
60
|
+
ensureResident(adapterId: string, request: RuntimeLoadRequest): Promise<RuntimeResidencyPlan>;
|
|
61
|
+
}
|
|
62
|
+
export declare class OllamaRuntimeResidencyAdapter implements RuntimeResidencyAdapter {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly residencyControl: "keep-alive";
|
|
65
|
+
private readonly runtime;
|
|
66
|
+
private readonly nowMs;
|
|
67
|
+
constructor(id: string, runtime: Pick<OllamaRuntime, "listResidentModels" | "ensureResident" | "releaseResident">, options?: {
|
|
68
|
+
nowMs?: () => number;
|
|
69
|
+
});
|
|
70
|
+
list(): Promise<RuntimeResidentModel[]>;
|
|
71
|
+
ensureResident(model: string): Promise<void>;
|
|
72
|
+
release(model: string): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export declare class TransformersRuntimeResidencyAdapter implements RuntimeResidencyAdapter {
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly residencyControl: "full";
|
|
77
|
+
private readonly runtime;
|
|
78
|
+
private readonly model;
|
|
79
|
+
private readonly bytes;
|
|
80
|
+
private readonly nowMs;
|
|
81
|
+
constructor(id: string, runtime: Pick<TransformersRuntime, "detect" | "start" | "stop">, model: string, bytes: number, options?: {
|
|
82
|
+
nowMs?: () => number;
|
|
83
|
+
});
|
|
84
|
+
list(): Promise<RuntimeResidentModel[]>;
|
|
85
|
+
ensureResident(_model: string): Promise<void>;
|
|
86
|
+
release(_model: string): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
export declare class AdvisoryRuntimeResidencyAdapter implements RuntimeResidencyAdapter {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly residencyControl: "advisory";
|
|
91
|
+
private readonly residents;
|
|
92
|
+
constructor(id: string, residents: Omit<RuntimeResidentModel, "adapterId" | "residencyControl">[]);
|
|
93
|
+
list(): Promise<RuntimeResidentModel[]>;
|
|
94
|
+
ensureResident(_model: string): Promise<void>;
|
|
95
|
+
release(_model: string): Promise<void>;
|
|
96
|
+
}
|
|
52
97
|
export declare function planRuntimeResidency(args: {
|
|
53
98
|
budgetBytes: number;
|
|
54
99
|
residents: readonly RuntimeResidentModel[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-arbiter.d.ts","sourceRoot":"","sources":["../../../src/core/models/runtime-arbiter.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;AAClE,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,gBAAgB,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACpC,eAAe,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,oBAAoB,GAC7B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC7F;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,IAAI,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACxC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC3C,OAAO,EAAE,kBAAkB,CAAC;CAC5B,GAAG,oBAAoB,CAkDvB","sourcesContent":["export type ResidencyControl = \"full\" | \"keep-alive\" | \"advisory\";\nexport type RuntimeRole = \"active\" | \"router\" | \"bench\" | \"probe\";\n\nexport interface RuntimeResidentModel {\n\tadapterId: string;\n\tmodel: string;\n\tbytes: number;\n\tlastUsedAtMs: number;\n\tpriority: number;\n\tpinned?: boolean;\n\tresidencyControl: ResidencyControl;\n}\n\nexport interface RuntimeLoadRequest {\n\tmodel: string;\n\tbytes: number;\n\trole: RuntimeRole;\n\tpriority: number;\n\tnowMs: number;\n\tminDwellMs?: number;\n\tpinActiveModel?: string;\n\treservations?: RuntimeReservation[];\n\trecentEvictions?: RuntimeEvictionRecord[];\n}\n\nexport interface RuntimeReservation {\n\tmodel: string;\n\tbytes: number;\n\tpriority: number;\n}\n\nexport interface RuntimeEvictionRecord {\n\tevicted: string;\n\tloaded: string;\n\tatMs: number;\n}\n\nexport type RuntimeResidencyPlan =\n\t| { status: \"fits\"; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number }\n\t| { status: \"refuse\"; reason: string; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number };\n\nexport interface RuntimeResidencyAdapter {\n\tid: string;\n\tresidencyControl: ResidencyControl;\n\tlist(): Promise<RuntimeResidentModel[]>;\n\tensureResident(model: string): Promise<void>;\n\trelease(model: string): Promise<void>;\n}\n\nexport function planRuntimeResidency(args: {\n\tbudgetBytes: number;\n\tresidents: readonly RuntimeResidentModel[];\n\trequest: RuntimeLoadRequest;\n}): RuntimeResidencyPlan {\n\tconst requestedModels = new Map<string, { bytes: number; priority: number }>();\n\trequestedModels.set(args.request.model, { bytes: args.request.bytes, priority: args.request.priority });\n\tfor (const reservation of args.request.reservations ?? []) {\n\t\tconst existing = requestedModels.get(reservation.model);\n\t\tif (!existing || reservation.bytes > existing.bytes) {\n\t\t\trequestedModels.set(reservation.model, { bytes: reservation.bytes, priority: reservation.priority });\n\t\t}\n\t}\n\n\tconst residentByModel = new Map(args.residents.map((resident) => [resident.model, resident]));\n\tconst residentBytes = args.residents.reduce((sum, resident) => sum + resident.bytes, 0);\n\tconst missingBytes = [...requestedModels].reduce((sum, [model, request]) => {\n\t\treturn residentByModel.has(model) ? sum : sum + request.bytes;\n\t}, 0);\n\tconst requiredBytes = residentBytes + missingBytes;\n\tif (requiredBytes <= args.budgetBytes) {\n\t\treturn { status: \"fits\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tif (hasPingPongRisk(args.request)) {\n\t\treturn { status: \"refuse\", reason: \"anti-thrash\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tlet projectedBytes = requiredBytes;\n\tconst evict: RuntimeResidentModel[] = [];\n\tconst candidates = args.residents\n\t\t.filter((resident) => !requestedModels.has(resident.model))\n\t\t.filter((resident) => !resident.pinned && resident.model !== args.request.pinActiveModel)\n\t\t.filter((resident) => resident.residencyControl !== \"advisory\")\n\t\t.filter(\n\t\t\t(resident) =>\n\t\t\t\targs.request.minDwellMs === undefined ||\n\t\t\t\targs.request.nowMs - resident.lastUsedAtMs >= args.request.minDwellMs,\n\t\t)\n\t\t.sort((left, right) => left.priority - right.priority || left.lastUsedAtMs - right.lastUsedAtMs);\n\tfor (const candidate of candidates) {\n\t\tevict.push(candidate);\n\t\tprojectedBytes -= candidate.bytes;\n\t\tif (projectedBytes <= args.budgetBytes) {\n\t\t\treturn { status: \"fits\", evict, requiredBytes, budgetBytes: args.budgetBytes };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"refuse\",\n\t\treason: \"insufficient-evictable-memory\",\n\t\tevict,\n\t\trequiredBytes,\n\t\tbudgetBytes: args.budgetBytes,\n\t};\n}\n\nfunction hasPingPongRisk(request: RuntimeLoadRequest): boolean {\n\treturn (request.recentEvictions ?? []).some(\n\t\t(record) => record.evicted === request.model && request.nowMs - record.atMs <= (request.minDwellMs ?? 0),\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-arbiter.d.ts","sourceRoot":"","sources":["../../../src/core/models/runtime-arbiter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OllamaRuntime, TransformersRuntime } from \"./local-runtime.ts\";\n\nexport type ResidencyControl = \"full\" | \"keep-alive\" | \"advisory\";\nexport type RuntimeRole = \"active\" | \"router\" | \"bench\" | \"probe\";\n\nexport interface RuntimeResidentModel {\n\tadapterId: string;\n\tmodel: string;\n\tbytes: number;\n\tlastUsedAtMs: number;\n\tpriority: number;\n\tpinned?: boolean;\n\tresidencyControl: ResidencyControl;\n}\n\nexport interface RuntimeLoadRequest {\n\tmodel: string;\n\tbytes: number;\n\trole: RuntimeRole;\n\tpriority: number;\n\tnowMs: number;\n\tminDwellMs?: number;\n\tpinActiveModel?: string;\n\treservations?: RuntimeReservation[];\n\trecentEvictions?: RuntimeEvictionRecord[];\n}\n\nexport interface RuntimeReservation {\n\tmodel: string;\n\tbytes: number;\n\tpriority: number;\n}\n\nexport interface RuntimeEvictionRecord {\n\tevicted: string;\n\tloaded: string;\n\tatMs: number;\n}\n\nexport type RuntimeResidencyPlan =\n\t| { status: \"fits\"; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number }\n\t| { status: \"refuse\"; reason: string; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number };\n\nexport interface RuntimeResidencyAdapter {\n\tid: string;\n\tresidencyControl: ResidencyControl;\n\tlist(): Promise<RuntimeResidentModel[]>;\n\tensureResident(model: string): Promise<void>;\n\trelease(model: string): Promise<void>;\n}\n\nexport class RuntimeResidencyArbiter {\n\tprivate readonly budgetBytes: number;\n\tprivate readonly adapters: Map<string, RuntimeResidencyAdapter>;\n\n\tconstructor(args: { budgetBytes: number; adapters: RuntimeResidencyAdapter[] }) {\n\t\tthis.budgetBytes = args.budgetBytes;\n\t\tthis.adapters = new Map(args.adapters.map((adapter) => [adapter.id, adapter]));\n\t}\n\n\tasync ensureResident(adapterId: string, request: RuntimeLoadRequest): Promise<RuntimeResidencyPlan> {\n\t\tconst adapter = this.adapters.get(adapterId);\n\t\tconst residents = (await Promise.all([...this.adapters.values()].map((entry) => entry.list()))).flat();\n\t\tif (!adapter) {\n\t\t\treturn {\n\t\t\t\tstatus: \"refuse\",\n\t\t\t\treason: \"unknown-adapter\",\n\t\t\t\tevict: [],\n\t\t\t\trequiredBytes: request.bytes,\n\t\t\t\tbudgetBytes: this.budgetBytes,\n\t\t\t};\n\t\t}\n\t\tconst plan = planRuntimeResidency({ budgetBytes: this.budgetBytes, residents, request });\n\t\tif (plan.status !== \"fits\") return plan;\n\t\tfor (const resident of plan.evict) {\n\t\t\tawait this.adapters.get(resident.adapterId)?.release(resident.model);\n\t\t}\n\t\tawait adapter.ensureResident(request.model);\n\t\treturn plan;\n\t}\n}\n\nexport class OllamaRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"keep-alive\" as const;\n\tprivate readonly runtime: Pick<OllamaRuntime, \"listResidentModels\" | \"ensureResident\" | \"releaseResident\">;\n\tprivate readonly nowMs: () => number;\n\n\tconstructor(\n\t\tid: string,\n\t\truntime: Pick<OllamaRuntime, \"listResidentModels\" | \"ensureResident\" | \"releaseResident\">,\n\t\toptions: { nowMs?: () => number } = {},\n\t) {\n\t\tthis.id = id;\n\t\tthis.runtime = runtime;\n\t\tthis.nowMs = options.nowMs ?? Date.now;\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\tconst now = this.nowMs();\n\t\treturn (await this.runtime.listResidentModels()).map((model) => ({\n\t\t\tadapterId: this.id,\n\t\t\tmodel: model.name,\n\t\t\tbytes: model.sizeBytes,\n\t\t\tlastUsedAtMs: now,\n\t\t\tpriority: 10,\n\t\t\tresidencyControl: this.residencyControl,\n\t\t}));\n\t}\n\n\tasync ensureResident(model: string): Promise<void> {\n\t\tconst result = await this.runtime.ensureResident(model);\n\t\tif (!result.ok) throw new Error(result.error);\n\t}\n\n\tasync release(model: string): Promise<void> {\n\t\tawait this.runtime.releaseResident(model);\n\t}\n}\n\nexport class TransformersRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"full\" as const;\n\tprivate readonly runtime: Pick<TransformersRuntime, \"detect\" | \"start\" | \"stop\">;\n\tprivate readonly model: string;\n\tprivate readonly bytes: number;\n\tprivate readonly nowMs: () => number;\n\n\tconstructor(\n\t\tid: string,\n\t\truntime: Pick<TransformersRuntime, \"detect\" | \"start\" | \"stop\">,\n\t\tmodel: string,\n\t\tbytes: number,\n\t\toptions: { nowMs?: () => number } = {},\n\t) {\n\t\tthis.id = id;\n\t\tthis.runtime = runtime;\n\t\tthis.model = model;\n\t\tthis.bytes = bytes;\n\t\tthis.nowMs = options.nowMs ?? Date.now;\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\tconst status = await this.runtime.detect();\n\t\tif (!status.serverUp) return [];\n\t\treturn [\n\t\t\t{\n\t\t\t\tadapterId: this.id,\n\t\t\t\tmodel: this.model,\n\t\t\t\tbytes: this.bytes,\n\t\t\t\tlastUsedAtMs: this.nowMs(),\n\t\t\t\tpriority: 10,\n\t\t\t\tresidencyControl: this.residencyControl,\n\t\t\t},\n\t\t];\n\t}\n\n\tasync ensureResident(_model: string): Promise<void> {\n\t\tconst result = await this.runtime.start();\n\t\tif (!result.started && result.reason !== \"already_running\") throw new Error(result.reason);\n\t}\n\n\tasync release(_model: string): Promise<void> {\n\t\tthis.runtime.stop();\n\t}\n}\n\nexport class AdvisoryRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"advisory\" as const;\n\tprivate readonly residents: RuntimeResidentModel[];\n\n\tconstructor(id: string, residents: Omit<RuntimeResidentModel, \"adapterId\" | \"residencyControl\">[]) {\n\t\tthis.id = id;\n\t\tthis.residents = residents.map((resident) => ({\n\t\t\t...resident,\n\t\t\tadapterId: id,\n\t\t\tresidencyControl: this.residencyControl,\n\t\t}));\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\treturn [...this.residents];\n\t}\n\n\tasync ensureResident(_model: string): Promise<void> {}\n\n\tasync release(_model: string): Promise<void> {}\n}\n\nexport function planRuntimeResidency(args: {\n\tbudgetBytes: number;\n\tresidents: readonly RuntimeResidentModel[];\n\trequest: RuntimeLoadRequest;\n}): RuntimeResidencyPlan {\n\tconst requestedModels = new Map<string, { bytes: number; priority: number }>();\n\trequestedModels.set(args.request.model, { bytes: args.request.bytes, priority: args.request.priority });\n\tfor (const reservation of args.request.reservations ?? []) {\n\t\tconst existing = requestedModels.get(reservation.model);\n\t\tif (!existing || reservation.bytes > existing.bytes) {\n\t\t\trequestedModels.set(reservation.model, { bytes: reservation.bytes, priority: reservation.priority });\n\t\t}\n\t}\n\n\tconst residentByModel = new Map(args.residents.map((resident) => [resident.model, resident]));\n\tconst residentBytes = args.residents.reduce((sum, resident) => sum + resident.bytes, 0);\n\tconst missingBytes = [...requestedModels].reduce((sum, [model, request]) => {\n\t\treturn residentByModel.has(model) ? sum : sum + request.bytes;\n\t}, 0);\n\tconst requiredBytes = residentBytes + missingBytes;\n\tif (requiredBytes <= args.budgetBytes) {\n\t\treturn { status: \"fits\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tif (hasPingPongRisk(args.request)) {\n\t\treturn { status: \"refuse\", reason: \"anti-thrash\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tlet projectedBytes = requiredBytes;\n\tconst evict: RuntimeResidentModel[] = [];\n\tconst candidates = args.residents\n\t\t.filter((resident) => !requestedModels.has(resident.model))\n\t\t.filter((resident) => !resident.pinned && resident.model !== args.request.pinActiveModel)\n\t\t.filter((resident) => resident.residencyControl !== \"advisory\")\n\t\t.filter(\n\t\t\t(resident) =>\n\t\t\t\targs.request.minDwellMs === undefined ||\n\t\t\t\targs.request.nowMs - resident.lastUsedAtMs >= args.request.minDwellMs,\n\t\t)\n\t\t.sort((left, right) => left.priority - right.priority || left.lastUsedAtMs - right.lastUsedAtMs);\n\tfor (const candidate of candidates) {\n\t\tevict.push(candidate);\n\t\tprojectedBytes -= candidate.bytes;\n\t\tif (projectedBytes <= args.budgetBytes) {\n\t\t\treturn { status: \"fits\", evict, requiredBytes, budgetBytes: args.budgetBytes };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"refuse\",\n\t\treason: \"insufficient-evictable-memory\",\n\t\tevict,\n\t\trequiredBytes,\n\t\tbudgetBytes: args.budgetBytes,\n\t};\n}\n\nfunction hasPingPongRisk(request: RuntimeLoadRequest): boolean {\n\treturn (request.recentEvictions ?? []).some(\n\t\t(record) => record.evicted === request.model && request.nowMs - record.atMs <= (request.minDwellMs ?? 0),\n\t);\n}\n"]}
|
|
@@ -1,3 +1,118 @@
|
|
|
1
|
+
export class RuntimeResidencyArbiter {
|
|
2
|
+
budgetBytes;
|
|
3
|
+
adapters;
|
|
4
|
+
constructor(args) {
|
|
5
|
+
this.budgetBytes = args.budgetBytes;
|
|
6
|
+
this.adapters = new Map(args.adapters.map((adapter) => [adapter.id, adapter]));
|
|
7
|
+
}
|
|
8
|
+
async ensureResident(adapterId, request) {
|
|
9
|
+
const adapter = this.adapters.get(adapterId);
|
|
10
|
+
const residents = (await Promise.all([...this.adapters.values()].map((entry) => entry.list()))).flat();
|
|
11
|
+
if (!adapter) {
|
|
12
|
+
return {
|
|
13
|
+
status: "refuse",
|
|
14
|
+
reason: "unknown-adapter",
|
|
15
|
+
evict: [],
|
|
16
|
+
requiredBytes: request.bytes,
|
|
17
|
+
budgetBytes: this.budgetBytes,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const plan = planRuntimeResidency({ budgetBytes: this.budgetBytes, residents, request });
|
|
21
|
+
if (plan.status !== "fits")
|
|
22
|
+
return plan;
|
|
23
|
+
for (const resident of plan.evict) {
|
|
24
|
+
await this.adapters.get(resident.adapterId)?.release(resident.model);
|
|
25
|
+
}
|
|
26
|
+
await adapter.ensureResident(request.model);
|
|
27
|
+
return plan;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class OllamaRuntimeResidencyAdapter {
|
|
31
|
+
id;
|
|
32
|
+
residencyControl = "keep-alive";
|
|
33
|
+
runtime;
|
|
34
|
+
nowMs;
|
|
35
|
+
constructor(id, runtime, options = {}) {
|
|
36
|
+
this.id = id;
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
39
|
+
}
|
|
40
|
+
async list() {
|
|
41
|
+
const now = this.nowMs();
|
|
42
|
+
return (await this.runtime.listResidentModels()).map((model) => ({
|
|
43
|
+
adapterId: this.id,
|
|
44
|
+
model: model.name,
|
|
45
|
+
bytes: model.sizeBytes,
|
|
46
|
+
lastUsedAtMs: now,
|
|
47
|
+
priority: 10,
|
|
48
|
+
residencyControl: this.residencyControl,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
async ensureResident(model) {
|
|
52
|
+
const result = await this.runtime.ensureResident(model);
|
|
53
|
+
if (!result.ok)
|
|
54
|
+
throw new Error(result.error);
|
|
55
|
+
}
|
|
56
|
+
async release(model) {
|
|
57
|
+
await this.runtime.releaseResident(model);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class TransformersRuntimeResidencyAdapter {
|
|
61
|
+
id;
|
|
62
|
+
residencyControl = "full";
|
|
63
|
+
runtime;
|
|
64
|
+
model;
|
|
65
|
+
bytes;
|
|
66
|
+
nowMs;
|
|
67
|
+
constructor(id, runtime, model, bytes, options = {}) {
|
|
68
|
+
this.id = id;
|
|
69
|
+
this.runtime = runtime;
|
|
70
|
+
this.model = model;
|
|
71
|
+
this.bytes = bytes;
|
|
72
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
73
|
+
}
|
|
74
|
+
async list() {
|
|
75
|
+
const status = await this.runtime.detect();
|
|
76
|
+
if (!status.serverUp)
|
|
77
|
+
return [];
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
adapterId: this.id,
|
|
81
|
+
model: this.model,
|
|
82
|
+
bytes: this.bytes,
|
|
83
|
+
lastUsedAtMs: this.nowMs(),
|
|
84
|
+
priority: 10,
|
|
85
|
+
residencyControl: this.residencyControl,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
async ensureResident(_model) {
|
|
90
|
+
const result = await this.runtime.start();
|
|
91
|
+
if (!result.started && result.reason !== "already_running")
|
|
92
|
+
throw new Error(result.reason);
|
|
93
|
+
}
|
|
94
|
+
async release(_model) {
|
|
95
|
+
this.runtime.stop();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class AdvisoryRuntimeResidencyAdapter {
|
|
99
|
+
id;
|
|
100
|
+
residencyControl = "advisory";
|
|
101
|
+
residents;
|
|
102
|
+
constructor(id, residents) {
|
|
103
|
+
this.id = id;
|
|
104
|
+
this.residents = residents.map((resident) => ({
|
|
105
|
+
...resident,
|
|
106
|
+
adapterId: id,
|
|
107
|
+
residencyControl: this.residencyControl,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
async list() {
|
|
111
|
+
return [...this.residents];
|
|
112
|
+
}
|
|
113
|
+
async ensureResident(_model) { }
|
|
114
|
+
async release(_model) { }
|
|
115
|
+
}
|
|
1
116
|
export function planRuntimeResidency(args) {
|
|
2
117
|
const requestedModels = new Map();
|
|
3
118
|
requestedModels.set(args.request.model, { bytes: args.request.bytes, priority: args.request.priority });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-arbiter.js","sourceRoot":"","sources":["../../../src/core/models/runtime-arbiter.ts"],"names":[],"mappings":"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","sourcesContent":["export type ResidencyControl = \"full\" | \"keep-alive\" | \"advisory\";\nexport type RuntimeRole = \"active\" | \"router\" | \"bench\" | \"probe\";\n\nexport interface RuntimeResidentModel {\n\tadapterId: string;\n\tmodel: string;\n\tbytes: number;\n\tlastUsedAtMs: number;\n\tpriority: number;\n\tpinned?: boolean;\n\tresidencyControl: ResidencyControl;\n}\n\nexport interface RuntimeLoadRequest {\n\tmodel: string;\n\tbytes: number;\n\trole: RuntimeRole;\n\tpriority: number;\n\tnowMs: number;\n\tminDwellMs?: number;\n\tpinActiveModel?: string;\n\treservations?: RuntimeReservation[];\n\trecentEvictions?: RuntimeEvictionRecord[];\n}\n\nexport interface RuntimeReservation {\n\tmodel: string;\n\tbytes: number;\n\tpriority: number;\n}\n\nexport interface RuntimeEvictionRecord {\n\tevicted: string;\n\tloaded: string;\n\tatMs: number;\n}\n\nexport type RuntimeResidencyPlan =\n\t| { status: \"fits\"; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number }\n\t| { status: \"refuse\"; reason: string; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number };\n\nexport interface RuntimeResidencyAdapter {\n\tid: string;\n\tresidencyControl: ResidencyControl;\n\tlist(): Promise<RuntimeResidentModel[]>;\n\tensureResident(model: string): Promise<void>;\n\trelease(model: string): Promise<void>;\n}\n\nexport function planRuntimeResidency(args: {\n\tbudgetBytes: number;\n\tresidents: readonly RuntimeResidentModel[];\n\trequest: RuntimeLoadRequest;\n}): RuntimeResidencyPlan {\n\tconst requestedModels = new Map<string, { bytes: number; priority: number }>();\n\trequestedModels.set(args.request.model, { bytes: args.request.bytes, priority: args.request.priority });\n\tfor (const reservation of args.request.reservations ?? []) {\n\t\tconst existing = requestedModels.get(reservation.model);\n\t\tif (!existing || reservation.bytes > existing.bytes) {\n\t\t\trequestedModels.set(reservation.model, { bytes: reservation.bytes, priority: reservation.priority });\n\t\t}\n\t}\n\n\tconst residentByModel = new Map(args.residents.map((resident) => [resident.model, resident]));\n\tconst residentBytes = args.residents.reduce((sum, resident) => sum + resident.bytes, 0);\n\tconst missingBytes = [...requestedModels].reduce((sum, [model, request]) => {\n\t\treturn residentByModel.has(model) ? sum : sum + request.bytes;\n\t}, 0);\n\tconst requiredBytes = residentBytes + missingBytes;\n\tif (requiredBytes <= args.budgetBytes) {\n\t\treturn { status: \"fits\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tif (hasPingPongRisk(args.request)) {\n\t\treturn { status: \"refuse\", reason: \"anti-thrash\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tlet projectedBytes = requiredBytes;\n\tconst evict: RuntimeResidentModel[] = [];\n\tconst candidates = args.residents\n\t\t.filter((resident) => !requestedModels.has(resident.model))\n\t\t.filter((resident) => !resident.pinned && resident.model !== args.request.pinActiveModel)\n\t\t.filter((resident) => resident.residencyControl !== \"advisory\")\n\t\t.filter(\n\t\t\t(resident) =>\n\t\t\t\targs.request.minDwellMs === undefined ||\n\t\t\t\targs.request.nowMs - resident.lastUsedAtMs >= args.request.minDwellMs,\n\t\t)\n\t\t.sort((left, right) => left.priority - right.priority || left.lastUsedAtMs - right.lastUsedAtMs);\n\tfor (const candidate of candidates) {\n\t\tevict.push(candidate);\n\t\tprojectedBytes -= candidate.bytes;\n\t\tif (projectedBytes <= args.budgetBytes) {\n\t\t\treturn { status: \"fits\", evict, requiredBytes, budgetBytes: args.budgetBytes };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"refuse\",\n\t\treason: \"insufficient-evictable-memory\",\n\t\tevict,\n\t\trequiredBytes,\n\t\tbudgetBytes: args.budgetBytes,\n\t};\n}\n\nfunction hasPingPongRisk(request: RuntimeLoadRequest): boolean {\n\treturn (request.recentEvictions ?? []).some(\n\t\t(record) => record.evicted === request.model && request.nowMs - record.atMs <= (request.minDwellMs ?? 0),\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-arbiter.js","sourceRoot":"","sources":["../../../src/core/models/runtime-arbiter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OllamaRuntime, TransformersRuntime } from \"./local-runtime.ts\";\n\nexport type ResidencyControl = \"full\" | \"keep-alive\" | \"advisory\";\nexport type RuntimeRole = \"active\" | \"router\" | \"bench\" | \"probe\";\n\nexport interface RuntimeResidentModel {\n\tadapterId: string;\n\tmodel: string;\n\tbytes: number;\n\tlastUsedAtMs: number;\n\tpriority: number;\n\tpinned?: boolean;\n\tresidencyControl: ResidencyControl;\n}\n\nexport interface RuntimeLoadRequest {\n\tmodel: string;\n\tbytes: number;\n\trole: RuntimeRole;\n\tpriority: number;\n\tnowMs: number;\n\tminDwellMs?: number;\n\tpinActiveModel?: string;\n\treservations?: RuntimeReservation[];\n\trecentEvictions?: RuntimeEvictionRecord[];\n}\n\nexport interface RuntimeReservation {\n\tmodel: string;\n\tbytes: number;\n\tpriority: number;\n}\n\nexport interface RuntimeEvictionRecord {\n\tevicted: string;\n\tloaded: string;\n\tatMs: number;\n}\n\nexport type RuntimeResidencyPlan =\n\t| { status: \"fits\"; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number }\n\t| { status: \"refuse\"; reason: string; evict: RuntimeResidentModel[]; requiredBytes: number; budgetBytes: number };\n\nexport interface RuntimeResidencyAdapter {\n\tid: string;\n\tresidencyControl: ResidencyControl;\n\tlist(): Promise<RuntimeResidentModel[]>;\n\tensureResident(model: string): Promise<void>;\n\trelease(model: string): Promise<void>;\n}\n\nexport class RuntimeResidencyArbiter {\n\tprivate readonly budgetBytes: number;\n\tprivate readonly adapters: Map<string, RuntimeResidencyAdapter>;\n\n\tconstructor(args: { budgetBytes: number; adapters: RuntimeResidencyAdapter[] }) {\n\t\tthis.budgetBytes = args.budgetBytes;\n\t\tthis.adapters = new Map(args.adapters.map((adapter) => [adapter.id, adapter]));\n\t}\n\n\tasync ensureResident(adapterId: string, request: RuntimeLoadRequest): Promise<RuntimeResidencyPlan> {\n\t\tconst adapter = this.adapters.get(adapterId);\n\t\tconst residents = (await Promise.all([...this.adapters.values()].map((entry) => entry.list()))).flat();\n\t\tif (!adapter) {\n\t\t\treturn {\n\t\t\t\tstatus: \"refuse\",\n\t\t\t\treason: \"unknown-adapter\",\n\t\t\t\tevict: [],\n\t\t\t\trequiredBytes: request.bytes,\n\t\t\t\tbudgetBytes: this.budgetBytes,\n\t\t\t};\n\t\t}\n\t\tconst plan = planRuntimeResidency({ budgetBytes: this.budgetBytes, residents, request });\n\t\tif (plan.status !== \"fits\") return plan;\n\t\tfor (const resident of plan.evict) {\n\t\t\tawait this.adapters.get(resident.adapterId)?.release(resident.model);\n\t\t}\n\t\tawait adapter.ensureResident(request.model);\n\t\treturn plan;\n\t}\n}\n\nexport class OllamaRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"keep-alive\" as const;\n\tprivate readonly runtime: Pick<OllamaRuntime, \"listResidentModels\" | \"ensureResident\" | \"releaseResident\">;\n\tprivate readonly nowMs: () => number;\n\n\tconstructor(\n\t\tid: string,\n\t\truntime: Pick<OllamaRuntime, \"listResidentModels\" | \"ensureResident\" | \"releaseResident\">,\n\t\toptions: { nowMs?: () => number } = {},\n\t) {\n\t\tthis.id = id;\n\t\tthis.runtime = runtime;\n\t\tthis.nowMs = options.nowMs ?? Date.now;\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\tconst now = this.nowMs();\n\t\treturn (await this.runtime.listResidentModels()).map((model) => ({\n\t\t\tadapterId: this.id,\n\t\t\tmodel: model.name,\n\t\t\tbytes: model.sizeBytes,\n\t\t\tlastUsedAtMs: now,\n\t\t\tpriority: 10,\n\t\t\tresidencyControl: this.residencyControl,\n\t\t}));\n\t}\n\n\tasync ensureResident(model: string): Promise<void> {\n\t\tconst result = await this.runtime.ensureResident(model);\n\t\tif (!result.ok) throw new Error(result.error);\n\t}\n\n\tasync release(model: string): Promise<void> {\n\t\tawait this.runtime.releaseResident(model);\n\t}\n}\n\nexport class TransformersRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"full\" as const;\n\tprivate readonly runtime: Pick<TransformersRuntime, \"detect\" | \"start\" | \"stop\">;\n\tprivate readonly model: string;\n\tprivate readonly bytes: number;\n\tprivate readonly nowMs: () => number;\n\n\tconstructor(\n\t\tid: string,\n\t\truntime: Pick<TransformersRuntime, \"detect\" | \"start\" | \"stop\">,\n\t\tmodel: string,\n\t\tbytes: number,\n\t\toptions: { nowMs?: () => number } = {},\n\t) {\n\t\tthis.id = id;\n\t\tthis.runtime = runtime;\n\t\tthis.model = model;\n\t\tthis.bytes = bytes;\n\t\tthis.nowMs = options.nowMs ?? Date.now;\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\tconst status = await this.runtime.detect();\n\t\tif (!status.serverUp) return [];\n\t\treturn [\n\t\t\t{\n\t\t\t\tadapterId: this.id,\n\t\t\t\tmodel: this.model,\n\t\t\t\tbytes: this.bytes,\n\t\t\t\tlastUsedAtMs: this.nowMs(),\n\t\t\t\tpriority: 10,\n\t\t\t\tresidencyControl: this.residencyControl,\n\t\t\t},\n\t\t];\n\t}\n\n\tasync ensureResident(_model: string): Promise<void> {\n\t\tconst result = await this.runtime.start();\n\t\tif (!result.started && result.reason !== \"already_running\") throw new Error(result.reason);\n\t}\n\n\tasync release(_model: string): Promise<void> {\n\t\tthis.runtime.stop();\n\t}\n}\n\nexport class AdvisoryRuntimeResidencyAdapter implements RuntimeResidencyAdapter {\n\treadonly id: string;\n\treadonly residencyControl = \"advisory\" as const;\n\tprivate readonly residents: RuntimeResidentModel[];\n\n\tconstructor(id: string, residents: Omit<RuntimeResidentModel, \"adapterId\" | \"residencyControl\">[]) {\n\t\tthis.id = id;\n\t\tthis.residents = residents.map((resident) => ({\n\t\t\t...resident,\n\t\t\tadapterId: id,\n\t\t\tresidencyControl: this.residencyControl,\n\t\t}));\n\t}\n\n\tasync list(): Promise<RuntimeResidentModel[]> {\n\t\treturn [...this.residents];\n\t}\n\n\tasync ensureResident(_model: string): Promise<void> {}\n\n\tasync release(_model: string): Promise<void> {}\n}\n\nexport function planRuntimeResidency(args: {\n\tbudgetBytes: number;\n\tresidents: readonly RuntimeResidentModel[];\n\trequest: RuntimeLoadRequest;\n}): RuntimeResidencyPlan {\n\tconst requestedModels = new Map<string, { bytes: number; priority: number }>();\n\trequestedModels.set(args.request.model, { bytes: args.request.bytes, priority: args.request.priority });\n\tfor (const reservation of args.request.reservations ?? []) {\n\t\tconst existing = requestedModels.get(reservation.model);\n\t\tif (!existing || reservation.bytes > existing.bytes) {\n\t\t\trequestedModels.set(reservation.model, { bytes: reservation.bytes, priority: reservation.priority });\n\t\t}\n\t}\n\n\tconst residentByModel = new Map(args.residents.map((resident) => [resident.model, resident]));\n\tconst residentBytes = args.residents.reduce((sum, resident) => sum + resident.bytes, 0);\n\tconst missingBytes = [...requestedModels].reduce((sum, [model, request]) => {\n\t\treturn residentByModel.has(model) ? sum : sum + request.bytes;\n\t}, 0);\n\tconst requiredBytes = residentBytes + missingBytes;\n\tif (requiredBytes <= args.budgetBytes) {\n\t\treturn { status: \"fits\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tif (hasPingPongRisk(args.request)) {\n\t\treturn { status: \"refuse\", reason: \"anti-thrash\", evict: [], requiredBytes, budgetBytes: args.budgetBytes };\n\t}\n\n\tlet projectedBytes = requiredBytes;\n\tconst evict: RuntimeResidentModel[] = [];\n\tconst candidates = args.residents\n\t\t.filter((resident) => !requestedModels.has(resident.model))\n\t\t.filter((resident) => !resident.pinned && resident.model !== args.request.pinActiveModel)\n\t\t.filter((resident) => resident.residencyControl !== \"advisory\")\n\t\t.filter(\n\t\t\t(resident) =>\n\t\t\t\targs.request.minDwellMs === undefined ||\n\t\t\t\targs.request.nowMs - resident.lastUsedAtMs >= args.request.minDwellMs,\n\t\t)\n\t\t.sort((left, right) => left.priority - right.priority || left.lastUsedAtMs - right.lastUsedAtMs);\n\tfor (const candidate of candidates) {\n\t\tevict.push(candidate);\n\t\tprojectedBytes -= candidate.bytes;\n\t\tif (projectedBytes <= args.budgetBytes) {\n\t\t\treturn { status: \"fits\", evict, requiredBytes, budgetBytes: args.budgetBytes };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"refuse\",\n\t\treason: \"insufficient-evictable-memory\",\n\t\tevict,\n\t\trequiredBytes,\n\t\tbudgetBytes: args.budgetBytes,\n\t};\n}\n\nfunction hasPingPongRisk(request: RuntimeLoadRequest): boolean {\n\treturn (request.recentEvictions ?? []).some(\n\t\t(record) => record.evicted === request.model && request.nowMs - record.atMs <= (request.minDwellMs ?? 0),\n\t);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-model-commands.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/local-model-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/models` local-model lifecycle and `/fitness` probe/role-assignment flows\n * extracted from interactive-mode.\n *\n * `/models` is a USER-invoked local-model manager (list/add/remove/stop, never a\n * model-invokable tool); the fitness helpers probe a model on the current host\n * and land a shaped role. The server/selector flows share a `LocalModelHost`\n * seam, while `runFitnessAndAssign`/`assignFitnessRole` take narrow host shapes\n * matching their prototype-driven behaviour tests (fitness-probe-gate,\n * fitness-role-assignment), which keep exercising interactive-mode's thin\n * wrappers unchanged.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport { getSupportedThinkingLevels } from \"@caupulican/pi-ai\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype SelectItem,\n\tSelectList,\n\ttype SelectListLayoutOptions,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@caupulican/pi-tui\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { AgentSession } from \"../../core/agent-session.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport { resolveCliModel } from \"../../core/model-resolver.ts\";\nimport { evaluateSurfaceFitness } from \"../../core/model-router/fitness-gate.ts\";\nimport {\n\tderiveLocalContextSizing,\n\trenderOllamaContextModelfile,\n\tsizedLocalModelRef,\n} from \"../../core/models/context-sizing.ts\";\nimport { DEFAULT_MODEL_SUGGESTIONS } from \"../../core/models/default-model-suggestions.ts\";\nimport { FitnessStore } from \"../../core/models/fitness-store.ts\";\nimport {\n\tHF_TRANSFORMERS_PROVIDER,\n\tregisterLocalModel,\n\tregisterTransformersModel,\n\tunregisterLocalModel,\n\tunregisterTransformersModel,\n} from \"../../core/models/local-registration.ts\";\nimport type { OllamaRuntime, TransformersRuntime } from \"../../core/models/local-runtime.ts\";\nimport { matchesInstalledLocalModel, normalizeModelSource } from \"../../core/models/model-ref.ts\";\nimport { formatModelFitnessReport, isProbeAllFailed } from \"../../core/research/model-fitness.ts\";\nimport type { SettingsManager } from \"../../core/settings-manager.ts\";\nimport { DynamicBorder } from \"./components/dynamic-border.ts\";\nimport { type FitnessRole, FitnessRoleSelectorComponent } from \"./components/fitness-role-selector.ts\";\nimport { ModelSelectorComponent } from \"./components/model-selector.ts\";\nimport { ModelSuggestionSelectorComponent } from \"./components/model-suggestion-selector.ts\";\nimport { getSelectListTheme } from \"./theme/theme.ts\";\n\ntype SelectorFactory = (done: () => void) => { component: Component; focus: Component };\n\nconst MODEL_ROUTER_THINKING_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {\n\tminPrimaryColumnWidth: 12,\n\tmaxPrimaryColumnWidth: 32,\n};\n\nconst MODEL_ROUTER_THINKING_INHERIT_VALUE = \"__inherit_model_router_thinking__\";\nconst MODEL_ROUTER_THINKING_INHERIT_LABEL = \"(inherit)\";\n\nconst THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning (~1k tokens)\",\n\tlow: \"Light reasoning (~2k tokens)\",\n\tmedium: \"Moderate reasoning (~8k tokens)\",\n\thigh: \"Deep reasoning (~16k tokens)\",\n\txhigh: \"Maximum reasoning (~32k tokens)\",\n};\n\nconst MODEL_ROUTER_THINKING_FALLBACK_LEVELS: readonly ThinkingLevel[] = [\n\t\"off\",\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n];\n\ntype RouterThinkingLevel = ThinkingLevel | typeof MODEL_ROUTER_THINKING_INHERIT_VALUE;\ntype ModelRouterThinkingField =\n\t| \"cheapThinking\"\n\t| \"mediumThinking\"\n\t| \"expensiveThinking\"\n\t| \"executorThinking\"\n\t| \"judgeThinking\";\n\n/** Narrow seam for persisting a probed model's role — matches the fitness-role test. */\nexport interface AssignRoleHost {\n\treadonly settingsManager: SettingsManager;\n\tshowStatus(message: string): void;\n}\n\n/** Seam for the fitness probe + role-selector flow — matches the fitness-probe-gate test. */\nexport interface RunFitnessHost {\n\treadonly session: Pick<AgentSession, \"runModelFitness\"> & { modelRegistry?: ModelRegistry };\n\treadonly settingsManager: SettingsManager;\n\treadonly chatContainer: Container;\n\treadonly ui: TUI;\n\tshowStatus(message: string): void;\n\tshowError(message: string): void;\n\tshowSelector(create: SelectorFactory): void;\n}\n\n/** Full seam for the `/models` server/selector flows. */\nexport interface LocalModelHost {\n\treadonly localRuntime: OllamaRuntime;\n\treadonly session: AgentSession;\n\treadonly settingsManager: SettingsManager;\n\treadonly ui: TUI;\n\treadonly chatContainer: Container;\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime;\n\tshowStatus(message: string): void;\n\tshowError(message: string): void;\n\tshowSelector(create: SelectorFactory): void;\n}\n\nclass ModelRouterThinkingSelectorComponent extends Container {\n\tprivate selectList: SelectList;\n\n\tconstructor(\n\t\tcurrentLevel: RouterThinkingLevel,\n\t\tavailableLevels: ThinkingLevel[],\n\t\tonSelect: (level: RouterThinkingLevel) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tconst thinkingOptions: SelectItem[] = [\n\t\t\t{\n\t\t\t\tvalue: MODEL_ROUTER_THINKING_INHERIT_VALUE,\n\t\t\t\tlabel: MODEL_ROUTER_THINKING_INHERIT_LABEL,\n\t\t\t\tdescription: \"Use current session default for this tier.\",\n\t\t\t},\n\t\t\t...availableLevels.map((level) => ({\n\t\t\t\tvalue: level,\n\t\t\t\tlabel: level,\n\t\t\t\tdescription: THINKING_LEVEL_DESCRIPTIONS[level],\n\t\t\t})),\n\t\t];\n\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.selectList = new SelectList(\n\t\t\tthinkingOptions,\n\t\t\tthinkingOptions.length,\n\t\t\tgetSelectListTheme(),\n\t\t\tMODEL_ROUTER_THINKING_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\tconst index = thinkingOptions.findIndex((item) => item.value === currentLevel);\n\t\tif (index !== -1) {\n\t\t\tthis.selectList.setSelectedIndex(index);\n\t\t}\n\n\t\tthis.selectList.onSelect = (item) => {\n\t\t\tonSelect(item.value as RouterThinkingLevel);\n\t\t};\n\t\tthis.selectList.onCancel = () => {\n\t\t\tonCancel();\n\t\t};\n\n\t\tthis.addChild(this.selectList);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n}\n/**\n * /models — USER-invoked local model lifecycle (never a model-invokable tool):\n * list/add/remove/stop per local-model-lifecycle-design.md. Removal is explicit-only with\n * full disclosure; a pasted install command is parsed for its ref, never executed.\n */\nexport async function handleModelsCommand(host: LocalModelHost, argsText: string): Promise<void> {\n\tconst [action = \"list\", ...rest] = argsText.split(/\\s+/).filter(Boolean);\n\ttry {\n\t\tif (action === \"suggest\" || action === \"suggestions\") {\n\t\t\tshowModelSuggestionSelector(host);\n\t\t\treturn;\n\t\t}\n\t\tif (action === \"stop\") {\n\t\t\tconst stopped = host.localRuntime.stop();\n\t\t\tlet stoppedTransformers = 0;\n\t\t\tfor (const model of host.session.modelRegistry\n\t\t\t\t.getAll()\n\t\t\t\t.filter((entry) => entry.provider === HF_TRANSFORMERS_PROVIDER)) {\n\t\t\t\tconst serverUrl = model.baseUrl.replace(/\\/v1\\/?$/, \"\");\n\t\t\t\tif (host.getTransformersRuntime(model.id, serverUrl).stop().stopped) stoppedTransformers++;\n\t\t\t}\n\t\t\tconst stoppedAny = stopped.stopped || stoppedTransformers > 0;\n\t\t\thost.showStatus(\n\t\t\t\tstoppedAny\n\t\t\t\t\t? `Pi-managed local model server stopped${stoppedTransformers > 0 ? ` (${stoppedTransformers} Transformers sidecar(s))` : \"\"}; models remain installed.`\n\t\t\t\t\t: \"No pi-managed server running (a system server, if any, is not pi's to stop).\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (action === \"add\") {\n\t\t\tconst rawRef = rest.join(\" \");\n\t\t\tif (!rawRef) {\n\t\t\t\thost.showStatus(\n\t\t\t\t\t\"Usage: /models add <ollama-tag | hf.co/org/repo[:quant] | curated HF full-base ref | huggingface URL | pasted install command>\",\n\t\t\t\t);\n\t\t\t\thost.showStatus(\"Or start from a validated suggestion: /models suggest\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst source = normalizeModelSource(rawRef);\n\t\t\tif (source.type === \"rejected\") {\n\t\t\t\thost.showStatus(`Not added: ${source.reason}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (source.type === \"api\") {\n\t\t\t\thost.showStatus(\n\t\t\t\t\t`${source.ref} is an API model — nothing to install. Configure auth for the provider, then probe it with /fitness ${source.ref}.`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (source.type === \"transformers\") {\n\t\t\t\tawait addTransformersModel(host, source.modelId);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait addLocalModel(host, source.pullRef);\n\t\t\treturn;\n\t\t}\n\n\t\tif (action === \"remove\") {\n\t\t\tconst ref = rest[0];\n\t\t\tconst confirmed = rest[1] === \"confirm\";\n\t\t\tif (!ref) {\n\t\t\t\thost.showStatus(\"Usage: /models remove <ref> confirm\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst source = normalizeModelSource(ref);\n\t\t\tif (source.type === \"transformers\") {\n\t\t\t\tawait removeTransformersModel(host, source.modelId, confirmed);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait removeLocalModel(host, ref, confirmed);\n\t\t\treturn;\n\t\t}\n\n\t\tawait listLocalModels(host);\n\t} catch (error) {\n\t\thost.showError(error instanceof Error ? error.message : String(error));\n\t}\n}\n\nexport async function ensureLocalServer(host: LocalModelHost): Promise<boolean> {\n\tconst status = await host.localRuntime.detect();\n\tif (status.serverUp) return true;\n\tif (!status.binaryPath) {\n\t\tfor (const line of host.localRuntime.installGuide()) host.showStatus(line);\n\t\treturn false;\n\t}\n\thost.showStatus(\n\t\t`Starting local model server (${status.binarySource} binary, owned storage: ${status.ownedModelsDir})…`,\n\t);\n\tconst started = await host.localRuntime.start();\n\tif (!started.started) {\n\t\thost.showStatus(`Could not start the local server: ${started.reason}`);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nexport async function listLocalModels(host: LocalModelHost): Promise<void> {\n\tconst status = await host.localRuntime.detect();\n\tconst models = status.serverUp ? await host.localRuntime.list() : [];\n\tconst ollamaLines = status.serverUp\n\t\t? [\n\t\t\t\t`Ollama models (${status.managedByPi ? `pi-managed server, storage: ${status.ownedModelsDir}` : \"system server — storage owned by the system daemon\"}):`,\n\t\t\t\t...(models.length === 0\n\t\t\t\t\t? [\" (none installed — /models add <ref>, or /models suggest for a validated roster)\"]\n\t\t\t\t\t: []),\n\t\t\t]\n\t\t: [\n\t\t\t\t\"Ollama models:\",\n\t\t\t\t...(status.binaryPath\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t` server not running (binary: ${status.binarySource}). /models add starts it on demand; /fitness probes registered models.`,\n\t\t\t\t\t\t]\n\t\t\t\t\t: host.localRuntime.installGuide().map((line) => ` ${line}`)),\n\t\t\t];\n\tconst fitness = FitnessStore.forAgentDir(getAgentDir()).getForHost();\n\tconst transformersModels = host.session.modelRegistry\n\t\t.getAll()\n\t\t.filter((model) => model.provider === HF_TRANSFORMERS_PROVIDER);\n\tconst transformersLines = await Promise.all(\n\t\ttransformersModels.map(async (model) => {\n\t\t\tconst serverUrl = model.baseUrl.replace(/\\/v1\\/?$/, \"\");\n\t\t\tconst runtime = host.getTransformersRuntime(model.id, serverUrl);\n\t\t\tconst runtimeStatus = await runtime.detect();\n\t\t\tconst report = fitness.find((entry) => entry.model === `${HF_TRANSFORMERS_PROVIDER}/${model.id}`);\n\t\t\tconst readiness = runtimeStatus.serverUp\n\t\t\t\t? \"server running\"\n\t\t\t\t: runtimeStatus.runtimeInstalled\n\t\t\t\t\t? \"runtime installed\"\n\t\t\t\t\t: \"runtime missing\";\n\t\t\tconst probe = report\n\t\t\t\t? `probed ${report.at.slice(0, 10)}: digest ${report.report.digest?.succeeded ?? \"?\"}/${report.report.digest?.total ?? \"?\"}, tool-calls ${report.report.toolCall.succeeded}/${report.report.toolCall.total}${report.report.tokensPerSecond ? `, ~${report.report.tokensPerSecond} tok/s` : \"\"}`\n\t\t\t\t: `unprobed — run /fitness ${HF_TRANSFORMERS_PROVIDER}/${model.id}`;\n\t\t\treturn ` - ${model.id} (${readiness}, cache: ${runtimeStatus.cacheDir}) · ${probe}`;\n\t\t}),\n\t);\n\tconst lines = [\n\t\t...ollamaLines,\n\t\t...models.map((model) => {\n\t\t\tconst report = fitness.find((entry) => entry.model === `ollama/${model.name}`);\n\t\t\tconst gb = (model.sizeBytes / 1e9).toFixed(2);\n\t\t\tconst probe = report\n\t\t\t\t? `probed ${report.at.slice(0, 10)}: digest ${report.report.digest?.succeeded ?? \"?\"}/${report.report.digest?.total ?? \"?\"}, tool-calls ${report.report.toolCall.succeeded}/${report.report.toolCall.total}${report.report.tokensPerSecond ? `, ~${report.report.tokensPerSecond} tok/s` : \"\"}`\n\t\t\t\t: `unprobed — run /fitness ollama/${model.name}`;\n\t\t\treturn ` - ${model.name} (${gb} GB) · ${probe}`;\n\t\t}),\n\t\t...(transformersLines.length > 0 ? [\"Pi-managed Transformers models:\", ...transformersLines] : []),\n\t\t\"Commands: /models add <ref> · /models remove <ref> confirm · /models stop\",\n\t];\n\tfor (const line of lines) host.showStatus(line);\n}\n\nexport async function addLocalModel(host: LocalModelHost, pullRef: string, preselectRole?: FitnessRole): Promise<void> {\n\tif (!(await ensureLocalServer(host))) return;\n\thost.showStatus(`Pulling ${pullRef}… (weights land in the server's model storage)`);\n\tlet lastShown = 0;\n\tconst pulled = await host.localRuntime.pull(pullRef, (progress) => {\n\t\tconst now = Date.now();\n\t\tif (now - lastShown > 2000) {\n\t\t\tlastShown = now;\n\t\t\thost.showStatus(` ${pullRef}: ${progress}`);\n\t\t}\n\t});\n\tif (!pulled.ok) {\n\t\thost.showStatus(`Pull failed: ${pulled.error}`);\n\t\treturn;\n\t}\n\tconst installed = await host.localRuntime.list().catch(() => []);\n\tconst installedEntry = installed.find((entry) => matchesInstalledLocalModel(pullRef, entry.name));\n\tconst shown = await host.localRuntime.show(pullRef);\n\tconst sizing =\n\t\tinstalledEntry && shown.ok\n\t\t\t? deriveLocalContextSizing({\n\t\t\t\t\thost: { totalMemBytes: totalmem() },\n\t\t\t\t\tmodel: { modelInfo: shown.info.modelInfo, weightsBytes: installedEntry.sizeBytes },\n\t\t\t\t\truntime: { supportsKvQuantization: true },\n\t\t\t\t})\n\t\t\t: undefined;\n\tlet registeredRef = pullRef;\n\tif (sizing) {\n\t\tconst sizedRef = sizedLocalModelRef(pullRef, sizing.numCtx);\n\t\tconst created = await host.localRuntime.createFromModelfile({\n\t\t\tname: sizedRef,\n\t\t\tmodelfile: renderOllamaContextModelfile({ from: pullRef, numCtx: sizing.numCtx }),\n\t\t});\n\t\tif (!created.ok) {\n\t\t\thost.showStatus(`Pulled, but could not create sized context model: ${created.error}`);\n\t\t\treturn;\n\t\t}\n\t\tregisteredRef = sizedRef;\n\t}\n\tconst registration = registerLocalModel({\n\t\tagentDir: getAgentDir(),\n\t\tref: registeredRef,\n\t\tbaseUrl: host.localRuntime.baseUrl,\n\t\tcontextWindow: sizing?.numCtx,\n\t\tservedContextWindow: sizing?.numCtx,\n\t});\n\tif (!registration.ok) {\n\t\thost.showStatus(`Pulled, but not auto-registered: ${registration.reason}`);\n\t\tif (registration.manualSnippet) {\n\t\t\thost.showStatus(`Add this to ${registration.modelsJsonPath} yourself:\\n${registration.manualSnippet}`);\n\t\t}\n\t\treturn;\n\t}\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(`${pullRef} installed and registered as ollama/${registeredRef}. Probing fitness…`);\n\tawait runFitnessAndAssign(host, `ollama/${registeredRef}`, preselectRole);\n}\n\nexport async function addTransformersModel(\n\thost: LocalModelHost,\n\tmodelId: string,\n\tpreselectRole?: FitnessRole,\n): Promise<void> {\n\tconst runtime = host.getTransformersRuntime(modelId);\n\tlet status = await runtime.detect();\n\tif (!status.runtimeInstalled) {\n\t\thost.showStatus(`Installing isolated Transformers runtime for ${modelId} at ${status.venvDir}…`);\n\t\tconst installed = await runtime.installManaged((progress) => host.showStatus(` transformers: ${progress}`));\n\t\tif (!installed.ok) {\n\t\t\thost.showStatus(`Transformers runtime install failed: ${installed.error}`);\n\t\t\treturn;\n\t\t}\n\t\tstatus = await runtime.detect();\n\t}\n\tif (!status.runtimeInstalled) {\n\t\thost.showStatus(`Transformers runtime is still unavailable at ${status.venvDir}.`);\n\t\treturn;\n\t}\n\n\tconst downloaded = await runtime.downloadModel((progress) => host.showStatus(` ${modelId}: ${progress}`));\n\tif (!downloaded.ok) {\n\t\thost.showStatus(`Model download failed: ${downloaded.error}`);\n\t\treturn;\n\t}\n\n\tconst started = await runtime.start();\n\tif (!started.started && started.reason !== \"already_running\") {\n\t\thost.showStatus(`Could not start Transformers sidecar: ${started.reason}`);\n\t\treturn;\n\t}\n\n\tconst registration = registerTransformersModel({\n\t\tagentDir: getAgentDir(),\n\t\tmodelId,\n\t\tbaseUrl: runtime.baseUrl,\n\t});\n\tif (!registration.ok) {\n\t\thost.showStatus(`Installed, but not auto-registered: ${registration.reason}`);\n\t\tif (registration.manualSnippet) {\n\t\t\thost.showStatus(`Add this to ${registration.modelsJsonPath} yourself:\\n${registration.manualSnippet}`);\n\t\t}\n\t\treturn;\n\t}\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(\n\t\t`${modelId} installed in pi-managed Transformers and registered as ${HF_TRANSFORMERS_PROVIDER}/${modelId}. Probing fitness…`,\n\t);\n\tawait runFitnessAndAssign(host, `${HF_TRANSFORMERS_PROVIDER}/${modelId}`, preselectRole);\n}\n\nexport async function removeLocalModel(host: LocalModelHost, ref: string, confirmed: boolean): Promise<void> {\n\tconst status = await host.localRuntime.detect();\n\tif (!status.serverUp) {\n\t\thost.showStatus(\"Local server not running — start it (any /models action) before removing.\");\n\t\treturn;\n\t}\n\tconst models = await host.localRuntime.list();\n\tconst target = models.find((model) => matchesInstalledLocalModel(ref, model.name));\n\tif (!target) {\n\t\thost.showStatus(\n\t\t\t`${ref} is not installed. Installed: ${models.map((model) => model.name).join(\", \") || \"(none)\"}`,\n\t\t);\n\t\treturn;\n\t}\n\tif (!confirmed) {\n\t\t// EXPLICIT USER ACTION ONLY: full disclosure, then require the confirm token.\n\t\tconst gb = (target.sizeBytes / 1e9).toFixed(2);\n\t\thost.showStatus(\n\t\t\t[\n\t\t\t\t`Removing ${ref} will delete:`,\n\t\t\t\t` - model weights (${gb} GB) from ${status.managedByPi ? status.ownedModelsDir : \"the system server's storage\"}`,\n\t\t\t\t` - the ollama/${ref} entry in models.json`,\n\t\t\t\t` - its cached fitness report for this host`,\n\t\t\t\t`Run: /models remove ${ref} confirm`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t\treturn;\n\t}\n\tconst removed = await host.localRuntime.remove(ref);\n\tif (!removed.ok) {\n\t\thost.showStatus(`Remove failed: ${removed.error}`);\n\t\treturn;\n\t}\n\tunregisterLocalModel({ agentDir: getAgentDir(), ref });\n\tFitnessStore.forAgentDir(getAgentDir()).remove(`ollama/${ref}`);\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(`${ref} removed: weights deleted, registration and fitness report dropped.`);\n}\n\nasync function removeTransformersModel(host: LocalModelHost, modelId: string, confirmed: boolean): Promise<void> {\n\tconst runtime = host.getTransformersRuntime(modelId);\n\tconst status = await runtime.detect();\n\tif (!confirmed) {\n\t\thost.showStatus(\n\t\t\t[\n\t\t\t\t`Removing ${modelId} will delete:`,\n\t\t\t\t` - the ${HF_TRANSFORMERS_PROVIDER}/${modelId} entry in models.json`,\n\t\t\t\t` - its cached fitness report for this host`,\n\t\t\t\t`It will stop this session's Transformers sidecar if running. Cached weights remain under ${status.cacheDir}.`,\n\t\t\t\t`Run: /models remove hf.co/${modelId} confirm`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t\treturn;\n\t}\n\truntime.stop();\n\tconst registration = unregisterTransformersModel({ agentDir: getAgentDir(), modelId });\n\tif (!registration.ok) {\n\t\thost.showStatus(`Remove failed: ${registration.reason}`);\n\t\treturn;\n\t}\n\tFitnessStore.forAgentDir(getAgentDir()).remove(`${HF_TRANSFORMERS_PROVIDER}/${modelId}`);\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(\n\t\t`${modelId} registration and fitness report dropped; cached weights remain under ${status.cacheDir}.`,\n\t);\n}\n\n/** /fitness with no args: pick a model from the configured registry, probe it, assign a role. */\n/** Pick a validated suggestion → install it → probe on this host → land its shaped role. */\nexport function showModelSuggestionSelector(host: LocalModelHost): void {\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelSuggestionSelectorComponent(\n\t\t\tDEFAULT_MODEL_SUGGESTIONS,\n\t\t\tasync (suggestion) => {\n\t\t\t\tdone();\n\t\t\t\t// The shaped role rides along so the post-probe selector lands on it pre-selected;\n\t\t\t\t// non-tool-callers carry curator/judge/none, never executor, so this can't footgun.\n\t\t\t\tconst source = normalizeModelSource(suggestion.pullRef);\n\t\t\t\tif (source.type === \"transformers\") {\n\t\t\t\t\tawait addTransformersModel(host, source.modelId, suggestion.assignRole);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (source.type === \"local\") {\n\t\t\t\t\tawait addLocalModel(host, source.pullRef, suggestion.assignRole);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thost.showStatus(\n\t\t\t\t\t`Suggestion ${suggestion.name} is not installable: ${source.type === \"rejected\" ? source.reason : source.ref}`,\n\t\t\t\t);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\nexport function showFitnessModelSelector(host: LocalModelHost): void {\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelSelectorComponent(\n\t\t\thost.ui,\n\t\t\thost.session.model,\n\t\t\thost.settingsManager,\n\t\t\thost.session.modelRegistry,\n\t\t\thost.session.scopedModels,\n\t\t\tasync (model) => {\n\t\t\t\tdone();\n\t\t\t\tawait runFitnessAndAssign(host, `${model.provider}/${model.id}`);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\n/** Resolve the routed model's supported thinking levels for a fitness-assignment flow, with a safe fallback. */\nfunction getModelThinkingLevels(modelRegistry: ModelRegistry | undefined, modelRef: string): ThinkingLevel[] {\n\tif (!modelRegistry) {\n\t\treturn [...MODEL_ROUTER_THINKING_FALLBACK_LEVELS];\n\t}\n\tconst resolved = resolveCliModel({ cliModel: modelRef, modelRegistry });\n\tif (!resolved.model) {\n\t\treturn [...MODEL_ROUTER_THINKING_FALLBACK_LEVELS];\n\t}\n\treturn getSupportedThinkingLevels(resolved.model);\n}\n\nfunction getModelRouterThinkingField(role: FitnessRole): ModelRouterThinkingField | undefined {\n\tif (role === \"router-cheap\") {\n\t\treturn \"cheapThinking\";\n\t}\n\tif (role === \"router-medium\") {\n\t\treturn \"mediumThinking\";\n\t}\n\tif (role === \"router-expensive\") {\n\t\treturn \"expensiveThinking\";\n\t}\n\tif (role === \"executor\") {\n\t\treturn \"executorThinking\";\n\t}\n\tif (role === \"judge\") {\n\t\treturn \"judgeThinking\";\n\t}\n\treturn undefined;\n}\n\nfunction promptForModelRouterThinking(host: RunFitnessHost, modelRef: string, role: FitnessRole): void {\n\tconst thinkingField = getModelRouterThinkingField(role);\n\tif (!thinkingField) {\n\t\treturn;\n\t}\n\n\tconst modelRegistry = host.session.modelRegistry;\n\tconst availableLevels = getModelThinkingLevels(modelRegistry, modelRef);\n\tconst settings = host.settingsManager.getModelRouterSettings();\n\tconst configuredThinking = settings[thinkingField];\n\tconst currentThinking =\n\t\tconfiguredThinking && availableLevels.includes(configuredThinking)\n\t\t\t? configuredThinking\n\t\t\t: MODEL_ROUTER_THINKING_INHERIT_VALUE;\n\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelRouterThinkingSelectorComponent(\n\t\t\tcurrentThinking,\n\t\t\tavailableLevels,\n\t\t\t(level) => {\n\t\t\t\tdone();\n\t\t\t\thost.settingsManager.setModelRouterSettings({\n\t\t\t\t\t...settings,\n\t\t\t\t\t[thinkingField]: level === MODEL_ROUTER_THINKING_INHERIT_VALUE ? undefined : level,\n\t\t\t\t});\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\n/** Probe a model's fitness, show the report, then offer one-step role assignment. When the model\n * came from a validated suggestion, `preselectRole` lands its shaped role already highlighted. */\nexport async function runFitnessAndAssign(\n\thost: RunFitnessHost,\n\tmodelRef: string,\n\tpreselectRole?: FitnessRole,\n): Promise<void> {\n\thost.showStatus(`Model fitness probe running on ${modelRef}… (6 surfaces; local models may take a few minutes)`);\n\ttry {\n\t\tconst outcome = await host.session.runModelFitness({ model: modelRef });\n\t\tif (!outcome.started) {\n\t\t\thost.showStatus(`Model fitness skipped: ${outcome.skipReason}`);\n\t\t\treturn;\n\t\t}\n\t\thost.chatContainer.addChild(new Spacer(1));\n\t\thost.chatContainer.addChild(new Text(formatModelFitnessReport(outcome.model, outcome.report), 1, 0));\n\t\thost.ui.requestRender();\n\t\t// Validate-before-load: zero successes on every probed surface means the model cannot\n\t\t// drive any of the harness's subagent contracts on this host — refuse adoption instead\n\t\t// of landing a role selector the user might reflexively confirm (this is the reported\n\t\t// bug: a 0/3-everywhere model still got set as judge model and saved to Model Router).\n\t\tif (isProbeAllFailed(outcome.report)) {\n\t\t\thost.showStatus(\n\t\t\t\t`${outcome.model} failed the fitness probe on all surfaces — not configured. Use /model to set it manually if you accept the risk.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\thost.showSelector((done) => {\n\t\t\tconst selector = new FitnessRoleSelectorComponent(\n\t\t\t\toutcome.model,\n\t\t\t\t(role) => {\n\t\t\t\t\tdone();\n\t\t\t\t\tif (role === \"scout\") {\n\t\t\t\t\t\tconst verdict = evaluateSurfaceFitness(\"scout_auto\", outcome.report);\n\t\t\t\t\t\tif (!verdict.fit) {\n\t\t\t\t\t\t\tconst reason = verdict.reason === \"lane_failed\" ? `failed ${verdict.lane}` : \"was not probed\";\n\t\t\t\t\t\t\thost.showStatus(\n\t\t\t\t\t\t\t\t`${outcome.model} not assigned as scout: ${reason} on the scout_auto fitness exam. Use the docs/scout.md Modelfile recipe, then rerun /fitness.`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignFitnessRole(host, outcome.model, role);\n\t\t\t\t\tpromptForModelRouterThinking(host, outcome.model, role);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\thost.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tpreselectRole,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t} catch (error) {\n\t\thost.showError(error instanceof Error ? error.message : String(error));\n\t}\n}\n\n/** Persist a role assignment from the post-probe selector into the matching settings. */\nexport function assignFitnessRole(host: AssignRoleHost, modelRef: string, role: FitnessRole): void {\n\tif (role === \"none\") {\n\t\thost.showStatus(`Fitness result for ${modelRef} saved. Assign a role later from /settings.`);\n\t\treturn;\n\t}\n\tif (role === \"curator\") {\n\t\tconst current = host.settingsManager.getContextCurationSettings();\n\t\thost.settingsManager.setContextCurationSettings({ ...current, enabled: true, model: modelRef });\n\t\thost.showStatus(`Context curation enabled with ${modelRef} as the curator.`);\n\t\treturn;\n\t}\n\tif (role === \"executor\") {\n\t\tconst router = host.settingsManager.getModelRouterSettings();\n\t\thost.settingsManager.setModelRouterSettings({ ...router, executorModel: modelRef });\n\t\tconst hint = router.enabled ? \"\" : \" Model router is currently disabled — enable it in /settings → Model Router.\";\n\t\thost.showStatus(`${modelRef} set as the toolkit executor (direct Level-0 hits route to it).${hint}`);\n\t\treturn;\n\t}\n\tif (role === \"scout\") {\n\t\thost.settingsManager.setScoutSettings({ enabled: true, model: modelRef });\n\t\thost.showStatus(`${modelRef} set as the repository scout (context_scout enabled).`);\n\t\treturn;\n\t}\n\tconst router = host.settingsManager.getModelRouterSettings();\n\tconst field =\n\t\trole === \"router-cheap\"\n\t\t\t? \"cheapModel\"\n\t\t\t: role === \"router-medium\"\n\t\t\t\t? \"mediumModel\"\n\t\t\t\t: role === \"router-expensive\"\n\t\t\t\t\t? \"expensiveModel\"\n\t\t\t\t\t: role === \"judge\"\n\t\t\t\t\t\t? \"judgeModel\"\n\t\t\t\t\t\t: \"learningModel\";\n\thost.settingsManager.setModelRouterSettings({ ...router, [field]: modelRef });\n\tconst hint = router.enabled ? \"\" : \" Model router is currently disabled — enable it in /settings → Model Router.\";\n\thost.showStatus(`${modelRef} set as ${role.replace(\"router-\", \"router \")} model.${hint}`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"local-model-commands.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/local-model-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/models` local-model lifecycle and `/fitness` probe/role-assignment flows\n * extracted from interactive-mode.\n *\n * `/models` is a USER-invoked local-model manager (list/add/remove/stop, never a\n * model-invokable tool); the fitness helpers probe a model on the current host\n * and land a shaped role. The server/selector flows share a `LocalModelHost`\n * seam, while `runFitnessAndAssign`/`assignFitnessRole` take narrow host shapes\n * matching their prototype-driven behaviour tests (fitness-probe-gate,\n * fitness-role-assignment), which keep exercising interactive-mode's thin\n * wrappers unchanged.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport { getSupportedThinkingLevels } from \"@caupulican/pi-ai\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype SelectItem,\n\tSelectList,\n\ttype SelectListLayoutOptions,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@caupulican/pi-tui\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { AgentSession } from \"../../core/agent-session.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport { resolveCliModel } from \"../../core/model-resolver.ts\";\nimport { evaluateSurfaceFitness } from \"../../core/model-router/fitness-gate.ts\";\nimport {\n\tderiveLocalContextSizing,\n\trenderOllamaContextModelfile,\n\tsizedLocalModelRef,\n} from \"../../core/models/context-sizing.ts\";\nimport { DEFAULT_MODEL_SUGGESTIONS } from \"../../core/models/default-model-suggestions.ts\";\nimport { FitnessStore } from \"../../core/models/fitness-store.ts\";\nimport {\n\tHF_TRANSFORMERS_PROVIDER,\n\tregisterLocalModel,\n\tregisterTransformersModel,\n\tunregisterLocalModel,\n\tunregisterTransformersModel,\n} from \"../../core/models/local-registration.ts\";\nimport type { OllamaRuntime, TransformersRuntime } from \"../../core/models/local-runtime.ts\";\nimport { matchesInstalledLocalModel, normalizeModelSource } from \"../../core/models/model-ref.ts\";\nimport { formatModelFitnessReport, isProbeAllFailed } from \"../../core/research/model-fitness.ts\";\nimport type { SettingsManager } from \"../../core/settings-manager.ts\";\nimport { DynamicBorder } from \"./components/dynamic-border.ts\";\nimport { type FitnessRole, FitnessRoleSelectorComponent } from \"./components/fitness-role-selector.ts\";\nimport { ModelSelectorComponent } from \"./components/model-selector.ts\";\nimport { ModelSuggestionSelectorComponent } from \"./components/model-suggestion-selector.ts\";\nimport { getSelectListTheme } from \"./theme/theme.ts\";\n\ntype SelectorFactory = (done: () => void) => { component: Component; focus: Component };\n\nconst MODEL_ROUTER_THINKING_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {\n\tminPrimaryColumnWidth: 12,\n\tmaxPrimaryColumnWidth: 32,\n};\n\nconst MODEL_ROUTER_THINKING_INHERIT_VALUE = \"__inherit_model_router_thinking__\";\nconst MODEL_ROUTER_THINKING_INHERIT_LABEL = \"(inherit)\";\n\nconst THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning (~1k tokens)\",\n\tlow: \"Light reasoning (~2k tokens)\",\n\tmedium: \"Moderate reasoning (~8k tokens)\",\n\thigh: \"Deep reasoning (~16k tokens)\",\n\txhigh: \"Maximum reasoning (~32k tokens)\",\n};\n\nconst MODEL_ROUTER_THINKING_FALLBACK_LEVELS: readonly ThinkingLevel[] = [\n\t\"off\",\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n];\n\ntype RouterThinkingLevel = ThinkingLevel | typeof MODEL_ROUTER_THINKING_INHERIT_VALUE;\ntype ModelRouterThinkingField =\n\t| \"cheapThinking\"\n\t| \"mediumThinking\"\n\t| \"expensiveThinking\"\n\t| \"executorThinking\"\n\t| \"judgeThinking\";\n\n/** Narrow seam for persisting a probed model's role — matches the fitness-role test. */\nexport interface AssignRoleHost {\n\treadonly settingsManager: SettingsManager;\n\tshowStatus(message: string): void;\n}\n\n/** Seam for the fitness probe + role-selector flow — matches the fitness-probe-gate test. */\nexport interface RunFitnessHost {\n\treadonly session: Pick<AgentSession, \"runModelFitness\"> & { modelRegistry?: ModelRegistry };\n\treadonly settingsManager: SettingsManager;\n\treadonly chatContainer: Container;\n\treadonly ui: TUI;\n\tshowStatus(message: string): void;\n\tshowError(message: string): void;\n\tshowSelector(create: SelectorFactory): void;\n}\n\n/** Full seam for the `/models` server/selector flows. */\nexport interface LocalModelHost {\n\treadonly localRuntime: OllamaRuntime;\n\treadonly session: AgentSession;\n\treadonly settingsManager: SettingsManager;\n\treadonly ui: TUI;\n\treadonly chatContainer: Container;\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime;\n\tshowStatus(message: string): void;\n\tshowError(message: string): void;\n\tshowSelector(create: SelectorFactory): void;\n}\n\nclass ModelRouterThinkingSelectorComponent extends Container {\n\tprivate selectList: SelectList;\n\n\tconstructor(\n\t\tcurrentLevel: RouterThinkingLevel,\n\t\tavailableLevels: ThinkingLevel[],\n\t\tonSelect: (level: RouterThinkingLevel) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tconst thinkingOptions: SelectItem[] = [\n\t\t\t{\n\t\t\t\tvalue: MODEL_ROUTER_THINKING_INHERIT_VALUE,\n\t\t\t\tlabel: MODEL_ROUTER_THINKING_INHERIT_LABEL,\n\t\t\t\tdescription: \"Use current session default for this tier.\",\n\t\t\t},\n\t\t\t...availableLevels.map((level) => ({\n\t\t\t\tvalue: level,\n\t\t\t\tlabel: level,\n\t\t\t\tdescription: THINKING_LEVEL_DESCRIPTIONS[level],\n\t\t\t})),\n\t\t];\n\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.selectList = new SelectList(\n\t\t\tthinkingOptions,\n\t\t\tthinkingOptions.length,\n\t\t\tgetSelectListTheme(),\n\t\t\tMODEL_ROUTER_THINKING_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\tconst index = thinkingOptions.findIndex((item) => item.value === currentLevel);\n\t\tif (index !== -1) {\n\t\t\tthis.selectList.setSelectedIndex(index);\n\t\t}\n\n\t\tthis.selectList.onSelect = (item) => {\n\t\t\tonSelect(item.value as RouterThinkingLevel);\n\t\t};\n\t\tthis.selectList.onCancel = () => {\n\t\t\tonCancel();\n\t\t};\n\n\t\tthis.addChild(this.selectList);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n}\n/**\n * /models — USER-invoked local model lifecycle (never a model-invokable tool):\n * list/add/remove/stop per local-model-lifecycle-design.md. Removal is explicit-only with\n * full disclosure; a pasted install command is parsed for its ref, never executed.\n */\nexport async function handleModelsCommand(host: LocalModelHost, argsText: string): Promise<void> {\n\tconst [action = \"list\", ...rest] = argsText.split(/\\s+/).filter(Boolean);\n\ttry {\n\t\tif (action === \"suggest\" || action === \"suggestions\") {\n\t\t\tshowModelSuggestionSelector(host);\n\t\t\treturn;\n\t\t}\n\t\tif (action === \"stop\") {\n\t\t\tconst stopped = host.localRuntime.stop();\n\t\t\tlet stoppedTransformers = 0;\n\t\t\tfor (const model of host.session.modelRegistry\n\t\t\t\t.getAll()\n\t\t\t\t.filter((entry) => entry.provider === HF_TRANSFORMERS_PROVIDER)) {\n\t\t\t\tconst serverUrl = model.baseUrl.replace(/\\/v1\\/?$/, \"\");\n\t\t\t\tif (host.getTransformersRuntime(model.id, serverUrl).stop().stopped) stoppedTransformers++;\n\t\t\t}\n\t\t\tconst stoppedAny = stopped.stopped || stoppedTransformers > 0;\n\t\t\thost.showStatus(\n\t\t\t\tstoppedAny\n\t\t\t\t\t? `Pi-managed local model server stopped${stoppedTransformers > 0 ? ` (${stoppedTransformers} Transformers sidecar(s))` : \"\"}; models remain installed.`\n\t\t\t\t\t: \"No pi-managed server running (a system server, if any, is not pi's to stop).\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (action === \"import\") {\n\t\t\tconst imported = host.localRuntime.importUserModels();\n\t\t\thost.showStatus(\n\t\t\t\t`Imported Ollama models from ${imported.sourceDir} into pi-owned store ${imported.targetDir}: ` +\n\t\t\t\t\t`${imported.manifestsImported} manifest(s), ${imported.blobsHardlinked} blob(s) hardlinked, ` +\n\t\t\t\t\t`${imported.blobsCopied} blob(s) copied, ${imported.blobsSkipped + imported.manifestsSkipped} existing file(s) skipped.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (action === \"add\") {\n\t\t\tconst rawRef = rest.join(\" \");\n\t\t\tif (!rawRef) {\n\t\t\t\thost.showStatus(\n\t\t\t\t\t\"Usage: /models add <ollama-tag | hf.co/org/repo[:quant] | curated HF full-base ref | huggingface URL | pasted install command>\",\n\t\t\t\t);\n\t\t\t\thost.showStatus(\"Or start from a validated suggestion: /models suggest\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst source = normalizeModelSource(rawRef);\n\t\t\tif (source.type === \"rejected\") {\n\t\t\t\thost.showStatus(`Not added: ${source.reason}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (source.type === \"api\") {\n\t\t\t\thost.showStatus(\n\t\t\t\t\t`${source.ref} is an API model — nothing to install. Configure auth for the provider, then probe it with /fitness ${source.ref}.`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (source.type === \"transformers\") {\n\t\t\t\tawait addTransformersModel(host, source.modelId);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait addLocalModel(host, source.pullRef);\n\t\t\treturn;\n\t\t}\n\n\t\tif (action === \"remove\") {\n\t\t\tconst ref = rest[0];\n\t\t\tconst confirmed = rest[1] === \"confirm\";\n\t\t\tif (!ref) {\n\t\t\t\thost.showStatus(\"Usage: /models remove <ref> confirm\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst source = normalizeModelSource(ref);\n\t\t\tif (source.type === \"transformers\") {\n\t\t\t\tawait removeTransformersModel(host, source.modelId, confirmed);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait removeLocalModel(host, ref, confirmed);\n\t\t\treturn;\n\t\t}\n\n\t\tawait listLocalModels(host);\n\t} catch (error) {\n\t\thost.showError(error instanceof Error ? error.message : String(error));\n\t}\n}\n\nfunction formatOllamaStore(store: { kind: string; path: string; modelCount: number } | undefined): string {\n\tif (!store) return \"unknown store (0 model(s))\";\n\treturn `${store.path} [${store.kind}, ${store.modelCount} model(s)]`;\n}\n\nexport async function ensureLocalServer(host: LocalModelHost): Promise<boolean> {\n\tconst status = await host.localRuntime.detect();\n\tif (status.serverUp) {\n\t\tif (status.activeStore?.kind === \"pi-owned\") return true;\n\t\thost.showStatus(\n\t\t\t`Ollama is already running on ${status.serverUrl} with store ${formatOllamaStore(status.activeStore)}; ` +\n\t\t\t\t`pi's canonical store is ${status.ownedModelsDir} (${status.ownedStore.modelCount} model(s)). ` +\n\t\t\t\t\"Stop the other serve or run /models import before adding models.\",\n\t\t);\n\t\treturn false;\n\t}\n\tif (!status.binaryPath) {\n\t\tfor (const line of host.localRuntime.installGuide()) host.showStatus(line);\n\t\treturn false;\n\t}\n\thost.showStatus(\n\t\t`Starting local model server (${status.binarySource} binary, owned storage: ${status.ownedModelsDir})…`,\n\t);\n\tconst started = await host.localRuntime.start();\n\tif (!started.started) {\n\t\thost.showStatus(`Could not start the local server: ${started.reason}`);\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nexport async function listLocalModels(host: LocalModelHost): Promise<void> {\n\tconst status = await host.localRuntime.detect();\n\tconst models = status.serverUp ? await host.localRuntime.list() : [];\n\tconst ollamaLines = status.serverUp\n\t\t? [\n\t\t\t\t`Ollama models (active store: ${formatOllamaStore(status.activeStore)}; pi-owned store: ${status.ownedModelsDir} with ${status.ownedStore.modelCount} model(s)):`,\n\t\t\t\t...(models.length === 0\n\t\t\t\t\t? [\" (none installed — /models add <ref>, /models import, or /models suggest for a validated roster)\"]\n\t\t\t\t\t: []),\n\t\t\t]\n\t\t: [\n\t\t\t\t\"Ollama models:\",\n\t\t\t\t...(status.binaryPath\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t` server not running (binary: ${status.binarySource}). /models add starts it on demand; /fitness probes registered models.`,\n\t\t\t\t\t\t]\n\t\t\t\t\t: host.localRuntime.installGuide().map((line) => ` ${line}`)),\n\t\t\t];\n\tconst fitness = FitnessStore.forAgentDir(getAgentDir()).getForHost();\n\tconst transformersModels = host.session.modelRegistry\n\t\t.getAll()\n\t\t.filter((model) => model.provider === HF_TRANSFORMERS_PROVIDER);\n\tconst transformersLines = await Promise.all(\n\t\ttransformersModels.map(async (model) => {\n\t\t\tconst serverUrl = model.baseUrl.replace(/\\/v1\\/?$/, \"\");\n\t\t\tconst runtime = host.getTransformersRuntime(model.id, serverUrl);\n\t\t\tconst runtimeStatus = await runtime.detect();\n\t\t\tconst report = fitness.find((entry) => entry.model === `${HF_TRANSFORMERS_PROVIDER}/${model.id}`);\n\t\t\tconst readiness = runtimeStatus.serverUp\n\t\t\t\t? \"server running\"\n\t\t\t\t: runtimeStatus.runtimeInstalled\n\t\t\t\t\t? \"runtime installed\"\n\t\t\t\t\t: \"runtime missing\";\n\t\t\tconst probe = report\n\t\t\t\t? `probed ${report.at.slice(0, 10)}: digest ${report.report.digest?.succeeded ?? \"?\"}/${report.report.digest?.total ?? \"?\"}, tool-calls ${report.report.toolCall.succeeded}/${report.report.toolCall.total}${report.report.tokensPerSecond ? `, ~${report.report.tokensPerSecond} tok/s` : \"\"}`\n\t\t\t\t: `unprobed — run /fitness ${HF_TRANSFORMERS_PROVIDER}/${model.id}`;\n\t\t\treturn ` - ${model.id} (${readiness}, cache: ${runtimeStatus.cacheDir}) · ${probe}`;\n\t\t}),\n\t);\n\tconst lines = [\n\t\t...ollamaLines,\n\t\t...models.map((model) => {\n\t\t\tconst report = fitness.find((entry) => entry.model === `ollama/${model.name}`);\n\t\t\tconst gb = (model.sizeBytes / 1e9).toFixed(2);\n\t\t\tconst probe = report\n\t\t\t\t? `probed ${report.at.slice(0, 10)}: digest ${report.report.digest?.succeeded ?? \"?\"}/${report.report.digest?.total ?? \"?\"}, tool-calls ${report.report.toolCall.succeeded}/${report.report.toolCall.total}${report.report.tokensPerSecond ? `, ~${report.report.tokensPerSecond} tok/s` : \"\"}`\n\t\t\t\t: `unprobed — run /fitness ollama/${model.name}`;\n\t\t\treturn ` - ${model.name} (${gb} GB) · ${probe}`;\n\t\t}),\n\t\t...(transformersLines.length > 0 ? [\"Pi-managed Transformers models:\", ...transformersLines] : []),\n\t\t\"Commands: /models import · /models add <ref> · /models remove <ref> confirm · /models stop\",\n\t];\n\tfor (const line of lines) host.showStatus(line);\n}\n\nexport async function addLocalModel(host: LocalModelHost, pullRef: string, preselectRole?: FitnessRole): Promise<void> {\n\tif (!(await ensureLocalServer(host))) return;\n\thost.showStatus(`Pulling ${pullRef}… (weights land in pi's owned Ollama store)`);\n\tlet lastShown = 0;\n\tconst pulled = await host.localRuntime.pull(pullRef, (progress) => {\n\t\tconst now = Date.now();\n\t\tif (now - lastShown > 2000) {\n\t\t\tlastShown = now;\n\t\t\thost.showStatus(` ${pullRef}: ${progress}`);\n\t\t}\n\t});\n\tif (!pulled.ok) {\n\t\thost.showStatus(`Pull failed: ${pulled.error}`);\n\t\treturn;\n\t}\n\tconst installed = await host.localRuntime.list().catch(() => []);\n\tconst installedEntry = installed.find((entry) => matchesInstalledLocalModel(pullRef, entry.name));\n\tconst shown = await host.localRuntime.show(pullRef);\n\tconst sizing =\n\t\tinstalledEntry && shown.ok\n\t\t\t? deriveLocalContextSizing({\n\t\t\t\t\thost: { totalMemBytes: totalmem() },\n\t\t\t\t\tmodel: { modelInfo: shown.info.modelInfo, weightsBytes: installedEntry.sizeBytes },\n\t\t\t\t\truntime: { supportsKvQuantization: true },\n\t\t\t\t})\n\t\t\t: undefined;\n\tlet registeredRef = pullRef;\n\tif (sizing) {\n\t\tconst sizedRef = sizedLocalModelRef(pullRef, sizing.numCtx);\n\t\tconst created = await host.localRuntime.createFromModelfile({\n\t\t\tname: sizedRef,\n\t\t\tmodelfile: renderOllamaContextModelfile({ from: pullRef, numCtx: sizing.numCtx }),\n\t\t});\n\t\tif (!created.ok) {\n\t\t\thost.showStatus(`Pulled, but could not create sized context model: ${created.error}`);\n\t\t\treturn;\n\t\t}\n\t\tregisteredRef = sizedRef;\n\t}\n\tconst registration = registerLocalModel({\n\t\tagentDir: getAgentDir(),\n\t\tref: registeredRef,\n\t\tbaseUrl: host.localRuntime.baseUrl,\n\t\tcontextWindow: sizing?.numCtx,\n\t\tservedContextWindow: sizing?.numCtx,\n\t});\n\tif (!registration.ok) {\n\t\thost.showStatus(`Pulled, but not auto-registered: ${registration.reason}`);\n\t\tif (registration.manualSnippet) {\n\t\t\thost.showStatus(`Add this to ${registration.modelsJsonPath} yourself:\\n${registration.manualSnippet}`);\n\t\t}\n\t\treturn;\n\t}\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(`${pullRef} installed and registered as ollama/${registeredRef}. Probing fitness…`);\n\tawait runFitnessAndAssign(host, `ollama/${registeredRef}`, preselectRole);\n}\n\nexport async function addTransformersModel(\n\thost: LocalModelHost,\n\tmodelId: string,\n\tpreselectRole?: FitnessRole,\n): Promise<void> {\n\tconst runtime = host.getTransformersRuntime(modelId);\n\tlet status = await runtime.detect();\n\tif (!status.runtimeInstalled) {\n\t\thost.showStatus(`Installing isolated Transformers runtime for ${modelId} at ${status.venvDir}…`);\n\t\tconst installed = await runtime.installManaged((progress) => host.showStatus(` transformers: ${progress}`));\n\t\tif (!installed.ok) {\n\t\t\thost.showStatus(`Transformers runtime install failed: ${installed.error}`);\n\t\t\treturn;\n\t\t}\n\t\tstatus = await runtime.detect();\n\t}\n\tif (!status.runtimeInstalled) {\n\t\thost.showStatus(`Transformers runtime is still unavailable at ${status.venvDir}.`);\n\t\treturn;\n\t}\n\n\tconst downloaded = await runtime.downloadModel((progress) => host.showStatus(` ${modelId}: ${progress}`));\n\tif (!downloaded.ok) {\n\t\thost.showStatus(`Model download failed: ${downloaded.error}`);\n\t\treturn;\n\t}\n\n\tconst started = await runtime.start();\n\tif (!started.started && started.reason !== \"already_running\") {\n\t\thost.showStatus(`Could not start Transformers sidecar: ${started.reason}`);\n\t\treturn;\n\t}\n\n\tconst registration = registerTransformersModel({\n\t\tagentDir: getAgentDir(),\n\t\tmodelId,\n\t\tbaseUrl: runtime.baseUrl,\n\t});\n\tif (!registration.ok) {\n\t\thost.showStatus(`Installed, but not auto-registered: ${registration.reason}`);\n\t\tif (registration.manualSnippet) {\n\t\t\thost.showStatus(`Add this to ${registration.modelsJsonPath} yourself:\\n${registration.manualSnippet}`);\n\t\t}\n\t\treturn;\n\t}\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(\n\t\t`${modelId} installed in pi-managed Transformers and registered as ${HF_TRANSFORMERS_PROVIDER}/${modelId}. Probing fitness…`,\n\t);\n\tawait runFitnessAndAssign(host, `${HF_TRANSFORMERS_PROVIDER}/${modelId}`, preselectRole);\n}\n\nexport async function removeLocalModel(host: LocalModelHost, ref: string, confirmed: boolean): Promise<void> {\n\tconst status = await host.localRuntime.detect();\n\tif (!status.serverUp) {\n\t\thost.showStatus(\"Local server not running — start it (any /models action) before removing.\");\n\t\treturn;\n\t}\n\tconst models = await host.localRuntime.list();\n\tconst target = models.find((model) => matchesInstalledLocalModel(ref, model.name));\n\tif (!target) {\n\t\thost.showStatus(\n\t\t\t`${ref} is not installed. Installed: ${models.map((model) => model.name).join(\", \") || \"(none)\"}`,\n\t\t);\n\t\treturn;\n\t}\n\tif (!confirmed) {\n\t\t// EXPLICIT USER ACTION ONLY: full disclosure, then require the confirm token.\n\t\tconst gb = (target.sizeBytes / 1e9).toFixed(2);\n\t\thost.showStatus(\n\t\t\t[\n\t\t\t\t`Removing ${ref} will delete:`,\n\t\t\t\t` - model weights (${gb} GB) from ${status.managedByPi ? status.ownedModelsDir : \"the system server's storage\"}`,\n\t\t\t\t` - the ollama/${ref} entry in models.json`,\n\t\t\t\t` - its cached fitness report for this host`,\n\t\t\t\t`Run: /models remove ${ref} confirm`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t\treturn;\n\t}\n\tconst removed = await host.localRuntime.remove(ref);\n\tif (!removed.ok) {\n\t\thost.showStatus(`Remove failed: ${removed.error}`);\n\t\treturn;\n\t}\n\tunregisterLocalModel({ agentDir: getAgentDir(), ref });\n\tFitnessStore.forAgentDir(getAgentDir()).remove(`ollama/${ref}`);\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(`${ref} removed: weights deleted, registration and fitness report dropped.`);\n}\n\nasync function removeTransformersModel(host: LocalModelHost, modelId: string, confirmed: boolean): Promise<void> {\n\tconst runtime = host.getTransformersRuntime(modelId);\n\tconst status = await runtime.detect();\n\tif (!confirmed) {\n\t\thost.showStatus(\n\t\t\t[\n\t\t\t\t`Removing ${modelId} will delete:`,\n\t\t\t\t` - the ${HF_TRANSFORMERS_PROVIDER}/${modelId} entry in models.json`,\n\t\t\t\t` - its cached fitness report for this host`,\n\t\t\t\t`It will stop this session's Transformers sidecar if running. Cached weights remain under ${status.cacheDir}.`,\n\t\t\t\t`Run: /models remove hf.co/${modelId} confirm`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t\treturn;\n\t}\n\truntime.stop();\n\tconst registration = unregisterTransformersModel({ agentDir: getAgentDir(), modelId });\n\tif (!registration.ok) {\n\t\thost.showStatus(`Remove failed: ${registration.reason}`);\n\t\treturn;\n\t}\n\tFitnessStore.forAgentDir(getAgentDir()).remove(`${HF_TRANSFORMERS_PROVIDER}/${modelId}`);\n\thost.session.modelRegistry.refresh();\n\thost.showStatus(\n\t\t`${modelId} registration and fitness report dropped; cached weights remain under ${status.cacheDir}.`,\n\t);\n}\n\n/** /fitness with no args: pick a model from the configured registry, probe it, assign a role. */\n/** Pick a validated suggestion → install it → probe on this host → land its shaped role. */\nexport function showModelSuggestionSelector(host: LocalModelHost): void {\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelSuggestionSelectorComponent(\n\t\t\tDEFAULT_MODEL_SUGGESTIONS,\n\t\t\tasync (suggestion) => {\n\t\t\t\tdone();\n\t\t\t\t// The shaped role rides along so the post-probe selector lands on it pre-selected;\n\t\t\t\t// non-tool-callers carry curator/judge/none, never executor, so this can't footgun.\n\t\t\t\tconst source = normalizeModelSource(suggestion.pullRef);\n\t\t\t\tif (source.type === \"transformers\") {\n\t\t\t\t\tawait addTransformersModel(host, source.modelId, suggestion.assignRole);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (source.type === \"local\") {\n\t\t\t\t\tawait addLocalModel(host, source.pullRef, suggestion.assignRole);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thost.showStatus(\n\t\t\t\t\t`Suggestion ${suggestion.name} is not installable: ${source.type === \"rejected\" ? source.reason : source.ref}`,\n\t\t\t\t);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\nexport function showFitnessModelSelector(host: LocalModelHost): void {\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelSelectorComponent(\n\t\t\thost.ui,\n\t\t\thost.session.model,\n\t\t\thost.settingsManager,\n\t\t\thost.session.modelRegistry,\n\t\t\thost.session.scopedModels,\n\t\t\tasync (model) => {\n\t\t\t\tdone();\n\t\t\t\tawait runFitnessAndAssign(host, `${model.provider}/${model.id}`);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\n/** Resolve the routed model's supported thinking levels for a fitness-assignment flow, with a safe fallback. */\nfunction getModelThinkingLevels(modelRegistry: ModelRegistry | undefined, modelRef: string): ThinkingLevel[] {\n\tif (!modelRegistry) {\n\t\treturn [...MODEL_ROUTER_THINKING_FALLBACK_LEVELS];\n\t}\n\tconst resolved = resolveCliModel({ cliModel: modelRef, modelRegistry });\n\tif (!resolved.model) {\n\t\treturn [...MODEL_ROUTER_THINKING_FALLBACK_LEVELS];\n\t}\n\treturn getSupportedThinkingLevels(resolved.model);\n}\n\nfunction getModelRouterThinkingField(role: FitnessRole): ModelRouterThinkingField | undefined {\n\tif (role === \"router-cheap\") {\n\t\treturn \"cheapThinking\";\n\t}\n\tif (role === \"router-medium\") {\n\t\treturn \"mediumThinking\";\n\t}\n\tif (role === \"router-expensive\") {\n\t\treturn \"expensiveThinking\";\n\t}\n\tif (role === \"executor\") {\n\t\treturn \"executorThinking\";\n\t}\n\tif (role === \"judge\") {\n\t\treturn \"judgeThinking\";\n\t}\n\treturn undefined;\n}\n\nfunction promptForModelRouterThinking(host: RunFitnessHost, modelRef: string, role: FitnessRole): void {\n\tconst thinkingField = getModelRouterThinkingField(role);\n\tif (!thinkingField) {\n\t\treturn;\n\t}\n\n\tconst modelRegistry = host.session.modelRegistry;\n\tconst availableLevels = getModelThinkingLevels(modelRegistry, modelRef);\n\tconst settings = host.settingsManager.getModelRouterSettings();\n\tconst configuredThinking = settings[thinkingField];\n\tconst currentThinking =\n\t\tconfiguredThinking && availableLevels.includes(configuredThinking)\n\t\t\t? configuredThinking\n\t\t\t: MODEL_ROUTER_THINKING_INHERIT_VALUE;\n\n\thost.showSelector((done) => {\n\t\tconst selector = new ModelRouterThinkingSelectorComponent(\n\t\t\tcurrentThinking,\n\t\t\tavailableLevels,\n\t\t\t(level) => {\n\t\t\t\tdone();\n\t\t\t\thost.settingsManager.setModelRouterSettings({\n\t\t\t\t\t...settings,\n\t\t\t\t\t[thinkingField]: level === MODEL_ROUTER_THINKING_INHERIT_VALUE ? undefined : level,\n\t\t\t\t});\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tdone();\n\t\t\t\thost.ui.requestRender();\n\t\t\t},\n\t\t);\n\t\treturn { component: selector, focus: selector };\n\t});\n}\n\n/** Probe a model's fitness, show the report, then offer one-step role assignment. When the model\n * came from a validated suggestion, `preselectRole` lands its shaped role already highlighted. */\nexport async function runFitnessAndAssign(\n\thost: RunFitnessHost,\n\tmodelRef: string,\n\tpreselectRole?: FitnessRole,\n): Promise<void> {\n\thost.showStatus(`Model fitness probe running on ${modelRef}… (6 surfaces; local models may take a few minutes)`);\n\ttry {\n\t\tconst outcome = await host.session.runModelFitness({ model: modelRef });\n\t\tif (!outcome.started) {\n\t\t\thost.showStatus(`Model fitness skipped: ${outcome.skipReason}`);\n\t\t\treturn;\n\t\t}\n\t\thost.chatContainer.addChild(new Spacer(1));\n\t\thost.chatContainer.addChild(new Text(formatModelFitnessReport(outcome.model, outcome.report), 1, 0));\n\t\thost.ui.requestRender();\n\t\t// Validate-before-load: zero successes on every probed surface means the model cannot\n\t\t// drive any of the harness's subagent contracts on this host — refuse adoption instead\n\t\t// of landing a role selector the user might reflexively confirm (this is the reported\n\t\t// bug: a 0/3-everywhere model still got set as judge model and saved to Model Router).\n\t\tif (isProbeAllFailed(outcome.report)) {\n\t\t\thost.showStatus(\n\t\t\t\t`${outcome.model} failed the fitness probe on all surfaces — not configured. Use /model to set it manually if you accept the risk.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\thost.showSelector((done) => {\n\t\t\tconst selector = new FitnessRoleSelectorComponent(\n\t\t\t\toutcome.model,\n\t\t\t\t(role) => {\n\t\t\t\t\tdone();\n\t\t\t\t\tif (role === \"scout\") {\n\t\t\t\t\t\tconst verdict = evaluateSurfaceFitness(\"scout_auto\", outcome.report);\n\t\t\t\t\t\tif (!verdict.fit) {\n\t\t\t\t\t\t\tconst reason = verdict.reason === \"lane_failed\" ? `failed ${verdict.lane}` : \"was not probed\";\n\t\t\t\t\t\t\thost.showStatus(\n\t\t\t\t\t\t\t\t`${outcome.model} not assigned as scout: ${reason} on the scout_auto fitness exam. Use the docs/scout.md Modelfile recipe, then rerun /fitness.`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignFitnessRole(host, outcome.model, role);\n\t\t\t\t\tpromptForModelRouterThinking(host, outcome.model, role);\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tdone();\n\t\t\t\t\thost.ui.requestRender();\n\t\t\t\t},\n\t\t\t\tpreselectRole,\n\t\t\t);\n\t\t\treturn { component: selector, focus: selector };\n\t\t});\n\t} catch (error) {\n\t\thost.showError(error instanceof Error ? error.message : String(error));\n\t}\n}\n\n/** Persist a role assignment from the post-probe selector into the matching settings. */\nexport function assignFitnessRole(host: AssignRoleHost, modelRef: string, role: FitnessRole): void {\n\tif (role === \"none\") {\n\t\thost.showStatus(`Fitness result for ${modelRef} saved. Assign a role later from /settings.`);\n\t\treturn;\n\t}\n\tif (role === \"curator\") {\n\t\tconst current = host.settingsManager.getContextCurationSettings();\n\t\thost.settingsManager.setContextCurationSettings({ ...current, enabled: true, model: modelRef });\n\t\thost.showStatus(`Context curation enabled with ${modelRef} as the curator.`);\n\t\treturn;\n\t}\n\tif (role === \"executor\") {\n\t\tconst router = host.settingsManager.getModelRouterSettings();\n\t\thost.settingsManager.setModelRouterSettings({ ...router, executorModel: modelRef });\n\t\tconst hint = router.enabled ? \"\" : \" Model router is currently disabled — enable it in /settings → Model Router.\";\n\t\thost.showStatus(`${modelRef} set as the toolkit executor (direct Level-0 hits route to it).${hint}`);\n\t\treturn;\n\t}\n\tif (role === \"scout\") {\n\t\thost.settingsManager.setScoutSettings({ enabled: true, model: modelRef });\n\t\thost.showStatus(`${modelRef} set as the repository scout (context_scout enabled).`);\n\t\treturn;\n\t}\n\tconst router = host.settingsManager.getModelRouterSettings();\n\tconst field =\n\t\trole === \"router-cheap\"\n\t\t\t? \"cheapModel\"\n\t\t\t: role === \"router-medium\"\n\t\t\t\t? \"mediumModel\"\n\t\t\t\t: role === \"router-expensive\"\n\t\t\t\t\t? \"expensiveModel\"\n\t\t\t\t\t: role === \"judge\"\n\t\t\t\t\t\t? \"judgeModel\"\n\t\t\t\t\t\t: \"learningModel\";\n\thost.settingsManager.setModelRouterSettings({ ...router, [field]: modelRef });\n\tconst hint = router.enabled ? \"\" : \" Model router is currently disabled — enable it in /settings → Model Router.\";\n\thost.showStatus(`${modelRef} set as ${role.replace(\"router-\", \"router \")} model.${hint}`);\n}\n"]}
|
|
@@ -109,6 +109,13 @@ export async function handleModelsCommand(host, argsText) {
|
|
|
109
109
|
: "No pi-managed server running (a system server, if any, is not pi's to stop).");
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
+
if (action === "import") {
|
|
113
|
+
const imported = host.localRuntime.importUserModels();
|
|
114
|
+
host.showStatus(`Imported Ollama models from ${imported.sourceDir} into pi-owned store ${imported.targetDir}: ` +
|
|
115
|
+
`${imported.manifestsImported} manifest(s), ${imported.blobsHardlinked} blob(s) hardlinked, ` +
|
|
116
|
+
`${imported.blobsCopied} blob(s) copied, ${imported.blobsSkipped + imported.manifestsSkipped} existing file(s) skipped.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
112
119
|
if (action === "add") {
|
|
113
120
|
const rawRef = rest.join(" ");
|
|
114
121
|
if (!rawRef) {
|
|
@@ -153,10 +160,21 @@ export async function handleModelsCommand(host, argsText) {
|
|
|
153
160
|
host.showError(error instanceof Error ? error.message : String(error));
|
|
154
161
|
}
|
|
155
162
|
}
|
|
163
|
+
function formatOllamaStore(store) {
|
|
164
|
+
if (!store)
|
|
165
|
+
return "unknown store (0 model(s))";
|
|
166
|
+
return `${store.path} [${store.kind}, ${store.modelCount} model(s)]`;
|
|
167
|
+
}
|
|
156
168
|
export async function ensureLocalServer(host) {
|
|
157
169
|
const status = await host.localRuntime.detect();
|
|
158
|
-
if (status.serverUp)
|
|
159
|
-
|
|
170
|
+
if (status.serverUp) {
|
|
171
|
+
if (status.activeStore?.kind === "pi-owned")
|
|
172
|
+
return true;
|
|
173
|
+
host.showStatus(`Ollama is already running on ${status.serverUrl} with store ${formatOllamaStore(status.activeStore)}; ` +
|
|
174
|
+
`pi's canonical store is ${status.ownedModelsDir} (${status.ownedStore.modelCount} model(s)). ` +
|
|
175
|
+
"Stop the other serve or run /models import before adding models.");
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
160
178
|
if (!status.binaryPath) {
|
|
161
179
|
for (const line of host.localRuntime.installGuide())
|
|
162
180
|
host.showStatus(line);
|
|
@@ -175,9 +193,9 @@ export async function listLocalModels(host) {
|
|
|
175
193
|
const models = status.serverUp ? await host.localRuntime.list() : [];
|
|
176
194
|
const ollamaLines = status.serverUp
|
|
177
195
|
? [
|
|
178
|
-
`Ollama models (${status.
|
|
196
|
+
`Ollama models (active store: ${formatOllamaStore(status.activeStore)}; pi-owned store: ${status.ownedModelsDir} with ${status.ownedStore.modelCount} model(s)):`,
|
|
179
197
|
...(models.length === 0
|
|
180
|
-
? [" (none installed — /models add <ref>, or /models suggest for a validated roster)"]
|
|
198
|
+
? [" (none installed — /models add <ref>, /models import, or /models suggest for a validated roster)"]
|
|
181
199
|
: []),
|
|
182
200
|
]
|
|
183
201
|
: [
|
|
@@ -218,7 +236,7 @@ export async function listLocalModels(host) {
|
|
|
218
236
|
return ` - ${model.name} (${gb} GB) · ${probe}`;
|
|
219
237
|
}),
|
|
220
238
|
...(transformersLines.length > 0 ? ["Pi-managed Transformers models:", ...transformersLines] : []),
|
|
221
|
-
"Commands: /models add <ref> · /models remove <ref> confirm · /models stop",
|
|
239
|
+
"Commands: /models import · /models add <ref> · /models remove <ref> confirm · /models stop",
|
|
222
240
|
];
|
|
223
241
|
for (const line of lines)
|
|
224
242
|
host.showStatus(line);
|
|
@@ -226,7 +244,7 @@ export async function listLocalModels(host) {
|
|
|
226
244
|
export async function addLocalModel(host, pullRef, preselectRole) {
|
|
227
245
|
if (!(await ensureLocalServer(host)))
|
|
228
246
|
return;
|
|
229
|
-
host.showStatus(`Pulling ${pullRef}… (weights land in
|
|
247
|
+
host.showStatus(`Pulling ${pullRef}… (weights land in pi's owned Ollama store)`);
|
|
230
248
|
let lastShown = 0;
|
|
231
249
|
const pulled = await host.localRuntime.pull(pullRef, (progress) => {
|
|
232
250
|
const now = Date.now();
|