summaryrefslogtreecommitdiffhomepage
path: root/.rules/plan/07-15-model-catalog.md
blob: c5a65b0e7939dc38553e2460edfc218f3c49f69a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# 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`.