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