feat: unify installation model catalog
This commit is contained in:
+59
-124
@@ -2,7 +2,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authconfig"
|
||||
@@ -21,6 +19,8 @@ import (
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/modelmigration"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/modelprojection"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/output"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/pi"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/project"
|
||||
@@ -42,6 +42,9 @@ descriptor in the current project tree.
|
||||
Commands:
|
||||
setup [--configure-only] [--installation-id ID] [--profile local|server]
|
||||
Create or validate the local non-secret installation configuration.
|
||||
installation migrate --output PATH --session-default PROVIDER/MODEL
|
||||
--embedding-id PROVIDER/MODEL --embedding-dimensions N
|
||||
Create a review-only schema-v2 candidate from all three legacy model sources.
|
||||
version [--json] Show the host CLI build identity.
|
||||
auth configure --mode local|oidc ...
|
||||
Configure local users or OIDC group mapping; see tht auth for exact options.
|
||||
@@ -65,8 +68,6 @@ Commands:
|
||||
pi doctor Check Pi preconditions without changing the installation.
|
||||
pi test Run the temporary Pi/core smoke checks.
|
||||
pi check Alias for pi test.
|
||||
pi configure [--provider P --model M --thinking low|medium|high]
|
||||
Select closed backend defaults interactively on a TTY; all flags are required otherwise.
|
||||
pi restart --yes [--drain]
|
||||
Recreate only core with the currently selected Pi image and verify readiness.
|
||||
pi update [--version V]
|
||||
@@ -125,6 +126,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
if command == "version" {
|
||||
return versionCommand(commandArgs, stdout, stderr)
|
||||
}
|
||||
if command == "installation" {
|
||||
return installationMigrationCommand(installationPath, commandArgs, stdout, stderr)
|
||||
}
|
||||
return commandUsageError(stderr, fmt.Sprintf("unknown command %q", command))
|
||||
}
|
||||
workingDirectory, err := os.Getwd()
|
||||
@@ -251,7 +255,51 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
func isBootstrapCommand(command string) bool {
|
||||
return command == "help" || command == "setup" || command == "version"
|
||||
return command == "help" || command == "setup" || command == "version" || command == "installation"
|
||||
}
|
||||
|
||||
func installationMigrationCommand(installationPath string, args []string, stdout, stderr io.Writer) int {
|
||||
if installationPath == "" {
|
||||
return commandUsageError(stderr, "installation migrate requires --installation with the legacy descriptor")
|
||||
}
|
||||
if len(args) == 0 || args[0] != "migrate" {
|
||||
return commandUsageError(stderr, "installation requires migrate")
|
||||
}
|
||||
values := make(map[string]string)
|
||||
for index := 1; index < len(args); index += 2 {
|
||||
if index+1 >= len(args) || !strings.HasPrefix(args[index], "--") {
|
||||
return commandUsageError(stderr, "installation migrate requires flag/value pairs")
|
||||
}
|
||||
if _, duplicate := values[args[index]]; duplicate {
|
||||
return commandUsageError(stderr, args[index]+" may be supplied once")
|
||||
}
|
||||
values[args[index]] = args[index+1]
|
||||
}
|
||||
for _, required := range []string{"--output", "--session-default", "--embedding-id", "--embedding-dimensions"} {
|
||||
if values[required] == "" {
|
||||
return commandUsageError(stderr, "installation migrate requires "+required)
|
||||
}
|
||||
}
|
||||
if len(values) != 4 {
|
||||
return commandUsageError(stderr, "installation migrate received an unknown option")
|
||||
}
|
||||
dimensions, err := modelmigration.ParseDimensions(values["--embedding-dimensions"])
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
request := modelmigration.Request{
|
||||
InstallationPath: installationPath,
|
||||
OutputPath: values["--output"],
|
||||
SessionDefault: values["--session-default"],
|
||||
EmbeddingID: values["--embedding-id"],
|
||||
EmbeddingDimensions: dimensions,
|
||||
}
|
||||
if err := modelmigration.Run(request); err != nil {
|
||||
fmt.Fprintf(stderr, "tht: %s\n", output.Sanitize(err.Error(), nil))
|
||||
return 2
|
||||
}
|
||||
fmt.Fprintf(stdout, "Installation schema-v2 candidate written to %s. Legacy inputs were not changed.\n", request.OutputPath)
|
||||
return 0
|
||||
}
|
||||
|
||||
type setupExecutor func(context.Context, compose.Runner, setup.Request, io.Reader, io.Writer) (setup.Result, error)
|
||||
@@ -479,6 +527,11 @@ func piCommand(ctx context.Context, installation config.Installation, runner com
|
||||
}
|
||||
defer func() { _ = lock.Release() }()
|
||||
}
|
||||
if args[0] == "restart" || args[0] == "update" || args[0] == "rollback" {
|
||||
if err := modelprojection.Generate(installation); err != nil {
|
||||
return commandUsageError(stderr, "installation model catalog could not be projected: "+err.Error())
|
||||
}
|
||||
}
|
||||
controlled := compose.InstallationRunner{Installation: installation, Runner: runner}
|
||||
switch args[0] {
|
||||
case "status":
|
||||
@@ -516,20 +569,6 @@ func piCommand(ctx context.Context, installation config.Installation, runner com
|
||||
logArgs := []string{"logs", "--tail", "200", "core"}
|
||||
result, err := controlled.Run(ctx, append([]string{"compose"}, logArgs...), nil)
|
||||
return writeResult(result, err, secretValues, stdout, stderr)
|
||||
case "configure":
|
||||
authFile, authErr := installation.EnvironmentValue("PI_AUTH_FILE")
|
||||
if authErr != nil || strings.TrimSpace(authFile) == "" {
|
||||
return commandUsageError(stderr, "PI_AUTH_FILE must name the actual protected host credential file")
|
||||
}
|
||||
defaults, err := resolvePiConfigure(ctx, controlled, args[1:], os.Stdin, stdout, stdinIsTTY(os.Stdin))
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
if err := pi.Configure(ctx, controlled, defaults); err != nil {
|
||||
return piFailure(stderr, err, secretValues)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Pi defaults applied and read back. Provider credentials remain only in the host file %s (mode 0600). Never pass credentials to tht.\n", authFile)
|
||||
return 0
|
||||
case "restart":
|
||||
request, err := parsePiRestartArgs(
|
||||
args[1:],
|
||||
@@ -617,110 +656,6 @@ func piCommand(ctx context.Context, installation config.Installation, runner com
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePiConfigure(
|
||||
ctx context.Context,
|
||||
runner pi.Runner,
|
||||
args []string,
|
||||
input io.Reader,
|
||||
prompt io.Writer,
|
||||
isTTY bool,
|
||||
) (pi.Defaults, error) {
|
||||
if len(args) > 0 {
|
||||
return parsePiConfigureArgs(args)
|
||||
}
|
||||
if !isTTY {
|
||||
return pi.Defaults{}, errors.New("non-interactive pi configure requires --provider --model --thinking")
|
||||
}
|
||||
options, err := pi.ConfigurationOptions(ctx, runner)
|
||||
if err != nil {
|
||||
return pi.Defaults{}, err
|
||||
}
|
||||
providers := uniqueProviders(options)
|
||||
scanner := bufio.NewScanner(input)
|
||||
provider, err := numberedChoice(scanner, prompt, "provider", providers)
|
||||
if err != nil {
|
||||
return pi.Defaults{}, err
|
||||
}
|
||||
models := make([]string, 0)
|
||||
for _, option := range options {
|
||||
if option.Provider == provider {
|
||||
models = append(models, option.ID)
|
||||
}
|
||||
}
|
||||
model, err := numberedChoice(scanner, prompt, "model", models)
|
||||
if err != nil {
|
||||
return pi.Defaults{}, err
|
||||
}
|
||||
thinking, err := numberedChoice(scanner, prompt, "thinking level", []string{"low", "medium", "high"})
|
||||
if err != nil {
|
||||
return pi.Defaults{}, err
|
||||
}
|
||||
return pi.Defaults{Provider: provider, Model: model, Thinking: thinking}, nil
|
||||
}
|
||||
|
||||
func uniqueProviders(options []pi.ModelOption) []string {
|
||||
seen := make(map[string]bool)
|
||||
providers := make([]string, 0)
|
||||
for _, option := range options {
|
||||
if !seen[option.Provider] {
|
||||
seen[option.Provider] = true
|
||||
providers = append(providers, option.Provider)
|
||||
}
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func numberedChoice(scanner *bufio.Scanner, output io.Writer, label string, choices []string) (string, error) {
|
||||
if len(choices) == 0 {
|
||||
return "", fmt.Errorf("Pi returned no %s choices", label)
|
||||
}
|
||||
fmt.Fprintf(output, "Select %s:\n", label)
|
||||
for index, choice := range choices {
|
||||
fmt.Fprintf(output, " %d) %s\n", index+1, choice)
|
||||
}
|
||||
for {
|
||||
fmt.Fprintf(output, "Choice [1-%d]: ", len(choices))
|
||||
if !scanner.Scan() {
|
||||
return "", fmt.Errorf("interactive %s selection ended before a choice was entered", label)
|
||||
}
|
||||
selected, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
|
||||
if err == nil && selected >= 1 && selected <= len(choices) {
|
||||
return choices[selected-1], nil
|
||||
}
|
||||
fmt.Fprintln(output, "Enter one of the listed numbers.")
|
||||
}
|
||||
}
|
||||
|
||||
func stdinIsTTY(input *os.File) bool {
|
||||
info, err := input.Stat()
|
||||
return err == nil && info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
func parsePiConfigureArgs(args []string) (pi.Defaults, error) {
|
||||
var value pi.Defaults
|
||||
for len(args) > 0 {
|
||||
if len(args) < 2 {
|
||||
return pi.Defaults{}, errors.New("configure options require values")
|
||||
}
|
||||
key, v := args[0], args[1]
|
||||
args = args[2:]
|
||||
switch key {
|
||||
case "--provider":
|
||||
value.Provider = v
|
||||
case "--model":
|
||||
value.Model = v
|
||||
case "--thinking":
|
||||
value.Thinking = v
|
||||
default:
|
||||
return pi.Defaults{}, fmt.Errorf("unknown pi configure option %q", key)
|
||||
}
|
||||
}
|
||||
if value.Provider == "" || value.Model == "" || value.Thinking == "" {
|
||||
return pi.Defaults{}, errors.New("pi configure requires --provider --model --thinking; THT_LLM_URL stays Compose-managed")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parsePiUpdateArgs(args []string, statePath, restartStatePath string) (pi.Request, error) {
|
||||
request := pi.Request{StatePath: statePath, RestartStatePath: restartStatePath}
|
||||
sourceSpecified := false
|
||||
@@ -1008,7 +943,7 @@ func piMutationRequiresLifecycleLock(args []string) bool {
|
||||
return false
|
||||
}
|
||||
switch args[0] {
|
||||
case "configure", "restart", "update", "rollback":
|
||||
case "restart", "update", "rollback":
|
||||
return true
|
||||
case "maintenance":
|
||||
return len(args) > 1 && args[1] == "recover"
|
||||
|
||||
@@ -46,6 +46,7 @@ func TestVersionCommandIsDescriptorFreeAndMachineReadable(t *testing.T) {
|
||||
func TestDoctorJSONWritesOnlyOneReportDocument(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
fixture.generateModelProjection(t)
|
||||
fullHealth := `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"},{"Service":"qdrant","State":"running","Health":"healthy"},{"Service":"embedding","State":"running","Health":"healthy"},{"Service":"embedding-model-init","State":"exited","ExitCode":0}]`
|
||||
t.Setenv("THT_FAKE_PS", fullHealth)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/modelprojection"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/pi"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/setup"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
|
||||
@@ -175,7 +176,6 @@ func TestPiMutationsUseTheSharedInstallationLifecycleLock(t *testing.T) {
|
||||
{args: []string{"doctor"}, want: false},
|
||||
{args: []string{"test"}, want: false},
|
||||
{args: []string{"logs"}, want: false},
|
||||
{args: []string{"configure"}, want: true},
|
||||
{args: []string{"restart"}, want: true},
|
||||
{args: []string{"update"}, want: true},
|
||||
{args: []string{"rollback"}, want: true},
|
||||
@@ -322,39 +322,6 @@ func TestSetupCommandBuildsAndStartsUnlessConfigureOnlyIsRequested(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// Catches interactive configuration prompts that use retired model-only data instead of the
|
||||
// provider, model, and reasoning choices supplied by the dedicated Pi Management API.
|
||||
func TestResolvePiConfigureUsesNumberedClosedChoicesOnlyForTTY(t *testing.T) {
|
||||
runner := &wizardRunner{}
|
||||
var prompt bytes.Buffer
|
||||
defaults, err := resolvePiConfigure(
|
||||
context.Background(), runner, nil, strings.NewReader("2\n1\n3\n"), &prompt, true,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := pi.Defaults{Provider: "zai", Model: "glm-5.2", Thinking: "high"}
|
||||
if defaults != want {
|
||||
t.Fatalf("defaults = %#v", defaults)
|
||||
}
|
||||
for _, expected := range []string{"1) deepseek", "2) zai", "1) glm-5.2", "3) high"} {
|
||||
if !strings.Contains(prompt.String(), expected) {
|
||||
t.Errorf("prompt %q missing %q", prompt.String(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePiConfigureRequiresExplicitFlagsWithoutTTY(t *testing.T) {
|
||||
runner := &wizardRunner{}
|
||||
_, err := resolvePiConfigure(context.Background(), runner, nil, strings.NewReader("1\n1\n1\n"), io.Discard, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-interactive") {
|
||||
t.Fatalf("resolvePiConfigure() error = %v, want explicit non-interactive guidance", err)
|
||||
}
|
||||
if len(runner.calls) != 0 {
|
||||
t.Fatalf("Docker calls = %v, want none", runner.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiLifecycleContractErrorsExitTwo(t *testing.T) {
|
||||
for _, lifecycleErr := range []error{pi.ErrActiveSessions, pi.ErrInterruptedUpdate} {
|
||||
var stderr bytes.Buffer
|
||||
@@ -421,12 +388,11 @@ func TestRunPiStatusUsesInstallationEnvironmentWithoutFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageDocumentsClosedConfigureUpdateSourcesRestartAndMaintenanceRecovery(t *testing.T) {
|
||||
func TestUsageDocumentsUpdateSourcesRestartAndMaintenanceRecovery(t *testing.T) {
|
||||
if strings.Contains(usage, "--follow") {
|
||||
t.Fatal("usage still advertises unbounded log following")
|
||||
}
|
||||
for _, required := range []string{
|
||||
"--provider P --model M --thinking low|medium|high",
|
||||
"--source build",
|
||||
"--source pull --image IMAGE@sha256:DIGEST",
|
||||
"pi restart --yes [--drain]",
|
||||
@@ -610,13 +576,6 @@ func TestRunRemoveDisplaysExactInstallationTargetsBeforeConfirmation(t *testing.
|
||||
assertInvocationContains(t, calls, "ps", "--all", "--format", "json", "core", "frontend")
|
||||
}
|
||||
|
||||
type wizardRunner struct{ calls []string }
|
||||
|
||||
func (r *wizardRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) {
|
||||
r.calls = append(r.calls, strings.Join(args, " "))
|
||||
return compose.Result{Stdout: `{"providers":["deepseek","zai"],"models":[{"provider":"deepseek","id":"deepseek-v4"},{"provider":"zai","id":"glm-5.2"}],"reasoning":["low","medium","high"]}`}, nil
|
||||
}
|
||||
|
||||
func TestRunLogsRedactsAnUnlabelledDeclaredSecret(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "UNLABELLED_SECRET_FILE=%s\n")
|
||||
secretPath := filepath.Join(fixture.root, "operator-secret")
|
||||
@@ -941,6 +900,7 @@ func TestRunStatusUsesStableComposeArguments(t *testing.T) {
|
||||
"--env-file", fixture.envFile,
|
||||
"-f", filepath.Join(fixture.projectDirectory, "compose.yaml"),
|
||||
"-f", filepath.Join(fixture.projectDirectory, "deploy", "compose.local.yaml"),
|
||||
"-f", filepath.Join(fixture.root, "generated", "compose.models.yaml"),
|
||||
"ps", "--format", "json",
|
||||
}
|
||||
got := invocations[0]
|
||||
@@ -998,6 +958,7 @@ func TestRunExplainsWhenDockerIsNotAvailable(t *testing.T) {
|
||||
func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
fixture.generateModelProjection(t)
|
||||
|
||||
report := runDoctorJSON(t, fixture)
|
||||
assertDoctorCheck(t, report, "configuration", doctor.StatusPassed)
|
||||
@@ -1007,6 +968,7 @@ func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) {
|
||||
func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
fixture.generateModelProjection(t)
|
||||
testsupport.SymlinkOrSkip(
|
||||
t,
|
||||
filepath.Join(fixture.projectDirectory, "missing-workspace.yaml"),
|
||||
@@ -1021,6 +983,7 @@ func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T)
|
||||
func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
fixture.generateModelProjection(t)
|
||||
t.Setenv(
|
||||
"THT_FAKE_PS",
|
||||
"{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
|
||||
@@ -1189,31 +1152,6 @@ func TestRunPiUpdateAcceptsExplicitVersionWithoutAdvancedFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPiConfigureReportsTheActualHostAuthFile(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "")
|
||||
authFile := filepath.Join(fixture.root, "pi-auth.json")
|
||||
if err := os.WriteFile(authFile, []byte(`{"provider":"credential"}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fixture.setEnvContents(t, "THT_LLM_URL=https://llm.example.invalid\nPI_AUTH_FILE="+authFile+"\n")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{
|
||||
"--installation", fixture.installationPath, "pi", "configure",
|
||||
"--provider", "provider", "--model", "model", "--thinking", "medium",
|
||||
}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit = %d, stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), authFile) {
|
||||
t.Fatalf("stdout = %q, want host auth path", stdout.String())
|
||||
}
|
||||
if strings.Contains(stdout.String(), "/home/thoth/.pi") {
|
||||
t.Fatalf("stdout exposed container-only auth path: %q", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPiMaintenanceStatusAndRecoverConfirmationContract(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
||||
fixture.setEnvironment(t)
|
||||
@@ -1493,7 +1431,9 @@ func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installationPath := filepath.Join(root, "thothii-installation.yaml")
|
||||
contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + authDirectory + "\n"
|
||||
contents := "schemaVersion: 2\nprofile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\n" +
|
||||
"modelCatalog:\n defaults:\n session: deepseek/deepseek-v4-pro\n embedding:\n id: ollama/qwen3-embedding:0.6b\n dimensions: 1024\n providers:\n deepseek:\n authentication: {mode: pi_auth}\n session: {mode: pi_builtin}\n models:\n deepseek-v4-pro:\n session: {}\n" +
|
||||
"authentication:\n configDirectory: " + authDirectory + "\n"
|
||||
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1589,13 +1529,26 @@ func (f cliFixture) setEnvContents(t *testing.T, env string) {
|
||||
t.Setenv("THT_FAKE_PI_VERSION", "0.80.3")
|
||||
}
|
||||
|
||||
func (f cliFixture) generateModelProjection(t *testing.T) {
|
||||
t.Helper()
|
||||
installation, err := config.Load(f.installationPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := modelprojection.Generate(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f cliFixture) setProfile(t *testing.T, profile string) {
|
||||
t.Helper()
|
||||
composePath := filepath.Join(f.projectDirectory, "deploy", "compose."+profile+".yaml")
|
||||
if err := os.WriteFile(composePath, []byte("services: {}\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents := "profile: " + profile + "\nprojectDirectory: " + f.projectDirectory + "\nenvFile: " + f.envFile + "\nauthentication:\n configDirectory: " + filepath.Join(f.root, "auth") + "\n"
|
||||
contents := "schemaVersion: 2\nprofile: " + profile + "\nprojectDirectory: " + f.projectDirectory + "\nenvFile: " + f.envFile + "\n" +
|
||||
"modelCatalog:\n defaults:\n session: deepseek/deepseek-v4-pro\n embedding:\n id: ollama/qwen3-embedding:0.6b\n dimensions: 1024\n providers:\n deepseek:\n authentication: {mode: pi_auth}\n session: {mode: pi_builtin}\n models:\n deepseek-v4-pro:\n session: {}\n" +
|
||||
"authentication:\n configDirectory: " + filepath.Join(f.root, "auth") + "\n"
|
||||
if err := os.WriteFile(f.installationPath, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user