@contentgrowth/llm-service 1.1.1 → 1.2.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.
package/dist/index.js CHANGED
@@ -71,10 +71,12 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
71
71
  _buildTenantConfig(tenantConfig, env) {
72
72
  return {
73
73
  provider: tenantConfig.provider,
74
- apiKey: tenantConfig.api_key,
75
74
  models: MODEL_CONFIGS[tenantConfig.provider],
75
+ apiKey: tenantConfig.api_key,
76
+ project: tenantConfig.project,
77
+ location: tenantConfig.location,
76
78
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
77
- maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "16384"),
79
+ maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "65536"),
78
80
  capabilities: tenantConfig.capabilities || { chat: true, image: false, video: false },
79
81
  isTenantOwned: true
80
82
  };
@@ -105,13 +107,35 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
105
107
  image: env.GEMINI_IMAGE_MODEL || providerDefaults.image,
106
108
  video: env.GEMINI_VIDEO_MODEL || providerDefaults.video
107
109
  };
110
+ } else if (provider === "vertex") {
111
+ apiKey = env.VERTEX_API_KEY;
112
+ const project = env.VERTEX_PROJECT || env.GOOGLE_CLOUD_PROJECT;
113
+ const location = env.VERTEX_LOCATION || env.GOOGLE_CLOUD_LOCATION || "us-central1";
114
+ models = {
115
+ default: env.VERTEX_MODEL || providerDefaults.default,
116
+ edge: env.VERTEX_MODEL_EDGE || providerDefaults.edge,
117
+ fast: env.VERTEX_MODEL_FAST || providerDefaults.fast,
118
+ cost: env.VERTEX_MODEL_COST || providerDefaults.cost,
119
+ free: env.VERTEX_MODEL_FREE || providerDefaults.free,
120
+ image: env.VERTEX_IMAGE_MODEL || providerDefaults.image,
121
+ video: env.VERTEX_VIDEO_MODEL || providerDefaults.video
122
+ };
123
+ return {
124
+ provider,
125
+ apiKey,
126
+ project,
127
+ location,
128
+ models,
129
+ temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
130
+ maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "65536")
131
+ };
108
132
  }
109
133
  return {
110
134
  provider,
111
135
  apiKey,
112
136
  models,
113
137
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
114
- maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "16384")
138
+ maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "65536")
115
139
  };
116
140
  }
117
141
  };
@@ -139,6 +163,15 @@ var MODEL_CONFIGS = {
139
163
  video: "veo",
140
164
  image: "gemini-3-pro-image-preview"
141
165
  // Default image generation model
166
+ },
167
+ vertex: {
168
+ default: "gemini-3-flash-preview",
169
+ edge: "gemini-3-pro-preview",
170
+ fast: "gemini-3-flash-preview",
171
+ cost: "gemini-3-flash-preview",
172
+ free: "gemini-3-flash-preview",
173
+ video: "veo",
174
+ image: "gemini-3-pro-image-preview"
142
175
  }
143
176
  };
144
177
  var ConfigManager = class {
@@ -536,25 +569,43 @@ var OpenAIProvider = class extends BaseLLMProvider {
536
569
  }
537
570
  };
538
571
 
539
- // src/llm/providers/gemini-provider.js
572
+ // src/llm/providers/google-provider.js
540
573
  import { GoogleGenAI } from "@google/genai";
