@kitn.ai/ui 0.21.0 → 0.22.1
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/dist/components/chat-thread.d.ts +9 -0
- package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
- package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
- package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
- package/dist/custom-elements.json +43 -7
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
- package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
- package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
- package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
- package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
- package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
- package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
- package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
- package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
- package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
- package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
- package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
- package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
- package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
- package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
- package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
- package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
- package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
- package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
- package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
- package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
- package/dist/elements/chunks/context-5qgypNHC.js +1 -0
- package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
- package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
- package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
- package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
- package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
- package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
- package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
- package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
- package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
- package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
- package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
- package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
- package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
- package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
- package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
- package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
- package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
- package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
- package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
- package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
- package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
- package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
- package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
- package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
- package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
- package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
- package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
- package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
- package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
- package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
- package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
- package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
- package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
- package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
- package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
- package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
- package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
- package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
- package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
- package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
- package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
- package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
- package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
- package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
- package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
- package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
- package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
- package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
- package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
- package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
- package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
- package/dist/elements/chunks/x-_-OPHept.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +27 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slot-text.d.ts +51 -0
- package/dist/elements/slots.d.ts +5 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +12 -6
- package/dist/index.js +1535 -1487
- package/dist/index.server.js +2330 -2296
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +12 -3
- package/dist/mcp.es.js +890 -78
- package/dist/react/index.d.ts +13 -3
- package/dist/react.js +14 -14
- package/dist/register-impl-AxIlqjo7.js +145 -0
- package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
- package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
- package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
- package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
- package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
- package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
- package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
- package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
- package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
- package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
- package/dist/wire/encode.d.ts +79 -6
- package/dist/wire/files.d.ts +116 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/media-types.d.ts +106 -0
- package/dist/wire.js +596 -368
- package/frameworks/react/index.tsx +9 -5
- package/llms-full.txt +12 -3
- package/package.json +4 -1
- package/src/agent-tooling/archetypes.ts +43 -12
- package/src/agent-tooling/integrations/anthropic.ts +29 -5
- package/src/agent-tooling/integrations/cloudflare.ts +5 -0
- package/src/agent-tooling/integrations/langgraph.ts +13 -0
- package/src/agent-tooling/integrations/mastra.ts +46 -5
- package/src/agent-tooling/integrations/mock.ts +4 -0
- package/src/agent-tooling/integrations/ollama.ts +7 -0
- package/src/agent-tooling/integrations/openai.ts +2 -0
- package/src/agent-tooling/integrations/openrouter.ts +2 -0
- package/src/agent-tooling/integrations/pi.ts +26 -1
- package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
- package/src/agent-tooling/mcp/tools/scaffold.ts +913 -32
- package/src/agent-tooling/registry.ts +57 -0
- package/src/agent-tooling/types.ts +132 -0
- package/src/components/chat-thread.tsx +10 -1
- package/src/components/context.tsx +18 -12
- package/src/components/conversation-item.tsx +6 -0
- package/src/components/model-switcher.tsx +38 -8
- package/src/components/toast.tsx +6 -1
- package/src/elements/button.tsx +26 -3
- package/src/elements/chat.tsx +28 -2
- package/src/elements/default-input.tsx +128 -5
- package/src/elements/element-meta.json +30 -4
- package/src/elements/element-types.d.ts +12 -6
- package/src/elements/menu.tsx +30 -3
- package/src/elements/slot-text.ts +73 -0
- package/src/elements/slots.ts +10 -0
- package/src/index.ts +9 -5
- package/src/wire/encode.ts +234 -14
- package/src/wire/files.ts +342 -0
- package/src/wire/index.ts +21 -0
- package/src/wire/media-types.ts +344 -0
- package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
- package/dist/elements/chunks/check-C8desc_h.js +0 -1
- package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
- package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
- package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
- package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
- package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
- package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
- package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
- package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
- package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
- package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
- package/dist/register-impl-bzrZ44ft.js +0 -145
package/src/wire/encode.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// No provider SDK, no fetch, no DOM. Pure functions over the content model.
|
|
6
6
|
import type { ChatMessage, MessagePart } from '../elements/chat-types';
|
|
7
7
|
import type { ToolPart } from '../components/tool-types';
|
|
8
|
+
import { classifyAttachment, textFileContent, type ClassifiedFile } from './files';
|
|
9
|
+
import { resolveMediaPolicy, type MediaPolicy, type MediaTypeFilter } from './media-types';
|
|
8
10
|
|
|
9
11
|
export interface OpenAIToolCall {
|
|
10
12
|
id: string;
|
|
@@ -18,9 +20,20 @@ export interface OpenAIToolCall {
|
|
|
18
20
|
* to have seen. */
|
|
19
21
|
export type OpenAIReasoningDetail = Record<string, unknown>;
|
|
20
22
|
|
|
23
|
+
/** A multimodal user message's content entries. `image_url` takes an https URL
|
|
24
|
+
* or a `data:` URI in the same field; `file` takes `file_data`, which is a DATA
|
|
25
|
+
* URI on this wire (`data:application/pdf;base64,...`) and not bare base64. */
|
|
26
|
+
export type OpenAIContentPart =
|
|
27
|
+
| { type: 'text'; text: string }
|
|
28
|
+
| { type: 'image_url'; image_url: { url: string } }
|
|
29
|
+
| { type: 'file'; file: { filename?: string; file_data: string } };
|
|
30
|
+
|
|
21
31
|
export interface OpenAIWireMessage {
|
|
22
32
|
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
23
|
-
|
|
33
|
+
/** An ARRAY only when the turn carries an encodable `file` part. A text-only
|
|
34
|
+
* turn stays a plain string, so adding attachment support changed nothing
|
|
35
|
+
* about what an existing thread puts on the wire. */
|
|
36
|
+
content: string | OpenAIContentPart[] | null;
|
|
24
37
|
tool_calls?: OpenAIToolCall[];
|
|
25
38
|
tool_call_id?: string;
|
|
26
39
|
name?: string;
|
|
@@ -29,7 +42,10 @@ export interface OpenAIWireMessage {
|
|
|
29
42
|
reasoning_details?: OpenAIReasoningDetail[];
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
/** EXTENDS rather than restates the file options: `onUnencodableFile` and
|
|
46
|
+
* `accept` mean the same thing on both wires, and a second declaration of them
|
|
47
|
+
* here is a second place to forget to update. */
|
|
48
|
+
export interface OpenAIEncodeOptions extends FileEncodeOptions {
|
|
33
49
|
/**
|
|
34
50
|
* Whether to send the assistant's own reasoning back with the thread.
|
|
35
51
|
*
|
|
@@ -54,6 +70,40 @@ export interface OpenAIEncodeOptions {
|
|
|
54
70
|
reasoning?: 'omit' | 'include';
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What to do with a `file` part this wire cannot carry.
|
|
75
|
+
*
|
|
76
|
+
* DEFAULT `'throw'`, and the default is the whole point. Skipping is how
|
|
77
|
+
* attachments came to render perfectly in the thread and reach the model as
|
|
78
|
+
* nothing: the developer wires up upload, watches it work, and ships a model
|
|
79
|
+
* that cannot see the file. A throw here names the message, the part and the
|
|
80
|
+
* reason, which is strictly more than a 400 at request time would tell you.
|
|
81
|
+
*
|
|
82
|
+
* `'skip'` restores the lenient behaviour for a host that would rather send a
|
|
83
|
+
* degraded turn than fail one. It is silent, but it is silence the developer
|
|
84
|
+
* asked for by name, which is the difference that matters.
|
|
85
|
+
*/
|
|
86
|
+
export type UnencodableFilePolicy = 'throw' | 'skip';
|
|
87
|
+
|
|
88
|
+
export interface FileEncodeOptions {
|
|
89
|
+
onUnencodableFile?: UnencodableFilePolicy;
|
|
90
|
+
/**
|
|
91
|
+
* Narrow which attachment media types reach the wire, as HTML `accept` syntax
|
|
92
|
+
* (`'image/*,application/pdf'`) or an array of the same.
|
|
93
|
+
*
|
|
94
|
+
* THE SAME STRING the composer takes as `<kai-chat accept="...">`, resolved by
|
|
95
|
+
* the same function against the same declaration -- so a developer writes the
|
|
96
|
+
* set once as a constant and hands it to both ends. Omitted means the kit's
|
|
97
|
+
* full capability set, which is `encodableMediaTypes()`.
|
|
98
|
+
*
|
|
99
|
+
* It can only NARROW. Naming a type the encoders cannot represent does not
|
|
100
|
+
* enable it; that would just move the failure to a provider 400.
|
|
101
|
+
*/
|
|
102
|
+
accept?: MediaTypeFilter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type AnthropicEncodeOptions = FileEncodeOptions;
|
|
106
|
+
|
|
57
107
|
/** Anthropic content blocks are an open, provider-owned union. Keeping them as
|
|
58
108
|
* records is what lets a verbatim `thinking` payload pass through UNTOUCHED,
|
|
59
109
|
* which is the entire point of this encoder. */
|
|
@@ -93,12 +143,112 @@ type SettledTool = ToolPart & { toolCallId: string };
|
|
|
93
143
|
|
|
94
144
|
const isTextPart = (p: MessagePart): p is TextPart => p.type === 'text';
|
|
95
145
|
|
|
146
|
+
// lint-silent-drops: drops reasoning,tool,card,source,file -- text projection by name and contract, used for the plain-string form of a turn; every caller that needs the other variants encodes them itself before falling back here.
|
|
96
147
|
const textOf = (parts: MessagePart[]): string =>
|
|
97
148
|
parts
|
|
98
149
|
.filter(isTextPart)
|
|
99
150
|
.map((p) => p.text)
|
|
100
151
|
.join('');
|
|
101
152
|
|
|
153
|
+
type FilePart = Extract<MessagePart, { type: 'file' }>;
|
|
154
|
+
|
|
155
|
+
/** A wire either shapes a classified file into a block, or explains why it
|
|
156
|
+
* cannot. `ok: false` is NOT the same as an unencodable attachment: the
|
|
157
|
+
* attachment can be perfectly sound and still have no form on this particular
|
|
158
|
+
* wire, which is exactly the remote-PDF case on OpenAI. */
|
|
159
|
+
type WireFileResult<T> = { ok: true; block: T } | { ok: false; reason: string };
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* One `file` part to one wire block, or `null` when it is skipped.
|
|
163
|
+
*
|
|
164
|
+
* Skipped means one of two things and never a third: the attachment is kit-side
|
|
165
|
+
* (a citation chip), or the host explicitly opted into `onUnencodableFile:
|
|
166
|
+
* 'skip'`. Everything else throws, because a dropped attachment is invisible in
|
|
167
|
+
* exactly the way the developer needs it not to be.
|
|
168
|
+
*/
|
|
169
|
+
function encodeFilePart<T>(
|
|
170
|
+
part: FilePart,
|
|
171
|
+
messageId: string,
|
|
172
|
+
partIndex: number,
|
|
173
|
+
policy: UnencodableFilePolicy,
|
|
174
|
+
media: MediaPolicy,
|
|
175
|
+
toBlock: (file: ClassifiedFile) => WireFileResult<T>,
|
|
176
|
+
): T | null {
|
|
177
|
+
const classified = classifyAttachment(part.attachment, media);
|
|
178
|
+
if (classified.status === 'kit-side') return null;
|
|
179
|
+
|
|
180
|
+
const refuse = (reason: string): null => {
|
|
181
|
+
if (policy === 'skip') return null;
|
|
182
|
+
throw new WireEncodeError(
|
|
183
|
+
`Cannot encode file part ${partIndex} of message "${messageId}": ${reason} Pass { onUnencodableFile: 'skip' } to drop it instead of failing.`,
|
|
184
|
+
messageId,
|
|
185
|
+
partIndex,
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (classified.status === 'unencodable') return refuse(classified.reason);
|
|
190
|
+
|
|
191
|
+
const shaped = toBlock(classified.file);
|
|
192
|
+
return shaped.ok ? shaped.block : refuse(shaped.reason);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `image_url` takes an https URL and a `data:` URI in the same field, so an
|
|
196
|
+
* image is one shape here. A PDF is not: `file_data` is base64-only. A text
|
|
197
|
+
* file is neither: it becomes ordinary text content, because that is the only
|
|
198
|
+
* thing either API's block set can express it as. */
|
|
199
|
+
const openAIFileBlock = (file: ClassifiedFile): WireFileResult<OpenAIContentPart> => {
|
|
200
|
+
if (file.kind === 'text') {
|
|
201
|
+
return { ok: true, block: { type: 'text', text: textFileContent(file) } };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (file.kind === 'image') {
|
|
205
|
+
const url = file.source.type === 'base64' ? file.source.dataUri : file.source.url;
|
|
206
|
+
return { ok: true, block: { type: 'image_url', image_url: { url } } };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (file.source.type === 'remote') {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: `it is a PDF at a remote URL ("${file.source.url}"), and a chat-completions \`file\` content part carries only \`file_id\` or \`file_data\` -- this wire has no URL form for a document. Fetch it and stage it as a \`data:\` URI, or upload it through the Files API and send \`file_id\` yourself; fetching it HERE would put I/O in the encoder. The Anthropic wire does take a remote PDF, so toAnthropicMessages accepts this same message.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
block: {
|
|
219
|
+
type: 'file',
|
|
220
|
+
file: {
|
|
221
|
+
...(file.filename !== undefined ? { filename: file.filename } : {}),
|
|
222
|
+
file_data: file.source.dataUri,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** The image and document blocks take the same two source shapes, which is why
|
|
229
|
+
* this wire has no gap where OpenAI has one. Text is the third case and is not
|
|
230
|
+
* a `document` block: this API's content-block set is text / image / document
|
|
231
|
+
* with no arbitrary-file member, so an inlined text file IS a text block.
|
|
232
|
+
* (A `document` block CAN carry text via the Files API, which would buy
|
|
233
|
+
* citations -- see the design note; it needs an upload step this layer cannot
|
|
234
|
+
* make, because it does no I/O.) */
|
|
235
|
+
const anthropicFileBlock = (file: ClassifiedFile): WireFileResult<AnthropicContentBlock> => {
|
|
236
|
+
if (file.kind === 'text') {
|
|
237
|
+
return { ok: true, block: { type: 'text', text: textFileContent(file) } };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
block: {
|
|
243
|
+
type: file.kind === 'image' ? 'image' : 'document',
|
|
244
|
+
source:
|
|
245
|
+
file.source.type === 'base64'
|
|
246
|
+
? { type: 'base64', media_type: file.source.mediaType, data: file.source.data }
|
|
247
|
+
: { type: 'url', url: file.source.url },
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
102
252
|
/** A tool is encodable only once it has a RESULT. Both APIs require every echoed
|
|
103
253
|
* tool call to have exactly one matching result, so a call with no answer yet is
|
|
104
254
|
* skipped entirely rather than sent half-formed. In a real loop the host
|
|
@@ -260,21 +410,69 @@ function reasoningDetailOf(part: ReasoningPart): OpenAIReasoningDetail | undefin
|
|
|
260
410
|
* text and no settled tool is skipped exactly as before, rather than becoming
|
|
261
411
|
* `{ content: null }` with no `tool_calls`.
|
|
262
412
|
*
|
|
263
|
-
* `card
|
|
264
|
-
*
|
|
265
|
-
*
|
|
413
|
+
* `card` and `source` parts are never encoded; they are kit-side.
|
|
414
|
+
*
|
|
415
|
+
* `file` parts ARE encoded, on a USER turn, and a turn carrying nothing but an
|
|
416
|
+
* attachment is now a real message rather than nothing. Images become
|
|
417
|
+
* `image_url` (https URL or `data:` URI alike); a base64 PDF becomes a `file`
|
|
418
|
+
* part whose `file_data` is the data URI. Two cases have no form here and THROW
|
|
419
|
+
* by default: a remote PDF, because this wire's `file` part has no URL variant,
|
|
420
|
+
* and anything that is neither -- see `UnencodableFilePolicy` for why the
|
|
421
|
+
* default is a throw and not a skip.
|
|
422
|
+
*
|
|
423
|
+
* A `file` part on an ASSISTANT turn is still dropped. Neither API accepts image
|
|
424
|
+
* or document content in an assistant message, so there is nothing to encode it
|
|
425
|
+
* to; attachments belong to the user turn that sent them.
|
|
266
426
|
*/
|
|
427
|
+
// lint-silent-drops: drops card,source -- kit-side parts with no OpenAI wire representation; cards come from tool calls, which ARE encoded, and sources are UI citations.
|
|
267
428
|
export function toOpenAIMessages(
|
|
268
429
|
messages: ChatMessage[],
|
|
269
430
|
options: OpenAIEncodeOptions = {},
|
|
270
431
|
): OpenAIWireMessage[] {
|
|
271
432
|
const includeReasoning = options.reasoning === 'include';
|
|
433
|
+
const filePolicy = options.onUnencodableFile ?? 'throw';
|
|
434
|
+
const media = resolveMediaPolicy({ accept: options.accept });
|
|
272
435
|
const out: OpenAIWireMessage[] = [];
|
|
273
436
|
|
|
274
437
|
for (const message of messages) {
|
|
275
438
|
if (message.role === 'user') {
|
|
276
|
-
|
|
277
|
-
|
|
439
|
+
// Built in PART ORDER, with runs of adjacent text merged into one entry.
|
|
440
|
+
// A file authored after the text stays after it: `parts` is an ordered
|
|
441
|
+
// content model and reordering it here would be the encoder second-guessing
|
|
442
|
+
// the author. Anthropic's docs do suggest putting images before text for
|
|
443
|
+
// best results, which is guidance for whoever builds the parts array.
|
|
444
|
+
const content: OpenAIContentPart[] = [];
|
|
445
|
+
let buffered = '';
|
|
446
|
+
let carriesFile = false;
|
|
447
|
+
|
|
448
|
+
const flushText = (): void => {
|
|
449
|
+
if (buffered === '') return;
|
|
450
|
+
content.push({ type: 'text', text: buffered });
|
|
451
|
+
buffered = '';
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no OpenAI wire form.
|
|
455
|
+
message.parts.forEach((part, partIndex) => {
|
|
456
|
+
if (part.type === 'text') {
|
|
457
|
+
buffered += part.text;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (part.type !== 'file') return;
|
|
461
|
+
const block = encodeFilePart(part, message.id, partIndex, filePolicy, media, openAIFileBlock);
|
|
462
|
+
if (!block) return;
|
|
463
|
+
flushText();
|
|
464
|
+
content.push(block);
|
|
465
|
+
carriesFile = true;
|
|
466
|
+
});
|
|
467
|
+
flushText();
|
|
468
|
+
|
|
469
|
+
// No file survived, so this is an ordinary turn and stays a plain string.
|
|
470
|
+
if (!carriesFile) {
|
|
471
|
+
const text = textOf(message.parts);
|
|
472
|
+
if (text !== '') out.push({ role: 'user', content: text });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
out.push({ role: 'user', content });
|
|
278
476
|
continue;
|
|
279
477
|
}
|
|
280
478
|
|
|
@@ -367,11 +565,23 @@ export function toOpenAIMessages(
|
|
|
367
565
|
* tool_result message always follows its assistant message and can never be
|
|
368
566
|
* appended after a plain user turn.
|
|
369
567
|
*
|
|
568
|
+
* `file` parts on a USER turn become `image` and `document` blocks, in part
|
|
569
|
+
* order. Both take `source: {type:'base64'}` and `source: {type:'url'}`, so this
|
|
570
|
+
* wire can carry a remote PDF that `toOpenAIMessages` has to refuse. Anything
|
|
571
|
+
* neither API accepts as message content THROWS by default; see
|
|
572
|
+
* `UnencodableFilePolicy`. A `file` part on an ASSISTANT turn is dropped, because
|
|
573
|
+
* an assistant message here carries only text, thinking and tool_use.
|
|
574
|
+
*
|
|
370
575
|
* Asymmetry worth knowing: `tool_use.input` is a parsed OBJECT on this wire, not
|
|
371
576
|
* a string, so it uses `input` and not `rawInput`. Only thinking blocks carry a
|
|
372
577
|
* verbatim requirement.
|
|
373
578
|
*/
|
|
374
|
-
export function toAnthropicMessages(
|
|
579
|
+
export function toAnthropicMessages(
|
|
580
|
+
messages: ChatMessage[],
|
|
581
|
+
options: AnthropicEncodeOptions = {},
|
|
582
|
+
): AnthropicWireMessage[] {
|
|
583
|
+
const filePolicy = options.onUnencodableFile ?? 'throw';
|
|
584
|
+
const media = resolveMediaPolicy({ accept: options.accept });
|
|
375
585
|
const out: AnthropicWireMessage[] = [];
|
|
376
586
|
|
|
377
587
|
/** Append to the trailing user message when there is one, so the wire never
|
|
@@ -384,10 +594,18 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
|
|
|
384
594
|
|
|
385
595
|
for (const message of messages) {
|
|
386
596
|
if (message.role === 'user') {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
597
|
+
// Part order, for the same reason as `toOpenAIMessages`.
|
|
598
|
+
const userBlocks: AnthropicContentBlock[] = [];
|
|
599
|
+
// lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no Anthropic wire form.
|
|
600
|
+
message.parts.forEach((part, partIndex) => {
|
|
601
|
+
if (part.type === 'text') {
|
|
602
|
+
if (part.text !== '') userBlocks.push({ type: 'text', text: part.text });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (part.type !== 'file') return;
|
|
606
|
+
const block = encodeFilePart(part, message.id, partIndex, filePolicy, media, anthropicFileBlock);
|
|
607
|
+
if (block) userBlocks.push(block);
|
|
608
|
+
});
|
|
391
609
|
if (userBlocks.length > 0) pushUser(userBlocks);
|
|
392
610
|
continue;
|
|
393
611
|
}
|
|
@@ -405,6 +623,7 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
|
|
|
405
623
|
results = [];
|
|
406
624
|
};
|
|
407
625
|
|
|
626
|
+
// lint-silent-drops: drops card,source,file -- cards and sources are kit-side UI; a file part on an ASSISTANT turn has no Anthropic representation, since the API takes image and document content on user turns only.
|
|
408
627
|
message.parts.forEach((part, partIndex) => {
|
|
409
628
|
switch (part.type) {
|
|
410
629
|
case 'reasoning': {
|
|
@@ -461,8 +680,9 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
|
|
|
461
680
|
break;
|
|
462
681
|
}
|
|
463
682
|
default:
|
|
464
|
-
// card
|
|
465
|
-
//
|
|
683
|
+
// card and source are kit-side. A `file` part reaches this arm only on
|
|
684
|
+
// an ASSISTANT turn, where the API accepts no image or document
|
|
685
|
+
// content at all -- user attachments are handled in the user branch.
|
|
466
686
|
break;
|
|
467
687
|
}
|
|
468
688
|
});
|