@zenera/neo 1.1.28 → 1.1.29

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/kernel.js CHANGED
@@ -770,6 +770,21 @@ export async function createChildState(state, forkId, branch, env) {
770
770
  };
771
771
  const b = begin(child, env);
772
772
  if (prefix.length) {
773
+ // Gemini signs only a parallel turn's first call: answer the siblings so none is dropped.
774
+ const turn = prefix.findLast((n) => n.type === 'llm_call' && n.toolCalls.some((c) => c.callId === fork.callId));
775
+ const answered = new Set(prefix.flatMap((n) => (n.type === 'tool_result' ? [n.callId] : [])));
776
+ for (const c of turn?.toolCalls ?? []) {
777
+ if (c.callId === fork.callId || answered.has(c.callId)) {
778
+ continue;
779
+ }
780
+ b.add({
781
+ type: 'tool_result',
782
+ callId: c.callId,
783
+ name: c.name,
784
+ result: await put(env, 'Not run in this branch: the parent runs this call, and its result is not visible here.'),
785
+ isError: false,
786
+ });
787
+ }
773
788
  // The branch inherited the turn that called `fork`, so the honest way to
774
789
  // hand it its assignment is to answer that call: the model sees a tool
775
790
  // result for a tool it just called, instead of a second user message
@@ -147,6 +147,8 @@ export interface ModelRequirement {
147
147
  satisfied: boolean;
148
148
  /** the SDK authenticates without a key, so `satisfied` says little */
149
149
  keyOptional: boolean;
150
+ /** set when the request goes through a gateway, whose key `apiKeyEnv` then names */
151
+ gateway?: string;
150
152
  }
151
153
  export declare const providerKinds: ProviderKind[];
152
154
  export declare function isProviderKind(name: string): name is ProviderKind;
@@ -6,6 +6,7 @@ import { OpenAIEmbedder } from "../embeddings/openai.js";
6
6
  import { OpenRouterEmbedder } from "../embeddings/openrouter.js";
7
7
  import { unfundedSymbol } from "../embeddings/rate-limit.js";
8
8
  import { AnthropicModel } from "./anthropic.js";
9
+ import { GATEWAY_KEY_ENV, GATEWAY_KEY_HEADER, GATEWAY_NAME, GATEWAY_PATHS, GATEWAY_UPSTREAMS, gatewayOf, upstreamFetch, } from "./gateway.js";
9
10
  import { GeminiModel } from "./gemini.js";
10
11
  import { OpenAIModel } from "./openai-chat.js";
11
12
  import { OpenAIResponsesModel } from "./openai-responses.js";
@@ -128,6 +129,12 @@ function protocolOf(kind, model) {
128
129
  function apisOf(kind, protocol) {
129
130
  return kind === 'vertex' && protocol === 'openai' ? ['chat'] : KINDS[kind].apis;
130
131
  }
132
+ /** Bifrost has no OpenRouter drop-in, so OpenRouter models go through its OpenAI one. */
133
+ function gatewayProtocol(kind, model) {
134
+ const protocol = protocolOf(kind, model);
135
+ return protocol === 'openrouter' ? 'openai' : protocol;
136
+ }
137
+ const GATEWAY_APIS = ['chat', 'responses'];
131
138
  // ---------------------------------------------------------------------------
132
139
  // Tuning knobs
133
140
  // ---------------------------------------------------------------------------
@@ -265,6 +272,31 @@ export class ModelRegistry {
265
272
  this.#clients.set(name, built);
266
273
  return built;
267
274
  }
275
+ /**
276
+ * Gateway mode takes every provider, declared ones included; only a
277
+ * pre-built client stays put, since there is nothing in it to repoint.
278
+ */
279
+ #route(spec, name) {
280
+ const kind = this.kindOf(name);
281
+ const gateway = spec.client || this.#spec(name).client ? undefined : gatewayOf();
282
+ if (gateway) {
283
+ const protocol = gatewayProtocol(kind, spec.model);
284
+ return { protocol, apis: protocol === 'openai' ? GATEWAY_APIS : [], gateway };
285
+ }
286
+ const protocol = protocolOf(kind, spec.model);
287
+ return { protocol, apis: apisOf(kind, protocol) };
288
+ }
289
+ /** One client per provider and protocol, keyed by the gateway it points at. */
290
+ #gateway(name, protocol, at) {
291
+ const key = `${name}\0gateway\0${protocol}\0${at.baseURL}\0${at.apiKey ?? ''}`;
292
+ const existing = this.#clients.get(key);
293
+ if (existing) {
294
+ return existing;
295
+ }
296
+ const built = buildGatewayClient(name, this.kindOf(name), protocol, this.#spec(name), at);
297
+ this.#clients.set(key, built);
298
+ return built;
299
+ }
268
300
  /** A Vertex provider's Claude or partner client: built once, beside its GenAI one. */
269
301
  #partner(name, protocol, spec) {
270
302
  const key = `${name}\0${protocol}`;
@@ -304,14 +336,12 @@ export class ModelRegistry {
304
336
  */
305
337
  knobs(ref) {
306
338
  const spec = typeof ref === 'string' ? this.parse(ref) : ref;
307
- const kind = this.kindOf(spec.provider ?? this.#default);
308
- const protocol = protocolOf(kind, spec.model);
339
+ const { protocol, apis } = this.#route(spec, spec.provider ?? this.#default);
309
340
  if (protocol !== 'openai') {
310
341
  return unreadKnobs(spec, protocol);
311
342
  }
312
343
  // An api this vendor does not speak already has an error of its own;
313
344
  // answering about its knobs would only bury it.
314
- const apis = apisOf(kind, protocol);
315
345
  const api = spec.api ?? apis[0];
316
346
  return api && apis.includes(api) ? unreadKnobs(spec, `openai/${api}`) : [];
317
347
  }
@@ -321,7 +351,7 @@ export class ModelRegistry {
321
351
  const name = spec.provider ?? this.#default;
322
352
  const provider = this.#spec(name);
323
353
  const kind = this.kindOf(name);
324
- const protocol = protocolOf(kind, spec.model);
354
+ const { protocol, apis, gateway } = this.#route(spec, name);
325
355
  // Before the client, so a knob for the wrong vendor is reported as the
326
356
  // config mistake it is rather than behind a missing credential.
327
357
  const [stray] = this.knobs(spec);
@@ -333,15 +363,17 @@ export class ModelRegistry {
333
363
  // different connection, and handing back the memoized one would
334
364
  // silently ignore them.
335
365
  const client = spec.client ??
336
- (protocol !== KINDS[kind].protocol
337
- ? this.#partner(name, protocol, provider)
338
- : hasCredentials(spec)
339
- ? buildClient(name, kind, spec, provider)
340
- : this.client(name));
366
+ (gateway
367
+ ? this.#gateway(name, protocol, gateway)
368
+ : protocol !== KINDS[kind].protocol
369
+ ? this.#partner(name, protocol, provider)
370
+ : hasCredentials(spec)
371
+ ? buildClient(name, kind, spec, provider)
372
+ : this.client(name));
341
373
  // The *resolved* name, not `spec.provider`: a bare ref carries none,
342
374
  // and a failure that cannot say which connection refused it is the
343
375
  // thing this is here to prevent.
344
- const options = { ...spec, provider: name };
376
+ const options = { ...spec, provider: gateway ? `${name} via ${GATEWAY_NAME}` : name };
345
377
  // Every protocol but OpenAI's has exactly one API, so naming one is a
346
378
  // mistake worth reporting rather than a field to ignore.
347
379
  if (protocol !== 'openai') {
@@ -357,7 +389,6 @@ export class ModelRegistry {
357
389
  return new OpenRouterModel(spec.model, client, options);
358
390
  }
359
391
  }
360
- const apis = apisOf(kind, protocol);
361
392
  const api = spec.api ?? apis[0];
362
393
  if (!apis.includes(api)) {
363
394
  throw new TypeError(`provider "${name}" (${kind}) does not speak the "${api}" api ` +
@@ -377,16 +408,22 @@ export class ModelRegistry {
377
408
  const name = spec.provider ?? this.#default;
378
409
  const provider = this.#spec(name);
379
410
  const kind = this.kindOf(name);
411
+ const { protocol, gateway } = this.#route(spec, name);
380
412
  const client = spec.client ??
381
- (hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
413
+ (gateway
414
+ ? this.#gateway(name, protocol, gateway)
415
+ : hasCredentials(spec)
416
+ ? buildClient(name, kind, spec, provider)
417
+ : this.client(name));
382
418
  // Inline credentials are a different connection, and so a different
383
419
  // budget: they get pacing of their own rather than the provider's.
420
+ const own = hasCredentials(spec) && !gateway;
384
421
  const options = {
385
422
  ...spec,
386
- provider: name,
387
- limiter: spec.limiter ?? (hasCredentials(spec) ? undefined : this.#limiter(name)),
423
+ provider: gateway ? `${name} via ${GATEWAY_NAME}` : name,
424
+ limiter: spec.limiter ?? (own ? undefined : this.#limiter(name)),
388
425
  };
389
- switch (KINDS[kind].protocol) {
426
+ switch (gateway ? protocol : KINDS[kind].protocol) {
390
427
  case 'openai':
391
428
  return new OpenAIEmbedder(spec.model, client, options);
392
429
  case 'gemini':
@@ -410,6 +447,17 @@ export class ModelRegistry {
410
447
  const name = spec.provider ?? this.#default;
411
448
  const provider = this.#spec(name);
412
449
  const kind = this.kindOf(name);
450
+ const gateway = this.#route(spec, name).gateway;
451
+ if (gateway) {
452
+ return {
453
+ provider: name,
454
+ kind,
455
+ apiKeyEnv: GATEWAY_KEY_ENV,
456
+ satisfied: Boolean(gateway.apiKey),
457
+ keyOptional: false,
458
+ gateway: GATEWAY_NAME,
459
+ };
460
+ }
413
461
  const defaults = KINDS[kind];
414
462
  const creds = hasCredentials(spec) ? spec : provider;
415
463
  const apiKeyEnv = creds.apiKeyEnv ?? defaults.apiKeyEnv;
@@ -633,15 +681,12 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
633
681
  */
634
682
  function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
635
683
  const { GoogleGenAI } = sdk('@google/genai', kind);
636
- // `attempts` counts the initial call, so a retry budget of zero is one
637
- // attempt. Delays are fractions of a second here, unlike everywhere else.
638
- const retryOptions = {
639
- attempts: retriesOf(opts) + 1,
640
- initialDelay: RETRY_INITIAL_MS / 1000,
641
- maxDelay: RETRY_MAX_MS / 1000,
642
- expBase: RETRY_EXPONENT,
684
+ const httpOptions = {
685
+ baseUrl: baseURL,
686
+ headers,
687
+ timeout: opts.timeoutMs,
688
+ retryOptions: genaiRetries(opts),
643
689
  };
644
- const httpOptions = { baseUrl: baseURL, headers, timeout: opts.timeoutMs, retryOptions };
645
690
  if (kind !== 'vertex') {
646
691
  return new GoogleGenAI({ apiKey, httpOptions });
647
692
  }
@@ -664,6 +709,60 @@ function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
664
709
  httpOptions,
665
710
  });
666
711
  }
712
+ /**
713
+ * `attempts` counts the initial call, so a retry budget of zero is one attempt.
714
+ * Delays are fractions of a second here, unlike everywhere else.
715
+ */
716
+ function genaiRetries(opts) {
717
+ return {
718
+ attempts: retriesOf(opts) + 1,
719
+ initialDelay: RETRY_INITIAL_MS / 1000,
720
+ maxDelay: RETRY_MAX_MS / 1000,
721
+ expBase: RETRY_EXPONENT,
722
+ };
723
+ }
724
+ /**
725
+ * A client aimed at the gateway's drop-in endpoint for `protocol`, with the
726
+ * virtual key in place of the vendor's. The provider keeps its transport
727
+ * settings and headers; its own key and base url are what the gateway replaces.
728
+ */
729
+ function buildGatewayClient(name, kind, protocol, opts, at) {
730
+ const baseURL = `${at.baseURL}${GATEWAY_PATHS[protocol]}`;
731
+ // Every SDK refuses to construct without a key, even for a gateway that wants none.
732
+ const apiKey = at.apiKey ?? GATEWAY_NAME;
733
+ const headers = {
734
+ ...expandHeaders(opts.headers, `provider "${name}"`),
735
+ ...(at.apiKey ? { [GATEWAY_KEY_HEADER]: at.apiKey } : {}),
736
+ };
737
+ const routed = upstreamFetch(GATEWAY_UPSTREAMS[kind], protocol);
738
+ if (protocol === 'gemini') {
739
+ const { GoogleGenAI } = sdk('@google/genai', kind);
740
+ return new GoogleGenAI({
741
+ apiKey,
742
+ httpOptions: {
743
+ baseUrl: baseURL,
744
+ headers,
745
+ timeout: opts.timeoutMs,
746
+ retryOptions: genaiRetries(opts),
747
+ fetch: routed,
748
+ },
749
+ });
750
+ }
751
+ const common = {
752
+ apiKey,
753
+ baseURL,
754
+ defaultHeaders: headers,
755
+ timeout: opts.timeoutMs,
756
+ maxRetries: retriesOf(opts),
757
+ fetch: funded(routed),
758
+ };
759
+ if (protocol === 'anthropic') {
760
+ const { Anthropic } = sdk('@anthropic-ai/sdk', kind);
761
+ return new Anthropic(common);
762
+ }
763
+ const { OpenAI } = sdk('openai', kind);
764
+ return new OpenAI(common);
765
+ }
667
766
  /**
668
767
  * `project_id` out of a service-account key file, when one is named.
669
768
  *
@@ -0,0 +1,29 @@
1
+ import type { ProviderKind } from './factory.ts';
2
+ export declare const GATEWAY_NAME = "bifrost";
3
+ export declare const GATEWAY_URL_ENV = "BIFROST_BASE_URL";
4
+ export declare const GATEWAY_KEY_ENV = "BIFROST_API_KEY";
5
+ /** Bifrost's own virtual-key header: still read when `Authorization` is taken by gateway login. */
6
+ export declare const GATEWAY_KEY_HEADER = "x-bf-vk";
7
+ export interface Gateway {
8
+ /** no trailing slash */
9
+ baseURL: string;
10
+ /** the virtual key; absent for a gateway that enforces none */
11
+ apiKey?: string;
12
+ }
13
+ /** The gateway this process is configured for, or undefined when vendors are called directly. */
14
+ export declare function gatewayOf(): Gateway | undefined;
15
+ /** The wire formats Bifrost has a drop-in endpoint for. */
16
+ export type GatewayProtocol = 'openai' | 'anthropic' | 'gemini';
17
+ export declare const GATEWAY_PATHS: Record<GatewayProtocol, string>;
18
+ /** Bifrost's name for the upstream each kind reaches; it routes on `<upstream>/<model>`. */
19
+ export declare const GATEWAY_UPSTREAMS: Record<ProviderKind, string>;
20
+ /**
21
+ * Qualifies the model id on the wire only. A `Model`'s or `Embedder`'s id stays
22
+ * the one the config names, because a memory or an index records the embedder
23
+ * id and refuses a different one — flipping gateway mode must not orphan them.
24
+ *
25
+ * Gemini names its model in the url; the other two in the body. `inner` is
26
+ * resolved per call so a stubbed global `fetch` is honoured.
27
+ */
28
+ export declare function upstreamFetch(upstream: string, protocol: GatewayProtocol, inner?: typeof fetch): typeof fetch;
29
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1,99 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Gateway mode
3
+ //
4
+ // With `BIFROST_BASE_URL` set, every provider is reached through one Bifrost
5
+ // gateway and one virtual key, and `agents.yaml` does not change: a
6
+ // `vertex:gemini-3.5-flash` still goes out through the GenAI SDK, in Gemini's
7
+ // own wire format, only to Bifrost's `/genai` drop-in endpoint instead of to
8
+ // Google. Keeping each vendor's protocol is the point — Bifrost's unified
9
+ // chat-completions endpoint would cost the thinking budgets, thought signatures
10
+ // and cache accounting the native adapters exist for.
11
+ //
12
+ // The switch is the environment rather than the config because it describes the
13
+ // machine, not the project: the same project runs direct on a laptop with
14
+ // vendor keys and through the gateway on a machine that only holds a virtual
15
+ // key.
16
+ // ---------------------------------------------------------------------------
17
+ export const GATEWAY_NAME = 'bifrost';
18
+ export const GATEWAY_URL_ENV = 'BIFROST_BASE_URL';
19
+ export const GATEWAY_KEY_ENV = 'BIFROST_API_KEY';
20
+ /** Bifrost's own virtual-key header: still read when `Authorization` is taken by gateway login. */
21
+ export const GATEWAY_KEY_HEADER = 'x-bf-vk';
22
+ /** The gateway this process is configured for, or undefined when vendors are called directly. */
23
+ export function gatewayOf() {
24
+ const url = process.env[GATEWAY_URL_ENV]?.trim();
25
+ if (!url) {
26
+ return undefined;
27
+ }
28
+ const apiKey = process.env[GATEWAY_KEY_ENV]?.trim();
29
+ return { baseURL: url.replace(/\/+$/, ''), ...(apiKey ? { apiKey } : {}) };
30
+ }
31
+ export const GATEWAY_PATHS = {
32
+ openai: '/openai',
33
+ anthropic: '/anthropic',
34
+ gemini: '/genai',
35
+ };
36
+ /** Bifrost's name for the upstream each kind reaches; it routes on `<upstream>/<model>`. */
37
+ export const GATEWAY_UPSTREAMS = {
38
+ openai: 'openai',
39
+ 'openai-compatible': 'openai',
40
+ anthropic: 'anthropic',
41
+ google: 'gemini',
42
+ vertex: 'vertex',
43
+ openrouter: 'openrouter',
44
+ };
45
+ const MODEL_PATH = /\/models\/([^/:?]+):(\w+)/;
46
+ /**
47
+ * Qualifies the model id on the wire only. A `Model`'s or `Embedder`'s id stays
48
+ * the one the config names, because a memory or an index records the embedder
49
+ * id and refuses a different one — flipping gateway mode must not orphan them.
50
+ *
51
+ * Gemini names its model in the url; the other two in the body. `inner` is
52
+ * resolved per call so a stubbed global `fetch` is honoured.
53
+ */
54
+ export function upstreamFetch(upstream, protocol, inner) {
55
+ const prefix = `${upstream}/`;
56
+ const qualify = (id) => (id.startsWith(prefix) ? id : prefix + id);
57
+ return async (input, init) => {
58
+ const send = inner ?? fetch;
59
+ const body = typeof init?.body === 'string' ? qualifyBody(init.body, protocol, qualify) : init?.body;
60
+ if (protocol !== 'gemini') {
61
+ return send(input, body === init?.body ? init : { ...init, body });
62
+ }
63
+ const url = input instanceof Request ? input.url : String(input);
64
+ const routed = url.replace(MODEL_PATH, (_match, id, verb) => `/models/${qualify(id)}:${verb}`);
65
+ return send(routed, { ...init, body });
66
+ };
67
+ }
68
+ function qualifyBody(body, protocol, qualify) {
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(body);
72
+ }
73
+ catch {
74
+ return body;
75
+ }
76
+ if (!parsed || typeof parsed !== 'object') {
77
+ return body;
78
+ }
79
+ const doc = parsed;
80
+ if (protocol !== 'gemini') {
81
+ if (typeof doc.model !== 'string') {
82
+ return body;
83
+ }
84
+ doc.model = qualify(doc.model);
85
+ return JSON.stringify(doc);
86
+ }
87
+ // batchEmbedContents repeats the model per request, as `models/<id>`.
88
+ if (!Array.isArray(doc.requests)) {
89
+ return body;
90
+ }
91
+ for (const request of doc.requests) {
92
+ const id = typeof request?.model === 'string' ? /^models\/(.+)$/.exec(request.model) : null;
93
+ if (id) {
94
+ request.model = `models/${qualify(id[1])}`;
95
+ }
96
+ }
97
+ return JSON.stringify(doc);
98
+ }
99
+ //# sourceMappingURL=gateway.js.map
@@ -1,5 +1,6 @@
1
1
  export * from './anthropic.ts';
2
2
  export * from './factory.ts';
3
+ export * from './gateway.ts';
3
4
  export * from './gemini.ts';
4
5
  export * from './openai-chat.ts';
5
6
  export * from './openai-responses.ts';
@@ -1,5 +1,6 @@
1
1
  export * from "./anthropic.js";
2
2
  export * from "./factory.js";
3
+ export * from "./gateway.js";
3
4
  export * from "./gemini.js";
4
5
  export * from "./openai-chat.js";
5
6
  export * from "./openai-responses.js";
@@ -7,9 +7,9 @@ import { z } from 'zod';
7
7
  declare const provider: z.ZodObject<{
8
8
  kind: z.ZodOptional<z.ZodEnum<{
9
9
  openai: "openai";
10
+ anthropic: "anthropic";
10
11
  google: "google";
11
12
  vertex: "vertex";
12
- anthropic: "anthropic";
13
13
  openrouter: "openrouter";
14
14
  "openai-compatible": "openai-compatible";
15
15
  }>>;
@@ -72,9 +72,9 @@ declare const modelSpec: z.ZodObject<{
72
72
  serviceTier: z.ZodOptional<z.ZodEnum<{
73
73
  default: "default";
74
74
  auto: "auto";
75
+ priority: "priority";
75
76
  fast: "fast";
76
77
  flex: "flex";
77
- priority: "priority";
78
78
  scale: "scale";
79
79
  }>>;
80
80
  apiKey: z.ZodOptional<z.ZodString>;
@@ -184,9 +184,9 @@ export declare const projectSchema: z.ZodObject<{
184
184
  providers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
185
185
  kind: z.ZodOptional<z.ZodEnum<{
186
186
  openai: "openai";
187
+ anthropic: "anthropic";
187
188
  google: "google";
188
189
  vertex: "vertex";
189
- anthropic: "anthropic";
190
190
  openrouter: "openrouter";
191
191
  "openai-compatible": "openai-compatible";
192
192
  }>>;
@@ -244,9 +244,9 @@ export declare const projectSchema: z.ZodObject<{
244
244
  serviceTier: z.ZodOptional<z.ZodEnum<{
245
245
  default: "default";
246
246
  auto: "auto";
247
+ priority: "priority";
247
248
  fast: "fast";
248
249
  flex: "flex";
249
- priority: "priority";
250
250
  scale: "scale";
251
251
  }>>;
252
252
  apiKey: z.ZodOptional<z.ZodString>;
@@ -347,7 +347,7 @@ export declare const projectSchema: z.ZodObject<{
347
347
  autoRecall: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
348
348
  limit: z.ZodDefault<z.ZodInt>;
349
349
  }, z.core.$strict>]>>;
350
- }, z.core.$strict>>>>;
350
+ }, z.core.$strict>>, unknown>>;
351
351
  sandbox: z.ZodOptional<z.ZodObject<{
352
352
  image: z.ZodOptional<z.ZodString>;
353
353
  build: z.ZodOptional<z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/neo",
3
- "version": "1.1.28",
3
+ "version": "1.1.29",
4
4
  "description": "A multi-agent runtime for Node.js: agents, models, tools, skills, memory and an append-only trajectory.",
5
5
  "keywords": [
6
6
  "agents",
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "graphology": "^0.26.0",
66
- "yaml": "^2.9.0",
67
- "zod": "^4.4.3"
66
+ "yaml": "^2.9.1",
67
+ "zod": "^4.6.7"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@anthropic-ai/sdk": "^0.120.0",
@@ -88,8 +88,8 @@
88
88
  },
89
89
  "devDependencies": {
90
90
  "@anthropic-ai/sdk": "^0.120.0",
91
- "@google/genai": "^2.18.0",
92
- "@openrouter/sdk": "^1.2.80",
91
+ "@google/genai": "^2.30.0",
92
+ "@openrouter/sdk": "^1.4.25",
93
93
  "openai": "^6.0.0"
94
94
  }
95
95
  }