541
- var GeminiProvider = class extends BaseLLMProvider {
574
+ var GoogleProvider = class extends BaseLLMProvider {
542
575
  constructor(config) {
543
576
  super(config);
544
- const clientConfig = {};
545
- if (config.project || config.location) {
546
- console.log(`[GeminiProvider] Initializing with Vertex AI (Project: ${config.project}, Location: ${config.location || "us-central1"})`);
547
- clientConfig.vertexAI = {
548
- project: config.project,
549
- location: config.location || "us-central1"
550
- };
551
- } else {
552
- clientConfig.apiKey = config.apiKey;
553
- }
554
- this.client = new GoogleGenAI(clientConfig);
555
577
  this.models = config.models;
556
578
  this.defaultModel = config.models.default;
557
579
  this._pendingOperations = /* @__PURE__ */ new Map();
580
+ if (config.provider === "vertex") {
581
+ if (config.apiKey) {
582
+ this.client = new GoogleGenAI({
583
+ vertexai: true,
584
+ apiKey: config.apiKey
585
+ });
586
+ } else {
587
+ if (!config.project) {
588
+ console.warn("[GoogleProvider] Vertex AI: no project ID and no API key. Calls will likely fail.");
589
+ }
590
+ this.client = new GoogleGenAI({
591
+ vertexai: true,
592
+ project: config.project,
593
+ location: config.location || "us-central1"
594
+ });
595
+ }
596
+ } else {
597
+ this.client = new GoogleGenAI({
598
+ apiKey: config.apiKey
599
+ });
600
+ }
601
+ }
602
+ /**
603
+ * Perform the actual API call. Both AI Studio and Vertex AI use the
604
+ * same @google/genai SDK method — the routing is determined by how
605
+ * the client was constructed.
606
+ */
607
+ async _generateContent(requestOptions) {
608
+ return this.client.models.generateContent(requestOptions);
558
609
  }
559
610
  async chat(userMessage, systemPrompt = "", options = {}) {
560
611
  const messages = [{ role: "user", content: userMessage }];
@@ -694,18 +745,12 @@ ${msg.content}`;
694
745
  if (tools && tools.length > 0) {
695
746
  requestOptions.config.tools = [{ functionDeclarations: tools.map((t) => t.function) }];
696
747
  if (requestOptions.config.responseMimeType === "application/json") {
697
- console.warn("[GeminiProvider] Disabling strict JSON mode because tools are present. Relying on system prompt.");
748
+ console.warn(`[${this.constructor.name}] Disabling strict JSON mode because tools are present. Relying on system prompt.`);
698
749
  delete requestOptions.config.responseMimeType;
699
750
  delete requestOptions.config.responseSchema;
700
751
  }
701
752
  }
702
- let response;
703
- try {
704
- response = await this.client.models.generateContent(requestOptions);
705
- } catch (error) {
706
- console.error(`[GeminiProvider] generateContent failed (API Key: ${this._getMaskedApiKey()}):`, error);
707
- throw error;
708
- }
753
+ const response = await this._generateContent(requestOptions);
709
754
  const candidate = (_c = response.candidates) == null ? void 0 : _c[0];
710
755
  if (!candidate) {
711
756
  throw new LLMServiceException("No candidates returned from model", 500);
@@ -735,10 +780,8 @@ ${msg.content}`;
735
780
  }
736
781
  }
737
782
  if (!textContent && (!toolCalls || toolCalls.length === 0)) {
738
- console.error("[GeminiProvider] Model returned empty response (no text, no tool calls)");
739
- console.error("[GeminiProvider] Finish Reason:", candidate.finishReason);
740
- console.error("[GeminiProvider] Safety Ratings:", JSON.stringify(candidate.safetyRatings, null, 2));
741
- console.error("[GeminiProvider] Full Candidate:", JSON.stringify(candidate, null, 2));
783
+ console.error(`[${this.constructor.name}] Model returned empty response (no text, no tool calls)`);
784
+ console.error(`[${this.constructor.name}] Finish Reason:`, candidate.finishReason);
742
785
  throw new LLMServiceException(
743
786
  `Model returned empty response. Finish Reason: ${candidate.finishReason}.`,
744
787
  500
@@ -748,18 +791,14 @@ ${msg.content}`;
748
791
  return {
749
792
  content: textContent,
750
793
  thought_signature: responseThoughtSignature,
751
- // Return signature to caller
752
794
  tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map((fc) => ({
753
795
  type: "function",
754
796
  function: fc,
755
797
  thought_signature: fc.thought_signature
756
798
  })) : null,
757
799
  finishReason: normalizedFinishReason,
758
- // Standardized: 'completed', 'truncated', etc.
759
800
  _rawFinishReason: candidate.finishReason,
760
- // Keep original for debugging
761
801
  _responseFormat: options.responseFormat,
762
- // Return usage stats
763
802
  usage: {
764
803
  prompt_tokens: ((_e = response.usageMetadata) == null ? void 0 : _e.promptTokenCount) || 0,
765
804
  completion_tokens: ((_f = response.usageMetadata) == null ? void 0 : _f.candidatesTokenCount) || 0,
@@ -785,7 +824,7 @@ ${msg.content}`;
785
824
  if (schema) {
786
825
  config.responseSchema = this._convertToGeminiSchema(schema);
787
826
  } else {
788
- console.warn("[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers");
827
+ console.warn(`[${this.constructor.name}] Using legacy JSON mode without schema - may produce markdown wrappers`);
789
828
  }
790
829
  }
791
830
  }
@@ -843,8 +882,7 @@ ${msg.content}`;
843
882
  if (!content) return null;
844
883
  const parsed = extractJsonFromResponse(content);
845
884
  if (!parsed) {
846
- console.error("[GeminiProvider] Failed to extract valid JSON from response");
847
- console.error("[GeminiProvider] Content preview:", content.substring(0, 200));
885
+ console.error(`[${this.constructor.name}] Failed to extract valid JSON from response`);
848
886
  }
849
887
  return parsed;
850
888
  }
@@ -874,9 +912,9 @@ ${msg.content}`;
874
912
  toolResults.forEach((result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output }));
875
913
  }
876
914
  async imageGeneration(prompt, systemPrompt, options = {}) {
877
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
915
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
878
916
  const modelName = options.model || this.models.image || "gemini-3-pro-image-preview";
879
- console.log(`[GeminiProvider] Generating image with model: ${modelName}`);
917
+ console.log(`[${this.constructor.name}] Generating image with model: ${modelName}`);
880
918
  const hasReferenceImages = options.images && options.images.length > 0;
881
919
  const generationConfig = {
882
920
  responseModalities: hasReferenceImages ? ["TEXT", "IMAGE"] : ["IMAGE"]
@@ -908,7 +946,7 @@ ${msg.content}`;
908
946
  if (systemPrompt) {
909
947
  requestOptions.config.systemInstruction = { parts: [{ text: systemPrompt }] };
910
948
  }
911
- const response = await this.client.models.generateContent(requestOptions);
949
+ const response = await this._generateContent(requestOptions);
912
950
  const imagePart = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d.find(
913
951
  (part) => {
914
952
  var _a2;
@@ -916,30 +954,21 @@ ${msg.content}`;
916
954
  }
917
955
  );
918
956
  if (!imagePart || !imagePart.inlineData) {
919
- const textPart = (_h = (_g = (_f = (_e = response.candidates) == null ? void 0 : _e[0]) == null ? void 0 : _f.content) == null ? void 0 : _g.parts) == null ? void 0 : _h.find((p) => p.text);
920
- const candidate = (_i = response.candidates) == null ? void 0 : _i[0];
921
- console.error("[GeminiProvider] Image generation failed (no image data)");
922
- if (candidate) {
923
- console.error("[GeminiProvider] Finish Reason:", candidate.finishReason);
924
- console.error("[GeminiProvider] Safety Ratings:", JSON.stringify(candidate.safetyRatings, null, 2));
925
- console.error("[GeminiProvider] Full Candidate:", JSON.stringify(candidate, null, 2));
926
- }
927
- if (textPart) {
928
- console.warn("[GeminiProvider] Model returned text instead of image:", textPart.text);
929
- }
957
+ const candidate = (_e = response.candidates) == null ? void 0 : _e[0];
958
+ console.error(`[${this.constructor.name}] Image generation failed (no image data)`);
930
959
  throw new Error(`No image data in response. Finish Reason: ${candidate == null ? void 0 : candidate.finishReason}`);
931
960
  }
932
961
  let thoughtSignature = null;
933
962
  if (imagePart.thought_signature || imagePart.thoughtSignature) {
934
963
  thoughtSignature = imagePart.thought_signature || imagePart.thoughtSignature;
935
964
  } else {
936
- const signaturePart = (_m = (_l = (_k = (_j = response.candidates) == null ? void 0 : _j[0]) == null ? void 0 : _k.content) == null ? void 0 : _l.parts) == null ? void 0 : _m.find((p) => p.thought_signature || p.thoughtSignature);
965
+ const signaturePart = (_i = (_h = (_g = (_f = response.candidates) == null ? void 0 : _f[0]) == null ? void 0 : _g.content) == null ? void 0 : _h.parts) == null ? void 0 : _i.find((p) => p.thought_signature || p.thoughtSignature);
937
966
  if (signaturePart) {
938
967
  thoughtSignature = signaturePart.thought_signature || signaturePart.thoughtSignature;
939
968
  }
940
969
  }
