Path: blob/main/extensions/copilot/src/platform/endpoint/vscode-node/extChatTokenizer.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { OutputMode, Raw } from '@vscode/prompt-tsx';6import { LanguageModelChat, LanguageModelChatTool } from 'vscode';7import { ITokenizer } from '../../../util/common/tokenizer';8import { assertNever } from '../../../util/vs/base/common/assert';9import { calculateImageTokenCost, estimateDocumentTokenCost } from '../../tokenizer/node/tokenizer';10import { convertToApiChatMessage } from './extChatEndpoint';1112/**13* BaseTokensPerCompletion is the minimum tokens for a completion request.14* Replies are primed with <|im_start|>assistant<|message|>, so these tokens represent the15* special token and the role name.16*/17const BaseTokensPerCompletion = 3;1819/*20* Each GPT 3.5 / GPT 4 message comes with 3 tokens per message due to special characters21*/22const BaseTokensPerMessage = 3;232425export class ExtensionContributedChatTokenizer implements ITokenizer {26public readonly mode = OutputMode.Raw;2728constructor(private readonly languageModel: LanguageModelChat) { }2930async tokenLength(text: string | Raw.ChatCompletionContentPart): Promise<number> {31if (typeof text === 'string') {32return this._textTokenLength(text);33}3435switch (text.type) {36case Raw.ChatCompletionContentPartKind.Text:37return this._textTokenLength(text.text);38case Raw.ChatCompletionContentPartKind.Opaque:39return text.tokenUsage || 0;40case Raw.ChatCompletionContentPartKind.Image:41if (text.imageUrl.url.startsWith('data:image/')) {42try {43return calculateImageTokenCost(text.imageUrl.url, text.imageUrl.detail);44} catch {45return this._textTokenLength(text.imageUrl.url);46}47}48return this._textTokenLength(text.imageUrl.url);49case Raw.ChatCompletionContentPartKind.CacheBreakpoint:50return 0;51case Raw.ChatCompletionContentPartKind.Document:52return estimateDocumentTokenCost(text.documentData.data);53default:54assertNever(text, `unknown content part (${JSON.stringify(text)})`);55}56}5758private async _textTokenLength(text: string): Promise<number> {59if (!text) {60return 0;61}62// Use the VS Code language model API to count tokens63return this.languageModel.countTokens(text);64}6566async countMessageTokens(message: Raw.ChatMessage): Promise<number> {67// Convert to VS Code message format and use the language model's countTokens68const apiMessages = convertToApiChatMessage([message]);69if (apiMessages.length === 0) {70return 0;71}7273// Count tokens for the message using VS Code API74const messageTokens = await this.languageModel.countTokens(apiMessages[0]);75return BaseTokensPerMessage + messageTokens;76}7778async countMessagesTokens(messages: Raw.ChatMessage[]): Promise<number> {79let numTokens = BaseTokensPerCompletion;80for (const message of messages) {81numTokens += await this.countMessageTokens(message);82}83return numTokens;84}8586async countToolTokens(tools: readonly LanguageModelChatTool[]): Promise<number> {87const baseToolTokens = 16;88let numTokens = 0;89if (tools.length) {90numTokens += baseToolTokens;91}9293const baseTokensPerTool = 8;94for (const tool of tools) {95numTokens += baseTokensPerTool;96numTokens += await this._countObjectTokens({ name: tool.name, description: tool.description, parameters: tool.inputSchema });97}9899// This is an estimate, so give a little safety margin100return Math.floor(numTokens * 1.1);101}102103private async _countObjectTokens(obj: Record<string, unknown>): Promise<number> {104let numTokens = 0;105for (const [key, value] of Object.entries(obj)) {106if (!value) {107continue;108}109110numTokens += await this._textTokenLength(key);111if (typeof value === 'string') {112numTokens += await this._textTokenLength(value);113} else if (typeof value === 'object') {114numTokens += await this._countObjectTokens(value as Record<string, unknown>);115}116}117118return numTokens;119}120}121122