Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fe976b5c0 |
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user