941
970
  if (thoughtSignature && thoughtSignature.length > 5e4) {
942
- console.warn(`[GeminiProvider] \u26A0\uFE0F Thought signature is abnormally large (${thoughtSignature.length} chars). Replacing with bypass token to save context.`);
971
+ console.warn(`[${this.constructor.name}] \u26A0\uFE0F Thought signature is abnormally large (${thoughtSignature.length} chars). Replacing with bypass token.`);
943
972
  thoughtSignature = "skip_thought_signature_validator";
944
973
  }
945
974
  return {
@@ -962,31 +991,20 @@ ${prompt}` : prompt;
962
991
  durationSeconds: options.durationSeconds || 6,
963
992
  aspectRatio: options.aspectRatio || "16:9",
964
993
  numberOfVideos: 1,
965
- // Pass reference images if provided
966
994
  ...images && images.length > 0 ? { referenceImages: images } : {}
967
995
  }
968
996
  };
969
- const logConfig = JSON.parse(JSON.stringify(requestConfig));
970
- if (logConfig.config && logConfig.config.referenceImages) {
971
- logConfig.config.referenceImages = logConfig.config.referenceImages.map((img) => ({
972
- ...img,
973
- data: `... (${img.data ? img.data.length : 0} bytes)`
974
- // Summarize data
975
- }));
976
- }
977
- console.log("[GeminiProvider] startVideoGeneration request:", JSON.stringify(logConfig, null, 2));
978
997
  try {
979
998
  const operation = await this.client.models.generateVideos(requestConfig);
980
999
  this._pendingOperations.set(operation.name, operation);
981
1000
  return { operationName: operation.name };
982
1001
  } catch (error) {
983
- console.error(`[GeminiProvider] startVideoGeneration failed (API Key: ${this._getMaskedApiKey()}):`, error);
1002
+ console.error(`[${this.constructor.name}] startVideoGeneration failed (API Key: ${this._getMaskedApiKey()}):`, error);
984
1003
  throw error;
985
1004
  }
986
1005
  }
987
1006
  async getVideoGenerationStatus(operationName) {
988
1007
  var _a, _b, _c, _d, _e, _f;
989
- console.log(`[GeminiProvider] Checking status for operation: ${operationName}`);
990
1008
  let operation = this._pendingOperations.get(operationName);
991
1009
  if (!operation) {
992
1010
  operation = await this.client.models.getOperation(operationName);
@@ -998,11 +1016,9 @@ ${prompt}` : prompt;
998
1016
  progress: ((_a = operation.metadata) == null ? void 0 : _a.progressPercent) || 0,
999
1017
  state: ((_b = operation.metadata) == null ? void 0 : _b.state) || (operation.done ? "COMPLETED" : "PROCESSING")
1000
1018
  };
1001
- console.log(`[GeminiProvider] Operation status: ${result.state}, Progress: ${result.progress}%`);
1002
1019
  if (operation.done) {
1003
1020
  this._pendingOperations.delete(operationName);
1004
1021
  if (operation.error) {
1005
- console.error("[GeminiProvider] Video generation failed:", JSON.stringify(operation.error, null, 2));
1006
1022
  result.error = operation.error;
1007
1023
  } else {
1008
1024
  const videoResult = operation.response;
@@ -1014,20 +1030,17 @@ ${prompt}` : prompt;
1014
1030
  }
1015
1031
  async startDeepResearch(prompt, options = {}) {
1016
1032
  const agent = options.agent || "deep-research-pro-preview-12-2025";
1017
- console.log(`[GeminiProvider] Starting Deep Research with agent: ${agent}`);
1033
+ console.log(`[${this.constructor.name}] Starting Deep Research with agent: ${agent}`);
1018
1034
  try {
1019
1035
  const interaction = await this.client.interactions.create({
1020
1036
  agent,
1021
1037
  input: prompt,
1022
1038
  background: true,
1023
- // Required for long running
1024
1039
  store: true
1025
- // Required for polling
1026
1040
  });
1027
- console.log(`[GeminiProvider] Deep Research started. Interaction ID: ${interaction.id}`);
1028
1041
  return { operationId: interaction.id };
1029
1042
  } catch (error) {
1030
- console.error(`[GeminiProvider] startDeepResearch failed:`, error);
1043
+ console.error(`[${this.constructor.name}] startDeepResearch failed:`, error);
1031
1044
  throw error;
1032
1045
  }
1033
1046
  }
@@ -1046,18 +1059,10 @@ ${prompt}` : prompt;
1046
1059
  }
1047
1060
  return response;
1048
1061
  } catch (error) {
1049
- console.error(`[GeminiProvider] getDeepResearchStatus failed for ${operationId}:`, error);
1062
+ console.error(`[${this.constructor.name}] getDeepResearchStatus failed for ${operationId}:`, error);
1050
1063
  throw error;
1051
1064
  }
