From eb0af5bbe5c40a3ac35212220a6331d4a76181e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2026 22:24:23 +0100 Subject: [PATCH] fix(models): block unsupported Grok multi-agent routing --- Examples/AI-CLI/README.md | 2 +- Examples/AI-CLI/Sources/AI-CLI.swift | 1 - .../Tachikoma/Core/ModelCapabilities.swift | 1 - Sources/Tachikoma/Models/Model.swift | 18 ++++++---- Sources/Tachikoma/Models/ModelSelection.swift | 8 +++-- .../Providers/Grok/GrokProvider.swift | 19 +++++++++- .../Tachikoma/Providers/ProviderParser.swift | 5 +-- .../Core/GrokModelCatalogTests.swift | 35 ++++++++++++++++--- docs/models.md | 1 - 9 files changed, 70 insertions(+), 20 deletions(-) diff --git a/Examples/AI-CLI/README.md b/Examples/AI-CLI/README.md index f61de26..13e09df 100644 --- a/Examples/AI-CLI/README.md +++ b/Examples/AI-CLI/README.md @@ -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 diff --git a/Examples/AI-CLI/Sources/AI-CLI.swift b/Examples/AI-CLI/Sources/AI-CLI.swift index d64a12d..4085dbe 100644 --- a/Examples/AI-CLI/Sources/AI-CLI.swift +++ b/Examples/AI-CLI/Sources/AI-CLI.swift @@ -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): diff --git a/Sources/Tachikoma/Core/ModelCapabilities.swift b/Sources/Tachikoma/Core/ModelCapabilities.swift index 5efb7b6..00a91a8 100644 --- a/Sources/Tachikoma/Core/ModelCapabilities.swift +++ b/Sources/Tachikoma/Core/ModelCapabilities.swift @@ -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 } diff --git a/Sources/Tachikoma/Models/Model.swift b/Sources/Tachikoma/Models/Model.swift index 119c9d6..2a1ba15 100644 --- a/Sources/Tachikoma/Models/Model.swift +++ b/Sources/Tachikoma/Models/Model.swift @@ -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") } diff --git a/Sources/Tachikoma/Models/ModelSelection.swift b/Sources/Tachikoma/Models/ModelSelection.swift index d5c65f5..81ce403 100644 --- a/Sources/Tachikoma/Models/ModelSelection.swift +++ b/Sources/Tachikoma/Models/ModelSelection.swift @@ -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") } diff --git a/Sources/Tachikoma/Providers/Grok/GrokProvider.swift b/Sources/Tachikoma/Providers/Grok/GrokProvider.swift index 9cf5752..6c99e11 100644 --- a/Sources/Tachikoma/Providers/Grok/GrokProvider.swift +++ b/Sources/Tachikoma/Providers/Grok/GrokProvider.swift @@ -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( diff --git a/Sources/Tachikoma/Providers/ProviderParser.swift b/Sources/Tachikoma/Providers/ProviderParser.swift index 1705e76..ad3a103 100644 --- a/Sources/Tachikoma/Providers/ProviderParser.swift +++ b/Sources/Tachikoma/Providers/ProviderParser.swift @@ -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") } diff --git a/Tests/TachikomaTests/Core/GrokModelCatalogTests.swift b/Tests/TachikomaTests/Core/GrokModelCatalogTests.swift index e3ce140..ea97ffd 100644 --- a/Tests/TachikomaTests/Core/GrokModelCatalogTests.swift +++ b/Tests/TachikomaTests/Core/GrokModelCatalogTests.swift @@ -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) + } } } } diff --git a/docs/models.md b/docs/models.md index 31476ee..cac804f 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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`)