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