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