fix(models): block unsupported Grok multi-agent routing

This commit is contained in:
Peter Steinberger 2026-06-08 22:24:23 +01:00
parent 50f59fee1b
commit eb0af5bbe5
No known key found for this signature in database
9 changed files with 70 additions and 20 deletions

View File

@ -71,7 +71,7 @@ Add to your shell profile (`~/.zshrc`, `~/.bashrc`) for persistence.
### Others
- **Mistral**: `mistral-large-latest`, `mistral-medium-latest`, `mistral-medium-3-5`, `mistral-small-latest`, `open-mistral-nemo-2407`, `codestral-latest`
- **Groq**: `openai/gpt-oss-120b`, `openai/gpt-oss-20b`, `llama-3.3-70b-versatile`, `llama-3.1-8b-instant`
- **Grok**: `grok-4.3`, `grok-4.20-multi-agent-0309`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`
- **Grok**: `grok-4.3`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`
- **Ollama** (local): `llama3.3`, `llava`, any installed model
### Model Shortcuts

View File

@ -244,7 +244,6 @@ struct AICLI {
Grok (xAI):
grok-4.3
grok-4.20-multi-agent-0309
grok-4.20-0309-reasoning, grok-4.20-0309-non-reasoning
Ollama (Local):

View File

@ -333,7 +333,6 @@ public final class ModelCapabilityRegistry: @unchecked Sendable {
)
self.capabilities["grok:grok-4.3"] = grokCapabilities
self.capabilities["grok:grok-4.20-multi-agent-0309"] = grokCapabilities
self.capabilities["grok:grok-4.20-0309-reasoning"] = grokCapabilities
self.capabilities["grok:grok-4.20-0309-non-reasoning"] = grokCapabilities
}

View File

@ -388,7 +388,6 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
public static var allCases: [Grok] {
[
.grok43,
.grok420MultiAgent,
.grok420Reasoning,
.grok420NonReasoning,
]
@ -415,7 +414,12 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
}
public var supportsTools: Bool {
true
switch self {
case .grok420MultiAgent:
false
default:
true
}
}
public var supportsAudioInput: Bool {
@ -1415,16 +1419,15 @@ extension LanguageModel {
normalized.hasPrefix("grok-4-fast") ||
normalized.hasPrefix("grok-code-fast") ||
normalized == "grok-4-0709" ||
normalized.contains("grok-4.20-multi-agent") ||
dotted.contains("grok-4-20-multi-agent") ||
compact.contains("grok420multiagent") ||
normalized.contains("grok-beta") ||
normalized.contains("grok-vision-beta")
if unsupportedGrok {
return nil
}
if dotted.contains("grok-4-20-multi-agent") || compact.contains("grok420multiagent") {
return .grok(.grok420MultiAgent)
}
if dotted.contains("grok-4-20-0309-reasoning") || compact.contains("grok4200309reasoning") {
return .grok(.grok420Reasoning)
}
@ -1587,6 +1590,9 @@ extension LanguageModel {
normalized.hasPrefix("grok-4-fast") ||
normalized.hasPrefix("grok-code-fast") ||
normalized == "grok-4-0709" ||
normalized.contains("grok-4.20-multi-agent") ||
normalized.contains("grok-4-20-multi-agent") ||
normalized.contains("grok420multiagent") ||
normalized.contains("grok-beta") ||
normalized.contains("grok-vision-beta")
}

View File

@ -44,10 +44,11 @@ public struct ModelSelector {
return .grok(grokModel)
}
let isProviderQualifiedGrokModel = normalized.contains("/") && normalized.contains("grok")
if
Self.isUnsupportedLegacyOpenAIModel(normalized) ||
Self.isUnsupportedLegacyAnthropicModel(normalized) ||
Self.isUnsupportedLegacyGrokModel(normalized)
(Self.isUnsupportedLegacyGrokModel(normalized) && !isProviderQualifiedGrokModel)
{
throw ModelValidationError.unsupportedModel(modelString)
}
@ -230,8 +231,6 @@ public struct ModelSelector {
// Direct matches for available models only
case "grok-4.3", "grok-4-3", "grok43", "grok-4.3-latest", "grok-4-latest", "grok-4", "grok-latest":
return .grok43
case "grok-4.20-multi-agent-0309", "grok-4-20-multi-agent-0309":
return .grok420MultiAgent
case "grok-4.20-0309-reasoning", "grok-4-20-0309-reasoning":
return .grok420Reasoning
case "grok-4.20-0309-non-reasoning", "grok-4-20-0309-non-reasoning":
@ -298,6 +297,9 @@ public struct ModelSelector {
normalized.hasPrefix("grok-4-fast") ||
normalized.hasPrefix("grok-code-fast") ||
normalized == "grok-4-0709" ||
normalized.contains("grok-4.20-multi-agent") ||
normalized.contains("grok-4-20-multi-agent") ||
normalized.contains("grok420multiagent") ||
normalized.contains("grok-beta") ||
normalized.contains("grok-vision-beta")
}

View File

@ -11,8 +11,15 @@ public final class GrokProvider: ModelProvider {
private let model: LanguageModel.Grok
public init(model: LanguageModel.Grok, configuration: TachikomaConfiguration) throws {
let modelId = model.modelId
guard !Self.requiresResponsesAPIRouting(modelId) else {
throw TachikomaError.unsupportedOperation(
"\(modelId) requires xAI Responses API routing"
)
}
self.model = model
self.modelId = model.modelId
self.modelId = modelId
self.baseURL = configuration.getBaseURL(for: .grok) ?? "https://api.x.ai/v1"
// Get API key from configuration system (environment or credentials)
@ -31,6 +38,16 @@ public final class GrokProvider: ModelProvider {
)
}
private static func requiresResponsesAPIRouting(_ modelId: String) -> Bool {
let normalized = modelId.lowercased()
let compact = normalized
.replacingOccurrences(of: "-", with: "")
.replacingOccurrences(of: ".", with: "")
return normalized.contains("grok-4.20-multi-agent") ||
normalized.contains("grok-4-20-multi-agent") ||
compact.contains("grok420multiagent")
}
public func generateText(request: ProviderRequest) async throws -> ProviderResponse {
// Grok uses OpenAI-compatible API format - delegate to shared implementation
try await OpenAICompatibleHelper.generateText(

View File

@ -337,8 +337,6 @@ public enum ProviderParser {
switch modelString.lowercased() {
case "grok-4.3", "grok-4-3", "grok43", "grok-4.3-latest", "grok-4-latest", "grok-4", "grok-latest":
return .grok(.grok43)
case "grok-4.20-multi-agent-0309", "grok-4-20-multi-agent-0309":
return .grok(.grok420MultiAgent)
case "grok-4.20-0309-reasoning", "grok-4-20-0309-reasoning":
return .grok(.grok420Reasoning)
case "grok-4.20-0309-non-reasoning", "grok-4-20-0309-non-reasoning":
@ -358,6 +356,9 @@ public enum ProviderParser {
normalized.hasPrefix("grok-4-fast") ||
normalized.hasPrefix("grok-code-fast") ||
normalized == "grok-4-0709" ||
normalized.contains("grok-4.20-multi-agent") ||
normalized.contains("grok-4-20-multi-agent") ||
normalized.contains("grok420multiagent") ||
normalized.contains("grok-beta") ||
normalized.contains("grok-vision-beta")
}

View File

@ -5,7 +5,6 @@ import Testing
struct GrokModelCatalogTests {
private static let catalog: [Model.Grok] = [
.grok43,
.grok420MultiAgent,
.grok420Reasoning,
.grok420NonReasoning,
]
@ -49,16 +48,25 @@ struct GrokModelCatalogTests {
func `Grok model vision support matches current xAI catalog`() {
self.requireModernPlatforms {
#expect(Model.grok(.grok43).supportsVision)
#expect(Model.grok(.grok420MultiAgent).supportsVision == false)
#expect(Model.grok(.grok420Reasoning).supportsVision)
#expect(Model.grok(.grok420NonReasoning).supportsVision)
#expect(Model.grok(.grok420MultiAgent).supportsVision == false)
#expect(Model.grok(.grok420MultiAgent).supportsTools == false)
}
}
@Test
func `ModelSelector rejects retired Grok identifiers`() {
func `ModelSelector rejects retired and unsupported Grok identifiers`() {
self.requireModernPlatforms {
for id in ["grok-4-0709", "grok-3", "grok-2-1212", "grok-4-fast", "grok-code-fast-1"] {
for id in [
"grok-4-0709",
"grok-3",
"grok-2-1212",
"grok-4-fast",
"grok-code-fast-1",
"grok-4.20-multi-agent-0309",
"grok420multiagent",
] {
#expect(throws: ModelValidationError.self) {
_ = try ModelSelector.parseModel(id)
}
@ -70,8 +78,27 @@ struct GrokModelCatalogTests {
func `ModelSelector preserves provider-qualified Grok slugs as OpenRouter IDs`() throws {
try self.requireModernPlatforms {
let parsed = try ModelSelector.parseModel("xai/grok-code-fast-1")
let multiAgent = try ModelSelector.parseModel("x-ai/grok-4.20-multi-agent")
#expect(parsed == .openRouter(modelId: "xai/grok-code-fast-1"))
#expect(multiAgent == .openRouter(modelId: "x-ai/grok-4.20-multi-agent"))
}
}
@Test
func `Grok provider rejects multi-agent until Responses routing exists`() throws {
self.requireModernPlatforms {
let config = TachikomaConfiguration(apiKeys: ["grok": "test-key"])
#expect(throws: TachikomaError.self) {
_ = try ProviderFactory.createProvider(for: .grok(.grok420MultiAgent), configuration: config)
}
#expect(throws: TachikomaError.self) {
_ = try GrokProvider(model: .grok420MultiAgent, configuration: config)
}
#expect(throws: TachikomaError.self) {
_ = try GrokProvider(model: .custom("grok420multiagent"), configuration: config)
}
}
}
}

View File

@ -39,7 +39,6 @@ Notes:
## xAI Grok (`LanguageModel.Grok`)
- `grok-4.3`
- `grok-4.20-multi-agent-0309`
- `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`
## Mistral (`LanguageModel.Mistral`)