mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : drop the anthropic protocol from the backends
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
import { findBackendPreset } from '$lib/utils';
|
||||
|
||||
const PROTOCOL_LABELS: Record<BackendProtocol, string> = {
|
||||
anthropic: 'Anthropic',
|
||||
'llama.cpp': 'llama.cpp',
|
||||
openai: 'OpenAI'
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
const PROTOCOL_OPTIONS: Array<{ label: string; value: BackendProtocol }> = [
|
||||
{ label: 'OpenAI-compatible', value: 'openai' },
|
||||
{ label: 'Anthropic-compatible', value: 'anthropic' },
|
||||
{ label: 'llama.cpp (llama-server)', value: 'llama.cpp' }
|
||||
];
|
||||
|
||||
@@ -26,7 +25,6 @@
|
||||
let protocolLabel = $derived(
|
||||
PROTOCOL_OPTIONS.find((option) => option.value === backend.protocol)?.label ?? ''
|
||||
);
|
||||
let isAnthropic = $derived(backend.protocol === 'anthropic');
|
||||
</script>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -95,13 +93,7 @@
|
||||
value={backend.apiKey ?? ''}
|
||||
/>
|
||||
|
||||
<p class="mt-1.5 text-xs text-muted-foreground">
|
||||
{#if isAnthropic}
|
||||
Sent as the x-api-key header.
|
||||
{:else}
|
||||
Sent as a Bearer token.
|
||||
{/if}
|
||||
</p>
|
||||
<p class="mt-1.5 text-xs text-muted-foreground">Sent as a Bearer token.</p>
|
||||
</div>
|
||||
|
||||
<Collapsible.Root bind:open={showAdvanced}>
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{isEdit ? 'Edit backend' : 'Add backend'}</Dialog.Title>
|
||||
|
||||
<Dialog.Description>Connect an OpenAI- or Anthropic-compatible endpoint.</Dialog.Description>
|
||||
<Dialog.Description>Connect an OpenAI-compatible endpoint.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if !isEdit}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<Empty.Title>Add another backend</Empty.Title>
|
||||
|
||||
<Empty.Description>Connect an OpenAI- or Anthropic-compatible endpoint.</Empty.Description>
|
||||
<Empty.Description>Connect an OpenAI-compatible endpoint.</Empty.Description>
|
||||
</Empty.Header>
|
||||
|
||||
<Empty.Content>
|
||||
|
||||
@@ -5,14 +5,11 @@ import type {
|
||||
BackendProtocol
|
||||
} from '$lib/types';
|
||||
|
||||
/** Version sent with the Anthropic Messages API. */
|
||||
export const ANTHROPIC_API_VERSION = '2023-06-01';
|
||||
|
||||
/** Prefix for generated ids of user-added backends. */
|
||||
export const BACKEND_ID_PREFIX = 'backend';
|
||||
|
||||
/** Protocols a configured backend can speak, in display order. */
|
||||
export const BACKEND_PROTOCOLS: readonly BackendProtocol[] = ['llama.cpp', 'openai', 'anthropic'];
|
||||
export const BACKEND_PROTOCOLS: readonly BackendProtocol[] = ['llama.cpp', 'openai'];
|
||||
|
||||
/** Chat completions path used when a backend does not override it. */
|
||||
export const DEFAULT_BACKEND_CHAT_PATH = '/v1/chat/completions';
|
||||
@@ -34,7 +31,7 @@ const LLAMA_CPP_CAPABILITIES: BackendCapabilities = {
|
||||
statusFeed: true,
|
||||
tools: true
|
||||
};
|
||||
/** Capabilities of a plain OpenAI- or Anthropic-compatible endpoint. */
|
||||
/** Capabilities of a plain OpenAI-compatible endpoint. */
|
||||
const COMPATIBLE_CAPABILITIES: BackendCapabilities = {
|
||||
corsProxy: false,
|
||||
loadUnload: false,
|
||||
@@ -48,15 +45,12 @@ const COMPATIBLE_CAPABILITIES: BackendCapabilities = {
|
||||
|
||||
/** Capabilities per backend protocol. */
|
||||
export const BACKEND_CAPABILITIES: Record<BackendProtocol, BackendCapabilities> = {
|
||||
anthropic: COMPATIBLE_CAPABILITIES,
|
||||
'llama.cpp': LLAMA_CPP_CAPABILITIES,
|
||||
openai: COMPATIBLE_CAPABILITIES
|
||||
};
|
||||
|
||||
/** Default wire quirks per protocol. */
|
||||
export const BACKEND_COMPAT: Record<BackendProtocol, BackendCompat> = {
|
||||
// the Messages API has no OpenAI-style token cap or usage-in-stream toggle
|
||||
anthropic: { maxTokensField: 'max_tokens', supportsUsageInStreaming: false },
|
||||
// llama-server reports its own timings, so it needs no usage chunk
|
||||
'llama.cpp': { maxTokensField: 'max_tokens', supportsUsageInStreaming: false },
|
||||
openai: { maxTokensField: 'max_tokens', supportsUsageInStreaming: true }
|
||||
|
||||
@@ -3,12 +3,6 @@ const MCP_SESSION_ID_VISIBLE_CHARS = 5;
|
||||
|
||||
/** HTTP header handling for API and MCP requests. */
|
||||
export const HEADERS = {
|
||||
/** Anthropic-compatible backends authenticate with this header instead of Authorization */
|
||||
ANTHROPIC_API_KEY: 'x-api-key',
|
||||
/** Browser opt-in required by the Anthropic Messages API direct browser access */
|
||||
ANTHROPIC_BROWSER_ACCESS: 'anthropic-dangerous-direct-browser-access',
|
||||
/** Required version header for the Anthropic Messages API */
|
||||
ANTHROPIC_VERSION: 'anthropic-version',
|
||||
/** Canonical casing for the Authorization header (RFC 7235) */
|
||||
AUTHORIZATION: 'Authorization',
|
||||
/** Bearer scheme prefix used for Authorization headers (RFC 6750) */
|
||||
|
||||
@@ -751,7 +751,7 @@ export class ChatService {
|
||||
break;
|
||||
|
||||
case 'usage':
|
||||
// providers may split usage across chunks (Anthropic reports input
|
||||
// providers may split usage across chunks (some report input
|
||||
// tokens on message_start and output tokens on message_delta)
|
||||
usage = { ...usage, ...event.usage };
|
||||
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
/**
|
||||
* Anthropic Messages protocol.
|
||||
*
|
||||
* The Messages API differs from the OpenAI shape in three ways that matter
|
||||
* here: the system prompt is a top level field, messages carry typed content
|
||||
* blocks, and tool calls are content blocks rather than a message field.
|
||||
*/
|
||||
|
||||
import type { ChatProtocolAdapter, ChatStreamEvent, ChatStreamReader } from './types';
|
||||
import { ANTHROPIC_API_VERSION, HEADERS } from '$lib/constants';
|
||||
import { ContentPartType, MessageRole } from '$lib/enums';
|
||||
import type { Backend } from '$lib/types';
|
||||
import type {
|
||||
ApiChatCompletionTool,
|
||||
ApiChatCompletionUsage,
|
||||
ApiChatMessageContentPart,
|
||||
ApiChatMessageData
|
||||
} from '$lib/types/api';
|
||||
|
||||
/** Output cap sent when the user did not configure one; the field is required. */
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
|
||||
interface AnthropicBlock {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AnthropicTool {
|
||||
description?: string;
|
||||
input_schema: Record<string, unknown>;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AnthropicStreamEvent {
|
||||
type?: string;
|
||||
index?: number;
|
||||
content_block?: { type?: string; id?: string; name?: string };
|
||||
delta?: { type?: string; text?: string; thinking?: string; partial_json?: string };
|
||||
error?: { message?: string };
|
||||
message?: {
|
||||
id?: string;
|
||||
model?: string;
|
||||
usage?: Record<string, unknown>;
|
||||
};
|
||||
usage?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function authHeaders(backend: Backend): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
...(backend.headers ?? {}),
|
||||
[HEADERS.ANTHROPIC_BROWSER_ACCESS]: 'true',
|
||||
[HEADERS.ANTHROPIC_VERSION]: ANTHROPIC_API_VERSION
|
||||
};
|
||||
const apiKey = backend.apiKey?.trim();
|
||||
|
||||
if (apiKey) {
|
||||
headers[HEADERS.ANTHROPIC_API_KEY] = apiKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function textOf(content: string | ApiChatMessageContentPart[]): string {
|
||||
if (typeof content === 'string') return content;
|
||||
|
||||
return content
|
||||
.filter((part) => part.type === ContentPartType.TEXT)
|
||||
.map((part) => part.text ?? '')
|
||||
.join('');
|
||||
}
|
||||
|
||||
function imageBlock(url: string): AnthropicBlock {
|
||||
const dataUrl = url.match(/^data:([^;,]+);base64,(.*)$/s);
|
||||
|
||||
if (dataUrl) {
|
||||
return { source: { data: dataUrl[2], media_type: dataUrl[1], type: 'base64' }, type: 'image' };
|
||||
}
|
||||
|
||||
return { source: { type: 'url', url }, type: 'image' };
|
||||
}
|
||||
|
||||
function userBlocks(content: string | ApiChatMessageContentPart[]): AnthropicBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return content ? [{ text: content, type: 'text' }] : [];
|
||||
}
|
||||
|
||||
return content.flatMap((part): AnthropicBlock[] => {
|
||||
if (part.type === ContentPartType.TEXT) {
|
||||
return part.text ? [{ text: part.text, type: 'text' }] : [];
|
||||
}
|
||||
|
||||
if (part.type === ContentPartType.IMAGE_URL && part.image_url?.url) {
|
||||
return [imageBlock(part.image_url.url)];
|
||||
}
|
||||
|
||||
// audio and video have no Messages API equivalent
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function assistantBlocks(message: ApiChatMessageData): AnthropicBlock[] {
|
||||
const blocks: AnthropicBlock[] = [];
|
||||
const text = textOf(message.content);
|
||||
|
||||
if (text) blocks.push({ text, type: 'text' });
|
||||
|
||||
for (const call of message.tool_calls ?? []) {
|
||||
let input: unknown = {};
|
||||
|
||||
try {
|
||||
input = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
||||
} catch {
|
||||
input = {};
|
||||
}
|
||||
|
||||
blocks.push({ id: call.id, input, name: call.function?.name, type: 'tool_use' });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function convertTools(tools: unknown): AnthropicTool[] | undefined {
|
||||
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
||||
|
||||
return (tools as ApiChatCompletionTool[]).map((tool) => ({
|
||||
description: tool.function?.description,
|
||||
input_schema: tool.function?.parameters ?? { properties: {}, type: 'object' },
|
||||
name: tool.function?.name
|
||||
}));
|
||||
}
|
||||
|
||||
function buildChatRequest(
|
||||
body: Record<string, unknown>,
|
||||
_backend: Backend
|
||||
): Record<string, unknown> {
|
||||
const messages = Array.isArray(body.messages) ? (body.messages as ApiChatMessageData[]) : [];
|
||||
const configuredMaxTokens = typeof body.max_tokens === 'number' ? body.max_tokens : 0;
|
||||
const maxTokens = configuredMaxTokens > 0 ? configuredMaxTokens : DEFAULT_MAX_TOKENS;
|
||||
const system: string[] = [];
|
||||
const converted: { content: AnthropicBlock[]; role: 'assistant' | 'user' }[] = [];
|
||||
// the Messages API requires strictly alternating roles, so adjacent blocks
|
||||
// of the same role (tool results, split user turns) are merged
|
||||
const push = (role: 'assistant' | 'user', content: AnthropicBlock[]): void => {
|
||||
if (content.length === 0) return;
|
||||
|
||||
const last = converted.at(-1);
|
||||
|
||||
if (last?.role === role) {
|
||||
last.content.push(...content);
|
||||
} else {
|
||||
converted.push({ content, role });
|
||||
}
|
||||
};
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === MessageRole.SYSTEM) {
|
||||
const text = textOf(message.content);
|
||||
|
||||
if (text) system.push(text);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.role === MessageRole.TOOL) {
|
||||
push('user', [
|
||||
{
|
||||
content: textOf(message.content),
|
||||
tool_use_id: message.tool_call_id,
|
||||
type: 'tool_result'
|
||||
}
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.role === MessageRole.ASSISTANT) {
|
||||
push('assistant', assistantBlocks(message));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
push('user', userBlocks(message.content));
|
||||
}
|
||||
|
||||
const request: Record<string, unknown> = {
|
||||
max_tokens: maxTokens,
|
||||
messages: converted,
|
||||
model: body.model
|
||||
};
|
||||
|
||||
if (system.length > 0) request.system = system.join('\n\n');
|
||||
|
||||
if (body.stream) request.stream = true;
|
||||
|
||||
if (typeof body.temperature === 'number') request.temperature = body.temperature;
|
||||
|
||||
if (typeof body.top_p === 'number') request.top_p = body.top_p;
|
||||
|
||||
const tools = convertTools(body.tools);
|
||||
|
||||
if (tools) request.tools = tools;
|
||||
|
||||
const chatTemplateKwargs = body.chat_template_kwargs as Record<string, unknown> | undefined;
|
||||
const budgetTokens =
|
||||
typeof body.thinking_budget_tokens === 'number' ? body.thinking_budget_tokens : 0;
|
||||
|
||||
// extended thinking needs a budget below max_tokens, and Anthropic rejects
|
||||
// temperature and top_p while thinking is enabled
|
||||
if (
|
||||
chatTemplateKwargs?.enable_thinking === true &&
|
||||
budgetTokens > 0 &&
|
||||
budgetTokens < maxTokens
|
||||
) {
|
||||
request.thinking = { budget_tokens: budgetTokens, type: 'enabled' };
|
||||
delete request.temperature;
|
||||
delete request.top_p;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/** Map an Anthropic usage object onto the compatible fields, defined keys only. */
|
||||
function usageOf(raw: Record<string, unknown> | undefined): ApiChatCompletionUsage | undefined {
|
||||
if (!raw) return undefined;
|
||||
|
||||
const usage: ApiChatCompletionUsage = {};
|
||||
const fields = [
|
||||
'cache_creation_input_tokens',
|
||||
'cache_read_input_tokens',
|
||||
'input_tokens',
|
||||
'output_tokens'
|
||||
] as const;
|
||||
|
||||
for (const field of fields) {
|
||||
const value = raw[field];
|
||||
|
||||
if (typeof value === 'number') usage[field] = value;
|
||||
}
|
||||
|
||||
return Object.keys(usage).length > 0 ? usage : undefined;
|
||||
}
|
||||
|
||||
function createStreamReader(): ChatStreamReader {
|
||||
// Anthropic indexes content blocks across text and tool_use, while the
|
||||
// canonical tool call deltas are indexed among tool calls only
|
||||
const toolIndexes = new Map<number, number>();
|
||||
|
||||
let toolCount = 0;
|
||||
|
||||
const toolIndex = (blockIndex: number | undefined): number => {
|
||||
if (typeof blockIndex !== 'number') return toolCount++;
|
||||
|
||||
const known = toolIndexes.get(blockIndex);
|
||||
|
||||
if (known !== undefined) return known;
|
||||
|
||||
const index = toolCount++;
|
||||
|
||||
toolIndexes.set(blockIndex, index);
|
||||
|
||||
return index;
|
||||
};
|
||||
|
||||
return {
|
||||
readChunk(payload: unknown): ChatStreamEvent[] {
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
|
||||
const event = payload as AnthropicStreamEvent;
|
||||
const events: ChatStreamEvent[] = [];
|
||||
|
||||
switch (event.type) {
|
||||
case 'message_start': {
|
||||
if (event.message?.id) events.push({ id: event.message.id, type: 'id' });
|
||||
|
||||
if (event.message?.model) events.push({ model: event.message.model, type: 'model' });
|
||||
|
||||
const usage = usageOf(event.message?.usage);
|
||||
|
||||
if (usage) events.push({ type: 'usage', usage });
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'content_block_start': {
|
||||
if (event.content_block?.type === 'tool_use') {
|
||||
events.push({
|
||||
deltas: [
|
||||
{
|
||||
function: { name: event.content_block.name },
|
||||
id: event.content_block.id,
|
||||
index: toolIndex(event.index),
|
||||
type: 'function'
|
||||
}
|
||||
],
|
||||
type: 'tool_calls'
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'content_block_delta': {
|
||||
const delta = event.delta;
|
||||
|
||||
if (delta?.type === 'text_delta' && delta.text) {
|
||||
events.push({ text: delta.text, type: 'text' });
|
||||
} else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
||||
events.push({ text: delta.thinking, type: 'thinking' });
|
||||
} else if (delta?.type === 'input_json_delta' && delta.partial_json) {
|
||||
events.push({
|
||||
deltas: [
|
||||
{
|
||||
function: { arguments: delta.partial_json },
|
||||
index: toolIndex(event.index)
|
||||
}
|
||||
],
|
||||
type: 'tool_calls'
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'message_delta': {
|
||||
const usage = usageOf(event.usage);
|
||||
|
||||
if (usage) events.push({ type: 'usage', usage });
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'message_stop':
|
||||
events.push({ type: 'done' });
|
||||
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
events.push({
|
||||
message: event.error?.message ?? 'Anthropic stream error',
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicAdapter: ChatProtocolAdapter = {
|
||||
authHeaders,
|
||||
buildChatRequest,
|
||||
createStreamReader
|
||||
};
|
||||
@@ -6,13 +6,11 @@
|
||||
* what the local llama-server speaks.
|
||||
*/
|
||||
|
||||
import { anthropicAdapter } from './anthropic';
|
||||
import { openaiAdapter } from './openai';
|
||||
import type { ChatProtocolAdapter } from './types';
|
||||
import type { Backend, BackendProtocol } from '$lib/types';
|
||||
|
||||
const ADAPTERS: Record<BackendProtocol, ChatProtocolAdapter> = {
|
||||
anthropic: anthropicAdapter,
|
||||
'llama.cpp': openaiAdapter,
|
||||
openai: openaiAdapter
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* SSE framing, resume offsets) and delegates the parts that differ per
|
||||
* protocol here: credential headers, request shaping and stream decoding.
|
||||
*
|
||||
* Decoding is per-stream: the Anthropic reader tracks content block state, so
|
||||
* it must not be shared between concurrent requests.
|
||||
* Decoding is per-stream: a reader keeps per-stream state, so it must not be
|
||||
* shared between concurrent requests.
|
||||
*/
|
||||
|
||||
import type { Backend } from '$lib/types';
|
||||
|
||||
Vendored
-4
@@ -379,12 +379,8 @@ export interface ApiChatCompletionStreamChunk {
|
||||
}
|
||||
|
||||
export interface ApiChatCompletionUsage {
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
cached_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
prompt_cache_hit_tokens?: number;
|
||||
prompt_tokens?: number;
|
||||
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
||||
|
||||
Vendored
+3
-3
@@ -3,12 +3,12 @@
|
||||
*
|
||||
* A backend is one API endpoint the UI can talk to. The built-in `local`
|
||||
* backend is the llama-server serving the UI. External backends are
|
||||
* user-configured endpoints that speak an OpenAI- or Anthropic-compatible
|
||||
* user-configured endpoints that speak an OpenAI-compatible
|
||||
* protocol.
|
||||
*/
|
||||
|
||||
/** Request/response shape a backend speaks. */
|
||||
export type BackendProtocol = 'llama.cpp' | 'openai' | 'anthropic';
|
||||
export type BackendProtocol = 'llama.cpp' | 'openai';
|
||||
|
||||
/**
|
||||
* Wire-level quirks of a backend's protocol. Capabilities gate llama.cpp
|
||||
@@ -23,7 +23,7 @@ export interface BackendCompat {
|
||||
|
||||
/**
|
||||
* Features a backend supports. A llama.cpp server exposes extra endpoints on
|
||||
* top of the OpenAI-compatible API; plain OpenAI- and Anthropic-compatible
|
||||
* top of the OpenAI-compatible API; plain OpenAI-compatible
|
||||
* endpoints only provide chat and model listing.
|
||||
*/
|
||||
export interface BackendCapabilities {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Client side timing fallback for backends that do not report their own.
|
||||
*
|
||||
* llama.cpp streams per-token timings; OpenAI and Anthropic compatible servers
|
||||
* llama.cpp streams per-token timings; OpenAI-compatible servers
|
||||
* do not. Token counts come from the usage block of the final chunk (or the
|
||||
* count of streamed deltas as a fallback), times are measured locally: the wait
|
||||
* for the first token is attributed to prompt processing, the rest to
|
||||
@@ -18,32 +18,27 @@ export interface StreamClock {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt/output/cache token counts, accepting OpenAI and Anthropic usage
|
||||
* fields. `promptTokens` excludes the cache read tokens, which are returned
|
||||
* separately as `cacheTokens`, so the two always add up to the prompt size.
|
||||
* Prompt/output/cache token counts. `promptTokens` excludes the cache read
|
||||
* tokens, which are returned separately as `cacheTokens`, so the two always
|
||||
* add up to the prompt size.
|
||||
*/
|
||||
export function usageTokenCounts(usage: ApiChatCompletionUsage | undefined): {
|
||||
cacheTokens: number;
|
||||
completionTokens: number;
|
||||
promptTokens: number;
|
||||
} {
|
||||
// Anthropic reports the input excluding cache tokens and splits reads from
|
||||
// writes; OpenAI-compatible servers report a total that includes the reads
|
||||
const isAnthropicStyle = usage?.input_tokens !== undefined;
|
||||
const cacheTokens = isAnthropicStyle
|
||||
? (usage?.cache_read_input_tokens ?? 0)
|
||||
: (usage?.prompt_tokens_details?.cached_tokens ??
|
||||
usage?.prompt_cache_hit_tokens ??
|
||||
usage?.cached_tokens ??
|
||||
0);
|
||||
const promptTotal = isAnthropicStyle
|
||||
? (usage?.input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0)
|
||||
: (usage?.prompt_tokens ?? 0);
|
||||
// a total that includes the cache reads, which are reported separately
|
||||
const cacheTokens =
|
||||
usage?.prompt_tokens_details?.cached_tokens ??
|
||||
usage?.prompt_cache_hit_tokens ??
|
||||
usage?.cached_tokens ??
|
||||
0;
|
||||
const promptTotal = usage?.prompt_tokens ?? 0;
|
||||
|
||||
return {
|
||||
cacheTokens,
|
||||
completionTokens: usage?.completion_tokens ?? usage?.output_tokens ?? 0,
|
||||
promptTokens: isAnthropicStyle ? promptTotal : Math.max(0, promptTotal - cacheTokens)
|
||||
completionTokens: usage?.completion_tokens ?? 0,
|
||||
promptTokens: Math.max(0, promptTotal - cacheTokens)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user