# Phase 07 — Model catalog **Estimated time:** ~15 minutes **Touches:** `lib/dispatch/adapter/minimax/model_catalog.rb`, `lib/dispatch/adapter/minimax.rb` (only if `list_models` references the catalog). ## Goal Update `ModelCatalog.build` and `ModelCatalog.build_from_api` so they reflect MiniMax's seven supported models and capabilities (per their docs). The class shape stays the same so callers / specs keep working. MiniMax compatibility doc capability flags: - All 7 models support tool calls. - All 7 models support streaming. - Vision is NOT supported in any model (image / document content blocks are explicitly listed as unsupported). - `premium_request_multiplier` is `nil` (Token Plan is request-quota, not multiplier-weighted). ## Steps ### 1. Update `lib/dispatch/adapter/minimax/model_catalog.rb` Two changes: - Set `supports_vision: false` (MiniMax does not accept image blocks). - Drop the `"(unrated) #{display_name}"` formatting in `build_from_api`. With every model in the bundled pricing table, the `nil` branch is unreachable in practice, but keep the guard: `display_name` should be returned verbatim. Unknown runtime models should still produce a usable `ModelInfo`. Final shape: ```ruby # frozen_string_literal: true module Dispatch module Adapter class MiniMax < Base module ModelCatalog DEFAULT_CONTEXT_WINDOW = 204_800 module_function # Build a ModelInfo for a known MiniMax model id. # # @param id [String] # @return [Dispatch::Adapter::ModelInfo] def build(id) ModelInfo.new( id: id, name: id, max_context_tokens: PricingTable.context_window(id) || DEFAULT_CONTEXT_WINDOW, supports_vision: false, supports_tool_use: true, supports_streaming: true, premium_request_multiplier: nil, pricing: PricingTable.lookup(id) ) end # Build a ModelInfo from a runtime API entry (from GET /v1/models). # # If the model id is in the bundled pricing table, the bundled # context window is used. Display name from the API entry takes # precedence over the id. # # @param api_entry [Hash] # @return [Dispatch::Adapter::ModelInfo] def build_from_api(api_entry) id = api_entry["id"].to_s display_name = api_entry["display_name"].to_s display_name = id if display_name.empty? pricing = PricingTable.lookup(id) context_win = PricingTable.context_window(id) || DEFAULT_CONTEXT_WINDOW ModelInfo.new( id: id, name: display_name, max_context_tokens: context_win, supports_vision: false, supports_tool_use: true, supports_streaming: true, premium_request_multiplier: nil, pricing: pricing ) end end end end end ``` ### 2. Sanity-check `list_models` Find `def list_models` in `lib/dispatch/adapter/minimax.rb`. It probably calls `ModelCatalog.build_from_api(entry)` for each runtime entry, then merges with the hardcoded catalog. Confirm: - The runtime fetch hits `MODELS_PATH` (`/v1/models`). - On any error (404, parse failure, network), it falls back to building `ModelInfo` for each id from `PricingTable.known_ids` via `ModelCatalog.build`. - The result is a deduplicated array (by `id`). - Caching (`@models_cache`, `@models_cache_at`) is intact. If MiniMax doesn't expose `/v1/models` (we don't know yet), the runtime branch will return `nil` / `[]` and the hardcoded catalog will be used. Both outcomes are acceptable. DO NOT add live network smoke testing here; live testing is post-plan. ## Acceptance criteria - `ModelCatalog.build("MiniMax-M2.7")` returns a `ModelInfo` with: - `id: "MiniMax-M2.7"` - `max_context_tokens: 204_800` - `supports_vision: false` - `supports_tool_use: true` - `supports_streaming: true` - `pricing` with all four `*_per_mtok` fields zero. - `ModelCatalog.build("not-a-real-model")` returns a `ModelInfo` with `pricing: nil` and `max_context_tokens: 204_800` (the default). - `bundle exec rubocop --autocorrect-all` exits 0. ## Verification Run `run_tests` with `project_path=reference/dispatch-adapter-minimax`. Rubocop must be clean. `model_catalog_spec.rb` failures are acceptable (retargeted in phase 16). Any failure outside that file must be fixed before calling `ask_for_next_plan`.