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