236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
import { inferTeamProviderIdFromModel } from './teamProvider';
|
|
|
|
import type { TeamProviderId } from '@shared/types/team';
|
|
|
|
const ANTHROPIC_DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
const ANTHROPIC_EXTENDED_CONTEXT_WINDOW = 1_000_000;
|
|
const OPENAI_COMPACT_CONTEXT_WINDOW = 200_000;
|
|
const OPENAI_DEFAULT_CONTEXT_WINDOW = 400_000;
|
|
const OPENAI_LONG_CONTEXT_WINDOW = 1_050_000;
|
|
|
|
export interface ContextUsageLike {
|
|
input_tokens?: number;
|
|
output_tokens?: number;
|
|
cache_read_input_tokens?: number;
|
|
cache_creation_input_tokens?: number;
|
|
prompt_tokens?: number;
|
|
completion_tokens?: number;
|
|
total_tokens?: number;
|
|
input_tokens_details?: {
|
|
cached_tokens?: number;
|
|
};
|
|
prompt_tokens_details?: {
|
|
cached_tokens?: number;
|
|
};
|
|
output_tokens_details?: {
|
|
reasoning_tokens?: number;
|
|
};
|
|
completion_tokens_details?: {
|
|
reasoning_tokens?: number;
|
|
};
|
|
}
|
|
|
|
export type PromptInputSource =
|
|
| 'anthropic_usage'
|
|
| 'openai_responses_usage'
|
|
| 'openai_chat_usage'
|
|
| 'unavailable';
|
|
|
|
export interface DerivedContextMetrics {
|
|
providerId: TeamProviderId | undefined;
|
|
modelName: string | undefined;
|
|
contextWindowTokens: number | null;
|
|
promptInputTokens: number | null;
|
|
outputTokens: number | null;
|
|
contextUsedTokens: number | null;
|
|
visibleContextTokens: number;
|
|
promptInputSource: PromptInputSource;
|
|
contextUsedSource: PromptInputSource | 'unavailable';
|
|
promptInputPercentOfContextWindow: number | null;
|
|
contextUsedPercentOfContextWindow: number | null;
|
|
visibleContextPercentOfPromptInput: number | null;
|
|
}
|
|
|
|
interface InferContextWindowTokensParams {
|
|
providerId?: TeamProviderId;
|
|
modelName?: string;
|
|
limitContext?: boolean;
|
|
}
|
|
|
|
interface DeriveContextMetricsParams extends InferContextWindowTokensParams {
|
|
usage?: ContextUsageLike | null;
|
|
contextWindowTokens?: number | null;
|
|
visibleContextTokens?: number;
|
|
}
|
|
|
|
function readFiniteNumber(value: unknown): number | null {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function readPositiveNumber(value: unknown): number | null {
|
|
const num = readFiniteNumber(value);
|
|
return num !== null && num > 0 ? num : null;
|
|
}
|
|
|
|
function computePercent(tokens: number | null, totalTokens: number | null): number | null {
|
|
if (tokens === null || totalTokens === null || totalTokens <= 0) {
|
|
return null;
|
|
}
|
|
if (!Number.isFinite(tokens) || tokens <= 0) {
|
|
return 0;
|
|
}
|
|
return Math.min((tokens / totalTokens) * 100, 100);
|
|
}
|
|
|
|
function isOpenAiLongContextModel(modelName: string | undefined): boolean {
|
|
const normalized = modelName?.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
normalized === 'gpt-5.4' ||
|
|
normalized.startsWith('gpt-5.4-202') ||
|
|
normalized === 'gpt-5.4-pro' ||
|
|
normalized.startsWith('gpt-5.4-pro-202')
|
|
);
|
|
}
|
|
|
|
function isOpenAiCompactContextModel(modelName: string | undefined): boolean {
|
|
const normalized = modelName?.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
|
|
return normalized === 'codex-mini-latest' || normalized.startsWith('codex-mini-latest-');
|
|
}
|
|
|
|
function isAnthropicNativeLongContextModel(modelName: string | undefined): boolean {
|
|
const normalized = modelName?.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
normalized.startsWith('claude-opus-4-7') ||
|
|
normalized.startsWith('claude-opus-4-6') ||
|
|
normalized.startsWith('claude-sonnet-4-6') ||
|
|
normalized.startsWith('claude-mythos')
|
|
);
|
|
}
|
|
|
|
function hasOpenAiPromptDetails(usage: ContextUsageLike): boolean {
|
|
return (
|
|
readFiniteNumber(usage.input_tokens_details?.cached_tokens) !== null ||
|
|
readFiniteNumber(usage.prompt_tokens_details?.cached_tokens) !== null
|
|
);
|
|
}
|
|
|
|
export function inferContextWindowTokens({
|
|
providerId,
|
|
modelName,
|
|
limitContext,
|
|
}: InferContextWindowTokensParams): number | null {
|
|
const resolvedProviderId = providerId ?? inferTeamProviderIdFromModel(modelName);
|
|
const normalizedModel = modelName?.trim().toLowerCase();
|
|
|
|
if (resolvedProviderId === 'anthropic') {
|
|
if (limitContext) {
|
|
return ANTHROPIC_DEFAULT_CONTEXT_WINDOW;
|
|
}
|
|
if (normalizedModel?.includes('[1m]') || isAnthropicNativeLongContextModel(normalizedModel)) {
|
|
return ANTHROPIC_EXTENDED_CONTEXT_WINDOW;
|
|
}
|
|
return ANTHROPIC_DEFAULT_CONTEXT_WINDOW;
|
|
}
|
|
|
|
if (resolvedProviderId === 'codex') {
|
|
if (isOpenAiCompactContextModel(normalizedModel)) {
|
|
return OPENAI_COMPACT_CONTEXT_WINDOW;
|
|
}
|
|
return isOpenAiLongContextModel(normalizedModel)
|
|
? OPENAI_LONG_CONTEXT_WINDOW
|
|
: OPENAI_DEFAULT_CONTEXT_WINDOW;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function deriveContextMetrics({
|
|
usage,
|
|
providerId,
|
|
modelName,
|
|
contextWindowTokens,
|
|
visibleContextTokens = 0,
|
|
limitContext,
|
|
}: DeriveContextMetricsParams): DerivedContextMetrics {
|
|
const resolvedProviderId = providerId ?? inferTeamProviderIdFromModel(modelName);
|
|
const resolvedContextWindowTokens =
|
|
readPositiveNumber(contextWindowTokens) ??
|
|
inferContextWindowTokens({
|
|
providerId: resolvedProviderId,
|
|
modelName,
|
|
limitContext,
|
|
});
|
|
const safeVisibleContextTokens =
|
|
Number.isFinite(visibleContextTokens) && visibleContextTokens > 0 ? visibleContextTokens : 0;
|
|
const safeUsage = usage ?? {};
|
|
const outputTokens =
|
|
readFiniteNumber(safeUsage.output_tokens) ?? readFiniteNumber(safeUsage.completion_tokens);
|
|
const promptTokens = readFiniteNumber(safeUsage.prompt_tokens);
|
|
const inputTokens = readFiniteNumber(safeUsage.input_tokens);
|
|
const cacheReadTokens = readFiniteNumber(safeUsage.cache_read_input_tokens) ?? 0;
|
|
const cacheCreationTokens = readFiniteNumber(safeUsage.cache_creation_input_tokens) ?? 0;
|
|
|
|
let promptInputTokens: number | null = null;
|
|
let promptInputSource: PromptInputSource = 'unavailable';
|
|
|
|
if (promptTokens !== null) {
|
|
promptInputTokens = promptTokens;
|
|
promptInputSource = 'openai_chat_usage';
|
|
} else if (inputTokens !== null) {
|
|
const shouldUseAnthropicFormula =
|
|
resolvedProviderId === 'anthropic' || cacheReadTokens > 0 || cacheCreationTokens > 0;
|
|
|
|
if (shouldUseAnthropicFormula) {
|
|
promptInputTokens = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
promptInputSource = 'anthropic_usage';
|
|
} else {
|
|
const missingOpenAiPromptTelemetry =
|
|
resolvedProviderId === 'codex' &&
|
|
inputTokens === 0 &&
|
|
cacheReadTokens === 0 &&
|
|
cacheCreationTokens === 0 &&
|
|
!hasOpenAiPromptDetails(safeUsage);
|
|
|
|
if (!missingOpenAiPromptTelemetry) {
|
|
promptInputTokens = inputTokens;
|
|
promptInputSource = 'openai_responses_usage';
|
|
}
|
|
}
|
|
}
|
|
|
|
const contextUsedTokens =
|
|
promptInputTokens !== null && outputTokens !== null ? promptInputTokens + outputTokens : null;
|
|
|
|
return {
|
|
providerId: resolvedProviderId,
|
|
modelName,
|
|
contextWindowTokens: resolvedContextWindowTokens,
|
|
promptInputTokens,
|
|
outputTokens,
|
|
contextUsedTokens,
|
|
visibleContextTokens: safeVisibleContextTokens,
|
|
promptInputSource,
|
|
contextUsedSource: contextUsedTokens !== null ? promptInputSource : 'unavailable',
|
|
promptInputPercentOfContextWindow: computePercent(
|
|
promptInputTokens,
|
|
resolvedContextWindowTokens
|
|
),
|
|
contextUsedPercentOfContextWindow: computePercent(
|
|
contextUsedTokens,
|
|
resolvedContextWindowTokens
|
|
),
|
|
visibleContextPercentOfPromptInput: computePercent(safeVisibleContextTokens, promptInputTokens),
|
|
};
|
|
}
|