Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline 3fe976b5c0 feat(sync): add CrofAI catalog sync 2026-08-17 16:49:02 +00:00
15 changed files with 221 additions and 139 deletions
+1
View File
@@ -23,6 +23,7 @@
"deepinfra:sync": "bun ./packages/core/script/sync-models.ts deepinfra",
"cloudflare:sync": "bun ./packages/core/script/sync-models.ts cloudflare-workers-ai",
"chutes:sync": "bun ./packages/core/script/sync-models.ts chutes",
"crof:sync": "bun ./packages/core/script/sync-models.ts crof",
"databricks:generate": "bun ./packages/core/script/generate-databricks.ts",
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
"huggingface:sync": "bun ./packages/core/script/sync-models.ts huggingface",
+4 -1
View File
@@ -11,6 +11,7 @@ import { baseten } from "./providers/baseten.js";
import { chutes } from "./providers/chutes.js";
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
import { cortecs } from "./providers/cortecs.js";
import { crof } from "./providers/crof.js";
import { crossmodel } from "./providers/crossmodel.js";
import { deepinfra } from "./providers/deepinfra.js";
import { digitalocean } from "./providers/digitalocean.js";
@@ -120,6 +121,7 @@ export const providers: {
chutes: SyncProvider<any>;
"cloudflare-workers-ai": SyncProvider<any>;
cortecs: SyncProvider<any>;
crof: SyncProvider<any>;
crossmodel: SyncProvider<any>;
deepinfra: SyncProvider<any>;
digitalocean: SyncProvider<any>;
@@ -151,6 +153,7 @@ export const providers: {
chutes,
"cloudflare-workers-ai": cloudflareWorkersAi,
cortecs,
crof,
crossmodel,
deepinfra,
digitalocean,
@@ -194,7 +197,7 @@ export const groups = {
"vercel",
],
cloudflare: ["cloudflare-workers-ai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "cortecs", "deepinfra", "digitalocean", "google", "hyper", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "cortecs", "crof", "deepinfra", "digitalocean", "google", "hyper", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
} as const;
type ProviderID = keyof typeof providers;
+126
View File
@@ -0,0 +1,126 @@
import { z } from "zod";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel } from "./openrouter.js";
const API_ENDPOINT = "https://crof.ai/v1/models";
const Price = z.string().regex(/^\d+(?:\.\d+)?$/).transform(Number);
export const CrofModel = z.object({
id: z.string().min(1),
name: z.string().min(1),
created: z.number().int().nonnegative(),
context_length: z.number().int().positive(),
max_completion_tokens: z.number().int().positive(),
custom_reasoning: z.boolean(),
reasoning_effort: z.boolean().optional(),
pricing: z.object({
prompt: Price,
completion: Price,
cache_prompt: Price.optional(),
discount: z.number().nonnegative().max(100).optional(),
}).passthrough(),
}).passthrough();
export const CrofResponse = z.object({
data: z.array(CrofModel),
}).passthrough();
export type CrofModel = z.infer<typeof CrofModel>;
export const crof = {
id: "crof",
name: "CrofAI",
modelsDir: "providers/crof/models",
skipCreates: true,
deleteMissing: true,
sourceID(model) {
return model.id;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} CrofAI models returned by the API were not created because the endpoint does not provide authoritative modalities, model capabilities, or descriptions. Existing models are still fully synchronized.`,
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
async fetchModels() {
const response = await fetch(API_ENDPOINT);
if (!response.ok) {
throw new Error(`CrofAI models request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
return CrofResponse.parse(raw).data;
},
translateModel(model, context) {
const existing = context.existing(model.id);
if (existing === undefined) return undefined;
return {
id: model.id,
model: buildCrofModel(model, existing, context.authored(model.id)),
};
},
} satisfies SyncProvider<CrofModel>;
export function buildCrofModel(
model: CrofModel,
existing: ExistingModel,
authored: ExistingModel | undefined,
): SyncedModel {
const required = {
name: existing.name,
description: existing.description,
release_date: existing.release_date,
last_updated: existing.last_updated,
attachment: existing.attachment,
temperature: existing.temperature,
tool_call: existing.tool_call,
open_weights: existing.open_weights,
modalities: existing.modalities,
};
for (const [field, value] of Object.entries(required)) {
if (value === undefined) throw new Error(`CrofAI model ${model.id} is missing local ${field} metadata`);
}
const reasoning = model.custom_reasoning;
const limit = {
input: existing.limit?.input,
context: model.context_length,
output: model.max_completion_tokens,
};
const synced: SyncedFullModel = {
name: required.name!,
description: required.description!,
family: existing.family,
release_date: required.release_date!,
last_updated: required.last_updated!,
attachment: required.attachment!,
reasoning,
reasoning_options: reasoning ? existing.reasoning_options ?? [] : undefined,
temperature: required.temperature!,
tool_call: required.tool_call!,
structured_output: existing.structured_output,
knowledge: existing.knowledge,
open_weights: required.open_weights!,
status: existing.status,
interleaved: reasoning ? existing.interleaved : undefined,
cost: {
input: model.pricing.prompt,
output: model.pricing.completion,
cache_read: model.pricing.cache_prompt,
cache_write: existing.cost?.cache_write,
reasoning: existing.cost?.reasoning,
tiers: existing.cost?.tiers,
},
limit,
modalities: required.modalities!,
provider: existing.provider,
};
return authored?.base_model === undefined
? synced
: factorBaseModel(authored.base_model, synced, limit, authored.base_model_omit);
}
+64
View File
@@ -11,6 +11,7 @@ import {
type AnthropicModel,
} from "../src/sync/providers/anthropic.js";
import { buildCortecsModel, type CortecsModel } from "../src/sync/providers/cortecs.js";
import { buildCrofModel, CrofResponse, type CrofModel } from "../src/sync/providers/crof.js";
import {
buildCrossModel,
CrossModelResponse,
@@ -2679,6 +2680,69 @@ test("overrides canonical metadata with Cortecs reasoning support", () => {
});
});
test("parses CrofAI string prices as per-million numbers", () => {
const parsed = CrofResponse.parse({
data: [{
id: "glm-5.2",
name: "Z.ai: GLM 5.2",
created: 1_781_650_191,
context_length: 1_000_000,
max_completion_tokens: 131_072,
custom_reasoning: true,
reasoning_effort: true,
pricing: {
prompt: "0.15",
completion: "0.52",
cache_prompt: "0.02",
discount: 50,
},
}],
});
expect(parsed.data[0]?.pricing).toMatchObject({
prompt: 0.15,
completion: 0.52,
cache_prompt: 0.02,
discount: 50,
});
});
test("updates CrofAI authoritative pricing and limits while preserving local metadata", () => {
const model: CrofModel = {
id: "glm-5.2",
name: "Z.ai: GLM 5.2",
created: 1_781_650_191,
context_length: 1_000_000,
max_completion_tokens: 131_072,
custom_reasoning: true,
reasoning_effort: true,
pricing: { prompt: 0.15, completion: 0.52, cache_prompt: 0.02, discount: 50 },
};
const existing: ExistingModel = {
name: "GLM 5.2",
description: "Preserved description",
release_date: "2026-06-15",
last_updated: "2026-06-15",
attachment: false,
reasoning: true,
reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }],
temperature: true,
tool_call: true,
open_weights: true,
cost: { input: 0.3, output: 1.05, cache_read: 0.05, cache_write: 0 },
limit: { context: 202_752, output: 202_752 },
modalities: { input: ["text"], output: ["text"] },
};
expect(buildCrofModel(model, existing, undefined)).toMatchObject({
name: "GLM 5.2",
description: "Preserved description",
reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }],
cost: { input: 0.15, output: 0.52, cache_read: 0.02, cache_write: 0 },
limit: { context: 1_000_000, output: 131_072 },
});
});
test("syncs OpenRouter reasoning efforts from model metadata", () => {
const model = buildOpenRouterModel(openRouterModel({
reasoning: {
@@ -1,17 +0,0 @@
base_model = "deepseek/deepseek-v4-pro"
name = "DeepSeek V4 Pro Lightning"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 0.8
output = 1.6
cache_read = 0.02
[limit]
output = 131_072
[provider]
npm = "@ai-sdk/openai-compatible"
-16
View File
@@ -1,16 +0,0 @@
base_model = "zhipuai/glm-4.7-flash"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[cost]
input = 0.04
output = 0.3
cache_read = 0.008
cache_write = 0
[limit]
context = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
-20
View File
@@ -1,20 +0,0 @@
base_model = "zhipuai/glm-4.7"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.25
output = 1.1
cache_read = 0.05
cache_write = 0
[limit]
context = 202_752
output = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
+7 -7
View File
@@ -1,16 +1,16 @@
base_model = "zhipuai/glm-5.2"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 0.3
output = 1.05
cache_read = 0.05
[[reasoning_options]]
type = "effort"
values = ["none", "low", "medium", "high"]
[limit]
output = 131_072
[cost]
input = 0.15
output = 0.52
cache_read = 0.02
[provider]
npm = "@ai-sdk/openai-compatible"
-20
View File
@@ -1,20 +0,0 @@
base_model = "zhipuai/glm-5"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.48
output = 1.9
cache_read = 0.1
cache_write = 0
[limit]
context = 202_752
output = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
@@ -1,28 +0,0 @@
name = "Kimi K2.5 (Lightning)"
description = "Kimi multimodal agent model for visual understanding, coding, and planning"
family = "kimi-k2"
release_date = "2026-02-06"
last_updated = "2026-02-06"
attachment = true
reasoning = true
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
temperature = false
tool_call = true
structured_output = true
open_weights = true
[interleaved]
field = "reasoning_content"
[cost]
input = 1.00
output = 3.00
cache_read = 0.20
[limit]
context = 131_072
output = 32_768
[modalities]
input = ["text", "image", "video"]
output = ["text"]
-13
View File
@@ -1,13 +0,0 @@
base_model = "moonshotai/kimi-k2.5"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 0.35
output = 1.7
cache_read = 0.07
[provider]
npm = "@ai-sdk/openai-compatible"
+4 -2
View File
@@ -1,10 +1,13 @@
base_model = "moonshotai/kimi-k3"
name = "Kimi K3 Eco"
reasoning_options = [{ type = "effort", values = ["none", "low", "high", "max"] }]
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "high", "max"]
[cost]
input = 1
output = 4
@@ -12,7 +15,6 @@ cache_read = 0.1
[limit]
context = 1_000_000
output = 131_072
[provider]
npm = "@ai-sdk/openai-compatible"
+7 -1
View File
@@ -1,9 +1,12 @@
base_model = "xiaomi/mimo-v2.5-pro"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "medium", "high"]
[cost]
input = 0.4
output = 0.8
@@ -15,5 +18,8 @@ input = 2
output = 6
cache_read = 0.4
[limit]
context = 1_000_000
[provider]
npm = "@ai-sdk/openai-compatible"
-14
View File
@@ -1,14 +0,0 @@
base_model = "minimax/MiniMax-M2.5"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning = true
reasoning_options = []
[cost]
input = 0.11
output = 0.95
cache_read = 0.02
cache_write = 0.375
[provider]
npm = "@ai-sdk/openai-compatible"
+8
View File
@@ -297,6 +297,14 @@ Venice is implemented in `packages/core/src/sync/providers/venice.ts`.
- Every Venice model uses `base_model`; flattened IDs are matched to provider-agnostic metadata before provider-specific overrides are written.
- Every Venice model declares `reasoning_options`; models without API-provided effort levels use an empty array.
## CrofAI Notes
- CrofAI is implemented in `packages/core/src/sync/providers/crof.ts` and uses the public `https://crof.ai/v1/models` endpoint.
- Run it with `bun models:sync crof` or `bun crof:sync`.
- Pricing, context limits, output limits, and reasoning availability are synchronized from the API for existing models.
- Local metadata is retained for fields the API does not expose, including descriptions, modalities, model capabilities, and reasoning option values.
- Models removed from the API are removed locally. New API models are reported but not created automatically because the endpoint does not expose enough metadata for complete catalog entries.
## Standalone Generators
Some provider scripts in `packages/core/script/generate-*.ts` are not wired into `bun models:sync`. When updating those scripts, preserve existing `base_model` and `base_model_omit` fields for generated TOMLs that already use model metadata inheritance. New inheritance-aware output should use `base_model`; do not reintroduce legacy `[extends]` syntax.