Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/core/task/StreamingMetricsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export class StreamingMetricsManager {
})
}

/** Compute the cost, using the provider-calculated value when available. */
getTotalCost(): number {
/** Compute the cost, using the provider-calculated value when available. Returns undefined when pricing is unknown. */
getTotalCost(): number | undefined {
return (
this.metrics.totalCost ??
calculateCost({
Expand Down
12 changes: 10 additions & 2 deletions src/core/task/__tests__/StreamingMetricsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ describe("StreamingMetricsManager", () => {
expect(manager.getTotalCost()).to.equal(1.23)
})

it("computes cost via calculateCost when provider totalCost is absent", () => {
it("returns undefined when provider totalCost is absent and model has no pricing", () => {
const manager = new StreamingMetricsManager({} as any, 0, stubApi(100_000) as any)
manager.updateFromChunk({ inputTokens: 0, outputTokens: 0 })
// zero tokens and no provider cost -> calculateCost yields 0
// no provider cost + no model pricing -> unknown, not zero
expect(manager.getTotalCost()).to.be.undefined
})

it("returns 0 when provider totalCost is absent but model has explicit zero pricing", () => {
const freeApi = { getModel: () => ({ info: { contextWindow: 100_000, inputPrice: 0, outputPrice: 0 } }) } as any
const manager = new StreamingMetricsManager({} as any, 0, freeApi)
manager.updateFromChunk({ inputTokens: 10, outputTokens: 5 })
// explicitly free model -> cost is genuinely 0
expect(manager.getTotalCost()).to.equal(0)
})

Expand Down
3 changes: 2 additions & 1 deletion src/core/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2185,7 +2185,8 @@ export class Task {
this.taskState.totalReasoningTokens += metrics.reasoningTokens
this.taskState.totalCacheWriteTokens += metrics.cacheWriteTokens
this.taskState.totalCacheReadTokens += metrics.cacheReadTokens
this.taskState.totalCost += metricsManager.getTotalCost()
const cost = metricsManager.getTotalCost()
if (cost !== undefined) this.taskState.totalCost += cost

const currentApiReqIndex = findLastIndex(
this.messageStateHandler.getDiracMessages(),
Expand Down
6 changes: 2 additions & 4 deletions src/core/task/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { ApiHandler } from "@core/api"
import { execSync } from "child_process"
import { showSystemNotification } from "@/integrations/notifications"
import { DiracApiReqCancelReason, DiracApiReqInfo, DiracMessageType } from "@/shared/ExtensionMessage"

import { calculateApiCostAnthropic } from "@/utils/cost"
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@/utils/cost"
import { calculateApiCostAnthropic, calculateApiCostOpenAI, calculateApiCostQwen } from "@/utils/cost"
import { MessageStateHandler } from "./message-state"

export const showNotificationForApproval = (message: string, notificationsEnabled: boolean) => {
Expand Down Expand Up @@ -40,7 +38,7 @@ export const calculateCost = (params: {
cacheReadTokens: number
reasoningTokens: number
api: ApiHandler
}): number => {
}): number | undefined => {
const info = params.api.getModel().info
const provider = params.api.constructor.name
if (provider === "ZAiHandler" || provider === "OpenAiHandler" || provider === "DeepSeekHandler") {
Expand Down
153 changes: 65 additions & 88 deletions src/shared/api/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,93 @@
// Barrel re-export — all model registries, types, and capabilities

export {
type ModelCapabilities,
type ModelInfo,
type PriceTier,
type OpenAiCompatibleProfile,
type ModelProviderPreset,
type ModelProviderSelection,
type OpenAiCompatibleModelInfo,
type OcaModelInfo,
type LiteLLMModelInfo,
type BasetenModelInfo,
} from "./types"
export { createModelProviderSelection } from "./types"
export { MODEL_CAPABILITIES } from "./capabilities"
export { GPT_5_5_TIERS, GPT_5_4_TIERS, GPT_5_4_PRO_TIERS } from "./shared-tiers"

// Anthropic
export {
ANTHROPIC_FAST_MODE_SUFFIX,
ANTHROPIC_MAX_THINKING_BUDGET,
ANTHROPIC_MIN_THINKING_BUDGET,
type AnthropicModelId,
anthropicDefaultModelId,
anthropicModels,
ANTHROPIC_FAST_MODE_SUFFIX,
ANTHROPIC_MIN_THINKING_BUDGET,
ANTHROPIC_MAX_THINKING_BUDGET,
isAnthropicAdaptiveThinkingSupported,
} from "./anthropic"
export { type ClaudeCodeModelId, claudeCodeDefaultModelId, claudeCodeModels } from "./claude-code"

// Baseten
export { type BasetenModelId, basetenDefaultModelId, basetenModels } from "./baseten"
// AWS Bedrock
export { type BedrockModelId, bedrockDefaultModelId, bedrockModels } from "./bedrock"

// Google Vertex AI
export { type VertexModelId, vertexDefaultModelId, vertexModels, vertexGlobalModels } from "./vertex"

export { MODEL_CAPABILITIES } from "./capabilities"
// Cerebras
export { type CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "./cerebras"
export { type ClaudeCodeModelId, claudeCodeDefaultModelId, claudeCodeModels } from "./claude-code"
// DeepSeek
export { type DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels } from "./deepseek"
// Doubao
export { type DoubaoModelId, doubaoDefaultModelId, doubaoModels } from "./doubao"
// Fireworks
export { type FireworksModelId, fireworksDefaultModelId, fireworksModels } from "./fireworks"
// Google Gemini
export { type GeminiModelId, geminiDefaultModelId, geminiModels } from "./gemini"

// Groq
export { type GroqModelId, groqDefaultModelId, groqModels } from "./groq"
// Huawei Cloud MaaS
export { type HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasModels } from "./huawei-cloud-maas"
// HuggingFace
export { type HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels } from "./huggingface"
// LiteLLM
export { type LiteLLMModelId, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "./litellm"
// Minimax
export { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "./minimax"
// Mistral
export { type MistralModelId, mistralDefaultModelId, mistralModels } from "./mistral"
// Moonshot
export { type MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "./moonshot"
// Nebius
export { type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "./nebius"
// NousResearch
export { type NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "./nousresearch"
export {
type OpenAiCodexModelId,
type OpenAiCodexModelInfo,
openAiCodexDefaultModelId,
openAiCodexModels,
} from "./openai-codex"
export { azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "./openai-defaults"
// OpenAI Native
export {
type OpenAiNativeModelId,
type OpenAiNativeModelInfo,
openAiNativeDefaultModelId,
openAiNativeModels,
} from "./openai-native"
export {
type OpenAiCodexModelId,
type OpenAiCodexModelInfo,
openAiCodexDefaultModelId,
openAiCodexModels,
} from "./openai-codex"
export { openAiModelInfoSaneDefaults, azureOpenAiDefaultApiVersion } from "./openai-defaults"

// DeepSeek
export { type DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels } from "./deepseek"

// HuggingFace
export { type HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels } from "./huggingface"

export { type QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "./qwen-code"
// Qwen
export { type InternationalQwenModelId, internationalQwenDefaultModelId, internationalQwenModels } from "./qwen-international"
export { type MainlandQwenModelId, mainlandQwenDefaultModelId, mainlandQwenModels, QwenApiRegions } from "./qwen-mainland"
export { type QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "./qwen-code"

// Doubao
export { type DoubaoModelId, doubaoDefaultModelId, doubaoModels } from "./doubao"

// Mistral
export { type MistralModelId, mistralDefaultModelId, mistralModels } from "./mistral"

// Nebius
export { type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "./nebius"

// Requesty
export { requestyDefaultModelId, requestyDefaultModelInfo } from "./requesty"
// Sambanova
export { type SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "./sambanova"
export { GPT_5_4_PRO_TIERS, GPT_5_4_TIERS, GPT_5_5_TIERS } from "./shared-tiers"
export {
type BasetenModelInfo,
createModelProviderSelection,
hasPricing,
isFreeModel,
type LiteLLMModelInfo,
type ModelCapabilities,
type ModelInfo,
type ModelProviderPreset,
type ModelProviderSelection,
type OcaModelInfo,
type OpenAiCompatibleModelInfo,
type OpenAiCompatibleProfile,
type PriceTier,
} from "./types"
// Google Vertex AI
export { type VertexModelId, vertexDefaultModelId, vertexGlobalModels, vertexModels } from "./vertex"
// Wandb
export { type WandbModelId, wandbDefaultModelId, wandbModels } from "./wandb"

// XAI
export { type XAIModelId, xaiDefaultModelId, xaiModels } from "./xai"

// Sambanova
export { type SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "./sambanova"

// Cerebras
export { type CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "./cerebras"

// Groq
export { type GroqModelId, groqDefaultModelId, groqModels } from "./groq"

// Moonshot
export { type MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "./moonshot"

// Huawei Cloud MaaS
export { type HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasModels } from "./huawei-cloud-maas"

// Baseten
export { type BasetenModelId, basetenDefaultModelId, basetenModels } from "./baseten"

// ZAI
export { type internationalZAiModelId, internationalZAiDefaultModelId, internationalZAiModels } from "./zai-international"
export { type mainlandZAiModelId, mainlandZAiDefaultModelId, mainlandZAiModels } from "./zai-mainland"

// Fireworks
export { type FireworksModelId, fireworksDefaultModelId, fireworksModels } from "./fireworks"

// Minimax
export { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "./minimax"

// NousResearch
export { type NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "./nousresearch"

// LiteLLM
export { type LiteLLMModelId, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "./litellm"

// Requesty
export { requestyDefaultModelId, requestyDefaultModelInfo } from "./requesty"
export { internationalZAiDefaultModelId, type internationalZAiModelId, internationalZAiModels } from "./zai-international"
export { mainlandZAiDefaultModelId, type mainlandZAiModelId, mainlandZAiModels } from "./zai-mainland"
5 changes: 1 addition & 4 deletions src/shared/api/models/litellm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
supportsTools: true,
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
// No inputPrice/outputPrice — unknown LiteLLM models have unknown pricing, not $0
temperature: 0,
}
3 changes: 1 addition & 2 deletions src/shared/api/models/openai-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
supportsReasoning: true,
supportsStrictTools: false,
isR1FormatRequired: false,
inputPrice: 0,
outputPrice: 0,
// No inputPrice/outputPrice — unknown models have unknown pricing, not $0
temperature: 0,
}

Expand Down
10 changes: 10 additions & 0 deletions src/shared/api/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,13 @@ export interface LiteLLMModelInfo extends ModelInfo {
export interface BasetenModelInfo extends ModelInfo {
supportedFeatures?: string[]
}

// True when the model has any pricing data (even $0); false when pricing is unknown.
export function hasPricing(modelInfo: ModelInfo): boolean {
return modelInfo.inputPrice !== undefined || modelInfo.outputPrice !== undefined
}

// True only for models with explicitly zero base prices (genuinely free).
export function isFreeModel(modelInfo: ModelInfo): boolean {
return modelInfo.inputPrice === 0 && modelInfo.outputPrice === 0
}
Loading