Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ef3469b
Add CheaperInference provider
aiapienthusiast Sep 13, 2026
702db21
Drop toggle from CheaperInference reasoning options
aiapienthusiast Sep 13, 2026
8d7453b
Restore toggle where the gateway forwards it
aiapienthusiast Sep 13, 2026
5f79a19
Sync CheaperInference model catalog
aiapienthusiast Sep 13, 2026
d99ca61
Refresh CheaperInference catalog from the live models endpoint
aiapienthusiast Sep 16, 2026
066556c
Add the google/ prefixed route to the CheaperInference catalog
aiapienthusiast Sep 16, 2026
66a455f
Keep reasoning toggle only where a live request confirms the gateway …
aiapienthusiast Sep 16, 2026
3d18407
Restore the reasoning toggle for glm-4.5-air with its verified wire form
aiapienthusiast Sep 16, 2026
e71f1b5
Author reasoning controls from live gateway probes
aiapienthusiast Sep 16, 2026
dca50c0
Drop budget_tokens where no reasoning budget field exists
aiapienthusiast Sep 16, 2026
6fdcfcb
Author reasoning, attachments and side channels from live probes
aiapienthusiast Sep 16, 2026
88d9d3e
Record probed image and reasoning behaviour on five CheaperInference …
aiapienthusiast Sep 16, 2026
ef9663c
Report an unresearched CheaperInference reasoner instead of failing t…
aiapienthusiast Sep 16, 2026
b1a5463
Cover the reported-not-fatal path for an unresearched CheaperInferenc…
aiapienthusiast Sep 16, 2026
3969c1f
Re-probe image input with a valid image and refresh the catalogue rates
aiapienthusiast Sep 17, 2026
e19c6f1
Re-probe image input on the prefixed gemini-3.5-flash-lite route
aiapienthusiast Sep 17, 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 @@ -74,6 +74,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
BASETEN_API_KEY: ${{ secrets.BASETEN_API_KEY }}
CHEAPERINFERENCE_API_KEY: ${{ secrets.CHEAPERINFERENCE_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DIGITALOCEAN_API_TOKEN: ${{ secrets.DIGITALOCEAN_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MissingReasoningOptionsError } from "./missing-reasoning-options.js";
import { ambient } from "./providers/ambient.js";
import { anthropic } from "./providers/anthropic.js";
import { baseten } from "./providers/baseten.js";
import { cheaperinference } from "./providers/cheaperinference.js";
import { chutes } from "./providers/chutes.js";
import { cloudflareAiGateway } from "./providers/cloudflare-ai-gateway.js";
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
Expand Down Expand Up @@ -133,6 +134,7 @@ export const providers: {
ambient: SyncProvider<any>;
anthropic: SyncProvider<any>;
baseten: SyncProvider<any>;
cheaperinference: SyncProvider<any>;
chutes: SyncProvider<any>;
"cloudflare-ai-gateway": SyncProvider<any>;
"cloudflare-workers-ai": SyncProvider<any>;
Expand Down Expand Up @@ -168,6 +170,7 @@ export const providers: {
ambient,
anthropic,
baseten,
cheaperinference,
chutes,
"cloudflare-ai-gateway": cloudflareAiGateway,
"cloudflare-workers-ai": cloudflareWorkersAi,
Expand Down Expand Up @@ -203,6 +206,7 @@ export const providers: {

export const groups = {
aggregators: [
"cheaperinference",
"crossmodel",
"edenai",
"empiriolabs",
Expand Down
195 changes: 195 additions & 0 deletions packages/core/src/sync/providers/cheaperinference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { z } from "zod";

import type { ExistingModel, SyncedFullModel, SyncedModel, SyncProvider } from "../index.js";
import { MissingReasoningOptionsError } from "../missing-reasoning-options.js";
import { factorBaseModel } from "./openrouter.js";

const API_ENDPOINT = "https://api.cheaperinference.com/v1/models";

/** Long-context band. The gateway bills prompts above the threshold at these rates. */
const AboveThreshold = z
.object({
input_token_price_threshold: z.number().int().positive(),
input_per_million: z.string().min(1),
output_per_million: z.string().min(1),
cache_read_input_per_million: z.string().min(1).nullish(),
cache_write_input_per_million: z.string().min(1).nullish(),
})
.passthrough();

const CheaperInferencePricing = z
.object({
currency: z.literal("USD"),
input_per_million: z.string().min(1),
output_per_million: z.string().min(1),
cache_read_input_per_million: z.string().min(1).nullish(),
cache_write_input_per_million: z.string().min(1).nullish(),
input_token_price_threshold: z.number().int().positive().nullish(),
above_threshold: AboveThreshold.nullish(),
})
.passthrough();

export const CheaperInferenceModel = z
.object({
id: z.string().min(1),
object: z.literal("model"),
type: z.string().min(1),
endpoint: z.string().min(1),
context_length: z.number().int().positive().nullish(),
max_output_tokens: z.number().int().positive().nullish(),
is_free: z.boolean(),
pricing: CheaperInferencePricing,
})
.passthrough();

export const CheaperInferenceResponse = z
.object({
object: z.literal("list"),
data: z.array(CheaperInferenceModel),
pricing_version: z.string().min(1),
pricing_checked_at: z.string().min(1),
})
.passthrough();

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

export const cheaperinference = {
id: "cheaperinference",
name: "CheaperInference",
modelsDir: "providers/cheaperinference/models",
// The catalog carries pricing and limits but no reasoning controls, so new
// models need hand-authored reasoning_options before they can be created.
skipCreates: true,
sourceID(model) {
return isTokenPricedTextModel(model) ? model.id : undefined;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} CheaperInference models were not created because the catalog exposes no reasoning controls, which this repo requires for reasoning models.`,
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
async fetchModels() {
return fetchCheaperInferenceModels();
},
parseModels(raw) {
return CheaperInferenceResponse.parse(raw).data;
},
translateModel(model, context) {
const existing = context.existing(model.id);
if (existing === undefined) return undefined;
return {
id: model.id,
model: buildCheaperInferenceModel(model, existing),
};
},
} satisfies SyncProvider<CheaperInferenceModel>;

export async function fetchCheaperInferenceModels(fetcher: typeof fetch = fetch) {
const apiKey = process.env["CHEAPERINFERENCE_API_KEY"];
if (apiKey === undefined || apiKey === "") {
throw new Error("CHEAPERINFERENCE_API_KEY is required to read the CheaperInference catalog");
}
const response = await fetcher(API_ENDPOINT, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
throw new Error(
`CheaperInference models request failed: ${response.status} ${response.statusText}`,
);
}
return CheaperInferenceResponse.parse(await response.json());
}

/**
* Only text models billed per token can be expressed by the catalog schema.
* Image and video routes price per unit of generated media, and free routes
* carry no rates worth syncing.
*/
function isTokenPricedTextModel(model: CheaperInferenceModel) {
return (
model.type === "text" &&
model.endpoint === "/v1/chat/completions" &&
!model.is_free &&
price(model.pricing.input_per_million) > 0 &&
price(model.pricing.output_per_million) > 0
);
}

function price(value: string) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`CheaperInference returned an unusable price: ${value}`);
}
return parsed;
}

function optionalPrice(value: string | null | undefined) {
return value === null || value === undefined ? undefined : price(value);
}

export function buildCheaperInferenceModel(
model: CheaperInferenceModel,
existing: ExistingModel,
): SyncedModel {
if (existing.reasoning !== false && existing.reasoning_options === undefined) {
// The runner keeps the local file and reports the ID instead of failing the
// whole sync, so one unresearched model cannot stop the hourly run.
throw new MissingReasoningOptionsError(
model.id,
"reasons on this host but the catalog exposes no reasoning controls, so reasoning_options must be hand-authored",
);
}

const { base_model: baseModel, base_model_omit: baseModelOmit, ...current } = existing;
const pricing = model.pricing;

const cost = {
...existing.cost,
input: price(pricing.input_per_million),
output: price(pricing.output_per_million),
cache_read: optionalPrice(pricing.cache_read_input_per_million),
cache_write: optionalPrice(pricing.cache_write_input_per_million),
tiers: buildTiers(pricing.above_threshold),
};

// The gateway is authoritative for what it serves, so its limits win when it
// publishes them. Missing values stay inherited from the lab entry.
const limit = {
...existing.limit,
...(model.context_length === null || model.context_length === undefined
? {}
: { context: model.context_length }),
...(model.max_output_tokens === null || model.max_output_tokens === undefined
? {}
: { output: model.max_output_tokens }),
};

const values = {
...current,
cost,
limit,
} as SyncedFullModel;

return baseModel === undefined
? values
: factorBaseModel(baseModel, values, limit, baseModelOmit);
}

function buildTiers(above: z.infer<typeof AboveThreshold> | null | undefined) {
if (above === null || above === undefined) return undefined;
// The threshold is the last token billed at the base rate, so the band starts
// one token later. The gateway reports both 271_999 and 272_000 for the same
// 272k boundary, which normalises to one tier size.
const size = above.input_token_price_threshold + 1;
return [
{
tier: { type: "context" as const, size: size - (size % 1000) },
input: price(above.input_per_million),
output: price(above.output_per_million),
cache_read: optionalPrice(above.cache_read_input_per_million),
cache_write: optionalPrice(above.cache_write_input_per_million),
},
];
}
Loading