@vellumai/assistant 0.11.2-staging.1 → 0.11.2-staging.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.
@@ -26,6 +26,7 @@ import {
26
26
  normalizeOpenAIAPIError,
27
27
  } from "./api-error-normalization.js";
28
28
  import { detectOpenAICompatibleContextOverflow } from "./chat-completions-provider.js";
29
+ import { serializeToolResult } from "./orphaned-tool-result.js";
29
30
 
30
31
  const log = getLogger("openai-responses");
31
32
 
@@ -758,11 +759,16 @@ export class OpenAIResponsesProvider implements Provider {
758
759
  messages = resolveMediaReferences(messages);
759
760
  const result: unknown[] = [];
760
761
 
762
+ // `call_id`s emitted as `function_call` items earlier in this request.
763
+ // The API rejects a `function_call_output` whose `call_id` has no
764
+ // preceding `function_call`, so tool results are only serialized as
765
+ // outputs when their call was emitted first (backward matches only).
766
+ const emittedCallIds = new Set<string>();
761
767
  for (const msg of messages) {
762
768
  if (msg.role === "assistant") {
763
- this.appendAssistantItems(result, msg);
769
+ this.appendAssistantItems(result, msg, emittedCallIds);
764
770
  } else {
765
- this.appendUserItems(result, msg);
771
+ this.appendUserItems(result, msg, emittedCallIds);
766
772
  }
767
773
  }
768
774
 
@@ -887,8 +893,16 @@ export class OpenAIResponsesProvider implements Provider {
887
893
  }
888
894
  }
889
895
 
890
- /** Convert an assistant message's content blocks to Responses input items. */
891
- private appendAssistantItems(result: unknown[], msg: Message): void {
896
+ /**
897
+ * Convert an assistant message's content blocks to Responses input items.
898
+ * Every `function_call` emitted is recorded in `emittedCallIds` so the
899
+ * user-item conversion can pair `tool_result` blocks against it.
900
+ */
901
+ private appendAssistantItems(
902
+ result: unknown[],
903
+ msg: Message,
904
+ emittedCallIds: Set<string>,
905
+ ): void {
892
906
  const textParts: string[] = [];
893
907
 
894
908
  for (const block of msg.content) {
@@ -912,6 +926,7 @@ export class OpenAIResponsesProvider implements Provider {
912
926
  name: block.name,
913
927
  arguments: JSON.stringify(block.input),
914
928
  });
929
+ emittedCallIds.add(block.id);
915
930
  break;
916
931
  case "server_tool_use":
917
932
  textParts.push(`[Web search: ${block.name}]`);
@@ -932,8 +947,18 @@ export class OpenAIResponsesProvider implements Provider {
932
947
  }
933
948
  }
934
949
 
935
- /** Convert a user message's content blocks to Responses input items. */
936
- private appendUserItems(result: unknown[], msg: Message): void {
950
+ /**
951
+ * Convert a user message's content blocks to Responses input items.
952
+ * A `tool_result` whose `tool_use_id` was not emitted as a `function_call`
953
+ * earlier in this request (`emittedCallIds`) is orphaned; the API rejects
954
+ * an unmatched `function_call_output`, so its content is degraded into the
955
+ * plain user message instead of dropped.
956
+ */
957
+ private appendUserItems(
958
+ result: unknown[],
959
+ msg: Message,
960
+ emittedCallIds: Set<string>,
961
+ ): void {
937
962
  // Separate tool results from other blocks
938
963
  const toolResults = msg.content.filter(
939
964
  (b): b is Extract<ContentBlock, { type: "tool_result" }> =>
@@ -948,33 +973,34 @@ export class OpenAIResponsesProvider implements Provider {
948
973
 
949
974
  // Emit tool results as function_call_output items
950
975
  const toolResultImages: ContentBlock[] = [];
976
+ const orphanedResultBlocks: ContentBlock[] = [];
951
977
  for (const tr of toolResults) {
952
- let textContent = tr.content;
953
- if (tr.contentBlocks && tr.contentBlocks.length > 0) {
954
- const extraText = tr.contentBlocks
955
- .filter(
956
- (cb): cb is Extract<ContentBlock, { type: "text" }> =>
957
- cb.type === "text",
958
- )
959
- .map((cb) => cb.text);
960
- if (extraText.length > 0) {
961
- textContent = textContent + "\n" + extraText.join("\n");
962
- }
963
- for (const cb of tr.contentBlocks) {
964
- if (cb.type === "image") {
965
- toolResultImages.push(cb);
966
- }
978
+ // This transport carries images only; the text payload and the orphan
979
+ // decision are the shared cross-transport rule.
980
+ for (const cb of tr.contentBlocks ?? []) {
981
+ if (cb.type === "image") {
982
+ toolResultImages.push(cb);
967
983
  }
968
984
  }
985
+ const serialized = serializeToolResult(tr, emittedCallIds);
986
+ if (serialized.kind === "orphaned") {
987
+ orphanedResultBlocks.push(serialized.block);
988
+ continue;
989
+ }
969
990
  result.push({
970
991
  type: "function_call_output",
971
992
  call_id: tr.tool_use_id,
972
- output: tr.is_error ? `[ERROR] ${textContent}` : textContent,
993
+ output: serialized.payload,
973
994
  });
974
995
  }
975
996
 
976
- // Emit remaining content + any tool result images as a user message
977
- const userContent = [...otherBlocks, ...toolResultImages];
997
+ // Emit remaining content, degraded orphaned results, and any tool result
998
+ // images as a user message
999
+ const userContent = [
1000
+ ...otherBlocks,
1001
+ ...orphanedResultBlocks,
1002
+ ...toolResultImages,
1003
+ ];
978
1004
  if (userContent.length > 0) {
979
1005
  result.push(this.toResponsesUserMessage(userContent));
980
1006
  }
@@ -756,8 +756,14 @@ export const ROUTES: RouteDefinition[] = [
756
756
  }),
757
757
  responseBody: z.object({
758
758
  success: z.boolean(),
759
- type: z.string(),
760
- name: z.string(),
759
+ type: z.string().optional(),
760
+ name: z.string().optional(),
761
+ error: z
762
+ .string()
763
+ .optional()
764
+ .describe(
765
+ "Why the secret was not stored (e.g. provider-side API key validation failed). Present only when success is false.",
766
+ ),
761
767
  }),
762
768
  handler: handleAddSecret,
763
769
  },