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