pi-teams 0.8.5 → 0.8.6

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/README.md CHANGED
@@ -83,6 +83,17 @@ pi install npm:pi-teams
83
83
  **Customize model and thinking level:**
84
84
  > **You:** "Spawn a teammate named 'architect-bot' using 'gpt-4o' with 'high' thinking level for deep reasoning."
85
85
 
86
+ **Smart Model Resolution:**
87
+ When you specify a model name without a provider (e.g., `gemini-2.5-flash`), pi-teams automatically:
88
+ - Queries available models from `pi --list-models`
89
+ - Prioritizes **OAuth/subscription providers** (cheaper/free) over API-key providers:
90
+ - `google-gemini-cli` (OAuth) is preferred over `google` (API key)
91
+ - `github-copilot`, `kimi-sub` are preferred over their API-key equivalents
92
+ - Falls back to API-key providers if OAuth providers aren't available
93
+ - Constructs the correct `--model provider/model:thinking` command
94
+
95
+ > **Example:** Specifying `gemini-2.5-flash` will automatically use `google-gemini-cli/gemini-2.5-flash` if available, saving API costs.
96
+
86
97
  ### 3. Assign Task & Get Approval
87
98
  > **You:** "Create a task for security-bot: 'Check the .env.example file for sensitive defaults' and set it to in_progress."
88
99
 
@@ -10,6 +10,136 @@ import { getTerminalAdapter } from "../src/adapters/terminal-registry";
10
10
  import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
11
11
  import * as path from "node:path";
12
12
  import * as fs from "node:fs";
