Skip to content

Commit ac0d4fe

Browse files
authored
feat: model profiles (#9387)
1 parent 636e809 commit ac0d4fe

File tree

43 files changed

+4442
-463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+4442
-463
lines changed

.changeset/grumpy-planes-hear.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@langchain/google-common": patch
3+
"@langchain/google-genai": patch
4+
"@langchain/anthropic": patch
5+
"@langchain/deepseek": patch
6+
"@langchain/openai": patch
7+
"@langchain/groq": patch
8+
"@langchain/xai": patch
9+
"@langchain/model-profiles": patch
10+
"@langchain/core": patch
11+
---
12+
13+
Add `ModelProfile` and `.profile` properties to ChatModel

internal/model-profiles/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Model Profiles Generator
2+
3+
A CLI tool for automatically generating TypeScript model profile files from the [models.dev](https://models.dev) API. This tool fetches model capabilities and constraints, applies provider-level and model-specific overrides, and generates type-safe TypeScript files using the TypeScript AST API.
4+
5+
## Overview
6+
7+
The model-profiles generator simplifies the process of maintaining model capability profiles across LangChain provider packages.
8+
9+
### Key Features
10+
11+
- 🔄 **Automatic Data Fetching**: Fetches latest model data from the models.dev API
12+
- 🎯 **Provider-Level Overrides**: Apply overrides to all models for a provider
13+
- 🔧 **Model-Specific Overrides**: Fine-tune individual model profiles
14+
- 📝 **TypeScript AST Generation**: Uses TypeScript compiler API for type-safe code generation
15+
- 🎨 **Prettier Integration**: Automatically formats generated code using your project's Prettier config
16+
- 📦 **Monorepo Friendly**: Works seamlessly with pnpm workspaces and `--filter` commands
17+
-**Type Safety**: Generates code that matches the `ModelProfile` interface from `@langchain/core`
18+
19+
## Architecture
20+
21+
The model-profiles generator consists of:
22+
23+
```text
24+
internal/model-profiles/
25+
├── src/
26+
│ ├── cli.ts # Command-line interface
27+
│ ├── config.ts # TOML config parsing and override logic
28+
│ ├── generator.ts # TypeScript code generation and API integration
29+
│ ├── api-schema.ts # TypeScript types for models.dev API
30+
│ └── tests/ # Test suite
31+
│ ├── config.test.ts
32+
│ └── generator.test.ts
33+
├── package.json # Tool dependencies
34+
├── vitest.config.ts # Test configuration
35+
└── README.md # This documentation
36+
```
37+
38+
## Usage
39+
40+
### Basic Usage
41+
42+
Create a TOML configuration file (e.g., `profiles.toml`) in a provider package:
43+
44+
```toml
45+
provider = "openai"
46+
output = "src/chat_models/profiles.ts"
47+
```
48+
49+
Then run the generator:
50+
51+
```bash
52+
# From the model-profiles package
53+
pnpm --filter @langchain/model-profiles make --config profiles.toml
54+
55+
# Or if running from within a provider package
56+
pnpm --filter @langchain/model-profiles make --config profiles.toml
57+
```
58+
59+
### Configuration File Format
60+
61+
The TOML configuration file supports the following structure:
62+
63+
```toml
64+
# Required: Provider ID from models.dev
65+
provider = "openai"
66+
67+
# Required: Output path for generated TypeScript file (relative to config file)
68+
output = "src/chat_models/profiles.ts"
69+
70+
# Optional: Provider-level overrides (applied to all models)
71+
[overrides]
72+
maxInputTokens = 100000
73+
toolCalling = true
74+
structuredOutput = true
75+
imageUrlInputs = true
76+
77+
# Optional: Model-specific overrides (override provider-level settings)
78+
[overrides."gpt-4"]
79+
maxOutputTokens = 8192
80+
81+
[overrides."gpt-3.5-turbo"]
82+
maxInputTokens = 16385
83+
imageUrlInputs = false
84+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@langchain/model-profiles",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"make": "tsx src/cli.ts",
8+
"test": "vitest run",
9+
"test:watch": "vitest"
10+
},
11+
"dependencies": {
12+
"@iarna/toml": "^2.2.5",
13+
"@langchain/core": "workspace:*",
14+
"commander": "^11.1.0",
15+
"prettier": "^2.8.3",
16+
"typescript": "^5.9.3",
17+
"zod": "^4.1.12"
18+
},
19+
"devDependencies": {
20+
"@types/iarna__toml": "^2.0.5",
21+
"@types/node": "^20.0.0",
22+
"tsx": "^4.20.3",
23+
"vitest": "^1.0.0"
24+
}
25+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Schema definitions for model and provider types.
3+
*
4+
* Adapted from: https://github.com/sst/models.dev/blob/dev/packages/core/src/schema.ts
5+
*
6+
* Original source: SST models.dev
7+
* License: Apache-2.0 (https://github.com/sst/models.dev/blob/dev/LICENSE)
8+
*
9+
* This file contains Zod schema definitions for validating model and provider
10+
* configurations used in the langchain-model-profiles package.
11+
*/
12+
13+
import { z } from "zod/v3";
14+
15+
export const Model = z
16+
.object({
17+
id: z.string(),
18+
name: z.string().min(1, "Model name cannot be empty"),
19+
attachment: z.boolean(),
20+
reasoning: z.boolean(),
21+
tool_call: z.boolean(),
22+
structured_output: z.boolean().optional(),
23+
temperature: z.boolean().optional(),
24+
knowledge: z
25+
.string()
26+
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
27+
message: "Must be in YYYY-MM or YYYY-MM-DD format",
28+
})
29+
.optional(),
30+
release_date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
31+
message: "Must be in YYYY-MM or YYYY-MM-DD format",
32+
}),
33+
last_updated: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
34+
message: "Must be in YYYY-MM or YYYY-MM-DD format",
35+
}),
36+
modalities: z.object({
37+
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
38+
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
39+
}),
40+
open_weights: z.boolean(),
41+
cost: z
42+
.object({
43+
input: z.number().min(0, "Input price cannot be negative"),
44+
output: z.number().min(0, "Output price cannot be negative"),
45+
reasoning: z
46+
.number()
47+
.min(0, "Input price cannot be negative")
48+
.optional(),
49+
cache_read: z
50+
.number()
51+
.min(0, "Cache read price cannot be negative")
52+
.optional(),
53+
cache_write: z
54+
.number()
55+
.min(0, "Cache write price cannot be negative")
56+
.optional(),
57+
input_audio: z
58+
.number()
59+
.min(0, "Audio input price cannot be negative")
60+
.optional(),
61+
output_audio: z
62+
.number()
63+
.min(0, "Audio output price cannot be negative")
64+
.optional(),
65+
})
66+
.optional(),
67+
limit: z.object({
68+
context: z.number().min(0, "Context window must be positive"),
69+
output: z.number().min(0, "Output tokens must be positive"),
70+
}),
71+
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
72+
provider: z
73+
.object({
74+
npm: z.string().optional(),
75+
api: z.string().optional(),
76+
})
77+
.optional(),
78+
})
79+
.strict()
80+
.refine(
81+
(data) => {
82+
return !(data.reasoning === false && data.cost?.reasoning !== undefined);
83+
},
84+
{
85+
message: "Cannot set cost.reasoning when reasoning is false",
86+
path: ["cost", "reasoning"],
87+
}
88+
);
89+
90+
export type Model = z.infer<typeof Model>;
91+
92+
export const Provider = z
93+
.object({
94+
id: z.string(),
95+
env: z.array(z.string()).min(1, "Provider env cannot be empty"),
96+
npm: z.string().min(1, "Provider npm module cannot be empty"),
97+
api: z.string().optional(),
98+
name: z.string().min(1, "Provider name cannot be empty"),
99+
doc: z
100+
.string()
101+
.min(
102+
1,
103+
"Please provide a link to the provider documentation where models are listed"
104+
),
105+
models: z.record(Model),
106+
})
107+
.strict();
108+
109+
export type Provider = z.infer<typeof Provider>;
110+
111+
export const ProviderMap = z.record(z.string(), Provider);
112+
export type ProviderMap = z.infer<typeof ProviderMap>;

internal/model-profiles/src/cli.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env node
2+
import { Command } from "commander";
3+
import { parseConfig, separateOverrides } from "./config.js";
4+
import { generateModelProfiles } from "./generator.js";
5+
6+
const program = new Command();
7+
8+
program
9+
.name("model-profiles")
10+
.description("Make model profiles for a provider")
11+
.requiredOption("--config <path>", "Path to the config TOML file")
12+
.action(async (options: { config: string }) => {
13+
try {
14+
const config = parseConfig(options.config);
15+
if (!config.provider) {
16+
throw new Error(
17+
'Provider name must be specified in the config file (provider = "...")'
18+
);
19+
}
20+
21+
const { providerOverrides, modelOverrides } = separateOverrides(
22+
config.overrides
23+
);
24+
25+
await generateModelProfiles(
26+
config.provider,
27+
providerOverrides,
28+
modelOverrides,
29+
config.output
30+
);
31+
} catch (error) {
32+
console.error(
33+
`Error: ${error instanceof Error ? error.message : String(error)}`
34+
);
35+
process.exit(1);
36+
}
37+
});
38+
39+
program.parse(process.argv);

0 commit comments

Comments
 (0)