Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4fc0329
add updater agent
oskarkocol Jun 29, 2026
623855a
Merge branch 'anomalyco:dev' into dev
oskarkocol Jun 29, 2026
ec23809
update updater action
oskarkocol Jun 29, 2026
56a0354
update the note on cache_read and cache_input
oskarkocol Jun 29, 2026
ee99c77
clean the message
oskarkocol Jun 29, 2026
b138bba
minimize noise
oskarkocol Jun 29, 2026
2a00ad9
select providers
oskarkocol Jun 29, 2026
ed588fd
chore: sync dev from anomalyco/models.dev
cursoragent Jul 2, 2026
26bb57a
add charm models
oskarkocol Jul 2, 2026
c8dba1f
fix output limits
oskarkocol Jul 2, 2026
ae61854
draft svg logo
oskarkocol Jul 2, 2026
d0e349c
Merge branch 'anomalyco:dev' into dev
oskarkocol Jul 3, 2026
ebf0114
Merge remote-tracking branch 'upstream/dev' into feat/add-charm-models
oskarkocol Jul 3, 2026
2cc4df0
update charm logo
oskarkocol Jul 3, 2026
c519002
charm auto script
oskarkocol Jul 3, 2026
bae76b9
add pricing
oskarkocol Jul 3, 2026
8897d46
chore: sync dev from anomalyco/models.dev
cursoragent Jul 5, 2026
7b2f92b
Merge branch 'anomalyco:dev' into dev
oskarkocol Jul 7, 2026
6011f59
Merge branch 'anomalyco:dev' into dev
oskarkocol Jul 8, 2026
ede30b0
Merge upstream/dev into feat/add-charm-models
oskarkocol Jul 8, 2026
c623827
run sync
oskarkocol Jul 8, 2026
2fccbb1
Create test-hyper-sync.yml
oskarkocol Jul 8, 2026
fff939f
Merge branch 'dev' of https://github.com/oskarkocol/models.dev-contri…
oskarkocol Jul 8, 2026
a39c95a
Update test-hyper-sync.yml
oskarkocol Jul 8, 2026
8c881e7
Merge upstream/dev into dev
oskarkocol Jul 10, 2026
c48b9a7
Merge branch 'anomalyco:dev' into dev
oskarkocol Jul 10, 2026
d829c02
Merge origin/dev into dev
oskarkocol Jul 10, 2026
510abae
Merge dev into feat/add-charm-models
oskarkocol Jul 10, 2026
314deab
Merge remote-tracking branch 'upstream/dev' into feat/add-charm-models
oskarkocol Jul 10, 2026
2cf1873
Merge branch 'feat/add-charm-models' of https://github.com/oskarkocol…
oskarkocol Jul 10, 2026
9065720
Revert "Merge branch 'feat/add-charm-models' of https://github.com/os…
oskarkocol Jul 10, 2026
07ca683
update charm mapping
oskarkocol Jul 10, 2026
daa61c4
re-sync models
oskarkocol Jul 10, 2026
f07e9f2
update mapping
oskarkocol Jul 10, 2026
87df33a
resync
oskarkocol Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
HYPER_API_KEY: ${{ secrets.HYPER_API_KEY }}
CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID }}
CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN }}

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { crossmodel } from "./providers/crossmodel.js";
import { deepinfra } from "./providers/deepinfra.js";
import { digitalocean } from "./providers/digitalocean.js";
import { google } from "./providers/google.js";
import { hyper } from "./providers/hyper.js";
import { huggingface } from "./providers/huggingface.js";
import { llmgateway } from "./providers/llmgateway.js";
import { openai } from "./providers/openai.js";
Expand Down Expand Up @@ -97,6 +98,7 @@ export const providers: {
deepinfra: SyncProvider<any>;
digitalocean: SyncProvider<any>;
google: SyncProvider<any>;
hyper: SyncProvider<any>;
huggingface: SyncProvider<any>;
llmgateway: SyncProvider<any>;
openai: SyncProvider<any>;
Expand All @@ -115,6 +117,7 @@ export const providers: {
deepinfra,
digitalocean,
google,
hyper,
huggingface,
llmgateway,
openai,
Expand All @@ -129,7 +132,7 @@ export const providers: {
export const groups = {
aggregators: ["crossmodel", "huggingface", "llmgateway", "openrouter", "vercel"],
cloudflare: ["cloudflare-workers-ai"],
direct: ["anthropic", "baseten", "chutes", "deepinfra", "digitalocean", "google", "openai", "ovhcloud", "venice", "wandb", "xai"],
direct: ["anthropic", "baseten", "chutes", "deepinfra", "digitalocean", "google", "hyper", "openai", "ovhcloud", "venice", "wandb", "xai"],
} as const;

type ProviderID = keyof typeof providers;
Expand Down
252 changes: 252 additions & 0 deletions packages/core/src/sync/providers/hyper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { z } from "zod";

import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel } from "./openrouter.js";
import { resolveVeniceBaseModel } from "./venice.js";

const API_ENDPOINT = "https://hyper.charm.land/v1/models";
const PROVIDER_ENDPOINT = "https://hyper.charm.land/v1/provider";

const PricingValue = z.union([z.string(), z.number()]);

const Pricing = z.object({
prompt: PricingValue.optional(),
completion: PricingValue.optional(),
input_cache_read: PricingValue.optional(),
input_cache_reads: PricingValue.optional(),
input_cache_write: PricingValue.optional(),
input_cache_writes: PricingValue.optional(),
internal_reasoning: PricingValue.optional(),
}).passthrough();

const ReasoningEffort = z.enum([
"default",
"max",
"low",
"high",
"none",
"medium",
"minimal",
"xhigh",
]);

export const HyperModel = z.object({
id: z.string(),
created: z.number(),
display_name: z.string(),
supports_reasoning: z.boolean(),
supports_reasoning_effort: z.boolean(),
reasoning_effort_levels: z.array(z.string()).optional(),
supports_attachments: z.boolean(),
context_window: z.number(),
max_output_tokens: z.number(),
pricing: Pricing.optional(),
cost_per_1m_in: z.number().optional(),
cost_per_1m_out: z.number().optional(),
cost_per_1m_in_cached: z.number().optional(),
cost_per_1m_out_cached: z.number().optional(),
}).passthrough();

export const HyperResponse = z.object({
data: z.array(HyperModel),
}).passthrough();

const HyperProviderModel = z.object({
id: z.string(),
cost_per_1m_in: z.number(),
cost_per_1m_out: z.number(),
cost_per_1m_in_cached: z.number().optional(),
cost_per_1m_out_cached: z.number().optional(),
}).passthrough();

const HyperProviderResponse = z.object({
models: z.array(HyperProviderModel),
}).passthrough();

export type HyperModel = z.infer<typeof HyperModel>;

const BASE_MODEL_ALIASES: Record<string, string> = {
"llama-4-maverick-17b-128e-instruct-fp8": "meta/llama-4-maverick-17b-instruct",
"minimax-m2.7": "minimax/MiniMax-M2.7",
"qwen3-coder-480b-a35b-instruct-int4-mixed-ar": "alibaba/qwen3-coder-480b-a35b-instruct",
"qwen3.6-max": "alibaba/qwen3.6-max-preview",
};

export const hyper = {
id: "hyper",
name: "Charm Hyper",
modelsDir: "providers/hyper/models",
preserveBaseModels: false,
async fetchModels() {
const headers = process.env.HYPER_API_KEY !== undefined
? { Authorization: `Bearer ${process.env.HYPER_API_KEY}` }
: undefined;
const [modelsResponse, providerResponse] = await Promise.all([
fetch(API_ENDPOINT, { headers }),
fetch(PROVIDER_ENDPOINT),
]);
if (!modelsResponse.ok) {
throw new Error(`Hyper models request failed: ${modelsResponse.status} ${modelsResponse.statusText}`);
}

const modelsRaw = await modelsResponse.json();
const parsed = HyperResponse.parse(modelsRaw);
if (!providerResponse.ok) return modelsRaw;

const pricingById = new Map(
HyperProviderResponse.parse(await providerResponse.json()).models.map((model) => [model.id, model]),
);

return {
...modelsRaw,
data: parsed.data.map((model) => enrichPricing(model, pricingById.get(model.id))),
};
},
parseModels(raw) {
return HyperResponse.parse(raw).data;
},
translateModel(model, context) {
const authored = context.authored(model.id);
const baseModel = authored?.base_model
?? BASE_MODEL_ALIASES[model.id]
?? resolveVeniceBaseModel(model.id, model.display_name)
?? undefined;
return {
id: model.id,
model: buildHyperModel(model, authored, baseModel),
};
},
} satisfies SyncProvider<HyperModel>;

function dateFromTimestamp(timestamp: number) {
return new Date(timestamp * 1000).toISOString().slice(0, 10);
}

function reasoningOptions(model: HyperModel) {
if (!model.supports_reasoning) return [];
if (model.supports_reasoning_effort) {
const values = model.reasoning_effort_levels?.filter(isReasoningEffort) ?? [];
if (values.length > 0) return [{ type: "effort" as const, values }];
}
// Hyper advertises reasoning but documents no toggle or effort control for these models.
return [];
}

function isReasoningEffort(value: string): value is z.infer<typeof ReasoningEffort> {
return ReasoningEffort.safeParse(value).success;
}

function hasApiPricing(model: HyperModel) {
return (
model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined
) || (
model.cost_per_1m_in !== undefined && model.cost_per_1m_out !== undefined
);
}

function enrichPricing(
model: HyperModel,
provider: z.infer<typeof HyperProviderModel> | undefined,
): HyperModel {
if (hasApiPricing(model) || provider === undefined) return model;
return {
...model,
cost_per_1m_in: provider.cost_per_1m_in,
cost_per_1m_out: provider.cost_per_1m_out,
cost_per_1m_in_cached: provider.cost_per_1m_in_cached,
cost_per_1m_out_cached: provider.cost_per_1m_out_cached,
};
}

function parsePrice(value: string | number | undefined) {
if (value === undefined) return undefined;
if (typeof value === "number") {
return Number.isFinite(value) && value >= 0 ? value : undefined;
}
const number = Number(value);
return Number.isFinite(number) && number >= 0
? Math.round(number * 1_000_000_000_000) / 1_000_000
: undefined;
}

function positivePrice(value: number | undefined) {
return value !== undefined && value > 0 ? value : undefined;
}

function buildCost(model: HyperModel, existing: ExistingModel["cost"] | undefined) {
const fromProviderFields = model.cost_per_1m_in !== undefined && model.cost_per_1m_out !== undefined
? {
input: model.cost_per_1m_in,
output: model.cost_per_1m_out,
// Hyper /provider names these fields by token direction, not catalog semantics:
// out_cached is the cheap cache-hit read price; in_cached is the cache-write price.
cache_read: model.cost_per_1m_out_cached,
cache_write: model.cost_per_1m_in_cached,
reasoning: undefined as number | undefined,
}
: undefined;

const pricing = model.pricing;
const fromPricingObject = pricing?.prompt !== undefined && pricing?.completion !== undefined
? {
input: parsePrice(pricing.prompt),
output: parsePrice(pricing.completion),
cache_read: parsePrice(pricing.input_cache_read ?? pricing.input_cache_reads),
cache_write: parsePrice(pricing.input_cache_write ?? pricing.input_cache_writes),
reasoning: parsePrice(pricing.internal_reasoning),
}
: undefined;

const resolved = fromProviderFields ?? fromPricingObject;
if (resolved?.input === undefined || resolved.output === undefined) return existing;

const providerPricing = fromProviderFields !== undefined;

return {
input: resolved.input,
output: resolved.output,
cache_read: providerPricing
? positivePrice(resolved.cache_read)
: positivePrice(resolved.cache_read) ?? existing?.cache_read,
cache_write: providerPricing
? positivePrice(resolved.cache_write)
: positivePrice(resolved.cache_write) ?? existing?.cache_write,
reasoning: positivePrice(resolved.reasoning) ?? existing?.reasoning,
input_audio: existing?.input_audio,
output_audio: existing?.output_audio,
tiers: existing?.tiers,
};
}

export function buildHyperModel(
model: HyperModel,
authored: ExistingModel | undefined,
baseModel: string | undefined,
today = new Date().toISOString().slice(0, 10),
): SyncedModel {
const limit = {
context: model.context_window,
input: authored?.limit?.input,
output: model.max_output_tokens,
};
const values: Partial<SyncedFullModel> = {
name: authored?.name ?? model.display_name,
attachment: model.supports_attachments,
reasoning: model.supports_reasoning,
reasoning_options: reasoningOptions(model),
release_date: authored?.release_date ?? dateFromTimestamp(model.created),
last_updated: authored?.last_updated ?? today,
interleaved: authored?.interleaved,
cost: buildCost(model, authored?.cost),
limit,
modalities: model.supports_attachments
? authored?.modalities
: authored?.modalities ?? { input: ["text"], output: ["text"] },
};

if (baseModel === undefined) {
throw new Error(`Hyper model ${model.id} has no matching base_model metadata`);
}

return factorBaseModel(baseModel, values, limit, authored?.base_model_omit);
}
84 changes: 84 additions & 0 deletions packages/core/test/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,77 @@ import { openai, parseOpenAIModels } from "../src/sync/providers/openai.js";
import { resolveVeniceBaseModel } from "../src/sync/providers/venice.js";
import { buildVercelModel, vercel } from "../src/sync/providers/vercel.js";
import { buildWandbModel, type WandbModel } from "../src/sync/providers/wandb.js";
import { buildHyperModel, type HyperModel } from "../src/sync/providers/hyper.js";
import { buildXAIModel } from "../src/sync/providers/xai.js";

test("syncs Hyper cache read and write from provider pricing fields", () => {
const model = hyperModel({
id: "qwen3.6-flash",
cost_per_1m_in: 1,
cost_per_1m_out: 4,
cost_per_1m_in_cached: 1.25,
cost_per_1m_out_cached: 0.1,
});

expect(buildHyperModel(model, undefined, "alibaba/qwen3.6-flash")).toMatchObject({
cost: { input: 1, output: 4, cache_read: 0.1, cache_write: 1.25 },
});
});

test("preserves hand-authored Hyper cost fields the API does not expose", () => {
const existing = {
cost: {
input: 1,
output: 2,
cache_write: 0.375,
tiers: [{ tier: { type: "context" as const, size: 200_000 }, input: 3, output: 4 }],
},
release_date: "2026-01-01",
last_updated: "2026-01-01",
};

expect(buildHyperModel(hyperModel({
id: "minimax-m2.7",
cost_per_1m_in: 0.82,
cost_per_1m_out: 2.64,
cost_per_1m_in_cached: 0.41,
cost_per_1m_out_cached: 0,
}), existing, "minimax/MiniMax-M2.7")).toMatchObject({
cost: {
input: 0.82,
output: 2.64,
cache_write: 0.41,
tiers: existing.cost.tiers,
},
});
});

test("emits text-only modalities when Hyper disables attachments", () => {
const built = buildHyperModel(hyperModel({
id: "gemma-4-26b-a4b-it",
supports_attachments: false,
}), undefined, "google/gemma-4-26b-a4b-it");

expect(built).toMatchObject({
attachment: false,
modalities: { input: ["text"] },
});
});

test("defaults Hyper reasoning models without effort levels to empty reasoning options", () => {
expect(buildHyperModel(hyperModel({ supports_reasoning_effort: false }), undefined, "minimax/MiniMax-M2.7"))
.toMatchObject({ reasoning_options: [] });
});

test("preserves Hyper display name overrides", () => {
expect(buildHyperModel(hyperModel({
id: "qwen3.6-max",
display_name: "Qwen3.6-Max",
}), { name: "Qwen3.6-Max" }, "alibaba/qwen3.6-max-preview")).toMatchObject({
name: "Qwen3.6-Max",
});
});

function anthropicModel(overrides: Partial<AnthropicModel> = {}): AnthropicModel {
return {
id: "claude-sonnet-5",
Expand Down Expand Up @@ -1061,6 +1130,21 @@ function llmGatewayModel(overrides: Partial<LLMGatewayModel> = {}): LLMGatewayMo
};
}

function hyperModel(overrides: Partial<HyperModel> = {}): HyperModel {
return {
id: "deepseek-v4-flash",
created: 1_780_592_628,
display_name: "DeepSeek V4 Flash",
supports_reasoning: true,
supports_reasoning_effort: true,
reasoning_effort_levels: ["high", "xhigh"],
supports_attachments: false,
context_window: 1_000_000,
max_output_tokens: 384_000,
...overrides,
};
}

function openRouterModel(overrides: Partial<OpenRouterModel> = {}): OpenRouterModel {
return {
id: "anthropic/claude-sonnet-5",
Expand Down
Loading
Loading