Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -90,6 +90,7 @@ jobs:
XAI_API_KEY: ${{ secrets.XAI_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 }}
TENSORX_API_KEY: ${{ secrets.TENSORX_API_KEY }}

- name: Validate models
run: bun validate
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 @@ -27,6 +27,7 @@ import { openrouter } from "./providers/openrouter.js";
import { ovhcloud } from "./providers/ovhcloud.js";
import { pioneer } from "./providers/pioneer.js";
import { requesty } from "./providers/requesty.js";
import { tensorx } from "./providers/tensorx.js";
import { tinfoil } from "./providers/tinfoil.js";
import { vercel } from "./providers/vercel.js";
import { venice } from "./providers/venice.js";
Expand Down Expand Up @@ -133,6 +134,7 @@ export const providers: {
ovhcloud: SyncProvider<any>;
pioneer: SyncProvider<any>;
requesty: SyncProvider<any>;
tensorx: SyncProvider<any>;
tinfoil: SyncProvider<any>;
vercel: SyncProvider<any>;
venice: SyncProvider<any>;
Expand Down Expand Up @@ -161,6 +163,7 @@ export const providers: {
ovhcloud,
pioneer,
requesty,
tensorx,
tinfoil,
vercel,
venice,
Expand All @@ -183,7 +186,7 @@ export const groups = {
"vercel",
],
cloudflare: ["cloudflare-workers-ai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "deepinfra", "digitalocean", "google", "hyper", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "deepinfra", "digitalocean", "google", "hyper", "openai", "ovhcloud", "pioneer", "tensorx", "tinfoil", "venice", "wandb", "xai"],
} as const;

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

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

const API_ENDPOINT = "https://api.tensorx.ai/v1/model/info";

// `supported_openai_params` is a static LiteLLM-style list: every catalog entry
// advertises the same params, including `temperature` and `response_format` on
// the Whisper and embedding models. It carries no per-model signal, so nothing
// is derived from it — `temperature` and `structured_output` stay lab metadata.
//
// `supports_*` uses three values: true, false, and null for "not published".
// Only real booleans are authored; null leaves the field to the base model.
const TensorXModelInfo = z.object({
mode: z.string().nullish(),
max_input_tokens: z.number().int().nonnegative().nullish(),
max_output_tokens: z.number().int().nonnegative().nullish(),
max_tokens: z.number().int().nonnegative().nullish(),
supports_reasoning: z.boolean().nullish(),
supports_tool_choice: z.boolean().nullish(),
supports_function_calling: z.boolean().nullish(),
supports_vision: z.boolean().nullish(),
input_cost_per_token: z.number().nonnegative().nullish(),
output_cost_per_token: z.number().nonnegative().nullish(),
cache_read_input_token_cost: z.number().nonnegative().nullish(),
cache_creation_input_token_cost: z.number().nonnegative().nullish(),
}).passthrough();

export const TensorXModel = z.object({
model_name: z.string().min(1),
model_info: TensorXModelInfo,
});

export const TensorXResponse = z.object({
data: z.array(TensorXModel),
});

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

export const tensorx = {
id: "tensorx",
name: "TensorX",
modelsDir: "providers/tensorx/models",
// /v1/model/info carries no reasoning controls and no side-channel field, so
// a created reasoner would be published with the runner's fallback
// `reasoning_options = []` — an assertion of "no caller control" that nothing
// here backs — and no `interleaved`. New IDs are reported for hand-authoring
// instead; updates to existing TOMLs are unaffected.
skipCreates: true,
preserveBaseModels: false,
// /v1/model/info returns a per-key view: a key scoped to a model group sees
// only that group, and chat requests for the rest fail with 403 rather than
// 404. Absence from the response is therefore not evidence that a model was
// retired, so local entries are never deleted on the strength of it.
deleteMissing: false,
sourceID(model) {
// Non-chat entries (embeddings, transcription, speech) are out of scope and
// are dropped silently; only unauthorable chat models get reported.
return model.model_info.mode === "chat" ? model.model_name : undefined;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} TensorX chat models were not created: /v1/model/info publishes no name, description, or release date, and no reasoning controls or side-channel field, so a complete model cannot be authored safely.`,
`Add the lab entry under \`models/\` and hand-author \`reasoning_options\` / \`interleaved\` against the live API. Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
missingNotice(paths) {
if (paths.length === 0) return [];
return [
`${paths.length} local TensorX models were absent from /v1/model/info and were retained for manual lifecycle review.`,
`That endpoint is a per-key view, so absence can mean the sync key lacks access rather than the model being retired: ${paths.map((path) => `\`${path}\``).join(", ")}`,
];
},
async fetchModels() {
const apiKey = process.env.TENSORX_API_KEY;
if (!apiKey) {
throw new Error("TensorX sync requires TENSORX_API_KEY environment variable");
}
const response = await fetch(API_ENDPOINT, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
throw new Error(`TensorX model info request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
const models = TensorXResponse.parse(raw).data;
const seen = new Set<string>();
return models.filter((model) => {
if (seen.has(model.model_name)) return false;
seen.add(model.model_name);
return true;
});
},
translateModel(model, context) {
if (model.model_info.mode !== "chat") return undefined;

const existing = context.existing(model.model_name);
// A base_model already authored locally wins over re-derivation. With
// preserveBaseModels false the runner will not put it back, so a resolution
// miss here (dropped alias, renamed lab entry, ID drift) would otherwise
// flatten the inherited lab fields into the provider TOML.
const baseModel = existing?.base_model ?? resolveBaseModel(model.model_name);
// Nothing in this catalog can stand in for lab metadata — no display name,
// no description, no release date — so an unknown model is reported for
// hand-authoring instead of being invented.
if (baseModel === undefined && existing === undefined) return undefined;

const built = buildTensorXModel(model, baseModel, existing);
return built === undefined ? undefined : { id: model.model_name, model: built };
},
} satisfies SyncProvider<TensorXModel>;

function buildTensorXModel(
model: TensorXModel,
baseModel: string | undefined,
existing: ExistingModel | undefined,
): SyncedModel | undefined {
const info = model.model_info;

const limit = {
context: info.max_input_tokens ?? info.max_tokens ?? existing?.limit?.context,
output: info.max_output_tokens ?? existing?.limit?.output,
};

const input = perMillion(info.input_cost_per_token) ?? existing?.cost?.input;
const output = perMillion(info.output_cost_per_token) ?? existing?.cost?.output;
// Pricing is published only as a complete pair. A base_model file validates
// against a deepPartial schema, so a half-resolved cost would be written out
// as real pricing rather than rejected; a full model would abort the sync.
const cost = input === undefined || output === undefined
? existing?.cost
: {
...existing?.cost,
input,
output,
cache_read: perMillion(info.cache_read_input_token_cost) ?? existing?.cost?.cache_read,
// cache_creation_input_token_cost is null for every model in the catalog,
// so a null is "not published" rather than "not charged". Keep the
// authored price; real values publish as soon as TensorX fills it in.
cache_write: perMillion(info.cache_creation_input_token_cost) ?? existing?.cost?.cache_write,
};

// Never bring a brand-new model into the catalog without real pricing.
if (existing === undefined && cost === undefined) return undefined;

const toolFlags = [info.supports_tool_choice, info.supports_function_calling]
.filter((flag) => flag !== null && flag !== undefined);

// supports_vision is the only modality signal the catalog carries, so
// `attachment` and `modalities.input` move together. Only `image` is edited —
// the flag says nothing about video or pdf — and `attachment` is then read
// back off the resulting list rather than off the flag, so a model that keeps
// video after losing image stays `attachment = true`.
const vision = info.supports_vision;
const inheritedInput = existing?.modalities?.input;
const modalities = vision === null || vision === undefined || inheritedInput === undefined
? existing?.modalities
: {
...existing?.modalities,
input: vision
? (inheritedInput.includes("image") ? inheritedInput : [...inheritedInput, "image"])
: inheritedInput.filter((modality) => modality !== "image"),
};
const resolvedInput = modalities?.input;
const attachment = vision === null || vision === undefined || resolvedInput === undefined
? existing?.attachment
: resolvedInput.some((modality) => modality !== "text");

// `existing` is the base-model-resolved view, so factorBaseModel drops every
// field that still matches the lab entry and keeps only the real deltas.
const values: Record<string, unknown> = {
...existing,
attachment,
reasoning: info.supports_reasoning ?? existing?.reasoning,
tool_call: toolFlags.length > 0 ? toolFlags.some(Boolean) : existing?.tool_call,
cost,
limit,
modalities,
};
delete values.base_model;
delete values.base_model_omit;

return baseModel === undefined
? values as SyncedFullModel
: factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
}

// Dated snapshots TensorX serves that have no lab entry of their own.
const BASE_MODEL_ALIASES: Record<string, string | undefined> = {
"deepseek/deepseek-r1-0528": "deepseek/deepseek-r1",
"deepseek/deepseek-r1-0625": "deepseek/deepseek-r1",
};

function resolveBaseModel(modelID: string): string | undefined {
// resolveCanonicalBaseModel owns the org-prefix map and matches metadata
// filenames case-insensitively, so `minimax/minimax-m3` still resolves to
// `models/minimax/MiniMax-M3.toml`.
return resolveCanonicalBaseModel(BASE_MODEL_ALIASES[modelID] ?? modelID);
}

function perMillion(costPerToken: number | null | undefined): number | undefined {
if (costPerToken === null || costPerToken === undefined) return undefined;
return Math.round(costPerToken * 1_000_000 * 1e10) / 1e10;
}
11 changes: 8 additions & 3 deletions providers/tensorx/models/deepseek/deepseek-chat-v3.1.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Not reachable with the sync key (403, models=['public']), so nothing here
# could be measured against this host. providers/openrouter/models/deepseek/
# deepseek-chat-v3.1.toml authors a toggle, so that control shape is copied
# rather than keeping a full effort enum.
# Toggle: chat_template_kwargs.thinking = true|false — the path every reachable
# DeepSeek model on this host uses, but unverified for this ID.
# [interleaved] is omitted rather than assumed, for the same reason.
name = "DeepSeek Chat V3.1"
description = "DeepSeek chat model for instruction following, coding, and analysis"
family = "deepseek"
Expand All @@ -10,10 +17,8 @@ tool_call = true
knowledge = "2024-11"
open_weights = true


[[reasoning_options]]
type = "effort" # API: {"reasoning_effort": <value>}; "none" disables reasoning
values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
type = "toggle"

[cost]
input = 0.2
Expand Down
9 changes: 6 additions & 3 deletions providers/tensorx/models/deepseek/deepseek-r1-0528.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# reasoning_effort is accepted but does not grade depth on this host, and none
# is rejected (400), so reasoning is always on. Matches the OpenRouter entry for
# this model.
base_model = "deepseek/deepseek-r1"
name = "DeepSeek R1-0528"
release_date = "2025-05-28"
last_updated = "2025-05-28"
reasoning_options = []

[[reasoning_options]]
type = "effort" # API: {"reasoning_effort": <value>}; reasoning is mandatory, "none" is rejected
values = ["minimal", "low", "medium", "high", "xhigh", "max"]
[interleaved]
field = "reasoning_content"

[cost]
input = 0.66
Expand Down
13 changes: 10 additions & 3 deletions providers/tensorx/models/deepseek/deepseek-v3.2.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Effort: reasoning_effort = none|high. This host does not reason by default —
# reasoning_content is empty unless an effort value is sent — and none keeps it
# off. chat_template_kwargs.enable_thinking and thinking are no-ops. Measured
# by reasoning_content length; invalid values return 400 and no graded level
# was separable, so one on level is published.
name = "DeepSeek V3.2"
description = "DeepSeek chat model for instruction following, coding, and analysis"
family = "deepseek"
Expand All @@ -10,10 +15,12 @@ tool_call = true
knowledge = "2025-05"
open_weights = true


[[reasoning_options]]
type = "effort" # API: {"reasoning_effort": <value>}; "none" disables reasoning
values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
type = "effort"
values = ["none", "high"]

[interleaved]
field = "reasoning_content"

[cost]
input = 0.3
Expand Down
17 changes: 13 additions & 4 deletions providers/tensorx/models/deepseek/deepseek-v4-flash-0731.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# Toggle: chat_template_kwargs.thinking = true|false (default off)
# reasoning_effort is accepted but not honored. Measured by reasoning_content
# length — not usage.reasoning_tokens, which this host under-reports — low,
# high, max and an invalid value all fall in the same range, so this host is
# toggle-only despite the lab exposing effort high|max.
base_model = "deepseek/deepseek-v4-flash-0731"

[[reasoning_options]]
type = "toggle" # API: {"chat_template_kwargs": {"thinking": true}} (default off)
type = "toggle"

[interleaved]
field = "reasoning_content"

[cost]
input = 0.25
output = 0.30
cache_read = 0.06
output = 0.3
cache_read = 0.0625

[limit]
context = 1048576
context = 1_048_576
output = 64_000
13 changes: 11 additions & 2 deletions providers/tensorx/models/deepseek/deepseek-v4-flash.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# Toggle: chat_template_kwargs.thinking = true|false (default off)
# reasoning_effort is accepted but not honored. Measured by reasoning_content
# length — not usage.reasoning_tokens, which this host under-reports — low,
# high, max and an invalid value all fall in the same range, so this host is
# toggle-only despite the lab exposing effort high|max.
base_model = "deepseek/deepseek-v4-flash"

[[reasoning_options]]
type = "toggle" # API: {"chat_template_kwargs": {"thinking": true}} (default off)
type = "toggle"

[interleaved]
field = "reasoning_content"

[cost]
input = 0.15
Expand All @@ -10,4 +18,5 @@ cache_read = 0.0375
cache_write = 0.1875

[limit]
context = 1048576
context = 1_048_576
output = 64_000
13 changes: 11 additions & 2 deletions providers/tensorx/models/deepseek/deepseek-v4-pro.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# Toggle: chat_template_kwargs.thinking = true|false (default off)
# reasoning_effort is accepted but not honored. Measured by reasoning_content
# length — not usage.reasoning_tokens, which this host under-reports — low,
# high, max and an invalid value all fall in the same range, so this host is
# toggle-only despite the lab exposing effort high|max.
base_model = "deepseek/deepseek-v4-pro"

[[reasoning_options]]
type = "toggle" # API: {"chat_template_kwargs": {"thinking": true}} (default off)
type = "toggle"

[interleaved]
field = "reasoning_content"

[cost]
input = 1.75
Expand All @@ -10,4 +18,5 @@ cache_read = 0.4375
cache_write = 2.185

[limit]
context = 1048576
context = 1_048_576
output = 64_000
Loading
Loading