@contentgrowth/llm-service 1.3.8 → 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,6 +693,14 @@ 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
706
  if (config.apiKey) {
@@ -721,8 +734,65 @@ var GoogleProvider = class extends BaseLLMProvider {
721
734
  * the client was constructed.
722
735
  */
723
736
  async _generateContent(requestOptions) {
737
+ if (this.useDirectAPI && this.accessToken) {
738
+ return this._directVertexAPICall(requestOptions);
739
+ }
724
740
  return this.client.models.generateContent(requestOptions);
725
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
+ }
726
796
  async chat(userMessage, systemPrompt = "", options = {}) {
727
797
  const messages = [{ role: "user", content: userMessage }];
728
798
  const tier = options.tier || "default";
@@ -1069,15 +1139,22 @@ ${msg.content}`;
1069
1139
  }
1070
1140
  async imageGeneration(prompt, systemPrompt, options = {}) {
1071
1141
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1072
- const modelName = normalizeModelId(options.model) || this.models.image || "gemini-3-pro-image";
1073
- 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()})`);
1074
1150
  const hasReferenceImages = options.images && options.images.length > 0;
1075
1151
  const generationConfig = {
1076
1152
  responseModalities: hasReferenceImages ? ["TEXT", "IMAGE"] : ["IMAGE"]
1077
1153
  };
1078
- if (options.aspectRatio) {
1154
+ if (options.aspectRatio || options.thinkingLevel) {
1079
1155
  generationConfig.imageConfig = {
1080
- aspectRatio: options.aspectRatio
1156
+ aspectRatio: options.aspectRatio || "1:1",
1157
+ ...options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}
1081
1158
  };
1082
1159
  }
1083
1160
  const parts = [{ text: prompt }];
@@ -1970,53 +2047,173 @@ function sanitizeError(error, context = "general") {
1970
2047
  import OpenAI2 from "openai";
1971
2048
 
1972
2049
  // src/utils/google-auth.js
1973
- import * as jose from "jose";
1974
- 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 {
1975
2067
  /**
1976
- * @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)
1977
2072
  */
1978
- constructor(credentials) {
1979
- if (!credentials || !credentials.private_key || !credentials.client_email) {
1980
- 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;
1981
2092
  }
1982
- this.credentials = credentials;
1983
- this.tokenUrl = "https://oauth2.googleapis.com/token";
1984
- 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;
2103
+ }
2104
+ /**
2105
+ * Check if cached token is still valid
2106
+ *
2107
+ * @returns {boolean} True if token is valid
2108
+ */
2109
+ isValid() {
2110
+ const now = Math.floor(Date.now() / 1e3);
2111
+ return this._tokenCache && now < this._expiresAt - 300;
1985
2112
  }
1986
2113
  /**
1987
- * Generate a signed JWT for the token exchange
2114
+ * Get time until token expires (in seconds)
2115
+ *
2116
+ * @returns {number} Seconds until expiration (0 if no token or expired)
1988
2117
  */
1989
- async createSignedJWT() {
1990
- const alg = "RS256";
1991
- const privateKey = await jose.importPKCS8(this.credentials.private_key, alg);
1992
- const jwt = await new jose.SignJWT({
1993
- scope: this.scopes.join(" ")
1994
- }).setProtectedHeader({ alg }).setIssuedAt().setIssuer(this.credentials.client_email).setAudience(this.tokenUrl).setExpirationTime("1h").sign(privateKey);
1995
- return jwt;
2118
+ getTimeToExpiry() {
2119
+ if (!this._tokenCache) return 0;
2120
+ const now = Math.floor(Date.now() / 1e3);
2121
+ return Math.max(0, this._expiresAt - now);
1996
2122
  }
1997
2123
  /**
1998
- * Get an access token from Google OAuth2
1999
- * @returns {Promise<string>} Access Token
2124
+ * Generate OAuth2 token via JWT flow
2125
+ *
2126
+ * @private
2127
+ * @returns {Promise<string>} Access token
2000
2128
  */
2001
- async getAccessToken() {
2002
- const jwt = await this.createSignedJWT();
2003
- const params = new URLSearchParams();
2004
- params.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
2005
- params.append("assertion", jwt);
2006
- const response = await fetch(this.tokenUrl, {
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", {
2007
2144
  method: "POST",
2008
2145
  headers: {
2009
2146
  "Content-Type": "application/x-www-form-urlencoded"
2010
2147
  },
2011
- body: params
2148
+ body: new URLSearchParams({
2149
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
2150
+ assertion: jwt
2151
+ })
2012
2152
  });
2013
2153
  if (!response.ok) {
2014
- const errorText = await response.text();
2015
- 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}`);
2016
2156
  }
2017
2157
  const data = await response.json();
2158
+ if (!data.access_token) {
2159
+ throw new Error("No access_token in OAuth2 response");
2160
+ }
2018
2161
  return data.access_token;
2019
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
+ }
2020
2217
  };
2021
2218
 
2022
2219
  // src/transcription-service.js
@@ -2036,7 +2233,7 @@ var TranscriptionService = class {
2036
2233
  if (this.provider === "openai") {
2037
2234
  this.client = new OpenAI2({ apiKey: this.apiKey });
2038
2235
  } else if (this.provider === "google") {
2039
- this.googleAuth = new GoogleAuth(config.googleCredentials);
2236
+ this.googleTokenManager = new GoogleTokenManager(config.googleCredentials);
2040
2237
  this.projectId = config.googleCredentials.project_id;
2041
2238
  }
2042
2239
  }
@@ -2117,7 +2314,7 @@ var TranscriptionService = class {
2117
2314
  }
2118
2315
  async _transcribeGoogle(audioFile, model, language) {
2119
2316
  try {
2120
- const accessToken = await this.googleAuth.getAccessToken();
2317
+ const accessToken = await this.googleTokenManager.getToken();
2121
2318
  const arrayBuffer = await audioFile.arrayBuffer();
2122
2319
  const audioBytes = Buffer.from(arrayBuffer).toString("base64");
2123
2320
  let location = "global";