@diegopetrucci/pi-extensions 0.1.74 → 0.1.78
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/LICENSE +2 -0
- package/README.md +17 -12
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +1631 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +846 -768
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +6 -6
- package/extensions/code-reviewer/index.ts +1825 -1373
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +50 -52
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +313 -305
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +1863 -1737
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +37 -41
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +11 -43
- package/extensions/dynamic-context-pruning/index.ts +2131 -2105
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +52 -14
- package/extensions/fast/index.ts +513 -317
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +87 -103
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +1877 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +987 -1012
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +1740 -1627
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +785 -612
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +376 -291
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +280 -280
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +6 -4
- package/extensions/triage-comments/index.ts +2620 -1888
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +9 -6
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -3,7 +3,13 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { StringEnum, type Usage } from "@earendil-works/pi-ai";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getAgentDir,
|
|
8
|
+
getMarkdownTheme,
|
|
9
|
+
type AgentToolResult,
|
|
10
|
+
type ExtensionAPI,
|
|
11
|
+
type ExtensionContext,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
13
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
14
|
import { Type } from "typebox";
|
|
9
15
|
|
|
@@ -11,93 +17,100 @@ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "
|
|
|
11
17
|
type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
|
|
12
18
|
|
|
13
19
|
type PiModel = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
provider: string;
|
|
21
|
+
id: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
reasoning?: boolean;
|
|
24
|
+
contextWindow?: number;
|
|
25
|
+
maxTokens?: number;
|
|
26
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
type ModelSelectionContext = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
model?: PiModel;
|
|
31
|
+
modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> };
|
|
32
|
+
scopedModels?: ReadonlyArray<{ model: PiModel; thinkingLevel?: ThinkingLevel }>;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
function modelKey(model: PiModel): string {
|
|
30
|
-
|
|
36
|
+
return `${model.provider}/${model.id}`.toLowerCase();
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
function hasModelScope(ctx: ModelSelectionContext): boolean {
|
|
34
|
-
|
|
40
|
+
return Array.isArray(ctx.scopedModels) && ctx.scopedModels.length > 0;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function isModelInScope(ctx: ModelSelectionContext, model: PiModel): boolean {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
if (!hasModelScope(ctx)) return true;
|
|
45
|
+
const key = modelKey(model);
|
|
46
|
+
return ctx.scopedModels!.some((entry) => modelKey(entry.model) === key);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
async function getAutoSelectableModels(ctx: ModelSelectionContext): Promise<PiModel[]> {
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const available = await ctx.modelRegistry.getAvailable();
|
|
51
|
+
return hasModelScope(ctx) ? available.filter((model) => isModelInScope(ctx, model)) : available;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
interface UsageStats {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
input: number;
|
|
56
|
+
output: number;
|
|
57
|
+
cacheRead: number;
|
|
58
|
+
cacheWrite: number;
|
|
59
|
+
cacheWrite1h?: number;
|
|
60
|
+
reasoning?: number;
|
|
61
|
+
totalTokens: number;
|
|
62
|
+
cost: number;
|
|
63
|
+
costInput: number;
|
|
64
|
+
costOutput: number;
|
|
65
|
+
costCacheRead: number;
|
|
66
|
+
costCacheWrite: number;
|
|
67
|
+
turns: number;
|
|
68
|
+
contextTokens: number;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
interface ContrarianSelection {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
modelRef: string;
|
|
73
|
+
provider: string;
|
|
74
|
+
modelId: string;
|
|
75
|
+
modelName?: string;
|
|
76
|
+
thinkingLevel: ThinkingLevel;
|
|
77
|
+
requestedThinkingLevel?: ThinkingLevel;
|
|
78
|
+
thinkingLevelClamped?: boolean;
|
|
79
|
+
autoSelected: boolean;
|
|
80
|
+
selectionReason: string;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
interface ContrarianDetails extends ContrarianSelection {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
includeBash: boolean;
|
|
85
|
+
usage: UsageStats;
|
|
86
|
+
stderr: string;
|
|
87
|
+
exitCode: number;
|
|
88
|
+
durationMs: number;
|
|
89
|
+
cwd: string;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
interface ContrarianUiRun {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
task: string;
|
|
94
|
+
includeBash: boolean;
|
|
95
|
+
startedAt: number;
|
|
96
|
+
selection?: ContrarianSelection;
|
|
97
|
+
preview?: string;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
interface ContrarianPreferences {
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
model?: string;
|
|
102
|
+
thinkingLevel?: ThinkingLevel;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
const
|
|
100
|
-
|
|
105
|
+
const SUBAGENT_MCP_TOOLS = [
|
|
106
|
+
"mcp__*",
|
|
107
|
+
"tool_search",
|
|
108
|
+
"list_mcp_resources",
|
|
109
|
+
"list_mcp_resource_templates",
|
|
110
|
+
"read_mcp_resource",
|
|
111
|
+
];
|
|
112
|
+
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", ...SUBAGENT_MCP_TOOLS];
|
|
113
|
+
const READ_ONLY_PLUS_BASH_TOOLS = ["read", "grep", "find", "ls", "bash", ...SUBAGENT_MCP_TOOLS];
|
|
101
114
|
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "high";
|
|
102
115
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
103
116
|
const COLLAPSED_LINE_LIMIT = 8;
|
|
@@ -105,841 +118,827 @@ const CONTRARIAN_STATUS_ID = "contrarian";
|
|
|
105
118
|
const CONTRARIAN_WIDGET_ID = "contrarian";
|
|
106
119
|
const CONTRARIAN_CONFIG_FILE = "contrarian.json";
|
|
107
120
|
const CONTRARIAN_MODEL_PREFERENCES = [
|
|
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
|
-
|
|
121
|
+
"openai/gpt-6-astra ",
|
|
122
|
+
"global.openai.gpt-6-astra ",
|
|
123
|
+
"us.openai.gpt-6-astra ",
|
|
124
|
+
"openai.gpt-6-astra ",
|
|
125
|
+
"gpt-6-astra ",
|
|
126
|
+
"gpt-6-astra",
|
|
127
|
+
"openai/gpt-6.1-sol ",
|
|
128
|
+
"global.openai.gpt-6.1-sol ",
|
|
129
|
+
"us.openai.gpt-6.1-sol ",
|
|
130
|
+
"openai.gpt-6.1-sol ",
|
|
131
|
+
"gpt-6.1-sol ",
|
|
132
|
+
"openai/gpt-6-sol ",
|
|
133
|
+
"gpt-6-sol ",
|
|
134
|
+
"gpt-6-sol",
|
|
135
|
+
"gpt-5.6-sol",
|
|
136
|
+
"gpt-5.6-terra",
|
|
137
|
+
"openai/gpt-6-luna ",
|
|
138
|
+
"gpt-6-luna ",
|
|
139
|
+
"gpt-6-luna",
|
|
140
|
+
"gpt-5.6-luna",
|
|
141
|
+
"gpt-5.5",
|
|
142
|
+
"anthropic/claude-opus-5.5 ",
|
|
143
|
+
"global.anthropic.claude-opus-5-5 ",
|
|
144
|
+
"au.anthropic.claude-opus-5-5 ",
|
|
145
|
+
"eu.anthropic.claude-opus-5-5 ",
|
|
146
|
+
"jp.anthropic.claude-opus-5-5 ",
|
|
147
|
+
"us.anthropic.claude-opus-5-5 ",
|
|
148
|
+
"anthropic.claude-opus-5-5 ",
|
|
149
|
+
"claude-opus-5.5 ",
|
|
150
|
+
"claude-opus-5-5 ",
|
|
151
|
+
"claude-opus-5-5",
|
|
152
|
+
"claude-opus-5.5",
|
|
153
|
+
"claude-opus-5",
|
|
154
|
+
"claude-opus-4-8",
|
|
155
|
+
"claude-opus-4.8",
|
|
156
|
+
"claude-sonnet-5",
|
|
157
|
+
"claude-sonnet-4-6",
|
|
158
|
+
"claude-sonnet-4.6",
|
|
159
|
+
"claude-sonnet-4-5",
|
|
160
|
+
"claude-sonnet-4.5",
|
|
161
|
+
"claude-sonnet-4",
|
|
162
|
+
"x-ai/grok-4.7 ",
|
|
163
|
+
"spacexai/grok-4.7 ",
|
|
164
|
+
"grok-4.7 ",
|
|
165
|
+
"grok-4.7",
|
|
166
|
+
"grok-4.6",
|
|
167
|
+
"grok-4.5",
|
|
168
|
+
"gemini-3.5-flash",
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const AZURE_MODEL_PREFERENCES = [
|
|
172
|
+
"gpt-6-astra ",
|
|
173
|
+
"gpt-6-astra",
|
|
174
|
+
"gpt-6.1-sol ",
|
|
175
|
+
"gpt-6-sol ",
|
|
176
|
+
"gpt-6-sol",
|
|
177
|
+
"gpt-5.6-sol",
|
|
178
|
+
"gpt-5.6-terra",
|
|
179
|
+
"gpt-6-luna ",
|
|
180
|
+
"gpt-6-luna",
|
|
181
|
+
"gpt-5.6-luna",
|
|
182
|
+
"gpt-5.5-pro",
|
|
183
|
+
"gpt-5.5",
|
|
184
|
+
"gpt-5.4-pro",
|
|
185
|
+
"gpt-5.4 ",
|
|
186
|
+
"gpt-5.3-codex",
|
|
187
|
+
"gpt-5-pro",
|
|
188
|
+
"gpt-5-chat-latest",
|
|
189
|
+
"gpt-5.2-pro",
|
|
190
|
+
"gpt-5.2",
|
|
191
|
+
"gpt-5.1",
|
|
192
|
+
"o3-pro",
|
|
193
|
+
"o1-pro",
|
|
194
|
+
"gpt-5.4-mini",
|
|
195
|
+
"gpt-5-mini",
|
|
156
196
|
];
|
|
157
197
|
|
|
158
198
|
const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"alibaba/qwen3.7-plus",
|
|
647
|
-
"alibaba/qwen3.6-plus",
|
|
648
|
-
"alibaba/qwen3.5-plus",
|
|
649
|
-
"alibaba/qwen3-max-thinking",
|
|
650
|
-
"google/gemini-3.1-pro-preview",
|
|
651
|
-
"google/gemini-3.5-flash",
|
|
652
|
-
"google/gemini-3-flash",
|
|
653
|
-
"spacexai/grok-4.7 ",
|
|
654
|
-
"spacexai/grok-4.7",
|
|
655
|
-
"spacexai/grok-4.6",
|
|
656
|
-
"xai/grok-4.5",
|
|
657
|
-
"xai/grok-4.20-reasoning",
|
|
658
|
-
"minimax/minimax-m3",
|
|
659
|
-
"zai/glm-5.3",
|
|
660
|
-
"zai/glm-5.2",
|
|
661
|
-
"zai/glm-5.1",
|
|
662
|
-
],
|
|
663
|
-
xai: [
|
|
664
|
-
"grok-4.7 ",
|
|
665
|
-
"grok-4.7",
|
|
666
|
-
"grok-4.6",
|
|
667
|
-
"grok-4.5",
|
|
668
|
-
"grok-4.3",
|
|
669
|
-
],
|
|
670
|
-
xiaomi: ["mimo-v2.5-pro-ultraspeed", "mimo-v2.5-pro", "mimo-v2.5"],
|
|
671
|
-
"xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
672
|
-
"xiaomi-token-plan-cn": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
673
|
-
"xiaomi-token-plan-sgp": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
674
|
-
nvidia: [
|
|
675
|
-
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
676
|
-
"nvidia/nemotron-3-super-120b-a12b",
|
|
677
|
-
"moonshotai/kimi-k3",
|
|
678
|
-
"moonshotai/kimi-k2.6",
|
|
679
|
-
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
|
680
|
-
],
|
|
681
|
-
zai: [
|
|
682
|
-
"glm-5.3",
|
|
683
|
-
"glm-5.2",
|
|
684
|
-
"glm-5-turbo",
|
|
685
|
-
"glm-4.7",
|
|
686
|
-
],
|
|
687
|
-
"zai-coding-cn": [
|
|
688
|
-
"glm-5.3",
|
|
689
|
-
"glm-5.3-highspeed",
|
|
690
|
-
"glm-5.3-flash",
|
|
691
|
-
"glm-4.6v",
|
|
692
|
-
],
|
|
693
|
-
moonshotai: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
694
|
-
"moonshotai-cn": ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
199
|
+
"amazon-bedrock": [
|
|
200
|
+
"global.anthropic.claude-opus-5-5 ",
|
|
201
|
+
"au.anthropic.claude-opus-5-5 ",
|
|
202
|
+
"eu.anthropic.claude-opus-5-5 ",
|
|
203
|
+
"jp.anthropic.claude-opus-5-5 ",
|
|
204
|
+
"us.anthropic.claude-opus-5-5 ",
|
|
205
|
+
"anthropic.claude-opus-5-5 ",
|
|
206
|
+
"claude-opus-5-5",
|
|
207
|
+
"claude-opus-5",
|
|
208
|
+
"claude-fable-5",
|
|
209
|
+
"claude-opus-4-8",
|
|
210
|
+
"claude-opus-4-7",
|
|
211
|
+
"claude-opus-4-6",
|
|
212
|
+
"claude-opus-4-5",
|
|
213
|
+
"claude-opus-4-1",
|
|
214
|
+
"claude-opus-4",
|
|
215
|
+
"global.openai.gpt-6-astra ",
|
|
216
|
+
"us.openai.gpt-6-astra ",
|
|
217
|
+
"openai.gpt-6-astra ",
|
|
218
|
+
"gpt-6-astra",
|
|
219
|
+
"global.openai.gpt-6.1-sol ",
|
|
220
|
+
"us.openai.gpt-6.1-sol ",
|
|
221
|
+
"openai.gpt-6.1-sol ",
|
|
222
|
+
"openai.gpt-5.6-sol",
|
|
223
|
+
"openai.gpt-5.6-terra",
|
|
224
|
+
"openai.gpt-5.6-luna",
|
|
225
|
+
"claude-sonnet-5",
|
|
226
|
+
"claude-sonnet-4-6",
|
|
227
|
+
"claude-sonnet-4-5",
|
|
228
|
+
"claude-sonnet-4",
|
|
229
|
+
"deepseek.v3.2",
|
|
230
|
+
"kimi-k2.5",
|
|
231
|
+
"minimax-m2.5",
|
|
232
|
+
"minimax-m2.1",
|
|
233
|
+
"zai.glm-5",
|
|
234
|
+
"xai.grok-4.6",
|
|
235
|
+
],
|
|
236
|
+
anthropic: [
|
|
237
|
+
"claude-opus-5-5 ",
|
|
238
|
+
"claude-opus-5-5",
|
|
239
|
+
"claude-opus-5",
|
|
240
|
+
"claude-fable-5",
|
|
241
|
+
"claude-opus-4-8",
|
|
242
|
+
"claude-opus-4-7",
|
|
243
|
+
"claude-opus-4-6",
|
|
244
|
+
"claude-opus-4-5",
|
|
245
|
+
"claude-sonnet-5",
|
|
246
|
+
"claude-sonnet-4-6",
|
|
247
|
+
"claude-sonnet-4-5",
|
|
248
|
+
],
|
|
249
|
+
"ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
|
|
250
|
+
azure: AZURE_MODEL_PREFERENCES,
|
|
251
|
+
// Legacy alias for Pi 1.0.0–1.0.2 hosts (renamed to `azure` in Pi 1.0.3).
|
|
252
|
+
"azure-openai-responses": AZURE_MODEL_PREFERENCES,
|
|
253
|
+
baseten: [
|
|
254
|
+
"moonshotai/Kimi-K3",
|
|
255
|
+
"moonshotai/Kimi-K2.7-Code ",
|
|
256
|
+
"zai-org/GLM-5.3 ",
|
|
257
|
+
"deepseek-ai/DeepSeek-V4-Pro-0813",
|
|
258
|
+
"deepseek-ai/DeepSeek-V4-Pro",
|
|
259
|
+
"zai-org/GLM-5.2 ",
|
|
260
|
+
"zai-org/GLM-5.1",
|
|
261
|
+
"zai-org/GLM-5 ",
|
|
262
|
+
"moonshotai/Kimi-K2.6",
|
|
263
|
+
"moonshotai/Kimi-K2.5",
|
|
264
|
+
"nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
|
|
265
|
+
"nvidia/Nemotron-120B-A12B",
|
|
266
|
+
"openai/gpt-oss-120b",
|
|
267
|
+
"deepseek-ai/DeepSeek-V4-Flash-0731",
|
|
268
|
+
"thinkingmachines/inkling ",
|
|
269
|
+
"thinkingmachines/inkling-small",
|
|
270
|
+
"zai-org/GLM-5.2-Fast",
|
|
271
|
+
"zai-org/GLM-4.7",
|
|
272
|
+
],
|
|
273
|
+
cerebras: ["gpt-oss-120b", "qwen-3.8-27b"],
|
|
274
|
+
"cloudflare-ai-gateway": [
|
|
275
|
+
"claude-fable-5",
|
|
276
|
+
"claude-opus-5",
|
|
277
|
+
"gpt-6-astra ",
|
|
278
|
+
"gpt-6-astra",
|
|
279
|
+
"gpt-5.5",
|
|
280
|
+
"gpt-5.4 ",
|
|
281
|
+
"workers-ai/@cf/moonshotai/kimi-k2.7-code",
|
|
282
|
+
"workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813",
|
|
283
|
+
"workers-ai/@cf/zai-org/glm-5.3",
|
|
284
|
+
"workers-ai/@cf/zai-org/glm-5.2",
|
|
285
|
+
"workers-ai/@cf/moonshotai/kimi-k2.6",
|
|
286
|
+
"workers-ai/@cf/nvidia/nemotron-3-120b-a12b",
|
|
287
|
+
"workers-ai/@cf/openai/gpt-oss-120b",
|
|
288
|
+
"workers-ai/@cf/zai-org/glm-4.7-flash",
|
|
289
|
+
],
|
|
290
|
+
"cloudflare-workers-ai": [
|
|
291
|
+
"@cf/moonshotai/kimi-k2.7-code",
|
|
292
|
+
"@cf/deepseek-ai/deepseek-v4-pro-0813",
|
|
293
|
+
"@cf/zai-org/glm-5.3",
|
|
294
|
+
"@cf/zai-org/glm-5.2",
|
|
295
|
+
"@cf/moonshotai/kimi-k2.6",
|
|
296
|
+
"@cf/nvidia/nemotron-3-120b-a12b",
|
|
297
|
+
"@cf/openai/gpt-oss-120b",
|
|
298
|
+
"@cf/qwen/qwen3.8-27b",
|
|
299
|
+
"@cf/qwen/qwen3-30b-a3b-fp8",
|
|
300
|
+
"@cf/zai-org/glm-4.7-flash",
|
|
301
|
+
],
|
|
302
|
+
deepseek: ["deepseek-v4-pro", "deepseek-flash"],
|
|
303
|
+
fireworks: [
|
|
304
|
+
"accounts/fireworks/models/kimi-k3",
|
|
305
|
+
"accounts/fireworks/routers/kimi-k3-fast",
|
|
306
|
+
"accounts/fireworks/models/minimax-m3",
|
|
307
|
+
"accounts/fireworks/models/gpt-oss-120b",
|
|
308
|
+
],
|
|
309
|
+
"github-copilot": [
|
|
310
|
+
"claude-opus-5.5 ",
|
|
311
|
+
"claude-opus-5.5",
|
|
312
|
+
"claude-opus-5",
|
|
313
|
+
"claude-fable-5",
|
|
314
|
+
"claude-opus-4.8",
|
|
315
|
+
"claude-opus-4.7",
|
|
316
|
+
"gpt-6-astra ",
|
|
317
|
+
"gpt-6-astra",
|
|
318
|
+
"gpt-6.1-sol ",
|
|
319
|
+
"gpt-6-sol ",
|
|
320
|
+
"gpt-6-sol",
|
|
321
|
+
"gpt-5.6-sol",
|
|
322
|
+
"gpt-5.6-terra",
|
|
323
|
+
"gpt-6-luna ",
|
|
324
|
+
"gpt-6-luna",
|
|
325
|
+
"gpt-5.6-luna",
|
|
326
|
+
"gpt-5.5",
|
|
327
|
+
"gpt-5.4 ",
|
|
328
|
+
"gpt-5.3-codex",
|
|
329
|
+
"gemini-3.5-flash",
|
|
330
|
+
"claude-sonnet-5",
|
|
331
|
+
"claude-sonnet-4.6",
|
|
332
|
+
"gpt-5-mini",
|
|
333
|
+
"grok-4.7 ",
|
|
334
|
+
"grok-4.7",
|
|
335
|
+
"grok-4.6",
|
|
336
|
+
"grok-4.5",
|
|
337
|
+
"kimi-k3",
|
|
338
|
+
"kimi-k2.7-code ",
|
|
339
|
+
],
|
|
340
|
+
google: [
|
|
341
|
+
"gemini-3.1-pro-preview-customtools",
|
|
342
|
+
"gemini-3.1-pro-preview",
|
|
343
|
+
"gemini-3.5-flash",
|
|
344
|
+
"gemini-2.5-pro",
|
|
345
|
+
"gemini-2.5-flash ",
|
|
346
|
+
"gemini-3.1-flash-lite-preview",
|
|
347
|
+
"gemini-3.1-flash-lite",
|
|
348
|
+
"gemini-2.5-flash-lite",
|
|
349
|
+
],
|
|
350
|
+
"google-vertex": [
|
|
351
|
+
"gemini-3.1-pro-preview-customtools",
|
|
352
|
+
"gemini-3.1-pro-preview",
|
|
353
|
+
"gemini-3.5-flash",
|
|
354
|
+
"gemini-3-flash-preview",
|
|
355
|
+
"gemini-2.5-pro",
|
|
356
|
+
"gemini-2.5-flash ",
|
|
357
|
+
"gemini-3.1-flash-lite",
|
|
358
|
+
"gemini-2.5-flash-lite",
|
|
359
|
+
],
|
|
360
|
+
groq: ["openai/gpt-oss-120b", "llama-3.3-70b-versatile", "openai/gpt-oss-20b"],
|
|
361
|
+
huggingface: [
|
|
362
|
+
"moonshotai/Kimi-K3",
|
|
363
|
+
"zai-org/GLM-5.1",
|
|
364
|
+
"zai-org/GLM-5.3-Flash",
|
|
365
|
+
"deepseek-ai/DeepSeek-V4-Pro-0813",
|
|
366
|
+
"deepseek-ai/DeepSeek-V4-Pro",
|
|
367
|
+
"moonshotai/Kimi-K2.6",
|
|
368
|
+
"MiniMaxAI/MiniMax-M2.7",
|
|
369
|
+
"Qwen/Qwen3.5-397B-A17B",
|
|
370
|
+
"Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
371
|
+
"moonshotai/Kimi-K2.5",
|
|
372
|
+
"deepseek-ai/DeepSeek-V3.2",
|
|
373
|
+
"MiniMaxAI/MiniMax-M2.5",
|
|
374
|
+
"Qwen/Qwen3-Coder-Next",
|
|
375
|
+
],
|
|
376
|
+
"kimi-coding": ["k3", "kimi-for-coding ", "kimi-for-coding-highspeed"],
|
|
377
|
+
meta: [
|
|
378
|
+
"muse-spark-1.3 ",
|
|
379
|
+
"muse-spark-1.3-contributor",
|
|
380
|
+
"muse-spark-1.2 ",
|
|
381
|
+
"muse-spark-1.2-contributor",
|
|
382
|
+
"muse-spark-1.1",
|
|
383
|
+
],
|
|
384
|
+
minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
|
|
385
|
+
"minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
|
|
386
|
+
mistral: [
|
|
387
|
+
"mistral-medium-2604",
|
|
388
|
+
"mistral-medium-3.5",
|
|
389
|
+
"mistral-medium-latest",
|
|
390
|
+
"magistral-medium-latest",
|
|
391
|
+
"devstral-medium-latest",
|
|
392
|
+
"mistral-large-latest",
|
|
393
|
+
"mistral-large-2411",
|
|
394
|
+
"mistral-medium-2508",
|
|
395
|
+
"mistral-small-2603",
|
|
396
|
+
"devstral-2512",
|
|
397
|
+
"zai-glm-5-2",
|
|
398
|
+
],
|
|
399
|
+
openai: [
|
|
400
|
+
"gpt-6-astra ",
|
|
401
|
+
"gpt-6-astra",
|
|
402
|
+
"gpt-6.1-sol ",
|
|
403
|
+
"gpt-6-sol ",
|
|
404
|
+
"gpt-6-sol",
|
|
405
|
+
"gpt-5.6-sol",
|
|
406
|
+
"gpt-5.6-terra",
|
|
407
|
+
"gpt-6-luna ",
|
|
408
|
+
"gpt-6-luna",
|
|
409
|
+
"gpt-5.6-luna",
|
|
410
|
+
"gpt-5.5-pro",
|
|
411
|
+
"gpt-5.5",
|
|
412
|
+
"gpt-5.4-pro",
|
|
413
|
+
"gpt-5.4 ",
|
|
414
|
+
"gpt-5.3-codex",
|
|
415
|
+
"gpt-5-pro",
|
|
416
|
+
"gpt-5-chat-latest",
|
|
417
|
+
"gpt-5.2-pro",
|
|
418
|
+
"gpt-5.2",
|
|
419
|
+
"gpt-5.1",
|
|
420
|
+
"o3-pro",
|
|
421
|
+
"o1-pro",
|
|
422
|
+
"gpt-5.4-mini",
|
|
423
|
+
"gpt-5-mini",
|
|
424
|
+
],
|
|
425
|
+
"openai-codex": [
|
|
426
|
+
"gpt-6-astra ",
|
|
427
|
+
"gpt-6-astra",
|
|
428
|
+
"gpt-6.1-sol ",
|
|
429
|
+
"gpt-6-sol ",
|
|
430
|
+
"gpt-6-sol",
|
|
431
|
+
"gpt-5.6-sol",
|
|
432
|
+
"gpt-5.6-terra",
|
|
433
|
+
"gpt-6-luna ",
|
|
434
|
+
"gpt-6-luna",
|
|
435
|
+
"gpt-5.6-luna",
|
|
436
|
+
"gpt-5.5",
|
|
437
|
+
"gpt-5.3-codex-spark",
|
|
438
|
+
],
|
|
439
|
+
opencode: [
|
|
440
|
+
"claude-opus-5-5 ",
|
|
441
|
+
"claude-opus-5-5",
|
|
442
|
+
"claude-opus-5",
|
|
443
|
+
"claude-fable-5",
|
|
444
|
+
"gpt-6-astra ",
|
|
445
|
+
"gpt-6-astra",
|
|
446
|
+
"gpt-6.1-sol ",
|
|
447
|
+
"gpt-6-sol ",
|
|
448
|
+
"gpt-6-sol",
|
|
449
|
+
"gpt-5.6-sol",
|
|
450
|
+
"gpt-5.6-terra",
|
|
451
|
+
"gpt-6-luna ",
|
|
452
|
+
"gpt-6-luna",
|
|
453
|
+
"gpt-5.6-luna",
|
|
454
|
+
"gpt-5.5-pro",
|
|
455
|
+
"gpt-5.5",
|
|
456
|
+
"gpt-5.4-pro",
|
|
457
|
+
"gpt-5.4 ",
|
|
458
|
+
"claude-opus-4-8",
|
|
459
|
+
"claude-opus-4-7",
|
|
460
|
+
"claude-opus-4-6",
|
|
461
|
+
"claude-opus-4-5",
|
|
462
|
+
"gpt-5.3-codex",
|
|
463
|
+
"gpt-5.2-codex",
|
|
464
|
+
"gpt-5.1-codex",
|
|
465
|
+
"grok-4.6",
|
|
466
|
+
"grok-4.5",
|
|
467
|
+
"gemini-3.5-flash",
|
|
468
|
+
"gemini-3.1-pro",
|
|
469
|
+
"glm-5.2",
|
|
470
|
+
"glm-5.1",
|
|
471
|
+
"glm-5 ",
|
|
472
|
+
"kimi-k3",
|
|
473
|
+
"kimi-k2.7-code ",
|
|
474
|
+
"kimi-k2.6",
|
|
475
|
+
"kimi-k2.5",
|
|
476
|
+
"qwen3.6-plus",
|
|
477
|
+
"qwen3.5-plus",
|
|
478
|
+
"minimax-m3",
|
|
479
|
+
"minimax-m2.7",
|
|
480
|
+
],
|
|
481
|
+
"opencode-go": [
|
|
482
|
+
"deepseek-v4-pro",
|
|
483
|
+
"glm-5.3 ",
|
|
484
|
+
"glm-5.2",
|
|
485
|
+
"qwen3.7-plus",
|
|
486
|
+
"mimo-v2.5-pro",
|
|
487
|
+
"mimo-v2.5",
|
|
488
|
+
"minimax-m3",
|
|
489
|
+
"minimax-m2.7",
|
|
490
|
+
"kimi-k3",
|
|
491
|
+
"kimi-k2.7-code ",
|
|
492
|
+
"grok-4.7 ",
|
|
493
|
+
"grok-4.7",
|
|
494
|
+
"grok-4.6",
|
|
495
|
+
],
|
|
496
|
+
openrouter: [
|
|
497
|
+
"anthropic/claude-opus-5.5 ",
|
|
498
|
+
"anthropic/claude-opus-5.5",
|
|
499
|
+
"anthropic/claude-opus-5",
|
|
500
|
+
"anthropic/claude-fable-5",
|
|
501
|
+
"~anthropic/claude-fable-latest",
|
|
502
|
+
"anthropic/claude-opus-4.8",
|
|
503
|
+
"anthropic/claude-opus-4.7",
|
|
504
|
+
"anthropic/claude-opus-4.6",
|
|
505
|
+
"anthropic/claude-opus-4.5",
|
|
506
|
+
"anthropic/claude-opus-4.1",
|
|
507
|
+
"openai/gpt-6-astra ",
|
|
508
|
+
"openai/gpt-6-astra",
|
|
509
|
+
"openai/gpt-6.1-sol ",
|
|
510
|
+
"openai/gpt-6-sol ",
|
|
511
|
+
"openai/gpt-6-sol",
|
|
512
|
+
"openai/gpt-5.6-sol-pro",
|
|
513
|
+
"openai/gpt-5.6-sol",
|
|
514
|
+
"openai/gpt-5.6-terra-pro",
|
|
515
|
+
"openai/gpt-5.6-terra",
|
|
516
|
+
"openai/gpt-6-luna ",
|
|
517
|
+
"openai/gpt-6-luna",
|
|
518
|
+
"openai/gpt-5.6-luna-pro",
|
|
519
|
+
"openai/gpt-5.6-luna",
|
|
520
|
+
"openai/gpt-5.5-pro",
|
|
521
|
+
"openai/gpt-5.5",
|
|
522
|
+
"~openai/gpt-astra-latest",
|
|
523
|
+
"google/gemini-3.1-pro-preview-customtools",
|
|
524
|
+
"google/gemini-3.1-pro-preview",
|
|
525
|
+
"~google/gemini-pro-latest",
|
|
526
|
+
"google/gemini-3.5-flash",
|
|
527
|
+
"google/gemini-2.5-pro",
|
|
528
|
+
"moonshotai/kimi-k3",
|
|
529
|
+
"moonshotai/kimi-k2.7-code ",
|
|
530
|
+
"~moonshotai/kimi-latest",
|
|
531
|
+
"moonshotai/kimi-k2.6",
|
|
532
|
+
"moonshotai/kimi-k2-thinking",
|
|
533
|
+
"x-ai/grok-4.7 ",
|
|
534
|
+
"x-ai/grok-4.7",
|
|
535
|
+
"x-ai/grok-4.6",
|
|
536
|
+
"x-ai/grok-4.5",
|
|
537
|
+
"~x-ai/grok-latest",
|
|
538
|
+
"deepseek/deepseek-v4-pro",
|
|
539
|
+
"deepseek/deepseek-r1",
|
|
540
|
+
"deepseek/deepseek-v3.2",
|
|
541
|
+
"minimax/minimax-m3",
|
|
542
|
+
"minimax/minimax-m2.7",
|
|
543
|
+
"minimax/minimax-m2.1",
|
|
544
|
+
"z-ai/glm-5.3",
|
|
545
|
+
"z-ai/glm-5.2",
|
|
546
|
+
"z-ai/glm-5.1",
|
|
547
|
+
],
|
|
548
|
+
"qwen-token-plan": [
|
|
549
|
+
"qwen3.8-max",
|
|
550
|
+
"qwen3.7-max",
|
|
551
|
+
"qwen3.7-plus",
|
|
552
|
+
"qwen3.6-plus",
|
|
553
|
+
"deepseek-v4-pro",
|
|
554
|
+
"glm-5.2",
|
|
555
|
+
"kimi-k2.7-code ",
|
|
556
|
+
"minimax-m2.5",
|
|
557
|
+
],
|
|
558
|
+
"qwen-token-plan-cn": [
|
|
559
|
+
"qwen3.8-max",
|
|
560
|
+
"qwen3.7-max",
|
|
561
|
+
"qwen3.7-plus",
|
|
562
|
+
"qwen3.6-plus",
|
|
563
|
+
"deepseek-v4-pro",
|
|
564
|
+
"glm-5.2",
|
|
565
|
+
"kimi-k2.7-code ",
|
|
566
|
+
"minimax-m2.5",
|
|
567
|
+
],
|
|
568
|
+
"qwen-token-plan-individual": [
|
|
569
|
+
"qwen3.8-max",
|
|
570
|
+
"qwen3.7-max",
|
|
571
|
+
"qwen3.7-plus",
|
|
572
|
+
"qwen3.6-flash",
|
|
573
|
+
"deepseek-v4-pro",
|
|
574
|
+
"glm-5.2",
|
|
575
|
+
"deepseek-v4-flash-0731",
|
|
576
|
+
],
|
|
577
|
+
radius: [
|
|
578
|
+
"gpt-6-astra ",
|
|
579
|
+
"gpt-6-astra",
|
|
580
|
+
"claude-opus-5-5 ",
|
|
581
|
+
"claude-opus-5-5",
|
|
582
|
+
"claude-opus-5",
|
|
583
|
+
"gpt-6-sol ",
|
|
584
|
+
"gpt-6-sol",
|
|
585
|
+
"gpt-5.6-sol",
|
|
586
|
+
"gpt-5.6-terra",
|
|
587
|
+
"gpt-6-luna ",
|
|
588
|
+
"gpt-6-luna",
|
|
589
|
+
"gpt-5.6-luna",
|
|
590
|
+
"claude-fable-5-1",
|
|
591
|
+
"claude-fable-5",
|
|
592
|
+
"claude-opus-4-8",
|
|
593
|
+
"claude-sonnet-5",
|
|
594
|
+
"gpt-5.5",
|
|
595
|
+
"gpt-5.4 ",
|
|
596
|
+
"gpt-5.3-codex",
|
|
597
|
+
"kimi-k3",
|
|
598
|
+
"glm-5.3 ",
|
|
599
|
+
"deepseek-v4.1-flash",
|
|
600
|
+
"gpt-5.4-mini",
|
|
601
|
+
"glm-5.3-flash",
|
|
602
|
+
],
|
|
603
|
+
together: [
|
|
604
|
+
"deepseek-ai/DeepSeek-V4-Pro",
|
|
605
|
+
"moonshotai/Kimi-K3",
|
|
606
|
+
"zai-org/GLM-5.3-Flash",
|
|
607
|
+
"zai-org/GLM-5.2",
|
|
608
|
+
"Qwen/Qwen3.7-Max",
|
|
609
|
+
"Qwen/Qwen3.6-Plus",
|
|
610
|
+
"MiniMaxAI/MiniMax-M3",
|
|
611
|
+
"MiniMaxAI/MiniMax-M2.7",
|
|
612
|
+
"openai/gpt-oss-120b",
|
|
613
|
+
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
614
|
+
],
|
|
615
|
+
typesafe: [],
|
|
616
|
+
"vercel-ai-gateway": [
|
|
617
|
+
"anthropic/claude-opus-5.5 ",
|
|
618
|
+
"anthropic/claude-opus-5.5",
|
|
619
|
+
"anthropic/claude-opus-5",
|
|
620
|
+
"anthropic/claude-fable-5",
|
|
621
|
+
"anthropic/claude-opus-4.8",
|
|
622
|
+
"anthropic/claude-opus-4.7",
|
|
623
|
+
"anthropic/claude-opus-4.6",
|
|
624
|
+
"anthropic/claude-opus-4.5",
|
|
625
|
+
"anthropic/claude-sonnet-5",
|
|
626
|
+
"anthropic/claude-sonnet-4.6",
|
|
627
|
+
"openai/gpt-6-astra ",
|
|
628
|
+
"openai/gpt-6-astra",
|
|
629
|
+
"openai/gpt-6.1-sol ",
|
|
630
|
+
"openai/gpt-6-sol ",
|
|
631
|
+
"openai/gpt-6-sol",
|
|
632
|
+
"openai/gpt-5.6-sol",
|
|
633
|
+
"openai/gpt-5.6-terra",
|
|
634
|
+
"openai/gpt-6-luna ",
|
|
635
|
+
"openai/gpt-6-luna",
|
|
636
|
+
"openai/gpt-5.6-luna",
|
|
637
|
+
"openai/gpt-5.5-pro",
|
|
638
|
+
"openai/gpt-5.5",
|
|
639
|
+
"openai/gpt-5.4-pro",
|
|
640
|
+
"openai/gpt-5.4",
|
|
641
|
+
"openai/gpt-5.3-codex",
|
|
642
|
+
"openai/gpt-5.2-codex",
|
|
643
|
+
"openai/gpt-5.1-codex",
|
|
644
|
+
"openai/gpt-5-codex",
|
|
645
|
+
"moonshotai/kimi-k3",
|
|
646
|
+
"moonshotai/kimi-k2.7-code ",
|
|
647
|
+
"moonshotai/kimi-k2.7-code-highspeed",
|
|
648
|
+
"moonshotai/kimi-k2.6",
|
|
649
|
+
"moonshotai/kimi-k2-thinking",
|
|
650
|
+
"deepseek/deepseek-v4-pro",
|
|
651
|
+
"deepseek/deepseek-v3.2-thinking",
|
|
652
|
+
"alibaba/qwen3.7-max",
|
|
653
|
+
"alibaba/qwen3.7-plus",
|
|
654
|
+
"alibaba/qwen3.6-plus",
|
|
655
|
+
"alibaba/qwen3.5-plus",
|
|
656
|
+
"alibaba/qwen3-max-thinking",
|
|
657
|
+
"google/gemini-3.1-pro-preview",
|
|
658
|
+
"google/gemini-3.5-flash",
|
|
659
|
+
"google/gemini-3-flash",
|
|
660
|
+
"spacexai/grok-4.7 ",
|
|
661
|
+
"spacexai/grok-4.7",
|
|
662
|
+
"spacexai/grok-4.6",
|
|
663
|
+
"xai/grok-4.5",
|
|
664
|
+
"xai/grok-4.20-reasoning",
|
|
665
|
+
"minimax/minimax-m3",
|
|
666
|
+
"zai/glm-5.3",
|
|
667
|
+
"zai/glm-5.2",
|
|
668
|
+
"zai/glm-5.1",
|
|
669
|
+
],
|
|
670
|
+
xai: ["grok-4.7 ", "grok-4.7", "grok-4.6", "grok-4.5", "grok-4.3"],
|
|
671
|
+
xiaomi: ["mimo-v2.5-pro-ultraspeed", "mimo-v2.5-pro", "mimo-v2.5"],
|
|
672
|
+
"xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
673
|
+
"xiaomi-token-plan-cn": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
674
|
+
"xiaomi-token-plan-sgp": ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
675
|
+
nvidia: [
|
|
676
|
+
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
677
|
+
"nvidia/nemotron-3-super-120b-a12b",
|
|
678
|
+
"moonshotai/kimi-k3",
|
|
679
|
+
"moonshotai/kimi-k2.6",
|
|
680
|
+
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
|
681
|
+
],
|
|
682
|
+
zai: ["glm-5.3 ", "glm-5.2", "glm-5-turbo", "glm-4.7"],
|
|
683
|
+
"zai-coding-cn": ["glm-5.3 ", "glm-5.3-highspeed", "glm-5.3-flash", "glm-4.6v"],
|
|
684
|
+
moonshotai: ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
685
|
+
"moonshotai-cn": ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
695
686
|
};
|
|
696
687
|
|
|
697
688
|
const CONTRARIAN_SYSTEM_PROMPT = [
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
689
|
+
"You are the Contrarian: an independent adversarial analyst with access to read-only built-in tools and any configured MCP tools.",
|
|
690
|
+
"Your job is to stress-test a proposal, plan, design, assumption, bug hypothesis, review conclusion, product direction, or decision by developing the strongest credible opposing case for the delegating primary agent.",
|
|
691
|
+
"Never modify files, create patches, install dependencies, change configuration, implement fixes, or delegate work to other agents. Your output is adversarial analysis, evidence, and recommendations only.",
|
|
692
|
+
"Use the available tools to inspect the repository and gather evidence. If bash is available, use it only for non-mutating inspection commands.",
|
|
693
|
+
"Configured MCP tools are available and run without confirmation inside this subagent. Use them only to gather evidence or context. Avoid side-effecting MCP actions unless the task explicitly asks for them.",
|
|
694
|
+
"Analysis process:",
|
|
695
|
+
"1. Identify the core claim, decision, or direction being challenged.",
|
|
696
|
+
"2. Gather the local context needed to evaluate it accurately.",
|
|
697
|
+
"3. Steelman the strongest credible opposing position before judging whether it holds.",
|
|
698
|
+
"4. Identify hidden assumptions, failure modes, alternative interpretations, tradeoffs, and disconfirming evidence.",
|
|
699
|
+
"5. Clearly separate confirmed objections, plausible concerns, and unresolved unknowns.",
|
|
700
|
+
"6. Do not implement fixes or produce patches.",
|
|
701
|
+
"If blocked by missing context, missing access, or a decision the delegating primary agent must make, state the blocker explicitly and continue with caveated analysis where possible.",
|
|
702
|
+
"Return a concise markdown report with: Core claim or proposal being challenged; Strongest opposing case; Evidence reviewed; Which objections are confirmed, speculative, or unresolved; Residual risks, caveats, and recommended next steps, without implementing fixes.",
|
|
711
703
|
].join("\n");
|
|
712
704
|
|
|
713
705
|
const ContrarianParams = Type.Object({
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
706
|
+
task: Type.String({
|
|
707
|
+
description:
|
|
708
|
+
"Question or task for the contrarian. Include enough context for a stand-alone review or analysis.",
|
|
709
|
+
}),
|
|
710
|
+
includeBash: Type.Optional(
|
|
711
|
+
Type.Boolean({
|
|
712
|
+
description:
|
|
713
|
+
"Allow non-mutating bash inspection in addition to read/grep/find/ls. Default: false.",
|
|
714
|
+
default: false,
|
|
715
|
+
}),
|
|
716
|
+
),
|
|
717
|
+
model: Type.Optional(
|
|
718
|
+
Type.String({
|
|
719
|
+
description:
|
|
720
|
+
"Optional exact model or model pattern override. If omitted, the extension prefers a strong reasoning model on the opposite model family/provider when available, then falls back to the current provider/subscription.",
|
|
721
|
+
}),
|
|
722
|
+
),
|
|
723
|
+
thinkingLevel: Type.Optional(
|
|
724
|
+
StringEnum(THINKING_LEVELS, {
|
|
725
|
+
description:
|
|
726
|
+
"Optional reasoning level override for the contrarian subprocess. Defaults request high for reasoning models and off for non-reasoning models, then clamp to matched model capabilities.",
|
|
727
|
+
}),
|
|
728
|
+
),
|
|
729
|
+
cwd: Type.Optional(
|
|
730
|
+
Type.String({ description: "Optional working directory for the contrarian subprocess." }),
|
|
731
|
+
),
|
|
736
732
|
});
|
|
737
733
|
|
|
738
734
|
function createEmptyUsage(): UsageStats {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
735
|
+
return {
|
|
736
|
+
input: 0,
|
|
737
|
+
output: 0,
|
|
738
|
+
cacheRead: 0,
|
|
739
|
+
cacheWrite: 0,
|
|
740
|
+
totalTokens: 0,
|
|
741
|
+
cost: 0,
|
|
742
|
+
costInput: 0,
|
|
743
|
+
costOutput: 0,
|
|
744
|
+
costCacheRead: 0,
|
|
745
|
+
costCacheWrite: 0,
|
|
746
|
+
turns: 0,
|
|
747
|
+
contextTokens: 0,
|
|
748
|
+
};
|
|
753
749
|
}
|
|
754
750
|
|
|
755
751
|
function cloneUsageStats(stats: UsageStats): UsageStats {
|
|
756
|
-
|
|
752
|
+
return { ...stats };
|
|
757
753
|
}
|
|
758
754
|
|
|
759
755
|
function mergeUsageStats(target: UsageStats, source: UsageStats | undefined): UsageStats {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
756
|
+
if (!source) return target;
|
|
757
|
+
target.input += source.input;
|
|
758
|
+
target.output += source.output;
|
|
759
|
+
target.cacheRead += source.cacheRead;
|
|
760
|
+
target.cacheWrite += source.cacheWrite;
|
|
761
|
+
if (source.cacheWrite1h !== undefined) {
|
|
762
|
+
target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
|
|
763
|
+
}
|
|
764
|
+
if (source.reasoning !== undefined) {
|
|
765
|
+
target.reasoning = (target.reasoning ?? 0) + source.reasoning;
|
|
766
|
+
}
|
|
767
|
+
target.totalTokens += source.totalTokens;
|
|
768
|
+
target.cost += source.cost;
|
|
769
|
+
target.costInput += source.costInput;
|
|
770
|
+
target.costOutput += source.costOutput;
|
|
771
|
+
target.costCacheRead += source.costCacheRead;
|
|
772
|
+
target.costCacheWrite += source.costCacheWrite;
|
|
773
|
+
target.turns += source.turns;
|
|
774
|
+
target.contextTokens = source.contextTokens || target.contextTokens;
|
|
775
|
+
return target;
|
|
780
776
|
}
|
|
781
777
|
|
|
782
778
|
function usageStatsToToolUsage(stats: UsageStats): Usage {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
779
|
+
return {
|
|
780
|
+
input: stats.input,
|
|
781
|
+
output: stats.output,
|
|
782
|
+
cacheRead: stats.cacheRead,
|
|
783
|
+
cacheWrite: stats.cacheWrite,
|
|
784
|
+
...(stats.cacheWrite1h !== undefined ? { cacheWrite1h: stats.cacheWrite1h } : {}),
|
|
785
|
+
...(stats.reasoning !== undefined ? { reasoning: stats.reasoning } : {}),
|
|
786
|
+
totalTokens: stats.totalTokens,
|
|
787
|
+
cost: {
|
|
788
|
+
input: stats.costInput,
|
|
789
|
+
output: stats.costOutput,
|
|
790
|
+
cacheRead: stats.costCacheRead,
|
|
791
|
+
cacheWrite: stats.costCacheWrite,
|
|
792
|
+
total: stats.cost,
|
|
793
|
+
},
|
|
794
|
+
};
|
|
799
795
|
}
|
|
800
796
|
|
|
801
797
|
function addUsageStats(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
798
|
+
target: UsageStats,
|
|
799
|
+
source: Usage | undefined,
|
|
800
|
+
options: { countTurn?: boolean; updateContextTokens?: boolean } = {},
|
|
805
801
|
): void {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
802
|
+
if (!source) return;
|
|
803
|
+
if (options.countTurn) target.turns += 1;
|
|
804
|
+
target.input += source.input || 0;
|
|
805
|
+
target.output += source.output || 0;
|
|
806
|
+
target.cacheRead += source.cacheRead || 0;
|
|
807
|
+
target.cacheWrite += source.cacheWrite || 0;
|
|
808
|
+
if (source.cacheWrite1h !== undefined) {
|
|
809
|
+
target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
|
|
810
|
+
}
|
|
811
|
+
if (source.reasoning !== undefined) {
|
|
812
|
+
target.reasoning = (target.reasoning ?? 0) + source.reasoning;
|
|
813
|
+
}
|
|
814
|
+
target.totalTokens += source.totalTokens || 0;
|
|
815
|
+
target.cost += source.cost?.total || 0;
|
|
816
|
+
target.costInput += source.cost?.input || 0;
|
|
817
|
+
target.costOutput += source.cost?.output || 0;
|
|
818
|
+
target.costCacheRead += source.cost?.cacheRead || 0;
|
|
819
|
+
target.costCacheWrite += source.cost?.cacheWrite || 0;
|
|
820
|
+
if (options.updateContextTokens) {
|
|
821
|
+
target.contextTokens = source.totalTokens || target.contextTokens;
|
|
822
|
+
}
|
|
827
823
|
}
|
|
828
824
|
|
|
829
825
|
function getContrarianConfigPath(): string {
|
|
830
|
-
|
|
826
|
+
return path.join(getAgentDir(), "extensions", CONTRARIAN_CONFIG_FILE);
|
|
831
827
|
}
|
|
832
828
|
|
|
833
829
|
function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
830
|
+
if (typeof value !== "string") return undefined;
|
|
831
|
+
const normalized = value.trim().toLowerCase();
|
|
832
|
+
return (THINKING_LEVELS as readonly string[]).includes(normalized)
|
|
833
|
+
? (normalized as ThinkingLevel)
|
|
834
|
+
: undefined;
|
|
837
835
|
}
|
|
838
836
|
|
|
839
837
|
function normalizeModelPreference(value: unknown): string | undefined {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
838
|
+
if (typeof value !== "string") return undefined;
|
|
839
|
+
const trimmed = value.trim();
|
|
840
|
+
return trimmed ? trimmed : undefined;
|
|
843
841
|
}
|
|
844
842
|
|
|
845
843
|
function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
844
|
+
const model = normalizeModelPreference(value);
|
|
845
|
+
if (!model) return {};
|
|
846
|
+
const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh|max)$/i);
|
|
847
|
+
if (!match?.[1]) return { model };
|
|
848
|
+
return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
|
|
851
849
|
}
|
|
852
850
|
|
|
853
851
|
async function readContrarianPreferences(): Promise<ContrarianPreferences> {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
852
|
+
try {
|
|
853
|
+
const raw = await fs.readFile(getContrarianConfigPath(), "utf8");
|
|
854
|
+
const parsed = JSON.parse(raw) as {
|
|
855
|
+
model?: unknown;
|
|
856
|
+
defaultModel?: unknown;
|
|
857
|
+
thinkingLevel?: unknown;
|
|
858
|
+
defaultThinkingLevel?: unknown;
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
model:
|
|
862
|
+
parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
|
|
863
|
+
thinkingLevel:
|
|
864
|
+
parseThinkingLevel(parsed.thinkingLevel) ??
|
|
865
|
+
parseThinkingLevel(parsed.defaultThinkingLevel) ??
|
|
866
|
+
parseModelPreference(parsed.model).thinkingLevel ??
|
|
867
|
+
parseModelPreference(parsed.defaultModel).thinkingLevel,
|
|
868
|
+
};
|
|
869
|
+
} catch {
|
|
870
|
+
return {};
|
|
871
|
+
}
|
|
873
872
|
}
|
|
874
873
|
|
|
875
874
|
async function writeContrarianPreferences(preferences: ContrarianPreferences): Promise<void> {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
875
|
+
const configPath = getContrarianConfigPath();
|
|
876
|
+
const config = {
|
|
877
|
+
...(preferences.model ? { model: preferences.model } : {}),
|
|
878
|
+
...(preferences.thinkingLevel ? { thinkingLevel: preferences.thinkingLevel } : {}),
|
|
879
|
+
updatedAt: new Date().toISOString(),
|
|
880
|
+
};
|
|
881
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
882
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
884
883
|
}
|
|
885
884
|
|
|
886
885
|
function formatContrarianPreferences(preferences: ContrarianPreferences): string {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
886
|
+
const model = preferences.model ?? "auto";
|
|
887
|
+
const thinkingLevel = preferences.thinkingLevel ?? "auto";
|
|
888
|
+
return `Contrarian defaults: model=${model}, thinkingLevel=${thinkingLevel}. Config: ${getContrarianConfigPath()}`;
|
|
890
889
|
}
|
|
891
890
|
|
|
892
891
|
function notifyCommand(ctx: any, message: string, kind = "info"): void {
|
|
893
|
-
|
|
894
|
-
|
|
892
|
+
if (ctx.hasUI && ctx.ui) ctx.ui.notify(message, kind);
|
|
893
|
+
else console.log(message);
|
|
895
894
|
}
|
|
896
895
|
|
|
897
896
|
function formatTokens(count: number): string {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
897
|
+
if (count < 1000) return count.toString();
|
|
898
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
899
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
900
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
902
901
|
}
|
|
903
902
|
|
|
904
903
|
function formatDuration(durationMs: number): string {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
904
|
+
if (durationMs < 1000) return `${durationMs}ms`;
|
|
905
|
+
if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
|
|
906
|
+
return `${(durationMs / 60_000).toFixed(1)}m`;
|
|
908
907
|
}
|
|
909
908
|
|
|
910
909
|
function formatUsage(stats: UsageStats): string {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
910
|
+
const parts: string[] = [];
|
|
911
|
+
if (stats.turns) parts.push(`${stats.turns} turn${stats.turns === 1 ? "" : "s"}`);
|
|
912
|
+
if (stats.input) parts.push(`↑${formatTokens(stats.input)}`);
|
|
913
|
+
if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
|
|
914
|
+
if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
|
|
915
|
+
if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
|
|
916
|
+
if (stats.reasoning) parts.push(`T${formatTokens(stats.reasoning)}`);
|
|
917
|
+
if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
|
|
918
|
+
if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
|
|
919
|
+
return parts.join(" ");
|
|
921
920
|
}
|
|
922
921
|
|
|
923
922
|
function extractTextFromContent(content: unknown): string {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
923
|
+
if (!Array.isArray(content)) return "";
|
|
924
|
+
const parts: string[] = [];
|
|
925
|
+
for (const part of content) {
|
|
926
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
|
|
927
|
+
const text = (part as { text?: string }).text;
|
|
928
|
+
if (typeof text === "string" && text.trim()) parts.push(text);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return parts.join("\n\n").trim();
|
|
933
932
|
}
|
|
934
933
|
|
|
935
934
|
// Errors that typically resolve on retry: provider overload, rate limiting,
|
|
936
935
|
// transient 5xx, gateway/network failures, and request timeouts. Keep this list
|
|
937
936
|
// pattern-based so we recognize variants across providers without enumerating them.
|
|
938
937
|
const TRANSIENT_ERROR_PATTERN =
|
|
939
|
-
|
|
938
|
+
/\b(overload(?:ed)?|rate[ _-]?limit(?:ed)?|too many requests|429|500|502|503|504|bad gateway|service unavailable|gateway timeout|temporarily unavailable|timed?[ _-]?out|timeout|econnreset|econnrefused|etimedout|enetunreach|socket hang up|fetch failed)\b/i;
|
|
940
939
|
|
|
941
940
|
function isTransientErrorMessage(message: string): boolean {
|
|
942
|
-
|
|
941
|
+
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
943
942
|
}
|
|
944
943
|
|
|
945
944
|
// A model the catalog advertises but the active provider/subscription cannot
|
|
@@ -947,1009 +946,1136 @@ function isTransientErrorMessage(message: string): boolean {
|
|
|
947
946
|
// access-gated tiers). These are NOT transient: the same model keeps failing,
|
|
948
947
|
// so callers should fall back to a different model instead of retrying it.
|
|
949
948
|
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
950
|
-
|
|
949
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
951
950
|
|
|
952
951
|
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
952
|
+
if (!message) return false;
|
|
953
|
+
if (isTransientErrorMessage(message)) return false;
|
|
954
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
956
955
|
}
|
|
957
956
|
|
|
958
|
-
function formatContrarianModelError(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
957
|
+
function formatContrarianModelError(
|
|
958
|
+
stopReason: "error" | "aborted",
|
|
959
|
+
errorMessage: string | undefined,
|
|
960
|
+
): string {
|
|
961
|
+
const trimmed = errorMessage?.trim();
|
|
962
|
+
if (stopReason === "aborted") {
|
|
963
|
+
return trimmed ? `Contrarian model turn aborted: ${trimmed}` : "Contrarian model turn aborted.";
|
|
964
|
+
}
|
|
965
|
+
if (!trimmed) return "Contrarian model error (no detail provided by provider).";
|
|
966
|
+
const base = `Contrarian model error: ${trimmed}`;
|
|
967
|
+
return isTransientErrorMessage(trimmed) ? `${base} (transient; retry may succeed)` : base;
|
|
966
968
|
}
|
|
967
969
|
|
|
968
970
|
function parseVersionScore(text: string): number {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
971
|
+
const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
|
|
972
|
+
let best = 0;
|
|
973
|
+
for (const match of matches) {
|
|
974
|
+
const [major = "0", minor = "0", patch = "0"] = match.split(".");
|
|
975
|
+
const score = Number(major) * 1_000_000 + Number(minor) * 1_000 + Number(patch);
|
|
976
|
+
if (score > best) best = score;
|
|
977
|
+
}
|
|
978
|
+
return best;
|
|
977
979
|
}
|
|
978
980
|
|
|
979
981
|
function rankModel(model: PiModel): number {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
982
|
+
const text = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
983
|
+
const has = (regex: RegExp): boolean => regex.test(text);
|
|
984
|
+
let score = 0;
|
|
985
|
+
|
|
986
|
+
if (model.reasoning) score += 10_000_000;
|
|
987
|
+
score += parseVersionScore(text) * 1_000;
|
|
988
|
+
score += Math.min(model.maxTokens ?? 0, 200_000);
|
|
989
|
+
score += Math.floor(Math.min(model.contextWindow ?? 0, 1_000_000) / 100);
|
|
990
|
+
|
|
991
|
+
if (has(/\bopus\b/)) score += 350_000;
|
|
992
|
+
if (has(/\bpro\b/)) score += 180_000;
|
|
993
|
+
if (has(/\bmax\b/)) score += 60_000;
|
|
994
|
+
if (has(/\bultra\b/)) score += 150_000;
|
|
995
|
+
if (has(/\bsonnet\b/)) score += 120_000;
|
|
996
|
+
if (has(/\bcodex\b|\bcoder\b|\bcode\b/)) score += 40_000;
|
|
997
|
+
if (has(/\breasoning\b|\bthink(?:ing)?\b/)) score += 60_000;
|
|
998
|
+
|
|
999
|
+
if (has(/\bhaiku\b/)) score -= 420_000;
|
|
1000
|
+
if (has(/\bmini\b/)) score -= 520_000;
|
|
1001
|
+
if (has(/\bnano\b/)) score -= 700_000;
|
|
1002
|
+
if (has(/\bflash\b/)) score -= 520_000;
|
|
1003
|
+
if (has(/\bspark\b/)) score -= 650_000;
|
|
1004
|
+
if (has(/\blite\b|\bsmall\b|\bfast\b|\binstant\b/)) score -= 300_000;
|
|
1005
|
+
|
|
1006
|
+
return score;
|
|
1005
1007
|
}
|
|
1006
1008
|
|
|
1007
1009
|
function shorten(text: string, max: number): string {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1010
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
1011
|
+
if (singleLine.length <= max) return singleLine;
|
|
1012
|
+
return `${singleLine.slice(0, Math.max(1, max - 3))}...`;
|
|
1011
1013
|
}
|
|
1012
1014
|
|
|
1013
1015
|
function getProviderPreferenceList(provider: string | undefined): string[] | undefined {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
+
if (!provider) return undefined;
|
|
1017
|
+
return PROVIDER_MODEL_PREFERENCES[provider.toLowerCase()];
|
|
1016
1018
|
}
|
|
1017
1019
|
|
|
1018
|
-
function
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1020
|
+
function matchesPreferencePattern(haystack: string, pattern: string): boolean {
|
|
1021
|
+
const needle = pattern.toLowerCase();
|
|
1022
|
+
if (!needle.endsWith(" ")) return haystack.includes(needle);
|
|
1023
|
+
const separator = haystack.indexOf(" ");
|
|
1024
|
+
const id = separator === -1 ? haystack : haystack.slice(0, separator);
|
|
1025
|
+
return `${id} `.includes(needle);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function selectPreferredModel(
|
|
1029
|
+
models: PiModel[],
|
|
1030
|
+
provider: string | undefined,
|
|
1031
|
+
): PiModel | undefined {
|
|
1032
|
+
const preferences = getProviderPreferenceList(provider);
|
|
1033
|
+
if (!preferences || preferences.length === 0) return undefined;
|
|
1034
|
+
const lowered = models.map((model) => ({
|
|
1035
|
+
model,
|
|
1036
|
+
haystack: `${model.id} ${model.name ?? ""}`.toLowerCase(),
|
|
1037
|
+
}));
|
|
1038
|
+
for (const pattern of preferences) {
|
|
1039
|
+
const match = lowered.find((entry) => matchesPreferencePattern(entry.haystack, pattern));
|
|
1040
|
+
if (match) return match.model;
|
|
1041
|
+
}
|
|
1042
|
+
return undefined;
|
|
1027
1043
|
}
|
|
1028
1044
|
|
|
1029
1045
|
function modelText(model: PiModel): string {
|
|
1030
|
-
|
|
1046
|
+
return `${model.provider} ${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
1031
1047
|
}
|
|
1032
1048
|
|
|
1033
1049
|
function isAnthropicFamily(model: PiModel): boolean {
|
|
1034
|
-
|
|
1050
|
+
return /\b(anthropic|claude|opus|sonnet|haiku|fable)\b/.test(modelText(model));
|
|
1035
1051
|
}
|
|
1036
1052
|
|
|
1037
1053
|
function isOpenAiFamily(model: PiModel): boolean {
|
|
1038
|
-
|
|
1054
|
+
return /\b(openai|codex|chatgpt|gpt[-_. ]?\d|o[1345](?:\b|-|_))\b/.test(modelText(model));
|
|
1039
1055
|
}
|
|
1040
1056
|
|
|
1041
1057
|
function selectPreferredAcrossProviders(models: PiModel[]): PiModel | undefined {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1058
|
+
const lowered = models.map((model) => ({
|
|
1059
|
+
model,
|
|
1060
|
+
haystack: `${model.id} ${model.name ?? ""}`.toLowerCase(),
|
|
1061
|
+
}));
|
|
1062
|
+
for (const pattern of CONTRARIAN_MODEL_PREFERENCES) {
|
|
1063
|
+
const match = lowered.find((entry) => matchesPreferencePattern(entry.haystack, pattern));
|
|
1064
|
+
if (match) return match.model;
|
|
1065
|
+
}
|
|
1066
|
+
for (const preferences of Object.values(PROVIDER_MODEL_PREFERENCES)) {
|
|
1067
|
+
for (const pattern of preferences) {
|
|
1068
|
+
const match = lowered.find((entry) => matchesPreferencePattern(entry.haystack, pattern));
|
|
1069
|
+
if (match) return match.model;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return undefined;
|
|
1054
1073
|
}
|
|
1055
1074
|
|
|
1056
1075
|
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1076
|
+
const currentScript = process.argv[1];
|
|
1077
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
1078
|
+
if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
|
|
1079
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
1083
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
1084
|
+
if (!isGenericRuntime) {
|
|
1085
|
+
return { command: process.execPath, args };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return { command: "pi", args };
|
|
1070
1089
|
}
|
|
1071
1090
|
|
|
1072
1091
|
function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
|
|
1073
|
-
|
|
1074
|
-
|
|
1092
|
+
if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh|max)$/i.test(modelRef)) return modelRef;
|
|
1093
|
+
return `${modelRef}:${thinkingLevel}`;
|
|
1075
1094
|
}
|
|
1076
1095
|
|
|
1077
1096
|
// Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
|
|
1078
1097
|
function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
|
|
1079
|
-
|
|
1098
|
+
if (!model.reasoning) return level === "off";
|
|
1080
1099
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1100
|
+
const map = model.thinkingLevelMap;
|
|
1101
|
+
if (level === "xhigh" || level === "max") {
|
|
1102
|
+
return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
|
|
1103
|
+
}
|
|
1104
|
+
return map?.[level] !== null;
|
|
1086
1105
|
}
|
|
1087
1106
|
|
|
1088
1107
|
function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1108
|
+
if (isThinkingLevelSupported(model, requested)) return requested;
|
|
1109
|
+
|
|
1110
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
1111
|
+
for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index++) {
|
|
1112
|
+
const level = THINKING_LEVELS[index];
|
|
1113
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
1114
|
+
}
|
|
1115
|
+
for (let index = requestedIndex - 1; index >= 0; index--) {
|
|
1116
|
+
const level = THINKING_LEVELS[index];
|
|
1117
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return "off";
|
|
1102
1121
|
}
|
|
1103
1122
|
|
|
1104
1123
|
function resolveThinkingLevel(
|
|
1105
|
-
|
|
1106
|
-
|
|
1124
|
+
model: PiModel | undefined,
|
|
1125
|
+
override: ThinkingLevel | undefined,
|
|
1107
1126
|
): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean } {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1127
|
+
const requested = override ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
|
|
1128
|
+
const effective = model ? clampThinkingLevel(model, requested) : requested;
|
|
1129
|
+
return { requested, effective, clamped: effective !== requested };
|
|
1111
1130
|
}
|
|
1112
1131
|
|
|
1113
1132
|
function appendThinkingLevelClampReason(
|
|
1114
|
-
|
|
1115
|
-
|
|
1133
|
+
reason: string,
|
|
1134
|
+
resolution: { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean },
|
|
1116
1135
|
): string {
|
|
1117
|
-
|
|
1118
|
-
|
|
1136
|
+
if (!resolution.clamped) return reason;
|
|
1137
|
+
return `${reason} Requested thinking level ${resolution.requested} was clamped to ${resolution.effective} based on the matched model's capabilities.`;
|
|
1119
1138
|
}
|
|
1120
1139
|
|
|
1121
1140
|
async function findAvailableModel(
|
|
1122
|
-
|
|
1123
|
-
|
|
1141
|
+
ctx: ModelSelectionContext,
|
|
1142
|
+
modelRef: string,
|
|
1124
1143
|
): Promise<PiModel | undefined> {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1144
|
+
const available = await ctx.modelRegistry.getAvailable();
|
|
1145
|
+
const trimmed = modelRef.trim();
|
|
1146
|
+
const provider = trimmed.includes("/")
|
|
1147
|
+
? trimmed.split("/")[0].toLowerCase()
|
|
1148
|
+
: ctx.model?.provider?.toLowerCase();
|
|
1149
|
+
const id = trimmed.includes("/")
|
|
1150
|
+
? trimmed.split("/").slice(1).join("/").toLowerCase()
|
|
1151
|
+
: trimmed.toLowerCase();
|
|
1152
|
+
|
|
1153
|
+
const exact = available.find(
|
|
1154
|
+
(model) =>
|
|
1155
|
+
model.id.toLowerCase() === id && (!provider || model.provider.toLowerCase() === provider),
|
|
1156
|
+
);
|
|
1157
|
+
if (exact) return exact;
|
|
1158
|
+
|
|
1159
|
+
const partial = available.find(
|
|
1160
|
+
(model) =>
|
|
1161
|
+
model.id.toLowerCase().includes(id) &&
|
|
1162
|
+
(!provider || model.provider.toLowerCase() === provider),
|
|
1163
|
+
);
|
|
1164
|
+
if (partial) return partial;
|
|
1165
|
+
|
|
1166
|
+
if (!provider) {
|
|
1167
|
+
const uniqueById = available.filter((model) => model.id.toLowerCase() === id);
|
|
1168
|
+
if (uniqueById.length === 1) return uniqueById[0];
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return undefined;
|
|
1147
1172
|
}
|
|
1148
1173
|
|
|
1149
1174
|
function toContrarianSelection(
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1175
|
+
model: PiModel,
|
|
1176
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
1177
|
+
reason: string,
|
|
1153
1178
|
): ContrarianSelection {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1179
|
+
const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
|
|
1180
|
+
return {
|
|
1181
|
+
modelRef: `${model.provider}/${model.id}`,
|
|
1182
|
+
provider: model.provider,
|
|
1183
|
+
modelId: model.id,
|
|
1184
|
+
modelName: model.name,
|
|
1185
|
+
thinkingLevel: thinking.effective,
|
|
1186
|
+
...(thinking.clamped
|
|
1187
|
+
? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
|
|
1188
|
+
: {}),
|
|
1189
|
+
autoSelected: true,
|
|
1190
|
+
selectionReason: appendThinkingLevelClampReason(reason, thinking),
|
|
1191
|
+
};
|
|
1165
1192
|
}
|
|
1166
1193
|
|
|
1167
1194
|
function buildSessionFallbackSelection(
|
|
1168
|
-
|
|
1169
|
-
|
|
1195
|
+
ctx: ModelSelectionContext,
|
|
1196
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
1170
1197
|
): ContrarianSelection | undefined {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1198
|
+
const model = ctx.model;
|
|
1199
|
+
if (!model || !isModelInScope(ctx, model)) return undefined;
|
|
1200
|
+
return toContrarianSelection(
|
|
1201
|
+
model,
|
|
1202
|
+
thinkingLevelOverride,
|
|
1203
|
+
`Used the current session model ${model.provider}/${model.id} as a final fallback.`,
|
|
1204
|
+
);
|
|
1178
1205
|
}
|
|
1179
1206
|
|
|
1180
|
-
function withFallbackReason(
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1207
|
+
function withFallbackReason(
|
|
1208
|
+
selection: ContrarianSelection,
|
|
1209
|
+
previous: ContrarianSelection,
|
|
1210
|
+
): ContrarianSelection {
|
|
1211
|
+
return {
|
|
1212
|
+
...selection,
|
|
1213
|
+
selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
|
|
1214
|
+
};
|
|
1185
1215
|
}
|
|
1186
1216
|
|
|
1187
1217
|
async function selectContrarianModel(
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
): Promise<
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1218
|
+
ctx: ModelSelectionContext,
|
|
1219
|
+
thinkingLevelOverride?: ThinkingLevel,
|
|
1220
|
+
): Promise<
|
|
1221
|
+
| { ok: true; selection: ContrarianSelection; ordered: ContrarianSelection[] }
|
|
1222
|
+
| { ok: false; error: string }
|
|
1223
|
+
> {
|
|
1224
|
+
const available = await getAutoSelectableModels(ctx);
|
|
1225
|
+
if (available.length === 0) {
|
|
1226
|
+
return {
|
|
1227
|
+
ok: false,
|
|
1228
|
+
error: hasModelScope(ctx)
|
|
1229
|
+
? "No authenticated models are available in the current session model scope. Adjust the scope, log in, or configure an API key."
|
|
1230
|
+
: "No authenticated models are available. Log in or configure an API key first.",
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const currentModel = ctx.model;
|
|
1235
|
+
const currentProvider = currentModel?.provider;
|
|
1236
|
+
const sameProvider = currentProvider
|
|
1237
|
+
? available.filter((model) => model.provider === currentProvider)
|
|
1238
|
+
: [];
|
|
1239
|
+
const sameProviderReasoning = sameProvider.filter((model) => model.reasoning);
|
|
1240
|
+
const allReasoning = available.filter((model) => model.reasoning);
|
|
1241
|
+
const oppositeFamily = currentModel
|
|
1242
|
+
? isAnthropicFamily(currentModel)
|
|
1243
|
+
? available.filter(isOpenAiFamily)
|
|
1244
|
+
: isOpenAiFamily(currentModel)
|
|
1245
|
+
? available.filter(isAnthropicFamily)
|
|
1246
|
+
: []
|
|
1247
|
+
: [];
|
|
1248
|
+
const oppositeProvider = currentProvider
|
|
1249
|
+
? available.filter((model) => model.provider !== currentProvider)
|
|
1250
|
+
: [];
|
|
1251
|
+
const oppositeProviderFamily = currentProvider
|
|
1252
|
+
? oppositeFamily.filter((model) => model.provider !== currentProvider)
|
|
1253
|
+
: oppositeFamily;
|
|
1254
|
+
const sameProviderOppositeFamily = currentProvider
|
|
1255
|
+
? oppositeFamily.filter((model) => model.provider === currentProvider)
|
|
1256
|
+
: [];
|
|
1257
|
+
const oppositeProviderFamilyReasoning = oppositeProviderFamily.filter((model) => model.reasoning);
|
|
1258
|
+
const sameProviderOppositeFamilyReasoning = sameProviderOppositeFamily.filter(
|
|
1259
|
+
(model) => model.reasoning,
|
|
1260
|
+
);
|
|
1261
|
+
const oppositeProviderReasoning = oppositeProvider.filter((model) => model.reasoning);
|
|
1262
|
+
|
|
1263
|
+
let candidates = oppositeProviderFamilyReasoning;
|
|
1264
|
+
let reason =
|
|
1265
|
+
"Selected the top-ranked reasoning model from the opposite model family on a different provider for an independent adversarial pass.";
|
|
1266
|
+
let providerForPreferences: string | undefined = undefined;
|
|
1267
|
+
|
|
1268
|
+
if (candidates.length === 0 && oppositeProviderFamily.length > 0) {
|
|
1269
|
+
candidates = oppositeProviderFamily;
|
|
1270
|
+
reason =
|
|
1271
|
+
"No reasoning models were available in the opposite model family on a different provider, so the top-ranked opposite-family model on a different provider was used.";
|
|
1272
|
+
} else if (candidates.length === 0 && oppositeProviderReasoning.length > 0) {
|
|
1273
|
+
candidates = oppositeProviderReasoning;
|
|
1274
|
+
reason =
|
|
1275
|
+
"No recognized opposite-family model was available, so the top-ranked reasoning model on a different provider was used.";
|
|
1276
|
+
} else if (candidates.length === 0 && oppositeProvider.length > 0) {
|
|
1277
|
+
candidates = oppositeProvider;
|
|
1278
|
+
reason =
|
|
1279
|
+
"No recognized opposite-family reasoning model was available, so the top-ranked model on a different provider was used.";
|
|
1280
|
+
} else if (candidates.length === 0 && sameProviderOppositeFamilyReasoning.length > 0) {
|
|
1281
|
+
candidates = sameProviderOppositeFamilyReasoning;
|
|
1282
|
+
reason =
|
|
1283
|
+
"No different provider was available, so the top-ranked reasoning model from the opposite model family on the current provider was used.";
|
|
1284
|
+
} else if (candidates.length === 0 && sameProviderOppositeFamily.length > 0) {
|
|
1285
|
+
candidates = sameProviderOppositeFamily;
|
|
1286
|
+
reason =
|
|
1287
|
+
"No different provider or opposite-family reasoning model was available, so the top-ranked opposite-family model on the current provider was used.";
|
|
1288
|
+
} else if (candidates.length === 0 && sameProviderReasoning.length > 0) {
|
|
1289
|
+
candidates = sameProviderReasoning;
|
|
1290
|
+
providerForPreferences = currentProvider;
|
|
1291
|
+
reason =
|
|
1292
|
+
"No opposite provider/model family was available, so the top-ranked reasoning model on the current provider was used.";
|
|
1293
|
+
} else if (candidates.length === 0 && sameProvider.length > 0) {
|
|
1294
|
+
candidates = sameProvider;
|
|
1295
|
+
providerForPreferences = currentProvider;
|
|
1296
|
+
reason =
|
|
1297
|
+
"No opposite provider/model family or current-provider reasoning model was available, so the top-ranked model on the current provider was used.";
|
|
1298
|
+
} else if (candidates.length === 0 && allReasoning.length > 0) {
|
|
1299
|
+
candidates = allReasoning;
|
|
1300
|
+
reason =
|
|
1301
|
+
"No current model/provider was active, so the top-ranked reasoning model across all available providers was used.";
|
|
1302
|
+
} else if (candidates.length === 0) {
|
|
1303
|
+
candidates = available;
|
|
1304
|
+
reason =
|
|
1305
|
+
"No reasoning models were available, so the top-ranked model across all available providers was used.";
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
|
|
1309
|
+
const preferred = providerForPreferences
|
|
1310
|
+
? selectPreferredModel(candidates, providerForPreferences)
|
|
1311
|
+
: selectPreferredAcrossProviders(candidates);
|
|
1312
|
+
const orderedModels = preferred
|
|
1313
|
+
? [preferred, ...sorted.filter((model) => model !== preferred)]
|
|
1314
|
+
: sorted;
|
|
1315
|
+
|
|
1316
|
+
const ordered = orderedModels.map((model, index) =>
|
|
1317
|
+
toContrarianSelection(
|
|
1318
|
+
model,
|
|
1319
|
+
thinkingLevelOverride,
|
|
1320
|
+
index === 0
|
|
1321
|
+
? preferred
|
|
1322
|
+
? `Selected ${model.id} via the hardcoded preference lists while preferring an opposite provider/model family.`
|
|
1323
|
+
: reason
|
|
1324
|
+
: `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
|
|
1325
|
+
),
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
return { ok: true, selection: ordered[0], ordered };
|
|
1282
1329
|
}
|
|
1283
1330
|
|
|
1284
1331
|
function updateContrarianUi(ctx: ExtensionContext, activeRuns: Map<string, ContrarianUiRun>): void {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1332
|
+
if (!ctx.hasUI) return;
|
|
1333
|
+
const theme = ctx.ui.theme;
|
|
1334
|
+
if (activeRuns.size === 0) {
|
|
1335
|
+
ctx.ui.setStatus(CONTRARIAN_STATUS_ID, undefined);
|
|
1336
|
+
ctx.ui.setWidget(CONTRARIAN_WIDGET_ID, undefined);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const runs = [...activeRuns.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
1341
|
+
const primary = runs[0];
|
|
1342
|
+
const activeCount = runs.length;
|
|
1343
|
+
const modelText = primary.selection?.modelId ?? "selecting…";
|
|
1344
|
+
const thinkingText =
|
|
1345
|
+
primary.selection?.thinkingLevel && primary.selection.thinkingLevel !== "off"
|
|
1346
|
+
? ` ${primary.selection.thinkingLevel}`
|
|
1347
|
+
: "";
|
|
1348
|
+
const elapsed = formatDuration(Date.now() - primary.startedAt);
|
|
1349
|
+
const mode = primary.includeBash ? "read-only+bash" : "read-only";
|
|
1350
|
+
const status =
|
|
1351
|
+
theme.fg("accent", "🔮 contrarian") +
|
|
1352
|
+
theme.fg("dim", ` ${modelText}${thinkingText} · ${mode} · ${elapsed}`) +
|
|
1353
|
+
(activeCount > 1 ? theme.fg("warning", ` · ${activeCount} active`) : "");
|
|
1354
|
+
ctx.ui.setStatus(CONTRARIAN_STATUS_ID, status);
|
|
1355
|
+
|
|
1356
|
+
const lines = [
|
|
1357
|
+
`🔮 Contrarian ${activeCount > 1 ? `(${activeCount} active)` : ""}`.trim(),
|
|
1358
|
+
`${primary.selection?.modelRef ?? "selecting model…"} · ${mode} · ${elapsed}`,
|
|
1359
|
+
`task: ${shorten(primary.task, 110)}`,
|
|
1360
|
+
];
|
|
1361
|
+
if (primary.preview && primary.preview.trim())
|
|
1362
|
+
lines.push(`preview: ${shorten(primary.preview, 110)}`);
|
|
1363
|
+
ctx.ui.setWidget(CONTRARIAN_WIDGET_ID, lines, { placement: "belowEditor" });
|
|
1315
1364
|
}
|
|
1316
1365
|
|
|
1317
1366
|
async function runContrarian(
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1367
|
+
selection: ContrarianSelection,
|
|
1368
|
+
params: { task: string; includeBash?: boolean; cwd?: string },
|
|
1369
|
+
signal: AbortSignal | undefined,
|
|
1370
|
+
onUpdate:
|
|
1371
|
+
| ((result: {
|
|
1372
|
+
content: Array<{ type: "text"; text: string }>;
|
|
1373
|
+
details: ContrarianDetails;
|
|
1374
|
+
}) => void)
|
|
1375
|
+
| undefined,
|
|
1376
|
+
defaultCwd: string,
|
|
1377
|
+
spawnImpl: typeof spawn = spawn,
|
|
1378
|
+
): Promise<
|
|
1379
|
+
| { ok: true; output: string; details: ContrarianDetails }
|
|
1380
|
+
| { ok: false; error: string; details: ContrarianDetails }
|
|
1381
|
+
> {
|
|
1382
|
+
const cwd = params.cwd ?? defaultCwd;
|
|
1383
|
+
const includeBash = params.includeBash ?? false;
|
|
1384
|
+
const tools = includeBash ? READ_ONLY_PLUS_BASH_TOOLS : READ_ONLY_TOOLS;
|
|
1385
|
+
const startedAt = Date.now();
|
|
1386
|
+
const usage = createEmptyUsage();
|
|
1387
|
+
let currentText = "";
|
|
1388
|
+
let finalOutput = "";
|
|
1389
|
+
let stderr = "";
|
|
1390
|
+
|
|
1391
|
+
let lastStopReason: string | undefined;
|
|
1392
|
+
let lastErrorMessage: string | undefined;
|
|
1393
|
+
|
|
1394
|
+
const details: ContrarianDetails = {
|
|
1395
|
+
...selection,
|
|
1396
|
+
includeBash,
|
|
1397
|
+
usage,
|
|
1398
|
+
stderr,
|
|
1399
|
+
exitCode: 0,
|
|
1400
|
+
durationMs: 0,
|
|
1401
|
+
cwd,
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
const emit = () => {
|
|
1405
|
+
details.stderr = stderr;
|
|
1406
|
+
details.durationMs = Date.now() - startedAt;
|
|
1407
|
+
onUpdate?.({
|
|
1408
|
+
content: [{ type: "text", text: currentText || finalOutput || "Consulting contrarian..." }],
|
|
1409
|
+
details,
|
|
1410
|
+
});
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
const modelArg = withThinking(selection.modelRef, selection.thinkingLevel);
|
|
1414
|
+
const args = [
|
|
1415
|
+
"--mode",
|
|
1416
|
+
"json",
|
|
1417
|
+
"-p",
|
|
1418
|
+
"--no-session",
|
|
1419
|
+
"--model",
|
|
1420
|
+
modelArg,
|
|
1421
|
+
"--tools",
|
|
1422
|
+
tools.join(","),
|
|
1423
|
+
"--append-system-prompt",
|
|
1424
|
+
CONTRARIAN_SYSTEM_PROMPT,
|
|
1425
|
+
params.task,
|
|
1426
|
+
];
|
|
1427
|
+
|
|
1428
|
+
const invocation = getPiInvocation(args);
|
|
1429
|
+
let wasAborted = false;
|
|
1430
|
+
|
|
1431
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
1432
|
+
const proc = spawnImpl(invocation.command, invocation.args, {
|
|
1433
|
+
cwd,
|
|
1434
|
+
shell: false,
|
|
1435
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1436
|
+
});
|
|
1437
|
+
let buffer = "";
|
|
1438
|
+
|
|
1439
|
+
const processLine = (line: string) => {
|
|
1440
|
+
if (!line.trim()) return;
|
|
1441
|
+
let event: any;
|
|
1442
|
+
try {
|
|
1443
|
+
event = JSON.parse(line);
|
|
1444
|
+
} catch {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
1449
|
+
// A later assistant turn supersedes any earlier tool/intermediate answer.
|
|
1450
|
+
// Keep only the current message so a tool-only final turn cannot return stale text.
|
|
1451
|
+
currentText = "";
|
|
1452
|
+
finalOutput = "";
|
|
1453
|
+
emit();
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
1458
|
+
currentText += event.assistantMessageEvent.delta ?? "";
|
|
1459
|
+
emit();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
1464
|
+
const text = extractTextFromContent(event.message.content) || currentText;
|
|
1465
|
+
// message_end is authoritative, including an empty tool-only message.
|
|
1466
|
+
finalOutput = text;
|
|
1467
|
+
currentText = "";
|
|
1468
|
+
|
|
1469
|
+
const stopReason = event.message.stopReason;
|
|
1470
|
+
lastStopReason = typeof stopReason === "string" ? stopReason : undefined;
|
|
1471
|
+
const errorMessageField = event.message.errorMessage;
|
|
1472
|
+
lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
|
|
1473
|
+
|
|
1474
|
+
addUsageStats(usage, event.message.usage as Usage | undefined, {
|
|
1475
|
+
countTurn: true,
|
|
1476
|
+
updateContextTokens: true,
|
|
1477
|
+
});
|
|
1478
|
+
emit();
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (event.type === "compaction_end") {
|
|
1483
|
+
addUsageStats(usage, event.result?.usage as Usage | undefined);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
proc.stdout.on("data", (data) => {
|
|
1488
|
+
buffer += data.toString();
|
|
1489
|
+
const lines = buffer.split("\n");
|
|
1490
|
+
buffer = lines.pop() || "";
|
|
1491
|
+
for (const line of lines) processLine(line);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
proc.stderr.on("data", (data) => {
|
|
1495
|
+
stderr += data.toString();
|
|
1496
|
+
emit();
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
let closed = false;
|
|
1500
|
+
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1501
|
+
let abortListener: (() => void) | undefined;
|
|
1502
|
+
|
|
1503
|
+
const childHasExited = () => proc.exitCode !== null || proc.signalCode !== null;
|
|
1504
|
+
const clearEscalation = () => {
|
|
1505
|
+
if (killTimer) {
|
|
1506
|
+
clearTimeout(killTimer);
|
|
1507
|
+
killTimer = undefined;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
const cleanup = () => {
|
|
1511
|
+
clearEscalation();
|
|
1512
|
+
if (signal && abortListener) {
|
|
1513
|
+
signal.removeEventListener("abort", abortListener);
|
|
1514
|
+
abortListener = undefined;
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
proc.on("exit", () => {
|
|
1519
|
+
clearEscalation();
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
proc.on("close", (code) => {
|
|
1523
|
+
closed = true;
|
|
1524
|
+
cleanup();
|
|
1525
|
+
if (buffer.trim()) processLine(buffer);
|
|
1526
|
+
resolve(code ?? 0);
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
proc.on("error", (error) => {
|
|
1530
|
+
cleanup();
|
|
1531
|
+
stderr += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
1532
|
+
resolve(1);
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
if (signal) {
|
|
1536
|
+
abortListener = () => {
|
|
1537
|
+
wasAborted = true;
|
|
1538
|
+
proc.kill("SIGTERM");
|
|
1539
|
+
if (closed || childHasExited()) return;
|
|
1540
|
+
killTimer = setTimeout(() => {
|
|
1541
|
+
if (!closed && !childHasExited()) proc.kill("SIGKILL");
|
|
1542
|
+
}, 5000);
|
|
1543
|
+
};
|
|
1544
|
+
if (signal.aborted) abortListener();
|
|
1545
|
+
else signal.addEventListener("abort", abortListener, { once: true });
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
details.stderr = stderr.trim();
|
|
1550
|
+
details.exitCode = exitCode;
|
|
1551
|
+
details.durationMs = Date.now() - startedAt;
|
|
1552
|
+
|
|
1553
|
+
if (wasAborted) {
|
|
1554
|
+
if (details.exitCode === 0) details.exitCode = 1;
|
|
1555
|
+
return { ok: false, error: "Contrarian was aborted.", details };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// The pi subprocess in `--mode json` does not promote an errored assistant
|
|
1559
|
+
// turn to a non-zero exit code or stderr; the error is only carried on the
|
|
1560
|
+
// streamed assistant message via stopReason/errorMessage. Surface that here
|
|
1561
|
+
// so callers can distinguish transient provider errors from a genuinely
|
|
1562
|
+
// empty response.
|
|
1563
|
+
if (lastStopReason === "error" || lastStopReason === "aborted") {
|
|
1564
|
+
if (details.exitCode === 0) details.exitCode = 1;
|
|
1565
|
+
return {
|
|
1566
|
+
ok: false,
|
|
1567
|
+
error: formatContrarianModelError(lastStopReason, lastErrorMessage),
|
|
1568
|
+
details,
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (exitCode !== 0) {
|
|
1573
|
+
return {
|
|
1574
|
+
ok: false,
|
|
1575
|
+
error: details.stderr || finalOutput || "Contrarian subprocess failed.",
|
|
1576
|
+
details,
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (!finalOutput.trim()) {
|
|
1581
|
+
if (details.exitCode === 0) details.exitCode = 1;
|
|
1582
|
+
return {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
error: details.stderr || "Contrarian finished without returning any text.",
|
|
1585
|
+
details,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
ok: true,
|
|
1591
|
+
output: finalOutput.trim(),
|
|
1592
|
+
details,
|
|
1593
|
+
};
|
|
1537
1594
|
}
|
|
1538
1595
|
|
|
1539
1596
|
function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): string {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1597
|
+
const lines = text.trim().split("\n");
|
|
1598
|
+
if (lines.length <= lineLimit) return lines.join("\n");
|
|
1599
|
+
return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
|
|
1543
1600
|
}
|
|
1544
1601
|
|
|
1545
1602
|
type ContrarianExtensionDeps = {
|
|
1546
|
-
|
|
1603
|
+
spawnImpl?: typeof spawn;
|
|
1547
1604
|
};
|
|
1548
1605
|
|
|
1549
1606
|
export const __test__ = {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1607
|
+
buildSessionFallbackSelection,
|
|
1608
|
+
createContrarianExtension,
|
|
1609
|
+
findAvailableModel,
|
|
1610
|
+
isModelAvailabilityError,
|
|
1611
|
+
parseModelPreference,
|
|
1612
|
+
runContrarian,
|
|
1613
|
+
resolveThinkingLevel,
|
|
1614
|
+
selectContrarianModel,
|
|
1558
1615
|
};
|
|
1559
1616
|
|
|
1560
1617
|
export default function contrarianExtension(pi: ExtensionAPI) {
|
|
1561
|
-
|
|
1618
|
+
createContrarianExtension(pi);
|
|
1562
1619
|
}
|
|
1563
1620
|
|
|
1564
1621
|
function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDeps = {}) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1622
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
1623
|
+
const activeRuns = new Map<string, ContrarianUiRun>();
|
|
1624
|
+
let preferences: ContrarianPreferences = {};
|
|
1625
|
+
|
|
1626
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1627
|
+
preferences = await readContrarianPreferences();
|
|
1628
|
+
activeRuns.clear();
|
|
1629
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
pi.registerCommand("contrarian", {
|
|
1633
|
+
description:
|
|
1634
|
+
"Configure Contrarian default model and thinking level for future contrarian tool calls",
|
|
1635
|
+
getArgumentCompletions: (prefix) => {
|
|
1636
|
+
const parts = prefix.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1637
|
+
const first = parts[0] ?? "";
|
|
1638
|
+
if (parts.length <= 1) {
|
|
1639
|
+
const commands = ["status", "model", "thinking", "clear"];
|
|
1640
|
+
const matches = commands.filter((command) => command.startsWith(first));
|
|
1641
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1642
|
+
}
|
|
1643
|
+
if (parts[0] === "thinking") {
|
|
1644
|
+
const query = parts[1] ?? "";
|
|
1645
|
+
const values = ["auto", ...THINKING_LEVELS];
|
|
1646
|
+
const matches = values.filter((value) => value.startsWith(query));
|
|
1647
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1648
|
+
}
|
|
1649
|
+
if (parts[0] === "clear") {
|
|
1650
|
+
const query = parts[1] ?? "";
|
|
1651
|
+
const values = ["all", "model", "thinking"];
|
|
1652
|
+
const matches = values.filter((value) => value.startsWith(query));
|
|
1653
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
},
|
|
1657
|
+
handler: async (args, ctx) => {
|
|
1658
|
+
const raw = args.trim();
|
|
1659
|
+
const tokens = raw ? raw.split(/\s+/) : [];
|
|
1660
|
+
const [command = "status", ...rest] = tokens;
|
|
1661
|
+
const action = command.toLowerCase();
|
|
1662
|
+
const configPath = getContrarianConfigPath();
|
|
1663
|
+
|
|
1664
|
+
const save = async (next: ContrarianPreferences): Promise<string | undefined> => {
|
|
1665
|
+
preferences = next;
|
|
1666
|
+
try {
|
|
1667
|
+
await writeContrarianPreferences(preferences);
|
|
1668
|
+
return undefined;
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1671
|
+
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
if (action === "status" || action === "show") {
|
|
1676
|
+
notifyCommand(ctx, formatContrarianPreferences(preferences));
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (action === "model") {
|
|
1681
|
+
const model = rest.join(" ").trim();
|
|
1682
|
+
const normalizedModelAction = model.toLowerCase();
|
|
1683
|
+
if (!model) {
|
|
1684
|
+
notifyCommand(ctx, "Usage: /contrarian model <provider/model|auto>", "warning");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (
|
|
1688
|
+
normalizedModelAction === "auto" ||
|
|
1689
|
+
normalizedModelAction === "clear" ||
|
|
1690
|
+
normalizedModelAction === "default"
|
|
1691
|
+
) {
|
|
1692
|
+
const warning = await save({ ...preferences, model: undefined });
|
|
1693
|
+
notifyCommand(
|
|
1694
|
+
ctx,
|
|
1695
|
+
`Contrarian default model cleared; future contrarian calls will auto-select. ${warning ?? formatContrarianPreferences(preferences)}`,
|
|
1696
|
+
warning ? "warning" : "info",
|
|
1697
|
+
);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const parsedModel = parseModelPreference(model);
|
|
1701
|
+
const next = {
|
|
1702
|
+
...preferences,
|
|
1703
|
+
model: parsedModel.model,
|
|
1704
|
+
thinkingLevel: parsedModel.thinkingLevel ?? preferences.thinkingLevel,
|
|
1705
|
+
};
|
|
1706
|
+
const warning = await save(next);
|
|
1707
|
+
notifyCommand(
|
|
1708
|
+
ctx,
|
|
1709
|
+
`Contrarian default model set to ${parsedModel.model}. ${warning ?? formatContrarianPreferences(preferences)}`,
|
|
1710
|
+
warning ? "warning" : "info",
|
|
1711
|
+
);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (action === "thinking" || action === "think" || action === "thinking-level") {
|
|
1716
|
+
const value = rest[0]?.trim().toLowerCase();
|
|
1717
|
+
if (!value) {
|
|
1718
|
+
notifyCommand(
|
|
1719
|
+
ctx,
|
|
1720
|
+
`Usage: /contrarian thinking ${THINKING_LEVELS.join(" | ")} | auto`,
|
|
1721
|
+
"warning",
|
|
1722
|
+
);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (value === "auto" || value === "clear" || value === "default") {
|
|
1726
|
+
const warning = await save({ ...preferences, thinkingLevel: undefined });
|
|
1727
|
+
notifyCommand(
|
|
1728
|
+
ctx,
|
|
1729
|
+
`Contrarian default thinking level cleared; future contrarian calls will use built-in defaults. ${warning ?? formatContrarianPreferences(preferences)}`,
|
|
1730
|
+
warning ? "warning" : "info",
|
|
1731
|
+
);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const thinkingLevel = parseThinkingLevel(value);
|
|
1735
|
+
if (!thinkingLevel) {
|
|
1736
|
+
notifyCommand(
|
|
1737
|
+
ctx,
|
|
1738
|
+
`Usage: /contrarian thinking ${THINKING_LEVELS.join(" | ")} | auto`,
|
|
1739
|
+
"warning",
|
|
1740
|
+
);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const warning = await save({ ...preferences, thinkingLevel });
|
|
1744
|
+
notifyCommand(
|
|
1745
|
+
ctx,
|
|
1746
|
+
`Contrarian default thinking level set to ${thinkingLevel}. ${warning ?? formatContrarianPreferences(preferences)}`,
|
|
1747
|
+
warning ? "warning" : "info",
|
|
1748
|
+
);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (action === "clear" || action === "reset") {
|
|
1753
|
+
const target = rest[0]?.trim().toLowerCase() || "all";
|
|
1754
|
+
let next: ContrarianPreferences;
|
|
1755
|
+
if (target === "all") next = {};
|
|
1756
|
+
else if (target === "model") next = { ...preferences, model: undefined };
|
|
1757
|
+
else if (target === "thinking" || target === "thinking-level")
|
|
1758
|
+
next = { ...preferences, thinkingLevel: undefined };
|
|
1759
|
+
else {
|
|
1760
|
+
notifyCommand(ctx, "Usage: /contrarian clear [all|model|thinking]", "warning");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const warning = await save(next);
|
|
1764
|
+
notifyCommand(
|
|
1765
|
+
ctx,
|
|
1766
|
+
`Contrarian defaults cleared (${target}). ${warning ?? formatContrarianPreferences(preferences)}`,
|
|
1767
|
+
warning ? "warning" : "info",
|
|
1768
|
+
);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
notifyCommand(
|
|
1773
|
+
ctx,
|
|
1774
|
+
"Usage: /contrarian status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|max|auto> | clear [all|model|thinking]",
|
|
1775
|
+
"warning",
|
|
1776
|
+
);
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
pi.registerCommand("contrarian-model", {
|
|
1781
|
+
description: "Show which model the contrarian would use right now",
|
|
1782
|
+
handler: async (_args, ctx) => {
|
|
1783
|
+
const defaultModel = parseModelPreference(preferences.model);
|
|
1784
|
+
if (defaultModel.model) {
|
|
1785
|
+
const matched = await findAvailableModel(ctx, defaultModel.model);
|
|
1786
|
+
const thinking = resolveThinkingLevel(
|
|
1787
|
+
matched,
|
|
1788
|
+
preferences.thinkingLevel ?? defaultModel.thinkingLevel,
|
|
1789
|
+
);
|
|
1790
|
+
const modelRef = matched ? `${matched.provider}/${matched.id}` : defaultModel.model;
|
|
1791
|
+
const suffix = matched
|
|
1792
|
+
? appendThinkingLevelClampReason(
|
|
1793
|
+
"Configured default contrarian model is active.",
|
|
1794
|
+
thinking,
|
|
1795
|
+
)
|
|
1796
|
+
: "Configured default contrarian model is active, but it was not matched against the authenticated model list.";
|
|
1797
|
+
notifyCommand(ctx, `Contrarian: ${modelRef} (${thinking.effective}) — ${suffix}`);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const selectionResult = await selectContrarianModel(ctx, preferences.thinkingLevel);
|
|
1801
|
+
if (!selectionResult.ok) {
|
|
1802
|
+
notifyCommand(ctx, selectionResult.error, "error");
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const { selection } = selectionResult;
|
|
1807
|
+
const message = `Contrarian: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
|
|
1808
|
+
notifyCommand(ctx, message);
|
|
1809
|
+
},
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
pi.on("tool_result", async (event) => {
|
|
1813
|
+
if (event.toolName !== "contrarian") return undefined;
|
|
1814
|
+
const details = event.details as ContrarianDetails | undefined;
|
|
1815
|
+
if ((details?.exitCode ?? 0) === 0) return undefined;
|
|
1816
|
+
return { isError: true };
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
pi.registerTool({
|
|
1820
|
+
name: "contrarian",
|
|
1821
|
+
label: "Contrarian",
|
|
1822
|
+
description:
|
|
1823
|
+
"Consult a separate contrarian subprocess with read-only built-in tools and configured MCP tools that stress-tests plans, designs, assumptions, bug hypotheses, and conclusions by steelmanning the strongest opposing case.",
|
|
1824
|
+
promptSnippet:
|
|
1825
|
+
"Consult a contrarian subprocess that prefers a strong opposite-family/provider model for adversarial analysis; exposes read-only built-in tools plus any configured MCP tools.",
|
|
1826
|
+
promptGuidelines: [
|
|
1827
|
+
"Use contrarian when you need to stress-test a proposal, implementation plan, design, assumption, bug hypothesis, review conclusion, or product direction.",
|
|
1828
|
+
"Ask contrarian to steelman the strongest opposing case and separate confirmed objections, plausible concerns, and unresolved unknowns.",
|
|
1829
|
+
"Do not use contrarian for routine low-value work; contrarian is slower than the main agent.",
|
|
1830
|
+
"The contrarian tool exposes read-only built-in tools (read, grep, find, and ls) plus any configured MCP tools; contrarian includeBash adds the bash inspection tool.",
|
|
1831
|
+
"Set contrarian includeBash only when the extra bash inspection tool is genuinely useful; keep contrarian relying on read, grep, find, and ls otherwise.",
|
|
1832
|
+
"Configured MCP tools are available to contrarian and run without confirmation inside the subagent; they may have side effects — use them only for gathering evidence, not for state-changing actions unless the task explicitly asks. MCP access requires host Pi >=1.0.4; older hosts degrade to no MCP, no error.",
|
|
1833
|
+
"The contrarian tool requests high by default for reasoning models; contrarian defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
|
|
1834
|
+
],
|
|
1835
|
+
parameters: ContrarianParams,
|
|
1836
|
+
|
|
1837
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1838
|
+
const rawTask = (params as { task?: unknown }).task;
|
|
1839
|
+
const task = typeof rawTask === "string" ? rawTask.trim() : "";
|
|
1840
|
+
if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
|
|
1841
|
+
|
|
1842
|
+
const explicitModel = parseModelPreference(params.model);
|
|
1843
|
+
const defaultModel = parseModelPreference(preferences.model);
|
|
1844
|
+
const configuredModel = explicitModel.model ?? defaultModel.model;
|
|
1845
|
+
const configuredThinkingLevel =
|
|
1846
|
+
params.thinkingLevel ??
|
|
1847
|
+
explicitModel.thinkingLevel ??
|
|
1848
|
+
preferences.thinkingLevel ??
|
|
1849
|
+
defaultModel.thinkingLevel;
|
|
1850
|
+
const uiRun: ContrarianUiRun = {
|
|
1851
|
+
task,
|
|
1852
|
+
includeBash: params.includeBash ?? false,
|
|
1853
|
+
startedAt: Date.now(),
|
|
1854
|
+
};
|
|
1855
|
+
activeRuns.set(toolCallId, uiRun);
|
|
1856
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1857
|
+
|
|
1858
|
+
try {
|
|
1859
|
+
const attempts: ContrarianSelection[] = [];
|
|
1860
|
+
const seen = new Set<string>();
|
|
1861
|
+
const aggregateUsage = createEmptyUsage();
|
|
1862
|
+
const finalizeResult = (result: {
|
|
1863
|
+
content: Array<{ type: "text"; text: string }>;
|
|
1864
|
+
details: ContrarianDetails;
|
|
1865
|
+
}): AgentToolResult<ContrarianDetails> => ({
|
|
1866
|
+
...result,
|
|
1867
|
+
details: { ...result.details, usage: cloneUsageStats(aggregateUsage) },
|
|
1868
|
+
usage: usageStatsToToolUsage(aggregateUsage),
|
|
1869
|
+
});
|
|
1870
|
+
const pushAttempt = (candidate: ContrarianSelection | undefined) => {
|
|
1871
|
+
if (!candidate) return;
|
|
1872
|
+
const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
|
|
1873
|
+
if (seen.has(key)) return;
|
|
1874
|
+
seen.add(key);
|
|
1875
|
+
attempts.push(candidate);
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
if (configuredModel) {
|
|
1879
|
+
const modelRef = configuredModel;
|
|
1880
|
+
const matched = await findAvailableModel(ctx, modelRef);
|
|
1881
|
+
const provider =
|
|
1882
|
+
matched?.provider ??
|
|
1883
|
+
(modelRef.includes("/") ? modelRef.split("/")[0] : (ctx.model?.provider ?? "unknown"));
|
|
1884
|
+
const modelId =
|
|
1885
|
+
matched?.id ??
|
|
1886
|
+
(modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
|
|
1887
|
+
const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
|
|
1888
|
+
const usedToolOverride = !!explicitModel.model;
|
|
1889
|
+
const selectionReason = matched
|
|
1890
|
+
? appendThinkingLevelClampReason(
|
|
1891
|
+
usedToolOverride
|
|
1892
|
+
? "Used the explicit model override provided in the tool call."
|
|
1893
|
+
: "Used the configured default contrarian model.",
|
|
1894
|
+
thinking,
|
|
1895
|
+
)
|
|
1896
|
+
: usedToolOverride
|
|
1897
|
+
? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
|
|
1898
|
+
: "Used the configured default contrarian model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
1899
|
+
pushAttempt({
|
|
1900
|
+
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
1901
|
+
provider,
|
|
1902
|
+
modelId,
|
|
1903
|
+
modelName: matched?.name,
|
|
1904
|
+
thinkingLevel: thinking.effective,
|
|
1905
|
+
...(matched && thinking.clamped
|
|
1906
|
+
? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
|
|
1907
|
+
: {}),
|
|
1908
|
+
autoSelected: false,
|
|
1909
|
+
selectionReason,
|
|
1910
|
+
});
|
|
1911
|
+
} else {
|
|
1912
|
+
const selectionResult = await selectContrarianModel(ctx, configuredThinkingLevel);
|
|
1913
|
+
if (!selectionResult.ok) {
|
|
1914
|
+
return finalizeResult({
|
|
1915
|
+
content: [{ type: "text", text: selectionResult.error }],
|
|
1916
|
+
details: {
|
|
1917
|
+
modelRef: "",
|
|
1918
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
1919
|
+
modelId: "",
|
|
1920
|
+
modelName: undefined,
|
|
1921
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1922
|
+
autoSelected: true,
|
|
1923
|
+
selectionReason: selectionResult.error,
|
|
1924
|
+
includeBash: params.includeBash ?? false,
|
|
1925
|
+
usage: createEmptyUsage(),
|
|
1926
|
+
stderr: "",
|
|
1927
|
+
exitCode: 1,
|
|
1928
|
+
durationMs: 0,
|
|
1929
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
1930
|
+
},
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
for (const candidate of selectionResult.ordered) pushAttempt(candidate);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
|
|
1937
|
+
|
|
1938
|
+
const handleUpdate = (partial: {
|
|
1939
|
+
content: Array<{ type: "text"; text: string }>;
|
|
1940
|
+
details: ContrarianDetails;
|
|
1941
|
+
}) => {
|
|
1942
|
+
uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
|
|
1943
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1944
|
+
onUpdate?.(partial);
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
let lastErrorResult: { ok: false; error: string; details: ContrarianDetails } | undefined;
|
|
1948
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
1949
|
+
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1950
|
+
const attempt = previous
|
|
1951
|
+
? withFallbackReason(attempts[index], previous)
|
|
1952
|
+
: attempts[index];
|
|
1953
|
+
uiRun.selection = attempt;
|
|
1954
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1955
|
+
|
|
1956
|
+
const result = await runContrarian(
|
|
1957
|
+
attempt,
|
|
1958
|
+
{ ...params, task },
|
|
1959
|
+
signal,
|
|
1960
|
+
handleUpdate,
|
|
1961
|
+
ctx.cwd,
|
|
1962
|
+
spawnImpl,
|
|
1963
|
+
);
|
|
1964
|
+
mergeUsageStats(aggregateUsage, result.details.usage);
|
|
1965
|
+
if (result.ok) {
|
|
1966
|
+
return finalizeResult({
|
|
1967
|
+
content: [{ type: "text", text: result.output }],
|
|
1968
|
+
details: result.details,
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
lastErrorResult = result;
|
|
1973
|
+
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1974
|
+
if (!canFallBack) {
|
|
1975
|
+
return finalizeResult({
|
|
1976
|
+
content: [{ type: "text", text: result.error }],
|
|
1977
|
+
details: result.details,
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
return finalizeResult({
|
|
1983
|
+
content: [
|
|
1984
|
+
{
|
|
1985
|
+
type: "text",
|
|
1986
|
+
text: lastErrorResult?.error ?? "Contrarian could not select an available model.",
|
|
1987
|
+
},
|
|
1988
|
+
],
|
|
1989
|
+
details: lastErrorResult?.details ?? {
|
|
1990
|
+
modelRef: "",
|
|
1991
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
1992
|
+
modelId: "",
|
|
1993
|
+
modelName: undefined,
|
|
1994
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1995
|
+
autoSelected: true,
|
|
1996
|
+
selectionReason: "No authenticated models were available to run the contrarian.",
|
|
1997
|
+
includeBash: params.includeBash ?? false,
|
|
1998
|
+
usage: createEmptyUsage(),
|
|
1999
|
+
stderr: "",
|
|
2000
|
+
exitCode: 1,
|
|
2001
|
+
durationMs: 0,
|
|
2002
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
2003
|
+
},
|
|
2004
|
+
});
|
|
2005
|
+
} finally {
|
|
2006
|
+
activeRuns.delete(toolCallId);
|
|
2007
|
+
updateContrarianUi(ctx, activeRuns);
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
|
|
2011
|
+
renderCall(args, theme) {
|
|
2012
|
+
const task = args.task.length > 90 ? `${args.task.slice(0, 90)}...` : args.task;
|
|
2013
|
+
const mode = args.includeBash ? "read-only+bash" : "read-only";
|
|
2014
|
+
const override = args.model ? ` ${theme.fg("muted", `[${args.model}]`)}` : "";
|
|
2015
|
+
const thinking = args.thinkingLevel
|
|
2016
|
+
? ` ${theme.fg("warning", `(${args.thinkingLevel})`)}`
|
|
2017
|
+
: "";
|
|
2018
|
+
return new Text(
|
|
2019
|
+
`${theme.fg("toolTitle", theme.bold("contrarian "))}${theme.fg("accent", mode)}${override}${thinking}\n ${theme.fg("dim", task)}`,
|
|
2020
|
+
0,
|
|
2021
|
+
0,
|
|
2022
|
+
);
|
|
2023
|
+
},
|
|
2024
|
+
|
|
2025
|
+
renderResult(result, { expanded }, theme) {
|
|
2026
|
+
const details = result.details as ContrarianDetails | undefined;
|
|
2027
|
+
const body = result.content[0]?.type === "text" ? result.content[0].text : "(no output)";
|
|
2028
|
+
if (!details) return new Text(body, 0, 0);
|
|
2029
|
+
|
|
2030
|
+
const isError = (details.exitCode ?? 0) !== 0;
|
|
2031
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
2032
|
+
const header = `${icon} ${theme.fg("toolTitle", theme.bold("contrarian "))}${theme.fg("accent", details.modelRef || "(auto)")}`;
|
|
2033
|
+
const subheader = [
|
|
2034
|
+
details.thinkingLevel !== "off" ? details.thinkingLevel : undefined,
|
|
2035
|
+
details.includeBash ? "read-only+bash" : "read-only",
|
|
2036
|
+
formatDuration(details.durationMs),
|
|
2037
|
+
]
|
|
2038
|
+
.filter(Boolean)
|
|
2039
|
+
.join(" · ");
|
|
2040
|
+
const usage = formatUsage(details.usage);
|
|
2041
|
+
|
|
2042
|
+
if (!expanded) {
|
|
2043
|
+
let text = header;
|
|
2044
|
+
if (subheader) text += `\n${theme.fg("dim", subheader)}`;
|
|
2045
|
+
text += `\n\n${theme.fg("toolOutput", renderCollapsedText(body))}`;
|
|
2046
|
+
if (usage) text += `\n\n${theme.fg("dim", usage)}`;
|
|
2047
|
+
if (isError && details.stderr)
|
|
2048
|
+
text += `\n${theme.fg("error", renderCollapsedText(details.stderr, 4))}`;
|
|
2049
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
2050
|
+
return new Text(text, 0, 0);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const container = new Container();
|
|
2054
|
+
container.addChild(new Text(header, 0, 0));
|
|
2055
|
+
if (subheader) container.addChild(new Text(theme.fg("dim", subheader), 0, 0));
|
|
2056
|
+
container.addChild(new Spacer(1));
|
|
2057
|
+
container.addChild(new Text(theme.fg("muted", "Selection"), 0, 0));
|
|
2058
|
+
container.addChild(new Text(theme.fg("dim", details.selectionReason), 0, 0));
|
|
2059
|
+
container.addChild(new Spacer(1));
|
|
2060
|
+
container.addChild(new Text(theme.fg("muted", "Output"), 0, 0));
|
|
2061
|
+
container.addChild(new Markdown(body.trim(), 0, 0, getMarkdownTheme()));
|
|
2062
|
+
if (usage) {
|
|
2063
|
+
container.addChild(new Spacer(1));
|
|
2064
|
+
container.addChild(new Text(theme.fg("muted", "Usage"), 0, 0));
|
|
2065
|
+
container.addChild(new Text(theme.fg("dim", usage), 0, 0));
|
|
2066
|
+
}
|
|
2067
|
+
if (details.stderr) {
|
|
2068
|
+
container.addChild(new Spacer(1));
|
|
2069
|
+
container.addChild(new Text(theme.fg("muted", "stderr"), 0, 0));
|
|
2070
|
+
container.addChild(
|
|
2071
|
+
new Text(
|
|
2072
|
+
isError ? theme.fg("error", details.stderr) : theme.fg("dim", details.stderr),
|
|
2073
|
+
0,
|
|
2074
|
+
0,
|
|
2075
|
+
),
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
return container;
|
|
2079
|
+
},
|
|
2080
|
+
});
|
|
1955
2081
|
}
|