@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/README.md +66 -1
- package/dist/index.cjs +235 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -28
- package/dist/index.d.ts +117 -28
- package/dist/index.js +235 -39
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
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-
|
|
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-
|
|
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-
|
|
1074
|
-
|
|
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
|
|
1975
|
-
|
|
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 {
|
|
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(
|
|
1980
|
-
|
|
1981
|
-
|
|
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.
|
|
1984
|
-
this.
|
|
1985
|
-
this.
|
|
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
|
-
*
|
|
2105
|
+
* Check if cached token is still valid
|
|
2106
|
+
*
|
|
2107
|
+
* @returns {boolean} True if token is valid
|
|
1989
2108
|
*/
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
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
|
|
2000
|
-
*
|
|
2114
|
+
* Get time until token expires (in seconds)
|
|
2115
|
+
*
|
|
2116
|
+
* @returns {number} Seconds until expiration (0 if no token or expired)
|
|
2001
2117
|
*/
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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:
|
|
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
|
|
2016
|
-
throw new Error(`Failed to get
|
|
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.
|
|
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.
|
|
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";
|