anthropic-messages.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. import { Effect, Schema } from "effect"
  2. import { Route } from "../route/client"
  3. import { Auth } from "../route/auth"
  4. import { Endpoint } from "../route/endpoint"
  5. import { Framing } from "../route/framing"
  6. import { Protocol } from "../route/protocol"
  7. import {
  8. Usage,
  9. type CacheHint,
  10. type FinishReason,
  11. type LLMEvent,
  12. type LLMRequest,
  13. type ProviderMetadata,
  14. type ToolCallPart,
  15. type ToolDefinition,
  16. type ToolResultPart,
  17. } from "../schema"
  18. import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
  19. import { ToolStream } from "./utils/tool-stream"
  20. const ADAPTER = "anthropic-messages"
  21. export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
  22. export const PATH = "/messages"
  23. // =============================================================================
  24. // Request Body Schema
  25. // =============================================================================
  26. const AnthropicCacheControl = Schema.Struct({ type: Schema.tag("ephemeral") })
  27. const AnthropicTextBlock = Schema.Struct({
  28. type: Schema.tag("text"),
  29. text: Schema.String,
  30. cache_control: Schema.optional(AnthropicCacheControl),
  31. })
  32. type AnthropicTextBlock = Schema.Schema.Type<typeof AnthropicTextBlock>
  33. const AnthropicThinkingBlock = Schema.Struct({
  34. type: Schema.tag("thinking"),
  35. thinking: Schema.String,
  36. signature: Schema.optional(Schema.String),
  37. cache_control: Schema.optional(AnthropicCacheControl),
  38. })
  39. const AnthropicToolUseBlock = Schema.Struct({
  40. type: Schema.tag("tool_use"),
  41. id: Schema.String,
  42. name: Schema.String,
  43. input: Schema.Unknown,
  44. cache_control: Schema.optional(AnthropicCacheControl),
  45. })
  46. type AnthropicToolUseBlock = Schema.Schema.Type<typeof AnthropicToolUseBlock>
  47. const AnthropicServerToolUseBlock = Schema.Struct({
  48. type: Schema.tag("server_tool_use"),
  49. id: Schema.String,
  50. name: Schema.String,
  51. input: Schema.Unknown,
  52. cache_control: Schema.optional(AnthropicCacheControl),
  53. })
  54. type AnthropicServerToolUseBlock = Schema.Schema.Type<typeof AnthropicServerToolUseBlock>
  55. // Server tool result blocks: web_search_tool_result, code_execution_tool_result,
  56. // and web_fetch_tool_result. The provider executes the tool and inlines the
  57. // structured result into the assistant turn — there is no client tool_result
  58. // round-trip. We round-trip the structured `content` payload as opaque JSON so
  59. // the next request can echo it back when continuing the conversation.
  60. const AnthropicServerToolResultType = Schema.Literals([
  61. "web_search_tool_result",
  62. "code_execution_tool_result",
  63. "web_fetch_tool_result",
  64. ])
  65. type AnthropicServerToolResultType = Schema.Schema.Type<typeof AnthropicServerToolResultType>
  66. const AnthropicServerToolResultBlock = Schema.Struct({
  67. type: AnthropicServerToolResultType,
  68. tool_use_id: Schema.String,
  69. content: Schema.Unknown,
  70. cache_control: Schema.optional(AnthropicCacheControl),
  71. })
  72. type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock>
  73. const AnthropicToolResultBlock = Schema.Struct({
  74. type: Schema.tag("tool_result"),
  75. tool_use_id: Schema.String,
  76. content: Schema.String,
  77. is_error: Schema.optional(Schema.Boolean),
  78. cache_control: Schema.optional(AnthropicCacheControl),
  79. })
  80. const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicToolResultBlock])
  81. const AnthropicAssistantBlock = Schema.Union([
  82. AnthropicTextBlock,
  83. AnthropicThinkingBlock,
  84. AnthropicToolUseBlock,
  85. AnthropicServerToolUseBlock,
  86. AnthropicServerToolResultBlock,
  87. ])
  88. type AnthropicAssistantBlock = Schema.Schema.Type<typeof AnthropicAssistantBlock>
  89. type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlock>
  90. const AnthropicMessage = Schema.Union([
  91. Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
  92. Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
  93. ]).pipe(Schema.toTaggedUnion("role"))
  94. type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
  95. const AnthropicTool = Schema.Struct({
  96. name: Schema.String,
  97. description: Schema.String,
  98. input_schema: JsonObject,
  99. cache_control: Schema.optional(AnthropicCacheControl),
  100. })
  101. type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
  102. const AnthropicToolChoice = Schema.Union([
  103. Schema.Struct({ type: Schema.Literals(["auto", "any"]) }),
  104. Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
  105. ])
  106. const AnthropicThinking = Schema.Struct({
  107. type: Schema.tag("enabled"),
  108. budget_tokens: Schema.Number,
  109. })
  110. const AnthropicBodyFields = {
  111. model: Schema.String,
  112. system: optionalArray(AnthropicTextBlock),
  113. messages: Schema.Array(AnthropicMessage),
  114. tools: optionalArray(AnthropicTool),
  115. tool_choice: Schema.optional(AnthropicToolChoice),
  116. stream: Schema.Literal(true),
  117. max_tokens: Schema.Number,
  118. temperature: Schema.optional(Schema.Number),
  119. top_p: Schema.optional(Schema.Number),
  120. top_k: Schema.optional(Schema.Number),
  121. stop_sequences: optionalArray(Schema.String),
  122. thinking: Schema.optional(AnthropicThinking),
  123. }
  124. const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
  125. export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
  126. const AnthropicUsage = Schema.Struct({
  127. input_tokens: Schema.optional(Schema.Number),
  128. output_tokens: Schema.optional(Schema.Number),
  129. cache_creation_input_tokens: optionalNull(Schema.Number),
  130. cache_read_input_tokens: optionalNull(Schema.Number),
  131. })
  132. type AnthropicUsage = Schema.Schema.Type<typeof AnthropicUsage>
  133. const AnthropicStreamBlock = Schema.Struct({
  134. type: Schema.String,
  135. id: Schema.optional(Schema.String),
  136. name: Schema.optional(Schema.String),
  137. text: Schema.optional(Schema.String),
  138. thinking: Schema.optional(Schema.String),
  139. signature: Schema.optional(Schema.String),
  140. input: Schema.optional(Schema.Unknown),
  141. // *_tool_result blocks arrive whole as content_block_start (no streaming
  142. // delta) with the structured payload in `content` and the originating
  143. // server_tool_use id in `tool_use_id`.
  144. tool_use_id: Schema.optional(Schema.String),
  145. content: Schema.optional(Schema.Unknown),
  146. })
  147. const AnthropicStreamDelta = Schema.Struct({
  148. type: Schema.optional(Schema.String),
  149. text: Schema.optional(Schema.String),
  150. thinking: Schema.optional(Schema.String),
  151. partial_json: Schema.optional(Schema.String),
  152. signature: Schema.optional(Schema.String),
  153. stop_reason: optionalNull(Schema.String),
  154. stop_sequence: optionalNull(Schema.String),
  155. })
  156. const AnthropicEvent = Schema.Struct({
  157. type: Schema.String,
  158. index: Schema.optional(Schema.Number),
  159. message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
  160. content_block: Schema.optional(AnthropicStreamBlock),
  161. delta: Schema.optional(AnthropicStreamDelta),
  162. usage: Schema.optional(AnthropicUsage),
  163. error: Schema.optional(Schema.Struct({ type: Schema.String, message: Schema.String })),
  164. })
  165. type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
  166. interface ParserState {
  167. readonly tools: ToolStream.State<number>
  168. readonly usage?: Usage
  169. }
  170. const invalid = ProviderShared.invalidRequest
  171. // =============================================================================
  172. // Request Lowering
  173. // =============================================================================
  174. const cacheControl = (cache: CacheHint | undefined) =>
  175. cache?.type === "ephemeral" ? { type: "ephemeral" as const } : undefined
  176. const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata })
  177. const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => {
  178. const anthropic = metadata?.anthropic
  179. if (!ProviderShared.isRecord(anthropic)) return undefined
  180. return typeof anthropic.signature === "string" ? anthropic.signature : undefined
  181. }
  182. const lowerTool = (tool: ToolDefinition): AnthropicTool => ({
  183. name: tool.name,
  184. description: tool.description,
  185. input_schema: tool.inputSchema,
  186. })
  187. const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
  188. ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
  189. auto: () => ({ type: "auto" as const }),
  190. none: () => undefined,
  191. required: () => ({ type: "any" as const }),
  192. tool: (name) => ({ type: "tool" as const, name }),
  193. })
  194. const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({
  195. type: "tool_use",
  196. id: part.id,
  197. name: part.name,
  198. input: part.input,
  199. })
  200. const lowerServerToolCall = (part: ToolCallPart): AnthropicServerToolUseBlock => ({
  201. type: "server_tool_use",
  202. id: part.id,
  203. name: part.name,
  204. input: part.input,
  205. })
  206. // Server tool result blocks are typed by name. Anthropic ships three today;
  207. // extend this list when new server tools land. The block content is the
  208. // structured payload returned by the provider, which we round-trip as-is.
  209. const serverToolResultType = (name: string): AnthropicServerToolResultType | undefined => {
  210. if (name === "web_search") return "web_search_tool_result"
  211. if (name === "code_execution") return "code_execution_tool_result"
  212. if (name === "web_fetch") return "web_fetch_tool_result"
  213. return undefined
  214. }
  215. const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part: ToolResultPart) {
  216. const wireType = serverToolResultType(part.name)
  217. if (!wireType)
  218. return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
  219. return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
  220. })
  221. const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (request: LLMRequest) {
  222. const messages: AnthropicMessage[] = []
  223. for (const message of request.messages) {
  224. if (message.role === "user") {
  225. const content: AnthropicTextBlock[] = []
  226. for (const part of message.content) {
  227. if (!ProviderShared.supportsContent(part, ["text"]))
  228. return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text"])
  229. content.push({ type: "text", text: part.text, cache_control: cacheControl(part.cache) })
  230. }
  231. messages.push({ role: "user", content })
  232. continue
  233. }
  234. if (message.role === "assistant") {
  235. const content: AnthropicAssistantBlock[] = []
  236. for (const part of message.content) {
  237. if (part.type === "text") {
  238. content.push({ type: "text", text: part.text, cache_control: cacheControl(part.cache) })
  239. continue
  240. }
  241. if (part.type === "reasoning") {
  242. content.push({
  243. type: "thinking",
  244. thinking: part.text,
  245. signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata),
  246. })
  247. continue
  248. }
  249. if (part.type === "tool-call") {
  250. content.push(part.providerExecuted ? lowerServerToolCall(part) : lowerToolCall(part))
  251. continue
  252. }
  253. if (part.type === "tool-result" && part.providerExecuted) {
  254. content.push(yield* lowerServerToolResult(part))
  255. continue
  256. }
  257. return yield* invalid(
  258. `Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
  259. )
  260. }
  261. messages.push({ role: "assistant", content })
  262. continue
  263. }
  264. const content: AnthropicToolResultBlock[] = []
  265. for (const part of message.content) {
  266. if (!ProviderShared.supportsContent(part, ["tool-result"]))
  267. return yield* ProviderShared.unsupportedContent("Anthropic Messages", "tool", ["tool-result"])
  268. content.push({
  269. type: "tool_result",
  270. tool_use_id: part.id,
  271. content: ProviderShared.toolResultText(part),
  272. is_error: part.result.type === "error" ? true : undefined,
  273. })
  274. }
  275. messages.push({ role: "user", content })
  276. }
  277. return messages
  278. })
  279. const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
  280. const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
  281. const thinking = anthropicOptions(request)?.thinking
  282. if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
  283. const budget =
  284. typeof thinking.budgetTokens === "number"
  285. ? thinking.budgetTokens
  286. : typeof thinking.budget_tokens === "number"
  287. ? thinking.budget_tokens
  288. : undefined
  289. if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
  290. return { type: "enabled" as const, budget_tokens: budget }
  291. })
  292. const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
  293. const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
  294. const generation = request.generation
  295. return {
  296. model: request.model.id,
  297. system:
  298. request.system.length === 0
  299. ? undefined
  300. : request.system.map((part) => ({
  301. type: "text" as const,
  302. text: part.text,
  303. cache_control: cacheControl(part.cache),
  304. })),
  305. messages: yield* lowerMessages(request),
  306. tools: request.tools.length === 0 || request.toolChoice?.type === "none" ? undefined : request.tools.map(lowerTool),
  307. tool_choice: toolChoice,
  308. stream: true as const,
  309. max_tokens: generation?.maxTokens ?? request.model.limits.output ?? 4096,
  310. temperature: generation?.temperature,
  311. top_p: generation?.topP,
  312. top_k: generation?.topK,
  313. stop_sequences: generation?.stop,
  314. thinking: yield* lowerThinking(request),
  315. }
  316. })
  317. // =============================================================================
  318. // Stream Parsing
  319. // =============================================================================
  320. const mapFinishReason = (reason: string | null | undefined): FinishReason => {
  321. if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop"
  322. if (reason === "max_tokens") return "length"
  323. if (reason === "tool_use") return "tool-calls"
  324. if (reason === "refusal") return "content-filter"
  325. return "unknown"
  326. }
  327. const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => {
  328. if (!usage) return undefined
  329. return new Usage({
  330. inputTokens: usage.input_tokens,
  331. outputTokens: usage.output_tokens,
  332. cacheReadInputTokens: usage.cache_read_input_tokens ?? undefined,
  333. cacheWriteInputTokens: usage.cache_creation_input_tokens ?? undefined,
  334. totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, undefined),
  335. native: usage,
  336. })
  337. }
  338. // Anthropic emits usage on `message_start` and again on `message_delta` — the
  339. // final delta carries the authoritative totals. Right-biased merge: each
  340. // field prefers `right` when defined, falls back to `left`. `totalTokens` is
  341. // recomputed from the merged input/output to stay consistent.
  342. const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
  343. if (!left) return right
  344. if (!right) return left
  345. const inputTokens = right.inputTokens ?? left.inputTokens
  346. const outputTokens = right.outputTokens ?? left.outputTokens
  347. return new Usage({
  348. inputTokens,
  349. outputTokens,
  350. cacheReadInputTokens: right.cacheReadInputTokens ?? left.cacheReadInputTokens,
  351. cacheWriteInputTokens: right.cacheWriteInputTokens ?? left.cacheWriteInputTokens,
  352. totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
  353. native: { ...left.native, ...right.native },
  354. })
  355. }
  356. // Server tool result blocks come whole in `content_block_start` (no streaming
  357. // delta sequence). We convert the payload to a `tool-result` event with
  358. // `providerExecuted: true`. The runtime appends it to the assistant message
  359. // for round-trip; downstream consumers can inspect `result.value` for the
  360. // structured payload.
  361. const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> = {
  362. web_search_tool_result: "web_search",
  363. code_execution_tool_result: "code_execution",
  364. web_fetch_tool_result: "web_fetch",
  365. }
  366. const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
  367. const serverToolResultEvent = (block: NonNullable<AnthropicEvent["content_block"]>): LLMEvent | undefined => {
  368. if (!block.type || !isServerToolResultType(block.type)) return undefined
  369. const errorPayload =
  370. typeof block.content === "object" && block.content !== null && "type" in block.content
  371. ? String((block.content as Record<string, unknown>).type)
  372. : ""
  373. const isError = errorPayload.endsWith("_tool_result_error")
  374. return {
  375. type: "tool-result",
  376. id: block.tool_use_id ?? "",
  377. name: SERVER_TOOL_RESULT_NAMES[block.type],
  378. result: isError ? { type: "error", value: block.content } : { type: "json", value: block.content },
  379. providerExecuted: true,
  380. providerMetadata: anthropicMetadata({ blockType: block.type }),
  381. }
  382. }
  383. type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
  384. const NO_EVENTS: StepResult["1"] = []
  385. const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
  386. const usage = mapUsage(event.message?.usage)
  387. return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS]
  388. }
  389. const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepResult => {
  390. const block = event.content_block
  391. if (!block) return [state, NO_EVENTS]
  392. if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
  393. return [
  394. {
  395. ...state,
  396. tools: ToolStream.start(state.tools, event.index, {
  397. id: block.id ?? String(event.index),
  398. name: block.name ?? "",
  399. providerExecuted: block.type === "server_tool_use",
  400. }),
  401. },
  402. NO_EVENTS,
  403. ]
  404. }
  405. if (block.type === "text" && block.text) {
  406. return [state, [{ type: "text-delta", text: block.text }]]
  407. }
  408. if (block.type === "thinking" && block.thinking) {
  409. return [
  410. state,
  411. [
  412. {
  413. type: "reasoning-delta",
  414. text: block.thinking,
  415. ...(block.signature ? { providerMetadata: anthropicMetadata({ signature: block.signature }) } : {}),
  416. },
  417. ],
  418. ]
  419. }
  420. const result = serverToolResultEvent(block)
  421. return [state, result ? [result] : NO_EVENTS]
  422. }
  423. const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
  424. state: ParserState,
  425. event: AnthropicEvent,
  426. ) {
  427. const delta = event.delta
  428. if (delta?.type === "text_delta" && delta.text) {
  429. return [state, [{ type: "text-delta", text: delta.text }]] satisfies StepResult
  430. }
  431. if (delta?.type === "thinking_delta" && delta.thinking) {
  432. return [state, [{ type: "reasoning-delta", text: delta.thinking }]] satisfies StepResult
  433. }
  434. if (delta?.type === "signature_delta" && delta.signature) {
  435. return [
  436. state,
  437. [{ type: "reasoning-delta", text: "", providerMetadata: anthropicMetadata({ signature: delta.signature }) }],
  438. ] satisfies StepResult
  439. }
  440. if (delta?.type === "input_json_delta" && event.index !== undefined) {
  441. if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult
  442. const result = ToolStream.appendExisting(
  443. ADAPTER,
  444. state.tools,
  445. event.index,
  446. delta.partial_json,
  447. "Anthropic Messages tool argument delta is missing its tool call",
  448. )
  449. if (ToolStream.isError(result)) return yield* result
  450. return [{ ...state, tools: result.tools }, result.event ? [result.event] : NO_EVENTS] satisfies StepResult
  451. }
  452. return [state, NO_EVENTS] satisfies StepResult
  453. })
  454. const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* (
  455. state: ParserState,
  456. event: AnthropicEvent,
  457. ) {
  458. if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult
  459. const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
  460. return [{ ...state, tools: result.tools }, result.event ? [result.event] : NO_EVENTS] satisfies StepResult
  461. })
  462. const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
  463. const usage = mergeUsage(state.usage, mapUsage(event.usage))
  464. return [
  465. { ...state, usage },
  466. [
  467. {
  468. type: "request-finish",
  469. reason: mapFinishReason(event.delta?.stop_reason),
  470. usage,
  471. ...(event.delta?.stop_sequence
  472. ? { providerMetadata: anthropicMetadata({ stopSequence: event.delta.stop_sequence }) }
  473. : {}),
  474. },
  475. ],
  476. ]
  477. }
  478. const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
  479. state,
  480. [{ type: "provider-error", message: event.error?.message ?? "Anthropic Messages stream error" }],
  481. ]
  482. const step = (state: ParserState, event: AnthropicEvent) => {
  483. if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event))
  484. if (event.type === "content_block_start") return Effect.succeed(onContentBlockStart(state, event))
  485. if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
  486. if (event.type === "content_block_stop") return onContentBlockStop(state, event)
  487. if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
  488. if (event.type === "error") return Effect.succeed(onError(state, event))
  489. return Effect.succeed<StepResult>([state, NO_EVENTS])
  490. }
  491. // =============================================================================
  492. // Protocol And Anthropic Route
  493. // =============================================================================
  494. /**
  495. * The Anthropic Messages protocol — request body construction, body schema,
  496. * and the streaming-event state machine. Used by native Anthropic Cloud and
  497. * (once registered) Vertex Anthropic / Bedrock-hosted Anthropic passthrough.
  498. */
  499. export const protocol = Protocol.make({
  500. id: ADAPTER,
  501. body: {
  502. schema: AnthropicMessagesBody,
  503. from: fromRequest,
  504. },
  505. stream: {
  506. event: Protocol.jsonEvent(AnthropicEvent),
  507. initial: () => ({ tools: ToolStream.empty<number>() }),
  508. step,
  509. },
  510. })
  511. export const route = Route.make({
  512. id: ADAPTER,
  513. protocol,
  514. endpoint: Endpoint.path(PATH),
  515. auth: Auth.apiKeyHeader("x-api-key"),
  516. framing: Framing.sse,
  517. headers: () => ({ "anthropic-version": "2023-06-01" }),
  518. })
  519. // =============================================================================
  520. // Model Helper
  521. // =============================================================================
  522. export const model = Route.model(route, {
  523. provider: "anthropic",
  524. baseURL: DEFAULT_BASE_URL,
  525. })
  526. export * as AnthropicMessages from "./anthropic-messages"