@poolzin/pool-bot 2026.2.4 → 2026.2.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.
Files changed (37) hide show
  1. package/dist/agents/auth-profiles/profiles.js +9 -0
  2. package/dist/agents/auth-profiles.js +1 -1
  3. package/dist/agents/huggingface-models.js +166 -0
  4. package/dist/agents/model-auth.js +6 -0
  5. package/dist/agents/model-forward-compat.js +187 -0
  6. package/dist/agents/pi-embedded-runner/model.js +10 -56
  7. package/dist/browser/constants.js +1 -1
  8. package/dist/browser/profiles.js +1 -1
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli/config-cli.js +17 -3
  11. package/dist/cli/program/register.onboard.js +38 -5
  12. package/dist/commands/auth-choice-options.js +71 -7
  13. package/dist/commands/auth-choice.apply.api-providers.js +202 -97
  14. package/dist/commands/auth-choice.apply.huggingface.js +130 -0
  15. package/dist/commands/auth-choice.apply.openrouter.js +77 -0
  16. package/dist/commands/auth-choice.apply.plugin-provider.js +1 -56
  17. package/dist/commands/auth-choice.apply.vllm.js +92 -0
  18. package/dist/commands/auth-choice.preferred-provider.js +10 -0
  19. package/dist/commands/models/auth.js +1 -58
  20. package/dist/commands/models/list.errors.js +14 -0
  21. package/dist/commands/models/list.list-command.js +32 -21
  22. package/dist/commands/models/list.registry.js +120 -28
  23. package/dist/commands/models/list.status-command.js +1 -0
  24. package/dist/commands/models/shared.js +14 -0
  25. package/dist/commands/onboard-auth.config-core.js +265 -8
  26. package/dist/commands/onboard-auth.credentials.js +47 -6
  27. package/dist/commands/onboard-auth.js +3 -3
  28. package/dist/commands/onboard-auth.models.js +67 -0
  29. package/dist/commands/onboard-custom.js +181 -70
  30. package/dist/commands/onboard-non-interactive/api-keys.js +10 -1
  31. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +15 -7
  32. package/dist/commands/onboard-non-interactive/local/auth-choice.js +322 -124
  33. package/dist/commands/provider-auth-helpers.js +61 -0
  34. package/dist/commands/zai-endpoint-detect.js +97 -0
  35. package/dist/config/legacy.migrations.part-3.js +57 -0
  36. package/dist/terminal/theme.js +1 -1
  37. package/package.json +1 -1
@@ -33,6 +33,15 @@ export function upsertAuthProfile(params) {
33
33
  store.profiles[params.profileId] = params.credential;
34
34
  saveAuthProfileStore(store, params.agentDir);
35
35
  }
