@opencode/ai 0.0.0-dev-19544 → 0.0.0-dev-19550

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.
@@ -10,6 +10,7 @@ import { BedrockAuth } from "./utils/bedrock-auth.js";
10
10
  import { BedrockCache } from "./utils/bedrock-cache.js";
11
11
  import { BedrockMedia } from "./utils/bedrock-media.js";
12
12
  import { Lifecycle } from "./utils/lifecycle.js";
13
+ import { MistralToolID } from "./utils/mistral-tool-id.js";
13
14
  import { ToolSchemaProjection } from "./utils/tool-schema.js";
14
15
  import { ToolStream } from "./utils/tool-stream.js";
15
16
  const ADAPTER = "bedrock-converse";
@@ -201,15 +202,15 @@ const removeEmptyToolInputKeys = (input) => {
201
202
  return input;
202
203
  return Object.fromEntries(Object.entries(input).flatMap(([key, value]) => (key === "" ? [] : [[key, removeEmptyToolInputKeys(value)]])));
203
204
  };
204
- const lowerToolCall = (part) => ({
205
+ const lowerToolCall = (part, normalizeID) => ({
205
206
  toolUse: {
206
- toolUseId: part.id,
207
+ toolUseId: normalizeID(part.id),
207
208
  // Models can emit names that Converse rejects when replayed in history.
208
209
  name: part.name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "_",
209
210
  input: removeEmptyToolInputKeys(part.input),
210
211
  },
211
212
  });
212
- const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (part) {
213
+ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (part, documentNames) {
213
214
  if (part.result.type === "text" || part.result.type === "error")
214
215
  return [{ text: ProviderShared.toolResultText(part) }];
215
216
  if (part.result.type === "json")
@@ -225,22 +226,25 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
225
226
  mediaType: item.mime,
226
227
  data: item.uri,
227
228
  filename: item.name,
228
- });
229
- content.push(media);
229
+ }, documentNames);
230
+ content.push(...media);
230
231
  }
231
232
  return content;
232
233
  });
233
- const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (part) {
234
+ const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (part, documentNames, normalizeID) {
234
235
  return {
235
236
  toolResult: {
236
- toolUseId: part.id,
237
- content: yield* lowerToolResultContent(part),
237
+ toolUseId: normalizeID(part.id),
238
+ content: yield* lowerToolResultContent(part, documentNames),
238
239
  status: part.result.type === "error" ? "error" : "success",
239
240
  },
240
241
  };
241
242
  });
242
243
  const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (request, breakpoints) {
243
244
  const messages = [];
245
+ const documentNames = new Set();
246
+ // Mistral can reject replay IDs even when they satisfy Converse's broader ID syntax.
247
+ const normalizeID = request.model.id.includes("mistral.") ? MistralToolID.normalizer(request) : (id) => id;
244
248
  const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
245
249
  for (const message of request.messages) {
246
250
  if (message.role === "system") {
@@ -263,7 +267,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
263
267
  continue;
264
268
  }
265
269
  if (part.type === "media") {
266
- content.push(yield* BedrockMedia.lower(part));
270
+ content.push(...(yield* BedrockMedia.lower(part, documentNames)));
267
271
  continue;
268
272
  }
269
273
  }
@@ -305,7 +309,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
305
309
  continue;
306
310
  }
307
311
  if (part.type === "tool-call") {
308
- content.push(lowerToolCall(part));
312
+ content.push(lowerToolCall(part, normalizeID));
309
313
  continue;
310
314
  }
311
315
  }
