Files
pbakaus_impeccable/tests/skill-behavior/providers.mjs
T
Paul BakausandClaude Opus 4.7 33467e5f0f skill: simplify context loading and inline register directive
Replaces load-context.mjs's JSON output with a tight markdown block from
the renamed context.mjs. The script now extracts PRODUCT.md's `## Register`
field and appends a `NEXT STEP:` directive naming the matching reference
(brand.md / product.md), which moved Gemini from skipping the register
load entirely to honoring it. Drops the `.impeccable.md` auto-migration;
makes IMPECCABLE_CONTEXT_DIR a lazy escape hatch consulted only when the
default paths come up empty.

Setup is now four bullets in one list. The DESIGN.md nudge is gone; in
its place, a "familiarize with the existing design system" step that
calls out CSS / tokens / running app as authoritative sources alongside
DESIGN.md. The standalone `### Register` H3 stays for the cascade rules
(task cue → surface → register field).

New LLM-backed test suite at tests/skill-behavior/ runs five scenarios
against claude-haiku-4-5, gpt-5.4-mini, and gemini-3.1-flash-lite via
Vercel AI SDK. Captures real tool traces, asserts on context.mjs calls,
brand.md loads, and teach.md fallback. Skips cleanly when API keys are
unset. 13-14/15 pass; only stable failure is the v3.2.0-era gpt-mini S4
"don't re-run" regression. Adds @ai-sdk/google as devDep and the
test:skill-behavior npm script.

Touches em-dashes in skill/SKILL.md and four reference files so
`bun run build:skills` passes its skill-prose validator. teach.md and
document.md drop their "re-run the loader to refresh session cache"
steps since the agent's own write is now the freshest source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:25:31 -07:00

92 lines
3.4 KiB
JavaScript

/**
* Multi-provider model factory for the skill-behavior test harness.
*
* The user explicitly asked for the cheapest tier of each major provider to
* keep CI cost in the cents-per-run range while still exercising real LLM
* decision-making against the skill body.
*
* Anthropic and OpenAI use the Vercel AI SDK providers. Google uses
* @ai-sdk/google for the same reason — uniform tool-use semantics across all
* three keeps the harness tiny.
*
* .env is loaded from the repo root (copied from impeccable-evals). Tests
* skip cleanly when the matching key is unset rather than failing CI.
*/
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { openai } from '@ai-sdk/openai';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..', '..');
function loadEnv() {
const envPath = path.join(REPO_ROOT, '.env');
if (!fs.existsSync(envPath)) return;
const text = fs.readFileSync(envPath, 'utf8');
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
if (!process.env[key]) process.env[key] = value;
}
}
loadEnv();
export const PROVIDERS = {
anthropic: { envKey: 'ANTHROPIC_API_KEY', label: 'Anthropic' },
openai: { envKey: 'OPENAI_API_KEY', label: 'OpenAI' },
google: { envKey: 'GOOGLE_CLOUD_API_KEY', label: 'Google' },
};
export function detectProvider(modelId) {
if (modelId.startsWith('claude-')) return 'anthropic';
if (modelId.startsWith('gpt-')) return 'openai';
if (modelId.startsWith('gemini-')) return 'google';
throw new Error(`Unsupported model id: "${modelId}"`);
}
export function hasKey(provider) {
const meta = PROVIDERS[provider];
if (!meta) return false;
return Boolean(process.env[meta.envKey]);
}
export function getModel(modelId) {
const provider = detectProvider(modelId);
if (provider === 'anthropic') return anthropic(modelId);
if (provider === 'openai') return openai(modelId);
if (provider === 'google') {
// The @ai-sdk/google provider reads GOOGLE_GENERATIVE_AI_API_KEY by
// default; the evals .env stores the same value under
// GOOGLE_CLOUD_API_KEY. Mirror it so the SDK picks it up automatically.
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && process.env.GOOGLE_CLOUD_API_KEY) {
process.env.GOOGLE_GENERATIVE_AI_API_KEY = process.env.GOOGLE_CLOUD_API_KEY;
}
return google(modelId);
}
throw new Error(`Unsupported provider: ${provider}`);
}
/**
* Default model lineup. Cheapest tier per provider — the test is about
* routing/loading behavior, not design output quality, so cheap is fine.
* Override with IMPECCABLE_SKILL_BEHAVIOR_MODELS=claude-foo,gpt-bar.
*/
export const DEFAULT_MODELS = ['claude-haiku-4-5', 'gpt-5.4-mini', 'gemini-3.1-flash-lite'];
export function resolveModelList() {
const override = process.env.IMPECCABLE_SKILL_BEHAVIOR_MODELS;
if (override && override.trim()) {
return override.split(',').map((s) => s.trim()).filter(Boolean);
}
return DEFAULT_MODELS;
}