feat: Support for skills (#76)

This commit is contained in:
Devansh Jain
2025-11-07 15:00:01 -08:00
committed by GitHub
parent a71ede95d4
commit ea313159ce
12 changed files with 596 additions and 24 deletions

3
.gitignore vendored
View File

@@ -2,6 +2,9 @@
.letta/settings.local.json
.letta
# User-defined skills
.skills
.idea
node_modules
bun.lockb

View File

@@ -76,6 +76,75 @@ Letta Code uses a hierarchical memory system:
Memory blocks are highly configurable — see our [docs](https://docs.letta.com/guides/agents/memory-blocks) for advanced configuration options.
Join our [Discord](https://discord.gg/letta) to share feedback on persistence patterns for coding agents.
## Skills
**Skills are automatically discovered from a `.skills` directory in your project.**
Skills allow you to define custom capabilities that the agent can reference and use. When you start a new session, Letta Code recursively scans for `SKILL.MD` files and loads any skill definitions found.
### Creating Skills
Create a `.skills` directory in your project root and organize skills in subdirectories:
```bash
mkdir -p .skills/data-analysis
```
Each skill is defined in a file named `SKILL.MD`. The directory structure determines the skill ID:
```
.skills/
├── data-analysis/
│ └── SKILL.MD # skill id: "data-analysis"
└── web/
└── scraper/
└── SKILL.MD # skill id: "web/scraper"
```
Create a skill file (`.skills/data-analysis/SKILL.MD`):
```markdown
---
name: Data Analysis Skill
description: Analyzes CSV files and generates statistical reports
category: Data Processing
tags:
- analytics
- statistics
- csv
---
# Data Analysis Skill
This skill analyzes data files and generates comprehensive reports.
## Usage
Use this skill to analyze CSV files and generate statistical summaries...
```
**Skill File Format:**
- **File name:** Must be named `SKILL.MD` (case-insensitive)
- **Required frontmatter:**
- `name` - Display name for the skill
- `description` - Brief description of what the skill does
- **Optional frontmatter:**
- `category` - Category for organizing skills (skills are grouped by category in the agent's memory)
- `tags` - Array of tags for filtering/searching
- **Body:** Additional details and documentation about the skill
Skills are automatically loaded into the agent's memory on startup, making them available for reference throughout your session.
### Custom Skills Directory
You can specify a custom skills directory using the `--skills` flag:
```bash
letta --skills /path/to/custom/skills
letta -p "Use the custom skills" --skills ~/my-skills
```
## Usage
### Interactive Mode

View File

@@ -2,6 +2,7 @@
* Utilities for creating an agent on the Letta API backend
**/
import { join } from "node:path";
import type { AgentType } from "@letta-ai/letta-client/resources/agents/agents";
import type {
BlockResponse,
@@ -11,9 +12,14 @@ import { settingsManager } from "../settings-manager";
import { getToolNames } from "../tools/manager";
import { getClient } from "./client";
import { getDefaultMemoryBlocks } from "./memory";
import { formatAvailableModels, resolveModel } from "./model";
import {
formatAvailableModels,
getModelUpdateArgs,
resolveModel,
} from "./model";
import { updateAgentLLMConfig } from "./modify";
import { SYSTEM_PROMPT } from "./promptAssets";
import { discoverSkills, formatSkillsForMemory, SKILLS_DIR } from "./skills";
export async function createAgent(
name = "letta-cli-agent",
@@ -21,6 +27,7 @@ export async function createAgent(
embeddingModel = "openai/text-embedding-3-small",
updateArgs?: Record<string, unknown>,
forceNewBlocks = false,
skillsDirectory?: string,
) {
// Resolve model identifier to handle
let modelHandle: string;
@@ -52,6 +59,36 @@ export async function createAgent(
// Load memory blocks from .mdx files
const defaultMemoryBlocks = await getDefaultMemoryBlocks();
// Resolve absolute path for skills directory
const resolvedSkillsDirectory =
skillsDirectory || join(process.cwd(), SKILLS_DIR);
// Discover skills from .skills directory and populate skills memory block
try {
const { skills, errors } = await discoverSkills(resolvedSkillsDirectory);
// Log any errors encountered during skill discovery
if (errors.length > 0) {
console.warn("Errors encountered during skill discovery:");
for (const error of errors) {
console.warn(` ${error.path}: ${error.message}`);
}
}
// Find and update the skills memory block with discovered skills
const skillsBlock = defaultMemoryBlocks.find((b) => b.label === "skills");
if (skillsBlock) {
skillsBlock.value = formatSkillsForMemory(
skills,
resolvedSkillsDirectory,
);
}
} catch (error) {
console.warn(
`Failed to discover skills: ${error instanceof Error ? error.message : String(error)}`,
);
}
// Load global shared memory blocks from user settings
const settings = settingsManager.getSettings();
const globalSharedBlockIds = settings.globalSharedBlockIds;
@@ -123,8 +160,8 @@ export async function createAgent(
}
blockIds.push(createdBlock.id);
// Categorize: style is local, persona/human are global
if (label === "project") {
// Categorize: project/skills are local, persona/human are global
if (label === "project" || label === "skills") {
newLocalBlockIds[label] = createdBlock.id;
} else {
newGlobalBlockIds[label] = createdBlock.id;
@@ -158,6 +195,10 @@ export async function createAgent(
);
}
// Get the model's context window from its configuration
const modelUpdateArgs = getModelUpdateArgs(modelHandle);
const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000;
// Create agent with all block IDs (existing + newly created)
const agent = await client.agents.create({
agent_type: "letta_v1_agent" as AgentType,
@@ -165,7 +206,7 @@ export async function createAgent(
name,
embedding: embeddingModel,
model: modelHandle,
context_window_limit: 200_000,
context_window_limit: contextWindow,
tools: toolNames,
block_ids: blockIds,
tags: ["origin:letta-code"],
@@ -182,9 +223,8 @@ export async function createAgent(
// Apply updateArgs if provided (e.g., reasoningEffort, contextWindow, etc.)
if (updateArgs && Object.keys(updateArgs).length > 0) {
await updateAgentLLMConfig(agent.id, modelHandle, updateArgs);
// Refresh agent state to get updated config
return await client.agents.retrieve(agent.id);
}
return agent; // { id, ... }
// Always retrieve the agent to ensure we get the full state with populated memory blocks
return await client.agents.retrieve(agent.id);
}

View File

@@ -43,7 +43,7 @@ function parseMdxFrontmatter(content: string): {
async function loadMemoryBlocksFromMdx(): Promise<CreateBlock[]> {
const memoryBlocks: CreateBlock[] = [];
const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx"];
const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx", "skills.mdx"];
// const mdxFiles = ["persona.mdx", "human.mdx", "style.mdx"];
// const mdxFiles = ["persona_kawaii.mdx", "human.mdx", "style.mdx"];

View File

@@ -4,6 +4,7 @@ import personaPrompt from "./prompts/persona.mdx";
import personaKawaiiPrompt from "./prompts/persona_kawaii.mdx";
import planModeReminder from "./prompts/plan_mode_reminder.txt";
import projectPrompt from "./prompts/project.mdx";
import skillsPrompt from "./prompts/skills.mdx";
import stylePrompt from "./prompts/style.mdx";
import systemPrompt from "./prompts/system_prompt.txt";
@@ -14,6 +15,7 @@ export const MEMORY_PROMPTS: Record<string, string> = {
"persona.mdx": personaPrompt,
"human.mdx": humanPrompt,
"project.mdx": projectPrompt,
"skills.mdx": skillsPrompt,
"style.mdx": stylePrompt,
"persona_kawaii.mdx": personaKawaiiPrompt,
};

View File

@@ -0,0 +1,6 @@
---
label: skills
description: A memory block to store all available Skills with their metadata (name and description). Whenever a new Skill is discovered / created or an existing Skill is updated, I should store it here. I should always check the `.skills` directory for an updated skill list.
---
[CURRENTLY EMPTY: CREATE A LIST OF AVAILABLE SKILLS (DIRECTORIES WITH SKILL.MD FILE) WITH DESCRIPTIONS IN THIS MEMORY BLOCK. ALWAYS CHECK THE `.skills` DIRECTORY TO MAKE SURE YOU HAVE AN UPDATED LIST OF SKILLS]

View File

@@ -22,4 +22,18 @@ Your memory consists of memory blocks and external memory:
Memory management tools allow you to edit existing memory blocks and query for external memories.
Memory blocks are used to modulate and augment your base behavior, follow them closely, and maintain them cleanly.
They are the foundation which makes you *you*.
They are the foundation which makes you *you*.
# Skills
You have access to Skills—folders of instructions, scripts, and resources that you can load dynamically to improve performance on specialized tasks. Skills teach you how to complete specific tasks in a repeatable way. Skills work through progressive disclosure—you should determine which Skills are relevant to complete a task and load them, helping to prevent context window overload.
Each Skill directory includes:
- `SKILL.md` file that starts with YAML frontmatter containing required metadata: name and description
- Additional files within the Skill directory referenced by name from `SKILL.md`. These additional linked files should be navigated and discovered only as needed.
How to use Skills:
- Skills are automatically discovered on bootup. The Skills directory and any available Skills are stored in the `skills` memory block.
- Review available Skills from the `skills` block when you are asked to complete a task.
- If a Skill is relevant, first load the Skill by reading its full `SKILL.md` into context.
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
- When you load / use Skills, explicitly mention which Skills are loaded / used.
- When the task is completed or when you load new Skills, unload irrelevant Skills by removing them from the context.
Remember to always keep only the full `SKILL.md` into context for all Skills relevant to the current task. Use additional files as needed.

336
src/agent/skills.ts Normal file
View File

@@ -0,0 +1,336 @@
/**
* Skills module - provides skill discovery and management functionality
*/
import { existsSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
/**
* Represents a skill that can be used by the agent
*/
export interface Skill {
/** Unique identifier for the skill */
id: string;
/** Human-readable name of the skill */
name: string;
/** Description of what the skill does */
description: string;
/** Optional category for organizing skills */
category?: string;
/** Optional tags for filtering/searching skills */
tags?: string[];
/** Path to the skill file */
path: string;
}
/**
* Represents the result of skill discovery
*/
export interface SkillDiscoveryResult {
/** List of discovered skills */
skills: Skill[];
/** Any errors encountered during discovery */
errors: SkillDiscoveryError[];
}
/**
* Represents an error that occurred during skill discovery
*/
export interface SkillDiscoveryError {
/** Path where the error occurred */
path: string;
/** Error message */
message: string;
}
/**
* Default directory name where skills are stored
*/
export const SKILLS_DIR = ".skills";
/**
* Parse frontmatter and content from a markdown file
*/
function parseFrontmatter(content: string): {
frontmatter: Record<string, string | string[]>;
body: string;
} {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match || !match[1] || !match[2]) {
return { frontmatter: {}, body: content };
}
const frontmatterText = match[1];
const body = match[2];
const frontmatter: Record<string, string | string[]> = {};
// Parse YAML-like frontmatter (simple key: value pairs and arrays)
const lines = frontmatterText.split("\n");
let currentKey: string | null = null;
let currentArray: string[] = [];
for (const line of lines) {
// Check if this is an array item
if (line.trim().startsWith("-") && currentKey) {
const value = line.trim().slice(1).trim();
currentArray.push(value);
continue;
}
// If we were building an array, save it
if (currentKey && currentArray.length > 0) {
frontmatter[currentKey] = currentArray;
currentKey = null;
currentArray = [];
}
const colonIndex = line.indexOf(":");
if (colonIndex > 0) {
const key = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
currentKey = key;
if (value) {
// Simple key: value pair
frontmatter[key] = value;
currentKey = null;
} else {
// Might be starting an array
currentArray = [];
}
}
}
// Save any remaining array
if (currentKey && currentArray.length > 0) {
frontmatter[currentKey] = currentArray;
}
return { frontmatter, body: body.trim() };
}
/**
* Discovers skills by recursively searching for SKILL.MD files
* @param skillsPath - The directory to search for skills (default: .skills in current directory)
* @returns A result containing discovered skills and any errors
*/
export async function discoverSkills(
skillsPath: string = join(process.cwd(), SKILLS_DIR),
): Promise<SkillDiscoveryResult> {
const errors: SkillDiscoveryError[] = [];
// Check if skills directory exists
if (!existsSync(skillsPath)) {
return { skills: [], errors: [] };
}
const skills: Skill[] = [];
try {
// Recursively find all SKILL.MD files
await findSkillFiles(skillsPath, skillsPath, skills, errors);
} catch (error) {
errors.push({
path: skillsPath,
message: `Failed to read skills directory: ${error instanceof Error ? error.message : String(error)}`,
});
}
return { skills, errors };
}
/**
* Recursively searches for SKILL.MD files in a directory
* @param currentPath - The current directory being searched
* @param rootPath - The root skills directory
* @param skills - Array to collect found skills
* @param errors - Array to collect errors
*/
async function findSkillFiles(
currentPath: string,
rootPath: string,
skills: Skill[],
errors: SkillDiscoveryError[],
): Promise<void> {
try {
const entries = await readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentPath, entry.name);
if (entry.isDirectory()) {
// Recursively search subdirectories
await findSkillFiles(fullPath, rootPath, skills, errors);
} else if (entry.isFile() && entry.name.toUpperCase() === "SKILL.MD") {
// Found a SKILL.MD file
try {
const skill = await parseSkillFile(fullPath, rootPath);
if (skill) {
skills.push(skill);
}
} catch (error) {
errors.push({
path: fullPath,
message: error instanceof Error ? error.message : String(error),
});
}
}
}
} catch (error) {
errors.push({
path: currentPath,
message: `Failed to read directory: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* Parses a skill file and extracts metadata
* @param filePath - Path to the skill file
* @param rootPath - Root skills directory to derive relative path
* @returns A Skill object or null if parsing fails
*/
async function parseSkillFile(
filePath: string,
rootPath: string,
): Promise<Skill | null> {
const content = await readFile(filePath, "utf-8");
// Parse frontmatter
const { frontmatter, body } = parseFrontmatter(content);
// Derive ID from directory structure relative to root
// E.g., .skills/data-analysis/SKILL.MD -> "data-analysis"
// E.g., .skills/web/scraper/SKILL.MD -> "web/scraper"
// Normalize rootPath to not have trailing slash
const normalizedRoot = rootPath.endsWith("/")
? rootPath.slice(0, -1)
: rootPath;
const relativePath = filePath.slice(normalizedRoot.length + 1); // +1 to remove leading slash
const dirPath = relativePath.slice(0, -"/SKILL.MD".length);
const defaultId = dirPath || "root";
const id =
(typeof frontmatter.id === "string" ? frontmatter.id : null) || defaultId;
// Use name from frontmatter or derive from ID
const name =
(typeof frontmatter.name === "string" ? frontmatter.name : null) ||
(typeof frontmatter.title === "string" ? frontmatter.title : null) ||
(id.split("/").pop() ?? "")
.replace(/-/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase());
// Description is required - either from frontmatter or first paragraph of content
let description =
typeof frontmatter.description === "string"
? frontmatter.description
: null;
if (!description) {
// Extract first paragraph from content as description
const firstParagraph = body.trim().split("\n\n")[0];
description = firstParagraph || "No description available";
}
// Strip surrounding quotes from description if present
description = description.trim();
if (
(description.startsWith('"') && description.endsWith('"')) ||
(description.startsWith("'") && description.endsWith("'"))
) {
description = description.slice(1, -1);
}
// Extract tags (handle both string and array)
let tags: string[] | undefined;
if (Array.isArray(frontmatter.tags)) {
tags = frontmatter.tags;
} else if (typeof frontmatter.tags === "string") {
tags = [frontmatter.tags];
}
return {
id,
name,
description,
category:
typeof frontmatter.category === "string"
? frontmatter.category
: undefined,
tags,
path: filePath,
};
}
/**
* Formats discovered skills as a string for the skills memory block
* @param skills - Array of discovered skills
* @param skillsDirectory - Absolute path to the skills directory
* @returns Formatted string representation of skills
*/
export function formatSkillsForMemory(
skills: Skill[],
skillsDirectory: string,
): string {
let output = `Skills Directory: ${skillsDirectory}\n\n`;
if (skills.length === 0) {
return `${output}[NO SKILLS AVAILABLE]`;
}
output += "Available Skills:\n\n";
// Group skills by category if categories exist
const categorized = new Map<string, Skill[]>();
const uncategorized: Skill[] = [];
for (const skill of skills) {
if (skill.category) {
const existing = categorized.get(skill.category) || [];
existing.push(skill);
categorized.set(skill.category, existing);
} else {
uncategorized.push(skill);
}
}
// Output categorized skills
for (const [category, categorySkills] of categorized) {
output += `## ${category}\n\n`;
for (const skill of categorySkills) {
output += formatSkill(skill);
}
output += "\n";
}
// Output uncategorized skills
if (uncategorized.length > 0) {
if (categorized.size > 0) {
output += "## Other\n\n";
}
for (const skill of uncategorized) {
output += formatSkill(skill);
}
}
return output.trim();
}
/**
* Formats a single skill for display
*/
function formatSkill(skill: Skill): string {
let output = `### ${skill.name}\n`;
output += `ID: \`${skill.id}\`\n`;
output += `Description: ${skill.description}\n`;
if (skill.tags && skill.tags.length > 0) {
output += `Tags: ${skill.tags.map((t) => `\`${t}\``).join(", ")}\n`;
}
output += "\n";
return output;
}

View File

@@ -1,6 +1,5 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink";
import Link from "ink-link";
import SpinnerLib from "ink-spinner";
import type { ComponentType } from "react";
import { useEffect, useRef, useState } from "react";

View File

@@ -17,19 +17,37 @@ import { drainStreamWithResume } from "./cli/helpers/stream";
import { settingsManager } from "./settings-manager";
import { checkToolPermission, executeTool } from "./tools/manager";
export async function handleHeadlessCommand(argv: string[], model?: string) {
export async function handleHeadlessCommand(
argv: string[],
model?: string,
skillsDirectory?: string,
) {
const settings = settingsManager.getSettings();
// Parse CLI args
// Include all flags from index.ts to prevent them from being treated as positionals
const { values, positionals } = parseArgs({
args: argv,
options: {
// Flags used in headless mode
continue: { type: "boolean", short: "c" },
new: { type: "boolean" },
agent: { type: "string", short: "a" },
model: { type: "string", short: "m" },
prompt: { type: "boolean", short: "p" },
"output-format": { type: "string" },
// Additional flags from index.ts that need to be filtered out
help: { type: "boolean", short: "h" },
version: { type: "boolean", short: "v" },
run: { type: "boolean" },
tools: { type: "string" },
allowedTools: { type: "string" },
disallowedTools: { type: "string" },
"permission-mode": { type: "string" },
yolo: { type: "boolean" },
skills: { type: "string" },
link: { type: "boolean" },
unlink: { type: "boolean" },
},
strict: false,
allowPositionals: true,
@@ -81,6 +99,7 @@ export async function handleHeadlessCommand(argv: string[], model?: string) {
undefined,
updateArgs,
forceNew,
skillsDirectory,
);
}
@@ -113,7 +132,14 @@ export async function handleHeadlessCommand(argv: string[], model?: string) {
// Priority 5: Create a new agent
if (!agent) {
const updateArgs = getModelUpdateArgs(model);
agent = await createAgent(undefined, model, undefined, updateArgs);
agent = await createAgent(
undefined,
model,
undefined,
updateArgs,
false,
skillsDirectory,
);
}
// Save agent ID to both project and global settings

View File

@@ -32,6 +32,7 @@ OPTIONS
-p, --prompt Headless prompt mode
--output-format <fmt> Output format for headless mode (text, json, stream-json)
Default: text
--skills <path> Custom path to skills directory (default: .skills in current directory)
BEHAVIOR
By default, letta auto-resumes the last agent used in the current directory
@@ -91,6 +92,7 @@ async function main() {
"permission-mode": { type: "string" },
yolo: { type: "boolean" },
"output-format": { type: "string" },
skills: { type: "string" },
link: { type: "boolean" },
unlink: { type: "boolean" },
},
@@ -134,6 +136,7 @@ async function main() {
const forceNew = (values.new as boolean | undefined) ?? false;
const specifiedAgentId = (values.agent as string | undefined) ?? null;
const specifiedModel = (values.model as string | undefined) ?? undefined;
const skillsDirectory = (values.skills as string | undefined) ?? undefined;
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
// Check if API key is configured
@@ -257,7 +260,7 @@ async function main() {
await upsertToolsToServer(client);
const { handleHeadlessCommand } = await import("./headless");
await handleHeadlessCommand(process.argv, specifiedModel);
await handleHeadlessCommand(process.argv, specifiedModel, skillsDirectory);
return;
}
@@ -273,11 +276,13 @@ async function main() {
forceNew,
agentIdArg,
model,
skillsDirectory,
}: {
continueSession: boolean;
forceNew: boolean;
agentIdArg: string | null;
model?: string;
skillsDirectory?: string;
}) {
const [loadingState, setLoadingState] = useState<
| "assembling"
@@ -357,6 +362,7 @@ async function main() {
undefined,
updateArgs,
forceNew,
skillsDirectory,
);
}
@@ -394,7 +400,14 @@ async function main() {
// Priority 5: Create a new agent
if (!agent) {
const updateArgs = getModelUpdateArgs(model);
agent = await createAgent(undefined, model, undefined, updateArgs);
agent = await createAgent(
undefined,
model,
undefined,
updateArgs,
false,
skillsDirectory,
);
}
// Ensure local project settings are loaded before updating
@@ -461,6 +474,7 @@ async function main() {
forceNew: forceNew,
agentIdArg: specifiedAgentId,
model: specifiedModel,
skillsDirectory: skillsDirectory,
}),
{
exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard

View File

@@ -42,15 +42,6 @@
"context_window": 272000
}
},
{
"id": "glm-4.6",
"handle": "openrouter/z-ai/glm-4.6:exacto",
"label": "GLM-4.6",
"description": "The best open weights coding model",
"updateArgs": {
"context_window": 200000
}
},
{
"id": "gpt-5-minimal",
"handle": "openai/gpt-5",
@@ -95,6 +86,64 @@
"context_window": 272000
}
},
{
"id": "gpt-5-mini-medium",
"handle": "openai/gpt-5-mini-2025-08-07",
"label": "GPT-5-Mini (medium)",
"description": "OpenAI's latest mini model (using their recommended reasoning level)",
"updateArgs": {
"reasoning_effort": "medium",
"verbosity": "medium",
"context_window": 272000
}
},
{
"id": "gpt-5-nano-medium",
"handle": "openai/gpt-5-nano-2025-08-07",
"label": "GPT-5-Nano (medium)",
"description": "OpenAI's latest nano model (using their recommended reasoning level)",
"updateArgs": {
"reasoning_effort": "medium",
"verbosity": "medium",
"context_window": 272000
}
},
{
"id": "glm-4.6",
"handle": "openrouter/z-ai/glm-4.6:exacto",
"label": "GLM-4.6",
"description": "The best open weights coding model",
"updateArgs": {
"context_window": 200000
}
},
{
"id": "minimax-m2",
"handle": "openrouter/minimax/minimax-m2",
"label": "Minimax M2",
"description": "Minimax's latest model",
"updateArgs": {
"context_window": 196000
}
},
{
"id": "kimi-k2",
"handle": "openrouter/moonshotai/kimi-k2-0905",
"label": "Kimi K2",
"description": "Kimi's latest model",
"updateArgs": {
"context_window": 262144
}
},
{
"id": "deepseek-chat-v3.1",
"handle": "openrouter/deepseek/deepseek-chat-v3.1",
"label": "DeepSeek Chat V3.1",
"description": "DeepSeek V3.1 model",
"updateArgs": {
"context_window": 128000
}
},
{
"id": "gemini-flash",
"handle": "google_ai/gemini-2.5-flash",
@@ -114,7 +163,21 @@
"handle": "openai/gpt-4.1",
"label": "GPT-4.1",
"description": "OpenAI's most recent non-reasoner model",
"updateArgs": { "context_window": 180000 }
"updateArgs": { "context_window": 1047576 }
},
{
"id": "gpt-4.1-mini",
"handle": "openai/gpt-4.1-mini-2025-04-14",
"label": "GPT-4.1-Mini",
"description": "OpenAI's most recent non-reasoner model (mini version)",
"updateArgs": { "context_window": 1047576 }
},
{
"id": "gpt-4.1-nano",
"handle": "openai/gpt-4.1-nano-2025-04-14",
"label": "GPT-4.1-Nano",
"description": "OpenAI's most recent non-reasoner model (nano version)",
"updateArgs": { "context_window": 1047576 }
},
{
"id": "o4-mini",