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