mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
feat(providers): refresh Fable, Cursor, and Devin catalogs (#12367)
Validado em lote numa worktree combinada com #12524, #12538, #12277 e #12367 sobre o tip de release/v3.8.51: os quatro boardaram sem conflito (áreas disjuntas — zai-web, nvidia, clova, cursor/devin/fable). typecheck:core limpo, check:provider-consistency OK (272 entradas REGISTRY, 355 providers canônicos), check:known-symbols OK, e 305/305 nos testes tocados pelos quatro PRs. Os IDs de modelo adicionados foram conferidos individualmente. Obrigado, @backryun.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { providerUsesAuthoritativeLiveCatalog } from "@omniroute/open-sse/config/providerRegistry";
|
||||
import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||||
import {
|
||||
getSyncedAvailableModels,
|
||||
getSyncedAvailableModelsByConnection,
|
||||
@@ -76,6 +77,19 @@ function collectModelsForConnections(
|
||||
return Array.from(models.values());
|
||||
}
|
||||
|
||||
function enrichCursorCatalog(
|
||||
providerId: string,
|
||||
models: SyncedAvailableModel[]
|
||||
): SyncedAvailableModel[] {
|
||||
// An empty sync means discovery has not completed (or failed). Do not let the
|
||||
// synthetic Cursor auto-router rows turn that empty state into an authoritative
|
||||
// catalog, otherwise every built-in model is incorrectly marked unavailable.
|
||||
if (models.length === 0) return models;
|
||||
return providerId === "cursor" || providerId === "cursor-api"
|
||||
? ensureCursorAutoCatalogEntry(models)
|
||||
: models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the unioned synced catalog belonging only to active connections.
|
||||
*
|
||||
@@ -105,7 +119,10 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
|
||||
.filter((connection): connection is ProviderConnectionRef => connection !== null)
|
||||
.map((connection) => connection.id);
|
||||
|
||||
const models = collectModelsForConnections(modelsByConnection, activeConnectionIds);
|
||||
const models = enrichCursorCatalog(
|
||||
storedProviderId,
|
||||
collectModelsForConnections(modelsByConnection, activeConnectionIds)
|
||||
);
|
||||
if (models.length > 0) {
|
||||
return {
|
||||
authoritative: providerUsesAuthoritativeLiveCatalog(providerId),
|
||||
@@ -125,7 +142,13 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
|
||||
// NON-authoritative: #9294's live-catalog gating is about what an active
|
||||
// connection actually serves, so a node-backed catalog must inform metadata
|
||||
// without ever being used to reject a model as unavailable.
|
||||
return { authoritative: false, models: await getSyncedAvailableModels(storedProviderId) };
|
||||
return {
|
||||
authoritative: false,
|
||||
models: enrichCursorCatalog(
|
||||
storedProviderId,
|
||||
await getSyncedAvailableModels(storedProviderId)
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return { authoritative: false, models: [] };
|
||||
}
|
||||
@@ -161,7 +184,10 @@ export async function getAllActiveSyncedModels(): Promise<Record<string, SyncedA
|
||||
Array.from(connectionIdsByProvider.entries()).map(async ([providerId, connectionIds]) => {
|
||||
const modelsByConnection = await getSyncedAvailableModelsByConnection(providerId);
|
||||
|
||||
const models = collectModelsForConnections(modelsByConnection, connectionIds);
|
||||
const models = enrichCursorCatalog(
|
||||
providerId,
|
||||
collectModelsForConnections(modelsByConnection, connectionIds)
|
||||
);
|
||||
|
||||
if (models.length > 0) {
|
||||
result[providerId] = models;
|
||||
|
||||
@@ -8,7 +8,6 @@ export type CursorAutoCatalogEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
owned_by?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const CURSOR_AUTO_ROUTER_VARIANT_IDS = [
|
||||
@@ -26,10 +25,56 @@ const CURSOR_AUTO_ROUTER_VARIANT_NAMES: Record<
|
||||
"auto-intelligence": "Auto (intelligence)",
|
||||
};
|
||||
|
||||
const CURSOR_ONE_MILLION_CONTEXT = 1_000_000;
|
||||
const CURSOR_CONTEXT_EFFORT = "(?:low|medium|high|xhigh|max)";
|
||||
const CURSOR_ONE_MILLION_MODEL_PATTERNS = [
|
||||
new RegExp(`^claude-fable-5-1-thinking-${CURSOR_CONTEXT_EFFORT}$`),
|
||||
new RegExp(`^claude-opus-5-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}(?:-fast)?$`),
|
||||
new RegExp(`^claude-opus-4-8-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}(?:-fast)?$`),
|
||||
new RegExp(`^claude-sonnet-5-(?:thinking-)?${CURSOR_CONTEXT_EFFORT}$`),
|
||||
new RegExp(`^claude-4\\.6-sonnet-${CURSOR_CONTEXT_EFFORT}(?:-thinking)?$`),
|
||||
new RegExp(`^gpt-5\\.6-(?:sol|terra|luna)-(?:none|${CURSOR_CONTEXT_EFFORT})$`),
|
||||
] as const;
|
||||
|
||||
const CURSOR_CONTEXT_FAMILY_NAMES = [
|
||||
"Claude Fable 5.1",
|
||||
"Claude Opus 5",
|
||||
"Claude Opus 4.8",
|
||||
"Claude Sonnet 5",
|
||||
"Claude Sonnet 4.6",
|
||||
"GPT-5.6 Sol",
|
||||
"GPT-5.6 Terra",
|
||||
"GPT-5.6 Luna",
|
||||
] as const;
|
||||
|
||||
function supportsCursorOneMillionContext(id: string): boolean {
|
||||
return CURSOR_ONE_MILLION_MODEL_PATTERNS.some((pattern) => pattern.test(id));
|
||||
}
|
||||
|
||||
function oneMillionDisplayName(name: string): string {
|
||||
const family = CURSOR_CONTEXT_FAMILY_NAMES.find((candidate) => name.startsWith(candidate));
|
||||
return family ? `${family} 1M${name.slice(family.length)}` : `${name} 1M`;
|
||||
}
|
||||
|
||||
/** Cursor auto-router: catalog id `auto`, wire id `default`. Always keep `auto` visible. */
|
||||
export function ensureCursorAutoCatalogEntry<T extends CursorAutoCatalogEntry>(models: T[]): T[] {
|
||||
const byId = new Map(models.map((m) => [m.id, m]));
|
||||
const out = [...models];
|
||||
const out: T[] = [];
|
||||
|
||||
for (const model of models) {
|
||||
const oneMillionId = `${model.id}-1m`;
|
||||
if (supportsCursorOneMillionContext(model.id) && !byId.has(oneMillionId)) {
|
||||
const oneMillionEntry = {
|
||||
...model,
|
||||
id: oneMillionId,
|
||||
name: oneMillionDisplayName(model.name),
|
||||
contextLength: CURSOR_ONE_MILLION_CONTEXT,
|
||||
} as T;
|
||||
out.push(oneMillionEntry);
|
||||
byId.set(oneMillionId, oneMillionEntry);
|
||||
}
|
||||
out.push(model);
|
||||
}
|
||||
|
||||
if (!byId.has("auto")) {
|
||||
const defaultEntry = byId.get("default");
|
||||
|
||||
@@ -9,8 +9,11 @@ import {
|
||||
humanizeCursorModelId,
|
||||
type CursorAgentModelEntry,
|
||||
} from "@/lib/providerModels/cursorAgent";
|
||||
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
export { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||||
|
||||
export type FetchCursorAvailableModelsOptions = {
|
||||
accessToken: string;
|
||||
machineId?: string | null;
|
||||
@@ -40,6 +43,45 @@ function pickModelName(entry: Record<string, unknown>, id: string): string {
|
||||
return humanizeCursorModelId(id);
|
||||
}
|
||||
|
||||
function collectArrays(record: Record<string, unknown>, keys: string[]): unknown[] {
|
||||
return keys.flatMap((key) => (Array.isArray(record[key]) ? record[key] : []));
|
||||
}
|
||||
|
||||
function collectModelCandidates(payload: unknown): unknown[] {
|
||||
const root = asRecord(payload) ?? {};
|
||||
const candidates = collectArrays(root, [
|
||||
"models",
|
||||
"availableModels",
|
||||
"available_models",
|
||||
"model",
|
||||
]);
|
||||
const nestedModels = asRecord(root.models);
|
||||
if (nestedModels) candidates.push(...collectArrays(nestedModels, ["models", "items", "list"]));
|
||||
if (Array.isArray(payload)) candidates.push(...payload);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function isUnavailableModel(entry: Record<string, unknown>): boolean {
|
||||
return (
|
||||
entry.disabled === true ||
|
||||
entry.isDisabled === true ||
|
||||
entry.usable === false ||
|
||||
entry.isUsable === false
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeModelCandidate(item: unknown): CursorAgentModelEntry | null {
|
||||
if (typeof item === "string") {
|
||||
const id = item.trim();
|
||||
return id ? { id, name: humanizeCursorModelId(id), owned_by: "cursor" } : null;
|
||||
}
|
||||
|
||||
const entry = asRecord(item);
|
||||
if (!entry || isUnavailableModel(entry)) return null;
|
||||
const id = pickModelId(entry);
|
||||
return id ? { id, name: pickModelName(entry, id), owned_by: "cursor" } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize AvailableModels JSON (Connect JSON or protobuf-json) into catalog rows.
|
||||
* Exported for unit tests.
|
||||
@@ -48,99 +90,18 @@ function pickModelName(entry: Record<string, unknown>, id: string): string {
|
||||
* only). OmniRoute clients request `cu/auto`; resolveRequestedModel maps it to `default`.
|
||||
*/
|
||||
export function normalizeCursorAvailableModelsPayload(payload: unknown): CursorAgentModelEntry[] {
|
||||
const root = asRecord(payload) ?? {};
|
||||
const candidates: unknown[] = [];
|
||||
|
||||
for (const key of ["models", "availableModels", "available_models", "model"]) {
|
||||
const v = root[key];
|
||||
if (Array.isArray(v)) candidates.push(...v);
|
||||
}
|
||||
|
||||
// Some Connect JSON responses nest under `models.models` or similar
|
||||
const nestedModels = asRecord(root.models);
|
||||
if (nestedModels) {
|
||||
for (const key of ["models", "items", "list"]) {
|
||||
const v = nestedModels[key];
|
||||
if (Array.isArray(v)) candidates.push(...v);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) candidates.push(...payload);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const out: CursorAgentModelEntry[] = [];
|
||||
for (const item of candidates) {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
const id = item.trim();
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
out.push({ id, name: humanizeCursorModelId(id), owned_by: "cursor" });
|
||||
continue;
|
||||
}
|
||||
const rec = asRecord(item);
|
||||
if (!rec) continue;
|
||||
const id = pickModelId(rec);
|
||||
if (!id || seen.has(id)) continue;
|
||||
// Prefer usable / non-disabled when flags exist
|
||||
if (rec.disabled === true || rec.isDisabled === true) continue;
|
||||
if (rec.usable === false || rec.isUsable === false) continue;
|
||||
seen.add(id);
|
||||
out.push({ id, name: pickModelName(rec, id), owned_by: "cursor" });
|
||||
for (const item of collectModelCandidates(payload)) {
|
||||
const model = normalizeModelCandidate(item);
|
||||
if (!model || seen.has(model.id)) continue;
|
||||
seen.add(model.id);
|
||||
out.push(model);
|
||||
}
|
||||
|
||||
return ensureCursorAutoCatalogEntry(out);
|
||||
}
|
||||
|
||||
/** OpenCodex-style Cursor Router optimization modes (catalog ids). */
|
||||
export const CURSOR_AUTO_ROUTER_VARIANT_IDS = [
|
||||
"auto-cost",
|
||||
"auto-balance",
|
||||
"auto-intelligence",
|
||||
] as const;
|
||||
|
||||
const CURSOR_AUTO_ROUTER_VARIANT_NAMES: Record<
|
||||
(typeof CURSOR_AUTO_ROUTER_VARIANT_IDS)[number],
|
||||
string
|
||||
> = {
|
||||
"auto-cost": "Auto (cost)",
|
||||
"auto-balance": "Auto (balance)",
|
||||
"auto-intelligence": "Auto (intelligence)",
|
||||
};
|
||||
|
||||
/** Cursor auto-router: catalog id `auto`, wire id `default`. Always keep `auto` visible. */
|
||||
export function ensureCursorAutoCatalogEntry(
|
||||
models: CursorAgentModelEntry[]
|
||||
): CursorAgentModelEntry[] {
|
||||
const byId = new Map(models.map((m) => [m.id, m]));
|
||||
const out = [...models];
|
||||
|
||||
if (!byId.has("auto")) {
|
||||
const defaultEntry = byId.get("default");
|
||||
const autoEntry: CursorAgentModelEntry = {
|
||||
id: "auto",
|
||||
name: defaultEntry?.name || "Auto (current, default)",
|
||||
owned_by: "cursor",
|
||||
};
|
||||
// Prefer `auto` as the public id; keep `default` for wire-compat listings.
|
||||
out.unshift(autoEntry);
|
||||
byId.set("auto", autoEntry);
|
||||
}
|
||||
|
||||
// Always expose Cost/Balance/Intelligence router modes (OpenCodex CURSOR_ROUTER_MODEL_IDS).
|
||||
for (const id of CURSOR_AUTO_ROUTER_VARIANT_IDS) {
|
||||
if (byId.has(id)) continue;
|
||||
const entry: CursorAgentModelEntry = {
|
||||
id,
|
||||
name: CURSOR_AUTO_ROUTER_VARIANT_NAMES[id],
|
||||
owned_by: "cursor",
|
||||
};
|
||||
out.push(entry);
|
||||
byId.set(id, entry);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function fetchCursorAvailableModels(
|
||||
options: FetchCursorAvailableModelsOptions
|
||||
): Promise<CursorAgentModelEntry[]> {
|
||||
|
||||
@@ -35,6 +35,7 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
|
||||
],
|
||||
antigravity: () => ANTIGRAVITY_PUBLIC_MODELS.map((model) => ({ ...model })),
|
||||
claude: () => [
|
||||
{ id: "claude-fable-5-1", name: "Claude Fable 5.1" },
|
||||
{ id: "claude-fable-5", name: "Claude Fable 5" },
|
||||
{ id: "claude-opus-5", name: "Claude Opus 5" },
|
||||
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
|
||||
|
||||
@@ -45,7 +45,7 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
|
||||
name: "Claude Fable",
|
||||
alias: "fable",
|
||||
envKey: "ANTHROPIC_DEFAULT_FABLE_MODEL",
|
||||
defaultValue: _cc.fable ? `cc/${_cc.fable}` : "cc/claude-fable-5",
|
||||
defaultValue: _cc.fable ? `cc/${_cc.fable}` : "cc/claude-fable-5-1",
|
||||
isTopLevel: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,9 +24,13 @@ export interface ModelSpec {
|
||||
// Model ONLY supports adaptive thinking: manual extended thinking was removed. Sending
|
||||
// `thinking.type:"enabled"` or any `thinking.budget_tokens` returns HTTP 400; reasoning
|
||||
// is steered exclusively by `output_config.effort` (low/medium/high/xhigh/max). True for
|
||||
// Claude Opus 4.7 and later (Opus 4.7/4.8/5, Fable 5). Per Anthropic's migration guide,
|
||||
// Claude Opus 4.7 and later (Opus 4.7/4.8/5, Fable 5/5.1). Per Anthropic's migration guide,
|
||||
// any request that tries to set a fixed thinking budget gets a 400 error.
|
||||
adaptiveThinkingOnly?: boolean;
|
||||
// The model rejects tool_choice values that require a tool call. Keep tools available,
|
||||
// but normalize a forced choice to the default auto behavior before dispatch. Fable 5.1 always runs
|
||||
// adaptive thinking, so forced tool use cannot be combined with any valid request.
|
||||
rejectsForcedToolChoice?: boolean;
|
||||
// Highest effort accepted while `thinking.type:"disabled"` is present. Claude Opus 5
|
||||
// rejects disabled thinking with xhigh/max, while accepting it through high.
|
||||
maxEffortWhenThinkingDisabled?: "high";
|
||||
@@ -371,6 +375,21 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-7", "claude-opus-4.7"),
|
||||
},
|
||||
|
||||
// ── Claude Fable 5.1 ────────────────────────────────────────────
|
||||
"claude-fable-5-1": {
|
||||
maxOutputTokens: 128000,
|
||||
contextWindow: 1000000,
|
||||
defaultThinkingBudget: 32000,
|
||||
thinkingBudgetCap: 120000,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
rejectsThinkingDisabled: true,
|
||||
adaptiveThinkingOnly: true,
|
||||
rejectsForcedToolChoice: true,
|
||||
aliases: BEDROCK_CLAUDE_ALIASES("claude-fable-5-1"),
|
||||
},
|
||||
|
||||
// ── Claude Fable 5 ──────────────────────────────────────────────
|
||||
"claude-fable-5": {
|
||||
maxOutputTokens: 128000,
|
||||
@@ -849,9 +868,39 @@ export function normalizeThinkingForModel<T extends Record<string, unknown>>(
|
||||
getModelSpec(modelId)?.rejectsThinkingDisabled
|
||||
) {
|
||||
const { thinking: _omitted, ...rest } = body as Record<string, unknown>;
|
||||
return rest as T;
|
||||
return normalizeForcedToolChoiceForModel(rest as T, modelId);
|
||||
}
|
||||
return body;
|
||||
return normalizeForcedToolChoiceForModel(body, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize tool-choice constraints that a resolved model cannot accept.
|
||||
*
|
||||
* Claude Fable 5.1 always uses adaptive thinking and rejects tool choices that force
|
||||
* either any tool or one named tool. Preserve the declared tools and every unrelated
|
||||
* request field, but drop the choice to select the default `auto` behavior so routing a
|
||||
* request to Fable 5.1 does not turn a recoverable preference into an upstream 400.
|
||||
*/
|
||||
export function normalizeForcedToolChoiceForModel<T extends Record<string, unknown>>(
|
||||
body: T,
|
||||
modelId: string
|
||||
): T {
|
||||
if (!getModelSpec(modelId)?.rejectsForcedToolChoice) return body;
|
||||
|
||||
const toolChoice = body.tool_choice;
|
||||
const forced =
|
||||
toolChoice === "required" ||
|
||||
toolChoice === "any" ||
|
||||
(toolChoice !== null &&
|
||||
typeof toolChoice === "object" &&
|
||||
!Array.isArray(toolChoice) &&
|
||||
["any", "tool", "function"].includes(
|
||||
String((toolChoice as Record<string, unknown>).type || "").toLowerCase()
|
||||
));
|
||||
if (!forced) return body;
|
||||
|
||||
const { tool_choice: _omitted, ...rest } = body;
|
||||
return rest as T;
|
||||
}
|
||||
|
||||
export function capMaxOutputTokens(modelId: string, requested?: number): number | undefined {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { DEFAULT_PRICING_OAUTH } from "./oauth-subscriptions";
|
||||
import { DEFAULT_PRICING_FRONTIER } from "./frontier-labs";
|
||||
import { DEFAULT_PRICING_INFERENCE } from "./inference-hosts";
|
||||
import { DEFAULT_PRICING_REGIONAL } from "./regional";
|
||||
import { DEFAULT_PRICING_DEVIN } from "./devin";
|
||||
|
||||
export const DEFAULT_PRICING = {
|
||||
...DEFAULT_PRICING_OAUTH,
|
||||
...DEFAULT_PRICING_FRONTIER,
|
||||
...DEFAULT_PRICING_INFERENCE,
|
||||
...DEFAULT_PRICING_REGIONAL,
|
||||
...DEFAULT_PRICING_DEVIN,
|
||||
};
|
||||
|
||||
142
src/shared/constants/pricing/devin.ts
Normal file
142
src/shared/constants/pricing/devin.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
type DevinTokenPricing = {
|
||||
input: number;
|
||||
cached: number;
|
||||
output: number;
|
||||
};
|
||||
|
||||
const QUALITY_EFFORTS = ["max", "xhigh", "high", "medium", "low"] as const;
|
||||
const GPT_EFFORTS = ["max", "xhigh", "high", "medium", "low", "none"] as const;
|
||||
|
||||
function variantIds(base: string, efforts: readonly string[]): string[] {
|
||||
return efforts.map((effort) => `${base}-${effort}`);
|
||||
}
|
||||
|
||||
function fastVariantIds(base: string): string[] {
|
||||
return QUALITY_EFFORTS.map((effort) => `${base}-${effort}-fast`);
|
||||
}
|
||||
|
||||
function priorityVariantIds(base: string): string[] {
|
||||
return GPT_EFFORTS.map((effort) => `${base}-${effort}-priority`);
|
||||
}
|
||||
|
||||
function priced(ids: readonly string[], pricing: DevinTokenPricing) {
|
||||
return Object.fromEntries(ids.map((id) => [id, pricing]));
|
||||
}
|
||||
|
||||
const CLAUDE_FABLE_5_1 = { input: 10, cached: 0.25, output: 50 };
|
||||
const CLAUDE_OPUS = { input: 5, cached: 0.5, output: 25 };
|
||||
const CLAUDE_OPUS_FAST = { input: 10, cached: 1, output: 50 };
|
||||
const CLAUDE_SONNET_5 = { input: 2, cached: 0.2, output: 10 };
|
||||
const CLAUDE_SONNET_4_6 = { input: 3, cached: 0.3, output: 15 };
|
||||
const CLAUDE_HAIKU_4_5 = { input: 1, cached: 0.1, output: 5 };
|
||||
|
||||
const GPT_5_6_SOL = { input: 4, cached: 0.4, output: 20 };
|
||||
const GPT_5_6_SOL_FAST = { input: 8, cached: 0.8, output: 40 };
|
||||
const GPT_5_6_TERRA = { input: 2, cached: 0.2, output: 12 };
|
||||
const GPT_5_6_TERRA_FAST = { input: 4, cached: 0.4, output: 24 };
|
||||
const GPT_5_6_LUNA = { input: 0.2, cached: 0.02, output: 1.2 };
|
||||
const GPT_5_6_LUNA_FAST = { input: 0.4, cached: 0.04, output: 2.4 };
|
||||
|
||||
/**
|
||||
* Exact per-UID rates returned by Devin's authenticated live catalog on
|
||||
* 2026-09-02. Rates are USD per one million tokens.
|
||||
*/
|
||||
export const DEVIN_MODEL_PRICING: Record<string, DevinTokenPricing> = {
|
||||
...priced(variantIds("claude-fable-5-1", QUALITY_EFFORTS), CLAUDE_FABLE_5_1),
|
||||
...priced(variantIds("claude-opus-5", QUALITY_EFFORTS), CLAUDE_OPUS),
|
||||
...priced(fastVariantIds("claude-opus-5"), CLAUDE_OPUS_FAST),
|
||||
...priced(variantIds("claude-opus-4-8", QUALITY_EFFORTS), CLAUDE_OPUS),
|
||||
...priced(fastVariantIds("claude-opus-4-8"), CLAUDE_OPUS_FAST),
|
||||
...priced(variantIds("claude-sonnet-5", QUALITY_EFFORTS), CLAUDE_SONNET_5),
|
||||
...priced(
|
||||
[
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4-6-thinking",
|
||||
"claude-sonnet-4-6-1m",
|
||||
"claude-sonnet-4-6-thinking-1m",
|
||||
],
|
||||
CLAUDE_SONNET_4_6
|
||||
),
|
||||
MODEL_PRIVATE_11: CLAUDE_HAIKU_4_5,
|
||||
|
||||
...priced(variantIds("gpt-5-6-sol", GPT_EFFORTS), GPT_5_6_SOL),
|
||||
...priced(priorityVariantIds("gpt-5-6-sol"), GPT_5_6_SOL_FAST),
|
||||
...priced(variantIds("gpt-5-6-terra", GPT_EFFORTS), GPT_5_6_TERRA),
|
||||
...priced(priorityVariantIds("gpt-5-6-terra"), GPT_5_6_TERRA_FAST),
|
||||
...priced(variantIds("gpt-5-6-luna", GPT_EFFORTS), GPT_5_6_LUNA),
|
||||
...priced(priorityVariantIds("gpt-5-6-luna"), GPT_5_6_LUNA_FAST),
|
||||
|
||||
...priced(variantIds("kimi-k3", ["max", "high", "low"]), {
|
||||
input: 3,
|
||||
cached: 0.3,
|
||||
output: 15,
|
||||
}),
|
||||
"kimi-k2-7": { input: 0.95, cached: 0.19, output: 4 },
|
||||
...priced(variantIds("glm-5-3", ["max", "high", "low"]), {
|
||||
input: 1.4,
|
||||
cached: 0.26,
|
||||
output: 4.4,
|
||||
}),
|
||||
...priced(variantIds("glm-5-3-flash", ["max", "high", "low"]), {
|
||||
input: 0.15,
|
||||
cached: 0.03,
|
||||
output: 0.5,
|
||||
}),
|
||||
|
||||
...priced(["swe-1-7", "swe-1-7-medium"], {
|
||||
input: 0.5,
|
||||
cached: 0.2,
|
||||
output: 2.5,
|
||||
}),
|
||||
...priced(["swe-1-7-lightning", "swe-1-7-lightning-medium"], {
|
||||
input: 2.5,
|
||||
cached: 1,
|
||||
output: 12.5,
|
||||
}),
|
||||
adaptive: { input: 0.5, cached: 0.1, output: 2 },
|
||||
...priced(variantIds("grok-4-6", ["xhigh", "high", "medium", "low"]), {
|
||||
input: 2,
|
||||
cached: 0.3,
|
||||
output: 6,
|
||||
}),
|
||||
...priced(variantIds("inkling", ["max", "xhigh", "high", "medium", "low", "none"]), {
|
||||
input: 1.4,
|
||||
cached: 0.26,
|
||||
output: 4.4,
|
||||
}),
|
||||
...priced(variantIds("deepseek-v4-flash", ["max", "high", "low"]), {
|
||||
input: 0.14,
|
||||
cached: 0.03,
|
||||
output: 0.28,
|
||||
}),
|
||||
...priced(variantIds("nemotron-3-ultra", ["high", "medium", "none"]), {
|
||||
input: 0.6,
|
||||
cached: 0.12,
|
||||
output: 2.4,
|
||||
}),
|
||||
...priced(variantIds("gemini-3-7-flash", ["high", "medium", "low"]), {
|
||||
input: 1.5,
|
||||
cached: 0.15,
|
||||
output: 7.5,
|
||||
}),
|
||||
...priced(variantIds("gemini-3-1-pro", ["high", "low"]), {
|
||||
input: 2,
|
||||
cached: 0.2,
|
||||
output: 12,
|
||||
}),
|
||||
...priced(variantIds("deepseek-v4-pro", ["max", "high", "low"]), {
|
||||
input: 1.32,
|
||||
cached: 0.04,
|
||||
output: 3.96,
|
||||
}),
|
||||
};
|
||||
|
||||
// Each transport gets its own provider namespace. They share today's upstream
|
||||
// rate snapshot, but can diverge independently if Devin changes one channel.
|
||||
export const DEFAULT_PRICING_DEVIN = {
|
||||
"devin-cli": { ...DEVIN_MODEL_PRICING },
|
||||
dv: { ...DEVIN_MODEL_PRICING },
|
||||
"devin-desktop": { ...DEVIN_MODEL_PRICING },
|
||||
"devin-cli-agentic": { ...DEVIN_MODEL_PRICING },
|
||||
dva: { ...DEVIN_MODEL_PRICING },
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
GPT_5_6_LUNA_PRICING,
|
||||
GPT_5_6_SOL_PRICING,
|
||||
GPT_5_6_TERRA_PRICING,
|
||||
CLAUDE_FABLE_5_1_PRICING,
|
||||
CLAUDE_FABLE_5_PRICING,
|
||||
CLAUDE_OPUS_5_PRICING,
|
||||
CLAUDE_OPUS_4_PRICING,
|
||||
@@ -213,6 +214,7 @@ export const DEFAULT_PRICING_FRONTIER = {
|
||||
// Common model IDs (without dates) used across providers
|
||||
// Intentional duplicates of dot-notation variants (e.g. claude-opus-4.6)
|
||||
// to cover hyphen-notation IDs (claude-opus-4-6) used by some clients
|
||||
"claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING,
|
||||
"claude-fable-5": CLAUDE_FABLE_5_PRICING,
|
||||
"claude-opus-5": CLAUDE_OPUS_5_PRICING,
|
||||
"claude-sonnet-5": CLAUDE_SONNET_5_PRICING,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Pure data; merged by default-pricing.ts via spread (god-file decomposition; semantic split).
|
||||
*/
|
||||
import {
|
||||
CLAUDE_FABLE_5_1_PRICING,
|
||||
CLAUDE_OPUS_5_PRICING,
|
||||
GEMINI_3_7_FLASH_PROMO_PRICING,
|
||||
GPT_5_3_CODEX_PRICING,
|
||||
@@ -20,6 +21,7 @@ const ANTIGRAVITY_GEMINI_3_7_PRICING = {
|
||||
|
||||
export const DEFAULT_PRICING_OAUTH = {
|
||||
cc: {
|
||||
"claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING,
|
||||
"claude-fable-5": {
|
||||
input: 10.0,
|
||||
output: 50.0,
|
||||
|
||||
@@ -60,6 +60,14 @@ export const CLAUDE_FABLE_5_PRICING = {
|
||||
cache_creation: 15.0,
|
||||
};
|
||||
|
||||
export const CLAUDE_FABLE_5_1_PRICING = {
|
||||
input: 10.0,
|
||||
output: 50.0,
|
||||
cached: 0.25,
|
||||
reasoning: 50.0,
|
||||
cache_creation: 12.5,
|
||||
};
|
||||
|
||||
export const CLAUDE_OPUS_5_PRICING = {
|
||||
input: 5.0,
|
||||
output: 25.0,
|
||||
|
||||
Reference in New Issue
Block a user