Agent Prompt
codev1.0.0

eval

Build, run, or update minimal evaluation benchmarks for AI prompts, LLM calls, multi-model comparisons, and core functions. Use when creating new eval scripts, comparing models side-by-side, filtering specific candidate models, running benchmarks, or inspecting eval.md output artifacts.

Use this skill in Cursor, Claude Code, or any LLM
Full Prompt

Copy directly or install via CLI command.

Raw SKILL.md
# Eval System

A clean, minimal benchmark system that runs production code against test cases and writes results to a co-located markdown artifact (`eval.md`). The `eval.md` is a **judgment surface**: it exists so a strong external model or a human can read the real outputs and judge quality, not so the eval can auto-declare a winner.

---

## Core Principles & Rules

### 1. Import Production Logic Directly
- **ALWAYS** import and invoke the actual production function, prompt constants, and schemas from the feature file.
- **NEVER** duplicate prompts, system messages, or JSON schemas inside `eval.ts`.
- *Why:* Re-implementing prompts in the eval causes prompt drift. When production changes, a duplicated eval becomes a false benchmark testing obsolete behavior.

### 2. Benchmark Real Runs — Never Estimate
- **ALWAYS** execute every candidate model to capture ground-truth latency, tokens, and cost.
- **NEVER** estimate or hallucinate latency, token counts, or pricing in a report without running the models.
- *Why:* Models cannot predict real streaming latency, throughput, or cost. Only direct execution records the truth.

### 3. "Passed" (✅) Means Schema Valid — NOT High Quality
- **ALWAYS** treat the status column as execution correctness (did the call succeed and conform to the Zod schema?).
- **NEVER** assume a "Passed" status implies the output is accurate, relevant, or good.
- *Why:* A model can return 100% valid JSON conforming to a schema while outputting completely wrong or noisy content.

### 4. Never Self-Score Quality — Surface Raw Outputs for Independent Judgment
- **NEVER** put the generating model's own relevance/quality/confidence score — or a count derived from it (e.g. "candidates scored 80+") — in the summary as if it were a quality metric.
- **ALWAYS** render each model's full raw output verbatim so quality can be judged from the output itself, not from the model's self-assessment.
- *Why:* A model scoring its own output rewards quantity and confidence, not correctness. A model that emits 7 loosely-relevant items looks like it "beats" one that emits 4 precise ones, when the opposite is true. Self-scores are biased and not comparable across models. Real quality only emerges when a strong external judge (or human) reads the actual outputs side-by-side.

### 5. Summary Table = Objective Machine Metrics Only
- **ALWAYS** limit the summary table to metrics the machine measures directly: latency, input/output tokens, cost, and pass/fail (schema/execution status).
- **ALWAYS** caption any raw output count (e.g. "Items") as a count, never as "Relevant", "Quality", or "Score".
- *Why:* Objective metrics are trustworthy and comparable; quality labels sneak the model's biased self-judgment back into the leaderboard.

### 6. Control Output Constraints for Fair Comparison
- **ALWAYS** enforce identical output constraints (e.g. `top_k`, length limits, schema bounds) across candidate models when benchmarking.
- *Why:* Unconstrained models that generate 1,500 tokens of verbose fluff will appear unfairly slow and expensive compared to concise models emitting 100 tokens, distorting latency and cost comparisons.

### 7. Track Real Billed Costs (Including Reasoning Tokens)
- **ALWAYS** calculate cost using total billed tokens from API response metadata, including hidden reasoning/thinking completion tokens.
- *Why:* Modern reasoning models (e.g., Gemini Flash thinking or OpenAI o-series) consume reasoning tokens that don't show up in visible text length but directly impact API billing.

### 8. Support Model Selection & Filtering
- **ALWAYS** support running the production model only by default, all candidates via `--matrix`, or a subset via `--models=a,b`.
- *Why:* Running the full suite on every iteration wastes time and API credits when a developer only needs 2–3 target models.

### 9. Output to Co-located Markdown (`eval.md`)
- **ALWAYS** compile output into a string buffer and write to `path.join(__dirname, 'eval.md')`.
- **NEVER** output results exclusively to stdout or an external database.
- *Why:* Co-located `eval.md` files diff cleanly in git, render in the editor, and persist benchmark history with zero infrastructure.

