From ddf146248f2d989cdf074f8a625571961aaa280a Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 16 Sep 2026 23:47:09 +0200 Subject: [PATCH] ui : drop the anthropic protocol from the backends Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../app/backends/BackendCard.svelte | 1 - .../app/backends/BackendForm.svelte | 10 +- .../app/backends/DialogBackendForm.svelte | 2 +- .../app/backends/SettingsBackends.svelte | 2 +- .../ui/src/lib/constants/backend.constants.ts | 10 +- .../ui/src/lib/constants/headers.constants.ts | 6 - tools/ui/src/lib/services/chat.service.ts | 2 +- .../src/lib/services/protocols/anthropic.ts | 355 ------------------ tools/ui/src/lib/services/protocols/index.ts | 2 - tools/ui/src/lib/services/protocols/types.ts | 4 +- tools/ui/src/lib/types/api.d.ts | 4 - tools/ui/src/lib/types/backend.d.ts | 6 +- tools/ui/src/lib/utils/timings.ts | 31 +- 13 files changed, 24 insertions(+), 411 deletions(-) delete mode 100644 tools/ui/src/lib/services/protocols/anthropic.ts diff --git a/tools/ui/src/lib/components/app/backends/BackendCard.svelte b/tools/ui/src/lib/components/app/backends/BackendCard.svelte index 03a9196cfc..4f2d57abb7 100644 --- a/tools/ui/src/lib/components/app/backends/BackendCard.svelte +++ b/tools/ui/src/lib/components/app/backends/BackendCard.svelte @@ -10,7 +10,6 @@ import { findBackendPreset } from '$lib/utils'; const PROTOCOL_LABELS: Record = { - anthropic: 'Anthropic', 'llama.cpp': 'llama.cpp', openai: 'OpenAI' }; diff --git a/tools/ui/src/lib/components/app/backends/BackendForm.svelte b/tools/ui/src/lib/components/app/backends/BackendForm.svelte index 992040f779..bbcc150fe0 100644 --- a/tools/ui/src/lib/components/app/backends/BackendForm.svelte +++ b/tools/ui/src/lib/components/app/backends/BackendForm.svelte @@ -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');
@@ -95,13 +93,7 @@ value={backend.apiKey ?? ''} /> -

- {#if isAnthropic} - Sent as the x-api-key header. - {:else} - Sent as a Bearer token. - {/if} -

+

Sent as a Bearer token.

diff --git a/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte b/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte index 64700dabcc..574a28c192 100644 --- a/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte +++ b/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte @@ -132,7 +132,7 @@ {isEdit ? 'Edit backend' : 'Add backend'} - Connect an OpenAI- or Anthropic-compatible endpoint. + Connect an OpenAI-compatible endpoint. {#if !isEdit} diff --git a/tools/ui/src/lib/components/app/backends/SettingsBackends.svelte b/tools/ui/src/lib/components/app/backends/SettingsBackends.svelte index 4621320b20..f1f7a7050c 100644 --- a/tools/ui/src/lib/components/app/backends/SettingsBackends.svelte +++ b/tools/ui/src/lib/components/app/backends/SettingsBackends.svelte @@ -72,7 +72,7 @@ Add another backend - Connect an OpenAI- or Anthropic-compatible endpoint. + Connect an OpenAI-compatible endpoint. diff --git a/tools/ui/src/lib/constants/backend.constants.ts b/tools/ui/src/lib/constants/backend.constants.ts index 6ad5c0a1a2..59f8023899 100644 --- a/tools/ui/src/lib/constants/backend.constants.ts +++ b/tools/ui/src/lib/constants/backend.constants.ts @@ -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 = { - anthropic: COMPATIBLE_CAPABILITIES, 'llama.cpp': LLAMA_CPP_CAPABILITIES, openai: COMPATIBLE_CAPABILITIES }; /** Default wire quirks per protocol. */ export const BACKEND_COMPAT: Record = { - // 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 } diff --git a/tools/ui/src/lib/constants/headers.constants.ts b/tools/ui/src/lib/constants/headers.constants.ts index 3f6e1207b7..d477fc8783 100644 --- a/tools/ui/src/lib/constants/headers.constants.ts +++ b/tools/ui/src/lib/constants/headers.constants.ts @@ -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) */ diff --git a/tools/ui/src/lib/services/chat.service.ts b/tools/ui/src/lib/services/chat.service.ts index 030e48f19b..c456e5b810 100644 --- a/tools/ui/src/lib/services/chat.service.ts +++ b/tools/ui/src/lib/services/chat.service.ts @@ -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 }; diff --git a/tools/ui/src/lib/services/protocols/anthropic.ts b/tools/ui/src/lib/services/protocols/anthropic.ts deleted file mode 100644 index 17ee30fae1..0000000000 --- a/tools/ui/src/lib/services/protocols/anthropic.ts +++ /dev/null @@ -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; - 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; - }; - usage?: Record; -} - -function authHeaders(backend: Backend): Record { - const headers: Record = { - ...(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, - _backend: Backend -): Record { - 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 = { - 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 | 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 | 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(); - - 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 -}; diff --git a/tools/ui/src/lib/services/protocols/index.ts b/tools/ui/src/lib/services/protocols/index.ts index 97e620d93d..db1f5668a5 100644 --- a/tools/ui/src/lib/services/protocols/index.ts +++ b/tools/ui/src/lib/services/protocols/index.ts @@ -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 = { - anthropic: anthropicAdapter, 'llama.cpp': openaiAdapter, openai: openaiAdapter }; diff --git a/tools/ui/src/lib/services/protocols/types.ts b/tools/ui/src/lib/services/protocols/types.ts index 416a9e3f02..2fb0a1eeb3 100644 --- a/tools/ui/src/lib/services/protocols/types.ts +++ b/tools/ui/src/lib/services/protocols/types.ts @@ -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'; diff --git a/tools/ui/src/lib/types/api.d.ts b/tools/ui/src/lib/types/api.d.ts index d3fe2161bc..700c463d78 100644 --- a/tools/ui/src/lib/types/api.d.ts +++ b/tools/ui/src/lib/types/api.d.ts @@ -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 }; diff --git a/tools/ui/src/lib/types/backend.d.ts b/tools/ui/src/lib/types/backend.d.ts index 8085d9b612..9ded9dc716 100644 --- a/tools/ui/src/lib/types/backend.d.ts +++ b/tools/ui/src/lib/types/backend.d.ts @@ -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 { diff --git a/tools/ui/src/lib/utils/timings.ts b/tools/ui/src/lib/utils/timings.ts index 401ecf579b..a6478e8441 100644 --- a/tools/ui/src/lib/utils/timings.ts +++ b/tools/ui/src/lib/utils/timings.ts @@ -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) }; }