@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/README.md +66 -1
- package/dist/index.cjs +234 -37
- 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 +234 -37
- 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,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-
|
|
1073
|
-
|
|
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
|
|
1974
|
-
|
|
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 {
|
|
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(
|
|
1979
|
-
|
|
1980
|
-
|
|
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.
|
|
1983
|
-
this.
|
|
1984
|
-
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;
|
|
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
|
-
*
|
|
2114
|
+
* Get time until token expires (in seconds)
|
|
2115
|
+
*
|
|
2116
|
+
* @returns {number} Seconds until expiration (0 if no token or expired)
|
|
1988
2117
|
*/
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
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
|
-
*
|
|
1999
|
-
*
|
|
2124
|
+
* Generate OAuth2 token via JWT flow
|
|
2125
|
+
*
|
|
2126
|
+
* @private
|
|
2127
|
+
* @returns {Promise<string>} Access token
|
|
2000
2128
|
*/
|
|
2001
|
-
async
|
|
2002
|
-
const
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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:
|
|
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
|
|
2015
|
-
throw new Error(`Failed to get
|
|
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.
|
|
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.
|
|
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";
|