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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# Phase 03 — KeyStore
**Estimated time:** ~15 minutes
**Touches:** `lib/dispatch/adapter/minimax/key_store.rb` (NEW),
`lib/dispatch/adapter/minimax/token_store.rb` (DELETE),
`lib/dispatch/adapter/minimax.rb`, `spec/dispatch/adapter/minimax/key_store_spec.rb` (NEW),
`spec/dispatch/adapter/minimax/token_store_spec.rb` (DELETE).
## Goal
Replace the OAuth `TokenStore` (which managed a JSON blob with access/refresh
tokens) with a tiny `KeyStore` that handles a single static API key.
API key resolution order, top to bottom:
1. Explicit `api_key:` constructor argument.
2. `ENV["MINIMAX_API_KEY"]`.
3. File `~/.config/dispatch/minimax_api_key` — plain UTF-8 text, key on
the first line, file mode `0600`. Path is overridable via constructor
keyword `key_path:` (used by tests).
4. Raise `ArgumentError, "MiniMax API key not found"`.
## Steps
### 1. Delete the existing `token_store.rb` and its spec
```text
DELETE lib/dispatch/adapter/minimax/token_store.rb
DELETE spec/dispatch/adapter/minimax/token_store_spec.rb
```
### 2. Create `lib/dispatch/adapter/minimax/key_store.rb`
```ruby
# frozen_string_literal: true
require "fileutils"
module Dispatch
module Adapter
class MiniMax < Base
# Resolves the MiniMax API key from (in order):
# 1. an explicit `api_key:` argument
# 2. ENV["MINIMAX_API_KEY"]
# 3. the file at `key_path` (default ~/.config/dispatch/minimax_api_key)
#
# Raises ArgumentError if no key can be found.
class KeyStore
DEFAULT_PATH = File.join(Dir.home, ".config", "dispatch", "minimax_api_key")
def initialize(key_path: DEFAULT_PATH)
@key_path = key_path
end
attr_reader :key_path
# @param explicit [String, nil] caller-supplied key (highest priority)
# @return [String] the resolved key
# @raise [ArgumentError] if no key is found anywhere
def load(explicit: nil)
return explicit.to_s if explicit.is_a?(String) && !explicit.empty?
env = ENV["MINIMAX_API_KEY"].to_s
return env unless env.empty?
if File.file?(@key_path)
text = File.read(@key_path, encoding: "UTF-8").to_s
line = text.lines.first.to_s.strip
return line unless line.empty?
end
raise ArgumentError,
"MiniMax API key not found. Set MINIMAX_API_KEY, " \
"write the key to #{@key_path}, or pass api_key: to the constructor."
end
# Persist a key to disk with mode 0600. Parent dir is created if missing.
# @param value [String]
# @return [String] the stored key
def save(value)
FileUtils.mkdir_p(File.dirname(@key_path))
File.write(@key_path, "#{value.to_s.strip}\n", encoding: "UTF-8")
File.chmod(0o600, @key_path)
value
end
# Remove the on-disk key (no-op if absent).
def delete
File.delete(@key_path) if File.file?(@key_path)
end
end
end
end
end
```
### 3. Wire `KeyStore` into the main adapter file
In `lib/dispatch/adapter/minimax.rb`:
- Add `require_relative "minimax/key_store"` near the other requires (in
alphabetical order with the rest).
- Update the constructor signature to:
```ruby
def initialize(
model: DEFAULT_MODEL,
api_key: nil,
key_path: nil,
base_url: DEFAULT_BASE_URL,
max_tokens: nil,
thinking: "high",
cache_retention: nil,
min_request_interval: DEFAULT_MIN_REQUEST_INTERVAL,
rate_limit: nil,
extra_betas: []
)
```
- Inside the body, replace the `@api_key.empty?` raise with:
```ruby
store = KeyStore.new(key_path: key_path || KeyStore::DEFAULT_PATH)
@api_key = store.load(explicit: api_key)
```
- Keep `@api_key` as an instance variable; do NOT store the `KeyStore`
instance — the gem only ever reads the key once at construction.
### 4. Create `spec/dispatch/adapter/minimax/key_store_spec.rb`
Cover at minimum:
- `#load` returns the explicit argument when given.
- `#load` returns `ENV["MINIMAX_API_KEY"]` when set and explicit is nil.
- `#load` reads the first line of the file when present and env is unset.
- `#load` strips trailing whitespace / newlines.
- `#load` raises `ArgumentError` when nothing is available.
- `#save` writes the key, sets mode `0600`, and creates the parent dir.
- `#delete` removes the file if present and is a no-op otherwise.
Use `Dir.mktmpdir` for `key_path:` and stub `ENV` with `ClimateControl` …
EXCEPT: do NOT add a new gem. Use `ENV.fetch` / `ENV.delete` and reset in
an `after` block, or stub directly with `allow(ENV).to receive(:[]).with("MINIMAX_API_KEY").and_return(...)`.
## Acceptance criteria
- `lib/dispatch/adapter/minimax/token_store.rb` does NOT exist.
- `spec/dispatch/adapter/minimax/token_store_spec.rb` does NOT exist.
- `lib/dispatch/adapter/minimax/key_store.rb` exists.
- `spec/dispatch/adapter/minimax/key_store_spec.rb` exists.
- `grep -rn 'TokenStore' lib/ spec/` returns ZERO matches.
- The constructor accepts `api_key:` and `key_path:` kwargs and resolves
in the documented order.
- `bundle exec rubocop --autocorrect-all` exits 0 (run via `run_tests`).
- `bundle exec rspec spec/dispatch/adapter/minimax/key_store_spec.rb`
passes.
Other specs may still fail; that is expected and will be addressed in
later phases.
## Verification
Run `run_tests` with `project_path=reference/dispatch-adapter-minimax`.
Resolve every rubocop offense before calling `ask_for_next_plan`. The
`key_store_spec.rb` examples must all pass. Other rspec failures from
not-yet-completed phases are acceptable as long as they are NOT
`LoadError` / `NameError` referencing `TokenStore` or `KeyStore`.
|