diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cd922a3..b02406247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,14 @@ ### Fixes - Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests. - Models: normalize `${ENV_VAR}` apiKey config values and auto-fill missing provider `apiKey` from env/auth when custom provider models are configured (fixes MiniMax “Unknown model” on fresh installs). +- Models/Tools: include `MiniMax-VL-01` in implicit MiniMax provider so image pairing uses a real vision model. - Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c. - Models: keep explicit GitHub Copilot provider config and honor agent-dir auth profiles for auto-injection. (#705) — thanks @TAGOOZ. - Auto-reply: restore 300-char heartbeat ack limit and keep >300 char replies instead of dropping them; adjust long heartbeat test content accordingly. - Gateway: `agents.list` now honors explicit `agents.list` config without pulling stray agents from disk; GitHub Copilot CLI auth path uses the updated provider build. - Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0. - Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately. +- Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”). ## 2026.1.11 diff --git a/src/agents/models-config.test.ts b/src/agents/models-config.test.ts index 936aa6c02..0ad66e87b 100644 --- a/src/agents/models-config.test.ts +++ b/src/agents/models-config.test.ts @@ -443,6 +443,7 @@ describe("models config", () => { expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY"); const ids = parsed.providers.minimax?.models?.map((model) => model.id); expect(ids).toContain("MiniMax-M2.1"); + expect(ids).toContain("MiniMax-VL-01"); } finally { if (prevKey === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = prevKey; diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 29f0a0ad7..5476b517d 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -11,142 +11,20 @@ import { ensureAuthProfileStore, listProfilesForProvider, } from "./auth-profiles.js"; -import { resolveEnvApiKey } from "./model-auth.js"; +import { + normalizeProviders, + type ProviderConfig, + resolveImplicitProviders, +} from "./models-config.providers.js"; type ModelsConfig = NonNullable; -type ProviderConfig = NonNullable[string]; const DEFAULT_MODE: NonNullable = "merge"; -const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; -const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1"; -const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000; -const MINIMAX_DEFAULT_MAX_TOKENS = 8192; -// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. -const MINIMAX_API_COST = { - input: 15, - output: 60, - cacheRead: 2, - cacheWrite: 10, -}; - -const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"; -const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview"; -const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000; -const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; -const MOONSHOT_DEFAULT_COST = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, -}; - function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } -function normalizeApiKeyConfig(value: string): string { - const trimmed = value.trim(); - const match = /^\$\{([A-Z0-9_]+)\}$/.exec(trimmed); - if (match?.[1]) return match[1]; - return trimmed; -} - -function resolveEnvApiKeyVarName(provider: string): string | undefined { - const resolved = resolveEnvApiKey(provider); - if (!resolved) return undefined; - const match = /^(?:env: |shell env: )([A-Z0-9_]+)$/.exec(resolved.source); - return match ? match[1] : undefined; -} - -function resolveApiKeyFromProfiles(params: { - provider: string; - store: ReturnType; -}): string | undefined { - const ids = listProfilesForProvider(params.store, params.provider); - for (const id of ids) { - const cred = params.store.profiles[id]; - if (!cred) continue; - if (cred.type === "api_key") return cred.key; - if (cred.type === "token") return cred.token; - } - return undefined; -} - -function normalizeGoogleModelId(id: string): string { - if (id === "gemini-3-pro") return "gemini-3-pro-preview"; - if (id === "gemini-3-flash") return "gemini-3-flash-preview"; - return id; -} - -function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig { - let mutated = false; - const models = provider.models.map((model) => { - const nextId = normalizeGoogleModelId(model.id); - if (nextId === model.id) return model; - mutated = true; - return { ...model, id: nextId }; - }); - return mutated ? { ...provider, models } : provider; -} - -function normalizeProviders(params: { - providers: ModelsConfig["providers"]; - agentDir: string; -}): ModelsConfig["providers"] { - const { providers } = params; - if (!providers) return providers; - const authStore = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: false, - }); - let mutated = false; - const next: Record = {}; - for (const [key, provider] of Object.entries(providers)) { - const normalizedKey = key.trim(); - let normalizedProvider = provider; - - // Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR". - if ( - normalizedProvider.apiKey && - normalizeApiKeyConfig(normalizedProvider.apiKey) !== - normalizedProvider.apiKey - ) { - mutated = true; - normalizedProvider = { - ...normalizedProvider, - apiKey: normalizeApiKeyConfig(normalizedProvider.apiKey), - }; - } - - // If a provider defines models, pi's ModelRegistry requires apiKey to be set. - // Fill it from the environment or auth profiles when possible. - const hasModels = - Array.isArray(normalizedProvider.models) && - normalizedProvider.models.length > 0; - if (hasModels && !normalizedProvider.apiKey?.trim()) { - const fromEnv = resolveEnvApiKeyVarName(normalizedKey); - const fromProfiles = resolveApiKeyFromProfiles({ - provider: normalizedKey, - store: authStore, - }); - const apiKey = fromEnv ?? fromProfiles; - if (apiKey?.trim()) { - mutated = true; - normalizedProvider = { ...normalizedProvider, apiKey }; - } - } - - if (normalizedKey === "google") { - const googleNormalized = normalizeGoogleProvider(normalizedProvider); - if (googleNormalized !== normalizedProvider) mutated = true; - normalizedProvider = googleNormalized; - } - - next[key] = normalizedProvider; - } - return mutated ? next : providers; -} - async function readJson(pathname: string): Promise { try { const raw = await fs.readFile(pathname, "utf8"); @@ -156,69 +34,6 @@ async function readJson(pathname: string): Promise { } } -function buildMinimaxApiProvider(): ProviderConfig { - return { - baseUrl: MINIMAX_API_BASE_URL, - api: "anthropic-messages", - models: [ - { - id: MINIMAX_DEFAULT_MODEL_ID, - name: "MiniMax M2.1", - reasoning: false, - input: ["text"], - cost: MINIMAX_API_COST, - contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, - maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, - }, - ], - }; -} - -function buildMoonshotProvider(): ProviderConfig { - return { - baseUrl: MOONSHOT_BASE_URL, - api: "openai-completions", - models: [ - { - id: MOONSHOT_DEFAULT_MODEL_ID, - name: "Kimi K2 0905 Preview", - reasoning: false, - input: ["text"], - cost: MOONSHOT_DEFAULT_COST, - contextWindow: MOONSHOT_DEFAULT_CONTEXT_WINDOW, - maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS, - }, - ], - }; -} - -function resolveImplicitProviders(params: { - cfg: ClawdbotConfig; - agentDir: string; -}): ModelsConfig["providers"] { - const providers: Record = {}; - - const authStore = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: false, - }); - - const minimaxKey = - resolveEnvApiKeyVarName("minimax") ?? - resolveApiKeyFromProfiles({ provider: "minimax", store: authStore }); - if (minimaxKey) { - providers.minimax = { ...buildMinimaxApiProvider(), apiKey: minimaxKey }; - } - - const moonshotKey = - resolveEnvApiKeyVarName("moonshot") ?? - resolveApiKeyFromProfiles({ provider: "moonshot", store: authStore }); - if (moonshotKey) { - providers.moonshot = { ...buildMoonshotProvider(), apiKey: moonshotKey }; - } - - return providers; -} - async function maybeBuildCopilotProvider(params: { agentDir: string; env?: NodeJS.ProcessEnv; @@ -287,7 +102,7 @@ export async function ensureClawdbotModelsJson( : resolveClawdbotAgentDir(); const explicitProviders = cfg.models?.providers ?? {}; - const implicitProviders = resolveImplicitProviders({ cfg, agentDir }); + const implicitProviders = resolveImplicitProviders({ agentDir }); const providers: Record = { ...implicitProviders, ...explicitProviders,