@velum-labs/routekit-gateway 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/acp/registry.js +1 -1
  2. package/dist/adapters/anthropic-codec.d.ts +1 -1
  3. package/dist/adapters/anthropic-codec.js +15 -4
  4. package/dist/adapters/anthropic-stream.js +29 -12
  5. package/dist/adapters/anthropic-wire.d.ts +5 -1
  6. package/dist/adapters/anthropic.d.ts +1 -1
  7. package/dist/adapters/anthropic.js +5 -3
  8. package/dist/adapters/cursor.js +2 -2
  9. package/dist/adapters/responses-codec.d.ts +12 -1
  10. package/dist/adapters/responses-codec.js +37 -9
  11. package/dist/adapters/responses-stream.d.ts +1 -1
  12. package/dist/adapters/responses-stream.js +4 -35
  13. package/dist/adapters/responses-wire.d.ts +2 -1
  14. package/dist/adapters/responses.d.ts +1 -1
  15. package/dist/adapters/responses.js +9 -8
  16. package/dist/adapters/server-tool-loop.d.ts +2 -2
  17. package/dist/adapters/server-tool-loop.js +5 -5
  18. package/dist/adapters/server-tool-transcript.d.ts +1 -1
  19. package/dist/adapters/server-tool-transcript.js +1 -1
  20. package/dist/adapters/standalone-listener.d.ts +6 -0
  21. package/dist/adapters/standalone-listener.js +26 -0
  22. package/dist/adapters/web-search.js +1 -1
  23. package/dist/annotations/anthropic-stream.d.ts +4 -0
  24. package/dist/annotations/anthropic-stream.js +45 -0
  25. package/dist/annotations/chat-stream.d.ts +4 -0
  26. package/dist/annotations/chat-stream.js +53 -0
  27. package/dist/annotations/codec.d.ts +23 -0
  28. package/dist/annotations/codec.js +335 -0
  29. package/dist/annotations/protocol.d.ts +77 -0
  30. package/dist/annotations/protocol.js +26 -0
  31. package/dist/annotations/response.d.ts +6 -0
  32. package/dist/annotations/response.js +76 -0
  33. package/dist/annotations/responses-stream.d.ts +7 -0
  34. package/dist/annotations/responses-stream.js +137 -0
  35. package/dist/annotations/sse-transform.d.ts +7 -0
  36. package/dist/annotations/sse-transform.js +148 -0
  37. package/dist/annotations/wire.d.ts +22 -0
  38. package/dist/annotations/wire.js +124 -0
  39. package/dist/catalog-service.d.ts +10 -1
  40. package/dist/catalog-service.js +101 -23
  41. package/dist/effect-api.d.ts +2 -1
  42. package/dist/effect-api.js +1 -1
  43. package/dist/endpoints/anthropic-messages-endpoint.d.ts +2 -1
  44. package/dist/endpoints/anthropic-messages-endpoint.js +46 -49
  45. package/dist/endpoints/chat-endpoint.d.ts +1 -1
  46. package/dist/endpoints/chat-endpoint.js +24 -38
  47. package/dist/endpoints/endpoint-module.d.ts +1 -1
  48. package/dist/endpoints/endpoint-module.js +1 -1
  49. package/dist/endpoints/models-endpoint.d.ts +1 -1
  50. package/dist/endpoints/models-endpoint.js +2 -2
  51. package/dist/endpoints/request-preparation.d.ts +15 -0
  52. package/dist/endpoints/request-preparation.js +40 -0
  53. package/dist/endpoints/responses-endpoint.d.ts +2 -1
  54. package/dist/endpoints/responses-endpoint.js +31 -53
  55. package/dist/fetch-api.d.ts +11 -0
  56. package/dist/fetch-api.js +7 -0
  57. package/dist/gateway-service.d.ts +29 -1
  58. package/dist/gateway-service.js +79 -61
  59. package/dist/http/app.d.ts +9 -1
  60. package/dist/http/app.js +84 -21
  61. package/dist/http/auth.d.ts +6 -0
  62. package/dist/http/auth.js +53 -1
  63. package/dist/http/errors.d.ts +0 -11
  64. package/dist/http/errors.js +10 -30
  65. package/dist/http/request.d.ts +2 -2
  66. package/dist/http/request.js +21 -26
  67. package/dist/index.d.ts +24 -38
  68. package/dist/index.js +12 -20
  69. package/dist/model-call-service.d.ts +22 -7
  70. package/dist/model-call-service.js +327 -79
  71. package/dist/observability/cost.d.ts +20 -2
  72. package/dist/observability/cost.js +97 -19
  73. package/dist/observability/provenance.d.ts +5 -2
  74. package/dist/observability/provenance.js +376 -21
  75. package/dist/observability/routing-cost-episode.d.ts +11 -0
  76. package/dist/observability/routing-cost-episode.js +29 -0
  77. package/dist/providers/codex-model-selection.d.ts +4 -2
  78. package/dist/providers/codex-model-selection.js +43 -34
  79. package/dist/providers/routekit-gateway-source.d.ts +20 -0
  80. package/dist/providers/routekit-gateway-source.js +85 -0
  81. package/dist/routing/classifier.d.ts +8 -0
  82. package/dist/routing/classifier.js +124 -15
  83. package/dist/routing/compositional.d.ts +1 -1
  84. package/dist/routing/core.d.ts +8 -5
  85. package/dist/routing/core.js +21 -4
  86. package/dist/routing/eval-policy.d.ts +1 -0
  87. package/dist/routing/eval-policy.js +10 -5
  88. package/dist/routing/requirements.d.ts +1 -1
  89. package/dist/routing/router.d.ts +30 -10
  90. package/dist/routing/router.js +195 -70
  91. package/dist/routing-api.d.ts +3 -2
  92. package/dist/routing-api.js +2 -1
  93. package/dist/server-api.d.ts +2 -2
  94. package/dist/server-api.js +1 -1
  95. package/dist/services/edge-annotations/service.d.ts +30 -0
  96. package/dist/services/edge-annotations/service.js +33 -0
  97. package/dist/switching-proxy.d.ts +7 -1
  98. package/dist/switching-proxy.js +52 -27
  99. package/dist/test/anthropic-stop-reason.test.js +1 -1
  100. package/dist/test/anthropic.test.js +304 -3
  101. package/dist/test/chat.test.js +109 -3
  102. package/dist/test/cluster-gateway-fixture.d.ts +10 -0
  103. package/dist/test/cluster-gateway-fixture.js +49 -0
  104. package/dist/test/cluster-gateway.test.js +133 -0
  105. package/dist/test/codex-content.test.d.ts +1 -0
  106. package/dist/test/codex-content.test.js +119 -0
  107. package/dist/test/codex-model-selection.test.js +144 -123
  108. package/dist/test/compositional-routing.test.js +45 -0
  109. package/dist/test/cost.test.js +60 -0
  110. package/dist/test/cursor.test.js +2 -2
  111. package/dist/test/drain.test.js +1 -1
  112. package/dist/test/dropped.test.js +1 -1
  113. package/dist/test/edge-annotation-codec.test.d.ts +1 -0
  114. package/dist/test/edge-annotation-codec.test.js +220 -0
  115. package/dist/test/edge-annotation-egress.test.d.ts +1 -0
  116. package/dist/test/edge-annotation-egress.test.js +300 -0
  117. package/dist/test/edge-annotation-ingress.test.d.ts +1 -0
  118. package/dist/test/edge-annotation-ingress.test.js +284 -0
  119. package/dist/test/edge-annotation-observation.test.d.ts +1 -0
  120. package/dist/test/edge-annotation-observation.test.js +100 -0
  121. package/dist/test/edge-annotation-plumbing.test.d.ts +1 -0
  122. package/dist/test/edge-annotation-plumbing.test.js +64 -0
  123. package/dist/test/effect-gateway.test.js +1 -1
  124. package/dist/test/endpoint-modules.test.js +1 -1
  125. package/dist/test/fetch-host.test.d.ts +1 -0
  126. package/dist/test/fetch-host.test.js +282 -0
  127. package/dist/test/foundry-provider-exposure.test.d.ts +1 -0
  128. package/dist/test/foundry-provider-exposure.test.js +203 -0
  129. package/dist/test/http-submission-server-tools.test.d.ts +1 -0
  130. package/dist/test/http-submission-server-tools.test.js +199 -0
  131. package/dist/test/openai-reasoning.test.js +5 -26
  132. package/dist/test/openai-responses-wire.test.js +30 -1
  133. package/dist/test/provenance-stream-errors.test.d.ts +1 -0
  134. package/dist/test/provenance-stream-errors.test.js +78 -0
  135. package/dist/test/provenance.test.js +413 -1
  136. package/dist/test/provider-backends-fixtures.d.ts +1 -1
  137. package/dist/test/provider-google-codex.test.js +55 -11
  138. package/dist/test/provider-protocol.test.js +69 -1
  139. package/dist/test/provider-source-fixture.d.ts +1 -2
  140. package/dist/test/provider-source-fixture.js +1 -4
  141. package/dist/test/provider-streaming.test.js +24 -5
  142. package/dist/test/provider-validation-anthropic.test.js +82 -23
  143. package/dist/test/published-catalog.test.d.ts +1 -0
  144. package/dist/test/published-catalog.test.js +196 -0
  145. package/dist/test/request-classifier.test.js +71 -1
  146. package/dist/test/responses-conversion.test.js +2 -2
  147. package/dist/test/responses-fixtures.d.ts +1 -1
  148. package/dist/test/responses-fixtures.js +2 -2
  149. package/dist/test/responses-reasoning.test.js +151 -7
  150. package/dist/test/responses-streaming-catalog.test.js +254 -2
  151. package/dist/test/responses-tools.test.js +2 -2
  152. package/dist/test/routed-backend.test.js +28 -3
  153. package/dist/test/routekit-gateway-source.test.d.ts +1 -0
  154. package/dist/test/routekit-gateway-source.test.js +74 -0
  155. package/dist/test/router.test.js +95 -32
  156. package/dist/test/server-resilience.test.js +2 -2
  157. package/dist/test/sse-codec.test.js +2 -2
  158. package/dist/test/standalone-listener.test.d.ts +1 -0
  159. package/dist/test/standalone-listener.test.js +43 -0
  160. package/dist/test/switching-proxy-retire.test.js +7 -2
  161. package/dist/test/upstream-diagnostics.test.d.ts +1 -0
  162. package/dist/test/upstream-diagnostics.test.js +155 -0
  163. package/dist/test/web-search-loop.test.js +4 -4
  164. package/package.json +13 -14
  165. package/dist/adapters/dropped.d.ts +0 -10
  166. package/dist/adapters/dropped.js +0 -24
  167. package/dist/adapters/openai-chat-wire.d.ts +0 -153
  168. package/dist/adapters/openai-chat-wire.js +0 -916
  169. package/dist/adapters/openai-responses-wire.d.ts +0 -42
  170. package/dist/adapters/openai-responses-wire.js +0 -352
  171. package/dist/capacity-pool.d.ts +0 -2
  172. package/dist/capacity-pool.js +0 -1
  173. package/dist/effect/gateway.d.ts +0 -6
  174. package/dist/effect/gateway.js +0 -16
  175. package/dist/http/response.d.ts +0 -5
  176. package/dist/http/response.js +0 -32
  177. package/dist/protocol-api.d.ts +0 -2
  178. package/dist/protocol-api.js +0 -1
  179. package/dist/providers/anthropic-backend.d.ts +0 -11
  180. package/dist/providers/anthropic-backend.js +0 -52
  181. package/dist/providers/anthropic-codec.d.ts +0 -15
  182. package/dist/providers/anthropic-codec.js +0 -678
  183. package/dist/providers/auth.d.ts +0 -6
  184. package/dist/providers/auth.js +0 -51
  185. package/dist/providers/backend-core.d.ts +0 -81
  186. package/dist/providers/backend-core.js +0 -135
  187. package/dist/providers/backend-factory.d.ts +0 -8
  188. package/dist/providers/backend-factory.js +0 -19
  189. package/dist/providers/backend.d.ts +0 -139
  190. package/dist/providers/backend.js +0 -129
  191. package/dist/providers/backends.d.ts +0 -5
  192. package/dist/providers/backends.js +0 -4
  193. package/dist/providers/bedrock-codec.d.ts +0 -11
  194. package/dist/providers/bedrock-codec.js +0 -484
  195. package/dist/providers/bedrock-source.d.ts +0 -37
  196. package/dist/providers/bedrock-source.js +0 -426
  197. package/dist/providers/codex-responses-backend.d.ts +0 -15
  198. package/dist/providers/codex-responses-backend.js +0 -120
  199. package/dist/providers/codex-responses-codec.d.ts +0 -43
  200. package/dist/providers/codex-responses-codec.js +0 -497
  201. package/dist/providers/google-backend.d.ts +0 -11
  202. package/dist/providers/google-backend.js +0 -48
  203. package/dist/providers/google-codec.d.ts +0 -28
  204. package/dist/providers/google-codec.js +0 -312
  205. package/dist/providers/openai-backend.d.ts +0 -41
  206. package/dist/providers/openai-backend.js +0 -176
  207. package/dist/providers/openai-reasoning.d.ts +0 -9
  208. package/dist/providers/openai-reasoning.js +0 -29
  209. package/dist/providers/protocol.d.ts +0 -56
  210. package/dist/providers/protocol.js +0 -368
  211. package/dist/providers/source.d.ts +0 -19
  212. package/dist/providers/source.js +0 -81
  213. package/dist/providers/types.d.ts +0 -44
  214. package/dist/providers/types.js +0 -2
  215. package/dist/sse/chat-assembler.d.ts +0 -42
  216. package/dist/sse/chat-assembler.js +0 -248
  217. package/dist/sse/parse.d.ts +0 -2
  218. package/dist/sse/parse.js +0 -1
  219. package/dist/test/bedrock-source.test.js +0 -786
  220. /package/dist/test/{bedrock-source.test.d.ts → cluster-gateway.test.d.ts} +0 -0