36
+ export async function upsertAuthProfileWithLock(params) {
37
+ return await updateAuthProfileStoreWithLock({
38
+ agentDir: params.agentDir,
39
+ updater: (store) => {
40
+ store.profiles[params.profileId] = params.credential;
41
+ return true;
42
+ },
43
+ });
44
+ }
36
45
  export function listProfilesForProvider(store, provider) {
37
46
  const providerKey = normalizeProviderId(provider);
38
47
  return Object.entries(store.profiles)
@@ -4,7 +4,7 @@ export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
4
4
  export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
5
5
  export { resolveAuthProfileOrder } from "./auth-profiles/order.js";
6
6
  export { resolveAuthStorePathForDisplay } from "./auth-profiles/paths.js";
7
- export { listProfilesForProvider, markAuthProfileGood, setAuthProfileOrder, upsertAuthProfile, } from "./auth-profiles/profiles.js";
7
+ export { listProfilesForProvider, markAuthProfileGood, setAuthProfileOrder, upsertAuthProfile, upsertAuthProfileWithLock, } from "./auth-profiles/profiles.js";
8
8
  export { repairOAuthProfileIdMismatch, suggestOAuthProfileIdForLegacyDefault, } from "./auth-profiles/repair.js";
9
9
  export { ensureAuthProfileStore, loadAuthProfileStore, saveAuthProfileStore, } from "./auth-profiles/store.js";
10
10
  export { calculateAuthProfileCooldownMs, clearAuthProfileCooldown, isProfileInCooldown, markAuthProfileCooldown, markAuthProfileFailure, markAuthProfileUsed, resolveProfileUnusableUntilForDisplay, } from "./auth-profiles/usage.js";
@@ -0,0 +1,166 @@
1
+ /** Hugging Face Inference Providers (router) — OpenAI-compatible chat completions. */
2
+ export const HUGGINGFACE_BASE_URL = "https://router.huggingface.co/v1";
3
+ /** Router policy suffixes: router picks backend by cost or speed; no specific provider selection. */
4
+ export const HUGGINGFACE_POLICY_SUFFIXES = ["cheapest", "fastest"];
5
+ /**
6
+ * True when the model ref uses :cheapest or :fastest. When true, provider choice is locked
7
+ * (router decides); do not show an interactive "prefer specific backend" option.
8
+ */
9
+ export function isHuggingfacePolicyLocked(modelRef) {
10
+ const ref = String(modelRef).trim();
11
+ return HUGGINGFACE_POLICY_SUFFIXES.some((s) => ref.endsWith(`:${s}`) || ref === s);
12
+ }
13
+ /** Default cost when not in static catalog (HF pricing varies by provider). */
14
+ const HUGGINGFACE_DEFAULT_COST = {
15
+ input: 0,
16
+ output: 0,
17
+ cacheRead: 0,
18
+ cacheWrite: 0,
19
+ };
20
+ /** Defaults for models discovered from GET /v1/models. */
21
+ const HUGGINGFACE_DEFAULT_CONTEXT_WINDOW = 131072;
22
+ const HUGGINGFACE_DEFAULT_MAX_TOKENS = 8192;
23
+ export const HUGGINGFACE_MODEL_CATALOG = [
24
+ {
25
+ id: "deepseek-ai/DeepSeek-R1",
26
+ name: "DeepSeek R1",
27
+ reasoning: true,
28
+ input: ["text"],
29
+ contextWindow: 131072,
30
+ maxTokens: 8192,
31
+ cost: { input: 3.0, output: 7.0, cacheRead: 3.0, cacheWrite: 3.0 },
32
+ },
33
+ {
34
+ id: "deepseek-ai/DeepSeek-V3.1",
35
+ name: "DeepSeek V3.1",
36
+ reasoning: false,
37
+ input: ["text"],
38
+ contextWindow: 131072,
39
+ maxTokens: 8192,
40
+ cost: { input: 0.6, output: 1.25, cacheRead: 0.6, cacheWrite: 0.6 },
41
+ },
42
+ {
43
+ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
44
+ name: "Llama 3.3 70B Instruct Turbo",
45
+ reasoning: false,
46
+ input: ["text"],
47
+ contextWindow: 131072,
48
+ maxTokens: 8192,
49
+ cost: { input: 0.88, output: 0.88, cacheRead: 0.88, cacheWrite: 0.88 },
50
+ },
51
+ {
52
+ id: "openai/gpt-oss-120b",
53
+ name: "GPT-OSS 120B",
54
+ reasoning: false,
55
+ input: ["text"],
56
+ contextWindow: 131072,
57
+ maxTokens: 8192,
58
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
59
+ },
60
+ ];
61
+ export function buildHuggingfaceModelDefinition(model) {
62
+ return {
63
+ id: model.id,
64
+ name: model.name,
65
+ reasoning: model.reasoning,
66
+ input: model.input,
67
+ cost: model.cost,
68
+ contextWindow: model.contextWindow,
69
+ maxTokens: model.maxTokens,
70
+ };
71
+ }
72
+ /**
73
+ * Infer reasoning and display name from Hub-style model id (e.g. "deepseek-ai/DeepSeek-R1").
74
+ */
75
+ function inferredMetaFromModelId(id) {
76
+ const base = id.split("/").pop() ?? id;
77
+ const reasoning = /r1|reasoning|thinking|reason/i.test(id) || /-\d+[tb]?-thinking/i.test(base);
78
+ const name = base.replace(/-/g, " ").replace(/\b(\w)/g, (c) => c.toUpperCase());
79
+ return { name, reasoning };
80
+ }
81
+ /** Prefer API-supplied display name, then owned_by/id, then inferred from id. */
82
+ function displayNameFromApiEntry(entry, inferredName) {
83
+ const fromApi = (typeof entry.name === "string" && entry.name.trim()) ||
84
+ (typeof entry.title === "string" && entry.title.trim()) ||
85
+ (typeof entry.display_name === "string" && entry.display_name.trim());
86
+ if (fromApi) {
87
+ return fromApi;
88
+ }
89
+ if (typeof entry.owned_by === "string" && entry.owned_by.trim()) {
90
+ const base = entry.id.split("/").pop() ?? entry.id;
91
+ return `${entry.owned_by.trim()}/${base}`;
92
+ }
93
+ return inferredName;
94
+ }
95
+ /**
96
+ * Discover chat-completion models from Hugging Face Inference Providers (GET /v1/models).
97
+ * Requires a valid HF token. Falls back to static catalog on failure or in test env.
98
+ */
99
+ export async function discoverHuggingfaceModels(apiKey) {
100
+ if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
101
+ return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
102
+ }
103
+ const trimmedKey = apiKey?.trim();
104
+ if (!trimmedKey) {
105
+ return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
106
+ }
107
+ try {
108
+ // GET https://router.huggingface.co/v1/models — response: { object, data: [{ id, owned_by, architecture: { input_modalities }, providers: [{ provider, context_length?, pricing? }] }] }. POST /v1/chat/completions requires Authorization.
109
+ const response = await fetch(`${HUGGINGFACE_BASE_URL}/models`, {
110
+ signal: AbortSignal.timeout(10_000),
111
+ headers: {
112
+ Authorization: `Bearer ${trimmedKey}`,
113
+ "Content-Type": "application/json",
114
+ },
115
+ });
116
+ if (!response.ok) {
117
+ console.warn(`[huggingface-models] GET /v1/models failed: HTTP ${response.status}, using static catalog`);
118
+ return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
119
+ }
120
+ const body = (await response.json());
121
+ const data = body?.data;
122
+ if (!Array.isArray(data) || data.length === 0) {
123
+ console.warn("[huggingface-models] No models in response, using static catalog");
124
+ return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
125
+ }
126
+ const catalogById = new Map(HUGGINGFACE_MODEL_CATALOG.map((m) => [m.id, m]));
127
+ const seen = new Set();
128
+ const models = [];
129
+ for (const entry of data) {
130
+ const id = typeof entry?.id === "string" ? entry.id.trim() : "";
131
+ if (!id || seen.has(id)) {
132
+ continue;
133
+ }
134
+ seen.add(id);
135
+ const catalogEntry = catalogById.get(id);
136
+ if (catalogEntry) {
137
+ models.push(buildHuggingfaceModelDefinition(catalogEntry));
138
+ }
139
+ else {
140
+ const inferred = inferredMetaFromModelId(id);
141
+ const name = displayNameFromApiEntry(entry, inferred.name);
142
+ const modalities = entry.architecture?.input_modalities;
143
+ const input = Array.isArray(modalities) && modalities.includes("image") ? ["text", "image"] : ["text"];
144
+ const providers = Array.isArray(entry.providers) ? entry.providers : [];
145
+ const providerWithContext = providers.find((p) => typeof p?.context_length === "number" && p.context_length > 0);
146
+ const contextLength = providerWithContext?.context_length ?? HUGGINGFACE_DEFAULT_CONTEXT_WINDOW;
147
+ models.push({
148
+ id,
149
+ name,
150
+ reasoning: inferred.reasoning,
151
+ input,
152
+ cost: HUGGINGFACE_DEFAULT_COST,
153
+ contextWindow: contextLength,
154
+ maxTokens: HUGGINGFACE_DEFAULT_MAX_TOKENS,
155
+ });
156
+ }
157
+ }
158
+ return models.length > 0
159
+ ? models
160
+ : HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
161
+ }
162
+ catch (error) {
163
+ console.warn(`[huggingface-models] Discovery failed: ${String(error)}, using static catalog`);
164
+ return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
165
+ }
166
+ }
@@ -205,6 +205,9 @@ export function resolveEnvApiKey(provider) {
205
205
  if (normalized === "kimi-coding") {
206
206
  return pick("KIMI_API_KEY") ?? pick("KIMICODE_API_KEY");
207
207
  }
208
+ if (normalized === "huggingface") {
209
+ return pick("HUGGINGFACE_HUB_TOKEN") ?? pick("HF_TOKEN");
210
+ }
208
211
  const envMap = {
209
212
  openai: "OPENAI_API_KEY",
210
213
  google: "GEMINI_API_KEY",
@@ -214,6 +217,7 @@ export function resolveEnvApiKey(provider) {
214
217
  cerebras: "CEREBRAS_API_KEY",
215
218
  xai: "XAI_API_KEY",
216
219
  openrouter: "OPENROUTER_API_KEY",
220
+ litellm: "LITELLM_API_KEY",
217
221
  "vercel-ai-gateway": "AI_GATEWAY_API_KEY",
218
222
  "cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
219
223
  moonshot: "MOONSHOT_API_KEY",
@@ -225,7 +229,9 @@ export function resolveEnvApiKey(provider) {
225
229
  opencode: "OPENCODE_API_KEY",
226
230
  together: "TOGETHER_API_KEY",
227
231
  qianfan: "QIANFAN_API_KEY",
232
+ nvidia: "NVIDIA_API_KEY",
228
233
  ollama: "OLLAMA_API_KEY",
234
+ vllm: "VLLM_API_KEY",
229
235
  };
230
236
  const envVar = envMap[normalized];
231
237
  if (!envVar)
@@ -0,0 +1,187 @@
1
+ import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js";
2
+ import { normalizeModelCompat } from "./model-compat.js";
3
+ import { normalizeProviderId } from "./model-selection.js";
4
+ const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex";
5
+ const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"];
6
+ const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
7
+ const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
8
+ const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"];
9
+ const ZAI_GLM5_MODEL_ID = "glm-5";
10
+ const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"];
11
+ const ANTIGRAVITY_OPUS_46_MODEL_ID = "claude-opus-4-6";
12
+ const ANTIGRAVITY_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
13
+ const ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"];
14
+ const ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID = "claude-opus-4-6-thinking";
15
+ const ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID = "claude-opus-4.6-thinking";
16
+ const ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS = [
17
+ "claude-opus-4-5-thinking",
18
+ "claude-opus-4.5-thinking",
19
+ ];
20
+ export const ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES = [
21
+ {
22
+ id: ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID,
23
+ templatePrefixes: [
24
+ "google-antigravity/claude-opus-4-5-thinking",
25
+ "google-antigravity/claude-opus-4.5-thinking",
26
+ ],
27
+ },
28
+ {
29
+ id: ANTIGRAVITY_OPUS_46_MODEL_ID,
30
+ templatePrefixes: ["google-antigravity/claude-opus-4-5", "google-antigravity/claude-opus-4.5"],
31
+ },
32
+ ];
33
+ function resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) {
34
+ const normalizedProvider = normalizeProviderId(provider);
35
+ const trimmedModelId = modelId.trim();
36
+ if (normalizedProvider !== "openai-codex") {
37
+ return undefined;
38
+ }
39
+ if (trimmedModelId.toLowerCase() !== OPENAI_CODEX_GPT_53_MODEL_ID) {
40
+ return undefined;
41
+ }
42
+ for (const templateId of OPENAI_CODEX_TEMPLATE_MODEL_IDS) {
43
+ const template = modelRegistry.find(normalizedProvider, templateId);
44
+ if (!template) {
45
+ continue;
46
+ }
47
+ return normalizeModelCompat({
48
+ ...template,
49
+ id: trimmedModelId,
50
+ name: trimmedModelId,
51
+ });
52
+ }
53
+ return normalizeModelCompat({
54
+ id: trimmedModelId,
55
+ name: trimmedModelId,
56
+ api: "openai-codex-responses",
57
+ provider: normalizedProvider,
58
+ baseUrl: "https://chatgpt.com/backend-api",
59
+ reasoning: true,
60
+ input: ["text", "image"],
61
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
62
+ contextWindow: DEFAULT_CONTEXT_TOKENS,
63
+ maxTokens: DEFAULT_CONTEXT_TOKENS,
64
+ });
65
+ }
66
+ function resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) {
67
+ const normalizedProvider = normalizeProviderId(provider);
68
+ if (normalizedProvider !== "anthropic") {
69
+ return undefined;
70
+ }
71
+ const trimmedModelId = modelId.trim();
72
+ const lower = trimmedModelId.toLowerCase();
73
+ const isOpus46 = lower === ANTHROPIC_OPUS_46_MODEL_ID ||
74
+ lower === ANTHROPIC_OPUS_46_DOT_MODEL_ID ||
75
+ lower.startsWith(`${ANTHROPIC_OPUS_46_MODEL_ID}-`) ||
76
+ lower.startsWith(`${ANTHROPIC_OPUS_46_DOT_MODEL_ID}-`);
77
+ if (!isOpus46) {
78
+ return undefined;
79
+ }
80
+ const templateIds = [];
81
+ if (lower.startsWith(ANTHROPIC_OPUS_46_MODEL_ID)) {
82
+ templateIds.push(lower.replace(ANTHROPIC_OPUS_46_MODEL_ID, "claude-opus-4-5"));
83
+ }
84
+ if (lower.startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID)) {
85
+ templateIds.push(lower.replace(ANTHROPIC_OPUS_46_DOT_MODEL_ID, "claude-opus-4.5"));
86
+ }
87
+ templateIds.push(...ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS);
88
+ for (const templateId of [...new Set(templateIds)].filter(Boolean)) {
89
+ const template = modelRegistry.find(normalizedProvider, templateId);
90
+ if (!template) {
91
+ continue;
92
+ }
93
+ return normalizeModelCompat({
94
+ ...template,
95
+ id: trimmedModelId,
96
+ name: trimmedModelId,
97
+ });
98
+ }
99
+ return undefined;
100
+ }
101
+ // Z.ai's GLM-5 may not be present in pi-ai's built-in model catalog yet.
102
+ // When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback.
103
+ function resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) {
104
+ if (normalizeProviderId(provider) !== "zai") {
105
+ return undefined;
106
+ }
107
+ const trimmed = modelId.trim();
108
+ const lower = trimmed.toLowerCase();
109
+ if (lower !== ZAI_GLM5_MODEL_ID && !lower.startsWith(`${ZAI_GLM5_MODEL_ID}-`)) {
110
+ return undefined;
111
+ }
112
+ for (const templateId of ZAI_GLM5_TEMPLATE_MODEL_IDS) {
113
+ const template = modelRegistry.find("zai", templateId);
114
+ if (!template) {
115
+ continue;
116
+ }
117
+ return normalizeModelCompat({
118
+ ...template,
119
+ id: trimmed,
120
+ name: trimmed,
121
+ reasoning: true,
122
+ });
123
+ }
124
+ return normalizeModelCompat({
125
+ id: trimmed,
126
+ name: trimmed,
127
+ api: "openai-completions",
128
+ provider: "zai",
129
+ reasoning: true,
130
+ input: ["text"],
131
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
132
+ contextWindow: DEFAULT_CONTEXT_TOKENS,
133
+ maxTokens: DEFAULT_CONTEXT_TOKENS,
134
+ });
135
+ }
136
+ function resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) {
137
+ const normalizedProvider = normalizeProviderId(provider);
138
+ if (normalizedProvider !== "google-antigravity") {
139
+ return undefined;
140
+ }
141
+ const trimmedModelId = modelId.trim();
142
+ const lower = trimmedModelId.toLowerCase();
143
+ const isOpus46 = lower === ANTIGRAVITY_OPUS_46_MODEL_ID ||
144
+ lower === ANTIGRAVITY_OPUS_46_DOT_MODEL_ID ||
145
+ lower.startsWith(`${ANTIGRAVITY_OPUS_46_MODEL_ID}-`) ||
146
+ lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_MODEL_ID}-`);
147
+ const isOpus46Thinking = lower === ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID ||
148
+ lower === ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID ||
149
+ lower.startsWith(`${ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID}-`) ||
150
+ lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID}-`);
151
+ if (!isOpus46 && !isOpus46Thinking) {
152
+ return undefined;
153
+ }
154
+ const templateIds = [];
155
+ if (lower.startsWith(ANTIGRAVITY_OPUS_46_MODEL_ID)) {
156
+ templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_MODEL_ID, "claude-opus-4-5"));
157
+ }
158
+ if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID)) {
159
+ templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID, "claude-opus-4.5"));
160
+ }
161
+ if (lower.startsWith(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID)) {
162
+ templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID, "claude-opus-4-5-thinking"));
163
+ }
164
+ if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID)) {
165
+ templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID, "claude-opus-4.5-thinking"));
166
+ }
167
+ templateIds.push(...ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS);
168
+ templateIds.push(...ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS);
169
+ for (const templateId of [...new Set(templateIds)].filter(Boolean)) {
170
+ const template = modelRegistry.find(normalizedProvider, templateId);
171
+ if (!template) {
172
+ continue;
173
+ }
174
+ return normalizeModelCompat({
175
+ ...template,
176
+ id: trimmedModelId,
177
+ name: trimmedModelId,
178
+ });
179
+ }
180
+ return undefined;
181
+ }
182
+ export function resolveForwardCompatModel(provider, modelId, modelRegistry) {
183
+ return (resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ??
184
+ resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ??
185
+ resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ??
186
+ resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry));
187
+ }
@@ -1,8 +1,9 @@
1
- import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
2
1
  import { resolvePoolbotAgentDir } from "../agent-paths.js";