1052
1065
  }
1053
- /**
1054
- * Extract structured data from a file (PDF, Image, etc.) using Gemini Multimodal capabilities.
1055
- * @param {Buffer|string} fileData - Base64 string or Buffer of the file
1056
- * @param {string} mimeType - Mime type (e.g., 'application/pdf', 'image/png')
1057
- * @param {string} prompt - Extraction prompt
1058
- * @param {Object} schema - JSON schema for the output
1059
- * @param {Object} options - Additional options
1060
- */
1061
1066
  async extractWithLLM(fileData, mimeType, prompt, schema = null, options = {}) {
1062
1067
  var _a, _b, _c, _d;
1063
1068
  const tier = options.tier || "default";
@@ -1069,9 +1074,7 @@ ${prompt}` : prompt;
1069
1074
  maxTokens,
1070
1075
  temperature
1071
1076
  );
1072
- const parts = [
1073
- { text: prompt }
1074
- ];
1077
+ const parts = [{ text: prompt }];
1075
1078
  let base64Data = fileData;
1076
1079
  if (typeof fileData !== "string") {
1077
1080
  try {
@@ -1094,7 +1097,7 @@ ${prompt}` : prompt;
1094
1097
  config: generationConfig
1095
1098
  };
1096
1099
  try {
1097
- const response = await this.client.models.generateContent(requestOptions);
1100
+ const response = await this._generateContent(requestOptions);
1098
1101
  const candidate = (_a = response.candidates) == null ? void 0 : _a[0];
1099
1102
  if (!candidate) {
1100
1103
  throw new LLMServiceException("No candidates returned from model during extraction", 500);
@@ -1105,7 +1108,7 @@ ${prompt}` : prompt;
1105
1108
  }
1106
1109
  return textContent;
1107
1110
  } catch (error) {
1108
- console.error(`[GeminiProvider] extractWithLLM failed (API Key: ${this._getMaskedApiKey()}):`, error);
1111
+ console.error(`[${this.constructor.name}] extractWithLLM failed (API Key: ${this._getMaskedApiKey()}):`, error);
1109
1112
  throw error;
1110
1113
  }
1111
1114
  }
@@ -1124,14 +1127,14 @@ var LLMService = class {
1124
1127
  return this.providerCache.get(cacheKey);
1125
1128
  }
1126
1129
  const config = await ConfigManager.getConfig(tenantId, this.env);
1127
- if (!config.apiKey) {
1130
+ if (!config.apiKey && config.provider !== "vertex") {
1128
1131
  throw new LLMServiceException(`LLM service is not configured for ${config.provider}. Missing API Key.`, 500);
1129
1132
  }
1130
1133
  let provider;
1131
1134
  if (config.provider === "openai") {
1132
1135
  provider = new OpenAIProvider(config);
1133
- } else if (config.provider === "gemini") {
1134
- provider = new GeminiProvider(config);
1136
+ } else if (config.provider === "gemini" || config.provider === "vertex") {
1137
+ provider = new GoogleProvider(config);
1135
1138
  } else {
1136
1139
  throw new LLMServiceException(`Unsupported LLM provider: ${config.provider}`, 500);
1137
1140
  }
@@ -1703,13 +1706,15 @@ export {
1703
1706
  ConfigManager,
1704
1707
  DefaultConfigProvider,
1705
1708
  FINISH_REASONS,
1706
- GeminiProvider,
1709
+ GoogleProvider as GeminiProvider,
1710
+ GoogleProvider,
1707
1711
  LLMService,
1708
1712
  LLMServiceException,
1709
1713
  MODEL_CONFIGS,
1710
1714
  OpenAIProvider,
1711
1715
  TranscriptionService,
1712
1716
  TranscriptionServiceException,
1717
+ GoogleProvider as VertexProvider,
1713
1718
  createSpeechHandler,
1714
1719
  extractJsonFromResponse,
1715
1720
  extractTextAndJson,