@@ -317,7 +321,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
317
321
  for (const part of message.content) {
318
322
  if (!ProviderShared.supportsContent(part, ["tool-result"]))
319
323
  return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"]);
320
- content.push(yield* lowerToolResult(part));
324
+ content.push(yield* lowerToolResult(part, documentNames, normalizeID));
321
325
  const cachePoint = BedrockCache.block(breakpoints, part.cache);
322
326
  if (cachePoint)
323
327
  content.push(cachePoint);
@@ -9,10 +9,10 @@ import { AIError, InvalidProviderOutputError, LLMEvent, ProviderInternalError, U
9
9
  import { classifyProviderFailure } from "../provider-error.js";
10
10
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
11
  import { Lifecycle } from "./utils/lifecycle.js";
12
+ import { MistralToolID } from "./utils/mistral-tool-id.js";
12
13
  import { ToolStream } from "./utils/tool-stream.js";
13
14
  const ADAPTER = "mistral-chat";
14
15
  const DONE = "[DONE]";
15
- const TOOL_ID = /^[A-Za-z0-9]{9}$/;
16
16
  export const DEFAULT_BASE_URL = "https://api.mistral.ai/v1";
17
17
  export const PATH = "/chat/completions";
18
18
  const MistralTextContent = Schema.Struct({
@@ -140,34 +140,6 @@ const MistralEvent = Schema.StructWithRest(Schema.Struct({
140
140
  error: optionalNull(MistralError),
141
141
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
142
142
  const MistralStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(MistralEvent)]);
143
- const hashID = (value) => {
144
- const hash = (seed) => {
145
- let result = seed;
146
- for (const char of value)
147
- result = Math.imul(result ^ char.charCodeAt(0), 16777619);
148
- return (result >>> 0).toString(36);
149
- };
150
- return `${hash(2166136261).padStart(7, "0")}${hash(2246822519).padStart(7, "0")}`.slice(-9);
151
- };
152
- const toolIDNormalizer = (request) => {
153
- const ids = request.messages.flatMap((message) => message.content.flatMap((part) => (part.type === "tool-call" || part.type === "tool-result" ? [part.id] : [])));
154
- const used = new Set(ids.filter((id) => TOOL_ID.test(id)));
155
- const normalized = new Map();
156
- return (id) => {
157
- if (TOOL_ID.test(id))
158
- return id;
159
- const previous = normalized.get(id);
160
- if (previous)
161
- return previous;
162
- let attempt = 0;
163
- let candidate = hashID(id);
164
- while (used.has(candidate))
165
- candidate = hashID(`${id}:${++attempt}`);
166
- used.add(candidate);
167
- normalized.set(id, candidate);
168
- return candidate;
169
- };
170
- };
171
143
  const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part) {
172
144
  const media = ProviderShared.normalizeMedia(part);
173
145
  const url = typeof part.data === "string" && /^(?:https?:|data:)/.test(part.data) ? part.data : media.dataUrl;
@@ -269,7 +241,7 @@ const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (me
269
241
  return output;
270
242
  });
271
243
  const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request) {
272
- const normalizeID = toolIDNormalizer(request);
244
+ const normalizeID = MistralToolID.normalizer(request);
273
245
  const messages = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }];
274
246
  for (const message of request.messages) {
275
247
  if (message.role === "system") {
@@ -439,15 +411,15 @@ const appendContent = (state, events, content) => {
439
411
  }, state);
440
412
  };
441
413
  const normalizeStreamToolID = (state, source) => {
442
- if (TOOL_ID.test(source))
414
+ if (MistralToolID.valid.test(source))
443
415
  return { id: source, state: { ...state, usedToolIDs: new Set([...state.usedToolIDs, source]) } };
444
416
  const previous = state.toolIDs.get(source);
445
417
  if (previous)
446
418
  return { id: previous, state };
447
419
  let attempt = 0;
448
- let id = hashID(source);
420
+ let id = MistralToolID.hash(source);
449
421
  while (state.usedToolIDs.has(id))
450
- id = hashID(`${source}:${++attempt}`);
422
+ id = MistralToolID.hash(`${source}:${++attempt}`);
451
423
  return {
452
424
  id,
453
425
  state: {
@@ -36,7 +36,14 @@ export declare const lower: (part: {
36
36
  };
37
37
  } | undefined;
38
38
  readonly filename?: string | undefined;
39
- }) => Effect.Effect<{
39
+ }, documentNames: Set<string>) => Effect.Effect<{
40
+ image: {
41
+ format: "png" | "jpeg" | "gif" | "webp";
42
+ source: {
43
+ bytes: string;
44
+ };
45
+ };
46
+ }[] | ({
40
47
  readonly document: {
41
48
  readonly format: "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
42
49
  readonly name: string;
@@ -45,11 +52,6 @@ export declare const lower: (part: {
45
52
  };
46
53
  };
47
54
  } | {
48
- image: {
49
- format: "png" | "jpeg" | "gif" | "webp";
50
- source: {
51
- bytes: string;
52
- };
53
- };
54
- }, import("../../schema/errors.js").AIError, never>;
55
+ text: string;
56
+ })[], import("../../schema/errors.js").AIError, never>;
55
57
  export * as BedrockMedia from "./bedrock-media.js";
@@ -44,6 +44,23 @@ const documentBlock = (name, format, bytes) => ({
44
44
  source: { bytes },
45
45
  },
46
46
  });
47
+ function documentName(filename, names) {
48
+ const base = (filename ?? "")
49
+ .replace(/\.[^.]*$/, "")
50
+ .replace(/[^a-zA-Z0-9 ()[\]-]/g, " ")
51
+ .replace(/\s+/g, " ")
52
+ .trim()
53
+ .slice(0, 200)
54
+ .trim() || "document";
55
+ let name = base;
56
+ // Converse requires labels to be unique across the entire request, including tool results.
57
+ for (let index = 2; names.has(name); index++) {
58
+ const suffix = ` ${index}`;
59
+ name = `${base.slice(0, 200 - suffix.length).trimEnd()}${suffix}`;
60
+ }
61
+ names.add(name);
62
+ return name;
63
+ }
47
64
  const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part) {
48
65
  const media = ProviderShared.normalizeMedia(part);
49
66
  const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(Effect.mapError((cause) => ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause)));
