@absolutejs/ai 0.0.48 → 0.0.50

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.
Files changed (44) hide show
  1. package/README.md +122 -21
  2. package/dist/ai/client/index.js +19 -22
  3. package/dist/ai/client/index.js.map +7 -8
  4. package/dist/ai/index.js +703 -63
  5. package/dist/ai/index.js.map +20 -18
  6. package/dist/ai/providers/anthropic.js +6 -2
  7. package/dist/ai/providers/anthropic.js.map +4 -4
  8. package/dist/ai/providers/gemini.js +33 -3
  9. package/dist/ai/providers/gemini.js.map +4 -4
  10. package/dist/ai/providers/ollama.js +3 -2
  11. package/dist/ai/providers/ollama.js.map +3 -3
  12. package/dist/ai/providers/openai.js +130 -35
  13. package/dist/ai/providers/openai.js.map +4 -4
  14. package/dist/ai/providers/openaiCompatible.js +130 -35
  15. package/dist/ai/providers/openaiCompatible.js.map +4 -4
  16. package/dist/ai/providers/openaiResponses.js +48 -18
  17. package/dist/ai/providers/openaiResponses.js.map +4 -4
  18. package/dist/ai/providers/openrouter.js +1757 -0
  19. package/dist/ai/providers/openrouter.js.map +17 -0
  20. package/dist/angular/ai/index.js +31 -3
  21. package/dist/angular/ai/index.js.map +6 -6
  22. package/dist/react/ai/index.js +18 -3
  23. package/dist/react/ai/index.js.map +6 -6
  24. package/dist/src/ai/client/actions.d.ts +22 -0
  25. package/dist/src/ai/client/createAIStream.d.ts +1 -0
  26. package/dist/src/ai/client/index.d.ts +0 -1
  27. package/dist/src/ai/conversationManager.d.ts +1 -0
  28. package/dist/src/ai/generateAI.d.ts +4 -1
  29. package/dist/src/ai/index.d.ts +2 -0
  30. package/dist/src/ai/providers/openai.d.ts +7 -3
  31. package/dist/src/ai/providers/openaiResponses.d.ts +9 -4
  32. package/dist/src/ai/providers/openrouter.d.ts +113 -0
  33. package/dist/src/ai/providers/openrouterClient.d.ts +147 -0
  34. package/dist/src/angular/ai/ai-stream.service.d.ts +1 -0
  35. package/dist/src/react/ai/useAIStream.d.ts +1 -0
  36. package/dist/src/svelte/ai/createAIStream.d.ts +1 -0
  37. package/dist/src/vue/ai/useAIStream.d.ts +2 -0
  38. package/dist/svelte/ai/index.js +18 -3
  39. package/dist/svelte/ai/index.js.map +6 -6
  40. package/dist/types/ai.d.ts +74 -7
  41. package/dist/vue/ai/index.js +18 -3
  42. package/dist/vue/ai/index.js.map +6 -6
  43. package/package.json +9 -2
  44. package/dist/src/ai/client/composer.d.ts +0 -15
package/dist/ai/index.js CHANGED
@@ -6,7 +6,8 @@ var PROVIDER_STATUS_PAGES = {
6
6
  anthropic: "https://status.claude.com",
7
7
  gemini: "https://status.cloud.google.com",
8
8
  google: "https://status.cloud.google.com",
9
- openai: "https://status.openai.com"
9
+ openai: "https://status.openai.com",
10
+ openrouter: "https://status.openrouter.ai"
10
11
  };
