mod8-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/bin/mod8.js +2 -0
- package/dist/cli.js +302 -0
- package/dist/commands/addProvider.js +105 -0
- package/dist/commands/all.js +158 -0
- package/dist/commands/chat.js +855 -0
- package/dist/commands/config.js +29 -0
- package/dist/commands/devAuthStatus.js +34 -0
- package/dist/commands/devHostAsk.js +51 -0
- package/dist/commands/devHostSystem.js +15 -0
- package/dist/commands/devResolve.js +54 -0
- package/dist/commands/devSimulate.js +235 -0
- package/dist/commands/devWorkAsk.js +55 -0
- package/dist/commands/intentRouting.js +280 -0
- package/dist/commands/keys.js +55 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/login.js +147 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/prompt.js +63 -0
- package/dist/commands/providers.js +30 -0
- package/dist/commands/verify.js +5 -0
- package/dist/input/compose.js +37 -0
- package/dist/input/files.js +49 -0
- package/dist/input/stdin.js +14 -0
- package/dist/providers/anthropic.js +115 -0
- package/dist/providers/displayName.js +25 -0
- package/dist/providers/errorHints.js +175 -0
- package/dist/providers/generic.js +331 -0
- package/dist/providers/genericChat.js +265 -0
- package/dist/providers/google.js +63 -0
- package/dist/providers/hostSystem.js +173 -0
- package/dist/providers/index.js +38 -0
- package/dist/providers/mock.js +87 -0
- package/dist/providers/modelResolution.js +42 -0
- package/dist/providers/openai.js +75 -0
- package/dist/providers/pricing.js +47 -0
- package/dist/providers/proxy.js +148 -0
- package/dist/providers/registry.js +196 -0
- package/dist/providers/types.js +1 -0
- package/dist/providers/workSystem.js +33 -0
- package/dist/storage/auth.js +65 -0
- package/dist/storage/config.js +35 -0
- package/dist/storage/keys.js +59 -0
- package/dist/storage/providers.js +337 -0
- package/dist/storage/sessions.js +150 -0
- package/dist/types.js +9 -0
- package/dist/util/debug.js +79 -0
- package/dist/util/errors.js +157 -0
- package/dist/util/prompt.js +111 -0
- package/dist/util/secrets.js +110 -0
- package/dist/util/text.js +53 -0
- package/dist/util/time.js +25 -0
- package/dist/verify/runner.js +437 -0
- package/package.json +69 -0
- package/specs/all-mode.yaml +44 -0
- package/specs/behavior/auto-fallback.yaml +49 -0
- package/specs/behavior/bare-name-routing.yaml +223 -0
- package/specs/behavior/bare-paste-confirm.yaml +125 -0
- package/specs/behavior/env-var-respected.yaml +108 -0
- package/specs/behavior/error-fidelity.yaml +92 -0
- package/specs/behavior/error-hints.yaml +160 -0
- package/specs/behavior/fresh-vs-resume.yaml +94 -0
- package/specs/behavior/fuzzy-match.yaml +208 -0
- package/specs/behavior/host-self-knowledge-fresh.yaml +66 -0
- package/specs/behavior/intent-no-mismatch.yaml +115 -0
- package/specs/behavior/login-logout.yaml +97 -0
- package/specs/behavior/no-model-allowlist.yaml +80 -0
- package/specs/behavior/paste-key.yaml +342 -0
- package/specs/behavior/provider-switching.yaml +186 -0
- package/specs/behavior/providers-json-respected.yaml +106 -0
- package/specs/behavior/self-knowledge.yaml +119 -0
- package/specs/behavior/stress-session.yaml +226 -0
- package/specs/behavior/switch-back-when-failing.yaml +90 -0
- package/specs/behavior/work-character.yaml +109 -0
- package/specs/chat-meta.yaml +349 -0
- package/specs/chat-startup.yaml +148 -0
- package/specs/chat.yaml +91 -0
- package/specs/config.yaml +42 -0
- package/specs/install.yaml +112 -0
- package/specs/keys.yaml +81 -0
- package/specs/one-shot.yaml +65 -0
- package/specs/pipe-and-files.yaml +40 -0
- package/specs/providers.yaml +172 -0
- package/specs/sessions.yaml +115 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
name: behavior — bare name & greeting routing
|
|
2
|
+
description: |
|
|
3
|
+
Pins the fix for "user names a provider but mod8 only lectures" — the
|
|
4
|
+
intent router now catches:
|
|
5
|
+
- bare provider name ("codex")
|
|
6
|
+
- first word + remainder ("codex tell me a joke")
|
|
7
|
+
- greeting + name ("hi codex", "hey gpt how are you")
|
|
8
|
+
…all locally, before the host LLM gets a chance to lecture.
|
|
9
|
+
|
|
10
|
+
Strict resolution (id or configured display-name only) for bare/first-word
|
|
11
|
+
prevents false positives like "haiku" or "claude alone is great". Greeting
|
|
12
|
+
matching is more permissive because the greeting itself signals intent.
|
|
13
|
+
|
|
14
|
+
tests:
|
|
15
|
+
# --- bare name ---
|
|
16
|
+
|
|
17
|
+
- name: 'bare "codex" (configured display name) routes to openai'
|
|
18
|
+
setup:
|
|
19
|
+
- shell: |
|
|
20
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
21
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
22
|
+
{
|
|
23
|
+
"openai": {
|
|
24
|
+
"apiKey": "sk-fake",
|
|
25
|
+
"apiType": "openai-compat",
|
|
26
|
+
"name": "codex",
|
|
27
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
28
|
+
"defaultModel": "gpt-4o",
|
|
29
|
+
"color": "#10B981"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
JSON
|
|
33
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
34
|
+
shell: 'mod8 dev:resolve "codex"'
|
|
35
|
+
expect:
|
|
36
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
37
|
+
|
|
38
|
+
- name: 'bare "Codex" (capitalized) is case-insensitive'
|
|
39
|
+
setup:
|
|
40
|
+
- shell: |
|
|
41
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
42
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
43
|
+
{
|
|
44
|
+
"openai": {
|
|
45
|
+
"apiKey": "sk-fake",
|
|
46
|
+
"apiType": "openai-compat",
|
|
47
|
+
"name": "codex",
|
|
48
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
49
|
+
"defaultModel": "gpt-4o",
|
|
50
|
+
"color": "#10B981"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
JSON
|
|
54
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
55
|
+
shell: 'mod8 dev:resolve "Codex"'
|
|
56
|
+
expect:
|
|
57
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
58
|
+
|
|
59
|
+
- name: 'bare built-in id "deepseek" routes (even when not configured)'
|
|
60
|
+
shell: 'mod8 dev:resolve "deepseek"'
|
|
61
|
+
expect:
|
|
62
|
+
stdout_contains: "route id=deepseek resolved=deepseek"
|
|
63
|
+
|
|
64
|
+
# --- first-word + rest ---
|
|
65
|
+
|
|
66
|
+
- name: '"codex tell me a joke" routes + carries rest="tell me a joke"'
|
|
67
|
+
setup:
|
|
68
|
+
- shell: |
|
|
69
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
70
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
71
|
+
{
|
|
72
|
+
"openai": {
|
|
73
|
+
"apiKey": "sk-fake",
|
|
74
|
+
"apiType": "openai-compat",
|
|
75
|
+
"name": "codex",
|
|
76
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
77
|
+
"defaultModel": "gpt-4o",
|
|
78
|
+
"color": "#10B981"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
JSON
|
|
82
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
83
|
+
shell: 'mod8 dev:resolve "codex tell me a joke"'
|
|
84
|
+
expect:
|
|
85
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
86
|
+
stdout_matches: 'rest="tell me a joke"'
|
|
87
|
+
|
|
88
|
+
- name: '"codex codex" switches once, sends "codex" as rest (no double-route)'
|
|
89
|
+
setup:
|
|
90
|
+
- shell: |
|
|
91
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
92
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
93
|
+
{
|
|
94
|
+
"openai": {
|
|
95
|
+
"apiKey": "sk-fake",
|
|
96
|
+
"apiType": "openai-compat",
|
|
97
|
+
"name": "codex",
|
|
98
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
99
|
+
"defaultModel": "gpt-4o",
|
|
100
|
+
"color": "#10B981"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
JSON
|
|
104
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
105
|
+
shell: 'mod8 dev:resolve "codex codex"'
|
|
106
|
+
expect:
|
|
107
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
108
|
+
stdout_matches: 'rest="codex"'
|
|
109
|
+
|
|
110
|
+
# --- greeting + name (full resolution including synonyms) ---
|
|
111
|
+
|
|
112
|
+
- name: '"hi codex" routes + forwards "hi" as the first message'
|
|
113
|
+
setup:
|
|
114
|
+
- shell: |
|
|
115
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
116
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
117
|
+
{
|
|
118
|
+
"openai": {
|
|
119
|
+
"apiKey": "sk-fake",
|
|
120
|
+
"apiType": "openai-compat",
|
|
121
|
+
"name": "codex",
|
|
122
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
123
|
+
"defaultModel": "gpt-4o",
|
|
124
|
+
"color": "#10B981"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
JSON
|
|
128
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
129
|
+
shell: 'mod8 dev:resolve "hi codex"'
|
|
130
|
+
expect:
|
|
131
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
132
|
+
stdout_matches: 'rest="hi"'
|
|
133
|
+
|
|
134
|
+
- name: '"hey codex how are you" routes + forwards "hey how are you"'
|
|
135
|
+
setup:
|
|
136
|
+
- shell: |
|
|
137
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
138
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
139
|
+
{
|
|
140
|
+
"openai": {
|
|
141
|
+
"apiKey": "sk-fake",
|
|
142
|
+
"apiType": "openai-compat",
|
|
143
|
+
"name": "codex",
|
|
144
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
145
|
+
"defaultModel": "gpt-4o",
|
|
146
|
+
"color": "#10B981"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
JSON
|
|
150
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
151
|
+
shell: 'mod8 dev:resolve "hey codex how are you"'
|
|
152
|
+
expect:
|
|
153
|
+
stdout_contains: "route id=codex resolved=openai"
|
|
154
|
+
stdout_matches: 'rest="hey how are you"'
|
|
155
|
+
|
|
156
|
+
- name: '"yo gpt" routes via greeting + synonym (gpt → openai)'
|
|
157
|
+
shell: 'mod8 dev:resolve "yo gpt"'
|
|
158
|
+
expect:
|
|
159
|
+
stdout_contains: "route id=gpt resolved=openai"
|
|
160
|
+
stdout_matches: 'rest="yo"'
|
|
161
|
+
|
|
162
|
+
- name: '"hello claude" routes via greeting + synonym (claude → anthropic)'
|
|
163
|
+
shell: 'mod8 dev:resolve "hello claude"'
|
|
164
|
+
expect:
|
|
165
|
+
stdout_contains: "route id=claude resolved=anthropic"
|
|
166
|
+
|
|
167
|
+
# --- false-positive avoidance ---
|
|
168
|
+
|
|
169
|
+
- name: 'bare "haiku" does NOT route (synonym, but bare-mode is strict)'
|
|
170
|
+
shell: 'mod8 dev:resolve "haiku"'
|
|
171
|
+
expect:
|
|
172
|
+
stdout_contains: "none"
|
|
173
|
+
|
|
174
|
+
- name: 'bare "claude" routes to anthropic (high-confidence brand alias)'
|
|
175
|
+
shell: 'mod8 dev:resolve "claude"'
|
|
176
|
+
expect:
|
|
177
|
+
stdout_contains: "route id=claude resolved=anthropic"
|
|
178
|
+
|
|
179
|
+
- name: 'bare "gpt" routes to openai (high-confidence brand alias)'
|
|
180
|
+
shell: 'mod8 dev:resolve "gpt"'
|
|
181
|
+
expect:
|
|
182
|
+
stdout_contains: "route id=gpt resolved=openai"
|
|
183
|
+
|
|
184
|
+
- name: 'bare "grok" routes to xai (high-confidence brand alias)'
|
|
185
|
+
shell: 'mod8 dev:resolve "grok"'
|
|
186
|
+
expect:
|
|
187
|
+
stdout_contains: "route id=grok resolved=xai"
|
|
188
|
+
|
|
189
|
+
- name: '"claude please" routes + carries rest="please"'
|
|
190
|
+
shell: 'mod8 dev:resolve "claude please"'
|
|
191
|
+
expect:
|
|
192
|
+
stdout_contains: "route id=claude resolved=anthropic"
|
|
193
|
+
stdout_matches: 'rest="please"'
|
|
194
|
+
|
|
195
|
+
- name: '"gpt help me with this" routes + carries rest'
|
|
196
|
+
shell: 'mod8 dev:resolve "gpt help me with this"'
|
|
197
|
+
expect:
|
|
198
|
+
stdout_contains: "route id=gpt resolved=openai"
|
|
199
|
+
stdout_matches: 'rest="help me with this"'
|
|
200
|
+
|
|
201
|
+
- name: '"hello world" does NOT route ("world" is not a provider)'
|
|
202
|
+
shell: 'mod8 dev:resolve "hello world"'
|
|
203
|
+
expect:
|
|
204
|
+
stdout_contains: "none"
|
|
205
|
+
|
|
206
|
+
- name: '"hi there" does NOT route ("there" is not a provider)'
|
|
207
|
+
shell: 'mod8 dev:resolve "hi there"'
|
|
208
|
+
expect:
|
|
209
|
+
stdout_contains: "none"
|
|
210
|
+
|
|
211
|
+
- name: 'multi-word non-route message stays plain ("write me a haiku")'
|
|
212
|
+
shell: 'mod8 dev:resolve "write me a haiku"'
|
|
213
|
+
expect:
|
|
214
|
+
stdout_contains: "none"
|
|
215
|
+
|
|
216
|
+
# --- priority: host-back still wins over bare-name ---
|
|
217
|
+
|
|
218
|
+
- name: 'bare "mod8" goes to host-back, not provider routing'
|
|
219
|
+
shell: 'mod8 dev:resolve "mod8"'
|
|
220
|
+
expect:
|
|
221
|
+
stdout_contains: "host-back"
|
|
222
|
+
stdout_omits:
|
|
223
|
+
- "route id="
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
name: behavior — bare-paste auto-detect (no consent first)
|
|
2
|
+
description: |
|
|
3
|
+
When the user pastes a recognizable API key into chat WITHOUT first saying
|
|
4
|
+
"add a key", mod8 should NOT pass the message to the host LLM (which then
|
|
5
|
+
lectures about `mod8 keys set <id>`). Instead:
|
|
6
|
+
|
|
7
|
+
1. Detect the key locally (sk-ant-/sk-or-/AIza/etc.).
|
|
8
|
+
2. Mask it for display.
|
|
9
|
+
3. Cache the raw key in memory only (NEVER session JSON).
|
|
10
|
+
4. Ask "Save it as `<id>`? (yes / no)".
|
|
11
|
+
5. Affirmative → save to providers.json, confirm.
|
|
12
|
+
Negative → discard, acknowledge.
|
|
13
|
+
Anything else → discard, fall through to normal dispatch (no trap).
|
|
14
|
+
|
|
15
|
+
Specs cover the happy path, the broadened paste-key intent regex (which
|
|
16
|
+
now accepts "this/that/these/those/it/them" as articles and the bare-
|
|
17
|
+
pronoun forms "save this", "use it", etc.), and the cancellation paths.
|
|
18
|
+
|
|
19
|
+
tests:
|
|
20
|
+
- name: 'bare paste of anthropic key → asks "save as anthropic?"'
|
|
21
|
+
shell: |
|
|
22
|
+
mod8 dev:simulate <<'INPUTS'
|
|
23
|
+
sk-ant-api03-FAKE-TEST-AAAAAAAAAAAAAAAAAAAAAAAA
|
|
24
|
+
yes
|
|
25
|
+
INPUTS
|
|
26
|
+
echo "---providers.json---"
|
|
27
|
+
cat $MOD8_CONFIG_DIR/providers.json
|
|
28
|
+
expect:
|
|
29
|
+
stdout_contains:
|
|
30
|
+
# Step 1: bare paste detected, key masked in stdout, pending state armed.
|
|
31
|
+
- 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
|
|
32
|
+
# Step 2: user confirms with "yes" → key is saved.
|
|
33
|
+
- 'step=2 input="yes" mode=host provider=host action=paste-saved id=anthropic'
|
|
34
|
+
# And the full key DOES land in providers.json.
|
|
35
|
+
- '"apiKey": "sk-ant-api03-FAKE-TEST-AAAAAAAAAAAAAAAAAAAAAAAA"'
|
|
36
|
+
stdout_omits:
|
|
37
|
+
# The raw key must never appear in stdout (only providers.json).
|
|
38
|
+
- 'step=1 input="sk-ant-api03-FAKE'
|
|
39
|
+
|
|
40
|
+
- name: '"save it" / "save this" / "use it" all trigger save in pendingKey state'
|
|
41
|
+
shell: |
|
|
42
|
+
mod8 dev:simulate <<'INPUTS'
|
|
43
|
+
sk-ant-api03-FAKE-KEY-1-AAAAAAAAAAAAAAAAAAAA
|
|
44
|
+
save it
|
|
45
|
+
INPUTS
|
|
46
|
+
echo "---providers.json---"
|
|
47
|
+
cat $MOD8_CONFIG_DIR/providers.json
|
|
48
|
+
expect:
|
|
49
|
+
stdout_contains:
|
|
50
|
+
- 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
|
|
51
|
+
- 'step=2 input="save it" mode=host provider=host action=paste-saved id=anthropic'
|
|
52
|
+
- '"apiKey": "sk-ant-api03-FAKE-KEY-1-AAAAAAAAAAAAAAAAAAAA"'
|
|
53
|
+
|
|
54
|
+
- name: 'broadened intent — "add this key!" triggers consent (was rejected before)'
|
|
55
|
+
shell: |
|
|
56
|
+
mod8 dev:simulate <<'INPUTS'
|
|
57
|
+
add this key!
|
|
58
|
+
INPUTS
|
|
59
|
+
expect:
|
|
60
|
+
stdout_contains:
|
|
61
|
+
- 'step=1 input="add this key!" mode=host provider=host action=paste-consent'
|
|
62
|
+
|
|
63
|
+
- name: '"no" cancels the bare paste and discards the key'
|
|
64
|
+
shell: |
|
|
65
|
+
mod8 dev:simulate <<'INPUTS'
|
|
66
|
+
AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA
|
|
67
|
+
no
|
|
68
|
+
INPUTS
|
|
69
|
+
echo "---providers.json---"
|
|
70
|
+
cat $MOD8_CONFIG_DIR/providers.json 2>/dev/null || echo "no providers.json"
|
|
71
|
+
expect:
|
|
72
|
+
stdout_contains:
|
|
73
|
+
- 'step=1 input="AIzaSy…AAAA" mode=host provider=host action=paste-pending id=google'
|
|
74
|
+
- 'step=2 input="no" mode=host provider=host action=paste-cancelled'
|
|
75
|
+
- 'no providers.json'
|
|
76
|
+
|
|
77
|
+
- name: 'unrelated input cancels paste and falls through (no trap)'
|
|
78
|
+
setup:
|
|
79
|
+
- shell: |
|
|
80
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
81
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
82
|
+
{
|
|
83
|
+
"openai": {"apiKey":"x","apiType":"openai-compat","name":"codex","baseUrl":"https://api.openai.com/v1","defaultModel":"gpt-4o","color":"#10B981"}
|
|
84
|
+
}
|
|
85
|
+
JSON
|
|
86
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
87
|
+
shell: |
|
|
88
|
+
mod8 dev:simulate <<'INPUTS'
|
|
89
|
+
sk-ant-api03-FAKE-2-AAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
90
|
+
use codex
|
|
91
|
+
INPUTS
|
|
92
|
+
expect:
|
|
93
|
+
stdout_contains:
|
|
94
|
+
- 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
|
|
95
|
+
# "use codex" is NOT an affirmative → cancel + fall through to normal
|
|
96
|
+
# routing (which switches to codex).
|
|
97
|
+
- 'step=2 input="use codex" mode=work provider=openai action=route'
|
|
98
|
+
|
|
99
|
+
- name: 'broadened article list — "let me add this anthropic key" triggers consent'
|
|
100
|
+
shell: |
|
|
101
|
+
mod8 dev:simulate <<'INPUTS'
|
|
102
|
+
let me add this anthropic key
|
|
103
|
+
INPUTS
|
|
104
|
+
expect:
|
|
105
|
+
stdout_contains:
|
|
106
|
+
- 'step=1 input="let me add this anthropic key" mode=host provider=host action=paste-consent'
|
|
107
|
+
|
|
108
|
+
- name: 'multiple bare pastes back-to-back each get saved'
|
|
109
|
+
shell: |
|
|
110
|
+
mod8 dev:simulate <<'INPUTS'
|
|
111
|
+
sk-ant-api03-CLAUDE-FAKE-AAAAAAAAAAAAAAAAAAAAAAAA
|
|
112
|
+
yes
|
|
113
|
+
AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA
|
|
114
|
+
save it
|
|
115
|
+
INPUTS
|
|
116
|
+
echo "---providers.json---"
|
|
117
|
+
cat $MOD8_CONFIG_DIR/providers.json
|
|
118
|
+
expect:
|
|
119
|
+
stdout_contains:
|
|
120
|
+
- 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
|
|
121
|
+
- 'step=2 input="yes" mode=host provider=host action=paste-saved id=anthropic'
|
|
122
|
+
- 'step=3 input="AIzaSy…AAAA" mode=host provider=host action=paste-pending id=google'
|
|
123
|
+
- 'step=4 input="save it" mode=host provider=host action=paste-saved id=google'
|
|
124
|
+
- '"apiKey": "sk-ant-api03-CLAUDE-FAKE-AAAAAAAAAAAAAAAAAAAAAAAA"'
|
|
125
|
+
- '"apiKey": "AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA"'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
name: behavior — MOD8_<ID>_MODEL env var is respected
|
|
2
|
+
description: |
|
|
3
|
+
Bug: the env var advertised in the "model not available" error message
|
|
4
|
+
was silently ignored. The dead code in src/providers/google.ts honored
|
|
5
|
+
MOD8_GOOGLE_MODEL but no one imports it; the live path in generic.ts
|
|
6
|
+
used `opts.model ?? entry.defaultModel` and never read the env.
|
|
7
|
+
|
|
8
|
+
Fix: every provider call goes through `resolveModel()` which checks
|
|
9
|
+
opts.model → MOD8_<ID>_MODEL → providers.json defaultModel. These specs
|
|
10
|
+
prove the env var beats the providers.json default (no allowlist is
|
|
11
|
+
consulted along the way; whatever string the env sets is what the SDK
|
|
12
|
+
receives).
|
|
13
|
+
|
|
14
|
+
tests:
|
|
15
|
+
- name: 'MOD8_GOOGLE_MODEL beats providers.json default'
|
|
16
|
+
setup:
|
|
17
|
+
- shell: |
|
|
18
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
19
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
20
|
+
{
|
|
21
|
+
"google": {
|
|
22
|
+
"apiKey": "x",
|
|
23
|
+
"apiType": "gemini",
|
|
24
|
+
"name": "Google (Gemini)",
|
|
25
|
+
"defaultModel": "gemini-2.0-flash",
|
|
26
|
+
"color": "#06B6D4"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
JSON
|
|
30
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
31
|
+
shell: 'MOD8_GOOGLE_MODEL=gemini-2.5-flash mod8 dev:resolve-model google'
|
|
32
|
+
expect:
|
|
33
|
+
stdout_contains:
|
|
34
|
+
- 'model="gemini-2.5-flash"'
|
|
35
|
+
- 'source=env'
|
|
36
|
+
- 'envVar=MOD8_GOOGLE_MODEL'
|
|
37
|
+
|
|
38
|
+
- name: 'env var passes through unchanged — even an arbitrary string'
|
|
39
|
+
setup:
|
|
40
|
+
- shell: |
|
|
41
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
42
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
43
|
+
{
|
|
44
|
+
"google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"x","color":"#06B6D4"}
|
|
45
|
+
}
|
|
46
|
+
JSON
|
|
47
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
48
|
+
shell: 'MOD8_GOOGLE_MODEL=this-could-be-any-string-mod8-does-not-allowlist mod8 dev:resolve-model google'
|
|
49
|
+
expect:
|
|
50
|
+
stdout_contains:
|
|
51
|
+
- 'model="this-could-be-any-string-mod8-does-not-allowlist"'
|
|
52
|
+
- 'source=env'
|
|
53
|
+
|
|
54
|
+
- name: 'env var name is built from id — works for non-google providers'
|
|
55
|
+
setup:
|
|
56
|
+
- shell: |
|
|
57
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
58
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
59
|
+
{
|
|
60
|
+
"anthropic": {"apiKey":"x","apiType":"anthropic","name":"Anthropic","defaultModel":"claude-sonnet-4-6","color":"#A78BFA"}
|
|
61
|
+
}
|
|
62
|
+
JSON
|
|
63
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
64
|
+
shell: 'MOD8_ANTHROPIC_MODEL=claude-opus-4-7 mod8 dev:resolve-model anthropic'
|
|
65
|
+
expect:
|
|
66
|
+
stdout_contains:
|
|
67
|
+
- 'model="claude-opus-4-7"'
|
|
68
|
+
- 'source=env'
|
|
69
|
+
- 'envVar=MOD8_ANTHROPIC_MODEL'
|
|
70
|
+
|
|
71
|
+
- name: 'empty env var is ignored, falls through to providers.json'
|
|
72
|
+
setup:
|
|
73
|
+
- shell: |
|
|
74
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
75
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
76
|
+
{
|
|
77
|
+
"google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.0-flash","color":"#06B6D4"}
|
|
78
|
+
}
|
|
79
|
+
JSON
|
|
80
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
81
|
+
shell: 'MOD8_GOOGLE_MODEL= mod8 dev:resolve-model google'
|
|
82
|
+
expect:
|
|
83
|
+
stdout_contains:
|
|
84
|
+
- 'model="gemini-2.0-flash"'
|
|
85
|
+
- 'source=providers.json'
|
|
86
|
+
|
|
87
|
+
- name: 'env-resolved model lands in the SDK URL the provider is about to hit'
|
|
88
|
+
setup:
|
|
89
|
+
- shell: |
|
|
90
|
+
mkdir -p $MOD8_CONFIG_DIR
|
|
91
|
+
cat > $MOD8_CONFIG_DIR/providers.json <<JSON
|
|
92
|
+
{
|
|
93
|
+
"google": {"apiKey":"AIzaSy-FAKE-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAAA","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.0-flash","color":"#06B6D4"}
|
|
94
|
+
}
|
|
95
|
+
JSON
|
|
96
|
+
chmod 600 $MOD8_CONFIG_DIR/providers.json
|
|
97
|
+
# No network — dev:debug-call runs the same resolution + URL builder as
|
|
98
|
+
# the actual provider call path. The URL we print here is identical
|
|
99
|
+
# to what the SDK will request.
|
|
100
|
+
shell: 'MOD8_GOOGLE_MODEL=gemini-2.5-flash mod8 dev:debug-call google'
|
|
101
|
+
expect:
|
|
102
|
+
stdout_contains:
|
|
103
|
+
- 'providerId=google'
|
|
104
|
+
- 'model="gemini-2.5-flash"'
|
|
105
|
+
- 'modelSource=env'
|
|
106
|
+
- 'models/gemini-2.5-flash:streamGenerateContent'
|
|
107
|
+
# API key is masked — never the raw value.
|
|
108
|
+
- 'key=AIzaSy…AAAA'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: behavior — provider error message fidelity (verbatim quote)
|
|
2
|
+
description: |
|
|
3
|
+
Bug: when a provider returned a specific error, mod8 dropped the message
|
|
4
|
+
and printed something generic ("model not available — set MOD8_..." for
|
|
5
|
+
what was actually a 400 / API_KEY_INVALID; "free-tier quota / billing"
|
|
6
|
+
for what was actually a 403 project block).
|
|
7
|
+
|
|
8
|
+
Fix: every error preserves the provider's raw human-readable message in
|
|
9
|
+
the short summary, with prefix noise (`[GoogleGenerativeAI Error]:`,
|
|
10
|
+
`Error fetching from <url>:`, structured-detail JSON tail) stripped.
|
|
11
|
+
The user can copy the quoted message and search for it.
|
|
12
|
+
|
|
13
|
+
These specs feed synthetic SDK-shape errors through dev:explain-error
|
|
14
|
+
and assert the verbatim quote survives + the diagnosis matches reality.
|
|
15
|
+
|
|
16
|
+
tests:
|
|
17
|
+
- name: 'Google "no longer available" model error → kind=model + verbatim quote'
|
|
18
|
+
shell: |
|
|
19
|
+
mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent: [404 Not Found] The model gemini-pro-vision is no longer available."
|
|
20
|
+
expect:
|
|
21
|
+
stdout_contains:
|
|
22
|
+
- 'kind=model'
|
|
23
|
+
- 'no longer available'
|
|
24
|
+
# The quoted raw text — survives extractRawMessage and stays
|
|
25
|
+
# search-friendly. Must NOT include the SDK wrapper prefix.
|
|
26
|
+
- "'The model gemini-pro-vision is no longer available.'"
|
|
27
|
+
stdout_omits:
|
|
28
|
+
- 'rate limited'
|
|
29
|
+
- 'set MOD8_GOOGLE_MODEL'
|
|
30
|
+
- 'GoogleGenerativeAI Error'
|
|
31
|
+
|
|
32
|
+
- name: 'Google "API key not valid" → kind=auth (NOT kind=model)'
|
|
33
|
+
# The exact misclassification the user reported: URL contains "models/<name>"
|
|
34
|
+
# AND message contains "valid", and the old `model.*invalid` regex matched.
|
|
35
|
+
shell: |
|
|
36
|
+
mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent: [400 Bad Request] API key not valid. Please pass a valid API key."
|
|
37
|
+
expect:
|
|
38
|
+
stdout_contains:
|
|
39
|
+
- 'kind=auth'
|
|
40
|
+
- "'API key not valid. Please pass a valid API key.'"
|
|
41
|
+
stdout_omits:
|
|
42
|
+
- 'kind=model'
|
|
43
|
+
- 'set MOD8_GOOGLE_MODEL'
|
|
44
|
+
|
|
45
|
+
- name: '403 project denied → kind=forbidden + verbatim quote (no fake billing diagnosis)'
|
|
46
|
+
shell: |
|
|
47
|
+
mod8 dev:explain-error google "[403 Forbidden] Your project has been denied access. Please contact support."
|
|
48
|
+
expect:
|
|
49
|
+
stdout_contains:
|
|
50
|
+
- 'kind=forbidden'
|
|
51
|
+
- "'Your project has been denied access. Please contact support.'"
|
|
52
|
+
stdout_omits:
|
|
53
|
+
- 'kind=no-credit'
|
|
54
|
+
- 'free-tier quota'
|
|
55
|
+
- 'Top up at'
|
|
56
|
+
|
|
57
|
+
- name: '429 with retry-after preserves the delay AND quotes the raw message'
|
|
58
|
+
shell: |
|
|
59
|
+
mod8 dev:explain-error anthropic "rate_limit_exceeded: please retry after 30 seconds"
|
|
60
|
+
expect:
|
|
61
|
+
stdout_contains:
|
|
62
|
+
- 'kind=rate-limit'
|
|
63
|
+
- 'retry in 30s'
|
|
64
|
+
- "'rate_limit_exceeded: please retry after 30 seconds'"
|
|
65
|
+
|
|
66
|
+
- name: 'unknown shape → kind=other, raw preserved (no fabricated diagnosis)'
|
|
67
|
+
shell: |
|
|
68
|
+
mod8 dev:explain-error openai "Some completely surprising message we have not seen before"
|
|
69
|
+
expect:
|
|
70
|
+
stdout_contains:
|
|
71
|
+
- 'kind=other'
|
|
72
|
+
- 'Some completely surprising message we have not seen before'
|
|
73
|
+
stdout_omits:
|
|
74
|
+
- 'kind=auth'
|
|
75
|
+
- 'kind=forbidden'
|
|
76
|
+
- 'kind=rate-limit'
|
|
77
|
+
- 'kind=model'
|
|
78
|
+
|
|
79
|
+
- name: 'extractRawMessage strips Google SDK wrapper prefix'
|
|
80
|
+
# Specifically: "[GoogleGenerativeAI Error]: Error fetching from <url>: "
|
|
81
|
+
# must be removed so the user sees just the meaningful summary.
|
|
82
|
+
shell: |
|
|
83
|
+
mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent: [400 Bad Request] API key not valid. Please pass a valid API key. [{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"API_KEY_INVALID\"}]"
|
|
84
|
+
expect:
|
|
85
|
+
stdout_contains:
|
|
86
|
+
- "'API key not valid. Please pass a valid API key.'"
|
|
87
|
+
stdout_omits:
|
|
88
|
+
# The wrapper noise must NOT survive into the user-facing quote.
|
|
89
|
+
- 'GoogleGenerativeAI Error'
|
|
90
|
+
- 'Error fetching from'
|
|
91
|
+
- '@type'
|
|
92
|
+
- 'type.googleapis.com/google.rpc.ErrorInfo'
|