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