### 10. Direct Terminal Progress
- **ALWAYS** log progress lines during the run, prefix success with `✅` and failure with `❌`, and log the final `eval.md` path.
- *Why:* CLI feedback tells the developer whether the run worked without tailing files mid-execution.

---

## Decision Flowchart: Eval Workflow

```
Are you creating or running an evaluation?
├── Running an existing eval?
│   ├── Production model only: `bun path/to/eval.ts`
│   ├── All candidates:        `bun path/to/eval.ts --matrix`
│   └── Specific models only:  `bun path/to/eval.ts --models=model-a,model-b`
└── Creating a new eval?
    ├── Does an eval file exist in the target module folder?
    │   ├── Yes → Edit `TEST_CASES` / `CANDIDATE_MODELS` in existing `eval.ts`
    │   └── No  → Identify evaluation target type:
    │       ├── AI / LLM feature (single or multi-model) → Pattern A
    │       ├── Automated quality scoring needed         → Pattern B (independent judge)
    │       └── Sync / async utility function            → Pattern C
```

**One file per feature.** Keep a single `eval.ts` that runs the production model by default and expands to a matrix via flags — do not split into `eval.ts` + `eval.matrix.ts`. One script and one `eval.md` keeps test cases in a single source of truth and stops agents guessing which file to run.

---

## Implementation Patterns

### Pattern A: AI Feature Evaluation (`eval.ts`)

Default runs the production model; `--matrix` runs all candidates; `--models=` runs a subset. The summary table stays objective; raw outputs carry the quality signal.

```typescript
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import { calculateModelCost } from '../client'
import { FEATURE_MODEL, runProductionFeature } from './generate-feature'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const TEST_CASES = [
  { name: 'TestCase1', input: { /* production function params */ } },
]

// Production default first, then comparison candidates
const CANDIDATE_MODELS = [
  FEATURE_MODEL,
  'google/gemini-3.6-flash',
  'openai/gpt-5.6-luna',
]

// Flags: --matrix (all candidates) | --models=a,b (subset) | none (production only)
const isMatrix = process.argv.includes('--matrix')
const requested = process.argv.find(a => a.startsWith('--models='))
  ?.split('=')[1]?.split(',').map(m => m.trim().toLowerCase())

const TEST_MODELS = requested
  ? CANDIDATE_MODELS.filter(m => requested.some(r => m.toLowerCase().includes(r)))
  : isMatrix
    ? CANDIDATE_MODELS
    : [FEATURE_MODEL]

interface ResultRow {
  model: string
  caseName: string
  timeMs: number
  cost: number
  inputTokens?: number
  outputTokens?: number
  itemCount?: number
  error?: string
  output?: string
}

async function runEval() {
  console.log(`Running evaluation across ${TEST_MODELS.length} model(s)...\n`)
  const results: ResultRow[] = []

  for (const model of TEST_MODELS) {
    for (const testCase of TEST_CASES) {
      const start = Date.now()
      try {
        const result = await runProductionFeature(testCase.input, { model })
        const timeMs = Date.now() - start
        results.push({
          model,
          caseName: testCase.name,
          timeMs,
          cost: calculateModelCost(model, result.usage),
          inputTokens: result.usage?.inputTokens,
          outputTokens: result.usage?.outputTokens,
          itemCount: Array.isArray(result.data) ? result.data.length : undefined,
          output: JSON.stringify(result.data, null, 2),
        })
        console.log(`✅ ${model} — ${testCase.name} (${timeMs}ms)`)
      } catch (err) {
        results.push({
          model,
          caseName: testCase.name,
          timeMs: Date.now() - start,
          cost: 0,
          error: err instanceof Error ? err.message : String(err),
        })
        console.log(`❌ ${model} — ${testCase.name}: ${err}`)
      }
    }
  }

  const md: string[] = []
  md.push('# Multi-Model Comparison Evaluation\n')

  // Summary: objective machine metrics ONLY. "Items" is a raw count, not a quality score.
  md.push('## Summary (objective metrics — judge quality from the raw outputs below)\n')
  md.push('| Model | Test Case | Latency (ms) | Tokens (In/Out) | Cost ($) | Items | Status |')
  md.push('| :--- | :--- | ---: | ---: | ---: | ---: | :--- |')
  results.forEach(r => {
    const tokens = r.inputTokens !== undefined ? `${r.inputTokens} / ${r.outputTokens}` : 'N/A'
    const items = r.itemCount !== undefined ? String(r.itemCount) : '—'
    md.push(`| \`${r.model}\` | ${r.caseName} | ${r.timeMs} | ${tokens} | $${r.cost.toFixed(5)} | ${items} | ${r.error ? '❌' : '✅'} |`)
  })

  // Raw outputs grouped by test case so models sit side-by-side for independent judgment.
  md.push('\n## Raw Outputs\n')
  for (const testCase of TEST_CASES) {
    md.push(`## ${testCase.name}\n`)
    results.filter(r => r.caseName === testCase.name).forEach(r => {
      md.push(`### \`${r.model}\` — ${r.timeMs}ms, $${r.cost.toFixed(5)}`)
      md.push(r.error ? `\n**Error**: ${r.error}\n` : '\n```json\n' + r.output + '\n```\n')
    })
    md.push('---\n')
  }

  const outputPath = path.join(__dirname, 'eval.md')
  await fs.writeFile(outputPath, md.join('\n'), 'utf-8')
  console.log(`\n✅ Results written to ${outputPath}`)
}

