Parcourir la source

fix(ai): stop generating response item ids

Aiden Cline il y a 1 semaine
Parent
commit
0657dcbad2

+ 1 - 32
packages/ai/src/llm.ts

@@ -9,7 +9,6 @@ import {
   LLMRequest,
   LLMResponse,
   Message,
-  ResponseItemID,
   LanguageModel,
   SystemPart,
   ToolChoice,
@@ -39,17 +38,6 @@ export const generate = LLMClient.generate
 
 export const stream = LLMClient.stream
 
-const providerItemID = (part: ContentPart, providerMetadataKey: string) => {
-  if (part.type === "media" || part.itemId !== undefined) return part.type === "media" ? undefined : part.itemId
-  const metadata = part.providerMetadata?.[providerMetadataKey]
-  return typeof metadata === "object" &&
-    metadata !== null &&
-    !Array.isArray(metadata) &&
-    typeof metadata.itemId === "string"
-    ? metadata.itemId
-    : undefined
-}
-
 export const request = <const SelectedLanguageModel extends LanguageModel>(
   input: RequestInput<SelectedLanguageModel>,
 ) => {
@@ -64,29 +52,10 @@ export const request = <const SelectedLanguageModel extends LanguageModel>(
     http: requestHttp,
     ...rest
   } = input
-  const normalized = [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])]
-  const providerMetadataKey = input.model.route.providerMetadataKey ?? input.model.provider
   return new LLMRequest({
     ...rest,
     system: SystemPart.content(requestSystem),
-    messages: normalized.map((message) => {
-      const id = message.id ?? ResponseItemID.create("msg")
-      return new Message({
-        id,
-        role: message.role,
-        content: message.content.map((part, index) => {
-          if (part.type === "media" || message.role === "user" || message.role === "system") return part
-          const itemId = providerItemID(part, providerMetadataKey)
-          if (itemId !== undefined) return { ...part, itemId }
-          if (part.type === "tool-call") return { ...part, itemId: `fc_${id}_${index}` }
-          if (part.type === "tool-result") return { ...part, itemId: `fco_${id}_${index}` }
-          if (part.type === "reasoning") return { ...part, itemId: `rs_${id}_${index}` }
-          return { ...part, itemId: `msg_${id}_${index}` }
-        }),
-        metadata: message.metadata,
-        native: message.native,
-      })
-    }),
+    messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
     tools: tools?.map(ToolDefinition.make) ?? [],
     toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
     generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),

+ 6 - 31
packages/ai/src/protocols/open-responses.ts

@@ -11,7 +11,6 @@ import {
   type LLMRequest,
   type MediaPart,
   type ProviderMetadata,
-  ResponseItemID,
   type ReasoningPart,
   type TextPart,
   type ToolCallPart,
@@ -336,13 +335,8 @@ const metadataItemID = (
     : undefined
 }
 
-const outboundItemID = (id: string | undefined) => (id !== undefined && ResponseItemID.isPrefixed(id) ? id : undefined)
-
-const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => ({
+const lowerToolCall = (part: ToolCallPart): OpenResponsesInputItem => ({
   type: "function_call",
-  ...(outboundItemID(metadataItemID(part, providerMetadataKey)) === undefined
-    ? {}
-    : { id: outboundItemID(metadataItemID(part, providerMetadataKey)) }),
   call_id: part.id,
   name: part.name,
   arguments: ProviderShared.encodeJson(part.input),
@@ -426,17 +420,8 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f
 
 const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) {
   const options = OpenResponsesOptions.resolve(request)
-  const systemItemID = request.id ?? options.promptCacheKey
   const system: LoweredInputItem[] =
-    request.system.length === 0
-      ? []
-      : [
-          {
-            role: "system",
-            ...(systemItemID === undefined ? {} : { id: `msg_${systemItemID}_system` }),
-            content: ProviderShared.joinText(request.system),
-          },
-        ]
+    request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
   const input: LoweredInputItem[] = [...system]
   const store = options.store
   const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses"
@@ -448,22 +433,15 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
       if (previous && "role" in previous && previous.role === "user" && Array.isArray(previous.content))
         input[input.length - 1] = {
           role: "user",
-          id: previous.id,
           content: [...previous.content, { type: "input_text", text: part.text }],
         }
-      else
-        input.push({
-          role: "user",
-          id: outboundItemID(message.id),
-          content: [{ type: "input_text", text: part.text }],
-        })
+      else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
       continue
     }
 
     if (message.role === "user") {
       input.push({
         role: "user",
-        ...(outboundItemID(message.id) === undefined ? {} : { id: outboundItemID(message.id) }),
         content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension)),
       })
       continue
@@ -489,7 +467,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
         input.push(
           ...groups.map((group) => ({
             role: "assistant" as const,
-            ...(outboundItemID(group.itemId) === undefined ? {} : { id: outboundItemID(group.itemId) }),
+            ...(group.itemId === undefined ? {} : { id: group.itemId }),
             content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })),
             ...(group.phase === undefined ? {} : { phase: group.phase }),
           })),
@@ -514,7 +492,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
           }
           const replay = {
             type: reasoning.type,
-            ...(outboundItemID(reasoning.id) === undefined ? {} : { id: outboundItemID(reasoning.id) }),
+            id: reasoning.id,
             summary: reasoning.summary,
             encrypted_content: reasoning.encrypted_content,
           }
@@ -525,7 +503,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
         if (part.type === "tool-call") {
           flushText()
           if (part.providerExecuted === true) continue
-          input.push(lowerToolCall(part, providerMetadataKey))
+          input.push(lowerToolCall(part))
           continue
         }
         if (part.type === "tool-result" && part.providerExecuted === true) {
@@ -561,9 +539,6 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
         return yield* ProviderShared.unsupportedContent(extension.name, "tool", ["tool-result"])
       input.push({
         type: "function_call_output",
-        ...(outboundItemID(metadataItemID(part, providerMetadataKey)) === undefined
-          ? {}
-          : { id: outboundItemID(metadataItemID(part, providerMetadataKey)) }),
         call_id: part.id,
         output: yield* lowerToolResultOutput(part, request, extension),
       })

+ 2 - 3
packages/ai/src/protocols/openai-responses.ts

@@ -4,7 +4,7 @@ import { Auth } from "../route/auth"
 import { Endpoint } from "../route/endpoint"
 import { Protocol } from "../route/protocol"
 import { HttpTransport, WebSocketTransport } from "../route/transport"
-import { LLMEvent, LLMRequest, ResponseItemID, type JsonSchema, type ToolDefinition } from "../schema"
+import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema"
 import { OpenResponses } from "./open-responses"
 import { optionalArray, ProviderShared } from "./shared"
 import { Lifecycle } from "./utils/lifecycle"
@@ -95,13 +95,12 @@ const extension = {
     if (metadata.item.type === "image_generation_call")
       return {
         type: metadata.item.type,
-        ...(id === undefined || !ResponseItemID.isPrefixed(id) ? {} : { id }),
+        ...(id === undefined ? {} : { id }),
         ...(typeof metadata.item.status === "string" ? { status: metadata.item.status } : {}),
         ...(typeof metadata.item.revised_prompt === "string" ? { revised_prompt: metadata.item.revised_prompt } : {}),
         ...(typeof metadata.item.result === "string" ? { result: metadata.item.result } : {}),
       }
     const item: Record<string, unknown> & { type: string } = { ...metadata.item, type: metadata.item.type }
-    if (id !== undefined && !ResponseItemID.isPrefixed(id)) delete item.id
     return item
   },
 } satisfies OpenResponses.Extension

+ 1 - 20
packages/ai/src/schema/ids.ts

@@ -21,26 +21,7 @@ export type ProviderID = typeof ProviderID.Type
 export const ResponseID = Schema.String
 export type ResponseID = Schema.Schema.Type<typeof ResponseID>
 
-const uuidv7 = () => {
-  const bytes = crypto.getRandomValues(new Uint8Array(16))
-  const timestamp = BigInt(Date.now())
-  bytes.set(
-    Array.from({ length: 6 }, (_, index) => Number((timestamp >> BigInt((5 - index) * 8)) & 0xffn)),
-    0,
-  )
-  bytes[6] = 0x70 | ((bytes[6] ?? 0) & 0x0f)
-  bytes[8] = 0x80 | ((bytes[8] ?? 0) & 0x3f)
-  const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
-  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
-}
-
-export const ResponseItemID = Object.assign(Schema.String, {
-  create: (prefix: string): ResponseItemID => `${prefix}_${uuidv7()}`,
-  isPrefixed: (value: string) => {
-    const separator = value.indexOf("_")
-    return separator > 0 && separator < value.length - 1
-  },
-})
+export const ResponseItemID = Schema.String
 export type ResponseItemID = Schema.Schema.Type<typeof ResponseItemID>
 
 export const ContentBlockID = Schema.String

+ 0 - 8
packages/ai/src/tool-runtime.ts

@@ -1,7 +1,6 @@
 import { Effect } from "effect"
 import {
   LLMEvent,
-  ResponseItemID,
   type ToolCallPart,
   ToolFailure,
   ToolOutput,
@@ -63,10 +62,6 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<Tool
 
 const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => {
   const settlement = ToolResultValue.is(value) ? { result: value } : value
-  const itemId =
-    call.itemId !== undefined && call.itemId.startsWith("fc_")
-      ? `fco_${call.itemId.slice(3)}`
-      : ResponseItemID.create("fco")
   return {
     result: settlement.result,
     output: settlement.output,
@@ -75,7 +70,6 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
         ? [
             LLMEvent.toolError({
               id: call.id,
-              itemId,
               name: call.name,
               message: String(settlement.result.value),
               error,
@@ -83,7 +77,6 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
             }),
             LLMEvent.toolResult({
               id: call.id,
-              itemId,
               name: call.name,
               result: settlement.result,
               ...(call.providerMetadata === undefined ? {} : { providerMetadata: call.providerMetadata }),
@@ -92,7 +85,6 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement,
         : [
             LLMEvent.toolResult({
               id: call.id,
-              itemId,
               name: call.name,
               result: settlement.result,
               output: settlement.output,

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
packages/ai/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json


Fichier diff supprimé car celui-ci est trop grand
+ 1 - 1
packages/ai/test/fixtures/recordings/openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool.json


+ 1 - 1
packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json

@@ -44,7 +44,7 @@
         "headers": {
           "content-type": "application/json"
         },
-        "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
+        "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\",\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}"
       },
       "response": {
         "status": 200,

+ 1 - 1
packages/ai/test/provider/openai-compatible-responses.test.ts

@@ -54,7 +54,7 @@ describe("Open Responses-compatible route", () => {
         stream: true,
       })
       expect(prepared.body.input[0]).not.toHaveProperty("id")
-      expect(prepared.body.input[1]?.id).toStartWith("msg_")
+      expect(prepared.body.input[1]).not.toHaveProperty("id")
     }),
   )
 

+ 24 - 37
packages/ai/test/provider/openai-responses.test.ts

@@ -54,27 +54,15 @@ const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAITool
   return output!
 }
 
-const generatedResponseItemID =
-  /^(?:(?:msg|fc|fco|rs)_[0-9a-f-]{36}|(?:msg|fc|fco|rs)_msg_[0-9a-f-]{36}_\d+|msg_req_[0-9a-f-]{36}_system)$/i
-const withoutGeneratedResponseItemIDs = (value: unknown): unknown => {
-  if (Array.isArray(value)) return value.map(withoutGeneratedResponseItemIDs)
-  if (typeof value !== "object" || value === null) return value
-  return Object.fromEntries(
-    Object.entries(value)
-      .filter(([key, item]) => !(key === "id" && typeof item === "string" && generatedResponseItemID.test(item)))
-      .map(([key, item]) => [key, withoutGeneratedResponseItemIDs(item)]),
-  )
-}
-
 describe("OpenAI Responses route", () => {
   it.effect("prepares OpenAI Responses target", () =>
     Effect.gen(function* () {
       const prepared = yield* compileRequest(request)
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body)).toEqual({
+      expect(prepared.body).toEqual({
         model: "gpt-4.1-mini",
         input: [
-          { role: "system", id: "msg_req_1_system", content: "You are concise." },
+          { role: "system", content: "You are concise." },
           { role: "user", content: [{ type: "input_text", text: "Say hello." }] },
         ],
         store: false,
@@ -218,7 +206,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           role: "user",
           content: [
@@ -294,7 +282,7 @@ describe("OpenAI Responses route", () => {
       expect(opened).toEqual([{ url: "wss://api.openai.test/v1/responses", authorization: "Bearer test" }])
       expect(closed).toBe(true)
       expect(sent).toHaveLength(1)
-      expect(withoutGeneratedResponseItemIDs(JSON.parse(sent[0]))).toEqual({
+      expect(JSON.parse(sent[0])).toEqual({
         type: "response.create",
         model: "gpt-4.1-mini",
         input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
@@ -442,13 +430,12 @@ describe("OpenAI Responses route", () => {
       })
       const call = prepared.body.input.find((item) => "type" in item && item.type === "function_call")
       const output = prepared.body.input.find((item) => "type" in item && item.type === "function_call_output")
-      expect(call?.id).toStartWith("fc_")
-      expect(output?.id).toStartWith("fco_")
-      expect(call?.id).not.toBe(output?.id)
+      expect(call?.id).toBeUndefined()
+      expect(output?.id).toBeUndefined()
     }),
   )
 
-  it.effect("generates canonical response item id prefixes", () =>
+  it.effect("does not generate response item ids for client-created history", () =>
     Effect.sync(() => {
       const canonical = LLM.request({
         model,
@@ -462,16 +449,16 @@ describe("OpenAI Responses route", () => {
         ],
       })
 
-      expect(canonical.messages[0]?.id).toStartWith("msg_")
-      expect(canonical.messages[1]?.id).toStartWith("msg_")
-      expect(canonical.messages[0]?.content[0]).toMatchObject({ itemId: expect.stringMatching(/^msg_/) })
-      expect(canonical.messages[0]?.content[1]).toMatchObject({ itemId: expect.stringMatching(/^rs_/) })
-      expect(canonical.messages[0]?.content[2]).toMatchObject({ itemId: expect.stringMatching(/^fc_/) })
-      expect(canonical.messages[1]?.content[0]).toMatchObject({ itemId: expect.stringMatching(/^fco_/) })
+      expect(canonical.messages[0]?.id).toBeUndefined()
+      expect(canonical.messages[1]?.id).toBeUndefined()
+      expect(canonical.messages[0]?.content.every((part) => part.type === "media" || part.itemId === undefined)).toBe(
+        true,
+      )
+      expect(canonical.messages[1]?.content[0]).not.toHaveProperty("itemId")
     }),
   )
 
-  it.effect("keeps unprefixed item ids canonical but omits them outbound", () =>
+  it.effect("preserves opaque assistant item ids without assigning ids to function items", () =>
     Effect.gen(function* () {
       const canonical = LLM.request({
         model,
@@ -490,8 +477,8 @@ describe("OpenAI Responses route", () => {
         "plain-call",
       ])
       expect(canonical.messages[1]?.content[0]).toMatchObject({ itemId: "plain-output" })
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
-        { role: "assistant", content: [{ type: "output_text", text: "Calling." }] },
+      expect(prepared.body.input).toEqual([
+        { role: "assistant", id: "plain-text", content: [{ type: "output_text", text: "Calling." }] },
         { type: "function_call", call_id: "call_1", name: "lookup", arguments: "{}" },
         { type: "function_call_output", call_id: "call_1", output: '"done"' },
       ])
@@ -1023,7 +1010,7 @@ describe("OpenAI Responses route", () => {
       ])
 
       const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           role: "assistant",
           id: "msg_commentary",
@@ -1415,7 +1402,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         { role: "assistant", content: [{ type: "output_text", text: "Before." }] },
         {
           type: "reasoning",
@@ -1446,7 +1433,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           type: "reasoning",
           id: "rs_1",
@@ -1486,7 +1473,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         { type: "item_reference", id: "ws_1" },
         { role: "user", content: [{ type: "input_text", text: "Continue." }] },
       ])
@@ -1527,7 +1514,7 @@ describe("OpenAI Responses route", () => {
       )
 
       expect(prepared.body.store).toBe(false)
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         { role: "user", content: [{ type: "input_text", text: "Generate a black triangle." }] },
         { role: "user", content: [{ type: "input_image", image_url: "data:image/png;base64,AQID" }] },
         { role: "user", content: [{ type: "input_text", text: "Make it blue." }] },
@@ -1559,7 +1546,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           type: "reasoning",
           id: "rs_1",
@@ -1951,7 +1938,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           role: "user",
           content: [
@@ -1983,7 +1970,7 @@ describe("OpenAI Responses route", () => {
         }),
       )
 
-      expect(withoutGeneratedResponseItemIDs(prepared.body.input)).toEqual([
+      expect(prepared.body.input).toEqual([
         {
           role: "user",
           content: [

+ 0 - 61
packages/ai/test/recorded-test.ts

@@ -17,66 +17,6 @@ import {
 const __dirname = path.dirname(fileURLToPath(import.meta.url))
 const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
 
-const canonical = (value: unknown): unknown => {
-  if (Array.isArray(value)) return value.map(canonical)
-  if (typeof value !== "object" || value === null) return value
-  return Object.fromEntries(
-    Object.entries(value)
-      .toSorted(([left], [right]) => left.localeCompare(right))
-      .map(([key, item]) => [key, canonical(item)]),
-  )
-}
-
-const generatedItemID =
-  /^(?:(?:msg|fc|fco|rs)_[0-9a-f-]{36}|(?:msg|fc|fco|rs)_msg_[0-9a-f-]{36}_\d+|msg_req_[0-9a-f-]{36}_system)$/i
-
-const responseItemBody = (body: string, url: string, omitIDs: ReadonlySet<number> = new Set()) => {
-  try {
-    const value: unknown = JSON.parse(body)
-    if (
-      !new URL(url).pathname.endsWith("/responses") ||
-      typeof value !== "object" ||
-      value === null ||
-      !("input" in value) ||
-      !Array.isArray(value.input)
-    )
-      return canonical(value)
-    return canonical({
-      ...value,
-      input: value.input.map((item: unknown, index: number) => {
-        if (typeof item !== "object" || item === null) return item
-        const id = "id" in item ? item.id : undefined
-        if (typeof id !== "string" || (!generatedItemID.test(id) && !omitIDs.has(index))) return item
-        return Object.fromEntries(Object.entries(item).filter(([key]) => key !== "id"))
-      }),
-    })
-  } catch {
-    return body
-  }
-}
-
-const missingResponseItemIDs = (body: string) => {
-  try {
-    const value: unknown = JSON.parse(body)
-    if (typeof value !== "object" || value === null || !("input" in value) || !Array.isArray(value.input))
-      return new Set<number>()
-    return new Set(
-      value.input.flatMap((item: unknown, index: number) =>
-        typeof item === "object" && item !== null && !("id" in item) ? [index] : [],
-      ),
-    )
-  } catch {
-    return new Set<number>()
-  }
-}
-
-const responseItemMatcher: HttpRecorder.RequestMatcher = (incoming, recorded) =>
-  incoming.method === recorded.method &&
-  incoming.url === recorded.url &&
-  JSON.stringify(canonical(incoming.headers)) === JSON.stringify(canonical(recorded.headers)) &&
-  JSON.stringify(responseItemBody(incoming.body, incoming.url, missingResponseItemIDs(recorded.body))) ===
-    JSON.stringify(responseItemBody(recorded.body, recorded.url))
-
 type RecordedEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService | ImageClientService
 
 type RecordedTestsOptions = RecordedGroupOptions & {
@@ -138,7 +78,6 @@ export const recordedTests = (options: RecordedTestsOptions) =>
           HttpRecorder.layerFetch(cassette, {
             ...recorderOptions,
             directory: FIXTURES_DIR,
-            match: recorderOptions?.match ?? responseItemMatcher,
             metadata: recorderMetadata,
           }),
         ),

+ 0 - 7
packages/ai/test/schema.test.ts

@@ -11,7 +11,6 @@ import {
   LanguageModel,
   ModelID,
   ProviderID,
-  ResponseItemID,
   Usage,
 } from "../src/schema"
 import { ProviderShared } from "../src/protocols/shared"
@@ -69,12 +68,6 @@ describe("llm schema", () => {
     expect(ContentPart.guards.text({ type: "text", text: "hi" })).toBe(true)
     expect(ContentPart.guards.media({ type: "text", text: "hi" })).toBe(false)
   })
-
-  test("creates prefixed UUIDv7 response item ids", () => {
-    const id = ResponseItemID.create("msg")
-    expect(id).toMatch(/^msg_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
-    expect(ResponseItemID.isPrefixed(id)).toBe(true)
-  })
 })
 
 describe("AI.Usage", () => {

+ 7 - 8
packages/ai/test/tool-runtime.test.ts

@@ -180,7 +180,7 @@ describe("LLMClient tools", () => {
           output: { structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] },
         }),
       ])
-      expect(dispatched.events[0]?.itemId).toStartWith("fco_")
+      expect(dispatched.events[0]?.itemId).toBeUndefined()
     }),
   )
 
@@ -207,7 +207,7 @@ describe("LLMClient tools", () => {
           providerMetadata,
         }),
       ])
-      expect(dispatched.events[0]?.itemId).toStartWith("fco_")
+      expect(dispatched.events[0]?.itemId).toBeUndefined()
 
       const failed = yield* ToolRuntime.dispatch(
         {},
@@ -229,13 +229,12 @@ describe("LLMClient tools", () => {
       ])
       const errorItemID = failed.events.find(LLMEvent.is.toolError)?.itemId
       const resultItemID = failed.events.find(LLMEvent.is.toolResult)?.itemId
-      expect(errorItemID).toBe("fco_failed")
-      expect(resultItemID).toBe("fco_failed")
-      expect(errorItemID).not.toBe("fc_failed")
+      expect(errorItemID).toBeUndefined()
+      expect(resultItemID).toBeUndefined()
     }),
   )
 
-  it.effect("derives a stable function output item id from the function call item id", () =>
+  it.effect("does not derive a function output item id from the function call item id", () =>
     Effect.gen(function* () {
       const tool = Tool.make({
         description: "Return text.",
@@ -248,7 +247,7 @@ describe("LLMClient tools", () => {
         LLMEvent.toolCall({ id: "call_1", itemId: "fc_existing", name: "tool", input: {} }),
       )
 
-      expect(dispatched.events.find(LLMEvent.is.toolResult)?.itemId).toBe("fco_existing")
+      expect(dispatched.events.find(LLMEvent.is.toolResult)?.itemId).toBeUndefined()
     }),
   )
 
@@ -469,7 +468,7 @@ describe("LLMClient tools", () => {
           output: { structured: { ok: true }, content: [] },
         }),
       ])
-      expect(dispatched.events[0]?.itemId).toStartWith("fco_")
+      expect(dispatched.events[0]?.itemId).toBeUndefined()
     }),
   )
 

+ 2 - 5
packages/core/src/session/runner/publish-llm-event.ts

@@ -1,4 +1,4 @@
-import { ResponseItemID, type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
+import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
 import { Effect } from "effect"
 import { Bus } from "../../bus"
 import { Model } from "../../model"
@@ -269,7 +269,6 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
       },
       ...failureSnapshot(tool),
       executed: false,
-      resultState: { itemId: ResponseItemID.create("fco") },
     })
   })
 
@@ -288,7 +287,6 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
       error,
       ...failureSnapshot(tool),
       executed: tool.providerExecuted,
-      resultState: tool.providerExecuted ? undefined : { itemId: ResponseItemID.create("fco") },
     })
     return true
   })
@@ -498,7 +496,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
               : { type: "tool.execution", message: event.message },
           ...failureSnapshot(tool),
           executed: tool.providerExecuted,
-          resultState: providerState(event.providerMetadata, event.itemId) ?? { itemId: ResponseItemID.create("fco") },
+          resultState: providerState(event.providerMetadata, event.itemId),
         })
         return
       }
@@ -555,7 +553,6 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
       content: [content[0], ...content.slice(1)],
       ...(result.metadata === undefined ? {} : { metadata: result.metadata }),
       executed: tool.providerExecuted,
-      resultState: tool.providerExecuted ? undefined : { itemId: ResponseItemID.create("fco") },
     })
   })
 

+ 1 - 1
packages/core/test/session-runner-tool-events.test.ts

@@ -85,7 +85,7 @@ test("local tool success serializes media base64 once through canonical content"
 
   const called = published.find((event) => event.type === "session.tool.called.1")?.data
   expect(called).toMatchObject({ state: { itemId: "fc_call-image" } })
-  expect(success?.data).toMatchObject({ resultState: { itemId: expect.stringMatching(/^fco_/) } })
+  expect(success?.data).not.toHaveProperty("resultState")
   expect(JSON.stringify(success?.data)).not.toContain('"itemId":"fc_call-image"')
 
   expect(success?.data).toMatchObject({

+ 0 - 6
packages/core/test/session-runner.test.ts

@@ -2534,12 +2534,10 @@ describe("SessionRunnerLLM", () => {
       yield* TestLLM.push([])
       yield* session.resume(sessionID)
 
-      const assistant = requests[1]?.messages[1]
       expect(requests[1]?.messages[1]?.content).toEqual([
         {
           type: "reasoning",
           text: "Signed thought",
-          itemId: `rs_${assistant?.id}_0`,
           providerMetadata: { openai: { signature: "sig_1" } },
         },
         {
@@ -2596,12 +2594,10 @@ describe("SessionRunnerLLM", () => {
       yield* TestLLM.push([])
       yield* session.resume(sessionID)
 
-      const assistant = requests[1]?.messages[1]
       expect(requests[1]?.messages[1]?.content).toEqual([
         {
           type: "text",
           text: "Checking.",
-          itemId: `msg_${assistant?.id}_0`,
           providerMetadata: { openai: { phase: "commentary" } },
         },
       ])
@@ -2639,7 +2635,6 @@ describe("SessionRunnerLLM", () => {
       yield* session.resume(sessionID)
 
       expect(messageRoles(requests[1])).toEqual(["user", "assistant", "user"])
-      const assistant = requests[1]?.messages[1]
       expect(requests[1]?.messages[1]?.content).toMatchObject([
         {
           type: "tool-call",
@@ -2653,7 +2648,6 @@ describe("SessionRunnerLLM", () => {
         {
           type: "tool-result",
           id: "hosted-search",
-          itemId: `fco_${assistant?.id}_1`,
           name: "web_search",
           // The generic replay result derives from canonical stored content.
           result: { type: "text", value: '[{"title":"Effect"}]' },

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff