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