Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/auto-detect-model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 15 additions & 4 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -33,7 +34,7 @@ export interface AgentManifest {
tags?: string[];
metadata?: Record<string, string | number | boolean>;
model: {
preferred: string;
preferred?: string;
fallback: string[];
constraints?: {
temperature?: number;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
);
}

Expand Down