mirror of
https://github.com/ollama/ollama.git
synced 2026-04-22 16:55:44 +02:00
316 lines
8.5 KiB
Go
316 lines
8.5 KiB
Go
package launch
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/envconfig"
|
|
)
|
|
|
|
// Kimi implements Runner for Kimi Code CLI integration.
|
|
type Kimi struct{}
|
|
|
|
const (
|
|
kimiDefaultModelAlias = "ollama"
|
|
kimiDefaultMaxContextSize = 32768
|
|
)
|
|
|
|
var (
|
|
kimiGOOS = runtime.GOOS
|
|
kimiModelShowTimeout = 5 * time.Second
|
|
)
|
|
|
|
func (k *Kimi) String() string { return "Kimi Code CLI" }
|
|
|
|
func (k *Kimi) args(config string, extra []string) []string {
|
|
args := []string{"--config", config}
|
|
args = append(args, extra...)
|
|
return args
|
|
}
|
|
|
|
func (k *Kimi) Run(model string, args []string) error {
|
|
if strings.TrimSpace(model) == "" {
|
|
return fmt.Errorf("model is required")
|
|
}
|
|
if err := validateKimiPassthroughArgs(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
config, err := buildKimiInlineConfig(model, resolveKimiMaxContextSize(model))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build kimi config: %w", err)
|
|
}
|
|
|
|
bin, err := ensureKimiInstalled()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(bin, k.args(config, args)...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func findKimiBinary() (string, error) {
|
|
if path, err := exec.LookPath("kimi"); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
var candidates []string
|
|
switch kimiGOOS {
|
|
case "windows":
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(home, ".local", "bin"))
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(home, "bin"))
|
|
|
|
if appData := strings.TrimSpace(os.Getenv("APPDATA")); appData != "" {
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(appData, "uv", "bin"))
|
|
}
|
|
if localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); localAppData != "" {
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(localAppData, "uv", "bin"))
|
|
}
|
|
default:
|
|
candidates = append(candidates,
|
|
filepath.Join(home, ".local", "bin", "kimi"),
|
|
filepath.Join(home, "bin", "kimi"),
|
|
filepath.Join(home, ".local", "share", "uv", "tools", "kimi-cli", "bin", "kimi"),
|
|
filepath.Join(home, ".local", "share", "uv", "tools", "kimi", "bin", "kimi"),
|
|
)
|
|
|
|
if xdgDataHome := strings.TrimSpace(os.Getenv("XDG_DATA_HOME")); xdgDataHome != "" {
|
|
candidates = append(candidates,
|
|
filepath.Join(xdgDataHome, "uv", "tools", "kimi-cli", "bin", "kimi"),
|
|
filepath.Join(xdgDataHome, "uv", "tools", "kimi", "bin", "kimi"),
|
|
)
|
|
}
|
|
|
|
// WSL users can inherit Windows env vars while launching from Linux shells.
|
|
if profile := windowsPathToWSL(os.Getenv("USERPROFILE")); profile != "" {
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(profile, ".local", "bin"))
|
|
}
|
|
if appData := windowsPathToWSL(os.Getenv("APPDATA")); appData != "" {
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(appData, "uv", "bin"))
|
|
}
|
|
if localAppData := windowsPathToWSL(os.Getenv("LOCALAPPDATA")); localAppData != "" {
|
|
candidates = appendWindowsKimiCandidates(candidates, filepath.Join(localAppData, "uv", "bin"))
|
|
}
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("kimi binary not found")
|
|
}
|
|
|
|
func appendWindowsKimiCandidates(candidates []string, dir string) []string {
|
|
if strings.TrimSpace(dir) == "" {
|
|
return candidates
|
|
}
|
|
|
|
return append(candidates,
|
|
filepath.Join(dir, "kimi.exe"),
|
|
filepath.Join(dir, "kimi.cmd"),
|
|
filepath.Join(dir, "kimi.bat"),
|
|
)
|
|
}
|
|
|
|
func windowsPathToWSL(path string) string {
|
|
trimmed := strings.TrimSpace(path)
|
|
if len(trimmed) < 3 || trimmed[1] != ':' {
|
|
return ""
|
|
}
|
|
|
|
drive := strings.ToLower(string(trimmed[0]))
|
|
rest := strings.ReplaceAll(trimmed[2:], "\\", "/")
|
|
rest = strings.TrimPrefix(rest, "/")
|
|
if rest == "" {
|
|
return filepath.Join("/mnt", drive)
|
|
}
|
|
|
|
return filepath.Join("/mnt", drive, rest)
|
|
}
|
|
|
|
func validateKimiPassthroughArgs(args []string) error {
|
|
for _, arg := range args {
|
|
switch {
|
|
case arg == "--config", strings.HasPrefix(arg, "--config="):
|
|
return fmt.Errorf("conflicting extra argument %q: ollama launch kimi manages --config", arg)
|
|
case arg == "--config-file", strings.HasPrefix(arg, "--config-file="):
|
|
return fmt.Errorf("conflicting extra argument %q: ollama launch kimi manages --config-file", arg)
|
|
case arg == "--model", strings.HasPrefix(arg, "--model="):
|
|
return fmt.Errorf("conflicting extra argument %q: ollama launch kimi manages --model", arg)
|
|
case arg == "-m", strings.HasPrefix(arg, "-m="):
|
|
return fmt.Errorf("conflicting extra argument %q: ollama launch kimi manages -m/--model", arg)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildKimiInlineConfig(model string, maxContextSize int) (string, error) {
|
|
cfg := map[string]any{
|
|
"default_model": kimiDefaultModelAlias,
|
|
"providers": map[string]any{
|
|
kimiDefaultModelAlias: map[string]any{
|
|
"type": "openai_legacy",
|
|
"base_url": envconfig.ConnectableHost().String() + "/v1",
|
|
"api_key": "ollama",
|
|
},
|
|
},
|
|
"models": map[string]any{
|
|
kimiDefaultModelAlias: map[string]any{
|
|
"provider": kimiDefaultModelAlias,
|
|
"model": model,
|
|
"max_context_size": maxContextSize,
|
|
},
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func resolveKimiMaxContextSize(model string) int {
|
|
if l, ok := lookupCloudModelLimit(model); ok {
|
|
return l.Context
|
|
}
|
|
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return kimiDefaultMaxContextSize
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), kimiModelShowTimeout)
|
|
defer cancel()
|
|
resp, err := client.Show(ctx, &api.ShowRequest{Model: model})
|
|
if err != nil {
|
|
return kimiDefaultMaxContextSize
|
|
}
|
|
|
|
if n, ok := modelInfoContextLength(resp.ModelInfo); ok {
|
|
return n
|
|
}
|
|
|
|
return kimiDefaultMaxContextSize
|
|
}
|
|
|
|
func modelInfoContextLength(modelInfo map[string]any) (int, bool) {
|
|
for key, val := range modelInfo {
|
|
if !strings.HasSuffix(key, ".context_length") {
|
|
continue
|
|
}
|
|
switch v := val.(type) {
|
|
case float64:
|
|
if v > 0 {
|
|
return int(v), true
|
|
}
|
|
case int:
|
|
if v > 0 {
|
|
return v, true
|
|
}
|
|
case int64:
|
|
if v > 0 {
|
|
return int(v), true
|
|
}
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func ensureKimiInstalled() (string, error) {
|
|
if path, err := findKimiBinary(); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
if err := checkKimiInstallerDependencies(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ok, err := ConfirmPrompt("Kimi is not installed. Install now?")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
return "", fmt.Errorf("kimi installation cancelled")
|
|
}
|
|
|
|
bin, args, err := kimiInstallerCommand(kimiGOOS)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "\nInstalling Kimi...\n")
|
|
cmd := exec.Command(bin, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("failed to install kimi: %w", err)
|
|
}
|
|
|
|
path, err := findKimiBinary()
|
|
if err != nil {
|
|
return "", fmt.Errorf("kimi was installed but the binary was not found on PATH\n\nYou may need to restart your shell")
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%sKimi installed successfully%s\n\n", ansiGreen, ansiReset)
|
|
return path, nil
|
|
}
|
|
|
|
func checkKimiInstallerDependencies() error {
|
|
switch kimiGOOS {
|
|
case "windows":
|
|
if _, err := exec.LookPath("powershell"); err != nil {
|
|
return fmt.Errorf("kimi is not installed and required dependencies are missing\n\nInstall the following first:\n PowerShell: https://learn.microsoft.com/powershell/\n\nThen re-run:\n ollama launch kimi")
|
|
}
|
|
default:
|
|
var missing []string
|
|
if _, err := exec.LookPath("curl"); err != nil {
|
|
missing = append(missing, "curl: https://curl.se/")
|
|
}
|
|
if _, err := exec.LookPath("bash"); err != nil {
|
|
missing = append(missing, "bash: https://www.gnu.org/software/bash/")
|
|
}
|
|
if len(missing) > 0 {
|
|
return fmt.Errorf("kimi is not installed and required dependencies are missing\n\nInstall the following first:\n %s\n\nThen re-run:\n ollama launch kimi", strings.Join(missing, "\n "))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func kimiInstallerCommand(goos string) (string, []string, error) {
|
|
switch goos {
|
|
case "windows":
|
|
return "powershell", []string{
|
|
"-NoProfile",
|
|
"-ExecutionPolicy",
|
|
"Bypass",
|
|
"-Command",
|
|
"Invoke-RestMethod https://code.kimi.com/install.ps1 | Invoke-Expression",
|
|
}, nil
|
|
case "darwin", "linux":
|
|
return "bash", []string{
|
|
"-c",
|
|
"curl -LsSf https://code.kimi.com/install.sh | bash",
|
|
}, nil
|
|
default:
|
|
return "", nil, fmt.Errorf("unsupported platform for kimi install: %s", goos)
|
|
}
|
|
}
|