@@ -10,7 +10,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
10
10
  import { join, resolve } from "node:path";
11
11
  import { executeWebRequest, RouteKitFailure, routeKitError, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
12
12
  import { Effect } from "effect";
13
- import { gatewayTry } from "../effect/gateway.js";
13
+ import { gatewayTry } from "@velum-labs/routekit-provider-core";
14
14
  export const ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
15
15
  function normalizeRegistry(raw) {
16
16
  if (typeof raw !== "object" || raw === null) {
@@ -4,7 +4,7 @@
4
4
  * Streaming translation lives in `anthropic-stream.ts`; HTTP handling lives
5
5
  * in `anthropic.ts`.
6
6
  */
7
- import type { OpenAiChatResponse } from "../providers/protocol.js";
7
+ import type { OpenAiChatResponse } from "@velum-labs/routekit-provider-core";
8
8
  import type { AnthropicRequest } from "./anthropic-wire.js";
9
9
  import type { ExecutedSearch, ServerToolLoopEvent } from "./server-tool-loop.js";
10
10
  export type { AnthropicRequest } from "./anthropic-wire.js";
@@ -5,9 +5,9 @@
5
5
  * in `anthropic.ts`.
6
6
  */
7
7
  import { estimateTokens, randomId } from "@velum-labs/routekit-runtime/timing";
8
- import { decodeToolResult } from "../providers/protocol.js";
9
- import { droppedField } from "./dropped.js";
10
- import { anthropicReasoningDetailsOf, anthropicReasoningExtension, attachAnthropicMessageContent, attachAnthropicRequestMetadata, attachReasoningSelection, attachReasoningSelectionError, reasoningIndex } from "./openai-chat-wire.js";
8
+ import { decodeToolResult } from "@velum-labs/routekit-provider-core";
9
+ import { droppedField } from "@velum-labs/routekit-provider-core";
10
+ import { anthropicReasoningDetailsOf, anthropicReasoningExtension, attachAnthropicMessageContent, attachAnthropicRequestMetadata, attachReasoningSelection, attachReasoningSelectionError, reasoningIndex } from "@velum-labs/routekit-provider-core";
11
11
  /**
12
12
  * Whether an Anthropic tool is *server-executed* (run by Anthropic's backend,
13
13
  * e.g. `web_search_20250305` / `code_execution_*`). Nothing behind this gateway
@@ -329,9 +329,20 @@ export function anthropicToChat(body, backendModel, options = {}) {
329
329
  });
330
330
  }
331
331
  }
332
+ // Claude Code can retain its global effort preference when the process
333
+ // disables thinking. Disabled thinking wins; forwarding the stale effort
334
+ // would make the canonical request metadata contradictory.
335
+ const outputConfig = body.thinking?.type === "disabled" && body.output_config != null
336
+ ? Object.fromEntries(Object.entries(body.output_config).filter(([key]) => key !== "effort"))
337
+ : body.output_config;
332
338
  const metadata = {
339
+ ...(body.service_tier !== undefined ? { service_tier: body.service_tier } : {}),
340
+ ...(body.speed !== undefined ? { speed: body.speed } : {}),
333
341
  ...(body.thinking != null ? { thinking: body.thinking } : {}),
334
- ...(body.output_config !== undefined ? { output_config: body.output_config } : {})
342
+ ...(outputConfig !== undefined &&
343
+ (outputConfig === null || Object.keys(outputConfig).length > 0)
344
+ ? { output_config: outputConfig }
345
+ : {})
335
346
  };
336
347
  if (Object.keys(metadata).length > 0) {
337
348
  attachAnthropicRequestMetadata(chat, metadata);
@@ -4,10 +4,10 @@
4
4
  */
5
5
  import { randomId } from "@velum-labs/routekit-runtime/timing";
6
6
  import { StreamPump } from "@velum-labs/routekit-runtime/sse";
7
- import { decodeOpenAiChatSseEvent } from "../providers/protocol.js";
8
- import { SseParseError } from "../sse/parse.js";
7
+ import { decodeOpenAiChatSseEvent } from "@velum-labs/routekit-provider-core";
8
+ import { SseParseError } from "@velum-labs/routekit-provider-core";
9
9
  import { mapStopReason } from "./anthropic-codec.js";
10
- import { anthropicReasoningDetailsOf, anthropicReasoningExtension } from "./openai-chat-wire.js";
10
+ import { anthropicReasoningDetailsOf, anthropicReasoningExtension } from "@velum-labs/routekit-provider-core";
11
11
  import { serverToolMarkerOf } from "./server-tool-loop.js";
12
12
  import { unwrapUpstreamError } from "./upstream-error.js";
13
13
  const ENCODER = new TextEncoder();
@@ -37,7 +37,9 @@ export function openAiSseToAnthropic(upstream, model) {
37
37
  nextIndex: 0,
38
38
  finished: false,
39
39
  inputTokens: undefined,
40
- outputTokens: undefined
40
+ outputTokens: undefined,
41
+ stopReason: undefined,
42
+ stopSequence: null
41
43
  };
42
44
  const ensureStarted = (controller) => {
43
45
  if (state.started)
@@ -292,7 +294,7 @@ export function openAiSseToAnthropic(upstream, model) {
292
294
  return;
293
295
  }
294
296
  const choice = chunk.choices?.[0];
295
- if (choice === undefined) {
297
+ if (choice === undefined || state.stopReason !== undefined) {
296
298
  if (chunk.usage?.inputTokens !== undefined)
297
299
  state.inputTokens = chunk.usage.inputTokens;
298
300
  if (chunk.usage?.outputTokens !== undefined)
@@ -387,18 +389,28 @@ export function openAiSseToAnthropic(upstream, model) {
387
389
  if (chunk.usage?.outputTokens !== undefined)
388
390
  state.outputTokens = chunk.usage.outputTokens;
389
391
  if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
390
- finalize(controller, typeof choice.anthropic_stop_reason === "string"
391
- ? choice.anthropic_stop_reason
392
- : mapStopReason(choice.finish_reason), typeof choice.anthropic_stop_sequence === "string" ? choice.anthropic_stop_sequence : null);
392
+ // OpenAI sends the usage-only chunk *after* the finish_reason when
393
+ // stream_options.include_usage is enabled. Defer the terminal Anthropic
394
+ // event so both the client and call accounting receive those counters.
395
+ state.stopReason =
396
+ typeof choice.anthropic_stop_reason === "string"
397
+ ? choice.anthropic_stop_reason
398
+ : mapStopReason(choice.finish_reason);
399
+ state.stopSequence =
400
+ typeof choice.anthropic_stop_sequence === "string" ? choice.anthropic_stop_sequence : null;
393
401
  }
394
402
  };
395
403
  const handleEvent = (controller, data) => {
396
- if (data.length === 0)
404
+ if (data.length === 0 || state.finished)
397
405
  return;
398
406
  if (data === "[DONE]") {
399
- // A `[DONE]` without a prior finish_reason is truncation, not a clean stop.
400
- if (!state.finished)
407
+ if (state.stopReason === undefined) {
408
+ // A `[DONE]` without a prior finish_reason is truncation, not a clean stop.
401
409
  finalizeTruncated(controller, "upstream sent [DONE] before a finish reason");
410
+ }
411
+ else {
412
+ finalize(controller, state.stopReason, state.stopSequence);
413
+ }
402
414
  return;
403
415
  }
404
416
  let chunk;
@@ -437,7 +449,12 @@ export function openAiSseToAnthropic(upstream, model) {
437
449
  },
438
450
  onEnd(controller) {
439
451
  if (!state.finished) {
440
- finalizeTruncated(controller, "upstream stream ended before a finish reason");
452
+ if (state.stopReason === undefined) {
453
+ finalizeTruncated(controller, "upstream stream ended before a finish reason");
454
+ }
455
+ else {
456
+ finalize(controller, state.stopReason, state.stopSequence);
457
+ }
441
458
  }
442
459
  }
443
460
  });
@@ -3,7 +3,7 @@
3
3
  * clients send to `/v1/messages`. Translation lives in `anthropic-codec.ts`;
4
4
  * HTTP handling lives in `anthropic.ts`.
5
5
  */
6
- import type { AnthropicThinkingConfig } from "./openai-chat-wire.js";
6
+ import type { AnthropicThinkingConfig } from "@velum-labs/routekit-provider-core";
7
7
  export type AnthropicTextBlock = {
8
8
  type: "text";
9
9
  text: string;
@@ -54,6 +54,10 @@ export type AnthropicRequest = {
54
54
  temperature?: number;
55
55
  top_p?: number;
56
56
  top_k?: number;
57
+ /** Anthropic capacity routing (`auto` or `standard_only`), not fast mode. */
58
+ service_tier?: string | null;
59
+ /** Anthropic fast-mode selector (`fast`); omitted means normal mode. */
60
+ speed?: string | null;
57
61
  thinking?: AnthropicThinking | null;
58
62
  output_config?: AnthropicOutputConfig | null;
59
63
  metadata?: Record<string, unknown> | null;
@@ -12,7 +12,7 @@
12
12
  * - `anthropic-models.ts` — Claude picker and `/v1/models`
13
13
  * - this module — HTTP handlers that wire the codec to a `Backend`
14
14
  */
15
- import type { Backend, BackendRequest, BackendRequestOptions } from "../providers/backend.js";
15
+ import type { Backend, BackendRequest, BackendRequestOptions } from "@velum-labs/routekit-provider-core";
16
16
  import type { AnthropicRequest } from "./anthropic-wire.js";
17
17
  export type { AnthropicTranslationOptions } from "./anthropic-codec.js";
18
18
  export { anthropicToChat, chatToAnthropicMessage, countTokensEstimate, mapStopReason } from "./anthropic-codec.js";
@@ -12,10 +12,9 @@
12
12
  * - `anthropic-models.ts` — Claude picker and `/v1/models`
13
13
  * - this module — HTTP handlers that wire the codec to a `Backend`
14
14
  */
15
+ import { decodeOpenAiChatResponse, gatewayTry, gatewayTryPromise, jsonResponse } from "@velum-labs/routekit-provider-core";
16
+ import { requireHttpSubmissionSupport } from "@velum-labs/routekit-runtime/effect";
15
17
  import { Effect } from "effect";
16
- import { gatewayTry, gatewayTryPromise } from "../effect/gateway.js";
17
- import { jsonResponse } from "../http/response.js";
18
- import { decodeOpenAiChatResponse } from "../providers/protocol.js";
19
18
  import { anthropicToChat, chatToAnthropicMessage, countTokensEstimate, isAnthropicServerTool, isAnthropicWebSearchTool, thinkingValidationError, WEB_SEARCH_TOOL_NAME } from "./anthropic-codec.js";
20
19
  import { openAiSseToAnthropic } from "./anthropic-stream.js";
21
20
  import { composeServerToolStream, runBufferedServerToolLoop } from "./server-tool-loop.js";
@@ -63,6 +62,9 @@ export function handleAnthropicMessages(backend, body, modelCallId, signal, back
63
62
  const declaresWebSearch = body.tools?.some(isAnthropicWebSearchTool) === true;
64
63
  const clientNameCollision = body.tools?.some((tool) => !isAnthropicServerTool(tool) && tool.name === WEB_SEARCH_TOOL_NAME) === true;
65
64
  const executor = declaresWebSearch && !clientNameCollision ? resolveWebSearchExecutor("anthropic") : undefined;
65
+ // Native provider support does not certify the server-tool loop's
66
+ // additional provider POSTs or its detached streaming runner.
67
+ yield* requireHttpSubmissionSupport(executor === undefined);
66
68
  const serverTools = executor !== undefined;
67
69
  const chat = anthropicToChat(body, upstreamModel, { serverTools });
68
70
  const requestOptions = {
@@ -15,8 +15,8 @@
15
15
  * boundary stays defensive without 4xx-ing on new shapes.
16
16
  */
17
17
  import { cursorModelName, EFFORT_QUALIFIED_MODEL_CODEC, enumerateModelEffortVariants, resolveModelEffortVariant, stripCursorNamespace } from "@velum-labs/routekit-contracts";
18
- import { droppedField } from "./dropped.js";
19
- import { attachReasoningSelection, attachReasoningSelectionError, hasExplicitReasoningSelection, reasoningSelectionErrorOf, reasoningSelectionOf } from "./openai-chat-wire.js";
18
+ import { droppedField } from "@velum-labs/routekit-provider-core";
19
+ import { attachReasoningSelection, attachReasoningSelectionError, hasExplicitReasoningSelection, reasoningSelectionErrorOf, reasoningSelectionOf } from "@velum-labs/routekit-provider-core";
20
20
  /** Fields copied through unchanged when present and non-null. */
21
21
  const PASSTHROUGH_FIELDS = [
22
22
  "model",
@@ -4,7 +4,7 @@
4
4
  * translation lives in `responses-stream.ts`; HTTP handling lives in
5
5
  * `responses.ts`.
6
6
  */
7
- import type { OpenAiChatResponse } from "../providers/protocol.js";
7
+ import type { OpenAiChatResponse } from "@velum-labs/routekit-provider-core";
8
8
  import type { ResponsesRequest, ResponsesTool } from "./responses-wire.js";
9
9
  import type { ExecutedSearch } from "./server-tool-loop.js";
10
10
  export type { ResponsesInputItem, ResponsesRequest } from "./responses-wire.js";
@@ -62,4 +62,15 @@ export type ResponsesTranslationOptions = {
62
62
  export declare function responsesToolRegistry(body: ResponsesRequest, options?: ResponsesTranslationOptions): ResponsesToolRegistry;
63
63
  /** Translate a Responses request to an OpenAI Chat Completions body. */
64
64
  export declare function responsesToChat(body: ResponsesRequest, backendModel: string | undefined, options?: ResponsesTranslationOptions): Record<string, unknown>;
65
+ /** The native item for a typed tool call (e.g. name "tool_search" ->
66
+ * `tool_search_call`). Codex dispatches typed tools by payload shape; a
67
+ * `function_call` under the same name fails with "unsupported payload". */
68
+ export declare function typedToolCallItem(input: {
69
+ name: string;
70
+ itemId: string;
71
+ callId: string;
72
+ args: string;
73
+ }): Record<string, unknown>;
74
+ /** The `query` from a web_search call's JSON arguments (raw args as fallback). */
75
+ export declare function webSearchQueryOf(args: string): string;
65
76
  export declare function chatToResponses(openai: OpenAiResponse, model: string, toolRegistry?: ResponsesToolRegistry, searches?: readonly ExecutedSearch[]): Record<string, unknown>;
@@ -5,8 +5,8 @@
5
5
  * `responses.ts`.
6
6
  */
7
7
  import { randomId } from "@velum-labs/routekit-runtime/timing";
8
- import { droppedField } from "./dropped.js";
9
- import { attachReasoningSelection, attachReasoningSelectionError, attachResponsesReasoningMetadata, hasExplicitReasoningSelection, reasoningSelectionErrorOf, reasoningSelectionOf, responsesReasoningItem, responsesReasoningMetadataOf } from "./openai-chat-wire.js";
8
+ import { droppedField } from "@velum-labs/routekit-provider-core";
9
+ import { attachReasoningSelection, attachReasoningSelectionError, attachResponsesReasoningMetadata, hasExplicitReasoningSelection, reasoningSelectionErrorOf, reasoningSelectionOf, responsesReasoningItem, responsesReasoningMetadataOf } from "@velum-labs/routekit-provider-core";
10
10
  import { chatUsageToResponses } from "./responses-usage.js";
11
11
  export class ResponsesTranslationError extends Error {
12
12
  code = "invalid_encrypted_reasoning_order";
@@ -53,11 +53,33 @@ function contentToText(content) {
53
53
  return content;
54
54
  return content.map(partText).join("");
55
55
  }
56
- function contentToParts(content) {
56
+ function contentToParts(content, destinationWireShape) {
57
57
  if (typeof content === "string")
58
58
  return content;
59
59
  const parts = [];
60
60
  for (const part of content) {
61
+ if (destinationWireShape === "openai-responses") {
62
+ // The Codex Chat bridge validates content before transport. Do not
63
+ // erase unsupported modalities before that codec can reject them.
64
+ if (part?.type === "input_image") {
65
+ parts.push({
66
+ type: "image_url",
67
+ image_url: {
68
+ url: part.image_url,
69
+ ...(part.detail !== undefined ? { detail: part.detail } : {})
70
+ }
71
+ });
72
+ }
73
+ else if (part?.type === "input_text" ||
74
+ part?.type === "output_text" ||
75
+ part?.type === "text") {
76
+ parts.push({ type: "text", text: part.text });
77
+ }
78
+ else {
79
+ parts.push(part);
80
+ }
81
+ continue;
82
+ }
61
83
  if (part.type === "input_image" && typeof part.image_url === "string") {
62
84
  parts.push({ type: "image_url", image_url: { url: part.image_url } });
63
85
  }
@@ -70,8 +92,8 @@ function contentToParts(content) {
70
92
  parts.push({ type: "text", text });
71
93
  }
72
94
  }
73
- if (parts.length === 1 && parts[0]?.type === "text") {
74
- return String(parts[0].text);
95
+ if (parts.length === 1 && parts[0]?.type === "text" && typeof parts[0].text === "string") {
96
+ return parts[0].text;
75
97
  }
76
98
  return parts;
77
99
  }
@@ -400,7 +422,11 @@ export function responsesToChat(body, backendModel, options = {}) {
400
422
  if (pendingEncryptedReasoning.length > 0)
401
423
  rejectPendingReasoning(item.type);
402
424
  const out = item;
403
- const content = typeof out.output === "string" ? out.output : JSON.stringify(out.output);
425
+ const content = options.destinationWireShape === "openai-responses" && Array.isArray(out.output)
426
+ ? contentToParts(out.output, options.destinationWireShape)
427
+ : typeof out.output === "string"
428
+ ? out.output
429
+ : JSON.stringify(out.output);
404
430
  messages.push({ role: "tool", tool_call_id: out.call_id, content });
405
431
  continue;
406
432
  }
@@ -430,7 +456,7 @@ export function responsesToChat(body, backendModel, options = {}) {
430
456
  }
431
457
  const chatMessage = {
432
458
  role,
433
- content: contentToParts(message.content)
459
+ content: contentToParts(message.content, options.destinationWireShape)
434
460
  };
435
461
  if (role === "assistant")
436
462
  attachPendingReasoning(chatMessage);
@@ -455,6 +481,8 @@ export function responsesToChat(body, backendModel, options = {}) {
455
481
  chat.top_p = body.top_p;
456
482
  if (typeof body.parallel_tool_calls === "boolean")
457
483
  chat.parallel_tool_calls = body.parallel_tool_calls;
484
+ if (typeof body.service_tier === "string")
485
+ chat.service_tier = body.service_tier;
458
486
  if (body.x_routekit !== undefined)
459
487
  chat.x_routekit = body.x_routekit;
460
488
  const originalSelectionError = reasoningSelectionErrorOf(body);
@@ -627,7 +655,7 @@ function typedToolArguments(args) {
627
655
  /** The native item for a typed tool call (e.g. name "tool_search" ->
628
656
  * `tool_search_call`). Codex dispatches typed tools by payload shape; a
629
657
  * `function_call` under the same name fails with "unsupported payload". */
630
- function typedToolCallItem(input) {
658
+ export function typedToolCallItem(input) {
631
659
  return {
632
660
  type: `${input.name}_call`,
633
661
  id: input.itemId,
@@ -638,7 +666,7 @@ function typedToolCallItem(input) {
638
666
  };
639
667
  }
640
668
  /** The `query` from a web_search call's JSON arguments (raw args as fallback). */
641
- function webSearchQueryOf(args) {
669
+ export function webSearchQueryOf(args) {
642
670
  try {
643
671
  const parsed = JSON.parse(args);
644
672
  if (typeof parsed.query === "string")
@@ -1,2 +1,2 @@
1
- import type { ResponsesToolRegistry } from "./responses-codec.js";
1
+ import { type ResponsesToolRegistry } from "./responses-codec.js";
2
2
  export declare function openAiSseToResponses(upstream: ReadableStream<Uint8Array>, model: string, toolRegistry?: ResponsesToolRegistry): ReadableStream<Uint8Array>;
@@ -1,32 +1,13 @@
1
1
  import { extensionValue } from "@velum-labs/routekit-contracts/protocol-ir";
2
2
  import { randomId } from "@velum-labs/routekit-runtime/timing";
3
3
  import { StreamPump } from "@velum-labs/routekit-runtime/sse";
4
- import { decodeOpenAiChatSseEvent } from "../providers/protocol.js";
5
- import { SseParseError } from "../sse/parse.js";
6
- import { responsesReasoningItem, responsesReasoningMetadataOf } from "./openai-chat-wire.js";
4
+ import { decodeOpenAiChatSseEvent } from "@velum-labs/routekit-provider-core";
5
+ import { SseParseError } from "@velum-labs/routekit-provider-core";
6
+ import { responsesReasoningItem, responsesReasoningMetadataOf } from "@velum-labs/routekit-provider-core";
7
+ import { typedToolCallItem, webSearchQueryOf } from "./responses-codec.js";
7
8
  import { chatUsageToResponses } from "./responses-usage.js";
8
9
  import { serverToolMarkerOf } from "./server-tool-loop.js";
9
10
  const ENCODER = new TextEncoder();
10
- function typedToolArguments(args) {
11
- if (args.trim().length === 0)
12
- return {};
13
- try {
14
- return JSON.parse(args);
15
- }
16
- catch {
17
- return {};
18
- }
19
- }
20
- function typedToolCallItem(input) {
21
- return {
22
- type: `${input.name}_call`,
23
- id: input.itemId,
24
- call_id: input.callId,
25
- status: "completed",
26
- execution: "client",
27
- arguments: typedToolArguments(input.args)
28
- };
29
- }
30
11
  function customToolInput(args) {
31
12
  if (args.trim().length === 0)
32
13
  return "";
@@ -86,18 +67,6 @@ function streamedToolItem(tool) {
86
67
  }
87
68
  }
88
69
  }
89
- /** The `query` from a web_search call's JSON arguments (raw args as fallback). */
90
- function webSearchQueryOf(args) {
91
- try {
92
- const parsed = JSON.parse(args);
93
- if (typeof parsed.query === "string")
94
- return parsed.query;
95
- }
96
- catch {
97
- // fall through to the raw argument string
98
- }
99
- return args;
100
- }
101
70
  export function openAiSseToResponses(upstream, model, toolRegistry = new Map()) {
102
71
  const responseId = `resp_${randomId()}`;
103
72
  const messageItemId = `msg_${randomId()}`;
@@ -3,7 +3,7 @@
3
3
  * send to `/v1/responses`. Translation lives in `responses-codec.ts`; HTTP
4
4
  * handling lives in `responses.ts`.
5
5
  */
6
- import type { RouteKitReasoningEnvelope } from "./openai-chat-wire.js";
6
+ import type { RouteKitReasoningEnvelope } from "@velum-labs/routekit-provider-core";
7
7
  export type ResponsesContentPart = {
8
8
  type: string;
9
9
  text?: string;
@@ -83,6 +83,7 @@ export type ResponsesRequest = {
83
83
  temperature?: number;
84
84
  top_p?: number;
85
85
  parallel_tool_calls?: boolean;
86
+ service_tier?: string | null;
86
87
  /**
87
88
  * Codex serializes `reasoning: null` (not an absent key) for models whose
88
89
  * catalog metadata carries no reasoning level — every custom provider
@@ -12,7 +12,7 @@
12
12
  * - `responses-usage.ts` — usage mapping
13
13
  * - this module — HTTP handlers that wire the codec to a `Backend`
14
14
  */
15
- import type { Backend, BackendRequest, BackendRequestOptions } from "../providers/backend.js";
15
+ import type { Backend, BackendRequest, BackendRequestOptions } from "@velum-labs/routekit-provider-core";
16
16
  import type { ResponsesRequest } from "./responses-wire.js";
17
17
  export type { ResponsesToolEntry, ResponsesToolKind, ResponsesToolRegistry, ResponsesTranslationOptions } from "./responses-codec.js";
18
18
  export { chatToResponses, responsesToChat, responsesToolRegistry, WEB_SEARCH_TOOL_NAME } from "./responses-codec.js";
@@ -12,14 +12,9 @@
12
12
  * - `responses-usage.ts` — usage mapping
13
13
  * - this module — HTTP handlers that wire the codec to a `Backend`
14
14
  */
15
- import { toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
15
+ import { decodeOpenAiChatResponse, droppedField, finalizeResponsesResponse, gatewayTry, gatewayTryPromise, jsonResponse, prepareResponsesReasoningInput, reasoningSelectionOf, routeKitRequestValidationErrorOf } from "@velum-labs/routekit-provider-core";
16
+ import { requireHttpSubmissionSupport, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
16
17
  import { Effect } from "effect";
17
- import { gatewayTry, gatewayTryPromise } from "../effect/gateway.js";
18
- import { jsonResponse } from "../http/response.js";
19
- import { decodeOpenAiChatResponse } from "../providers/protocol.js";
20
- import { droppedField } from "./dropped.js";
21
- import { reasoningSelectionOf, routeKitRequestValidationErrorOf } from "./openai-chat-wire.js";
22
- import { prepareResponsesReasoningInput, wrapResponsesReasoningResponse } from "./openai-responses-wire.js";
23
18
  import { chatToResponses, isServerWebSearchTool, ResponsesTranslationError, responsesToChat, responsesToolRegistry, WEB_SEARCH_TOOL_NAME } from "./responses-codec.js";
24
19
  import { openAiSseToResponses } from "./responses-stream.js";
25
20
  import { composeServerToolStream, runBufferedServerToolLoop } from "./server-tool-loop.js";
@@ -126,7 +121,9 @@ export function handleResponses(backend, body, modelCallId, signal, backendOptio
126
121
  ...backendOptions,
127
122
  modelCallId
128
123
  });
129
- return yield* wrapResponsesReasoningResponse(response, reasoningOwner);
124
+ return yield* finalizeResponsesResponse(response, reasoningOwner, {
125
+ stream: body.stream === true
126
+ });
130
127
  }
131
128
  // Server-executed web search is honored when the caller declared the tool,
132
129
  // an executor is available (a provider key exists), and no *client* tool
@@ -135,6 +132,10 @@ export function handleResponses(backend, body, modelCallId, signal, backendOptio
135
132
  const declaresWebSearch = body.tools?.some(isServerWebSearchTool) === true;
136
133
  const clientNameCollision = body.tools?.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME) === true;
137
134
  const executor = declaresWebSearch && !clientNameCollision ? resolveWebSearchExecutor("responses") : undefined;
135
+ // The server-tool owner has additional provider POSTs and an async
136
+ // streaming runner that do not yet carry strict request authority.
137
+ // Refuse the compound operation before its first model submission.
138
+ yield* requireHttpSubmissionSupport(executor === undefined);
138
139
  const serverTools = executor !== undefined;
139
140
  const toolRegistry = responsesToolRegistry(body, { serverTools });
140
141
  const includeCompatibleBody = requestsEncryptedReasoning && !preservesOpaqueReasoning
@@ -26,8 +26,8 @@
26
26
  import { type Reasoning, type ToolResult } from "@velum-labs/routekit-contracts/protocol-ir";
27
27
  import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
28
28
  import { type Context, Effect } from "effect";
29
- import type { BackendRequest } from "../providers/backend.js";
30
- import { type OpenAiChatResponse } from "../providers/protocol.js";
29
+ import type { BackendRequest } from "@velum-labs/routekit-provider-core";
30
+ import { type OpenAiChatResponse } from "@velum-labs/routekit-provider-core";
31
31
  import type { WebSearchExecutor } from "./web-search.js";
32
32
  /** In-process marker chunk field the loop injects between composed steps. */
33
33
  export declare const SERVER_TOOL_MARKER_FIELD = "routekit_server_tool";
@@ -28,11 +28,11 @@ import { randomId } from "@velum-labs/routekit-runtime/timing";
28
28
  import { RouteKitFailure, runRouteKitEffect, runRouteKitEffectWith } from "@velum-labs/routekit-runtime/effect";
29
29
  import { StreamPump } from "@velum-labs/routekit-runtime/sse";
30
30
  import { Effect } from "effect";
31
- import { gatewayTry, gatewayTryPromise } from "../effect/gateway.js";
32
- import { decodeOpenAiChatResponse, decodeOpenAiChatSseEvent, decodeOpenAiToolCalls, isProviderRecord } from "../providers/protocol.js";
33
- import { ChatStreamAssembler } from "../sse/chat-assembler.js";
34
- import { SseParseError } from "../sse/parse.js";
35
- import { googleToolCallIndexesOf, responsesReasoningMetadataOf } from "./openai-chat-wire.js";
31
+ import { gatewayTry, gatewayTryPromise } from "@velum-labs/routekit-provider-core";
32
+ import { decodeOpenAiChatResponse, decodeOpenAiChatSseEvent, decodeOpenAiToolCalls, isProviderRecord } from "@velum-labs/routekit-provider-core";
33
+ import { ChatStreamAssembler } from "@velum-labs/routekit-provider-core";
34
+ import { SseParseError } from "@velum-labs/routekit-provider-core";
35
+ import { googleToolCallIndexesOf, responsesReasoningMetadataOf } from "@velum-labs/routekit-provider-core";
36
36
  import { canonicalServerToolReasoning, nativeAnthropicServerToolReasoning, serverToolAssistantMessage } from "./server-tool-transcript.js";
37
37
  import { MAX_WEB_SEARCHES_PER_TURN } from "./web-search.js";
38
38
  const ENCODER = new TextEncoder();
@@ -4,7 +4,7 @@
4
4
  * the synthetic tool turn. Loop orchestration lives in `server-tool-loop.ts`.
5
5
  */
6
6
  import type { Reasoning } from "@velum-labs/routekit-contracts/protocol-ir";
7
- import { type ResponsesReasoningState } from "./openai-chat-wire.js";
7
+ import { type ResponsesReasoningState } from "@velum-labs/routekit-provider-core";
8
8
  export type ServerToolTranscriptCall = {
9
9
  providerIndex?: number;
10
10
  id?: string;
@@ -4,7 +4,7 @@
4
4
  * the synthetic tool turn. Loop orchestration lives in `server-tool-loop.ts`.
5
5
  */
6
6
  import { randomId } from "@velum-labs/routekit-runtime/timing";
7
- import { ANTHROPIC_MESSAGE_CONTENT, anthropicReasoningDetailsOf, anthropicReasoningExtension, attachGoogleToolCallIndexes, attachResponsesReasoningMetadata, googleThoughtDetailsOf, reasoningIndex } from "./openai-chat-wire.js";
7
+ import { ANTHROPIC_MESSAGE_CONTENT, anthropicReasoningDetailsOf, anthropicReasoningExtension, attachGoogleToolCallIndexes, attachResponsesReasoningMetadata, googleThoughtDetailsOf, reasoningIndex } from "@velum-labs/routekit-provider-core";
8
8
  export function canonicalServerToolReasoning(details) {
9
9
  return [
10
10
  ...anthropicReasoningDetailsOf(details, "message"),
@@ -0,0 +1,6 @@
1
+ import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
2
+ import { Context, Effect, Scope } from "effect";
3
+ /** Own the runtime created for a published, standalone listener. */
4
+ export declare function startStandaloneListener<I, A extends {
5
+ readonly close: Effect.Effect<void, unknown>;
6
+ }>(service: Context.Key<I, A>, start: Effect.Effect<A, Error, RouteKitPlatform | Scope.Scope>): Promise<A>;
@@ -0,0 +1,26 @@
1
+ import { RouteKitLive, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
2
+ import { Effect, Layer, ManagedRuntime } from "effect";
3
+ /** Own the runtime created for a published, standalone listener. */
4
+ export async function startStandaloneListener(service, start) {
5
+ const layer = Layer.effect(service, Effect.acquireRelease(start, (listener) => listener.close.pipe(Effect.ignore))).pipe(Layer.provide(RouteKitLive));
6
+ const runtime = ManagedRuntime.make(layer);
7
+ try {
8
+ const listener = await runtime.runPromise(service);
9
+ let closeRun;
10
+ const disposeRuntime = Effect.tryPromise({
11
+ try: () => {
12
+ closeRun ??= runtime.dispose();
13
+ return closeRun;
14
+ },
15
+ catch: toRouteKitFailure
16
+ }).pipe(Effect.ignore);
17
+ return {
18
+ ...listener,
19
+ close: listener.close.pipe(Effect.ensuring(disposeRuntime))
20
+ };
21
+ }
22
+ catch (error) {
23
+ await runtime.dispose().catch(() => undefined);
24
+ throw error;
25
+ }
26
+ }
@@ -17,7 +17,7 @@
17
17
  import { withDeadline } from "@velum-labs/routekit-runtime/timing";
18
18
  import { executeWebRequest, RouteKitFailure, routeKitError, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
19
19
  import { Effect } from "effect";
20
- import { decodeAnthropicWebSearchResult, decodeOpenAiWebSearchResult } from "../providers/protocol.js";
20
+ import { decodeAnthropicWebSearchResult, decodeOpenAiWebSearchResult } from "@velum-labs/routekit-provider-core";
21
21
  /** Hard ceiling on gateway-executed searches within one caller turn. */
22
22
  export const MAX_WEB_SEARCHES_PER_TURN = 8;
23
23
  /** Per-search wall clock before the delegated call is aborted. */
@@ -0,0 +1,4 @@
1
+ /** Prefix the first non-empty Anthropic public text block or text delta. */
2
+ export declare function decorateAnthropicStream(body: ReadableStream<Uint8Array>, prefix: string): ReadableStream<Uint8Array>;
3
+ /** Decorate a buffered Anthropic Messages response without adding a content block. */
4
+ export declare function decorateAnthropicJson(value: unknown, prefix: string): boolean;
@@ -0,0 +1,45 @@
1
+ import { prefixOnce, recordOf, transformJsonSse } from "./sse-transform.js";
2
+ /** Prefix the first non-empty Anthropic public text block or text delta. */
3
+ export function decorateAnthropicStream(body, prefix) {
4
+ let emitted = false;
5
+ return transformJsonSse(body, (value) => {
6
+ if (emitted)
7
+ return undefined;
8
+ const event = recordOf(value);
9
+ if (event?.type === "content_block_start") {
10
+ const block = recordOf(event.content_block);
11
+ if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) {
12
+ block.text = prefixOnce(prefix, block.text);
13
+ emitted = true;
14
+ return value;
15
+ }
16
+ }
17
+ if (event?.type === "content_block_delta") {
18
+ const delta = recordOf(event.delta);
19
+ if (delta?.type === "text_delta" &&
20
+ typeof delta.text === "string" &&
21
+ delta.text.length > 0) {
22
+ delta.text = prefixOnce(prefix, delta.text);
23
+ emitted = true;
24
+ return value;
25
+ }
26
+ }
27
+ return undefined;
28
+ });
29
+ }
30
+ /** Decorate a buffered Anthropic Messages response without adding a content block. */
31
+ export function decorateAnthropicJson(value, prefix) {
32
+ const content = recordOf(value)?.content;
33
+ if (!Array.isArray(content))
34
+ return false;
35
+ for (const part of content) {
36
+ const block = recordOf(part);
37
+ if (block?.type === "text" &&
38
+ typeof block.text === "string" &&
39
+ block.text.length > 0) {
40
+ block.text = prefixOnce(prefix, block.text);
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
@@ -0,0 +1,4 @@
1
+ /** Prefix the first non-empty Chat Completions public content delta. */
2
+ export declare function decorateChatStream(body: ReadableStream<Uint8Array>, prefix: string): ReadableStream<Uint8Array>;
3
+ /** Decorate a buffered Chat Completions response without creating a choice or message. */
4
+ export declare function decorateChatJson(value: unknown, prefix: string): boolean;