# Model Normalization Across Different AI Providers in the Compound-Engineering Plugin

> Normalize AI models across providers like Anthropic, OpenAI, and Google with the Compound Engineering Plugin. Our `normalizeModel` function simplifies model identifiers for seamless integration.

- Repository: [Every/compound-engineering-plugin](https://github.com/everyinc/compound-engineering-plugin)
- Tags: deep-dive
- Published: 2026-02-16

---

**The Compound-Engineering Plugin automatically converts Claude Code plugins to OpenCode format by normalizing model identifiers through the `normalizeModel` function in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts), which maps aliases like "haiku" to fully-qualified strings like "anthropic/claude-haiku-4-5" while supporting Anthropic, OpenAI, and Google providers.**

The EveryInc/compound-engineering-plugin bridges Claude Code plugins with the OpenCode ecosystem, requiring seamless translation of model identifiers across incompatible provider formats. Model normalization across different AI providers ensures that short aliases, partial names, and fully-qualified identifiers all resolve to consistent, provider-prefixed strings that downstream writers can interpret unambiguously.

## How Model Normalization Works in [`claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/claude-to-opencode.ts)

The normalization logic lives in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts) and follows a strict four-step precedence chain to resolve any input string into a qualified identifier.

### The `normalizeModel` Function Implementation

The core `normalizeModel` function (lines 261-274) implements the resolution logic:

```typescript
function normalizeModel(model: string): string {
  // 1️⃣ Already qualified? → return unchanged  
  if (model.includes("/")) return model

  // 2️⃣ Short Claude alias? → map via CLAUDE_FAMILY_ALIASES  
  if (CLAUDE_FAMILY_ALIASES[model]) {
    const resolved = `anthropic/${CLAUDE_FAMILY_ALIASES[model]}`
    console.warn(`Warning: bare model alias "${model}" mapped to "${resolved}". …`);
    return resolved
  }

  // 3️⃣ Model name starts with a known provider prefix → prepend the proper namespace  
  if (/^claude-/.test(model)) return `anthropic/${model}`
  if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
  if (/^gemini-/.test(model)) return `google/${model}`

  // 4️⃣ Fallback – assume it is a Claude model  
  return `anthropic/${model}`
}

```

### Claude Family Aliases Mapping

Lines 255-259 define the `CLAUDE_FAMILY_ALIASES` table that maps convenient short names to official Claude model identifiers:

```typescript
const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
  haiku: "claude-haiku-4-5",
  sonnet: "claude-sonnet-4-5",
  opus: "claude-opus-4-5",
}

```

When a plugin author specifies `model: haiku`, the normalization process emits `anthropic/claude-haiku-4-5` and logs a warning about the alias resolution.

## Provider-Specific Normalization Rules

The plugin recognizes three major AI providers through regex pattern matching, ensuring that model identifiers from different ecosystems receive the correct namespace prefix.

### Anthropic Claude Models

Any string beginning with `claude-` is prefixed with `anthropic/`. This catches full model names like `claude-sonnet-4-5` that users might provide without the provider prefix. The alias table handles the shorter nicknames (`haiku`, `sonnet`, `opus`) before this regex check occurs.

### OpenAI GPT and O-Series Models

OpenAI identifiers matching `^gpt-`, `^o1-`, or `^o3-` receive the `openai/` prefix. This covers current and future GPT-4 variants, reasoning models like `o1-preview`, and upcoming `o3` series models, ensuring they resolve to strings like `openai/gpt-4o` or `openai/o1-mini`.

### Google Gemini Models

Strings starting with `gemini-` are prefixed with `google/`, converting identifiers like `gemini-1.5-flash` into fully-qualified `google/gemini-1.5-flash` strings that downstream writers can route to the correct Google AI API endpoints.

## Integration with Agent and Command Conversion

The normalization logic is invoked at specific points during the conversion pipeline to ensure every model reference is resolved before writing the OpenCode output.

### Normalizing Agent Models

During agent conversion (lines 95-96), when an agent declares a `model` field that is not `"inherit"`, the value passes through `normalizeModel`:

```typescript
// From convertAgent function
if (agent.model && agent.model !== "inherit") {
  frontmatter.model = normalizeModel(agent.model);
}

```

