akanjs 3.0.0-beta.2 → 3.0.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.3",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -58,6 +58,18 @@ export class AnthropicLlm
58
58
  */
59
59
  static readonly defaultMaxTokens = 8192;
60
60
 
61
+ /**
62
+ * The four the API's image block reads. An exact set rather than an `image/*` prefix, because by the time an
63
+ * attachment reaches here `accepts.image` has already carried it past `AgentService.readable`: a phone's
64
+ * `image/heic` — the iPhone camera default, so the likeliest non-canonical image an app sees — arrives as bytes,
65
+ * becomes a block the API refuses, and takes the **whole turn** down on a 400 rather than going unread.
66
+ *
67
+ * The app cannot gate it either: `AttachReader` answers `null` for "not mine", which falls through to the
68
+ * built-in reader that base64s any `image/*`, so there is no way for a reader to refuse one. The check belongs
69
+ * where the block vocabulary is known, which is here.
70
+ */
71
+ static readonly imageTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
72
+
61
73
  get #host() {
62
74
  return this.llmOption.host ?? "https://api.anthropic.com/v1";
63
75
  }
@@ -266,9 +278,13 @@ export class AnthropicLlm
266
278
  ];
267
279
  const source = AnthropicLlm.sourceOf(attachment);
268
280
  if (!source) return [];
269
- if (accepts?.image && attachment.mimeType.startsWith("image/")) return [{ type: "image", source }];
281
+
282
+ const mimeType = attachment.mimeType.split(";")[0].trim().toLowerCase();
283
+ if (accepts?.image && AnthropicLlm.imageTypes.has(mimeType))
284
+ return [{ type: "image", source: AnthropicLlm.typed(source, mimeType) }];
270
285
 
271
- if (accepts?.document && attachment.mimeType === "application/pdf") return [{ type: "document", source }];
286
+ if (accepts?.document && mimeType === "application/pdf")
287
+ return [{ type: "document", source: AnthropicLlm.typed(source, mimeType) }];
272
288
  notes.push(`[Attachment not read: ${attachment.name} (${attachment.mimeType}) — this API has no block for it.]`);
273
289
  return [];
274
290
  });
@@ -276,6 +292,11 @@ export class AnthropicLlm
276
292
  return [...(text ? [{ type: "text" as const, text }] : []), ...blocks];
277
293
  }
278
294
 