13
+ import { spawnSync } from "node:child_process";
14
+
15
+ // Cache for available models
16
+ let availableModelsCache: Array<{ provider: string; model: string }> | null = null;
17
+ let modelsCacheTime = 0;
18
+ const MODELS_CACHE_TTL = 60000; // 1 minute
19
+
20
+ /**
21
+ * Query available models from pi --list-models
22
+ */
23
+ function getAvailableModels(): Array<{ provider: string; model: string }> {
24
+ const now = Date.now();
25
+ if (availableModelsCache && now - modelsCacheTime < MODELS_CACHE_TTL) {
26
+ return availableModelsCache;
27
+ }
28
+
29
+ try {
30
+ const result = spawnSync("pi", ["--list-models"], {
31
+ encoding: "utf-8",
32
+ timeout: 10000,
33
+ });
34
+
35
+ if (result.status !== 0 || !result.stdout) {
36
+ return [];
37
+ }
38
+
39
+ const models: Array<{ provider: string; model: string }> = [];
40
+ const lines = result.stdout.split("\n");
41
+
42
+ for (const line of lines) {
43
+ // Skip header line and empty lines
44
+ if (!line.trim() || line.startsWith("provider")) continue;
45
+
46
+ // Parse: provider model context max-out thinking images
47
+ const parts = line.trim().split(/\s+/);
48
+ if (parts.length >= 2) {
49
+ const provider = parts[0];
50
+ const model = parts[1];
51
+ if (provider && model) {
52
+ models.push({ provider, model });
53
+ }
54
+ }
55
+ }
56
+
57
+ availableModelsCache = models;
58
+ modelsCacheTime = now;
59
+ return models;
60
+ } catch (e) {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Provider priority list - OAuth/subscription providers first (cheaper), then API-key providers
67
+ */
68
+ const PROVIDER_PRIORITY = [
69
+ // OAuth / Subscription providers (typically free/cheaper)
70
+ "google-gemini-cli", // Google Gemini CLI - OAuth, free tier
71
+ "github-copilot", // GitHub Copilot - subscription
72
+ "kimi-sub", // Kimi subscription
73
+ // API key providers
74
+ "anthropic",
75
+ "openai",
76
+ "google",
77
+ "zai",
78
+ "openrouter",
79
+ "azure-openai",
80
+ "amazon-bedrock",
81
+ "mistral",
82
+ "groq",
83
+ "cerebras",
84
+ "xai",
85
+ "vercel-ai-gateway",
86
+ ];
87
+
88
+ /**
89
+ * Find the best matching provider for a given model name.
90
+ * Returns the full provider/model string or null if not found.
91
+ */
92
+ function resolveModelWithProvider(modelName: string): string | null {
93
+ // If already has provider prefix, return as-is
94
+ if (modelName.includes("/")) {
95
+ return modelName;
96
+ }
97
+
98
+ const availableModels = getAvailableModels();
99
+ if (availableModels.length === 0) {
100
+ return null;
101
+ }
102
+
103
+ const lowerModelName = modelName.toLowerCase();
104
+
105
+ // Find all exact matches (case-insensitive) and sort by provider priority
106
+ const exactMatches = availableModels.filter(
107
+ (m) => m.model.toLowerCase() === lowerModelName
108
+ );
109
+
110
+ if (exactMatches.length > 0) {
111
+ // Sort by provider priority (lower index = higher priority)
112
+ exactMatches.sort((a, b) => {
113
+ const aIndex = PROVIDER_PRIORITY.indexOf(a.provider);
114
+ const bIndex = PROVIDER_PRIORITY.indexOf(b.provider);
115
+ // If provider not in priority list, put it at the end
116
+ const aPriority = aIndex === -1 ? 999 : aIndex;
117
+ const bPriority = bIndex === -1 ? 999 : bIndex;
118
+ return aPriority - bPriority;
119
+ });
120
+ return `${exactMatches[0].provider}/${exactMatches[0].model}`;
121
+ }
122
+
123
+ // Try partial match (model name contains the search term)
124
+ const partialMatches = availableModels.filter((m) =>
125
+ m.model.toLowerCase().includes(lowerModelName)
126
+ );
127
+
128
+ if (partialMatches.length > 0) {
129
+ for (const preferredProvider of PROVIDER_PRIORITY) {
130
+ const match = partialMatches.find(
131
+ (m) => m.provider === preferredProvider
132
+ );
133
+ if (match) {
134
+ return `${match.provider}/${match.model}`;
135
+ }
136
+ }
137
+ // Return first match if no preferred provider found
138
+ return `${partialMatches[0].provider}/${partialMatches[0].model}`;
139
+ }
140
+
141
+ return null;
142
+ }
13
143
 
14
144
  export default function (pi: ExtensionAPI) {
15
145
  const isTeammate = !!process.env.PI_AGENT_NAME;
@@ -165,15 +295,17 @@ export default function (pi: ExtensionAPI) {
165
295
  const teamConfig = await teams.readConfig(safeTeamName);
166
296
  let chosenModel = params.model || teamConfig.defaultModel;
167
297
 
168
- if (chosenModel && !chosenModel.includes('/')) {
169
- if (teamConfig.defaultModel && teamConfig.defaultModel.includes('/')) {
170
- const [provider] = teamConfig.defaultModel.split('/');
171
- chosenModel = `${provider}/${chosenModel}`;
172
- } else {
173
- if (chosenModel.startsWith('glm-')) {
174
- chosenModel = `zai/${chosenModel}`;
175
- } else if (chosenModel.startsWith('claude-')) {
176
- chosenModel = `anthropic/${chosenModel}`;
298
+ // Resolve model to provider/model format
299
+ if (chosenModel) {
300
+ if (!chosenModel.includes('/')) {
301
+ // Try to resolve using available models from pi --list-models
302
+ const resolved = resolveModelWithProvider(chosenModel);
303
+ if (resolved) {
304
+ chosenModel = resolved;
305
+ } else if (teamConfig.defaultModel && teamConfig.defaultModel.includes('/')) {
306
+ // Fall back to team default provider
307
+ const [provider] = teamConfig.defaultModel.split('/');
308
+ chosenModel = `${provider}/${chosenModel}`;
177
309
  }
178
310
  }
179
311
  }
@@ -205,12 +337,11 @@ export default function (pi: ExtensionAPI) {
205
337
  let piCmd = piBinary;
206
338
 
207
339
  if (chosenModel) {
208
- const [provider, ...modelParts] = chosenModel.split('/');
209
- const modelName = modelParts.join('/');
340
+ // Use the combined --model provider/model:thinking format
210
341
  if (params.thinking) {
211
- piCmd = `${piBinary} --provider ${provider} --model ${modelName}:${params.thinking}`;
342
+ piCmd = `${piBinary} --model ${chosenModel}:${params.thinking}`;
212
343
  } else {
213
- piCmd = `${piBinary} --provider ${provider} --model ${modelName}`;
344
+ piCmd = `${piBinary} --model ${chosenModel}`;
214
345
  }
215
346
  } else if (params.thinking) {
216
347
  piCmd = `${piBinary} --thinking ${params.thinking}`;
@@ -284,8 +415,8 @@ export default function (pi: ExtensionAPI) {
284
415
  const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
285
416
  let piCmd = piBinary;
286
417
  if (teamConfig.defaultModel) {
287
- const [provider, ...modelParts] = teamConfig.defaultModel.split('/');
288
- piCmd = `${piBinary} --provider ${provider} --model ${modelParts.join('/')}`;
418
+ // Use the combined --model provider/model format
419
+ piCmd = `${piBinary} --model ${teamConfig.defaultModel}`;
289
420
  }
290
421
 
291
422
  const env = { ...process.env, PI_TEAM_NAME: safeTeamName, PI_AGENT_NAME: "team-lead" };
@@ -530,11 +661,14 @@ export default function (pi: ExtensionAPI) {
530
661
  async execute(toolCallId, params: any, signal, onUpdate, ctx) {
531
662
  const config = await teams.readConfig(params.team_name);
532
663
  const member = config.members.find(m => m.name === params.agent_name);
533
- if (member) {
534
- await killTeammate(params.team_name, member);
535
- await teams.removeMember(params.team_name, params.agent_name);
536
- }
537
- return { content: [{ type: "text", text: `Teammate ${params.agent_name} shutdown processed.` }], details: {} };
664
+ if (!member) throw new Error(`Teammate ${params.agent_name} not found`);
665
+
666
+ await killTeammate(params.team_name, member);
667
+ await teams.removeMember(params.team_name, params.agent_name);
668
+ return {
669
+ content: [{ type: "text", text: `Teammate ${params.agent_name} has been shut down.` }],
670
+ details: {},
671
+ };
538
672
  },
539
673
  });
540
674
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-teams",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Agent teams for pi, ported from claude-code-teams-mcp",
5
5
  "repository": {
6
6
  "type": "git",