@yourgpt/llm-sdk 2.5.1-beta.5 → 2.6.0-beta.0

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.
@@ -91,14 +91,19 @@ function normalizeObjectJsonSchema(schema) {
91
91
  (value) => typeof value === "string"
92
92
  ) : [];
93
93
  normalized.required = Array.from(/* @__PURE__ */ new Set([...required, ...propertyKeys]));
94
- if (normalized.additionalProperties === void 0) {
95
- normalized.additionalProperties = false;
96
- }
94
+ normalized.additionalProperties = false;
97
95
  } else if (type === "array" && normalized.items && typeof normalized.items === "object") {
98
96
  normalized.items = normalizeObjectJsonSchema(
99
97
  normalized.items
100
98
  );
101
99
  }
100
+ for (const keyword of ["anyOf", "oneOf", "allOf"]) {
101
+ const branches = normalized[keyword];
102
+ if (!Array.isArray(branches)) continue;
103
+ normalized[keyword] = branches.map(
104
+ (branch) => branch && typeof branch === "object" ? normalizeObjectJsonSchema(branch) : branch
105
+ );
106
+ }
102
107
  return normalized;
103
108
  }
104
109
  function isOpenAIReasoningModel(modelId) {
@@ -554,18 +559,79 @@ var OpenAIAdapter = class _OpenAIAdapter {
554
559
  return this.client;
555
560
  }
556
561
  shouldUseResponsesApi(request) {
557
- return request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false && Array.isArray(request.toolDefinitions) && request.toolDefinitions.length > 0;
562
+ const hasTools = Array.isArray(request.toolDefinitions) && request.toolDefinitions.length > 0;
563
+ if (!hasTools) return false;
564
+ if (request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false) {
565
+ return true;
566
+ }
567
+ return this.requiresResponsesApiForTools(
568
+ request.config?.model || this.model
569
+ );
570
+ }
571
+ /**
572
+ * Whether this model rejects function tools on /v1/chat/completions.
573
+ *
574
+ * Applies to OpenAI proper only — the adapter is reused for OpenAI-compatible
575
+ * providers (Azure, xAI, Google, Fireworks) whose endpoints have no such
576
+ * restriction and may not implement /v1/responses at all.
577
+ */
578
+ requiresResponsesApiForTools(modelId) {
579
+ if (!modelId) return false;
580
+ if (!/^gpt-5\.(?:[4-9]|\d{2,})/i.test(modelId)) return false;
581
+ const baseUrl = this.config.baseUrl;
582
+ if (!baseUrl) return true;
583
+ try {
584
+ return new URL(baseUrl).hostname === "api.openai.com";
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
589
+ /**
590
+ * Convert one message's content to Responses content parts.
591
+ *
592
+ * The Responses API does not accept Chat Completions content blocks: input
593
+ * parts are `input_text` / `input_image` (with a bare `image_url` string)
594
+ * rather than `text` / `image_url: { url }`. Passing the Chat Completions
595
+ * shape through unchanged makes the API reject the request, so anything
596
+ * array-shaped has to be translated rather than forwarded.
597
+ */
598
+ toResponsesContent(content, role) {
599
+ const textType = role === "assistant" ? "output_text" : "input_text";
600
+ if (typeof content === "string") return content;
601
+ if (!Array.isArray(content)) {
602
+ return content == null ? "" : JSON.stringify(content);
603
+ }
604
+ return content.map((part) => {
605
+ const p = part;
606
+ if (p?.type === "text") return { type: textType, text: p.text ?? "" };
607
+ if (p?.type === "image_url") {
608
+ const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url;
609
+ return {
610
+ type: "input_image",
611
+ image_url: url,
612
+ ...p.image_url?.detail ? { detail: p.image_url.detail } : {}
613
+ };
614
+ }
615
+ return p;
616
+ });
558
617
  }
559
618
  buildResponsesInput(request) {
560
619
  const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? request.rawMessages : formatMessagesForOpenAI(request.messages, void 0);
561
620
  const input = [];
562
621
  for (const message of sourceMessages) {
563
622
  if (message.role === "system") {
623
+ if (!request.systemPrompt && message.content) {
624
+ input.push({
625
+ type: "message",
626
+ role: "developer",
627
+ content: this.toResponsesContent(message.content, "developer")
628
+ });
629
+ }
564
630
  continue;
565
631
  }
566
632
  if (message.role === "assistant") {
567
- const content = typeof message.content === "string" ? message.content : Array.isArray(message.content) ? message.content : message.content ? JSON.stringify(message.content) : "";
568
- if (content) {
633
+ const content = this.toResponsesContent(message.content, "assistant");
634
+ if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
569
635
  input.push({
570
636
  type: "message",
571
637
  role: "assistant",
@@ -591,15 +657,67 @@ var OpenAIAdapter = class _OpenAIAdapter {
591
657
  });
592
658
  continue;
593
659
  }
660
+ const role = message.role === "developer" ? "developer" : "user";
594
661
  input.push({
595
662
  type: "message",
596
- role: message.role === "developer" ? "developer" : "user",
597
- content: typeof message.content === "string" ? message.content : Array.isArray(message.content) ? message.content : JSON.stringify(message.content ?? "")
663
+ role,
664
+ content: this.toResponsesContent(message.content, role)
598
665
  });
599
666
  }
600
667
  return input;
601
668
  }
602
- buildResponsesTools(tools) {
669
+ /**
670
+ * Build the web_search tool for the Responses API, or undefined when web
671
+ * search is off.
672
+ *
673
+ * The Chat Completions path uses `web_search_preview`; the Responses API
674
+ * names the same built-in `web_search`. Without this, enabling web search
675
+ * alongside a gpt-5.4+ function tool silently did nothing, because routing
676
+ * to /v1/responses skipped the Chat Completions tool assembly entirely.
677
+ */
678
+ buildResponsesWebSearchTool(request) {
679
+ const webSearchConfig = request.webSearch ?? this.config.webSearch;
680
+ if (!webSearchConfig) return void 0;
681
+ const tool = { type: "web_search" };
682
+ const wsConfig = typeof webSearchConfig === "object" ? webSearchConfig : {};
683
+ if (wsConfig.userLocation) {
684
+ tool.search_context_size = "medium";
685
+ }
686
+ return tool;
687
+ }
688
+ /**
689
+ * Extract url_citation annotations from Responses output items.
690
+ *
691
+ * Annotations hang off the output_text content parts rather than arriving as
692
+ * their own stream events, so both the streaming and non-streaming paths read
693
+ * them from the same place.
694
+ */
695
+ collectResponsesCitations(output, startIndex = 0) {
696
+ if (!Array.isArray(output)) return [];
697
+ const citations = [];
698
+ let index = startIndex;
699
+ for (const item of output) {
700
+ if (item?.type !== "message" || !Array.isArray(item.content)) continue;
701
+ for (const part of item.content) {
702
+ if (!Array.isArray(part?.annotations)) continue;
703
+ for (const annotation of part.annotations) {
704
+ const url = annotation?.url;
705
+ if (annotation?.type !== "url_citation" || !url) continue;
706
+ index++;
707
+ const domain = extractDomain(url);
708
+ citations.push({
709
+ index,
710
+ url,
711
+ title: annotation.title || domain,
712
+ domain,
713
+ favicon: domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=32` : void 0
714
+ });
715
+ }
716
+ }
717
+ }
718
+ return citations;
719
+ }
720
+ buildResponsesTools(tools, includeToolSearch, webSearchTool) {
603
721
  const nativeTools = tools.filter((tool) => tool.available !== false).map((tool) => ({
604
722
  type: "function",
605
723
  name: tool.name,
@@ -614,10 +732,18 @@ var OpenAIAdapter = class _OpenAIAdapter {
614
732
  strict: true,
615
733
  defer_loading: tool.deferLoading === true
616
734
  }));
617
- return [{ type: "tool_search" }, ...nativeTools];
735
+ const builtins = [];
736
+ if (includeToolSearch) builtins.push({ type: "tool_search" });
737
+ if (webSearchTool) builtins.push(webSearchTool);
738
+ return [...builtins, ...nativeTools];
618
739
  }
619
740
  parseResponsesResult(response) {
620
- const content = typeof response?.output_text === "string" ? response.output_text : "";
741
+ const textFromOutput = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "message").flatMap(
742
+ (item) => Array.isArray(item.content) ? item.content.filter(
743
+ (part) => part?.type === "output_text" && typeof part.text === "string"
744
+ ).map((part) => part.text) : []
745
+ ).join("") : "";
746
+ const content = textFromOutput || (typeof response?.output_text === "string" ? response.output_text : "");
621
747
  const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => ({
622
748
  id: item.call_id ?? item.id ?? generateToolCallId(),
623
749
  name: item.name,
@@ -640,59 +766,139 @@ var OpenAIAdapter = class _OpenAIAdapter {
640
766
  rawResponse: response
641
767
  };
642
768
  }
643
- async completeWithResponses(request) {
644
- const client = await this.getClient();
769
+ buildResponsesPayload(request, stream) {
645
770
  const openaiToolOptions = request.providerToolOptions?.openai;
646
771
  const responsesTextFormat = toOpenAIResponsesTextFormat(
647
772
  request.config?.responseFormat
648
773
  );
774
+ const modelIdForPayload = request.config?.model || this.model;
649
775
  const payload = {
650
- model: request.config?.model || this.model,
776
+ model: modelIdForPayload,
651
777
  instructions: request.systemPrompt,
652
778
  input: this.buildResponsesInput(request),
653
- tools: this.buildResponsesTools(request.toolDefinitions ?? []),
779
+ tools: this.buildResponsesTools(
780
+ request.toolDefinitions ?? [],
781
+ request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true,
782
+ this.buildResponsesWebSearchTool(request)
783
+ ),
654
784
  tool_choice: openaiToolOptions?.toolChoice === "required" ? "required" : openaiToolOptions?.toolChoice === "auto" ? "auto" : void 0,
655
785
  parallel_tool_calls: openaiToolOptions?.parallelToolCalls,
656
- temperature: request.config?.temperature ?? this.config.temperature,
786
+ // Reasoning models reject `temperature` outright — sending it would swap one
787
+ // 400 for another. isOpenAIReasoningModel already encodes that family test.
788
+ ...isOpenAIReasoningModel(modelIdForPayload) ? {} : {
789
+ temperature: request.config?.temperature ?? this.config.temperature
790
+ },
657
791
  max_output_tokens: request.config?.maxTokens ?? this.config.maxTokens,
658
792
  ...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
659
- stream: false
793
+ stream
660
794
  };
795
+ return payload;
796
+ }
797
+ async completeWithResponses(request) {
798
+ const client = await this.getClient();
799
+ const payload = this.buildResponsesPayload(request, false);
661
800
  logProviderPayload("openai", "request payload", payload, request.debug);
662
801
  const response = await client.responses.create(payload);
663
802
  logProviderPayload("openai", "response payload", response, request.debug);
664
803
  return this.parseResponsesResult(response);
665
804
  }
805
+ /**
806
+ * Stream from /v1/responses as real SSE.
807
+ *
808
+ * The Responses API emits semantic events rather than Chat Completions
809
+ * deltas, so the interesting ones are:
810
+ * response.output_text.delta — incremental assistant text
811
+ * response.output_item.added — a function_call item begins (name/id)
812
+ * response.function_call_arguments.delta / .done — tool arguments
813
+ * response.completed — terminal, carries usage
814
+ *
815
+ * Tool calls are buffered and flushed as contiguous start→args→end triples
816
+ * because the runtime pairs action:args with the action:end that follows it.
817
+ */
818
+ async *streamWithResponses(request) {
819
+ const client = await this.getClient();
820
+ const payload = this.buildResponsesPayload(request, true);
821
+ logProviderPayload("openai", "request payload", payload, request.debug);
822
+ const stream = await client.responses.create(payload);
823
+ const toolCalls = /* @__PURE__ */ new Map();
824
+ let usage;
825
+ const citations = [];
826
+ function* flushToolCalls() {
827
+ for (const call of toolCalls.values()) {
828
+ yield { type: "action:start", id: call.id, name: call.name };
829
+ yield { type: "action:args", id: call.id, args: call.arguments };
830
+ yield { type: "action:end", id: call.id, name: call.name };
831
+ }
832
+ toolCalls.clear();
833
+ }
834
+ for await (const event of stream) {
835
+ if (request.signal?.aborted) break;
836
+ logProviderPayload("openai", "stream chunk", event, request.debug);
837
+ const index = event?.output_index ?? 0;
838
+ switch (event?.type) {
839
+ case "response.output_text.delta":
840
+ if (event.delta) {
841
+ yield { type: "message:delta", content: event.delta };
842
+ }
843
+ break;
844
+ case "response.output_item.added":
845
+ if (event.item?.type === "function_call") {
846
+ toolCalls.set(index, {
847
+ id: event.item.call_id ?? event.item.id ?? generateToolCallId(),
848
+ name: event.item.name ?? "",
849
+ arguments: event.item.arguments ?? ""
850
+ });
851
+ }
852
+ break;
853
+ case "response.function_call_arguments.delta": {
854
+ const existing = toolCalls.get(index);
855
+ if (existing && event.delta) existing.arguments += event.delta;
856
+ break;
857
+ }
858
+ case "response.function_call_arguments.done": {
859
+ const existing = toolCalls.get(index);
860
+ if (existing && typeof event.arguments === "string") {
861
+ existing.arguments = event.arguments;
862
+ }
863
+ break;
864
+ }
865
+ case "response.completed":
866
+ case "response.incomplete": {
867
+ const found = this.collectResponsesCitations(
868
+ event.response?.output,
869
+ citations.length
870
+ );
871
+ if (found.length > 0) citations.push(...found);
872
+ const u = event.response?.usage;
873
+ if (u) {
874
+ usage = {
875
+ prompt_tokens: u.input_tokens ?? 0,
876
+ completion_tokens: u.output_tokens ?? 0,
877
+ total_tokens: u.total_tokens ?? (u.input_tokens ?? 0) + (u.output_tokens ?? 0)
878
+ };
879
+ }
880
+ break;
881
+ }
882
+ case "error":
883
+ case "response.failed":
884
+ throw new Error(
885
+ event.message ?? event.response?.error?.message ?? "Responses stream failed"
886
+ );
887
+ }
888
+ }
889
+ yield* flushToolCalls();
890
+ if (citations.length > 0) {
891
+ yield { type: "citation", citations: deduplicateCitations(citations) };
892
+ }
893
+ yield { type: "message:end" };
894
+ yield { type: "done", usage };
895
+ }
666
896
  async *stream(request) {
667
897
  if (this.shouldUseResponsesApi(request)) {
668
898
  const messageId2 = generateMessageId();
669
899
  yield { type: "message:start", id: messageId2 };
670
900
  try {
671
- const result = await this.completeWithResponses(request);
672
- if (result.content) {
673
- yield { type: "message:delta", content: result.content };
674
- }
675
- for (const toolCall of result.toolCalls) {
676
- yield {
677
- type: "action:start",
678
- id: toolCall.id,
679
- name: toolCall.name
680
- };
681
- yield {
682
- type: "action:args",
683
- id: toolCall.id,
684
- args: JSON.stringify(toolCall.args)
685
- };
686
- }
687
- yield { type: "message:end" };
688
- yield {
689
- type: "done",
690
- usage: result.usage ? {
691
- prompt_tokens: result.usage.promptTokens,
692
- completion_tokens: result.usage.completionTokens,
693
- total_tokens: result.usage.totalTokens
694
- } : void 0
695
- };
901
+ yield* this.streamWithResponses(request);
696
902
  return;
697
903
  } catch (error) {
698
904
  yield {