295
+ /** The block's `media_type` is the essence, not whatever parameters the browser attached to it. */
296
+ static typed(source: AnthropicSource, mimeType: string): AnthropicSource {
297
+ return source.type === "base64" ? { ...source, media_type: mimeType } : source;
298
+ }
299
+
279
300
  static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null {
280
301
  if (attachment.url) return { type: "url", url: attachment.url };
281
302
  if (attachment.data) return { type: "base64", media_type: attachment.mimeType, data: attachment.data };
@@ -34,6 +34,14 @@ export interface OpenaiMessage {
34
34
  * read to a note in the text.
35
35
  */
36
36
  export class OpenaiDialect {
37
+ /**
38
+ * The types this dialect's image part reads. Exact rather than an `image/*` prefix and declared apart from
39
+ * Anthropic's identical-looking set, because the two are each a provider's own list and only happen to agree:
40
+ * an unsupported one passed through is a refused *request*, not an unread attachment, so the safe direction is
41
+ * to name what is known to work and note the rest.
42
+ */
43
+ static readonly imageTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
44
+
37
45
  static requestBody(
38
46
  model: string,
39
47
  request: LlmTurnRequest,
@@ -118,16 +126,28 @@ export class OpenaiDialect {
118
126
  */
119
127
  static userContent(message: AgentWireMessage, accepts?: LlmAccepts): string | OpenaiContentPart[] {
120
128
  const attachments = message.attachments ?? [];
129
+ const notes: string[] = [];
121
130
  const blocks = attachments.flatMap((attachment) =>
122
131
  attachment.text ? [`--- attachment: ${attachment.name} (${attachment.mimeType}) ---\n${attachment.text}`] : [],
123
132
  );
124
- const text = [message.text, ...blocks].filter(Boolean).join("\n\n");
125
- if (!accepts?.image) return text;
126
- const images = attachments.flatMap((attachment) => {
127
- if (!attachment.mimeType.startsWith("image/")) return [];
128
- const url = attachment.url ?? (attachment.data ? `data:${attachment.mimeType};base64,${attachment.data}` : "");
129
- return url ? [{ type: "image_url" as const, image_url: { url } }] : [];
130
- });
133
+ const images = !accepts?.image
134
+ ? []
135
+ : attachments.flatMap((attachment) => {
136
+ if (attachment.text) return [];
137
+
138
+ const mimeType = attachment.mimeType.split(";")[0].trim().toLowerCase();
139
+
140
+ if (!mimeType.startsWith("image/")) return [];
141
+ if (!OpenaiDialect.imageTypes.has(mimeType)) {
142
+ notes.push(
143
+ `[Attachment not read: ${attachment.name} (${attachment.mimeType}) — this API reads no image of that type.]`,
144
+ );
145
+ return [];
146
+ }
147
+ const url = attachment.url ?? (attachment.data ? `data:${mimeType};base64,${attachment.data}` : "");
148
+ return url ? [{ type: "image_url" as const, image_url: { url } }] : [];
149
+ });
150
+ const text = [message.text, ...blocks, ...notes].filter(Boolean).join("\n\n");
131
151
  if (!images.length) return text;
132
152
  return [...(text ? [{ type: "text" as const, text }] : []), ...images];
133
153
  }
@@ -64,6 +64,17 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
64
64
  * model refusing, so it is `option.setLlm({ maxTokens })` and not a constant.
65
65
  */
66
66
  static readonly defaultMaxTokens = 8192;
67
+ /**
68
+ * The four the API's image block reads. An exact set rather than an `image/*` prefix, because by the time an
69
+ * attachment reaches here `accepts.image` has already carried it past `AgentService.readable`: a phone's
70
+ * `image/heic` — the iPhone camera default, so the likeliest non-canonical image an app sees — arrives as bytes,
71
+ * becomes a block the API refuses, and takes the **whole turn** down on a 400 rather than going unread.
72
+ *
73
+ * The app cannot gate it either: `AttachReader` answers `null` for "not mine", which falls through to the
74
+ * built-in reader that base64s any `image/*`, so there is no way for a reader to refuse one. The check belongs
75
+ * where the block vocabulary is known, which is here.
76
+ */
77
+ static readonly imageTypes: Set<string>;
67
78
  /** What the API's blocks carry. A model of the family that reads neither takes the `accepts` override. */
68
79
  get accepts(): LlmAccepts;
69
80
  chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
@@ -96,6 +107,8 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
96
107
  static providerMessages(messages: AgentWireMessage[], accepts?: LlmAccepts): AnthropicMessage[];
97
108
  static providerMessage(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicMessage;
98
109
  static userContent(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicBlock[];
110
+ /** The block's `media_type` is the essence, not whatever parameters the browser attached to it. */
111
+ static typed(source: AnthropicSource, mimeType: string): AnthropicSource;
99
112
  static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null;
100
113
  static turnAnswer(answer: AnthropicAnswer): LlmTurnAnswer;
101
114
  /** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
@@ -47,6 +47,13 @@ export interface OpenaiMessage {
47
47
  * read to a note in the text.
48
48
  */
49
49
  export declare class OpenaiDialect {
50
+ /**
51
+ * The types this dialect's image part reads. Exact rather than an `image/*` prefix and declared apart from
52
+ * Anthropic's identical-looking set, because the two are each a provider's own list and only happen to agree:
53
+ * an unsupported one passed through is a refused *request*, not an unread attachment, so the safe direction is
54
+ * to name what is known to work and note the rest.
55
+ */
56
+ static readonly imageTypes: Set<string>;
50
57
  static requestBody(model: string, request: LlmTurnRequest, { accepts, stream }?: {
51
58
  accepts?: LlmAccepts;
52
59
  stream?: boolean;
@@ -70,6 +70,11 @@ export interface ChatProps {
70
70
  * (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
71
71
  * cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
72
72
  * the built-in, so it can also replace how an image is prepared.
73
+ *
74
+ * **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
75
+ * cannot reach it. A reader that uploads is the natural place to get this wrong: the default storage backend
76
+ * serves a path only this app can resolve, and a model handed one answers about a picture it never saw, with
77
+ * nothing anywhere reporting a failure.
73
78
  */
74
79
  attach?: AttachReader;
75
80
  /**
package/ui/Agent/Chat.tsx CHANGED
@@ -109,6 +109,11 @@ export interface ChatProps {
109
109
  * (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
110
110
  * cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
111
111
  * the built-in, so it can also replace how an image is prepared.
112
+ *
113
+ * **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
114
+ * cannot reach it. A reader that uploads is the natural place to get this wrong: the default storage backend
115
+ * serves a path only this app can resolve, and a model handed one answers about a picture it never saw, with
116
+ * nothing anywhere reporting a failure.
112
117
  */
113
118
  attach?: AttachReader;
114
119
  /**