241 lines · 8.2 KB · TypeScript
RawDownload
import type {  CustomModelSetting,  ProviderDriverKind,  ModelCapabilities,  ServerProvider,  ServerProviderAuth,  ServerProviderSkill,  ServerProviderSlashCommand,  ServerProviderModel,  ServerProviderState,  ServerProviderUsageLimits,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import { readCustomModelEntries } from "@t3tools/shared/model";import { isWindowsCommandNotFound } from "../processRunner.ts";import { createProviderVersionAdvisory } from "./providerMaintenance.ts";import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; export const DEFAULT_TIMEOUT_MS = 4_000;// Auth status checks involve disk/network lookups and can be slow on first run (especially Windows)export const AUTH_PROBE_TIMEOUT_MS = 10_000; export const COMPACT_SLASH_COMMAND = {  name: "compact",  description: "Summarize the conversation and reduce context usage",} satisfies ServerProviderSlashCommand; export interface CommandResult {  readonly stdout: string;  readonly stderr: string;  readonly code: number;} export class ProviderCommandNotFoundError extends Schema.TaggedError<ProviderCommandNotFoundError>()(  "ProviderCommandNotFoundError",  {    binaryPath: Schema.String,    exitCode: Schema.Number,    stdoutLength: Schema.Number,    stderrLength: Schema.Number,  },) {  override get message(): string {    return `Provider command ${this.binaryPath} was not found (exit code ${this.exitCode}).`;  }} const isProviderCommandNotFoundError = Schema.is(ProviderCommandNotFoundError); export interface ProviderProbeResult {  readonly installed: boolean;  readonly version: string | null;  readonly status: Exclude<ServerProviderState, "disabled">;  readonly auth: ServerProviderAuth;  readonly message?: string;  readonly usageLimits?: ServerProviderUsageLimits;} export interface ServerProviderPresentation {  readonly displayName: string;  readonly badgeLabel?: string;  readonly showInteractionModeToggle?: boolean;  readonly reportsContextWindow?: boolean;  readonly requiresNewThreadForModelChange?: boolean;} export type ServerProviderDraft = Omit<ServerProvider, "instanceId" | "driver">; export function nonEmptyTrimmed(value: string | undefined): string | undefined {  if (!value) return undefined;  const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : undefined;} export function isCommandMissingCause(error: unknown): boolean {  if (isProviderCommandNotFoundError(error)) return true;  return error instanceof PlatformError.PlatformError && error.reason._tag === "NotFound";} export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Command) =>  Effect.gen(function* () {    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;    const child = yield* spawner.spawn(command);    const [stdout, stderr, exitCode] = yield* Effect.all(      [        collectStreamAsString(child.stdout),        collectStreamAsString(child.stderr),        child.exitCode.pipe(Effect.map(Number)),      ],      { concurrency: "unbounded" },    );     const result: CommandResult = { stdout, stderr, code: exitCode };    if (yield* isWindowsCommandNotFound(exitCode, stderr)) {      return yield* new ProviderCommandNotFoundError({        binaryPath,        exitCode,        stdoutLength: stdout.length,        stderrLength: stderr.length,      });    }    return result;  }).pipe(Effect.scoped); export function parseGenericCliVersion(output: string): string | null {  const match = output.match(/\b(\d+\.\d+\.\d+)\b/);  return match?.[1] ?? null;} /** * Append the user's custom models after the built-ins. A custom entry that * declares its own capabilities keeps them; a bare slug gets the driver's * default set. Slugs that collide with a built-in are dropped. */export function providerModelsFromSettings(  builtInModels: ReadonlyArray<ServerProviderModel>,  customModels: ReadonlyArray<CustomModelSetting>,  customModelCapabilities: ModelCapabilities,): ReadonlyArray<ServerProviderModel> {  const resolvedBuiltInModels = [...builtInModels];  const seen = new Set(resolvedBuiltInModels.map((model) => model.slug));  const customEntries: ServerProviderModel[] = [];   for (const entry of readCustomModelEntries(customModels)) {    if (seen.has(entry.slug)) {      continue;    }    seen.add(entry.slug);    customEntries.push({      slug: entry.slug,      name: entry.name,      isCustom: true,      capabilities: entry.capabilities ?? customModelCapabilities,    });  }   return [...resolvedBuiltInModels, ...customEntries];} export function buildSelectOptionDescriptor(input: {  readonly id: string;  readonly label: string;  readonly options:    | ReadonlyArray<{        value: string;        label: string;        description?: string | undefined;        isDefault?: boolean | undefined;      }>    | undefined;  readonly description?: string;  readonly promptInjectedValues?: ReadonlyArray<string>;}) {  const options = (input.options ?? []).map((option) => ({    id: option.value,    label: option.label,    ...(option.description ? { description: option.description } : {}),    ...(option.isDefault ? { isDefault: true } : {}),  }));  const currentValue = options.find((option) => option.isDefault)?.id;  return {    id: input.id,    label: input.label,    type: "select" as const,    options,    ...(currentValue ? { currentValue } : {}),    ...(input.description ? { description: input.description } : {}),    ...(input.promptInjectedValues && input.promptInjectedValues.length > 0      ? { promptInjectedValues: [...input.promptInjectedValues] }      : {}),  };} export function buildBooleanOptionDescriptor(input: {  readonly id: string;  readonly label: string;  readonly currentValue?: boolean;  readonly description?: string;}) {  return {    id: input.id,    label: input.label,    type: "boolean" as const,    ...(input.description ? { description: input.description } : {}),    ...(typeof input.currentValue === "boolean" ? { currentValue: input.currentValue } : {}),  };} export function buildServerProvider(input: {  driver?: ProviderDriverKind;  presentation: ServerProviderPresentation;  enabled: boolean;  checkedAt: string;  models: ReadonlyArray<ServerProviderModel>;  slashCommands?: ReadonlyArray<ServerProviderSlashCommand>;  skills?: ReadonlyArray<ServerProviderSkill>;  probe: ProviderProbeResult;}): ServerProviderDraft {  const versionAdvisory = input.driver    ? createProviderVersionAdvisory({        driver: input.driver,        currentVersion: input.probe.version,        checkedAt: input.checkedAt,      })    : undefined;  return {    displayName: input.presentation.displayName,    ...(input.presentation.badgeLabel ? { badgeLabel: input.presentation.badgeLabel } : {}),    ...(typeof input.presentation.showInteractionModeToggle === "boolean"      ? { showInteractionModeToggle: input.presentation.showInteractionModeToggle }      : {}),    ...(typeof input.presentation.reportsContextWindow === "boolean"      ? { reportsContextWindow: input.presentation.reportsContextWindow }      : {}),    ...(typeof input.presentation.requiresNewThreadForModelChange === "boolean"      ? { requiresNewThreadForModelChange: input.presentation.requiresNewThreadForModelChange }      : {}),    enabled: input.enabled,    installed: input.probe.installed,    version: input.probe.version,    status: input.enabled ? input.probe.status : "disabled",    auth: input.probe.auth,    checkedAt: input.checkedAt,    ...(input.probe.message ? { message: input.probe.message } : {}),    models: input.models,    slashCommands: [...(input.slashCommands ?? [])],    skills: [...(input.skills ?? [])],    ...(input.probe.usageLimits ? { usageLimits: input.probe.usageLimits } : {}),    ...(versionAdvisory ? { versionAdvisory } : {}),  };} export const collectStreamAsString = <E>(  stream: Stream.Stream<Uint8Array, E>,  options?: { readonly maxBytes?: number | undefined },): Effect.Effect<string, E> =>  collectUint8StreamText({ stream, ...options }).pipe(Effect.map((collected) => collected.text));