diff --git a/src/auto-detect-model.ts b/src/auto-detect-model.ts new file mode 100644 index 0000000..3f57b6a --- /dev/null +++ b/src/auto-detect-model.ts @@ -0,0 +1,47 @@ +// Zero-config model auto-detection (issue #14). +// +// When no model is configured via --model, agent.yaml, or env config override, +// check known provider API keys (via pi-ai's getEnvApiKey, so Bedrock / Vertex / +// Azure / Copilot ambient credentials are picked up too, not just a hardcoded +// env var list) and fall back to a sensible default model for the first +// provider that has credentials available. + +import { getEnvApiKey } from "@mariozechner/pi-ai"; + +// Checked in this order; first provider with credentials available wins. +// Versionless aliases only, so this never goes stale as new point releases ship. +// +// Direct API-key providers first (explicit, deliberate signal); ambient/ +// enterprise credential sources (Bedrock, Vertex, Azure, Copilot) checked +// after, since those can be present incidentally (e.g. a leftover AWS_PROFILE +// or an active gcloud login unrelated to this agent). +const DEFAULT_MODEL_BY_PROVIDER: Array<{ provider: string; model: string }> = [ + { provider: "anthropic", model: "claude-sonnet-4-6" }, + { provider: "openai", model: "gpt-4o" }, + { provider: "google", model: "gemini-2.0-flash" }, + { provider: "xai", model: "grok-4-fast" }, + { provider: "groq", model: "llama-3.3-70b-versatile" }, + { provider: "mistral", model: "mistral-large-latest" }, + { provider: "amazon-bedrock", model: "anthropic.claude-sonnet-4-6" }, + { provider: "google-vertex", model: "gemini-2.0-flash" }, + { provider: "azure-openai-responses", model: "gpt-4o" }, + { provider: "github-copilot", model: "claude-sonnet-4.6" }, +]; + +export interface AutoDetectedModel { + modelStr: string; + provider: string; +} + +/** + * Find the first provider with a usable API key/credential in the environment + * and return its default model, or undefined if none are configured. + */ +export function autoDetectModel(): AutoDetectedModel | undefined { + for (const { provider, model } of DEFAULT_MODEL_BY_PROVIDER) { + if (getEnvApiKey(provider)) { + return { modelStr: `${provider}:${model}`, provider }; + } + } + return undefined; +} diff --git a/src/loader.ts b/src/loader.ts index b8d193e..b548635 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -19,6 +19,7 @@ import { loadExamples, formatExamplesForPrompt } from "./examples.js"; import type { ExampleEntry } from "./examples.js"; import { validateCompliance, loadComplianceContext, formatComplianceWarnings } from "./compliance.js"; import type { ComplianceWarning } from "./compliance.js"; +import { autoDetectModel } from "./auto-detect-model.js"; import { discoverAndLoadPlugins } from "./plugins.js"; import type { LoadedPlugin } from "./plugin-types.js"; import type { PluginConfig } from "./plugin-types.js"; @@ -33,7 +34,7 @@ export interface AgentManifest { tags?: string[]; metadata?: Record; model: { - preferred: string; + preferred?: string; fallback: string[]; constraints?: { temperature?: number; @@ -241,6 +242,9 @@ export async function loadAgent( // Parse agent.yaml const manifestRaw = await readFile(join(agentDir, "agent.yaml"), "utf-8"); let manifest = yaml.load(manifestRaw) as AgentManifest; + // model: is optional in agent.yaml (auto-detection can fill it in later) — + // normalize here so downstream code can always assume manifest.model exists. + manifest.model ??= { fallback: [] }; // Load environment config const envConfig = await loadEnvConfig(agentDir, envFlag); @@ -379,11 +383,18 @@ Do NOT track trivial single-command tasks (e.g. "what time is it"). But DO check const systemPrompt = parts.join("\n\n"); - // Resolve model — env config model_override > CLI flag > manifest preferred - const modelStr = envConfig.model_override || modelFlag || manifest.model.preferred; + // Resolve model — env config model_override > CLI flag > manifest preferred > auto-detected from API key + let modelStr = envConfig.model_override || modelFlag || manifest.model?.preferred; + if (!modelStr) { + const detected = autoDetectModel(); + if (detected) { + console.log(`Using ${detected.modelStr} (auto-detected from ${detected.provider} credentials)`); + modelStr = detected.modelStr; + } + } if (!modelStr) { throw new Error( - 'No model configured. Either:\n - Set model.preferred in agent.yaml (e.g., "anthropic:claude-sonnet-4-5-20250929")\n - Pass --model provider:model on the command line', + 'No model configured. Either:\n - Set model.preferred in agent.yaml (e.g., "anthropic:claude-sonnet-4-5-20250929")\n - Pass --model provider:model on the command line\n - Set an API key for a known provider (e.g. ANTHROPIC_API_KEY) to auto-select a default model', ); }