This ensures that the OpenCode frontmatter receives a fully-qualified identifier regardless of how the original Claude plugin specified the model.

### Normalizing Command-Level Model Overrides

Command conversion (lines 22-24) applies the same normalization to command-specific model overrides:

```typescript
// From convertCommands function
if (command.model) {
  commandMap[command.name].model = normalizeModel(command.model);
}

```

Commands that set `disable-model-invocation: true` are skipped entirely, preserving the original intent while ensuring that any active model reference is properly qualified.

## Code Examples

### Normalizing a Bare Claude Alias

```typescript
import { normalizeModel } from "./converters/claude-to-opencode";

const raw = "haiku";
const qualified = normalizeModel(raw);
// qualified === "anthropic/claude-haiku-4-5"

```

### Converting an Agent with a Model Field

```typescript
// Claude plugin snippet
{
  name: "Security Auditor",
  description: "Audits code for vulnerabilities",
  model: "gpt-4o",          // OpenAI model, short form
  body: "...",
}

```

During conversion (`convertAgent`), the plugin produces:

```yaml

# OpenCode front‑matter (excerpt)

model: openai/gpt-4o

```

### Command-Level Model Override

```typescript
// In a Claude command
{
  name: "write-doc",
  description: "Generates documentation",
  model: "sonnet",          // Bare Claude alias
  body: "...",
}

```

After `convertCommands`:

```json
{
  "write-doc": {
    "description": "Generates documentation",
    "template": "...",
    "model": "anthropic/claude-sonnet-4-5"
  }
}

```

### Handling an Already-Qualified Identifier

```typescript
normalizeModel("google/gemini-1.5-flash");
// Returns "google/gemini-1.5-flash" unchanged

```

## Summary

- The **Compound-Engineering Plugin** converts Claude Code plugins to OpenCode format using the `normalizeModel` function in [`src/converters/claude-to-opencode.ts`](https://github.com/EveryInc/compound-engineering-plugin/blob/main/src/converters/claude-to-opencode.ts).
- **Model normalization across different AI providers** follows a four-step precedence: fully-qualified IDs pass through unchanged, bare Claude aliases map to official names, regex patterns detect provider families (Anthropic, OpenAI, Google), and unknown strings default to Anthropic.
- The logic handles **agent-level** (lines 95-96) and **command-level** (lines 22-24) model declarations, ensuring every OpenCode output contains provider-prefixed identifiers like `anthropic/claude-sonnet-4-5` or `openai/gpt-4o`.
- Downstream writers (Pi, Gemini, Codex targets) receive consistent, fully-qualified model strings regardless of the original plugin's alias format.

## Frequently Asked Questions

### What happens if I use a bare alias like "sonnet" in my Claude plugin?

The `normalizeModel` function detects the bare alias through the `CLAUDE_FAMILY_ALIASES` table (lines 255-259) and maps it to the fully-qualified identifier `anthropic/claude-sonnet-4-5`. A warning is logged to inform you that the alias was resolved, encouraging the use of fully-qualified names in future configurations.

### How does the plugin handle already qualified model identifiers?

If the input string contains a forward slash (`/`), the function returns it unchanged immediately (line 263). This allows users to specify exact provider/model combinations like `google/gemini-1.5-flash` or `openai/gpt-4o` without interference from the normalization logic.

### Which AI providers are supported by the normalization logic?

The plugin recognizes three major provider families through regex pattern matching (lines 270-272): **Anthropic** (patterns starting with `claude-`), **OpenAI** (patterns starting with `gpt-`, `o1-`, or `o3-`), and **Google** (patterns starting with `gemini-`). Any unrecognized string defaults to the Anthropic namespace as a fallback.

### What is the difference between agent-level and command-level model normalization?

During **agent conversion** (`convertAgent`, lines 95-96), the plugin normalizes the `model` field from the agent definition into the OpenCode frontmatter. During **command conversion** (`convertCommands`, lines 22-24), it normalizes model overrides specified at the individual command level. Both paths use the same `normalizeModel` function, ensuring consistency whether the model is defined globally for an agent or locally for a specific command.