@opencode-ai/ai 0.0.0-dev-18049 → 0.0.0-dev-18051

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.
@@ -434,6 +434,7 @@ export declare const Event: Schema.StructWithRest<Schema.Struct<{
434
434
  readonly headers: Schema.optional<Schema.Unknown>;
435
435
  }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>;
436
436
  export type Event = Schema.Schema.Type<typeof Event>;
437
+ export type ItemKind = "message" | "reasoning" | "function-call" | "reference";
437
438
  export interface Extension {
438
439
  readonly id: string;
439
440
  readonly name: string;
@@ -442,6 +443,7 @@ export interface Extension {
442
443
  readonly media: ProviderShared.NormalizedMedia;
443
444
  readonly request: LLMRequest;
444
445
  }) => MediaInput | undefined;
446
+ readonly acceptsItemID?: (kind: ItemKind, id: string) => boolean;
445
447
  }
446
448
  export interface ParserState {
447
449
  readonly id: string;
@@ -245,26 +245,34 @@ export const lowerToolChoice = (protocolName, toolChoice) => ProviderShared.matc
245
245
  required: () => "required",
246
246
  tool: (toolName) => ({ type: "function", name: toolName }),
247
247
  });
248
+ // Servers validate item ids on replayed history, and a malformed or oversized
249
+ // id can fail an otherwise valid request. Only server-issued tokens are worth
250
+ // resending; anything else is treated as absent so the item is resent without
251
+ // an id (or skipped, for items that cannot be expressed without one).
252
+ const ITEM_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
248
253
  const itemID = (providerMetadata, providerMetadataKey) => {
249
254
  const metadata = providerMetadata?.[providerMetadataKey];
250
- return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0
255
+ return ProviderShared.isRecord(metadata) &&
256
+ typeof metadata.itemId === "string" &&
257
+ ITEM_ID_PATTERN.test(metadata.itemId)
251
258
  ? metadata.itemId
252
259
  : undefined;
253
260
  };
254
- const lowerToolCall = (part, providerMetadataKey) => {
261
+ const acceptsItemID = (extension, kind, id) => id !== undefined && (extension.acceptsItemID?.(kind, id) ?? true);
262
+ const lowerToolCall = (part, providerMetadataKey, extension) => {
255
263
  const id = itemID(part.providerMetadata, providerMetadataKey);
256
264
  return {
257
265
  type: "function_call",
258
- ...(id ? { id } : {}),
266
+ ...(acceptsItemID(extension, "function-call", id) ? { id } : {}),
259
267
  call_id: part.id,
260
268
  name: part.name,
261
269
  arguments: ProviderShared.encodeJson(part.input),
262
270
  };
263
271
  };
264
- const lowerReasoning = (part, providerMetadataKey) => {
272
+ const lowerReasoning = (part, providerMetadataKey, extension) => {
265
273
  const metadata = part.providerMetadata?.[providerMetadataKey];
266
274
  const id = itemID(part.providerMetadata, providerMetadataKey);
267
- if (!ProviderShared.isRecord(metadata) || !id)
275
+ if (!ProviderShared.isRecord(metadata) || !acceptsItemID(extension, "reasoning", id))
268
276
  return undefined;
269
277
  const encryptedContent = typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
270
278
  ? metadata.reasoningEncryptedContent
@@ -359,7 +367,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
359
367
  return;
360
368
  const groups = content.reduce((groups, part) => {
361
369
  const metadata = part.providerMetadata?.[providerMetadataKey];
362
- const id = itemID(part.providerMetadata, providerMetadataKey);
370
+ const rawID = itemID(part.providerMetadata, providerMetadataKey);
371
+ const id = acceptsItemID(extension, "message", rawID) ? rawID : undefined;
363
372
  const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase) : undefined;
364
373
  const group = groups.at(-1);
365
374
  if (group && group.id === id && group.phase === phase)
@@ -384,7 +393,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
384
393
  }
385
394
  if (part.type === "reasoning") {
386
395
  flushText();
387
- const reasoning = lowerReasoning(part, providerMetadataKey);
396
+ const reasoning = lowerReasoning(part, providerMetadataKey, extension);
388
397
  if (!reasoning)
389
398
  continue;
390
399
  if (store !== false) {
@@ -408,14 +417,15 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
408
417
  flushText();
409
418
  if (part.providerExecuted === true)
410
419
  continue;
411
- input.push(lowerToolCall(part, providerMetadataKey));
420
+ input.push(lowerToolCall(part, providerMetadataKey, extension));
412
421
  continue;
413
422
  }
414
423
  if (part.type === "tool-result" && part.providerExecuted === true) {
415
424
  flushText();
416
425
  const id = itemID(part.providerMetadata, providerMetadataKey);
417
- if (store !== false && id && !hostedToolReferences.has(id))
418
- input.push({ type: "item_reference", id });
426
+ const reference = acceptsItemID(extension, "reference", id) ? id : undefined;
427
+ if (store !== false && reference && !hostedToolReferences.has(reference))
428
+ input.push({ type: "item_reference", id: reference });
419
429
  if (store === false) {
420
430
  // The server is not storing this exchange, so the tool outcome has to
421
431
  // travel in the input. Non-content results degrade to their text form.
@@ -427,8 +437,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
427
437
  content: yield* Effect.forEach(content, (item) => lowerHostedToolResultContentItem(item, request, extension)),
428
438
  });
429
439
  }
430
- if (id)
431
- hostedToolReferences.add(id);
440
+ if (reference)
441
+ hostedToolReferences.add(reference);
432
442
  continue;
433
443
  }
434
444
  return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [
@@ -43,9 +43,27 @@ const OpenAIResponsesBody = Schema.Struct({
43
43
  ...OpenAIResponsesCoreFields,
44
44
  stream: Schema.Literal(true),
45
45
  });
46
+ // Replayed items are paired with stored server state by id, so a foreign or
47
+ // synthetic token can fail request validation even when `call_id` pairing is
48
+ // intact. Only resend ids in each item kind's own grammar; hosted tool
49
+ // references keep generic validation because every hosted tool mints its own
50
+ // prefix. The same allowlist approach codex uses before resending history
51
+ // (codex-rs core/src/client.rs, `prepare_response_items_for_request`).
52
+ const ITEM_ID_PREFIXES = {
53
+ message: ["msg_"],
54
+ reasoning: ["rs_"],
55
+ "function-call": ["fc_"],
56
+ // Every hosted tool mints its own id prefix, so references keep generic
57
+ // validation only.
58
+ reference: [],
59
+ };
46
60
  const extension = {
47
61
  id: ADAPTER,
48
62
  name: NAME,
63
+ acceptsItemID: (kind, id) => {
64
+ const prefixes = ITEM_ID_PREFIXES[kind];
65
+ return prefixes.length === 0 || prefixes.some((prefix) => id.startsWith(prefix));
66
+ },
49
67
  };
50
68
  const nativeImageToolInput = (tool) => {
51
69
  const native = tool.native?.openai;
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-18049",
3
+ "version": "0.0.0-dev-18051",
4
4
  "name": "@opencode-ai/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.111",
33
- "@opencode-ai/http-recorder": "0.0.0-dev-18049",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-18051",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-dev-18049",
42
+ "@opencode-ai/schema": "0.0.0-dev-18051",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"