@zenera/neo 1.1.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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.js +97 -0
- package/dist/architecture.d.ts +74 -0
- package/dist/architecture.js +105 -0
- package/dist/embedding.d.ts +60 -0
- package/dist/embedding.js +21 -0
- package/dist/embeddings/gemini.d.ts +24 -0
- package/dist/embeddings/gemini.js +71 -0
- package/dist/embeddings/index.d.ts +4 -0
- package/dist/embeddings/index.js +4 -0
- package/dist/embeddings/openai.d.ts +18 -0
- package/dist/embeddings/openai.js +27 -0
- package/dist/embeddings/openrouter.d.ts +21 -0
- package/dist/embeddings/openrouter.js +54 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +110 -0
- package/dist/ids.d.ts +14 -0
- package/dist/ids.js +33 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/inspect/client.d.ts +2 -0
- package/dist/inspect/client.js +1326 -0
- package/dist/inspect/css.d.ts +2 -0
- package/dist/inspect/css.js +164 -0
- package/dist/inspect/index.d.ts +9 -0
- package/dist/inspect/index.js +12 -0
- package/dist/inspect/page.d.ts +3 -0
- package/dist/inspect/page.js +93 -0
- package/dist/inspect/report.d.ts +54 -0
- package/dist/inspect/report.js +62 -0
- package/dist/kernel.d.ts +178 -0
- package/dist/kernel.js +929 -0
- package/dist/memory-stores/factory.d.ts +15 -0
- package/dist/memory-stores/factory.js +29 -0
- package/dist/memory-stores/file.d.ts +37 -0
- package/dist/memory-stores/file.js +174 -0
- package/dist/memory-stores/in-memory.d.ts +17 -0
- package/dist/memory-stores/in-memory.js +80 -0
- package/dist/memory-stores/index.d.ts +4 -0
- package/dist/memory-stores/index.js +5 -0
- package/dist/memory.d.ts +115 -0
- package/dist/memory.js +213 -0
- package/dist/model.d.ts +25 -0
- package/dist/model.js +2 -0
- package/dist/models/anthropic.d.ts +36 -0
- package/dist/models/anthropic.js +293 -0
- package/dist/models/factory.d.ts +202 -0
- package/dist/models/factory.js +533 -0
- package/dist/models/gemini.d.ts +45 -0
- package/dist/models/gemini.js +368 -0
- package/dist/models/index.d.ts +7 -0
- package/dist/models/index.js +7 -0
- package/dist/models/openai-chat.d.ts +19 -0
- package/dist/models/openai-chat.js +203 -0
- package/dist/models/openai-responses.d.ts +31 -0
- package/dist/models/openai-responses.js +258 -0
- package/dist/models/openrouter.d.ts +44 -0
- package/dist/models/openrouter.js +230 -0
- package/dist/payload-stores/factory.d.ts +15 -0
- package/dist/payload-stores/factory.js +30 -0
- package/dist/payload-stores/file.d.ts +29 -0
- package/dist/payload-stores/file.js +112 -0
- package/dist/payload-stores/in-memory.d.ts +17 -0
- package/dist/payload-stores/in-memory.js +40 -0
- package/dist/payload-stores/index.d.ts +4 -0
- package/dist/payload-stores/index.js +5 -0
- package/dist/payload.d.ts +89 -0
- package/dist/payload.js +181 -0
- package/dist/project/config.d.ts +302 -0
- package/dist/project/config.js +290 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +4 -0
- package/dist/project/load.d.ts +142 -0
- package/dist/project/load.js +430 -0
- package/dist/project/refs.d.ts +23 -0
- package/dist/project/refs.js +70 -0
- package/dist/prompt.d.ts +66 -0
- package/dist/prompt.js +62 -0
- package/dist/runner.d.ts +103 -0
- package/dist/runner.js +537 -0
- package/dist/services.d.ts +23 -0
- package/dist/services.js +44 -0
- package/dist/skill-providers/factory.d.ts +16 -0
- package/dist/skill-providers/factory.js +26 -0
- package/dist/skill-providers/file.d.ts +44 -0
- package/dist/skill-providers/file.js +209 -0
- package/dist/skill-providers/index.d.ts +4 -0
- package/dist/skill-providers/index.js +5 -0
- package/dist/skill-providers/static.d.ts +13 -0
- package/dist/skill-providers/static.js +47 -0
- package/dist/skills.d.ts +102 -0
- package/dist/skills.js +232 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.js +49 -0
- package/dist/tools/exa.d.ts +23 -0
- package/dist/tools/exa.js +389 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/sandbox.d.ts +172 -0
- package/dist/tools/sandbox.js +602 -0
- package/dist/tools/workspace.d.ts +53 -0
- package/dist/tools/workspace.js +1034 -0
- package/dist/trajectory.d.ts +213 -0
- package/dist/trajectory.js +232 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.js +134 -0
- package/package.json +98 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { GeminiEmbedder } from "../embeddings/gemini.js";
|
|
4
|
+
import { OpenAIEmbedder } from "../embeddings/openai.js";
|
|
5
|
+
import { OpenRouterEmbedder } from "../embeddings/openrouter.js";
|
|
6
|
+
import { AnthropicModel } from "./anthropic.js";
|
|
7
|
+
import { GeminiModel } from "./gemini.js";
|
|
8
|
+
import { OpenAIModel } from "./openai-chat.js";
|
|
9
|
+
import { OpenAIResponsesModel } from "./openai-responses.js";
|
|
10
|
+
import { OpenRouterModel } from "./openrouter.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// The vendor SDKs
|
|
13
|
+
//
|
|
14
|
+
// All four are *optional peer dependencies*: an application installs the one
|
|
15
|
+
// vendor it talks to and pays for nothing else — which matters most for
|
|
16
|
+
// `@google/genai`, whose own tree carries google-auth-library and protobufjs.
|
|
17
|
+
//
|
|
18
|
+
// That only holds if importing this library does not reach for all four, so
|
|
19
|
+
// nothing above is a value import: the SDKs are pulled in here, when a client
|
|
20
|
+
// of that protocol is first built, and never at module scope.
|
|
21
|
+
//
|
|
22
|
+
// `createRequire` rather than `await import()` because it is synchronous, and
|
|
23
|
+
// so `createModel()` and `ModelRegistry.client()` keep handing back a usable
|
|
24
|
+
// object instead of a promise — which is also what keeps a bad credential
|
|
25
|
+
// throwing at the call that named it rather than at some later request. Three
|
|
26
|
+
// of the four publish a CommonJS build; `@openrouter/sdk` is ESM-only, which
|
|
27
|
+
// `require` has loaded since Node 22 for module graphs without a top-level
|
|
28
|
+
// await, and this library already requires Node 24.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const requirePeer = createRequire(import.meta.url);
|
|
31
|
+
/**
|
|
32
|
+
* An uninstalled optional peer is a setup mistake, not a bug, so it is reported
|
|
33
|
+
* as the one-line fix rather than as a resolution failure from inside Node.
|
|
34
|
+
*/
|
|
35
|
+
function sdk(pkg, kind) {
|
|
36
|
+
try {
|
|
37
|
+
return requirePeer(pkg);
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (cause.code !== 'MODULE_NOT_FOUND') {
|
|
41
|
+
throw cause;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`provider kind "${kind}" needs ${pkg}, which is not installed — run: npm i ${pkg}`, { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Only two of these need no adapter of their own.
|
|
48
|
+
*
|
|
49
|
+
* `openai-compatible` is the shim kind — vLLM, a self-hosted gateway — and the
|
|
50
|
+
* OpenAI client is exactly right for it. Everything else gets its vendor's own
|
|
51
|
+
* SDK, because the compatibility endpoints each vendor publishes are porting
|
|
52
|
+
* aids: Google's drops thinking budgets, thought signatures and cached-content
|
|
53
|
+
* accounting, Anthropic's drops cache accounting and extended thinking. Those
|
|
54
|
+
* are the things this runtime is built around, so paying for two more
|
|
55
|
+
* dependencies is the cheaper trade.
|
|
56
|
+
*
|
|
57
|
+
* `openrouter` used to be that shim with the two constants filled in, which
|
|
58
|
+
* cost nothing until the parts of OpenRouter worth having turned out to be the
|
|
59
|
+
* parts chat completions cannot say: which upstream provider serves a request,
|
|
60
|
+
* what to fall back to when it cannot, and what the call actually cost. Its
|
|
61
|
+
* own SDK carries those as first-class fields, so it now gets an adapter like
|
|
62
|
+
* every other vendor. `openai-compatible` against the same base url is still
|
|
63
|
+
* there for anyone who wants the older, laxer client back.
|
|
64
|
+
*
|
|
65
|
+
* `google` and `vertex` differ only in how the client authenticates and which
|
|
66
|
+
* backend it addresses; both speak the same API through the same adapter.
|
|
67
|
+
*/
|
|
68
|
+
const KINDS = {
|
|
69
|
+
openai: {
|
|
70
|
+
protocol: 'openai',
|
|
71
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
72
|
+
baseURLEnv: 'OPENAI_BASE_URL',
|
|
73
|
+
apis: ['chat', 'responses'],
|
|
74
|
+
},
|
|
75
|
+
google: {
|
|
76
|
+
protocol: 'gemini',
|
|
77
|
+
apiKeyEnv: 'GEMINI_API_KEY',
|
|
78
|
+
baseURLEnv: 'GEMINI_BASE_URL',
|
|
79
|
+
apis: [],
|
|
80
|
+
},
|
|
81
|
+
vertex: {
|
|
82
|
+
// Vertex authenticates with Application Default Credentials, which the
|
|
83
|
+
// GenAI SDK resolves itself — so unlike every other kind this one
|
|
84
|
+
// usually has no key at all. `VERTEX_API_KEY` covers express mode.
|
|
85
|
+
protocol: 'gemini',
|
|
86
|
+
apiKeyEnv: 'VERTEX_API_KEY',
|
|
87
|
+
baseURLEnv: 'VERTEX_BASE_URL',
|
|
88
|
+
apis: [],
|
|
89
|
+
keyOptional: true,
|
|
90
|
+
},
|
|
91
|
+
anthropic: {
|
|
92
|
+
protocol: 'anthropic',
|
|
93
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
94
|
+
baseURLEnv: 'ANTHROPIC_BASE_URL',
|
|
95
|
+
apis: [],
|
|
96
|
+
},
|
|
97
|
+
openrouter: {
|
|
98
|
+
protocol: 'openrouter',
|
|
99
|
+
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
100
|
+
baseURLEnv: 'OPENROUTER_BASE_URL',
|
|
101
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
102
|
+
apis: [],
|
|
103
|
+
},
|
|
104
|
+
'openai-compatible': {
|
|
105
|
+
protocol: 'openai',
|
|
106
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
107
|
+
baseURLEnv: 'OPENAI_BASE_URL',
|
|
108
|
+
apis: ['chat', 'responses'],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
export const providerKinds = Object.keys(KINDS);
|
|
112
|
+
export function isProviderKind(name) {
|
|
113
|
+
return Object.hasOwn(KINDS, name);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Environment references
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// `${VAR}` and `${VAR:-fallback}`. Chosen over a bare `env.VAR` token because a
|
|
119
|
+
// literal secret never contains `${`, so there is no rule to remember about
|
|
120
|
+
// which strings are magic — and because it composes inside a longer value,
|
|
121
|
+
// which a whole-value token cannot: `https://${GATEWAY}/v1`.
|
|
122
|
+
const ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
123
|
+
/**
|
|
124
|
+
* Substitutes `${VAR}` references. An unset variable with no fallback is an
|
|
125
|
+
* error naming both the variable and where it was written, because the
|
|
126
|
+
* alternative is an empty api key and a 401 three layers away.
|
|
127
|
+
*/
|
|
128
|
+
export function expandEnv(value, where) {
|
|
129
|
+
return value.replace(ENV_REF, (_match, name, fallback) => {
|
|
130
|
+
const found = fromEnv(name);
|
|
131
|
+
if (found !== undefined) {
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
if (fallback !== undefined) {
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`${where}: \${${name}} is not set`);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function fromEnv(name) {
|
|
141
|
+
const value = process.env[name];
|
|
142
|
+
return value?.trim() ? value : undefined;
|
|
143
|
+
}
|
|
144
|
+
function expand(value, where) {
|
|
145
|
+
return value === undefined ? undefined : expandEnv(value, where);
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Registry
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Named providers, and the clients behind them.
|
|
152
|
+
*
|
|
153
|
+
* The clients are why this is an object rather than a function: one `OpenAI`
|
|
154
|
+
* per provider name, built on first use and shared by every model and every
|
|
155
|
+
* agent that names it. A registry is also the seam a host uses to point a name
|
|
156
|
+
* at different credentials, or at a stub — neither of which should mean
|
|
157
|
+
* touching a `switch`.
|
|
158
|
+
*/
|
|
159
|
+
export class ModelRegistry {
|
|
160
|
+
#providers = new Map();
|
|
161
|
+
#clients = new Map();
|
|
162
|
+
#default = 'openai';
|
|
163
|
+
/** Declares (or replaces) a provider. Nothing is contacted until it is used. */
|
|
164
|
+
provider(name, spec = {}) {
|
|
165
|
+
if (spec.kind !== undefined && !isProviderKind(spec.kind)) {
|
|
166
|
+
throw new TypeError(`provider "${name}": unknown kind "${spec.kind}" ` +
|
|
167
|
+
`(known: ${providerKinds.join(', ')})`);
|
|
168
|
+
}
|
|
169
|
+
this.#providers.set(name, spec);
|
|
170
|
+
// Re-declaring after a client was handed out would otherwise leave the
|
|
171
|
+
// old credentials in service for the rest of the process.
|
|
172
|
+
this.#clients.delete(name);
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
/** Names the provider a bare model id belongs to. */
|
|
176
|
+
setDefault(name) {
|
|
177
|
+
if (!this.has(name)) {
|
|
178
|
+
throw new Error(`default provider "${name}" is not declared (known: ${this.names().join(', ')})`);
|
|
179
|
+
}
|
|
180
|
+
this.#default = name;
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
get defaultProvider() {
|
|
184
|
+
return this.#default;
|
|
185
|
+
}
|
|
186
|
+
/** Declared here, or a built-in kind usable with no declaration at all. */
|
|
187
|
+
has(name) {
|
|
188
|
+
return this.#providers.has(name) || isProviderKind(name);
|
|
189
|
+
}
|
|
190
|
+
names() {
|
|
191
|
+
return [...new Set([...this.#providers.keys(), ...providerKinds])];
|
|
192
|
+
}
|
|
193
|
+
/** The shared client for a provider, built on first use. */
|
|
194
|
+
client(name) {
|
|
195
|
+
const existing = this.#clients.get(name);
|
|
196
|
+
if (existing) {
|
|
197
|
+
return existing;
|
|
198
|
+
}
|
|
199
|
+
const spec = this.#spec(name);
|
|
200
|
+
const built = spec.client ?? buildClient(name, this.kindOf(name), spec, spec);
|
|
201
|
+
this.#clients.set(name, built);
|
|
202
|
+
return built;
|
|
203
|
+
}
|
|
204
|
+
/** Turns a shorthand or a spec into a `Model`. */
|
|
205
|
+
model(ref) {
|
|
206
|
+
const spec = typeof ref === 'string' ? this.parse(ref) : ref;
|
|
207
|
+
const name = spec.provider ?? this.#default;
|
|
208
|
+
const provider = this.#spec(name);
|
|
209
|
+
const kind = this.kindOf(name);
|
|
210
|
+
const defaults = KINDS[kind];
|
|
211
|
+
// Inline credentials opt out of the shared client: they describe a
|
|
212
|
+
// different connection, and handing back the memoized one would
|
|
213
|
+
// silently ignore them.
|
|
214
|
+
const client = spec.client ??
|
|
215
|
+
(hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
|
|
216
|
+
// Every protocol but OpenAI's has exactly one API, so naming one is a
|
|
217
|
+
// mistake worth reporting rather than a field to ignore.
|
|
218
|
+
if (defaults.protocol !== 'openai') {
|
|
219
|
+
if (spec.api) {
|
|
220
|
+
throw new TypeError(`provider "${name}" (${kind}) has one api, so "${spec.api}" means nothing here`);
|
|
221
|
+
}
|
|
222
|
+
switch (defaults.protocol) {
|
|
223
|
+
case 'anthropic':
|
|
224
|
+
return new AnthropicModel(spec.model, client, spec);
|
|
225
|
+
case 'gemini':
|
|
226
|
+
return new GeminiModel(spec.model, client, spec);
|
|
227
|
+
case 'openrouter':
|
|
228
|
+
return new OpenRouterModel(spec.model, client, spec);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const api = spec.api ?? defaults.apis[0];
|
|
232
|
+
if (!defaults.apis.includes(api)) {
|
|
233
|
+
throw new TypeError(`provider "${name}" (${kind}) does not speak the "${api}" api ` +
|
|
234
|
+
`(supported: ${defaults.apis.join(', ')})`);
|
|
235
|
+
}
|
|
236
|
+
return api === 'responses'
|
|
237
|
+
? new OpenAIResponsesModel(spec.model, client, spec)
|
|
238
|
+
: new OpenAIModel(spec.model, client, spec);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Turns a shorthand or a spec into an `Embedder`, over the same providers
|
|
242
|
+
* and the same shared clients as `model()` — an `openai` key declared once
|
|
243
|
+
* generates and embeds without being mentioned twice.
|
|
244
|
+
*/
|
|
245
|
+
embedder(ref) {
|
|
246
|
+
const spec = typeof ref === 'string' ? this.#parseEmbedding(ref) : ref;
|
|
247
|
+
const name = spec.provider ?? this.#default;
|
|
248
|
+
const provider = this.#spec(name);
|
|
249
|
+
const kind = this.kindOf(name);
|
|
250
|
+
const client = spec.client ??
|
|
251
|
+
(hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
|
|
252
|
+
switch (KINDS[kind].protocol) {
|
|
253
|
+
case 'openai':
|
|
254
|
+
return new OpenAIEmbedder(spec.model, client, spec);
|
|
255
|
+
case 'gemini':
|
|
256
|
+
return new GeminiEmbedder(spec.model, client, spec);
|
|
257
|
+
case 'openrouter':
|
|
258
|
+
return new OpenRouterEmbedder(spec.model, client, spec);
|
|
259
|
+
case 'anthropic':
|
|
260
|
+
// Not an omission to be filled in later: Anthropic publishes no
|
|
261
|
+
// embeddings API at all, and points at third parties instead.
|
|
262
|
+
throw new TypeError(`provider "${name}" (${kind}) has no embeddings api — ` +
|
|
263
|
+
`declare a provider of another kind for \`embeddings:\``);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* What a ref needs to reach its vendor, resolved but not contacted. An
|
|
268
|
+
* unknown provider still throws: that is a broken config, not a missing
|
|
269
|
+
* credential, and the two want different words.
|
|
270
|
+
*/
|
|
271
|
+
requirement(ref) {
|
|
272
|
+
const spec = typeof ref === 'string' ? this.parse(ref) : ref;
|
|
273
|
+
const name = spec.provider ?? this.#default;
|
|
274
|
+
const provider = this.#spec(name);
|
|
275
|
+
const kind = this.kindOf(name);
|
|
276
|
+
const defaults = KINDS[kind];
|
|
277
|
+
const creds = hasCredentials(spec) ? spec : provider;
|
|
278
|
+
const apiKeyEnv = creds.apiKeyEnv ?? defaults.apiKeyEnv;
|
|
279
|
+
// An unset `${VAR}` is precisely the absent credential being asked
|
|
280
|
+
// about, so it answers the question rather than interrupting it.
|
|
281
|
+
let apiKey;
|
|
282
|
+
try {
|
|
283
|
+
apiKey = expand(creds.apiKey, `provider "${name}": apiKey`) ?? fromEnv(apiKeyEnv);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
apiKey = undefined;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
provider: name,
|
|
290
|
+
kind,
|
|
291
|
+
apiKeyEnv,
|
|
292
|
+
satisfied: Boolean(apiKey ??
|
|
293
|
+
provider.token ??
|
|
294
|
+
(spec.client || provider.client) ??
|
|
295
|
+
defaults.keyOptional),
|
|
296
|
+
keyOptional: Boolean(defaults.keyOptional),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** `[provider[/api]:]model` → spec. Exposed so a caller can validate a ref. */
|
|
300
|
+
parse(ref) {
|
|
301
|
+
// Only the first colon separates the prefix: model ids may contain colons.
|
|
302
|
+
const colon = ref.indexOf(':');
|
|
303
|
+
if (colon < 0) {
|
|
304
|
+
return { model: ref };
|
|
305
|
+
}
|
|
306
|
+
const [provider, api] = ref.slice(0, colon).split('/');
|
|
307
|
+
const model = ref.slice(colon + 1);
|
|
308
|
+
if (!model) {
|
|
309
|
+
throw new TypeError(`missing model id in "${ref}"`);
|
|
310
|
+
}
|
|
311
|
+
if (!provider || !this.has(provider)) {
|
|
312
|
+
throw new TypeError(`unknown provider "${provider}" in "${ref}" ` +
|
|
313
|
+
`(known: ${this.names().join(', ')}; a model id that itself contains a ` +
|
|
314
|
+
`colon must name its provider, e.g. "openai:${ref}")`);
|
|
315
|
+
}
|
|
316
|
+
if (api !== undefined && api !== 'chat' && api !== 'responses') {
|
|
317
|
+
throw new TypeError(`unknown api "${api}" in "${ref}" (expected chat or responses)`);
|
|
318
|
+
}
|
|
319
|
+
return { provider, api, model };
|
|
320
|
+
}
|
|
321
|
+
#parseEmbedding(ref) {
|
|
322
|
+
const { provider, api, model } = this.parse(ref);
|
|
323
|
+
if (api) {
|
|
324
|
+
throw new TypeError(`"${ref}": embeddings have one api, so "${api}" means nothing here`);
|
|
325
|
+
}
|
|
326
|
+
return { provider, model };
|
|
327
|
+
}
|
|
328
|
+
#spec(name) {
|
|
329
|
+
const declared = this.#providers.get(name);
|
|
330
|
+
if (declared) {
|
|
331
|
+
return declared;
|
|
332
|
+
}
|
|
333
|
+
if (isProviderKind(name)) {
|
|
334
|
+
return { kind: name };
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`unknown provider "${name}" (known: ${this.names().join(', ')}; ` +
|
|
337
|
+
`declare it under \`providers:\` or in ProjectOptions.providers)`);
|
|
338
|
+
}
|
|
339
|
+
/** A declared kind wins; otherwise a built-in name is its own kind. */
|
|
340
|
+
kindOf(name) {
|
|
341
|
+
const declared = this.#providers.get(name)?.kind;
|
|
342
|
+
return declared ?? (isProviderKind(name) ? name : 'openai');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Credential resolution, in one place so a `${VAR}` reference behaves the same
|
|
347
|
+
* whether it came from yaml or from application code. `creds` may be a model
|
|
348
|
+
* spec overriding the connection; `opts` always carries the provider's
|
|
349
|
+
* transport settings, which an override does not get to reinvent.
|
|
350
|
+
*/
|
|
351
|
+
function buildClient(name, kind, creds, opts) {
|
|
352
|
+
const where = `provider "${name}"`;
|
|
353
|
+
const defaults = KINDS[kind];
|
|
354
|
+
// A token callback replaces the key rather than supplementing it: the
|
|
355
|
+
// header is rewritten per request, so whatever the SDK was constructed with
|
|
356
|
+
// never reaches the wire.
|
|
357
|
+
const apiKeyEnv = creds.apiKeyEnv ?? defaults.apiKeyEnv;
|
|
358
|
+
const apiKey = expand(creds.apiKey, `${where}: apiKey`) ?? fromEnv(apiKeyEnv);
|
|
359
|
+
if (!apiKey && !opts.token && !defaults.keyOptional) {
|
|
360
|
+
throw new Error(`${where}: no api key — set \`apiKey\`, or set ${apiKeyEnv}`);
|
|
361
|
+
}
|
|
362
|
+
// An absent base url means "vendor default", so it may stay undefined.
|
|
363
|
+
const baseURL = expand(creds.baseURL, `${where}: baseURL`) ??
|
|
364
|
+
fromEnv(creds.baseURLEnv ?? defaults.baseURLEnv) ??
|
|
365
|
+
defaults.baseURL;
|
|
366
|
+
const headers = expandHeaders(opts.headers, where);
|
|
367
|
+
if (defaults.protocol === 'gemini') {
|
|
368
|
+
return buildGenAI(kind, apiKey, baseURL, headers, opts, where);
|
|
369
|
+
}
|
|
370
|
+
if (defaults.protocol === 'openrouter') {
|
|
371
|
+
return buildOpenRouter(kind, apiKey, baseURL, headers, opts);
|
|
372
|
+
}
|
|
373
|
+
const common = {
|
|
374
|
+
apiKey: apiKey ?? OAUTH_PLACEHOLDER,
|
|
375
|
+
baseURL,
|
|
376
|
+
defaultHeaders: headers,
|
|
377
|
+
timeout: opts.timeoutMs,
|
|
378
|
+
maxRetries: opts.maxRetries,
|
|
379
|
+
...(opts.token ? { fetch: bearerFetch(opts.token) } : {}),
|
|
380
|
+
};
|
|
381
|
+
if (defaults.protocol === 'anthropic') {
|
|
382
|
+
const { Anthropic } = sdk('@anthropic-ai/sdk', kind);
|
|
383
|
+
return new Anthropic(common);
|
|
384
|
+
}
|
|
385
|
+
const { OpenAI } = sdk('openai', kind);
|
|
386
|
+
return new OpenAI(common);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* OpenRouter's client takes its base url as `serverURL` and its retry policy as
|
|
390
|
+
* a strategy object rather than a count, so the shared `common` shape does not
|
|
391
|
+
* fit — but the two things that usually need a custom `fetch` are cheaper here.
|
|
392
|
+
*
|
|
393
|
+
* A `token` callback goes straight in as `apiKey`, which this SDK accepts in
|
|
394
|
+
* callable form and re-invokes per request; there is nothing to intercept.
|
|
395
|
+
* Default headers have no constructor slot at all, so they are added by a
|
|
396
|
+
* `beforeRequest` hook — the same seam the SDK uses for tracing headers.
|
|
397
|
+
*/
|
|
398
|
+
function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
|
|
399
|
+
const { OpenRouter, HTTPClient } = sdk('@openrouter/sdk', kind);
|
|
400
|
+
let httpClient;
|
|
401
|
+
if (headers) {
|
|
402
|
+
httpClient = new HTTPClient();
|
|
403
|
+
httpClient.addHook('beforeRequest', (req) => {
|
|
404
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
405
|
+
req.headers.set(key, value);
|
|
406
|
+
}
|
|
407
|
+
return req;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const { token } = opts;
|
|
411
|
+
return new OpenRouter({
|
|
412
|
+
apiKey: token ? async () => token() : apiKey,
|
|
413
|
+
serverURL: baseURL,
|
|
414
|
+
timeoutMs: opts.timeoutMs,
|
|
415
|
+
// A count of zero is the one value that has to change shape: this SDK
|
|
416
|
+
// says "do not retry" with a strategy, not with a number.
|
|
417
|
+
retryConfig: opts.maxRetries === 0 ? { strategy: 'none' } : undefined,
|
|
418
|
+
httpClient,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The GenAI client is configured by *backend* rather than by url: `vertexai`
|
|
423
|
+
* selects the Gemini Enterprise endpoint and the SDK derives the address from
|
|
424
|
+
* the project and region, so there is no base url to assemble here.
|
|
425
|
+
*
|
|
426
|
+
* Authentication differs the same way. The Gemini API takes a key; Vertex
|
|
427
|
+
* normally takes none, because the SDK resolves Application Default
|
|
428
|
+
* Credentials — a service account, a workload identity, `gcloud auth
|
|
429
|
+
* application-default login` — and refreshes them itself. That is why `vertex`
|
|
430
|
+
* is `keyOptional`, and why it needs none of the bearer-token machinery the
|
|
431
|
+
* OpenAI protocol uses for the same job.
|
|
432
|
+
*/
|
|
433
|
+
function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
|
|
434
|
+
const { GoogleGenAI } = sdk('@google/genai', kind);
|
|
435
|
+
const httpOptions = { baseUrl: baseURL, headers, timeout: opts.timeoutMs };
|
|
436
|
+
if (kind !== 'vertex') {
|
|
437
|
+
return new GoogleGenAI({ apiKey, httpOptions });
|
|
438
|
+
}
|
|
439
|
+
const project = expand(opts.project, `${where}: project`) ??
|
|
440
|
+
fromEnv('GOOGLE_CLOUD_PROJECT') ??
|
|
441
|
+
projectFromKeyFile();
|
|
442
|
+
if (!project && !apiKey) {
|
|
443
|
+
throw new Error(`${where}: vertex needs \`project\`, or GOOGLE_CLOUD_PROJECT ` +
|
|
444
|
+
`(or \`apiKey\` for express mode)`);
|
|
445
|
+
}
|
|
446
|
+
return new GoogleGenAI({
|
|
447
|
+
vertexai: true,
|
|
448
|
+
apiKey,
|
|
449
|
+
project,
|
|
450
|
+
location: expand(opts.location, `${where}: location`) ??
|
|
451
|
+
fromEnv('GOOGLE_CLOUD_LOCATION') ??
|
|
452
|
+
'global',
|
|
453
|
+
httpOptions,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* `project_id` out of a service-account key file, when one is named.
|
|
458
|
+
*
|
|
459
|
+
* The GenAI SDK resolves Application Default Credentials itself but takes the
|
|
460
|
+
* project only from its constructor or `GOOGLE_CLOUD_PROJECT` — so a key file
|
|
461
|
+
* that already states which project it belongs to still has to have that
|
|
462
|
+
* repeated in the environment. Reading it here removes the second variable for
|
|
463
|
+
* the common case.
|
|
464
|
+
*
|
|
465
|
+
* Only that one route carries a project id: credentials from `gcloud auth
|
|
466
|
+
* application-default login` do not, and a metadata server answers a different
|
|
467
|
+
* endpoint entirely. Both still need the project named, which is why a
|
|
468
|
+
* failure to read anything here is not an error — the caller's own check is.
|
|
469
|
+
*/
|
|
470
|
+
function projectFromKeyFile() {
|
|
471
|
+
const path = fromEnv('GOOGLE_APPLICATION_CREDENTIALS');
|
|
472
|
+
if (!path) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
const key = JSON.parse(readFileSync(path, 'utf8'));
|
|
477
|
+
const id = key.project_id;
|
|
478
|
+
return typeof id === 'string' && id.trim() ? id : undefined;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// An unreadable or malformed file is the SDK's to report, at the point
|
|
482
|
+
// where it tries to authenticate with it and can say so precisely.
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Both SDKs refuse to construct without a key. When a token callback owns the
|
|
487
|
+
// Authorization header this value is never sent, so it only has to be non-empty.
|
|
488
|
+
const OAUTH_PLACEHOLDER = 'oauth';
|
|
489
|
+
/**
|
|
490
|
+
* Rewrites `Authorization` on every request from a callback, so a credential
|
|
491
|
+
* that expires — an Azure AD token, a gateway's short-lived grant — is
|
|
492
|
+
* refreshed without the client, and its connection pool, being rebuilt. Vertex
|
|
493
|
+
* has no need of it: its SDK does the same job from ambient credentials.
|
|
494
|
+
*/
|
|
495
|
+
function bearerFetch(token) {
|
|
496
|
+
return async (input, init) => {
|
|
497
|
+
const headers = new Headers(init?.headers);
|
|
498
|
+
headers.set('Authorization', `Bearer ${await token()}`);
|
|
499
|
+
return fetch(input, { ...init, headers });
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function expandHeaders(headers, where) {
|
|
503
|
+
if (!headers) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, expandEnv(v, `${where}: headers.${k}`)]));
|
|
507
|
+
}
|
|
508
|
+
function hasCredentials(spec) {
|
|
509
|
+
return Boolean(spec.apiKey || spec.apiKeyEnv || spec.baseURL || spec.baseURLEnv);
|
|
510
|
+
}
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Default registry
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
/**
|
|
515
|
+
* The registry behind the bare `createModel`. An application that never
|
|
516
|
+
* declares a provider still gets shared clients through it, because the
|
|
517
|
+
* built-in names (`openai`, `google`, …) resolve without declaration.
|
|
518
|
+
*/
|
|
519
|
+
export const defaultModels = new ModelRegistry();
|
|
520
|
+
/**
|
|
521
|
+
* One entry point for every provider. Accepts a shorthand string or a full
|
|
522
|
+
* spec, so `createModel('openai/responses:o3')` and
|
|
523
|
+
* `createModel({ model: 'o3', api: 'responses', apiKeyEnv: 'MY_KEY' })` are
|
|
524
|
+
* both valid.
|
|
525
|
+
*/
|
|
526
|
+
export function createModel(ref, registry = defaultModels) {
|
|
527
|
+
return registry.model(ref);
|
|
528
|
+
}
|
|
529
|
+
/** `createModel`'s counterpart for vectors, over the same registry and clients. */
|
|
530
|
+
export function createEmbedder(ref, registry = defaultModels) {
|
|
531
|
+
return registry.embedder(ref);
|
|
532
|
+
}
|
|
533
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
|
+
/** The coarse dial Gemini 3 takes in place of a token budget. */
|
|
5
|
+
export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
|
|
6
|
+
/**
|
|
7
|
+
* Provider-specific knobs.
|
|
8
|
+
*
|
|
9
|
+
* Thinking is configured two different ways depending on the generation, and
|
|
10
|
+
* both are exposed rather than unified: 2.5 takes a token `thinkingBudget`,
|
|
11
|
+
* 3 takes a coarse `thinkingLevel`, and passing the wrong one is an API error
|
|
12
|
+
* rather than something this adapter can paper over.
|
|
13
|
+
*/
|
|
14
|
+
export interface GeminiModelOptions {
|
|
15
|
+
/** hard cap on output tokens; the model's own default applies when unset */
|
|
16
|
+
maxTokens?: number;
|
|
17
|
+
/** 2.5-era budget, in tokens: `0` disables thinking, `-1` lets the model decide */
|
|
18
|
+
thinkingBudget?: number;
|
|
19
|
+
/** 3-era dial; mutually exclusive with `thinkingBudget` */
|
|
20
|
+
thinkingLevel?: GeminiThinkingLevel;
|
|
21
|
+
/**
|
|
22
|
+
* Ask for thought *summaries* — the only reasoning text the API exposes.
|
|
23
|
+
* On by default: the runtime has a `thinking` channel and the tokens are
|
|
24
|
+
* billed whether or not the summary is returned.
|
|
25
|
+
*/
|
|
26
|
+
includeThoughts?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The `Model` contract over `generateContent`.
|
|
30
|
+
*
|
|
31
|
+
* Google publishes an OpenAI-compatible endpoint, and reaching Gemini through
|
|
32
|
+
* it costs nothing but a base url. What it costs *afterwards* is everything
|
|
33
|
+
* this file exists for: thinking budgets and thought summaries, thought
|
|
34
|
+
* signatures (without which Gemini 3 function calling is rejected outright),
|
|
35
|
+
* cached-content accounting, and the block reasons that come back when a safety
|
|
36
|
+
* filter fires. None of those have an OpenAI field to be carried in.
|
|
37
|
+
*/
|
|
38
|
+
export declare class GeminiModel implements Model {
|
|
39
|
+
#private;
|
|
40
|
+
readonly id: string;
|
|
41
|
+
constructor(id: string, client: GoogleGenAI, options?: GeminiModelOptions);
|
|
42
|
+
generate(req: ModelRequest): Promise<ModelResponse>;
|
|
43
|
+
stream(req: ModelRequest, onDelta: (d: StreamDelta) => void): Promise<ModelResponse>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=gemini.d.ts.map
|