GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
字體大小: +
4 分鐘閱讀時間 (814 個字)

GPT API 圖片輸入:Base64 vs URL,Token 成本差多大?

GPT API 圖片輸入:Base64 vs URL,Token 成本差多大?

有人可能會覺得 gpt-4oooo, gpt-4.1 這種多模態 LLM,呼叫 ChatCompletion API 給 Image 時,
如果給 Image 的 Base64 內容,所花的 Token 數會比給 Image URL 來得多很多 (´− `) ンー (¬_¬)
是因為 Image 的 Base64 字串長度比 Image URL 的字串內容多很多,所以 Token 數自然也就多很多 !!! 

所以,如果要給圖檔時,要想儘辦法讓 OpenAI API 可以讀取到圖檔,
這樣子不就需要把圖檔放在 internet 上面,變成任何人都可以存取得到? 如果企業有資安的考量,若圖檔中包含機敏資料或內部資訊,將其暴露在 internet  上可能會產生資料外洩的風險,一定要用 URL 才能省 Token 嗎?

Image 的 Base64 內容,所花的 Token 數會比給 Image URL 來得多很多,這是真的嗎?

以下我們就來驗證看看是不是這個樣子:

 

9c596e37cf0259bc47d5aab5beb7fc388f2bd469

 

測試

使用 Semantic Kernel C#,使用 ImageContent 分別給 url 及 file bytes (base64),程式如下,

IKernelBuilder builder = Kernel.CreateBuilder();
const string apikey = "sk-請給 openai apikey";
const string model = "gpt-4.1-mini";

builder.AddOpenAIChatCompletion(model, apikey);
Kernel kernel = builder.Build();

var chatCompletionService = kernel.GetRequiredService();

ChatHistory chatHistory = new();
string textContent = "請將摘要這張圖片中的文字。\r\n";
bool isUseUri = true; //or false

if (isUseUri)
{
chatHistory.Add(
new()
{
Role = AuthorRole.User,
Items = [
new TextContent(textContent),
new ImageContent(new Uri($"{對外的ImageUrl}"))
]
}
);
}
else
{
byte[] imageBytes = File.ReadAllBytes("path/to/your/image.png");
chatHistory.Add(
new()
{
Role = AuthorRole.User,
Items = [
new TextContent(textContent),
new ImageContent(imageBytes, "image/png")
]
}
);
}


var reply = await chatCompletionService.GetChatMessageContentAsync(chatHistory);
Console.WriteLine("================");
Console.WriteLine(reply.Content);
Console.WriteLine("================");
Helper.OutputInnerContent(reply.InnerContent as OpenAI.Chat.ChatCompletion);



public static void OutputInnerContent(OpenAI.Chat.ChatCompletion innerContent)
{
Console.WriteLine($"Message role: {innerContent.Role}"); // Available as a property of ChatMessageContent
Console.WriteLine($"Message content: {innerContent.Content[0].Text}"); // Available as a property of ChatMessageContent

Console.WriteLine($"Model: {innerContent.Model}"); // Model doesn't change per chunk, so we can get it from the first chunk only
Console.WriteLine($"Created At: {innerContent.CreatedAt}");

Console.WriteLine($"Finish reason: {innerContent.FinishReason}");
Console.WriteLine($"Input tokens usage: {innerContent.Usage.InputTokenCount}");
Console.WriteLine($"Output tokens usage: {innerContent.Usage.OutputTokenCount}");
Console.WriteLine($"Total tokens usage: {innerContent.Usage.TotalTokenCount}");
Console.WriteLine($"Refusal: {innerContent.Refusal} ");
Console.WriteLine($"Id: {innerContent.Id}");
Console.WriteLine($"System fingerprint: {innerContent.SystemFingerprint}");

if (innerContent.ContentTokenLogProbabilities.Count > 0)
{
Console.WriteLine("Content token log probabilities:");
foreach (var contentTokenLogProbability in innerContent.ContentTokenLogProbabilities)
{
Console.WriteLine($"Token: {contentTokenLogProbability.Token}");
Console.WriteLine($"Log probability: {contentTokenLogProbability.LogProbability}");

Console.WriteLine(" Top log probabilities for this token:");
foreach (var topLogProbability in contentTokenLogProbability.TopLogProbabilities)
{
Console.WriteLine($" Token: {topLogProbability.Token}");
Console.WriteLine($" Log probability: {topLogProbability.LogProbability}");
Console.WriteLine(" =======");
}

Console.WriteLine("--------------");
}
}

if (innerContent.RefusalTokenLogProbabilities.Count > 0)
{
Console.WriteLine("Refusal token log probabilities:");
foreach (var refusalTokenLogProbability in innerContent.RefusalTokenLogProbabilities)
{
Console.WriteLine($"Token: {refusalTokenLogProbability.Token}");
Console.WriteLine($"Log probability: {refusalTokenLogProbability.LogProbability}");

Console.WriteLine(" Refusal top log probabilities for this token:");
foreach (var topLogProbability in refusalTokenLogProbability.TopLogProbabilities)
{
Console.WriteLine($" Token: {topLogProbability.Token}");
Console.WriteLine($" Log probability: {topLogProbability.LogProbability}");
Console.WriteLine(" =======");
}
}
}
}

 

  • 註: new ImageContent(imageBytes, "image/png")中的 imageBytes會被轉成 Base64 字串(DataEncodingHelpers.cs),如下程式,

public static string CreateDataUri(BinaryData bytes, string bytesMediaType)
{
string base64Bytes = Convert.ToBase64String(bytes.ToArray());
return $"data:{bytesMediaType};base64,{base64Bytes}";
}

 

程式 Log 出來的輸入 Token 是 2,456 個,如下圖:

從 OpenAI 的 Log 來看,2 次的 Input Token 都是 2,456 個,跟我們程式 Log 出來的結果相同,如下圖:

另外,現在 gpt-4.1 比 gpt-4o 還便宜,而且還有 100萬個 Token ,所以原本使用 gpt-4o 的可以改用 gpt-4.1 ,如果用在圖片辨識上面,可以用 gpt-4.1-mini 更便宜,效果也不錯。

總結

使用多模態 LLM 時,無論是提供圖片的 URL 或是 Base64 編碼字串,所消耗的 Input Token 數是相同的;差別只在於 API 傳送時 Payload 的大小。
對於企業內部的圖檔,建議採用 Base64 的方式上傳,不僅能避免圖檔對外公開的資安風險,也更適合處理內網或敏感資料。

最後,再強調一次,

使用多模態 LLM,給 Image 的 Url 或是給 Base64 字串,所花費的 Input Token 數是一樣的 (>人<)

參考資源

Multi-modal chat completion
DataEncodingHelpers.cs - CreateDataUri

×
Stay Informed

When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.

透過 Microsoft Graph API 取得 Teams 線上會議文字記錄完整教學
IIS Application Pool 檔案目錄權限解析:為什麼不用加帳號也能運作?

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
2025年9月26日, 星期五

Captcha 圖像