( messages: Anthropic.Beta.Messages.BetaMessageParam[], tools: Anthropic.Beta.Messages.BetaToolUnion[], )
| 255 | * otherwise uses the configured SmallFastModel. |
| 256 | */ |
| 257 | export async function countTokensViaHaikuFallback( |
| 258 | messages: Anthropic.Beta.Messages.BetaMessageParam[], |
| 259 | tools: Anthropic.Beta.Messages.BetaToolUnion[], |
| 260 | ): Promise<number | null> { |
| 261 | // Check if messages contain thinking blocks |
| 262 | const containsThinking = hasThinkingBlocks(messages) |
| 263 | |
| 264 | // If we're on Vertex and using global region, always use Sonnet since Haiku is not available there. |
| 265 | const isVertexGlobalEndpoint = |
| 266 | isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) && |
| 267 | getVertexRegionForModel(getSmallFastModel()) === 'global' |
| 268 | // If we're on Bedrock with thinking blocks, use Sonnet since Haiku 3.5 doesn't support thinking |
| 269 | const isBedrockWithThinking = |
| 270 | isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && containsThinking |
| 271 | // If we're on Vertex with thinking blocks, use Sonnet since Haiku 3.5 doesn't support thinking |
| 272 | const isVertexWithThinking = |
| 273 | isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) && containsThinking |
| 274 | // On Noumena-managed first-party, route token counting through the main |
| 275 | // loop model so the reported usage matches the tokenizer of the serving |
| 276 | // model. GLM, Kimi, and DeepSeek V4 Flash each have provider-specific |
| 277 | // tokenizers; using a different model family for counting would silently |
| 278 | // mis-estimate input tokens, breaking auto-compact thresholds. |
| 279 | const isManagedFirstParty = getAPIProvider() === 'firstParty' |
| 280 | const model = isManagedFirstParty |
| 281 | ? getMainLoopModel() |
| 282 | : isVertexGlobalEndpoint || isBedrockWithThinking || isVertexWithThinking |
| 283 | ? getDefaultFlashModel() |
| 284 | : getSmallFastModel() |
| 285 | const inferenceClient = await getInferenceClient({ |
| 286 | maxRetries: 1, |
| 287 | model, |
| 288 | source: 'count_tokens', |
| 289 | }) |
| 290 | |
| 291 | // Strip tool search-specific fields (caller, tool_reference) before sending |
| 292 | // These fields are only valid with the tool search beta header |
| 293 | const normalizedMessages = stripToolSearchFieldsFromMessages(messages) |
| 294 | |
| 295 | const messagesToSend: MessageParam[] = |
| 296 | normalizedMessages.length > 0 |
| 297 | ? (normalizedMessages as MessageParam[]) |
| 298 | : [{ role: 'user', content: 'count' }] |
| 299 | |
| 300 | const betas = getModelBetas(model) |
| 301 | // Filter betas for Vertex - some betas (like web-search) cause 400 errors |
| 302 | // on certain Vertex endpoints. See issue #10789. |
| 303 | const filteredBetas = |
| 304 | getAPIProvider() === 'vertex' |
| 305 | ? betas.filter(b => VERTEX_COUNT_TOKENS_ALLOWED_BETAS.has(b)) |
| 306 | : betas |
| 307 | |
| 308 | // biome-ignore lint/plugin: token counting needs specialized parameters (thinking, betas) that sideQuery doesn't support |
| 309 | const response = await inferenceClient.createMessage({ |
| 310 | model: normalizeModelStringForAPI(model), |
| 311 | max_tokens: containsThinking ? TOKEN_COUNT_MAX_TOKENS : 1, |
| 312 | messages: messagesToSend, |
| 313 | tools: tools.length > 0 ? tools : undefined, |
| 314 | ...(filteredBetas.length > 0 && { betas: filteredBetas }), |
no test coverage detected