11
12
  var RETRYABLE_STATUSES = new Set([
12
13
  408,
@@ -456,7 +457,7 @@ var mapContentBlockToOpenAI = (block) => {
456
457
  if (block.type === "image") {
457
458
  return {
458
459
  image_url: {
459
- url: `data:${block.source.media_type};base64,${block.source.data}`
460
+ url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
460
461
  },
461
462
  type: "image_url"
462
463
  };
@@ -464,12 +465,26 @@ var mapContentBlockToOpenAI = (block) => {
464
465
  if (block.type === "document") {
465
466
  return {
466
467
  file: {
467
- file_data: `data:${block.source.media_type};base64,${block.source.data}`,
468
+ file_data: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`,
468
469
  filename: block.name ?? "document.pdf"
469
470
  },
470
471
  type: "file"
471
472
  };
472
473
  }
474
+ if (block.type === "audio") {
475
+ return {
476
+ input_audio: { data: block.source.data, format: block.source.format },
477
+ type: "input_audio"
478
+ };
479
+ }
480
+ if (block.type === "video") {
481
+ return {
482
+ type: "video_url",
483
+ video_url: {
484
+ url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
485
+ }
486
+ };
487
+ }
473
488
  if (block.type === "text") {
474
489
  return { text: block.content, type: "text" };
475
490
  }
@@ -479,13 +494,13 @@ var mapOpenAIContent = (msg) => {
479
494
  if (typeof msg.content === "string") {
480
495
  return msg.content;
481
496
  }
482
- const hasMedia = msg.content.some((block) => block.type === "image" || block.type === "document");
497
+ const hasMedia = msg.content.some((block) => block.type === "image" || block.type === "document" || block.type === "audio" || block.type === "video");
483
498
  if (!hasMedia) {
484
499
  return null;
485
500
  }
486
501
  return msg.content.map(mapContentBlockToOpenAI).filter((mapped) => mapped !== null);
487
502
  };
488
- var buildRequestBody = (params) => {
503
+ var buildRequestBody = (params, capabilityModel = params.model) => {
489
504
  const messages = convertToolResultMessages(params.messages.map((msg) => ({
490
505
  content: mapOpenAIContent(msg),
491
506
  role: msg.role
@@ -510,12 +525,12 @@ var buildRequestBody = (params) => {
510
525
  body.parallel_tool_calls = params.parallelToolCalls;
511
526
  }
512
527
  }
513
- if (isOpenAIReasoningModel(params.model)) {
528
+ if (isOpenAIReasoningModel(capabilityModel)) {
514
529
  if (typeof params.maxTokens === "number") {
515
530
  body.max_completion_tokens = params.maxTokens;
516
531
  }
517
532
  if (params.reasoning) {
518
- const effort = openaiEffortValue(params.model, params.reasoning);
533
+ const effort = openaiEffortValue(capabilityModel, params.reasoning);
519
534
  if (effort)
520
535
  body.reasoning_effort = effort;
521
536
  }
@@ -576,8 +591,12 @@ var extractUsage = (parsedUsage) => {
576
591
  const cached = parsedUsage.cached_tokens ?? 0;
577
592
  return {
578
593
  cacheReadInputTokens: cached,
594
+ cacheWriteInputTokens: parsedUsage.cache_write_tokens || undefined,
595
+ costCredits: parsedUsage.cost,
579
596
  inputTokens: Math.max(0, prompt - cached),
580
- outputTokens: parsedUsage.completion_tokens ?? 0
597
+ outputTokens: parsedUsage.completion_tokens ?? 0,
598
+ reasoningTokens: parsedUsage.reasoning_tokens || undefined,
599
+ upstreamInferenceCostCredits: parsedUsage.upstream_inference_cost || undefined
581
600
  };
582
601
  };
583
602
  var resolveToolCallIndex = (toolCall) => {
@@ -632,6 +651,35 @@ var processDelta = function* (delta, pendingToolCalls) {
632
651
  if (isRecordArray(delta.tool_calls)) {
633
652
  processToolCallDeltas(delta.tool_calls, pendingToolCalls);
634
653
  }
654
+ if (Array.isArray(delta.annotations)) {
655
+ for (const annotation of delta.annotations) {
656
+ if (!isRecord(annotation) || annotation.type !== "url_citation")
657
+ continue;
658
+ const citation = isRecord(annotation.url_citation) ? annotation.url_citation : annotation;
659
+ if (typeof citation.url !== "string")
660
+ continue;
661
+ yield {
662
+ content: typeof citation.content === "string" ? citation.content : undefined,
663
+ endIndex: typeof citation.end_index === "number" ? citation.end_index : undefined,
664
+ startIndex: typeof citation.start_index === "number" ? citation.start_index : undefined,
665
+ title: typeof citation.title === "string" ? citation.title : undefined,
666
+ type: "citation",
667
+ url: citation.url
668
+ };
669
+ }
670
+ }
671
+ };
672
+ var narrowResponseMetadata = (parsed) => {
673
+ const providerMetadata = isRecord(parsed.openrouter_metadata) ? parsed.openrouter_metadata : undefined;
674
+ const generationId = typeof parsed.id === "string" ? parsed.id : undefined;
675
+ const model = typeof parsed.model === "string" ? parsed.model : undefined;
676
+ const serviceTier = typeof parsed.service_tier === "string" ? parsed.service_tier : undefined;
677
+ const selected = providerMetadata && isRecord(providerMetadata.endpoints) ? providerMetadata.endpoints.available : undefined;
678
+ const selectedEndpoint = Array.isArray(selected) ? selected.find((entry) => isRecord(entry) && entry.selected === true) : undefined;
679
+ const provider = isRecord(selectedEndpoint) && typeof selectedEndpoint.provider === "string" ? selectedEndpoint.provider : undefined;
680
+ if (!providerMetadata && !generationId && !model && !serviceTier)
681
+ return;
682
+ return { generationId, model, provider, providerMetadata, serviceTier };
635
683
  };
636
684
  var processChoice = function* (choice, pendingToolCalls) {
637
685
  const delta = isRecord(choice.delta) ? choice.delta : null;
@@ -650,13 +698,25 @@ var narrowUsageRecord = (parsed) => {
650
698
  const promptTokens = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
651
699
  const completionTokens = typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0;
652
700
  const cachedTokens = isRecord(usage.prompt_tokens_details) && typeof usage.prompt_tokens_details.cached_tokens === "number" ? usage.prompt_tokens_details.cached_tokens : 0;
653
- return extractUsage({
701
+ const cacheWriteTokens = isRecord(usage.prompt_tokens_details) && typeof usage.prompt_tokens_details.cache_write_tokens === "number" ? usage.prompt_tokens_details.cache_write_tokens : 0;
702
+ const reasoningTokens = isRecord(usage.completion_tokens_details) && typeof usage.completion_tokens_details.reasoning_tokens === "number" ? usage.completion_tokens_details.reasoning_tokens : 0;
703
+ const cost = typeof usage.cost === "number" ? usage.cost : undefined;
704
+ const upstreamInferenceCost = isRecord(usage.cost_details) && typeof usage.cost_details.upstream_inference_cost === "number" ? usage.cost_details.upstream_inference_cost : undefined;
705
+ const normalized = extractUsage({
706
+ cache_write_tokens: cacheWriteTokens,
654
707
  cached_tokens: cachedTokens,
655
708
  completion_tokens: completionTokens,
656
- prompt_tokens: promptTokens
709
+ cost,
710
+ prompt_tokens: promptTokens,
711
+ reasoning_tokens: reasoningTokens,
712
+ upstream_inference_cost: upstreamInferenceCost
657
713
  });
714
+ if (isRecord(usage.server_tool_use)) {
715
+ normalized.serverToolUse = Object.fromEntries(Object.entries(usage.server_tool_use).filter((entry) => typeof entry[1] === "number"));
716
+ }
717
+ return normalized;
658
718
  };
659
- var processSSELine = function* (line, pendingToolCalls, currentUsage) {
719
+ var processSSELine = function* (line, pendingToolCalls) {
660
720
  const trimmed = line.trim();
661
721
  if (!trimmed || !trimmed.startsWith("data: ")) {
662
722
  return;
@@ -664,7 +724,6 @@ var processSSELine = function* (line, pendingToolCalls, currentUsage) {
664
724
  const data = trimmed.slice(SSE_DATA_PREFIX_LENGTH);
665
725
  if (data === DONE_SENTINEL) {
666
726
  yield* flushPendingToolCalls(pendingToolCalls);
667
- yield { type: "done", usage: currentUsage };
668
727
  return;
669
728
  }
670
729
  let parsed;
@@ -677,6 +736,10 @@ var processSSELine = function* (line, pendingToolCalls, currentUsage) {
677
736
  if (usageUpdate) {
678
737
  yield { type: "usage_update", usage: usageUpdate };
679
738
  }
739
+ const metadata = narrowResponseMetadata(parsed);
740
+ if (metadata) {
741
+ yield { metadata, type: "response_metadata" };
742
+ }
680
743
  const { choices } = parsed;
681
744
  if (!isRecordArray(choices)) {
682
745
  return;
@@ -688,18 +751,30 @@ var processSSELine = function* (line, pendingToolCalls, currentUsage) {
688
751
  yield* processChoice(firstChoice, pendingToolCalls);
689
752
  };
690
753
  var isUsageUpdate = (chunk) => chunk.type === "usage_update";
691
- var collectYieldableChunks = (line, pendingToolCalls, usageRef) => {
692
- const allChunks = Array.from(processSSELine(line, pendingToolCalls, usageRef.current));
754
+ var collectYieldableChunks = (line, pendingToolCalls, usageRef, metadataRef) => {
755
+ const allChunks = Array.from(processSSELine(line, pendingToolCalls));
693
756
  const usageChunks = allChunks.filter(isUsageUpdate);
694
757
  const lastUsage = usageChunks.at(NOT_FOUND);
695
758
  if (lastUsage) {
696
759
  usageRef.current = lastUsage.usage;
697
760
  }
698
- return allChunks.filter((chunk) => !isUsageUpdate(chunk));
761
+ const metadataChunks = allChunks.filter((chunk) => chunk.type === "response_metadata");
762
+ const lastMetadata = metadataChunks.at(NOT_FOUND);
763
+ if (lastMetadata && "metadata" in lastMetadata) {
764
+ metadataRef.current = {
765
+ ...metadataRef.current,
766
+ ...lastMetadata.metadata,
767
+ providerMetadata: {
768
+ ...metadataRef.current?.providerMetadata,
769
+ ...lastMetadata.metadata.providerMetadata
770
+ }
771
+ };
772
+ }
773
+ return allChunks.filter((chunk) => !isUsageUpdate(chunk) && chunk.type !== "response_metadata");
699
774
  };
700
- var processSSELines = function* (lines, pendingToolCalls, usageRef) {
775
+ var processSSELines = function* (lines, pendingToolCalls, usageRef, metadataRef) {
701
776
  for (const line of lines) {
702
- yield* collectYieldableChunks(line, pendingToolCalls, usageRef);
777
+ yield* collectYieldableChunks(line, pendingToolCalls, usageRef, metadataRef);
703
778
  }
704
779
  };
705
780
  var processStreamValue = (value, decoder, state) => {
@@ -712,51 +787,66 @@ var processStreamValue = (value, decoder, state) => {
712
787
  var drainReader = async function* (reader, decoder, state, signal) {
713
788
  for (let result = await reader.read();!result.done && !signal?.aborted; result = await reader.read()) {
714
789
  const lines = processStreamValue(result.value, decoder, state);
715
- yield* processSSELines(lines, state.pendingToolCalls, state.usageRef);
790
+ yield* processSSELines(lines, state.pendingToolCalls, state.usageRef, state.metadataRef);
716
791
  }
717
792
  };
718
- var parseSSEStream = async function* (body, signal) {
793
+ var parseSSEStream = async function* (body, initialMetadata, signal) {
719
794
  const reader = body.getReader();
720
795
  const decoder = new TextDecoder;
721
796
  const state = {
722
797
  buffer: "",
798
+ metadataRef: { current: initialMetadata },
723
799
  pendingToolCalls: new Map,
724
800
  usageRef: { current: undefined }
725
801
  };
726
802
  try {
727
803
  yield* drainReader(reader, decoder, state, signal);
728
- yield { type: "done", usage: state.usageRef.current };
804
+ yield {
805
+ metadata: state.metadataRef.current,
806
+ type: "done",
807
+ usage: state.usageRef.current
808
+ };
729
809
  } finally {
730
810
  reader.releaseLock();
731
811
  }
732
812
  };
733
- var fetchOpenAIStream = async function* (baseUrl, apiKey, body, signal) {
813
+ var fetchOpenAIStream = async function* (baseUrl, apiKey, body, fetchImpl, headers, providerName, signal) {
734
814
  const target = `${baseUrl}/v1/chat/completions`;
735
- const response = await fetch(target, {
815
+ const requestHeaders = new Headers(headers);
816
+ requestHeaders.set("Authorization", `Bearer ${apiKey}`);
817
+ requestHeaders.set("Content-Type", "application/json");
818
+ const response = await fetchImpl(target, {
736
819
  ...h2IfHttps(target),
737
820
  body: JSON.stringify(body),
738
- headers: {
739
- Authorization: `Bearer ${apiKey}`,
740
- "Content-Type": "application/json"
741
- },
821
+ headers: requestHeaders,
742
822
  method: "POST",
743
823
  signal
744
824
  });
745
825
  if (!response.ok) {
746
826
  const errorText = await response.text();
747
- throw ProviderError.fromResponse("openai", response.status, errorText);
827
+ throw ProviderError.fromResponse(providerName, response.status, errorText);
748
828
  }
749
829
  if (!response.body) {
750
830
  throw new ProviderError({
751
- message: "OpenAI API returned no response body",
752
- provider: "openai",
831
+ message: `${providerName} API returned no response body`,
832
+ provider: providerName,
753
833
  retryable: true
754
834
  });
755
835
  }
756
- yield* parseSSEStream(response.body, signal);
836
+ yield* parseSSEStream(response.body, {
837
+ generationId: response.headers.get("X-Generation-Id") ?? undefined,
838
+ providerMetadata: {
839
+ cacheAge: response.headers.get("X-OpenRouter-Cache-Age") ?? undefined,
840
+ cacheSourceId: response.headers.get("X-OpenRouter-Cache-Source-Id") ?? undefined,
841
+ cacheStatus: response.headers.get("X-OpenRouter-Cache-Status") ?? undefined,
842
+ cacheTtl: response.headers.get("X-OpenRouter-Cache-TTL") ?? undefined
843
+ }
844
+ }, signal);
757
845
  };
758
846
  var openai = (config2) => {
759
847
  const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL;
848
+ const fetchImpl = config2.fetch ?? globalThis.fetch;
849
+ const providerName = config2.providerName ?? "openai";
760
850
  if (!config2.apiKey && !config2.tokenSource) {
761
851
  throw new Error("openai() requires either apiKey or tokenSource");
762
852
  }
@@ -766,15 +856,20 @@ var openai = (config2) => {
766
856
  }
767
857
  return config2.apiKey;
768
858
  };
859
+ const resolveHeaders = async (params) => typeof config2.headers === "function" ? await config2.headers(params) : config2.headers ?? {};
769
860
  return instrumentAIProvider({
770
861
  stream: (params) => {
771
- const body = buildRequestBody(params);
862
+ const openaiBody = buildRequestBody(params, config2.modelForCapabilities?.(params.model) ?? params.model);
863
+ const body = config2.transformRequestBody ? config2.transformRequestBody(openaiBody, params) : openaiBody;
772
864
  return async function* () {
773
- const apiKey = await resolveKey();
774
- yield* fetchOpenAIStream(baseUrl, apiKey, body, params.signal);
865
+ const [apiKey, headers] = await Promise.all([
866
+ resolveKey(),
867
+ resolveHeaders(params)
868
+ ]);
869
+ yield* fetchOpenAIStream(baseUrl, apiKey, body, fetchImpl, headers, providerName, params.signal);
775
870
  }();
776
871
  }
777
- }, "openai");
872
+ }, providerName);
778
873
  };
779
874
 
780
875
  // src/ai/providers/openaiCompatible.ts
@@ -826,7 +921,7 @@ var mapBlockToResponsesFormat = (block) => {
826
921
  if (block.type === "image") {
827
922
  return {
828
923
  image_url: {
829
- url: `data:${block.source.media_type};base64,${block.source.data}`
924
+ url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
830
925
  },
831
926
  type: "input_image"
832
927
  };
@@ -834,12 +929,24 @@ var mapBlockToResponsesFormat = (block) => {
834
929
  if (block.type === "document") {
835
930
  return {
836
931
  file: {
837
- file_data: `data:${block.source.media_type};base64,${block.source.data}`,
932
+ file_data: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`,
838
933
  filename: block.name ?? "document.pdf"
839
934
  },
840
935
  type: "input_file"
841
936
  };
842
937
  }
938
+ if (block.type === "audio") {
939
+ return {
940
+ input_audio: { data: block.source.data, format: block.source.format },
941
+ type: "input_audio"
942
+ };
943
+ }
944
+ if (block.type === "video") {
945
+ return {
946
+ type: "input_video",
947
+ video_url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
948
+ };
949
+ }
843
950
  return null;
844
951
  };
845
952
  var mapContentToResponsesFormat = (content) => {
@@ -903,7 +1010,7 @@ var buildTools = (tools, isImageModel) => {
903
1010
  }
904
1011
  return result.length > 0 ? result : undefined;
905
1012
  };
906
- var buildRequestBody2 = (params, isImageModel) => {
1013
+ var buildRequestBody2 = (params, isImageModel, capabilityModel = params.model) => {
907
1014
  const body = {
908
1015
  input: buildInput(params.messages),
909
1016
  model: params.model,
@@ -955,8 +1062,8 @@ var buildRequestBody2 = (params, isImageModel) => {
955
1062
  };
956
1063
  }
957
1064
  }
958
- if (params.reasoning && isOpenAIReasoningModel(params.model)) {
959
- const effort = openaiEffortValue(params.model, params.reasoning);
1065
+ if (params.reasoning && isOpenAIReasoningModel(capabilityModel)) {
1066
+ const effort = openaiEffortValue(capabilityModel, params.reasoning);
960
1067
  if (effort) {
961
1068
  body.reasoning = {
962
1069
  effort,
@@ -1193,24 +1300,28 @@ var parseSSEStream2 = async function* (body, signal) {
1193
1300
  reader.releaseLock();
1194
1301
  }
1195
1302
  };
1196
- var fetchResponsesStream = async function* (baseUrl, apiKey, body, signal) {
1303
+ var fetchResponsesStream = async function* (baseUrl, apiKey, body, fetchImpl, headers, providerName, signal) {
1197
1304
  const target = `${baseUrl}/v1/responses`;
1198
- const response = await fetch(target, {
1305
+ const requestHeaders = new Headers(headers);
1306
+ requestHeaders.set("Authorization", `Bearer ${apiKey}`);
1307
+ requestHeaders.set("Content-Type", "application/json");
1308
+ const response = await fetchImpl(target, {
1199
1309
  ...h2IfHttps2(target),
1200
1310
  body: JSON.stringify(body),
1201
- headers: {
1202
- Authorization: `Bearer ${apiKey}`,
1203
- "Content-Type": "application/json"
1204
- },
1311
+ headers: requestHeaders,
1205
1312
  method: "POST",
1206
1313
  signal
1207
1314
  });
1208
1315
  if (!response.ok) {
1209
1316
  const errorText = await response.text();
1210
- throw new Error(`OpenAI Responses API error ${response.status}: ${errorText}`);
1317
+ throw ProviderError.fromResponse(providerName, response.status, errorText);
1211
1318
  }
1212
1319
  if (!response.body) {
1213
- throw new Error("OpenAI Responses API returned no response body");
1320
+ throw new ProviderError({
1321
+ message: `${providerName} Responses API returned no response body`,
1322
+ provider: providerName,
1323
+ retryable: true
1324
+ });
1214
1325
  }
1215
1326
  yield* parseSSEStream2(response.body, signal);
1216
1327
  };
@@ -1224,15 +1335,28 @@ var resolveImageModels = (imageModels) => {
1224
1335
  return new Set(imageModels);
1225
1336
  };
1226
1337
  var openaiResponses = (config2) => {
1338
+ if (!config2.apiKey && !config2.tokenSource)
1339
+ throw new Error("openaiResponses() requires either apiKey or tokenSource");
1227
1340
  const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL2;
1341
+ const fetchImpl = config2.fetch ?? globalThis.fetch;
1228
1342
  const imageModels = resolveImageModels(config2.imageModels);
1343
+ const providerName = config2.providerName ?? "openai-responses";
1344
+ const resolveKey = async () => config2.tokenSource ? await Promise.resolve(config2.tokenSource()) : config2.apiKey;
1345
+ const resolveHeaders = async (params) => typeof config2.headers === "function" ? await config2.headers(params) : config2.headers ?? {};
1229
1346
  return instrumentAIProvider({
1230
1347
  stream: (params) => {
1231
1348
  const isImageModel = imageModels.has(params.model);
1232
- const body = buildRequestBody2(params, isImageModel);
1233
- return fetchResponsesStream(baseUrl, config2.apiKey, body, params.signal);
1349
+ const builtBody = buildRequestBody2(params, isImageModel, config2.modelForCapabilities?.(params.model) ?? params.model);
1350
+ const body = config2.transformRequestBody ? config2.transformRequestBody(builtBody, params) : builtBody;
1351
+ return async function* () {
1352
+ const [apiKey, headers] = await Promise.all([
1353
+ resolveKey(),
1354
+ resolveHeaders(params)
1355
+ ]);
1356
+ yield* fetchResponsesStream(baseUrl, apiKey, body, fetchImpl, headers, providerName, params.signal);
1357
+ }();
1234
1358
  }
1235
- }, "openai-responses");
1359
+ }, providerName);
1236
1360
  };
1237
1361
 
1238
1362
  // src/ai/providers/gemini.ts
@@ -1251,14 +1375,43 @@ var mapContentBlock = (block) => {
1251
1375
  case "text":
1252
1376
  return { text: block.content };
1253
1377
  case "image":
1254
- return {
1378
+ return block.source.type === "url" ? {
1379
+ fileData: {
1380
+ fileUri: block.source.url,
1381
+ mimeType: block.source.media_type
1382
+ }
1383
+ } : {
1255
1384
  inlineData: {
1256
1385
  data: block.source.data,
1257
1386
  mimeType: block.source.media_type
1258
1387
  }
1259
1388
  };
1260
1389
  case "document":
1390
+ return block.source.type === "url" ? {
1391
+ fileData: {
1392
+ fileUri: block.source.url,
1393
+ mimeType: block.source.media_type
1394
+ }
1395
+ } : {
1396
+ inlineData: {
1397
+ data: block.source.data,
1398
+ mimeType: block.source.media_type
1399
+ }
1400
+ };
1401
+ case "audio":
1261
1402
  return {
1403
+ inlineData: {
1404
+ data: block.source.data,
1405
+ mimeType: `audio/${block.source.format}`
1406
+ }
1407
+ };
1408
+ case "video":
1409
+ return block.source.type === "url" ? {
1410
+ fileData: {
1411
+ fileUri: block.source.url,
1412
+ mimeType: block.source.media_type
1413
+ }
1414
+ } : {
1262
1415
  inlineData: {
1263
1416
  data: block.source.data,
1264
1417
  mimeType: block.source.media_type
@@ -1571,6 +1724,9 @@ var mapContentBlock2 = (block) => {
1571
1724
  type: "tool_use"
1572
1725
  };
1573
1726
  }
1727
+ if (block.type === "audio" || block.type === "video") {
1728
+ throw new Error(`Anthropic does not support ${block.type} content blocks`);
1729
+ }
1574
1730
  return { text: block.content, type: "text" };
1575
1731
  };
1576
1732
  var mapMessage = (msg) => ({
@@ -2228,6 +2384,429 @@ var ollama = (config2 = {}) => {
2228
2384
  }, "ollama");
2229
2385
  };
2230
2386
 
2387
+ // src/ai/providers/openrouterClient.ts
2388
+ var DEFAULT_BASE_URL6 = "https://openrouter.ai/api/v1";
2389
+ var withoutLatestPrefix = (model) => model.startsWith("~") ? model.slice(1) : model;
2390
+ var openRouterModelMatchesRule = (model, rule) => {
2391
+ const normalizedModel = withoutLatestPrefix(model);
2392
+ const normalizedRule = withoutLatestPrefix(rule);
2393
+ return normalizedRule.endsWith("/*") ? normalizedModel.startsWith(normalizedRule.slice(0, -1)) : normalizedModel === normalizedRule;
2394
+ };
2395
+ var assertAllowedModel = (model, allowedModels) => {
2396
+ if (!allowedModels)
2397
+ return;
2398
+ if (allowedModels.some((rule) => openRouterModelMatchesRule(model, rule)))
2399
+ return;
2400
+ throw new Error(`OpenRouter model "${model}" is not allowed`);
2401
+ };
2402
+ var normalizePath = (path) => path.startsWith("/") ? path : `/${path}`;
2403
+ var encodeModelPath = (model) => model.split("/").map(encodeURIComponent).join("/");
2404
+ var withQuery = (url, query) => {
2405
+ if (!query)
2406
+ return url;
2407
+ const result = new URL(url);
2408
+ for (const [key, value] of Object.entries(query)) {
2409
+ if (value !== undefined)
2410
+ result.searchParams.set(key, String(value));
2411
+ }
2412
+ return result.toString();
2413
+ };
2414
+ var createOpenRouterClient = (config2) => {
2415
+ if (!config2.apiKey && !config2.tokenSource)
2416
+ throw new Error("createOpenRouterClient() requires either apiKey or tokenSource");
2417
+ const baseUrl = (config2.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
2418
+ const fetchImpl = config2.fetch ?? globalThis.fetch;
2419
+ const allowedModels = config2.allowedModels ? [...config2.allowedModels] : undefined;
2420
+ const requestRaw = async (path, options = {}) => {
2421
+ const token = config2.tokenSource ? await Promise.resolve(config2.tokenSource()) : config2.apiKey;
2422
+ const suppliedHeaders = typeof config2.headers === "function" ? await config2.headers() : config2.headers ?? {};
2423
+ const headers = new Headers(suppliedHeaders);
2424
+ new Headers(options.headers).forEach((value, key) => headers.set(key, value));
2425
+ headers.set("Authorization", `Bearer ${token}`);
2426
+ let body;
2427
+ if (options.body instanceof FormData || options.body instanceof Blob) {
2428
+ body = options.body;
2429
+ } else if (options.body !== undefined) {
2430
+ headers.set("Content-Type", "application/json");
2431
+ body = JSON.stringify(options.body);
2432
+ }
2433
+ const { query, ...requestInit } = options;
2434
+ const response = await fetchImpl(withQuery(`${baseUrl}${normalizePath(path)}`, options.query), { ...requestInit, body, headers });
2435
+ if (!response.ok) {
2436
+ throw ProviderError.fromResponse("openrouter", response.status, await response.text());
2437
+ }
2438
+ return response;
2439
+ };
2440
+ const request = async (path, options = {}) => (await requestRaw(path, options)).json();
2441
+ const listModels = async (query) => {
2442
+ const result = await request("/models", { query });
2443
+ if (!allowedModels)
2444
+ return result;
2445
+ return {
2446
+ ...result,
2447
+ data: result.data.filter((model) => allowedModels.some((rule) => openRouterModelMatchesRule(model.id, rule)))
2448
+ };
2449
+ };
2450
+ const filterModelList = (result) => {
2451
+ if (!allowedModels)
2452
+ return result;
2453
+ return {
2454
+ ...result,
2455
+ data: result.data.filter((model) => allowedModels.some((rule) => openRouterModelMatchesRule(model.id, rule)))
2456
+ };
2457
+ };
2458
+ return {
2459
+ cancelBatch: (id) => request(`/batches/${encodeURIComponent(id)}/cancel`, {
2460
+ method: "POST"
2461
+ }),
2462
+ createBatch: (body) => request("/batches", { body, method: "POST" }),
2463
+ createEmbedding: (body) => {
2464
+ assertAllowedModel(body.model, allowedModels);
2465
+ return request("/embeddings", {
2466
+ body,
2467
+ method: "POST"
2468
+ });
2469
+ },
2470
+ generateImage: (body) => {
2471
+ assertAllowedModel(body.model, allowedModels);
2472
+ return request("/images", {
2473
+ body,
2474
+ method: "POST"
2475
+ });
2476
+ },
2477
+ generateVideo: (body) => {
2478
+ assertAllowedModel(body.model, allowedModels);
2479
+ return request("/videos", {
2480
+ body,
2481
+ method: "POST"
2482
+ });
2483
+ },
2484
+ getBatch: (id) => request(`/batches/${encodeURIComponent(id)}`),
2485
+ getCredits: () => request("/credits"),
2486
+ getCurrentKey: () => request("/key"),
2487
+ getGeneration: (id) => request("/generation", {
2488
+ query: { id }
2489
+ }),
2490
+ getModelEndpoints: (model) => {
2491
+ assertAllowedModel(model, allowedModels);
2492
+ return request(`/models/${encodeModelPath(model)}/endpoints`);
2493
+ },
2494
+ getVideo: (id) => request(`/videos/${encodeURIComponent(id)}`),
2495
+ listImageModels: async () => filterModelList(await request("/images/models")),
2496
+ listModels,
2497
+ listPresets: (offset = 0, limit = 100) => request("/presets", { query: { limit, offset } }),
2498
+ listProviders: () => request("/providers"),
2499
+ listRerankModels: async () => filterModelList(await request("/rerank/models")),
2500
+ listVideoModels: async () => filterModelList(await request("/videos/models")),
2501
+ request,
2502
+ requestRaw,
2503
+ respond: (body) => {
2504
+ assertAllowedModel(body.model, allowedModels);
2505
+ return body.stream ? requestRaw("/responses", { body, method: "POST" }) : request("/responses", {
2506
+ body,
2507
+ method: "POST"
2508
+ });
2509
+ },
2510
+ rerank: (body) => {
2511
+ assertAllowedModel(body.model, allowedModels);
2512
+ return request("/rerank", {
2513
+ body,
2514
+ method: "POST"
2515
+ });
2516
+ },
2517
+ speak: (body) => {
2518
+ assertAllowedModel(body.model, allowedModels);
2519
+ return requestRaw("/audio/speech", { body, method: "POST" });
2520
+ },
2521
+ transcribe: (body) => {
2522
+ if (body instanceof FormData) {
2523
+ const model = body.get("model");
2524
+ if (typeof model !== "string")
2525
+ throw new Error("OpenRouter transcription FormData requires model");
2526
+ assertAllowedModel(model, allowedModels);
2527
+ } else {
2528
+ assertAllowedModel(body.model, allowedModels);
2529
+ }
2530
+ return request("/audio/transcriptions", {
2531
+ body,
2532
+ method: "POST"
2533
+ });
2534
+ }
2535
+ };
2536
+ };
2537
+
2538
+ // src/ai/providers/openrouter.ts
2539
+ var DEFAULT_BASE_URL7 = "https://openrouter.ai/api";
2540
+ var MAX_APP_CATEGORIES = 2;
2541
+ var withoutLatestPrefix2 = (model) => model.startsWith("~") ? model.slice(1) : model;
2542
+ var modelForOpenAICapabilities = (model) => {
2543
+ const normalized = withoutLatestPrefix2(model);
2544
+ return normalized.startsWith("openai/") ? normalized.slice("openai/".length) : normalized;
2545
+ };
2546
+ var modelMatchesRule = (model, rule) => {
2547
+ const normalizedModel = withoutLatestPrefix2(model);
2548
+ const normalizedRule = withoutLatestPrefix2(rule);
2549
+ if (normalizedRule.endsWith("/*")) {
2550
+ return normalizedModel.startsWith(normalizedRule.slice(0, -1));
2551
+ }
2552
+ return normalizedModel === normalizedRule;
2553
+ };
2554
+ var providerMatchesRule = (provider, rule) => provider === rule || provider.startsWith(`${rule}/`);
2555
+ var assertNonEmptyPolicy = (label, value) => {
2556
+ if (value && value.length === 0) {
2557
+ throw new Error(`openrouter() ${label} must not be empty`);
2558
+ }
2559
+ };
2560
+ var assertRoutingPolicy = (config2) => {
2561
+ assertNonEmptyPolicy("allowedProviders", config2.allowedProviders);
2562
+ assertNonEmptyPolicy("routing.only", config2.routing?.only);
2563
+ assertNonEmptyPolicy("allowedPresets", config2.allowedPresets);
2564
+ if (config2.appCategories && config2.appCategories.length > MAX_APP_CATEGORIES) {
2565
+ throw new Error("openrouter() appCategories supports at most 2 entries");
2566
+ }
2567
+ if (!config2.allowedProviders)
2568
+ return;
2569
+ const selected = [
2570
+ ...config2.routing?.only ?? [],
2571
+ ...config2.routing?.order ?? []
2572
+ ];
2573
+ const denied = selected.find((provider) => !config2.allowedProviders.some((rule) => providerMatchesRule(provider, rule)));
2574
+ if (denied) {
2575
+ throw new Error(`openrouter() provider "${denied}" is outside allowedProviders`);
2576
+ }
2577
+ };
2578
+ var assertRequestRoutingPolicy = (routing, allowedProviders) => {
2579
+ assertRoutingPolicy({
2580
+ allowedProviders,
2581
+ apiKey: "policy-validation",
2582
+ routing
2583
+ });
2584
+ };
2585
+ var mapRouting = (routing, allowedProviders) => {
2586
+ const only = routing?.only ?? allowedProviders;
2587
+ const wire = {};
2588
+ if (typeof routing?.allowFallbacks === "boolean")
2589
+ wire.allow_fallbacks = routing.allowFallbacks;
2590
+ if (routing?.dataCollection)
2591
+ wire.data_collection = routing.dataCollection;
2592
+ if (typeof routing?.enforceDistillableText === "boolean")
2593
+ wire.enforce_distillable_text = routing.enforceDistillableText;
2594
+ if (routing?.ignore)
2595
+ wire.ignore = [...routing.ignore];
2596
+ if (routing?.maxPrice)
2597
+ wire.max_price = { ...routing.maxPrice };
2598
+ if (only)
2599
+ wire.only = [...only];
2600
+ if (routing?.order)
2601
+ wire.order = [...routing.order];
2602
+ if (routing?.preferredMaxLatency !== undefined)
2603
+ wire.preferred_max_latency = routing.preferredMaxLatency;
2604
+ if (routing?.preferredMinThroughput !== undefined)
2605
+ wire.preferred_min_throughput = routing.preferredMinThroughput;
2606
+ if (routing?.quantizations)
2607
+ wire.quantizations = [...routing.quantizations];
2608
+ if (typeof routing?.requireParameters === "boolean")
2609
+ wire.require_parameters = routing.requireParameters;
2610
+ if (routing?.sort)
2611
+ wire.sort = routing.sort;
2612
+ if (typeof routing?.zdr === "boolean")
2613
+ wire.zdr = routing.zdr;
2614
+ return wire;
2615
+ };
2616
+ var requestOptionsFor = (params, defaults) => {
2617
+ const supplied = params.providerOptions?.openrouter;
2618
+ if (supplied !== undefined && (typeof supplied !== "object" || !supplied)) {
2619
+ throw new Error("providerOptions.openrouter must be an object");
2620
+ }
2621
+ return {
2622
+ ...defaults,
2623
+ ...supplied
2624
+ };
2625
+ };
2626
+ var resolveAttributionHeaders = async (config2, params) => {
2627
+ const supplied = typeof config2.headers === "function" ? await config2.headers() : config2.headers ?? {};
2628
+ const headers = new Headers(supplied);
2629
+ if (config2.appUrl)
2630
+ headers.set("HTTP-Referer", config2.appUrl);
2631
+ if (config2.appName)
2632
+ headers.set("X-OpenRouter-Title", config2.appName);
2633
+ if (config2.appCategories?.length) {
2634
+ headers.set("X-OpenRouter-Categories", config2.appCategories.join(","));
2635
+ }
2636
+ const options = requestOptionsFor(params, config2.requestOptions);
2637
+ if (options.routerMetadata ?? true)
2638
+ headers.set("X-OpenRouter-Metadata", "enabled");
2639
+ if (options.sessionId)
2640
+ headers.set("X-Session-Id", options.sessionId);
2641
+ if (options.responseCache) {
2642
+ headers.set("X-OpenRouter-Cache", options.responseCache.enabled ? "true" : "false");
2643
+ if (options.responseCache.ttlSeconds !== undefined)
2644
+ headers.set("X-OpenRouter-Cache-TTL", String(options.responseCache.ttlSeconds));
2645
+ if (options.responseCache.clear)
2646
+ headers.set("X-OpenRouter-Cache-Clear", "true");
2647
+ }
2648
+ return headers;
2649
+ };
2650
+ var SECURITY_SENSITIVE_EXTRA_BODY_FIELDS = new Set([
2651
+ "messages",
2652
+ "model",
2653
+ "models",
2654
+ "plugins",
2655
+ "preset",
2656
+ "provider",
2657
+ "stream",
2658
+ "tools"
2659
+ ]);
2660
+ var assertAllowedPreset = (preset, allowedPresets) => {
2661
+ if (!preset)
2662
+ return;
2663
+ if (allowedPresets?.includes(preset))
2664
+ return;
2665
+ throw new Error(`OpenRouter preset "${preset}" is not allowed`);
2666
+ };
2667
+ var assertIndirectModels = (value, allowedModels, key = "") => {
2668
+ if (key === "model" && typeof value === "string")
2669
+ assertAllowedModel2(value, allowedModels);
2670
+ if (key === "models" && Array.isArray(value)) {
2671
+ for (const model of value) {
2672
+ if (typeof model === "string")
2673
+ assertAllowedModel2(model, allowedModels);
2674
+ }
2675
+ }
2676
+ if (Array.isArray(value)) {
2677
+ for (const item of value)
2678
+ assertIndirectModels(item, allowedModels);
2679
+ } else if (value && typeof value === "object") {
2680
+ for (const [childKey, child] of Object.entries(value))
2681
+ assertIndirectModels(child, allowedModels, childKey);
2682
+ }
2683
+ };
2684
+ var assertRequestOptions = (options, allowedModels, allowedPresets, allowedProviders) => {
2685
+ assertAllowedPreset(options.preset, allowedPresets);
2686
+ assertRequestRoutingPolicy(options.routing, allowedProviders);
2687
+ if (options.sessionId && options.sessionId.length > 256)
2688
+ throw new Error("OpenRouter sessionId must be at most 256 characters");
2689
+ const ttl = options.responseCache?.ttlSeconds;
2690
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 1 || ttl > 86400))
2691
+ throw new Error("OpenRouter response-cache TTL must be 1-86400 seconds");
2692
+ if (options.responseCache?.clear && !options.responseCache.enabled)
2693
+ throw new Error("OpenRouter cache clear requires response caching enabled");
2694
+ if (options.fallbackModels) {
2695
+ if (options.fallbackModels.length === 0)
2696
+ throw new Error("OpenRouter fallbackModels must not be empty");
2697
+ for (const model of options.fallbackModels)
2698
+ assertAllowedModel2(model, allowedModels);
2699
+ }
2700
+ assertIndirectModels(options.serverTools, allowedModels);
2701
+ assertIndirectModels(options.plugins, allowedModels);
2702
+ if (options.extraBody) {
2703
+ const unsafe = Object.keys(options.extraBody).find((key) => SECURITY_SENSITIVE_EXTRA_BODY_FIELDS.has(key));
2704
+ if (unsafe)
2705
+ throw new Error(`OpenRouter extraBody cannot override "${unsafe}"`);
2706
+ }
2707
+ };
2708
+ var assertAllowedModel2 = (model, allowedModels) => {
2709
+ if (!allowedModels)
2710
+ return;
2711
+ if (allowedModels.some((rule) => modelMatchesRule(model, rule)))
2712
+ return;
2713
+ throw new Error(`OpenRouter model "${model}" is not allowed`);
2714
+ };
2715
+ var snapshotPolicy = (config2) => ({
2716
+ allowedModels: config2.allowedModels ? [...config2.allowedModels] : undefined,
2717
+ allowedPresets: config2.allowedPresets ? [...config2.allowedPresets] : undefined
2718
+ });
2719
+ var transformOpenRouterRequest = (config2, allowedModels, allowedPresets, body, params) => {
2720
+ const options = requestOptionsFor(params, config2.requestOptions);
2721
+ assertRequestOptions(options, allowedModels, allowedPresets, config2.allowedProviders);
2722
+ const transformed = { ...body, ...options.extraBody };
2723
+ if (params.reasoning?.budgetTokens !== undefined) {
2724
+ transformed.reasoning = { max_tokens: params.reasoning.budgetTokens };
2725
+ delete transformed.reasoning_effort;
2726
+ } else if (params.reasoning?.effort) {
2727
+ transformed.reasoning = { effort: params.reasoning.effort };
2728
+ delete transformed.reasoning_effort;
2729
+ }
2730
+ const routing = mapRouting({ ...config2.routing, ...options.routing }, config2.allowedProviders);
2731
+ if (Object.keys(routing).length > 0)
2732
+ transformed.provider = routing;
2733
+ if (options.fallbackModels)
2734
+ transformed.models = [...options.fallbackModels];
2735
+ if (options.includeReasoning !== undefined)
2736
+ transformed.include_reasoning = options.includeReasoning;
2737
+ if (options.maxToolCalls !== undefined)
2738
+ transformed.max_tool_calls = options.maxToolCalls;
2739
+ if (options.plugins)
2740
+ transformed.plugins = [...options.plugins];
2741
+ if (options.preset)
2742
+ transformed.preset = options.preset;
2743
+ if (options.serverTools) {
2744
+ transformed.tools = [
2745
+ ...Array.isArray(transformed.tools) ? transformed.tools : [],
2746
+ ...options.serverTools
2747
+ ];
2748
+ }
2749
+ if (options.serviceTier)
2750
+ transformed.service_tier = options.serviceTier;
2751
+ if (options.sessionId)
2752
+ transformed.session_id = options.sessionId;
2753
+ if (options.stopServerToolsWhen)
2754
+ transformed.stop_server_tools_when = options.stopServerToolsWhen;
2755
+ if (options.transforms)
2756
+ transformed.transforms = [...options.transforms];
2757
+ if (options.user)
2758
+ transformed.user = options.user;
2759
+ if (options.verbosity)
2760
+ transformed.verbosity = options.verbosity;
2761
+ return transformed;
2762
+ };
2763
+ var withOpenRouterPolicy = (provider, allowedModels, allowedPresets) => ({
2764
+ stream: (params) => {
2765
+ if (params.model.startsWith("@preset/")) {
2766
+ assertAllowedPreset(params.model.slice("@preset/".length), allowedPresets);
2767
+ } else {
2768
+ const presetSeparator = params.model.indexOf("@preset/");
2769
+ if (presetSeparator >= 0) {
2770
+ assertAllowedModel2(params.model.slice(0, presetSeparator), allowedModels);
2771
+ assertAllowedPreset(params.model.slice(presetSeparator + "@preset/".length), allowedPresets);
2772
+ } else {
2773
+ assertAllowedModel2(params.model, allowedModels);
2774
+ }
2775
+ }
2776
+ return provider.stream(params);
2777
+ }
2778
+ });
2779
+ var openrouter = (config2) => {
2780
+ assertRoutingPolicy(config2);
2781
+ const { allowedModels, allowedPresets } = snapshotPolicy(config2);
2782
+ const provider = openai({
2783
+ apiKey: config2.apiKey,
2784
+ baseUrl: config2.baseUrl ?? DEFAULT_BASE_URL7,
2785
+ fetch: config2.fetch,
2786
+ headers: (params) => resolveAttributionHeaders(config2, params),
2787
+ modelForCapabilities: modelForOpenAICapabilities,
2788
+ providerName: "openrouter",
2789
+ tokenSource: config2.tokenSource,
2790
+ transformRequestBody: (body, params) => transformOpenRouterRequest(config2, allowedModels, allowedPresets, body, params)
2791
+ });
2792
+ return withOpenRouterPolicy(provider, allowedModels, allowedPresets);
2793
+ };
2794
+ var openrouterResponses = (config2) => {
2795
+ assertRoutingPolicy(config2);
2796
+ const { allowedModels, allowedPresets } = snapshotPolicy(config2);
2797
+ const provider = openaiResponses({
2798
+ apiKey: config2.apiKey,
2799
+ baseUrl: config2.baseUrl ?? DEFAULT_BASE_URL7,
2800
+ fetch: config2.fetch,
2801
+ headers: (params) => resolveAttributionHeaders(config2, params),
2802
+ modelForCapabilities: modelForOpenAICapabilities,
2803
+ providerName: "openrouter",
2804
+ tokenSource: config2.tokenSource,
2805
+ transformRequestBody: (body, params) => transformOpenRouterRequest(config2, allowedModels, allowedPresets, body, params)
2806
+ });
2807
+ return withOpenRouterPolicy(provider, allowedModels, allowedPresets);
2808
+ };
2809
+
2231
2810
  // src/plugins/aiChat.ts
2232
2811
  import { Elysia } from "elysia";
2233
2812
 
@@ -2382,6 +2961,7 @@ var isValidAIClientMessage = (data) => {
2382
2961
  case "cancel":
2383
2962
  return "conversationId" in data && typeof data.conversationId === "string";
2384
2963
  case "branch":
2964
+ case "edit":
2385
2965
  return "messageId" in data && typeof data.messageId === "string" && "content" in data && typeof data.content === "string" && "conversationId" in data && typeof data.conversationId === "string";
2386
2966
  default:
2387
2967
  return false;
@@ -2409,7 +2989,7 @@ var isValidAIServerMessage = (data) => {
2409
2989
  case "turn_started":
2410
2990
  return "conversationId" in data && typeof data.conversationId === "string" && "messageId" in data && typeof data.messageId === "string";
2411
2991
  case "branched":
2412
- return "content" in data && typeof data.content === "string" && "fromMessageId" in data && typeof data.fromMessageId === "string" && "messageId" in data && typeof data.messageId === "string" && "newConversationId" in data && typeof data.newConversationId === "string" && "oldConversationId" in data && typeof data.oldConversationId === "string";
2992
+ return "content" in data && typeof data.content === "string" && "fromMessageId" in data && typeof data.fromMessageId === "string" && "messageId" in data && typeof data.messageId === "string" && "newConversationId" in data && typeof data.newConversationId === "string" && "oldConversationId" in data && typeof data.oldConversationId === "string" && (!("mode" in data) || data.mode === "append" || data.mode === "replace");
2413
2993
  case "rag_retrieved":
2414
2994
  return "conversationId" in data && "messageId" in data && "sources" in data && Array.isArray(data.sources);
2415
2995
  case "error":
@@ -2643,6 +3223,7 @@ var processToolTurn = async (socket, options, state, messageId, conversationId,
2643
3223
  messages: state.currentMessages,
2644
3224
  model: options.model,
2645
3225
  promptCaching: options.promptCaching,
3226
+ providerOptions: options.providerOptions,
2646
3227
  reasoning: options.reasoning,
2647
3228
  signal,
2648
3229
  systemPrompt: options.systemPrompt,
@@ -2729,6 +3310,7 @@ var processStream = async (socket, options, messages, messageId, conversationId,
2729
3310
  messages,
2730
3311
  model: options.model,
2731
3312
  promptCaching: options.promptCaching,
3313
+ providerOptions: options.providerOptions,
2732
3314
  reasoning: options.reasoning,
2733
3315
  signal,
2734
3316
  systemPrompt: options.systemPrompt,
@@ -3110,6 +3692,7 @@ var streamTurns = async function* (options, renderers, messages, signal, startTi
3110
3692
  messages: turnState.currentMessages,
3111
3693
  model: options.model,
3112
3694
  promptCaching: options.promptCaching,
3695
+ providerOptions: options.providerOptions,
3113
3696
  reasoning: options.reasoning,
3114
3697
  signal,
3115
3698
  systemPrompt: options.systemPrompt,
@@ -3122,6 +3705,13 @@ var streamTurns = async function* (options, renderers, messages, signal, startTi
3122
3705
  aggregateUsage.outputTokens += chunkState.usage?.outputTokens ?? 0;
3123
3706
  aggregateUsage.cacheReadInputTokens = (aggregateUsage.cacheReadInputTokens ?? 0) + (chunkState.usage?.cacheReadInputTokens ?? 0);
3124
3707
  aggregateUsage.cacheWriteInputTokens = (aggregateUsage.cacheWriteInputTokens ?? 0) + (chunkState.usage?.cacheWriteInputTokens ?? 0);
3708
+ aggregateUsage.costCredits = (aggregateUsage.costCredits ?? 0) + (chunkState.usage?.costCredits ?? 0);
3709
+ aggregateUsage.reasoningTokens = (aggregateUsage.reasoningTokens ?? 0) + (chunkState.usage?.reasoningTokens ?? 0);
3710
+ for (const [key, value] of Object.entries(chunkState.usage?.serverToolUse ?? {})) {
3711
+ aggregateUsage.serverToolUse ??= {};
3712
+ aggregateUsage.serverToolUse[key] = (aggregateUsage.serverToolUse[key] ?? 0) + value;
3713
+ }
3714
+ aggregateUsage.upstreamInferenceCostCredits = (aggregateUsage.upstreamInferenceCostCredits ?? 0) + (chunkState.usage?.upstreamInferenceCostCredits ?? 0);
3125
3715
  const runningTotalTokens = aggregateUsage.inputTokens + aggregateUsage.outputTokens;
3126
3716
  if (chunkState.stopReason === "max_tokens") {
3127
3717
  finishReason = "max_tokens";
@@ -3260,13 +3850,16 @@ var getHistory = (conversation) => conversation.messages.map((msg) => ({
3260
3850
  content: msg.content,
3261
3851
  role: msg.role
3262
3852
  }));
3263
- var branchConversation = (source, fromMessageId) => {
3853
+ var branchConversation = (source, fromMessageId, mode) => {
3264
3854
  const cutoffIndex = source.messages.findIndex((msg) => msg.id === fromMessageId);
3265
3855
  if (cutoffIndex === NOT_FOUND2) {
3266
3856
  return null;
3267
3857
  }
3858
+ if (mode === "replace" && source.messages[cutoffIndex]?.role !== "user") {
3859
+ return null;
3860
+ }
3268
3861
  const newId = generateId();
3269
- const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: newId }));
3862
+ const branchedMessages = source.messages.slice(0, cutoffIndex + (mode === "replace" ? 0 : 1)).map((msg) => ({ ...msg, conversationId: newId }));
3270
3863
  const newConversation = {
3271
3864
  createdAt: Date.now(),
3272
3865
  id: newId,
@@ -3338,24 +3931,28 @@ var aiChat = (config2) => {
3338
3931
  abortControllers.delete(conversationId);
3339
3932
  }
3340
3933
  };
3341
- const handleBranch = async (ws, messageId, conversationId, content) => {
3934
+ const handleBranch = async (ws, messageId, conversationId, content, mode) => {
3342
3935
  const source = await store.get(conversationId);
3343
3936
  if (!source) {
3344
3937
  return;
3345
3938
  }
3346
- const newConv = branchConversation(source, messageId);
3939
+ const editedAttachments = mode === "replace" ? source.messages.find(({ id }) => id === messageId)?.attachments : undefined;
3940
+ const newConv = branchConversation(source, messageId, mode);
3347
3941
  if (newConv) {
3348
3942
  await store.set(newConv.id, newConv);
3349
3943
  const clientMessageId = generateId();
3350
3944
  sendServerEvent(ws, {
3945
+ attachments: editedAttachments,
3351
3946
  content,
3352
3947
  fromMessageId: messageId,
3353
3948
  messageId: clientMessageId,
3949
+ mode,
3354
3950
  newConversationId: newConv.id,
3355
3951
  oldConversationId: conversationId,
3356
3952
  type: "branched"
3357
3953
  });
3358
3954
  enqueueUserMessage({
3955
+ attachments: editedAttachments,
3359
3956
  clientMessageId,
3360
3957
  content,
3361
3958
  conversationId: newConv.id,
@@ -3555,7 +4152,11 @@ var aiChat = (config2) => {
3555
4152
  return;
3556
4153
  }
3557
4154
  if (msg.type === "branch") {
3558
- await handleBranch(ws, msg.messageId, msg.conversationId, msg.content);
4155
+ await handleBranch(ws, msg.messageId, msg.conversationId, msg.content, "append");
4156
+ return;
4157
+ }
4158
+ if (msg.type === "edit") {
4159
+ await handleBranch(ws, msg.messageId, msg.conversationId, msg.content, "replace");
3559
4160
  return;
3560
4161
  }
3561
4162
  if (msg.type === "message") {
@@ -3594,6 +4195,7 @@ var generateAI = async (options) => {
3594
4195
  messages: options.messages,
3595
4196
  model: options.model,
3596
4197
  promptCaching: options.promptCaching,
4198
+ providerOptions: options.providerOptions,
3597
4199
  reasoning: options.reasoning,
3598
4200
  responseFormat: options.responseFormat,
3599
4201
  signal: options.signal,
@@ -3606,17 +4208,22 @@ var generateAI = async (options) => {
3606
4208
  });
3607
4209
  let text = "";
3608
4210
  const toolCalls = [];
4211
+ const citations = [];
3609
4212
  let usage;
4213
+ let metadata;
3610
4214
  for await (const chunk of stream) {
3611
4215
  if (chunk.type === "text") {
3612
4216
  text += chunk.content;
3613
4217
  } else if (chunk.type === "tool_use") {
3614
4218
  toolCalls.push({ id: chunk.id, input: chunk.input, name: chunk.name });
4219
+ } else if (chunk.type === "citation") {
4220
+ citations.push(chunk);
3615
4221
  } else if (chunk.type === "done") {
3616
4222
  usage = chunk.usage;
4223
+ metadata = chunk.metadata;
3617
4224
  }
3618
4225
  }
3619
- return { text, toolCalls, usage };
4226
+ return { citations, metadata, text, toolCalls, usage };
3620
4227
  };
3621
4228
  var DEFAULT_TOOL_MAX_TURNS = 6;
3622
4229
  var mergeUsage = (left, right) => {
@@ -3625,11 +4232,18 @@ var mergeUsage = (left, right) => {
3625
4232
  if (!right)
3626
4233
  return left;
3627
4234
  const add = (a, b) => a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);
4235
+ const serverToolUse = { ...left.serverToolUse };
4236
+ for (const [key, value] of Object.entries(right.serverToolUse ?? {}))
4237
+ serverToolUse[key] = (serverToolUse[key] ?? 0) + value;
3628
4238
  return {
3629
4239
  cacheReadInputTokens: add(left.cacheReadInputTokens, right.cacheReadInputTokens),
3630
4240
  cacheWriteInputTokens: add(left.cacheWriteInputTokens, right.cacheWriteInputTokens),
4241
+ costCredits: add(left.costCredits, right.costCredits),
3631
4242
  inputTokens: (left.inputTokens ?? 0) + (right.inputTokens ?? 0),
3632
- outputTokens: (left.outputTokens ?? 0) + (right.outputTokens ?? 0)
4243
+ outputTokens: (left.outputTokens ?? 0) + (right.outputTokens ?? 0),
4244
+ reasoningTokens: add(left.reasoningTokens, right.reasoningTokens),
4245
+ serverToolUse: Object.keys(serverToolUse).length > 0 ? serverToolUse : undefined,
4246
+ upstreamInferenceCostCredits: add(left.upstreamInferenceCostCredits, right.upstreamInferenceCostCredits)
3633
4247
  };
3634
4248
  };
3635
4249
  var toProviderTools = (tools) => Object.entries(tools).map(([name, definition]) => ({
@@ -3813,6 +4427,7 @@ var streamAIWithTools = async function* (options) {
3813
4427
  messages,
3814
4428
  model: base.model,
3815
4429
  promptCaching: base.promptCaching,
4430
+ providerOptions: base.providerOptions,
3816
4431
  reasoning: base.reasoning,
3817
4432
  signal: base.signal,
3818
4433
  stopSequences: base.stopSequences,
@@ -5211,7 +5826,7 @@ var createConversationManager = () => {
5211
5826
  conversation.title = message.content.slice(0, TITLE_MAX_LENGTH2);
5212
5827
  }
5213
5828
  };
5214
- const branch = (fromMessageId, sourceConversationId) => {
5829
+ const fork = (fromMessageId, sourceConversationId, mode) => {
5215
5830
  const source = conversations.get(sourceConversationId);
5216
5831
  if (!source) {
5217
5832
  return null;
@@ -5220,8 +5835,11 @@ var createConversationManager = () => {
5220
5835
  if (cutoffIndex === NOT_FOUND3) {
5221
5836
  return null;
5222
5837
  }
5838
+ if (mode === "replace" && source.messages[cutoffIndex]?.role !== "user") {
5839
+ return null;
5840
+ }
5223
5841
  const newId = generateId();
5224
- const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: newId }));
5842
+ const branchedMessages = source.messages.slice(0, cutoffIndex + (mode === "replace" ? 0 : 1)).map((msg) => ({ ...msg, conversationId: newId }));
5225
5843
  const newConversation = {
5226
5844
  createdAt: Date.now(),
5227
5845
  id: newId,
@@ -5230,6 +5848,8 @@ var createConversationManager = () => {
5230
5848
  conversations.set(newId, newConversation);
5231
5849
  return newId;
5232
5850
  };
5851
+ const branch = (fromMessageId, sourceConversationId) => fork(fromMessageId, sourceConversationId, "append");
5852
+ const edit = (messageId, sourceConversationId) => fork(messageId, sourceConversationId, "replace");
5233
5853
  const emptyHistory = [];
5234
5854
  const getHistory2 = (conversationId) => {
5235
5855
  const conversation = conversations.get(conversationId);
@@ -5275,6 +5895,7 @@ var createConversationManager = () => {
5275
5895
  abort,
5276
5896
  appendMessage: appendMessage2,
5277
5897
  branch,
5898
+ edit,
5278
5899
  get,
5279
5900
  getAbortController,
5280
5901
  getHistory: getHistory2,
@@ -5413,9 +6034,11 @@ var serverMessageToAction = (message) => {
5413
6034
  };
5414
6035
  case "branched":
5415
6036
  return {
6037
+ attachments: message.attachments,
5416
6038
  content: message.content,
5417
6039
  fromMessageId: message.fromMessageId,
5418
6040
  messageId: message.messageId,
6041
+ mode: message.mode,
5419
6042
  newConversationId: message.newConversationId,
5420
6043
  oldConversationId: message.oldConversationId,
5421
6044
  type: "branch"
@@ -5819,13 +6442,15 @@ var handleBranch = (state, action) => {
5819
6442
  if (cutoffIndex < 0) {
5820
6443
  return;
5821
6444
  }
5822
- const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: action.newConversationId }));
6445
+ const cutoffOffset = action.mode === "replace" ? 0 : 1;
6446
+ const branchedMessages = source.messages.slice(0, cutoffIndex + cutoffOffset).map((msg) => ({ ...msg, conversationId: action.newConversationId }));
5823
6447
  const newConversation = {
5824
6448
  createdAt: Date.now(),
5825
6449
  id: action.newConversationId,
5826
6450
  messages: [
5827
6451
  ...branchedMessages,
5828
6452
  {
6453
+ attachments: action.attachments,
5829
6454
  content: action.content,
5830
6455
  conversationId: action.newConversationId,
5831
6456
  id: action.messageId,
@@ -5943,6 +6568,16 @@ var createAIStream = (path, conversationId) => {
5943
6568
  });
5944
6569
  }
5945
6570
  };
6571
+ const edit = (messageId, content) => {
6572
+ if (activeConversationId) {
6573
+ connection.send({
6574
+ content,
6575
+ conversationId: activeConversationId,
6576
+ messageId,
6577
+ type: "edit"
6578
+ });
6579
+ }
6580
+ };
5946
6581
  const cancel = () => {
5947
6582
  if (activeConversationId) {
5948
6583
  store.dispatch({ type: "cancel" });
@@ -5986,6 +6621,7 @@ var createAIStream = (path, conversationId) => {
5986
6621
  branch,
5987
6622
  cancel,
5988
6623
  destroy,
6624
+ edit,
5989
6625
  send,
5990
6626
  subscribe,
5991
6627
  get error() {
@@ -6190,9 +6826,12 @@ export {
6190
6826
  parseChoiceSpec,
6191
6827
  parseChartSpec,
6192
6828
  parseAIMessage,
6829
+ openrouterResponses,
6830
+ openrouter,
6193
6831
  openaiResponses,
6194
6832
  openaiCompatible,
6195
6833
  openai,
6834
+ openRouterModelMatchesRule,
6196
6835
  ollama,
6197
6836
  moonshot,
6198
6837
  mistralai,
@@ -6212,6 +6851,7 @@ export {
6212
6851
  createUiCards,
6213
6852
  createSyncConversationStore,
6214
6853
  createProviderProxyResponse,
6854
+ createOpenRouterClient,
6215
6855
  createOAuth2ClientCredentialsTokenSource,
6216
6856
  createMemoryStore,
6217
6857
  createConversationManager,
@@ -6249,5 +6889,5 @@ export {
6249
6889
  BUILTIN_UI_CARDS
6250
6890
  };
6251
6891
 
6252
- //# debugId=79EC11089E8CB24964756E2164756E21
6892
+ //# debugId=A06FECA819C53C2464756E2164756E21
6253
6893
  //# sourceMappingURL=index.js.map