runEval().catch(console.error)
```

### Pattern B: Independent Judge (optional, for automated quality scoring)

Only when you need a repeatable quality number. Run a SEPARATE strong judge model that reads all candidate outputs against a rubric. The judge must never be the model that generated the output.

```typescript
import { generateObject } from 'ai'
import { z } from 'zod'
import { getModel } from '../client'

const JUDGE_MODEL = 'openai/gpt-5.6-sol' // strong model, distinct from candidates

// `outputs` = the raw candidate outputs collected in Pattern A
async function judge(testCasePrompt: string, outputs: { model: string; output: string }[]) {
  const { object } = await generateObject({
    model: getModel(JUDGE_MODEL),
    schema: z.object({
      rankings: z.array(z.object({
        model: z.string(),
        score: z.number().min(0).max(10),
        precision: z.string(),  // are the items directly relevant, not just plausible?
        rationale: z.string(),
      })),
    }),
    // Include the rubric: reward precision, directness, ranking quality; penalize
    // broad/redundant/wrong items. Do NOT reward emitting more candidates.
    prompt: `Task prompt:\n${testCasePrompt}\n\nCandidate outputs:\n${JSON.stringify(outputs, null, 2)}`,
  })
  return object.rankings
}
```

### Pattern C: Pure Function / Utility Evaluation (`eval.ts`)

For algorithms, parsers, formatters, throughput, or accuracy assertions.

```typescript
import fs from 'fs'
import path from 'path'
import { productionUtility } from './index'

const TEST_INPUT = 'example-input'
const ITERATIONS = 100_000

function runEval() {
  console.log('Running utility evaluation...\n')
  let md = `# Utility Evaluation\n\n`

  const actual = productionUtility(TEST_INPUT)
  const expected = 'expected-output'
  if (actual !== expected) {
    console.error(`❌ Assertion failed: expected '${expected}', got '${actual}'`)
    process.exit(1)
  }
  console.log('✅ Correctness assertions passed')

  const start = performance.now()
  for (let i = 0; i < ITERATIONS; i++) productionUtility(TEST_INPUT)
  const totalMs = performance.now() - start
  const perCallMs = totalMs / ITERATIONS

  md += `Total time: ${totalMs.toFixed(2)}ms\n`
  md += `Time per call: ${perCallMs.toFixed(6)}ms\n`
  md += `Calls per second: ${(1000 / perCallMs).toFixed(0)}\n`
  md += `Sample Output: '${actual}'\n`

  const outputPath = path.join(__dirname, 'eval.md')
  fs.writeFileSync(outputPath, md, 'utf-8')
  console.log(`\n✅ Results written to ${outputPath}`)
}

runEval()
```

---

## Execution Checklist

1. Production model only: `bun <path-to-module>/eval.ts`.
2. Full matrix: `bun <path-to-module>/eval.ts --matrix`.
3. Specific candidates: `bun <path-to-module>/eval.ts --models=gemini-3.6-flash,gpt-5.6-luna`.
4. Read the **raw outputs** in `eval.md` (or run Pattern B's judge) to assess quality — never rank models by their own self-reported scores.
5. Inspect `git diff <path-to-module>/eval.md` to review latency, cost, and output changes before committing.

Source: https://il.ly/skills/eval