3
2
  import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
4
3
  import { normalizeModelCompat } from "../model-compat.js";
4
+ import { resolveForwardCompatModel } from "../model-forward-compat.js";
5
5
  import { normalizeProviderId } from "../model-selection.js";
6
+ import { discoverAuthStorage, discoverModels, } from "../pi-model-discovery.js";
6
7
  export function buildInlineProviderModels(providers) {
7
8
  return Object.entries(providers).flatMap(([providerId, entry]) => {
8
9
  const trimmed = providerId.trim();
@@ -12,7 +13,7 @@ export function buildInlineProviderModels(providers) {
12
13
  ...model,
13
14
  provider: trimmed,
14
15
  baseUrl: entry?.baseUrl,
15
- ...(entry?.api && !model.api ? { api: entry.api } : {}),
16
+ api: model.api ?? entry?.api,
16
17
  }));
17
18
  });
18
19
  }
@@ -32,65 +33,12 @@ export function buildModelAliasLines(cfg) {
32
33
  .toSorted((a, b) => a.alias.localeCompare(b.alias))
33
34
  .map((entry) => `- ${entry.alias}: ${entry.model}`);
34
35
  }
35
- // Well-known model IDs for forward-compatible resolution
36
- export const OPENAI_CODEX_GPT_53_MODEL_ID = "codex-gpt-5.3";
37
- export const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["codex-mini-*"];
38
- export const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-0624";
39
- export const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
40
- export const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-*", "claude-opus-4.*"];
41
- /**
42
- * Resolve forward-compat fallback for OpenAI codex-gpt-5.3 requests.
43
- * If the exact model is not in the registry, try `codex-mini-*` template matches.
44
- */
45
- export function resolveOpenAICodexGpt53FallbackModel(modelRegistry, provider) {
46
- for (const templateId of OPENAI_CODEX_TEMPLATE_MODEL_IDS) {
47
- const match = modelRegistry.find(provider, templateId);
48
- if (match)
49
- return match;
50
- }
51
- return null;
52
- }
53
- /**
54
- * Resolve forward-compat fallback for Anthropic claude-opus-4.6 requests.
55
- * If the exact model is not in the registry, try `claude-opus-4-*` template matches.
56
- */
57
- export function resolveAnthropicOpus46ForwardCompatModel(modelRegistry, provider, modelId) {
58
- if (modelId !== ANTHROPIC_OPUS_46_MODEL_ID && modelId !== ANTHROPIC_OPUS_46_DOT_MODEL_ID) {
59
- return null;
60
- }
61
- for (const templateId of ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS) {
62
- const match = modelRegistry.find(provider, templateId);
63
- if (match)
64
- return match;
65
- }
66
- return null;
67
- }
68
36
  export function resolveModel(provider, modelId, agentDir, cfg) {
69
37
  const resolvedAgentDir = agentDir ?? resolvePoolbotAgentDir();
70
38
  const authStorage = discoverAuthStorage(resolvedAgentDir);
71
39
  const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
72
40
  const model = modelRegistry.find(provider, modelId);
73
41
  if (!model) {
74
- // Try codex forward-compat fallback
75
- if (modelId === OPENAI_CODEX_GPT_53_MODEL_ID) {
76
- const codexFallback = resolveOpenAICodexGpt53FallbackModel(modelRegistry, provider);
77
- if (codexFallback) {
78
- return {
79
- model: normalizeModelCompat(codexFallback),
80
- authStorage,
81
- modelRegistry,
82
- };
83
- }
84
- }
85
- // Try Anthropic opus forward-compat fallback
86
- const opusFallback = resolveAnthropicOpus46ForwardCompatModel(modelRegistry, provider, modelId);
87
- if (opusFallback) {
88
- return {
89
- model: normalizeModelCompat(opusFallback),
90
- authStorage,
91
- modelRegistry,
92
- };
93
- }
94
42
  const providers = cfg?.models?.providers ?? {};
95
43
  const inlineModels = buildInlineProviderModels(providers);
96
44
  const normalizedProvider = normalizeProviderId(provider);
@@ -103,6 +51,12 @@ export function resolveModel(provider, modelId, agentDir, cfg) {
103
51
  modelRegistry,
104
52
  };
105
53
  }
54
+ // Forward-compat fallbacks must be checked BEFORE the generic providerCfg fallback.
55
+ // Otherwise, configured providers can default to a generic API and break specific transports.
56
+ const forwardCompat = resolveForwardCompatModel(provider, modelId, modelRegistry);
57
+ if (forwardCompat) {
58
+ return { model: forwardCompat, authStorage, modelRegistry };
59
+ }
106
60
  const providerCfg = providers[provider];
107
61
  if (providerCfg || modelId.startsWith("mock-")) {
108
62
  const fallbackModel = normalizeModelCompat({
@@ -110,12 +64,12 @@ export function resolveModel(provider, modelId, agentDir, cfg) {
110
64
  name: modelId,
111
65
  api: providerCfg?.api ?? "openai-responses",
112
66
  provider,
67
+ baseUrl: providerCfg?.baseUrl,
113
68
  reasoning: false,
114
69
  input: ["text"],
115
70
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
116
71
  contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
117
72
  maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS,
118
- baseUrl: providerCfg?.baseUrl,
119
73
  });
120
74
  return { model: fallbackModel, authStorage, modelRegistry };
121
75
  }
@@ -1,6 +1,6 @@
1
1
  export const DEFAULT_CLAWD_BROWSER_ENABLED = true;
2
2
  export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
3
- export const DEFAULT_CLAWD_BROWSER_COLOR = "#FF4500";
3
+ export const DEFAULT_CLAWD_BROWSER_COLOR = "#A855F7";
4
4
  export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd";
5
5
  export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
6
6
  export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
@@ -63,7 +63,7 @@ export function getUsedPorts(profiles) {
63
63
  return used;
64
64
  }
65
65
  export const PROFILE_COLORS = [
66
- "#FF4500", // Orange-red (clawd default)
66
+ "#A855F7", // Purple (poolbot default)
67
67
  "#0066CC", // Blue
68
68
  "#00AA00", // Green
69
69
  "#9933FF", // Purple
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.2.4",
3
- "commit": "c64f0b4b98cd3ef3ffe676270cab95c2c859479d",
4
- "builtAt": "2026-02-14T01:53:48.010Z"
2
+ "version": "2026.2.6",
3
+ "commit": "d67aa6d87b042556d38fbe64f7ed4ce6a22dbf4e",
4
+ "builtAt": "2026-02-14T08:48:33.834Z"
5
5
  }
@@ -1,5 +1,5 @@
1
1
  import JSON5 from "json5";
2
- import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
2
+ import { readConfigFileSnapshot, validateConfigObjectWithPlugins, writeConfigFile, } from "../config/config.js";
3
3
  import { danger, info } from "../globals.js";
4
4
  import { defaultRuntime } from "../runtime.js";
5
5
  import { formatDocsLink } from "../terminal/links.js";
@@ -259,7 +259,14 @@ export function registerConfigCli(program) {
259
259
  const snapshot = await loadValidConfig();
260
260
  const next = snapshot.config;
261
261
  setAtPath(next, parsedPath, parsedValue);
262
- await writeConfigFile(next);
262
+ const validated = validateConfigObjectWithPlugins(next);
263
+ if (!validated.ok) {
264
+ const issue = validated.issues[0];
265
+ defaultRuntime.error(danger(`Config invalid after set: ${issue.path}: ${issue.message}`));
266
+ defaultRuntime.exit(1);
267
+ return;
268
+ }
269
+ await writeConfigFile(validated.config);
263
270
  defaultRuntime.log(info(`Updated ${path}. Restart the gateway to apply.`));
264
271
  }
265
272
  catch (err) {
@@ -284,7 +291,14 @@ export function registerConfigCli(program) {
284
291
  defaultRuntime.exit(1);
285
292
  return;
286
293
  }
287
- await writeConfigFile(next);
294
+ const validated = validateConfigObjectWithPlugins(next);
295
+ if (!validated.ok) {
296
+ const issue = validated.issues[0];
297
+ defaultRuntime.error(danger(`Config invalid after unset: ${issue.path}: ${issue.message}`));
298
+ defaultRuntime.exit(1);
299
+ return;
300
+ }
301
+ await writeConfigFile(validated.config);
288
302
  defaultRuntime.log(info(`Removed ${path}. Restart the gateway to apply.`));
289
303
  }
290
304
  catch (err) {