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