@contentgrowth/llm-service 1.3.7 → 1.4.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
@@ -196,7 +196,7 @@ var MODEL_CONFIGS = {
196
196
  live: "gemini-3.8-live",
197
197
  liveThinking: "gemini-3.8-live-extended-thinking",
198
198
  video: "veo",
199
- image: "gemini-3-pro-image"
199
+ image: "gemini-nano-banana-2.1"
200
200
  },
201
201
  // Vertex AI / Gemini Enterprise Agent Platform
202
202
  vertex: {
@@ -208,7 +208,7 @@ var MODEL_CONFIGS = {
208
208
  live: "gemini-3.8-live",
209
209
  liveThinking: "gemini-3.8-live-extended-thinking",
210
210
  video: "veo",
211
- image: "gemini-3.1-flash-image"
211
+ image: "gemini-nano-banana-2.1"
212
212
  },
213
213
  // Anthropic IDs are "claude-<family>-<major>-<minor>", e.g. claude-sonnet-5-5 (not claude-5.5-sonnet).
214
214
  anthropic: {
@@ -223,11 +223,16 @@ var LEGACY_MODEL_ALIASES = {
223
223
  // OpenAI: ChatGPT product names, not API model IDs
224
224
  "gpt-5.5-instant": "gpt-5.6-luna",
225
225
  "gpt-5.5-thinking": "gpt-5.6-sol",
226
- // Gemini: never existed
226
+ // Gemini: never existed or were preview names
227
227
  "gemini-3.5-pro": "gemini-3.1-pro-preview",
228
228
  "gemini-3.7-pro": "gemini-3.1-pro-preview",
229
229
  "gemini-3.5-pro-image-preview": "gemini-3-pro-image",
230
230
  "gemini-3.7-pro-image-preview": "gemini-3-pro-image",
231
+ // Old Gemini image models (pre-Nano Banana branding)
232
+ "gemini-3-pro-image": "gemini-nano-banana-2.1",
233
+ "gemini-3.1-flash-image": "gemini-nano-banana-2.1",
234
+ "gemini-3.1-flash-lite-image": "gemini-nano-banana-2.1",
235
+ "gemini-2.5-flash-image": "gemini-nano-banana-2.1",
231
236
  // Anthropic: wrong segment order
232
237
  "claude-4.5-haiku": "claude-haiku-4-5",
233
238
  "claude-4.6-sonnet": "claude-sonnet-4-6",
@@ -688,17 +693,24 @@ var GoogleProvider = class extends BaseLLMProvider {
688
693
  this.models = normalizeModelMap(config.models);
689
694
  this.defaultModel = this.models.default;
690
695
  this._pendingOperations = /* @__PURE__ */ new Map();
696
+ if (config.accessToken) {
697
+ this.accessToken = config.accessToken;
698
+ this.project = config.project;
699
+ this.location = config.location || "global";
700
+ this.useDirectAPI = true;
701
+ console.log(`[GoogleProvider] Using pre-generated access token for project: ${this.project}`);
702
+ return;
703
+ }
691
704
  const httpOptions = config.baseURL ? { baseUrl: config.baseURL } : void 0;
692
705
  if (config.provider === "vertex") {
693
- const vertexLocation = config.location || "global";
694
706
  if (config.apiKey) {
695
707
  this.client = new GoogleGenAI({
696
708
  vertexai: true,
697
709
  apiKey: config.apiKey,
698
- location: vertexLocation,
699
710
  ...httpOptions ? { httpOptions } : {}
700
711
  });
701
712
  } else {
713
+ const vertexLocation = config.location || "global";
702
714
  if (!config.project) {
703
715
  console.warn("[GoogleProvider] Vertex AI: no project ID and no API key. Calls will likely fail.");
704
716
  }
@@ -722,8 +734,65 @@ var GoogleProvider = class extends BaseLLMProvider {
722
734
  * the client was constructed.
723
735
  */
724
736
  async _generateContent(requestOptions) {
737
+ if (this.useDirectAPI && this.accessToken) {
738
+ return this._directVertexAPICall(requestOptions);
739
+ }
725
740
  return this.client.models.generateContent(requestOptions);
726
741
  }
742
+ /**
743
+ * Direct Vertex AI API call using pre-generated access token
744
+ * @private
745
+ */
746
+ async _directVertexAPICall(requestOptions) {
747
+ var _a, _b, _c;
748
+ const modelPath = `projects/${this.project}/locations/${this.location}/publishers/google/models/${requestOptions.model}`;
749
+ const endpoint = `https://${this.location}-aiplatform.googleapis.com/v1/${modelPath}:generateContent`;
750
+ const body = {
751
+ contents: requestOptions.contents,
752
+ generationConfig: requestOptions.config
753
+ };
754
+ if ((_a = requestOptions.config) == null ? void 0 : _a.systemInstruction) {
755
+ body.systemInstruction = requestOptions.config.systemInstruction;
756
+ }
757
+ if ((_b = requestOptions.config) == null ? void 0 : _b.tools) {
758
+ body.tools = requestOptions.config.tools;
759
+ }
760
+ const response = await fetch(endpoint, {
761
+ method: "POST",
762
+ headers: {
763
+ "Authorization": `Bearer ${this.accessToken}`,
764
+ "Content-Type": "application/json"
765
+ },
766
+ body: JSON.stringify(body)
767
+ });
768
+ if (!response.ok) {
769
+ const errorText = await response.text();
770
+ let errorData;
771
+ try {
772
+ errorData = JSON.parse(errorText);
773
+ } catch (e) {
774
+ errorData = { message: errorText };
775
+ }
776
+ const errorMessage = ((_c = errorData.error) == null ? void 0 : _c.message) || errorData.message || `Vertex API error: ${response.status}`;
777
+ throw new LLMServiceException(errorMessage, response.status);
778
+ }
779
+ return response.json();
780
+ }
781
+ /**
782
+ * Determine authentication mode from config for logging/debugging
783
+ * @private
784
+ * @returns {string} Auth mode description
785
+ */
786
+ _getAuthMode() {
787
+ if (this.config.provider === "vertex") {
788
+ if (this.config.apiKey) {
789
+ return "Vertex Express (API Key)";
790
+ } else {
791
+ return "Vertex ADC (Service Account)";
792
+ }
793
+ }
794
+ return "Gemini AI Studio";
795
+ }
727
796
  async chat(userMessage, systemPrompt = "", options = {}) {
728
797
  const messages = [{ role: "user", content: userMessage }];
729
798
  const tier = options.tier || "default";
@@ -1070,15 +1139,22 @@ ${msg.content}`;
1070
1139
  }
1071
1140
  async imageGeneration(prompt, systemPrompt, options = {}) {
1072
1141
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1073
- const modelName = normalizeModelId(options.model) || this.models.image || "gemini-3-pro-image";
1074
- console.log(`[${this.constructor.name}] Generating image with model: ${modelName}`);
1142
+ const modelName = normalizeModelId(options.model) || this.models.image || "gemini-nano-banana-2.1";
1143
+ if (modelName.startsWith("imagen-") || modelName.startsWith("imagegeneration@")) {
1144
+ throw new LLMServiceException(
1145
+ `Imagen models were shut down August 17, 2026. Requested: "${modelName}". Use Gemini image models instead (e.g., "gemini-nano-banana-2.1").`,
1146
+ 400
1147
+ );
1148
+ }
1149
+ console.log(`[${this.constructor.name}] Generating image with model: ${modelName} (${this._getAuthMode()})`);
1075
1150
  const hasReferenceImages = options.images && options.images.length > 0;
1076
1151
  const generationConfig = {
1077
1152
  responseModalities: hasReferenceImages ? ["TEXT", "IMAGE"] : ["IMAGE"]
1078
1153
  };
1079
- if (options.aspectRatio) {
1154
+ if (options.aspectRatio || options.thinkingLevel) {
1080
1155
  generationConfig.imageConfig = {
1081
- aspectRatio: options.aspectRatio
1156
+ aspectRatio: options.aspectRatio || "1:1",
1157
+ ...options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}
1082
1158
  };
1083
1159
  }
1084
1160
  const parts = [{ text: prompt }];
@@ -1971,53 +2047,173 @@ function sanitizeError(error, context = "general") {
1971
2047
  import OpenAI2 from "openai";
1972
2048
 
1973
2049
  // src/utils/google-auth.js
1974
- import * as jose from "jose";
1975
- var GoogleAuth = class {
2050
+ import { Buffer as Buffer2 } from "buffer";
2051
+ function validateServiceAccount(sa) {
2052
+ if (!sa || typeof sa !== "object") {
2053
+ throw new Error("Service account must be an object");
2054
+ }
2055
+ if (!sa.client_email) {
2056
+ throw new Error("Service account missing client_email");
2057
+ }
2058
+ if (!sa.private_key) {
2059
+ throw new Error("Service account missing private_key");
2060
+ }
2061
+ if (!sa.project_id) {
2062
+ console.warn("[GoogleAuth] Service account missing project_id - may need to specify project explicitly");
2063
+ }
2064
+ return true;
2065
+ }
2066
+ var GoogleTokenManager = class {
1976
2067
  /**
1977
- * @param {Object} credentials - Parsed Service Account JSON
2068
+ * @param {object} serviceAccount - Parsed service account with client_email and private_key
2069
+ * @param {object} options - Optional configuration
2070
+ * @param {string} options.scope - OAuth2 scope (default: cloud-platform)
2071
+ * @param {number} options.tokenLifetime - Token lifetime in seconds (default: 3600)
1978
2072
  */
1979
- constructor(credentials) {
1980
- if (!credentials || !credentials.private_key || !credentials.client_email) {
1981
- throw new Error("Invalid Google credentials. Missing private_key or client_email.");
2073
+ constructor(serviceAccount, options = {}) {
2074
+ validateServiceAccount(serviceAccount);
2075
+ this.clientEmail = serviceAccount.client_email;
2076
+ this.privateKey = serviceAccount.private_key;
2077
+ this.projectId = serviceAccount.project_id;
2078
+ this.scope = options.scope || "https://www.googleapis.com/auth/cloud-platform";
2079
+ this.tokenLifetime = options.tokenLifetime || 3600;
2080
+ this._tokenCache = null;
2081
+ this._expiresAt = 0;
2082
+ }
2083
+ /**
2084
+ * Get a valid access token (cached or fresh)
2085
+ *
2086
+ * @returns {Promise<string>} OAuth2 access token
2087
+ */
2088
+ async getToken() {
2089
+ const now = Math.floor(Date.now() / 1e3);
2090
+ if (this._tokenCache && now < this._expiresAt - 300) {
2091
+ return this._tokenCache;
1982
2092
  }
1983
- this.credentials = credentials;
1984
- this.tokenUrl = "https://oauth2.googleapis.com/token";
1985
- this.scopes = ["https://www.googleapis.com/auth/cloud-platform"];
2093
+ this._tokenCache = await this._generateToken();
2094
+ this._expiresAt = now + this.tokenLifetime;
2095
+ return this._tokenCache;
2096
+ }
2097
+ /**
2098
+ * Clear cached token (force refresh on next getToken call)
2099
+ */
2100
+ invalidate() {
2101
+ this._tokenCache = null;
2102
+ this._expiresAt = 0;
1986
2103
  }
1987
2104
  /**
1988
- * Generate a signed JWT for the token exchange
2105
+ * Check if cached token is still valid
2106
+ *
2107
+ * @returns {boolean} True if token is valid
1989
2108
  */
1990
- async createSignedJWT() {
1991
- const alg = "RS256";
1992
- const privateKey = await jose.importPKCS8(this.credentials.private_key, alg);
1993
- const jwt = await new jose.SignJWT({
1994
- scope: this.scopes.join(" ")
1995
- }).setProtectedHeader({ alg }).setIssuedAt().setIssuer(this.credentials.client_email).setAudience(this.tokenUrl).setExpirationTime("1h").sign(privateKey);
1996
- return jwt;
2109
+ isValid() {
2110
+ const now = Math.floor(Date.now() / 1e3);
2111
+ return this._tokenCache && now < this._expiresAt - 300;
1997
2112
  }
1998
2113
  /**
1999
- * Get an access token from Google OAuth2
2000
- * @returns {Promise<string>} Access Token
2114
+ * Get time until token expires (in seconds)
2115
+ *
2116
+ * @returns {number} Seconds until expiration (0 if no token or expired)
2001
2117
  */
2002
- async getAccessToken() {
2003
- const jwt = await this.createSignedJWT();
2004
- const params = new URLSearchParams();
2005
- params.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
2006
- params.append("assertion", jwt);
2007
- const response = await fetch(this.tokenUrl, {
2118
+ getTimeToExpiry() {
2119
+ if (!this._tokenCache) return 0;
2120
+ const now = Math.floor(Date.now() / 1e3);
2121
+ return Math.max(0, this._expiresAt - now);
2122
+ }
2123
+ /**
2124
+ * Generate OAuth2 token via JWT flow
2125
+ *
2126
+ * @private
2127
+ * @returns {Promise<string>} Access token
2128
+ */
2129
+ async _generateToken() {
2130
+ const now = Math.floor(Date.now() / 1e3);
2131
+ const header = {
2132
+ alg: "RS256",
2133
+ typ: "JWT"
2134
+ };
2135
+ const payload = {
2136
+ iss: this.clientEmail,
2137
+ scope: this.scope,
2138
+ aud: "https://oauth2.googleapis.com/token",
2139
+ exp: now + this.tokenLifetime,
2140
+ iat: now
2141
+ };
2142
+ const jwt = await this._signJWT(header, payload);
2143
+ const response = await fetch("https://oauth2.googleapis.com/token", {
2008
2144
  method: "POST",
2009
2145
  headers: {
2010
2146
  "Content-Type": "application/x-www-form-urlencoded"
2011
2147
  },
2012
- body: params
2148
+ body: new URLSearchParams({
2149
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
2150
+ assertion: jwt
2151
+ })
2013
2152
  });
2014
2153
  if (!response.ok) {
2015
- const errorText = await response.text();
2016
- throw new Error(`Failed to get Google Access Token: ${response.status} ${errorText}`);
2154
+ const error = await response.text();
2155
+ throw new Error(`Failed to get OAuth2 token: ${response.status} - ${error}`);
2017
2156
  }
2018
2157
  const data = await response.json();
2158
+ if (!data.access_token) {
2159
+ throw new Error("No access_token in OAuth2 response");
2160
+ }
2019
2161
  return data.access_token;
2020
2162
  }
2163
+ /**
2164
+ * Sign JWT using service account private key
2165
+ *
2166
+ * @private
2167
+ * @param {object} header - JWT header
2168
+ * @param {object} payload - JWT payload
2169
+ * @returns {Promise<string>} Signed JWT
2170
+ */
2171
+ async _signJWT(header, payload) {
2172
+ const encoder = new TextEncoder();
2173
+ const headerB64 = this._base64UrlEncode(JSON.stringify(header));
2174
+ const payloadB64 = this._base64UrlEncode(JSON.stringify(payload));
2175
+ const unsignedToken = `${headerB64}.${payloadB64}`;
2176
+ const privateKey = await this._importPrivateKey(this.privateKey);
2177
+ const signature = await crypto.subtle.sign(
2178
+ { name: "RSASSA-PKCS1-v1_5" },
2179
+ privateKey,
2180
+ encoder.encode(unsignedToken)
2181
+ );
2182
+ const signatureB64 = this._base64UrlEncode(signature);
2183
+ return `${unsignedToken}.${signatureB64}`;
2184
+ }
2185
+ /**
2186
+ * Import PEM private key for signing
2187
+ *
2188
+ * @private
2189
+ * @param {string} pemKey - PEM formatted private key
2190
+ * @returns {Promise<CryptoKey>} Imported key
2191
+ */
2192
+ async _importPrivateKey(pemKey) {
2193
+ const pemContents = pemKey.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\\n/g, "").replace(/\n/g, "").trim();
2194
+ const binaryKey = Buffer2.from(pemContents, "base64");
2195
+ return crypto.subtle.importKey(
2196
+ "pkcs8",
2197
+ binaryKey,
2198
+ {
2199
+ name: "RSASSA-PKCS1-v1_5",
2200
+ hash: "SHA-256"
2201
+ },
2202
+ false,
2203
+ ["sign"]
2204
+ );
2205
+ }
2206
+ /**
2207
+ * Base64 URL encode (without padding)
2208
+ *
2209
+ * @private
2210
+ * @param {string|ArrayBuffer} input - Data to encode
2211
+ * @returns {string} Base64 URL encoded string
2212
+ */
2213
+ _base64UrlEncode(input) {
2214
+ const buffer = typeof input === "string" ? Buffer2.from(input, "utf-8") : Buffer2.from(input);
2215
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2216
+ }
2021
2217
  };
2022
2218
 
2023
2219
  // src/transcription-service.js
@@ -2037,7 +2233,7 @@ var TranscriptionService = class {
2037
2233
  if (this.provider === "openai") {
2038
2234
  this.client = new OpenAI2({ apiKey: this.apiKey });
2039
2235
  } else if (this.provider === "google") {
2040
- this.googleAuth = new GoogleAuth(config.googleCredentials);
2236
+ this.googleTokenManager = new GoogleTokenManager(config.googleCredentials);
2041
2237
  this.projectId = config.googleCredentials.project_id;
2042
2238
  }
2043
2239
  }
@@ -2118,7 +2314,7 @@ var TranscriptionService = class {
2118
2314
  }
2119
2315
  async _transcribeGoogle(audioFile, model, language) {
2120
2316
  try {
2121
- const accessToken = await this.googleAuth.getAccessToken();
2317
+ const accessToken = await this.googleTokenManager.getToken();
2122
2318
  const arrayBuffer = await audioFile.arrayBuffer();
2123
2319
  const audioBytes = Buffer.from(arrayBuffer).toString("base64");
2124
2320
  let location = "global";