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