diff --git a/backend/internal/adapters/agent/kimi/kimi.go b/backend/internal/adapters/agent/kimi/kimi.go index 63d6210994..77965a3091 100644 --- a/backend/internal/adapters/agent/kimi/kimi.go +++ b/backend/internal/adapters/agent/kimi/kimi.go @@ -77,6 +77,22 @@ func (p *Plugin) Manifest() adapters.Manifest { } } +// GetConfigSpec reports the per-project agent config keys Kimi understands. +func (p *Plugin) GetConfigSpec(ctx context.Context) (ports.ConfigSpec, error) { + if err := ctx.Err(); err != nil { + return ports.ConfigSpec{}, err + } + return ports.ConfigSpec{ + Fields: []ports.ConfigField{ + { + Key: "model", + Type: ports.ConfigFieldString, + Description: "Model override passed to `kimi --model`.", + }, + }, + }, nil +} + // GetLaunchCommand builds the argv to start a new Kimi session: // // kimi [--auto|-y] (interactive) @@ -94,6 +110,7 @@ func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) ( cmd = []string{binary} appendApprovalFlags(&cmd, cfg.Permissions) + appendModelFlag(&cmd, cfg.Config) return cmd, nil } @@ -133,6 +150,9 @@ func (p *Plugin) PromptReadinessHints(ctx context.Context, _ ports.LaunchConfig) // approval settings of the original session -- so cfg.Permissions is // intentionally ignored here. func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) (cmd []string, ok bool, err error) { + // Deliberately does not forward cfg.Config.Model: --model + --session + // composition is unverified against a real kimi install (see #2890 and + // appendModelFlag). Pinned by TestGetRestoreCommandDoesNotForwardModelOverride. if err := ctx.Err(); err != nil { return nil, false, err } @@ -170,6 +190,21 @@ func appendApprovalFlags(cmd *[]string, permissions ports.PermissionMode) { } } +// appendModelFlag appends `--model ` when a role-specific model +// override is configured. A blank/whitespace-only value means no override was +// configured, so the flag is omitted and Kimi's own default model applies. +// +// Restore-path note: this is intentionally NOT called from +// GetRestoreCommand. Kimi's docs do not confirm --model composes with +// --session the way it does on the launch path (see issue #2890); wiring it +// blind risks silently breaking session resume. Left as a fast-follow pending +// verification against a real kimi install. +func appendModelFlag(cmd *[]string, cfg ports.AgentConfig) { + if model := strings.TrimSpace(cfg.Model); model != "" { + *cmd = append(*cmd, "--model", model) + } +} + func normalizePermissionMode(mode ports.PermissionMode) ports.PermissionMode { switch mode { case ports.PermissionModeDefault, diff --git a/backend/internal/adapters/agent/kimi/kimi_test.go b/backend/internal/adapters/agent/kimi/kimi_test.go index a012305c2a..25a83e96d6 100644 --- a/backend/internal/adapters/agent/kimi/kimi_test.go +++ b/backend/internal/adapters/agent/kimi/kimi_test.go @@ -32,13 +32,84 @@ func TestManifest(t *testing.T) { } } -func TestGetConfigSpecEmpty(t *testing.T) { +func TestGetConfigSpecAdvertisesModelField(t *testing.T) { spec, err := (&Plugin{}).GetConfigSpec(context.Background()) if err != nil { t.Fatalf("err: %v", err) } - if len(spec.Fields) != 0 { - t.Fatalf("expected no fields, got %d", len(spec.Fields)) + if len(spec.Fields) != 1 { + t.Fatalf("expected 1 field, got %d", len(spec.Fields)) + } + if spec.Fields[0].Key != "model" || spec.Fields[0].Type != ports.ConfigFieldString { + t.Fatalf("field = %#v, want model/string", spec.Fields[0]) + } +} + +func TestGetLaunchCommandAppendsModelFlagWhenConfigured(t *testing.T) { + p := &Plugin{resolvedBinary: "kimi"} + cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ + Config: ports.AgentConfig{Model: "k2.5"}, + }) + if err != nil { + t.Fatal(err) + } + want := []string{"kimi", "--model", "k2.5"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + +func TestGetLaunchCommandOmitsModelFlagWhenBlank(t *testing.T) { + p := &Plugin{resolvedBinary: "kimi"} + cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ + Config: ports.AgentConfig{Model: " "}, + }) + if err != nil { + t.Fatal(err) + } + want := []string{"kimi"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + +func TestGetLaunchCommandModelFlagComposesWithApprovalFlags(t *testing.T) { + p := &Plugin{resolvedBinary: "kimi"} + cmd, err := p.GetLaunchCommand(context.Background(), ports.LaunchConfig{ + Permissions: ports.PermissionModeBypassPermissions, + Config: ports.AgentConfig{Model: "k2.5"}, + }) + if err != nil { + t.Fatal(err) + } + want := []string{"kimi", "-y", "--model", "k2.5"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v", cmd, want) + } +} + +// Mirrors the issue #2890 harness note: --model + --session composition is +// unverified against a real kimi install, so appendModelFlag is intentionally +// not wired into GetRestoreCommand. This test locks that decision in so a +// future "just mirror Copilot/Codex" edit doesn't silently forward the model +// override on restore without re-verifying first. +func TestGetRestoreCommandDoesNotForwardModelOverride(t *testing.T) { + p := &Plugin{resolvedBinary: "kimi"} + cmd, ok, err := p.GetRestoreCommand(context.Background(), ports.RestoreConfig{ + Session: ports.SessionRef{ + Metadata: map[string]string{ports.MetadataKeyAgentSessionID: "01HZABC"}, + }, + Config: ports.AgentConfig{Model: "k2.5"}, + }) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("ok=false, want true") + } + want := []string{"kimi", "--session", "01HZABC"} + if !reflect.DeepEqual(cmd, want) { + t.Fatalf("cmd = %#v, want %#v (model override must not be forwarded on restore)", cmd, want) } }