@zenera/neo 1.1.24 → 1.1.26
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/agent.js +2 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +82 -11
- package/dist/inspect/css.d.ts +1 -1
- package/dist/inspect/css.js +1 -0
- package/dist/inspect/index.d.ts +1 -1
- package/dist/inspect/report.d.ts +9 -0
- package/dist/inspect/report.js +1 -0
- package/dist/lock.d.ts +27 -0
- package/dist/lock.js +66 -0
- package/dist/match.d.ts +44 -0
- package/dist/match.js +100 -0
- package/dist/memory/diff.d.ts +39 -0
- package/dist/memory/diff.js +95 -0
- package/dist/memory/files.d.ts +1 -1
- package/dist/memory/grep.d.ts +1 -1
- package/dist/memory/grep.js +2 -2
- package/dist/memory/instructions.d.ts +15 -0
- package/dist/memory/instructions.js +103 -0
- package/dist/memory/merge.js +1 -31
- package/dist/memory/store.d.ts +1 -5
- package/dist/memory/store.js +7 -53
- package/dist/memory/twin.d.ts +6 -0
- package/dist/memory/twin.js +40 -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/models/factory.d.ts +24 -0
- package/dist/models/factory.js +153 -14
- package/dist/project/config.d.ts +9 -9
- package/dist/runner.js +7 -0
- package/dist/tools/sandbox.js +40 -2
- package/package.json +1 -1
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type AnyTool } from './types.ts';
|
|
2
|
+
export interface MemoryRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
/** memory space this record belongs to */
|
|
5
|
+
scope: string;
|
|
6
|
+
/** app-defined: 'fact' | 'preference' | 'episode' | … */
|
|
7
|
+
kind: string;
|
|
8
|
+
text: string;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
/** optimistic-concurrency token */
|
|
13
|
+
revision: number;
|
|
14
|
+
}
|
|
15
|
+
export interface MemoryDraft {
|
|
16
|
+
kind?: string;
|
|
17
|
+
text: string;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface MemoryPatch {
|
|
21
|
+
kind?: string;
|
|
22
|
+
text?: string;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
/** when given, the update fails unless the stored revision matches */
|
|
25
|
+
expectedRevision?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface MemoryQuery {
|
|
28
|
+
/** semantic query; omitted ⇒ pure filter listing */
|
|
29
|
+
text?: string;
|
|
30
|
+
filter?: Record<string, unknown>;
|
|
31
|
+
kind?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
minScore?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface MemoryHit {
|
|
36
|
+
record: MemoryRecord;
|
|
37
|
+
/** 0..1, backend-normalized */
|
|
38
|
+
score: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Embedding generation is the store's business, not the kernel's: a pgvector
|
|
42
|
+
* implementation embeds inside `write`/`search`, the in-memory one below falls
|
|
43
|
+
* back to token overlap. The kernel never sees vectors.
|
|
44
|
+
*/
|
|
45
|
+
export interface MemoryStore {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
search(scope: string, q: MemoryQuery): Promise<MemoryHit[]>;
|
|
48
|
+
get(scope: string, id: string): Promise<MemoryRecord | undefined>;
|
|
49
|
+
/** `opId` makes writes idempotent under retry/replay */
|
|
50
|
+
write(scope: string, rec: MemoryDraft, opId: string): Promise<MemoryRecord>;
|
|
51
|
+
update(scope: string, id: string, patch: MemoryPatch, opId: string): Promise<MemoryRecord>;
|
|
52
|
+
delete(scope: string, id: string, opId: string): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface MemoryBinding<TCtx = unknown> {
|
|
55
|
+
/** MemoryStore id */
|
|
56
|
+
store: string;
|
|
57
|
+
/** namespace; default `agent:${agent.name}`. A function is resolved per run. */
|
|
58
|
+
scope?: string | ((ctx: TCtx) => string);
|
|
59
|
+
access: 'read' | 'read-write';
|
|
60
|
+
/** inject top-k matches before an LLM call */
|
|
61
|
+
autoRecall?: {
|
|
62
|
+
query: 'last_user_input' | 'none';
|
|
63
|
+
limit: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** A binding with its scope already resolved against the run context. */
|
|
67
|
+
export interface ResolvedBinding {
|
|
68
|
+
store: string;
|
|
69
|
+
scope: string;
|
|
70
|
+
access: 'read' | 'read-write';
|
|
71
|
+
autoRecall?: {
|
|
72
|
+
query: 'last_user_input' | 'none';
|
|
73
|
+
limit: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface MemoryRecallSpec {
|
|
77
|
+
kind: 'recall';
|
|
78
|
+
store: string;
|
|
79
|
+
scope: string;
|
|
80
|
+
query: MemoryQuery;
|
|
81
|
+
hits: {
|
|
82
|
+
id: string;
|
|
83
|
+
score: number;
|
|
84
|
+
revision: number;
|
|
85
|
+
}[];
|
|
86
|
+
/** the rendered block the model actually saw */
|
|
87
|
+
content: string;
|
|
88
|
+
}
|
|
89
|
+
export interface MemoryOpSpec {
|
|
90
|
+
kind: 'op';
|
|
91
|
+
op: 'write' | 'update' | 'delete';
|
|
92
|
+
store: string;
|
|
93
|
+
scope: string;
|
|
94
|
+
/** sha256(runId, callId) — deterministic, so replay re-issues the same write */
|
|
95
|
+
opId: string;
|
|
96
|
+
recordId: string;
|
|
97
|
+
revision: number;
|
|
98
|
+
before?: string;
|
|
99
|
+
after?: string;
|
|
100
|
+
}
|
|
101
|
+
export declare function memoryOpId(runId: string, callId: string): string;
|
|
102
|
+
export declare function renderMemories(hits: MemoryHit[]): string;
|
|
103
|
+
/**
|
|
104
|
+
* Filtering plus token-overlap scoring, shared by the backends that have no
|
|
105
|
+
* embeddings of their own. A vector store ignores this and ranks in the
|
|
106
|
+
* database instead — the kernel only ever sees `MemoryHit[]`.
|
|
107
|
+
*/
|
|
108
|
+
export declare function rankMemories(records: Iterable<MemoryRecord>, q: MemoryQuery): MemoryHit[];
|
|
109
|
+
/**
|
|
110
|
+
* Memory is reachable both explicitly (these tools) and implicitly (auto-recall
|
|
111
|
+
* in the runner). Both land in the trajectory; only the explicit path is
|
|
112
|
+
* visible to the model as a tool.
|
|
113
|
+
*/
|
|
114
|
+
export declare function memoryTools<TCtx>(bindings: ResolvedBinding[]): AnyTool<TCtx>[];
|
|
115
|
+
//# sourceMappingURL=memory.d.ts.map
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { hash } from "./payload.js";
|
|
2
|
+
import { MEMORY_DELETE_TOOL, MEMORY_SEARCH_TOOL, MEMORY_UPDATE_TOOL, MEMORY_WRITE_TOOL, tool, withEffects, } from "./types.js";
|
|
3
|
+
export function memoryOpId(runId, callId) {
|
|
4
|
+
return hash(`${runId}\u0000${callId}`);
|
|
5
|
+
}
|
|
6
|
+
export function renderMemories(hits) {
|
|
7
|
+
if (!hits.length) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
const lines = hits.map((h) => `- [${h.record.id}] (${h.record.kind}, score ${h.score.toFixed(2)}) ${h.record.text}`);
|
|
11
|
+
return `Relevant memories:\n${lines.join('\n')}`;
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared ranking
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Filtering plus token-overlap scoring, shared by the backends that have no
|
|
18
|
+
* embeddings of their own. A vector store ignores this and ranks in the
|
|
19
|
+
* database instead — the kernel only ever sees `MemoryHit[]`.
|
|
20
|
+
*/
|
|
21
|
+
export function rankMemories(records, q) {
|
|
22
|
+
const limit = q.limit ?? 8;
|
|
23
|
+
const terms = tokenize(q.text ?? '');
|
|
24
|
+
const hits = [];
|
|
25
|
+
for (const rec of records) {
|
|
26
|
+
if (q.kind && rec.kind !== q.kind) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (q.filter && !matches(rec.metadata, q.filter)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const score = terms.length ? overlap(terms, tokenize(rec.text)) : 1;
|
|
33
|
+
if (score <= 0 || (q.minScore !== undefined && score < q.minScore)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
hits.push({ record: rec, score });
|
|
37
|
+
}
|
|
38
|
+
return hits.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
39
|
+
}
|
|
40
|
+
function tokenize(v) {
|
|
41
|
+
return v
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.split(/[^a-z0-9]+/)
|
|
44
|
+
.filter((t) => t.length > 2);
|
|
45
|
+
}
|
|
46
|
+
function overlap(a, b) {
|
|
47
|
+
const set = new Set(b);
|
|
48
|
+
const shared = a.filter((t) => set.has(t)).length;
|
|
49
|
+
return a.length ? shared / a.length : 0;
|
|
50
|
+
}
|
|
51
|
+
function matches(meta, filter) {
|
|
52
|
+
return Object.entries(filter).every(([k, v]) => meta?.[k] === v);
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Built-in memory tools
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function spaceParam(bindings) {
|
|
58
|
+
return bindings.length > 1
|
|
59
|
+
? { type: 'string', enum: bindings.map((b) => b.scope), description: 'memory space' }
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
function pick(bindings, space) {
|
|
63
|
+
const b = space ? bindings.find((x) => x.scope === space) : bindings[0];
|
|
64
|
+
if (!b) {
|
|
65
|
+
throw new Error(`unknown memory space: ${space ?? '(default)'}`);
|
|
66
|
+
}
|
|
67
|
+
return b;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Memory is reachable both explicitly (these tools) and implicitly (auto-recall
|
|
71
|
+
* in the runner). Both land in the trajectory; only the explicit path is
|
|
72
|
+
* visible to the model as a tool.
|
|
73
|
+
*/
|
|
74
|
+
export function memoryTools(bindings) {
|
|
75
|
+
if (!bindings.length) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const writable = bindings.filter((b) => b.access === 'read-write');
|
|
79
|
+
const space = spaceParam(bindings);
|
|
80
|
+
const withSpace = (props) => (space ? { ...props, space } : props);
|
|
81
|
+
const tools = [
|
|
82
|
+
tool({
|
|
83
|
+
name: MEMORY_SEARCH_TOOL,
|
|
84
|
+
description: 'Search long-lived memories.',
|
|
85
|
+
parameters: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: withSpace({
|
|
88
|
+
query: { type: 'string' },
|
|
89
|
+
kind: { type: 'string' },
|
|
90
|
+
limit: { type: 'integer' },
|
|
91
|
+
}),
|
|
92
|
+
required: ['query'],
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
},
|
|
95
|
+
execute: async (args, tc) => {
|
|
96
|
+
const b = pick(bindings, args.space);
|
|
97
|
+
const store = tc.services.memoryStore(b.store);
|
|
98
|
+
const hits = await store.search(b.scope, {
|
|
99
|
+
text: args.query,
|
|
100
|
+
kind: args.kind,
|
|
101
|
+
limit: args.limit,
|
|
102
|
+
});
|
|
103
|
+
return hits.map((h) => ({
|
|
104
|
+
id: h.record.id,
|
|
105
|
+
kind: h.record.kind,
|
|
106
|
+
text: h.record.text,
|
|
107
|
+
revision: h.record.revision,
|
|
108
|
+
score: Number(h.score.toFixed(3)),
|
|
109
|
+
}));
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
];
|
|
113
|
+
if (!writable.length) {
|
|
114
|
+
return tools;
|
|
115
|
+
}
|
|
116
|
+
const wSpace = spaceParam(writable);
|
|
117
|
+
const withWSpace = (props) => wSpace ? { ...props, space: wSpace } : props;
|
|
118
|
+
tools.push(tool({
|
|
119
|
+
name: MEMORY_WRITE_TOOL,
|
|
120
|
+
description: 'Store a new long-lived memory.',
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: withWSpace({ text: { type: 'string' }, kind: { type: 'string' } }),
|
|
124
|
+
required: ['text'],
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
},
|
|
127
|
+
execute: async (args, tc) => {
|
|
128
|
+
const b = pick(writable, args.space);
|
|
129
|
+
const opId = memoryOpId(tc.state.runId, tc.callId);
|
|
130
|
+
const rec = await tc.services
|
|
131
|
+
.memoryStore(b.store)
|
|
132
|
+
.write(b.scope, { text: args.text, kind: args.kind }, opId);
|
|
133
|
+
return withEffects(`stored memory ${rec.id}`, {
|
|
134
|
+
kind: 'memory_op',
|
|
135
|
+
spec: {
|
|
136
|
+
kind: 'op',
|
|
137
|
+
op: 'write',
|
|
138
|
+
store: b.store,
|
|
139
|
+
scope: b.scope,
|
|
140
|
+
opId,
|
|
141
|
+
recordId: rec.id,
|
|
142
|
+
revision: rec.revision,
|
|
143
|
+
after: rec.text,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
}), tool({
|
|
148
|
+
name: MEMORY_UPDATE_TOOL,
|
|
149
|
+
description: 'Update an existing memory by id.',
|
|
150
|
+
parameters: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: withWSpace({
|
|
153
|
+
id: { type: 'string' },
|
|
154
|
+
text: { type: 'string' },
|
|
155
|
+
kind: { type: 'string' },
|
|
156
|
+
}),
|
|
157
|
+
required: ['id'],
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
execute: async (args, tc) => {
|
|
161
|
+
const b = pick(writable, args.space);
|
|
162
|
+
const store = tc.services.memoryStore(b.store);
|
|
163
|
+
const before = await store.get(b.scope, args.id);
|
|
164
|
+
const opId = memoryOpId(tc.state.runId, tc.callId);
|
|
165
|
+
const rec = await store.update(b.scope, args.id, { text: args.text, kind: args.kind }, opId);
|
|
166
|
+
return withEffects(`updated memory ${rec.id}`, {
|
|
167
|
+
kind: 'memory_op',
|
|
168
|
+
spec: {
|
|
169
|
+
kind: 'op',
|
|
170
|
+
op: 'update',
|
|
171
|
+
store: b.store,
|
|
172
|
+
scope: b.scope,
|
|
173
|
+
opId,
|
|
174
|
+
recordId: rec.id,
|
|
175
|
+
revision: rec.revision,
|
|
176
|
+
before: before?.text,
|
|
177
|
+
after: rec.text,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
}), tool({
|
|
182
|
+
name: MEMORY_DELETE_TOOL,
|
|
183
|
+
description: 'Delete a memory by id.',
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: withWSpace({ id: { type: 'string' } }),
|
|
187
|
+
required: ['id'],
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
},
|
|
190
|
+
execute: async (args, tc) => {
|
|
191
|
+
const b = pick(writable, args.space);
|
|
192
|
+
const store = tc.services.memoryStore(b.store);
|
|
193
|
+
const before = await store.get(b.scope, args.id);
|
|
194
|
+
const opId = memoryOpId(tc.state.runId, tc.callId);
|
|
195
|
+
await store.delete(b.scope, args.id, opId);
|
|
196
|
+
return withEffects(`deleted memory ${args.id}`, {
|
|
197
|
+
kind: 'memory_op',
|
|
198
|
+
spec: {
|
|
199
|
+
kind: 'op',
|
|
200
|
+
op: 'delete',
|
|
201
|
+
store: b.store,
|
|
202
|
+
scope: b.scope,
|
|
203
|
+
opId,
|
|
204
|
+
recordId: args.id,
|
|
205
|
+
revision: before?.revision ?? 0,
|
|
206
|
+
before: before?.text,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
return tools;
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=memory.js.map
|
package/dist/models/factory.d.ts
CHANGED
|
@@ -150,6 +150,15 @@ export interface ModelRequirement {
|
|
|
150
150
|
}
|
|
151
151
|
export declare const providerKinds: ProviderKind[];
|
|
152
152
|
export declare function isProviderKind(name: string): name is ProviderKind;
|
|
153
|
+
/**
|
|
154
|
+
* Vertex serves three request formats behind one account, and the model id
|
|
155
|
+
* says which: Google's own models through the GenAI API, Claude through
|
|
156
|
+
* Anthropic's Messages format at `rawPredict`, and every other publisher's
|
|
157
|
+
* (`xai/grok-4.1-fast-reasoning`, `qwen/qwen3-…-maas`) through the
|
|
158
|
+
* OpenAI-compatible endpoint, which names its models `publisher/model`.
|
|
159
|
+
*/
|
|
160
|
+
export type VertexRoute = 'gemini' | 'anthropic' | 'openai';
|
|
161
|
+
export declare function vertexRoute(model: string): VertexRoute;
|
|
153
162
|
/**
|
|
154
163
|
* Which knobs each adapter actually reads.
|
|
155
164
|
*
|
|
@@ -240,6 +249,21 @@ export declare class ModelRegistry {
|
|
|
240
249
|
/** A declared kind wins; otherwise a built-in name is its own kind. */
|
|
241
250
|
kindOf(name: string): ProviderKind;
|
|
242
251
|
}
|
|
252
|
+
export interface VertexEndpoint {
|
|
253
|
+
project: string;
|
|
254
|
+
location: string;
|
|
255
|
+
/** `https://…/v1/projects/<project>/locations/<location>` */
|
|
256
|
+
base: string;
|
|
257
|
+
/** a fresh access token from Application Default Credentials, or `spec.token` */
|
|
258
|
+
token: () => Promise<string>;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Where a Vertex provider's partner models are served, and how to authenticate
|
|
262
|
+
* there. The GenAI SDK does both for Gemini; nothing does them for Claude or
|
|
263
|
+
* the OpenAI-compatible endpoint, so they are resolved here the same way:
|
|
264
|
+
* `project`, then GOOGLE_CLOUD_PROJECT, then the service-account file.
|
|
265
|
+
*/
|
|
266
|
+
export declare function vertexEndpoint(spec?: ProviderSpec, name?: string): VertexEndpoint;
|
|
243
267
|
/**
|
|
244
268
|
* The registry behind the bare `createModel`. An application that never
|
|
245
269
|
* declares a provider still gets shared clients through it, because the
|
package/dist/models/factory.js
CHANGED
|
@@ -114,6 +114,20 @@ export const providerKinds = Object.keys(KINDS);
|
|
|
114
114
|
export function isProviderKind(name) {
|
|
115
115
|
return Object.hasOwn(KINDS, name);
|
|
116
116
|
}
|
|
117
|
+
export function vertexRoute(model) {
|
|
118
|
+
if (/^(anthropic\/)?claude-/.test(model)) {
|
|
119
|
+
return 'anthropic';
|
|
120
|
+
}
|
|
121
|
+
return model.includes('/') ? 'openai' : 'gemini';
|
|
122
|
+
}
|
|
123
|
+
/** The protocol one model on one kind is spoken in. Only Vertex has more than one. */
|
|
124
|
+
function protocolOf(kind, model) {
|
|
125
|
+
return kind === 'vertex' ? vertexRoute(model) : KINDS[kind].protocol;
|
|
126
|
+
}
|
|
127
|
+
/** The OpenAI-compatible endpoint on Vertex has chat completions and nothing else. */
|
|
128
|
+
function apisOf(kind, protocol) {
|
|
129
|
+
return kind === 'vertex' && protocol === 'openai' ? ['chat'] : KINDS[kind].apis;
|
|
130
|
+
}
|
|
117
131
|
// ---------------------------------------------------------------------------
|
|
118
132
|
// Tuning knobs
|
|
119
133
|
// ---------------------------------------------------------------------------
|
|
@@ -214,7 +228,11 @@ export class ModelRegistry {
|
|
|
214
228
|
this.#providers.set(name, spec);
|
|
215
229
|
// Re-declaring after a client was handed out would otherwise leave the
|
|
216
230
|
// old credentials in service for the rest of the process.
|
|
217
|
-
this.#clients.
|
|
231
|
+
for (const key of this.#clients.keys()) {
|
|
232
|
+
if (key === name || key.startsWith(`${name}\0`)) {
|
|
233
|
+
this.#clients.delete(key);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
218
236
|
this.#limiters.delete(name);
|
|
219
237
|
return this;
|
|
220
238
|
}
|
|
@@ -247,6 +265,17 @@ export class ModelRegistry {
|
|
|
247
265
|
this.#clients.set(name, built);
|
|
248
266
|
return built;
|
|
249
267
|
}
|
|
268
|
+
/** A Vertex provider's Claude or partner client: built once, beside its GenAI one. */
|
|
269
|
+
#partner(name, protocol, spec) {
|
|
270
|
+
const key = `${name}\0${protocol}`;
|
|
271
|
+
const existing = this.#clients.get(key);
|
|
272
|
+
if (existing) {
|
|
273
|
+
return existing;
|
|
274
|
+
}
|
|
275
|
+
const built = buildVertexPartner(name, protocol, spec);
|
|
276
|
+
this.#clients.set(key, built);
|
|
277
|
+
return built;
|
|
278
|
+
}
|
|
250
279
|
/**
|
|
251
280
|
* The pacing shared by every embedder on a provider, so that what one of
|
|
252
281
|
* them learns from a 429 the others do not have to learn again — and so
|
|
@@ -275,14 +304,16 @@ export class ModelRegistry {
|
|
|
275
304
|
*/
|
|
276
305
|
knobs(ref) {
|
|
277
306
|
const spec = typeof ref === 'string' ? this.parse(ref) : ref;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
307
|
+
const kind = this.kindOf(spec.provider ?? this.#default);
|
|
308
|
+
const protocol = protocolOf(kind, spec.model);
|
|
309
|
+
if (protocol !== 'openai') {
|
|
310
|
+
return unreadKnobs(spec, protocol);
|
|
281
311
|
}
|
|
282
312
|
// An api this vendor does not speak already has an error of its own;
|
|
283
313
|
// answering about its knobs would only bury it.
|
|
284
|
-
const
|
|
285
|
-
|
|
314
|
+
const apis = apisOf(kind, protocol);
|
|
315
|
+
const api = spec.api ?? apis[0];
|
|
316
|
+
return api && apis.includes(api) ? unreadKnobs(spec, `openai/${api}`) : [];
|
|
286
317
|
}
|
|
287
318
|
/** Turns a shorthand or a spec into a `Model`. */
|
|
288
319
|
model(ref) {
|
|
@@ -290,7 +321,7 @@ export class ModelRegistry {
|
|
|
290
321
|
const name = spec.provider ?? this.#default;
|
|
291
322
|
const provider = this.#spec(name);
|
|
292
323
|
const kind = this.kindOf(name);
|
|
293
|
-
const
|
|
324
|
+
const protocol = protocolOf(kind, spec.model);
|
|
294
325
|
// Before the client, so a knob for the wrong vendor is reported as the
|
|
295
326
|
// config mistake it is rather than behind a missing credential.
|
|
296
327
|
const [stray] = this.knobs(spec);
|
|
@@ -302,30 +333,35 @@ export class ModelRegistry {
|
|
|
302
333
|
// different connection, and handing back the memoized one would
|
|
303
334
|
// silently ignore them.
|
|
304
335
|
const client = spec.client ??
|
|
305
|
-
(
|
|
336
|
+
(protocol !== KINDS[kind].protocol
|
|
337
|
+
? this.#partner(name, protocol, provider)
|
|
338
|
+
: hasCredentials(spec)
|
|
339
|
+
? buildClient(name, kind, spec, provider)
|
|
340
|
+
: this.client(name));
|
|
306
341
|
// The *resolved* name, not `spec.provider`: a bare ref carries none,
|
|
307
342
|
// and a failure that cannot say which connection refused it is the
|
|
308
343
|
// thing this is here to prevent.
|
|
309
344
|
const options = { ...spec, provider: name };
|
|
310
345
|
// Every protocol but OpenAI's has exactly one API, so naming one is a
|
|
311
346
|
// mistake worth reporting rather than a field to ignore.
|
|
312
|
-
if (
|
|
347
|
+
if (protocol !== 'openai') {
|
|
313
348
|
if (spec.api) {
|
|
314
349
|
throw new TypeError(`provider "${name}" (${kind}) has one api, so "${spec.api}" means nothing here`);
|
|
315
350
|
}
|
|
316
|
-
switch (
|
|
351
|
+
switch (protocol) {
|
|
317
352
|
case 'anthropic':
|
|
318
|
-
return new AnthropicModel(spec.model, client, options);
|
|
353
|
+
return new AnthropicModel(spec.model.replace(/^anthropic\//, ''), client, options);
|
|
319
354
|
case 'gemini':
|
|
320
355
|
return new GeminiModel(spec.model, client, options);
|
|
321
356
|
case 'openrouter':
|
|
322
357
|
return new OpenRouterModel(spec.model, client, options);
|
|
323
358
|
}
|
|
324
359
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
360
|
+
const apis = apisOf(kind, protocol);
|
|
361
|
+
const api = spec.api ?? apis[0];
|
|
362
|
+
if (!apis.includes(api)) {
|
|
327
363
|
throw new TypeError(`provider "${name}" (${kind}) does not speak the "${api}" api ` +
|
|
328
|
-
`(supported: ${
|
|
364
|
+
`(supported: ${apis.join(', ')})`);
|
|
329
365
|
}
|
|
330
366
|
return api === 'responses'
|
|
331
367
|
? new OpenAIResponsesModel(spec.model, client, options)
|
|
@@ -658,6 +694,109 @@ function projectFromKeyFile() {
|
|
|
658
694
|
return undefined;
|
|
659
695
|
}
|
|
660
696
|
}
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
// Vertex partner models
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
/** Gemini has these; the partner endpoints do not, and answer them with a bare 404. */
|
|
701
|
+
const MULTI_REGIONS = new Set(['us', 'eu']);
|
|
702
|
+
/**
|
|
703
|
+
* Where a Vertex provider's partner models are served, and how to authenticate
|
|
704
|
+
* there. The GenAI SDK does both for Gemini; nothing does them for Claude or
|
|
705
|
+
* the OpenAI-compatible endpoint, so they are resolved here the same way:
|
|
706
|
+
* `project`, then GOOGLE_CLOUD_PROJECT, then the service-account file.
|
|
707
|
+
*/
|
|
708
|
+
export function vertexEndpoint(spec = {}, name = 'vertex') {
|
|
709
|
+
const where = `provider "${name}"`;
|
|
710
|
+
const project = expand(spec.project, `${where}: project`) ??
|
|
711
|
+
fromEnv('GOOGLE_CLOUD_PROJECT') ??
|
|
712
|
+
projectFromKeyFile();
|
|
713
|
+
if (!project) {
|
|
714
|
+
throw new Error(`${where}: Claude and partner models on vertex need \`project\`, or ` +
|
|
715
|
+
`GOOGLE_CLOUD_PROJECT, and a service account — an express-mode key reaches Gemini only`);
|
|
716
|
+
}
|
|
717
|
+
const named = expand(spec.location, `${where}: location`) ?? fromEnv('GOOGLE_CLOUD_LOCATION') ?? 'global';
|
|
718
|
+
const location = MULTI_REGIONS.has(named) ? 'global' : named;
|
|
719
|
+
const host = location === 'global'
|
|
720
|
+
? 'https://aiplatform.googleapis.com'
|
|
721
|
+
: `https://${location}-aiplatform.googleapis.com`;
|
|
722
|
+
const token = spec.token;
|
|
723
|
+
return {
|
|
724
|
+
project,
|
|
725
|
+
location,
|
|
726
|
+
base: `${host}/v1/projects/${project}/locations/${location}`,
|
|
727
|
+
token: token ? async () => token() : adcToken(),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** Mints and refreshes tokens the way the GenAI SDK does for Gemini. */
|
|
731
|
+
function adcToken() {
|
|
732
|
+
let auth;
|
|
733
|
+
return async () => {
|
|
734
|
+
if (!auth) {
|
|
735
|
+
// `@google/genai`'s own dependency, so it is resolved from there rather than declared twice.
|
|
736
|
+
const genai = sdkPath('@google/genai', 'vertex');
|
|
737
|
+
const { GoogleAuth } = createRequire(genai)('google-auth-library');
|
|
738
|
+
auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });
|
|
739
|
+
}
|
|
740
|
+
const token = await auth.getAccessToken();
|
|
741
|
+
if (!token) {
|
|
742
|
+
throw new Error('vertex: Application Default Credentials gave no access token');
|
|
743
|
+
}
|
|
744
|
+
return token;
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function sdkPath(pkg, kind) {
|
|
748
|
+
try {
|
|
749
|
+
return requirePeer.resolve(pkg);
|
|
750
|
+
}
|
|
751
|
+
catch (cause) {
|
|
752
|
+
throw new Error(`provider kind "${kind}" needs ${pkg}, which is not installed — run: npm i ${pkg}`, { cause });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function buildVertexPartner(name, protocol, opts) {
|
|
756
|
+
const at = vertexEndpoint(opts, name);
|
|
757
|
+
const common = {
|
|
758
|
+
apiKey: OAUTH_PLACEHOLDER,
|
|
759
|
+
defaultHeaders: expandHeaders(opts.headers, `provider "${name}"`),
|
|
760
|
+
timeout: opts.timeoutMs,
|
|
761
|
+
maxRetries: retriesOf(opts),
|
|
762
|
+
};
|
|
763
|
+
if (protocol === 'anthropic') {
|
|
764
|
+
const { Anthropic } = sdk('@anthropic-ai/sdk', 'vertex');
|
|
765
|
+
return new Anthropic({
|
|
766
|
+
...common,
|
|
767
|
+
baseURL: at.base,
|
|
768
|
+
fetch: funded(rawPredictFetch(at.base, at.token)),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const { OpenAI } = sdk('openai', 'vertex');
|
|
772
|
+
return new OpenAI({
|
|
773
|
+
...common,
|
|
774
|
+
baseURL: `${at.base}/endpoints/openapi`,
|
|
775
|
+
fetch: funded(bearerFetch(at.token)),
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Anthropic's client posts to `/v1/messages`; Vertex wants the model in the
|
|
780
|
+
* path and its own `anthropic_version` in the body instead.
|
|
781
|
+
*/
|
|
782
|
+
function rawPredictFetch(base, token) {
|
|
783
|
+
const authed = bearerFetch(token);
|
|
784
|
+
return async (input, init) => {
|
|
785
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
786
|
+
if (!new URL(url).pathname.endsWith('/v1/messages') || typeof init?.body !== 'string') {
|
|
787
|
+
return authed(input, init);
|
|
788
|
+
}
|
|
789
|
+
const { model, ...body } = JSON.parse(init.body);
|
|
790
|
+
const headers = new Headers(init.headers);
|
|
791
|
+
headers.delete('x-api-key');
|
|
792
|
+
const verb = body.stream ? 'streamRawPredict' : 'rawPredict';
|
|
793
|
+
return authed(`${base}/publishers/anthropic/models/${model}:${verb}`, {
|
|
794
|
+
...init,
|
|
795
|
+
headers,
|
|
796
|
+
body: JSON.stringify({ ...body, anthropic_version: 'vertex-2023-10-16' }),
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
}
|
|
661
800
|
// Both SDKs refuse to construct without a key. When a token callback owns the
|
|
662
801
|
// Authorization header this value is never sent, so it only has to be non-empty.
|
|
663
802
|
const OAUTH_PLACEHOLDER = 'oauth';
|
package/dist/project/config.d.ts
CHANGED
|
@@ -48,10 +48,10 @@ declare const modelSpec: z.ZodObject<{
|
|
|
48
48
|
thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
|
|
49
49
|
thinkingBudget: z.ZodOptional<z.ZodInt>;
|
|
50
50
|
thinkingLevel: z.ZodOptional<z.ZodEnum<{
|
|
51
|
+
minimal: "minimal";
|
|
51
52
|
low: "low";
|
|
52
53
|
medium: "medium";
|
|
53
54
|
high: "high";
|
|
54
|
-
minimal: "minimal";
|
|
55
55
|
}>>;
|
|
56
56
|
includeThoughts: z.ZodOptional<z.ZodBoolean>;
|
|
57
57
|
routing: z.ZodOptional<z.ZodObject<{
|
|
@@ -163,15 +163,15 @@ declare const sandbox: z.ZodObject<{
|
|
|
163
163
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
164
164
|
memory: z.ZodOptional<z.ZodInt>;
|
|
165
165
|
network: z.ZodOptional<z.ZodEnum<{
|
|
166
|
+
bridge: "bridge";
|
|
166
167
|
none: "none";
|
|
167
168
|
host: "host";
|
|
168
|
-
bridge: "bridge";
|
|
169
169
|
}>>;
|
|
170
170
|
workdir: z.ZodOptional<z.ZodString>;
|
|
171
171
|
timeout: z.ZodOptional<z.ZodInt>;
|
|
172
172
|
hardening: z.ZodOptional<z.ZodEnum<{
|
|
173
|
-
strict: "strict";
|
|
174
173
|
standard: "standard";
|
|
174
|
+
strict: "strict";
|
|
175
175
|
}>>;
|
|
176
176
|
user: z.ZodOptional<z.ZodString>;
|
|
177
177
|
persist: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -220,10 +220,10 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
220
220
|
thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
|
|
221
221
|
thinkingBudget: z.ZodOptional<z.ZodInt>;
|
|
222
222
|
thinkingLevel: z.ZodOptional<z.ZodEnum<{
|
|
223
|
+
minimal: "minimal";
|
|
223
224
|
low: "low";
|
|
224
225
|
medium: "medium";
|
|
225
226
|
high: "high";
|
|
226
|
-
minimal: "minimal";
|
|
227
227
|
}>>;
|
|
228
228
|
includeThoughts: z.ZodOptional<z.ZodBoolean>;
|
|
229
229
|
routing: z.ZodOptional<z.ZodObject<{
|
|
@@ -293,15 +293,15 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
293
293
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
294
294
|
memory: z.ZodOptional<z.ZodInt>;
|
|
295
295
|
network: z.ZodOptional<z.ZodEnum<{
|
|
296
|
+
bridge: "bridge";
|
|
296
297
|
none: "none";
|
|
297
298
|
host: "host";
|
|
298
|
-
bridge: "bridge";
|
|
299
299
|
}>>;
|
|
300
300
|
workdir: z.ZodOptional<z.ZodString>;
|
|
301
301
|
timeout: z.ZodOptional<z.ZodInt>;
|
|
302
302
|
hardening: z.ZodOptional<z.ZodEnum<{
|
|
303
|
-
strict: "strict";
|
|
304
303
|
standard: "standard";
|
|
304
|
+
strict: "strict";
|
|
305
305
|
}>>;
|
|
306
306
|
user: z.ZodOptional<z.ZodString>;
|
|
307
307
|
persist: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -325,8 +325,8 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
325
325
|
provider: z.ZodOptional<z.ZodString>;
|
|
326
326
|
discovery: z.ZodDefault<z.ZodEnum<{
|
|
327
327
|
none: "none";
|
|
328
|
-
search: "search";
|
|
329
328
|
index: "index";
|
|
329
|
+
search: "search";
|
|
330
330
|
}>>;
|
|
331
331
|
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
332
332
|
preload: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -357,15 +357,15 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
357
357
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
358
358
|
memory: z.ZodOptional<z.ZodInt>;
|
|
359
359
|
network: z.ZodOptional<z.ZodEnum<{
|
|
360
|
+
bridge: "bridge";
|
|
360
361
|
none: "none";
|
|
361
362
|
host: "host";
|
|
362
|
-
bridge: "bridge";
|
|
363
363
|
}>>;
|
|
364
364
|
workdir: z.ZodOptional<z.ZodString>;
|
|
365
365
|
timeout: z.ZodOptional<z.ZodInt>;
|
|
366
366
|
hardening: z.ZodOptional<z.ZodEnum<{
|
|
367
|
-
strict: "strict";
|
|
368
367
|
standard: "standard";
|
|
368
|
+
strict: "strict";
|
|
369
369
|
}>>;
|
|
370
370
|
user: z.ZodOptional<z.ZodString>;
|
|
371
371
|
persist: z.ZodOptional<z.ZodBoolean>;
|