@@ -54,19 +71,26 @@ const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part) {
54
71
  // document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
55
72
  // get an image-specific error so the caller knows it's a format-support issue,
56
73
  // not a kind-detection issue.
57
- export const lower = Effect.fn("BedrockMedia.lower")(function* (part) {
74
+ export const lower = Effect.fn("BedrockMedia.lower")(function* (part, documentNames) {
58
75
  const mime = part.mediaType.toLowerCase();
59
76
  const imageFormat = IMAGE_FORMATS[mime];
60
77
  if (imageFormat) {
61
- return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } };
78
+ return [{ image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } }];
62
79
  }
63
80
  if (mime.startsWith("image/"))
64
81
  return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`);
65
82
  const documentFormat = DOCUMENT_FORMATS[mime];
66
83
  if (documentFormat) {
67
- if (!part.filename)
68
- return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename");
69
- return documentBlock(part.filename, documentFormat, yield* mediaBase64(part));
84
+ const name = documentName(part.filename, documentNames);
85
+ const block = documentBlock(name, documentFormat, yield* mediaBase64(part));
86
+ return part.filename !== undefined && part.filename !== name
87
+ ? [
88
+ {
89
+ text: `Attached file ${ProviderShared.encodeJson(part.filename)} has document label ${ProviderShared.encodeJson(name)}.`,
90
+ },
91
+ block,
92
+ ]
93
+ : [block];
70
94
  }
71
95
  return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`);
72
96
  });
@@ -0,0 +1,5 @@
1
+ import type { LLMRequest } from "../../schema/index.js";
2
+ export declare const valid: RegExp;
3
+ export declare const hash: (value: string) => string;
4
+ export declare const normalizer: (request: LLMRequest) => (id: string) => string;
5
+ export * as MistralToolID from "./mistral-tool-id.js";
@@ -0,0 +1,31 @@
1
+ export const valid = /^[A-Za-z0-9]{9}$/;
2
+ export const hash = (value) => {
3
+ const hash = (seed) => {
4
+ let result = seed;
5
+ for (const char of value)
6
+ result = Math.imul(result ^ char.charCodeAt(0), 16777619);
7
+ return (result >>> 0).toString(36);
8
+ };
9
+ return `${hash(2166136261).padStart(7, "0")}${hash(2246822519).padStart(7, "0")}`.slice(-9);
10
+ };
11
+ export const normalizer = (request) => {
12
+ const ids = request.messages.flatMap((message) => message.content.flatMap((part) => (part.type === "tool-call" || part.type === "tool-result" ? [part.id] : [])));
13
+ // Reserve valid IDs before projecting any history, including IDs encountered later.
14
+ const used = new Set(ids.filter((id) => valid.test(id)));
15
+ const normalized = new Map();
16
+ return (id) => {
17
+ if (valid.test(id))
18
+ return id;
19
+ const previous = normalized.get(id);
20
+ if (previous)
21
+ return previous;
22
+ let attempt = 0;
23
+ let candidate = hash(id);
24
+ while (used.has(candidate))
25
+ candidate = hash(`${id}:${++attempt}`);
26
+ used.add(candidate);
27
+ normalized.set(id, candidate);
28
+ return candidate;
29
+ };
30
+ };
31
+ export * as MistralToolID from "./mistral-tool-id.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-19544",
3
+ "version": "0.0.0-dev-19550",
4
4
  "name": "@opencode/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-rc.112",
33
- "@opencode/http-recorder": "0.0.0-dev-19544",
33
+ "@opencode/http-recorder": "0.0.0-dev-19550",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.4.0",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -40,7 +40,7 @@
40
40
  "@aws-sdk/credential-providers": "3.1057.0",
41
41
  "@smithy/eventstream-codec": "4.2.14",
42
42
  "@smithy/util-utf8": "4.2.2",
43
- "@opencode/schema": "0.0.0-dev-19544",
43
+ "@opencode/schema": "0.0.0-dev-19550",
44
44
  "aws4fetch": "1.0.20",
45
45
  "effect": "4.0.0-rc.112",
46
46
  "google-auth-library": "10.5.0"