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