- Delete ghost package packages/pi-agent-core (no dist, no consumers, TS build errors; JS source sf-db.js had 3 commits not mirrored in TS) - Remove build:pi-agent-core from root package.json build:pi pipeline - Merge all models from MODEL_COST_PER_1K_INPUT into BUNDLED_COST_TABLE (model-cost-table.js is now the single canonical cost source) - Remove duplicate MODEL_COST_PER_1K_INPUT object and getModelCost() from model-router.js; use lookupModelCost() from model-cost-table.js - Replace hand-rolled isTransientNetworkError in preferences-models.js with delegation to classifyError() in error-classifier.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
590 lines
12 KiB
JavaScript
590 lines
12 KiB
JavaScript
// SF Extension — Model Cost Table
|
|
// Static cost reference for known models, used by the dynamic router
|
|
// for cross-provider cost comparison.
|
|
//
|
|
// Costs are approximate per-1K-token rates in USD (input tokens).
|
|
// Updated with SF releases. Users can override via preferences.
|
|
/**
|
|
* Bundled cost table for known models.
|
|
* Updated periodically with SF releases.
|
|
*/
|
|
export const BUNDLED_COST_TABLE = [
|
|
// Anthropic
|
|
{
|
|
id: "claude-opus-4-6",
|
|
inputPer1k: 0.015,
|
|
outputPer1k: 0.075,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-sonnet-4-6",
|
|
inputPer1k: 0.003,
|
|
outputPer1k: 0.015,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-haiku-4-5",
|
|
inputPer1k: 0.0008,
|
|
outputPer1k: 0.004,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-sonnet-4-5-20250514",
|
|
inputPer1k: 0.003,
|
|
outputPer1k: 0.015,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-3-5-sonnet-latest",
|
|
inputPer1k: 0.003,
|
|
outputPer1k: 0.015,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-3-5-haiku-latest",
|
|
inputPer1k: 0.0008,
|
|
outputPer1k: 0.004,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "claude-3-opus-latest",
|
|
inputPer1k: 0.015,
|
|
outputPer1k: 0.075,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
// OpenAI
|
|
{
|
|
id: "gpt-4o",
|
|
inputPer1k: 0.0025,
|
|
outputPer1k: 0.01,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "gpt-4o-mini",
|
|
inputPer1k: 0.00015,
|
|
outputPer1k: 0.0006,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "gpt-4.1",
|
|
inputPer1k: 0.002,
|
|
outputPer1k: 0.008,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-4.1-mini",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.0016,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-4.1-nano",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{ id: "gpt-5", inputPer1k: 0.01, outputPer1k: 0.04, updatedAt: "2026-03-29" },
|
|
{
|
|
id: "gpt-5-mini",
|
|
inputPer1k: 0.0003,
|
|
outputPer1k: 0.0012,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5-nano",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5-pro",
|
|
inputPer1k: 0.015,
|
|
outputPer1k: 0.06,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{ id: "o1", inputPer1k: 0.015, outputPer1k: 0.06, updatedAt: "2025-03-15" },
|
|
{ id: "o3", inputPer1k: 0.015, outputPer1k: 0.06, updatedAt: "2025-03-15" },
|
|
{
|
|
id: "o4-mini",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "o4-mini-deep-research",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-4-turbo",
|
|
inputPer1k: 0.01,
|
|
outputPer1k: 0.03,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
// OpenAI Codex
|
|
{
|
|
id: "gpt-5.1",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.1-codex-max",
|
|
inputPer1k: 0.003,
|
|
outputPer1k: 0.012,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.1-codex-mini",
|
|
inputPer1k: 0.0003,
|
|
outputPer1k: 0.0012,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.2",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.2-codex",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.3-codex",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.3-codex-spark",
|
|
inputPer1k: 0.0003,
|
|
outputPer1k: 0.0012,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.4",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.02,
|
|
updatedAt: "2026-03-29",
|
|
},
|
|
{
|
|
id: "gpt-5.4-mini",
|
|
inputPer1k: 0.00075,
|
|
outputPer1k: 0.0045,
|
|
updatedAt: "2026-04-18",
|
|
},
|
|
// GPT-5.5 API list price, also used for live Codex OAuth routing.
|
|
// Source: https://openai.com/api/pricing/
|
|
{
|
|
id: "gpt-5.5",
|
|
inputPer1k: 0.005,
|
|
outputPer1k: 0.03,
|
|
updatedAt: "2026-04-23",
|
|
},
|
|
// Google
|
|
{
|
|
id: "gemini-2.0-flash",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "gemini-flash-2.0",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
{
|
|
id: "gemini-2.5-pro",
|
|
inputPer1k: 0.00125,
|
|
outputPer1k: 0.005,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
// Mistral
|
|
{
|
|
id: "codestral-latest",
|
|
inputPer1k: 0.0003,
|
|
outputPer1k: 0.0009,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "devstral-2512",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "devstral-medium-latest",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "devstral-small-2507",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0003,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "magistral-medium-latest",
|
|
inputPer1k: 0.002,
|
|
outputPer1k: 0.005,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "magistral-small",
|
|
inputPer1k: 0.0005,
|
|
outputPer1k: 0.0015,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "ministral-3b-latest",
|
|
inputPer1k: 0.00004,
|
|
outputPer1k: 0.00004,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "ministral-8b-latest",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0001,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-large-2512",
|
|
inputPer1k: 0.0005,
|
|
outputPer1k: 0.0015,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-large-latest",
|
|
inputPer1k: 0.0005,
|
|
outputPer1k: 0.0015,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-medium-latest",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-nemo",
|
|
inputPer1k: 0.00015,
|
|
outputPer1k: 0.00015,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-small-2603",
|
|
inputPer1k: 0.00015,
|
|
outputPer1k: 0.0006,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "mistral-small-latest",
|
|
inputPer1k: 0.00015,
|
|
outputPer1k: 0.0006,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "pixtral-12b",
|
|
inputPer1k: 0.00015,
|
|
outputPer1k: 0.00015,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
{
|
|
id: "pixtral-large-latest",
|
|
inputPer1k: 0.002,
|
|
outputPer1k: 0.006,
|
|
updatedAt: "2026-04-29",
|
|
},
|
|
// DeepSeek
|
|
{
|
|
id: "deepseek-chat",
|
|
inputPer1k: 0.00014,
|
|
outputPer1k: 0.00028,
|
|
updatedAt: "2025-03-15",
|
|
},
|
|
// Gemini preview / future models
|
|
{
|
|
id: "gemini-3.1-pro-preview",
|
|
inputPer1k: 0.00125,
|
|
outputPer1k: 0.005,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "gemini-3.1-flash-lite-preview",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "gemini-3-pro-preview",
|
|
inputPer1k: 0.00125,
|
|
outputPer1k: 0.005,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "gemini-3-flash-preview",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "gemini-2.5-flash",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0004,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "gemini-2.5-flash-lite",
|
|
inputPer1k: 0.00005,
|
|
outputPer1k: 0.0002,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
// GLM (ZhipuAI)
|
|
{
|
|
id: "glm-4.7",
|
|
inputPer1k: 0.0006,
|
|
outputPer1k: 0.0024,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-4.7-flash",
|
|
inputPer1k: 0,
|
|
outputPer1k: 0,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-4.7-flashx",
|
|
inputPer1k: 0.00007,
|
|
outputPer1k: 0.00028,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-5",
|
|
inputPer1k: 0.001,
|
|
outputPer1k: 0.004,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-5-turbo",
|
|
inputPer1k: 0.0012,
|
|
outputPer1k: 0.0048,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-5.1",
|
|
inputPer1k: 0.0014,
|
|
outputPer1k: 0.0056,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "glm-5v-turbo",
|
|
inputPer1k: 0.0012,
|
|
outputPer1k: 0.0048,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
// Qwen (Alibaba)
|
|
{
|
|
id: "qwen3-coder:480b",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.0016,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "qwen3-coder-next",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.0016,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "qwen3-next:80b",
|
|
inputPer1k: 0.0002,
|
|
outputPer1k: 0.0008,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
// Kimi (Moonshot)
|
|
{
|
|
id: "kimi-k2.6",
|
|
inputPer1k: 0.0006,
|
|
outputPer1k: 0.0024,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "kimi-for-coding",
|
|
inputPer1k: 0.0006,
|
|
outputPer1k: 0.0024,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "kimi-k2-thinking",
|
|
inputPer1k: 0.001,
|
|
outputPer1k: 0.004,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
// MiniMax
|
|
{
|
|
id: "MiniMax-M2.7",
|
|
inputPer1k: 0.0006,
|
|
outputPer1k: 0.0024,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "MiniMax-M2.7-highspeed",
|
|
inputPer1k: 0.0006,
|
|
outputPer1k: 0.0024,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
// Mistral versioned variants
|
|
{
|
|
id: "devstral-medium-2507",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "devstral-small-2505",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0003,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "labs-devstral-small-2512",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0003,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "mistral-large-2411",
|
|
inputPer1k: 0.002,
|
|
outputPer1k: 0.006,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "mistral-medium-2505",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "mistral-medium-2508",
|
|
inputPer1k: 0.0004,
|
|
outputPer1k: 0.002,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "mistral-small-2506",
|
|
inputPer1k: 0.0001,
|
|
outputPer1k: 0.0006,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "open-mistral-7b",
|
|
inputPer1k: 0.00025,
|
|
outputPer1k: 0.00025,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "open-mixtral-8x22b",
|
|
inputPer1k: 0.002,
|
|
outputPer1k: 0.006,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
{
|
|
id: "open-mixtral-8x7b",
|
|
inputPer1k: 0.0007,
|
|
outputPer1k: 0.0007,
|
|
updatedAt: "2026-05-01",
|
|
},
|
|
];
|
|
/**
|
|
* Lookup cost for a model ID. Returns undefined if not found.
|
|
*/
|
|
export function lookupModelCost(modelId) {
|
|
const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
return (
|
|
BUNDLED_COST_TABLE.find((e) => e.id === bareId) ??
|
|
BUNDLED_COST_TABLE.find(
|
|
(e) => bareId.includes(e.id) || e.id.includes(bareId),
|
|
)
|
|
);
|
|
}
|
|
/**
|
|
* Compare two models by input cost. Returns negative if a is cheaper.
|
|
*/
|
|
export function compareModelCost(modelIdA, modelIdB) {
|
|
const costA = lookupModelCost(modelIdA)?.inputPer1k ?? 999;
|
|
const costB = lookupModelCost(modelIdB)?.inputPer1k ?? 999;
|
|
return costA - costB;
|
|
}
|
|
/**
|
|
* Return the effective per-token cost (in USD per token, not per 1K) for a
|
|
* given provider/model pair, taking subscription amortization into account.
|
|
*
|
|
* Resolution order:
|
|
* 1. If the provider matches the configured subscription provider and
|
|
* `monthly_cost_usd` is set, compute:
|
|
* amortized = monthly_cost_usd / max(tokens_used_this_month, 1_000_000)
|
|
* (The denominator floor of 1 M tokens prevents unrealistically high cost
|
|
* estimates early in the month while keeping the number meaningful.)
|
|
* 2. Otherwise fall back to the static BUNDLED_COST_TABLE input rate / 1000.
|
|
* 3. If the model is not in the table either, return 0 (unknown / free).
|
|
*
|
|
* The returned value is in USD per single token (not per 1K), so callers can
|
|
* multiply directly by token counts.
|
|
*/
|
|
export function getEffectiveTokenCost(provider, modelId, subscription) {
|
|
const providerKey = provider.toLowerCase();
|
|
const subProvider = subscription?.provider?.toLowerCase();
|
|
if (
|
|
subProvider &&
|
|
providerKey === subProvider &&
|
|
subscription?.monthly_cost_usd != null &&
|
|
subscription.monthly_cost_usd > 0
|
|
) {
|
|
// Amortize monthly cost over tokens consumed this month.
|
|
// Use a floor of 1_000_000 tokens so cost is non-trivially large early
|
|
// in the month (prevents showing $100/token in week 1).
|
|
const tokensUsed = Math.max(
|
|
subscription.tokens_used_this_month ?? 0,
|
|
1_000_000,
|
|
);
|
|
const amortized = subscription.monthly_cost_usd / tokensUsed;
|
|
return {
|
|
inputPerToken: amortized,
|
|
outputPerToken: amortized, // treat input/output symmetrically for subscriptions
|
|
isSubscription: true,
|
|
};
|
|
}
|
|
const entry = lookupModelCost(modelId);
|
|
if (!entry) {
|
|
return { inputPerToken: 0, outputPerToken: 0, isSubscription: false };
|
|
}
|
|
return {
|
|
inputPerToken: entry.inputPer1k / 1000,
|
|
outputPerToken: entry.outputPer1k / 1000,
|
|
isSubscription: false,
|
|
};
|
|
}
|
|
/**
|
|
* Estimate total USD cost for a completed request given token counts.
|
|
* Uses getEffectiveTokenCost internally so subscription amortization applies.
|
|
*/
|
|
export function estimateRequestCost(
|
|
provider,
|
|
modelId,
|
|
inputTokens,
|
|
outputTokens,
|
|
subscription,
|
|
) {
|
|
const { inputPerToken, outputPerToken } = getEffectiveTokenCost(
|
|
provider,
|
|
modelId,
|
|
subscription,
|
|
);
|
|
return inputTokens * inputPerToken + outputTokens * outputPerToken;
|
|
}
|