/** * kosha-discovery — OpenAI ↔ Anthropic wire-format translator. * * The proxy accepts an OpenAI chat/completions request; Anthropic speaks * `/v1/messages`. This module bridges the two in both directions: * * - **request** system → top-level `system`; `tools` / `tool_choice` → * Anthropic tools; assistant `tool_calls` → `tool_use` blocks; `tool` * role → `tool_result` blocks; `image_url` parts → `image` blocks; * `response_format: json_schema` → `output_config.format`; * `reasoning_effort` → `output_config.effort`; sampling parameters are * dropped on Claude generations that reject them. * - **response** text + `tool_use` blocks → `message.content` + * `tool_calls`; Anthropic usage (cache fields included) → OpenAI usage. * - **stream** Anthropic SSE events → OpenAI `chat.completion.chunk` SSE, * with the final usage exposed to the caller for ledger reconciliation. * * Still unsupported — these throw {@link UnsupportedWireContentError} so the * proxy fails over to a native OpenAI-compatible route instead of shipping a * silently-mangled request: audio / file input parts, non-`function` tool * types, and `response_format: json_schema` on Claude generations without * native structured outputs. Fields with no Anthropic equivalent (`n`, * `seed`, `logit_bias`, penalties, logprobs) are dropped and reported in the * translation notes; `user` maps to `metadata.user_id`. * * Pure functions; no I/O. * @module */ import { type ClaudeEffort as AnthropicEffortLevel, claudeSamplingSupport, claudeSupportsForcedToolChoice, claudeSupportsNativeJsonSchema, type SamplingSupport } from "./claude-generation.js"; /** One OpenAI tool call as it appears on an assistant message. */ export interface OpenAIToolCall { id: string; type: "function"; function: { name: string; arguments: string; }; } /** One OpenAI chat message. `content` may be null on tool-calling assistant turns. */ export interface OpenAIChatMessage { role: "system" | "developer" | "user" | "assistant" | "tool" | string; content: string | Array | null; tool_calls?: unknown; tool_call_id?: string; name?: string; } /** Subset of the OpenAI chat-completions request body we know how to map. */ export interface OpenAIChatRequest { model: string; messages: OpenAIChatMessage[]; max_tokens?: number; /** Newer OpenAI spelling of `max_tokens`; wins when both are present. */ max_completion_tokens?: number; temperature?: number; top_p?: number; stop?: string | string[]; stream?: boolean; stream_options?: { include_usage?: boolean; }; tools?: unknown; tool_choice?: unknown; parallel_tool_calls?: boolean; response_format?: unknown; /** OpenAI reasoning effort: minimal | low | medium | high (| xhigh). */ reasoning_effort?: string; /** OpenAI end-user identifier; maps to Anthropic `metadata.user_id`. */ user?: string; /** * OpenAI fields that have no Anthropic equivalent (`n`, `seed`, * `logit_bias`, `presence_penalty`, `frequency_penalty`, …). Recorded so * the translator can report them in its notes instead of dropping them * silently. */ unsupportedFields?: string[]; } /** OpenAI usage block, including the cached-token detail OpenAI SDKs understand. */ export interface OpenAIUsage { prompt_tokens: number; completion_tokens: number; total_tokens: number; prompt_tokens_details?: { cached_tokens: number; }; } /** OpenAI chat-completions response shape returned to the caller. */ export interface OpenAIChatResponse { id: string; object: "chat.completion"; created: number; model: string; choices: Array<{ index: 0; message: { role: "assistant"; content: string | null; tool_calls?: OpenAIToolCall[]; }; finish_reason: string; }>; usage?: OpenAIUsage; } export type AnthropicContentBlock = { type: "text"; text: string; } | { type: "image"; source: { type: "base64"; media_type: string; data: string; } | { type: "url"; url: string; }; } | { type: "tool_use"; id: string; name: string; input: unknown; } | { type: "tool_result"; tool_use_id: string; content?: string; is_error?: boolean; }; export interface AnthropicMessage { role: "user" | "assistant"; content: string | AnthropicContentBlock[]; } export interface AnthropicTool { name: string; description?: string; input_schema: unknown; strict?: boolean; } export type AnthropicToolChoice = { type: "auto" | "any" | "none"; disable_parallel_tool_use?: boolean; } | { type: "tool"; name: string; disable_parallel_tool_use?: boolean; }; export type AnthropicEffort = AnthropicEffortLevel; /** Anthropic /v1/messages request body. */ export interface AnthropicMessagesRequest { model: string; max_tokens: number; messages: AnthropicMessage[]; system?: string; temperature?: number; top_p?: number; stop_sequences?: string[]; stream?: boolean; tools?: AnthropicTool[]; tool_choice?: AnthropicToolChoice; metadata?: { user_id?: string; }; output_config?: { effort?: AnthropicEffort; format?: { type: "json_schema"; schema: unknown; }; }; } /** Anthropic usage block. `message_delta` events report `output_tokens` cumulatively. */ export interface AnthropicUsage { input_tokens?: number; output_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; } /** Subset of the Anthropic /v1/messages response we map back to OpenAI. */ export interface AnthropicMessagesResponse { id: string; model: string; role: "assistant"; content: Array<{ type: string; text?: string; id?: string; name?: string; input?: unknown; }>; stop_reason: string | null; usage?: AnthropicUsage; } /** Result of translating a request: the Anthropic body plus human-readable notes about lossy mappings. */ export interface WireTranslation { request: AnthropicMessagesRequest; /** Each note describes one field kosha changed or dropped to satisfy the target model. */ notes: string[]; } /** * Raised when an OpenAI chat-completions body carries wire features the * Anthropic translator cannot faithfully carry. The proxy catches this by * class and falls back to a native route rather than shipping a * silently-mangled request. Throwing beats silent data loss on the path * kosha:cheapest[…] resolves through here. */ export declare class UnsupportedWireContentError extends Error { constructor(message: string); } export { claudeSamplingSupport, claudeSupportsForcedToolChoice, claudeSupportsNativeJsonSchema }; export type { SamplingSupport }; /** * Narrow a parsed OpenAI chat-completions body (`Record`) into * a typed {@link OpenAIChatRequest} via runtime guards. This is the single * untyped-JSON → typed boundary for the translator. */ export declare function coerceOpenAIChatRequest(body: Record): OpenAIChatRequest; /** Translate an OpenAI chat-completions request body into an Anthropic /v1/messages body. */ export declare function translateOpenAIToAnthropic(req: OpenAIChatRequest): AnthropicMessagesRequest; /** * Translate an OpenAI chat-completions request and report every lossy * mapping made along the way (dropped sampling params, degraded forced tool * choice, …). The proxy reflects the notes to the caller in a response * header so nothing kosha changed is invisible. */ export declare function translateOpenAIToAnthropicWithNotes(req: OpenAIChatRequest): WireTranslation; /** Translate an Anthropic /v1/messages response back into OpenAI chat-completions shape. */ export declare function translateAnthropicToOpenAI(res: AnthropicMessagesResponse, originalModel: string): OpenAIChatResponse; /** * Anthropic usage → OpenAI usage. OpenAI's `prompt_tokens` counts every * input token including cached ones, so cache reads/writes fold into it; * the cached portion is echoed under `prompt_tokens_details.cached_tokens` * (only when non-zero, so responses without caching stay byte-compatible). */ export declare function toOpenAIUsage(usage: AnthropicUsage | undefined): OpenAIUsage; /** A translated OpenAI SSE stream plus the upstream usage, resolved when the stream ends. */ export interface AnthropicStreamTranslation { stream: ReadableStream; /** * Settles on every terminal path — normal end, upstream error, or the * client cancelling — with the usage Anthropic reported, or `null` when the * message never reached `message_delta` (so output tokens are unknown and * the caller should keep its pre-flight estimate). */ usage: Promise; } /** * Translate an Anthropic `/v1/messages` SSE stream into OpenAI * `chat.completion.chunk` SSE. * * Event mapping: * - `message_start` → first chunk with `delta.role = "assistant"`; captures id + input usage * - `content_block_start` → for `tool_use` blocks, a chunk announcing `tool_calls[i].id/name` * - `content_block_delta` → `text_delta` → `delta.content`; `input_json_delta` → `tool_calls[i].function.arguments` * - `message_delta` → chunk with `finish_reason`; captures output usage * - `message_stop` → optional usage chunk (when `include_usage`), then `data: [DONE]` * - `error` → `data: {"error": …}` then `[DONE]` * - `ping`, `content_block_stop`, thinking deltas → ignored * * The stream is fault-tolerant: if the upstream closes without * `message_stop`, the finish chunk and `[DONE]` are still emitted so the * caller's SDK doesn't hang. */ export declare function translateAnthropicStreamToOpenAI(upstream: ReadableStream, originalModel: string, options?: { includeUsage?: boolean; }): AnthropicStreamTranslation; //# sourceMappingURL=wire-anthropic.d.ts.map