@kitn.ai/ui 0.28.0 → 0.29.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.
- package/README.md +1 -1
- package/dist/agent-tooling/construct/public.d.ts +15 -0
- package/dist/agent-tooling/construct/schema.d.ts +111 -0
- package/dist/construct-cli.es.js +3 -3
- package/dist/construct.d.ts +5 -0
- package/dist/construct.js +327 -0
- package/dist/core-C8fzo39E.js +1 -12
- package/dist/engine-javascript-C1x7zo1_.js +1 -141
- package/dist/mcp.es.js +1 -1
- package/package.json +16 -16
- package/bin/route.test.js +0 -58
- package/src/agent-tooling/README.md +0 -192
- package/src/agent-tooling/archetypes.ts +0 -205
- package/src/agent-tooling/catalog/README.md +0 -676
- package/src/agent-tooling/catalog/catalog-types.ts +0 -231
- package/src/agent-tooling/catalog/fabrications.ts +0 -96
- package/src/agent-tooling/catalog/invariants.ts +0 -284
- package/src/agent-tooling/catalog/labs-titles.ts +0 -114
- package/src/agent-tooling/catalog/scenarios.ts +0 -87
- package/src/agent-tooling/catalog/surfaces.ts +0 -347
- package/src/agent-tooling/construct/cli-entry.ts +0 -8
- package/src/agent-tooling/construct/cli.ts +0 -147
- package/src/agent-tooling/construct/codegen.ts +0 -1635
- package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
- package/src/agent-tooling/construct/dev.ts +0 -148
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
- package/src/agent-tooling/construct/schema.ts +0 -452
- package/src/agent-tooling/integrations/anthropic.ts +0 -442
- package/src/agent-tooling/integrations/cloudflare.ts +0 -218
- package/src/agent-tooling/integrations/langgraph.ts +0 -133
- package/src/agent-tooling/integrations/mastra.ts +0 -182
- package/src/agent-tooling/integrations/mock.ts +0 -148
- package/src/agent-tooling/integrations/ollama.ts +0 -97
- package/src/agent-tooling/integrations/openai.ts +0 -95
- package/src/agent-tooling/integrations/openrouter.ts +0 -82
- package/src/agent-tooling/integrations/pi.ts +0 -176
- package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
- package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
- package/src/agent-tooling/mcp/manifest.ts +0 -422
- package/src/agent-tooling/mcp/server.ts +0 -116
- package/src/agent-tooling/mcp/stdio.ts +0 -18
- package/src/agent-tooling/mcp/tools/construct.ts +0 -130
- package/src/agent-tooling/mcp/tools/debug.ts +0 -621
- package/src/agent-tooling/mcp/tools/reference.ts +0 -980
- package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
- package/src/agent-tooling/mcp/tools/theme.ts +0 -456
- package/src/agent-tooling/mcp/tools/types.ts +0 -14
- package/src/agent-tooling/mcp/validate-args.ts +0 -141
- package/src/agent-tooling/recipes/composed-thread.ts +0 -714
- package/src/agent-tooling/recipes/index.ts +0 -22
- package/src/agent-tooling/recipes/types.ts +0 -32
- package/src/agent-tooling/registry.ts +0 -132
- package/src/agent-tooling/route-emit.ts +0 -305
- package/src/agent-tooling/types.ts +0 -390
- package/src/components/artifact-card.tsx +0 -120
- package/src/components/artifact.tsx +0 -914
- package/src/components/attachment-types.ts +0 -66
- package/src/components/attachments.tsx +0 -536
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
- package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
- package/src/components/audio-visualizer/fit-scale.ts +0 -112
- package/src/components/audio-visualizer/index.tsx +0 -528
- package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
- package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
- package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
- package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
- package/src/components/audio-visualizer/sizes.ts +0 -54
- package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
- package/src/components/audio-visualizer/variant-bar.tsx +0 -175
- package/src/components/audio-visualizer/variant-custom.tsx +0 -344
- package/src/components/audio-visualizer/variant-grid.tsx +0 -175
- package/src/components/audio-visualizer/variant-radial.tsx +0 -163
- package/src/components/audio-visualizer/variant-wave.tsx +0 -130
- package/src/components/audio-visualizer/wave.glsl.ts +0 -130
- package/src/components/card-fallback.tsx +0 -60
- package/src/components/card-renderer.tsx +0 -220
- package/src/components/card.tsx +0 -212
- package/src/components/chain-of-thought.tsx +0 -348
- package/src/components/chat-container.tsx +0 -89
- package/src/components/chat-scope-picker.tsx +0 -57
- package/src/components/chat-thread.tsx +0 -1014
- package/src/components/checkpoint.tsx +0 -88
- package/src/components/choice-card.tsx +0 -743
- package/src/components/coachmark.tsx +0 -249
- package/src/components/code-block.tsx +0 -181
- package/src/components/composer-dom.ts +0 -159
- package/src/components/composer-highlight.ts +0 -242
- package/src/components/composer-history.ts +0 -82
- package/src/components/composer.tsx +0 -1075
- package/src/components/confirm-card.tsx +0 -402
- package/src/components/context.tsx +0 -377
- package/src/components/conversation-item.tsx +0 -241
- package/src/components/conversation-list.tsx +0 -429
- package/src/components/conversation-panel.tsx +0 -118
- package/src/components/dismissed-stub.tsx +0 -80
- package/src/components/embed.tsx +0 -200
- package/src/components/empty.tsx +0 -177
- package/src/components/feedback-bar.tsx +0 -166
- package/src/components/file-tree.tsx +0 -530
- package/src/components/file-upload.tsx +0 -167
- package/src/components/form-widgets.tsx +0 -525
- package/src/components/form.tsx +0 -1296
- package/src/components/home-panel.tsx +0 -159
- package/src/components/image.tsx +0 -65
- package/src/components/link-preview.tsx +0 -195
- package/src/components/loader.tsx +0 -367
- package/src/components/markdown.tsx +0 -162
- package/src/components/message-skills.tsx +0 -40
- package/src/components/message.tsx +0 -765
- package/src/components/model-switcher.tsx +0 -132
- package/src/components/prompt-input.tsx +0 -188
- package/src/components/prompt-suggestion.tsx +0 -150
- package/src/components/reasoning.tsx +0 -293
- package/src/components/response-compare-types.ts +0 -152
- package/src/components/response-compare.tsx +0 -455
- package/src/components/response-stream.tsx +0 -109
- package/src/components/screen.tsx +0 -181
- package/src/components/scroll-button.tsx +0 -93
- package/src/components/source.tsx +0 -192
- package/src/components/tasks-card.tsx +0 -801
- package/src/components/text-shimmer.tsx +0 -37
- package/src/components/thinking-bar.tsx +0 -50
- package/src/components/thread.tsx +0 -228
- package/src/components/toast.tsx +0 -503
- package/src/components/tool-classify.ts +0 -26
- package/src/components/tool-types.ts +0 -49
- package/src/components/tool.tsx +0 -190
- package/src/components/use-card-resolution.ts +0 -44
- package/src/components/voice-input.tsx +0 -222
- package/src/components/voice-output.tsx +0 -242
- package/src/components/widget-tab-bar.tsx +0 -73
- package/src/components/workspace-shell.tsx +0 -285
- package/src/diagnostics/hook.ts +0 -352
- package/src/diagnostics/index.ts +0 -82
- package/src/diagnostics/report-request.ts +0 -296
- package/src/elements/agent-card.tsx +0 -67
- package/src/elements/artifact.tsx +0 -194
- package/src/elements/attachments.tsx +0 -140
- package/src/elements/audio-visualizer.tsx +0 -227
- package/src/elements/autoloader.ts +0 -89
- package/src/elements/avatar.tsx +0 -33
- package/src/elements/badge.tsx +0 -32
- package/src/elements/button.tsx +0 -149
- package/src/elements/card.tsx +0 -133
- package/src/elements/cards.tsx +0 -266
- package/src/elements/chain-of-thought.tsx +0 -141
- package/src/elements/chat-scope-picker.tsx +0 -65
- package/src/elements/chat-types.ts +0 -102
- package/src/elements/chat-workspace.tsx +0 -188
- package/src/elements/chat.tsx +0 -304
- package/src/elements/checkbox-group.tsx +0 -164
- package/src/elements/checkbox.tsx +0 -134
- package/src/elements/checkpoint.tsx +0 -47
- package/src/elements/choice.tsx +0 -100
- package/src/elements/coachmark.tsx +0 -90
- package/src/elements/code-block.tsx +0 -60
- package/src/elements/command.tsx +0 -197
- package/src/elements/compare.tsx +0 -111
- package/src/elements/compiled.css +0 -2
- package/src/elements/composer.tsx +0 -162
- package/src/elements/confirm-card.tsx +0 -76
- package/src/elements/context-meter.tsx +0 -107
- package/src/elements/conversation-item.tsx +0 -120
- package/src/elements/conversation-list.tsx +0 -203
- package/src/elements/css.ts +0 -5
- package/src/elements/default-input.tsx +0 -392
- package/src/elements/define-entry.ts +0 -12
- package/src/elements/define.tsx +0 -563
- package/src/elements/diagnostic-events.ts +0 -114
- package/src/elements/dialog.tsx +0 -145
- package/src/elements/disclosure.ts +0 -95
- package/src/elements/dock.tsx +0 -215
- package/src/elements/dropdown.tsx +0 -123
- package/src/elements/editable-label.tsx +0 -140
- package/src/elements/element-data-types.ts +0 -152
- package/src/elements/element-diagnostics.ts +0 -392
- package/src/elements/element-manifest.json +0 -354
- package/src/elements/element-nonscalar.json +0 -187
- package/src/elements/element-types.d.ts +0 -4353
- package/src/elements/embed.tsx +0 -35
- package/src/elements/empty.tsx +0 -29
- package/src/elements/feedback-bar.tsx +0 -66
- package/src/elements/file-tree.tsx +0 -62
- package/src/elements/file-upload.tsx +0 -44
- package/src/elements/form.tsx +0 -94
- package/src/elements/hover-card.tsx +0 -80
- package/src/elements/icon.tsx +0 -41
- package/src/elements/image.tsx +0 -32
- package/src/elements/input.tsx +0 -492
- package/src/elements/kbd.tsx +0 -45
- package/src/elements/link-preview.tsx +0 -34
- package/src/elements/loader.tsx +0 -25
- package/src/elements/markdown.tsx +0 -38
- package/src/elements/menu.tsx +0 -241
- package/src/elements/message-skills.tsx +0 -83
- package/src/elements/message.tsx +0 -361
- package/src/elements/model-switcher.tsx +0 -121
- package/src/elements/nav.tsx +0 -87
- package/src/elements/notice.tsx +0 -53
- package/src/elements/pane-grid.tsx +0 -111
- package/src/elements/pane-group.tsx +0 -119
- package/src/elements/pane.tsx +0 -129
- package/src/elements/popover.tsx +0 -81
- package/src/elements/progress-bar.tsx +0 -39
- package/src/elements/prompt-dock.tsx +0 -82
- package/src/elements/prompt-input.tsx +0 -240
- package/src/elements/prompt-suggestions.tsx +0 -113
- package/src/elements/radio-group.tsx +0 -129
- package/src/elements/reasoning.tsx +0 -71
- package/src/elements/register-impl.ts +0 -148
- package/src/elements/register.ts +0 -76
- package/src/elements/remote.tsx +0 -225
- package/src/elements/resizable.globals.d.ts +0 -36
- package/src/elements/resizable.tsx +0 -704
- package/src/elements/response-stream.tsx +0 -40
- package/src/elements/screen.tsx +0 -90
- package/src/elements/scroll-area.tsx +0 -33
- package/src/elements/scroll-button.tsx +0 -179
- package/src/elements/search.tsx +0 -185
- package/src/elements/segmented.tsx +0 -129
- package/src/elements/select.tsx +0 -165
- package/src/elements/separator.tsx +0 -36
- package/src/elements/setting-item.tsx +0 -50
- package/src/elements/settings-group.tsx +0 -37
- package/src/elements/skeleton.tsx +0 -45
- package/src/elements/slider.tsx +0 -171
- package/src/elements/slot-text.ts +0 -73
- package/src/elements/slots.ts +0 -787
- package/src/elements/source.tsx +0 -126
- package/src/elements/status.tsx +0 -42
- package/src/elements/styles.css +0 -328
- package/src/elements/switch.tsx +0 -118
- package/src/elements/tabs.tsx +0 -95
- package/src/elements/tasks.tsx +0 -103
- package/src/elements/text-shimmer.tsx +0 -28
- package/src/elements/thinking-bar.tsx +0 -43
- package/src/elements/thread.tsx +0 -139
- package/src/elements/toast.tsx +0 -106
- package/src/elements/tool.tsx +0 -78
- package/src/elements/tooltip.tsx +0 -85
- package/src/elements/validate-messages.ts +0 -76
- package/src/elements/voice-input.tsx +0 -100
- package/src/elements/voice-output.tsx +0 -82
- package/src/index.ts +0 -430
- package/src/primitives/audio-bands.ts +0 -174
- package/src/primitives/card-component-types.ts +0 -63
- package/src/primitives/card-contract.ts +0 -80
- package/src/primitives/card-data-types.ts +0 -361
- package/src/primitives/card-host.tsx +0 -37
- package/src/primitives/card-recovery.ts +0 -160
- package/src/primitives/card-registry.tsx +0 -78
- package/src/primitives/card-resolution.ts +0 -36
- package/src/primitives/card-routing.ts +0 -85
- package/src/primitives/card-schemas/artifact.schema.json +0 -92
- package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
- package/src/primitives/card-schemas/card-event.schema.json +0 -12
- package/src/primitives/card-schemas/choice.schema.json +0 -75
- package/src/primitives/card-schemas/confirm.schema.json +0 -65
- package/src/primitives/card-schemas/embed.schema.json +0 -63
- package/src/primitives/card-schemas/form.result.schema.json +0 -7
- package/src/primitives/card-schemas/form.schema.json +0 -53
- package/src/primitives/card-schemas/link.schema.json +0 -54
- package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
- package/src/primitives/card-schemas/tasks.schema.json +0 -83
- package/src/primitives/card-tags.ts +0 -61
- package/src/primitives/card-validate-cards.ts +0 -255
- package/src/primitives/card-validate-schemas.ts +0 -58
- package/src/primitives/card-validate.ts +0 -131
- package/src/primitives/chat-config.tsx +0 -103
- package/src/primitives/composer-model.ts +0 -49
- package/src/primitives/composer-triggers.ts +0 -35
- package/src/primitives/controllable.ts +0 -21
- package/src/primitives/conversation-store.ts +0 -248
- package/src/primitives/create-kai-chat.ts +0 -66
- package/src/primitives/create-tween.ts +0 -266
- package/src/primitives/embed-providers.ts +0 -254
- package/src/primitives/field-mask.ts +0 -256
- package/src/primitives/field-semantics.ts +0 -115
- package/src/primitives/highlighter.ts +0 -157
- package/src/primitives/input-mask.ts +0 -853
- package/src/primitives/link-preview.ts +0 -87
- package/src/primitives/message-feedback.ts +0 -124
- package/src/primitives/pdf-preview.ts +0 -121
- package/src/primitives/toast-store.ts +0 -338
- package/src/primitives/url-scheme-policy.ts +0 -70
- package/src/primitives/use-audio-analysis.ts +0 -340
- package/src/primitives/use-auto-resize.ts +0 -31
- package/src/primitives/use-resize-observer.ts +0 -45
- package/src/primitives/use-sequencer.ts +0 -72
- package/src/primitives/use-speech-recognition.ts +0 -146
- package/src/primitives/use-stick-to-bottom.ts +0 -75
- package/src/primitives/use-text-stream.ts +0 -143
- package/src/primitives/use-voice-recorder.ts +0 -62
- package/src/primitives/visualizer-sequences.ts +0 -350
- package/src/remote/host-embed.ts +0 -345
- package/src/remote/index.ts +0 -2
- package/src/remote/origin.ts +0 -30
- package/src/remote/provider-runtime.ts +0 -262
- package/src/remote/provider.ts +0 -2
- package/src/remote/validate.ts +0 -22
- package/src/remote/version.ts +0 -12
- package/src/remote/wire.ts +0 -48
- package/src/schemas/from-tool-call.ts +0 -151
- package/src/schemas/index.ts +0 -341
- package/src/schemas/provider-subsets.ts +0 -538
- package/src/schemas/registry.ts +0 -355
- package/src/schemas/tool-defs.ts +0 -793
- package/src/solid.ts +0 -173
- package/src/state/index.ts +0 -44
- package/src/state/messages.ts +0 -53
- package/src/state/mock.ts +0 -297
- package/src/state/parts.ts +0 -295
- package/src/state/persistence.ts +0 -180
- package/src/state/stream.ts +0 -246
- package/src/state/suggestions.ts +0 -9
- package/src/state/threads.ts +0 -142
- package/src/test-utils/fake-clock.ts +0 -88
- package/src/types.ts +0 -95
- package/src/ui/action-icons.ts +0 -53
- package/src/ui/agent-card.tsx +0 -218
- package/src/ui/avatar.tsx +0 -23
- package/src/ui/badge.tsx +0 -21
- package/src/ui/button.tsx +0 -49
- package/src/ui/card.tsx +0 -245
- package/src/ui/checkbox-group.tsx +0 -153
- package/src/ui/checkbox.tsx +0 -63
- package/src/ui/collapsible.tsx +0 -163
- package/src/ui/command.tsx +0 -103
- package/src/ui/dialog.tsx +0 -223
- package/src/ui/dock.tsx +0 -630
- package/src/ui/dropdown.tsx +0 -636
- package/src/ui/editable-label.tsx +0 -112
- package/src/ui/hover-card.tsx +0 -345
- package/src/ui/icon.tsx +0 -167
- package/src/ui/input.tsx +0 -391
- package/src/ui/kbd.tsx +0 -118
- package/src/ui/nav.tsx +0 -282
- package/src/ui/notice.tsx +0 -84
- package/src/ui/overlay.tsx +0 -277
- package/src/ui/pane-grid.tsx +0 -117
- package/src/ui/pane-group.tsx +0 -291
- package/src/ui/pane.tsx +0 -193
- package/src/ui/popover.tsx +0 -106
- package/src/ui/progress-bar.tsx +0 -83
- package/src/ui/prompt-dock.tsx +0 -145
- package/src/ui/radio.tsx +0 -150
- package/src/ui/resizable.tsx +0 -693
- package/src/ui/scroll-area.tsx +0 -26
- package/src/ui/segmented.tsx +0 -109
- package/src/ui/select.tsx +0 -168
- package/src/ui/separator.tsx +0 -10
- package/src/ui/settings-group.tsx +0 -67
- package/src/ui/skeleton.tsx +0 -73
- package/src/ui/slider.tsx +0 -178
- package/src/ui/status.tsx +0 -48
- package/src/ui/switch.tsx +0 -119
- package/src/ui/tabs.tsx +0 -168
- package/src/ui/textarea.tsx +0 -21
- package/src/ui/tooltip.tsx +0 -118
- package/src/utils/cn.ts +0 -30
- package/src/wire/chunk.ts +0 -248
- package/src/wire/consume.ts +0 -599
- package/src/wire/diagnostics.ts +0 -727
- package/src/wire/encode-probe.ts +0 -214
- package/src/wire/encode.ts +0 -947
- package/src/wire/files.ts +0 -342
- package/src/wire/formats/anthropic.ts +0 -274
- package/src/wire/formats/openai.ts +0 -228
- package/src/wire/index.ts +0 -95
- package/src/wire/media-types.ts +0 -344
- package/src/wire/read.ts +0 -396
- package/src/wire/sink-helpers.ts +0 -50
- package/src/wire/sse.ts +0 -153
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import type { Integration } from '../types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Anthropic Messages, and NOT a copy of the OpenRouter route.
|
|
5
|
-
*
|
|
6
|
-
* Four constraints established by live testing and recorded in
|
|
7
|
-
* docs/superpowers/HANDOFF-model-driven-components.md §4. Every one of them is a
|
|
8
|
-
* 400 (or worse, a silent 200) if you port an OpenAI-shaped body over verbatim:
|
|
9
|
-
*
|
|
10
|
-
* 1. `system` is a TOP-LEVEL field. A `{ role: 'system' }` entry in `messages`
|
|
11
|
-
* is rejected.
|
|
12
|
-
* 2. `tool_choice` is an OBJECT, never the bare string OpenAI accepts.
|
|
13
|
-
* 3. `max_tokens` is REQUIRED. OpenAI defaults it; this API does not.
|
|
14
|
-
* 4. Content blocks stream strictly sequentially and are indexed by CONTENT
|
|
15
|
-
* BLOCK, so a thinking block pushes a tool call from block 0 to block 1.
|
|
16
|
-
* OpenAI's `tool_calls[].index` counts TOOL CALLS, so the two indices are
|
|
17
|
-
* not the same number and passing one through as the other is the original
|
|
18
|
-
* Critical.
|
|
19
|
-
*
|
|
20
|
-
* The thinking constraint in that document has since MOVED, and the route says
|
|
21
|
-
* so rather than repeating it: `budget_tokens` is not a tunable with a floor of
|
|
22
|
-
* 1024 any more, it is REMOVED from the current models and returns a 400. See
|
|
23
|
-
* the block comment beside `thinking` in the emitted body.
|
|
24
|
-
*/
|
|
25
|
-
const anthropic: Integration = {
|
|
26
|
-
id: 'anthropic',
|
|
27
|
-
title: 'Anthropic',
|
|
28
|
-
category: 'provider',
|
|
29
|
-
language: 'ts',
|
|
30
|
-
// 'native': the Messages API streams its OWN SSE dialect (message_start,
|
|
31
|
-
// content_block_delta, message_stop), not OpenAI's. The route re-frames it —
|
|
32
|
-
// see `reframeToOpenAISse` and the note in streamMapping for why.
|
|
33
|
-
streamFormat: 'native',
|
|
34
|
-
envVars: ['ANTHROPIC_API_KEY'],
|
|
35
|
-
// No per-framework templates: the handler below is web-standard, so the
|
|
36
|
-
// scaffolder wraps it in whatever the target framework routes with.
|
|
37
|
-
routeTemplates: {},
|
|
38
|
-
webRoute: `/** Where an image or document's bytes come from. Anthropic takes both forms
|
|
39
|
-
* for both block types, which is why this route has no gap where the
|
|
40
|
-
* OpenAI-shaped one does: a chat-completions \`file\` part is base64-only. */
|
|
41
|
-
type AnthropicSource =
|
|
42
|
-
| { type: 'base64'; media_type: string; data: string }
|
|
43
|
-
| { type: 'url'; url: string };
|
|
44
|
-
|
|
45
|
-
/** One Anthropic content block. Open union on the wire; these are the ones the
|
|
46
|
-
* scaffold's own thread can produce. */
|
|
47
|
-
type AnthropicBlock =
|
|
48
|
-
| { type: 'text'; text: string }
|
|
49
|
-
| { type: 'image'; source: AnthropicSource }
|
|
50
|
-
| { type: 'document'; source: AnthropicSource }
|
|
51
|
-
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
52
|
-
| { type: 'tool_result'; tool_use_id: string; content: string };
|
|
53
|
-
|
|
54
|
-
/** Anthropic messages carry ONLY 'user' and 'assistant'. There is no 'system'
|
|
55
|
-
* role here and no 'tool' role: a system prompt is a top-level field, and a
|
|
56
|
-
* tool RESULT rides on a user message. Both are handled in toAnthropicBody. */
|
|
57
|
-
type AnthropicMessage = { role: 'user' | 'assistant'; content: AnthropicBlock[] };
|
|
58
|
-
|
|
59
|
-
/** Anthropic calls the JSON Schema \`input_schema\`; OpenAI calls it
|
|
60
|
-
* \`parameters\`, and nests everything under \`function\`. Handing the OpenAI
|
|
61
|
-
* shape over unconverted is a 400. */
|
|
62
|
-
type AnthropicTool = { name: string; description?: string; input_schema: unknown };
|
|
63
|
-
|
|
64
|
-
/** The OpenAI tool shape the front end sends, narrowed from \`unknown[]\`. */
|
|
65
|
-
type OpenAIFunctionTool = {
|
|
66
|
-
function?: { name?: string; description?: string; parameters?: unknown };
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/** One decoded Messages-API SSE frame. Every field is optional because this is
|
|
70
|
-
* a provider-owned union and an unrecognised frame must be skipped, not throw. */
|
|
71
|
-
type AnthropicEvent = {
|
|
72
|
-
type?: string;
|
|
73
|
-
index?: number;
|
|
74
|
-
content_block?: { type?: string; id?: string; name?: string };
|
|
75
|
-
delta?: {
|
|
76
|
-
type?: string;
|
|
77
|
-
text?: string;
|
|
78
|
-
thinking?: string;
|
|
79
|
-
partial_json?: string;
|
|
80
|
-
stop_reason?: string;
|
|
81
|
-
};
|
|
82
|
-
error?: { message?: string };
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/** Anthropic stop reasons -> OpenAI finish_reason. 'refusal' has no OpenAI
|
|
86
|
-
* equivalent and is reported in band instead (see the message_delta case). */
|
|
87
|
-
const FINISH_REASONS: Record<string, string> = {
|
|
88
|
-
end_turn: 'stop',
|
|
89
|
-
stop_sequence: 'stop',
|
|
90
|
-
max_tokens: 'length',
|
|
91
|
-
tool_use: 'tool_calls',
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
function toAnthropicTools(tools: unknown[] | undefined): AnthropicTool[] | undefined {
|
|
95
|
-
if (!tools || tools.length === 0) return undefined;
|
|
96
|
-
return tools.map((raw) => {
|
|
97
|
-
const fn = (raw as OpenAIFunctionTool).function ?? {};
|
|
98
|
-
return {
|
|
99
|
-
name: fn.name ?? '',
|
|
100
|
-
description: fn.description,
|
|
101
|
-
input_schema: fn.parameters ?? { type: 'object', properties: {} },
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The OpenAI-shaped thread the front end posts -> an Anthropic Messages body.
|
|
108
|
-
*
|
|
109
|
-
* \`system\` comes back SEPARATELY because it is a top-level request field here.
|
|
110
|
-
* A \`{ role: 'system' }\` entry left in \`messages\` is a 400, and it is the most
|
|
111
|
-
* common thing to get wrong when porting a route across.
|
|
112
|
-
*/
|
|
113
|
-
function toAnthropicBody(messages: ChatRequestBody['messages']): {
|
|
114
|
-
system?: string;
|
|
115
|
-
messages: AnthropicMessage[];
|
|
116
|
-
} {
|
|
117
|
-
let system: string | undefined;
|
|
118
|
-
const out: AnthropicMessage[] = [];
|
|
119
|
-
|
|
120
|
-
// Append to the trailing user message when there is one. A tool result and the
|
|
121
|
-
// user's next turn are one turn on this wire, and several Anthropic-compatible
|
|
122
|
-
// proxies enforce strict alternation.
|
|
123
|
-
const pushUser = (content: AnthropicBlock[]): void => {
|
|
124
|
-
const last = out[out.length - 1];
|
|
125
|
-
if (last?.role === 'user') last.content.push(...content);
|
|
126
|
-
else out.push({ role: 'user', content });
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
for (const message of messages) {
|
|
130
|
-
switch (message.role) {
|
|
131
|
-
case 'system': {
|
|
132
|
-
// TOP-LEVEL, not a message. Several system turns concatenate.
|
|
133
|
-
const text = wireText(message.content);
|
|
134
|
-
system = system === undefined ? text : \`\${system}\\n\\n\${text}\`;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
case 'user': {
|
|
138
|
-
// Attachments ride HERE, as image and document blocks beside the text,
|
|
139
|
-
// in the order the thread authored them.
|
|
140
|
-
const blocks = wireParts(message.content).map((part): AnthropicBlock => {
|
|
141
|
-
if (part.kind === 'text') return { type: 'text', text: part.text };
|
|
142
|
-
const source: AnthropicSource =
|
|
143
|
-
part.source.type === 'data'
|
|
144
|
-
? { type: 'base64', media_type: part.mediaType, data: part.source.data }
|
|
145
|
-
: { type: 'url', url: part.source.url };
|
|
146
|
-
// \`image/png\` and the bare top-level \`image\` both mean an image; a URL
|
|
147
|
-
// source reports only the segment because the wire carries no more.
|
|
148
|
-
return part.mediaType.startsWith('image') ? { type: 'image', source } : { type: 'document', source };
|
|
149
|
-
});
|
|
150
|
-
if (blocks.length > 0) pushUser(blocks);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case 'tool': {
|
|
154
|
-
// A tool RESULT is a block on a USER message here, not a role of its own.
|
|
155
|
-
pushUser([
|
|
156
|
-
{
|
|
157
|
-
type: 'tool_result',
|
|
158
|
-
tool_use_id: message.tool_call_id ?? '',
|
|
159
|
-
content: wireText(message.content),
|
|
160
|
-
},
|
|
161
|
-
]);
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case 'assistant': {
|
|
165
|
-
const content: AnthropicBlock[] = [];
|
|
166
|
-
const text = wireText(message.content);
|
|
167
|
-
if (text) content.push({ type: 'text', text });
|
|
168
|
-
for (const call of message.tool_calls ?? []) {
|
|
169
|
-
content.push({
|
|
170
|
-
type: 'tool_use',
|
|
171
|
-
id: call.id,
|
|
172
|
-
name: call.function.name,
|
|
173
|
-
// \`input\` is a parsed OBJECT here. OpenAI carries the same value as a
|
|
174
|
-
// JSON STRING in \`function.arguments\`; forwarding the string is a 400.
|
|
175
|
-
input: call.function.arguments ? JSON.parse(call.function.arguments) : {},
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
// An assistant turn with nothing in it is not a legal message.
|
|
179
|
-
if (content.length > 0) out.push({ role: 'assistant', content });
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return { system, messages: out };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Anthropic Messages SSE -> OpenAI-format SSE.
|
|
190
|
-
*
|
|
191
|
-
* WHY THIS EXISTS AT ALL: the front end reads with readOpenAIStream. Handing it
|
|
192
|
-
* Anthropic frames does not throw — it parses to NOTHING, so the turn ends empty
|
|
193
|
-
* with no error anywhere, which is the worst failure this kit has. Re-framing
|
|
194
|
-
* here keeps the browser half of an anthropic scaffold identical to every other
|
|
195
|
-
* integration's, the same way the cloudflare Worker route re-frames env.AI.
|
|
196
|
-
*
|
|
197
|
-
* The alternative, if you would rather keep the native frames: return
|
|
198
|
-
* \`upstream.body\` untouched and read it with readAnthropicStream from
|
|
199
|
-
* '@kitn.ai/ui/wire'. That is also the ONLY way to round-trip thinking blocks
|
|
200
|
-
* verbatim (toAnthropicMessages echoes \`part.raw\` — a rebuilt block is a hard
|
|
201
|
-
* 400), so it is the path to take the moment you enable thinking below.
|
|
202
|
-
*/
|
|
203
|
-
function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
204
|
-
const encoder = new TextEncoder();
|
|
205
|
-
const decoder = new TextDecoder();
|
|
206
|
-
// THE INDEX TRAP. Anthropic's \`index\` counts CONTENT BLOCKS; OpenAI's
|
|
207
|
-
// \`tool_calls[].index\` counts TOOL CALLS. A thinking or text block ahead of a
|
|
208
|
-
// tool call shifts the block index, but the call is still tool call 0 — so the
|
|
209
|
-
// block index is translated through this map rather than passed through.
|
|
210
|
-
const toolIndexOfBlock = new Map<number, number>();
|
|
211
|
-
let toolCount = 0;
|
|
212
|
-
|
|
213
|
-
return new ReadableStream<Uint8Array>({
|
|
214
|
-
async start(controller) {
|
|
215
|
-
const send = (chunk: unknown): void => {
|
|
216
|
-
controller.enqueue(encoder.encode(\`data: \${JSON.stringify(chunk)}\\n\\n\`));
|
|
217
|
-
};
|
|
218
|
-
const reader = body.getReader();
|
|
219
|
-
let buffer = '';
|
|
220
|
-
try {
|
|
221
|
-
for (;;) {
|
|
222
|
-
const { value, done } = await reader.read();
|
|
223
|
-
if (done) break;
|
|
224
|
-
buffer += decoder.decode(value, { stream: true });
|
|
225
|
-
// Split on \\n rather than readline: readline also breaks on the
|
|
226
|
-
// Unicode separators U+2028/U+2029, which appear inside model output.
|
|
227
|
-
const lines = buffer.split('\\n');
|
|
228
|
-
buffer = lines.pop() ?? ''; // hold the partial line for the next chunk
|
|
229
|
-
for (const line of lines) {
|
|
230
|
-
const trimmed = line.trim();
|
|
231
|
-
// \`event:\` lines are redundant — every frame's data carries \`type\`.
|
|
232
|
-
if (!trimmed.startsWith('data:')) continue;
|
|
233
|
-
let event: AnthropicEvent;
|
|
234
|
-
try {
|
|
235
|
-
event = JSON.parse(trimmed.slice(5)) as AnthropicEvent;
|
|
236
|
-
} catch {
|
|
237
|
-
continue; // skip a malformed frame rather than kill the turn
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (event.type === 'content_block_start') {
|
|
241
|
-
const block = event.content_block;
|
|
242
|
-
if (block?.type !== 'tool_use') continue;
|
|
243
|
-
const index = toolCount;
|
|
244
|
-
toolCount += 1;
|
|
245
|
-
toolIndexOfBlock.set(event.index ?? 0, index);
|
|
246
|
-
send({
|
|
247
|
-
choices: [{
|
|
248
|
-
delta: {
|
|
249
|
-
tool_calls: [{
|
|
250
|
-
index,
|
|
251
|
-
id: block.id,
|
|
252
|
-
type: 'function',
|
|
253
|
-
function: { name: block.name, arguments: '' },
|
|
254
|
-
}],
|
|
255
|
-
},
|
|
256
|
-
}],
|
|
257
|
-
});
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (event.type === 'content_block_delta') {
|
|
262
|
-
const delta = event.delta;
|
|
263
|
-
if (delta?.type === 'text_delta') {
|
|
264
|
-
send({ choices: [{ delta: { content: delta.text } }] });
|
|
265
|
-
} else if (delta?.type === 'thinking_delta') {
|
|
266
|
-
send({ choices: [{ delta: { reasoning: delta.thinking } }] });
|
|
267
|
-
} else if (delta?.type === 'input_json_delta') {
|
|
268
|
-
const index = toolIndexOfBlock.get(event.index ?? 0);
|
|
269
|
-
// No entry means these fragments belong to a block that is not a
|
|
270
|
-
// tool_use. Dropping them beats inventing a tool call.
|
|
271
|
-
if (index !== undefined) {
|
|
272
|
-
send({
|
|
273
|
-
choices: [{
|
|
274
|
-
delta: {
|
|
275
|
-
tool_calls: [{ index, function: { arguments: delta.partial_json } }],
|
|
276
|
-
},
|
|
277
|
-
}],
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (event.type === 'message_delta') {
|
|
285
|
-
const reason = event.delta?.stop_reason;
|
|
286
|
-
if (!reason) continue;
|
|
287
|
-
if (reason === 'refusal') {
|
|
288
|
-
// A safety classifier declined. HTTP 200, no content, so without
|
|
289
|
-
// this the turn just ends blank. Report it IN BAND:
|
|
290
|
-
// readOpenAIStream lands it on turn.error.
|
|
291
|
-
send({ error: { message: 'The model declined this request (stop_reason: refusal).' } });
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
send({ choices: [{ delta: {}, finish_reason: FINISH_REASONS[reason] ?? 'stop' }] });
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (event.type === 'error') {
|
|
299
|
-
send({ error: { message: event.error?.message ?? 'Anthropic returned an error frame.' } });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
// The headers went out with the first byte, so the status is spent:
|
|
305
|
-
// report IN BAND. readOpenAIStream lands this on turn.error and keeps
|
|
306
|
-
// whatever already streamed.
|
|
307
|
-
const message = err instanceof Error ? err.message : 'Anthropic stream failed';
|
|
308
|
-
send({ error: { message } });
|
|
309
|
-
}
|
|
310
|
-
// Always: an unclosed stream leaves the browser waiting forever.
|
|
311
|
-
controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
|
|
312
|
-
controller.close();
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function chatHandler(request: Request): Promise<Response> {
|
|
318
|
-
// Both model and tools come from the browser. \`tools\` arrives in OpenAI
|
|
319
|
-
// function form and is converted to this API's shape below.
|
|
320
|
-
let chatBody: ChatRequestBody;
|
|
321
|
-
try {
|
|
322
|
-
chatBody = await readChatRequest(request);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
return toChatErrorResponse(error);
|
|
325
|
-
}
|
|
326
|
-
const { model, messages, tools } = chatBody;
|
|
327
|
-
const { system, messages: anthropicMessages } = toAnthropicBody(messages);
|
|
328
|
-
const anthropicTools = toAnthropicTools(tools);
|
|
329
|
-
|
|
330
|
-
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
|
|
331
|
-
method: 'POST',
|
|
332
|
-
headers: {
|
|
333
|
-
'x-api-key': \`\${process.env.ANTHROPIC_API_KEY}\`,
|
|
334
|
-
'anthropic-version': '2023-06-01',
|
|
335
|
-
'Content-Type': 'application/json',
|
|
336
|
-
},
|
|
337
|
-
body: JSON.stringify({
|
|
338
|
-
// Model ids here are Anthropic's own — 'claude-opus-5', 'claude-sonnet-5',
|
|
339
|
-
// 'claude-haiku-4-5'. An OpenAI id or an OpenRouter 'vendor/model' slug is
|
|
340
|
-
// a 404. The front end's \`model\` const is the one to edit; it is seeded
|
|
341
|
-
// from CLIENT_MODEL_IDS.anthropic.
|
|
342
|
-
model,
|
|
343
|
-
// REQUIRED. OpenAI defaults it; this API 400s without it. It caps thinking
|
|
344
|
-
// AND response text together, so raise it if you turn thinking on.
|
|
345
|
-
max_tokens: 4096,
|
|
346
|
-
// TOP-LEVEL, never a { role: 'system' } entry in \`messages\`.
|
|
347
|
-
...(system ? { system } : {}),
|
|
348
|
-
messages: anthropicMessages,
|
|
349
|
-
...(anthropicTools ? { tools: anthropicTools } : {}),
|
|
350
|
-
// tool_choice, if you set one, is an OBJECT: { type: 'auto' } |
|
|
351
|
-
// { type: 'any' } | { type: 'tool', name: 'search' }. OpenAI's bare string
|
|
352
|
-
// ('auto') is a 400 here.
|
|
353
|
-
// tool_choice: { type: 'auto' },
|
|
354
|
-
//
|
|
355
|
-
// THINKING. The rules changed with the model generation, so check yours:
|
|
356
|
-
// · claude-opus-5 / sonnet-5 / opus-4.8 / opus-4.7 / fable-5 —
|
|
357
|
-
// \`budget_tokens\` is REMOVED and returns a 400. Use
|
|
358
|
-
// \`thinking: { type: 'adaptive', display: 'summarized' }\` plus
|
|
359
|
-
// \`output_config: { effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' }\`.
|
|
360
|
-
// On claude-opus-5 thinking is already ON by default; what is NOT on is
|
|
361
|
-
// \`display\`, which defaults to 'omitted' and streams thinking blocks
|
|
362
|
-
// with EMPTY text. That reads exactly like "this model has no reasoning
|
|
363
|
-
// mode" — a silent 200 with nothing in the panel — and it is the same
|
|
364
|
-
// shape of mistake as the budget bug below. Set display explicitly.
|
|
365
|
-
// · older models (claude-haiku-4-5, claude-sonnet-4-5) — still
|
|
366
|
-
// \`thinking: { type: 'enabled', budget_tokens: N }\`, with N >= 1024 and
|
|
367
|
-
// max_tokens STRICTLY GREATER than N. Deriving N as a fraction of a
|
|
368
|
-
// small max_tokens returns HTTP 200 with no thinking and no error.
|
|
369
|
-
// Either way, once thinking is on a tool loop must echo every thinking
|
|
370
|
-
// block back VERBATIM, which this route cannot do: it re-frames to OpenAI
|
|
371
|
-
// SSE and the thread is re-encoded with toOpenAIMessages each round. Switch
|
|
372
|
-
// to readAnthropicStream + toAnthropicMessages (both from '@kitn.ai/ui/wire')
|
|
373
|
-
// and pass upstream.body through untouched before you enable it.
|
|
374
|
-
stream: true,
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// FORWARD THE STATUS. Returning 200 here is how a missing key turns into
|
|
379
|
-
// silence: the 401 body is JSON, it goes out labelled text/event-stream, the
|
|
380
|
-
// SSE reader finds no frame, and the turn resolves empty with nothing logged
|
|
381
|
-
// and no bubble. With the status intact readOpenAIStream throws a WireError
|
|
382
|
-
// carrying Anthropic's own message — which for this API is usually the exact
|
|
383
|
-
// field you got wrong.
|
|
384
|
-
if (!upstream.ok) {
|
|
385
|
-
return new Response(await upstream.text(), {
|
|
386
|
-
status: upstream.status,
|
|
387
|
-
headers: {
|
|
388
|
-
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
if (!upstream.body) {
|
|
393
|
-
return new Response(JSON.stringify({ error: { message: 'Anthropic returned no body to stream.' } }), {
|
|
394
|
-
status: 502,
|
|
395
|
-
headers: { 'Content-Type': 'application/json' },
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return new Response(reframeToOpenAISse(upstream.body), {
|
|
400
|
-
status: 200,
|
|
401
|
-
headers: {
|
|
402
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
403
|
-
// no-transform stops a proxy buffering the stream into one blob.
|
|
404
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
405
|
-
Connection: 'keep-alive',
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
}`,
|
|
409
|
-
streamMapping:
|
|
410
|
-
"The Anthropic Messages API streams its OWN SSE dialect: message_start, content_block_start / content_block_delta / content_block_stop, message_delta, message_stop. It is NOT OpenAI-format, and feeding it to readOpenAIStream does not throw — it parses to nothing and the turn ends silently empty. The route therefore re-frames each frame to OpenAI form before it reaches the browser: text_delta -> delta.content, thinking_delta -> delta.reasoning, a tool_use content_block_start plus its input_json_delta fragments -> delta.tool_calls, message_delta.stop_reason -> finish_reason, an error frame -> an in-band {error:{message}}. readOpenAIStream from @kitn.ai/ui/wire then parses it exactly as it does every other integration. Anthropic indexes by CONTENT BLOCK while OpenAI's tool_calls[].index counts TOOL CALLS, so the route keeps a block-index -> tool-index map: a thinking block ahead of a tool call shifts the block index but not the tool index. Usage is the one thing not re-framed (input_tokens arrives on message_start and output_tokens on message_delta, so joining them needs state) — map both onto a {usage:{prompt_tokens,completion_tokens}} frame if you want token counts. To keep the native frames instead, return upstream.body untouched and read it with readAnthropicStream; that is also the only way to round-trip thinking blocks verbatim.",
|
|
411
|
-
runNote:
|
|
412
|
-
"Set ANTHROPIC_API_KEY (console.anthropic.com). The route pins claude-opus-5; claude-sonnet-5 and claude-haiku-4-5 are the cheaper swaps. Four things this API does NOT share with the OpenAI one, all of them 400s: system is a top-level field and not a message, max_tokens is required, tool_choice is an object ({ type: 'auto' }) and not a string, and tool schemas use input_schema rather than function.parameters. Extended thinking: on claude-opus-5 and the other current models budget_tokens is REMOVED (400) — use thinking: { type: 'adaptive', display: 'summarized' } with output_config.effort, and note that display defaults to 'omitted', which streams empty thinking blocks that look exactly like no reasoning at all. On older models (claude-haiku-4-5, claude-sonnet-4-5) budget_tokens still applies and must be >= 1024 with max_tokens strictly greater.",
|
|
413
|
-
docsSlug: 'integrations/connect-any-model',
|
|
414
|
-
// 'model' was withheld until scaffold.ts could seed it correctly: the shared
|
|
415
|
-
// fallback is 'openai/gpt-4o-mini', which api.anthropic.com rejects outright,
|
|
416
|
-
// and shipping an editable const with a broken default is the dead-const defect
|
|
417
|
-
// forwardsFromClient exists to prevent. CLIENT_MODEL_IDS now carries a real
|
|
418
|
-
// Anthropic id, so the const is live and the route reads it.
|
|
419
|
-
forwardsFromClient: ['model', 'tools'],
|
|
420
|
-
// 'openai', NOT 'anthropic' — and this is the counter-intuitive one, so read
|
|
421
|
-
// `toAnthropicTools` above before changing it. This route CONVERTS the array
|
|
422
|
-
// server-side: it reads `raw.function.name` and `raw.function.parameters` off
|
|
423
|
-
// each entry, i.e. the OpenAI envelope, and rebuilds it as
|
|
424
|
-
// `{ name, description, input_schema }`. Sending Anthropic's own shape from the
|
|
425
|
-
// client would leave `.function` undefined, so every tool would go upstream
|
|
426
|
-
// with a blank name and an empty schema. The envelope belongs to the ROUTE's
|
|
427
|
-
// request contract, not to the provider it happens to POST to — which is
|
|
428
|
-
// exactly why this is declared here rather than derived from `streamFormat`.
|
|
429
|
-
clientToolFormat: 'openai',
|
|
430
|
-
// Nothing to install. The route hand-rolls the Messages API over global
|
|
431
|
-
// `fetch` and its own re-framer, so it imports no module — not even
|
|
432
|
-
// @anthropic-ai/sdk.
|
|
433
|
-
deps: { npm: [], pip: [] },
|
|
434
|
-
// The route reads ANTHROPIC_API_KEY into an `x-api-key` header. Note the
|
|
435
|
-
// header is NOT `Authorization`, which is why the schema's detector matches
|
|
436
|
-
// both spellings.
|
|
437
|
-
keyExposure: 'needs-proxy',
|
|
438
|
-
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
439
|
-
outOfBand: 'none',
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
export default anthropic;
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import type { Integration } from '../types';
|
|
2
|
-
|
|
3
|
-
const cloudflare: Integration = {
|
|
4
|
-
id: 'cloudflare',
|
|
5
|
-
title: 'Cloudflare AI',
|
|
6
|
-
category: 'provider',
|
|
7
|
-
language: 'ts',
|
|
8
|
-
streamFormat: 'openai-sse',
|
|
9
|
-
envVars: ['CF_ACCOUNT_ID', 'CF_API_TOKEN'],
|
|
10
|
-
routeTemplates: {
|
|
11
|
-
// Kept as a framework-specific template because it cannot be expressed as a
|
|
12
|
-
// portable handler: `env.AI` is a Worker binding, and only a Worker has one.
|
|
13
|
-
// Everything else uses `webRoute` below.
|
|
14
|
-
worker: `// Worker handler: env.AI is bound in wrangler.toml
|
|
15
|
-
// env.AI.run emits Cloudflare-native SSE (data: {"response":"<token>"}).
|
|
16
|
-
// The TransformStream below re-frames each chunk to OpenAI-format SSE so
|
|
17
|
-
// readOpenAIStream reads it without any client-side changes.
|
|
18
|
-
import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* What the front end POSTs. \`req.json()\` is \`unknown\` (it is whatever the
|
|
22
|
-
* client sent), so the body is narrowed once here instead of at every use —
|
|
23
|
-
* without it this Worker does not compile. Widen it as you add fields.
|
|
24
|
-
*/
|
|
25
|
-
type ChatRequestBody = { messages: OpenAIWireMessage[] };
|
|
26
|
-
|
|
27
|
-
class ChatRequestError extends Error {
|
|
28
|
-
constructor(readonly status: number, message: string) { super(message); }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a
|
|
32
|
-
* missing messages array is a ChatRequestError with a status — NEVER an
|
|
33
|
-
* unhandled SyntaxError (findings F-10). */
|
|
34
|
-
async function readChatRequest(request: Request): Promise<ChatRequestBody> {
|
|
35
|
-
if (request.method !== 'POST') {
|
|
36
|
-
throw new ChatRequestError(405, \`Method \${request.method} not allowed — POST /api/chat.\`);
|
|
37
|
-
}
|
|
38
|
-
let parsed: unknown;
|
|
39
|
-
try { parsed = await request.json(); } catch {
|
|
40
|
-
throw new ChatRequestError(400, 'Request body is not valid JSON.');
|
|
41
|
-
}
|
|
42
|
-
const body = parsed as ChatRequestBody;
|
|
43
|
-
if (!Array.isArray(body?.messages)) {
|
|
44
|
-
throw new ChatRequestError(400, 'Request body must carry a messages array.');
|
|
45
|
-
}
|
|
46
|
-
return body;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Map a guard rejection to the Response its status demands; rethrow anything
|
|
50
|
-
* else — an unexpected error should be loud, not laundered into a 400. */
|
|
51
|
-
function toChatErrorResponse(error: unknown): Response {
|
|
52
|
-
if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default {
|
|
57
|
-
async fetch(req: Request, env: Env): Promise<Response> {
|
|
58
|
-
let chatBody: ChatRequestBody;
|
|
59
|
-
try {
|
|
60
|
-
chatBody = await readChatRequest(req);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
return toChatErrorResponse(error);
|
|
63
|
-
}
|
|
64
|
-
const { messages } = chatBody;
|
|
65
|
-
|
|
66
|
-
let nativeStream: ReadableStream<Uint8Array>;
|
|
67
|
-
try {
|
|
68
|
-
// env.AI.run is typed to return the non-streaming shape; \`stream: true\`
|
|
69
|
-
// makes it a ReadableStream at runtime, which the types cannot express, so
|
|
70
|
-
// the hop through \`unknown\` is required rather than cosmetic.
|
|
71
|
-
nativeStream = (await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
72
|
-
messages,
|
|
73
|
-
stream: true,
|
|
74
|
-
})) as unknown as ReadableStream<Uint8Array>;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
// A REAL status, before a byte is streamed. Returning 200 here would send
|
|
77
|
-
// a JSON error labelled text/event-stream: the reader finds no frame and
|
|
78
|
-
// the turn resolves empty, with nothing logged and no bubble.
|
|
79
|
-
return new Response(
|
|
80
|
-
JSON.stringify({ error: { message: err instanceof Error ? err.message : 'Workers AI call failed' } }),
|
|
81
|
-
{ status: 502, headers: { 'Content-Type': 'application/json' } },
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Re-frame Cloudflare-native SSE → OpenAI-format SSE
|
|
86
|
-
const { readable, writable } = new TransformStream();
|
|
87
|
-
const writer = writable.getWriter();
|
|
88
|
-
const encoder = new TextEncoder();
|
|
89
|
-
const decoder = new TextDecoder();
|
|
90
|
-
|
|
91
|
-
(async () => {
|
|
92
|
-
const reader = nativeStream.getReader();
|
|
93
|
-
let buffer = '';
|
|
94
|
-
try {
|
|
95
|
-
while (true) {
|
|
96
|
-
const { value, done } = await reader.read();
|
|
97
|
-
if (done) break;
|
|
98
|
-
buffer += decoder.decode(value, { stream: true });
|
|
99
|
-
const lines = buffer.split('\\n');
|
|
100
|
-
buffer = lines.pop() ?? '';
|
|
101
|
-
for (const line of lines) {
|
|
102
|
-
const s = line.trim();
|
|
103
|
-
if (!s.startsWith('data:')) continue;
|
|
104
|
-
const payload = s.slice(5).trim();
|
|
105
|
-
if (payload === '[DONE]') continue;
|
|
106
|
-
try {
|
|
107
|
-
const { response } = JSON.parse(payload) as { response?: string };
|
|
108
|
-
if (response == null) continue;
|
|
109
|
-
const openaiChunk = JSON.stringify({ choices: [{ delta: { content: response } }] });
|
|
110
|
-
await writer.write(encoder.encode(\`data: \${openaiChunk}\\n\\n\`));
|
|
111
|
-
} catch { /* skip malformed lines */ }
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
} catch (err) {
|
|
115
|
-
// The response is already streaming, so the status is spent: report the
|
|
116
|
-
// failure IN BAND. readOpenAIStream lands this on turn.error and keeps
|
|
117
|
-
// whatever already streamed.
|
|
118
|
-
const message = err instanceof Error ? err.message : 'Workers AI stream failed';
|
|
119
|
-
await writer.write(encoder.encode(\`data: \${JSON.stringify({ error: { message } })}\\n\\n\`));
|
|
120
|
-
} finally {
|
|
121
|
-
// Always: an unclosed writer leaves the browser waiting forever.
|
|
122
|
-
await writer.write(encoder.encode('data: [DONE]\\n\\n'));
|
|
123
|
-
await writer.close();
|
|
124
|
-
}
|
|
125
|
-
})();
|
|
126
|
-
|
|
127
|
-
return new Response(readable, {
|
|
128
|
-
status: 200,
|
|
129
|
-
headers: {
|
|
130
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
131
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
},
|
|
135
|
-
};`,
|
|
136
|
-
},
|
|
137
|
-
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
138
|
-
// Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
|
|
139
|
-
let chatBody: ChatRequestBody;
|
|
140
|
-
try {
|
|
141
|
-
chatBody = await readChatRequest(request);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return toChatErrorResponse(error);
|
|
144
|
-
}
|
|
145
|
-
const { messages } = chatBody;
|
|
146
|
-
|
|
147
|
-
const upstream = await fetch(
|
|
148
|
-
\`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
|
|
149
|
-
{
|
|
150
|
-
method: 'POST',
|
|
151
|
-
headers: {
|
|
152
|
-
Authorization: \`Bearer \${process.env.CF_API_TOKEN}\`,
|
|
153
|
-
'Content-Type': 'application/json',
|
|
154
|
-
},
|
|
155
|
-
body: JSON.stringify({
|
|
156
|
-
model: '@cf/meta/llama-3.1-8b-instruct',
|
|
157
|
-
messages,
|
|
158
|
-
stream: true,
|
|
159
|
-
}),
|
|
160
|
-
},
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// FORWARD THE STATUS. A wrong CF_API_TOKEN is a 401/403: returning 200 sends
|
|
164
|
-
// its JSON body out labelled text/event-stream, the SSE reader finds no frame,
|
|
165
|
-
// and the turn resolves empty with nothing logged and no bubble.
|
|
166
|
-
if (!upstream.ok) {
|
|
167
|
-
return new Response(await upstream.text(), {
|
|
168
|
-
status: upstream.status,
|
|
169
|
-
headers: {
|
|
170
|
-
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
if (!upstream.body) {
|
|
175
|
-
return new Response(JSON.stringify({ error: { message: 'Workers AI returned no body to stream.' } }), {
|
|
176
|
-
status: 502,
|
|
177
|
-
headers: { 'Content-Type': 'application/json' },
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Workers AI returns OpenAI-format SSE. Pass it straight through.
|
|
182
|
-
return new Response(upstream.body, {
|
|
183
|
-
status: 200,
|
|
184
|
-
headers: {
|
|
185
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
186
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
187
|
-
Connection: 'keep-alive',
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}`,
|
|
191
|
-
streamMapping:
|
|
192
|
-
"Workers AI via the OpenAI-compatible HTTP endpoint returns OpenAI-format SSE. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning. The native env.AI binding streams Cloudflare's own format (data: {\"response\":\"...token...\"}); the worker route template re-frames these chunks to OpenAI-format SSE via a TransformStream before returning.",
|
|
193
|
-
runNote: 'Set CF_ACCOUNT_ID and CF_API_TOKEN. Model ids are prefixed with @cf/, e.g. @cf/meta/llama-3.1-8b-instruct. For the AI binding (worker key), add an [ai] block with binding = "AI" in wrangler.toml.',
|
|
194
|
-
docsSlug: 'integrations/cloudflare-ai',
|
|
195
|
-
// Nothing. Both routes pin '@cf/meta/llama-3.1-8b-instruct' (the generic
|
|
196
|
-
// 'openai/gpt-4o-mini' a scaffold used to default to is not even a valid
|
|
197
|
-
// Workers AI id), and the worker route's re-framing reader forwards only the
|
|
198
|
-
// text `response` field, so a tool call could not reach the browser anyway.
|
|
199
|
-
forwardsFromClient: [],
|
|
200
|
-
// Nothing. The webRoute is global `fetch`, and the worker template's only
|
|
201
|
-
// import is a type from '@kitn.ai/ui/wire' — the kit itself, which every
|
|
202
|
-
// scaffold already depends on. Its `Env` is an ambient wrangler type, not an
|
|
203
|
-
// import, and the Worker skeleton owns wrangler/@cloudflare/workers-types.
|
|
204
|
-
deps: { npm: [], pip: [] },
|
|
205
|
-
// The webRoute reads CF_API_TOKEN into an `Authorization: Bearer` header.
|
|
206
|
-
//
|
|
207
|
-
// The two routes differ and the stricter one governs: the worker template uses
|
|
208
|
-
// the `env.AI` binding and holds no token in code at all, but a binding is a
|
|
209
|
-
// Worker capability, not a browser one. Either way a server is required.
|
|
210
|
-
keyExposure: 'needs-proxy',
|
|
211
|
-
// Workers AI is a remote HTTPS endpoint (the REST route) or a platform binding
|
|
212
|
-
// (the worker route) — either way there is nothing on the developer's machine
|
|
213
|
-
// to install or run. `wrangler` is a devDependency of a Worker app, not an
|
|
214
|
-
// out-of-band prerequisite of this integration.
|
|
215
|
-
outOfBand: 'none',
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
export default cloudflare;
|