@ainyc/canonry 1.26.2 → 1.27.1

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.
@@ -5051,6 +5051,14 @@ async function settingsRoutes(app, opts) {
5051
5051
  const err = validationError("baseUrl is required for local provider");
5052
5052
  return reply.status(err.statusCode).send(err.toJSON());
5053
5053
  }
5054
+ } else if (name === "gemini" && !apiKey) {
5055
+ const geminiSummary = (opts.providerSummary ?? []).find((p) => p.name === "gemini");
5056
+ if (!geminiSummary?.vertexConfigured) {
5057
+ const err = validationError(
5058
+ "apiKey is required for Gemini unless Vertex AI is configured (set GEMINI_VERTEX_PROJECT env var or vertexProject in config file)"
5059
+ );
5060
+ return reply.status(err.statusCode).send(err.toJSON());
5061
+ }
5054
5062
  } else {
5055
5063
  if (!apiKey || typeof apiKey !== "string") {
5056
5064
  const err = validationError("apiKey is required");
@@ -7631,18 +7639,58 @@ async function apiRoutes(app, opts) {
7631
7639
 
7632
7640
  // ../provider-gemini/src/normalize.ts
7633
7641
  import { GoogleGenerativeAI } from "@google/generative-ai";
7642
+ import { VertexAI } from "@google-cloud/vertexai";
7634
7643
  var DEFAULT_MODEL = "gemini-3-flash";
7635
7644
  var VALIDATION_PATTERN = /^gemini-/;
7645
+ function isVertexConfig(config) {
7646
+ return !!config.vertexProject;
7647
+ }
7636
7648
  function resolveModel(config) {
7637
7649
  const m = config.model;
7638
7650
  if (!m) return DEFAULT_MODEL;
7639
7651
  if (VALIDATION_PATTERN.test(m)) return m;
7652
+ const backend = isVertexConfig(config) ? "Vertex AI" : "AI Studio";
7640
7653
  console.warn(
7641
- `[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini AI Studio API (generativelanguage.googleapis.com) which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
7654
+ `[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini ${backend} API which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
7642
7655
  );
7643
7656
  return DEFAULT_MODEL;
7644
7657
  }
7658
+ function wrapVertexResponse(response) {
7659
+ return {
7660
+ text: () => {
7661
+ const parts = response.candidates?.[0]?.content?.parts;
7662
+ return parts?.map((p) => p.text ?? "").join("") ?? "";
7663
+ },
7664
+ candidates: response.candidates?.map((c) => ({
7665
+ content: c.content,
7666
+ finishReason: c.finishReason,
7667
+ groundingMetadata: c.groundingMetadata
7668
+ })),
7669
+ usageMetadata: response.usageMetadata
7670
+ };
7671
+ }
7672
+ function createVertexModel(config, model, tools) {
7673
+ const vertexOpts = {
7674
+ project: config.vertexProject,
7675
+ location: config.vertexRegion || "us-central1",
7676
+ googleAuthOptions: config.vertexCredentials ? { keyFilename: config.vertexCredentials } : void 0
7677
+ };
7678
+ const vertexAI = new VertexAI(vertexOpts);
7679
+ return vertexAI.getGenerativeModel({
7680
+ model,
7681
+ ...tools ? { tools } : {}
7682
+ });
7683
+ }
7645
7684
  function validateConfig(config) {
7685
+ if (isVertexConfig(config)) {
7686
+ const model2 = resolveModel(config);
7687
+ return {
7688
+ ok: true,
7689
+ provider: "gemini",
7690
+ message: "config valid (Vertex AI)",
7691
+ model: model2
7692
+ };
7693
+ }
7646
7694
  if (!config.apiKey || config.apiKey.length === 0) {
7647
7695
  return { ok: false, provider: "gemini", message: "missing api key" };
7648
7696
  }
@@ -7659,16 +7707,29 @@ async function healthcheck(config) {
7659
7707
  const validation = validateConfig(config);
7660
7708
  if (!validation.ok) return validation;
7661
7709
  try {
7662
- const genAI = new GoogleGenerativeAI(config.apiKey);
7663
- const model = genAI.getGenerativeModel({ model: resolveModel(config) });
7664
- const result = await model.generateContent('Say "ok"');
7665
- const text2 = result.response.text();
7666
- return {
7667
- ok: text2.length > 0,
7668
- provider: "gemini",
7669
- message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
7670
- model: config.model ?? DEFAULT_MODEL
7671
- };
7710
+ const model = resolveModel(config);
7711
+ if (isVertexConfig(config)) {
7712
+ const generativeModel = createVertexModel(config, model);
7713
+ const result = await generativeModel.generateContent('Say "ok"');
7714
+ const text2 = result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7715
+ return {
7716
+ ok: text2.length > 0,
7717
+ provider: "gemini",
7718
+ message: text2.length > 0 ? "gemini vertex ai verified" : "empty response from gemini vertex ai",
7719
+ model
7720
+ };
7721
+ } else {
7722
+ const genAI = new GoogleGenerativeAI(config.apiKey);
7723
+ const generativeModel = genAI.getGenerativeModel({ model });
7724
+ const result = await generativeModel.generateContent('Say "ok"');
7725
+ const text2 = result.response.text();
7726
+ return {
7727
+ ok: text2.length > 0,
7728
+ provider: "gemini",
7729
+ message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
7730
+ model
7731
+ };
7732
+ }
7672
7733
  } catch (err) {
7673
7734
  return {
7674
7735
  ok: false,
@@ -7680,23 +7741,41 @@ async function healthcheck(config) {
7680
7741
  }
7681
7742
  async function executeTrackedQuery(input) {
7682
7743
  const model = resolveModel(input.config);
7683
- const genAI = new GoogleGenerativeAI(input.config.apiKey);
7684
- const generativeModel = genAI.getGenerativeModel({
7685
- model,
7686
- tools: [{ googleSearch: {} }]
7687
- });
7688
7744
  const prompt = buildPrompt(input.keyword, input.location);
7689
- const result = await generativeModel.generateContent(prompt);
7690
- const response = result.response;
7691
- const groundingMetadata = extractGroundingMetadata(response);
7692
- const searchQueries = extractSearchQueries(response);
7693
- return {
7694
- provider: "gemini",
7695
- rawResponse: responseToRecord(response),
7696
- model,
7697
- groundingSources: groundingMetadata,
7698
- searchQueries
7699
- };
7745
+ if (isVertexConfig(input.config)) {
7746
+ const vertexSearchTool = { googleSearchRetrieval: {} };
7747
+ const generativeModel = createVertexModel(input.config, model, [vertexSearchTool]);
7748
+ const result = await generativeModel.generateContent(prompt);
7749
+ const response = result.response;
7750
+ const unified = wrapVertexResponse(response);
7751
+ const groundingMetadata = extractGroundingMetadataFromUnified(unified);
7752
+ const searchQueries = extractSearchQueriesFromUnified(unified);
7753
+ return {
7754
+ provider: "gemini",
7755
+ rawResponse: unifiedToRecord(unified),
7756
+ model,
7757
+ groundingSources: groundingMetadata,
7758
+ searchQueries
7759
+ };
7760
+ } else {
7761
+ const searchTool = { googleSearch: {} };
7762
+ const genAI = new GoogleGenerativeAI(input.config.apiKey);
7763
+ const generativeModel = genAI.getGenerativeModel({
7764
+ model,
7765
+ tools: [searchTool]
7766
+ });
7767
+ const result = await generativeModel.generateContent(prompt);
7768
+ const response = result.response;
7769
+ const groundingMetadata = extractGroundingMetadata(response);
7770
+ const searchQueries = extractSearchQueries(response);
7771
+ return {
7772
+ provider: "gemini",
7773
+ rawResponse: responseToRecord(response),
7774
+ model,
7775
+ groundingSources: groundingMetadata,
7776
+ searchQueries
7777
+ };
7778
+ }
7700
7779
  }
7701
7780
  function normalizeResult(raw) {
7702
7781
  const answerText = extractAnswerText(raw.rawResponse);
@@ -7742,6 +7821,22 @@ function extractGroundingMetadata(response) {
7742
7821
  return [];
7743
7822
  }
7744
7823
  }
7824
+ function extractGroundingMetadataFromUnified(response) {
7825
+ try {
7826
+ const candidate = response.candidates?.[0];
7827
+ if (!candidate) return [];
7828
+ const metadata = candidate.groundingMetadata;
7829
+ if (!metadata) return [];
7830
+ const chunks = metadata.groundingChunks;
7831
+ if (!chunks) return [];
7832
+ return chunks.filter((chunk) => chunk.web?.uri).map((chunk) => ({
7833
+ uri: chunk.web.uri,
7834
+ title: chunk.web?.title ?? ""
7835
+ }));
7836
+ } catch {
7837
+ return [];
7838
+ }
7839
+ }
7745
7840
  function extractSearchQueries(response) {
7746
7841
  try {
7747
7842
  const candidate = response.candidates?.[0];
@@ -7750,6 +7845,14 @@ function extractSearchQueries(response) {
7750
7845
  return [];
7751
7846
  }
7752
7847
  }
7848
+ function extractSearchQueriesFromUnified(response) {
7849
+ try {
7850
+ const candidate = response.candidates?.[0];
7851
+ return candidate?.groundingMetadata?.webSearchQueries ?? [];
7852
+ } catch {
7853
+ return [];
7854
+ }
7855
+ }
7753
7856
  function extractCitedDomains(raw) {
7754
7857
  const domains = /* @__PURE__ */ new Set();
7755
7858
  for (const source of raw.groundingSources) {
@@ -7797,10 +7900,16 @@ function extractDomainFromUri(uri) {
7797
7900
  }
7798
7901
  async function generateText(prompt, config) {
7799
7902
  const model = resolveModel(config);
7800
- const genAI = new GoogleGenerativeAI(config.apiKey);
7801
- const generativeModel = genAI.getGenerativeModel({ model });
7802
- const result = await generativeModel.generateContent(prompt);
7803
- return result.response.text();
7903
+ if (isVertexConfig(config)) {
7904
+ const generativeModel = createVertexModel(config, model);
7905
+ const result = await generativeModel.generateContent(prompt);
7906
+ return result.response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
7907
+ } else {
7908
+ const genAI = new GoogleGenerativeAI(config.apiKey);
7909
+ const generativeModel = genAI.getGenerativeModel({ model });
7910
+ const result = await generativeModel.generateContent(prompt);
7911
+ return result.response.text();
7912
+ }
7804
7913
  }
7805
7914
  function responseToRecord(response) {
7806
7915
  try {
@@ -7820,13 +7929,34 @@ function responseToRecord(response) {
7820
7929
  return { error: "failed to serialize response" };
7821
7930
  }
7822
7931
  }
7932
+ function unifiedToRecord(response) {
7933
+ try {
7934
+ const candidates = response.candidates?.map((c) => ({
7935
+ content: c.content,
7936
+ finishReason: c.finishReason,
7937
+ groundingMetadata: c.groundingMetadata ? {
7938
+ webSearchQueries: c.groundingMetadata.webSearchQueries,
7939
+ groundingChunks: c.groundingMetadata.groundingChunks
7940
+ } : void 0
7941
+ }));
7942
+ return {
7943
+ candidates: candidates ?? [],
7944
+ usageMetadata: response.usageMetadata ?? null
7945
+ };
7946
+ } catch {
7947
+ return { error: "failed to serialize response" };
7948
+ }
7949
+ }
7823
7950
 
7824
7951
  // ../provider-gemini/src/adapter.ts
7825
7952
  function toGeminiConfig(config) {
7826
7953
  return {
7827
7954
  apiKey: config.apiKey ?? "",
7828
7955
  model: config.model,
7829
- quotaPolicy: config.quotaPolicy
7956
+ quotaPolicy: config.quotaPolicy,
7957
+ vertexProject: config.vertexProject,
7958
+ vertexRegion: config.vertexRegion,
7959
+ vertexCredentials: config.vertexCredentials
7830
7960
  };
7831
7961
  }
7832
7962
  var geminiAdapter = {
@@ -10876,19 +11006,22 @@ async function createServer(opts) {
10876
11006
  }
10877
11007
  log6.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
10878
11008
  const p = providers[k];
10879
- return p?.apiKey || p?.baseUrl;
11009
+ return p?.apiKey || p?.baseUrl || p?.vertexProject;
10880
11010
  }) });
10881
11011
  for (const adapter of API_ADAPTERS) {
10882
11012
  const entry = providers[adapter.name];
10883
11013
  if (!entry) continue;
10884
- const isConfigured = adapter.name === "local" ? !!entry.baseUrl : !!entry.apiKey;
11014
+ const isConfigured = adapter.name === "local" ? !!entry.baseUrl : adapter.name === "gemini" ? !!(entry.apiKey || entry.vertexProject) : !!entry.apiKey;
10885
11015
  if (isConfigured) {
10886
11016
  registry.register(adapter, {
10887
11017
  provider: adapter.name,
10888
11018
  apiKey: entry.apiKey,
10889
11019
  baseUrl: entry.baseUrl,
10890
11020
  model: entry.model,
10891
- quotaPolicy: entry.quota ?? DEFAULT_QUOTA
11021
+ quotaPolicy: entry.quota ?? DEFAULT_QUOTA,
11022
+ vertexProject: entry.vertexProject,
11023
+ vertexRegion: entry.vertexRegion,
11024
+ vertexCredentials: entry.vertexCredentials
10892
11025
  });
10893
11026
  }
10894
11027
  }
@@ -10922,7 +11055,8 @@ async function createServer(opts) {
10922
11055
  modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
10923
11056
  model: registry.get(adapter.name)?.config.model,
10924
11057
  configured: !!registry.get(adapter.name),
10925
- quota: registry.get(adapter.name)?.config.quotaPolicy
11058
+ quota: registry.get(adapter.name)?.config.quotaPolicy,
11059
+ vertexConfigured: adapter.name === "gemini" ? !!opts.config.providers?.gemini?.vertexProject : void 0
10926
11060
  }));
10927
11061
  const googleSettingsSummary = {
10928
11062
  configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
@@ -11216,7 +11350,12 @@ async function createServer(opts) {
11216
11350
  apiKey: apiKey || existing?.apiKey,
11217
11351
  baseUrl: baseUrl || existing?.baseUrl,
11218
11352
  model: model || existing?.model,
11219
- quota: mergedQuota
11353
+ quota: mergedQuota,
11354
+ // Preserve Vertex AI config (Gemini provider) — these are set via
11355
+ // config file or env vars, not through the dashboard update payload
11356
+ vertexProject: existing?.vertexProject,
11357
+ vertexRegion: existing?.vertexRegion,
11358
+ vertexCredentials: existing?.vertexCredentials
11220
11359
  };
11221
11360
  try {
11222
11361
  saveConfig(opts.config);
@@ -11230,13 +11369,19 @@ async function createServer(opts) {
11230
11369
  apiKey: apiKey || existing?.apiKey,
11231
11370
  baseUrl: baseUrl || existing?.baseUrl,
11232
11371
  model: model || existing?.model,
11233
- quotaPolicy: quota
11372
+ quotaPolicy: quota,
11373
+ vertexProject: existing?.vertexProject,
11374
+ vertexRegion: existing?.vertexRegion,
11375
+ vertexCredentials: existing?.vertexCredentials
11234
11376
  });
11235
11377
  const entry = providerSummary.find((p) => p.name === name);
11236
11378
  if (entry) {
11237
11379
  entry.configured = true;
11238
11380
  entry.model = model || registry.get(name)?.config.model;
11239
11381
  entry.quota = quota;
11382
+ if (name === "gemini") {
11383
+ entry.vertexConfigured = !!opts.config.providers?.[name]?.vertexProject;
11384
+ }
11240
11385
  }
11241
11386
  const afterConfig = summarizeProviderConfig(name, opts.config.providers[name]);
11242
11387
  if (JSON.stringify(beforeConfig) !== JSON.stringify(afterConfig)) {
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  setGoogleAuthConfig,
20
20
  showFirstRunNotice,
21
21
  trackEvent
22
- } from "./chunk-NVCAUQ33.js";
22
+ } from "./chunk-UTRZ3UB5.js";
23
23
 
24
24
  // src/cli.ts
25
25
  import { pathToFileURL } from "url";
@@ -4174,6 +4174,10 @@ var envSchema = z.object({
4174
4174
  GEMINI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
4175
4175
  GEMINI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
4176
4176
  GEMINI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
4177
+ // Gemini Vertex AI (alternative to API key auth)
4178
+ GEMINI_VERTEX_PROJECT: z.string().optional(),
4179
+ GEMINI_VERTEX_REGION: z.string().optional(),
4180
+ GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
4177
4181
  // OpenAI
4178
4182
  OPENAI_API_KEY: z.string().optional(),
4179
4183
  OPENAI_MODEL: z.string().optional(),
@@ -4199,6 +4203,9 @@ var bootstrapEnvSchema = z.object({
4199
4203
  CANONRY_DATABASE_PATH: z.string().optional(),
4200
4204
  GEMINI_API_KEY: z.string().optional(),
4201
4205
  GEMINI_MODEL: z.string().optional(),
4206
+ GEMINI_VERTEX_PROJECT: z.string().optional(),
4207
+ GEMINI_VERTEX_REGION: z.string().optional(),
4208
+ GEMINI_VERTEX_CREDENTIALS: z.string().optional(),
4202
4209
  OPENAI_API_KEY: z.string().optional(),
4203
4210
  OPENAI_MODEL: z.string().optional(),
4204
4211
  ANTHROPIC_API_KEY: z.string().optional(),
@@ -4215,15 +4222,18 @@ function getBootstrapEnv(source, overrides) {
4215
4222
  const filtered = overrides ? Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)) : {};
4216
4223
  const parsed = bootstrapEnvSchema.parse({ ...source, ...filtered });
4217
4224
  const providers = {};
4218
- if (parsed.GEMINI_API_KEY) {
4225
+ if (parsed.GEMINI_API_KEY || parsed.GEMINI_VERTEX_PROJECT) {
4219
4226
  providers.gemini = {
4220
- apiKey: parsed.GEMINI_API_KEY,
4227
+ apiKey: parsed.GEMINI_API_KEY ?? "",
4221
4228
  model: parsed.GEMINI_MODEL || "gemini-3-flash",
4222
4229
  quota: providerQuotaPolicySchema.parse({
4223
4230
  maxConcurrency: 2,
4224
4231
  maxRequestsPerMinute: 10,
4225
4232
  maxRequestsPerDay: 500
4226
- })
4233
+ }),
4234
+ vertexProject: parsed.GEMINI_VERTEX_PROJECT,
4235
+ vertexRegion: parsed.GEMINI_VERTEX_REGION,
4236
+ vertexCredentials: parsed.GEMINI_VERTEX_CREDENTIALS
4227
4237
  };
4228
4238
  }
4229
4239
  if (parsed.OPENAI_API_KEY) {
package/dist/index.d.ts CHANGED
@@ -8,6 +8,12 @@ interface ProviderConfigEntry {
8
8
  baseUrl?: string;
9
9
  model?: string;
10
10
  quota?: ProviderQuotaPolicy;
11
+ /** Vertex AI GCP project ID (Gemini provider only) */
12
+ vertexProject?: string;
13
+ /** Vertex AI region, e.g. "us-central1" (Gemini provider only) */
14
+ vertexRegion?: string;
15
+ /** Path to service account JSON for Vertex AI auth (falls back to ADC) */
16
+ vertexCredentials?: string;
11
17
  }
12
18
  interface CdpConfigEntry {
13
19
  host?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-NVCAUQ33.js";
4
+ } from "./chunk-UTRZ3UB5.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.26.2",
3
+ "version": "1.27.1",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "@anthropic-ai/sdk": "^0.78.0",
38
38
  "@fastify/static": "^8.1.0",
39
+ "@google-cloud/vertexai": "^1.10.3",
39
40
  "@google/generative-ai": "^0.24.1",
40
41
  "better-sqlite3": "^12.6.2",
41
42
  "drizzle-orm": "^0.45.1",
@@ -52,12 +53,12 @@
52
53
  "@types/node-cron": "^3.0.11",
53
54
  "tsup": "^8.5.1",
54
55
  "tsx": "^4.19.0",
55
- "@ainyc/canonry-contracts": "0.0.0",
56
- "@ainyc/canonry-api-routes": "0.0.0",
57
- "@ainyc/canonry-db": "0.0.0",
58
56
  "@ainyc/canonry-config": "0.0.0",
57
+ "@ainyc/canonry-contracts": "0.0.0",
59
58
  "@ainyc/canonry-provider-claude": "0.0.0",
59
+ "@ainyc/canonry-api-routes": "0.0.0",
60
60
  "@ainyc/canonry-provider-gemini": "0.0.0",
61
+ "@ainyc/canonry-db": "0.0.0",
61
62
  "@ainyc/canonry-provider-cdp": "0.0.0",
62
63
  "@ainyc/canonry-provider-local": "0.0.0",
63
64
  "@ainyc/canonry-integration-bing": "0.0.0",