Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ extension AgentCommand {
if Self.supportedMiniMaxInputs.contains(model) {
return .minimaxCN(model)
}
case let .kimi(model):
if Self.supportedKimiInputs.contains(model) {
return .kimi(model)
}
case .ollama, .lmstudio:
return parsed.supportsTools ? parsed : nil
case .openRouter:
Expand Down Expand Up @@ -156,6 +160,11 @@ extension AgentCommand {
.m27Highspeed,
]

private static let supportedKimiInputs: Set<LanguageModel.Kimi> = [
.k26,
.k27,
]

private static let reservedProviderInputs: Set<String> = [
"openai",
"anthropic",
Expand All @@ -167,6 +176,8 @@ extension AgentCommand {
"minimax-cn",
"minimax_cn",
"minimaxi",
"kimi",
"moonshot",
"ollama",
"lmstudio",
"lm-studio",
Expand All @@ -177,10 +188,12 @@ extension AgentCommand {
let anthropicModels = Self.supportedAnthropicInputs.map(\.modelId)
let googleModels = Self.supportedGoogleInputs.map(\.userFacingModelId)
let miniMaxModels = Self.supportedMiniMaxInputs.map(\.modelId)
return (openAIModels + anthropicModels + googleModels + miniMaxModels + [
let kimiModels = Self.supportedKimiInputs.map(\.modelId)
return (openAIModels + anthropicModels + googleModels + miniMaxModels + kimiModels + [
"grok/<model>",
"xai/<model>",
"minimax-cn/<model>",
"kimi/<model>",
"ollama/<model>",
"lmstudio/<model>",
"openrouter/<provider>/<model>",
Expand Down Expand Up @@ -246,6 +259,8 @@ extension AgentCommand {
return configuration.getMiniMaxAPIKey()?.isEmpty == false
case .minimaxCN:
return configuration.getMiniMaxChinaAPIKey()?.isEmpty == false
case .kimi:
return configuration.getKimiAPIKey()?.isEmpty == false
case .grok:
return configuration.getGrokAPIKey()?.isEmpty == false
case .openRouter:
Expand All @@ -269,6 +284,8 @@ extension AgentCommand {
"MiniMax"
case .minimaxCN:
"MiniMax China"
case .kimi:
"Kimi"
case .ollama:
"Ollama"
case .lmstudio:
Expand Down Expand Up @@ -296,6 +313,8 @@ extension AgentCommand {
"MINIMAX_API_KEY"
case .minimaxCN:
"MINIMAX_CN_API_KEY or MINIMAX_API_KEY"
case .kimi:
"MOONSHOT_API_KEY or KIMI_API_KEY"
case .ollama:
"OLLAMA_BASE_URL or PEEKABOO_OLLAMA_BASE_URL"
case .lmstudio:
Expand Down
14 changes: 14 additions & 0 deletions Apps/CLI/Tests/CoreCLITests/AgentCommandModelParsingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ struct AgentCommandTests {
#expect(command.parseModelString("minimax-cn/not-a-supported-model") == nil)
}

@Test
func `Kimi (Moonshot) models are accepted`() throws {
let command = try AgentCommand.parse([])

#expect(command.parseModelString("kimi/k2p6") == .kimi(.k26))
#expect(command.parseModelString("kimi/k2p7") == .kimi(.k27))
#expect(command.parseModelString("kimi/kimi-k2.7-code") == .kimi(.k27))
#expect(command.parseModelString("moonshot/k2p7") == .kimi(.k27))
#expect(command.parseModelString("kimi-k2.6") == .kimi(.k26))
#expect(command.parseModelString("kimi/unknown-model") == nil)
}

@Test
func `OpenRouter provider model IDs are accepted`() throws {
let command = try AgentCommand.parse([])
Expand Down Expand Up @@ -376,6 +388,8 @@ struct ModelSelectionIntegrationTests {
("claude-opus-4.8", .anthropic(.opus48)),
("gemini-3.5-flash", .google(.gemini35Flash)),
("MiniMax-M2.7", .minimax(.m27)),
("kimi/k2p6", .kimi(.k26)),
("kimi/k2p7", .kimi(.k27)),
("ollama/llama3.3", .ollama(.llama33)),
("openrouter/xiaomi/mimo-v2.5-pro", .openRouter(modelId: "xiaomi/mimo-v2.5-pro")),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ extension PeekabooAgentService {
8192
case .minimax, .minimaxCN:
8192
case .kimi:
32_768
case .mistral, .groq, .grok, .ollama, .lmstudio, .azureOpenAI, .replicate:
4096
case let .openRouter(modelId), let .together(modelId), let .openaiCompatible(modelId, _):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public final class PeekabooAgentService: AgentServiceProtocol {
case let .lmstudio(model): "lmstudio/\(model.modelId)"
case let .minimax(model): "minimax/\(model.modelId)"
case let .minimaxCN(model): "minimax-cn/\(model.modelId)"
case let .kimi(model): "kimi/\(model.modelId)"
case let .openRouter(modelID): "openrouter/\(modelID)"
case let .together(modelID): "together/\(modelID)"
case let .replicate(modelID): "replicate/\(modelID)"
Expand Down Expand Up @@ -105,6 +106,8 @@ public final class PeekabooAgentService: AgentServiceProtocol {
config.getAPIKey(for: .minimax)
case .minimaxCN:
config.getAPIKey(for: .minimaxCN)
case .kimi:
config.getAPIKey(for: .kimi)
case .mistral:
config.getAPIKey(for: .mistral)
case .groq:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ extension ConfigurationManager {
return fallbackToSharedKey ? self.getMiniMaxAPIKey() : nil
}

/// Get Kimi (Moonshot) API key with proper precedence.
public func getKimiAPIKey() -> String? {
if let envValue = self.environmentValue(for: "MOONSHOT_API_KEY") {
return envValue
}

if let credValue = credentials["MOONSHOT_API_KEY"] ?? credentials["KIMI_API_KEY"] {
return credValue
}

return self.environmentValue(for: "KIMI_API_KEY")
}

/// Get OpenRouter API key with proper precedence.
public func getOpenRouterAPIKey() -> String? {
if let envValue = self.environmentValue(for: "OPENROUTER_API_KEY") {
Expand Down Expand Up @@ -261,6 +274,9 @@ extension ConfigurationManager {
if let key = self.getMiniMaxChinaAPIKey(fallbackToSharedKey: false), !key.isEmpty {
configuration.setAPIKey(key, for: .minimaxCN)
}
if let key = self.getKimiAPIKey(), !key.isEmpty {
configuration.setAPIKey(key, for: .kimi)
}
if let key = self.getOpenRouterAPIKey(), !key.isEmpty {
configuration.setAPIKey(key, for: "openrouter")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ public final class PeekabooAIService {
private static func isHostedProviderIdentifier(_ provider: String) -> Bool {
switch provider {
case "openai", "anthropic", "google", "gemini", "minimax", "minimax-cn", "minimax_cn", "minimaxi",
"kimi", "moonshot",
"openrouter", "mistral", "groq", "grok", "xai":
true
default:
Expand Down Expand Up @@ -381,6 +382,9 @@ public final class PeekabooAIService {
let parsed = LanguageModel.parse(from: "minimax-cn/\(modelString)")
if case .minimaxCN = parsed { return parsed }
return nil
case "kimi", "moonshot":
if case .kimi = loose { return loose }
return nil
case "openrouter":
return .openRouter(modelId: modelString)
case "mistral":
Expand Down Expand Up @@ -463,6 +467,9 @@ public final class PeekabooAIService {
if let key = configuration.getMiniMaxAPIKey(), !key.isEmpty {
return self.appendingGeneratedVisionFallbacks(from: parsed, to: [.minimax(.m27)])
}
if let key = configuration.getKimiAPIKey(), !key.isEmpty {
return self.appendingGeneratedVisionFallbacks(from: parsed, to: [.kimi(.k26)])
}
if let key = configuration.getOpenRouterAPIKey(), !key.isEmpty {
return self.appendingGeneratedVisionFallbacks(
from: parsed,
Expand Down Expand Up @@ -550,6 +557,8 @@ public final class PeekabooAIService {
configuration.getMiniMaxAPIKey()?.isEmpty == false
case .minimaxCN:
configuration.getMiniMaxChinaAPIKey()?.isEmpty == false
case .kimi:
configuration.getKimiAPIKey()?.isEmpty == false
case .grok:
configuration.getGrokAPIKey()?.isEmpty == false
case .openRouter:
Expand All @@ -575,6 +584,7 @@ public final class PeekabooAIService {
case let .lmstudio(m): ("lmstudio", m.modelId)
case let .minimax(m): ("minimax", m.modelId)
case let .minimaxCN(m): ("minimax-cn", m.modelId)
case let .kimi(m): ("kimi", m.modelId)
case let .azureOpenAI(deployment, _, _, _): ("azure-openai", deployment)
case let .openRouter(modelId): ("openrouter", modelId)
case let .together(modelId): ("together", modelId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ struct AIProviderParserTests {
#expect(AIProviderParser.parse("minimax-cn/MiniMax-M2.7") == AIProviderParser.ProviderConfig(
provider: "minimax-cn",
model: "MiniMax-M2.7"))
#expect(AIProviderParser.parse("kimi/k2p7") == AIProviderParser.ProviderConfig(
provider: "kimi",
model: "k2p7"))
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion Tachikoma